@fission-ai/openspec 0.18.0 → 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.
Files changed (44) hide show
  1. package/README.md +52 -0
  2. package/dist/cli/index.js +32 -2
  3. package/dist/commands/artifact-workflow.js +6 -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 +32 -0
  13. package/dist/core/completions/generators/powershell-generator.js +198 -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/skill-templates.d.ts +10 -0
  37. package/dist/core/templates/skill-templates.js +482 -20
  38. package/dist/telemetry/config.d.ts +32 -0
  39. package/dist/telemetry/config.js +68 -0
  40. package/dist/telemetry/index.d.ts +31 -0
  41. package/dist/telemetry/index.js +145 -0
  42. package/dist/utils/file-system.d.ts +6 -0
  43. package/dist/utils/file-system.js +43 -2
  44. package/package.json +2 -1
@@ -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.19.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -57,6 +57,7 @@
57
57
  "commander": "^14.0.0",
58
58
  "fast-glob": "^3.3.3",
59
59
  "ora": "^8.2.0",
60
+ "posthog-node": "^5.20.0",
60
61
  "yaml": "^2.8.2",
61
62
  "zod": "^4.0.17"
62
63
  },