@fission-ai/openspec 0.18.0 → 0.20.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.
Files changed (47) hide show
  1. package/README.md +59 -0
  2. package/dist/cli/index.js +32 -2
  3. package/dist/commands/artifact-workflow.js +11 -1
  4. package/dist/commands/completion.js +42 -6
  5. package/dist/core/completions/command-registry.js +7 -1
  6. package/dist/core/completions/factory.d.ts +15 -2
  7. package/dist/core/completions/factory.js +19 -1
  8. package/dist/core/completions/generators/bash-generator.d.ts +32 -0
  9. package/dist/core/completions/generators/bash-generator.js +174 -0
  10. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  11. package/dist/core/completions/generators/fish-generator.js +157 -0
  12. package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
  13. package/dist/core/completions/generators/powershell-generator.js +207 -0
  14. package/dist/core/completions/generators/zsh-generator.d.ts +0 -14
  15. package/dist/core/completions/generators/zsh-generator.js +55 -124
  16. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  17. package/dist/core/completions/installers/bash-installer.js +318 -0
  18. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  19. package/dist/core/completions/installers/fish-installer.js +143 -0
  20. package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
  21. package/dist/core/completions/installers/powershell-installer.js +327 -0
  22. package/dist/core/completions/installers/zsh-installer.d.ts +1 -12
  23. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  24. package/dist/core/completions/templates/bash-templates.js +24 -0
  25. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  26. package/dist/core/completions/templates/fish-templates.js +39 -0
  27. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  28. package/dist/core/completions/templates/powershell-templates.js +25 -0
  29. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  30. package/dist/core/completions/templates/zsh-templates.js +36 -0
  31. package/dist/core/config.js +1 -0
  32. package/dist/core/configurators/slash/codebuddy.js +6 -9
  33. package/dist/core/configurators/slash/continue.d.ts +9 -0
  34. package/dist/core/configurators/slash/continue.js +46 -0
  35. package/dist/core/configurators/slash/registry.js +3 -0
  36. package/dist/core/templates/agents-template.d.ts +1 -1
  37. package/dist/core/templates/agents-template.js +7 -7
  38. package/dist/core/templates/skill-templates.d.ts +19 -0
  39. package/dist/core/templates/skill-templates.js +817 -20
  40. package/dist/core/templates/slash-command-templates.js +2 -2
  41. package/dist/telemetry/config.d.ts +32 -0
  42. package/dist/telemetry/config.js +68 -0
  43. package/dist/telemetry/index.d.ts +31 -0
  44. package/dist/telemetry/index.js +145 -0
  45. package/dist/utils/file-system.d.ts +6 -0
  46. package/dist/utils/file-system.js +43 -2
  47. package/package.json +3 -2
@@ -11,7 +11,7 @@ const proposalSteps = `**Steps**
11
11
  4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
12
12
  5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
13
13
  6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
14
- 7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
14
+ 7. Validate with \`openspec validate <id> --strict --no-interactive\` and resolve every issue before sharing the proposal.`;
15
15
  const proposalReferences = `**Reference**
16
16
  - Use \`openspec show <id> --json --deltas-only\` or \`openspec show <spec> --type spec\` to inspect details when validation fails.
17
17
  - Search existing requirements with \`rg -n "Requirement:|Scenario:" openspec/specs\` before writing new ones.
@@ -34,7 +34,7 @@ const archiveSteps = `**Steps**
34
34
  2. Validate the change ID by running \`openspec list\` (or \`openspec show <id>\`) and stop if the change is missing, already archived, or otherwise not ready to archive.
35
35
  3. Run \`openspec archive <id> --yes\` so the CLI moves the change and applies spec updates without prompts (use \`--skip-specs\` only for tooling-only work).
36
36
  4. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
37
- 5. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;
37
+ 5. Validate with \`openspec validate --strict --no-interactive\` and inspect with \`openspec show <id>\` if anything looks off.`;
38
38
  const archiveReferences = `**Reference**
39
39
  - Use \`openspec list\` to confirm change IDs before archiving.
40
40
  - Inspect refreshed specs with \`openspec list --specs\` and address any validation issues before handing off.`;
@@ -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
@@ -9,6 +9,12 @@ export declare class FileSystemUtils {
9
9
  static joinPath(basePath: string, ...segments: string[]): string;
10
10
  static createDirectory(dirPath: string): Promise<void>;
11
11
  static fileExists(filePath: string): Promise<boolean>;
12
+ /**
13
+ * Finds the first existing parent directory by walking up the directory tree.
14
+ * @param dirPath Starting directory path
15
+ * @returns The first existing directory path, or null if root is reached without finding one
16
+ */
17
+ private static findFirstExistingDirectory;
12
18
  static canWriteFile(filePath: string): Promise<boolean>;
13
19
  static directoryExists(dirPath: string): Promise<boolean>;
14
20
  static writeFile(filePath: string, content: string): Promise<void>;
@@ -73,6 +73,41 @@ export class FileSystemUtils {
73
73
  return false;
74
74
  }
75
75
  }
76
+ /**
77
+ * Finds the first existing parent directory by walking up the directory tree.
78
+ * @param dirPath Starting directory path
79
+ * @returns The first existing directory path, or null if root is reached without finding one
80
+ */
81
+ static async findFirstExistingDirectory(dirPath) {
82
+ let currentDir = dirPath;
83
+ while (true) {
84
+ try {
85
+ const stats = await fs.stat(currentDir);
86
+ if (stats.isDirectory()) {
87
+ return currentDir;
88
+ }
89
+ // Path component exists but is not a directory (edge case)
90
+ console.debug(`Path component ${currentDir} exists but is not a directory`);
91
+ return null;
92
+ }
93
+ catch (error) {
94
+ if (error.code === 'ENOENT') {
95
+ // Directory doesn't exist, move up one level
96
+ const parentDir = path.dirname(currentDir);
97
+ if (parentDir === currentDir) {
98
+ // Reached filesystem root without finding existing directory
99
+ return null;
100
+ }
101
+ currentDir = parentDir;
102
+ }
103
+ else {
104
+ // Unexpected error (permissions, I/O error, etc.)
105
+ console.debug(`Error checking directory ${currentDir}: ${error.message}`);
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ }
76
111
  static async canWriteFile(filePath) {
77
112
  try {
78
113
  const stats = await fs.stat(filePath);
@@ -91,10 +126,16 @@ export class FileSystemUtils {
91
126
  }
92
127
  catch (error) {
93
128
  if (error.code === 'ENOENT') {
94
- // File doesn't exist; check if we can write to the parent directory
129
+ // File doesn't exist - find first existing parent directory and check its permissions
95
130
  const parentDir = path.dirname(filePath);
131
+ const existingDir = await this.findFirstExistingDirectory(parentDir);
132
+ if (existingDir === null) {
133
+ // No existing parent directory found (edge case)
134
+ return false;
135
+ }
136
+ // Check if the existing parent directory is writable
96
137
  try {
97
- await fs.access(parentDir, fsConstants.W_OK);
138
+ await fs.access(existingDir, fsConstants.W_OK);
98
139
  return true;
99
140
  }
100
141
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -42,6 +42,7 @@
42
42
  "node": ">=20.19.0"
43
43
  },
44
44
  "devDependencies": {
45
+ "@changesets/changelog-github": "^0.5.2",
45
46
  "@changesets/cli": "^2.27.7",
46
47
  "@types/node": "^24.2.0",
47
48
  "@vitest/ui": "^3.2.4",
@@ -57,6 +58,7 @@
57
58
  "commander": "^14.0.0",
58
59
  "fast-glob": "^3.3.3",
59
60
  "ora": "^8.2.0",
61
+ "posthog-node": "^5.20.0",
60
62
  "yaml": "^2.8.2",
61
63
  "zod": "^4.0.17"
62
64
  },
@@ -74,7 +76,6 @@
74
76
  "check:pack-version": "node scripts/pack-version-check.mjs",
75
77
  "release": "pnpm run release:ci",
76
78
  "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
77
- "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
78
79
  "changeset": "changeset"
79
80
  }
80
81
  }