@studyzy/openspec-cn
Advanced tools
| /** | ||
| * Available Tools Detection | ||
| * | ||
| * Detects which AI tools are available in a project by scanning | ||
| * for their configuration directories. | ||
| */ | ||
| import { type AIToolOption } from './config.js'; | ||
| /** | ||
| * Scans the project path for AI tool configuration directories and returns | ||
| * the tools that are present. | ||
| * | ||
| * Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the | ||
| * project root. Only tools with a `skillsDir` property are considered. | ||
| */ | ||
| export declare function getAvailableTools(projectPath: string): AIToolOption[]; | ||
| //# sourceMappingURL=available-tools.d.ts.map |
| /** | ||
| * Available Tools Detection | ||
| * | ||
| * Detects which AI tools are available in a project by scanning | ||
| * for their configuration directories. | ||
| */ | ||
| import path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { AI_TOOLS } from './config.js'; | ||
| /** | ||
| * Scans the project path for AI tool configuration directories and returns | ||
| * the tools that are present. | ||
| * | ||
| * Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the | ||
| * project root. Only tools with a `skillsDir` property are considered. | ||
| */ | ||
| export function getAvailableTools(projectPath) { | ||
| return AI_TOOLS.filter((tool) => { | ||
| if (!tool.skillsDir) | ||
| return false; | ||
| const dirPath = path.join(projectPath, tool.skillsDir); | ||
| try { | ||
| return fs.statSync(dirPath).isDirectory(); | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| }); | ||
| } | ||
| //# sourceMappingURL=available-tools.js.map |
| /** | ||
| * Kiro Command Adapter | ||
| * | ||
| * Formats commands for Kiro following its .prompt.md specification. | ||
| */ | ||
| import type { ToolCommandAdapter } from '../types.js'; | ||
| /** | ||
| * Kiro adapter for command generation. | ||
| * File path: .kiro/prompts/opsx-<id>.prompt.md | ||
| * Frontmatter: description | ||
| */ | ||
| export declare const kiroAdapter: ToolCommandAdapter; | ||
| //# sourceMappingURL=kiro.d.ts.map |
| /** | ||
| * Kiro Command Adapter | ||
| * | ||
| * Formats commands for Kiro following its .prompt.md specification. | ||
| */ | ||
| import path from 'path'; | ||
| /** | ||
| * Kiro adapter for command generation. | ||
| * File path: .kiro/prompts/opsx-<id>.prompt.md | ||
| * Frontmatter: description | ||
| */ | ||
| export const kiroAdapter = { | ||
| toolId: 'kiro', | ||
| getFilePath(commandId) { | ||
| return path.join('.kiro', 'prompts', `opsx-${commandId}.prompt.md`); | ||
| }, | ||
| formatFile(content) { | ||
| return `--- | ||
| description: ${content.description} | ||
| --- | ||
| ${content.body} | ||
| `; | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=kiro.js.map |
| /** | ||
| * Pi Command Adapter | ||
| * | ||
| * Formats commands for Pi (pi.dev) following its prompt template specification. | ||
| * Pi prompt templates live in .pi/prompts/*.md with description frontmatter. | ||
| */ | ||
| import type { ToolCommandAdapter } from '../types.js'; | ||
| /** | ||
| * Pi adapter for prompt template generation. | ||
| * File path: .pi/prompts/opsx-<id>.md | ||
| * Frontmatter: description | ||
| */ | ||
| export declare const piAdapter: ToolCommandAdapter; | ||
| //# sourceMappingURL=pi.d.ts.map |
| /** | ||
| * Pi Command Adapter | ||
| * | ||
| * Formats commands for Pi (pi.dev) following its prompt template specification. | ||
| * Pi prompt templates live in .pi/prompts/*.md with description frontmatter. | ||
| */ | ||
| import path from 'path'; | ||
| /** | ||
| * Escapes a string value for safe YAML output. | ||
| * Quotes the string if it contains special YAML characters. | ||
| */ | ||
| function escapeYamlValue(value) { | ||
| // Check if value needs quoting (contains special YAML characters or starts/ends with whitespace) | ||
| const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); | ||
| if (needsQuoting) { | ||
| // Use double quotes and escape internal double quotes and backslashes | ||
| const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); | ||
| return `"${escaped}"`; | ||
| } | ||
| return value; | ||
| } | ||
| /** | ||
| * Pi adapter for prompt template generation. | ||
| * File path: .pi/prompts/opsx-<id>.md | ||
| * Frontmatter: description | ||
| */ | ||
| export const piAdapter = { | ||
| toolId: 'pi', | ||
| getFilePath(commandId) { | ||
| return path.join('.pi', 'prompts', `opsx-${commandId}.md`); | ||
| }, | ||
| formatFile(content) { | ||
| return `--- | ||
| description: ${escapeYamlValue(content.description)} | ||
| --- | ||
| ${content.body} | ||
| `; | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=pi.js.map |
| /** | ||
| * Migration Utilities | ||
| * | ||
| * One-time migration logic for existing projects when profile system is introduced. | ||
| * Called by both init and update commands before profile resolution. | ||
| */ | ||
| import type { AIToolOption } from './config.js'; | ||
| /** | ||
| * Scans installed workflow files across all detected tools and returns | ||
| * the union of installed workflow IDs. | ||
| */ | ||
| export declare function scanInstalledWorkflows(projectPath: string, tools: AIToolOption[]): string[]; | ||
| /** | ||
| * 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' | ||
| * 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. | ||
| */ | ||
| export declare function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): void; | ||
| //# sourceMappingURL=migration.d.ts.map |
| /** | ||
| * Migration Utilities | ||
| * | ||
| * One-time migration logic for existing projects when profile system is introduced. | ||
| * Called by both init and update commands before profile resolution. | ||
| */ | ||
| import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig } 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 path from 'path'; | ||
| import * as fs from 'fs'; | ||
| function scanInstalledWorkflowArtifacts(projectPath, tools) { | ||
| const installed = new Set(); | ||
| let hasSkills = false; | ||
| let hasCommands = false; | ||
| for (const tool of tools) { | ||
| if (!tool.skillsDir) | ||
| continue; | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| for (const workflowId of ALL_WORKFLOWS) { | ||
| const skillDirName = WORKFLOW_TO_SKILL_DIR[workflowId]; | ||
| const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md'); | ||
| if (fs.existsSync(skillFile)) { | ||
| installed.add(workflowId); | ||
| hasSkills = true; | ||
| } | ||
| } | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (!adapter) | ||
| continue; | ||
| for (const workflowId of ALL_WORKFLOWS) { | ||
| const commandPath = adapter.getFilePath(workflowId); | ||
| const fullPath = path.isAbsolute(commandPath) | ||
| ? commandPath | ||
| : path.join(projectPath, commandPath); | ||
| if (fs.existsSync(fullPath)) { | ||
| installed.add(workflowId); | ||
| hasCommands = true; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| workflows: ALL_WORKFLOWS.filter((workflowId) => installed.has(workflowId)), | ||
| hasSkills, | ||
| hasCommands, | ||
| }; | ||
| } | ||
| /** | ||
| * Scans installed workflow files across all detected tools and returns | ||
| * the union of installed workflow IDs. | ||
| */ | ||
| export function scanInstalledWorkflows(projectPath, tools) { | ||
| return scanInstalledWorkflowArtifacts(projectPath, tools).workflows; | ||
| } | ||
| function inferDelivery(artifacts) { | ||
| if (artifacts.hasSkills && artifacts.hasCommands) { | ||
| return 'both'; | ||
| } | ||
| if (artifacts.hasCommands) { | ||
| return 'commands'; | ||
| } | ||
| return 'skills'; | ||
| } | ||
| /** | ||
| * 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' | ||
| * 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. | ||
| */ | ||
| export function migrateIfNeeded(projectPath, tools) { | ||
| const config = getGlobalConfig(); | ||
| // Check raw config file for profile field presence | ||
| const configPath = getGlobalConfigPath(); | ||
| let rawConfig = {}; | ||
| try { | ||
| if (fs.existsSync(configPath)) { | ||
| rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); | ||
| } | ||
| } | ||
| catch { | ||
| return; // Can't read config, skip migration | ||
| } | ||
| // If profile is already explicitly set, no migration needed | ||
| if (rawConfig.profile !== undefined) { | ||
| return; | ||
| } | ||
| // Scan for installed workflows | ||
| const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools); | ||
| const installedWorkflows = artifacts.workflows; | ||
| if (installedWorkflows.length === 0) { | ||
| // No workflows installed, new user — defaults will apply | ||
| return; | ||
| } | ||
| // Migrate: set profile to custom with detected workflows | ||
| config.profile = 'custom'; | ||
| config.workflows = installedWorkflows; | ||
| if (rawConfig.delivery === undefined) { | ||
| config.delivery = inferDelivery(artifacts); | ||
| } | ||
| saveGlobalConfig(config); | ||
| console.log(`已迁移:自定义配置,共 ${installedWorkflows.length} 个工作流程`); | ||
| console.log("本版本新增:/opsx:propose。尝试 'openspec-cn config profile core' 获得精简体验。"); | ||
| } | ||
| //# sourceMappingURL=migration.js.map |
| import type { Delivery } from './global-config.js'; | ||
| import { ALL_WORKFLOWS } from './profiles.js'; | ||
| type WorkflowId = (typeof ALL_WORKFLOWS)[number]; | ||
| /** | ||
| * Maps workflow IDs to their skill directory names. | ||
| */ | ||
| export declare const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string>; | ||
| /** | ||
| * Checks whether a tool has at least one generated OpenSpec command file. | ||
| */ | ||
| export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean; | ||
| /** | ||
| * Returns tools with at least one generated command file on disk. | ||
| */ | ||
| export declare function getCommandConfiguredTools(projectPath: string): string[]; | ||
| /** | ||
| * Returns tools that are configured via either skills or commands. | ||
| */ | ||
| export declare function getConfiguredToolsForProfileSync(projectPath: string): string[]; | ||
| /** | ||
| * Detects if a single tool has profile/delivery drift against the desired state. | ||
| * | ||
| * This function covers: | ||
| * - required artifacts missing for selected workflows | ||
| * - artifacts that should not exist for the selected delivery mode | ||
| * - artifacts for workflows that were deselected from the current profile | ||
| */ | ||
| export declare function hasToolProfileOrDeliveryDrift(projectPath: string, toolId: string, desiredWorkflows: readonly string[], delivery: Delivery): boolean; | ||
| /** | ||
| * Returns configured tools that currently need a profile/delivery sync. | ||
| */ | ||
| export declare function getToolsNeedingProfileSync(projectPath: string, desiredWorkflows: readonly string[], delivery: Delivery, configuredTools?: readonly string[]): string[]; | ||
| /** | ||
| * Detects whether the current project has any profile/delivery drift. | ||
| */ | ||
| export declare function hasProjectConfigDrift(projectPath: string, desiredWorkflows: readonly string[], delivery: Delivery): boolean; | ||
| export {}; | ||
| //# sourceMappingURL=profile-sync-drift.d.ts.map |
| import path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { AI_TOOLS } from './config.js'; | ||
| import { ALL_WORKFLOWS } from './profiles.js'; | ||
| import { CommandAdapterRegistry } from './command-generation/index.js'; | ||
| import { COMMAND_IDS, getConfiguredTools } from './shared/index.js'; | ||
| /** | ||
| * Maps workflow IDs to their skill directory names. | ||
| */ | ||
| export const WORKFLOW_TO_SKILL_DIR = { | ||
| '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', | ||
| }; | ||
| function toKnownWorkflows(workflows) { | ||
| return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow)); | ||
| } | ||
| /** | ||
| * Checks whether a tool has at least one generated OpenSpec command file. | ||
| */ | ||
| export function toolHasAnyConfiguredCommand(projectPath, toolId) { | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| if (!adapter) | ||
| return false; | ||
| for (const commandId of COMMAND_IDS) { | ||
| const cmdPath = adapter.getFilePath(commandId); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| if (fs.existsSync(fullPath)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Returns tools with at least one generated command file on disk. | ||
| */ | ||
| export function getCommandConfiguredTools(projectPath) { | ||
| return AI_TOOLS | ||
| .filter((tool) => { | ||
| if (!tool.skillsDir) | ||
| return false; | ||
| const toolDir = path.join(projectPath, tool.skillsDir); | ||
| try { | ||
| return fs.statSync(toolDir).isDirectory(); | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| }) | ||
| .map((tool) => tool.value) | ||
| .filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId)); | ||
| } | ||
| /** | ||
| * Returns tools that are configured via either skills or commands. | ||
| */ | ||
| export function getConfiguredToolsForProfileSync(projectPath) { | ||
| const skillConfigured = getConfiguredTools(projectPath); | ||
| const commandConfigured = getCommandConfiguredTools(projectPath); | ||
| return [...new Set([...skillConfigured, ...commandConfigured])]; | ||
| } | ||
| /** | ||
| * Detects if a single tool has profile/delivery drift against the desired state. | ||
| * | ||
| * This function covers: | ||
| * - required artifacts missing for selected workflows | ||
| * - artifacts that should not exist for the selected delivery mode | ||
| * - artifacts for workflows that were deselected from the current profile | ||
| */ | ||
| export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery) { | ||
| const tool = AI_TOOLS.find((t) => t.value === toolId); | ||
| if (!tool?.skillsDir) | ||
| return false; | ||
| const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows); | ||
| const desiredWorkflowSet = new Set(knownDesiredWorkflows); | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| const shouldGenerateSkills = delivery !== 'commands'; | ||
| const shouldGenerateCommands = delivery !== 'skills'; | ||
| if (shouldGenerateSkills) { | ||
| for (const workflow of knownDesiredWorkflows) { | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); | ||
| if (!fs.existsSync(skillFile)) { | ||
| return true; | ||
| } | ||
| } | ||
| // Deselecting workflows in a profile should trigger sync. | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| if (desiredWorkflowSet.has(workflow)) | ||
| continue; | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| if (fs.existsSync(skillDir)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| if (fs.existsSync(skillDir)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| if (shouldGenerateCommands && adapter) { | ||
| for (const workflow of knownDesiredWorkflows) { | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| if (!fs.existsSync(fullPath)) { | ||
| return true; | ||
| } | ||
| } | ||
| // Deselecting workflows in a profile should trigger sync. | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| if (desiredWorkflowSet.has(workflow)) | ||
| continue; | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| if (fs.existsSync(fullPath)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| else if (!shouldGenerateCommands && adapter) { | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| if (fs.existsSync(fullPath)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Returns configured tools that currently need a profile/delivery sync. | ||
| */ | ||
| export function getToolsNeedingProfileSync(projectPath, desiredWorkflows, delivery, configuredTools) { | ||
| const tools = configuredTools ? [...new Set(configuredTools)] : getConfiguredToolsForProfileSync(projectPath); | ||
| return tools.filter((toolId) => hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery)); | ||
| } | ||
| function getInstalledWorkflowsForTool(projectPath, toolId, options) { | ||
| const tool = AI_TOOLS.find((t) => t.value === toolId); | ||
| if (!tool?.skillsDir) | ||
| return []; | ||
| const installed = new Set(); | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| if (options.includeSkills) { | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); | ||
| if (fs.existsSync(skillFile)) { | ||
| installed.add(workflow); | ||
| } | ||
| } | ||
| } | ||
| if (options.includeCommands) { | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| if (adapter) { | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| if (fs.existsSync(fullPath)) { | ||
| installed.add(workflow); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return [...installed]; | ||
| } | ||
| /** | ||
| * Detects whether the current project has any profile/delivery drift. | ||
| */ | ||
| export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) { | ||
| const configuredTools = getConfiguredToolsForProfileSync(projectPath); | ||
| if (getToolsNeedingProfileSync(projectPath, desiredWorkflows, delivery, configuredTools).length > 0) { | ||
| return true; | ||
| } | ||
| const desiredSet = new Set(toKnownWorkflows(desiredWorkflows)); | ||
| const includeSkills = delivery !== 'commands'; | ||
| const includeCommands = delivery !== 'skills'; | ||
| for (const toolId of configuredTools) { | ||
| const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands }); | ||
| if (installed.some((workflow) => !desiredSet.has(workflow))) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| //# sourceMappingURL=profile-sync-drift.js.map |
| /** | ||
| * Profile System | ||
| * | ||
| * Defines workflow profiles that control which workflows are installed. | ||
| * Profiles determine WHICH workflows; delivery (in global config) determines HOW. | ||
| */ | ||
| import type { Profile } from './global-config.js'; | ||
| /** | ||
| * Core workflows included in the 'core' profile. | ||
| * These provide the streamlined experience for new users. | ||
| */ | ||
| export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "archive"]; | ||
| /** | ||
| * All available workflows in the system. | ||
| */ | ||
| export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"]; | ||
| export type WorkflowId = (typeof ALL_WORKFLOWS)[number]; | ||
| export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number]; | ||
| /** | ||
| * Resolves which workflows should be active for a given profile configuration. | ||
| * | ||
| * - 'core' profile always returns CORE_WORKFLOWS | ||
| * - 'custom' profile returns the provided customWorkflows, or empty array if not provided | ||
| */ | ||
| export declare function getProfileWorkflows(profile: Profile, customWorkflows?: string[]): readonly string[]; | ||
| //# sourceMappingURL=profiles.d.ts.map |
| /** | ||
| * Profile System | ||
| * | ||
| * Defines workflow profiles that control which workflows are installed. | ||
| * Profiles determine WHICH workflows; delivery (in global config) determines HOW. | ||
| */ | ||
| /** | ||
| * Core workflows included in the 'core' profile. | ||
| * These provide the streamlined experience for new users. | ||
| */ | ||
| export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive']; | ||
| /** | ||
| * All available workflows in the system. | ||
| */ | ||
| export const ALL_WORKFLOWS = [ | ||
| 'propose', | ||
| 'explore', | ||
| 'new', | ||
| 'continue', | ||
| 'apply', | ||
| 'ff', | ||
| 'sync', | ||
| 'archive', | ||
| 'bulk-archive', | ||
| 'verify', | ||
| 'onboard', | ||
| ]; | ||
| /** | ||
| * Resolves which workflows should be active for a given profile configuration. | ||
| * | ||
| * - 'core' profile always returns CORE_WORKFLOWS | ||
| * - 'custom' profile returns the provided customWorkflows, or empty array if not provided | ||
| */ | ||
| export function getProfileWorkflows(profile, customWorkflows) { | ||
| if (profile === 'custom') { | ||
| return customWorkflows ?? []; | ||
| } | ||
| return CORE_WORKFLOWS; | ||
| } | ||
| //# sourceMappingURL=profiles.js.map |
| /** | ||
| * Core template types for skills and slash commands. | ||
| */ | ||
| export interface SkillTemplate { | ||
| name: string; | ||
| description: string; | ||
| instructions: string; | ||
| license?: string; | ||
| compatibility?: string; | ||
| metadata?: Record<string, string>; | ||
| } | ||
| export interface CommandTemplate { | ||
| name: string; | ||
| description: string; | ||
| category: string; | ||
| tags: string[]; | ||
| content: string; | ||
| } | ||
| //# sourceMappingURL=types.d.ts.map |
| /** | ||
| * Core template types for skills and slash commands. | ||
| */ | ||
| export {}; | ||
| //# sourceMappingURL=types.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getApplyChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxApplyCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=apply-change.d.ts.map |
| export function getApplyChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-apply-change', | ||
| description: '实现 OpenSpec 变更中的任务。当用户想要开始实现、继续实现或处理任务时使用。', | ||
| instructions: `实现 OpenSpec 变更中的任务。 | ||
| **输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。 | ||
| **步骤** | ||
| 1. **选择变更** | ||
| 如果提供了名称,使用它。否则: | ||
| - 如果用户提到了某个变更,从对话上下文中推断 | ||
| - 如果只存在一个活动变更,自动选择 | ||
| - 如果不明确,运行 \`openspec-cn list --json\` 获取可用变更,并使用 **AskUserQuestion tool** 让用户选择 | ||
| 始终宣布:"正在使用变更:<name>"以及如何覆盖(例如,\`/opsx:apply <other>\`)。 | ||
| 2. **检查状态以了解 Schema** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流(例如:"spec-driven") | ||
| - 哪个产出物包含任务(对于 spec-driven 通常是 "tasks",检查其他产出物的状态) | ||
| 3. **获取应用指令** | ||
| \`\`\`bash | ||
| openspec-cn instructions apply --change "<name>" --json | ||
| \`\`\` | ||
| 这返回: | ||
| - 上下文文件路径(因 Schema 而异 - 可能是 proposal/specs/design/tasks 或 spec/tests/implementation/docs) | ||
| - 进度(总计,完成,剩余) | ||
| - 带有状态的任务列表 | ||
| - 基于当前状态的动态指令 | ||
| **处理状态:** | ||
| - 如果 \`state: "blocked"\`(缺少产出物):显示消息,建议使用 openspec-continue-change | ||
| - 如果 \`state: "all_done"\`:祝贺,建议归档 | ||
| - 否则:继续实现 | ||
| 4. **阅读上下文文件** | ||
| 阅读 apply instructions 输出中 \`contextFiles\` 列出的文件。 | ||
| 文件取决于正在使用的 Schema: | ||
| - **spec-driven**: proposal, specs, design, tasks | ||
| - 其他模式:遵循 CLI 输出中的 contextFiles | ||
| 5. **显示当前进度** | ||
| 显示: | ||
| - 正在使用的 Schema | ||
| - 进度:"N/M 任务已完成" | ||
| - 剩余任务概览 | ||
| - 来自 CLI 的动态指令 | ||
| 6. **实现任务(循环直到完成或受阻)** | ||
| 对于每个待处理任务: | ||
| - 显示正在处理哪个任务 | ||
| - 进行所需的代码更改 | ||
| - 保持更改最小化且专注 | ||
| - 在任务文件中标记任务完成:\`- [ ]\` → \`- [x]\` | ||
| - 继续下一个任务 | ||
| **暂停如果:** | ||
| - 任务不清楚 → 询问澄清 | ||
| - 实现揭示了设计问题 → 建议更新产出物 | ||
| - 遇到错误或阻碍 → 报告并等待指导 | ||
| - 用户中断 | ||
| 7. **完成或暂停时,显示状态** | ||
| 显示: | ||
| - 本次会话完成的任务 | ||
| - 总体进度:"N/M 任务已完成" | ||
| - 如果全部完成:建议归档 | ||
| - 如果暂停:解释原因并等待指导 | ||
| **实现期间的输出** | ||
| \`\`\` | ||
| ## 正在实现:<change-name> (schema: <schema-name>) | ||
| 正在处理任务 3/7:<task description> | ||
| [...正在进行实现...] | ||
| ✓ 任务完成 | ||
| 正在处理任务 4/7:<task description> | ||
| [...正在进行实现...] | ||
| ✓ 任务完成 | ||
| \`\`\` | ||
| **完成时的输出** | ||
| \`\`\` | ||
| ## 实现完成 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **进度:** 7/7 任务已完成 ✓ | ||
| ### 本次会话已完成 | ||
| - [x] 任务 1 | ||
| - [x] 任务 2 | ||
| ... | ||
| 所有任务已完成!准备归档此变更。 | ||
| \`\`\` | ||
| **暂停时的输出(遇到问题)** | ||
| \`\`\` | ||
| ## 实现暂停 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **进度:** 4/7 任务已完成 | ||
| ### 遇到的问题 | ||
| <问题描述> | ||
| **选项:** | ||
| 1. <选项 1> | ||
| 2. <选项 2> | ||
| 3. 其他方法 | ||
| 您想怎么做? | ||
| \`\`\` | ||
| **护栏** | ||
| - 继续执行任务直到完成或受阻 | ||
| - 开始前始终阅读上下文文件(来自 apply instructions 输出) | ||
| - 如果任务模棱两可,暂停并在实现前询问 | ||
| - 如果实现揭示了问题,暂停并建议更新产出物 | ||
| - 保持代码更改最小化并限定在每个任务范围内 | ||
| - 完成每个任务后立即更新任务复选框 | ||
| - 遇到错误、阻碍或不清楚的需求时暂停 - 不要猜测 | ||
| - 使用 CLI 输出中的 contextFiles,不要假设特定的文件名 | ||
| **流畅的工作流集成** | ||
| 此技能支持"变更上的操作"模型: | ||
| - **可以随时调用**:在所有产出物完成之前(如果存在任务),部分实现之后,与其他操作交错 | ||
| - **允许产出物更新**:如果实现揭示了设计问题,建议更新产出物 - 不是阶段锁定的,流畅地工作`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxApplyCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 应用', | ||
| description: '实现 OpenSpec 变更中的任务(实验性)', | ||
| category: '工作流', | ||
| tags: ['workflow', 'artifacts', 'experimental'], | ||
| content: `实现 OpenSpec 变更中的任务。 | ||
| **输入**:可选择指定变更名称(例如,\`/opsx:apply add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。 | ||
| **步骤** | ||
| 1. **选择变更** | ||
| 如果提供了名称,使用它。否则: | ||
| - 如果用户提到了某个变更,从对话上下文中推断 | ||
| - 如果只存在一个活动变更,自动选择 | ||
| - 如果不明确,运行 \`openspec-cn list --json\` 获取可用变更,并使用 **AskUserQuestion tool** 让用户选择 | ||
| 始终宣布:"正在使用变更:<name>"以及如何覆盖(例如,\`/opsx:apply <other>\`)。 | ||
| 2. **检查状态以了解 Schema** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流(例如:"spec-driven") | ||
| - 哪个产出物包含任务(对于 spec-driven 通常是 "tasks",检查其他产出物的状态) | ||
| 3. **获取应用指令** | ||
| \`\`\`bash | ||
| openspec-cn instructions apply --change "<name>" --json | ||
| \`\`\` | ||
| 这返回: | ||
| - 上下文文件路径(因 Schema 而异) | ||
| - 进度(总计,完成,剩余) | ||
| - 带有状态的任务列表 | ||
| - 基于当前状态的动态指令 | ||
| **处理状态:** | ||
| - 如果 \`state: "blocked"\`(缺少产出物):显示消息,建议使用 \`/opsx:continue\` | ||
| - 如果 \`state: "all_done"\`:祝贺,建议归档 | ||
| - 否则:继续实现 | ||
| 4. **阅读上下文文件** | ||
| 阅读 apply instructions 输出中 \`contextFiles\` 列出的文件。 | ||
| 文件取决于正在使用的 Schema: | ||
| - **spec-driven**: proposal, specs, design, tasks | ||
| - 其他模式:遵循 CLI 输出中的 contextFiles | ||
| 5. **显示当前进度** | ||
| 显示: | ||
| - 正在使用的 Schema | ||
| - 进度:"N/M 任务已完成" | ||
| - 剩余任务概览 | ||
| - 来自 CLI 的动态指令 | ||
| 6. **实现任务(循环直到完成或受阻)** | ||
| 对于每个待处理任务: | ||
| - 显示正在处理哪个任务 | ||
| - 进行所需的代码更改 | ||
| - 保持更改最小化且专注 | ||
| - 在任务文件中标记任务完成:\`- [ ]\` → \`- [x]\` | ||
| - 继续下一个任务 | ||
| **暂停如果:** | ||
| - 任务不清楚 → 询问澄清 | ||
| - 实现揭示了设计问题 → 建议更新产出物 | ||
| - 遇到错误或阻碍 → 报告并等待指导 | ||
| - 用户中断 | ||
| 7. **完成或暂停时,显示状态** | ||
| 显示: | ||
| - 本次会话完成的任务 | ||
| - 总体进度:"N/M 任务已完成" | ||
| - 如果全部完成:建议归档 | ||
| - 如果暂停:解释原因并等待指导 | ||
| **实现期间的输出** | ||
| \`\`\` | ||
| ## 正在实现:<change-name> (schema: <schema-name>) | ||
| 正在处理任务 3/7:<task description> | ||
| [...正在进行实现...] | ||
| ✓ 任务完成 | ||
| 正在处理任务 4/7:<task description> | ||
| [...正在进行实现...] | ||
| ✓ 任务完成 | ||
| \`\`\` | ||
| **完成时的输出** | ||
| \`\`\` | ||
| ## 实现完成 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **进度:** 7/7 任务已完成 ✓ | ||
| ### 本次会话已完成 | ||
| - [x] 任务 1 | ||
| - [x] 任务 2 | ||
| ... | ||
| 所有任务已完成!您可以使用 \`/opsx:archive\` 归档此变更。 | ||
| \`\`\` | ||
| **暂停时的输出(遇到问题)** | ||
| \`\`\` | ||
| ## 实现暂停 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **进度:** 4/7 任务已完成 | ||
| ### 遇到的问题 | ||
| <问题描述> | ||
| **选项:** | ||
| 1. <选项 1> | ||
| 2. <选项 2> | ||
| 3. 其他方法 | ||
| 您想怎么做? | ||
| \`\`\` | ||
| **护栏** | ||
| - 继续执行任务直到完成或受阻 | ||
| - 开始前始终阅读上下文文件(来自 apply instructions 输出) | ||
| - 如果任务模棱两可,暂停并在实现前询问 | ||
| - 如果实现揭示了问题,暂停并建议更新产出物 | ||
| - 保持代码更改最小化并限定在每个任务范围内 | ||
| - 完成每个任务后立即更新任务复选框 | ||
| - 遇到错误、阻碍或不清楚的需求时暂停 - 不要猜测 | ||
| - 使用 CLI 输出中的 contextFiles,不要假设特定的文件名 | ||
| **流畅的工作流集成** | ||
| 此技能支持"变更上的操作"模型: | ||
| - **可以随时调用**:在所有产出物完成之前(如果存在任务),部分实现之后,与其他操作交错 | ||
| - **允许产出物更新**:如果实现揭示了设计问题,建议更新产出物 - 不是阶段锁定的,流畅地工作` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=apply-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getArchiveChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxArchiveCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=archive-change.d.ts.map |
| export function getArchiveChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-archive-change', | ||
| description: '归档实验性工作流中已完成的变更。当用户想要在实现完成后最终确定并归档变更时使用。', | ||
| instructions: `归档实验性工作流中已完成的变更。 | ||
| **输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 仅显示活动变更(未归档的)。 | ||
| 如果可用,包括每个变更使用的 Schema。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查产出物完成状态** | ||
| 运行 \`openspec-cn status --change "<name>" --json\` 检查产出物完成情况。 | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流 | ||
| - \`artifacts\`:产出物列表及其状态(\`done\` 或其他) | ||
| **如果有任何产出物未 \`done\`:** | ||
| - 显示列出未完成产出物的警告 | ||
| - 使用 **AskUserQuestion tool** 确认用户是否要继续 | ||
| - 如果用户确认,则继续 | ||
| 3. **检查任务完成状态** | ||
| 阅读任务文件(通常是 \`tasks.md\`)以检查未完成的任务。 | ||
| 统计标记为 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)的任务。 | ||
| **如果发现未完成的任务:** | ||
| - 显示警告,显示未完成任务的数量 | ||
| - 使用 **AskUserQuestion tool** 确认用户是否要继续 | ||
| - 如果用户确认,则继续 | ||
| **如果没有任务文件存在:** 继续,无需任务相关警告。 | ||
| 4. **评估增量规范同步状态** | ||
| 检查 \`openspec/changes/<name>/specs/\` 中的增量规范。如果不存在,则在没有同步提示的情况下继续。 | ||
| **如果存在增量规范:** | ||
| - 将每个增量规范与其在 \`openspec/specs/<capability>/spec.md\` 对应的各主规范进行比较 | ||
| - 确定将应用哪些更改(添加、修改、移除、重命名) | ||
| - 在提示前显示综合摘要 | ||
| **提示选项:** | ||
| - 如果需要更改:"立即同步(推荐)"、"归档而不同步" | ||
| - 如果已同步:"立即归档"、"仍然同步"、"取消" | ||
| 如果用户选择同步,使用 Task tool(subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>")。无论选择如何,都继续归档。 | ||
| 5. **执行归档** | ||
| 如果归档目录不存在,则创建它: | ||
| \`\`\`bash | ||
| mkdir -p openspec/changes/archive | ||
| \`\`\` | ||
| 使用当前日期生成目标名称:\`YYYY-MM-DD-<change-name>\` | ||
| **检查目标是否已存在:** | ||
| - 如果是:失败并报错,建议重命名现有归档或使用不同日期 | ||
| - 如果否:将变更目录移动到归档 | ||
| \`\`\`bash | ||
| mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> | ||
| \`\`\` | ||
| 6. **显示摘要** | ||
| 显示归档完成摘要,包括: | ||
| - 变更名称 | ||
| - 使用的 Schema | ||
| - 归档位置 | ||
| - 规范是否已同步(如果适用) | ||
| - 关于任何警告的说明(未完成的产出物/任务) | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 归档完成 | ||
| **变更:** <change-name> | ||
| **模式:** <schema-name> | ||
| **归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/ | ||
| **规范:** ✓ 已同步到主规范(或 "无增量规范" 或 "同步已跳过") | ||
| 所有产出物已完成。所有任务已完成。 | ||
| \`\`\` | ||
| **防护措施** | ||
| - 如果未提供变更,始终提示选择 | ||
| - 使用产出物图(openspec-cn status --json)进行完成度检查 | ||
| - 不要在警告时阻止归档 - 只需告知并确认 | ||
| - 移动到归档时保留 .openspec.yaml(它与目录一起移动) | ||
| - 显示清晰的操作摘要 | ||
| - 如果请求同步,使用 openspec-sync-specs 方法(代理驱动) | ||
| - 如果存在增量规格说明,始终运行同步评估并在提示前显示综合摘要`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxArchiveCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 归档', | ||
| description: '归档实验性工作流中已完成的变更', | ||
| category: '工作流', | ||
| tags: ['workflow', 'archive', 'experimental'], | ||
| content: `归档实验性工作流中已完成的变更。 | ||
| **输入**:可选择在 \`/opsx:archive\` 后指定变更名称(例如,\`/opsx:archive add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 仅显示活动变更(未归档的)。 | ||
| 如果可用,包括每个变更使用的 Schema。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查产出物完成状态** | ||
| 运行 \`openspec-cn status --change "<name>" --json\` 检查产出物完成情况。 | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流 | ||
| - \`artifacts\`:产出物列表及其状态(\`done\` 或其他) | ||
| **如果有任何产出物未 \`done\`:** | ||
| - 显示列出未完成产出物的警告 | ||
| - 提示用户确认是否继续 | ||
| - 如果用户确认,则继续 | ||
| 3. **检查任务完成状态** | ||
| 阅读任务文件(通常是 \`tasks.md\`)以检查未完成的任务。 | ||
| 统计标记为 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)的任务。 | ||
| **如果发现未完成的任务:** | ||
| - 显示警告,显示未完成任务的数量 | ||
| - 提示用户确认是否继续 | ||
| - 如果用户确认,则继续 | ||
| **如果没有任务文件存在:** 继续,无需任务相关警告。 | ||
| 4. **评估增量规格说明同步状态** | ||
| 在 \`openspec/changes/<name>/specs/\` 检查增量规格说明。如果不存在,不提示同步直接继续。 | ||
| **如果存在增量规格说明:** | ||
| - 将每个增量规格说明与其在 \`openspec/specs/<capability>/spec.md\` 的相应主规格说明进行比较 | ||
| - 确定将应用哪些更改(添加、修改、删除、重命名) | ||
| - 在提示前显示合并摘要 | ||
| **提示选项:** | ||
| - 如果需要更改:"立即同步(推荐)","不同步直接归档" | ||
| - 如果已同步:"立即归档","仍要同步","取消" | ||
| 如果用户选择同步,执行 \`/opsx:sync\` 逻辑。无论选择如何都继续归档。 | ||
| 5. **执行归档** | ||
| 如果归档目录不存在,则创建它: | ||
| \`\`\`bash | ||
| mkdir -p openspec/changes/archive | ||
| \`\`\` | ||
| 使用当前日期生成目标名称:\`YYYY-MM-DD-<change-name>\` | ||
| **检查目标是否已存在:** | ||
| - 如果是:失败并报错,建议重命名现有归档或使用不同日期 | ||
| - 如果否:将变更目录移动到归档 | ||
| \`\`\`bash | ||
| mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> | ||
| \`\`\` | ||
| 6. **显示摘要** | ||
| 显示归档完成摘要,包括: | ||
| - 变更名称 | ||
| - 使用的 Schema | ||
| - 归档位置 | ||
| - 规格说明同步状态(已同步 / 跳过同步 / 无增量规格说明) | ||
| - 任何警告的注释(未完成的产出物/任务) | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 归档完成 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/ | ||
| **规范:** ✓ 已同步到主规范 | ||
| 所有产出物已完成。所有任务已完成。 | ||
| \`\`\` | ||
| **成功时的输出(无增量规范)** | ||
| \`\`\` | ||
| ## 归档完成 | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/ | ||
| **规范:** 无增量规范 | ||
| 所有产出物已完成。所有任务已完成。 | ||
| \`\`\` | ||
| **成功时的输出(带警告)** | ||
| \`\`\` | ||
| ## 归档完成(带警告) | ||
| **变更:** <change-name> | ||
| **Schema:** <schema-name> | ||
| **归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/ | ||
| **规格说明:** 跳过同步(用户选择跳过) | ||
| **警告:** | ||
| - 带有 2 个未完成产出物的归档 | ||
| - 带有 3 个未完成任务的归档 | ||
| - 增量规格说明同步已跳过(用户选择跳过) | ||
| 如果这不是故意的,请检查归档。 | ||
| \`\`\` | ||
| **错误时的输出(归档已存在)** | ||
| \`\`\` | ||
| ## 归档失败 | ||
| **变更:** <change-name> | ||
| **目标:** openspec/changes/archive/YYYY-MM-DD-<name>/ | ||
| 目标归档目录已存在。 | ||
| **选项:** | ||
| 1. 重命名现有归档 | ||
| 2. 如果是重复的,删除现有归档 | ||
| 3. 等待不同的日期再归档 | ||
| \`\`\` | ||
| **防护措施** | ||
| - 如果未提供变更,始终提示选择 | ||
| - 使用产出物图(openspec-cn status --json)进行完成度检查 | ||
| - 不要在警告时阻止归档 - 只需告知并确认 | ||
| - 移动到归档时保留 .openspec.yaml(它与目录一起移动) | ||
| - 显示清晰的操作摘要 | ||
| - 如果请求同步,使用 Skill tool 调用 \`openspec-sync-specs\`(代理驱动) | ||
| - 如果存在增量规格说明,请始终运行同步评估,并在提示前显示综合摘要` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=archive-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getBulkArchiveChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxBulkArchiveCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=bulk-archive-change.d.ts.map |
| export function getBulkArchiveChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-bulk-archive-change', | ||
| description: '一次归档多个已完成的变更。用于归档多个并行变更。', | ||
| instructions: `在单个操作中归档多个已完成的变更。 | ||
| 此技能允许您批量归档变更,通过检查代码库以确定实际实现了什么来智能处理规格说明冲突。 | ||
| **输入**:无需要求(会提示选择) | ||
| **步骤** | ||
| 1. **获取活动变更** | ||
| 运行 \`openspec-cn list --json\` 获取所有活动变更。 | ||
| 如果不存在活动变更,通知用户并停止。 | ||
| 2. **提示变更选择** | ||
| 使用 **AskUserQuestion 工具**进行多选,让用户选择变更: | ||
| - 显示每个变更及其 Schema | ||
| - 包含"所有变更"选项 | ||
| - 允许任意数量的选择(1+ 可用,2+ 是典型用例) | ||
| **重要提示**:不要自动选择。始终让用户选择。 | ||
| 3. **批量验证 - 收集所有选定变更的状态** | ||
| 对于每个选定的变更,收集: | ||
| a. **产出物状态** - 运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 解析 \`schemaName\` 和 \`artifacts\` 列表 | ||
| - 注意哪些产出物是 \`done\` 状态而非其他状态 | ||
| b. **任务完成度** - 读取 \`openspec/changes/<name>/tasks.md\` | ||
| - 统计 \`- [ ]\`(未完成)与 \`- [x]\`(已完成) | ||
| - 如果不存在任务文件,标注为"无任务" | ||
| c. **增量规格说明** - 检查 \`openspec/changes/<name>/specs/\` 目录 | ||
| - 列出存在哪些能力规格说明 | ||
| - 对于每个,提取需求名称(匹配 \`### 需求: <name>\` 的行) | ||
| 4. **检测规格说明冲突** | ||
| 构建 \`capability -> [涉及它的变更]\` 映射: | ||
| \`\`\` | ||
| auth -> [change-a, change-b] <- 冲突(2+ 个变更) | ||
| api -> [change-c] <- 正常(仅 1 个变更) | ||
| \`\`\` | ||
| 当 2+ 个选定的变更具有相同能力的增量规格说明时,存在冲突。 | ||
| 5. **代理式解决冲突** | ||
| **对于每个冲突**,调查代码库: | ||
| a. **读取增量规格说明** 从每个冲突的变更中了解每个声称添加/修改的内容 | ||
| b. **搜索代码库** 寻找实现证据: | ||
| - 查找实现每个增量规格说明中需求的代码 | ||
| - 检查相关文件、函数或测试 | ||
| c. **确定解决方案**: | ||
| - 如果只有一个变更实际实现 -> 同步该变更的规格说明 | ||
| - 如果两者都实现 -> 按时间顺序应用(旧的先,新的覆盖) | ||
| - 如果两者都未实现 -> 跳过规格说明同步,警告用户 | ||
| d. **记录解决方案** 对于每个冲突: | ||
| - 应用哪个变更的规格说明 | ||
| - 按什么顺序(如果两者都有) | ||
| - 原理(在代码库中找到了什么) | ||
| 6. **显示合并状态表** | ||
| 显示汇总所有变更的表: | ||
| \`\`\` | ||
| | 变更 | 产出物 | 任务 | 规格说明 | 冲突 | 状态 | | ||
| |---------------------|-----------|-------|---------|-----------|--------| | ||
| | schema-management | 完成 | 5/5 | 2 增量 | 无 | 就绪 | | ||
| | project-config | 完成 | 3/3 | 1 增量 | 无 | 就绪 | | ||
| | add-oauth | 完成 | 4/4 | 1 增量 | auth (!) | 就绪* | | ||
| | add-verify-skill | 剩余 1 | 2/5 | 无 | 无 | 警告 | | ||
| \`\`\` | ||
| 对于冲突,显示解决方案: | ||
| \`\`\` | ||
| * 冲突解决方案: | ||
| - auth 规格说明:将先应用 add-oauth 然后 add-jwt(两者都已实现,按时间顺序) | ||
| \`\`\` | ||
| 对于未完成的变更,显示警告: | ||
| \`\`\` | ||
| 警告: | ||
| - add-verify-skill:1 个未完成产出物,3 个未完成任务 | ||
| \`\`\` | ||
| 7. **确认批量操作** | ||
| 使用 **AskUserQuestion 工具**进行单次确认: | ||
| - "归档 N 个变更?"根据状态提供选项 | ||
| - 选项可能包括: | ||
| - "归档所有 N 个变更" | ||
| - "仅归档 N 个就绪变更(跳过未完成的)" | ||
| - "取消" | ||
| 如果存在未完成的变更,请明确说明它们将带着警告被归档。 | ||
| 8. **对每个确认的变更执行归档** | ||
| 按确定的顺序处理变更(遵循冲突解决方案): | ||
| a. **如果存在增量规格说明则同步规格说明**: | ||
| - 使用 openspec-sync-specs 方法(代理驱动的智能合并) | ||
| - 对于冲突,按已解决的顺序应用 | ||
| - 跟踪是否已完成同步 | ||
| b. **执行归档**: | ||
| \`\`\`bash | ||
| mkdir -p openspec/changes/archive | ||
| mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> | ||
| \`\`\` | ||
| c. **跟踪每个变更的结果**: | ||
| - 成功:成功归档 | ||
| - 失败:归档期间出错(记录错误) | ||
| - 跳过:用户选择不归档(如适用) | ||
| 9. **显示摘要** | ||
| 显示最终结果: | ||
| \`\`\` | ||
| ## 批量归档完成 | ||
| 已归档 3 个变更: | ||
| - schema-management-cli -> archive/2026-01-19-schema-management-cli/ | ||
| - project-config -> archive/2026-01-19-project-config/ | ||
| - add-oauth -> archive/2026-01-19-add-oauth/ | ||
| 跳过 1 个变更: | ||
| - add-verify-skill(用户选择不归档未完成的) | ||
| 规格说明同步摘要: | ||
| - 4 个增量规格说明已同步到主规格说明 | ||
| - 1 个冲突已解决(auth:按时间顺序应用两者) | ||
| \`\`\` | ||
| 如果有任何失败: | ||
| \`\`\` | ||
| 失败 1 个变更: | ||
| - some-change:归档目录已存在 | ||
| \`\`\` | ||
| **冲突解决示例** | ||
| 示例 1:仅一个已实现 | ||
| \`\`\` | ||
| 冲突:specs/auth/spec.md 被 [add-oauth, add-jwt] 涉及 | ||
| 检查 add-oauth: | ||
| - 增量添加"OAuth 提供商集成"需求 | ||
| - 搜索代码库... 找到 src/auth/oauth.ts 实现 OAuth 流程 | ||
| 检查 add-jwt: | ||
| - 增量添加"JWT 令牌处理"需求 | ||
| - 搜索代码库... 未找到 JWT 实现 | ||
| 解决方案:仅 add-oauth 已实现。将仅同步 add-oauth 规格说明。 | ||
| \`\`\` | ||
| 示例 2:两者都已实现 | ||
| \`\`\` | ||
| 冲突:specs/api/spec.md 被 [add-rest-api, add-graphql] 涉及 | ||
| 检查 add-rest-api(创建于 2026-01-10): | ||
| - 增量添加"REST 端点"需求 | ||
| - 搜索代码库... 找到 src/api/rest.ts | ||
| 检查 add-graphql(创建于 2026-01-15): | ||
| - 增量添加"GraphQL 架构"需求 | ||
| - 搜索代码库... 找到 src/api/graphql.ts | ||
| 解决方案:两者都已实现。将先应用 add-rest-api 规格说明, | ||
| 然后应用 add-graphql 规格说明(按时间顺序,较新的优先)。 | ||
| \`\`\` | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 批量归档完成 | ||
| 已归档 N 个变更: | ||
| - <change-1> -> archive/YYYY-MM-DD-<change-1>/ | ||
| - <change-2> -> archive/YYYY-MM-DD-<change-2>/ | ||
| 规格说明同步摘要: | ||
| - N 个增量规格说明已同步到主规格说明 | ||
| - 无冲突(或:M 个冲突已解决) | ||
| \`\`\` | ||
| **部分成功时的输出** | ||
| \`\`\` | ||
| ## 批量归档完成(部分) | ||
| 已归档 N 个变更: | ||
| - <change-1> -> archive/YYYY-MM-DD-<change-1>/ | ||
| 跳过 M 个变更: | ||
| - <change-2>(用户选择不归档未完成的) | ||
| 失败 K 个变更: | ||
| - <change-3>:归档目录已存在 | ||
| \`\`\` | ||
| **没有变更时的输出** | ||
| \`\`\` | ||
| ## 无需归档的变更 | ||
| 未找到活动变更。使用 \`/opsx:new\` 创建新变更。 | ||
| \`\`\` | ||
| **防护措施** | ||
| - 允许任意数量的变更(1+ 可以,2+ 是典型用例) | ||
| - 始终提示选择,永不自动选择 | ||
| - 及早检测规格说明冲突并通过检查代码库解决 | ||
| - 当两个变更都已实现时,按时间顺序应用规格说明 | ||
| - 仅当实现缺失时跳过规格说明同步(警告用户) | ||
| - 在确认前显示清晰的每个变更状态 | ||
| - 对整个批次使用单次确认 | ||
| - 跟踪并报告所有结果(成功/跳过/失败) | ||
| - 移动到归档时保留 .openspec.yaml | ||
| - 归档目录目标使用当前日期:YYYY-MM-DD-<name> | ||
| - 如果归档目标已存在,该变更失败但继续处理其他变更`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxBulkArchiveCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 批量归档', | ||
| description: '一次归档多个已完成的变更', | ||
| category: '工作流', | ||
| tags: ['workflow', 'archive', 'experimental', 'bulk'], | ||
| content: `在单个操作中归档多个已完成的变更。 | ||
| 此技能允许您批量归档变更,通过检查代码库以确定实际实现了什么来智能处理规格说明冲突。 | ||
| **输入**:无需要求(会提示选择) | ||
| **步骤** | ||
| 1. **获取活动变更** | ||
| 运行 \`openspec-cn list --json\` 获取所有活动变更。 | ||
| 如果不存在活动变更,通知用户并停止。 | ||
| 2. **提示变更选择** | ||
| 使用 **AskUserQuestion 工具**进行多选,让用户选择变更: | ||
| - 显示每个变更及其 Schema | ||
| - 包含"所有变更"选项 | ||
| - 允许任意数量的选择(1+ 可用,2+ 是典型用例) | ||
| **重要提示**:不要自动选择。始终让用户选择。 | ||
| 3. **批量验证 - 收集所有选定变更的状态** | ||
| 对于每个选定的变更,收集: | ||
| a. **产出物状态** - 运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 解析 \`schemaName\` 和 \`artifacts\` 列表 | ||
| - 注意哪些产出物是 \`done\` 状态而非其他状态 | ||
| b. **任务完成度** - 读取 \`openspec/changes/<name>/tasks.md\` | ||
| - 统计 \`- [ ]\`(未完成)与 \`- [x]\`(已完成) | ||
| - 如果不存在任务文件,标注为"无任务" | ||
| c. **增量规格说明** - 检查 \`openspec/changes/<name>/specs/\` 目录 | ||
| - 列出存在哪些能力规格说明 | ||
| - 对于每个,提取需求名称(匹配 \`### 需求: <name>\` 的行) | ||
| 4. **检测规格说明冲突** | ||
| 构建 \`capability -> [涉及它的变更]\` 映射: | ||
| \`\`\` | ||
| auth -> [change-a, change-b] <- 冲突(2+ 个变更) | ||
| api -> [change-c] <- 正常(仅 1 个变更) | ||
| \`\`\` | ||
| 当 2+ 个选定的变更具有相同能力的增量规格说明时,存在冲突。 | ||
| 5. **代理式解决冲突** | ||
| **对于每个冲突**,调查代码库: | ||
| a. **读取增量规格说明** 从每个冲突的变更中了解每个声称添加/修改的内容 | ||
| b. **搜索代码库** 寻找实现证据: | ||
| - 查找实现每个增量规格说明中需求的代码 | ||
| - 检查相关文件、函数或测试 | ||
| c. **确定解决方案**: | ||
| - 如果只有一个变更实际实现 -> 同步该变更的规格说明 | ||
| - 如果两者都实现 -> 按时间顺序应用(旧的先,新的覆盖) | ||
| - 如果两者都未实现 -> 跳过规格说明同步,警告用户 | ||
| d. **记录解决方案** 对于每个冲突: | ||
| - 应用哪个变更的规格说明 | ||
| - 按什么顺序(如果两者都有) | ||
| - 原理(在代码库中找到了什么) | ||
| 6. **显示合并状态表** | ||
| 显示汇总所有变更的表: | ||
| \`\`\` | ||
| | 变更 | 产出物 | 任务 | 规格说明 | 冲突 | 状态 | | ||
| |---------------------|-----------|-------|---------|-----------|--------| | ||
| | schema-management | 完成 | 5/5 | 2 增量 | 无 | 就绪 | | ||
| | project-config | 完成 | 3/3 | 1 增量 | 无 | 就绪 | | ||
| | add-oauth | 完成 | 4/4 | 1 增量 | auth (!) | 就绪* | | ||
| | add-verify-skill | 剩余 1 | 2/5 | 无 | 无 | 警告 | | ||
| \`\`\` | ||
| 对于冲突,显示解决方案: | ||
| \`\`\` | ||
| * 冲突解决方案: | ||
| - auth 规格说明:将先应用 add-oauth 然后 add-jwt(两者都已实现,按时间顺序) | ||
| \`\`\` | ||
| 对于未完成的变更,显示警告: | ||
| \`\`\` | ||
| 警告: | ||
| - add-verify-skill:1 个未完成产出物,3 个未完成任务 | ||
| \`\`\` | ||
| 7. **确认批量操作** | ||
| 使用 **AskUserQuestion 工具**进行单次确认: | ||
| - "归档 N 个变更?"根据状态提供选项 | ||
| - 选项可能包括: | ||
| - "归档所有 N 个变更" | ||
| - "仅归档 N 个就绪变更(跳过未完成的)" | ||
| - "取消" | ||
| 如果存在未完成的变更,请明确说明它们将带着警告被归档。 | ||
| 8. **对每个确认的变更执行归档** | ||
| 按确定的顺序处理变更(遵循冲突解决方案): | ||
| a. **如果存在增量规格说明则同步规格说明**: | ||
| - 使用 openspec-sync-specs 方法(代理驱动的智能合并) | ||
| - 对于冲突,按已解决的顺序应用 | ||
| - 跟踪是否已完成同步 | ||
| b. **执行归档**: | ||
| \`\`\`bash | ||
| mkdir -p openspec/changes/archive | ||
| mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> | ||
| \`\`\` | ||
| c. **跟踪每个变更的结果**: | ||
| - 成功:成功归档 | ||
| - 失败:归档期间出错(记录错误) | ||
| - 跳过:用户选择不归档(如适用) | ||
| 9. **显示摘要** | ||
| 显示最终结果: | ||
| \`\`\` | ||
| ## 批量归档完成 | ||
| 已归档 3 个变更: | ||
| - schema-management-cli -> archive/2026-01-19-schema-management-cli/ | ||
| - project-config -> archive/2026-01-19-project-config/ | ||
| - add-oauth -> archive/2026-01-19-add-oauth/ | ||
| 跳过 1 个变更: | ||
| - add-verify-skill(用户选择不归档未完成的) | ||
| 规格说明同步摘要: | ||
| - 4 个增量规格说明已同步到主规格说明 | ||
| - 1 个冲突已解决(auth:按时间顺序应用两者) | ||
| \`\`\` | ||
| 如果有任何失败: | ||
| \`\`\` | ||
| 失败 1 个变更: | ||
| - some-change:归档目录已存在 | ||
| \`\`\` | ||
| **冲突解决示例** | ||
| 示例 1:仅一个已实现 | ||
| \`\`\` | ||
| 冲突:specs/auth/spec.md 被 [add-oauth, add-jwt] 涉及 | ||
| 检查 add-oauth: | ||
| - 增量添加"OAuth 提供商集成"需求 | ||
| - 搜索代码库... 找到 src/auth/oauth.ts 实现 OAuth 流程 | ||
| 检查 add-jwt: | ||
| - 增量添加"JWT 令牌处理"需求 | ||
| - 搜索代码库... 未找到 JWT 实现 | ||
| 解决方案:仅 add-oauth 已实现。将仅同步 add-oauth 规格说明。 | ||
| \`\`\` | ||
| 示例 2:两者都已实现 | ||
| \`\`\` | ||
| 冲突:specs/api/spec.md 被 [add-rest-api, add-graphql] 涉及 | ||
| 检查 add-rest-api(创建于 2026-01-10): | ||
| - 增量添加"REST 端点"需求 | ||
| - 搜索代码库... 找到 src/api/rest.ts | ||
| 检查 add-graphql(创建于 2026-01-15): | ||
| - 增量添加"GraphQL 架构"需求 | ||
| - 搜索代码库... 找到 src/api/graphql.ts | ||
| 解决方案:两者都已实现。将先应用 add-rest-api 规格说明, | ||
| 然后应用 add-graphql 规格说明(按时间顺序,较新的优先)。 | ||
| \`\`\` | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 批量归档完成 | ||
| 已归档 N 个变更: | ||
| - <change-1> -> archive/YYYY-MM-DD-<change-1>/ | ||
| - <change-2> -> archive/YYYY-MM-DD-<change-2>/ | ||
| 规格说明同步摘要: | ||
| - N 个增量规格说明已同步到主规格说明 | ||
| - 无冲突(或:M 个冲突已解决) | ||
| \`\`\` | ||
| **部分成功时的输出** | ||
| \`\`\` | ||
| ## 批量归档完成(部分) | ||
| 已归档 N 个变更: | ||
| - <change-1> -> archive/YYYY-MM-DD-<change-1>/ | ||
| 跳过 M 个变更: | ||
| - <change-2>(用户选择不归档未完成的) | ||
| 失败 K 个变更: | ||
| - <change-3>:归档目录已存在 | ||
| \`\`\` | ||
| **没有变更时的输出** | ||
| \`\`\` | ||
| ## 无需归档的变更 | ||
| 未找到活动变更。使用 \`/opsx:new\` 创建新变更。 | ||
| \`\`\` | ||
| **防护措施** | ||
| - 允许任意数量的变更(1+ 可以,2+ 是典型用例) | ||
| - 始终提示选择,永不自动选择 | ||
| - 及早检测规格说明冲突并通过检查代码库解决 | ||
| - 当两个变更都已实现时,按时间顺序应用规格说明 | ||
| - 仅当实现缺失时跳过规格说明同步(警告用户) | ||
| - 在确认前显示清晰的每个变更状态 | ||
| - 对整个批次使用单次确认 | ||
| - 跟踪并报告所有结果(成功/跳过/失败) | ||
| - 移动到归档时保留 .openspec.yaml | ||
| - 归档目录目标使用当前日期:YYYY-MM-DD-<name> | ||
| - 如果归档目标已存在,该变更失败但继续处理其他变更` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=bulk-archive-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getContinueChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxContinueCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=continue-change.d.ts.map |
| export function getContinueChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-continue-change', | ||
| description: '通过创建下一个产出物继续处理 OpenSpec 变更。当用户想要推进其变更、创建下一个产出物或继续其工作流程时使用。', | ||
| instructions: `通过创建下一个产出物继续处理变更。 | ||
| **输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取按最近修改排序的可用变更。然后使用 **AskUserQuestion tool** 让用户选择要处理哪个变更。 | ||
| 展示前 3-4 个最近修改的变更作为选项,显示: | ||
| - 变更名称 | ||
| - Schema(如果存在 \`schema\` 字段,否则为 "spec-driven") | ||
| - 状态(例如:"0/5 tasks", "complete", "no tasks") | ||
| - 最近修改时间(来自 \`lastModified\` 字段) | ||
| 将最近修改的变更标记为 "(推荐)",因为它很可能是用户想要继续的。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查当前状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解当前状态。响应包括: | ||
| - \`schemaName\`:正在使用的工作流 schema(例如:"spec-driven") | ||
| - \`artifacts\`:产出物数组及其状态("done"、"ready"、"blocked") | ||
| - \`isComplete\`:布尔值,表示是否所有产出物都已完成 | ||
| 3. **根据状态行动**: | ||
| --- | ||
| **如果所有产出物已完成 (\`isComplete: true\`)**: | ||
| - 祝贺用户 | ||
| - 显示最终状态,包括使用的 Schema | ||
| - 建议:"所有产出物已创建!您现在可以实现此变更或将其归档。" | ||
| - 停止 | ||
| --- | ||
| **如果产出物准备好创建**(状态显示有 \`status: "ready"\` 的产出物): | ||
| - 从状态输出中选择第一个 \`status: "ready"\` 的产出物 | ||
| - 获取其指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 解析 JSON。关键字段包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:输出文件使用的结构 | ||
| - \`instruction\`:schema 特定指导 | ||
| - \`outputPath\`:产出物写入路径 | ||
| - \`dependencies\`:已完成的依赖产出物(用于读取上下文) | ||
| - **创建产出物文件**: | ||
| - 阅读任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构 - 填充各个章节 | ||
| - 在编写时将 \`context\` 和 \`rules\` 作为约束 - 但不要把它们复制进文件 | ||
| - 写入指令中指定的 outputPath | ||
| - 展示创建了什么,以及现在解锁了什么 | ||
| - 创建一个产出物后停止 | ||
| --- | ||
| **如果没有产出物准备好(全部受阻)**: | ||
| - 在有效的 Schema 下不应发生这种情况 | ||
| - 显示状态并建议检查问题 | ||
| 4. **创建产出物后,显示进度** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 每次调用后,显示: | ||
| - 创建了哪个产出物 | ||
| - 正在使用的 Schema 工作流 | ||
| - 当前进度(N/M 完成) | ||
| - 现在解锁了哪些产出物 | ||
| - 提示:"想要继续吗?只需让我继续或告诉我下一步做什么。" | ||
| **产出物创建指南** | ||
| 产出物类型及其用途取决于 Schema。使用指令输出中的 \`instruction\` 字段来了解要创建什么。 | ||
| 常见的产出物模式: | ||
| **spec-driven schema**(proposal → specs → design → tasks): | ||
| - **proposal.md**:如果变更不清楚,先向用户确认。填写"为什么""什么变化""能力""影响"。 | ||
| - "能力"部分很关键——列出的每个能力都需要一个 spec 文件。 | ||
| - **specs/<capability>/spec.md**:为提案"能力"部分列出的每个能力创建一个 spec(使用 capability 名称,而不是 change 名称)。 | ||
| - **design.md**:记录技术决策、架构和实现方法。 | ||
| - **tasks.md**:把实现拆分为带复选框的任务。 | ||
| 对于其他 schema,遵循 CLI 输出中的 \`instruction\` 字段。 | ||
| **护栏** | ||
| - 每次调用只创建一个产出物 | ||
| - 创建新产出物前,总是先阅读依赖产出物 | ||
| - 不要跳过产出物,也不要乱序创建 | ||
| - 如果上下文不清楚,创建前先询问用户 | ||
| - 写入后先确认产出物文件存在,再标记进度 | ||
| - 使用 schema 的产出物顺序,不要假设固定的产出物名称 | ||
| - **重要**:\`context\` 和 \`rules\` 是对你的约束,不是文件内容 | ||
| - 不要把 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制进产出物 | ||
| - 它们用于指导你写作,但绝不能出现在输出中`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxContinueCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 继续', | ||
| description: '继续处理变更 - 创建下一个产出物(实验性)', | ||
| category: '工作流', | ||
| tags: ['workflow', 'artifacts', 'experimental'], | ||
| content: `通过创建下一个产出物继续处理变更。 | ||
| **输入**:可选择在 \`/opsx:continue\` 后指定变更名称(例如,\`/opsx:continue add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取按最近修改排序的可用变更。然后使用 **AskUserQuestion tool** 让用户选择要处理哪个变更。 | ||
| 展示前 3-4 个最近修改的变更作为选项,显示: | ||
| - 变更名称 | ||
| - Schema(如果存在 \`schema\` 字段,否则为 "spec-driven") | ||
| - 状态(例如:"0/5 tasks", "complete", "no tasks") | ||
| - 最近修改时间(来自 \`lastModified\` 字段) | ||
| 将最近修改的变更标记为 "(推荐)",因为它很可能是用户想要继续的。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查当前状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解当前状态。响应包括: | ||
| - \`schemaName\`:正在使用的工作流 schema(例如:"spec-driven") | ||
| - \`artifacts\`:产出物数组及其状态("done"、"ready"、"blocked") | ||
| - \`isComplete\`:布尔值,表示是否所有产出物都已完成 | ||
| 3. **根据状态行动**: | ||
| --- | ||
| **如果所有产出物已完成 (\`isComplete: true\`)**: | ||
| - 祝贺用户 | ||
| - 显示最终状态,包括使用的 Schema | ||
| - 建议:"所有产出物已创建!您现在可以使用 \`/opsx:apply\` 实施此变更或使用 \`/opsx:archive\` 归档它。" | ||
| - 停止 | ||
| --- | ||
| **如果产出物准备好创建**(状态显示有 \`status: "ready"\` 的产出物): | ||
| - 从状态输出中选择第一个 \`status: "ready"\` 的产出物 | ||
| - 获取其指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 解析 JSON。关键字段包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:用于输出文件的结构 | ||
| - \`instruction\`:Schema 特定指导 | ||
| - \`outputPath\`:写入产出物的位置 | ||
| - \`dependencies\`:已完成的产出物,用于读取上下文 | ||
| - **创建产出物文件**: | ||
| - 读取任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构 - 填充其各个部分 | ||
| - 在编写时应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中 | ||
| - 写入指令中指定的输出路径 | ||
| - 显示创建的内容以及现在解锁的内容 | ||
| - 创建一个产出物后停止 | ||
| --- | ||
| **如果没有产出物准备好(全部受阻)**: | ||
| - 在有效的 Schema 下不应发生这种情况 | ||
| - 显示状态并建议检查问题 | ||
| 4. **创建产出物后,显示进度** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 每次调用后,显示: | ||
| - 创建了哪个产出物 | ||
| - 正在使用的 Schema 工作流 | ||
| - 当前进度(N/M 完成) | ||
| - 现在解锁了哪些产出物 | ||
| - 提示:"运行 \`/opsx:continue\` 以创建下一个产出物" | ||
| **产出物创建指南** | ||
| 产出物类型及其用途取决于 Schema。使用指令输出中的 \`instruction\` 字段来了解要创建什么。 | ||
| 常见的产出物模式: | ||
| **spec-driven schema**(proposal → specs → design → tasks): | ||
| - **proposal.md**:如果变更不清楚,先向用户确认。填写"为什么""什么变化""能力""影响"。 | ||
| - "能力"部分很关键——列出的每个能力都需要一个 spec 文件。 | ||
| - **specs/<capability>/spec.md**:为提案"能力"部分列出的每个能力创建一个 spec(使用 capability 名称,而不是 change 名称)。 | ||
| - **design.md**:记录技术决策、架构和实现方法。 | ||
| - **tasks.md**:把实现拆分为带复选框的任务。 | ||
| 对于其他 schema,遵循 CLI 输出中的 \`instruction\` 字段。 | ||
| **护栏** | ||
| - 每次调用只创建一个产出物 | ||
| - 创建新产出物前,总是先阅读依赖产出物 | ||
| - 不要跳过产出物,也不要乱序创建 | ||
| - 如果上下文不清楚,创建前先询问用户 | ||
| - 写入后先确认产出物文件存在,再标记进度 | ||
| - 使用 schema 的产出物顺序,不要假设固定的产出物名称 | ||
| - **重要**:\`context\` 和 \`rules\` 是对你的约束,不是文件内容 | ||
| - 不要把 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制进产出物 | ||
| - 它们用于指导你写作,但绝不能出现在输出中` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=continue-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getExploreSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxExploreCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=explore.d.ts.map |
| export function getExploreSkillTemplate() { | ||
| return { | ||
| name: 'openspec-explore', | ||
| description: '进入探索模式 - 一个用于探索想法、调查问题和澄清需求的思考伙伴。当用户想要在进行更改之前或期间深入思考某事时使用。', | ||
| instructions: `进入探索模式。深入思考。自由想象。跟随对话的任何方向。 | ||
| **重要提示:探索模式是为了思考,而不是为了实施。** 你可以阅读文件、搜索代码和调查代码库,但你绝不能编写代码或实现功能。如果用户要求你实现某些内容,请提醒他们先退出探索模式(例如,使用 \`/opsx:new\` 或 \`/opsx:ff\` 开始变更)。如果用户要求,你可以创建 OpenSpec 产出物(提案、设计、规格说明)——这是捕捉思考,而不是实施。 | ||
| **这是一种姿态,而不是一种工作流。** 没有固定的步骤,没有要求的顺序,没有强制性的输出。你是一个思考伙伴,帮助用户进行探索。 | ||
| --- | ||
| ## 姿态 | ||
| - **好奇而非说教** - 提出自然产生的问题,不要照本宣科 | ||
| - **开放话题而非审问** - 浮现多个有趣的方向,让用户选择产生共鸣的部分。不要把他们限制在单一的提问路径中。 | ||
| - **可视化** - 在有助于澄清思路时大方使用 ASCII 图表 | ||
| - **自适应** - 跟随有趣的话题,当新信息出现时及时转换 | ||
| - **耐心** - 不要急于下结论,让问题的轮廓自然显现 | ||
| - **务实** - 在相关时探索实际的代码库,不要仅仅停留在理论上 | ||
| --- | ||
| ## 你可能做的事情 | ||
| 根据用户提出的内容,你可能会: | ||
| **探索问题空间** | ||
| - 针对他们所说的内容提出澄清性问题 | ||
| - 挑战假设 | ||
| - 重新构建问题 | ||
| - 寻找类比 | ||
| **调查代码库** | ||
| - 绘制与讨论相关的现有架构图 | ||
| - 寻找集成点 | ||
| - 识别已在使用的模式 | ||
| - 揭示隐藏的复杂性 | ||
| **比较选项** | ||
| - 头脑风暴多种方法 | ||
| - 构建比较表 | ||
| - 勾勒权衡 | ||
| - 推荐路径(如果被询问) | ||
| **可视化** | ||
| \`\`\` | ||
| ┌─────────────────────────────────────────┐ | ||
| │ 大量使用 ASCII 图表 │ | ||
| ├─────────────────────────────────────────┤ | ||
| │ │ | ||
| │ ┌────────┐ ┌────────┐ │ | ||
| │ │ 状态 │────────▶│ 状态 │ │ | ||
| │ │ A │ │ B │ │ | ||
| │ └────────┘ └────────┘ │ | ||
| │ │ | ||
| │ 系统图、状态机、数据流、 │ | ||
| │ 架构草图、依赖图、比较表 │ | ||
| │ │ | ||
| └─────────────────────────────────────────┘ | ||
| \`\`\` | ||
| **揭示风险和未知数** | ||
| - 识别可能出错的地方 | ||
| - 发现理解上的差距 | ||
| - 建议进行探针(Spike)或调查 | ||
| --- | ||
| ## OpenSpec 意识 | ||
| 你拥有 OpenSpec 系统的完整上下文。自然地使用它,不要强行使用。 | ||
| ### 检查上下文 | ||
| 开始时,快速检查存在什么: | ||
| \`\`\`bash | ||
| openspec-cn list --json | ||
| \`\`\` | ||
| 这会告诉你: | ||
| - 是否有活跃的变更 | ||
| - 它们的名称、模式和状态 | ||
| - 用户可能正在处理的内容 | ||
| ### 当不存在变更时 | ||
| 自由思考。当见解清晰时,你可以提议: | ||
| - "这感觉足够扎实,可以开始一个变更了。要我创建一个吗?" | ||
| → 可以过渡到 \`/opsx:new\` 或 \`/opsx:ff\` | ||
| - 或者继续探索 - 没有形式化的压力 | ||
| ### 当存在变更时 | ||
| 如果用户提到变更或你检测到相关变更: | ||
| 1. **阅读现有产出物以获取上下文** | ||
| - \`openspec/changes/<name>/proposal.md\` | ||
| - \`openspec/changes/<name>/design.md\` | ||
| - \`openspec/changes/<name>/tasks.md\` | ||
| - 等等 | ||
| 2. **在对话中自然地引用它们** | ||
| - "你的设计提到使用 Redis,但我们刚刚意识到 SQLite 更合适……" | ||
| - "提案将此范围限定为高级用户,但我们现在考虑面向所有人……" | ||
| 3. **当做出决定时提议捕获** | ||
| | 见解类型 | 捕获位置 | | ||
| |--------------|------------------| | ||
| | 发现新需求 | \`specs/<capability>/spec.md\` | | ||
| | 需求变更 | \`specs/<capability>/spec.md\` | | ||
| | 做出设计决策 | \`design.md\` | | ||
| | 范围变更 | \`proposal.md\` | | ||
| | 识别出新工作 | \`tasks.md\` | | ||
| | 假设失效 | 相关产出物 | | ||
| 提议示例: | ||
| - "这是一个设计决策。要把它记录在 design.md 中吗?" | ||
| - "这是一个新需求。要把它添加到 specs 中吗?" | ||
| - "这改变了范围。要更新 proposal 吗?" | ||
| 4. **由用户决定** - 提议并继续。不要施压。不要自动捕获。 | ||
| --- | ||
| ## 你不必做的事情 | ||
| - 遵循脚本 | ||
| - 每次都问相同的问题 | ||
| - 生成特定的产出物 | ||
| - 得出结论 | ||
| - 如果离题有价值,不必保持在主题上 | ||
| - 简短(这是思考时间) | ||
| --- | ||
| ## 处理不同的切入点 | ||
| **用户提出一个模糊的想法:** | ||
| \`\`\` | ||
| 用户: 我正在考虑添加实时协作 | ||
| 你: 实时协作是一个很大的领域。让我想想…… | ||
| 协作谱系 | ||
| ════════════════════════════════════════════ | ||
| 感知 协调 同步 | ||
| │ │ │ | ||
| ▼ ▼ ▼ | ||
| ┌────────┐ ┌────────┐ ┌────────┐ | ||
| │ 存在 │ │ 光标 │ │ CRDT │ | ||
| │ "3人 │ │ 多选 │ │ 无冲突 │ | ||
| │ 在线" │ │ │ │ │ | ||
| └────────┘ └────────┘ └────────┘ | ||
| │ │ │ | ||
| 简单 中等 复杂 | ||
| 你的想法倾向于哪里? | ||
| \`\`\` | ||
| **用户提出一个具体问题:** | ||
| \`\`\` | ||
| 用户: 认证系统一团糟 | ||
| 你: [阅读代码库] | ||
| ┌─────────────────────────────────────────────┐ | ||
| │ 当前认证流程 │ | ||
| └─────────────────────────────────────────────┘ | ||
| │ | ||
| ┌─────────────┼─────────────┐ | ||
| ▼ ▼ ▼ | ||
| ┌─────────┐ ┌─────────┐ ┌─────────┐ | ||
| │ Google │ │ GitHub │ │ Email │ | ||
| │ OAuth │ │ OAuth │ │ Magic │ | ||
| └────┬────┘ └────┬────┘ └────┬────┘ | ||
| │ │ │ | ||
| └─────────────┼─────────────┘ | ||
| ▼ | ||
| ┌───────────┐ | ||
| │ 会话 │ | ||
| └─────┬─────┘ | ||
| │ | ||
| ▼ | ||
| ┌───────────┐ | ||
| │ 权限 │ | ||
| └───────────┘ | ||
| 我看到三个纠结的地方。哪个最紧急? | ||
| \`\`\` | ||
| **用户在实施中途卡住:** | ||
| \`\`\` | ||
| 用户: /opsx:explore add-auth-system | ||
| OAuth 集成比预期的要复杂 | ||
| 你: [阅读变更产出物] | ||
| 你在任务 4:"实现 OAuth 流程" | ||
| 让我追踪一下涉及的内容…… | ||
| [绘制图表,探索选项,建议路径] | ||
| 想要更新设计以反映这一点吗? | ||
| 或者添加一个探针任务来调查? | ||
| \`\`\` | ||
| **用户想要比较选项:** | ||
| \`\`\` | ||
| 用户: 我们应该用 Postgres 还是 SQLite? | ||
| 你: 通用的回答很无聊。上下文是什么? | ||
| 用户: 一个跟踪本地开发环境的 CLI 工具 | ||
| 你: 那改变了一切。 | ||
| ┌─────────────────────────────────────────────────┐ | ||
| │ CLI 工具数据存储 │ | ||
| └─────────────────────────────────────────────────┘ | ||
| 关键约束: | ||
| • 无后台进程运行 | ||
| • 必须离线工作 | ||
| • 单用户 | ||
| SQLite Postgres | ||
| 部署 嵌入式 ✓ 需要服务器 ✗ | ||
| 离线 是 ✓ 否 ✗ | ||
| 单文件 是 ✓ 否 ✗ | ||
| SQLite。毫无疑问。 | ||
| 除非……有同步组件吗? | ||
| \`\`\` | ||
| --- | ||
| ## 结束探索 | ||
| 没有要求的结束方式。探索可能会: | ||
| - **流入行动**:"准备好开始了吗? /opsx:new 或 /opsx:ff" | ||
| - **导致产出物更新**:"已用这些决定更新 design.md" | ||
| - **仅提供清晰度**:用户得到了他们需要的,继续前进 | ||
| - **稍后继续**:"我们可以随时继续这个话题" | ||
| 当感觉事情变得清晰时,你可以总结: | ||
| \`\`\` | ||
| ## 我们弄清楚了什么 | ||
| **问题**:[清晰的理解] | ||
| **方法**:[如果出现了一个] | ||
| **未决问题**:[如果还有] | ||
| **下一步**(如果准备好了): | ||
| - 创建变更:/opsx:new <name> | ||
| - 快进到任务:/opsx:ff <name> | ||
| - 继续探索:继续交谈 | ||
| \`\`\` | ||
| 但这个总结是可选的。有时思考本身就是价值。 | ||
| --- | ||
| ## 护栏 | ||
| - **不要实施** - 绝不编写代码或实现功能。创建 OpenSpec 产出物是可以的,编写应用程序代码是不行的。 | ||
| - **不要假装理解** - 如果某些事情不清楚,请深入挖掘 | ||
| - **不要匆忙** - 发现是思考时间,而不是任务时间 | ||
| - **不要强加结构** - 让模式自然浮现 | ||
| - **不要自动捕捉** - 提议保存见解,不要直接做 | ||
| - **要可视化** - 一个好的图表胜过千言万语 | ||
| - **要探索代码库** - 将讨论建立在现实基础上 | ||
| - **要质疑假设** - 包括用户的和你自己的`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxExploreCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 探索', | ||
| description: '进入探索模式 - 构思想法、调查问题、澄清需求', | ||
| category: '工作流', | ||
| tags: ['workflow', 'explore', 'experimental', 'thinking'], | ||
| content: `进入探索模式。深入思考。自由可视化。跟随对话的发展。 | ||
| **重要提示:探索模式是为了思考,而不是为了实施。** 你可以阅读文件、搜索代码和调查代码库,但你绝不能编写代码或实现功能。如果用户要求你实现某些内容,请提醒他们先退出探索模式(例如,使用 \`/opsx:new\` 或 \`/opsx:ff\` 开始变更)。如果用户要求,你可以创建 OpenSpec 产出物(提案、设计、规格说明)——这是捕捉思考,而不是实施。 | ||
| **这是一种姿态,而不是一种工作流。** 没有固定的步骤,没有要求的顺序,没有强制性的输出。你是一个思考伙伴,帮助用户进行探索。 | ||
| **输入**:\`/opsx:explore\` 之后的参数是用户想要思考的任何内容。可能是: | ||
| - 一个模糊的想法:"实时协作" | ||
| - 一个具体的问题:"认证系统越来越难维护" | ||
| - 一个变更名称:"add-dark-mode"(在该变更的上下文中探索) | ||
| - 一个比较:"这个场景下该用 Postgres 还是 SQLite" | ||
| - 无(仅进入探索模式) | ||
| --- | ||
| ## 姿态 | ||
| - **好奇而非说教** - 提出自然产生的问题,不要照本宣科 | ||
| - **开放话题而非审问** - 浮现多个有趣的方向,让用户选择产生共鸣的部分。不要把他们限制在单一的提问路径中。 | ||
| - **可视化** - 在有助于澄清思路时大方使用 ASCII 图表 | ||
| - **自适应** - 跟随有趣的话题,当新信息出现时及时转换 | ||
| - **耐心** - 不要急于下结论,让问题的轮廓自然显现 | ||
| - **务实** - 在相关时探索实际的代码库,不要仅仅停留在理论上 | ||
| --- | ||
| ## 你可能做的事情 | ||
| 根据用户提出的内容,你可能会: | ||
| **探索问题空间** | ||
| - 针对他们所说的内容提出澄清性问题 | ||
| - 挑战假设 | ||
| - 重新构建问题 | ||
| - 寻找类比 | ||
| **调查代码库** | ||
| - 绘制与讨论相关的现有架构图 | ||
| - 寻找集成点 | ||
| - 识别已在使用的模式 | ||
| - 揭示隐藏的复杂性 | ||
| **比较选项** | ||
| - 头脑风暴多种方法 | ||
| - 构建比较表 | ||
| - 勾勒权衡 | ||
| - 推荐路径(如果被询问) | ||
| **可视化** | ||
| \`\`\` | ||
| ┌─────────────────────────────────────────┐ | ||
| │ 大量使用 ASCII 图表 │ | ||
| ├─────────────────────────────────────────┤ | ||
| │ │ | ||
| │ ┌────────┐ ┌────────┐ │ | ||
| │ │ 状态 │────────▶│ 状态 │ │ | ||
| │ │ A │ │ B │ │ | ||
| │ └────────┘ └────────┘ │ | ||
| │ │ | ||
| │ 系统图、状态机、数据流、 │ | ||
| │ 架构草图、依赖图、比较表 │ | ||
| │ │ | ||
| └─────────────────────────────────────────┘ | ||
| \`\`\` | ||
| **揭示风险和未知数** | ||
| - 识别可能出错的地方 | ||
| - 发现理解上的差距 | ||
| - 建议进行探针(Spike)或调查 | ||
| --- | ||
| ## OpenSpec 意识 | ||
| 你拥有 OpenSpec 系统的完整上下文。自然地使用它,不要强行使用。 | ||
| ### 检查上下文 | ||
| 开始时,快速检查存在什么: | ||
| \`\`\`bash | ||
| openspec-cn list --json | ||
| \`\`\` | ||
| 这会告诉你: | ||
| - 是否有活跃的变更 | ||
| - 它们的名称、模式和状态 | ||
| - 用户可能正在处理的内容 | ||
| 如果用户提到了特定的变更名称,请阅读其产出物以获取上下文。 | ||
| ### 当不存在变更时 | ||
| 自由思考。当见解清晰时,你可以提议: | ||
| - "这感觉足够扎实,可以开始一个变更了。要我创建一个吗?" | ||
| → 可以过渡到 \`/opsx:new\` 或 \`/opsx:ff\` | ||
| - 或者继续探索 - 没有形式化的压力 | ||
| ### 当存在变更时 | ||
| 如果用户提到变更或你检测到相关变更: | ||
| 1. **阅读现有产出物以获取上下文** | ||
| - \`openspec/changes/<name>/proposal.md\` | ||
| - \`openspec/changes/<name>/design.md\` | ||
| - \`openspec/changes/<name>/tasks.md\` | ||
| - 等等 | ||
| 2. **在对话中自然地引用它们** | ||
| - "你的设计提到使用 Redis,但我们刚刚意识到 SQLite 更合适……" | ||
| - "提案将此范围限定为高级用户,但我们现在考虑面向所有人……" | ||
| 3. **当做出决定时提议捕获** | ||
| | 见解类型 | 捕获位置 | | ||
| |--------------|------------------| | ||
| | 发现新需求 | \`specs/<capability>/spec.md\` | | ||
| | 需求变更 | \`specs/<capability>/spec.md\` | | ||
| | 做出设计决策 | \`design.md\` | | ||
| | 范围变更 | \`proposal.md\` | | ||
| | 识别出新工作 | \`tasks.md\` | | ||
| | 假设失效 | 相关产出物 | | ||
| 提议示例: | ||
| - "这是一个设计决策。要把它记录在 design.md 中吗?" | ||
| - "这是一个新需求。要把它添加到 specs 中吗?" | ||
| - "这改变了范围。要更新 proposal 吗?" | ||
| 4. **由用户决定** - 提议并继续。不要施压。不要自动捕获。 | ||
| --- | ||
| ## 你不必做的事情 | ||
| - 遵循脚本 | ||
| - 每次都问相同的问题 | ||
| - 生成特定的产出物 | ||
| - 得出结论 | ||
| - 如果离题有价值,不必保持在主题上 | ||
| - 简短(这是思考时间) | ||
| --- | ||
| ## 结束探索 | ||
| 没有要求的结束方式。探索可能会: | ||
| - **流入行动**:"准备好开始了吗? \`/opsx:new\` 或 \`/opsx:ff\`" | ||
| - **导致产出物更新**:"已用这些决定更新 design.md" | ||
| - **仅提供清晰度**:用户得到了他们需要的,继续前进 | ||
| - **稍后继续**:"我们可以随时继续这个话题" | ||
| 当感觉事情变得清晰时,你可以总结 - 但这是可选的。有时思考本身就是价值。 | ||
| --- | ||
| ## 护栏 | ||
| - **不要实施** - 绝不编写代码或实现功能。创建 OpenSpec 产出物是可以的,编写应用程序代码是不行的。 | ||
| - **不要假装理解** - 如果某些事情不清楚,请深入挖掘 | ||
| - **不要匆忙** - 发现是思考时间,而不是任务时间 | ||
| - **不要强加结构** - 让模式自然浮现 | ||
| - **不要自动捕捉** - 提议保存见解,不要直接做 | ||
| - **要可视化** - 一个好的图表胜过千言万语 | ||
| - **要探索代码库** - 将讨论建立在现实基础上 | ||
| - **要质疑假设** - 包括用户的和你自己的` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=explore.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate } from '../types.js'; | ||
| export declare function getFeedbackSkillTemplate(): SkillTemplate; | ||
| //# sourceMappingURL=feedback.d.ts.map |
| export function getFeedbackSkillTemplate() { | ||
| return { | ||
| name: 'feedback', | ||
| description: '收集并提交有关 OpenSpec 的用户反馈,包含上下文增强和匿名化。', | ||
| instructions: `帮助用户提交有关 OpenSpec 的反馈。 | ||
| **目标**:引导用户完成收集、增强和提交反馈的过程,同时通过匿名化确保隐私。 | ||
| **过程** | ||
| 1. **从对话中收集上下文** | ||
| - 审查最近的对话历史以获取上下文 | ||
| - 识别正在执行的任务 | ||
| - 记录哪些工作顺利或不顺利 | ||
| - 捕捉具体的摩擦点或赞美 | ||
| 2. **起草增强的反馈** | ||
| - 创建一个清晰、具描述性的标题(单句,无需"反馈:"前缀) | ||
| - 编写包含以下内容的正文: | ||
| - 用户试图做什么 | ||
| - 发生了什么(好或坏) | ||
| - 来自对话的相关上下文 | ||
| - 任何具体的建议或要求 | ||
| 3. **对敏感信息进行匿名化** | ||
| - 将文件路径替换为 \`<path>\` 或通用描述 | ||
| - 将 API 密钥、令牌、机密替换为 \`<redacted>\` | ||
| - 将公司/组织名称替换为 \`<company>\` | ||
| - 将个人姓名替换为 \`<user>\` | ||
| - 将特定的 URL 替换为 \`<url>\`(除非是公开的/相关的) | ||
| - 保留有助于理解问题的技术细节 | ||
| 4. **展示草案以供批准** | ||
| - 向用户展示完整的草案 | ||
| - 清晰地展示标题和正文 | ||
| - 在提交前征求明确的批准 | ||
| - 允许用户要求修改 | ||
| 5. **确认后提交** | ||
| - 使用 \`openspec-cn feedback\` 命令进行提交 | ||
| - 格式:\`openspec-cn feedback "标题" --body "正文内容"\` | ||
| - 该命令将自动添加元数据(版本、平台、时间戳) | ||
| **草案示例** | ||
| \`\`\` | ||
| 标题:产出物工作流中的错误处理需要改进 | ||
| 正文: | ||
| 我正在尝试创建一个新变更,并在产出物工作流中遇到了一个问题。 | ||
| 当我尝试在创建提案后继续时,系统没有清晰地表明我需要先完成 | ||
| 规范(specs)。 | ||
| 建议:在产出物工作流中添加更清晰的错误消息,解释依赖链。 | ||
| 例如:"无法创建 design.md,因为规范尚未完成(0/2 已完成)。" | ||
| 上下文:使用 spec-driven 模式与 <path>/my-project | ||
| \`\`\` | ||
| **匿名化示例** | ||
| 之前: | ||
| \`\`\` | ||
| 正在处理 /Users/john/mycompany/auth-service/src/oauth.ts | ||
| 失败,API 密钥为:sk_live_abc123xyz | ||
| 在 Acme Corp 工作 | ||
| \`\`\` | ||
| 之后: | ||
| \`\`\` | ||
| 正在处理 <path>/oauth.ts | ||
| 失败,API 密钥为:<redacted> | ||
| 在 <company> 工作 | ||
| \`\`\` | ||
| **护栏** | ||
| - 必须在提交前展示完整草案 | ||
| - 必须征求明确的批准 | ||
| - 必须对敏感信息进行匿名化 | ||
| - 允许用户在提交前修改草案 | ||
| - 严禁在未经用户确认的情况下提交 | ||
| - 务必包含相关的技术上下文 | ||
| - 务必保留对话特定的见解 | ||
| **需要用户确认** | ||
| 始终询问: | ||
| \`\`\` | ||
| 这是我起草的反馈: | ||
| 标题:[标题] | ||
| 正文: | ||
| [正文] | ||
| 看起来可以吗?如果您愿意,我可以修改它,或者按原样提交。 | ||
| \`\`\` | ||
| 只有在用户确认后才继续提交。`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| //# sourceMappingURL=feedback.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getFfChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxFfCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=ff-change.d.ts.map |
| export function getFfChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-ff-change', | ||
| description: '快速创建实现所需的所有产出物。当用户想要快速创建实现所需的所有产出物,而不是逐个创建时使用。', | ||
| instructions: `快速完成产出物创建 - 一次性生成开始实现所需的一切。 | ||
| **输入**:用户的请求应包含变更名称(kebab-case)或对他们想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供明确的输入,询问他们想要构建什么** | ||
| 使用 **AskUserQuestion tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 这将在 \`openspec/changes/<name>/\` 创建一个脚手架变更。 | ||
| 3. **获取产出物构建顺序** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以获取: | ||
| - \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`) | ||
| - \`artifacts\`: 所有产出物及其状态和依赖项的列表 | ||
| 4. **按顺序创建产出物直到准备好应用** | ||
| 使用 **TodoWrite tool** 跟踪产出物的进度。 | ||
| 按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先): | ||
| a. **对于每个 \`ready\`(依赖项已满足)的产出物**: | ||
| - 获取指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 指令 JSON 包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:用于输出文件的结构 | ||
| - \`instruction\`:此产出物类型的模式特定指导 | ||
| - \`outputPath\`:写入产出物的位置 | ||
| - \`dependencies\`:已完成的产出物,用于读取上下文 | ||
| - 读取任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构创建产出物文件 | ||
| - 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中 | ||
| - 显示简短进度:"✓ 已创建 <artifact-id>" | ||
| b. **继续直到所有 \`applyRequires\` 产出物完成** | ||
| - 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\` | ||
| - 当所有 \`applyRequires\` 产出物完成时停止 | ||
| c. **如果产出物需要用户输入**(上下文不清楚): | ||
| - 使用 **AskUserQuestion tool** 进行澄清 | ||
| - 然后继续创建 | ||
| 5. **显示最终状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 完成所有产出物后,总结: | ||
| - 变更名称和位置 | ||
| - 已创建产出物的列表及简要描述 | ||
| - 准备就绪:"所有产出物已创建!准备好实现。" | ||
| - 提示:"运行 \`/opsx:apply\` 或要求我实现以开始处理任务。" | ||
| **产出物创建指南** | ||
| - 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段 | ||
| - 模式定义了每个产出物应包含的内容 - 遵循它 | ||
| - 在创建新产出物之前阅读依赖产出物以获取上下文 | ||
| - 使用 \`template\` 作为输出文件的结构 - 填充其各个部分 | ||
| - **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容 | ||
| - 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中 | ||
| - 这些引导你编写内容,但不应出现在输出中 | ||
| **护栏** | ||
| - 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义) | ||
| - 在创建新产出物之前始终阅读依赖产出物 | ||
| - 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头 | ||
| - 如果同名变更已存在,建议继续处理该变更 | ||
| - 在继续下一个之前,验证写入后每个产出物文件是否存在`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxFfCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 快进', | ||
| description: '一键创建变更并生成实现所需的所有产出物', | ||
| category: '工作流', | ||
| tags: ['workflow', 'artifacts', 'experimental'], | ||
| content: `快速完成产出物创建 - 生成开始实现所需的一切。 | ||
| **输入**:\`/opsx:ff\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供输入,询问他们想要构建什么** | ||
| 使用 **AskUserQuestion tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 这将在 \`openspec/changes/<name>/\` 创建一个脚手架变更。 | ||
| 3. **获取产出物构建顺序** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以获取: | ||
| - \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`) | ||
| - \`artifacts\`: 所有产出物及其状态和依赖项的列表 | ||
| 4. **按顺序创建产出物直到准备好应用** | ||
| 使用 **TodoWrite tool** 跟踪产出物的进度。 | ||
| 按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先): | ||
| a. **对于每个 \`ready\`(依赖项已满足)的产出物**: | ||
| - 获取指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 指令 JSON 包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:用于输出文件的结构 | ||
| - \`instruction\`:此产出物类型的模式特定指导 | ||
| - \`outputPath\`:写入产出物的位置 | ||
| - \`dependencies\`:已完成的产出物,用于读取上下文 | ||
| - 读取任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构创建产出物文件 | ||
| - 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中 | ||
| - 显示简短进度:"✓ 已创建 <artifact-id>" | ||
| b. **继续直到所有 \`applyRequires\` 产出物完成** | ||
| - 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\` | ||
| - 当所有 \`applyRequires\` 产出物完成时停止 | ||
| c. **如果产出物需要用户输入**(上下文不清楚): | ||
| - 使用 **AskUserQuestion tool** 进行澄清 | ||
| - 然后继续创建 | ||
| 5. **显示最终状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 完成所有产出物后,总结: | ||
| - 变更名称和位置 | ||
| - 已创建产出物的列表及简要描述 | ||
| - 准备就绪:"所有产出物已创建!准备好实现。" | ||
| - 提示:"运行 \`/opsx:apply\` 以开始实现。" | ||
| **产出物创建指南** | ||
| - 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段 | ||
| - Schema 定义了每个产出物应包含的内容 - 遵循它 | ||
| - 在创建新产出物之前阅读依赖产出物以获取上下文 | ||
| - 使用 \`template\` 作为起点,根据上下文填写 | ||
| **护栏** | ||
| - 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义) | ||
| - 在创建新产出物之前始终阅读依赖产出物 | ||
| - 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头 | ||
| - 如果同名变更已存在,询问用户是否要继续它或创建一个新的 | ||
| - 在继续下一个之前,验证写入后每个产出物文件是否存在` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=ff-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getNewChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxNewCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=new-change.d.ts.map |
| export function getNewChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-new-change', | ||
| description: '使用实验性的产出物工作流启动一个新的 OpenSpec 变更。当用户想要通过结构化的分步方法创建新功能、修复或修改时使用。', | ||
| instructions: `使用实验性的产出物驱动方法启动新变更。 | ||
| **输入**:用户的请求应当包含变更名称(kebab-case)或对想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供明确的输入,询问用户想要构建什么** | ||
| 使用 **AskUserQuestion Tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **确定工作流 Schema** | ||
| 除非用户明确要求不同的工作流,否则使用默认 Schema(省略 \`--schema\`)。 | ||
| **仅当用户提到以下内容时才使用不同的模式:** | ||
| - 特定模式名称 → 使用 \`--schema <name>\` | ||
| - "显示工作流" 或 "有哪些工作流" → 运行 \`openspec-cn schemas --json\` 并让他们选择 | ||
| **否则**:省略 \`--schema\` 以使用默认值。 | ||
| 3. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 仅当用户请求特定工作流时才添加 \`--schema <name>\`。 | ||
| 这将在 \`openspec/changes/<name>/\` 下使用所选 Schema 创建一个脚手架变更。 | ||
| 4. **显示产出物状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| 这会显示哪些产出物需要创建,以及哪些已就绪(依赖项已满足)。 | ||
| 5. **获取第一个产出物的指令** | ||
| 第一个产出物取决于所使用的 schema(例如:spec-driven 通常先生成 \`proposal\`)。 | ||
| 检查 status 输出,找到第一个状态为 "ready" 的产出物。 | ||
| \`\`\`bash | ||
| openspec-cn instructions <first-artifact-id> --change "<name>" | ||
| \`\`\` | ||
| 这会输出创建第一个产出物所需的模板和上下文。 | ||
| 6. **停止并等待用户指示** | ||
| **输出** | ||
| 完成上述步骤后,进行总结: | ||
| - 变更名称和位置 | ||
| - 正在使用的 Schema/工作流及其产出物顺序 | ||
| - 当前状态(0/N 个产出物已完成) | ||
| - 第一个产出物的模板 | ||
| - 提示:"准备好创建第一个产出物了吗?请描述此变更的内容,我将为您起草,或者要求我继续。" | ||
| **护栏** | ||
| - 不要立即创建任何产出物 —— 仅显示指令 | ||
| - 不要跳过显示第一个产出物模板的步骤 | ||
| - 如果名称无效(非 kebab-case),请求有效的名称 | ||
| - 如果同名变更已存在,建议继续处理该变更 | ||
| - 如果使用非默认工作流,请传递 --schema`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxNewCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 新建', | ||
| description: '使用实验性的产出物工作流 (OPSX) 启动新变更', | ||
| category: '工作流', | ||
| tags: ['workflow', 'artifacts', 'experimental'], | ||
| content: `使用实验性的产出物驱动方法启动新变更。 | ||
| **输入**:\`/opsx:new\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供输入,询问他们想要构建什么** | ||
| 使用 **AskUserQuestion tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **确定工作流 Schema** | ||
| 除非用户明确要求不同的工作流,否则使用默认 Schema(省略 \`--schema\`)。 | ||
| **仅当用户提到以下内容时才使用不同的模式:** | ||
| - 特定模式名称 → 使用 \`--schema <name>\` | ||
| - "显示工作流" 或 "有哪些工作流" → 运行 \`openspec-cn schemas --json\` 并让他们选择 | ||
| **否则**:省略 \`--schema\` 以使用默认值。 | ||
| 3. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 仅当用户请求特定工作流时才添加 \`--schema <name>\`。 | ||
| 这将在 \`openspec/changes/<name>/\` 下使用所选 Schema 创建一个脚手架变更。 | ||
| 4. **显示产出物状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| 这会显示哪些产出物需要创建,以及哪些已就绪(依赖项已满足)。 | ||
| 5. **获取第一个产出物的指令** | ||
| 第一个产出物取决于 Schema。检查状态输出,找到第一个状态为 "ready" 的产出物。 | ||
| \`\`\`bash | ||
| openspec-cn instructions <first-artifact-id> --change "<name>" | ||
| \`\`\` | ||
| 这会输出创建第一个产出物所需的模板和上下文。 | ||
| 6. **停止并等待用户指示** | ||
| **输出** | ||
| 完成上述步骤后,进行总结: | ||
| - 变更名称和位置 | ||
| - 正在使用的 Schema/工作流及其产出物顺序 | ||
| - 当前状态(0/N 个产出物已完成) | ||
| - 第一个产出物的模板 | ||
| - 提示:"准备好创建第一个产出物了吗?运行 \`/opsx:continue\` 或描述此变更的内容,我将为您起草。" | ||
| **护栏** | ||
| - 不要立即创建任何产出物 —— 仅显示指令 | ||
| - 不要跳过显示第一个产出物模板的步骤 | ||
| - 如果名称无效(非 kebab-case),请求有效的名称 | ||
| - 如果同名变更已存在,建议使用 \`/opsx:continue\` 代替 | ||
| - 如果使用非默认工作流,请传递 --schema` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=new-change.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getOnboardSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxOnboardCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=onboard.d.ts.map |
| export function getOnboardSkillTemplate() { | ||
| return { | ||
| name: 'openspec-onboard', | ||
| description: 'OpenSpec 引导式入门 - 通过讲解与在真实代码库中动手,走完一个完整的工作流周期。', | ||
| instructions: getOnboardInstructions(), | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| function getOnboardInstructions() { | ||
| return `引导用户完成他们的第一个完整OpenSpec工作流周期。这是一个教学体验——你将在他们的代码库中完成实际工作,同时解释每个步骤。 | ||
| --- | ||
| ## 准备阶段 | ||
| 开始前,检查OpenSpec CLI是否已安装: | ||
| \`\`\`bash | ||
| # Unix/macOS | ||
| openspec-cn --version 2>&1 || echo "CLI_NOT_INSTALLED" | ||
| # Windows (PowerShell) | ||
| # if (Get-Command openspec-cn -ErrorAction SilentlyContinue) { openspec-cn --version } else { echo "CLI_NOT_INSTALLED" } | ||
| \`\`\` | ||
| **如果CLI未安装:** | ||
| > OpenSpec CLI 未安装。请先安装它,然后返回 \`/opsx:onboard\`。 | ||
| 如果未安装,请在此停止。 | ||
| --- | ||
| ## 阶段1:欢迎 | ||
| 显示: | ||
| \`\`\` | ||
| ## 欢迎使用OpenSpec! | ||
| 我将引导您完成一个完整的变更周期——从想法到实现——使用您代码库中的真实任务。在此过程中,您将通过实践学习工作流程。 | ||
| **我们将要做的事情:** | ||
| 1. 在您的代码库中选择一个小的真实任务 | ||
| 2. 简要探索问题 | ||
| 3. 创建一个变更(我们工作的容器) | ||
| 4. 构建产出物:提案 → 规格说明 → 设计 → 任务 | ||
| 5. 实现任务 | ||
| 6. 归档完成的变更 | ||
| **时间:** ~15-20分钟 | ||
| 让我们开始寻找要处理的内容。 | ||
| \`\`\` | ||
| --- | ||
| ## 阶段2:任务选择 | ||
| ### 代码库分析 | ||
| 扫描代码库寻找小的改进机会。寻找: | ||
| 1. **TODO/FIXME注释** - 在代码文件中搜索 \`TODO\`、\`FIXME\`、\`HACK\`、\`XXX\` | ||
| 2. **缺少错误处理** - 吞没错误的 \`catch\` 块,没有try-catch的风险操作 | ||
| 3. **没有测试的函数** - 交叉引用 \`src/\` 和测试目录 | ||
| 4. **类型问题** - TypeScript文件中的 \`any\` 类型(\`: any\`、\`as any\`) | ||
| 5. **调试产出物** - 非调试代码中的 \`console.log\`、\`console.debug\`、\`debugger\` 语句 | ||
| 6. **缺少验证** - 没有验证的用户输入处理程序 | ||
| 同时检查最近的git活动: | ||
| \`\`\`bash | ||
| # Unix/macOS | ||
| git log --oneline -10 2>/dev/null || echo "No git history" | ||
| # Windows (PowerShell) | ||
| # git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } | ||
| \`\`\` | ||
| ### 提出建议 | ||
| 根据您的分析,提出3-4个具体建议: | ||
| \`\`\` | ||
| ## 任务建议 | ||
| 基于扫描您的代码库,以下是一些好的入门任务: | ||
| **1. [最有希望的任务]** | ||
| 位置:\`src/path/to/file.ts:42\` | ||
| 范围:~1-2个文件,~20-30行 | ||
| 为什么好:[简要原因] | ||
| **2. [第二个任务]** | ||
| 位置:\`src/another/file.ts\` | ||
| 范围:~1个文件,~15行 | ||
| 为什么好:[简要原因] | ||
| **3. [第三个任务]** | ||
| 位置:[位置] | ||
| 范围:[估计] | ||
| 为什么好:[简要原因] | ||
| **4. 其他内容?** | ||
| 告诉我您想要处理什么。 | ||
| 哪个任务让您感兴趣?(选择一个数字或描述您自己的) | ||
| \`\`\` | ||
| **如果未找到任何内容:** 回退到询问用户想要构建什么: | ||
| > 我在您的代码库中没有找到明显的快速改进机会。您一直想要添加或修复什么小东西? | ||
| ### 范围护栏 | ||
| 如果用户选择或描述的内容太大(主要功能,多天工作): | ||
| \`\`\` | ||
| 这是一个有价值的任务,但对于您的第一次OpenSpec体验来说可能太大了。 | ||
| 对于学习工作流程,越小越好——它让您能够看到完整周期而不会陷入实现细节。 | ||
| **选项:** | ||
| 1. **切分成更小的部分** - [他们的任务]中最小的有用部分是什么?也许只是[具体切片]? | ||
| 2. **选择其他内容** - 其他建议之一,或不同的任务? | ||
| 3. **无论如何都做** - 如果您真的想处理这个,我们可以做。只是要知道需要更长时间。 | ||
| 您更喜欢哪种? | ||
| \`\`\` | ||
| 如果用户坚持,让他们覆盖——这是一个软护栏。 | ||
| --- | ||
| ## 阶段3:探索演示 | ||
| 一旦选择了任务,简要演示探索模式: | ||
| \`\`\` | ||
| 在我们创建变更之前,让我快速向您展示**探索模式**——这是在承诺方向之前思考问题的方式。 | ||
| \`\`\` | ||
| 花1-2分钟调查相关代码: | ||
| - 阅读涉及的文件 | ||
| - 如果需要,绘制快速ASCII图表 | ||
| - 注意任何考虑事项 | ||
| \`\`\` | ||
| ## 快速探索 | ||
| [您的简要分析——您发现了什么,任何考虑事项] | ||
| ┌─────────────────────────────────────────┐ | ||
| │ [可选:如果有帮助的ASCII图表] │ | ||
| └─────────────────────────────────────────┘ | ||
| 探索模式(\`/opsx:explore\`)用于这种思考——在实现之前进行调查。您可以在需要思考问题时随时使用它。 | ||
| 现在让我们创建一个变更来保存我们的工作。 | ||
| \`\`\` | ||
| **暂停** - 等待用户确认后再继续。 | ||
| --- | ||
| ## 阶段4:创建变更 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 创建变更 | ||
| OpenSpec中的"变更"是围绕一项工作的所有思考和规划的容器。它位于 \`openspec/changes/<name>/\` 中,保存您的产出物——提案、规格说明、设计、任务。 | ||
| 让我为我们的任务创建一个。 | ||
| \`\`\` | ||
| **执行:** 使用派生的kebab-case名称创建变更: | ||
| \`\`\`bash | ||
| openspec-cn new change "<derived-name>" | ||
| \`\`\` | ||
| **显示:** | ||
| \`\`\` | ||
| 已创建:\`openspec/changes/<name>/\` | ||
| 文件夹结构: | ||
| \`\`\` | ||
| openspec/changes/<name>/ | ||
| ├── proposal.md ← 为什么我们要做这个(空,我们将填充它) | ||
| ├── design.md ← 我们将如何构建它(空) | ||
| ├── specs/ ← 详细需求(空) | ||
| └── tasks.md ← 实现检查清单(空) | ||
| \`\`\` | ||
| 现在让我们填充第一个产出物——提案。 | ||
| \`\`\` | ||
| --- | ||
| ## 阶段5:提案 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 提案 | ||
| 提案捕获**为什么**我们要进行此变更以及**什么**在高层级上涉及。这是工作的"电梯演讲"。 | ||
| 我将根据我们的任务起草一个。 | ||
| \`\`\` | ||
| **执行:** 起草提案内容(暂时不保存): | ||
| \`\`\` | ||
| 这是一个草案提案: | ||
| --- | ||
| ## 为什么 | ||
| [1-2句话解释问题/机会] | ||
| ## 什么变化 | ||
| [将要不同的要点] | ||
| ## 能力 | ||
| ### 新能力 | ||
| - \`<能力名称>\`: [简要描述] | ||
| ### 修改的能力 | ||
| <!-- 如果修改现有行为 --> | ||
| ## 影响 | ||
| - \`src/path/to/file.ts\`: [什么变化] | ||
| - [其他文件如果适用] | ||
| --- | ||
| 这能捕捉意图吗?我可以在保存前调整。 | ||
| \`\`\` | ||
| **暂停** - 等待用户批准/反馈。 | ||
| 批准后,保存提案: | ||
| \`\`\`bash | ||
| openspec-cn instructions proposal --change "<name>" --json | ||
| \`\`\` | ||
| 然后将内容写入 \`openspec/changes/<name>/proposal.md\`。 | ||
| \`\`\` | ||
| 提案已保存。这是您的"为什么"文档——您随时可以回来在理解发展时完善它。 | ||
| 接下来:规格说明。 | ||
| \`\`\` | ||
| --- | ||
| ## 阶段6:规格说明 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 规格说明 | ||
| 规格说明以精确、可测试的术语定义**什么**我们正在构建。它们使用需求/场景格式,使预期行为清晰明了。 | ||
| 对于像这样的小任务,我们可能只需要一个规格说明文件。 | ||
| \`\`\` | ||
| **执行:** 创建规格说明文件: | ||
| \`\`\`bash | ||
| # Unix/macOS | ||
| mkdir -p openspec/changes/<name>/specs/<capability-name> | ||
| # Windows (PowerShell) | ||
| # New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>" | ||
| \`\`\` | ||
| 起草规格说明内容: | ||
| \`\`\` | ||
| 这是规格说明: | ||
| --- | ||
| ## 新增需求 | ||
| ### 需求: <名称> | ||
| <系统应该做什么的描述> | ||
| #### 场景: <场景名称> | ||
| - **当** <触发条件> | ||
| - **那么** <预期结果> | ||
| - **并且** <如果需要额外结果> | ||
| --- | ||
| 这种格式——当/那么/并且——使需求可测试。您可以将它们字面地读作测试用例。 | ||
| \`\`\` | ||
| 保存到 \`openspec/changes/<name>/specs/<capability>/spec.md\`。 | ||
| --- | ||
| ## 阶段7:设计 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 设计 | ||
| 设计捕获**如何**我们将构建它——技术决策、权衡、方法。 | ||
| 对于小变更,这可能很简短。没关系——不是每个变更都需要深入的设计讨论。 | ||
| \`\`\` | ||
| **执行:** 起草design.md: | ||
| \`\`\` | ||
| 这是设计: | ||
| --- | ||
| ## 上下文 | ||
| [关于当前状态的简要上下文] | ||
| ## 目标/非目标 | ||
| **目标:** | ||
| - [我们试图实现什么] | ||
| **非目标:** | ||
| - [明确超出范围的内容] | ||
| ## 决策 | ||
| ### 决策1:[关键决策] | ||
| [方法解释和理由] | ||
| --- | ||
| 对于小任务,这捕获了关键决策而不过度工程化。 | ||
| \`\`\` | ||
| 保存到 \`openspec/changes/<name>/design.md\`。 | ||
| --- | ||
| ## 阶段8:任务 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 任务 | ||
| 最后,我们将工作分解为实现任务——驱动应用阶段的复选框。 | ||
| 这些应该小、清晰且逻辑顺序。 | ||
| \`\`\` | ||
| **执行:** 基于规格说明和设计生成任务: | ||
| \`\`\` | ||
| 这是实现任务: | ||
| --- | ||
| ## 1. [类别或文件] | ||
| - [ ] 1.1 [具体任务] | ||
| - [ ] 1.2 [具体任务] | ||
| ## 2. 验证 | ||
| - [ ] 2.1 [验证步骤] | ||
| --- | ||
| 每个复选框成为应用阶段的工作单元。准备好实现了吗? | ||
| \`\`\` | ||
| **暂停** - 等待用户确认他们准备好实现。 | ||
| 保存到 \`openspec/changes/<name>/tasks.md\`。 | ||
| --- | ||
| ## 阶段9:应用(实现) | ||
| **解释:** | ||
| \`\`\` | ||
| ## 实现 | ||
| 现在我们实现每个任务,在过程中勾选它们。我将宣布每个任务,并偶尔注意规格说明/设计如何影响方法。 | ||
| \`\`\` | ||
| **执行:** 对于每个任务: | ||
| 1. 宣布:"正在处理任务N:[描述]" | ||
| 2. 在代码库中实现变更 | ||
| 3. 自然地引用规格说明/设计:"规格说明说X,所以我做Y" | ||
| 4. 在tasks.md中标记完成:\`- [ ]\` → \`- [x]\` | ||
| 5. 简要状态:"✓ 任务N完成" | ||
| 保持叙述轻量——不要过度解释每一行代码。 | ||
| 所有任务后: | ||
| \`\`\` | ||
| ## 实现完成 | ||
| 所有任务完成: | ||
| - [x] 任务1 | ||
| - [x] 任务2 | ||
| - [x] ... | ||
| 变更已实现!还有一个步骤——让我们归档它。 | ||
| \`\`\` | ||
| --- | ||
| ## 阶段10:归档 | ||
| **解释:** | ||
| \`\`\` | ||
| ## 归档 | ||
| 当变更完成时,我们归档它。这将把它从 \`openspec/changes/\` 移动到 \`openspec/changes/archive/YYYY-MM-DD-<name>/\`。 | ||
| 归档的变更成为您项目的决策历史——您随时可以找到它们来理解为什么某物以某种方式构建。 | ||
| \`\`\` | ||
| **执行:** | ||
| \`\`\`bash | ||
| openspec-cn archive "<name>" | ||
| \`\`\` | ||
| **显示:** | ||
| \`\`\` | ||
| 已归档到:\`openspec/changes/archive/YYYY-MM-DD-<name>/\` | ||
| 变更现在是您项目历史的一部分。代码在您的代码库中,决策记录被保留。 | ||
| \`\`\` | ||
| --- | ||
| ## 阶段11:回顾与下一步 | ||
| \`\`\` | ||
| ## 恭喜! | ||
| 您刚刚完成了一个完整的OpenSpec周期: | ||
| 1. **探索** - 思考问题 | ||
| 2. **新建** - 创建变更容器 | ||
| 3. **提案** - 捕获为什么 | ||
| 4. **规格说明** - 详细定义什么 | ||
| 5. **设计** - 决定如何 | ||
| 6. **任务** - 分解为步骤 | ||
| 7. **应用** - 实现工作 | ||
| 8. **归档** - 保留记录 | ||
| 同样的节奏适用于任何大小的变更——小修复或主要功能。 | ||
| --- | ||
| ## 命令参考 | ||
| **核心工作流:** | ||
| | 命令 | 做什么 | | ||
| |---------|--------------| | ||
| | \`/opsx:propose\` | 创建变更并生成所有产出物 | | ||
| | \`/opsx:explore\` | 在工作之前/期间思考问题 | | ||
| | \`/opsx:apply\` | 实现变更中的任务 | | ||
| | \`/opsx:archive\` | 归档完成的变更 | | ||
| **其他命令:** | ||
| | 命令 | 做什么 | | ||
| |---------|--------------| | ||
| | \`/opsx:new\` | 开始新变更,逐步通过产出物 | | ||
| | \`/opsx:continue\` | 继续处理现有变更 | | ||
| | \`/opsx:ff\` | 快进:一次创建所有产出物 | | ||
| | \`/opsx:verify\` | 验证实现是否匹配产出物 | | ||
| --- | ||
| ## 下一步是什么? | ||
| 尝试 \`/opsx:propose\` 在您实际想要构建的内容上。您现在掌握了节奏! | ||
| \`\`\` | ||
| --- | ||
| ## 优雅退出处理 | ||
| ### 用户想要中途停止 | ||
| 如果用户说他们需要停止、想要暂停或似乎不投入: | ||
| \`\`\` | ||
| 没问题!您的变更保存在 \`openspec/changes/<name>/\`。 | ||
| 要在以后继续: | ||
| - \`/opsx:continue <name>\` - 恢复产出物创建 | ||
| - \`/opsx:apply <name>\` - 跳转到实现(如果任务存在) | ||
| 工作不会丢失。随时回来。 | ||
| \`\`\` | ||
| 优雅退出,不施加压力。 | ||
| ### 用户只想要命令参考 | ||
| 如果用户说他们只想看命令或跳过教程: | ||
| \`\`\` | ||
| ## OpenSpec快速参考 | ||
| **核心工作流:** | ||
| | 命令 | 做什么 | | ||
| |---------|--------------| | ||
| | \`/opsx:propose <name>\` | 创建变更并生成所有产出物 | | ||
| | \`/opsx:explore\` | 思考问题(无代码更改) | | ||
| | \`/opsx:apply <name>\` | 实现任务 | | ||
| | \`/opsx:archive <name>\` | 完成后归档 | | ||
| **其他命令:** | ||
| | 命令 | 做什么 | | ||
| |---------|--------------| | ||
| | \`/opsx:new <name>\` | 开始新变更,逐步进行 | | ||
| | \`/opsx:continue <name>\` | 继续现有变更 | | ||
| | \`/opsx:ff <name>\` | 快进:一次创建所有产出物 | | ||
| | \`/opsx:verify <name>\` | 验证实现 | | ||
| 尝试 \`/opsx:propose\` 开始您的第一个变更。 | ||
| \`\`\` | ||
| 优雅退出。 | ||
| --- | ||
| ## 护栏 | ||
| - **遵循解释→执行→显示→暂停模式**在关键转换点(探索后、提案草案后、任务后、归档后) | ||
| - **在实现期间保持叙述轻量**——教学而不说教 | ||
| - **不要跳过阶段**即使变更很小——目标是教学工作流程 | ||
| - **在标记点暂停等待确认**,但不要过度暂停 | ||
| - **优雅处理退出**——从不施压用户继续 | ||
| - **使用真实代码库任务**——不模拟或使用虚假示例 | ||
| - **温和调整范围**——引导向更小任务但尊重用户选择`; | ||
| } | ||
| export function getOpsxOnboardCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 入门', | ||
| description: '引导式入门 - 通过完整的OpenSpec工作流周期进行讲解', | ||
| category: '工作流', | ||
| tags: ['workflow', 'onboarding', 'tutorial', 'learning'], | ||
| content: getOnboardInstructions(), | ||
| }; | ||
| } | ||
| //# sourceMappingURL=onboard.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getOpsxProposeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxProposeCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=propose.d.ts.map |
| export function getOpsxProposeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-propose', | ||
| description: '一步提案新变更并生成所有产出物。当用户想要快速描述他们想要构建的内容,并获得包含设计、规格说明和任务的完整提案以准备实现时使用。', | ||
| instructions: `提案新变更 - 一步创建变更并生成所有产出物。 | ||
| 我将创建一个包含以下产出物的变更: | ||
| - proposal.md(什么和为什么) | ||
| - design.md(如何) | ||
| - tasks.md(实现步骤) | ||
| 准备好实现后,运行 /opsx:apply | ||
| --- | ||
| **输入**:用户的请求应包含变更名称(kebab-case)或对他们想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供明确的输入,询问他们想要构建什么** | ||
| 使用 **AskUserQuestion tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 这将在 \`openspec/changes/<name>/\` 创建一个带有 \`.openspec.yaml\` 的脚手架变更。 | ||
| 3. **获取产出物构建顺序** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以获取: | ||
| - \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`) | ||
| - \`artifacts\`: 所有产出物及其状态和依赖项的列表 | ||
| 4. **按顺序创建产出物直到准备好应用** | ||
| 使用 **TodoWrite tool** 跟踪产出物的进度。 | ||
| 按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先): | ||
| a. **对于每个 \`ready\`(依赖项已满足)的产出物**: | ||
| - 获取指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 指令 JSON 包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:用于输出文件的结构 | ||
| - \`instruction\`:此产出物类型的 Schema 特定指导 | ||
| - \`outputPath\`:写入产出物的位置 | ||
| - \`dependencies\`:已完成的产出物,用于读取上下文 | ||
| - 读取任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构创建产出物文件 | ||
| - 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中 | ||
| - 显示简短进度:"✓ 已创建 <artifact-id>" | ||
| b. **继续直到所有 \`applyRequires\` 产出物完成** | ||
| - 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\` | ||
| - 当所有 \`applyRequires\` 产出物完成时停止 | ||
| c. **如果产出物需要用户输入**(上下文不清楚): | ||
| - 使用 **AskUserQuestion tool** 进行澄清 | ||
| - 然后继续创建 | ||
| 5. **显示最终状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 完成所有产出物后,总结: | ||
| - 变更名称和位置 | ||
| - 已创建产出物的列表及简要描述 | ||
| - 准备就绪:"所有产出物已创建!准备好实现。" | ||
| - 提示:"运行 \`/opsx:apply\` 或要求我实现以开始处理任务。" | ||
| **产出物创建指南** | ||
| - 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段 | ||
| - Schema 定义了每个产出物应包含的内容 - 遵循它 | ||
| - 在创建新产出物之前阅读依赖产出物以获取上下文 | ||
| - 使用 \`template\` 作为输出文件的结构 - 填充其各个部分 | ||
| - **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容 | ||
| - 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中 | ||
| - 这些引导你编写内容,但不应出现在输出中 | ||
| **护栏** | ||
| - 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义) | ||
| - 在创建新产出物之前始终阅读依赖产出物 | ||
| - 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头 | ||
| - 如果同名变更已存在,询问用户是否要继续它或创建一个新的 | ||
| - 在继续下一个之前,验证写入后每个产出物文件是否存在`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxProposeCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 提案', | ||
| description: '提案新变更 - 一步创建并生成所有产出物', | ||
| category: '工作流', | ||
| tags: ['workflow', 'artifacts', 'experimental'], | ||
| content: `提案新变更 - 一步创建变更并生成所有产出物。 | ||
| 我将创建一个包含以下产出物的变更: | ||
| - proposal.md(什么和为什么) | ||
| - design.md(如何) | ||
| - tasks.md(实现步骤) | ||
| 准备好实现后,运行 /opsx:apply | ||
| --- | ||
| **输入**:\`/opsx:propose\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。 | ||
| **步骤** | ||
| 1. **如果没有提供输入,询问他们想要构建什么** | ||
| 使用 **AskUserQuestion tool**(开放式,无预设选项)询问: | ||
| > "您想要处理什么变更?请描述您想要构建或修复的内容。" | ||
| 根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。 | ||
| **重要提示**:在不了解用户想要构建什么的情况下,请勿继续。 | ||
| 2. **创建变更目录** | ||
| \`\`\`bash | ||
| openspec-cn new change "<name>" | ||
| \`\`\` | ||
| 这将在 \`openspec/changes/<name>/\` 创建一个带有 \`.openspec.yaml\` 的脚手架变更。 | ||
| 3. **获取产出物构建顺序** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以获取: | ||
| - \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`) | ||
| - \`artifacts\`: 所有产出物及其状态和依赖项的列表 | ||
| 4. **按顺序创建产出物直到准备好应用** | ||
| 使用 **TodoWrite tool** 跟踪产出物的进度。 | ||
| 按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先): | ||
| a. **对于每个 \`ready\`(依赖项已满足)的产出物**: | ||
| - 获取指令: | ||
| \`\`\`bash | ||
| openspec-cn instructions <artifact-id> --change "<name>" --json | ||
| \`\`\` | ||
| - 指令 JSON 包括: | ||
| - \`context\`:项目背景(对你的约束 - 不要包含在输出中) | ||
| - \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中) | ||
| - \`template\`:用于输出文件的结构 | ||
| - \`instruction\`:此产出物类型的 Schema 特定指导 | ||
| - \`outputPath\`:写入产出物的位置 | ||
| - \`dependencies\`:已完成的产出物,用于读取上下文 | ||
| - 读取任何已完成的依赖文件以获取上下文 | ||
| - 使用 \`template\` 作为结构创建产出物文件 | ||
| - 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中 | ||
| - 显示简短进度:"✓ 已创建 <artifact-id>" | ||
| b. **继续直到所有 \`applyRequires\` 产出物完成** | ||
| - 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\` | ||
| - 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\` | ||
| - 当所有 \`applyRequires\` 产出物完成时停止 | ||
| c. **如果产出物需要用户输入**(上下文不清楚): | ||
| - 使用 **AskUserQuestion tool** 进行澄清 | ||
| - 然后继续创建 | ||
| 5. **显示最终状态** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" | ||
| \`\`\` | ||
| **输出** | ||
| 完成所有产出物后,总结: | ||
| - 变更名称和位置 | ||
| - 已创建产出物的列表及简要描述 | ||
| - 准备就绪:"所有产出物已创建!准备好实现。" | ||
| - 提示:"运行 \`/opsx:apply\` 开始实现。" | ||
| **产出物创建指南** | ||
| - 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段 | ||
| - Schema 定义了每个产出物应包含的内容 - 遵循它 | ||
| - 在创建新产出物之前阅读依赖产出物以获取上下文 | ||
| - 使用 \`template\` 作为输出文件的结构 - 填充其各个部分 | ||
| - **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容 | ||
| - 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中 | ||
| - 这些引导你编写内容,但不应出现在输出中 | ||
| **护栏** | ||
| - 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义) | ||
| - 在创建新产出物之前始终阅读依赖产出物 | ||
| - 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头 | ||
| - 如果同名变更已存在,询问用户是否要继续它或创建一个新的 | ||
| - 在继续下一个之前,验证写入后每个产出物文件是否存在` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=propose.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getSyncSpecsSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxSyncCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=sync-specs.d.ts.map |
| export function getSyncSpecsSkillTemplate() { | ||
| return { | ||
| name: 'openspec-sync-specs', | ||
| description: '将变更中的增量规范同步到主规范。当用户想要使用增量规范中的更改更新主规范,而不归档该变更时使用。', | ||
| instructions: `将变更中的增量规范同步到主规范。 | ||
| 这是一个 **Agent 驱动** 的操作 - 你将读取增量规范并直接编辑主规范以应用更改。这允许智能合并(例如,添加场景而不复制整个需求)。 | ||
| **输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 显示具有增量规范(在 \`specs/\` 目录下)的变更。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **查找增量规范** | ||
| 在 \`openspec/changes/<name>/specs/*/spec.md\` 中查找增量规范文件。 | ||
| 每个增量规范文件包含如下部分: | ||
| - \`## 新增需求\` - 要添加的新需求 | ||
| - \`## 修改需求\` - 对现有需求的更改 | ||
| - \`## 移除需求\` - 要移除的需求 | ||
| - \`## 重命名需求\` - 要重命名的需求(从/到 格式) | ||
| 如果没有找到增量规范,通知用户并停止。 | ||
| 3. **对于每个增量规范,将更改应用到主规范** | ||
| 对于在 \`openspec/changes/<name>/specs/<capability>/spec.md\` 处具有增量规范的每个 capability: | ||
| a. **阅读增量规范** 以了解预期的更改 | ||
| b. **阅读主规范** 于 \`openspec/specs/<capability>/spec.md\`(可能尚不存在) | ||
| c. **智能地应用更改**: | ||
| **新增需求:** | ||
| - 如果需求在主规范中不存在 → 添加它 | ||
| - 如果需求已存在 → 更新它以匹配(视为隐式 MODIFIED) | ||
| **修改需求:** | ||
| - 在主规范中找到该需求 | ||
| - 应用更改 - 这可能是: | ||
| - 添加新场景(不需要复制现有场景) | ||
| - 修改现有场景 | ||
| - 更改需求描述 | ||
| - 保留增量中未提及的场景/内容 | ||
| **移除需求:** | ||
| - 从主规范中移除整个需求块 | ||
| **重命名需求:** | ||
| - 找到 FROM 需求,重命名为 TO | ||
| d. **创建新主规范** 如果 capability 尚不存在: | ||
| - 创建 \`openspec/specs/<capability>/spec.md\` | ||
| - 添加 目的 部分(可以简短,标记为 待定) | ||
| - 添加 需求 部分以及 新增需求 | ||
| 4. **显示摘要** | ||
| 应用所有更改后,总结: | ||
| - 哪些 capability 已更新 | ||
| - 做了什么更改(需求添加/修改/移除/重命名) | ||
| **增量规范格式参考** | ||
| \`\`\`markdown | ||
| ## 新增需求 | ||
| ### 需求: 新功能 | ||
| 系统 应当 实现新的能力。 | ||
| #### 场景: 基本场景 | ||
| - **当** 用户执行 X | ||
| - **那么** 系统执行 Y | ||
| ## 修改需求 | ||
| ### 需求: 现有功能 | ||
| #### 场景: 需要新增的场景 | ||
| - **当** 用户执行 A | ||
| - **那么** 系统执行 B | ||
| ## 移除需求 | ||
| ### 需求: 已废弃功能 | ||
| ## 重命名需求 | ||
| - 从: \`### 需求: Old Name\` | ||
| - 到: \`### 需求: New Name\` | ||
| \`\`\` | ||
| **关键原则:智能合并** | ||
| 与程序化合并不同,你可以应用 **部分更新**: | ||
| - 要添加场景,只需将该场景包含在 MODIFIED 下 - 不要复制现有场景 | ||
| - 增量代表 *意图*,而不是整体替换 | ||
| - 使用你的判断力合理地合并更改 | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 规范已同步:<change-name> | ||
| 已更新主规范: | ||
| **<capability-1>**: | ||
| - 添加需求:"新功能" | ||
| - 修改需求:"现有功能"(添加了 1 个场景) | ||
| **<capability-2>**: | ||
| - 创建了新规范文件 | ||
| - 添加需求:"另一个功能" | ||
| 主规范现已更新。变更保持活动状态 - 在实现完成后归档。 | ||
| \`\`\` | ||
| **护栏** | ||
| - 在进行更改之前阅读增量规范和主规范 | ||
| - 保留增量中未提及的现有内容 | ||
| - 如果不清楚,询问澄清 | ||
| - 在进行时显示你正在更改的内容 | ||
| - 操作应该是幂等的 - 运行两次应给出相同的结果`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxSyncCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 同步', | ||
| description: '将变更中的增量规范同步到主规范', | ||
| category: '工作流', | ||
| tags: ['workflow', 'specs', 'experimental'], | ||
| content: `将变更中的增量规范同步到主规范。 | ||
| 这是一个 **Agent 驱动** 的操作 - 你将读取增量规范并直接编辑主规范以应用更改。这允许智能合并(例如,添加场景而不复制整个需求)。 | ||
| **输入**:可选择在 \`/opsx:sync\` 后指定变更名称(例如,\`/opsx:sync add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 显示具有增量规范(在 \`specs/\` 目录下)的变更。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **查找增量规范** | ||
| 在 \`openspec/changes/<name>/specs/*/spec.md\` 中查找增量规范文件。 | ||
| 每个增量规范文件包含如下部分: | ||
| - \`## 新增需求\` - 要添加的新需求 | ||
| - \`## 修改需求\` - 对现有需求的更改 | ||
| - \`## 移除需求\` - 要移除的需求 | ||
| - \`## 重命名需求\` - 要重命名的需求(从/到 格式) | ||
| 如果没有找到增量规范,通知用户并停止。 | ||
| 3. **对于每个增量规范,将更改应用到主规范** | ||
| 对于在 \`openspec/changes/<name>/specs/<capability>/spec.md\` 处具有增量规范的每个 capability: | ||
| a. **阅读增量规范** 以了解预期的更改 | ||
| b. **阅读主规范** 于 \`openspec/specs/<capability>/spec.md\`(可能尚不存在) | ||
| c. **智能地应用更改**: | ||
| **新增需求:** | ||
| - 如果需求在主规范中不存在 → 添加它 | ||
| - 如果需求已存在 → 更新它以匹配(视为隐式 MODIFIED) | ||
| **修改需求:** | ||
| - 在主规范中找到该需求 | ||
| - 应用更改 - 这可能是: | ||
| - 添加新场景(不需要复制现有场景) | ||
| - 修改现有场景 | ||
| - 更改需求描述 | ||
| - 保留增量中未提及的场景/内容 | ||
| **移除需求:** | ||
| - 从主规范中移除整个需求块 | ||
| **重命名需求:** | ||
| - 找到 FROM 需求,重命名为 TO | ||
| d. **创建新主规范** 如果 capability 尚不存在: | ||
| - 创建 \`openspec/specs/<capability>/spec.md\` | ||
| - 添加 目的 部分(可以简短,标记为 待定) | ||
| - 添加 需求 部分以及 新增需求 | ||
| 4. **显示摘要** | ||
| 应用所有更改后,总结: | ||
| - 哪些 capability 已更新 | ||
| - 做了什么更改(需求添加/修改/移除/重命名) | ||
| **增量规范格式参考** | ||
| \`\`\`markdown | ||
| ## 新增需求 | ||
| ### 需求: 新功能 | ||
| 系统 应当 实现新的能力。 | ||
| #### 场景: 基本场景 | ||
| - **当** 用户执行 X | ||
| - **那么** 系统执行 Y | ||
| ## 修改需求 | ||
| ### 需求: 现有功能 | ||
| #### 场景: 需要新增的场景 | ||
| - **当** 用户执行 A | ||
| - **那么** 系统执行 B | ||
| ## 移除需求 | ||
| ### 需求: 已废弃功能 | ||
| ## 重命名需求 | ||
| - 从: \`### 需求: Old Name\` | ||
| - 到: \`### 需求: New Name\` | ||
| \`\`\` | ||
| **关键原则:智能合并** | ||
| 与程序化合并不同,你可以应用 **部分更新**: | ||
| - 要添加场景,只需将该场景包含在 MODIFIED 下 - 不要复制现有场景 | ||
| - 增量代表 *意图*,而不是整体替换 | ||
| - 使用你的判断力合理地合并更改 | ||
| **成功时的输出** | ||
| \`\`\` | ||
| ## 规范已同步:<change-name> | ||
| 已更新主规范: | ||
| **<capability-1>**: | ||
| - 添加需求:"新功能" | ||
| - 修改需求:"现有功能"(添加了 1 个场景) | ||
| **<capability-2>**: | ||
| - 创建了新规范文件 | ||
| - 添加需求:"另一个功能" | ||
| 主规范现已更新。变更保持活动状态 - 在实现完成后归档。 | ||
| \`\`\` | ||
| **护栏** | ||
| - 在进行更改之前阅读增量规范和主规范 | ||
| - 保留增量中未提及的现有内容 | ||
| - 如果不清楚,询问澄清 | ||
| - 在进行时显示你正在更改的内容 | ||
| - 操作应该是幂等的 - 运行两次应给出相同的结果` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=sync-specs.js.map |
| /** | ||
| * Skill Template Workflow Modules | ||
| * | ||
| * This file is generated by splitting the legacy monolithic | ||
| * templates file into workflow-focused modules. | ||
| */ | ||
| import type { SkillTemplate, CommandTemplate } from '../types.js'; | ||
| export declare function getVerifyChangeSkillTemplate(): SkillTemplate; | ||
| export declare function getOpsxVerifyCommandTemplate(): CommandTemplate; | ||
| //# sourceMappingURL=verify-change.d.ts.map |
| export function getVerifyChangeSkillTemplate() { | ||
| return { | ||
| name: 'openspec-verify-change', | ||
| description: '验证实现是否与变更产出物匹配。当用户想要在归档前验证实现是否完整、正确且一致时使用。', | ||
| instructions: `验证实现是否与变更产出物(规范、任务、设计)匹配。 | ||
| **输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 显示具有实现任务的变更(存在任务产出物)。 | ||
| 如果可用,包括每个变更使用的 Schema。 | ||
| 将任务未完成的变更标记为 "(进行中)"。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查状态以了解 Schema** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流模式(例如:"spec-driven") | ||
| - 此变更存在哪些产出物 | ||
| 3. **获取变更目录并加载产出物** | ||
| \`\`\`bash | ||
| openspec-cn instructions apply --change "<name>" --json | ||
| \`\`\` | ||
| 这会返回变更目录和上下文文件。从 \`contextFiles\` 读取所有可用产出物。 | ||
| 4. **初始化验证报告结构** | ||
| 创建具有三个维度的报告结构: | ||
| - **完整性**:跟踪任务和规范覆盖率 | ||
| - **正确性**:跟踪需求实现和场景覆盖率 | ||
| - **一致性**:跟踪设计遵循情况和模式一致性 | ||
| 每个维度可以有 CRITICAL、WARNING 或 SUGGESTION 问题。 | ||
| 5. **验证完整性** | ||
| **任务完成情况**: | ||
| - 如果 contextFiles 中存在 tasks.md,读取它 | ||
| - 解析复选框:\`- [ ]\`(未完成)vs \`- [x]\`(已完成) | ||
| - 统计已完成 vs 总任务数 | ||
| - 如果存在未完成的任务: | ||
| - 为每个未完成任务添加 CRITICAL 问题 | ||
| - 建议:"完成任务:<描述>" 或 "如果已实现则标记为完成" | ||
| **规范覆盖率**: | ||
| - 如果 \`openspec/changes/<name>/specs/\` 中存在增量规范: | ||
| - 提取所有需求(标记为 "### 需求:") | ||
| - 对于每个需求: | ||
| - 在代码库中搜索与需求相关的关键词 | ||
| - 评估实现是否可能存在 | ||
| - 如果需求看起来未实现: | ||
| - 添加 CRITICAL 问题:"未找到需求:<需求名称>" | ||
| - 建议:"实现需求 X:<描述>" | ||
| 6. **验证正确性** | ||
| **需求实现映射**: | ||
| - 对于增量规范中的每个需求: | ||
| - 在代码库中搜索实现证据 | ||
| - 如果找到,记录文件路径和行范围 | ||
| - 评估实现是否符合需求意图 | ||
| - 如果检测到偏差: | ||
| - 添加 WARNING:"实现可能偏离规范:<详情>" | ||
| - 建议:"根据需求 X 审查 <文件>:<行>" | ||
| **场景覆盖率**: | ||
| - 对于增量规范中的每个场景(标记为 "#### 场景:"): | ||
| - 检查代码中是否处理了条件 | ||
| - 检查是否存在覆盖该场景的测试 | ||
| - 如果场景看起来未覆盖: | ||
| - 添加 WARNING:"场景未覆盖:<场景名称>" | ||
| - 建议:"为场景添加测试或实现:<描述>" | ||
| 7. **验证一致性** | ||
| **设计遵循情况**: | ||
| - 如果 contextFiles 中存在 design.md: | ||
| - 提取关键决策(查找 "Decision:"、"Approach:"、"Architecture:" 等部分) | ||
| - 验证实现是否遵循这些决策 | ||
| - 如果检测到矛盾: | ||
| - 添加 WARNING:"未遵循设计决策:<决策>" | ||
| - 建议:"更新实现或修订 design.md 以匹配实际情况" | ||
| - 如果没有 design.md:跳过设计遵循检查,注明 "没有 design.md 可供验证" | ||
| **代码模式一致性**: | ||
| - 审查新代码与项目模式的一致性 | ||
| - 检查文件命名、目录结构、编码风格 | ||
| - 如果发现重大偏差: | ||
| - 添加 SUGGESTION:"代码模式偏差:<详情>" | ||
| - 建议:"考虑遵循项目模式:<示例>" | ||
| 8. **生成验证报告** | ||
| **摘要记分卡**: | ||
| \`\`\` | ||
| ## 验证报告:<change-name> | ||
| ### 摘要 | ||
| | 维度 | 状态 | | ||
| |----------|------------------| | ||
| | 完整性 | X/Y 任务,N 需求 | | ||
| | 正确性 | M/N 需求已覆盖 | | ||
| | 一致性 | 已遵循/存在问题 | | ||
| \`\`\` | ||
| **按优先级分类的问题**: | ||
| 1. **CRITICAL**(归档前必须修复): | ||
| - 未完成的任务 | ||
| - 缺失的需求实现 | ||
| - 每个都有具体的、可操作的建议 | ||
| 2. **WARNING**(应该修复): | ||
| - 规范/设计偏差 | ||
| - 缺失的场景覆盖 | ||
| - 每个都有具体的建议 | ||
| 3. **SUGGESTION**(最好修复): | ||
| - 模式不一致 | ||
| - 小改进 | ||
| - 每个都有具体的建议 | ||
| **最终评估**: | ||
| - 如果有 CRITICAL 问题:"发现 X 个关键问题。归档前请修复。" | ||
| - 如果只有警告:"没有关键问题。有 Y 个警告需要考虑。可以归档(但建议改进)。" | ||
| - 如果全部通过:"所有检查通过。可以归档。" | ||
| **验证启发式方法** | ||
| - **完整性**:关注客观的检查清单项(复选框、需求列表) | ||
| - **正确性**:使用关键词搜索、文件路径分析、合理推断 - 不要求完全确定 | ||
| - **一致性**:寻找明显的不一致,不要挑剔风格 | ||
| - **误报**:不确定时,优先使用 SUGGESTION 而非 WARNING,WARNING 而非 CRITICAL | ||
| - **可操作性**:每个问题都必须有具体的建议,并在适用时提供文件/行引用 | ||
| **优雅降级** | ||
| - 如果只存在 tasks.md:仅验证任务完成情况,跳过规范/设计检查 | ||
| - 如果存在任务 + 规范:验证完整性和正确性,跳过设计 | ||
| - 如果存在完整产出物:验证所有三个维度 | ||
| - 始终注明跳过了哪些检查以及原因 | ||
| **输出格式** | ||
| 使用清晰的 Markdown: | ||
| - 摘要记分卡使用表格 | ||
| - 问题按组列出(CRITICAL/WARNING/SUGGESTION) | ||
| - 代码引用格式:\`file.ts:123\` | ||
| - 具体的、可操作的建议 | ||
| - 不要使用模糊的建议,如 "考虑审查"`, | ||
| license: 'MIT', | ||
| compatibility: '需要 openspec CLI。', | ||
| metadata: { author: 'openspec', version: '1.0' }, | ||
| }; | ||
| } | ||
| export function getOpsxVerifyCommandTemplate() { | ||
| return { | ||
| name: 'OPSX: 验证', | ||
| description: '在归档前验证实现是否与变更产出物匹配', | ||
| category: '工作流', | ||
| tags: ['workflow', 'verify', 'experimental'], | ||
| content: `验证实现是否与变更产出物(规范、任务、设计)匹配。 | ||
| **输入**:可选择在 \`/opsx:verify\` 后指定变更名称(例如,\`/opsx:verify add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。 | ||
| **步骤** | ||
| 1. **如果没有提供变更名称,提示选择** | ||
| 运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。 | ||
| 显示具有实现任务的变更(存在任务产出物)。 | ||
| 如果可用,包括每个变更使用的 Schema。 | ||
| 将任务未完成的变更标记为 "(进行中)"。 | ||
| **重要提示**:不要猜测或自动选择变更。始终让用户选择。 | ||
| 2. **检查状态以了解 Schema** | ||
| \`\`\`bash | ||
| openspec-cn status --change "<name>" --json | ||
| \`\`\` | ||
| 解析 JSON 以了解: | ||
| - \`schemaName\`:正在使用的工作流模式(例如:"spec-driven") | ||
| - 此变更存在哪些产出物 | ||
| 3. **获取变更目录并加载产出物** | ||
| \`\`\`bash | ||
| openspec-cn instructions apply --change "<name>" --json | ||
| \`\`\` | ||
| 这会返回变更目录和上下文文件。从 \`contextFiles\` 读取所有可用产出物。 | ||
| 4. **初始化验证报告结构** | ||
| 创建具有三个维度的报告结构: | ||
| - **完整性**:跟踪任务和规范覆盖率 | ||
| - **正确性**:跟踪需求实现和场景覆盖率 | ||
| - **一致性**:跟踪设计遵循情况和模式一致性 | ||
| 每个维度可以有 CRITICAL、WARNING 或 SUGGESTION 问题。 | ||
| 5. **验证完整性** | ||
| **任务完成情况**: | ||
| - 如果 contextFiles 中存在 tasks.md,读取它 | ||
| - 解析复选框:\`- [ ]\`(未完成)vs \`- [x]\`(已完成) | ||
| - 统计已完成 vs 总任务数 | ||
| - 如果存在未完成的任务: | ||
| - 为每个未完成任务添加 CRITICAL 问题 | ||
| - 建议:"完成任务:<描述>" 或 "如果已实现则标记为完成" | ||
| **规范覆盖率**: | ||
| - 如果 \`openspec/changes/<name>/specs/\` 中存在增量规范: | ||
| - 提取所有需求(标记为 "### 需求:") | ||
| - 对于每个需求: | ||
| - 在代码库中搜索与需求相关的关键词 | ||
| - 评估实现是否可能存在 | ||
| - 如果需求看起来未实现: | ||
| - 添加 CRITICAL 问题:"未找到需求:<需求名称>" | ||
| - 建议:"实现需求 X:<描述>" | ||
| 6. **验证正确性** | ||
| **需求实现映射**: | ||
| - 对于增量规范中的每个需求: | ||
| - 在代码库中搜索实现证据 | ||
| - 如果找到,记录文件路径和行范围 | ||
| - 评估实现是否符合需求意图 | ||
| - 如果检测到偏差: | ||
| - 添加 WARNING:"实现可能偏离规范:<详情>" | ||
| - 建议:"根据需求 X 审查 <文件>:<行>" | ||
| **场景覆盖率**: | ||
| - 对于增量规范中的每个场景(标记为 "#### 场景:"): | ||
| - 检查代码中是否处理了条件 | ||
| - 检查是否存在覆盖该场景的测试 | ||
| - 如果场景看起来未覆盖: | ||
| - 添加 WARNING:"场景未覆盖:<场景名称>" | ||
| - 建议:"为场景添加测试或实现:<描述>" | ||
| 7. **验证一致性** | ||
| **设计遵循情况**: | ||
| - 如果 contextFiles 中存在 design.md: | ||
| - 提取关键决策(查找 "Decision:"、"Approach:"、"Architecture:" 等部分) | ||
| - 验证实现是否遵循这些决策 | ||
| - 如果检测到矛盾: | ||
| - 添加 WARNING:"未遵循设计决策:<决策>" | ||
| - 建议:"更新实现或修订 design.md 以匹配实际情况" | ||
| - 如果没有 design.md:跳过设计遵循检查,注明 "没有 design.md 可供验证" | ||
| **代码模式一致性**: | ||
| - 审查新代码与项目模式的一致性 | ||
| - 检查文件命名、目录结构、编码风格 | ||
| - 如果发现重大偏差: | ||
| - 添加 SUGGESTION:"代码模式偏差:<详情>" | ||
| - 建议:"考虑遵循项目模式:<示例>" | ||
| 8. **生成验证报告** | ||
| **摘要记分卡**: | ||
| \`\`\` | ||
| ## 验证报告:<change-name> | ||
| ### 摘要 | ||
| | 维度 | 状态 | | ||
| |----------|------------------| | ||
| | 完整性 | X/Y 任务,N 需求 | | ||
| | 正确性 | M/N 需求已覆盖 | | ||
| | 一致性 | 已遵循/存在问题 | | ||
| \`\`\` | ||
| **按优先级分类的问题**: | ||
| 1. **CRITICAL**(归档前必须修复): | ||
| - 未完成的任务 | ||
| - 缺失的需求实现 | ||
| - 每个都有具体的、可操作的建议 | ||
| 2. **WARNING**(应该修复): | ||
| - 规范/设计偏差 | ||
| - 缺失的场景覆盖 | ||
| - 每个都有具体的建议 | ||
| 3. **SUGGESTION**(最好修复): | ||
| - 模式不一致 | ||
| - 小改进 | ||
| - 每个都有具体的建议 | ||
| **最终评估**: | ||
| - 如果有 CRITICAL 问题:"发现 X 个关键问题。归档前请修复。" | ||
| - 如果只有警告:"没有关键问题。有 Y 个警告需要考虑。可以归档(但建议改进)。" | ||
| - 如果全部通过:"所有检查通过。可以归档。" | ||
| **验证启发式方法** | ||
| - **完整性**:关注客观的检查清单项(复选框、需求列表) | ||
| - **正确性**:使用关键词搜索、文件路径分析、合理推断 - 不要求完全确定 | ||
| - **一致性**:寻找明显的不一致,不要挑剔风格 | ||
| - **误报**:不确定时,优先使用 SUGGESTION 而非 WARNING,WARNING 而非 CRITICAL | ||
| - **可操作性**:每个问题都必须有具体的建议,并在适用时提供文件/行引用 | ||
| **优雅降级** | ||
| - 如果只存在 tasks.md:仅验证任务完成情况,跳过规范/设计检查 | ||
| - 如果存在任务 + 规范:验证完整性和正确性,跳过设计 | ||
| - 如果存在完整产出物:验证所有三个维度 | ||
| - 始终注明跳过了哪些检查以及原因 | ||
| **输出格式** | ||
| 使用清晰的 Markdown: | ||
| - 摘要记分卡使用表格 | ||
| - 问题按组列出(CRITICAL/WARNING/SUGGESTION) | ||
| - 代码引用格式:\`file.ts:123\` | ||
| - 具体的、可操作的建议 | ||
| - 不要使用模糊的建议,如 "考虑审查"` | ||
| }; | ||
| } | ||
| //# sourceMappingURL=verify-change.js.map |
@@ -75,2 +75,3 @@ import { Command } from 'commander'; | ||
| .option('--force', '自动清理旧文件而不提示') | ||
| .option('--profile <profile>', 'Override global config profile (core or custom)') | ||
| .action(async (targetPath = '.', options) => { | ||
@@ -102,2 +103,3 @@ try { | ||
| force: options?.force, | ||
| profile: options?.profile, | ||
| }); | ||
@@ -216,3 +218,3 @@ await initCommand.execute(targetPath); | ||
| try { | ||
| console.error('警告:"openspec change list" 已弃用。请使用 "openspec-cn list"。'); | ||
| console.error('警告:"openspec-cn change list" 已弃用。请使用 "openspec-cn list"。'); | ||
| const changeCommand = new ChangeCommand(); | ||
@@ -219,0 +221,0 @@ await changeCommand.list(options); |
@@ -271,3 +271,3 @@ import { promises as fs } from 'fs'; | ||
| bullets.push('- 确保变更在specs/中有增量:使用标题## 新增|修改|移除|重命名需求'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 调试解析的增量:openspec-cn change show <id> --json --deltas-only'); | ||
@@ -274,0 +274,0 @@ console.error('后续步骤:'); |
@@ -37,3 +37,3 @@ import ora from 'ora'; | ||
| console.error('错误:无法自动检测 Shell。请明确指定 Shell。'); | ||
| console.error(`用法:openspec completion ${operationName} [shell]`); | ||
| console.error(`用法:openspec-cn completion ${operationName} [shell]`); | ||
| console.error(`当前支持的 Shell 有:${CompletionFactory.getSupportedShells().join(', ')}`); | ||
@@ -40,0 +40,0 @@ process.exitCode = 1; |
| import { Command } from 'commander'; | ||
| import { GlobalConfig } from '../core/global-config.js'; | ||
| import type { Profile, Delivery } from '../core/global-config.js'; | ||
| interface ProfileState { | ||
| profile: Profile; | ||
| delivery: Delivery; | ||
| workflows: string[]; | ||
| } | ||
| interface ProfileStateDiff { | ||
| hasChanges: boolean; | ||
| lines: string[]; | ||
| } | ||
| /** | ||
| * Resolve the effective current profile state from global config defaults. | ||
| */ | ||
| export declare function resolveCurrentProfileState(config: GlobalConfig): ProfileState; | ||
| /** | ||
| * Derive profile type from selected workflows. | ||
| */ | ||
| export declare function deriveProfileFromWorkflowSelection(selectedWorkflows: string[]): Profile; | ||
| /** | ||
| * Format a compact workflow summary for the profile header. | ||
| */ | ||
| export declare function formatWorkflowSummary(workflows: readonly string[], profile: Profile): string; | ||
| /** | ||
| * Build a user-facing diff summary between two profile states. | ||
| */ | ||
| export declare function diffProfileState(before: ProfileState, after: ProfileState): ProfileStateDiff; | ||
| /** | ||
| * Register the config command and all its subcommands. | ||
@@ -8,2 +35,3 @@ * | ||
| export declare function registerConfigCommand(program: Command): void; | ||
| export {}; | ||
| //# sourceMappingURL=config.d.ts.map |
+361
-7
@@ -1,6 +0,146 @@ | ||
| import { spawn } from 'node:child_process'; | ||
| import { spawn, execSync } from 'node:child_process'; | ||
| import * as fs from 'node:fs'; | ||
| import * as path from 'node:path'; | ||
| import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js'; | ||
| import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js'; | ||
| import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js'; | ||
| import { OPENSPEC_DIR_NAME } from '../core/config.js'; | ||
| import { hasProjectConfigDrift } from '../core/profile-sync-drift.js'; | ||
| const WORKFLOW_PROMPT_META = { | ||
| propose: { | ||
| name: 'Propose change', | ||
| description: 'Create proposal, design, and tasks from a request', | ||
| }, | ||
| explore: { | ||
| name: 'Explore ideas', | ||
| description: 'Investigate a problem before implementation', | ||
| }, | ||
| new: { | ||
| name: 'New change', | ||
| description: 'Create a new change scaffold quickly', | ||
| }, | ||
| continue: { | ||
| name: 'Continue change', | ||
| description: 'Resume work on an existing change', | ||
| }, | ||
| apply: { | ||
| name: 'Apply tasks', | ||
| description: 'Implement tasks from the current change', | ||
| }, | ||
| ff: { | ||
| name: 'Fast-forward', | ||
| description: 'Run a faster implementation workflow', | ||
| }, | ||
| sync: { | ||
| name: 'Sync specs', | ||
| description: 'Sync change artifacts with specs', | ||
| }, | ||
| archive: { | ||
| name: 'Archive change', | ||
| description: 'Finalize and archive a completed change', | ||
| }, | ||
| 'bulk-archive': { | ||
| name: 'Bulk archive', | ||
| description: 'Archive multiple completed changes together', | ||
| }, | ||
| verify: { | ||
| name: 'Verify change', | ||
| description: 'Run verification checks against a change', | ||
| }, | ||
| onboard: { | ||
| name: 'Onboard', | ||
| description: 'Guided onboarding flow for OpenSpec', | ||
| }, | ||
| }; | ||
| function isPromptCancellationError(error) { | ||
| return (error instanceof Error && | ||
| (error.name === 'ExitPromptError' || error.message.includes('force closed the prompt with SIGINT'))); | ||
| } | ||
| /** | ||
| * Resolve the effective current profile state from global config defaults. | ||
| */ | ||
| export function resolveCurrentProfileState(config) { | ||
| const profile = config.profile || 'core'; | ||
| const delivery = config.delivery || 'both'; | ||
| const workflows = [ | ||
| ...getProfileWorkflows(profile, config.workflows ? [...config.workflows] : undefined), | ||
| ]; | ||
| return { profile, delivery, workflows }; | ||
| } | ||
| /** | ||
| * Derive profile type from selected workflows. | ||
| */ | ||
| export function deriveProfileFromWorkflowSelection(selectedWorkflows) { | ||
| const isCoreMatch = selectedWorkflows.length === CORE_WORKFLOWS.length && | ||
| CORE_WORKFLOWS.every((w) => selectedWorkflows.includes(w)); | ||
| return isCoreMatch ? 'core' : 'custom'; | ||
| } | ||
| /** | ||
| * Format a compact workflow summary for the profile header. | ||
| */ | ||
| export function formatWorkflowSummary(workflows, profile) { | ||
| return `${workflows.length} selected (${profile})`; | ||
| } | ||
| function stableWorkflowOrder(workflows) { | ||
| const seen = new Set(); | ||
| const ordered = []; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| if (workflows.includes(workflow) && !seen.has(workflow)) { | ||
| ordered.push(workflow); | ||
| seen.add(workflow); | ||
| } | ||
| } | ||
| const extras = workflows.filter((w) => !ALL_WORKFLOWS.includes(w)); | ||
| extras.sort(); | ||
| for (const extra of extras) { | ||
| if (!seen.has(extra)) { | ||
| ordered.push(extra); | ||
| seen.add(extra); | ||
| } | ||
| } | ||
| return ordered; | ||
| } | ||
| /** | ||
| * Build a user-facing diff summary between two profile states. | ||
| */ | ||
| export function diffProfileState(before, after) { | ||
| const lines = []; | ||
| if (before.delivery !== after.delivery) { | ||
| lines.push(`delivery: ${before.delivery} -> ${after.delivery}`); | ||
| } | ||
| if (before.profile !== after.profile) { | ||
| lines.push(`profile: ${before.profile} -> ${after.profile}`); | ||
| } | ||
| const beforeOrdered = stableWorkflowOrder(before.workflows); | ||
| const afterOrdered = stableWorkflowOrder(after.workflows); | ||
| const beforeSet = new Set(beforeOrdered); | ||
| const afterSet = new Set(afterOrdered); | ||
| const added = afterOrdered.filter((w) => !beforeSet.has(w)); | ||
| const removed = beforeOrdered.filter((w) => !afterSet.has(w)); | ||
| if (added.length > 0 || removed.length > 0) { | ||
| const tokens = []; | ||
| if (added.length > 0) { | ||
| tokens.push(`added ${added.join(', ')}`); | ||
| } | ||
| if (removed.length > 0) { | ||
| tokens.push(`removed ${removed.join(', ')}`); | ||
| } | ||
| lines.push(`workflows: ${tokens.join('; ')}`); | ||
| } | ||
| return { | ||
| hasChanges: lines.length > 0, | ||
| lines, | ||
| }; | ||
| } | ||
| function maybeWarnConfigDrift(projectDir, state, colorize) { | ||
| const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME); | ||
| if (!fs.existsSync(openspecDir)) { | ||
| return; | ||
| } | ||
| if (!hasProjectConfigDrift(projectDir, state.workflows, state.delivery)) { | ||
| return; | ||
| } | ||
| console.log(colorize('Warning: Global config is not applied to this project. Run `openspec update` to sync.')); | ||
| } | ||
| /** | ||
| * Register the config command and all its subcommands. | ||
@@ -40,3 +180,29 @@ * | ||
| else { | ||
| // Read raw config to determine which values are explicit vs defaults | ||
| const configPath = getGlobalConfigPath(); | ||
| let rawConfig = {}; | ||
| try { | ||
| if (fs.existsSync(configPath)) { | ||
| rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); | ||
| } | ||
| } | ||
| catch { | ||
| // If reading fails, treat all as defaults | ||
| } | ||
| console.log(formatValueYaml(config)); | ||
| // Annotate profile settings | ||
| const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)'; | ||
| const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)'; | ||
| console.log(`\nProfile settings:`); | ||
| console.log(` profile: ${config.profile} ${profileSource}`); | ||
| console.log(` delivery: ${config.delivery} ${deliverySource}`); | ||
| if (config.profile === 'core') { | ||
| console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`); | ||
| } | ||
| else if (config.workflows && config.workflows.length > 0) { | ||
| console.log(` workflows: ${config.workflows.join(', ')} (explicit)`); | ||
| } | ||
| else { | ||
| console.log(` workflows: (none)`); | ||
| } | ||
| } | ||
@@ -74,3 +240,3 @@ }); | ||
| console.error(`错误:无效的配置键 "${key}"。${reason}`); | ||
| console.error('使用 "openspec config list" 查看可用键。'); | ||
| console.error('使用 "openspec-cn config list" 查看可用键。'); | ||
| console.error('传递 --allow-unknown 以跳过此检查。'); | ||
@@ -122,3 +288,3 @@ process.exitCode = 1; | ||
| console.error('错误:重置时必须指定 --all 参数'); | ||
| console.error('用法:openspec config reset --all [-y]'); | ||
| console.error('用法:openspec-cn config reset --all [-y]'); | ||
| process.exitCode = 1; | ||
@@ -129,6 +295,17 @@ return; | ||
| const { confirm } = await import('@inquirer/prompts'); | ||
| const confirmed = await confirm({ | ||
| message: '是否将所有配置重置为默认值?', | ||
| default: false, | ||
| }); | ||
| let confirmed; | ||
| try { | ||
| confirmed = await confirm({ | ||
| message: '是否将所有配置重置为默认值?', | ||
| default: false, | ||
| }); | ||
| } | ||
| catch (error) { | ||
| if (isPromptCancellationError(error)) { | ||
| console.log('Reset cancelled.'); | ||
| process.exitCode = 130; | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
| if (!confirmed) { | ||
@@ -201,3 +378,180 @@ console.log('已取消重置。'); | ||
| }); | ||
| // config profile [preset] | ||
| configCmd | ||
| .command('profile [preset]') | ||
| .description('Configure workflow profile (interactive picker or preset shortcut)') | ||
| .action(async (preset) => { | ||
| // Preset shortcut: `openspec config profile core` | ||
| if (preset === 'core') { | ||
| const config = getGlobalConfig(); | ||
| config.profile = 'core'; | ||
| config.workflows = [...CORE_WORKFLOWS]; | ||
| // Preserve delivery setting | ||
| saveGlobalConfig(config); | ||
| console.log('Config updated. Run `openspec update` in your projects to apply.'); | ||
| return; | ||
| } | ||
| if (preset) { | ||
| console.error(`Error: Unknown profile preset "${preset}". Available presets: core`); | ||
| 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.'); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| // Interactive picker | ||
| const { select, checkbox, confirm } = await import('@inquirer/prompts'); | ||
| const chalk = (await import('chalk')).default; | ||
| try { | ||
| const config = getGlobalConfig(); | ||
| const currentState = resolveCurrentProfileState(config); | ||
| console.log(chalk.bold('\nCurrent profile settings')); | ||
| console.log(` Delivery: ${currentState.delivery}`); | ||
| console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`); | ||
| console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)')); | ||
| console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)')); | ||
| console.log(); | ||
| const action = await select({ | ||
| message: 'What do you want to configure?', | ||
| choices: [ | ||
| { | ||
| value: 'both', | ||
| name: 'Delivery and workflows', | ||
| description: 'Update install mode and available actions together', | ||
| }, | ||
| { | ||
| value: 'delivery', | ||
| name: 'Delivery only', | ||
| description: 'Change where workflows are installed', | ||
| }, | ||
| { | ||
| value: 'workflows', | ||
| name: 'Workflows only', | ||
| description: 'Change which workflow actions are available', | ||
| }, | ||
| { | ||
| value: 'keep', | ||
| name: 'Keep current settings (exit)', | ||
| description: 'Leave configuration unchanged and exit', | ||
| }, | ||
| ], | ||
| }); | ||
| if (action === 'keep') { | ||
| console.log('No config changes.'); | ||
| maybeWarnConfigDrift(process.cwd(), currentState, chalk.yellow); | ||
| return; | ||
| } | ||
| const nextState = { | ||
| profile: currentState.profile, | ||
| delivery: currentState.delivery, | ||
| workflows: [...currentState.workflows], | ||
| }; | ||
| if (action === 'both' || action === 'delivery') { | ||
| const deliveryChoices = [ | ||
| { | ||
| value: 'both', | ||
| name: 'Both (skills + commands)', | ||
| description: 'Install workflows as both skills and slash commands', | ||
| }, | ||
| { | ||
| value: 'skills', | ||
| name: 'Skills only', | ||
| description: 'Install workflows only as skills', | ||
| }, | ||
| { | ||
| value: 'commands', | ||
| name: 'Commands only', | ||
| description: 'Install workflows only as slash commands', | ||
| }, | ||
| ]; | ||
| for (const choice of deliveryChoices) { | ||
| if (choice.value === currentState.delivery) { | ||
| choice.name += ' [current]'; | ||
| } | ||
| } | ||
| nextState.delivery = await select({ | ||
| message: 'Delivery mode (how workflows are installed):', | ||
| choices: deliveryChoices, | ||
| default: currentState.delivery, | ||
| }); | ||
| } | ||
| if (action === 'both' || action === 'workflows') { | ||
| const formatWorkflowChoice = (workflow) => { | ||
| const metadata = WORKFLOW_PROMPT_META[workflow] ?? { | ||
| name: workflow, | ||
| description: `Workflow: ${workflow}`, | ||
| }; | ||
| return { | ||
| value: workflow, | ||
| name: metadata.name, | ||
| description: metadata.description, | ||
| short: metadata.name, | ||
| checked: currentState.workflows.includes(workflow), | ||
| }; | ||
| }; | ||
| const selectedWorkflows = await checkbox({ | ||
| message: 'Select workflows to make available:', | ||
| instructions: 'Space to toggle, Enter to confirm', | ||
| pageSize: ALL_WORKFLOWS.length, | ||
| theme: { | ||
| icon: { | ||
| checked: '[x]', | ||
| unchecked: '[ ]', | ||
| }, | ||
| }, | ||
| choices: ALL_WORKFLOWS.map(formatWorkflowChoice), | ||
| }); | ||
| nextState.workflows = selectedWorkflows; | ||
| nextState.profile = deriveProfileFromWorkflowSelection(selectedWorkflows); | ||
| } | ||
| const diff = diffProfileState(currentState, nextState); | ||
| if (!diff.hasChanges) { | ||
| console.log('No config changes.'); | ||
| maybeWarnConfigDrift(process.cwd(), nextState, chalk.yellow); | ||
| return; | ||
| } | ||
| console.log(chalk.bold('\nConfig changes:')); | ||
| for (const line of diff.lines) { | ||
| console.log(` ${line}`); | ||
| } | ||
| console.log(); | ||
| config.profile = nextState.profile; | ||
| config.delivery = nextState.delivery; | ||
| config.workflows = nextState.workflows; | ||
| saveGlobalConfig(config); | ||
| // Check if inside an OpenSpec project | ||
| const projectDir = process.cwd(); | ||
| const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME); | ||
| if (fs.existsSync(openspecDir)) { | ||
| const applyNow = await confirm({ | ||
| message: 'Apply changes to this project now?', | ||
| default: true, | ||
| }); | ||
| if (applyNow) { | ||
| try { | ||
| execSync('npx openspec update', { stdio: 'inherit', cwd: projectDir }); | ||
| console.log('Run `openspec update` in your other projects to apply.'); | ||
| } | ||
| catch { | ||
| console.error('`openspec update` failed. Please run it manually to apply the profile changes.'); | ||
| process.exitCode = 1; | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| console.log('Config updated. Run `openspec update` in your projects to apply.'); | ||
| } | ||
| catch (error) { | ||
| if (isPromptCancellationError(error)) { | ||
| console.log('Config profile cancelled.'); | ||
| process.exitCode = 130; | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
| }); | ||
| } | ||
| //# sourceMappingURL=config.js.map |
@@ -63,6 +63,6 @@ import { execSync, execFileSync } from 'child_process'; | ||
| return `--- | ||
| Submitted via OpenSpec CLI | ||
| - Version: ${version} | ||
| - Platform: ${platform} | ||
| - Timestamp: ${timestamp}`; | ||
| 通过 OpenSpec CLI 提交 | ||
| - 版本: ${version} | ||
| - 平台: ${platform} | ||
| - 时间戳: ${timestamp}`; | ||
| } | ||
@@ -73,3 +73,3 @@ /** | ||
| function formatTitle(message) { | ||
| return `Feedback: ${message}`; | ||
| return `反馈: ${message}`; | ||
| } | ||
@@ -92,3 +92,3 @@ /** | ||
| function generateManualSubmissionUrl(title, body) { | ||
| const repo = 'Fission-AI/OpenSpec'; | ||
| const repo = 'studyzy/OpenSpec-cn'; | ||
| const encodedTitle = encodeURIComponent(title); | ||
@@ -103,8 +103,8 @@ const encodedBody = encodeURIComponent(body); | ||
| function displayFormattedFeedback(title, body) { | ||
| console.log('\n--- FORMATTED FEEDBACK ---'); | ||
| console.log(`Title: ${title}`); | ||
| console.log(`Labels: feedback`); | ||
| console.log('\nBody:'); | ||
| console.log('\n--- 格式化后的反馈内容 ---'); | ||
| console.log(`标题: ${title}`); | ||
| console.log(`标签: feedback`); | ||
| console.log('\n正文:'); | ||
| console.log(body); | ||
| console.log('--- END FEEDBACK ---\n'); | ||
| console.log('--- 反馈结束 ---\n'); | ||
| } | ||
@@ -121,3 +121,3 @@ /** | ||
| '--repo', | ||
| 'Fission-AI/OpenSpec', | ||
| 'studyzy/OpenSpec-cn', | ||
| '--title', | ||
@@ -131,4 +131,4 @@ title, | ||
| const issueUrl = result.trim(); | ||
| console.log(`\n✓ Feedback submitted successfully!`); | ||
| console.log(`Issue URL: ${issueUrl}\n`); | ||
| console.log(`\n✓ 反馈提交成功!`); | ||
| console.log(`Issue 链接: ${issueUrl}\n`); | ||
| } | ||
@@ -152,13 +152,13 @@ catch (error) { | ||
| if (reason === 'missing') { | ||
| console.log('⚠️ GitHub CLI not found. Manual submission required.'); | ||
| console.log('⚠️ 未找到 GitHub CLI。需要手动提交。'); | ||
| } | ||
| else { | ||
| console.log('⚠️ GitHub authentication required. Manual submission required.'); | ||
| console.log('⚠️ GitHub 未认证。需要手动提交。'); | ||
| } | ||
| displayFormattedFeedback(title, body); | ||
| const manualUrl = generateManualSubmissionUrl(title, body); | ||
| console.log('Please submit your feedback manually:'); | ||
| console.log('请手动提交您的反馈:'); | ||
| console.log(manualUrl); | ||
| if (reason === 'unauthenticated') { | ||
| console.log('\nTo auto-submit in the future: gh auth login'); | ||
| console.log('\n若要将来自动提交,请运行: gh auth login'); | ||
| } | ||
@@ -165,0 +165,0 @@ // Exit with success code (fallback is successful) |
+138
-138
@@ -81,3 +81,3 @@ import * as fs from 'node:fs'; | ||
| if (verbose) { | ||
| console.log(' Checking schema.yaml exists...'); | ||
| console.log(' 正在检查 schema.yaml 是否存在...'); | ||
| } | ||
@@ -88,3 +88,3 @@ if (!fs.existsSync(schemaPath)) { | ||
| path: 'schema.yaml', | ||
| message: 'schema.yaml not found', | ||
| message: '未找到 schema.yaml', | ||
| }); | ||
@@ -95,3 +95,3 @@ return { valid: false, issues }; | ||
| if (verbose) { | ||
| console.log(' Parsing YAML...'); | ||
| console.log(' 正在解析 YAML...'); | ||
| } | ||
@@ -106,3 +106,3 @@ let content; | ||
| path: 'schema.yaml', | ||
| message: `Failed to read file: ${err.message}`, | ||
| message: `读取文件失败: ${err.message}`, | ||
| }); | ||
@@ -113,3 +113,3 @@ return { valid: false, issues }; | ||
| if (verbose) { | ||
| console.log(' Validating schema structure...'); | ||
| console.log(' 正在验证 Schema 结构...'); | ||
| } | ||
@@ -132,3 +132,3 @@ let schema; | ||
| path: 'schema.yaml', | ||
| message: `Parse error: ${err.message}`, | ||
| message: `解析错误: ${err.message}`, | ||
| }); | ||
@@ -141,3 +141,3 @@ } | ||
| if (verbose) { | ||
| console.log(' Checking template files...'); | ||
| console.log(' 正在检查模板文件...'); | ||
| } | ||
@@ -152,3 +152,3 @@ for (const artifact of schema.artifacts) { | ||
| path: `artifacts.${artifact.id}.template`, | ||
| message: `Template file '${artifact.template}' not found for artifact '${artifact.id}'`, | ||
| message: `未找到 Artifact '${artifact.id}' 的模板文件 '${artifact.template}'`, | ||
| }); | ||
@@ -160,3 +160,3 @@ } | ||
| if (verbose) { | ||
| console.log(' Dependency graph validation passed (via parseSchema)'); | ||
| console.log(' 依赖图验证通过 (经由 parseSchema)'); | ||
| } | ||
@@ -194,3 +194,3 @@ return { valid: issues.length === 0, issues }; | ||
| id: 'proposal', | ||
| description: 'High-level description of the change, its motivation, and scope', | ||
| description: '变更的高层描述、动机和范围', | ||
| generates: 'proposal.md', | ||
@@ -201,3 +201,3 @@ template: 'proposal.md', | ||
| id: 'specs', | ||
| description: 'Detailed specifications with requirements and scenarios', | ||
| description: '包含需求和场景的详细规格说明', | ||
| generates: 'specs/**/*.md', | ||
@@ -208,3 +208,3 @@ template: 'specs/spec.md', | ||
| id: 'design', | ||
| description: 'Technical design decisions and implementation approach', | ||
| description: '技术设计决策和实施方法', | ||
| generates: 'design.md', | ||
@@ -215,3 +215,3 @@ template: 'design.md', | ||
| id: 'tasks', | ||
| description: 'Implementation checklist with trackable tasks', | ||
| description: '包含可追踪任务的实施清单', | ||
| generates: 'tasks.md', | ||
@@ -227,6 +227,6 @@ template: 'tasks.md', | ||
| .command('schema') | ||
| .description('Manage workflow schemas [experimental]'); | ||
| .description('管理工作流 Schema [实验性]'); | ||
| // Experimental warning | ||
| schemaCmd.hook('preAction', () => { | ||
| console.error('Note: Schema commands are experimental and may change.'); | ||
| console.error('注意:Schema 命令处于实验阶段,可能会发生变化。'); | ||
| }); | ||
@@ -236,5 +236,5 @@ // schema which | ||
| .command('which [name]') | ||
| .description('Show where a schema resolves from') | ||
| .option('--json', 'Output as JSON') | ||
| .option('--all', 'List all schemas with their resolution sources') | ||
| .description('显示 Schema 的解析来源') | ||
| .option('--json', '以 JSON 格式输出') | ||
| .option('--all', '列出所有 Schema 及其解析来源') | ||
| .action(async (name, options) => { | ||
@@ -251,3 +251,3 @@ try { | ||
| if (schemas.length === 0) { | ||
| console.log('No schemas found.'); | ||
| console.log('未找到 Schema。'); | ||
| return; | ||
@@ -262,6 +262,6 @@ } | ||
| if (bySource.project.length > 0) { | ||
| console.log('\nProject schemas:'); | ||
| console.log('\n项目 Schema:'); | ||
| for (const schema of bySource.project) { | ||
| const shadowInfo = schema.shadows.length > 0 | ||
| ? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})` | ||
| ? ` (遮蔽: ${schema.shadows.map((s) => s.source).join(', ')})` | ||
| : ''; | ||
@@ -272,6 +272,6 @@ console.log(` ${schema.name}${shadowInfo}`); | ||
| if (bySource.user.length > 0) { | ||
| console.log('\nUser schemas:'); | ||
| console.log('\n用户 Schema:'); | ||
| for (const schema of bySource.user) { | ||
| const shadowInfo = schema.shadows.length > 0 | ||
| ? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})` | ||
| ? ` (遮蔽: ${schema.shadows.map((s) => s.source).join(', ')})` | ||
| : ''; | ||
@@ -282,3 +282,3 @@ console.log(` ${schema.name}${shadowInfo}`); | ||
| if (bySource.package.length > 0) { | ||
| console.log('\nPackage schemas:'); | ||
| console.log('\n包 Schema:'); | ||
| for (const schema of bySource.package) { | ||
@@ -292,3 +292,3 @@ console.log(` ${schema.name}`); | ||
| if (!name) { | ||
| console.error('Error: Schema name is required (or use --all to list all schemas)'); | ||
| console.error('错误:必须指定 Schema 名称(或使用 --all 列出所有 Schema)'); | ||
| process.exitCode = 1; | ||
@@ -302,3 +302,3 @@ return; | ||
| console.log(JSON.stringify({ | ||
| error: `Schema '${name}' not found`, | ||
| error: `未找到 Schema '${name}'`, | ||
| available, | ||
@@ -308,4 +308,4 @@ }, null, 2)); | ||
| else { | ||
| console.error(`Error: Schema '${name}' not found`); | ||
| console.error(`Available schemas: ${available.join(', ')}`); | ||
| console.error(`错误:未找到 Schema '${name}'`); | ||
| console.error(`可用 Schema: ${available.join(', ')}`); | ||
| } | ||
@@ -319,7 +319,7 @@ process.exitCode = 1; | ||
| else { | ||
| console.log(`Schema: ${resolution.name}`); | ||
| console.log(`Source: ${resolution.source}`); | ||
| console.log(`Path: ${resolution.path}`); | ||
| console.log(`Schema:${resolution.name}`); | ||
| console.log(`来源: ${resolution.source}`); | ||
| console.log(`路径: ${resolution.path}`); | ||
| if (resolution.shadows.length > 0) { | ||
| console.log('\nShadows:'); | ||
| console.log('\n遮蔽 (Shadows):'); | ||
| for (const shadow of resolution.shadows) { | ||
@@ -332,3 +332,3 @@ console.log(` ${shadow.source}: ${shadow.path}`); | ||
| catch (error) { | ||
| console.error(`Error: ${error.message}`); | ||
| console.error(`错误:${error.message}`); | ||
| process.exitCode = 1; | ||
@@ -340,5 +340,5 @@ } | ||
| .command('validate [name]') | ||
| .description('Validate a schema structure and templates') | ||
| .option('--json', 'Output as JSON') | ||
| .option('--verbose', 'Show detailed validation steps') | ||
| .description('验证 Schema 结构和模板') | ||
| .option('--json', '以 JSON 格式输出') | ||
| .option('--verbose', '显示详细验证步骤') | ||
| .action(async (name, options) => { | ||
@@ -354,3 +354,3 @@ try { | ||
| valid: true, | ||
| message: 'No project schemas directory found', | ||
| message: '未找到项目 Schema 目录', | ||
| schemas: [], | ||
@@ -360,3 +360,3 @@ }, null, 2)); | ||
| else { | ||
| console.log('No project schemas directory found.'); | ||
| console.log('未找到项目 Schema 目录。'); | ||
| } | ||
@@ -376,3 +376,3 @@ return; | ||
| if (options?.verbose && !options?.json) { | ||
| console.log(`\nValidating ${entry.name}...`); | ||
| console.log(`\n正在验证 ${entry.name}...`); | ||
| } | ||
@@ -398,6 +398,6 @@ const result = validateSchema(schemaDir, options?.verbose && !options?.json); | ||
| if (schemaResults.length === 0) { | ||
| console.log('No schemas found in project.'); | ||
| console.log('项目中未找到 Schema。'); | ||
| return; | ||
| } | ||
| console.log('\nValidation Results:'); | ||
| console.log('\n验证结果:'); | ||
| for (const result of schemaResults) { | ||
@@ -423,3 +423,3 @@ const status = result.valid ? '✓' : '✗'; | ||
| valid: false, | ||
| error: `Schema '${name}' not found`, | ||
| error: `未找到 Schema '${name}'`, | ||
| available, | ||
@@ -429,4 +429,4 @@ }, null, 2)); | ||
| else { | ||
| console.error(`Error: Schema '${name}' not found`); | ||
| console.error(`Available schemas: ${available.join(', ')}`); | ||
| console.error(`错误:未找到 Schema '${name}'`); | ||
| console.error(`可用 Schema: ${available.join(', ')}`); | ||
| } | ||
@@ -437,3 +437,3 @@ process.exitCode = 1; | ||
| if (options?.verbose && !options?.json) { | ||
| console.log(`Validating ${name}...`); | ||
| console.log(`正在验证 ${name}...`); | ||
| } | ||
@@ -451,6 +451,6 @@ const result = validateSchema(schemaDir, options?.verbose && !options?.json); | ||
| if (result.valid) { | ||
| console.log(`✓ Schema '${name}' is valid`); | ||
| console.log(`✓ Schema '${name}' 有效`); | ||
| } | ||
| else { | ||
| console.log(`✗ Schema '${name}' has errors:`); | ||
| console.log(`✗ Schema '${name}' 存在错误:`); | ||
| for (const issue of result.issues) { | ||
@@ -471,3 +471,3 @@ console.log(` ${issue.level}: ${issue.message}`); | ||
| else { | ||
| console.error(`Error: ${error.message}`); | ||
| console.error(`错误:${error.message}`); | ||
| } | ||
@@ -480,5 +480,5 @@ process.exitCode = 1; | ||
| .command('fork <source> [name]') | ||
| .description('Copy an existing schema to project for customization') | ||
| .option('--json', 'Output as JSON') | ||
| .option('--force', 'Overwrite existing destination') | ||
| .description('复制现有 Schema 到项目中以进行自定义') | ||
| .option('--json', '以 JSON 格式输出') | ||
| .option('--force', '覆盖现有目标') | ||
| .action(async (source, name, options) => { | ||
@@ -494,8 +494,8 @@ const spinner = options?.json ? null : ora(); | ||
| forked: false, | ||
| error: `Invalid schema name '${destinationName}'. Use kebab-case (e.g., my-workflow)`, | ||
| error: `Schema 名称 '${destinationName}' 无效。请使用短横线连接的小写字母 (例如: my-workflow)`, | ||
| }, null, 2)); | ||
| } | ||
| else { | ||
| console.error(`Error: Invalid schema name '${destinationName}'`); | ||
| console.error('Schema names must be kebab-case (e.g., my-workflow)'); | ||
| console.error(`错误:Schema 名称 '${destinationName}' 无效`); | ||
| console.error('Schema 名称必须使用短横线连接的小写字母 (例如: my-workflow)'); | ||
| } | ||
@@ -512,3 +512,3 @@ process.exitCode = 1; | ||
| forked: false, | ||
| error: `Schema '${source}' not found`, | ||
| error: `未找到 Schema '${source}'`, | ||
| available, | ||
@@ -518,4 +518,4 @@ }, null, 2)); | ||
| else { | ||
| console.error(`Error: Schema '${source}' not found`); | ||
| console.error(`Available schemas: ${available.join(', ')}`); | ||
| console.error(`错误:未找到 Schema '${source}'`); | ||
| console.error(`可用 Schema: ${available.join(', ')}`); | ||
| } | ||
@@ -535,9 +535,9 @@ process.exitCode = 1; | ||
| forked: false, | ||
| error: `Schema '${destinationName}' already exists`, | ||
| suggestion: 'Use --force to overwrite', | ||
| error: `Schema '${destinationName}' 已存在`, | ||
| suggestion: '使用 --force 覆盖', | ||
| }, null, 2)); | ||
| } | ||
| else { | ||
| console.error(`Error: Schema '${destinationName}' already exists at ${destinationDir}`); | ||
| console.error('Use --force to overwrite'); | ||
| console.error(`错误:Schema '${destinationName}' 已存在于 ${destinationDir}`); | ||
| console.error('使用 --force 覆盖'); | ||
| } | ||
@@ -549,3 +549,3 @@ process.exitCode = 1; | ||
| if (spinner) | ||
| spinner.start(`Removing existing schema '${destinationName}'...`); | ||
| spinner.start(`正在删除现有 Schema '${destinationName}'...`); | ||
| fs.rmSync(destinationDir, { recursive: true }); | ||
@@ -555,3 +555,3 @@ } | ||
| if (spinner) | ||
| spinner.start(`Forking '${source}' to '${destinationName}'...`); | ||
| spinner.start(`正在将 '${source}' Fork 到 '${destinationName}'...`); | ||
| copyDirRecursive(sourceDir, destinationDir); | ||
@@ -565,3 +565,3 @@ // Update name in schema.yaml | ||
| if (spinner) | ||
| spinner.succeed(`Forked '${source}' to '${destinationName}'`); | ||
| spinner.succeed(`已将 '${source}' Fork 到 '${destinationName}'`); | ||
| if (options?.json) { | ||
@@ -578,5 +578,5 @@ console.log(JSON.stringify({ | ||
| else { | ||
| console.log(`\nSource: ${sourceDir} (${sourceLocation})`); | ||
| console.log(`Destination: ${destinationDir}`); | ||
| console.log(`\nYou can now customize the schema at:`); | ||
| console.log(`\n来源: ${sourceDir} (${sourceLocation})`); | ||
| console.log(`目标: ${destinationDir}`); | ||
| console.log(`\n现在您可以在以下位置自定义 Schema:`); | ||
| console.log(` ${destinationDir}/schema.yaml`); | ||
@@ -587,3 +587,3 @@ } | ||
| if (spinner) | ||
| spinner.fail(`Fork failed`); | ||
| spinner.fail(`Fork 失败`); | ||
| if (options?.json) { | ||
@@ -596,3 +596,3 @@ console.log(JSON.stringify({ | ||
| else { | ||
| console.error(`Error: ${error.message}`); | ||
| console.error(`错误:${error.message}`); | ||
| } | ||
@@ -605,9 +605,9 @@ process.exitCode = 1; | ||
| .command('init <name>') | ||
| .description('Create a new project-local schema') | ||
| .option('--json', 'Output as JSON') | ||
| .option('--description <text>', 'Schema description') | ||
| .option('--artifacts <list>', 'Comma-separated artifact IDs (proposal,specs,design,tasks)') | ||
| .option('--default', 'Set as project default schema') | ||
| .option('--no-default', 'Do not prompt to set as default') | ||
| .option('--force', 'Overwrite existing schema') | ||
| .description('创建一个新的项目本地 Schema') | ||
| .option('--json', '以 JSON 格式输出') | ||
| .option('--description <text>', 'Schema 描述') | ||
| .option('--artifacts <list>', '逗号分隔的 Artifact ID (proposal,specs,design,tasks)') | ||
| .option('--default', '设为项目默认 Schema') | ||
| .option('--no-default', '不提示设为默认') | ||
| .option('--force', '覆盖现有 Schema') | ||
| .action(async (name, options) => { | ||
@@ -622,8 +622,8 @@ const spinner = options?.json ? null : ora(); | ||
| created: false, | ||
| error: `Invalid schema name '${name}'. Use kebab-case (e.g., my-workflow)`, | ||
| error: `Schema 名称 '${name}' 无效。请使用短横线连接的小写字母 (例如: my-workflow)`, | ||
| }, null, 2)); | ||
| } | ||
| else { | ||
| console.error(`Error: Invalid schema name '${name}'`); | ||
| console.error('Schema names must be kebab-case (e.g., my-workflow)'); | ||
| console.error(`错误:Schema 名称 '${name}' 无效`); | ||
| console.error('Schema 名称必须使用短横线连接的小写字母 (例如: my-workflow)'); | ||
| } | ||
@@ -640,9 +640,9 @@ process.exitCode = 1; | ||
| created: false, | ||
| error: `Schema '${name}' already exists`, | ||
| suggestion: 'Use --force to overwrite or "openspec schema fork" to copy', | ||
| error: `Schema '${name}' 已存在`, | ||
| suggestion: '使用 --force 覆盖或使用 "openspec-cn schema fork" 进行复制', | ||
| }, null, 2)); | ||
| } | ||
| else { | ||
| console.error(`Error: Schema '${name}' already exists at ${schemaDir}`); | ||
| console.error('Use --force to overwrite or "openspec schema fork" to copy'); | ||
| console.error(`错误:Schema '${name}' 已存在于 ${schemaDir}`); | ||
| console.error('使用 --force 覆盖或使用 "openspec-cn schema fork" 进行复制'); | ||
| } | ||
@@ -653,3 +653,3 @@ process.exitCode = 1; | ||
| if (spinner) | ||
| spinner.start(`Removing existing schema '${name}'...`); | ||
| spinner.start(`正在删除现有 Schema '${name}'...`); | ||
| fs.rmSync(schemaDir, { recursive: true }); | ||
@@ -667,4 +667,4 @@ } | ||
| description = await input({ | ||
| message: 'Schema description:', | ||
| default: `Custom workflow schema for ${name}`, | ||
| message: 'Schema 描述:', | ||
| default: `${name} 的自定义工作流 Schema`, | ||
| }); | ||
@@ -677,7 +677,7 @@ const artifactChoices = DEFAULT_ARTIFACTS.map((a) => ({ | ||
| selectedArtifactIds = await checkbox({ | ||
| message: 'Select artifacts to include:', | ||
| message: '选择要包含的 Artifact:', | ||
| choices: artifactChoices, | ||
| }); | ||
| if (selectedArtifactIds.length === 0) { | ||
| console.error('Error: At least one artifact must be selected'); | ||
| console.error('错误:必须至少选择一个 Artifact'); | ||
| process.exitCode = 1; | ||
@@ -689,3 +689,3 @@ return; | ||
| const setAsDefault = await confirm({ | ||
| message: 'Set as project default schema?', | ||
| message: '设为项目默认 Schema?', | ||
| default: false, | ||
@@ -700,3 +700,3 @@ }); | ||
| // Non-interactive mode | ||
| description = options?.description || `Custom workflow schema for ${name}`; | ||
| description = options?.description || `${name} 的自定义工作流 Schema`; | ||
| if (options?.artifacts) { | ||
@@ -711,3 +711,3 @@ selectedArtifactIds = options.artifacts.split(',').map((a) => a.trim()); | ||
| created: false, | ||
| error: `Unknown artifact '${id}'`, | ||
| error: `未知 Artifact '${id}'`, | ||
| valid: validIds, | ||
@@ -717,4 +717,4 @@ }, null, 2)); | ||
| else { | ||
| console.error(`Error: Unknown artifact '${id}'`); | ||
| console.error(`Valid artifacts: ${validIds.join(', ')}`); | ||
| console.error(`错误:未知 Artifact '${id}'`); | ||
| console.error(`有效 Artifact: ${validIds.join(', ')}`); | ||
| } | ||
@@ -733,3 +733,3 @@ process.exitCode = 1; | ||
| if (spinner) | ||
| spinner.start(`Creating schema '${name}'...`); | ||
| spinner.start(`正在创建 Schema '${name}'...`); | ||
| fs.mkdirSync(schemaDir, { recursive: true }); | ||
@@ -810,3 +810,3 @@ // Build artifacts array with proper dependencies | ||
| if (spinner) | ||
| spinner.succeed(`Created schema '${name}'`); | ||
| spinner.succeed(`已创建 Schema '${name}'`); | ||
| if (options?.json) { | ||
@@ -822,11 +822,11 @@ console.log(JSON.stringify({ | ||
| else { | ||
| console.log(`\nSchema created at: ${schemaDir}`); | ||
| console.log(`\nSchema 创建于: ${schemaDir}`); | ||
| console.log(`\nArtifacts: ${selectedArtifactIds.join(', ')}`); | ||
| if (options?.default) { | ||
| console.log(`\nSet as project default schema.`); | ||
| console.log(`\n已设为项目默认 Schema。`); | ||
| } | ||
| console.log(`\nNext steps:`); | ||
| console.log(` 1. Edit ${schemaDir}/schema.yaml to customize artifacts`); | ||
| console.log(` 2. Modify templates in the schema directory`); | ||
| console.log(` 3. Use with: openspec new --schema ${name}`); | ||
| console.log(`\n后续步骤:`); | ||
| console.log(` 1. 编辑 ${schemaDir}/schema.yaml 以自定义 Artifact`); | ||
| console.log(` 2. 修改 Schema 目录中的模板`); | ||
| console.log(` 3. 使用命令: openspec-cn new --schema ${name}`); | ||
| } | ||
@@ -836,3 +836,3 @@ } | ||
| if (spinner) | ||
| spinner.fail(`Creation failed`); | ||
| spinner.fail(`创建失败`); | ||
| if (options?.json) { | ||
@@ -857,65 +857,65 @@ console.log(JSON.stringify({ | ||
| case 'proposal': | ||
| return `## Why | ||
| return `## 为什么 | ||
| <!-- Describe the motivation for this change --> | ||
| <!-- 描述此变更的动机 --> | ||
| ## What Changes | ||
| ## 变更内容 | ||
| <!-- Describe what will change --> | ||
| <!-- 描述将要变更的内容 --> | ||
| ## Capabilities | ||
| ## 能力 | ||
| ### New Capabilities | ||
| <!-- List new capabilities --> | ||
| ### 新增能力 | ||
| <!-- 列出新增能力 --> | ||
| ### Modified Capabilities | ||
| <!-- List modified capabilities --> | ||
| ### 修改的能力 | ||
| <!-- 列出修改的能力 --> | ||
| ## Impact | ||
| ## 影响 | ||
| <!-- Describe the impact on existing functionality --> | ||
| <!-- 描述对现有功能的影响 --> | ||
| `; | ||
| case 'specs': | ||
| return `## ADDED Requirements | ||
| return `## 新增需求 | ||
| ### Requirement: Example requirement | ||
| ### 需求: 示例需求 | ||
| Description of the requirement. | ||
| 需求描述。 | ||
| #### Scenario: Example scenario | ||
| - **WHEN** some condition | ||
| - **THEN** some outcome | ||
| #### 场景: 示例场景 | ||
| - **当** 满足某些条件 | ||
| - **则** 产生某些结果 | ||
| `; | ||
| case 'design': | ||
| return `## Context | ||
| return `## 背景 | ||
| <!-- Background and context --> | ||
| <!-- 背景和上下文 --> | ||
| ## Goals / Non-Goals | ||
| ## 目标 / 非目标 | ||
| **Goals:** | ||
| <!-- List goals --> | ||
| **目标:** | ||
| <!-- 列出目标 --> | ||
| **Non-Goals:** | ||
| <!-- List non-goals --> | ||
| **非目标:** | ||
| <!-- 列出非目标 --> | ||
| ## Decisions | ||
| ## 决策 | ||
| ### 1. Decision Name | ||
| ### 1. 决策名称 | ||
| Description and rationale. | ||
| 描述和理由。 | ||
| **Alternatives considered:** | ||
| - Alternative 1: Rejected because... | ||
| **考虑过的替代方案:** | ||
| - 替代方案 1: 被拒绝,因为... | ||
| ## Risks / Trade-offs | ||
| ## 风险 / 权衡 | ||
| <!-- List risks and trade-offs --> | ||
| <!-- 列出风险和权衡 --> | ||
| `; | ||
| case 'tasks': | ||
| return `## Implementation Tasks | ||
| return `## 实施任务 | ||
| - [ ] Task 1 | ||
| - [ ] Task 2 | ||
| - [ ] Task 3 | ||
| - [ ] 任务 1 | ||
| - [ ] 任务 2 | ||
| - [ ] 任务 3 | ||
| `; | ||
@@ -925,3 +925,3 @@ default: | ||
| <!-- Add content here --> | ||
| <!-- 在此添加内容 --> | ||
| `; | ||
@@ -928,0 +928,0 @@ } |
@@ -141,3 +141,3 @@ import ora from 'ora'; | ||
| bullets.push('- 确保变更在specs/中有增量:使用标题## 新增|修改|移除|重命名需求'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 调试解析的增量:openspec-cn change show <id> --json --deltas-only'); | ||
@@ -147,3 +147,3 @@ } | ||
| bullets.push('- 确保规范包含## 目的和## 需求部分'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 每个需求必须至少包含一个#### 场景:块'); | ||
| bullets.push('- 使用--json重新运行以查看结构化报告'); | ||
@@ -150,0 +150,0 @@ } |
@@ -56,3 +56,3 @@ /** | ||
| console.log(`Schema: ${schemaName}`); | ||
| console.log(`Source: ${source}`); | ||
| console.log(`来源:${source}`); | ||
| console.log(); | ||
@@ -59,0 +59,0 @@ for (const t of templates) { |
@@ -22,3 +22,5 @@ /** | ||
| export { kilocodeAdapter } from './kilocode.js'; | ||
| export { kiroAdapter } from './kiro.js'; | ||
| export { opencodeAdapter } from './opencode.js'; | ||
| export { piAdapter } from './pi.js'; | ||
| export { qoderAdapter } from './qoder.js'; | ||
@@ -25,0 +27,0 @@ export { qwenAdapter } from './qwen.js'; |
@@ -22,3 +22,5 @@ /** | ||
| export { kilocodeAdapter } from './kilocode.js'; | ||
| export { kiroAdapter } from './kiro.js'; | ||
| export { opencodeAdapter } from './opencode.js'; | ||
| export { piAdapter } from './pi.js'; | ||
| export { qoderAdapter } from './qoder.js'; | ||
@@ -25,0 +27,0 @@ export { qwenAdapter } from './qwen.js'; |
@@ -23,3 +23,5 @@ /** | ||
| import { kilocodeAdapter } from './adapters/kilocode.js'; | ||
| import { kiroAdapter } from './adapters/kiro.js'; | ||
| import { opencodeAdapter } from './adapters/opencode.js'; | ||
| import { piAdapter } from './adapters/pi.js'; | ||
| import { qoderAdapter } from './adapters/qoder.js'; | ||
@@ -52,3 +54,5 @@ import { qwenAdapter } from './adapters/qwen.js'; | ||
| CommandAdapterRegistry.register(kilocodeAdapter); | ||
| CommandAdapterRegistry.register(kiroAdapter); | ||
| CommandAdapterRegistry.register(opencodeAdapter); | ||
| CommandAdapterRegistry.register(piAdapter); | ||
| CommandAdapterRegistry.register(qoderAdapter); | ||
@@ -55,0 +59,0 @@ CommandAdapterRegistry.register(qwenAdapter); |
@@ -377,2 +377,7 @@ /** | ||
| }, | ||
| { | ||
| name: 'profile', | ||
| description: 'Configure workflow profile (interactive picker or preset shortcut)', | ||
| flags: [], | ||
| }, | ||
| ], | ||
@@ -379,0 +384,0 @@ }, |
@@ -143,3 +143,5 @@ import { promises as fs } from 'fs'; | ||
| const newContent = profileContent + openspecBlock; | ||
| await fs.writeFile(profilePath, newContent, 'utf-8'); | ||
| // Use UTF-8 with BOM for Windows PowerShell 5.1 compatibility | ||
| const bomContent = '\uFEFF' + newContent; | ||
| await fs.writeFile(profilePath, bomContent, 'utf-8'); | ||
| anyConfigured = true; | ||
@@ -190,3 +192,5 @@ } | ||
| const newContent = (beforeBlock.trimEnd() + '\n' + afterBlock.trimStart()).trim() + '\n'; | ||
| await fs.writeFile(profilePath, newContent, 'utf-8'); | ||
| // Use UTF-8 with BOM for Windows PowerShell 5.1 compatibility | ||
| const bomContent = '\uFEFF' + newContent; | ||
| await fs.writeFile(profilePath, bomContent, 'utf-8'); | ||
| anyRemoved = true; | ||
@@ -212,3 +216,7 @@ } | ||
| try { | ||
| const existingContent = await fs.readFile(targetPath, 'utf-8'); | ||
| let existingContent = await fs.readFile(targetPath, 'utf-8'); | ||
| // Remove BOM if present for comparison (BOM is added on write for Windows compatibility) | ||
| if (existingContent.charCodeAt(0) === 0xfeff) { | ||
| existingContent = existingContent.slice(1); | ||
| } | ||
| if (existingContent === completionScript) { | ||
@@ -238,4 +246,5 @@ // Already installed and up to date | ||
| const backupPath = isUpdate ? await this.backupExistingFile(targetPath) : undefined; | ||
| // Write the completion script | ||
| await fs.writeFile(targetPath, completionScript, 'utf-8'); | ||
| // Write the completion script with UTF-8 BOM for Windows PowerShell 5.1 compatibility | ||
| const bomCompletionScript = '\uFEFF' + completionScript; | ||
| await fs.writeFile(targetPath, bomCompletionScript, 'utf-8'); | ||
| // Auto-configure PowerShell profile | ||
@@ -242,0 +251,0 @@ const profileConfigured = await this.configureProfile(targetPath); |
@@ -5,3 +5,3 @@ /** | ||
| */ | ||
| export declare const ZSH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\n# Use openspec __complete to get available changes\n_openspec_complete_changes() {\n local -a changes\n while IFS=$'\\t' read -r id desc; do\n changes+=(\"$id:$desc\")\n done < <(openspec __complete changes 2>/dev/null)\n _describe \"change\" changes\n}\n\n# Use openspec __complete to get available specs\n_openspec_complete_specs() {\n local -a specs\n while IFS=$'\\t' read -r id desc; do\n specs+=(\"$id:$desc\")\n done < <(openspec __complete specs 2>/dev/null)\n _describe \"spec\" specs\n}\n\n# Get both changes and specs\n_openspec_complete_items() {\n local -a items\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec __complete changes 2>/dev/null)\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec __complete specs 2>/dev/null)\n _describe \"item\" items\n}"; | ||
| export declare const ZSH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\n# Use openspec-cn __complete to get available changes\n_openspec_complete_changes() {\n local -a changes\n while IFS=$'\\t' read -r id desc; do\n changes+=(\"$id:$desc\")\n done < <(openspec-cn __complete changes 2>/dev/null)\n _describe \"change\" changes\n}\n\n# Use openspec-cn __complete to get available specs\n_openspec_complete_specs() {\n local -a specs\n while IFS=$'\\t' read -r id desc; do\n specs+=(\"$id:$desc\")\n done < <(openspec-cn __complete specs 2>/dev/null)\n _describe \"spec\" specs\n}\n\n# Get both changes and specs\n_openspec_complete_items() {\n local -a items\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec-cn __complete changes 2>/dev/null)\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec-cn __complete specs 2>/dev/null)\n _describe \"item\" items\n}"; | ||
| //# sourceMappingURL=zsh-templates.d.ts.map |
@@ -7,3 +7,3 @@ /** | ||
| # Use openspec __complete to get available changes | ||
| # Use openspec-cn __complete to get available changes | ||
| _openspec_complete_changes() { | ||
@@ -13,7 +13,7 @@ local -a changes | ||
| changes+=("$id:$desc") | ||
| done < <(openspec __complete changes 2>/dev/null) | ||
| done < <(openspec-cn __complete changes 2>/dev/null) | ||
| _describe "change" changes | ||
| } | ||
| # Use openspec __complete to get available specs | ||
| # Use openspec-cn __complete to get available specs | ||
| _openspec_complete_specs() { | ||
@@ -23,3 +23,3 @@ local -a specs | ||
| specs+=("$id:$desc") | ||
| done < <(openspec __complete specs 2>/dev/null) | ||
| done < <(openspec-cn __complete specs 2>/dev/null) | ||
| _describe "spec" specs | ||
@@ -33,8 +33,8 @@ } | ||
| items+=("$id:$desc") | ||
| done < <(openspec __complete changes 2>/dev/null) | ||
| done < <(openspec-cn __complete changes 2>/dev/null) | ||
| while IFS=$'\\t' read -r id desc; do | ||
| items+=("$id:$desc") | ||
| done < <(openspec __complete specs 2>/dev/null) | ||
| done < <(openspec-cn __complete specs 2>/dev/null) | ||
| _describe "item" items | ||
| }`; | ||
| //# sourceMappingURL=zsh-templates.js.map |
@@ -13,23 +13,23 @@ /** | ||
| // Context section with comments | ||
| lines.push('# Project context (optional)'); | ||
| lines.push('# This is shown to AI when creating artifacts.'); | ||
| lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.'); | ||
| lines.push('# Example:'); | ||
| lines.push('# 项目上下文(可选)'); | ||
| lines.push('# 在创建工件时向 AI 显示此信息。'); | ||
| lines.push('# 添加您的技术栈、约定、风格指南、领域知识等。'); | ||
| lines.push('# 示例:'); | ||
| lines.push('# context: |'); | ||
| lines.push('# Tech stack: TypeScript, React, Node.js'); | ||
| lines.push('# We use conventional commits'); | ||
| lines.push('# Domain: e-commerce platform'); | ||
| lines.push('# 技术栈:TypeScript, React, Node.js'); | ||
| lines.push('# 我们使用约定式提交'); | ||
| lines.push('# 领域:电商平台'); | ||
| lines.push(''); | ||
| // Rules section with comments | ||
| lines.push('# Per-artifact rules (optional)'); | ||
| lines.push('# Add custom rules for specific artifacts.'); | ||
| lines.push('# Example:'); | ||
| lines.push('# 每个工件的规则(可选)'); | ||
| lines.push('# 为特定工件添加自定义规则。'); | ||
| lines.push('# 示例:'); | ||
| lines.push('# rules:'); | ||
| lines.push('# proposal:'); | ||
| lines.push('# - Keep proposals under 500 words'); | ||
| lines.push('# - Always include a "Non-goals" section'); | ||
| lines.push('# - 保持提案在500字以内'); | ||
| lines.push('# - 始终包含"非目标"部分'); | ||
| lines.push('# tasks:'); | ||
| lines.push('# - Break tasks into chunks of max 2 hours'); | ||
| lines.push('# - 将任务分解为最多2小时的块'); | ||
| return lines.join('\n') + '\n'; | ||
| } | ||
| //# sourceMappingURL=config-prompts.js.map |
@@ -8,2 +8,12 @@ import { z } from 'zod'; | ||
| featureFlags: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>>; | ||
| profile: z.ZodDefault<z.ZodOptional<z.ZodEnum<{ | ||
| core: "core"; | ||
| custom: "custom"; | ||
| }>>>; | ||
| delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{ | ||
| commands: "commands"; | ||
| skills: "skills"; | ||
| both: "both"; | ||
| }>>>; | ||
| workflows: z.ZodOptional<z.ZodArray<z.ZodString>>; | ||
| }, z.core.$loose>; | ||
@@ -10,0 +20,0 @@ export type GlobalConfigType = z.infer<typeof GlobalConfigSchema>; |
@@ -12,2 +12,13 @@ import { z } from 'zod'; | ||
| .default({}), | ||
| profile: z | ||
| .enum(['core', 'custom']) | ||
| .optional() | ||
| .default('core'), | ||
| delivery: z | ||
| .enum(['both', 'skills', 'commands']) | ||
| .optional() | ||
| .default('both'), | ||
| workflows: z | ||
| .array(z.string()) | ||
| .optional(), | ||
| }) | ||
@@ -20,4 +31,6 @@ .passthrough(); | ||
| featureFlags: {}, | ||
| profile: 'core', | ||
| delivery: 'both', | ||
| }; | ||
| const KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(DEFAULT_CONFIG)); | ||
| const KNOWN_TOP_LEVEL_KEYS = new Set([...Object.keys(DEFAULT_CONFIG), 'workflows']); | ||
| /** | ||
@@ -24,0 +37,0 @@ * Validate a config key path for CLI set operations. |
@@ -23,3 +23,5 @@ export const OPENSPEC_DIR_NAME = 'openspec'; | ||
| { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' }, | ||
| { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, | ||
| { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, | ||
| { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' }, | ||
| { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, | ||
@@ -26,0 +28,0 @@ { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, |
| export declare const GLOBAL_CONFIG_DIR_NAME = "openspec"; | ||
| export declare const GLOBAL_CONFIG_FILE_NAME = "config.json"; | ||
| export declare const GLOBAL_DATA_DIR_NAME = "openspec"; | ||
| export type Profile = 'core' | 'custom'; | ||
| export type Delivery = 'both' | 'skills' | 'commands'; | ||
| export interface GlobalConfig { | ||
| featureFlags?: Record<string, boolean>; | ||
| profile?: Profile; | ||
| delivery?: Delivery; | ||
| workflows?: string[]; | ||
| } | ||
@@ -7,0 +12,0 @@ /** |
@@ -9,3 +9,5 @@ import * as fs from 'node:fs'; | ||
| const DEFAULT_CONFIG = { | ||
| featureFlags: {} | ||
| featureFlags: {}, | ||
| profile: 'core', | ||
| delivery: 'both', | ||
| }; | ||
@@ -85,3 +87,3 @@ /** | ||
| // Merge with defaults (loaded values take precedence) | ||
| return { | ||
| const merged = { | ||
| ...DEFAULT_CONFIG, | ||
@@ -95,2 +97,10 @@ ...parsed, | ||
| }; | ||
| // Schema evolution: apply defaults for new fields if not present in loaded config | ||
| if (parsed.profile === undefined) { | ||
| merged.profile = DEFAULT_CONFIG.profile; | ||
| } | ||
| if (parsed.delivery === undefined) { | ||
| merged.delivery = DEFAULT_CONFIG.delivery; | ||
| } | ||
| return merged; | ||
| } | ||
@@ -97,0 +107,0 @@ catch (error) { |
@@ -11,2 +11,3 @@ /** | ||
| interactive?: boolean; | ||
| profile?: string; | ||
| }; | ||
@@ -17,2 +18,3 @@ export declare class InitCommand { | ||
| private readonly interactiveOption?; | ||
| private readonly profileOverride?; | ||
| constructor(options?: InitCommandOptions); | ||
@@ -22,2 +24,3 @@ execute(targetPath: string): Promise<void>; | ||
| private canPromptInteractively; | ||
| private resolveProfileOverride; | ||
| private handleLegacyCleanup; | ||
@@ -33,4 +36,6 @@ private performLegacyCleanup; | ||
| private startSpinner; | ||
| private removeSkillDirs; | ||
| private removeCommandFiles; | ||
| } | ||
| export {}; | ||
| //# sourceMappingURL=init.d.ts.map |
+205
-44
@@ -21,2 +21,6 @@ /** | ||
| import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js'; | ||
| import { getGlobalConfig } from './global-config.js'; | ||
| import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js'; | ||
| import { getAvailableTools } from './available-tools.js'; | ||
| import { migrateIfNeeded } from './migration.js'; | ||
| const require = createRequire(import.meta.url); | ||
@@ -32,2 +36,15 @@ const { version: OPENSPEC_VERSION } = require('../../package.json'); | ||
| }; | ||
| const WORKFLOW_TO_SKILL_DIR = { | ||
| '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', | ||
| }; | ||
| // ----------------------------------------------------------------------------- | ||
@@ -40,2 +57,3 @@ // Init Command Class | ||
| interactiveOption; | ||
| profileOverride; | ||
| constructor(options = {}) { | ||
@@ -45,2 +63,3 @@ this.toolsArg = options.tools; | ||
| this.interactiveOption = options.interactive; | ||
| this.profileOverride = options.profile; | ||
| } | ||
@@ -55,2 +74,8 @@ async execute(targetPath) { | ||
| await this.handleLegacyCleanup(projectPath, extendMode); | ||
| // Detect available tools in the project (task 7.1) | ||
| const detectedTools = getAvailableTools(projectPath); | ||
| // Migration check: migrate existing projects to profile system (task 7.3) | ||
| if (extendMode) { | ||
| migrateIfNeeded(projectPath, detectedTools); | ||
| } | ||
| // Show animated welcome screen (interactive mode only) | ||
@@ -62,6 +87,9 @@ const canPrompt = this.canPromptInteractively(); | ||
| } | ||
| // Validate profile override early so invalid values fail before tool setup. | ||
| // The resolved value is consumed later when generation reads effective config. | ||
| this.resolveProfileOverride(); | ||
| // Get tool states before processing | ||
| const toolStates = getToolStates(projectPath); | ||
| // Get tool selection | ||
| const selectedToolIds = await this.getSelectedTools(toolStates, extendMode); | ||
| // Get tool selection (pass detected tools for pre-selection) | ||
| const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath); | ||
| // Validate selected tools | ||
@@ -96,2 +124,11 @@ const validatedTools = this.validateTools(selectedToolIds, toolStates); | ||
| } | ||
| resolveProfileOverride() { | ||
| if (this.profileOverride === undefined) { | ||
| return undefined; | ||
| } | ||
| if (this.profileOverride === 'core' || this.profileOverride === 'custom') { | ||
| return this.profileOverride; | ||
| } | ||
| throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`); | ||
| } | ||
| // ═══════════════════════════════════════════════════════════ | ||
@@ -149,3 +186,3 @@ // LEGACY CLEANUP | ||
| // ═══════════════════════════════════════════════════════════ | ||
| async getSelectedTools(toolStates, extendMode) { | ||
| async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) { | ||
| // Check for --tools flag first | ||
@@ -157,9 +194,21 @@ const nonInteractiveSelection = this.resolveToolsArg(); | ||
| const validTools = getToolsWithSkillsDir(); | ||
| const detectedToolIds = new Set(detectedTools.map((t) => t.value)); | ||
| const configuredToolIds = new Set([...toolStates.entries()] | ||
| .filter(([, status]) => status.configured) | ||
| .map(([toolId]) => toolId)); | ||
| const shouldPreselectDetected = !extendMode && configuredToolIds.size === 0; | ||
| const canPrompt = this.canPromptInteractively(); | ||
| if (!canPrompt || validTools.length === 0) { | ||
| throw new Error(`Missing required option --tools. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`); | ||
| // Non-interactive mode: use detected tools as fallback (task 7.8) | ||
| if (!canPrompt) { | ||
| if (detectedToolIds.size > 0) { | ||
| return [...detectedToolIds]; | ||
| } | ||
| throw new Error(`No tools detected and no --tools flag provided. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`); | ||
| } | ||
| if (validTools.length === 0) { | ||
| throw new Error(`No tools available for skill generation.`); | ||
| } | ||
| // Interactive mode: show searchable multi-select | ||
| const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js'); | ||
| // Build choices with configured status and sort configured tools first | ||
| // Build choices: pre-select configured tools; keep detected tools visible but unselected. | ||
| const sortedChoices = validTools | ||
@@ -170,2 +219,3 @@ .map((toolId) => { | ||
| const configured = status?.configured ?? false; | ||
| const detected = detectedToolIds.has(toolId); | ||
| return { | ||
@@ -175,7 +225,8 @@ name: tool?.name || toolId, | ||
| configured, | ||
| preSelected: configured, // Pre-select configured tools for easy refresh | ||
| detected: detected && !configured, | ||
| preSelected: configured || (shouldPreselectDetected && detected && !configured), | ||
| }; | ||
| }) | ||
| .sort((a, b) => { | ||
| // Configured tools first | ||
| // Configured tools first, then detected (not configured), then everything else. | ||
| if (a.configured && !b.configured) | ||
@@ -185,4 +236,23 @@ return -1; | ||
| return 1; | ||
| if (a.detected && !b.detected) | ||
| return -1; | ||
| if (!a.detected && b.detected) | ||
| return 1; | ||
| return 0; | ||
| }); | ||
| const configuredNames = validTools | ||
| .filter((toolId) => configuredToolIds.has(toolId)) | ||
| .map((toolId) => AI_TOOLS.find((t) => t.value === toolId)?.name || toolId); | ||
| if (configuredNames.length > 0) { | ||
| console.log(`OpenSpec 已配置:${configuredNames.join(', ')}(已预选)`); | ||
| } | ||
| const detectedOnlyNames = detectedTools | ||
| .filter((tool) => !configuredToolIds.has(tool.value)) | ||
| .map((tool) => tool.name); | ||
| if (detectedOnlyNames.length > 0) { | ||
| const detectionLabel = shouldPreselectDetected | ||
| ? '首次设置已预选' | ||
| : '未预选'; | ||
| console.log(`检测到工具目录:${detectedOnlyNames.join(', ')}(${detectionLabel})`); | ||
| } | ||
| const selectedTools = await searchableMultiSelect({ | ||
@@ -303,5 +373,14 @@ message: `选择要设置的工具(共 ${validTools.length} 个可用)`, | ||
| const commandsSkipped = []; | ||
| // Get skill and command templates once (shared across all tools) | ||
| const skillTemplates = getSkillTemplates(); | ||
| const commandContents = getCommandContents(); | ||
| let removedCommandCount = 0; | ||
| let removedSkillCount = 0; | ||
| // Read global config for profile and delivery settings (use --profile override if set) | ||
| const globalConfig = getGlobalConfig(); | ||
| const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core'; | ||
| const delivery = globalConfig.delivery ?? 'both'; | ||
| const workflows = getProfileWorkflows(profile, globalConfig.workflows); | ||
| // Get skill and command templates filtered by profile workflows | ||
| const shouldGenerateSkills = delivery !== 'commands'; | ||
| const shouldGenerateCommands = delivery !== 'skills'; | ||
| const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : []; | ||
| const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : []; | ||
| // Process each tool | ||
@@ -311,26 +390,38 @@ for (const tool of tools) { | ||
| try { | ||
| // Use tool-specific skillsDir | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| // Create skill directories and SKILL.md files | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Generate SKILL.md content with YAML frontmatter including generatedBy | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| // Write the skill file | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| // Generate skill files if delivery includes skills | ||
| if (shouldGenerateSkills) { | ||
| // Use tool-specific skillsDir | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| // Create skill directories and SKILL.md files | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Generate SKILL.md content with YAML frontmatter including generatedBy | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| // Write the skill file | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| } | ||
| } | ||
| // Generate commands using the adapter system | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| if (!shouldGenerateSkills) { | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| removedSkillCount += await this.removeSkillDirs(skillsDir); | ||
| } | ||
| // Generate commands if delivery includes commands | ||
| if (shouldGenerateCommands) { | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| } | ||
| } | ||
| else { | ||
| commandsSkipped.push(tool.value); | ||
| } | ||
| } | ||
| else { | ||
| commandsSkipped.push(tool.value); | ||
| if (!shouldGenerateCommands) { | ||
| removedCommandCount += await this.removeCommandFiles(projectPath, tool.value); | ||
| } | ||
@@ -350,3 +441,10 @@ spinner.succeed(`Setup complete for ${tool.name}`); | ||
| } | ||
| return { createdTools, refreshedTools, failedTools, commandsSkipped }; | ||
| return { | ||
| createdTools, | ||
| refreshedTools, | ||
| failedTools, | ||
| commandsSkipped, | ||
| removedCommandCount, | ||
| removedSkillCount, | ||
| }; | ||
| } | ||
@@ -391,13 +489,21 @@ // ═══════════════════════════════════════════════════════════ | ||
| } | ||
| // Show counts | ||
| // Show counts (respecting profile filter) | ||
| const successfulTools = [...results.createdTools, ...results.refreshedTools]; | ||
| if (successfulTools.length > 0) { | ||
| const globalConfig = getGlobalConfig(); | ||
| const profile = this.profileOverride ?? globalConfig.profile ?? 'core'; | ||
| const delivery = globalConfig.delivery ?? 'both'; | ||
| const workflows = getProfileWorkflows(profile, globalConfig.workflows); | ||
| const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', '); | ||
| const hasCommands = results.commandsSkipped.length < successfulTools.length; | ||
| if (hasCommands) { | ||
| console.log(`${getSkillTemplates().length} 个技能和 ${getCommandContents().length} 个命令在 ${toolDirs}/ 中`); | ||
| const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0; | ||
| const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0; | ||
| if (skillCount > 0 && commandCount > 0) { | ||
| console.log(`${skillCount} 个技能和 ${commandCount} 个命令在 ${toolDirs}/ 中`); | ||
| } | ||
| else { | ||
| console.log(`${getSkillTemplates().length} 个技能在 ${toolDirs}/ 中`); | ||
| else if (skillCount > 0) { | ||
| console.log(`${skillCount} 个技能在 ${toolDirs}/ 中`); | ||
| } | ||
| else if (commandCount > 0) { | ||
| console.log(`${commandCount} 个命令在 ${toolDirs}/ 中`); | ||
| } | ||
| } | ||
@@ -412,2 +518,8 @@ // Show failures | ||
| } | ||
| if (results.removedCommandCount > 0) { | ||
| console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`)); | ||
| } | ||
| if (results.removedSkillCount > 0) { | ||
| console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`)); | ||
| } | ||
| // Config status | ||
@@ -427,8 +539,18 @@ if (configStatus === 'created') { | ||
| } | ||
| // Getting started | ||
| // Getting started (task 7.6: show propose if in profile) | ||
| const globalCfg = getGlobalConfig(); | ||
| const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core'; | ||
| const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)]; | ||
| console.log(); | ||
| console.log(chalk.bold('开始使用:')); | ||
| console.log(' /opsx:new 开始一个新变更'); | ||
| console.log(' /opsx:continue 创建下一个产出物'); | ||
| console.log(' /opsx:apply 实现任务'); | ||
| if (activeWorkflows.includes('propose')) { | ||
| console.log(chalk.bold('开始使用:')); | ||
| console.log(' 开始您的第一个变更:/opsx:propose "您的想法"'); | ||
| } | ||
| else if (activeWorkflows.includes('new')) { | ||
| console.log(chalk.bold('开始使用:')); | ||
| console.log(' 开始您的第一个变更:/opsx:new "您的想法"'); | ||
| } | ||
| else { | ||
| console.log("完成。运行 'openspec-cn config profile' 配置您的工作流程。"); | ||
| } | ||
| // Links | ||
@@ -453,3 +575,42 @@ console.log(); | ||
| } | ||
| async removeSkillDirs(skillsDir) { | ||
| let removed = 0; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| if (!dirName) | ||
| continue; | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| try { | ||
| if (fs.existsSync(skillDir)) { | ||
| await fs.promises.rm(skillDir, { recursive: true, force: true }); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| async removeCommandFiles(projectPath, toolId) { | ||
| let removed = 0; | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| if (!adapter) | ||
| return 0; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| try { | ||
| if (fs.existsSync(fullPath)) { | ||
| await fs.promises.unlink(fullPath); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| } | ||
| //# sourceMappingURL=init.js.map |
@@ -40,2 +40,3 @@ /** | ||
| 'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' }, | ||
| 'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' }, | ||
| 'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' }, | ||
@@ -42,0 +43,0 @@ 'amazon-q': { type: 'files', pattern: '.amazonq/prompts/openspec-*.md' }, |
@@ -6,4 +6,4 @@ /** | ||
| */ | ||
| export { SKILL_NAMES, type SkillName, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js'; | ||
| export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js'; | ||
| export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -6,4 +6,4 @@ /** | ||
| */ | ||
| export { SKILL_NAMES, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js'; | ||
| export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js'; | ||
| export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js'; | ||
| //# sourceMappingURL=index.js.map |
@@ -9,3 +9,3 @@ /** | ||
| /** | ||
| * Skill template with directory name mapping. | ||
| * Skill template with directory name and workflow ID mapping. | ||
| */ | ||
@@ -15,2 +15,3 @@ export interface SkillTemplateEntry { | ||
| dirName: string; | ||
| workflowId: string; | ||
| } | ||
@@ -25,13 +26,19 @@ /** | ||
| /** | ||
| * Gets all skill templates with their directory names. | ||
| * Gets skill templates with their directory names, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return templates whose workflowId is in this array | ||
| */ | ||
| export declare function getSkillTemplates(): SkillTemplateEntry[]; | ||
| export declare function getSkillTemplates(workflowFilter?: readonly string[]): SkillTemplateEntry[]; | ||
| /** | ||
| * Gets all command templates with their IDs. | ||
| * Gets command templates with their IDs, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return templates whose id is in this array | ||
| */ | ||
| export declare function getCommandTemplates(): CommandTemplateEntry[]; | ||
| export declare function getCommandTemplates(workflowFilter?: readonly string[]): CommandTemplateEntry[]; | ||
| /** | ||
| * Converts command templates to CommandContent array. | ||
| * Converts command templates to CommandContent array, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return contents whose id is in this array | ||
| */ | ||
| export declare function getCommandContents(): CommandContent[]; | ||
| export declare function getCommandContents(workflowFilter?: readonly string[]): CommandContent[]; | ||
| /** | ||
@@ -38,0 +45,0 @@ * Generates skill file content with YAML frontmatter. |
@@ -6,25 +6,34 @@ /** | ||
| */ | ||
| import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, } from '../templates/skill-templates.js'; | ||
| import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, getOpsxProposeCommandTemplate, } from '../templates/skill-templates.js'; | ||
| /** | ||
| * Gets all skill templates with their directory names. | ||
| * Gets skill templates with their directory names, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return templates whose workflowId is in this array | ||
| */ | ||
| export function getSkillTemplates() { | ||
| return [ | ||
| { template: getExploreSkillTemplate(), dirName: 'openspec-explore' }, | ||
| { template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change' }, | ||
| { template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change' }, | ||
| { template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change' }, | ||
| { template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change' }, | ||
| { template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs' }, | ||
| { template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change' }, | ||
| { template: getBulkArchiveChangeSkillTemplate(), dirName: 'openspec-bulk-archive-change' }, | ||
| { template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change' }, | ||
| { template: getOnboardSkillTemplate(), dirName: 'openspec-onboard' }, | ||
| export function getSkillTemplates(workflowFilter) { | ||
| const all = [ | ||
| { template: getExploreSkillTemplate(), dirName: 'openspec-explore', workflowId: 'explore' }, | ||
| { template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change', workflowId: 'new' }, | ||
| { template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change', workflowId: 'continue' }, | ||
| { template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change', workflowId: 'apply' }, | ||
| { template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change', workflowId: 'ff' }, | ||
| { template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs', workflowId: 'sync' }, | ||
| { template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change', workflowId: 'archive' }, | ||
| { template: getBulkArchiveChangeSkillTemplate(), dirName: 'openspec-bulk-archive-change', workflowId: 'bulk-archive' }, | ||
| { template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change', workflowId: 'verify' }, | ||
| { template: getOnboardSkillTemplate(), dirName: 'openspec-onboard', workflowId: 'onboard' }, | ||
| { template: getOpsxProposeSkillTemplate(), dirName: 'openspec-propose', workflowId: 'propose' }, | ||
| ]; | ||
| if (!workflowFilter) | ||
| return all; | ||
| const filterSet = new Set(workflowFilter); | ||
| return all.filter(entry => filterSet.has(entry.workflowId)); | ||
| } | ||
| /** | ||
| * Gets all command templates with their IDs. | ||
| * Gets command templates with their IDs, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return templates whose id is in this array | ||
| */ | ||
| export function getCommandTemplates() { | ||
| return [ | ||
| export function getCommandTemplates(workflowFilter) { | ||
| const all = [ | ||
| { template: getOpsxExploreCommandTemplate(), id: 'explore' }, | ||
@@ -40,9 +49,16 @@ { template: getOpsxNewCommandTemplate(), id: 'new' }, | ||
| { template: getOpsxOnboardCommandTemplate(), id: 'onboard' }, | ||
| { template: getOpsxProposeCommandTemplate(), id: 'propose' }, | ||
| ]; | ||
| if (!workflowFilter) | ||
| return all; | ||
| const filterSet = new Set(workflowFilter); | ||
| return all.filter(entry => filterSet.has(entry.id)); | ||
| } | ||
| /** | ||
| * Converts command templates to CommandContent array. | ||
| * Converts command templates to CommandContent array, optionally filtered by workflow IDs. | ||
| * | ||
| * @param workflowFilter - If provided, only return contents whose id is in this array | ||
| */ | ||
| export function getCommandContents() { | ||
| const commandTemplates = getCommandTemplates(); | ||
| export function getCommandContents(workflowFilter) { | ||
| const commandTemplates = getCommandTemplates(workflowFilter); | ||
| return commandTemplates.map(({ template, id }) => ({ | ||
@@ -49,0 +65,0 @@ id, |
@@ -9,5 +9,10 @@ /** | ||
| */ | ||
| export declare const SKILL_NAMES: readonly ["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"]; | ||
| export declare const SKILL_NAMES: readonly ["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", "openspec-propose"]; | ||
| export type SkillName = (typeof SKILL_NAMES)[number]; | ||
| /** | ||
| * IDs of command templates created by openspec init. | ||
| */ | ||
| export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"]; | ||
| export type CommandId = (typeof COMMAND_IDS)[number]; | ||
| /** | ||
| * Status of skill configuration for a tool. | ||
@@ -18,5 +23,5 @@ */ | ||
| configured: boolean; | ||
| /** Whether all 9 skills are configured */ | ||
| /** Whether all skills are configured */ | ||
| fullyConfigured: boolean; | ||
| /** Number of skills currently configured (0-9) */ | ||
| /** Number of skills currently configured */ | ||
| skillCount: number; | ||
@@ -23,0 +28,0 @@ } |
@@ -22,4 +22,22 @@ /** | ||
| 'openspec-verify-change', | ||
| 'openspec-onboard', | ||
| 'openspec-propose', | ||
| ]; | ||
| /** | ||
| * IDs of command templates created by openspec init. | ||
| */ | ||
| export const COMMAND_IDS = [ | ||
| 'explore', | ||
| 'new', | ||
| 'continue', | ||
| 'apply', | ||
| 'ff', | ||
| 'sync', | ||
| 'archive', | ||
| 'bulk-archive', | ||
| 'verify', | ||
| 'onboard', | ||
| 'propose', | ||
| ]; | ||
| /** | ||
| * Gets the list of tools with skillsDir configured. | ||
@@ -26,0 +44,0 @@ */ |
+14
-14
@@ -70,3 +70,3 @@ /** | ||
| if (addedNames.has(name)) { | ||
| throw new Error(`${specName} 验证失败 - "新增需求"部分存在重复需求: "### 需求: ${add.name}"`); | ||
| throw new Error(`${specName} 验证失败 - "新增需求"部分存在重复需求: "### 需求: ${add.name}"`); | ||
| } | ||
@@ -79,3 +79,3 @@ addedNames.add(name); | ||
| if (modifiedNames.has(name)) { | ||
| throw new Error(`${specName} 验证失败 - "修改需求"部分存在重复需求: "### 需求: ${mod.name}"`); | ||
| throw new Error(`${specName} 验证失败 - "修改需求"部分存在重复需求: "### 需求: ${mod.name}"`); | ||
| } | ||
@@ -88,3 +88,3 @@ modifiedNames.add(name); | ||
| if (removedNamesSet.has(name)) { | ||
| throw new Error(`${specName} 验证失败 - "移除需求"部分存在重复需求: "### 需求: ${rem}"`); | ||
| throw new Error(`${specName} 验证失败 - "移除需求"部分存在重复需求: "### 需求: ${rem}"`); | ||
| } | ||
@@ -99,6 +99,6 @@ removedNamesSet.add(name); | ||
| if (renamedFromSet.has(fromNorm)) { | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"部分的 FROM 存在重复: "### 需求: ${from}"`); | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"部分的 FROM 存在重复: "### 需求: ${from}"`); | ||
| } | ||
| if (renamedToSet.has(toNorm)) { | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"部分的 TO 存在重复: "### 需求: ${to}"`); | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"部分的 TO 存在重复: "### 需求: ${to}"`); | ||
| } | ||
@@ -125,7 +125,7 @@ renamedFromSet.add(fromNorm); | ||
| if (modifiedNames.has(fromNorm)) { | ||
| throw new Error(`${specName} 验证失败 - 当存在重命名时,"修改需求"必须引用新标题 "### 需求: ${to}"`); | ||
| throw new Error(`${specName} 验证失败 - 当存在重命名时,"修改需求"必须引用新标题 "### 需求: ${to}"`); | ||
| } | ||
| // Detect ADDED colliding with a RENAMED TO | ||
| if (addedNames.has(toNorm)) { | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"中的 TO 标题与"新增需求"冲突: "### 需求: ${to}"`); | ||
| throw new Error(`${specName} 验证失败 - "重命名需求"中的 TO 标题与"新增需求"冲突: "### 需求: ${to}"`); | ||
| } | ||
@@ -135,3 +135,3 @@ } | ||
| const c = conflicts[0]; | ||
| throw new Error(`${specName} 验证失败 - 需求在多个部分(${c.a} 和 ${c.b})同时出现: "### 需求: ${c.name}"`); | ||
| throw new Error(`${specName} 验证失败 - 需求在多个部分(${c.a} 和 ${c.b})同时出现: "### 需求: ${c.name}"`); | ||
| } | ||
@@ -174,6 +174,6 @@ const hasAnyDelta = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0; | ||
| if (!nameToBlock.has(from)) { | ||
| throw new Error(`${specName} 重命名失败:未找到源需求 "### 需求: ${r.from}"`); | ||
| throw new Error(`${specName} 重命名失败:未找到源需求 "### 需求: ${r.from}"`); | ||
| } | ||
| if (nameToBlock.has(to)) { | ||
| throw new Error(`${specName} 重命名失败:目标需求已存在 "### 需求: ${r.to}"`); | ||
| throw new Error(`${specName} 重命名失败:目标需求已存在 "### 需求: ${r.to}"`); | ||
| } | ||
@@ -199,3 +199,3 @@ const block = nameToBlock.get(from); | ||
| if (!isNewSpec) { | ||
| throw new Error(`${specName} 移除失败:未找到需求 "### 需求: ${name}"`); | ||
| throw new Error(`${specName} 移除失败:未找到需求 "### 需求: ${name}"`); | ||
| } | ||
@@ -211,3 +211,3 @@ // Skip removal for new specs (already warned above) | ||
| if (!nameToBlock.has(key)) { | ||
| throw new Error(`${specName} 修改失败:未找到需求 "### 需求: ${mod.name}"`); | ||
| throw new Error(`${specName} 修改失败:未找到需求 "### 需求: ${mod.name}"`); | ||
| } | ||
@@ -217,3 +217,3 @@ // Replace block with provided raw (ensure header line matches key) | ||
| if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) { | ||
| throw new Error(`${specName} 修改失败:内容中的标题不匹配 "### 需求: ${mod.name}"`); | ||
| throw new Error(`${specName} 修改失败:内容中的标题不匹配 "### 需求: ${mod.name}"`); | ||
| } | ||
@@ -226,3 +226,3 @@ nameToBlock.set(key, mod); | ||
| if (nameToBlock.has(key)) { | ||
| throw new Error(`${specName} 新增失败:需求已存在 "### 需求: ${add.name}"`); | ||
| throw new Error(`${specName} 新增失败:需求已存在 "### 需求: ${add.name}"`); | ||
| } | ||
@@ -229,0 +229,0 @@ nameToBlock.set(key, add); |
@@ -7,3 +7,3 @@ /** | ||
| */ | ||
| export { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, } from './skill-templates.js'; | ||
| export * from './skill-templates.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -7,4 +7,4 @@ /** | ||
| */ | ||
| // Re-export skill templates for convenience | ||
| export { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, } from './skill-templates.js'; | ||
| // Re-export all skill templates and related types through the compatibility facade. | ||
| export * from './skill-templates.js'; | ||
| //# sourceMappingURL=index.js.map |
| /** | ||
| * Agent Skill Templates | ||
| * | ||
| * Templates for generating Agent Skills compatible with: | ||
| * - Claude Code | ||
| * - Cursor (Settings → Rules → Import Settings) | ||
| * - Windsurf | ||
| * - Other Agent Skills-compatible editors | ||
| * Compatibility facade that re-exports split workflow template modules. | ||
| */ | ||
| export interface SkillTemplate { | ||
| name: string; | ||
| description: string; | ||
| instructions: string; | ||
| license?: string; | ||
| compatibility?: string; | ||
| metadata?: Record<string, string>; | ||
| } | ||
| /** | ||
| * Template for openspec-explore skill | ||
| * Explore mode - adaptive thinking partner for exploring ideas and problems | ||
| */ | ||
| export declare function getExploreSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-new-change skill | ||
| * Based on /opsx:new command | ||
| */ | ||
| export declare function getNewChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-continue-change skill | ||
| * Based on /opsx:continue command | ||
| */ | ||
| export declare function getContinueChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-apply-change skill | ||
| * For implementing tasks from a completed (or in-progress) change | ||
| */ | ||
| export declare function getApplyChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-ff-change skill | ||
| * Fast-forward through artifact creation | ||
| */ | ||
| export declare function getFfChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-sync-specs skill | ||
| * For syncing delta specs from a change to main specs (agent-driven) | ||
| */ | ||
| export declare function getSyncSpecsSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-onboard skill | ||
| * Guided onboarding through the complete OpenSpec workflow | ||
| */ | ||
| export declare function getOnboardSkillTemplate(): SkillTemplate; | ||
| export interface CommandTemplate { | ||
| name: string; | ||
| description: string; | ||
| category: string; | ||
| tags: string[]; | ||
| content: string; | ||
| } | ||
| /** | ||
| * Template for /opsx:explore slash command | ||
| * Explore mode - adaptive thinking partner | ||
| */ | ||
| export declare function getOpsxExploreCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:new slash command | ||
| */ | ||
| export declare function getOpsxNewCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:continue slash command | ||
| */ | ||
| export declare function getOpsxContinueCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:apply slash command | ||
| */ | ||
| export declare function getOpsxApplyCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:ff slash command | ||
| */ | ||
| export declare function getOpsxFfCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for openspec-archive-change skill | ||
| * For archiving completed changes in the experimental workflow | ||
| */ | ||
| export declare function getArchiveChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for openspec-bulk-archive-change skill | ||
| * For archiving multiple completed changes at once | ||
| */ | ||
| export declare function getBulkArchiveChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for /opsx:sync slash command | ||
| */ | ||
| export declare function getOpsxSyncCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for openspec-verify-change skill | ||
| * For verifying implementation matches change artifacts before archiving | ||
| */ | ||
| export declare function getVerifyChangeSkillTemplate(): SkillTemplate; | ||
| /** | ||
| * Template for /opsx:archive slash command | ||
| */ | ||
| export declare function getOpsxArchiveCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:onboard slash command | ||
| * Guided onboarding through the complete OpenSpec workflow | ||
| */ | ||
| export declare function getOpsxOnboardCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:bulk-archive slash command | ||
| */ | ||
| export declare function getOpsxBulkArchiveCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for /opsx:verify slash command | ||
| */ | ||
| export declare function getOpsxVerifyCommandTemplate(): CommandTemplate; | ||
| /** | ||
| * Template for feedback skill | ||
| * For collecting and submitting user feedback with context enrichment | ||
| */ | ||
| export declare function getFeedbackSkillTemplate(): SkillTemplate; | ||
| export type { SkillTemplate, CommandTemplate } from './types.js'; | ||
| export { getExploreSkillTemplate, getOpsxExploreCommandTemplate } from './workflows/explore.js'; | ||
| export { getNewChangeSkillTemplate, getOpsxNewCommandTemplate } from './workflows/new-change.js'; | ||
| export { getContinueChangeSkillTemplate, getOpsxContinueCommandTemplate } from './workflows/continue-change.js'; | ||
| export { getApplyChangeSkillTemplate, getOpsxApplyCommandTemplate } from './workflows/apply-change.js'; | ||
| export { getFfChangeSkillTemplate, getOpsxFfCommandTemplate } from './workflows/ff-change.js'; | ||
| export { getSyncSpecsSkillTemplate, getOpsxSyncCommandTemplate } from './workflows/sync-specs.js'; | ||
| export { getArchiveChangeSkillTemplate, getOpsxArchiveCommandTemplate } from './workflows/archive-change.js'; | ||
| export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate } from './workflows/bulk-archive-change.js'; | ||
| export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js'; | ||
| export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js'; | ||
| export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js'; | ||
| export { getFeedbackSkillTemplate } from './workflows/feedback.js'; | ||
| //# sourceMappingURL=skill-templates.d.ts.map |
@@ -5,3 +5,3 @@ /** | ||
| * Refreshes OpenSpec skills and commands for configured tools. | ||
| * Supports smart update detection to skip updates when already current. | ||
| * Supports profile-aware updates, delivery changes, migration, and smart update detection. | ||
| */ | ||
@@ -15,2 +15,9 @@ /** | ||
| } | ||
| /** | ||
| * Scans installed workflow artifacts (skills and managed commands) across all configured tools. | ||
| * Returns the union of detected workflow IDs that match ALL_WORKFLOWS. | ||
| * | ||
| * Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs. | ||
| */ | ||
| export declare function scanInstalledWorkflows(projectPath: string, toolIds: string[]): string[]; | ||
| export declare class UpdateCommand { | ||
@@ -29,2 +36,30 @@ private readonly force; | ||
| /** | ||
| * Detects new tool directories that aren't currently configured and displays a hint. | ||
| */ | ||
| private detectNewTools; | ||
| /** | ||
| * Displays a note about extra workflows installed that aren't in the current profile. | ||
| */ | ||
| private displayExtraWorkflowsNote; | ||
| /** | ||
| * Removes skill directories for workflows when delivery changed to commands-only. | ||
| * Returns the number of directories removed. | ||
| */ | ||
| private removeSkillDirs; | ||
| /** | ||
| * Removes skill directories for workflows that are no longer selected in the active profile. | ||
| * Returns the number of directories removed. | ||
| */ | ||
| private removeUnselectedSkillDirs; | ||
| /** | ||
| * Removes command files for workflows when delivery changed to skills-only. | ||
| * Returns the number of files removed. | ||
| */ | ||
| private removeCommandFiles; | ||
| /** | ||
| * Removes command files for workflows that are no longer selected in the active profile. | ||
| * Returns the number of files removed. | ||
| */ | ||
| private removeUnselectedCommandFiles; | ||
| /** | ||
| * Detect and handle legacy OpenSpec artifacts. | ||
@@ -31,0 +66,0 @@ * Unlike init, update warns but continues if legacy files found in non-interactive mode. |
+301
-76
@@ -5,3 +5,3 @@ /** | ||
| * Refreshes OpenSpec skills and commands for configured tools. | ||
| * Supports smart update detection to skip updates when already current. | ||
| * Supports profile-aware updates, delivery changes, migration, and smart update detection. | ||
| */ | ||
@@ -11,2 +11,3 @@ import path from 'path'; | ||
| import ora from 'ora'; | ||
| import * as fs from 'fs'; | ||
| import { createRequire } from 'module'; | ||
@@ -17,7 +18,24 @@ import { FileSystemUtils } from '../utils/file-system.js'; | ||
| import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js'; | ||
| import { getConfiguredTools, getAllToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js'; | ||
| import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js'; | ||
| import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js'; | ||
| import { isInteractive } from '../utils/interactive.js'; | ||
| import { getGlobalConfig } from './global-config.js'; | ||
| import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js'; | ||
| import { getAvailableTools } from './available-tools.js'; | ||
| import { WORKFLOW_TO_SKILL_DIR, getCommandConfiguredTools, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js'; | ||
| import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js'; | ||
| const require = createRequire(import.meta.url); | ||
| const { version: OPENSPEC_VERSION } = require('../../package.json'); | ||
| /** | ||
| * Scans installed workflow artifacts (skills and managed commands) across all configured tools. | ||
| * Returns the union of detected workflow IDs that match ALL_WORKFLOWS. | ||
| * | ||
| * Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs. | ||
| */ | ||
| export function scanInstalledWorkflows(projectPath, toolIds) { | ||
| const tools = toolIds | ||
| .map((id) => AI_TOOLS.find((t) => t.value === id)) | ||
| .filter((t) => t != null); | ||
| return scanInstalledWorkflowsShared(projectPath, tools); | ||
| } | ||
| export class UpdateCommand { | ||
@@ -35,6 +53,18 @@ force; | ||
| } | ||
| // 2. Detect and handle legacy artifacts + upgrade legacy tools to new skills | ||
| const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath); | ||
| // 3. Find configured tools | ||
| const configuredTools = getConfiguredTools(resolvedProjectPath); | ||
| // 2. Perform one-time migration if needed before any legacy upgrade generation. | ||
| // Use detected tool directories to preserve existing opsx skills/commands. | ||
| const detectedTools = getAvailableTools(resolvedProjectPath); | ||
| migrateIfNeededShared(resolvedProjectPath, detectedTools); | ||
| // 3. Read global config for profile/delivery | ||
| const globalConfig = getGlobalConfig(); | ||
| const profile = globalConfig.profile ?? 'core'; | ||
| const delivery = globalConfig.delivery ?? 'both'; | ||
| const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows); | ||
| const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow)); | ||
| const shouldGenerateSkills = delivery !== 'commands'; | ||
| const shouldGenerateCommands = delivery !== 'skills'; | ||
| // 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config | ||
| const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery); | ||
| // 5. Find configured tools | ||
| const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath); | ||
| if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) { | ||
@@ -45,13 +75,32 @@ console.log(chalk.yellow('未找到配置的工具。')); | ||
| } | ||
| // 4. Check version status for all configured tools | ||
| const toolStatuses = getAllToolVersionStatus(resolvedProjectPath, OPENSPEC_VERSION); | ||
| // 5. Smart update detection | ||
| const toolsNeedingUpdate = toolStatuses.filter((s) => s.needsUpdate); | ||
| const toolsUpToDate = toolStatuses.filter((s) => !s.needsUpdate); | ||
| if (!this.force && toolsNeedingUpdate.length === 0) { | ||
| // 6. Check version status for all configured tools | ||
| const commandConfiguredTools = getCommandConfiguredTools(resolvedProjectPath); | ||
| const commandConfiguredSet = new Set(commandConfiguredTools); | ||
| const toolStatuses = configuredTools.map((toolId) => { | ||
| const status = getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION); | ||
| if (!status.configured && commandConfiguredSet.has(toolId)) { | ||
| return { ...status, configured: true }; | ||
| } | ||
| return status; | ||
| }); | ||
| const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status])); | ||
| // 7. Smart update detection | ||
| const toolsNeedingVersionUpdate = toolStatuses | ||
| .filter((s) => s.needsUpdate) | ||
| .map((s) => s.toolId); | ||
| const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools); | ||
| const toolsToUpdateSet = new Set([ | ||
| ...toolsNeedingVersionUpdate, | ||
| ...toolsNeedingConfigSync, | ||
| ]); | ||
| const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId)); | ||
| if (!this.force && toolsToUpdateSet.size === 0) { | ||
| // All tools are up to date | ||
| this.displayUpToDateMessage(toolStatuses); | ||
| // Still check for new tool directories and extra workflows | ||
| this.detectNewTools(resolvedProjectPath, configuredTools); | ||
| this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows); | ||
| return; | ||
| } | ||
| // 6. Display update plan | ||
| // 8. Display update plan | ||
| if (this.force) { | ||
@@ -61,12 +110,16 @@ console.log(`强制更新 ${configuredTools.length} 个工具: ${configuredTools.join(', ')}`); | ||
| else { | ||
| this.displayUpdatePlan(toolsNeedingUpdate, toolsUpToDate); | ||
| this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate); | ||
| } | ||
| console.log(); | ||
| // 7. Prepare templates | ||
| const skillTemplates = getSkillTemplates(); | ||
| const commandContents = getCommandContents(); | ||
| // 8. Update tools (all if force, otherwise only those needing update) | ||
| const toolsToUpdate = this.force ? configuredTools : toolsNeedingUpdate.map((s) => s.toolId); | ||
| // 9. Determine what to generate based on delivery | ||
| const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : []; | ||
| const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : []; | ||
| // 10. Update tools (all if force, otherwise only those needing update) | ||
| const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet]; | ||
| const updatedTools = []; | ||
| const failedTools = []; | ||
| let removedCommandCount = 0; | ||
| let removedSkillCount = 0; | ||
| let removedDeselectedCommandCount = 0; | ||
| let removedDeselectedSkillCount = 0; | ||
| for (const toolId of toolsToUpdate) { | ||
@@ -79,20 +132,34 @@ const tool = AI_TOOLS.find((t) => t.value === toolId); | ||
| const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills'); | ||
| // Update skill files | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| // Generate skill files if delivery includes skills | ||
| if (shouldGenerateSkills) { | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| } | ||
| removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows); | ||
| } | ||
| // Update commands | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| // Delete skill directories if delivery is commands-only | ||
| if (!shouldGenerateSkills) { | ||
| removedSkillCount += await this.removeSkillDirs(skillsDir); | ||
| } | ||
| // Generate commands if delivery includes commands | ||
| if (shouldGenerateCommands) { | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| } | ||
| removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, desiredWorkflows); | ||
| } | ||
| } | ||
| // Delete command files if delivery is skills-only | ||
| if (!shouldGenerateCommands) { | ||
| removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId); | ||
| } | ||
| spinner.succeed(`已更新 ${tool.name}`); | ||
@@ -109,3 +176,3 @@ updatedTools.push(tool.name); | ||
| } | ||
| // 9. Summary | ||
| // 11. Summary | ||
| console.log(); | ||
@@ -118,3 +185,15 @@ if (updatedTools.length > 0) { | ||
| } | ||
| // 10. Show onboarding message for newly configured tools from legacy upgrade | ||
| if (removedCommandCount > 0) { | ||
| console.log(chalk.dim(`已删除: ${removedCommandCount} 个命令文件(交付模式: skills)`)); | ||
| } | ||
| if (removedSkillCount > 0) { | ||
| console.log(chalk.dim(`已删除: ${removedSkillCount} 个技能目录(交付模式: commands)`)); | ||
| } | ||
| if (removedDeselectedCommandCount > 0) { | ||
| console.log(chalk.dim(`已删除: ${removedDeselectedCommandCount} 个命令文件(已取消选择的工作流)`)); | ||
| } | ||
| if (removedDeselectedSkillCount > 0) { | ||
| console.log(chalk.dim(`已删除: ${removedDeselectedSkillCount} 个技能目录(已取消选择的工作流)`)); | ||
| } | ||
| // 12. Show onboarding message for newly configured tools from legacy upgrade | ||
| if (newlyConfiguredTools.length > 0) { | ||
@@ -129,2 +208,12 @@ console.log(); | ||
| } | ||
| const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])]; | ||
| // 13. Detect new tool directories not currently configured | ||
| this.detectNewTools(resolvedProjectPath, configuredAndNewTools); | ||
| // 14. Display note about extra workflows not in profile | ||
| this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows); | ||
| // 15. List affected tools | ||
| if (updatedTools.length > 0) { | ||
| const toolDisplayNames = updatedTools; | ||
| console.log(chalk.dim(`工具: ${toolDisplayNames.join(', ')}`)); | ||
| } | ||
| console.log(); | ||
@@ -141,3 +230,3 @@ console.log(chalk.dim('重启 IDE 以使更改生效。')); | ||
| console.log(); | ||
| console.log(chalk.dim('使用 --force 强制刷新 skills。')); | ||
| console.log(chalk.dim('使用 --force 强制刷新文件.')); | ||
| } | ||
@@ -147,8 +236,12 @@ /** | ||
| */ | ||
| displayUpdatePlan(needingUpdate, upToDate) { | ||
| const updates = needingUpdate.map((s) => { | ||
| const fromVersion = s.generatedByVersion ?? 'unknown'; | ||
| return `${s.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`; | ||
| displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) { | ||
| const updates = toolsToUpdate.map((toolId) => { | ||
| const status = statusByTool.get(toolId); | ||
| if (status?.needsUpdate) { | ||
| const fromVersion = status.generatedByVersion ?? 'unknown'; | ||
| return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`; | ||
| } | ||
| return `${toolId} (配置同步)`; | ||
| }); | ||
| console.log(`正在更新 ${needingUpdate.length} 个工具: ${updates.join(', ')}`); | ||
| console.log(`正在更新 ${toolsToUpdate.length} 个工具: ${updates.join(', ')}`); | ||
| if (upToDate.length > 0) { | ||
@@ -160,2 +253,128 @@ const upToDateNames = upToDate.map((s) => s.toolId); | ||
| /** | ||
| * Detects new tool directories that aren't currently configured and displays a hint. | ||
| */ | ||
| detectNewTools(projectPath, configuredTools) { | ||
| const availableTools = getAvailableTools(projectPath); | ||
| const configuredSet = new Set(configuredTools); | ||
| const newTools = availableTools.filter((t) => !configuredSet.has(t.value)); | ||
| if (newTools.length > 0) { | ||
| const newToolNames = newTools.map((tool) => tool.name); | ||
| const isSingleTool = newToolNames.length === 1; | ||
| const toolNoun = isSingleTool ? '工具' : '工具'; | ||
| console.log(); | ||
| console.log(chalk.yellow(`检测到新的${toolNoun}:${newToolNames.join(', ')}。运行 'openspec-cn init' 以添加${isSingleTool ? '它' : '它们'}。`)); | ||
| } | ||
| } | ||
| /** | ||
| * Displays a note about extra workflows installed that aren't in the current profile. | ||
| */ | ||
| displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) { | ||
| const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools); | ||
| const profileSet = new Set(profileWorkflows); | ||
| const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w)); | ||
| if (extraWorkflows.length > 0) { | ||
| console.log(chalk.dim(`注意:有 ${extraWorkflows.length} 个额外工作流不在当前配置文件中(使用 \`openspec-cn config profile\` 管理)`)); | ||
| } | ||
| } | ||
| /** | ||
| * Removes skill directories for workflows when delivery changed to commands-only. | ||
| * Returns the number of directories removed. | ||
| */ | ||
| async removeSkillDirs(skillsDir) { | ||
| let removed = 0; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| if (!dirName) | ||
| continue; | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| try { | ||
| if (fs.existsSync(skillDir)) { | ||
| await fs.promises.rm(skillDir, { recursive: true, force: true }); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| /** | ||
| * Removes skill directories for workflows that are no longer selected in the active profile. | ||
| * Returns the number of directories removed. | ||
| */ | ||
| async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) { | ||
| const desiredSet = new Set(desiredWorkflows); | ||
| let removed = 0; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| if (desiredSet.has(workflow)) | ||
| continue; | ||
| const dirName = WORKFLOW_TO_SKILL_DIR[workflow]; | ||
| if (!dirName) | ||
| continue; | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| try { | ||
| if (fs.existsSync(skillDir)) { | ||
| await fs.promises.rm(skillDir, { recursive: true, force: true }); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| /** | ||
| * Removes command files for workflows when delivery changed to skills-only. | ||
| * Returns the number of files removed. | ||
| */ | ||
| async removeCommandFiles(projectPath, toolId) { | ||
| let removed = 0; | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| if (!adapter) | ||
| return 0; | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| try { | ||
| if (fs.existsSync(fullPath)) { | ||
| await fs.promises.unlink(fullPath); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| /** | ||
| * Removes command files for workflows that are no longer selected in the active profile. | ||
| * Returns the number of files removed. | ||
| */ | ||
| async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) { | ||
| let removed = 0; | ||
| const adapter = CommandAdapterRegistry.get(toolId); | ||
| if (!adapter) | ||
| return 0; | ||
| const desiredSet = new Set(desiredWorkflows); | ||
| for (const workflow of ALL_WORKFLOWS) { | ||
| if (desiredSet.has(workflow)) | ||
| continue; | ||
| const cmdPath = adapter.getFilePath(workflow); | ||
| const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath); | ||
| try { | ||
| if (fs.existsSync(fullPath)) { | ||
| await fs.promises.unlink(fullPath); | ||
| removed++; | ||
| } | ||
| } | ||
| catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| /** | ||
| * Detect and handle legacy OpenSpec artifacts. | ||
@@ -165,3 +384,3 @@ * Unlike init, update warns but continues if legacy files found in non-interactive mode. | ||
| */ | ||
| async handleLegacyCleanup(projectPath) { | ||
| async handleLegacyCleanup(projectPath, desiredWorkflows, delivery) { | ||
| // Detect legacy artifacts | ||
@@ -181,3 +400,3 @@ const detection = await detectLegacyArtifacts(projectPath); | ||
| // Then upgrade legacy tools to new skills | ||
| return this.upgradeLegacyTools(projectPath, detection, canPrompt); | ||
| return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery); | ||
| } | ||
@@ -187,3 +406,3 @@ if (!canPrompt) { | ||
| // (Unlike init, update doesn't abort - user may just want to update skills) | ||
| console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.')); | ||
| console.log(chalk.yellow('⚠ 使用 --force 自动清理旧文件,或在交互模式下运行。')); | ||
| console.log(); | ||
@@ -195,3 +414,3 @@ return []; | ||
| const shouldCleanup = await confirm({ | ||
| message: 'Upgrade and clean up legacy files?', | ||
| message: '升级并清理旧版文件?', | ||
| default: true, | ||
@@ -202,6 +421,6 @@ }); | ||
| // Then upgrade legacy tools to new skills | ||
| return this.upgradeLegacyTools(projectPath, detection, canPrompt); | ||
| return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery); | ||
| } | ||
| else { | ||
| console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...')); | ||
| console.log(chalk.dim('跳过旧版清理。继续更新技能...')); | ||
| console.log(); | ||
@@ -215,5 +434,5 @@ return []; | ||
| async performLegacyCleanup(projectPath, detection) { | ||
| const spinner = ora('Cleaning up legacy files...').start(); | ||
| const spinner = ora('正在清理旧版文件...').start(); | ||
| const result = await cleanupLegacyArtifacts(projectPath, detection); | ||
| spinner.succeed('Legacy files cleaned up'); | ||
| spinner.succeed('旧版文件已清理完成'); | ||
| const summary = formatCleanupSummary(result); | ||
@@ -230,3 +449,3 @@ if (summary) { | ||
| */ | ||
| async upgradeLegacyTools(projectPath, detection, canPrompt) { | ||
| async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) { | ||
| // Get tools that had legacy artifacts | ||
@@ -238,3 +457,3 @@ const legacyTools = getToolsFromLegacyArtifacts(detection); | ||
| // Get currently configured tools | ||
| const configuredTools = getConfiguredTools(projectPath); | ||
| const configuredTools = getConfiguredToolsForProfileSync(projectPath); | ||
| const configuredSet = new Set(configuredTools); | ||
@@ -263,3 +482,3 @@ // Filter to tools that aren't already configured | ||
| selectedTools = validUnconfiguredTools; | ||
| console.log(`Setting up skills for: ${selectedTools.join(', ')}`); | ||
| console.log(`正在为以下工具设置技能:${selectedTools.join(', ')}`); | ||
| } | ||
@@ -279,3 +498,3 @@ else { | ||
| selectedTools = await searchableMultiSelect({ | ||
| message: 'Select tools to set up with the new skill system:', | ||
| message: '选择要使用新技能系统设置的工具:', | ||
| pageSize: 15, | ||
@@ -286,3 +505,3 @@ choices: sortedChoices, | ||
| if (selectedTools.length === 0) { | ||
| console.log(chalk.dim('Skipping tool setup.')); | ||
| console.log(chalk.dim('跳过工具设置。')); | ||
| console.log(); | ||
@@ -292,6 +511,8 @@ return []; | ||
| } | ||
| // Create skills for selected tools | ||
| // Create skills/commands for selected tools using effective profile+delivery. | ||
| const newlyConfigured = []; | ||
| const skillTemplates = getSkillTemplates(); | ||
| const commandContents = getCommandContents(); | ||
| const shouldGenerateSkills = delivery !== 'commands'; | ||
| const shouldGenerateCommands = delivery !== 'skills'; | ||
| const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : []; | ||
| const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : []; | ||
| for (const toolId of selectedTools) { | ||
@@ -301,28 +522,32 @@ const tool = AI_TOOLS.find((t) => t.value === toolId); | ||
| continue; | ||
| const spinner = ora(`Setting up ${tool.name}...`).start(); | ||
| const spinner = ora(`正在设置 ${tool.name}...`).start(); | ||
| try { | ||
| const skillsDir = path.join(projectPath, tool.skillsDir, 'skills'); | ||
| // Create skill files | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| // Create skill files when delivery includes skills | ||
| if (shouldGenerateSkills) { | ||
| for (const { template, dirName } of skillTemplates) { | ||
| const skillDir = path.join(skillsDir, dirName); | ||
| const skillFile = path.join(skillDir, 'SKILL.md'); | ||
| // Use hyphen-based command references for OpenCode | ||
| const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; | ||
| const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); | ||
| await FileSystemUtils.writeFile(skillFile, skillContent); | ||
| } | ||
| } | ||
| // Create commands | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| // Create commands when delivery includes commands | ||
| if (shouldGenerateCommands) { | ||
| const adapter = CommandAdapterRegistry.get(tool.value); | ||
| if (adapter) { | ||
| const generatedCommands = generateCommands(commandContents, adapter); | ||
| for (const cmd of generatedCommands) { | ||
| const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path); | ||
| await FileSystemUtils.writeFile(commandFile, cmd.fileContent); | ||
| } | ||
| } | ||
| } | ||
| spinner.succeed(`Setup complete for ${tool.name}`); | ||
| spinner.succeed(`${tool.name} 设置完成`); | ||
| newlyConfigured.push(toolId); | ||
| } | ||
| catch (error) { | ||
| spinner.fail(`Failed to set up ${tool.name}`); | ||
| spinner.fail(`${tool.name} 设置失败`); | ||
| console.log(chalk.red(` ${error instanceof Error ? error.message : String(error)}`)); | ||
@@ -329,0 +554,0 @@ } |
@@ -29,7 +29,7 @@ /** | ||
| readonly DELTA_MISSING_REQUIREMENTS: "增量应包含需求"; | ||
| readonly GUIDE_NO_DELTAS: "未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个\"#### 场景:\"块。提示:运行\"openspec-cn change show <change-id> --json --deltas-only\"来检查解析的增量。"; | ||
| readonly GUIDE_MISSING_SPEC_SECTIONS: "缺少必需部分。预期标题:\"## 目的\"和\"## 需求\"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ..."; | ||
| readonly GUIDE_NO_DELTAS: "未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个\"#### 场景:\"块。提示:运行\"openspec-cn change show <change-id> --json --deltas-only\"来检查解析的增量。"; | ||
| readonly GUIDE_MISSING_SPEC_SECTIONS: "缺少必需部分。预期标题:\"## 目的\"和\"## 需求\"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ..."; | ||
| readonly GUIDE_MISSING_CHANGE_SECTIONS: "缺少必需部分。预期标题:\"## 为什么\"和\"## 变更内容\"。确保在specs/中使用增量标题记录增量。"; | ||
| readonly GUIDE_SCENARIO_FORMAT: "场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ..."; | ||
| readonly GUIDE_SCENARIO_FORMAT: "场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ..."; | ||
| }; | ||
| //# sourceMappingURL=constants.d.ts.map |
@@ -35,7 +35,7 @@ /** | ||
| // Guidance snippets (appended to primary messages for remediation) | ||
| GUIDE_NO_DELTAS: '未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个"#### 场景:"块。提示:运行"openspec-cn change show <change-id> --json --deltas-only"来检查解析的增量。', | ||
| GUIDE_MISSING_SPEC_SECTIONS: '缺少必需部分。预期标题:"## 目的"和"## 需求"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...', | ||
| GUIDE_NO_DELTAS: '未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个"#### 场景:"块。提示:运行"openspec-cn change show <change-id> --json --deltas-only"来检查解析的增量。', | ||
| GUIDE_MISSING_SPEC_SECTIONS: '缺少必需部分。预期标题:"## 目的"和"## 需求"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...', | ||
| GUIDE_MISSING_CHANGE_SECTIONS: '缺少必需部分。预期标题:"## 为什么"和"## 变更内容"。确保在specs/中使用增量标题记录增量。', | ||
| GUIDE_SCENARIO_FORMAT: '场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...', | ||
| GUIDE_SCENARIO_FORMAT: '场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...', | ||
| }; | ||
| //# sourceMappingURL=constants.js.map |
@@ -243,3 +243,3 @@ import { readFileSync, promises as fs } from 'fs'; | ||
| path: specPath, | ||
| message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求:" 块(移除需求部分可以使用项目符号列表语法)。`, | ||
| message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求:" 块(移除需求部分可以使用项目符号列表语法)。`, | ||
| }); | ||
@@ -246,0 +246,0 @@ } |
@@ -6,2 +6,3 @@ interface Choice { | ||
| configured?: boolean; | ||
| detected?: boolean; | ||
| configuredLabel?: string; | ||
@@ -22,5 +23,5 @@ preSelected?: boolean; | ||
| * - ↑↓ to navigate | ||
| * - Enter to add highlighted item | ||
| * - Space to toggle highlighted item selection | ||
| * - Backspace to remove last selected item (or delete search char) | ||
| * - Tab to confirm selections | ||
| * - Enter to confirm selections | ||
| */ | ||
@@ -27,0 +28,0 @@ export declare function searchableMultiSelect(config: Config): Promise<string[]>; |
@@ -29,4 +29,4 @@ import chalk from 'chalk'; | ||
| return; | ||
| // Tab to confirm | ||
| if (key.name === 'tab') { | ||
| // Enter to confirm/submit | ||
| if (isEnterKey(key)) { | ||
| if (validate) { | ||
@@ -43,9 +43,12 @@ const result = validate(selectedValues); | ||
| } | ||
| // Enter to add item | ||
| if (isEnterKey(key)) { | ||
| // Space to toggle selection | ||
| if (key.name === 'space') { | ||
| const choice = filteredChoices[cursor]; | ||
| if (choice && !selectedSet.has(choice.value)) { | ||
| setSelectedValues([...selectedValues, choice.value]); | ||
| setSearchText(''); | ||
| setCursor(0); | ||
| if (choice) { | ||
| if (selectedSet.has(choice.value)) { | ||
| setSelectedValues(selectedValues.filter(v => v !== choice.value)); | ||
| } | ||
| else { | ||
| setSelectedValues([...selectedValues, choice.value]); | ||
| } | ||
| } | ||
@@ -100,3 +103,3 @@ return; | ||
| // Instructions | ||
| lines.push(` ${chalk.cyan('↑↓')} 导航 • ${chalk.cyan('Enter')} 添加 • ${chalk.cyan('Backspace')} 删除 • ${chalk.cyan('Tab')} 确认`); | ||
| lines.push(` ${chalk.cyan('↑↓')} 导航 • ${chalk.cyan('Space')} 添加 • ${chalk.cyan('Backspace')} 删除 • ${chalk.cyan('Enter')} 确认`); | ||
| // List | ||
@@ -120,5 +123,12 @@ if (filteredChoices.length === 0) { | ||
| const isRefresh = selected && item.configured; | ||
| const statusLabel = !selected | ||
| ? item.configured | ||
| ? ' (configured)' | ||
| : item.detected | ||
| ? ' (detected)' | ||
| : '' | ||
| : ''; | ||
| const suffix = selected | ||
| ? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)') | ||
| : ''; | ||
| : chalk.dim(statusLabel); | ||
| lines.push(` ${arrow} ${icon} ${name}${suffix}`); | ||
@@ -144,5 +154,5 @@ } | ||
| * - ↑↓ to navigate | ||
| * - Enter to add highlighted item | ||
| * - Space to toggle highlighted item selection | ||
| * - Backspace to remove last selected item (or delete search char) | ||
| * - Tab to confirm selections | ||
| * - Enter to confirm selections | ||
| */ | ||
@@ -149,0 +159,0 @@ export async function searchableMultiSelect(config) { |
+20
-18
| { | ||
| "name": "@studyzy/openspec-cn", | ||
| "version": "1.1.1", | ||
| "version": "1.2.0-1", | ||
| "description": "面向AI编程助手的规范驱动开发框架OpenSpec的简体中文汉化版本", | ||
@@ -43,2 +43,20 @@ "keywords": [ | ||
| ], | ||
| "scripts": { | ||
| "lint": "eslint src/", | ||
| "build": "node build.js", | ||
| "dev": "tsc --watch", | ||
| "dev:cli": "pnpm build && node bin/openspec.js", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest", | ||
| "test:ui": "vitest --ui", | ||
| "test:coverage": "vitest --coverage", | ||
| "test:postinstall": "node scripts/postinstall.js", | ||
| "prepare": "pnpm run build", | ||
| "prepublishOnly": "pnpm run build", | ||
| "postinstall": "node scripts/postinstall.js", | ||
| "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", | ||
| "changeset": "changeset" | ||
| }, | ||
| "engines": { | ||
@@ -67,19 +85,3 @@ "node": ">=20.19.0" | ||
| "zod": "^4.0.17" | ||
| }, | ||
| "scripts": { | ||
| "lint": "eslint src/", | ||
| "build": "node build.js", | ||
| "dev": "tsc --watch", | ||
| "dev:cli": "pnpm build && node bin/openspec.js", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest", | ||
| "test:ui": "vitest --ui", | ||
| "test:coverage": "vitest --coverage", | ||
| "test:postinstall": "node scripts/postinstall.js", | ||
| "postinstall": "node scripts/postinstall.js", | ||
| "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", | ||
| "changeset": "changeset" | ||
| } | ||
| } | ||
| } |
@@ -45,5 +45,5 @@ name: spec-driven | ||
| 格式要求: | ||
| - 每个需求:`### 需求:<名称>` 后面跟描述 | ||
| - 每个需求:`### 需求:<名称>` 后面跟描述 | ||
| - 使用 SHALL/MUST/必须/禁止等规范性词汇(避免使用 should/may) | ||
| - 每个场景:`#### 场景:<名称>` 使用 当/那么(WHEN/THEN)格式 | ||
| - 每个场景:`#### 场景:<名称>` 使用 当/那么(WHEN/THEN)格式 | ||
| - **关键提示**:场景标题必须恰好使用 4 个井号 (`####`)。使用 3 个井号或列表将导致处理失败。 | ||
@@ -54,3 +54,3 @@ - 每个需求必须至少有一个场景。 | ||
| 1. 在项目的 specs/<capability>/spec.md 中找到现有的需求 | ||
| 2. 复制整个需求块(从 `### 需求:` 到所有场景) | ||
| 2. 复制整个需求块(从 `### 需求:` 到所有场景) | ||
| 3. 粘贴到 `## 修改需求` 下并编辑以反映新行为 | ||
@@ -65,6 +65,6 @@ 4. 确保标题文本完全匹配(空格不敏感) | ||
| ### 需求:用户可以导出数据 | ||
| ### 需求:用户可以导出数据 | ||
| 系统应当允许用户以 CSV 格式导出其数据。 | ||
| #### 场景:成功导出 | ||
| #### 场景:成功导出 | ||
| - **当** 用户点击“导出”按钮 | ||
@@ -75,3 +75,3 @@ - **那么** 系统下载包含所有用户数据的 CSV 文件 | ||
| ### 需求:旧版导出 | ||
| ### 需求:旧版导出 | ||
| **Reason**: 被新导出系统取代 | ||
@@ -78,0 +78,0 @@ **Migration**: 使用 /api/v2/export 处的新导出端点 |
| ## 新增需求 | ||
| ### 需求:<!-- 需求名称 --> | ||
| ### 需求:<!-- 需求名称 --> | ||
| <!-- 需求描述文本(必须包含"必须"、"禁止"等关键词) --> | ||
| #### 场景:<!-- 场景名称 --> | ||
| #### 场景:<!-- 场景名称 --> | ||
| - **当** <!-- 条件 --> | ||
@@ -8,0 +8,0 @@ - **那么** <!-- 预期结果 --> |
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
875252
9.18%270
16.38%21080
8.39%1
Infinity%92
6.98%