Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 0021526efd docs: add spec-kit comparison and comparison overview section
Added a "How OpenSpec compares (at a glance)" section to highlight key differentiators early in the README. Expanded the comparison section with a dedicated spec-kit comparison, emphasizing OpenSpec's two-folder model for managing existing features and cross-spec updates.
2025-10-09 00:08:43 +11:00
Tabish Bidiwale 8c974f8a80 update discord invite link (#126) 2025-10-08 23:29:14 +11:00
github-actions[bot]andTabish Bidiwale 25289b510d chore(release): version packages (#124)
* Version Packages

* empty

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-08 14:59:33 +11:00
Tabish Bidiwale d070d08aa8 fix: correct CLI version mismatch and add release guard (0.8.1) (#123)
* fix: correct CLI version mismatch and add release guard\n\n- Add patch changeset for 0.8.1\n- Add pack-version check to validate tarball version\n- Update release script to include versioning and guard

* chore(release): simplify release script and harden pack-version-check\n\n- Run pack-version check before publish only\n- Remove redundant changeset version + explicit build in release script\n- Always cleanup temp dir and tgz\n- Quieter, faster npm install during guard

* chore(release): clarify CI vs local release scripts; refine pack guard\n\n- Add scripts: release:ci (no version), release:local (runs changeset version)\n- Workflow uses release:ci to ensure version PR bump precedes publish\n- Pack guard: document npm vs pnpm choice; improve JSON fallback handling
2025-10-08 14:46:15 +11:00
github-actions[bot]andTabish Bidiwale 6539ceb54a chore(release): version packages (#117)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-04 02:22:12 +10:00
Tabish Bidiwale c29b06da42 chore: changeset for Windsurf support (#116) 2025-10-04 02:11:14 +10:00
Tabish Bidiwale 4a2b23942c chore(release): phase 2 – enable publish via changesets action, add release script, remove legacy release-publish workflow (#115) 2025-10-04 01:56:11 +10:00
Tabish Bidiwale 807b9d32a6 chore(release): phase 1 – wire changesets (dry-run), add npm auth config (#114)
* Clarify release automation proposal

* chore(release): phase 1 – wire up changesets action (dry-run publish) and GitHub release drafting

* ci(release): phase 1 – add NODE_AUTH_TOKEN alias and registry/auth config for npm (dry-run)
2025-10-04 01:28:47 +10:00
b3d05d2f78 Add Windsurf IDE support with slash commands (#113)
* docs(windsurf): propose workflow support

* restore missing opencode spec

* Add Windsurf IDE support with slash commands

* feat(windsurf): add Windsurf workflows support under .windsurf/workflows and simplify templates\n\n- Write workflows to .windsurf/workflows instead of .windsurf/commands\n- Remove YAML frontmatter; add concise intro before managed markers\n- Add init/update tests for Windsurf and marker preservation\n- List Windsurf in README native tools table\n- Normalize registry indentation

* chore(windsurf): remove optional intro content to simplify workflows\n\n- Drop intro hook and headings for Windsurf workflows\n- Keep OPENSPEC markers-only body for safe updates\n- Adjust tests to assert marker-managed content

* feat(windsurf): add required YAML frontmatter to workflows\n\n- Include description and auto_execution_mode: 3 for proposal/apply/archive\n- Keep content minimal; body remains marker-managed

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-04 00:54:07 +10:00
13 changed files with 381 additions and 77 deletions
+15 -2
View File
@@ -8,8 +8,13 @@ permissions:
contents: write
pull-requests: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
prepare:
if: github.repository == 'Fission-AI/OpenSpec'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -24,14 +29,22 @@ jobs:
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
scope: '@fission-ai'
always-auth: true
- run: pnpm install --frozen-lockfile
# Opens/updates the Version Packages PR; no publishing here
# Opens/updates the Version Packages PR; publishes when the Version PR merges
- name: Create/Update Version PR
uses: changesets/action@v1
with:
title: 'chore(release): version packages'
createGithubReleases: true
# Use CI-specific release script: relies on version PR having been merged
# so package.json already contains the bumped version.
publish: pnpm run release:ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-72
View File
@@ -1,72 +0,0 @@
name: Publish to npm
on:
release:
types: [published]
workflow_dispatch: {}
permissions:
contents: read
id-token: write
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
scope: '@fission-ai'
always-auth: true
- run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Ensure running from a tag
run: |
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
echo "This workflow must run from a tag (got: $GITHUB_REF)";
exit 1;
fi
- name: Verify release tag matches package.json
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG" != "$PKG_VERSION" ]; then
echo "Tag v$TAG does not match package.json $PKG_VERSION"; exit 1
fi
- name: Debug npm auth and context
run: |
test -n "$NODE_AUTH_TOKEN" || (echo "NODE_AUTH_TOKEN is missing" && exit 1)
echo "NODE_AUTH_TOKEN present"
npm --version
pnpm --version
node --version
npm config get registry
npm whoami
npm ping
- run: pnpm test
- name: Publish
run: pnpm publish --access public --provenance --no-git-checks
+12
View File
@@ -1,5 +1,17 @@
# @fission-ai/openspec
## 0.8.1
### Patch Changes
- d070d08: Fix CLI version mismatch and add a release guard that validates the packed tarball prints the same version as package.json via `openspec --version`.
## 0.8.0
### Minor Changes
- c29b06d: Add Windsurf support.
## 0.7.0
### Minor Changes
+15 -2
View File
@@ -15,7 +15,7 @@
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
<a href="https://discord.gg/saTQQGQZ"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
<a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
</p>
<p align="center">
@@ -23,7 +23,7 @@
</p>
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/saTQQGQZ">OpenSpec Discord</a> for help and questions.
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
</p>
# OpenSpec
@@ -40,6 +40,15 @@ Key outcomes:
- Shared visibility into what's proposed, active, or archived.
- Works with the AI tools you already use: custom slash commands where supported, context rules everywhere else.
## How OpenSpec compares (at a glance)
- **Lightweight**: simple workflow, no API keys, minimal setup.
- **Brownfield-first**: works great beyond 0→1. OpenSpec separates the source of truth from proposals: `openspec/specs/` (current truth) and `openspec/changes/` (proposed updates). This keeps diffs explicit and manageable across features.
- **Change tracking**: proposals, tasks, and spec deltas live together; archiving merges the approved updates back into specs.
- **Compared to spec-kit & Kiro**: those shine for brand-new features (0→1). OpenSpec also excels when modifying existing behavior (1→n), especially when updates span multiple specs.
See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
## How It Works
```
@@ -85,6 +94,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
@@ -295,6 +305,9 @@ Deltas are "patches" that show how specs change:
## How OpenSpec Compares
### vs. spec-kit
OpenSpec’s two-folder model (`openspec/specs/` for the current truth, `openspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
### vs. Kiro.dev
OpenSpec groups every change for a feature in one folder (`openspec/changes/feature-name/`), making it easy to track related specs, tasks, and designs together. Kiro spreads updates across multiple spec folders, which can make feature tracking harder.
@@ -0,0 +1,49 @@
## Why
Today’s process requires maintainers to merge the Changesets PR, cut a tag, and draft the GitHub release by hand. npm publish then runs from our existing workflow after the GitHub release is published. The human-in-the-loop steps (versioning, tagging, release notes) slow us down and risk drift between npm, tags, and changelog.
## What Changes
- Use the single `changesets/action` on pushes to `main` to either open/update the version PR or, when the release PR is merged, run our publish command automatically using repository secrets.
- Add a `release` script that builds and runs `changeset publish` so the action handles version bumps, changelog commits, npm publish, and GitHub releases end-to-end.
- Enable `createGithubReleases: true` so GitHub releases are created from the changeset data right after publishing.
- Document the automated flow, required secrets, guardrails, and recovery steps (rollback, hotfixes).
## Two-Phase Rollout (Two PRs)
1) Phase 1 — Dry run (no publish)
- Update the existing `release-prepare.yml` to wire up `changesets/action` with `createGithubReleases: true` and a no-op `publish` command (e.g., `echo 'dry run'`).
- Keep `.github/workflows/release-publish.yml` intact. This avoids any publish path changes while we verify that the version PR behavior and permissions are correct.
- Add a repository guard (`if: github.repository == 'Fission-AI/OpenSpec'`) and a concurrency group for safety.
2) Phase 2 — Enable publish and consolidate
- Add `"release": "pnpm run build && pnpm exec changeset publish"` to `package.json`.
- Change `release-prepare.yml` to use `with: publish: pnpm run release` and `env: NPM_TOKEN: \\${{ secrets.NPM_TOKEN }}` plus the default `GITHUB_TOKEN`.
- Remove `.github/workflows/release-publish.yml` to avoid double-publish. Publishing now happens when the version PR is merged.
## Guardrails
- Concurrency: `concurrency: { group: release-\\${{ github.ref }}, cancel-in-progress: false }` on the workflow to serialize releases.
- Repository/branch guard: run publish logic only on upstream `main` (`if: github.repository == 'Fission-AI/OpenSpec' && github.ref == 'refs/heads/main'`).
- Permissions: ensure `contents: write` and `pull-requests: write` for opening/updating the version PR; `packages: read` optional.
## Rollback and Hotfixes
- Rollback: revert the release PR merge (which reverts version bumps/changelog); if a tag or GitHub release was created, delete the tag and release; deprecate the npm version if necessary (`npm deprecate @fission-ai/openspec@x.y.z 'reason'`).
- Hotfix (urgent, no pending changesets): create a changeset for the fix and merge the release PR; in emergencies, run a manual bump/publish but reconcile with Changesets by adding a follow-up changeset to align versions.
## Required Secrets
- `NPM_TOKEN` with publish rights for the `@fission-ai` scope.
- Default `GITHUB_TOKEN` (provided by GitHub) for opening/updating the version PR and creating GitHub releases.
## How the Maintainer Flow Changes
| Step | Current process | Future process |
| --- | --- | --- |
| Prepare release | Merge changeset PR, then manually draft release notes and tags | Merge release PR; action updates versions and handles changelog automatically |
| Publish npm package | Happens automatically after GitHub release | Happens automatically via `changeset publish` invoked by the action |
| GitHub release | Draft manually and sync with changelog | Action creates GitHub releases from changeset data |
| Docs/process | Follow manual tagging/release steps | Docs describe automated flow + recovery and hotfix paths |
## Impact
- Automation: reuse `.github/workflows/release-prepare.yml` (phase 1: dry-run, phase 2: publish) and remove `.github/workflows/release-publish.yml` in phase 2.
- Package metadata: add `release` script to `package.json`.
- Docs: update README or `/docs` to show the automated flow, secrets, guardrails, and recovery steps.
## Acceptance Criteria
- Phase 1: merges to `main` open/update a version PR; on merge, the action’s `publish` step is a no-op; no npm publish occurs; logs confirm intended behavior; GitHub releases creation is wired but inert due to no publish.
- Phase 2: merges to `main` run `pnpm run release` from the action; npm package publishes successfully; GitHub release is created automatically; `.github/workflows/release-publish.yml` is removed; no duplicate publishes occur.
@@ -0,0 +1,12 @@
## 1. Release workflow automation
- [ ] 1.1 Add a `.github/workflows/release.yml` that runs on pushes to `main`, sets up pnpm + Node 20, installs dependencies, and invokes `changesets/action@v1` with `publish: pnpm run release`.
- [ ] 1.2 Configure the action with `createGithubReleases: true` and document required secrets (`NPM_TOKEN`, default `GITHUB_TOKEN`) plus recommended concurrency safeguards.
- [ ] 1.3 Validate the workflow using `act` or a dry-run push to confirm the action opens release PRs when changesets exist and publishes when the release PR merge lands.
## 2. Package release script
- [ ] 2.1 Add a `release` script to `package.json` that builds the project and runs `changeset publish` using pnpm.
- [ ] 2.2 Ensure the script respects the existing `prepare`/`prepublishOnly` hooks to avoid duplicate builds and update documentation or scripts if adjustments are needed.
## 3. Documentation and recovery steps
- [ ] 3.1 Update maintainer docs (e.g., README or `/docs`) with the end-to-end automated release flow, explicitly removing the manual tag/release steps that are no longer required and explaining how changesets drive the release PR.
- [ ] 3.2 Document fallback steps for failed publishes (rerun workflow, manual publish) and the hotfix path when a release must be cut without pending changesets.
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.7.0",
"version": "0.8.1",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
@@ -46,6 +46,10 @@
"test:coverage": "vitest --coverage",
"prepare": "pnpm run build",
"prepublishOnly": "pnpm run build",
"check:pack-version": "node scripts/pack-version-check.mjs",
"release": "pnpm run release:ci",
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
"changeset": "changeset"
},
"engines": {
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
// Guard: Ensure the packed tarball's CLI `--version` matches package.json.
//
// Notes:
// - We intentionally use `npm pack` (not pnpm) because `npm pack --json` is
// consistently supported and returns the tarball metadata we need. The
// project uses pnpm for install/publish, but this guard only needs to pack
// locally and verify the installed CLI output.
// - `npm pack` triggers the package's `prepare` script (build), and
// `changeset publish` triggers `prepublishOnly` (also builds here). This
// means an explicit build is not strictly necessary for the guard.
import { execFileSync } from 'child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
function log(msg) {
if (process.env.CI) return; // keep CI logs quiet by default
console.log(msg);
}
function run(cmd, args, opts = {}) {
return execFileSync(cmd, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], ...opts });
}
function npmPack() {
try {
const jsonOut = run('npm', ['pack', '--json', '--silent']);
const arr = JSON.parse(jsonOut);
if (Array.isArray(arr) && arr.length > 0) {
const last = arr[arr.length - 1];
const file = (last && typeof last === 'object' && last.filename) || (typeof last === 'string' ? last : null);
if (file) return String(file).trim();
}
// Unexpected JSON shape or empty array; fallback to plain output
const out = run('npm', ['pack', '--silent']).trim();
const lines = out.split(/\r?\n/);
return lines[lines.length - 1].trim();
} catch (e) {
// Fallback for environments not supporting --json
const out = run('npm', ['pack', '--silent']).trim();
const lines = out.split(/\r?\n/);
return lines[lines.length - 1].trim();
}
}
function main() {
const pkg = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
const expected = pkg.version;
let work;
let tgzPath;
try {
log(`Packing @fission-ai/openspec@${expected}...`);
const filename = npmPack();
tgzPath = path.resolve(filename);
log(`Created: ${tgzPath}`);
work = mkdtempSync(path.join(tmpdir(), 'openspec-pack-check-'));
log(`Temp dir: ${work}`);
// Make a tiny project
writeFileSync(
path.join(work, 'package.json'),
JSON.stringify({ name: 'pack-check', private: true }, null, 2)
);
// Try to avoid noisy output and speed up
const env = {
...process.env,
npm_config_loglevel: 'silent',
npm_config_audit: 'false',
npm_config_fund: 'false',
npm_config_progress: 'false',
};
// Install the tarball
run('npm', ['install', tgzPath, '--silent', '--no-audit', '--no-fund'], { cwd: work, env });
// Run the installed CLI via Node to avoid bin resolution/platform issues
const binRel = path.join('node_modules', '@fission-ai', 'openspec', 'bin', 'openspec.js');
const actual = run(process.execPath, [binRel, '--version'], { cwd: work }).trim();
if (actual !== expected) {
throw new Error(
`Packed CLI version mismatch: expected ${expected}, got ${actual}. ` +
'Ensure the dist is built and the CLI reads version from package.json.'
);
}
log('Version check passed.');
} finally {
// Always attempt cleanup
if (work) {
try { rmSync(work, { recursive: true, force: true }); } catch {}
}
if (tgzPath) {
try { rmSync(tgzPath, { force: true }); } catch {}
}
}
}
try {
main();
console.log('✅ pack-version-check: OK');
} catch (err) {
console.error(`❌ pack-version-check: ${err.message}`);
process.exit(1);
}
+1
View File
@@ -21,5 +21,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
{ name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
+3
View File
@@ -1,6 +1,7 @@
import { SlashCommandConfigurator } from './base.js';
import { ClaudeSlashCommandConfigurator } from './claude.js';
import { CursorSlashCommandConfigurator } from './cursor.js';
import { WindsurfSlashCommandConfigurator } from './windsurf.js';
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
@@ -10,11 +11,13 @@ export class SlashCommandRegistry {
static {
const claude = new ClaudeSlashCommandConfigurator();
const cursor = new CursorSlashCommandConfigurator();
const windsurf = new WindsurfSlashCommandConfigurator();
const kilocode = new KiloCodeSlashCommandConfigurator();
const opencode = new OpenCodeSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(cursor.toolId, cursor);
this.configurators.set(windsurf.toolId, windsurf);
this.configurators.set(kilocode.toolId, kilocode);
this.configurators.set(opencode.toolId, opencode);
}
+27
View File
@@ -0,0 +1,27 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.windsurf/workflows/openspec-proposal.md',
apply: '.windsurf/workflows/openspec-apply.md',
archive: '.windsurf/workflows/openspec-archive.md'
};
export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'windsurf';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string | undefined {
const descriptions: Record<SlashCommandId, string> = {
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
archive: 'Archive a deployed OpenSpec change and update specs.'
};
const description = descriptions[id];
return `---\ndescription: ${description}\nauto_execution_mode: 3\n---`;
}
}
+56
View File
@@ -129,6 +129,50 @@ describe('InitCommand', () => {
expect(updatedContent).toContain('Custom instructions here');
});
it('should create Windsurf workflows when Windsurf is selected', async () => {
queueSelections('windsurf', DONE);
await initCommand.execute(testDir);
const wsProposal = path.join(
testDir,
'.windsurf/workflows/openspec-proposal.md'
);
const wsApply = path.join(
testDir,
'.windsurf/workflows/openspec-apply.md'
);
const wsArchive = path.join(
testDir,
'.windsurf/workflows/openspec-archive.md'
);
expect(await fileExists(wsProposal)).toBe(true);
expect(await fileExists(wsApply)).toBe(true);
expect(await fileExists(wsArchive)).toBe(true);
const proposalContent = await fs.readFile(wsProposal, 'utf-8');
expect(proposalContent).toContain('---');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('auto_execution_mode: 3');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
const applyContent = await fs.readFile(wsApply, 'utf-8');
expect(applyContent).toContain('---');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('auto_execution_mode: 3');
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(wsArchive, 'utf-8');
expect(archiveContent).toContain('---');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('auto_execution_mode: 3');
expect(archiveContent).toContain('<!-- OPENSPEC:START -->');
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
});
it('should always create AGENTS.md in project root', async () => {
queueSelections(DONE);
@@ -399,6 +443,18 @@ describe('InitCommand', () => {
const preselected = secondRunArgs.initialSelected ?? [];
expect(preselected).toContain('kilocode');
});
it('should mark Windsurf as already configured during extend mode', async () => {
queueSelections('windsurf', DONE, 'windsurf', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const wsChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'windsurf'
);
expect(wsChoice.configured).toBe(true);
});
});
describe('error handling', () => {
+75
View File
@@ -220,6 +220,81 @@ Old body
consoleSpy.mockRestore();
});
it('should refresh existing Windsurf workflows', async () => {
const wsPath = path.join(
testDir,
'.windsurf/workflows/openspec-apply.md'
);
await fs.mkdir(path.dirname(wsPath), { recursive: true });
const initialContent = `## OpenSpec: Apply (Windsurf)
Intro
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(wsPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(wsPath, 'utf-8');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
expect(updated).toContain('## OpenSpec: Apply (Windsurf)');
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .windsurf/workflows/openspec-apply.md'
);
consoleSpy.mockRestore();
});
it('should preserve Windsurf content outside markers during update', async () => {
const wsPath = path.join(
testDir,
'.windsurf/workflows/openspec-proposal.md'
);
await fs.mkdir(path.dirname(wsPath), { recursive: true });
const initialContent = `## Custom Intro Title\nSome intro text\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->\n\nFooter stays`;
await fs.writeFile(wsPath, initialContent);
await updateCommand.execute(testDir);
const updated = await fs.readFile(wsPath, 'utf-8');
expect(updated).toContain('## Custom Intro Title');
expect(updated).toContain('Footer stays');
expect(updated).not.toContain('Old body');
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
});
it('should not create missing Windsurf workflows on update', async () => {
const wsApply = path.join(
testDir,
'.windsurf/workflows/openspec-apply.md'
);
// Only create apply; leave proposal and archive missing
await fs.mkdir(path.dirname(wsApply), { recursive: true });
await fs.writeFile(
wsApply,
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
);
await updateCommand.execute(testDir);
const wsProposal = path.join(
testDir,
'.windsurf/workflows/openspec-proposal.md'
);
const wsArchive = path.join(
testDir,
'.windsurf/workflows/openspec-archive.md'
);
// Confirm they weren't created by update
await expect(FileSystemUtils.fileExists(wsProposal)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(wsArchive)).resolves.toBe(false);
});
it('should handle no AI tool files present', async () => {
// Execute update command with no AI tool files
const consoleSpy = vi.spyOn(console, 'log');