@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.
- package/README.md +52 -0
- package/dist/cli/index.js +32 -2
- package/dist/commands/artifact-workflow.js +6 -1
- package/dist/commands/completion.js +42 -6
- 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/templates/skill-templates.d.ts +10 -0
- package/dist/core/templates/skill-templates.js +482 -20
- 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/file-system.d.ts +6 -0
- package/dist/utils/file-system.js +43 -2
- 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
|
|
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(
|
|
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.
|
|
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
|
},
|