mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-15 05:30:21 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
239ddaf21f | ||
|
|
29a3df1bcb | ||
|
|
2563f0f48b | ||
|
|
019acb0011 | ||
|
|
9111a7654d |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@fission-ai/openspec": patch
|
||||
---
|
||||
|
||||
Generated command files are now marked deprecated. While your delivery setting gives a configured tool command files, `openspec update` ends with a short notice: the next major version installs skills only by default, how to switch now (`openspec config set delivery skills && openspec update`), how to keep commands (`openspec config set delivery both`), and a link to the plan. The notice also appears when every tool is already up to date, and it never appears once delivery is `skills` or for tools that get no command files, such as Codex. Nothing about what OpenSpec installs changes in this release.
|
||||
@@ -44,6 +44,9 @@ The picker asks what to configure ([delivery](#delivery-skills-commands-or-both)
|
||||
|
||||
Delivery is a profile setting that lets you choose to have only skills or only commands installed. The default is `both`. [Set up your project](../start/setup.md#the-workflow-files-skills-and-commands) explains the two forms and why both exist. The field's exact contract is in [CLI settings (config.json)](../reference/configuration/config-json.md#delivery).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Command files are deprecated. The next major version of OpenSpec installs skills only by default, and `openspec update` says so on every run while your delivery setting gives a configured tool command files, including runs where everything is already up to date. To switch now, set `delivery` to `skills` and run `openspec update`: it removes the command files OpenSpec generated and lists any you edited instead of deleting them. To keep commands through the next major, set `delivery` to `both` explicitly. The plan is tracked in [#2022](https://github.com/Fission-AI/OpenSpec/issues/2022).
|
||||
|
||||
Two ways to change it:
|
||||
|
||||
**Interactively**: run `openspec config profile` and choose "Delivery only". Here's switching to skills only:
|
||||
|
||||
@@ -24,7 +24,7 @@ Which workflow set `openspec init` installs. Defaults to `core`: propose, explor
|
||||
|
||||
### delivery
|
||||
|
||||
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`.
|
||||
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`. Commands are deprecated: the next major version changes the default to `skills`, and an explicit `both` or `commands` keeps its value ([#2022](https://github.com/Fission-AI/OpenSpec/issues/2022)).
|
||||
|
||||
### workflows
|
||||
|
||||
|
||||
@@ -26,3 +26,15 @@ overrides:
|
||||
# Dev-only (transitive via @vitest/ui); never in the published CLI. Remove once
|
||||
# transitive fflate is >=0.8.3 (check: pnpm why fflate).
|
||||
fflate@<0.8.3: '>=0.8.3 <0.9'
|
||||
|
||||
auditConfig:
|
||||
ignoreGhsas:
|
||||
# GHSA-vfj7-8cjw-p6xm — braces <=3.0.3 stack exhaustion on deeply nested
|
||||
# patterns. Runtime (fast-glob > micromatch > braces), and no patched braces
|
||||
# exists, so no override can fix it. Accepted residual risk: schema artifact
|
||||
# `generates` and `apply.tracks` patterns reach fast-glob through
|
||||
# resolveArtifactOutputs without a brace-depth guard, and the 10,000-character
|
||||
# input limit does not prevent stack exhaustion. A crafted project-local or
|
||||
# user schema can still terminate the openspec process. Remove this exception
|
||||
# once braces ships a patched release (check: pnpm why braces; pnpm audit --prod).
|
||||
- GHSA-vfj7-8cjw-p6xm
|
||||
|
||||
@@ -41,3 +41,38 @@ export function shouldGenerateCommandsForTool(toolId: string, delivery: Delivery
|
||||
export function shouldReconcileCommandFilesForTool(toolId: string, delivery: Delivery): boolean {
|
||||
return delivery === 'skills' && resolveCommandSurfaceCapability(toolId) === 'adapter-backed';
|
||||
}
|
||||
|
||||
export const COMMAND_DEPRECATION_URL = 'https://github.com/Fission-AI/OpenSpec/issues/2022';
|
||||
|
||||
/**
|
||||
* The notice `openspec update` prints while delivery gives a tool command files,
|
||||
* which are deprecated in favor of skills. It is shown only for a tool that
|
||||
* switching to skills-only delivery would change: one that gets command files
|
||||
* now and would not under `skills`. A skills-only project is never told about a
|
||||
* change that does not affect it, and a tool that keeps its commands under
|
||||
* every delivery is never advised to switch away from its only working surface.
|
||||
*
|
||||
* Worded "by default" so it holds both for users on the implicit default, whom
|
||||
* the next major moves to skills, and for users who set `delivery` explicitly,
|
||||
* whose setting the next major keeps.
|
||||
*/
|
||||
export function formatCommandDeprecationNotice(
|
||||
toolIds: readonly string[],
|
||||
delivery: Delivery
|
||||
): string[] {
|
||||
const switchingChangesSomething = toolIds.some(
|
||||
(toolId) =>
|
||||
shouldGenerateCommandsForTool(toolId, delivery) &&
|
||||
!shouldGenerateCommandsForTool(toolId, 'skills')
|
||||
);
|
||||
if (!switchingChangesSomething) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'Command files are deprecated: the next major version of OpenSpec installs skills only by default.',
|
||||
' Switch to skills now: openspec config set delivery skills && openspec update',
|
||||
' Keep commands for now: openspec config set delivery both',
|
||||
` Details: ${COMMAND_DEPRECATION_URL}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
resolveCommandSurfaceCapability,
|
||||
resolveCommandInvocation,
|
||||
shouldGenerateCommandsForTool,
|
||||
formatCommandDeprecationNotice,
|
||||
shouldGenerateSkillsForTool,
|
||||
shouldReconcileCommandFilesForTool,
|
||||
shouldRemoveSkillsForTool,
|
||||
@@ -280,6 +281,7 @@ export class UpdateCommand {
|
||||
this.detectNewTools(resolvedProjectPath, configuredTools);
|
||||
this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
|
||||
this.displaySetupNotes(configuredTools);
|
||||
this.displayCommandDeprecation(configuredTools, delivery);
|
||||
throwIfUpdateFailed(legacyUpgradeFailures);
|
||||
return;
|
||||
}
|
||||
@@ -525,6 +527,7 @@ export class UpdateCommand {
|
||||
// 14. Display the profile notes
|
||||
this.displayProfileNotes(resolvedProjectPath, configuredAndNewTools, desiredWorkflows, profile, delivery);
|
||||
this.displaySetupNotes(configuredAndNewTools);
|
||||
this.displayCommandDeprecation(configuredAndNewTools, delivery);
|
||||
|
||||
// 15. List affected tools
|
||||
if (updatedTools.length > 0) {
|
||||
@@ -696,6 +699,23 @@ export class UpdateCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces that generated command files are deprecated, on every run while
|
||||
* delivery gives a configured tool command files, whether or not this run
|
||||
* rewrote them. `update` is run on upgrade, so this reaches users
|
||||
* before the next major changes the default, without nagging day to day.
|
||||
*/
|
||||
private displayCommandDeprecation(toolIds: string[], delivery: Delivery): void {
|
||||
const [headline, ...detail] = formatCommandDeprecationNotice(toolIds, delivery);
|
||||
if (!headline) return;
|
||||
|
||||
console.log();
|
||||
console.log(chalk.yellow(headline));
|
||||
for (const line of detail) {
|
||||
console.log(chalk.dim(line));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects new tool directories that aren't currently configured and displays a hint.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
COMMAND_DEPRECATION_URL,
|
||||
formatCommandDeprecationNotice,
|
||||
} from '../../src/core/command-surface.js';
|
||||
|
||||
describe('formatCommandDeprecationNotice', () => {
|
||||
it.each(['both', 'commands'] as const)(
|
||||
'announces the change when delivery %s still generates command files',
|
||||
(delivery) => {
|
||||
expect(formatCommandDeprecationNotice(['claude'], delivery)).toEqual([
|
||||
'Command files are deprecated: the next major version of OpenSpec installs skills only by default.',
|
||||
' Switch to skills now: openspec config set delivery skills && openspec update',
|
||||
' Keep commands for now: openspec config set delivery both',
|
||||
` Details: ${COMMAND_DEPRECATION_URL}`,
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it('says nothing to a project already on skills-only delivery', () => {
|
||||
expect(formatCommandDeprecationNotice(['claude', 'cursor'], 'skills')).toEqual([]);
|
||||
});
|
||||
|
||||
it('says nothing when no configured tool gets command files', () => {
|
||||
// Codex is skills-invocable and has no command adapter, so a Codex-only
|
||||
// project is unaffected by the deprecation even on the default delivery.
|
||||
expect(formatCommandDeprecationNotice(['codex'], 'both')).toEqual([]);
|
||||
expect(formatCommandDeprecationNotice([], 'both')).toEqual([]);
|
||||
});
|
||||
|
||||
it('announces it once for a mixed project where one tool gets commands', () => {
|
||||
expect(formatCommandDeprecationNotice(['codex', 'claude'], 'both')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
@@ -1905,6 +1905,52 @@ metadata:
|
||||
});
|
||||
});
|
||||
|
||||
describe('command deprecation notice', () => {
|
||||
const headline = 'Command files are deprecated';
|
||||
|
||||
async function runUpdate(command: UpdateCommand = new UpdateCommand()): Promise<string[]> {
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await command.execute(testDir);
|
||||
const logs = consoleSpy.mock.calls.flat().map(String);
|
||||
consoleSpy.mockRestore();
|
||||
return logs;
|
||||
}
|
||||
|
||||
it('announces the deprecation when update regenerates command files', async () => {
|
||||
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
|
||||
|
||||
const logs = await runUpdate(new UpdateCommand({ force: true }));
|
||||
|
||||
expect(logs.some((entry) => entry.includes(headline))).toBe(true);
|
||||
expect(
|
||||
logs.some((entry) => entry.includes('openspec config set delivery skills && openspec update'))
|
||||
).toBe(true);
|
||||
expect(logs.some((entry) => entry.includes('issues/2022'))).toBe(true);
|
||||
});
|
||||
|
||||
it('still announces it when every tool is already up to date', async () => {
|
||||
// The up-to-date path returns early, and is what most users hit on a
|
||||
// routine run. Missing it there would hide the notice from them.
|
||||
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
|
||||
|
||||
const logs = await runUpdate();
|
||||
|
||||
expect(logs.some((entry) => entry.includes('up to date'))).toBe(true);
|
||||
expect(logs.some((entry) => entry.includes(headline))).toBe(true);
|
||||
});
|
||||
|
||||
it('stays quiet once delivery is skills-only', async () => {
|
||||
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
|
||||
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
|
||||
|
||||
const forced = await runUpdate(new UpdateCommand({ force: true }));
|
||||
const routine = await runUpdate();
|
||||
|
||||
expect(forced.some((entry) => entry.includes(headline))).toBe(false);
|
||||
expect(routine.some((entry) => entry.includes(headline))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should report a failed legacy-only Codex bootstrap to automation', async () => {
|
||||
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'commands' });
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
COMMAND_DEPRECATION_URL,
|
||||
formatCommandDeprecationNotice,
|
||||
} from '../src/core/command-surface.js';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const read = (...segments: string[]) =>
|
||||
fs.readFileSync(path.join(REPO_ROOT, 'docs-lab', ...segments), 'utf-8');
|
||||
|
||||
const PROFILES = read('customize', 'profiles.md');
|
||||
const DELIVERY_SECTION = PROFILES.split('## Delivery: skills, commands, or both')[1].split(
|
||||
'## Switching profiles'
|
||||
)[0];
|
||||
const CONFIG_JSON = read('reference', 'configuration', 'config-json.md');
|
||||
const DELIVERY_FIELD = CONFIG_JSON.split('### delivery')[1].split('### workflows')[0];
|
||||
|
||||
// The CLI notice and the docs announce the same deprecation. Pin them to each
|
||||
// other so neither can change its advice without the other (#2022).
|
||||
describe('command deprecation documentation', () => {
|
||||
const notice = formatCommandDeprecationNotice(['claude'], 'both').join('\n');
|
||||
|
||||
it('says what the CLI notice says in the delivery guide', () => {
|
||||
expect(notice).toContain('the next major version of OpenSpec installs skills only by default');
|
||||
expect(DELIVERY_SECTION).toContain(
|
||||
'The next major version of OpenSpec installs skills only by default'
|
||||
);
|
||||
});
|
||||
|
||||
it('gives the same two choices as the notice', () => {
|
||||
expect(notice).toContain('openspec config set delivery skills && openspec update');
|
||||
expect(DELIVERY_SECTION).toContain('set `delivery` to `skills` and run `openspec update`');
|
||||
expect(notice).toContain('openspec config set delivery both');
|
||||
expect(DELIVERY_SECTION).toContain('set `delivery` to `both` explicitly');
|
||||
});
|
||||
|
||||
it('links the same tracking issue from the notice and both pages', () => {
|
||||
const issue = COMMAND_DEPRECATION_URL;
|
||||
expect(notice).toContain(issue);
|
||||
expect(DELIVERY_SECTION).toContain(issue);
|
||||
expect(DELIVERY_FIELD).toContain(issue);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user