mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-16 05:40:40 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561bc44f5d | ||
|
|
497ff3c36c | ||
|
|
ce49e88a78 | ||
|
|
9111a7654d |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@fission-ai/openspec": patch
|
||||
---
|
||||
|
||||
Amazon Q Developer now keeps its prompts under every delivery setting, including `skills`. Amazon Q reads prompts from `.amazonq/prompts/` but no skills folder, so the skills OpenSpec wrote to `.amazonq/skills/` never loaded, and switching to skills-only delivery would have left Amazon Q with no working workflow. OpenSpec no longer writes those skills. Existing ones are left in place under `both` and `skills` delivery and can be deleted by hand; `commands` delivery still removes them, as it does for every tool. Two side effects are handled: leftover skill files no longer make `openspec update` report Amazon Q as needing an update on every run, and a project where only Amazon Q is configured no longer saves `delivery: commands` to your global config on first run, which would have removed skills from your other projects.
|
||||
@@ -14,7 +14,7 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
|
||||
|
||||
| Tool | `--tools` id | Skills | Skill invocation | Commands | Command invocation |
|
||||
|---|---|---|---|---|---|
|
||||
| Amazon Q Developer | `amazon-q` | `.amazonq/skills/` | `/openspec-apply-change` | `.amazonq/prompts/` | `@opsx-apply` |
|
||||
| Amazon Q Developer | `amazon-q` | none | none | `.amazonq/prompts/` | `@opsx-apply` |
|
||||
| Amp | `amp` | `.agents/skills/` | `/openspec-apply-change` | none | none |
|
||||
| Antigravity | `antigravity` | `.agents/skills/` | `/openspec-apply-change` | `.agents/workflows/` | `/opsx-apply` |
|
||||
| AtomCode | `atomcode` | `.atomcode/skills/` | `/openspec-apply-change` | `.atomcode/commands/` | `/opsx-apply` |
|
||||
@@ -72,6 +72,10 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
|
||||
|
||||
A tool not listed here behaves exactly as its row reads.
|
||||
|
||||
### Amazon Q Developer
|
||||
|
||||
Amazon Q reads prompts from `.amazonq/prompts/` but no skills folder, so OpenSpec installs its workflows as prompts only, under every delivery setting, including `skills`. Invoke one with `@`, as in `@opsx-propose`. Earlier releases also wrote `.amazonq/skills/`, which Amazon Q never loaded. Those folders do nothing, and OpenSpec leaves them in place under `both` and `skills` delivery, so you can delete them yourself; `commands` delivery removes them, as it removes every tool's skills. AWS [ends support for the Amazon Q Developer IDE plugins on April 30, 2027](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-developer-ide-end-of-support.html) and points users to Kiro, which OpenSpec also supports (`--tools kiro`).
|
||||
|
||||
### Amp
|
||||
|
||||
- **Project skills**: Amp reads OpenSpec skills from `.agents/skills/`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,21 @@ export function resolveCommandSurfaceCapability(toolId: string): CommandSurfaceC
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools that load OpenSpec workflows only as command files, because nothing
|
||||
* they run reads a SKILL.md folder. Amazon Q Developer reads its prompts from
|
||||
* `.amazonq/prompts` but no skills directory, and AWS is retiring it in favor
|
||||
* of Kiro. Such a tool keeps its commands under every delivery setting: skills
|
||||
* alone would leave it with no way to start a workflow.
|
||||
*/
|
||||
const COMMANDS_ONLY_TOOLS: ReadonlySet<string> = new Set(['amazon-q']);
|
||||
|
||||
export function isCommandsOnlyTool(toolId: string): boolean {
|
||||
return COMMANDS_ONLY_TOOLS.has(toolId);
|
||||
}
|
||||
|
||||
export function shouldGenerateSkillsForTool(toolId: string, delivery: Delivery): boolean {
|
||||
if (isCommandsOnlyTool(toolId)) return false;
|
||||
return delivery !== 'commands' || resolveCommandSurfaceCapability(toolId) === 'skills-invocable';
|
||||
}
|
||||
|
||||
@@ -35,9 +49,16 @@ export function shouldRemoveSkillsForTool(toolId: string, delivery: Delivery): b
|
||||
}
|
||||
|
||||
export function shouldGenerateCommandsForTool(toolId: string, delivery: Delivery): boolean {
|
||||
return delivery !== 'skills' && resolveCommandSurfaceCapability(toolId) === 'adapter-backed';
|
||||
return (
|
||||
resolveCommandSurfaceCapability(toolId) === 'adapter-backed' &&
|
||||
(delivery !== 'skills' || isCommandsOnlyTool(toolId))
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldReconcileCommandFilesForTool(toolId: string, delivery: Delivery): boolean {
|
||||
return delivery === 'skills' && resolveCommandSurfaceCapability(toolId) === 'adapter-backed';
|
||||
return (
|
||||
delivery === 'skills' &&
|
||||
resolveCommandSurfaceCapability(toolId) === 'adapter-backed' &&
|
||||
!isCommandsOnlyTool(toolId)
|
||||
);
|
||||
}
|
||||
|
||||
+26
-5
@@ -10,6 +10,7 @@ import { getGlobalConfig, getGlobalConfigPath, isGlobalConfigUnreadable, saveGlo
|
||||
import { CommandAdapterRegistry } from './command-generation/index.js';
|
||||
import {
|
||||
resolveCommandInvocation,
|
||||
isCommandsOnlyTool,
|
||||
resolveCommandSurfaceCapability,
|
||||
shouldGenerateCommandsForTool,
|
||||
} from './command-surface.js';
|
||||
@@ -467,6 +468,12 @@ function scanInstalledWorkflowArtifacts(
|
||||
for (const tool of tools) {
|
||||
if (!toolSupportsSkills(tool)) continue;
|
||||
|
||||
// A commands-only tool has command files under every delivery setting, so
|
||||
// its files show which workflows are installed but say nothing about the
|
||||
// user's delivery preference. Counting them would let one such project
|
||||
// save `delivery: commands` globally and strip skills from every other.
|
||||
const showsDeliveryPreference = !isCommandsOnlyTool(tool.value);
|
||||
|
||||
const skillsDirs: string[] = [];
|
||||
if (tool.globalSkillsDir) {
|
||||
skillsDirs.push(resolveToolSkillsDir(projectPath, tool));
|
||||
@@ -487,7 +494,7 @@ function scanInstalledWorkflowArtifacts(
|
||||
const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md');
|
||||
if (fs.existsSync(skillFile)) {
|
||||
installed.add(workflowId);
|
||||
hasSkills = true;
|
||||
if (showsDeliveryPreference) hasSkills = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,7 +525,7 @@ function scanInstalledWorkflowArtifacts(
|
||||
: path.join(projectPath, candidate);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
installed.add(workflowId);
|
||||
hasCommands = true;
|
||||
if (showsDeliveryPreference) hasCommands = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -540,14 +547,21 @@ export function scanInstalledWorkflows(projectPath: string, tools: AIToolOption[
|
||||
return scanInstalledWorkflowArtifacts(projectPath, tools).workflows;
|
||||
}
|
||||
|
||||
function inferDelivery(artifacts: InstalledWorkflowArtifacts): Delivery {
|
||||
/**
|
||||
* The delivery the installed files imply, or undefined when they imply none:
|
||||
* every file found belongs to a tool whose surface does not follow delivery.
|
||||
*/
|
||||
function inferDelivery(artifacts: InstalledWorkflowArtifacts): Delivery | undefined {
|
||||
if (artifacts.hasSkills && artifacts.hasCommands) {
|
||||
return 'both';
|
||||
}
|
||||
if (artifacts.hasCommands) {
|
||||
return 'commands';
|
||||
}
|
||||
return 'skills';
|
||||
if (artifacts.hasSkills) {
|
||||
return 'skills';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,7 +612,14 @@ export function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): voi
|
||||
config.profile = 'custom';
|
||||
config.workflows = installedWorkflows;
|
||||
if (rawConfig.delivery === undefined) {
|
||||
config.delivery = inferDelivery(artifacts);
|
||||
const inferred = inferDelivery(artifacts);
|
||||
if (inferred) {
|
||||
config.delivery = inferred;
|
||||
} else {
|
||||
// No preference was shown, so record none: the user stays on the
|
||||
// default, rather than being pinned to whatever it is today.
|
||||
delete config.delivery;
|
||||
}
|
||||
}
|
||||
saveGlobalConfig(config);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
shouldGenerateCommandsForTool,
|
||||
shouldGenerateSkillsForTool,
|
||||
resolveCommandSurfaceCapability,
|
||||
isCommandsOnlyTool,
|
||||
} from '../command-surface.js';
|
||||
import {
|
||||
getSkillCapableTools,
|
||||
@@ -105,6 +106,12 @@ export function getToolSkillStatus(projectRoot: string, toolId: string): ToolSki
|
||||
if (tool.skillsDir && !isSharedSkillTargetActive(projectRoot, toolId)) {
|
||||
return { configured: false, fullyConfigured: false, skillCount: 0 };
|
||||
}
|
||||
// Skill files left over for a tool that cannot load skills are inert. They
|
||||
// must not make the tool look skill-configured, or detection would keep
|
||||
// reading a version from files this CLI no longer writes.
|
||||
if (isCommandsOnlyTool(toolId)) {
|
||||
return { configured: false, fullyConfigured: false, skillCount: 0 };
|
||||
}
|
||||
|
||||
const skillsDirs = [
|
||||
resolveToolSkillsDir(projectRoot, tool),
|
||||
@@ -324,12 +331,16 @@ export function getToolVersionStatus(
|
||||
};
|
||||
}
|
||||
|
||||
const skillsDirs = [
|
||||
resolveToolSkillsDir(projectRoot, tool),
|
||||
...(tool.legacySkillsDirs ?? []).map((root) =>
|
||||
path.join(projectRoot, root, 'skills')
|
||||
),
|
||||
];
|
||||
// A commands-only tool is versioned by its command files alone; see
|
||||
// getToolSkillStatus for why its leftover skill files are ignored.
|
||||
const skillsDirs = isCommandsOnlyTool(toolId)
|
||||
? []
|
||||
: [
|
||||
resolveToolSkillsDir(projectRoot, tool),
|
||||
...(tool.legacySkillsDirs ?? []).map((root) =>
|
||||
path.join(projectRoot, root, 'skills')
|
||||
),
|
||||
];
|
||||
let generatedByVersion: string | null = null;
|
||||
let foundSkill = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isCommandsOnlyTool,
|
||||
shouldGenerateCommandsForTool,
|
||||
shouldGenerateSkillsForTool,
|
||||
shouldReconcileCommandFilesForTool,
|
||||
shouldRemoveSkillsForTool,
|
||||
} from '../../src/core/command-surface.js';
|
||||
|
||||
const DELIVERIES = ['both', 'skills', 'commands'] as const;
|
||||
|
||||
describe('commands-only tools', () => {
|
||||
it('treats Amazon Q as commands-only, and an ordinary command tool as not', () => {
|
||||
expect(isCommandsOnlyTool('amazon-q')).toBe(true);
|
||||
expect(isCommandsOnlyTool('claude')).toBe(false);
|
||||
expect(isCommandsOnlyTool('codex')).toBe(false);
|
||||
});
|
||||
|
||||
it.each(DELIVERIES)('gives Amazon Q commands and no skills under delivery %s', (delivery) => {
|
||||
expect(shouldGenerateCommandsForTool('amazon-q', delivery)).toBe(true);
|
||||
expect(shouldGenerateSkillsForTool('amazon-q', delivery)).toBe(false);
|
||||
// Its commands are its only surface, so they are never cleared as the
|
||||
// leftovers of a skills-only switch.
|
||||
expect(shouldReconcileCommandFilesForTool('amazon-q', delivery)).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves its leftover skills in place except where commands-only delivery always removed them', () => {
|
||||
expect(shouldRemoveSkillsForTool('amazon-q', 'both')).toBe(false);
|
||||
expect(shouldRemoveSkillsForTool('amazon-q', 'skills')).toBe(false);
|
||||
// Unchanged: `delivery: commands` already removed every tool's skills.
|
||||
expect(shouldRemoveSkillsForTool('amazon-q', 'commands')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not change any other tool', () => {
|
||||
expect(shouldGenerateCommandsForTool('claude', 'skills')).toBe(false);
|
||||
expect(shouldGenerateSkillsForTool('claude', 'skills')).toBe(true);
|
||||
expect(shouldReconcileCommandFilesForTool('claude', 'skills')).toBe(true);
|
||||
expect(shouldGenerateSkillsForTool('codex', 'commands')).toBe(true);
|
||||
});
|
||||
});
|
||||
+8
-10
@@ -2623,20 +2623,18 @@ describe('InitCommand - profile and detection features', () => {
|
||||
it('should print the @-prefixed prompt hint for amazon-q (prompt library, no slash surface)', async () => {
|
||||
// Amazon Q loads .amazonq/prompts/opsx-<id>.md into its prompt library,
|
||||
// invoked as @opsx-<id>. It registers no slash command under any spelling,
|
||||
// so neither the hint, the generated prompts, the skills, nor the restart
|
||||
// line may name one.
|
||||
// so neither the hint, the generated prompts, nor the restart line may name
|
||||
// one. It reads no skills directory, so no skills are written for it.
|
||||
const initCommand = new InitCommand({ tools: 'amazon-q', force: true });
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const promptFile = path.join(testDir, '.amazonq', 'prompts', 'opsx-apply.md');
|
||||
const skillFile = path.join(testDir, '.amazonq', 'skills', 'openspec-apply-change', 'SKILL.md');
|
||||
for (const file of [promptFile, skillFile]) {
|
||||
expect(await fileExists(file)).toBe(true);
|
||||
const content = await fs.readFile(file, 'utf-8');
|
||||
expect(content).toContain('@opsx-apply');
|
||||
expect(content).not.toContain('/opsx:');
|
||||
expect(content).not.toContain('/opsx-');
|
||||
}
|
||||
expect(await fileExists(promptFile)).toBe(true);
|
||||
const content = await fs.readFile(promptFile, 'utf-8');
|
||||
expect(content).toContain('@opsx-apply');
|
||||
expect(content).not.toContain('/opsx:');
|
||||
expect(content).not.toContain('/opsx-');
|
||||
expect(await fileExists(path.join(testDir, '.amazonq', 'skills'))).toBe(false);
|
||||
|
||||
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
|
||||
const startHint = logCalls.find((entry) => entry.includes('Start your first change'));
|
||||
|
||||
@@ -124,6 +124,40 @@ describe('migration', () => {
|
||||
expect(config.workflows).toEqual(['explore', 'archive']);
|
||||
});
|
||||
|
||||
it('does not save a delivery from an Amazon Q project, whose prompts exist under every delivery', async () => {
|
||||
// Amazon Q keeps its prompts whatever delivery is set, so they show which
|
||||
// workflows are installed but not what delivery the user wants. Inferring
|
||||
// `commands` from them would be saved globally and strip skills from
|
||||
// every other project on the machine.
|
||||
await writeManagedCommand(projectDir, 'explore', 'amazon-q');
|
||||
await writeManagedCommand(projectDir, 'archive', 'amazon-q');
|
||||
|
||||
migrateIfNeeded(projectDir, [requireTool('amazon-q')]);
|
||||
|
||||
const config = readRawConfig();
|
||||
expect(config.profile).toBe('custom');
|
||||
expect(config.workflows).toEqual(['explore', 'archive']);
|
||||
expect(config).not.toHaveProperty('delivery');
|
||||
});
|
||||
|
||||
it('ignores leftover Amazon Q skills too when inferring delivery', async () => {
|
||||
await writeManagedCommand(projectDir, 'explore', 'amazon-q');
|
||||
await writeSkill(projectDir, 'openspec-explore', '.amazonq');
|
||||
|
||||
migrateIfNeeded(projectDir, [requireTool('amazon-q')]);
|
||||
|
||||
expect(readRawConfig()).not.toHaveProperty('delivery');
|
||||
});
|
||||
|
||||
it('still infers delivery from the other tools in a project that has Amazon Q', async () => {
|
||||
await writeManagedCommand(projectDir, 'explore', 'amazon-q');
|
||||
await writeSkill(projectDir, 'openspec-explore');
|
||||
|
||||
migrateIfNeeded(projectDir, [requireTool('amazon-q'), ensureClaudeTool()]);
|
||||
|
||||
expect(readRawConfig().delivery).toBe('skills');
|
||||
});
|
||||
|
||||
it('migrates to custom both delivery when managed skills and commands are detected', async () => {
|
||||
await writeSkill(projectDir, 'openspec-explore');
|
||||
await writeManagedCommand(projectDir, 'apply');
|
||||
|
||||
+70
-19
@@ -1254,29 +1254,18 @@ metadata:
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const promptFile = path.join(testDir, '.amazonq', 'prompts', 'opsx-apply.md');
|
||||
const skillFile = path.join(
|
||||
testDir,
|
||||
'.amazonq',
|
||||
'skills',
|
||||
'openspec-apply-change',
|
||||
'SKILL.md'
|
||||
);
|
||||
for (const file of [promptFile, skillFile]) {
|
||||
const stale = (await fs.readFile(file, 'utf-8')).replace(/@opsx-/g, '/opsx:');
|
||||
await fs.writeFile(file, stale);
|
||||
}
|
||||
const stale = (await fs.readFile(promptFile, 'utf-8')).replace(/@opsx-/g, '/opsx:');
|
||||
await fs.writeFile(promptFile, stale);
|
||||
expect(await fs.readFile(promptFile, 'utf-8')).toContain('/opsx:apply');
|
||||
|
||||
await new UpdateCommand({ force: true }).execute(testDir);
|
||||
|
||||
for (const file of [promptFile, skillFile]) {
|
||||
const refreshed = await fs.readFile(file, 'utf-8');
|
||||
// Positive assertion too: dropping every reference would satisfy the
|
||||
// negative ones. And no stray slash may survive the rewrite.
|
||||
expect(refreshed).toContain('@opsx-apply');
|
||||
expect(refreshed).not.toContain('/opsx:');
|
||||
expect(refreshed).not.toContain('/opsx-');
|
||||
}
|
||||
const refreshed = await fs.readFile(promptFile, 'utf-8');
|
||||
// Positive assertion too: dropping every reference would satisfy the
|
||||
// negative ones. And no stray slash may survive the rewrite.
|
||||
expect(refreshed).toContain('@opsx-apply');
|
||||
expect(refreshed).not.toContain('/opsx:');
|
||||
expect(refreshed).not.toContain('/opsx-');
|
||||
// The prompt body cross-references other prompts; those move too.
|
||||
expect(await fs.readFile(promptFile, 'utf-8')).toContain('@opsx-archive');
|
||||
});
|
||||
@@ -2138,6 +2127,68 @@ metadata:
|
||||
});
|
||||
});
|
||||
|
||||
describe('commands-only tools (Amazon Q)', () => {
|
||||
const prompts = () => path.join(testDir, '.amazonq', 'prompts');
|
||||
const skills = () => path.join(testDir, '.amazonq', 'skills');
|
||||
|
||||
it('keeps Amazon Q prompts under skills-only delivery, its only working surface', async () => {
|
||||
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
|
||||
await new InitCommand({ tools: 'amazon-q', force: true }).execute(testDir);
|
||||
await new UpdateCommand({ force: true }).execute(testDir);
|
||||
|
||||
expect((await fs.readdir(prompts())).sort()).toContain('opsx-propose.md');
|
||||
expect(await FileSystemUtils.fileExists(skills())).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves skill files from an older install untouched rather than deleting them', async () => {
|
||||
// Written by an earlier release, which gave Amazon Q skills it never
|
||||
// loaded. They are inert; removing directories as a side effect of an
|
||||
// upgrade is not worth the risk.
|
||||
await new InitCommand({ tools: 'amazon-q', force: true }).execute(testDir);
|
||||
const leftover = path.join(skills(), 'openspec-propose', 'SKILL.md');
|
||||
await fs.mkdir(path.dirname(leftover), { recursive: true });
|
||||
const leftoverContent = '---\nname: openspec-propose\nmetadata:\n generatedBy: "0.1.0"\n---\nold\n';
|
||||
await fs.writeFile(leftover, leftoverContent);
|
||||
|
||||
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
|
||||
await new UpdateCommand({ force: true }).execute(testDir);
|
||||
|
||||
expect(await fs.readFile(leftover, 'utf-8')).toBe(leftoverContent);
|
||||
});
|
||||
|
||||
it('does not ask to update Amazon Q forever because of old skill files', async () => {
|
||||
// Detection used to read a version from the skill files. Once this CLI
|
||||
// stops writing them, an old version there would mark the tool stale on
|
||||
// every run.
|
||||
await new InitCommand({ tools: 'amazon-q', force: true }).execute(testDir);
|
||||
const leftover = path.join(skills(), 'openspec-propose', 'SKILL.md');
|
||||
await fs.mkdir(path.dirname(leftover), { recursive: true });
|
||||
await fs.writeFile(leftover, '---\nname: openspec-propose\nmetadata:\n generatedBy: "0.1.0"\n---\nold\n');
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await new UpdateCommand().execute(testDir);
|
||||
const logs = consoleSpy.mock.calls.flat().map(String);
|
||||
|
||||
expect(logs.some((entry) => entry.includes('up to date'))).toBe(true);
|
||||
expect(logs.some((entry) => entry.includes('Updating 1 tool'))).toBe(false);
|
||||
});
|
||||
|
||||
it('clears another tool\'s commands under skills-only delivery but keeps Amazon Q\'s', async () => {
|
||||
await new InitCommand({ tools: 'claude,amazon-q', force: true }).execute(testDir);
|
||||
|
||||
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
|
||||
await new UpdateCommand({ force: true }).execute(testDir);
|
||||
|
||||
expect(
|
||||
await FileSystemUtils.fileExists(path.join(testDir, '.claude', 'commands', 'opsx', 'propose.md'))
|
||||
).toBe(false);
|
||||
expect(
|
||||
await FileSystemUtils.fileExists(path.join(testDir, '.claude', 'skills', 'openspec-propose', 'SKILL.md'))
|
||||
).toBe(true);
|
||||
expect(await FileSystemUtils.fileExists(path.join(prompts(), 'opsx-propose.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool detection', () => {
|
||||
it('should detect tool as configured only when skill file exists', async () => {
|
||||
// Create skills directory but no skill files
|
||||
|
||||
Reference in New Issue
Block a user