@fission-ai/openspec 0.17.2 → 0.19.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 +52 -0
- package/dist/cli/index.js +39 -3
- package/dist/commands/artifact-workflow.d.ts +17 -0
- package/dist/commands/artifact-workflow.js +823 -0
- package/dist/commands/completion.js +42 -6
- package/dist/core/archive.d.ts +0 -5
- package/dist/core/archive.js +4 -257
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +130 -0
- package/dist/core/artifact-graph/instruction-loader.js +173 -0
- package/dist/core/artifact-graph/resolver.d.ts +61 -0
- package/dist/core/artifact-graph/resolver.js +187 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/completions/command-registry.js +7 -1
- package/dist/core/completions/factory.d.ts +15 -2
- package/dist/core/completions/factory.js +19 -1
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +32 -0
- package/dist/core/completions/generators/powershell-generator.js +198 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +0 -14
- package/dist/core/completions/generators/zsh-generator.js +55 -124
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +1 -12
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/config.js +1 -0
- package/dist/core/configurators/slash/codebuddy.js +6 -9
- package/dist/core/configurators/slash/continue.d.ts +9 -0
- package/dist/core/configurators/slash/continue.js +46 -0
- package/dist/core/configurators/slash/registry.js +3 -0
- package/dist/core/converters/json-converter.js +2 -1
- package/dist/core/global-config.d.ts +10 -0
- package/dist/core/global-config.js +28 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/list.d.ts +6 -1
- package/dist/core/list.js +88 -6
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/templates/skill-templates.d.ts +86 -0
- package/dist/core/templates/skill-templates.js +1934 -0
- package/dist/core/update.js +1 -1
- package/dist/core/validation/validator.js +2 -1
- package/dist/core/view.js +28 -8
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +145 -0
- package/dist/utils/change-metadata.d.ts +47 -0
- package/dist/utils/change-metadata.js +130 -0
- package/dist/utils/change-utils.d.ts +51 -0
- package/dist/utils/change-utils.js +100 -0
- package/dist/utils/file-system.d.ts +11 -0
- package/dist/utils/file-system.js +50 -2
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +4 -1
- package/package.json +5 -1
- package/schemas/spec-driven/schema.yaml +148 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +15 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
package/dist/core/update.js
CHANGED
|
@@ -71,7 +71,7 @@ export class UpdateCommand {
|
|
|
71
71
|
}
|
|
72
72
|
if (updatedSlashFiles.length > 0) {
|
|
73
73
|
// Normalize to forward slashes for cross-platform log consistency
|
|
74
|
-
const normalized = updatedSlashFiles.map((p) =>
|
|
74
|
+
const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p));
|
|
75
75
|
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
|
|
76
76
|
}
|
|
77
77
|
const failedItems = [
|
|
@@ -5,6 +5,7 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
6
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
7
7
|
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
|
|
8
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
8
9
|
export class Validator {
|
|
9
10
|
strictMode;
|
|
10
11
|
constructor(strictMode = false) {
|
|
@@ -330,7 +331,7 @@ export class Validator {
|
|
|
330
331
|
return msg;
|
|
331
332
|
}
|
|
332
333
|
extractNameFromPath(filePath) {
|
|
333
|
-
const normalizedPath =
|
|
334
|
+
const normalizedPath = FileSystemUtils.toPosixPath(filePath);
|
|
334
335
|
const parts = normalizedPath.split('/');
|
|
335
336
|
// Look for the directory name after 'specs' or 'changes'
|
|
336
337
|
for (let i = parts.length - 1; i >= 0; i--) {
|
package/dist/core/view.js
CHANGED
|
@@ -17,11 +17,19 @@ export class ViewCommand {
|
|
|
17
17
|
const specsData = await this.getSpecsData(openspecDir);
|
|
18
18
|
// Display summary metrics
|
|
19
19
|
this.displaySummary(changesData, specsData);
|
|
20
|
+
// Display draft changes
|
|
21
|
+
if (changesData.draft.length > 0) {
|
|
22
|
+
console.log(chalk.bold.gray('\nDraft Changes'));
|
|
23
|
+
console.log('─'.repeat(60));
|
|
24
|
+
changesData.draft.forEach((change) => {
|
|
25
|
+
console.log(` ${chalk.gray('○')} ${change.name}`);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
20
28
|
// Display active changes
|
|
21
29
|
if (changesData.active.length > 0) {
|
|
22
30
|
console.log(chalk.bold.cyan('\nActive Changes'));
|
|
23
31
|
console.log('─'.repeat(60));
|
|
24
|
-
changesData.active.forEach(change => {
|
|
32
|
+
changesData.active.forEach((change) => {
|
|
25
33
|
const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
|
|
26
34
|
const percentage = change.progress.total > 0
|
|
27
35
|
? Math.round((change.progress.completed / change.progress.total) * 100)
|
|
@@ -33,7 +41,7 @@ export class ViewCommand {
|
|
|
33
41
|
if (changesData.completed.length > 0) {
|
|
34
42
|
console.log(chalk.bold.green('\nCompleted Changes'));
|
|
35
43
|
console.log('─'.repeat(60));
|
|
36
|
-
changesData.completed.forEach(change => {
|
|
44
|
+
changesData.completed.forEach((change) => {
|
|
37
45
|
console.log(` ${chalk.green('✓')} ${change.name}`);
|
|
38
46
|
});
|
|
39
47
|
}
|
|
@@ -54,23 +62,32 @@ export class ViewCommand {
|
|
|
54
62
|
async getChangesData(openspecDir) {
|
|
55
63
|
const changesDir = path.join(openspecDir, 'changes');
|
|
56
64
|
if (!fs.existsSync(changesDir)) {
|
|
57
|
-
return { active: [], completed: [] };
|
|
65
|
+
return { draft: [], active: [], completed: [] };
|
|
58
66
|
}
|
|
67
|
+
const draft = [];
|
|
59
68
|
const active = [];
|
|
60
69
|
const completed = [];
|
|
61
70
|
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
62
71
|
for (const entry of entries) {
|
|
63
72
|
if (entry.isDirectory() && entry.name !== 'archive') {
|
|
64
73
|
const progress = await getTaskProgressForChange(changesDir, entry.name);
|
|
65
|
-
if (progress.total === 0
|
|
74
|
+
if (progress.total === 0) {
|
|
75
|
+
// No tasks defined yet - still in planning/draft phase
|
|
76
|
+
draft.push({ name: entry.name });
|
|
77
|
+
}
|
|
78
|
+
else if (progress.completed === progress.total) {
|
|
79
|
+
// All tasks complete
|
|
66
80
|
completed.push({ name: entry.name });
|
|
67
81
|
}
|
|
68
82
|
else {
|
|
83
|
+
// Has tasks but not all complete
|
|
69
84
|
active.push({ name: entry.name, progress });
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
|
-
// Sort
|
|
88
|
+
// Sort all categories by name for deterministic ordering
|
|
89
|
+
draft.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
// Sort active changes by completion percentage (ascending) and then by name
|
|
74
91
|
active.sort((a, b) => {
|
|
75
92
|
const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
|
|
76
93
|
const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
|
|
@@ -81,7 +98,7 @@ export class ViewCommand {
|
|
|
81
98
|
return a.name.localeCompare(b.name);
|
|
82
99
|
});
|
|
83
100
|
completed.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
-
return { active, completed };
|
|
101
|
+
return { draft, active, completed };
|
|
85
102
|
}
|
|
86
103
|
async getSpecsData(openspecDir) {
|
|
87
104
|
const specsDir = path.join(openspecDir, 'specs');
|
|
@@ -111,13 +128,13 @@ export class ViewCommand {
|
|
|
111
128
|
return specs;
|
|
112
129
|
}
|
|
113
130
|
displaySummary(changesData, specsData) {
|
|
114
|
-
const totalChanges = changesData.active.length + changesData.completed.length;
|
|
131
|
+
const totalChanges = changesData.draft.length + changesData.active.length + changesData.completed.length;
|
|
115
132
|
const totalSpecs = specsData.length;
|
|
116
133
|
const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
|
|
117
134
|
// Calculate total task progress
|
|
118
135
|
let totalTasks = 0;
|
|
119
136
|
let completedTasks = 0;
|
|
120
|
-
changesData.active.forEach(change => {
|
|
137
|
+
changesData.active.forEach((change) => {
|
|
121
138
|
totalTasks += change.progress.total;
|
|
122
139
|
completedTasks += change.progress.completed;
|
|
123
140
|
});
|
|
@@ -127,6 +144,9 @@ export class ViewCommand {
|
|
|
127
144
|
});
|
|
128
145
|
console.log(chalk.bold('Summary:'));
|
|
129
146
|
console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
|
|
147
|
+
if (changesData.draft.length > 0) {
|
|
148
|
+
console.log(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`);
|
|
149
|
+
}
|
|
130
150
|
console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
|
|
131
151
|
console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
|
|
132
152
|
if (totalTasks > 0) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface TelemetryConfig {
|
|
2
|
+
anonymousId?: string;
|
|
3
|
+
noticeSeen?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface GlobalConfig {
|
|
6
|
+
telemetry?: TelemetryConfig;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get the path to the global config file.
|
|
11
|
+
* Uses ~/.config/openspec/config.json on all platforms.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getConfigPath(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Read the global config file.
|
|
16
|
+
* Returns an empty object if the file doesn't exist.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readConfig(): Promise<GlobalConfig>;
|
|
19
|
+
/**
|
|
20
|
+
* Write to the global config file.
|
|
21
|
+
* Preserves existing fields and merges in new values.
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeConfig(updates: Partial<GlobalConfig>): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Get the telemetry config section.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getTelemetryConfig(): Promise<TelemetryConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Update the telemetry config section.
|
|
30
|
+
*/
|
|
31
|
+
export declare function updateTelemetryConfig(updates: Partial<TelemetryConfig>): Promise<void>;
|
|
32
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global configuration for telemetry state.
|
|
3
|
+
* Stores anonymous ID and notice-seen flag in ~/.config/openspec/config.json
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
/**
|
|
9
|
+
* Get the path to the global config file.
|
|
10
|
+
* Uses ~/.config/openspec/config.json on all platforms.
|
|
11
|
+
*/
|
|
12
|
+
export function getConfigPath() {
|
|
13
|
+
const configDir = path.join(os.homedir(), '.config', 'openspec');
|
|
14
|
+
return path.join(configDir, 'config.json');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read the global config file.
|
|
18
|
+
* Returns an empty object if the file doesn't exist.
|
|
19
|
+
*/
|
|
20
|
+
export async function readConfig() {
|
|
21
|
+
const configPath = getConfigPath();
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
// If parse fails or other error, return empty config
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Write to the global config file.
|
|
36
|
+
* Preserves existing fields and merges in new values.
|
|
37
|
+
*/
|
|
38
|
+
export async function writeConfig(updates) {
|
|
39
|
+
const configPath = getConfigPath();
|
|
40
|
+
const configDir = path.dirname(configPath);
|
|
41
|
+
// Ensure directory exists
|
|
42
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
43
|
+
// Read existing config and merge
|
|
44
|
+
const existing = await readConfig();
|
|
45
|
+
const merged = { ...existing, ...updates };
|
|
46
|
+
// Deep merge for telemetry object
|
|
47
|
+
if (updates.telemetry && existing.telemetry) {
|
|
48
|
+
merged.telemetry = { ...existing.telemetry, ...updates.telemetry };
|
|
49
|
+
}
|
|
50
|
+
await fs.writeFile(configPath, JSON.stringify(merged, null, 2) + '\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the telemetry config section.
|
|
54
|
+
*/
|
|
55
|
+
export async function getTelemetryConfig() {
|
|
56
|
+
const config = await readConfig();
|
|
57
|
+
return config.telemetry ?? {};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Update the telemetry config section.
|
|
61
|
+
*/
|
|
62
|
+
export async function updateTelemetryConfig(updates) {
|
|
63
|
+
const existing = await getTelemetryConfig();
|
|
64
|
+
await writeConfig({
|
|
65
|
+
telemetry: { ...existing, ...updates },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if telemetry is enabled.
|
|
3
|
+
*
|
|
4
|
+
* Disabled when:
|
|
5
|
+
* - OPENSPEC_TELEMETRY=0
|
|
6
|
+
* - DO_NOT_TRACK=1
|
|
7
|
+
* - CI=true (any CI environment)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isTelemetryEnabled(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Get or create the anonymous user ID.
|
|
12
|
+
* Lazily generates a UUID on first call and persists it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getOrCreateAnonymousId(): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Track a command execution.
|
|
17
|
+
*
|
|
18
|
+
* @param commandName - The command name (e.g., 'init', 'change:apply')
|
|
19
|
+
* @param version - The OpenSpec version
|
|
20
|
+
*/
|
|
21
|
+
export declare function trackCommand(commandName: string, version: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Show first-run telemetry notice if not already seen.
|
|
24
|
+
*/
|
|
25
|
+
export declare function maybeShowTelemetryNotice(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Shutdown the PostHog client and flush pending events.
|
|
28
|
+
* Call this before CLI exit.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shutdown(): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry module for anonymous usage analytics.
|
|
3
|
+
*
|
|
4
|
+
* Privacy-first design:
|
|
5
|
+
* - Only tracks command name and version
|
|
6
|
+
* - No arguments, file paths, or content
|
|
7
|
+
* - Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1
|
|
8
|
+
* - Auto-disabled in CI environments
|
|
9
|
+
* - Anonymous ID is a random UUID with no relation to the user
|
|
10
|
+
*/
|
|
11
|
+
import { PostHog } from 'posthog-node';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { getTelemetryConfig, updateTelemetryConfig } from './config.js';
|
|
14
|
+
// PostHog API key - public key for client-side analytics
|
|
15
|
+
// This is safe to embed as it only allows sending events, not reading data
|
|
16
|
+
const POSTHOG_API_KEY = 'phc_Hthu8YvaIJ9QaFKyTG4TbVwkbd5ktcAFzVTKeMmoW2g';
|
|
17
|
+
// Using reverse proxy to avoid ad blockers and keep traffic on our domain
|
|
18
|
+
const POSTHOG_HOST = 'https://edge.openspec.dev';
|
|
19
|
+
let posthogClient = null;
|
|
20
|
+
let anonymousId = null;
|
|
21
|
+
/**
|
|
22
|
+
* Check if telemetry is enabled.
|
|
23
|
+
*
|
|
24
|
+
* Disabled when:
|
|
25
|
+
* - OPENSPEC_TELEMETRY=0
|
|
26
|
+
* - DO_NOT_TRACK=1
|
|
27
|
+
* - CI=true (any CI environment)
|
|
28
|
+
*/
|
|
29
|
+
export function isTelemetryEnabled() {
|
|
30
|
+
// Check explicit opt-out
|
|
31
|
+
if (process.env.OPENSPEC_TELEMETRY === '0') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Respect DO_NOT_TRACK standard
|
|
35
|
+
if (process.env.DO_NOT_TRACK === '1') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Auto-disable in CI environments
|
|
39
|
+
if (process.env.CI === 'true') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get or create the anonymous user ID.
|
|
46
|
+
* Lazily generates a UUID on first call and persists it.
|
|
47
|
+
*/
|
|
48
|
+
export async function getOrCreateAnonymousId() {
|
|
49
|
+
// Return cached value if available
|
|
50
|
+
if (anonymousId) {
|
|
51
|
+
return anonymousId;
|
|
52
|
+
}
|
|
53
|
+
// Try to load from config
|
|
54
|
+
const config = await getTelemetryConfig();
|
|
55
|
+
if (config.anonymousId) {
|
|
56
|
+
anonymousId = config.anonymousId;
|
|
57
|
+
return anonymousId;
|
|
58
|
+
}
|
|
59
|
+
// Generate new UUID and persist
|
|
60
|
+
anonymousId = randomUUID();
|
|
61
|
+
await updateTelemetryConfig({ anonymousId });
|
|
62
|
+
return anonymousId;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the PostHog client instance.
|
|
66
|
+
* Creates it on first call with CLI-optimized settings.
|
|
67
|
+
*/
|
|
68
|
+
function getClient() {
|
|
69
|
+
if (!posthogClient) {
|
|
70
|
+
posthogClient = new PostHog(POSTHOG_API_KEY, {
|
|
71
|
+
host: POSTHOG_HOST,
|
|
72
|
+
flushAt: 1, // Send immediately, don't batch
|
|
73
|
+
flushInterval: 0, // No timer-based flushing
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return posthogClient;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Track a command execution.
|
|
80
|
+
*
|
|
81
|
+
* @param commandName - The command name (e.g., 'init', 'change:apply')
|
|
82
|
+
* @param version - The OpenSpec version
|
|
83
|
+
*/
|
|
84
|
+
export async function trackCommand(commandName, version) {
|
|
85
|
+
if (!isTelemetryEnabled()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const userId = await getOrCreateAnonymousId();
|
|
90
|
+
const client = getClient();
|
|
91
|
+
client.capture({
|
|
92
|
+
distinctId: userId,
|
|
93
|
+
event: 'command_executed',
|
|
94
|
+
properties: {
|
|
95
|
+
command: commandName,
|
|
96
|
+
version: version,
|
|
97
|
+
surface: 'cli',
|
|
98
|
+
$ip: null, // Explicitly disable IP tracking
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Silent failure - telemetry should never break CLI
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Show first-run telemetry notice if not already seen.
|
|
108
|
+
*/
|
|
109
|
+
export async function maybeShowTelemetryNotice() {
|
|
110
|
+
if (!isTelemetryEnabled()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const config = await getTelemetryConfig();
|
|
115
|
+
if (config.noticeSeen) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Display notice
|
|
119
|
+
console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0');
|
|
120
|
+
// Mark as seen
|
|
121
|
+
await updateTelemetryConfig({ noticeSeen: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Silent failure - telemetry should never break CLI
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Shutdown the PostHog client and flush pending events.
|
|
129
|
+
* Call this before CLI exit.
|
|
130
|
+
*/
|
|
131
|
+
export async function shutdown() {
|
|
132
|
+
if (!posthogClient) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await posthogClient.shutdown();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Silent failure - telemetry should never break CLI exit
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
posthogClient = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ChangeMetadata } from '../core/artifact-graph/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when change metadata validation fails.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ChangeMetadataError extends Error {
|
|
6
|
+
readonly metadataPath: string;
|
|
7
|
+
readonly cause?: Error | undefined;
|
|
8
|
+
constructor(message: string, metadataPath: string, cause?: Error | undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a schema name is valid (exists in available schemas).
|
|
12
|
+
*
|
|
13
|
+
* @param schemaName - The schema name to validate
|
|
14
|
+
* @returns The validated schema name
|
|
15
|
+
* @throws Error if schema is not found
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateSchemaName(schemaName: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Writes change metadata to .openspec.yaml in the change directory.
|
|
20
|
+
*
|
|
21
|
+
* @param changeDir - The path to the change directory
|
|
22
|
+
* @param metadata - The metadata to write
|
|
23
|
+
* @throws ChangeMetadataError if validation fails or write fails
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata): void;
|
|
26
|
+
/**
|
|
27
|
+
* Reads change metadata from .openspec.yaml in the change directory.
|
|
28
|
+
*
|
|
29
|
+
* @param changeDir - The path to the change directory
|
|
30
|
+
* @returns The validated metadata, or null if no metadata file exists
|
|
31
|
+
* @throws ChangeMetadataError if the file exists but is invalid
|
|
32
|
+
*/
|
|
33
|
+
export declare function readChangeMetadata(changeDir: string): ChangeMetadata | null;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the schema for a change, with explicit override taking precedence.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order:
|
|
38
|
+
* 1. Explicit schema (if provided)
|
|
39
|
+
* 2. Schema from .openspec.yaml metadata (if exists)
|
|
40
|
+
* 3. Default 'spec-driven'
|
|
41
|
+
*
|
|
42
|
+
* @param changeDir - The path to the change directory
|
|
43
|
+
* @param explicitSchema - Optional explicit schema override
|
|
44
|
+
* @returns The resolved schema name
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string): string;
|
|
47
|
+
//# sourceMappingURL=change-metadata.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as yaml from 'yaml';
|
|
4
|
+
import { ChangeMetadataSchema } from '../core/artifact-graph/types.js';
|
|
5
|
+
import { listSchemas } from '../core/artifact-graph/resolver.js';
|
|
6
|
+
const METADATA_FILENAME = '.openspec.yaml';
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown when change metadata validation fails.
|
|
9
|
+
*/
|
|
10
|
+
export class ChangeMetadataError extends Error {
|
|
11
|
+
metadataPath;
|
|
12
|
+
cause;
|
|
13
|
+
constructor(message, metadataPath, cause) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.metadataPath = metadataPath;
|
|
16
|
+
this.cause = cause;
|
|
17
|
+
this.name = 'ChangeMetadataError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validates that a schema name is valid (exists in available schemas).
|
|
22
|
+
*
|
|
23
|
+
* @param schemaName - The schema name to validate
|
|
24
|
+
* @returns The validated schema name
|
|
25
|
+
* @throws Error if schema is not found
|
|
26
|
+
*/
|
|
27
|
+
export function validateSchemaName(schemaName) {
|
|
28
|
+
const availableSchemas = listSchemas();
|
|
29
|
+
if (!availableSchemas.includes(schemaName)) {
|
|
30
|
+
throw new Error(`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return schemaName;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Writes change metadata to .openspec.yaml in the change directory.
|
|
36
|
+
*
|
|
37
|
+
* @param changeDir - The path to the change directory
|
|
38
|
+
* @param metadata - The metadata to write
|
|
39
|
+
* @throws ChangeMetadataError if validation fails or write fails
|
|
40
|
+
*/
|
|
41
|
+
export function writeChangeMetadata(changeDir, metadata) {
|
|
42
|
+
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
43
|
+
// Validate schema exists
|
|
44
|
+
validateSchemaName(metadata.schema);
|
|
45
|
+
// Validate with Zod
|
|
46
|
+
const parseResult = ChangeMetadataSchema.safeParse(metadata);
|
|
47
|
+
if (!parseResult.success) {
|
|
48
|
+
throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
|
|
49
|
+
}
|
|
50
|
+
// Write YAML file
|
|
51
|
+
const content = yaml.stringify(parseResult.data);
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(metaPath, content, 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
57
|
+
throw new ChangeMetadataError(`Failed to write metadata: ${ioError.message}`, metaPath, ioError);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads change metadata from .openspec.yaml in the change directory.
|
|
62
|
+
*
|
|
63
|
+
* @param changeDir - The path to the change directory
|
|
64
|
+
* @returns The validated metadata, or null if no metadata file exists
|
|
65
|
+
* @throws ChangeMetadataError if the file exists but is invalid
|
|
66
|
+
*/
|
|
67
|
+
export function readChangeMetadata(changeDir) {
|
|
68
|
+
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
69
|
+
if (!fs.existsSync(metaPath)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = fs.readFileSync(metaPath, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
78
|
+
throw new ChangeMetadataError(`Failed to read metadata: ${ioError.message}`, metaPath, ioError);
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = yaml.parse(content);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const parseError = err instanceof Error ? err : new Error(String(err));
|
|
86
|
+
throw new ChangeMetadataError(`Invalid YAML in metadata file: ${parseError.message}`, metaPath, parseError);
|
|
87
|
+
}
|
|
88
|
+
// Validate with Zod
|
|
89
|
+
const parseResult = ChangeMetadataSchema.safeParse(parsed);
|
|
90
|
+
if (!parseResult.success) {
|
|
91
|
+
throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
|
|
92
|
+
}
|
|
93
|
+
// Validate that the schema exists
|
|
94
|
+
const availableSchemas = listSchemas();
|
|
95
|
+
if (!availableSchemas.includes(parseResult.data.schema)) {
|
|
96
|
+
throw new ChangeMetadataError(`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`, metaPath);
|
|
97
|
+
}
|
|
98
|
+
return parseResult.data;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolves the schema for a change, with explicit override taking precedence.
|
|
102
|
+
*
|
|
103
|
+
* Resolution order:
|
|
104
|
+
* 1. Explicit schema (if provided)
|
|
105
|
+
* 2. Schema from .openspec.yaml metadata (if exists)
|
|
106
|
+
* 3. Default 'spec-driven'
|
|
107
|
+
*
|
|
108
|
+
* @param changeDir - The path to the change directory
|
|
109
|
+
* @param explicitSchema - Optional explicit schema override
|
|
110
|
+
* @returns The resolved schema name
|
|
111
|
+
*/
|
|
112
|
+
export function resolveSchemaForChange(changeDir, explicitSchema) {
|
|
113
|
+
// 1. Explicit override wins
|
|
114
|
+
if (explicitSchema) {
|
|
115
|
+
return explicitSchema;
|
|
116
|
+
}
|
|
117
|
+
// 2. Try reading from metadata
|
|
118
|
+
try {
|
|
119
|
+
const metadata = readChangeMetadata(changeDir);
|
|
120
|
+
if (metadata?.schema) {
|
|
121
|
+
return metadata.schema;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// If metadata read fails, fall back to default
|
|
126
|
+
}
|
|
127
|
+
// 3. Default
|
|
128
|
+
return 'spec-driven';
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=change-metadata.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for creating a change.
|
|
3
|
+
*/
|
|
4
|
+
export interface CreateChangeOptions {
|
|
5
|
+
/** The workflow schema to use (default: 'spec-driven') */
|
|
6
|
+
schema?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Result of validating a change name.
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a change name follows kebab-case conventions.
|
|
17
|
+
*
|
|
18
|
+
* Valid names:
|
|
19
|
+
* - Start with a lowercase letter
|
|
20
|
+
* - Contain only lowercase letters, numbers, and hyphens
|
|
21
|
+
* - Do not start or end with a hyphen
|
|
22
|
+
* - Do not contain consecutive hyphens
|
|
23
|
+
*
|
|
24
|
+
* @param name - The change name to validate
|
|
25
|
+
* @returns Validation result with `valid: true` or `valid: false` with an error message
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* validateChangeName('add-auth') // { valid: true }
|
|
29
|
+
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateChangeName(name: string): ValidationResult;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new change directory with metadata file.
|
|
34
|
+
*
|
|
35
|
+
* @param projectRoot - The root directory of the project (where `openspec/` lives)
|
|
36
|
+
* @param name - The change name (must be valid kebab-case)
|
|
37
|
+
* @param options - Optional settings for the change
|
|
38
|
+
* @throws Error if the change name is invalid
|
|
39
|
+
* @throws Error if the schema name is invalid
|
|
40
|
+
* @throws Error if the change directory already exists
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Creates openspec/changes/add-auth/ with default schema
|
|
44
|
+
* await createChange('/path/to/project', 'add-auth')
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Creates openspec/changes/add-auth/ with TDD schema
|
|
48
|
+
* await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
|
|
49
|
+
*/
|
|
50
|
+
export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<void>;
|
|
51
|
+
//# sourceMappingURL=change-utils.d.ts.map
|