@codewalla_india/openspec 1.0.6 → 1.2.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 +2 -4
- package/dist/cli/index.js +47 -6
- package/dist/commands/config.js +8 -0
- package/dist/commands/feedback.js +2 -0
- package/dist/commands/store.js +18 -1
- package/dist/commands/validate.js +11 -1
- package/dist/commands/workflow/instructions.js +109 -13
- package/dist/commands/workflow/new-change.d.ts +4 -0
- package/dist/commands/workflow/new-change.js +28 -4
- package/dist/commands/workflow/shared.d.ts +2 -0
- package/dist/commands/workflow/status.js +28 -1
- package/dist/commands/workset.js +12 -0
- package/dist/core/archive.js +20 -0
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/init.js +2 -0
- package/dist/core/templates/workflows/apply-change.js +4 -0
- package/dist/core/templates/workflows/ff-change.js +9 -3
- package/dist/core/templates/workflows/new-change.js +9 -3
- package/dist/core/templates/workflows/propose.js +9 -3
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
- package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
- package/dist/core/update.js +2 -0
- package/dist/telemetry/caller.d.ts +5 -0
- package/dist/telemetry/caller.js +29 -0
- package/dist/telemetry/client.d.ts +27 -0
- package/dist/telemetry/client.js +127 -0
- package/dist/telemetry/command-context.d.ts +13 -0
- package/dist/telemetry/command-context.js +59 -0
- package/dist/telemetry/comprehension.d.ts +44 -0
- package/dist/telemetry/comprehension.js +105 -0
- package/dist/telemetry/config.d.ts +2 -29
- package/dist/telemetry/config.js +11 -87
- package/dist/telemetry/content.d.ts +10 -0
- package/dist/telemetry/content.js +56 -0
- package/dist/telemetry/git-stats.d.ts +12 -0
- package/dist/telemetry/git-stats.js +69 -0
- package/dist/telemetry/identify-cache.d.ts +7 -0
- package/dist/telemetry/identify-cache.js +47 -0
- package/dist/telemetry/identity.d.ts +23 -0
- package/dist/telemetry/identity.js +125 -0
- package/dist/telemetry/index.d.ts +15 -29
- package/dist/telemetry/index.js +36 -155
- package/dist/telemetry/input.d.ts +17 -0
- package/dist/telemetry/input.js +68 -0
- package/dist/telemetry/marker.d.ts +31 -0
- package/dist/telemetry/marker.js +67 -0
- package/dist/telemetry/workflow.d.ts +75 -0
- package/dist/telemetry/workflow.js +290 -0
- package/package.json +18 -20
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
export declare const CONFIG_DIR_NAME = "openspec";
|
|
2
2
|
export declare const CONFIG_FILE_NAME = "config.json";
|
|
3
|
-
export interface TelemetryConfig {
|
|
4
|
-
anonymousId?: string;
|
|
5
|
-
noticeSeen?: boolean;
|
|
6
|
-
}
|
|
7
3
|
export interface GlobalConfig {
|
|
8
|
-
telemetry?: TelemetryConfig;
|
|
9
4
|
[key: string]: unknown;
|
|
10
5
|
}
|
|
11
|
-
/**
|
|
12
|
-
* Get the path to the global config file.
|
|
13
|
-
* Follows XDG Base Directory Specification and platform conventions.
|
|
14
|
-
*
|
|
15
|
-
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
16
|
-
* - Unix/macOS fallback: ~/.config/openspec/
|
|
17
|
-
* - Windows fallback: %APPDATA%/openspec/
|
|
18
|
-
*/
|
|
19
6
|
export declare function getConfigPath(): string;
|
|
20
|
-
/**
|
|
21
|
-
* Read the global config file.
|
|
22
|
-
* Returns an empty object if the file doesn't exist.
|
|
23
|
-
*/
|
|
24
7
|
export declare function readConfig(): Promise<GlobalConfig>;
|
|
25
|
-
/**
|
|
26
|
-
* Write to the global config file.
|
|
27
|
-
* Preserves existing fields and merges in new values.
|
|
28
|
-
*/
|
|
29
8
|
export declare function writeConfig(updates: Partial<GlobalConfig>): Promise<void>;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
export declare function getTelemetryConfig(): Promise<TelemetryConfig>;
|
|
34
|
-
/**
|
|
35
|
-
* Update the telemetry config section.
|
|
36
|
-
*/
|
|
37
|
-
export declare function updateTelemetryConfig(updates: Partial<TelemetryConfig>): Promise<void>;
|
|
9
|
+
export declare function getTelemetryConfig(): Promise<Record<string, unknown>>;
|
|
10
|
+
export declare function updateTelemetryConfig(updates: Record<string, unknown>): Promise<void>;
|
|
38
11
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/telemetry/config.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global configuration for
|
|
3
|
-
* Stores
|
|
2
|
+
* Global configuration for OpenSpec CLI state.
|
|
3
|
+
* Stores settings in the platform-appropriate config directory.
|
|
4
4
|
*/
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
|
-
import os from 'os';
|
|
8
7
|
import { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, getGlobalConfigDir, } from '../core/global-config.js';
|
|
9
|
-
// Constants
|
|
10
8
|
export const CONFIG_DIR_NAME = GLOBAL_CONFIG_DIR_NAME;
|
|
11
9
|
export const CONFIG_FILE_NAME = GLOBAL_CONFIG_FILE_NAME;
|
|
12
10
|
function getConfigDir() {
|
|
13
11
|
return getGlobalConfigDir();
|
|
14
12
|
}
|
|
15
|
-
function getLegacyConfigPath() {
|
|
16
|
-
return path.join(os.homedir(), '.config', CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
17
|
-
}
|
|
18
13
|
async function readConfigFile(configPath) {
|
|
19
14
|
try {
|
|
20
15
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
@@ -24,7 +19,6 @@ async function readConfigFile(configPath) {
|
|
|
24
19
|
if (error.code === 'ENOENT') {
|
|
25
20
|
return { status: 'missing' };
|
|
26
21
|
}
|
|
27
|
-
// If parse fails or another read error occurs, ignore the file.
|
|
28
22
|
return { status: 'invalid', config: {} };
|
|
29
23
|
}
|
|
30
24
|
}
|
|
@@ -32,101 +26,31 @@ async function writeConfigFile(configPath, config) {
|
|
|
32
26
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
33
27
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
34
28
|
}
|
|
35
|
-
function hasMissingTelemetryFields(config) {
|
|
36
|
-
const telemetry = config.telemetry;
|
|
37
|
-
return (!telemetry ||
|
|
38
|
-
telemetry.anonymousId === undefined ||
|
|
39
|
-
telemetry.noticeSeen === undefined);
|
|
40
|
-
}
|
|
41
|
-
function mergeLegacyTelemetry(config, legacyConfig) {
|
|
42
|
-
const legacyTelemetry = legacyConfig.telemetry;
|
|
43
|
-
if (!legacyTelemetry) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
const currentTelemetry = config.telemetry ?? {};
|
|
47
|
-
const shouldMigrate = (currentTelemetry.anonymousId === undefined && legacyTelemetry.anonymousId !== undefined) ||
|
|
48
|
-
(currentTelemetry.noticeSeen === undefined && legacyTelemetry.noticeSeen !== undefined);
|
|
49
|
-
if (!shouldMigrate) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
...config,
|
|
54
|
-
telemetry: {
|
|
55
|
-
...legacyTelemetry,
|
|
56
|
-
...currentTelemetry,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
async function migrateLegacyTelemetryConfig(configPath, config, persist) {
|
|
61
|
-
const legacyConfigPath = getLegacyConfigPath();
|
|
62
|
-
if (path.resolve(configPath) === path.resolve(legacyConfigPath) || !hasMissingTelemetryFields(config)) {
|
|
63
|
-
return config;
|
|
64
|
-
}
|
|
65
|
-
const legacyRead = await readConfigFile(legacyConfigPath);
|
|
66
|
-
if (legacyRead.status !== 'ok') {
|
|
67
|
-
return config;
|
|
68
|
-
}
|
|
69
|
-
const migrated = mergeLegacyTelemetry(config, legacyRead.config);
|
|
70
|
-
if (!migrated) {
|
|
71
|
-
return config;
|
|
72
|
-
}
|
|
73
|
-
if (persist) {
|
|
74
|
-
try {
|
|
75
|
-
await writeConfigFile(configPath, migrated);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Preserve telemetry for this run even if the one-time migration cannot be persisted.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return migrated;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Get the path to the global config file.
|
|
85
|
-
* Follows XDG Base Directory Specification and platform conventions.
|
|
86
|
-
*
|
|
87
|
-
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
88
|
-
* - Unix/macOS fallback: ~/.config/openspec/
|
|
89
|
-
* - Windows fallback: %APPDATA%/openspec/
|
|
90
|
-
*/
|
|
91
29
|
export function getConfigPath() {
|
|
92
|
-
|
|
93
|
-
return path.join(configDir, CONFIG_FILE_NAME);
|
|
30
|
+
return path.join(getConfigDir(), CONFIG_FILE_NAME);
|
|
94
31
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Read the global config file.
|
|
97
|
-
* Returns an empty object if the file doesn't exist.
|
|
98
|
-
*/
|
|
99
32
|
export async function readConfig() {
|
|
100
33
|
const configPath = getConfigPath();
|
|
101
34
|
const read = await readConfigFile(configPath);
|
|
102
|
-
|
|
103
|
-
return migrateLegacyTelemetryConfig(configPath, config, read.status !== 'invalid');
|
|
35
|
+
return read.status === 'ok' ? read.config : {};
|
|
104
36
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Write to the global config file.
|
|
107
|
-
* Preserves existing fields and merges in new values.
|
|
108
|
-
*/
|
|
109
37
|
export async function writeConfig(updates) {
|
|
110
38
|
const configPath = getConfigPath();
|
|
111
|
-
// Read existing config and merge
|
|
112
39
|
const existing = await readConfig();
|
|
113
40
|
const merged = { ...existing, ...updates };
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
41
|
+
if (updates.telemetry && existing.telemetry && typeof existing.telemetry === 'object') {
|
|
42
|
+
merged.telemetry = {
|
|
43
|
+
...existing.telemetry,
|
|
44
|
+
...updates.telemetry,
|
|
45
|
+
};
|
|
117
46
|
}
|
|
118
47
|
await writeConfigFile(configPath, merged);
|
|
119
48
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Get the telemetry config section.
|
|
122
|
-
*/
|
|
123
49
|
export async function getTelemetryConfig() {
|
|
124
50
|
const config = await readConfig();
|
|
125
|
-
|
|
51
|
+
const telemetry = config.telemetry;
|
|
52
|
+
return telemetry && typeof telemetry === 'object' ? telemetry : {};
|
|
126
53
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Update the telemetry config section.
|
|
129
|
-
*/
|
|
130
54
|
export async function updateTelemetryConfig(updates) {
|
|
131
55
|
const existing = await getTelemetryConfig();
|
|
132
56
|
await writeConfig({
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function toChangeRelativePaths(changeDir: string, absolutePaths: string[]): string[];
|
|
2
|
+
export declare function readPrimaryArtifactBody(changeDir: string, absolutePaths: string[]): Promise<string | undefined>;
|
|
3
|
+
export declare function collectArtifactPathsMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string[]>>;
|
|
4
|
+
export declare function collectArtifactBodiesMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string>>;
|
|
5
|
+
export declare function readSanitizedFileAt(filePath: string, maxLength?: number): Promise<string | undefined>;
|
|
6
|
+
export declare function sanitizeErrorForTelemetry(error: unknown): {
|
|
7
|
+
error_message: string;
|
|
8
|
+
stack_trace?: string;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { MAX_ARTIFACT_BODY_LENGTH, readSanitizedFile, sanitizeTelemetryContent, } from './input.js';
|
|
3
|
+
export function toChangeRelativePaths(changeDir, absolutePaths) {
|
|
4
|
+
return absolutePaths.map((filePath) => {
|
|
5
|
+
const relative = path.relative(changeDir, filePath);
|
|
6
|
+
return relative.split(path.sep).join('/');
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function readPrimaryArtifactBody(changeDir, absolutePaths) {
|
|
10
|
+
const primary = absolutePaths[0];
|
|
11
|
+
if (!primary) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await readSanitizedFile(primary, MAX_ARTIFACT_BODY_LENGTH);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function collectArtifactPathsMap(changeDir, contextFiles) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [artifactId, paths] of Object.entries(contextFiles)) {
|
|
24
|
+
if (paths.length > 0) {
|
|
25
|
+
result[artifactId] = toChangeRelativePaths(changeDir, paths);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
export async function collectArtifactBodiesMap(changeDir, contextFiles) {
|
|
31
|
+
const bodies = {};
|
|
32
|
+
for (const [artifactId, paths] of Object.entries(contextFiles)) {
|
|
33
|
+
const body = await readPrimaryArtifactBody(changeDir, paths);
|
|
34
|
+
if (body) {
|
|
35
|
+
bodies[artifactId] = body;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return bodies;
|
|
39
|
+
}
|
|
40
|
+
export async function readSanitizedFileAt(filePath, maxLength = MAX_ARTIFACT_BODY_LENGTH) {
|
|
41
|
+
try {
|
|
42
|
+
return await readSanitizedFile(filePath, maxLength);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function sanitizeErrorForTelemetry(error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'unknown error';
|
|
50
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
51
|
+
return {
|
|
52
|
+
error_message: sanitizeTelemetryContent(message, 2000),
|
|
53
|
+
...(stack ? { stack_trace: sanitizeTelemetryContent(stack, 8000) } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GitDiffStats {
|
|
2
|
+
files_changed: number;
|
|
3
|
+
lines_added: number;
|
|
4
|
+
lines_removed: number;
|
|
5
|
+
lines_changed: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function captureGitHead(cwd: string): Promise<string | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Diff working tree (including staged/unstaged) against ref.
|
|
10
|
+
*/
|
|
11
|
+
export declare function diffStatsSince(ref: string, cwd: string, excludeOpenspec?: boolean): Promise<GitDiffStats | null>;
|
|
12
|
+
//# sourceMappingURL=git-stats.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort git stats for implementation_changed telemetry.
|
|
3
|
+
*/
|
|
4
|
+
import { execFile } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export async function captureGitHead(cwd) {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
10
|
+
cwd,
|
|
11
|
+
timeout: 2000,
|
|
12
|
+
});
|
|
13
|
+
const head = stdout.trim();
|
|
14
|
+
return head || null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function shouldExcludePath(filePath, excludeOpenspec) {
|
|
21
|
+
if (!excludeOpenspec) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
25
|
+
return normalized.startsWith('openspec/') || normalized.includes('/openspec/');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Diff working tree (including staged/unstaged) against ref.
|
|
29
|
+
*/
|
|
30
|
+
export async function diffStatsSince(ref, cwd, excludeOpenspec = true) {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execFileAsync('git', ['diff', '--numstat', ref], {
|
|
33
|
+
cwd,
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
let filesChanged = 0;
|
|
38
|
+
let linesAdded = 0;
|
|
39
|
+
let linesRemoved = 0;
|
|
40
|
+
for (const line of stdout.split('\n')) {
|
|
41
|
+
if (!line.trim()) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const parts = line.split('\t');
|
|
45
|
+
if (parts.length < 3) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const filePath = parts[2];
|
|
49
|
+
if (shouldExcludePath(filePath, excludeOpenspec)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
filesChanged++;
|
|
53
|
+
const added = parts[0] === '-' ? 0 : parseInt(parts[0], 10);
|
|
54
|
+
const removed = parts[1] === '-' ? 0 : parseInt(parts[1], 10);
|
|
55
|
+
linesAdded += Number.isNaN(added) ? 0 : added;
|
|
56
|
+
linesRemoved += Number.isNaN(removed) ? 0 : removed;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
files_changed: filesChanged,
|
|
60
|
+
lines_added: linesAdded,
|
|
61
|
+
lines_removed: linesRemoved,
|
|
62
|
+
lines_changed: linesAdded + linesRemoved,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=git-stats.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const IDENTIFY_STATE_FILENAME = "telemetry-identify-state.json";
|
|
2
|
+
export declare function getIdentifyStatePath(): string;
|
|
3
|
+
export declare function shouldIdentifyUser(userId: string): Promise<boolean>;
|
|
4
|
+
export declare function markUserIdentified(userId: string): Promise<void>;
|
|
5
|
+
/** @internal Test helper */
|
|
6
|
+
export declare function clearIdentifyStateForTests(): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=identify-cache.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process throttle for PostHog identify() calls.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { getGlobalConfigDir } from '../core/global-config.js';
|
|
7
|
+
export const IDENTIFY_STATE_FILENAME = 'telemetry-identify-state.json';
|
|
8
|
+
const IDENTIFY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
export function getIdentifyStatePath() {
|
|
10
|
+
return path.join(getGlobalConfigDir(), IDENTIFY_STATE_FILENAME);
|
|
11
|
+
}
|
|
12
|
+
export async function shouldIdentifyUser(userId) {
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(getIdentifyStatePath(), 'utf-8');
|
|
15
|
+
const parsed = JSON.parse(content);
|
|
16
|
+
if (parsed.userId !== userId || !parsed.identifiedAt) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const identifiedAt = Date.parse(parsed.identifiedAt);
|
|
20
|
+
if (Number.isNaN(identifiedAt)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return Date.now() - identifiedAt >= IDENTIFY_TTL_MS;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function markUserIdentified(userId) {
|
|
30
|
+
const dir = getGlobalConfigDir();
|
|
31
|
+
await fs.mkdir(dir, { recursive: true });
|
|
32
|
+
const state = {
|
|
33
|
+
userId,
|
|
34
|
+
identifiedAt: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
await fs.writeFile(getIdentifyStatePath(), JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
/** @internal Test helper */
|
|
39
|
+
export async function clearIdentifyStateForTests() {
|
|
40
|
+
try {
|
|
41
|
+
await fs.unlink(getIdentifyStatePath());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=identify-cache.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const IDENTITY_FILENAME = "telemetry-identity.json";
|
|
2
|
+
export declare class TelemetryIdentityRequiredError extends Error {
|
|
3
|
+
readonly code = "telemetry_identity_required";
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function buildIdentityRequiredMessage(): string;
|
|
7
|
+
export declare function validateUserId(value: string): true | string;
|
|
8
|
+
export declare function getIdentityFilePath(): string;
|
|
9
|
+
export declare function readStoredUserId(): Promise<string | null>;
|
|
10
|
+
export declare function writeStoredUserId(userId: string): Promise<void>;
|
|
11
|
+
export declare function resolveTelemetryUserId(options?: {
|
|
12
|
+
prompt?: boolean;
|
|
13
|
+
}): Promise<string | null>;
|
|
14
|
+
export declare function promptAndStoreTelemetryIdentity(): Promise<string>;
|
|
15
|
+
export declare function requireTelemetryIdentity(): Promise<string>;
|
|
16
|
+
export declare function setupTelemetryIdentity(options: {
|
|
17
|
+
interactive: boolean;
|
|
18
|
+
}): Promise<string>;
|
|
19
|
+
/** @deprecated Use setupTelemetryIdentity or requireTelemetryIdentity */
|
|
20
|
+
export declare function ensureTelemetryIdentity(): Promise<string | null>;
|
|
21
|
+
/** @internal Test helper */
|
|
22
|
+
export declare function resetIdentityCacheForTests(): void;
|
|
23
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codewalla telemetry identity — human-readable userId (email or username).
|
|
3
|
+
* Stored globally at ~/.config/openspec/telemetry-identity.json (mode 0600).
|
|
4
|
+
*/
|
|
5
|
+
import { input } from '@inquirer/prompts';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getGlobalConfigDir } from '../core/global-config.js';
|
|
9
|
+
export const IDENTITY_FILENAME = 'telemetry-identity.json';
|
|
10
|
+
const USERNAME_REGEX = /^[a-zA-Z0-9._-]{2,64}$/;
|
|
11
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
let cachedUserId = null;
|
|
13
|
+
export class TelemetryIdentityRequiredError extends Error {
|
|
14
|
+
code = 'telemetry_identity_required';
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message ?? buildIdentityRequiredMessage());
|
|
17
|
+
this.name = 'TelemetryIdentityRequiredError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function buildIdentityRequiredMessage() {
|
|
21
|
+
return (`Telemetry identity required. Run \`openspec init\` or \`openspec update\` interactively, ` +
|
|
22
|
+
`or create ${getIdentityFilePath()}.`);
|
|
23
|
+
}
|
|
24
|
+
export function validateUserId(value) {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (!trimmed) {
|
|
27
|
+
return 'Email or username is required';
|
|
28
|
+
}
|
|
29
|
+
if (EMAIL_REGEX.test(trimmed) || USERNAME_REGEX.test(trimmed)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return 'Enter a valid email (user@example.com) or username (2–64 chars: letters, numbers, . _ -)';
|
|
33
|
+
}
|
|
34
|
+
export function getIdentityFilePath() {
|
|
35
|
+
return path.join(getGlobalConfigDir(), IDENTITY_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
export async function readStoredUserId() {
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(getIdentityFilePath(), 'utf-8');
|
|
40
|
+
const parsed = JSON.parse(content);
|
|
41
|
+
if (parsed.userId && validateUserId(parsed.userId) === true) {
|
|
42
|
+
return parsed.userId.trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Missing or invalid file — treat as no identity
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export async function writeStoredUserId(userId) {
|
|
51
|
+
const filePath = getIdentityFilePath();
|
|
52
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
53
|
+
const payload = { userId: userId.trim() };
|
|
54
|
+
await fs.writeFile(filePath, JSON.stringify(payload, null, 2) + '\n', { mode: 0o600 });
|
|
55
|
+
cachedUserId = userId.trim();
|
|
56
|
+
}
|
|
57
|
+
export async function resolveTelemetryUserId(options) {
|
|
58
|
+
if (cachedUserId) {
|
|
59
|
+
return cachedUserId;
|
|
60
|
+
}
|
|
61
|
+
const envUser = process.env.OPENSPEC_TELEMETRY_USER?.trim();
|
|
62
|
+
if (envUser && validateUserId(envUser) === true) {
|
|
63
|
+
cachedUserId = envUser;
|
|
64
|
+
return envUser;
|
|
65
|
+
}
|
|
66
|
+
const stored = await readStoredUserId();
|
|
67
|
+
if (stored) {
|
|
68
|
+
cachedUserId = stored;
|
|
69
|
+
return stored;
|
|
70
|
+
}
|
|
71
|
+
if (options?.prompt === true) {
|
|
72
|
+
try {
|
|
73
|
+
return await promptAndStoreTelemetryIdentity();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof TelemetryIdentityRequiredError) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
export async function promptAndStoreTelemetryIdentity() {
|
|
85
|
+
const existing = await resolveTelemetryUserId({ prompt: false });
|
|
86
|
+
if (existing) {
|
|
87
|
+
return existing;
|
|
88
|
+
}
|
|
89
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
90
|
+
throw new TelemetryIdentityRequiredError();
|
|
91
|
+
}
|
|
92
|
+
console.log('Codewalla OpenSpec collects usage analytics tied to your email/username.');
|
|
93
|
+
const answer = await input({
|
|
94
|
+
message: 'Enter your Codewalla email or username:',
|
|
95
|
+
validate: validateUserId,
|
|
96
|
+
});
|
|
97
|
+
await writeStoredUserId(answer);
|
|
98
|
+
return answer.trim();
|
|
99
|
+
}
|
|
100
|
+
export async function requireTelemetryIdentity() {
|
|
101
|
+
const userId = await resolveTelemetryUserId({ prompt: false });
|
|
102
|
+
if (!userId) {
|
|
103
|
+
throw new TelemetryIdentityRequiredError();
|
|
104
|
+
}
|
|
105
|
+
return userId;
|
|
106
|
+
}
|
|
107
|
+
export async function setupTelemetryIdentity(options) {
|
|
108
|
+
const existing = await resolveTelemetryUserId({ prompt: false });
|
|
109
|
+
if (existing) {
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
if (options.interactive) {
|
|
113
|
+
return promptAndStoreTelemetryIdentity();
|
|
114
|
+
}
|
|
115
|
+
throw new TelemetryIdentityRequiredError();
|
|
116
|
+
}
|
|
117
|
+
/** @deprecated Use setupTelemetryIdentity or requireTelemetryIdentity */
|
|
118
|
+
export async function ensureTelemetryIdentity() {
|
|
119
|
+
return resolveTelemetryUserId({ prompt: true });
|
|
120
|
+
}
|
|
121
|
+
/** @internal Test helper */
|
|
122
|
+
export function resetIdentityCacheForTests() {
|
|
123
|
+
cachedUserId = null;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export declare function
|
|
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
|
-
*/
|
|
1
|
+
import type { CommandTelemetryContext } from './command-context.js';
|
|
2
|
+
export declare function canSendTelemetry(): Promise<boolean>;
|
|
3
|
+
export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
|
|
4
|
+
export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
|
|
5
|
+
export { sanitizeWorkflowInput, sanitizeTelemetryContent, readSanitizedFile, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, MAX_ARTIFACT_BODY_LENGTH, type WorkflowEditor, } from './input.js';
|
|
6
|
+
export { resolveTelemetryCommandPath, buildCommandTelemetryContext, type CommandTelemetryContext, type CommandCategory, } from './command-context.js';
|
|
7
|
+
export { resolveCaller } from './caller.js';
|
|
8
|
+
export declare function trackCommand(commandName: string, version: string, context?: CommandTelemetryContext): Promise<void>;
|
|
9
|
+
export declare function trackEvent(event: string, properties?: Record<string, unknown>): Promise<void>;
|
|
10
|
+
export declare function trackCommandFailed(command: string, error: unknown, errorCode?: string): Promise<void>;
|
|
30
11
|
export declare function shutdown(): Promise<void>;
|
|
12
|
+
/** @internal Test helper */
|
|
13
|
+
export declare function resetTelemetryForTests(): void;
|
|
14
|
+
export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
|
|
15
|
+
export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
|
|
16
|
+
export type { EntryPoint } from './marker.js';
|
|
31
17
|
//# sourceMappingURL=index.d.ts.map
|