mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-14 05:19:53 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b5007dbc3 | ||
|
|
cce787ec40 | ||
|
|
94d651de8c | ||
|
|
040e382d64 | ||
|
|
caafd7c9bf | ||
|
|
144528257d | ||
|
|
af0b3418d0 | ||
|
|
7fd5417ed0 | ||
|
|
5ac1e12b83 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": patch
|
||||
---
|
||||
|
||||
fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": patch
|
||||
---
|
||||
|
||||
fix: `openspec status` now exits gracefully when no changes exist instead of throwing a fatal error. Fixes #714.
|
||||
@@ -156,3 +156,6 @@ opencode.json
|
||||
|
||||
# Codex
|
||||
.codex/
|
||||
|
||||
# Bob
|
||||
.bob/
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#952](https://github.com/Fission-AI/OpenSpec/pull/952) [`cce787e`](https://github.com/Fission-AI/OpenSpec/commit/cce787ec4083da2b27781f6786f5ce0002909a7b) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
|
||||
|
||||
- **Junie support** — Added tool and command generation for JetBrains Junie
|
||||
- **Lingma IDE support** — Added configuration support for Lingma IDE
|
||||
- **ForgeCode support** — Added tool support for ForgeCode
|
||||
- **IBM Bob support** — Added support for IBM Bob coding assistant
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Shell completions opt-in** — Completion install is now opt-in, fixing PowerShell encoding corruption
|
||||
- **Copilot auto-detection** — Prevented false GitHub Copilot detection from a bare `.github/` directory
|
||||
- **pi.dev command generation** — Fixed command reference transforms and template argument passing
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#760](https://github.com/Fission-AI/OpenSpec/pull/760) [`61eb999`](https://github.com/Fission-AI/OpenSpec/commit/61eb999f7c6c0fc98d2e7f3678756fce6a3f4378) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.
|
||||
|
||||
- [#759](https://github.com/Fission-AI/OpenSpec/pull/759) [`afdca0d`](https://github.com/Fission-AI/OpenSpec/commit/afdca0d5dab1aa109cfd8848b2512333ccad60c3) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: `openspec status` now exits gracefully when no changes exist instead of throwing a fatal error. Fixes #714.
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -103,7 +103,7 @@ Now tell your AI: `/opsx:propose <what-you-want-to-build>`
|
||||
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
||||
|
||||
> [!NOTE]
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
|
||||
>
|
||||
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `b
|
||||
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
|
||||
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
|
||||
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
|
||||
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
|
||||
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
|
||||
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
|
||||
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
|
||||
@@ -37,6 +38,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `b
|
||||
| Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/<id>.toml` |
|
||||
| GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-<id>.prompt.md`\*\* |
|
||||
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
|
||||
| Junie (`junie`) | `.junie/skills/openspec-*/SKILL.md` | `.junie/commands/opsx-<id>.md` |
|
||||
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
|
||||
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
|
||||
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-<id>.md` |
|
||||
@@ -69,7 +71,7 @@ openspec init --tools none
|
||||
openspec init --profile core
|
||||
```
|
||||
|
||||
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `forgecode`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `forgecode`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
|
||||
## Workflow-Dependent Installation
|
||||
|
||||
|
||||
Generated
+13
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1803,6 +1803,7 @@
|
||||
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1852,6 +1853,7 @@
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
@@ -2182,6 +2184,7 @@
|
||||
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -2219,6 +2222,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2685,6 +2689,7 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4448,6 +4453,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4546,6 +4552,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4611,6 +4618,7 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4727,6 +4735,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4740,6 +4749,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -4919,6 +4929,7 @@
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+8
-72
@@ -1,9 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Postinstall script for auto-installing shell completions
|
||||
* Postinstall script that hints about shell completions
|
||||
*
|
||||
* This script runs automatically after npm install unless:
|
||||
* Completion installation is opt-in: the user must run
|
||||
* `openspec completion install` explicitly. This script only
|
||||
* prints a one-line tip after npm install.
|
||||
*
|
||||
* The tip is suppressed when:
|
||||
* - CI=true environment variable is set
|
||||
* - OPENSPEC_NO_COMPLETIONS=1 environment variable is set
|
||||
* - dist/ directory doesn't exist (dev setup scenario)
|
||||
@@ -48,65 +52,6 @@ async function distExists() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the user's shell
|
||||
*/
|
||||
async function detectShell() {
|
||||
try {
|
||||
const { detectShell } = await import('../dist/utils/shell-detection.js');
|
||||
const result = detectShell();
|
||||
return result.shell;
|
||||
} catch (error) {
|
||||
// Fail silently if detection module doesn't exist
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install completions for the detected shell
|
||||
*/
|
||||
async function installCompletions(shell) {
|
||||
try {
|
||||
const { CompletionFactory } = await import('../dist/core/completions/factory.js');
|
||||
const { COMMAND_REGISTRY } = await import('../dist/core/completions/command-registry.js');
|
||||
|
||||
// Check if shell is supported
|
||||
if (!CompletionFactory.isSupported(shell)) {
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate completion script
|
||||
const generator = CompletionFactory.createGenerator(shell);
|
||||
const script = generator.generate(COMMAND_REGISTRY);
|
||||
|
||||
// Install completion script
|
||||
const installer = CompletionFactory.createInstaller(shell);
|
||||
const result = await installer.install(script);
|
||||
|
||||
if (result.success) {
|
||||
// Show success message based on installation type
|
||||
if (result.isOhMyZsh) {
|
||||
console.log(`✓ Shell completions installed`);
|
||||
console.log(` Restart shell: exec zsh`);
|
||||
} else if (result.zshrcConfigured) {
|
||||
console.log(`✓ Shell completions installed and configured`);
|
||||
console.log(` Restart shell: exec zsh`);
|
||||
} else {
|
||||
console.log(`✓ Shell completions installed to ~/.zsh/completions/`);
|
||||
console.log(` Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)`);
|
||||
console.log(` Then: exec zsh`);
|
||||
}
|
||||
} else {
|
||||
// Installation failed, show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Fail gracefully - show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
@@ -124,19 +69,10 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect shell
|
||||
const shell = await detectShell();
|
||||
if (!shell) {
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Install completions
|
||||
await installCompletions(shell);
|
||||
// Completions are opt-in — just print a hint
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
} catch (error) {
|
||||
// Fail gracefully - never break npm install
|
||||
// Show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ ORIGINAL_CI="${CI:-}"
|
||||
ORIGINAL_OPENSPEC_NO_COMPLETIONS="${OPENSPEC_NO_COMPLETIONS:-}"
|
||||
|
||||
# Test 1: Normal install
|
||||
echo "Test 1: Normal install (should attempt to install completions)"
|
||||
echo "Test 1: Normal install (should print tip about completions)"
|
||||
echo "--------------------------------------"
|
||||
unset CI
|
||||
unset OPENSPEC_NO_COMPLETIONS
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Bob Shell Command Adapter
|
||||
*
|
||||
* Formats commands for Bob Shell following its markdown specification.
|
||||
* Commands are stored in .bob/commands/ directory.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
||||
|
||||
/**
|
||||
* Escapes a string value for safe YAML output.
|
||||
* Quotes the string if it contains special YAML characters.
|
||||
*/
|
||||
function escapeYamlValue(value: string): string {
|
||||
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
||||
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
||||
if (needsQuoting) {
|
||||
// Use double quotes and escape internal double quotes and backslashes
|
||||
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob Shell adapter for command generation.
|
||||
* File path: .bob/commands/opsx-<id>.md
|
||||
* Frontmatter: description, argument-hint
|
||||
*/
|
||||
export const bobAdapter: ToolCommandAdapter = {
|
||||
toolId: 'bob',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.bob', 'commands', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
// Transform command references from colon to hyphen format for Bob
|
||||
const transformedBody = transformToHyphenCommands(content.body);
|
||||
|
||||
return `---
|
||||
description: ${escapeYamlValue(content.description)}
|
||||
argument-hint: command arguments
|
||||
---
|
||||
|
||||
${transformedBody}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
export { amazonQAdapter } from './amazon-q.js';
|
||||
export { antigravityAdapter } from './antigravity.js';
|
||||
export { auggieAdapter } from './auggie.js';
|
||||
export { bobAdapter } from './bob.js';
|
||||
export { claudeAdapter } from './claude.js';
|
||||
export { clineAdapter } from './cline.js';
|
||||
export { codexAdapter } from './codex.js';
|
||||
@@ -19,11 +20,13 @@ export { factoryAdapter } from './factory.js';
|
||||
export { geminiAdapter } from './gemini.js';
|
||||
export { githubCopilotAdapter } from './github-copilot.js';
|
||||
export { iflowAdapter } from './iflow.js';
|
||||
export { junieAdapter } from './junie.js';
|
||||
export { kilocodeAdapter } from './kilocode.js';
|
||||
export { kiroAdapter } from './kiro.js';
|
||||
export { opencodeAdapter } from './opencode.js';
|
||||
export { piAdapter } from './pi.js';
|
||||
export { qoderAdapter } from './qoder.js';
|
||||
export { lingmaAdapter } from './lingma.js';
|
||||
export { qwenAdapter } from './qwen.js';
|
||||
export { roocodeAdapter } from './roocode.js';
|
||||
export { windsurfAdapter } from './windsurf.js';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Junie Command Adapter
|
||||
*
|
||||
* Formats commands for Junie following its frontmatter specification.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
|
||||
/**
|
||||
* Junie adapter for command generation.
|
||||
* File path: .junie/commands/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*/
|
||||
export const junieAdapter: ToolCommandAdapter = {
|
||||
toolId: 'junie',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.junie', 'commands', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
return `---
|
||||
description: ${content.description}
|
||||
---
|
||||
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Lingma Command Adapter
|
||||
*
|
||||
* Formats commands for Lingma following its frontmatter specification.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
|
||||
/**
|
||||
* Lingma adapter for command generation.
|
||||
* File path: .lingma/commands/opsx/<id>.md
|
||||
* Frontmatter: name, description, category, tags
|
||||
*/
|
||||
export const lingmaAdapter: ToolCommandAdapter = {
|
||||
toolId: 'lingma',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.lingma', 'commands', 'opsx', `${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
const tagsStr = content.tags.join(', ');
|
||||
return `---
|
||||
name: ${content.name}
|
||||
description: ${content.description}
|
||||
category: ${content.category}
|
||||
tags: [${tagsStr}]
|
||||
---
|
||||
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -7,6 +7,20 @@
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
||||
|
||||
const PI_INPUT_HEADING = /^\*\*Input\*\*:[^\n]*$/m;
|
||||
|
||||
function injectPiArgs(body: string): string {
|
||||
if (body.includes('$@') || body.includes('$ARGUMENTS')) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return body.replace(
|
||||
PI_INPUT_HEADING,
|
||||
(heading) => `${heading}\n**Provided arguments**: $@`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string value for safe YAML output.
|
||||
@@ -27,6 +41,10 @@ function escapeYamlValue(value: string): string {
|
||||
* Pi adapter for prompt template generation.
|
||||
* File path: .pi/prompts/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*
|
||||
* Pi uses the filename (minus .md) as the slash command name, so
|
||||
* opsx-propose.md → /opsx-propose. Command references in the body
|
||||
* are transformed from /opsx: to /opsx- for consistency.
|
||||
*/
|
||||
export const piAdapter: ToolCommandAdapter = {
|
||||
toolId: 'pi',
|
||||
@@ -36,11 +54,14 @@ export const piAdapter: ToolCommandAdapter = {
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
// Transform /opsx: references to /opsx- and inject $@ for template args
|
||||
const transformedBody = transformToHyphenCommands(content.body);
|
||||
|
||||
return `---
|
||||
description: ${escapeYamlValue(content.description)}
|
||||
---
|
||||
|
||||
${content.body}
|
||||
${injectPiArgs(transformedBody)}
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ToolCommandAdapter } from './types.js';
|
||||
import { amazonQAdapter } from './adapters/amazon-q.js';
|
||||
import { antigravityAdapter } from './adapters/antigravity.js';
|
||||
import { auggieAdapter } from './adapters/auggie.js';
|
||||
import { bobAdapter } from './adapters/bob.js';
|
||||
import { claudeAdapter } from './adapters/claude.js';
|
||||
import { clineAdapter } from './adapters/cline.js';
|
||||
import { codexAdapter } from './adapters/codex.js';
|
||||
@@ -21,11 +22,13 @@ import { factoryAdapter } from './adapters/factory.js';
|
||||
import { geminiAdapter } from './adapters/gemini.js';
|
||||
import { githubCopilotAdapter } from './adapters/github-copilot.js';
|
||||
import { iflowAdapter } from './adapters/iflow.js';
|
||||
import { junieAdapter } from './adapters/junie.js';
|
||||
import { kilocodeAdapter } from './adapters/kilocode.js';
|
||||
import { kiroAdapter } from './adapters/kiro.js';
|
||||
import { opencodeAdapter } from './adapters/opencode.js';
|
||||
import { piAdapter } from './adapters/pi.js';
|
||||
import { qoderAdapter } from './adapters/qoder.js';
|
||||
import { lingmaAdapter } from './adapters/lingma.js';
|
||||
import { qwenAdapter } from './adapters/qwen.js';
|
||||
import { roocodeAdapter } from './adapters/roocode.js';
|
||||
import { windsurfAdapter } from './adapters/windsurf.js';
|
||||
@@ -41,6 +44,7 @@ export class CommandAdapterRegistry {
|
||||
CommandAdapterRegistry.register(amazonQAdapter);
|
||||
CommandAdapterRegistry.register(antigravityAdapter);
|
||||
CommandAdapterRegistry.register(auggieAdapter);
|
||||
CommandAdapterRegistry.register(bobAdapter);
|
||||
CommandAdapterRegistry.register(claudeAdapter);
|
||||
CommandAdapterRegistry.register(clineAdapter);
|
||||
CommandAdapterRegistry.register(codexAdapter);
|
||||
@@ -53,11 +57,13 @@ export class CommandAdapterRegistry {
|
||||
CommandAdapterRegistry.register(geminiAdapter);
|
||||
CommandAdapterRegistry.register(githubCopilotAdapter);
|
||||
CommandAdapterRegistry.register(iflowAdapter);
|
||||
CommandAdapterRegistry.register(junieAdapter);
|
||||
CommandAdapterRegistry.register(kilocodeAdapter);
|
||||
CommandAdapterRegistry.register(kiroAdapter);
|
||||
CommandAdapterRegistry.register(opencodeAdapter);
|
||||
CommandAdapterRegistry.register(piAdapter);
|
||||
CommandAdapterRegistry.register(qoderAdapter);
|
||||
CommandAdapterRegistry.register(lingmaAdapter);
|
||||
CommandAdapterRegistry.register(qwenAdapter);
|
||||
CommandAdapterRegistry.register(roocodeAdapter);
|
||||
CommandAdapterRegistry.register(windsurfAdapter);
|
||||
|
||||
@@ -23,6 +23,49 @@ export class PowerShellInstaller {
|
||||
this.homeDir = homeDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the encoding of a file by inspecting its BOM (Byte Order Mark).
|
||||
* Returns the Node.js BufferEncoding and the raw BOM bytes to preserve on write.
|
||||
*/
|
||||
private detectEncoding(buffer: Buffer): { encoding: BufferEncoding; bom: Buffer } {
|
||||
// UTF-16 LE BOM: FF FE
|
||||
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
||||
return { encoding: 'utf16le', bom: Buffer.from([0xff, 0xfe]) };
|
||||
}
|
||||
// UTF-16 BE BOM: FE FF — not natively supported by Node
|
||||
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
||||
throw new Error(
|
||||
'File is encoded as UTF-16 BE which is not supported. ' +
|
||||
'Please re-save as UTF-8 or UTF-16 LE, then retry.',
|
||||
);
|
||||
}
|
||||
// UTF-8 BOM: EF BB BF
|
||||
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
||||
return { encoding: 'utf-8', bom: Buffer.from([0xef, 0xbb, 0xbf]) };
|
||||
}
|
||||
// No BOM → default UTF-8
|
||||
return { encoding: 'utf-8', bom: Buffer.alloc(0) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a profile file, preserving its encoding metadata for round-trip writes.
|
||||
* Throws if the file uses UTF-16 BE (unsupported by Node).
|
||||
*/
|
||||
private async readProfileFile(filePath: string): Promise<{ content: string; encoding: BufferEncoding; bom: Buffer }> {
|
||||
const raw = await fs.readFile(filePath);
|
||||
const { encoding, bom } = this.detectEncoding(raw);
|
||||
const content = raw.subarray(bom.length).toString(encoding);
|
||||
return { content, encoding, bom };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a profile file, preserving the original BOM and encoding.
|
||||
*/
|
||||
private async writeProfileFile(filePath: string, content: string, encoding: BufferEncoding, bom: Buffer): Promise<void> {
|
||||
const body = Buffer.from(content, encoding);
|
||||
await fs.writeFile(filePath, Buffer.concat([bom, body]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PowerShell profile path
|
||||
* Prefers $PROFILE environment variable, falls back to platform defaults
|
||||
@@ -132,10 +175,22 @@ export class PowerShellInstaller {
|
||||
await fs.mkdir(profileDir, { recursive: true });
|
||||
|
||||
let profileContent = '';
|
||||
let fileEncoding: BufferEncoding = 'utf-8';
|
||||
let fileBom: Buffer = Buffer.alloc(0);
|
||||
try {
|
||||
profileContent = await fs.readFile(profilePath, 'utf-8');
|
||||
} catch {
|
||||
// Profile doesn't exist yet, that's fine
|
||||
const file = await this.readProfileFile(profilePath);
|
||||
profileContent = file.content;
|
||||
fileEncoding = file.encoding;
|
||||
fileBom = file.bom;
|
||||
} catch (err: any) {
|
||||
// If the file doesn't exist that's fine — we'll create it as UTF-8.
|
||||
// Any other read error (permissions, unsupported encoding, etc.) → skip this profile.
|
||||
if (err?.code === 'ENOENT') {
|
||||
// keep defaults
|
||||
} else {
|
||||
console.warn(`Warning: Skipping ${profilePath}: ${err?.message ?? String(err)}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already configured
|
||||
@@ -154,7 +209,7 @@ export class PowerShellInstaller {
|
||||
].join('\n');
|
||||
|
||||
const newContent = profileContent + openspecBlock;
|
||||
await fs.writeFile(profilePath, newContent, 'utf-8');
|
||||
await this.writeProfileFile(profilePath, newContent, fileEncoding, fileBom);
|
||||
anyConfigured = true;
|
||||
} catch (error) {
|
||||
// Continue to next profile if this one fails
|
||||
@@ -177,12 +232,21 @@ export class PowerShellInstaller {
|
||||
|
||||
for (const profilePath of profilePaths) {
|
||||
try {
|
||||
// Read profile content
|
||||
// Read profile content with encoding detection
|
||||
let profileContent: string;
|
||||
let fileEncoding: BufferEncoding = 'utf-8';
|
||||
let fileBom: Buffer = Buffer.alloc(0);
|
||||
try {
|
||||
profileContent = await fs.readFile(profilePath, 'utf-8');
|
||||
} catch {
|
||||
continue; // Profile doesn't exist, nothing to remove
|
||||
const file = await this.readProfileFile(profilePath);
|
||||
profileContent = file.content;
|
||||
fileEncoding = file.encoding;
|
||||
fileBom = file.bom;
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
continue; // Profile doesn't exist, nothing to remove
|
||||
}
|
||||
console.warn(`Warning: Could not read ${profilePath}: ${err?.message ?? String(err)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove OPENSPEC:START -> OPENSPEC:END block
|
||||
@@ -207,7 +271,7 @@ export class PowerShellInstaller {
|
||||
// Clean up extra newlines
|
||||
const newContent = (beforeBlock.trimEnd() + '\n' + afterBlock.trimStart()).trim() + '\n';
|
||||
|
||||
await fs.writeFile(profilePath, newContent, 'utf-8');
|
||||
await this.writeProfileFile(profilePath, newContent, fileEncoding, fileBom);
|
||||
anyRemoved = true;
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not clean ${profilePath}: ${error}`);
|
||||
|
||||
@@ -22,6 +22,7 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
|
||||
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
|
||||
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
|
||||
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
|
||||
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
|
||||
@@ -35,11 +36,13 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
|
||||
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },
|
||||
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
|
||||
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
|
||||
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
||||
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
||||
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
|
||||
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
|
||||
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
||||
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
|
||||
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
|
||||
|
||||
+2
-2
@@ -537,8 +537,8 @@ export class InitCommand {
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
|
||||
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
||||
// Use hyphen-based command references for OpenCode
|
||||
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
||||
// Use hyphen-based command references for tools where filename = command name
|
||||
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
||||
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
||||
|
||||
// Write the skill file
|
||||
|
||||
@@ -34,6 +34,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPatter
|
||||
'claude': { type: 'directory', path: '.claude/commands/openspec' },
|
||||
'codebuddy': { type: 'directory', path: '.codebuddy/commands/openspec' },
|
||||
'qoder': { type: 'directory', path: '.qoder/commands/openspec' },
|
||||
'lingma': { type: 'directory', path: '.lingma/commands/openspec' },
|
||||
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
||||
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
||||
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
||||
@@ -53,6 +54,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPatter
|
||||
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
||||
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
||||
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
||||
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
||||
'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
|
||||
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
||||
Display a table summarizing all changes:
|
||||
|
||||
\`\`\`
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
@@ -332,7 +332,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
||||
Display a table summarizing all changes:
|
||||
|
||||
\`\`\`
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
|
||||
@@ -57,10 +57,10 @@ Depending on what the user brings, you might:
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
@@ -115,14 +115,14 @@ If the user mentions a change or you detect one is relevant:
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
||||
| Requirement changed | \`specs/<capability>/spec.md\` |
|
||||
| Design decision made | \`design.md\` |
|
||||
| Scope changed | \`proposal.md\` |
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|--------------------------------|
|
||||
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
||||
| Requirement changed | \`specs/<capability>/spec.md\` |
|
||||
| Design decision made | \`design.md\` |
|
||||
| Scope changed | \`proposal.md\` |
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
@@ -228,7 +228,7 @@ User: A CLI tool that tracks local dev environments
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
@@ -353,10 +353,10 @@ Depending on what the user brings, you might:
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
@@ -413,14 +413,14 @@ If the user mentions a change or you detect one is relevant:
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
||||
| Requirement changed | \`specs/<capability>/spec.md\` |
|
||||
| Design decision made | \`design.md\` |
|
||||
| Scope changed | \`proposal.md\` |
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|--------------------------------|
|
||||
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
||||
| Requirement changed | \`specs/<capability>/spec.md\` |
|
||||
| Design decision made | \`design.md\` |
|
||||
| Scope changed | \`proposal.md\` |
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
|
||||
@@ -477,21 +477,21 @@ This same rhythm works for any size change—a small fix or a major feature.
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
||||
| \`/opsx:explore\` | Think through problems before/during work |
|
||||
| \`/opsx:apply\` | Implement tasks from a change |
|
||||
| \`/opsx:archive\` | Archive a completed change |
|
||||
| Command | What it does |
|
||||
|-------------------|--------------------------------------------|
|
||||
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
||||
| \`/opsx:explore\` | Think through problems before/during work |
|
||||
| \`/opsx:apply\` | Implement tasks from a change |
|
||||
| \`/opsx:archive\` | Archive a completed change |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
|
||||
| \`/opsx:continue\` | Continue working on an existing change |
|
||||
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
|
||||
| \`/opsx:verify\` | Verify implementation matches artifacts |
|
||||
| Command | What it does |
|
||||
|--------------------|----------------------------------------------------------|
|
||||
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
|
||||
| \`/opsx:continue\` | Continue working on an existing change |
|
||||
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
|
||||
| \`/opsx:verify\` | Verify implementation matches artifacts |
|
||||
|
||||
---
|
||||
|
||||
@@ -529,21 +529,21 @@ If the user says they just want to see the commands or skip the tutorial:
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
||||
| \`/opsx:explore\` | Think through problems (no code changes) |
|
||||
| \`/opsx:apply <name>\` | Implement tasks |
|
||||
| \`/opsx:archive <name>\` | Archive when done |
|
||||
| Command | What it does |
|
||||
|--------------------------|--------------------------------------------|
|
||||
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
||||
| \`/opsx:explore\` | Think through problems (no code changes) |
|
||||
| \`/opsx:apply <name>\` | Implement tasks |
|
||||
| \`/opsx:archive <name>\` | Archive when done |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| \`/opsx:new <name>\` | Start a new change, step by step |
|
||||
| \`/opsx:continue <name>\` | Continue an existing change |
|
||||
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
|
||||
| \`/opsx:verify <name>\` | Verify implementation |
|
||||
| Command | What it does |
|
||||
|---------------------------|-------------------------------------|
|
||||
| \`/opsx:new <name>\` | Start a new change, step by step |
|
||||
| \`/opsx:continue <name>\` | Continue an existing change |
|
||||
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
|
||||
| \`/opsx:verify <name>\` | Verify implementation |
|
||||
|
||||
Try \`/opsx:propose\` to start your first change.
|
||||
\`\`\`
|
||||
|
||||
+2
-2
@@ -195,7 +195,7 @@ export class UpdateCommand {
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
|
||||
// Use hyphen-based command references for OpenCode
|
||||
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
||||
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
||||
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
||||
await FileSystemUtils.writeFile(skillFile, skillContent);
|
||||
}
|
||||
@@ -666,7 +666,7 @@ export class UpdateCommand {
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
|
||||
// Use hyphen-based command references for OpenCode
|
||||
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
||||
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
||||
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
||||
await FileSystemUtils.writeFile(skillFile, skillContent);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from 'path';
|
||||
import { amazonQAdapter } from '../../../src/core/command-generation/adapters/amazon-q.js';
|
||||
import { antigravityAdapter } from '../../../src/core/command-generation/adapters/antigravity.js';
|
||||
import { auggieAdapter } from '../../../src/core/command-generation/adapters/auggie.js';
|
||||
import { bobAdapter } from '../../../src/core/command-generation/adapters/bob.js';
|
||||
import { claudeAdapter } from '../../../src/core/command-generation/adapters/claude.js';
|
||||
import { clineAdapter } from '../../../src/core/command-generation/adapters/cline.js';
|
||||
import { codexAdapter } from '../../../src/core/command-generation/adapters/codex.js';
|
||||
@@ -183,6 +184,71 @@ describe('command-generation/adapters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('bobAdapter', () => {
|
||||
it('should have correct toolId', () => {
|
||||
expect(bobAdapter.toolId).toBe('bob');
|
||||
});
|
||||
|
||||
it('should generate correct file path', () => {
|
||||
const filePath = bobAdapter.getFilePath('explore');
|
||||
expect(filePath).toBe(path.join('.bob', 'commands', 'opsx-explore.md'));
|
||||
});
|
||||
|
||||
it('should generate correct file paths for different commands', () => {
|
||||
expect(bobAdapter.getFilePath('new')).toBe(path.join('.bob', 'commands', 'opsx-new.md'));
|
||||
expect(bobAdapter.getFilePath('bulk-archive')).toBe(path.join('.bob', 'commands', 'opsx-bulk-archive.md'));
|
||||
});
|
||||
|
||||
it('should format file with description and argument-hint frontmatter', () => {
|
||||
const output = bobAdapter.formatFile(sampleContent);
|
||||
expect(output).toContain('---\n');
|
||||
expect(output).toContain('description: Enter explore mode for thinking');
|
||||
expect(output).toContain('argument-hint: command arguments');
|
||||
expect(output).toContain('---\n\n');
|
||||
expect(output).toContain('This is the command body.\n\nWith multiple lines.');
|
||||
});
|
||||
|
||||
it('should transform colon command references to hyphen format', () => {
|
||||
const contentWithRefs: CommandContent = {
|
||||
...sampleContent,
|
||||
body: 'Run /opsx:apply to implement. Then use /opsx:verify.',
|
||||
};
|
||||
const output = bobAdapter.formatFile(contentWithRefs);
|
||||
expect(output).toContain('/opsx-apply');
|
||||
expect(output).toContain('/opsx-verify');
|
||||
expect(output).not.toContain('/opsx:apply');
|
||||
expect(output).not.toContain('/opsx:verify');
|
||||
});
|
||||
|
||||
it('should escape YAML special characters in description', () => {
|
||||
const contentWithSpecialChars: CommandContent = {
|
||||
...sampleContent,
|
||||
description: 'Fix: regression in "auth" feature',
|
||||
};
|
||||
const output = bobAdapter.formatFile(contentWithSpecialChars);
|
||||
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
|
||||
});
|
||||
|
||||
it('should escape newlines in description', () => {
|
||||
const contentWithNewline: CommandContent = {
|
||||
...sampleContent,
|
||||
description: 'Line 1\nLine 2',
|
||||
};
|
||||
const output = bobAdapter.formatFile(contentWithNewline);
|
||||
expect(output).toContain('description: "Line 1\\nLine 2"');
|
||||
});
|
||||
|
||||
it('should handle empty description', () => {
|
||||
const contentEmptyDesc: CommandContent = {
|
||||
...sampleContent,
|
||||
description: '',
|
||||
};
|
||||
const output = bobAdapter.formatFile(contentEmptyDesc);
|
||||
expect(output).toContain('description: \n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clineAdapter', () => {
|
||||
it('should have correct toolId', () => {
|
||||
expect(clineAdapter.toolId).toBe('cline');
|
||||
@@ -547,6 +613,28 @@ describe('command-generation/adapters', () => {
|
||||
expect(output).toContain('This is the command body.');
|
||||
});
|
||||
|
||||
it('should transform command references from colon to hyphen format', () => {
|
||||
const contentWithRefs: CommandContent = {
|
||||
...sampleContent,
|
||||
body: 'Run /opsx:apply to implement. Then /opsx:archive when done.',
|
||||
};
|
||||
|
||||
const output = piAdapter.formatFile(contentWithRefs);
|
||||
expect(output).toContain('/opsx-apply');
|
||||
expect(output).toContain('/opsx-archive');
|
||||
expect(output).not.toContain('/opsx:apply');
|
||||
});
|
||||
|
||||
it('should inject template arguments into the input section', () => {
|
||||
const contentWithInput: CommandContent = {
|
||||
...sampleContent,
|
||||
body: '**Input**: The argument after `/opsx:explore` is the topic.\n\n**Steps**\n1. Think.',
|
||||
};
|
||||
|
||||
const output = piAdapter.formatFile(contentWithInput);
|
||||
expect(output).toContain('**Provided arguments**: $@');
|
||||
});
|
||||
|
||||
it('should escape YAML special characters in description', () => {
|
||||
const contentWithSpecialChars: CommandContent = {
|
||||
...sampleContent,
|
||||
@@ -606,7 +694,7 @@ describe('command-generation/adapters', () => {
|
||||
it('All adapters use path.join for paths', () => {
|
||||
// Verify all adapters produce valid paths
|
||||
const adapters = [
|
||||
amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter,
|
||||
amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter,
|
||||
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
|
||||
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
|
||||
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
|
||||
|
||||
@@ -21,6 +21,12 @@ describe('command-generation/registry', () => {
|
||||
expect(adapter?.toolId).toBe('windsurf');
|
||||
});
|
||||
|
||||
it('should return Junie adapter for "junie"', () => {
|
||||
const adapter = CommandAdapterRegistry.get('junie');
|
||||
expect(adapter).toBeDefined();
|
||||
expect(adapter?.toolId).toBe('junie');
|
||||
});
|
||||
|
||||
it('should return undefined for unregistered tool', () => {
|
||||
const adapter = CommandAdapterRegistry.get('unknown-tool');
|
||||
expect(adapter).toBeUndefined();
|
||||
@@ -54,6 +60,7 @@ describe('command-generation/registry', () => {
|
||||
expect(CommandAdapterRegistry.has('claude')).toBe(true);
|
||||
expect(CommandAdapterRegistry.has('cursor')).toBe(true);
|
||||
expect(CommandAdapterRegistry.has('windsurf')).toBe(true);
|
||||
expect(CommandAdapterRegistry.has('junie')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unregistered tools', () => {
|
||||
|
||||
@@ -544,6 +544,173 @@ Register-ArgumentCompleter -CommandName openspec -ScriptBlock $openspecCompleter
|
||||
});
|
||||
});
|
||||
|
||||
describe('encoding preservation', () => {
|
||||
const mockScriptPath = '/path/to/OpenSpecCompletion.ps1';
|
||||
const utf16leBom = Buffer.from([0xff, 0xfe]);
|
||||
const utf8Bom = Buffer.from([0xef, 0xbb, 0xbf]);
|
||||
|
||||
/**
|
||||
* Helper: write a file in UTF-16 LE with BOM, the way Windows PowerShell does.
|
||||
*/
|
||||
function writeUtf16LeFile(filePath: string, text: string): Promise<void> {
|
||||
const body = Buffer.from(text, 'utf16le');
|
||||
return fs.writeFile(filePath, Buffer.concat([utf16leBom, body]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: write a file in UTF-8 with BOM.
|
||||
*/
|
||||
function writeUtf8BomFile(filePath: string, text: string): Promise<void> {
|
||||
const body = Buffer.from(text, 'utf-8');
|
||||
return fs.writeFile(filePath, Buffer.concat([utf8Bom, body]));
|
||||
}
|
||||
|
||||
it('should preserve UTF-16 LE BOM when configuring profile', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
const originalText = '. "C:\\Code\\SystemConfig\\Powershell\\profile.ps1"\r\n';
|
||||
await writeUtf16LeFile(profilePath, originalText);
|
||||
|
||||
const result = await installer.configureProfile(mockScriptPath);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Read back raw bytes and verify BOM is preserved
|
||||
const raw = await fs.readFile(profilePath);
|
||||
expect(raw[0]).toBe(0xff);
|
||||
expect(raw[1]).toBe(0xfe);
|
||||
|
||||
// Decode and verify content is intact
|
||||
const content = raw.subarray(2).toString('utf16le');
|
||||
expect(content).toContain('. "C:\\Code\\SystemConfig\\Powershell\\profile.ps1"');
|
||||
expect(content).toContain('# OPENSPEC:START');
|
||||
expect(content).toContain(`. "${mockScriptPath}"`);
|
||||
expect(content).toContain('# OPENSPEC:END');
|
||||
});
|
||||
|
||||
it('should preserve UTF-16 LE BOM when removing profile config', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
const textWithBlock = [
|
||||
'. "C:\\Code\\profile.ps1"',
|
||||
'# OPENSPEC:START',
|
||||
'. "/path/to/OpenSpecCompletion.ps1"',
|
||||
'# OPENSPEC:END',
|
||||
'',
|
||||
].join('\n');
|
||||
await writeUtf16LeFile(profilePath, textWithBlock);
|
||||
|
||||
const result = await installer.removeProfileConfig();
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify BOM is preserved
|
||||
const raw = await fs.readFile(profilePath);
|
||||
expect(raw[0]).toBe(0xff);
|
||||
expect(raw[1]).toBe(0xfe);
|
||||
|
||||
// Verify content: original line kept, OpenSpec block removed
|
||||
const content = raw.subarray(2).toString('utf16le');
|
||||
expect(content).toContain('. "C:\\Code\\profile.ps1"');
|
||||
expect(content).not.toContain('# OPENSPEC:START');
|
||||
expect(content).not.toContain('# OPENSPEC:END');
|
||||
});
|
||||
|
||||
it('should preserve UTF-8 BOM when configuring profile', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
await writeUtf8BomFile(profilePath, '# My profile\n');
|
||||
|
||||
const result = await installer.configureProfile(mockScriptPath);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const raw = await fs.readFile(profilePath);
|
||||
expect(raw[0]).toBe(0xef);
|
||||
expect(raw[1]).toBe(0xbb);
|
||||
expect(raw[2]).toBe(0xbf);
|
||||
|
||||
const content = raw.subarray(3).toString('utf-8');
|
||||
expect(content).toContain('# My profile');
|
||||
expect(content).toContain('# OPENSPEC:START');
|
||||
});
|
||||
|
||||
it('should skip UTF-16 BE profile and leave it unchanged', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
process.env.PROFILE = path.join(testHomeDir, 'custom-profile.ps1');
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
// Write a fake UTF-16 BE file (FE FF BOM + some bytes)
|
||||
const utf16beBom = Buffer.from([0xfe, 0xff]);
|
||||
const body = Buffer.from([0x00, 0x23]); // '#' in UTF-16 BE
|
||||
const originalBytes = Buffer.concat([utf16beBom, body]);
|
||||
await fs.writeFile(profilePath, originalBytes);
|
||||
|
||||
const result = await installer.configureProfile(mockScriptPath);
|
||||
expect(result).toBe(false);
|
||||
|
||||
// File should be untouched
|
||||
const raw = await fs.readFile(profilePath);
|
||||
expect(Buffer.compare(raw, originalBytes)).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle plain UTF-8 files without BOM (no regression)', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
await fs.writeFile(profilePath, '# Plain UTF-8\n', 'utf-8');
|
||||
|
||||
const result = await installer.configureProfile(mockScriptPath);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const raw = await fs.readFile(profilePath);
|
||||
// Should NOT have any BOM
|
||||
expect(raw[0]).not.toBe(0xff);
|
||||
expect(raw[0]).not.toBe(0xfe);
|
||||
expect(raw[0]).not.toBe(0xef);
|
||||
|
||||
const content = raw.toString('utf-8');
|
||||
expect(content).toContain('# Plain UTF-8');
|
||||
expect(content).toContain('# OPENSPEC:START');
|
||||
});
|
||||
|
||||
it('should round-trip UTF-16 LE through install → uninstall without corruption', async () => {
|
||||
delete process.env.OPENSPEC_NO_AUTO_CONFIG;
|
||||
const profilePath = installer.getProfilePath();
|
||||
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
||||
|
||||
const originalText = '. "C:\\Code\\SystemConfig\\Powershell\\profile.ps1"\r\n';
|
||||
await writeUtf16LeFile(profilePath, originalText);
|
||||
|
||||
// Install adds the OpenSpec block
|
||||
const mockScript = '# completion script';
|
||||
await installer.install(mockScript);
|
||||
|
||||
// Verify the profile was modified but encoding preserved
|
||||
let raw = await fs.readFile(profilePath);
|
||||
expect(raw[0]).toBe(0xff);
|
||||
expect(raw[1]).toBe(0xfe);
|
||||
let content = raw.subarray(2).toString('utf16le');
|
||||
expect(content).toContain('# OPENSPEC:START');
|
||||
expect(content).toContain(originalText.trimEnd());
|
||||
|
||||
// Uninstall removes the OpenSpec block
|
||||
await installer.uninstall();
|
||||
|
||||
raw = await fs.readFile(profilePath);
|
||||
expect(raw[0]).toBe(0xff);
|
||||
expect(raw[1]).toBe(0xfe);
|
||||
content = raw.subarray(2).toString('utf16le');
|
||||
expect(content).not.toContain('# OPENSPEC:START');
|
||||
expect(content).toContain('. "C:\\Code\\SystemConfig\\Powershell\\profile.ps1"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninstall', () => {
|
||||
const mockCompletionScript = `# PowerShell completion script
|
||||
$openspecCompleter = {}
|
||||
|
||||
@@ -30,25 +30,25 @@ import {
|
||||
import { generateSkillContent } from '../../../src/core/shared/skill-generation.js';
|
||||
|
||||
const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
|
||||
getExploreSkillTemplate: '55a2a1afcba0af88c638e77e4e3870f65ed82c030b4a2056d39812ae13a616be',
|
||||
getExploreSkillTemplate: '3f73b4d7ab189ef6367fccc9d99308bee35c6a89dae4c8044582a01cb01b335b',
|
||||
getNewChangeSkillTemplate: '5989672758eccf54e3bb554ab97f2c129a192b12bbb7688cc1ffcf6bccb1ae9d',
|
||||
getContinueChangeSkillTemplate: 'f2e413f0333dfd6641cc2bd1a189273fdea5c399eecdde98ef528b5216f097b3',
|
||||
getApplyChangeSkillTemplate: '26e52e67693e93fbcdd40dcd3e20949c07ce019183d55a8149d0260c791cd7f4',
|
||||
getFfChangeSkillTemplate: 'a7332fb14c8dc3f9dec71f5d332790b4a8488191e7db4ab6132ccbefecf9ded9',
|
||||
getSyncSpecsSkillTemplate: 'bded184e4c345619148de2c0ad80a5b527d4ffe45c87cc785889b9329e0f465b',
|
||||
getOnboardSkillTemplate: '819a2d117ad1386187975686839cb0584b41484013d0ca6a6691f7a439a11a4a',
|
||||
getOpsxExploreCommandTemplate: '91353d9e8633a3a9ce7339e796f1283478fca279153f3807c92f4f8ece246b19',
|
||||
getOnboardSkillTemplate: 'c9e719a02d2ae7f74a0e978f9ad4e767c1921248a9e3724c3321c58a15c38ba9',
|
||||
getOpsxExploreCommandTemplate: 'b421b88c7a532385f7b1404736d7893eb35a05573b4a04a96f72379ac1bbf148',
|
||||
getOpsxNewCommandTemplate: '62eee32d6d81a376e7be845d0891e28e6262ad07482f9bfe6af12a9f0366c364',
|
||||
getOpsxContinueCommandTemplate: '8bbaedcc95287f9e822572608137df4f49ad54cedfb08d3342d0d1c4e9716caa',
|
||||
getOpsxApplyCommandTemplate: 'a9d631a07fcd832b67d263ff3800b98604ab8d378baf1b0d545907ef3affa3b5',
|
||||
getOpsxFfCommandTemplate: 'cdebe872cc8e0fcc25c8864b98ffd66a93484c0657db94bd1285b8113092702a',
|
||||
getArchiveChangeSkillTemplate: '6f8ca383fdb5a4eb9872aca81e07bf0ba7f25e4de8617d7a047ca914ca7f14b9',
|
||||
getBulkArchiveChangeSkillTemplate: 'b40fc44ea4e420bdc9c803985b10e5c091fc472cdfc69153b962be6be303bddd',
|
||||
getBulkArchiveChangeSkillTemplate: '8049897ce1ddb2ff6c0d4b72e22636f9ecfd083b5f2c2a30cf3bb1cb828a2f93',
|
||||
getOpsxSyncCommandTemplate: '378d035fe7cc30be3e027b66dcc4b8afc78ef1c8369c39479c9b05a582fb5ccf',
|
||||
getVerifyChangeSkillTemplate: '63a213ba3b42af54a1cd56f5072234a03b265c3fe4a1da12cd6fbbef5ee46c4b',
|
||||
getOpsxArchiveCommandTemplate: 'b44cc9748109f61687f9f596604b037bc3ea803abc143b22f09a76aebd98b493',
|
||||
getOpsxOnboardCommandTemplate: '10052d05a4e2cdade7fdfa549b3444f7a92f55a39bf81ddd6af7e0e9e83a7302',
|
||||
getOpsxBulkArchiveCommandTemplate: 'eaaba253a950b9e681d8427a5cbc6b50c4e91137fb37fd2360859e08f63a0c14',
|
||||
getOpsxOnboardCommandTemplate: 'fce531f952e939ee85a41848fc21e4cc720b0f3eb62737adc3a51ee6ad2dfc57',
|
||||
getOpsxBulkArchiveCommandTemplate: '0d77c82de43840a28c74f5181cb21e33b9a9d00454adf4bc92bdc9e69817d6f5',
|
||||
getOpsxVerifyCommandTemplate: '9b4d3ca422553b7534764eb3a009da87a051612c5238e9baab294c7b1233e9a2',
|
||||
getOpsxProposeSkillTemplate: 'd67f937d44650e9c61d2158c865309fbab23cb3f50a3d4868a640a97776e3999',
|
||||
getOpsxProposeCommandTemplate: '41ad59b37eafd7a161bab5c6e41997a37368f9c90b194451295ede5cd42e4d46',
|
||||
@@ -56,16 +56,16 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
|
||||
};
|
||||
|
||||
const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
|
||||
'openspec-explore': '90463d00761417dfbca5cb09361adcf8bbdbbb24000b86dd03647869a4104479',
|
||||
'openspec-explore': '08e1ec9958eb04653707dd3e198c3fd69cf1b3acd3cf95a1022693cca83c60fc',
|
||||
'openspec-new-change': 'c324a7ace1f244aa3f534ac8e3370a2c11190d6d1b85a315f26a211398310f0f',
|
||||
'openspec-continue-change': '463cf0b980ec9c3c24774414ef2a3e48e9faa8577bc8748990f45ab3d5efe960',
|
||||
'openspec-apply-change': 'a0084442b59be9d7e22a0382a279d470501e1ecf74bdd5347e169951c9be191c',
|
||||
'openspec-ff-change': '672c3a5b8df152d959b15bd7ae2be7a75ab7b8eaa2ec1e0daa15c02479b27937',
|
||||
'openspec-sync-specs': 'b8859cf454379a19ca35dbf59eedca67306607f44a355327f9dc851114e50bde',
|
||||
'openspec-archive-change': 'f83c85452bd47de0dee6b8efbcea6a62534f8a175480e9044f3043f887cebf0f',
|
||||
'openspec-bulk-archive-change': 'a235a539f7729ab7669e45256905808789240ecd02820e044f4d0eef67b0c2ab',
|
||||
'openspec-bulk-archive-change': '10477399bb07c7ba67f78e315bd68fb1901af8866720545baf4c62a6a679493b',
|
||||
'openspec-verify-change': '30d07c6f7051965f624f5964db51844ec17c7dfd05f0da95281fe0ca73616326',
|
||||
'openspec-onboard': 'dbce376cf895f3fe4f63b4bce66d258c35b7b8884ac746670e5e35fabcefd255',
|
||||
'openspec-onboard': 'c1444e026028210efd699110f7e9079bcb486d85ccf27f743213a81cb1084303',
|
||||
'openspec-propose': '20e36dabefb90e232bad0667292bd5007ec280f8fc4fc995dbc4282bf45a22e7',
|
||||
};
|
||||
|
||||
|
||||
@@ -250,6 +250,7 @@ Old instructions content
|
||||
expect(exists).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('multi-tool support', () => {
|
||||
|
||||
Reference in New Issue
Block a user