From 735fa842fed54193c9373bc16bbf2c99796fa48a Mon Sep 17 00:00:00 2001 From: gcw_xsdtlYdI Date: Fri, 12 Jun 2026 15:48:25 +0800 Subject: [PATCH] feat(profile): add 'all' profile preset for installing all workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a third profile option 'all' alongside 'core' and 'custom': - openspec init --profile all installs all 11 workflows - openspec config profile all preset shortcut - deriveProfileFromWorkflowSelection returns 'all' when all workflows selected - Migration sets 'all' profile when all workflows are already installed - Interactive picker auto-derives 'all' when all 11 workflows are checked Source changes: Profile type, Zod schemas, getProfileWorkflows, resolveProfileOverride, migration logic, config command presets, workspace skill state schemas, CLI descriptions, completions. Test additions: profile resolution, init --profile all, config schema validation, deriveProfileFromWorkflowSelection, config profile preset, migration to 'all', global-config round-trip. Docs updates: cli.md, commands.md, getting-started.md, workflows.md, migration-guide.md, supported-tools.md, CHANGELOG.md, spec.md. 🤖‍ AI[100%] 👌 AI Adopted[100%] 🧑 Human[0%] Co-authored-by: opencode (glm-5.1) --- AGENTS.md | 0 CHANGELOG.md | 2 +- docs/cli.md | 5 +++- docs/commands.md | 4 +-- docs/getting-started.md | 4 +-- docs/migration-guide.md | 4 +-- docs/supported-tools.md | 3 +- docs/workflows.md | 4 +-- openspec/specs/cli-config/spec.md | 2 +- src/cli/index.ts | 2 +- src/commands/config.ts | 20 +++++++++++-- src/core/completions/command-registry.ts | 4 +-- src/core/config-schema.ts | 2 +- src/core/global-config.ts | 2 +- src/core/init.ts | 4 +-- src/core/migration.ts | 26 ++++++++++++----- src/core/profiles.ts | 4 +++ src/core/workspace/foundation.ts | 4 +-- src/core/workspace/legacy-state.ts | 2 +- test/commands/config-profile.test.ts | 36 ++++++++++++++++++++++++ test/commands/config.test.ts | 7 ++++- test/core/global-config.test.ts | 15 ++++++++++ test/core/init.test.ts | 17 +++++++++++ test/core/migration.test.ts | 25 ++++++++++++++++ test/core/profiles.test.ts | 10 +++++++ 25 files changed, 176 insertions(+), 32 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d858446..ebd667c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,7 +82,7 @@ - [#747](https://github.com/Fission-AI/OpenSpec/pull/747) [`1e94443`](https://github.com/Fission-AI/OpenSpec/commit/1e94443a3551b228eecbc89e95d96d3b9600a192) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features - - **Profile system** — Choose between `core` (4 essential workflows) and `custom` (pick any subset) profiles to control which skills get installed. Manage profiles with the new `openspec config profile` command + - **Profile system** — Choose between `core` (5 essential workflows), `all` (all 11 workflows), and `custom` (pick any subset) profiles to control which skills get installed. Manage profiles with the new `openspec config profile` command - **Propose workflow** — New one-step workflow creates a complete change proposal with design, specs, and tasks from a single request — no need to run `new` then `ff` separately - **AI tool auto-detection** — `openspec init` now scans your project for existing tool directories (`.claude/`, `.cursor/`, etc.) and pre-selects detected tools - **Pi (pi.dev) support** — Pi coding agent is now a supported tool with prompt and skill generation diff --git a/docs/cli.md b/docs/cli.md index 103dd7d4f..d0a6723d0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -103,7 +103,7 @@ openspec init [path] [options] |--------|-------------| | `--tools ` | Configure AI tools non-interactively. Use `all`, `none`, or comma-separated list | | `--force` | Auto-cleanup legacy files without prompting | -| `--profile ` | Override global profile for this init run (`core` or `custom`) | +| `--profile ` | Override global profile for this init run (`core`, `custom`, or `all`) | `--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`). @@ -1189,6 +1189,9 @@ openspec config profile # Fast preset: switch workflows to core (keeps delivery mode) openspec config profile core + +# Fast preset: install all workflows (keeps delivery mode) +openspec config profile all ``` `openspec config profile` starts with a current-state summary, then lets you choose: diff --git a/docs/commands.md b/docs/commands.md index 8b0d81839..f505b371d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -16,7 +16,7 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md | `/opsx:sync` | Merge delta specs into main specs | | `/opsx:archive` | Archive a completed change | -### Expanded Workflow Commands (custom workflow selection) +### Expanded Workflow Commands (`custom` or `all` profile) | Command | Purpose | |---------|---------| @@ -27,7 +27,7 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md | `/opsx:bulk-archive` | Archive multiple changes at once | | `/opsx: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. +The default global profile is `core`. To enable all expanded workflow commands, run `openspec config profile all` and then `openspec update`. To pick a custom subset, run `openspec config profile` (interactive), select workflows, then `openspec update`. --- diff --git a/docs/getting-started.md b/docs/getting-started.md index 3d0e9e95b..5fdd36ba1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,13 +12,13 @@ OpenSpec helps you and your AI coding assistant agree on what to build before an /opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive ``` -**Expanded path (custom workflow selection):** +**Expanded path (all or custom profile):** ```text /opsx:new ──► /opsx:ff or /opsx:continue ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive ``` -The default global profile is `core`, which includes `propose`, `explore`, `apply`, `sync`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`. +The default global profile is `core`, which includes `propose`, `explore`, `apply`, `sync`, and `archive`. You can enable all expanded workflow commands with `openspec config profile all`, or pick a custom subset with `openspec config profile`, then `openspec update`. ## What OpenSpec Creates diff --git a/docs/migration-guide.md b/docs/migration-guide.md index d6355740f..6fa9e0276 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -85,7 +85,7 @@ Don't worry about getting it perfect. We're still learning what works best here, Both `openspec init` and `openspec update` detect legacy files and guide you through the same cleanup process. Use whichever fits your situation: - New installs default to profile `core` (`propose`, `explore`, `apply`, `sync`, `archive`). -- Migrated installs preserve your previously installed workflows by writing a `custom` profile when needed. +- Migrated installs preserve your previously installed workflows: if all 11 workflows are installed, you'll get the `all` profile; otherwise a `custom` profile with your detected workflows. ### Using `openspec init` @@ -289,7 +289,7 @@ Command availability is profile-dependent: | `/opsx:apply` | Implement tasks from tasks.md | | `/opsx:archive` | Finalize and archive the change | -**Expanded workflow (custom selection):** +**Expanded workflow (all or custom profile):** | Command | Purpose | |---------|---------| diff --git a/docs/supported-tools.md b/docs/supported-tools.md index b2ee30fb4..fc18894ae 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -82,8 +82,9 @@ openspec init --profile core OpenSpec installs workflow artifacts based on selected workflows: - **Core profile (default):** `propose`, `explore`, `apply`, `sync`, `archive` -- **Custom selection:** any subset of all workflow IDs: +- **All profile:** all 11 workflow IDs: `propose`, `explore`, `new`, `continue`, `apply`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard` +- **Custom selection:** any subset of all workflow IDs In other words, skill/command counts are profile-dependent and delivery-dependent, not fixed. diff --git a/docs/workflows.md b/docs/workflows.md index 7e03b9655..49426999b 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -45,12 +45,12 @@ Typical flow: /opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive ``` -### Expanded/Full Workflow (custom selection) +### Expanded/Full Workflow (all or custom profile) If you want explicit scaffold-and-build commands (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), enable them with: ```bash -openspec config profile +openspec config profile all openspec update ``` diff --git a/openspec/specs/cli-config/spec.md b/openspec/specs/cli-config/spec.md index f3c9a11fa..6afd1b2f0 100644 --- a/openspec/specs/cli-config/spec.md +++ b/openspec/specs/cli-config/spec.md @@ -176,7 +176,7 @@ The `openspec config profile` command SHALL provide an action-first interactive - **WHEN** user runs `openspec config profile` in an interactive terminal - **THEN** display a current-state header with: - current delivery value - - workflow count with profile label (core or custom) + - workflow count with profile label (core, custom, or all) #### Scenario: Action-first menu offers skippable paths diff --git a/src/cli/index.ts b/src/cli/index.ts index 0c42f43cb..f9d4dd1e9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -118,7 +118,7 @@ program .description('Initialize OpenSpec in your project') .option('--tools ', toolsOptionDescription) .option('--force', 'Auto-cleanup legacy files without prompting') - .option('--profile ', 'Override global config profile (core or custom)') + .option('--profile ', 'Override global config profile (core, custom, or all)') .action(async (targetPath = '.', options?: { tools?: string; force?: boolean; profile?: string }) => { try { // Validate that the path is a valid directory diff --git a/src/commands/config.ts b/src/commands/config.ts index 871d3a085..68c8f2bd2 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -124,6 +124,10 @@ export function deriveProfileFromWorkflowSelection(selectedWorkflows: string[]): const isCoreMatch = selectedWorkflows.length === CORE_WORKFLOWS.length && CORE_WORKFLOWS.every((w) => selectedWorkflows.includes(w)); + const isAllMatch = + selectedWorkflows.length === ALL_WORKFLOWS.length && + ALL_WORKFLOWS.every((w) => selectedWorkflows.includes(w)); + if (isAllMatch) return 'all'; return isCoreMatch ? 'core' : 'custom'; } @@ -318,6 +322,8 @@ export function registerConfigCommand(program: Command): void { console.log(` delivery: ${config.delivery} ${deliverySource}`); if (config.profile === 'core') { console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`); + } else if (config.profile === 'all') { + console.log(` workflows: ${ALL_WORKFLOWS.join(', ')} (from all profile)`); } else if (config.workflows && config.workflows.length > 0) { console.log(` workflows: ${config.workflows.join(', ')} (explicit)`); } else { @@ -525,15 +531,25 @@ export function registerConfigCommand(program: Command): void { return; } + if (preset === 'all') { + const config = getGlobalConfig(); + config.profile = 'all'; + config.workflows = [...ALL_WORKFLOWS]; + saveGlobalConfig(config); + const workspaceContext = await resolveWorkspaceConfigProfileContext(); + printConfigProfileApplyGuidance(workspaceContext); + return; + } + if (preset) { - console.error(`Error: Unknown profile preset "${preset}". Available presets: core`); + console.error(`Error: Unknown profile preset "${preset}". Available presets: core, all`); process.exitCode = 1; return; } // Non-interactive check if (!process.stdout.isTTY) { - console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.'); + console.error('Interactive mode required. Use `openspec config profile core`, `openspec config profile all`, or set config via environment/flags.'); process.exitCode = 1; return; } diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index 88ec88e05..a5fd2cc26 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -19,9 +19,9 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ }, { name: 'profile', - description: 'Override global config profile (core or custom)', + description: 'Override global config profile (core, custom, or all)', takesValue: true, - values: ['core', 'custom'], + values: ['core', 'custom', 'all'], }, ], }, diff --git a/src/core/config-schema.ts b/src/core/config-schema.ts index 0614ed33e..08c35f13e 100644 --- a/src/core/config-schema.ts +++ b/src/core/config-schema.ts @@ -11,7 +11,7 @@ export const GlobalConfigSchema = z .optional() .default({}), profile: z - .enum(['core', 'custom']) + .enum(['core', 'custom', 'all']) .optional() .default('core'), delivery: z diff --git a/src/core/global-config.ts b/src/core/global-config.ts index ad321ceb8..89b4c9011 100644 --- a/src/core/global-config.ts +++ b/src/core/global-config.ts @@ -8,7 +8,7 @@ export const GLOBAL_CONFIG_FILE_NAME = 'config.json'; export const GLOBAL_DATA_DIR_NAME = 'openspec'; // TypeScript types -export type Profile = 'core' | 'custom'; +export type Profile = 'core' | 'custom' | 'all'; export type Delivery = 'both' | 'skills' | 'commands'; // TypeScript interfaces diff --git a/src/core/init.ts b/src/core/init.ts index aa38408f2..2bf31d0e9 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -182,11 +182,11 @@ export class InitCommand { return undefined; } - if (this.profileOverride === 'core' || this.profileOverride === 'custom') { + if (this.profileOverride === 'core' || this.profileOverride === 'custom' || this.profileOverride === 'all') { return this.profileOverride; } - throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`); + throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom, all`); } // ═══════════════════════════════════════════════════════════ diff --git a/src/core/migration.ts b/src/core/migration.ts index 48aaa41ee..96afd7a80 100644 --- a/src/core/migration.ts +++ b/src/core/migration.ts @@ -6,10 +6,10 @@ */ import type { AIToolOption } from './config.js'; -import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig, type Delivery } from './global-config.js'; +import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig, type Delivery, type Profile } from './global-config.js'; import { CommandAdapterRegistry } from './command-generation/index.js'; import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js'; -import { ALL_WORKFLOWS } from './profiles.js'; +import { ALL_WORKFLOWS, getProfileWorkflows } from './profiles.js'; import path from 'path'; import * as fs from 'fs'; @@ -84,7 +84,8 @@ function inferDelivery(artifacts: InstalledWorkflowArtifacts): Delivery { * Performs one-time migration if the global config does not yet have a profile field. * Called by both init and update before profile resolution. * - * - If no profile field exists and workflows are installed: sets profile to 'custom' + * - If no profile field exists and all workflows are installed: sets profile to 'all'. + * - If no profile field exists and some workflows are installed: sets profile to 'custom' * with the detected workflows, preserving the user's existing setup. * - If no profile field exists and no workflows are installed: no-op (defaults apply). * - If profile field already exists: no-op. @@ -118,14 +119,25 @@ export function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): voi return; } - // Migrate: set profile to custom with detected workflows - config.profile = 'custom'; - config.workflows = installedWorkflows; + // Migrate: choose profile based on detected workflows + const allWorkflowSet = new Set(ALL_WORKFLOWS); + const isAllWorkflows = installedWorkflows.length === ALL_WORKFLOWS.length && + installedWorkflows.every((w) => allWorkflowSet.has(w)); + + let migratedProfile: Profile; + if (isAllWorkflows) { + migratedProfile = 'all'; + } else { + migratedProfile = 'custom'; + } + + config.profile = migratedProfile; + config.workflows = [...getProfileWorkflows(migratedProfile, installedWorkflows)]; if (rawConfig.delivery === undefined) { config.delivery = inferDelivery(artifacts); } saveGlobalConfig(config); - console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`); + console.log(`Migrated: ${migratedProfile} profile with ${installedWorkflows.length} workflows`); console.log("New in this version: /opsx:propose. Try 'openspec config profile core' for the streamlined experience."); } diff --git a/src/core/profiles.ts b/src/core/profiles.ts index 29d492746..de0cd6663 100644 --- a/src/core/profiles.ts +++ b/src/core/profiles.ts @@ -37,6 +37,7 @@ export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number]; * Resolves which workflows should be active for a given profile configuration. * * - 'core' profile always returns CORE_WORKFLOWS + * - 'all' profile always returns ALL_WORKFLOWS * - 'custom' profile returns the provided customWorkflows, or empty array if not provided */ export function getProfileWorkflows( @@ -46,5 +47,8 @@ export function getProfileWorkflows( if (profile === 'custom') { return customWorkflows ?? []; } + if (profile === 'all') { + return ALL_WORKFLOWS; + } return CORE_WORKFLOWS; } diff --git a/src/core/workspace/foundation.ts b/src/core/workspace/foundation.ts index 80fcb50d6..5b74fe0df 100644 --- a/src/core/workspace/foundation.ts +++ b/src/core/workspace/foundation.ts @@ -62,7 +62,7 @@ export interface WorkspaceViewState { export interface WorkspaceSkillState { selected_agents: string[]; - last_applied_profile?: 'core' | 'custom'; + last_applied_profile?: 'core' | 'custom' | 'all'; last_applied_delivery?: 'both' | 'skills' | 'commands'; last_applied_workflow_ids?: string[]; last_applied_at?: string; @@ -192,7 +192,7 @@ const WorkspaceContextSchema = WorkspaceInitiativeContextSchema; const WorkspaceSkillStateSchema = z .object({ selected_agents: z.array(z.string()), - last_applied_profile: z.enum(['core', 'custom']).optional(), + last_applied_profile: z.enum(['core', 'custom', 'all']).optional(), last_applied_delivery: z.enum(['both', 'skills', 'commands']).optional(), last_applied_workflow_ids: z.array(z.string()).optional(), last_applied_at: z.string().optional(), diff --git a/src/core/workspace/legacy-state.ts b/src/core/workspace/legacy-state.ts index 14ca74eeb..ac485943b 100644 --- a/src/core/workspace/legacy-state.ts +++ b/src/core/workspace/legacy-state.ts @@ -73,7 +73,7 @@ const PreferredOpenerSchema = z const WorkspaceSkillStateSchema = z .object({ selected_agents: z.array(z.string()), - last_applied_profile: z.enum(['core', 'custom']).optional(), + last_applied_profile: z.enum(['core', 'custom', 'all']).optional(), last_applied_delivery: z.enum(['both', 'skills', 'commands']).optional(), last_applied_workflow_ids: z.array(z.string()).optional(), last_applied_at: z.string().optional(), diff --git a/test/commands/config-profile.test.ts b/test/commands/config-profile.test.ts index 3cd40b3ac..184410b5f 100644 --- a/test/commands/config-profile.test.ts +++ b/test/commands/config-profile.test.ts @@ -80,6 +80,17 @@ describe('deriveProfileFromWorkflowSelection', () => { const { deriveProfileFromWorkflowSelection } = await import('../../src/commands/config.js'); expect(deriveProfileFromWorkflowSelection(['archive', 'sync', 'apply', 'explore', 'propose'])).toBe('core'); }); + + it('returns all when selection has exactly all workflows', async () => { + const { deriveProfileFromWorkflowSelection } = await import('../../src/commands/config.js'); + const { ALL_WORKFLOWS } = await import('../../src/core/profiles.js'); + expect(deriveProfileFromWorkflowSelection([...ALL_WORKFLOWS])).toBe('all'); + }); + + it('returns all when selection has all workflows in different order', async () => { + const { deriveProfileFromWorkflowSelection } = await import('../../src/commands/config.js'); + expect(deriveProfileFromWorkflowSelection(['onboard', 'verify', 'bulk-archive', 'archive', 'sync', 'ff', 'apply', 'continue', 'new', 'explore', 'propose'])).toBe('all'); + }); }); describe('config profile interactive flow', () => { @@ -497,6 +508,31 @@ describe('config profile interactive flow', () => { expect(confirm).not.toHaveBeenCalled(); }); + it('all preset should install all workflows and preserve delivery setting', async () => { + const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js'); + const { ALL_WORKFLOWS } = await import('../../src/core/profiles.js'); + const { select, checkbox, confirm } = await getPromptMocks(); + + saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'commands', workflows: ['propose', 'explore', 'apply', 'sync', 'archive'] }); + + await runConfigCommand(['profile', 'all']); + + const config = getGlobalConfig(); + expect(config.profile).toBe('all'); + expect(config.delivery).toBe('commands'); + expect(config.workflows).toEqual([...ALL_WORKFLOWS]); + expect(select).not.toHaveBeenCalled(); + expect(checkbox).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); + }); + + it('unknown preset should show error and list available presets', async () => { + await runConfigCommand(['profile', 'unknown']); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown profile preset "unknown"')); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Available presets: core, all')); + }); + it('core preset inside a workspace should stay non-interactive and print workspace update guidance', async () => { const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js'); const { select, checkbox, confirm } = await getPromptMocks(); diff --git a/test/commands/config.test.ts b/test/commands/config.test.ts index 6e65068b8..f2c4d7ccf 100644 --- a/test/commands/config.test.ts +++ b/test/commands/config.test.ts @@ -232,6 +232,10 @@ describe('config profile command', () => { // Simulate custom selection that differs from core const selectedWorkflows = ['explore', 'new', 'apply', 'ff', 'verify']; + const { ALL_WORKFLOWS } = await import('../../src/core/profiles.js'); + const isAllMatch = + selectedWorkflows.length === ALL_WORKFLOWS.length && + ALL_WORKFLOWS.every((w: string) => selectedWorkflows.includes(w)); const isCoreMatch = selectedWorkflows.length === CORE_WORKFLOWS.length && CORE_WORKFLOWS.every((w: string) => selectedWorkflows.includes(w)); @@ -240,7 +244,7 @@ describe('config profile command', () => { saveGlobalConfig({ featureFlags: {}, - profile: isCoreMatch ? 'core' : 'custom', + profile: isAllMatch ? 'all' : isCoreMatch ? 'core' : 'custom', delivery: 'both', workflows: selectedWorkflows, }); @@ -267,6 +271,7 @@ describe('config profile command', () => { expect(validateConfig({ featureFlags: {}, profile: 'core', delivery: 'both' }).success).toBe(true); expect(validateConfig({ featureFlags: {}, profile: 'custom', delivery: 'skills' }).success).toBe(true); expect(validateConfig({ featureFlags: {}, profile: 'custom', delivery: 'commands', workflows: ['explore'] }).success).toBe(true); + expect(validateConfig({ featureFlags: {}, profile: 'all', delivery: 'both' }).success).toBe(true); }); it('config schema should reject invalid profile values', async () => { diff --git a/test/core/global-config.test.ts b/test/core/global-config.test.ts index 03310060e..dd4e2350a 100644 --- a/test/core/global-config.test.ts +++ b/test/core/global-config.test.ts @@ -287,6 +287,21 @@ describe('global-config', () => { expect(loadedConfig.workflows).toEqual(['propose']); }); + it('should round-trip all profile correctly', () => { + process.env.XDG_CONFIG_HOME = tempDir; + const originalConfig = { + featureFlags: {}, + profile: 'all' as Profile, + delivery: 'both' as Delivery, + }; + + saveGlobalConfig(originalConfig); + const loadedConfig = getGlobalConfig(); + + expect(loadedConfig.profile).toBe('all'); + expect(loadedConfig.delivery).toBe('both'); + }); + it('should default workflows to undefined when not in config', () => { process.env.XDG_CONFIG_HOME = tempDir; const configDir = path.join(tempDir, 'openspec'); diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6a436eaed..1b3bc2ffd 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -536,6 +536,23 @@ describe('InitCommand - profile and detection features', () => { expect(await fileExists(newChangeSkill)).toBe(false); }); + it('should use --profile all to install all workflows', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + const initCommand = new InitCommand({ tools: 'claude', force: true, profile: 'all' }); + await initCommand.execute(testDir); + + const proposeSkill = path.join(testDir, '.claude', 'skills', 'openspec-propose', 'SKILL.md'); + expect(await fileExists(proposeSkill)).toBe(true); + + const onboardSkill = path.join(testDir, '.claude', 'skills', 'openspec-onboard', 'SKILL.md'); + expect(await fileExists(onboardSkill)).toBe(true); + }); + it('should reject invalid --profile values', async () => { const initCommand = new InitCommand({ tools: 'claude', diff --git a/test/core/migration.test.ts b/test/core/migration.test.ts index 409206e94..d7999aa3d 100644 --- a/test/core/migration.test.ts +++ b/test/core/migration.test.ts @@ -135,6 +135,31 @@ describe('migration', () => { expect(fs.existsSync(getGlobalConfigPath())).toBe(false); }); + it('migrates to all profile when all workflows are installed', async () => { + const allDirNames = [ + 'openspec-propose', + 'openspec-explore', + 'openspec-new-change', + 'openspec-continue-change', + 'openspec-apply-change', + 'openspec-ff-change', + 'openspec-sync-specs', + 'openspec-archive-change', + 'openspec-bulk-archive-change', + 'openspec-verify-change', + 'openspec-onboard', + ]; + for (const dirName of allDirNames) { + await writeSkill(projectDir, dirName); + } + + migrateIfNeeded(projectDir, [ensureClaudeTool()]); + + const config = readRawConfig(); + expect(config.profile).toBe('all'); + expect(config.delivery).toBe('skills'); + }); + 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'); diff --git a/test/core/profiles.test.ts b/test/core/profiles.test.ts index b46901fa2..b188081c7 100644 --- a/test/core/profiles.test.ts +++ b/test/core/profiles.test.ts @@ -44,6 +44,16 @@ describe('profiles', () => { expect(result).toEqual(CORE_WORKFLOWS); }); + it('should return all workflows for all profile', () => { + const result = getProfileWorkflows('all'); + expect(result).toEqual(ALL_WORKFLOWS); + }); + + it('should return all workflows for all profile even if customWorkflows provided', () => { + const result = getProfileWorkflows('all', ['explore']); + expect(result).toEqual(ALL_WORKFLOWS); + }); + it('should return custom workflows for custom profile', () => { const customWorkflows = ['explore', 'new', 'apply', 'ff']; const result = getProfileWorkflows('custom', customWorkflows);