@fission-ai/openspec 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.js +60 -9
- package/dist/commands/change.d.ts +24 -0
- package/dist/commands/change.js +223 -0
- package/dist/commands/config.js +14 -3
- package/dist/commands/feedback.js +28 -7
- package/dist/commands/schema.d.ts +5 -0
- package/dist/commands/schema.js +201 -35
- package/dist/commands/show.js +1 -1
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +76 -12
- package/dist/core/archive.js +64 -7
- package/dist/core/artifact-graph/instruction-loader.js +2 -7
- package/dist/core/artifact-graph/outputs.d.ts +4 -0
- package/dist/core/artifact-graph/outputs.js +7 -0
- package/dist/core/artifact-graph/resolver.js +15 -9
- package/dist/core/available-tools.js +16 -1
- package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -2
- package/dist/core/command-generation/adapters/opencode.d.ts +2 -1
- package/dist/core/command-generation/adapters/opencode.js +13 -2
- package/dist/core/completion-tip.d.ts +15 -0
- package/dist/core/completion-tip.js +133 -0
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/completions/factory.d.ts +11 -0
- package/dist/core/completions/generators/fish-generator.d.ts +12 -0
- package/dist/core/completions/generators/fish-generator.js +79 -47
- package/dist/core/completions/installers/bash-installer.d.ts +7 -0
- package/dist/core/completions/installers/bash-installer.js +15 -0
- package/dist/core/completions/installers/fish-installer.d.ts +7 -0
- package/dist/core/completions/installers/fish-installer.js +15 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +7 -0
- package/dist/core/completions/installers/powershell-installer.js +15 -0
- package/dist/core/completions/templates/fish-templates.d.ts +1 -1
- package/dist/core/completions/templates/fish-templates.js +107 -10
- package/dist/core/completions/types.d.ts +4 -0
- package/dist/core/config-prompts.js +19 -10
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +2 -0
- package/dist/core/config.js +7 -1
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +117 -14
- package/dist/core/legacy-cleanup.js +6 -0
- package/dist/core/migration.js +42 -12
- package/dist/core/parsers/requirement-blocks.d.ts +1 -0
- package/dist/core/parsers/requirement-blocks.js +2 -0
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +11 -2
- package/dist/core/shared/tool-detection.js +9 -3
- package/dist/core/shared-skill-target.d.ts +13 -0
- package/dist/core/shared-skill-target.js +43 -1
- package/dist/core/specs-apply.js +16 -8
- package/dist/core/templates/workflows/explore.js +79 -73
- package/dist/core/templates/workflows/feedback.js +1 -0
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/update.js +39 -26
- package/dist/core/validation/constants.d.ts +3 -0
- package/dist/core/validation/constants.js +12 -0
- package/dist/core/validation/purpose-placeholder.d.ts +45 -0
- package/dist/core/validation/purpose-placeholder.js +117 -0
- package/dist/core/validation/validator.js +16 -1
- package/dist/telemetry/index.js +3 -2
- package/dist/utils/change-metadata.js +17 -13
- package/dist/utils/change-utils.js +5 -0
- package/dist/utils/requirement-diff.d.ts +42 -0
- package/dist/utils/requirement-diff.js +61 -0
- package/package.json +4 -6
- package/schemas/spec-driven/schema.yaml +17 -7
- package/scripts/postinstall.js +0 -83
|
@@ -110,6 +110,21 @@ export class PowerShellInstaller {
|
|
|
110
110
|
const profileDir = path.dirname(profilePath);
|
|
111
111
|
return path.join(profileDir, 'OpenSpecCompletion.ps1');
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if a completion script is currently installed.
|
|
115
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
116
|
+
*
|
|
117
|
+
* @returns true if the completion script exists
|
|
118
|
+
*/
|
|
119
|
+
async isInstalled() {
|
|
120
|
+
try {
|
|
121
|
+
// stat, not access: a directory at the install path is not a script.
|
|
122
|
+
return (await fs.stat(this.getInstallationPath())).isFile();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
113
128
|
/**
|
|
114
129
|
* Backup an existing completion file if it exists
|
|
115
130
|
*
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Static template strings for Fish completion scripts.
|
|
3
3
|
* These are Fish-specific helper functions that never change.
|
|
4
4
|
*/
|
|
5
|
-
export declare const FISH_STATIC_HELPERS = "# Helper function to
|
|
5
|
+
export declare const FISH_STATIC_HELPERS = "# Helper function to match the command path at the start of the invocation\nfunction __fish_openspec_using_command_path\n set -l expected\n set -l value_flags\n set -l reading_value_flags 0\n for argument in $argv\n if test \"$argument\" = --\n set reading_value_flags 1\n continue\n end\n if test $reading_value_flags -eq 1\n set -a value_flags $argument\n else\n set -a expected $argument\n end\n end\n\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l path\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test \"$token\" = --no-color\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n set -a path $token\n if test (count $path) -eq (count $expected)\n break\n end\n end\n test (count $path) -eq (count $expected); or return 1\n for index in (seq (count $expected))\n test \"$path[$index]\" = \"$expected[$index]\"; or return 1\n end\n return 0\nend\n\nfunction __fish_openspec_no_subcommand\n set -l tokens (commandline -opc)\n set -e tokens[1]\n for token in $tokens\n if test \"$token\" != --no-color\n return 1\n end\n end\n return 0\nend\n\nfunction __fish_openspec_completing_option_value\n set -l current (commandline -ct)\n for option in $argv\n if string match -q -- \"$option=*\" \"$current\"\n return 0\n end\n end\n\n set -l tokens (commandline -opc)\n test (count $tokens) -gt 0; or return 1\n contains -- $tokens[-1] $argv\nend\n\nfunction __fish_openspec_complete_attached_short_path\n set -l option $argv[1]\n set -l current (commandline -ct)\n test \"$current\" != \"$option\"; or return 1\n string match -q -- \"$option*\" \"$current\"; or return 1\n set -l value (string sub -s (math (string length -- \"$option\") + 1) -- \"$current\")\n __fish_complete_path \"$value\"\nend\n\nfunction __fish_openspec_positional_index\n set -l target $argv[1]\n set -l depth $argv[2]\n set -l value_flags $argv[3..]\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l count 0\n set -l options 1\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test $options -eq 1\n if test \"$token\" = --\n set options 0\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n end\n set count (math $count + 1)\n end\n test $skip -eq 0; or return 1\n test $count -eq (math $target + $depth)\nend";
|
|
6
6
|
export declare const FISH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\nfunction __fish_openspec_changes\n openspec __complete changes 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_specs\n openspec __complete specs 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_items\n __fish_openspec_changes\n __fish_openspec_specs\nend\n\nfunction __fish_openspec_schemas\n openspec __complete schemas 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend";
|
|
7
7
|
//# sourceMappingURL=fish-templates.d.ts.map
|
|
@@ -2,21 +2,118 @@
|
|
|
2
2
|
* Static template strings for Fish completion scripts.
|
|
3
3
|
* These are Fish-specific helper functions that never change.
|
|
4
4
|
*/
|
|
5
|
-
export const FISH_STATIC_HELPERS = `# Helper function to
|
|
6
|
-
function
|
|
7
|
-
set -l
|
|
8
|
-
set -
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export const FISH_STATIC_HELPERS = `# Helper function to match the command path at the start of the invocation
|
|
6
|
+
function __fish_openspec_using_command_path
|
|
7
|
+
set -l expected
|
|
8
|
+
set -l value_flags
|
|
9
|
+
set -l reading_value_flags 0
|
|
10
|
+
for argument in $argv
|
|
11
|
+
if test "$argument" = --
|
|
12
|
+
set reading_value_flags 1
|
|
13
|
+
continue
|
|
14
|
+
end
|
|
15
|
+
if test $reading_value_flags -eq 1
|
|
16
|
+
set -a value_flags $argument
|
|
17
|
+
else
|
|
18
|
+
set -a expected $argument
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
|
-
|
|
21
|
+
|
|
22
|
+
set -l tokens (commandline -opc)
|
|
23
|
+
set -e tokens[1]
|
|
24
|
+
set -l path
|
|
25
|
+
set -l skip 0
|
|
26
|
+
for token in $tokens
|
|
27
|
+
if test $skip -eq 1
|
|
28
|
+
set skip 0
|
|
29
|
+
continue
|
|
30
|
+
end
|
|
31
|
+
if test "$token" = --no-color
|
|
32
|
+
continue
|
|
33
|
+
end
|
|
34
|
+
if contains -- $token $value_flags
|
|
35
|
+
set skip 1
|
|
36
|
+
continue
|
|
37
|
+
end
|
|
38
|
+
if string match -q -- '-*' $token
|
|
39
|
+
continue
|
|
40
|
+
end
|
|
41
|
+
set -a path $token
|
|
42
|
+
if test (count $path) -eq (count $expected)
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
test (count $path) -eq (count $expected); or return 1
|
|
47
|
+
for index in (seq (count $expected))
|
|
48
|
+
test "$path[$index]" = "$expected[$index]"; or return 1
|
|
49
|
+
end
|
|
50
|
+
return 0
|
|
15
51
|
end
|
|
16
52
|
|
|
17
53
|
function __fish_openspec_no_subcommand
|
|
18
|
-
set -l
|
|
19
|
-
|
|
54
|
+
set -l tokens (commandline -opc)
|
|
55
|
+
set -e tokens[1]
|
|
56
|
+
for token in $tokens
|
|
57
|
+
if test "$token" != --no-color
|
|
58
|
+
return 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function __fish_openspec_completing_option_value
|
|
65
|
+
set -l current (commandline -ct)
|
|
66
|
+
for option in $argv
|
|
67
|
+
if string match -q -- "$option=*" "$current"
|
|
68
|
+
return 0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
set -l tokens (commandline -opc)
|
|
73
|
+
test (count $tokens) -gt 0; or return 1
|
|
74
|
+
contains -- $tokens[-1] $argv
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function __fish_openspec_complete_attached_short_path
|
|
78
|
+
set -l option $argv[1]
|
|
79
|
+
set -l current (commandline -ct)
|
|
80
|
+
test "$current" != "$option"; or return 1
|
|
81
|
+
string match -q -- "$option*" "$current"; or return 1
|
|
82
|
+
set -l value (string sub -s (math (string length -- "$option") + 1) -- "$current")
|
|
83
|
+
__fish_complete_path "$value"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
function __fish_openspec_positional_index
|
|
87
|
+
set -l target $argv[1]
|
|
88
|
+
set -l depth $argv[2]
|
|
89
|
+
set -l value_flags $argv[3..]
|
|
90
|
+
set -l tokens (commandline -opc)
|
|
91
|
+
set -e tokens[1]
|
|
92
|
+
set -l count 0
|
|
93
|
+
set -l options 1
|
|
94
|
+
set -l skip 0
|
|
95
|
+
for token in $tokens
|
|
96
|
+
if test $skip -eq 1
|
|
97
|
+
set skip 0
|
|
98
|
+
continue
|
|
99
|
+
end
|
|
100
|
+
if test $options -eq 1
|
|
101
|
+
if test "$token" = --
|
|
102
|
+
set options 0
|
|
103
|
+
continue
|
|
104
|
+
end
|
|
105
|
+
if contains -- $token $value_flags
|
|
106
|
+
set skip 1
|
|
107
|
+
continue
|
|
108
|
+
end
|
|
109
|
+
if string match -q -- '-*' $token
|
|
110
|
+
continue
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
set count (math $count + 1)
|
|
114
|
+
end
|
|
115
|
+
test $skip -eq 0; or return 1
|
|
116
|
+
test $count -eq (math $target + $depth)
|
|
20
117
|
end`;
|
|
21
118
|
export const FISH_DYNAMIC_HELPERS = `# Dynamic completion helpers
|
|
22
119
|
|
|
@@ -19,6 +19,10 @@ export interface FlagDefinition {
|
|
|
19
19
|
* Whether the flag takes an argument value
|
|
20
20
|
*/
|
|
21
21
|
takesValue?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Completion type for the flag value.
|
|
24
|
+
*/
|
|
25
|
+
completionType?: PositionalType;
|
|
22
26
|
/**
|
|
23
27
|
* Possible values for the flag (for completion suggestions)
|
|
24
28
|
*/
|
|
@@ -9,16 +9,25 @@ export function serializeConfig(config) {
|
|
|
9
9
|
// Schema (required)
|
|
10
10
|
lines.push(`schema: ${config.schema}`);
|
|
11
11
|
lines.push('');
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
if (config.context !== undefined) {
|
|
13
|
+
lines.push('context: |');
|
|
14
|
+
for (const line of config.context.split('\n')) {
|
|
15
|
+
lines.push(` ${line}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push('');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Context section with comments
|
|
21
|
+
lines.push('# Project context (optional)');
|
|
22
|
+
lines.push('# This is shown to AI when creating artifacts.');
|
|
23
|
+
lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
|
|
24
|
+
lines.push('# Example:');
|
|
25
|
+
lines.push('# context: |');
|
|
26
|
+
lines.push('# Tech stack: TypeScript, React, Node.js');
|
|
27
|
+
lines.push('# We use conventional commits');
|
|
28
|
+
lines.push('# Domain: e-commerce platform');
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
22
31
|
// Rules section with comments
|
|
23
32
|
lines.push('# Per-artifact rules (optional)');
|
|
24
33
|
lines.push('# Add custom rules for specific artifacts.');
|
|
@@ -19,6 +19,7 @@ export declare const GlobalConfigSchema: z.ZodObject<{
|
|
|
19
19
|
telemetry: z.ZodOptional<z.ZodObject<{
|
|
20
20
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
21
21
|
}, z.core.$loose>>;
|
|
22
|
+
completionTipSeen: z.ZodOptional<z.ZodBoolean>;
|
|
22
23
|
}, z.core.$loose>;
|
|
23
24
|
export type GlobalConfigType = z.infer<typeof GlobalConfigSchema>;
|
|
24
25
|
/**
|
package/dist/core/config.js
CHANGED
|
@@ -19,7 +19,12 @@ export const OPENSPEC_MARKERS = {
|
|
|
19
19
|
};
|
|
20
20
|
export const AI_TOOLS = [
|
|
21
21
|
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true },
|
|
22
|
-
|
|
22
|
+
// Antigravity moved workspace skills and workflows from `.agent` to the
|
|
23
|
+
// shared `.agents` root in v1.20.5. Detection keys off `.agent` and
|
|
24
|
+
// `.agents/workflows` rather than the bare `.agents` root: that root is
|
|
25
|
+
// shared with Codex, Zed, and the vendor-neutral target, so its presence
|
|
26
|
+
// alone says nothing about Antigravity.
|
|
27
|
+
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agents', legacySkillsDirs: ['.agent'], detectionPaths: ['.agent', '.agents/workflows'], requiresIdeRestart: true },
|
|
23
28
|
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
|
|
24
29
|
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
|
|
25
30
|
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
|
|
@@ -54,6 +59,7 @@ export const AI_TOOLS = [
|
|
|
54
59
|
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
|
|
55
60
|
{ name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true },
|
|
56
61
|
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true },
|
|
62
|
+
{ name: 'Zed Agent', value: 'zed', available: true, successLabel: 'Zed Agent', skillsDir: '.agents', detectionPaths: ['.zed', '.agents/skills'] },
|
|
57
63
|
{ name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
|
|
58
64
|
// Vendor-neutral target for assistants that read the shared `.agents` root.
|
|
59
65
|
// Detection keys off `.agents/skills` rather than the bare root: frameworks use
|
|
@@ -26,6 +26,8 @@ export interface GlobalConfig {
|
|
|
26
26
|
openers?: unknown;
|
|
27
27
|
/** Anonymous usage analytics settings and identity. */
|
|
28
28
|
telemetry?: TelemetryConfig;
|
|
29
|
+
/** Whether the first-run shell-completions tip has been shown. */
|
|
30
|
+
completionTipSeen?: boolean;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Gets the global configuration directory path following XDG Base Directory Specification.
|
package/dist/core/init.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
type InitCommandOptions = {
|
|
8
8
|
tools?: string;
|
|
9
|
+
language?: string;
|
|
9
10
|
force?: boolean;
|
|
10
11
|
interactive?: boolean;
|
|
11
12
|
profile?: string;
|
|
@@ -20,6 +21,7 @@ type InitCommandOptions = {
|
|
|
20
21
|
};
|
|
21
22
|
export declare class InitCommand {
|
|
22
23
|
private readonly toolsArg?;
|
|
24
|
+
private readonly language?;
|
|
23
25
|
private readonly force;
|
|
24
26
|
private readonly interactiveOption?;
|
|
25
27
|
private readonly profileOverride?;
|
|
@@ -85,6 +87,9 @@ export declare class InitCommand {
|
|
|
85
87
|
* @returns Created, refreshed, and failed tools plus removed artifact counts
|
|
86
88
|
*/
|
|
87
89
|
private generateSkillsAndCommands;
|
|
90
|
+
private normalizeLanguage;
|
|
91
|
+
private languageContext;
|
|
92
|
+
private assertLanguageCanBeApplied;
|
|
88
93
|
private createConfig;
|
|
89
94
|
private displaySuccessMessage;
|
|
90
95
|
private startSpinner;
|
package/dist/core/init.js
CHANGED
|
@@ -10,7 +10,7 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
-
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
13
|
+
import { classifyOpenSpecDir, MAX_CONTEXT_SIZE, readProjectConfig, storePointerProblem, } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
15
|
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
16
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
@@ -23,7 +23,7 @@ import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandCont
|
|
|
23
23
|
import { getGlobalConfig } from './global-config.js';
|
|
24
24
|
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
25
25
|
import { getAvailableTools } from './available-tools.js';
|
|
26
|
-
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
26
|
+
import { resolveSharedSkillWriters, sharedSkillRootOwner, writeSharedSkillTarget, } from './shared-skill-target.js';
|
|
27
27
|
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
28
28
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
29
29
|
import { writeCopilotCloudFiles, readCopilotCloudOptIn, hasExistingManagedCloudFiles, persistCopilotCloudOptIn, removeCopilotCloudFiles, findUnmanagedCloudFiles, listManagedCloudFiles, } from './github-copilot/cloud-agent.js';
|
|
@@ -33,6 +33,13 @@ const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
|
33
33
|
// Constants
|
|
34
34
|
// -----------------------------------------------------------------------------
|
|
35
35
|
const DEFAULT_SCHEMA = 'spec-driven';
|
|
36
|
+
function formatLanguageContext(language) {
|
|
37
|
+
return [
|
|
38
|
+
`Language: ${language}`,
|
|
39
|
+
`All artifacts must be written in ${language}.`,
|
|
40
|
+
'Keep OpenSpec structural headings and SHALL/MUST keywords in English.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
36
43
|
const PROGRESS_SPINNER = {
|
|
37
44
|
interval: 80,
|
|
38
45
|
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
@@ -56,6 +63,7 @@ const WORKFLOW_TO_SKILL_DIR = {
|
|
|
56
63
|
// -----------------------------------------------------------------------------
|
|
57
64
|
export class InitCommand {
|
|
58
65
|
toolsArg;
|
|
66
|
+
language;
|
|
59
67
|
force;
|
|
60
68
|
interactiveOption;
|
|
61
69
|
profileOverride;
|
|
@@ -63,6 +71,7 @@ export class InitCommand {
|
|
|
63
71
|
copilotCloudOption;
|
|
64
72
|
constructor(options = {}) {
|
|
65
73
|
this.toolsArg = options.tools;
|
|
74
|
+
this.language = this.normalizeLanguage(options.language);
|
|
66
75
|
this.force = options.force ?? false;
|
|
67
76
|
this.interactiveOption = options.interactive;
|
|
68
77
|
this.profileOverride = options.profile;
|
|
@@ -97,6 +106,7 @@ export class InitCommand {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
}
|
|
109
|
+
await this.assertLanguageCanBeApplied(projectPath, openspecPath);
|
|
100
110
|
// Check for legacy artifacts and handle cleanup
|
|
101
111
|
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
|
|
102
112
|
// Migrate OpenSpec-managed skills left in renamed tool directories
|
|
@@ -516,14 +526,8 @@ export class InitCommand {
|
|
|
516
526
|
return deduped;
|
|
517
527
|
}
|
|
518
528
|
validateTools(toolIds, toolStates, projectPath) {
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
? toolIds.filter((toolId) => toolId !== 'agents')
|
|
522
|
-
: toolIds;
|
|
523
|
-
if (reconciledToolIds.length !== toolIds.length) {
|
|
524
|
-
console.log(chalk.dim('Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'));
|
|
525
|
-
}
|
|
526
|
-
for (const toolId of reconciledToolIds) {
|
|
529
|
+
const selectedTools = [];
|
|
530
|
+
for (const toolId of toolIds) {
|
|
527
531
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
528
532
|
if (!tool) {
|
|
529
533
|
const validToolIds = getToolsWithSkillsDir();
|
|
@@ -533,6 +537,48 @@ export class InitCommand {
|
|
|
533
537
|
const validToolsWithSkills = getToolsWithSkillsDir();
|
|
534
538
|
throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
|
|
535
539
|
}
|
|
540
|
+
selectedTools.push(tool);
|
|
541
|
+
}
|
|
542
|
+
// A selected tool may share its physical skills root with an already
|
|
543
|
+
// configured owner. Include that owner in the refresh without dropping the
|
|
544
|
+
// selected tool: it may still have an independent command surface.
|
|
545
|
+
const generationTools = [...selectedTools];
|
|
546
|
+
const delivery = getGlobalConfig().delivery ?? 'both';
|
|
547
|
+
for (const selected of selectedTools) {
|
|
548
|
+
if (!selected.skillsDir)
|
|
549
|
+
continue;
|
|
550
|
+
const selectedOwner = selected.value === 'codex' ||
|
|
551
|
+
!shouldGenerateSkillsForTool(selected.value, delivery)
|
|
552
|
+
? undefined
|
|
553
|
+
: sharedSkillRootOwner(projectPath, selected.value);
|
|
554
|
+
for (const candidate of AI_TOOLS) {
|
|
555
|
+
if (candidate.skillsDir === selected.skillsDir &&
|
|
556
|
+
toolStates.get(candidate.value)?.configured &&
|
|
557
|
+
candidate.value === selectedOwner &&
|
|
558
|
+
!generationTools.includes(candidate)) {
|
|
559
|
+
generationTools.push(candidate);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const skillWriters = resolveSharedSkillWriters(projectPath, generationTools);
|
|
564
|
+
const sharedRoots = new Map();
|
|
565
|
+
for (const tool of generationTools) {
|
|
566
|
+
if (!tool.skillsDir)
|
|
567
|
+
continue;
|
|
568
|
+
const group = sharedRoots.get(tool.skillsDir) ?? [];
|
|
569
|
+
group.push(tool);
|
|
570
|
+
sharedRoots.set(tool.skillsDir, group);
|
|
571
|
+
}
|
|
572
|
+
for (const [root, group] of sharedRoots) {
|
|
573
|
+
if (group.length < 2)
|
|
574
|
+
continue;
|
|
575
|
+
const owner = group.find((tool) => skillWriters.has(tool.value));
|
|
576
|
+
console.log(chalk.dim(`${group.map((tool) => tool.name).join(', ')} share ${root}/skills; writing one tree for ${owner?.value}.`));
|
|
577
|
+
}
|
|
578
|
+
const validatedTools = [];
|
|
579
|
+
for (const tool of generationTools) {
|
|
580
|
+
if (!toolSupportsSkills(tool))
|
|
581
|
+
continue;
|
|
536
582
|
const preState = toolStates.get(tool.value);
|
|
537
583
|
const skillsPath = resolveToolSkillsDir(projectPath, tool);
|
|
538
584
|
const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
|
|
@@ -545,6 +591,7 @@ export class InitCommand {
|
|
|
545
591
|
isGlobalSkillTarget,
|
|
546
592
|
wasConfigured: preState?.configured ?? false,
|
|
547
593
|
requiresIdeRestart: tool.requiresIdeRestart,
|
|
594
|
+
writesSkills: !tool.skillsDir || skillWriters.has(tool.value),
|
|
548
595
|
});
|
|
549
596
|
}
|
|
550
597
|
return validatedTools;
|
|
@@ -618,7 +665,7 @@ export class InitCommand {
|
|
|
618
665
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
619
666
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
620
667
|
// Generate skill files if the selected delivery and tool capability allow skills
|
|
621
|
-
if (shouldGenerateSkills) {
|
|
668
|
+
if (shouldGenerateSkills && tool.writesSkills) {
|
|
622
669
|
// Create skill directories and SKILL.md files
|
|
623
670
|
for (const { template, dirName } of skillTemplates) {
|
|
624
671
|
const skillDir = path.join(tool.skillsPath, dirName);
|
|
@@ -632,7 +679,9 @@ export class InitCommand {
|
|
|
632
679
|
}
|
|
633
680
|
writeSharedSkillTarget(projectPath, tool.value);
|
|
634
681
|
}
|
|
635
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
682
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
683
|
+
tool.writesSkills &&
|
|
684
|
+
!tool.isGlobalSkillTarget) {
|
|
636
685
|
removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
|
|
637
686
|
// Retain an explicit selection even when this delivery mode produces
|
|
638
687
|
// no skills, so a divergent legacy sibling cannot reclaim ownership.
|
|
@@ -699,6 +748,53 @@ export class InitCommand {
|
|
|
699
748
|
// ═══════════════════════════════════════════════════════════
|
|
700
749
|
// CONFIG FILE
|
|
701
750
|
// ═══════════════════════════════════════════════════════════
|
|
751
|
+
normalizeLanguage(language) {
|
|
752
|
+
if (language === undefined)
|
|
753
|
+
return undefined;
|
|
754
|
+
const normalized = language.trim();
|
|
755
|
+
if (!normalized) {
|
|
756
|
+
throw new Error('The --language option requires a non-empty value.');
|
|
757
|
+
}
|
|
758
|
+
if (/\p{Cc}|\p{Bidi_Control}|[\u200B\u2028\u2029\uFEFF]/u.test(normalized)) {
|
|
759
|
+
throw new Error('The --language option must be a single line without control or invisible formatting characters.');
|
|
760
|
+
}
|
|
761
|
+
const serializedContext = `${formatLanguageContext(normalized)}\n`;
|
|
762
|
+
if (Buffer.byteLength(serializedContext, 'utf8') > MAX_CONTEXT_SIZE) {
|
|
763
|
+
throw new Error(`The --language option is too long for OpenSpec's ${MAX_CONTEXT_SIZE / 1024}KB project context limit.`);
|
|
764
|
+
}
|
|
765
|
+
return normalized;
|
|
766
|
+
}
|
|
767
|
+
languageContext() {
|
|
768
|
+
if (!this.language)
|
|
769
|
+
return undefined;
|
|
770
|
+
return formatLanguageContext(this.language);
|
|
771
|
+
}
|
|
772
|
+
async assertLanguageCanBeApplied(projectPath, openspecPath) {
|
|
773
|
+
const languageContext = this.languageContext();
|
|
774
|
+
if (!languageContext)
|
|
775
|
+
return;
|
|
776
|
+
const configPath = path.join(openspecPath, 'config.yaml');
|
|
777
|
+
const hasConfig = fs.existsSync(configPath) ||
|
|
778
|
+
fs.existsSync(path.join(openspecPath, 'config.yml'));
|
|
779
|
+
if (!hasConfig) {
|
|
780
|
+
try {
|
|
781
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, configPath);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
const reason = error instanceof Error ? `: ${error.message}` : '';
|
|
785
|
+
throw new Error(`Cannot create openspec/config.yaml for --language${reason}`);
|
|
786
|
+
}
|
|
787
|
+
if (!(await FileSystemUtils.canWriteFile(configPath))) {
|
|
788
|
+
throw new Error('Cannot create openspec/config.yaml for --language: the destination is not writable.');
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const existingContext = readProjectConfig(projectPath)?.context;
|
|
793
|
+
if (existingContext?.includes(languageContext))
|
|
794
|
+
return;
|
|
795
|
+
throw new Error('--language does not overwrite an existing OpenSpec config. ' +
|
|
796
|
+
'Add the language instruction to its context field instead.');
|
|
797
|
+
}
|
|
702
798
|
async createConfig(openspecPath, extendMode) {
|
|
703
799
|
const configPath = path.join(openspecPath, 'config.yaml');
|
|
704
800
|
const configYmlPath = path.join(openspecPath, 'config.yml');
|
|
@@ -708,12 +804,19 @@ export class InitCommand {
|
|
|
708
804
|
return 'exists';
|
|
709
805
|
}
|
|
710
806
|
try {
|
|
711
|
-
const yamlContent = serializeConfig({
|
|
807
|
+
const yamlContent = serializeConfig({
|
|
808
|
+
schema: DEFAULT_SCHEMA,
|
|
809
|
+
context: this.languageContext(),
|
|
810
|
+
});
|
|
712
811
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
|
|
713
812
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
714
813
|
return 'created';
|
|
715
814
|
}
|
|
716
|
-
catch {
|
|
815
|
+
catch (error) {
|
|
816
|
+
if (this.language) {
|
|
817
|
+
const reason = error instanceof Error ? `: ${error.message}` : '';
|
|
818
|
+
throw new Error(`Failed to create openspec/config.yaml for --language${reason}`);
|
|
819
|
+
}
|
|
717
820
|
return 'skipped';
|
|
718
821
|
}
|
|
719
822
|
}
|
|
@@ -51,6 +51,12 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
51
51
|
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
|
|
52
52
|
'opencode': { type: 'files', pattern: ['.opencode/command/opsx-*.md', '.opencode/command/openspec-*.md'] },
|
|
53
53
|
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
54
|
+
// Scoped to the pre-opsx filenames under Antigravity's former `.agent` root.
|
|
55
|
+
// The current `.agents/workflows/opsx-*.md` files postdate that rename, and
|
|
56
|
+
// the `.agent` copies of them are relocated by LEGACY_TOOL_ROOTS, which
|
|
57
|
+
// preserves a customized file instead of deleting it. These patterns are
|
|
58
|
+
// matched in every project, so a shared root like `.agents` is not listed:
|
|
59
|
+
// OpenSpec never wrote `openspec-*` files there, and a user might have.
|
|
54
60
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
55
61
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
56
62
|
'qwen': { type: 'files', pattern: ['.qwen/commands/opsx-*.toml', '.qwen/commands/openspec-*.toml'] },
|
package/dist/core/migration.js
CHANGED
|
@@ -34,6 +34,12 @@ export const LEGACY_TOOL_ROOTS = {
|
|
|
34
34
|
// Codex now reads the canonical shared .agents root. Generate the current
|
|
35
35
|
// replacement first so a divergent legacy file is preserved, not overwritten.
|
|
36
36
|
codex: [{ root: '.codex', needsConsent: false, timing: 'after-generation' }],
|
|
37
|
+
// Antigravity v1.20.5 moved workspace skills and workflows to `.agents` and
|
|
38
|
+
// reads the old `.agent` root only as a fallback, so leaving a copy there
|
|
39
|
+
// just gives the agent two of everything. Same after-generation timing as
|
|
40
|
+
// Codex: the replacement is written first, so a divergent legacy file is
|
|
41
|
+
// reported and kept rather than overwritten.
|
|
42
|
+
antigravity: [{ root: '.agent', needsConsent: false, timing: 'after-generation' }],
|
|
37
43
|
};
|
|
38
44
|
function classifyManagedFile(source, destination) {
|
|
39
45
|
if (isSamePath(source, destination))
|
|
@@ -121,7 +127,7 @@ function collectLegacyToolMigrations(projectPath, apply, toolIds, timing = 'befo
|
|
|
121
127
|
continue;
|
|
122
128
|
}
|
|
123
129
|
const skills = migrateSkillDirs(projectPath, tool.skillsDir, legacy.root, apply, legacyTiming === 'after-generation');
|
|
124
|
-
const commands = migrateCommandFiles(projectPath, tool, legacy.root, apply);
|
|
130
|
+
const commands = migrateCommandFiles(projectPath, tool, legacy.root, apply, legacyTiming === 'after-generation');
|
|
125
131
|
if (apply) {
|
|
126
132
|
removeDirIfEmpty(path.join(legacyRootPath, 'skills'));
|
|
127
133
|
removeDirIfEmpty(path.join(legacyRootPath, 'workflows'));
|
|
@@ -201,7 +207,7 @@ function migrateSkillDirs(projectPath, currentRoot, legacyRoot, apply, requireDe
|
|
|
201
207
|
}
|
|
202
208
|
return { moved, kept };
|
|
203
209
|
}
|
|
204
|
-
function migrateCommandFiles(projectPath, tool, legacyRoot, apply) {
|
|
210
|
+
function migrateCommandFiles(projectPath, tool, legacyRoot, apply, requireDestination = false) {
|
|
205
211
|
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
206
212
|
if (!adapter || !tool.skillsDir)
|
|
207
213
|
return { moved: 0, kept: 0 };
|
|
@@ -212,10 +218,16 @@ function migrateCommandFiles(projectPath, tool, legacyRoot, apply) {
|
|
|
212
218
|
const legacyPath = legacyCommandPath(currentPath, tool.skillsDir, legacyRoot);
|
|
213
219
|
if (!legacyPath)
|
|
214
220
|
continue;
|
|
215
|
-
const source =
|
|
221
|
+
const source = FileSystemUtils.resolveProjectArtifactPath(projectPath, legacyPath);
|
|
216
222
|
if (!fs.existsSync(source))
|
|
217
223
|
continue;
|
|
218
|
-
const destination =
|
|
224
|
+
const destination = FileSystemUtils.resolveProjectArtifactPath(projectPath, currentPath.split(/[\\/]/).join(path.sep));
|
|
225
|
+
// An after-generation move runs once the tool has written its replacement.
|
|
226
|
+
// No replacement means this command is not one OpenSpec installs now — a
|
|
227
|
+
// skills-only delivery or a deselected workflow — so relocating the legacy
|
|
228
|
+
// file would resurrect it under the current root.
|
|
229
|
+
if (requireDestination && !fs.existsSync(destination))
|
|
230
|
+
continue;
|
|
219
231
|
if (!areProjectArtifacts(projectPath, source, destination)) {
|
|
220
232
|
console.warn(`Skipping legacy ${legacyPath} migration because it resolves outside this project.`);
|
|
221
233
|
continue;
|
|
@@ -332,7 +344,7 @@ function removeDirIfEmpty(dirPath) {
|
|
|
332
344
|
// Missing or non-empty directory — nothing to do
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
|
-
function scanInstalledWorkflowArtifacts(projectPath, tools,
|
|
347
|
+
function scanInstalledWorkflowArtifacts(projectPath, tools, includeLegacyRoots = false) {
|
|
336
348
|
const installed = new Set();
|
|
337
349
|
let hasSkills = false;
|
|
338
350
|
let hasCommands = false;
|
|
@@ -345,7 +357,7 @@ function scanInstalledWorkflowArtifacts(projectPath, tools, includeLegacySkills
|
|
|
345
357
|
}
|
|
346
358
|
else if (isSharedSkillTargetActive(projectPath, tool.value)) {
|
|
347
359
|
skillsDirs.push(resolveToolSkillsDir(projectPath, tool));
|
|
348
|
-
if (
|
|
360
|
+
if (includeLegacyRoots) {
|
|
349
361
|
skillsDirs.push(...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectPath, root, 'skills')));
|
|
350
362
|
}
|
|
351
363
|
}
|
|
@@ -362,14 +374,32 @@ function scanInstalledWorkflowArtifacts(projectPath, tools, includeLegacySkills
|
|
|
362
374
|
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
363
375
|
if (!adapter)
|
|
364
376
|
continue;
|
|
377
|
+
// A root the tool has moved away from still holds the command files the
|
|
378
|
+
// user installed there. Reading only the current root would report a
|
|
379
|
+
// commands install as skills-only, and delivery inferred from that answer
|
|
380
|
+
// deletes those commands on the next update.
|
|
381
|
+
const legacyRoots = includeLegacyRoots
|
|
382
|
+
? (LEGACY_TOOL_ROOTS[tool.value] ?? []).map((legacy) => legacy.root)
|
|
383
|
+
: [];
|
|
365
384
|
for (const workflowId of ALL_WORKFLOWS) {
|
|
366
385
|
const commandPath = adapter.getFilePath(workflowId);
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
const candidates = [commandPath];
|
|
387
|
+
if (tool.skillsDir) {
|
|
388
|
+
for (const root of legacyRoots) {
|
|
389
|
+
const legacyPath = legacyCommandPath(commandPath, tool.skillsDir, root);
|
|
390
|
+
if (legacyPath)
|
|
391
|
+
candidates.push(legacyPath);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const candidate of candidates) {
|
|
395
|
+
const fullPath = path.isAbsolute(candidate)
|
|
396
|
+
? candidate
|
|
397
|
+
: path.join(projectPath, candidate);
|
|
398
|
+
if (fs.existsSync(fullPath)) {
|
|
399
|
+
installed.add(workflowId);
|
|
400
|
+
hasCommands = true;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
373
403
|
}
|
|
374
404
|
}
|
|
375
405
|
}
|