@fission-ai/openspec 1.6.0 → 1.8.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/README.md +31 -2
- package/dist/cli/index.js +79 -8
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +105 -51
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +110 -31
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/validate.js +20 -5
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +121 -33
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +9 -4
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1332 -155
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +45 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +450 -69
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +18 -0
- package/dist/core/parsers/requirement-blocks.js +120 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +48 -49
- package/dist/core/project-config.d.ts +28 -6
- package/dist/core/project-config.js +108 -8
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +166 -36
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +659 -171
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +48 -158
- package/dist/core/templates/workflows/archive-change.js +170 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
- package/dist/core/templates/workflows/continue-change.js +38 -46
- package/dist/core/templates/workflows/explore.js +54 -18
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +13 -7
- package/dist/core/templates/workflows/propose.js +116 -46
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +260 -32
- package/dist/core/templates/workflows/update-change.js +26 -16
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +389 -93
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +22 -8
- package/dist/core/validation/validator.js +222 -50
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +10 -5
- package/dist/telemetry/index.js +75 -41
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +14 -12
- package/schemas/spec-driven/schema.yaml +67 -10
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +10 -4
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const OPERATION_IDS: readonly ["apply", "archive"];
|
|
3
|
+
export type OperationId = (typeof OPERATION_IDS)[number];
|
|
4
|
+
export interface OperationConfig {
|
|
5
|
+
guidance?: string[];
|
|
6
|
+
}
|
|
7
|
+
export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
|
|
2
8
|
/**
|
|
3
9
|
* Zod schema for project configuration.
|
|
4
10
|
*
|
|
@@ -16,7 +22,18 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
16
22
|
schema: z.ZodString;
|
|
17
23
|
context: z.ZodOptional<z.ZodString>;
|
|
18
24
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
25
|
+
operations: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
apply: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
archive: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
19
33
|
store: z.ZodOptional<z.ZodString>;
|
|
34
|
+
githubCopilot: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
cloudAgent: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
}, z.core.$strip>>;
|
|
20
37
|
}, z.core.$strip>;
|
|
21
38
|
/** Normalized in-memory shape of a referenced store declaration. */
|
|
22
39
|
export interface DeclarationEntry {
|
|
@@ -27,6 +44,11 @@ export interface DeclarationEntry {
|
|
|
27
44
|
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
28
45
|
references?: DeclarationEntry[];
|
|
29
46
|
};
|
|
47
|
+
export interface OperationInputs {
|
|
48
|
+
context?: string;
|
|
49
|
+
operationGuidance?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
|
|
30
52
|
export declare const MAX_CONTEXT_SIZE: number;
|
|
31
53
|
/**
|
|
32
54
|
* Read and parse openspec/config.yaml from project root.
|
|
@@ -49,16 +71,16 @@ export declare const MAX_CONTEXT_SIZE: number;
|
|
|
49
71
|
*/
|
|
50
72
|
export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
|
|
51
73
|
/**
|
|
52
|
-
* Validate artifact IDs in rules against
|
|
53
|
-
*
|
|
54
|
-
*
|
|
74
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
75
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
76
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
77
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
55
78
|
*
|
|
56
79
|
* @param rules - The rules object from config
|
|
57
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
58
|
-
* @param schemaName - Name of the schema for error messages
|
|
80
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
59
81
|
* @returns Array of warning messages for unknown artifact IDs
|
|
60
82
|
*/
|
|
61
|
-
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string
|
|
83
|
+
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
|
|
62
84
|
/**
|
|
63
85
|
* Suggest valid schema names when user provides invalid schema.
|
|
64
86
|
* Uses fuzzy matching to find similar names.
|
|
@@ -2,6 +2,10 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
export const OPERATION_IDS = ['apply', 'archive'];
|
|
6
|
+
const OperationConfigSchema = z.object({
|
|
7
|
+
guidance: z.array(z.string()).optional(),
|
|
8
|
+
});
|
|
5
9
|
/**
|
|
6
10
|
* Zod schema for project configuration.
|
|
7
11
|
*
|
|
@@ -34,6 +38,14 @@ export const ProjectConfigSchema = z.object({
|
|
|
34
38
|
)
|
|
35
39
|
.optional()
|
|
36
40
|
.describe('Per-artifact rules, keyed by artifact ID'),
|
|
41
|
+
// Optional: per-operation advisory guidance, kept separate from artifact rules.
|
|
42
|
+
operations: z
|
|
43
|
+
.object({
|
|
44
|
+
apply: OperationConfigSchema.optional(),
|
|
45
|
+
archive: OperationConfigSchema.optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Per-operation advisory guidance'),
|
|
37
49
|
// Note: the `references` field (id strings or {id, remote} maps) is
|
|
38
50
|
// deliberately absent here — readProjectConfig parses and normalizes
|
|
39
51
|
// it by hand (see DeclarationEntry below); a schema entry nothing
|
|
@@ -45,7 +57,70 @@ export const ProjectConfigSchema = z.object({
|
|
|
45
57
|
.string()
|
|
46
58
|
.optional()
|
|
47
59
|
.describe('Store id used as the OpenSpec root when no local planning shape exists'),
|
|
60
|
+
// Optional: GitHub Copilot integration preferences. `cloudAgent` is the
|
|
61
|
+
// opt-in for generating the Copilot cloud coding-agent files (a GitHub
|
|
62
|
+
// Actions workflow + agent file); absent means "not yet decided".
|
|
63
|
+
githubCopilot: z
|
|
64
|
+
.object({
|
|
65
|
+
cloudAgent: z.boolean().optional(),
|
|
66
|
+
})
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('GitHub Copilot integration preferences'),
|
|
48
69
|
});
|
|
70
|
+
export function loadOperationInputs(projectConfig, operationId) {
|
|
71
|
+
const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
|
|
72
|
+
? projectConfig.context
|
|
73
|
+
: undefined;
|
|
74
|
+
const guidance = projectConfig?.operations?.[operationId]?.guidance;
|
|
75
|
+
const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
|
|
76
|
+
return {
|
|
77
|
+
...(context !== undefined ? { context } : {}),
|
|
78
|
+
...(operationGuidance !== undefined ? { operationGuidance } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseOperations(raw) {
|
|
82
|
+
if (raw === undefined) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
86
|
+
console.warn(`Invalid 'operations' field in config (must be object)`);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const supported = new Set(OPERATION_IDS);
|
|
90
|
+
const operations = {};
|
|
91
|
+
for (const [operationId, value] of Object.entries(raw)) {
|
|
92
|
+
if (!supported.has(operationId)) {
|
|
93
|
+
console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const typedOperationId = operationId;
|
|
97
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
98
|
+
console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const operation = value;
|
|
102
|
+
const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
|
|
103
|
+
if (unknownFields.length > 0) {
|
|
104
|
+
console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
|
|
105
|
+
}
|
|
106
|
+
if (operation.guidance === undefined) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
|
|
110
|
+
if (!guidanceResult.success) {
|
|
111
|
+
console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
|
|
115
|
+
if (guidance.length < guidanceResult.data.length) {
|
|
116
|
+
console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
|
|
117
|
+
}
|
|
118
|
+
if (guidance.length > 0) {
|
|
119
|
+
operations[typedOperationId] = { guidance };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Object.keys(operations).length > 0 ? operations : undefined;
|
|
123
|
+
}
|
|
49
124
|
/**
|
|
50
125
|
* Parser for `references:` declarations: string entries or
|
|
51
126
|
* {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
|
|
@@ -169,7 +244,11 @@ export function readProjectConfig(projectRoot) {
|
|
|
169
244
|
const rulesField = z.record(z.string(), z.array(z.string()));
|
|
170
245
|
// First check if it's an object structure (guard against null since typeof null === 'object')
|
|
171
246
|
if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
|
|
172
|
-
|
|
247
|
+
// Artifact ids are intentionally not restricted to the built-in naming
|
|
248
|
+
// convention, so keys such as "constructor" remain valid for custom
|
|
249
|
+
// schemas. A null-prototype map preserves those keys as data without
|
|
250
|
+
// letting "__proto__" mutate the lookup object's prototype.
|
|
251
|
+
const parsedRules = Object.create(null);
|
|
173
252
|
let hasValidRules = false;
|
|
174
253
|
for (const [artifactId, rules] of Object.entries(raw.rules)) {
|
|
175
254
|
const rulesArrayResult = z.array(z.string()).safeParse(rules);
|
|
@@ -196,6 +275,10 @@ export function readProjectConfig(projectRoot) {
|
|
|
196
275
|
console.warn(`Invalid 'rules' field in config (must be object)`);
|
|
197
276
|
}
|
|
198
277
|
}
|
|
278
|
+
const operations = parseOperations(raw.operations);
|
|
279
|
+
if (operations) {
|
|
280
|
+
config.operations = operations;
|
|
281
|
+
}
|
|
199
282
|
const references = parseDeclarationList(raw.references);
|
|
200
283
|
if (references) {
|
|
201
284
|
config.references = references;
|
|
@@ -211,6 +294,23 @@ export function readProjectConfig(projectRoot) {
|
|
|
211
294
|
console.warn(`Warning: ignoring invalid store: field in ${configPathForWarnings(projectRoot)} (must be a single store id string).`);
|
|
212
295
|
}
|
|
213
296
|
}
|
|
297
|
+
// Parse githubCopilot preferences (only cloudAgent is recognized today).
|
|
298
|
+
if (raw.githubCopilot !== undefined) {
|
|
299
|
+
if (typeof raw.githubCopilot === 'object' &&
|
|
300
|
+
raw.githubCopilot !== null &&
|
|
301
|
+
!Array.isArray(raw.githubCopilot)) {
|
|
302
|
+
const cloudAgent = raw.githubCopilot.cloudAgent;
|
|
303
|
+
if (typeof cloudAgent === 'boolean') {
|
|
304
|
+
config.githubCopilot = { cloudAgent };
|
|
305
|
+
}
|
|
306
|
+
else if (cloudAgent !== undefined) {
|
|
307
|
+
console.warn(`Invalid 'githubCopilot.cloudAgent' field in config (must be a boolean)`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.warn(`Invalid 'githubCopilot' field in config (must be an object)`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
214
314
|
// Return partial config even if some fields failed
|
|
215
315
|
return Object.keys(config).length > 0 ? config : null;
|
|
216
316
|
}
|
|
@@ -223,22 +323,22 @@ function configPathForWarnings(projectRoot) {
|
|
|
223
323
|
return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'openspec', 'config.yaml');
|
|
224
324
|
}
|
|
225
325
|
/**
|
|
226
|
-
* Validate artifact IDs in rules against
|
|
227
|
-
*
|
|
228
|
-
*
|
|
326
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
327
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
328
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
329
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
229
330
|
*
|
|
230
331
|
* @param rules - The rules object from config
|
|
231
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
232
|
-
* @param schemaName - Name of the schema for error messages
|
|
332
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
233
333
|
* @returns Array of warning messages for unknown artifact IDs
|
|
234
334
|
*/
|
|
235
|
-
export function validateConfigRules(rules, validArtifactIds
|
|
335
|
+
export function validateConfigRules(rules, validArtifactIds) {
|
|
236
336
|
const warnings = [];
|
|
237
337
|
for (const artifactId of Object.keys(rules)) {
|
|
238
338
|
if (!validArtifactIds.has(artifactId)) {
|
|
239
339
|
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
|
240
340
|
warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
|
|
241
|
-
`
|
|
341
|
+
`It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
|
|
242
342
|
}
|
|
243
343
|
}
|
|
244
344
|
return warnings;
|
package/dist/core/references.js
CHANGED
|
@@ -53,6 +53,46 @@ function registerFix(id, remote) {
|
|
|
53
53
|
}
|
|
54
54
|
return `Get a checkout from a teammate and run: openspec store register <path> --id ${id}`;
|
|
55
55
|
}
|
|
56
|
+
const WHITESPACE = /\s/;
|
|
57
|
+
/**
|
|
58
|
+
* Drop a CommonMark closing sequence (`## Purpose ##`). The closing run only
|
|
59
|
+
* counts when whitespace precedes it, so `Purpose###` keeps its hashes.
|
|
60
|
+
* Scans from the end so the cost stays linear in the title length.
|
|
61
|
+
*/
|
|
62
|
+
function stripClosingSequence(title) {
|
|
63
|
+
let end = title.length;
|
|
64
|
+
while (end > 0 && WHITESPACE.test(title[end - 1])) {
|
|
65
|
+
end--;
|
|
66
|
+
}
|
|
67
|
+
const hashEnd = end;
|
|
68
|
+
while (end > 0 && title[end - 1] === '#') {
|
|
69
|
+
end--;
|
|
70
|
+
}
|
|
71
|
+
const noClosingRun = end === hashEnd;
|
|
72
|
+
const missingLeadingSpace = end === 0 || !WHITESPACE.test(title[end - 1]);
|
|
73
|
+
if (noClosingRun || missingLeadingSpace) {
|
|
74
|
+
return title.trim();
|
|
75
|
+
}
|
|
76
|
+
return title.slice(0, end).trim();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Heading title, or null when the line is not an ATX heading. Hand-rolled
|
|
80
|
+
* rather than a regex so a title padded with whitespace cannot backtrack.
|
|
81
|
+
*/
|
|
82
|
+
function parseHeadingTitle(line) {
|
|
83
|
+
let level = 0;
|
|
84
|
+
while (level < 6 && level < line.length && line[level] === '#') {
|
|
85
|
+
level++;
|
|
86
|
+
}
|
|
87
|
+
if (level === 0 || level >= line.length || !WHITESPACE.test(line[level])) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let start = level;
|
|
91
|
+
while (start < line.length && WHITESPACE.test(line[start])) {
|
|
92
|
+
start++;
|
|
93
|
+
}
|
|
94
|
+
return stripClosingSequence(line.slice(start));
|
|
95
|
+
}
|
|
56
96
|
/**
|
|
57
97
|
* Tolerant first-Purpose-line extraction. parseSpec() throws on specs
|
|
58
98
|
* without Purpose/Requirements sections; the index must never fail on an
|
|
@@ -79,12 +119,11 @@ export function extractFirstPurposeLine(markdown) {
|
|
|
79
119
|
if (fenceMarker !== null) {
|
|
80
120
|
continue;
|
|
81
121
|
}
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
122
|
+
const title = parseHeadingTitle(line);
|
|
123
|
+
if (title !== null) {
|
|
84
124
|
if (inPurpose) {
|
|
85
125
|
return '';
|
|
86
126
|
}
|
|
87
|
-
const title = heading[2].replace(/\s+#+\s*$/, '').trim();
|
|
88
127
|
inPurpose = title.toLowerCase() === 'purpose';
|
|
89
128
|
continue;
|
|
90
129
|
}
|
|
@@ -152,7 +191,6 @@ export function renderReferencedStoresSection(entries) {
|
|
|
152
191
|
* let hostile content forge instruction lines (slice 6.1 hardening).
|
|
153
192
|
*/
|
|
154
193
|
export function sanitizeInline(value, maxLength = 300) {
|
|
155
|
-
// eslint-disable-next-line no-control-regex
|
|
156
194
|
const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
|
|
157
195
|
return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
|
|
158
196
|
}
|
|
@@ -26,6 +26,10 @@ export interface RelationshipHealth {
|
|
|
26
26
|
remote?: string;
|
|
27
27
|
};
|
|
28
28
|
origin_url?: string;
|
|
29
|
+
drift?: {
|
|
30
|
+
ahead: number;
|
|
31
|
+
behind: number;
|
|
32
|
+
};
|
|
29
33
|
status: StoreDiagnostic[];
|
|
30
34
|
} | null;
|
|
31
35
|
references: ReferenceIndexEntry[];
|
|
@@ -42,6 +46,10 @@ export interface InspectRelationshipsInput {
|
|
|
42
46
|
metadataValid: boolean;
|
|
43
47
|
canonicalRemote?: string;
|
|
44
48
|
originUrl?: string;
|
|
49
|
+
drift?: {
|
|
50
|
+
ahead: number;
|
|
51
|
+
behind: number;
|
|
52
|
+
};
|
|
45
53
|
};
|
|
46
54
|
referenceEntries: ReferenceIndexEntry[];
|
|
47
55
|
registryUnreadable: boolean;
|
|
@@ -37,6 +37,18 @@ export function inspectRelationships(input) {
|
|
|
37
37
|
input.storeFacts.canonicalRemote !== input.storeFacts.originUrl) {
|
|
38
38
|
storeStatus.push(makeStoreDiagnostic('info', 'store_remote_divergence', `The store.yaml remote (${sanitizeInline(input.storeFacts.canonicalRemote, 200)}) differs from the checkout's origin (${sanitizeInline(input.storeFacts.originUrl, 200)}).`, { target: 'store.metadata' }));
|
|
39
39
|
}
|
|
40
|
+
// Checkout behind its upstream tracking ref: a read-only staleness
|
|
41
|
+
// signal, not a version pin — OpenSpec never syncs stores, so this
|
|
42
|
+
// compares against the local upstream ref, not the live remote.
|
|
43
|
+
// Behind means teammates on newer commits may resolve different specs.
|
|
44
|
+
// Ahead-only is normal (OpenSpec never pushes stores), so it stays quiet.
|
|
45
|
+
const drift = input.storeFacts.drift;
|
|
46
|
+
if (drift && drift.behind > 0) {
|
|
47
|
+
const behindCommits = `${drift.behind} commit${drift.behind === 1 ? '' : 's'}`;
|
|
48
|
+
storeStatus.push(makeStoreDiagnostic('info', 'store_checkout_drift', drift.ahead > 0
|
|
49
|
+
? `This store checkout has diverged from its upstream tracking branch (${drift.behind} behind, ${drift.ahead} ahead); teammates on newer commits may resolve different specs.`
|
|
50
|
+
: `This store checkout is ${behindCommits} behind its upstream tracking branch; teammates on newer commits may resolve different specs.`, { target: 'store.git' }));
|
|
51
|
+
}
|
|
40
52
|
store = {
|
|
41
53
|
id: input.storeFacts.id,
|
|
42
54
|
metadata: {
|
|
@@ -47,6 +59,7 @@ export function inspectRelationships(input) {
|
|
|
47
59
|
: {}),
|
|
48
60
|
},
|
|
49
61
|
...(input.storeFacts.originUrl ? { origin_url: input.storeFacts.originUrl } : {}),
|
|
62
|
+
...(drift ? { drift } : {}),
|
|
50
63
|
status: storeStatus,
|
|
51
64
|
};
|
|
52
65
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - `--store <id>` selects a registered store's root.
|
|
8
8
|
* - Without `--store`, the nearest ancestor containing `openspec/` wins.
|
|
9
9
|
* Leftover workspace view state is never considered a root here.
|
|
10
|
-
* - With no nearest root,
|
|
11
|
-
*
|
|
10
|
+
* - With no nearest root, a global `defaultStore` (if set) is the last
|
|
11
|
+
* machine-level fallback before the selection hint error.
|
|
12
|
+
* - With no nearest root and no default, registered stores produce a
|
|
13
|
+
* selection hint error; otherwise commands may treat the current
|
|
14
|
+
* directory as an implicit root.
|
|
12
15
|
*
|
|
13
16
|
* Diagnostic codes reuse the store taxonomy where an error passes
|
|
14
17
|
* through unchanged (`invalid_store_id`, metadata parse failures);
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
* `no_openspec_root`).
|
|
20
23
|
*/
|
|
21
24
|
import { type PlanningHome } from './planning-home.js';
|
|
22
|
-
export type OpenSpecRootSource = 'store' | 'declared' | 'nearest' | 'implicit';
|
|
25
|
+
export type OpenSpecRootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
|
|
23
26
|
export interface StoreSelectorOptions {
|
|
24
27
|
store?: string;
|
|
25
28
|
storePath?: string;
|
|
@@ -84,9 +87,9 @@ export interface RootOutput {
|
|
|
84
87
|
}
|
|
85
88
|
export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
|
|
86
89
|
/**
|
|
87
|
-
* A store-selected root — explicit `--store
|
|
88
|
-
* Cross-root behavior (absolute paths, --store hints,
|
|
89
|
-
* noun-form suggestions) keys on this, never on `source` directly.
|
|
90
|
+
* A store-selected root — explicit `--store`, a declared pointer, or the
|
|
91
|
+
* global default. Cross-root behavior (absolute paths, --store hints,
|
|
92
|
+
* suppressed noun-form suggestions) keys on this, never on `source` directly.
|
|
90
93
|
*/
|
|
91
94
|
export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
|
|
92
95
|
storeId: string;
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - `--store <id>` selects a registered store's root.
|
|
8
8
|
* - Without `--store`, the nearest ancestor containing `openspec/` wins.
|
|
9
9
|
* Leftover workspace view state is never considered a root here.
|
|
10
|
-
* - With no nearest root,
|
|
11
|
-
*
|
|
10
|
+
* - With no nearest root, a global `defaultStore` (if set) is the last
|
|
11
|
+
* machine-level fallback before the selection hint error.
|
|
12
|
+
* - With no nearest root and no default, registered stores produce a
|
|
13
|
+
* selection hint error; otherwise commands may treat the current
|
|
14
|
+
* directory as an implicit root.
|
|
12
15
|
*
|
|
13
16
|
* Diagnostic codes reuse the store taxonomy where an error passes
|
|
14
17
|
* through unchanged (`invalid_store_id`, metadata parse failures);
|
|
@@ -26,6 +29,7 @@ import { getStoreRootForBackend } from './store/registry.js';
|
|
|
26
29
|
import { inspectOpenSpecRoot } from './openspec-root.js';
|
|
27
30
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
28
31
|
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
32
|
+
import { getGlobalConfig } from './global-config.js';
|
|
29
33
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
30
34
|
export class RootSelectionError extends Error {
|
|
31
35
|
diagnostic;
|
|
@@ -221,6 +225,32 @@ async function resolveNearestOrDeclaredRoot(nearestRoot, globalDataDir) {
|
|
|
221
225
|
throw error;
|
|
222
226
|
}
|
|
223
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The machine-level fallback: the global `defaultStore` resolved as a root,
|
|
230
|
+
* with its own provenance (`global_default`) so JSON surfaces can tell a
|
|
231
|
+
* machine-wide default from a repo's `store:` pointer. Mirrors the
|
|
232
|
+
* declared-pointer catch — a stale or unregistered id degrades to the
|
|
233
|
+
* underlying error, reshaped to point at clearing the global default
|
|
234
|
+
* rather than passing --store.
|
|
235
|
+
*/
|
|
236
|
+
async function resolveDefaultStoreRoot(id, globalDataDir) {
|
|
237
|
+
try {
|
|
238
|
+
return await resolveStoreRoot(id, globalDataDir, 'global_default');
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof RootSelectionError) {
|
|
242
|
+
const staleFix = error.diagnostic.code === 'unknown_store' ||
|
|
243
|
+
error.diagnostic.code === 'no_registered_stores'
|
|
244
|
+
? `Register the store (openspec store register <path> --id ${id}) or clear the stale global default (openspec config unset defaultStore).`
|
|
245
|
+
: error.diagnostic.fix;
|
|
246
|
+
throw new RootSelectionError(`Global defaultStore '${id}': ${error.message}`, error.diagnostic.code, {
|
|
247
|
+
...(error.diagnostic.target ? { target: error.diagnostic.target } : {}),
|
|
248
|
+
...(staleFix ? { fix: staleFix } : {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
224
254
|
export async function resolveOpenSpecRoot(options = {}) {
|
|
225
255
|
if (options.storePath !== undefined) {
|
|
226
256
|
throw new RootSelectionError('--store-path is not supported. Register the path with openspec store register <path>, then select it with --store <id>.', 'store_path_not_supported', {
|
|
@@ -236,6 +266,13 @@ export async function resolveOpenSpecRoot(options = {}) {
|
|
|
236
266
|
if (nearestRoot) {
|
|
237
267
|
return resolveNearestOrDeclaredRoot(nearestRoot, options.globalDataDir);
|
|
238
268
|
}
|
|
269
|
+
// Machine-level fallback: a global defaultStore is consulted only after
|
|
270
|
+
// --store, the nearest local root, and project-level pointers have all
|
|
271
|
+
// failed to resolve — it changes the failure path, never the precedence.
|
|
272
|
+
const defaultStore = getGlobalConfig().defaultStore;
|
|
273
|
+
if (defaultStore) {
|
|
274
|
+
return resolveDefaultStoreRoot(defaultStore, options.globalDataDir);
|
|
275
|
+
}
|
|
239
276
|
let registry;
|
|
240
277
|
try {
|
|
241
278
|
registry = await readStoreRegistryState(options.globalDataDir ? { globalDataDir: options.globalDataDir } : {});
|
|
@@ -265,9 +302,9 @@ export function toRootOutput(root) {
|
|
|
265
302
|
};
|
|
266
303
|
}
|
|
267
304
|
/**
|
|
268
|
-
* A store-selected root — explicit `--store
|
|
269
|
-
* Cross-root behavior (absolute paths, --store hints,
|
|
270
|
-
* noun-form suggestions) keys on this, never on `source` directly.
|
|
305
|
+
* A store-selected root — explicit `--store`, a declared pointer, or the
|
|
306
|
+
* global default. Cross-root behavior (absolute paths, --store hints,
|
|
307
|
+
* suppressed noun-form suggestions) keys on this, never on `source` directly.
|
|
271
308
|
*/
|
|
272
309
|
export function isStoreSelectedRoot(root) {
|
|
273
310
|
return root.storeId !== undefined;
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
|
+
export { type SkillCapableTool, toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
|
+
export { toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether a legacy Codex skill differs from the current canonical
|
|
3
|
+
* replacement only by generated version, checkout line endings/BOM, or the
|
|
4
|
+
* known Codex/generic dual-reference expansion.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isLegacyCodexSkillEquivalentToCurrent(legacyContent: string, currentContent: string): boolean;
|
|
7
|
+
//# sourceMappingURL=skill-content-equivalence.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { OPENSPEC_SKILL_NAMES } from '../config.js';
|
|
2
|
+
const GENERATED_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3
|
+
const OPENSPEC_SKILL_NAME_SET = new Set(OPENSPEC_SKILL_NAMES);
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes checkout line endings and a valid generated version inside the
|
|
6
|
+
* YAML frontmatter. Free-form `generatedBy` text in the instructions remains
|
|
7
|
+
* material.
|
|
8
|
+
*/
|
|
9
|
+
function normalizeGeneratedSkill(content) {
|
|
10
|
+
const normalized = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
11
|
+
const frontmatter = normalized.match(/^---\n[\s\S]*?\n---(?:\n|$)/)?.[0];
|
|
12
|
+
if (!frontmatter)
|
|
13
|
+
return normalized;
|
|
14
|
+
const versionLine = /^(\s*generatedBy:\s*)(?:"([^"\n]+)"|'([^'\n]+)'|([^\s"'#]+))\s*$/m;
|
|
15
|
+
const normalizedFrontmatter = frontmatter.replace(versionLine, (line, prefix, doubleQuoted, singleQuoted, bare) => {
|
|
16
|
+
const version = doubleQuoted ?? singleQuoted ?? bare;
|
|
17
|
+
return version && GENERATED_VERSION.test(version)
|
|
18
|
+
? `${prefix}"<generated-version>"`
|
|
19
|
+
: line;
|
|
20
|
+
});
|
|
21
|
+
return normalizedFrontmatter + normalized.slice(frontmatter.length);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts only known generated dual references in current Codex content back
|
|
25
|
+
* to the direct syntax used by legacy `.codex` output.
|
|
26
|
+
*/
|
|
27
|
+
function toLegacyCodexReferences(content) {
|
|
28
|
+
return content.replace(/\$(openspec-[a-z0-9-]+) \(Codex\) or \/\1 \(other agents\)/g, (match, skillName) => OPENSPEC_SKILL_NAME_SET.has(skillName) ? `$${skillName}` : match);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns whether a legacy Codex skill differs from the current canonical
|
|
32
|
+
* replacement only by generated version, checkout line endings/BOM, or the
|
|
33
|
+
* known Codex/generic dual-reference expansion.
|
|
34
|
+
*/
|
|
35
|
+
export function isLegacyCodexSkillEquivalentToCurrent(legacyContent, currentContent) {
|
|
36
|
+
const normalizedLegacy = normalizeGeneratedSkill(legacyContent);
|
|
37
|
+
const normalizedCurrent = normalizeGeneratedSkill(currentContent);
|
|
38
|
+
return (normalizedLegacy === normalizedCurrent ||
|
|
39
|
+
normalizedLegacy === toLegacyCodexReferences(normalizedCurrent));
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=skill-content-equivalence.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AIToolOption } from '../config.js';
|
|
2
|
+
export type SkillCapableTool = AIToolOption & ({
|
|
3
|
+
skillsDir: string;
|
|
4
|
+
} | {
|
|
5
|
+
globalSkillsDir: string;
|
|
6
|
+
});
|
|
7
|
+
export declare function toolSupportsSkills(tool: AIToolOption): tool is SkillCapableTool;
|
|
8
|
+
export declare function getSkillCapableTools(): SkillCapableTool[];
|
|
9
|
+
export declare function hasGlobalSkillTarget(tool: AIToolOption): boolean;
|
|
10
|
+
export declare function resolveToolSkillsDir(projectRoot: string, tool: SkillCapableTool, options?: {
|
|
11
|
+
homeDir?: string;
|
|
12
|
+
}): string;
|
|
13
|
+
//# sourceMappingURL=skill-paths.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AI_TOOLS } from '../config.js';
|
|
4
|
+
export function toolSupportsSkills(tool) {
|
|
5
|
+
return Boolean(tool.skillsDir || tool.globalSkillsDir);
|
|
6
|
+
}
|
|
7
|
+
export function getSkillCapableTools() {
|
|
8
|
+
return AI_TOOLS.filter(toolSupportsSkills);
|
|
9
|
+
}
|
|
10
|
+
export function hasGlobalSkillTarget(tool) {
|
|
11
|
+
return Boolean(tool.globalSkillsDir);
|
|
12
|
+
}
|
|
13
|
+
export function resolveToolSkillsDir(projectRoot, tool, options = {}) {
|
|
14
|
+
if (tool.globalSkillsDir) {
|
|
15
|
+
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
|
|
16
|
+
return path.join(homeDir, tool.globalSkillsDir, 'skills');
|
|
17
|
+
}
|
|
18
|
+
if (tool.skillsDir) {
|
|
19
|
+
return path.join(projectRoot, tool.skillsDir, 'skills');
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Tool '${tool.value}' does not support skill generation.`);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=skill-paths.js.map
|
|
@@ -32,9 +32,13 @@ export interface ToolVersionStatus {
|
|
|
32
32
|
toolId: string;
|
|
33
33
|
/** The tool's display name */
|
|
34
34
|
toolName: string;
|
|
35
|
-
/** Whether the tool has any skills configured */
|
|
35
|
+
/** Whether the tool has any skills or commands configured */
|
|
36
36
|
configured: boolean;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The generatedBy version recorded in the tool's skill files. For a tool that
|
|
39
|
+
* has commands but no skills, the current version when the command files match
|
|
40
|
+
* what would be generated now. Null when neither says the files are current.
|
|
41
|
+
*/
|
|
38
42
|
generatedByVersion: string | null;
|
|
39
43
|
/** Whether the tool needs updating (version mismatch or missing) */
|
|
40
44
|
needsUpdate: boolean;
|
|
@@ -47,6 +51,19 @@ export declare function getToolsWithSkillsDir(): string[];
|
|
|
47
51
|
* Checks which skill files exist for a tool.
|
|
48
52
|
*/
|
|
49
53
|
export declare function getToolSkillStatus(projectRoot: string, toolId: string): ToolSkillStatus;
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Checks whether command files for a tool on disk match current generated command contents.
|
|
60
|
+
*
|
|
61
|
+
* Command files carry no version stamp, so content equality is the only available
|
|
62
|
+
* "is this current?" signal for a commands-only install.
|
|
63
|
+
*/
|
|
64
|
+
export declare function areCommandFilesUpToDate(projectRoot: string, toolId: string, options?: {
|
|
65
|
+
workflows?: readonly string[];
|
|
66
|
+
}): boolean;
|
|
50
67
|
/**
|
|
51
68
|
* Gets the skill status for all tools with skillsDir configured.
|
|
52
69
|
*/
|
|
@@ -57,11 +74,14 @@ export declare function getToolStates(projectRoot: string): Map<string, ToolSkil
|
|
|
57
74
|
*/
|
|
58
75
|
export declare function extractGeneratedByVersion(skillFilePath: string): string | null;
|
|
59
76
|
/**
|
|
60
|
-
* Gets version status for a tool by reading
|
|
77
|
+
* Gets version status for a tool by reading its skill files, falling back to a
|
|
78
|
+
* command-content fingerprint for installs that have commands but no skills.
|
|
61
79
|
*/
|
|
62
|
-
export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string
|
|
80
|
+
export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string, options?: {
|
|
81
|
+
workflows?: readonly string[];
|
|
82
|
+
}): ToolVersionStatus;
|
|
63
83
|
/**
|
|
64
|
-
* Gets all configured tools in the project.
|
|
84
|
+
* Gets all configured tools in the project (configured via skills or commands).
|
|
65
85
|
*/
|
|
66
86
|
export declare function getConfiguredTools(projectRoot: string): string[];
|
|
67
87
|
/**
|