mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
fix: make onboarding guidance tool-aware
This commit is contained in:
@@ -98,9 +98,20 @@ cd your-project
|
||||
openspec init
|
||||
```
|
||||
|
||||
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
|
||||
By default, OpenSpec installs the `core` workflow set only:
|
||||
- `propose`
|
||||
- `explore`
|
||||
- `apply`
|
||||
- `archive`
|
||||
|
||||
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`.
|
||||
Then tell your AI using the syntax that matches your tool:
|
||||
- **Claude Code and other colon-style tools:** `/opsx:propose <what-you-want-to-build>`
|
||||
- **OpenCode, Cursor, Windsurf, Copilot IDE, and other hyphen-style tools:** `/opsx-propose <what-you-want-to-build>`
|
||||
- **Skills-only delivery or skill-based tools:** `/openspec-propose <what-you-want-to-build>`
|
||||
|
||||
If command files are missing, your setup may be using `skills` delivery by design. See [Supported Tools](docs/supported-tools.md) for the full delivery and syntax model.
|
||||
|
||||
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), run `openspec config profile` and then `openspec update`.
|
||||
|
||||
> [!NOTE]
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
|
||||
|
||||
+30
-26
@@ -4,28 +4,42 @@ This is the reference for OpenSpec's slash commands. These commands are invoked
|
||||
|
||||
For workflow patterns and when to use each command, see [Workflows](workflows.md). For CLI commands, see [CLI](cli.md).
|
||||
|
||||
## First: Use the Syntax Your Tool Expects
|
||||
|
||||
OpenSpec command intent is shared, but the invocation syntax is tool-dependent. Check this before debugging a setup.
|
||||
|
||||
| Tool family | Syntax example |
|
||||
|-------------|----------------|
|
||||
| Claude Code and other colon-style integrations | `/opsx:propose`, `/opsx:apply` |
|
||||
| OpenCode, Cursor, Windsurf, GitHub Copilot IDE, Bob Shell, Pi, and other hyphen-style integrations | `/opsx-propose`, `/opsx-apply` |
|
||||
| Skill-based tools or skills-only delivery | `/openspec-propose`, `/openspec-apply-change` |
|
||||
|
||||
If you are not sure which surface your tool uses, check [Supported Tools](supported-tools.md).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
By default, OpenSpec installs the `core` profile only: `propose`, `explore`, `apply`, and `archive`. Expanded workflows are opt-in.
|
||||
|
||||
### Default Quick Path (`core` profile)
|
||||
|
||||
| Command | Purpose |
|
||||
| Command intent | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
|
||||
| `/opsx:explore` | Think through ideas before committing to a change |
|
||||
| `/opsx:apply` | Implement tasks from the change |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
| `propose` | Create a change and generate planning artifacts in one step |
|
||||
| `explore` | Think through ideas before committing to a change |
|
||||
| `apply` | Implement tasks from the change |
|
||||
| `archive` | Archive a completed change |
|
||||
|
||||
### Expanded Workflow Commands (custom workflow selection)
|
||||
### Expanded Workflow Commands (opt-in via `openspec config profile` + `openspec update`)
|
||||
|
||||
| Command | Purpose |
|
||||
| Command intent | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:new` | Start a new change scaffold |
|
||||
| `/opsx:continue` | Create the next artifact based on dependencies |
|
||||
| `/opsx:ff` | Fast-forward: create all planning artifacts at once |
|
||||
| `/opsx:verify` | Validate implementation matches artifacts |
|
||||
| `/opsx:sync` | Merge delta specs into main specs |
|
||||
| `/opsx:bulk-archive` | Archive multiple changes at once |
|
||||
| `/opsx:onboard` | Guided tutorial through the complete workflow |
|
||||
| `new` | Start a new change scaffold |
|
||||
| `continue` | Create the next artifact based on dependencies |
|
||||
| `ff` | Fast-forward: create all planning artifacts at once |
|
||||
| `verify` | Validate implementation matches artifacts |
|
||||
| `sync` | Merge delta specs into main specs |
|
||||
| `bulk-archive` | Archive multiple changes at once |
|
||||
| `onboard` | Guided tutorial through the complete workflow |
|
||||
|
||||
The default global profile is `core`. To enable expanded workflow commands, run `openspec config profile`, select workflows, then run `openspec update` in your project.
|
||||
|
||||
@@ -610,19 +624,9 @@ AI: Welcome to OpenSpec!
|
||||
|
||||
## Command Syntax by AI Tool
|
||||
|
||||
Different AI tools use slightly different command syntax. Use the format that matches your tool:
|
||||
The syntax table now lives near the top of this page because it is usually the first thing to verify.
|
||||
|
||||
| Tool | Syntax Example |
|
||||
|------|----------------|
|
||||
| Claude Code | `/opsx:propose`, `/opsx:apply` |
|
||||
| Cursor | `/opsx-propose`, `/opsx-apply` |
|
||||
| Windsurf | `/opsx-propose`, `/opsx-apply` |
|
||||
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
|
||||
| Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) |
|
||||
|
||||
The intent is the same across tools, but how commands are surfaced can differ by integration.
|
||||
|
||||
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files — see [Supported Tools](supported-tools.md) for details and workarounds.
|
||||
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files. See [Supported Tools](supported-tools.md) for details and workarounds.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ OpenSpec helps you and your AI coding assistant agree on what to build before an
|
||||
|
||||
The default global profile is `core`, which includes `propose`, `explore`, `apply`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
|
||||
|
||||
> [!NOTE]
|
||||
> Examples on this page use Claude-style `/opsx:...` syntax for readability. If your tool expects `/opsx-...` or direct `openspec-*` skill invocations instead, check [Commands](commands.md#first-use-the-syntax-your-tool-expects).
|
||||
|
||||
## What OpenSpec Creates
|
||||
|
||||
After running `openspec init`, your project has this structure:
|
||||
|
||||
@@ -17,6 +17,20 @@ By default, OpenSpec uses the `core` profile, which includes:
|
||||
|
||||
You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `bulk-archive`, `onboard`) via `openspec config profile`, then run `openspec update`.
|
||||
|
||||
## What to Expect After `openspec init`
|
||||
|
||||
- **Only 4 workflows installed?** That is the default `core` profile. You should expect `propose`, `explore`, `apply`, and `archive` unless you explicitly opt into a broader workflow set.
|
||||
- **Missing command files?** Check your delivery mode. `skills` delivery installs only `openspec-*` skills, `commands` delivery installs only `opsx-*` command files, and `both` installs both surfaces when the tool supports both.
|
||||
- **Wrong command syntax?** The same workflow may surface as `/opsx:propose`, `/opsx-propose`, or `/openspec-propose` depending on the tool and delivery mode.
|
||||
|
||||
## Command Syntax by Tool Family
|
||||
|
||||
| Tool family | Syntax example |
|
||||
|-------------|----------------|
|
||||
| Claude Code and other colon-style integrations | `/opsx:propose`, `/opsx:apply` |
|
||||
| OpenCode, Cursor, Windsurf, GitHub Copilot IDE, Bob Shell, Pi, and other hyphen-style integrations | `/opsx-propose`, `/opsx-apply` |
|
||||
| ForgeCode, Trae, or any project using skills-only delivery | `/openspec-propose`, `/openspec-apply-change` |
|
||||
|
||||
## Tool Directory Reference
|
||||
|
||||
| Tool (ID) | Skills path pattern | Command path pattern |
|
||||
@@ -101,6 +115,33 @@ When selected by profile/workflow config, OpenSpec generates these skills:
|
||||
|
||||
See [Commands](commands.md) for command behavior and [CLI](cli.md) for `init`/`update` options.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Why did I only get 4 workflows?
|
||||
|
||||
Because OpenSpec defaults to the `core` profile. Run `openspec config profile`, choose the expanded workflows you want, then run `openspec update` in the project.
|
||||
|
||||
### Why are command files missing?
|
||||
|
||||
Two common reasons:
|
||||
|
||||
1. **Delivery is set to `skills`**. That installs only `openspec-*` skills, so missing `opsx-*` files are expected.
|
||||
2. **Your tool is skill-based**. Tools such as ForgeCode and Trae do not currently get generated `opsx-*` command files, so use the installed skills instead.
|
||||
|
||||
### Which syntax should I use?
|
||||
|
||||
Use the syntax family that matches your tool:
|
||||
|
||||
- `/opsx:...` for colon-style integrations
|
||||
- `/opsx-...` for hyphen-style integrations
|
||||
- `/openspec-...` when you are using skills directly
|
||||
|
||||
If you are unsure, compare your generated file paths against the table above or check [Commands](commands.md) first.
|
||||
|
||||
### Why does OpenCode documentation sometimes mention `.opencode/command/` and sometimes `.opencode/commands/`?
|
||||
|
||||
Current OpenSpec releases generate `.opencode/commands/`. Older OpenCode setups and older docs may still mention `.opencode/command/`. If you see the singular path in older reports, treat it as version drift rather than a separate workflow model.
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI Reference](cli.md) — Terminal commands
|
||||
|
||||
+11
-6
@@ -45,6 +45,7 @@ import { getGlobalConfig, type Delivery, type Profile } from './global-config.js
|
||||
import { getProfileWorkflows, CORE_WORKFLOWS, ALL_WORKFLOWS } from './profiles.js';
|
||||
import { getAvailableTools } from './available-tools.js';
|
||||
import { migrateIfNeeded } from './migration.js';
|
||||
import { buildOnboardingGuidance } from './onboarding-guidance.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
||||
@@ -696,17 +697,21 @@ export class InitCommand {
|
||||
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
||||
}
|
||||
|
||||
// Getting started (task 7.6: show propose if in profile)
|
||||
// Getting started
|
||||
const globalCfg = getGlobalConfig();
|
||||
const activeProfile: Profile = (this.profileOverride as Profile) ?? globalCfg.profile ?? 'core';
|
||||
const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
|
||||
console.log();
|
||||
if (activeWorkflows.includes('propose')) {
|
||||
if (activeWorkflows.length > 0) {
|
||||
console.log(chalk.bold('Getting started:'));
|
||||
console.log(' Start your first change: /opsx:propose "your idea"');
|
||||
} else if (activeWorkflows.includes('new')) {
|
||||
console.log(chalk.bold('Getting started:'));
|
||||
console.log(' Start your first change: /opsx:new "your idea"');
|
||||
const onboardingLines = buildOnboardingGuidance({
|
||||
workflows: activeWorkflows,
|
||||
delivery: globalCfg.delivery ?? 'both',
|
||||
toolIds: successfulTools.map((tool) => tool.value),
|
||||
});
|
||||
for (const line of onboardingLines) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
} else {
|
||||
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig, type Delivery }
|
||||
import { CommandAdapterRegistry } from './command-generation/index.js';
|
||||
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
|
||||
import { ALL_WORKFLOWS } from './profiles.js';
|
||||
import { buildOnboardingGuidance } from './onboarding-guidance.js';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
@@ -127,5 +128,12 @@ export function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): voi
|
||||
saveGlobalConfig(config);
|
||||
|
||||
console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`);
|
||||
console.log("New in this version: /opsx:propose. Try 'openspec config profile core' for the streamlined experience.");
|
||||
const onboardingLines = buildOnboardingGuidance({
|
||||
workflows: installedWorkflows,
|
||||
delivery: config.delivery ?? inferDelivery(artifacts),
|
||||
});
|
||||
for (const line of onboardingLines) {
|
||||
console.log(line);
|
||||
}
|
||||
console.log("New projects default to the streamlined core profile. Run 'openspec config profile core', then 'openspec update' if you want that setup here.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import path from 'path';
|
||||
import { AI_TOOLS } from './config.js';
|
||||
import { CommandAdapterRegistry } from './command-generation/index.js';
|
||||
import type { Delivery } from './global-config.js';
|
||||
|
||||
const WORKFLOW_TO_SKILL_NAME: Record<string, string> = {
|
||||
'explore': 'openspec-explore',
|
||||
'new': 'openspec-new-change',
|
||||
'continue': 'openspec-continue-change',
|
||||
'apply': 'openspec-apply-change',
|
||||
'ff': 'openspec-ff-change',
|
||||
'sync': 'openspec-sync-specs',
|
||||
'archive': 'openspec-archive-change',
|
||||
'bulk-archive': 'openspec-bulk-archive-change',
|
||||
'verify': 'openspec-verify-change',
|
||||
'onboard': 'openspec-onboard',
|
||||
'propose': 'openspec-propose',
|
||||
};
|
||||
|
||||
const EXPANDED_WORKFLOW_IDS = ['new', 'continue', 'ff', 'verify', 'sync'];
|
||||
|
||||
export interface OnboardingGuidanceOptions {
|
||||
workflows: string[];
|
||||
delivery: Delivery;
|
||||
toolIds?: string[];
|
||||
maxExamples?: number;
|
||||
}
|
||||
|
||||
function describeDelivery(delivery: Delivery): string {
|
||||
if (delivery === 'skills') return 'skills only';
|
||||
if (delivery === 'commands') return 'commands only';
|
||||
return 'both (skills + commands)';
|
||||
}
|
||||
|
||||
function getToolLabel(toolId: string): string {
|
||||
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
||||
return tool?.successLabel ?? tool?.name ?? toolId;
|
||||
}
|
||||
|
||||
function getPrimaryWorkflow(workflows: string[]): string | undefined {
|
||||
if (workflows.includes('propose')) return 'propose';
|
||||
if (workflows.includes('new')) return 'new';
|
||||
return workflows[0];
|
||||
}
|
||||
|
||||
function getSkillInvocation(workflowId: string): string | null {
|
||||
const skillName = WORKFLOW_TO_SKILL_NAME[workflowId];
|
||||
if (!skillName) return null;
|
||||
return `/${skillName}`;
|
||||
}
|
||||
|
||||
function getCommandInvocation(toolId: string, workflowId: string): string | null {
|
||||
const adapter = CommandAdapterRegistry.get(toolId);
|
||||
if (!adapter) return null;
|
||||
|
||||
const normalizedPath = adapter.getFilePath(workflowId).replace(/\\/g, '/');
|
||||
const basename = path.posix.basename(normalizedPath);
|
||||
|
||||
if (normalizedPath.includes('/opsx/')) {
|
||||
return `/opsx:${workflowId}`;
|
||||
}
|
||||
|
||||
if (basename.startsWith('opsx-')) {
|
||||
return `/opsx-${workflowId}`;
|
||||
}
|
||||
|
||||
return `/opsx:${workflowId}`;
|
||||
}
|
||||
|
||||
function getInvocationExample(toolId: string, workflowId: string, delivery: Delivery): string | null {
|
||||
if (delivery !== 'skills') {
|
||||
const commandInvocation = getCommandInvocation(toolId, workflowId);
|
||||
if (commandInvocation) {
|
||||
return `${commandInvocation} \"your idea\"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (delivery !== 'commands' || !getCommandInvocation(toolId, workflowId)) {
|
||||
const skillInvocation = getSkillInvocation(workflowId);
|
||||
if (skillInvocation) {
|
||||
return `${skillInvocation} \"your idea\"`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSkillOnlyTools(toolIds: string[]): string[] {
|
||||
return toolIds.filter((toolId) => CommandAdapterRegistry.get(toolId) == null);
|
||||
}
|
||||
|
||||
function buildGroupedExamples(toolIds: string[], workflowId: string, delivery: Delivery): string[] {
|
||||
const grouped = new Map<string, string[]>();
|
||||
|
||||
for (const toolId of toolIds) {
|
||||
const example = getInvocationExample(toolId, workflowId, delivery);
|
||||
if (!example) continue;
|
||||
|
||||
const labels = grouped.get(example) ?? [];
|
||||
labels.push(getToolLabel(toolId));
|
||||
grouped.set(example, labels);
|
||||
}
|
||||
|
||||
return [...grouped.entries()].map(([example, labels]) => `${labels.join(', ')}: ${example}`);
|
||||
}
|
||||
|
||||
export function buildOnboardingGuidance(options: OnboardingGuidanceOptions): string[] {
|
||||
const workflows = [...options.workflows];
|
||||
const delivery = options.delivery;
|
||||
const toolIds = [...new Set(options.toolIds ?? [])];
|
||||
const maxExamples = options.maxExamples ?? 3;
|
||||
const lines: string[] = [];
|
||||
|
||||
if (workflows.length > 0) {
|
||||
lines.push(`Available workflows: ${workflows.join(', ')}`);
|
||||
}
|
||||
lines.push(`Delivery: ${describeDelivery(delivery)}`);
|
||||
|
||||
const primaryWorkflow = getPrimaryWorkflow(workflows);
|
||||
if (primaryWorkflow && toolIds.length > 0) {
|
||||
const examples = buildGroupedExamples(toolIds, primaryWorkflow, delivery).slice(0, maxExamples);
|
||||
if (examples.length > 0) {
|
||||
lines.push('Try:');
|
||||
for (const example of examples) {
|
||||
lines.push(` ${example}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!EXPANDED_WORKFLOW_IDS.every((workflowId) => workflows.includes(workflowId))) {
|
||||
lines.push('Want expanded workflows like /opsx:new and /opsx:continue? Run `openspec config profile`, then `openspec update`.');
|
||||
}
|
||||
|
||||
if (delivery === 'skills') {
|
||||
lines.push('Command files were not generated because delivery is set to skills-only.');
|
||||
}
|
||||
|
||||
const skillOnlyTools = getSkillOnlyTools(toolIds);
|
||||
if (skillOnlyTools.length > 0 && delivery !== 'skills') {
|
||||
lines.push(`These tools use skill-based invocations here: ${skillOnlyTools.map(getToolLabel).join(', ')}.`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
+9
-3
@@ -37,6 +37,7 @@ import { isInteractive } from '../utils/interactive.js';
|
||||
import { getGlobalConfig, type Delivery } from './global-config.js';
|
||||
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
||||
import { getAvailableTools } from './available-tools.js';
|
||||
import { buildOnboardingGuidance } from './onboarding-guidance.js';
|
||||
import {
|
||||
WORKFLOW_TO_SKILL_DIR,
|
||||
getCommandConfiguredTools,
|
||||
@@ -268,9 +269,14 @@ export class UpdateCommand {
|
||||
if (newlyConfiguredTools.length > 0) {
|
||||
console.log();
|
||||
console.log(chalk.bold('Getting started:'));
|
||||
console.log(' /opsx:new Start a new change');
|
||||
console.log(' /opsx:continue Create the next artifact');
|
||||
console.log(' /opsx:apply Implement tasks');
|
||||
const onboardingLines = buildOnboardingGuidance({
|
||||
workflows: desiredWorkflows,
|
||||
delivery,
|
||||
toolIds: newlyConfiguredTools,
|
||||
});
|
||||
for (const line of onboardingLines) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ function getWelcomeText(): string[] {
|
||||
chalk.dim(' • Agent Skills for AI tools'),
|
||||
chalk.dim(' • /opsx:* slash commands'),
|
||||
'',
|
||||
chalk.white('Quick start after setup:'),
|
||||
` ${chalk.yellow('/opsx:new')} ${chalk.dim('Create a change')}`,
|
||||
` ${chalk.yellow('/opsx:continue')} ${chalk.dim('Next artifact')}`,
|
||||
` ${chalk.yellow('/opsx:apply')} ${chalk.dim('Implement tasks')}`,
|
||||
chalk.white('Default workflow after setup:'),
|
||||
` ${chalk.yellow('/opsx:propose')} ${chalk.dim('Start a change on colon-style tools')}`,
|
||||
` ${chalk.yellow('/opsx-propose')} ${chalk.dim('Start a change on hyphen-style tools')}`,
|
||||
` ${chalk.yellow('core')} ${chalk.dim('Installs propose, explore, apply, archive')}`,
|
||||
'',
|
||||
chalk.cyan('Press Enter to select tools...'),
|
||||
];
|
||||
|
||||
@@ -741,6 +741,31 @@ describe('InitCommand - profile and detection features', () => {
|
||||
const skillFile = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md');
|
||||
expect(await fileExists(skillFile)).toBe(true);
|
||||
});
|
||||
|
||||
it('should print tool-aware onboarding guidance after setup', async () => {
|
||||
const initCommand = new InitCommand({ tools: 'opencode', force: true });
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
|
||||
expect(logCalls.some((entry) => entry.includes('Available workflows: propose, explore, apply, archive'))).toBe(true);
|
||||
expect(logCalls.some((entry) => entry.includes('OpenCode: /opsx-propose "your idea"'))).toBe(true);
|
||||
expect(logCalls.some((entry) => entry.includes('Want expanded workflows like /opsx:new and /opsx:continue?'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should explain skills-only delivery in setup output', async () => {
|
||||
saveGlobalConfig({
|
||||
featureFlags: {},
|
||||
profile: 'core',
|
||||
delivery: 'skills',
|
||||
});
|
||||
|
||||
const initCommand = new InitCommand({ tools: 'claude', force: true });
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
|
||||
expect(logCalls.some((entry) => entry.includes('Delivery: skills only'))).toBe(true);
|
||||
expect(logCalls.some((entry) => entry.includes('Command files were not generated because delivery is set to skills-only.'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { randomUUID } from 'crypto';
|
||||
@@ -135,6 +135,22 @@ describe('migration', () => {
|
||||
expect(fs.existsSync(getGlobalConfigPath())).toBe(false);
|
||||
});
|
||||
|
||||
it('prints workflow and delivery guidance during migration', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await writeSkill(projectDir, 'openspec-explore');
|
||||
await writeManagedCommand(projectDir, 'apply');
|
||||
|
||||
migrateIfNeeded(projectDir, [ensureClaudeTool()]);
|
||||
|
||||
const logLines = consoleSpy.mock.calls.flat().map(String);
|
||||
expect(logLines.some((line) => line.includes('Available workflows: explore, apply'))).toBe(true);
|
||||
expect(logLines.some((line) => line.includes('Delivery: both (skills + commands)'))).toBe(true);
|
||||
expect(logLines.some((line) => line.includes("Run 'openspec config profile core', then 'openspec update'"))).toBe(true);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('ignores unknown custom skill and command files when scanning workflows', async () => {
|
||||
await writeSkill(projectDir, 'my-custom-skill');
|
||||
const customCommandPath = path.join(projectDir, '.claude', 'commands', 'opsx', 'my-custom.md');
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildOnboardingGuidance } from '../../src/core/onboarding-guidance.js';
|
||||
|
||||
describe('buildOnboardingGuidance', () => {
|
||||
it('uses colon syntax for Claude Code command examples', () => {
|
||||
const lines = buildOnboardingGuidance({
|
||||
workflows: ['propose', 'explore', 'apply', 'archive'],
|
||||
delivery: 'both',
|
||||
toolIds: ['claude'],
|
||||
});
|
||||
|
||||
expect(lines).toContain('Available workflows: propose, explore, apply, archive');
|
||||
expect(lines).toContain('Delivery: both (skills + commands)');
|
||||
expect(lines).toContain(' Claude Code: /opsx:propose "your idea"');
|
||||
});
|
||||
|
||||
it('uses hyphen syntax for OpenCode command examples', () => {
|
||||
const lines = buildOnboardingGuidance({
|
||||
workflows: ['propose', 'explore', 'apply', 'archive'],
|
||||
delivery: 'both',
|
||||
toolIds: ['opencode'],
|
||||
});
|
||||
|
||||
expect(lines).toContain(' OpenCode: /opsx-propose "your idea"');
|
||||
});
|
||||
|
||||
it('falls back to skill invocations for skills-only delivery', () => {
|
||||
const lines = buildOnboardingGuidance({
|
||||
workflows: ['propose', 'explore', 'apply', 'archive'],
|
||||
delivery: 'skills',
|
||||
toolIds: ['claude'],
|
||||
});
|
||||
|
||||
expect(lines).toContain(' Claude Code: /openspec-propose "your idea"');
|
||||
expect(lines).toContain('Command files were not generated because delivery is set to skills-only.');
|
||||
});
|
||||
|
||||
it('explains skill-based tools when no command adapter exists', () => {
|
||||
const lines = buildOnboardingGuidance({
|
||||
workflows: ['propose', 'explore', 'apply', 'archive'],
|
||||
delivery: 'both',
|
||||
toolIds: ['forgecode'],
|
||||
});
|
||||
|
||||
expect(lines).toContain(' ForgeCode: /openspec-propose "your idea"');
|
||||
expect(lines).toContain('These tools use skill-based invocations here: ForgeCode.');
|
||||
});
|
||||
});
|
||||
@@ -1142,7 +1142,7 @@ More user content after markers.
|
||||
expect.stringContaining('Getting started')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/opsx:new')
|
||||
expect.stringContaining('/opsx:propose')
|
||||
);
|
||||
|
||||
// Skills should be created
|
||||
|
||||
Reference in New Issue
Block a user