openspec-cn 0.23.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 (235) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +153 -0
  3. package/bin/openspec.js +3 -0
  4. package/dist/cli/index.d.ts +2 -0
  5. package/dist/cli/index.js +480 -0
  6. package/dist/commands/change.d.ts +35 -0
  7. package/dist/commands/change.js +277 -0
  8. package/dist/commands/completion.d.ts +72 -0
  9. package/dist/commands/completion.js +257 -0
  10. package/dist/commands/config.d.ts +8 -0
  11. package/dist/commands/config.js +198 -0
  12. package/dist/commands/feedback.d.ts +9 -0
  13. package/dist/commands/feedback.js +183 -0
  14. package/dist/commands/schema.d.ts +6 -0
  15. package/dist/commands/schema.js +869 -0
  16. package/dist/commands/show.d.ts +14 -0
  17. package/dist/commands/show.js +132 -0
  18. package/dist/commands/spec.d.ts +15 -0
  19. package/dist/commands/spec.js +225 -0
  20. package/dist/commands/validate.d.ts +24 -0
  21. package/dist/commands/validate.js +294 -0
  22. package/dist/commands/workflow/index.d.ts +17 -0
  23. package/dist/commands/workflow/index.js +12 -0
  24. package/dist/commands/workflow/instructions.d.ts +29 -0
  25. package/dist/commands/workflow/instructions.js +381 -0
  26. package/dist/commands/workflow/new-change.d.ts +11 -0
  27. package/dist/commands/workflow/new-change.js +44 -0
  28. package/dist/commands/workflow/schemas.d.ts +10 -0
  29. package/dist/commands/workflow/schemas.js +34 -0
  30. package/dist/commands/workflow/shared.d.ts +52 -0
  31. package/dist/commands/workflow/shared.js +111 -0
  32. package/dist/commands/workflow/status.d.ts +14 -0
  33. package/dist/commands/workflow/status.js +58 -0
  34. package/dist/commands/workflow/templates.d.ts +16 -0
  35. package/dist/commands/workflow/templates.js +68 -0
  36. package/dist/core/archive.d.ts +11 -0
  37. package/dist/core/archive.js +280 -0
  38. package/dist/core/artifact-graph/graph.d.ts +56 -0
  39. package/dist/core/artifact-graph/graph.js +141 -0
  40. package/dist/core/artifact-graph/index.d.ts +7 -0
  41. package/dist/core/artifact-graph/index.js +13 -0
  42. package/dist/core/artifact-graph/instruction-loader.d.ts +143 -0
  43. package/dist/core/artifact-graph/instruction-loader.js +214 -0
  44. package/dist/core/artifact-graph/resolver.d.ts +81 -0
  45. package/dist/core/artifact-graph/resolver.js +257 -0
  46. package/dist/core/artifact-graph/schema.d.ts +13 -0
  47. package/dist/core/artifact-graph/schema.js +108 -0
  48. package/dist/core/artifact-graph/state.d.ts +12 -0
  49. package/dist/core/artifact-graph/state.js +54 -0
  50. package/dist/core/artifact-graph/types.d.ts +45 -0
  51. package/dist/core/artifact-graph/types.js +43 -0
  52. package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
  53. package/dist/core/command-generation/adapters/amazon-q.js +26 -0
  54. package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
  55. package/dist/core/command-generation/adapters/antigravity.js +26 -0
  56. package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
  57. package/dist/core/command-generation/adapters/auggie.js +27 -0
  58. package/dist/core/command-generation/adapters/claude.d.ts +13 -0
  59. package/dist/core/command-generation/adapters/claude.js +50 -0
  60. package/dist/core/command-generation/adapters/cline.d.ts +14 -0
  61. package/dist/core/command-generation/adapters/cline.js +27 -0
  62. package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
  63. package/dist/core/command-generation/adapters/codebuddy.js +28 -0
  64. package/dist/core/command-generation/adapters/codex.d.ts +13 -0
  65. package/dist/core/command-generation/adapters/codex.js +27 -0
  66. package/dist/core/command-generation/adapters/continue.d.ts +13 -0
  67. package/dist/core/command-generation/adapters/continue.js +28 -0
  68. package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
  69. package/dist/core/command-generation/adapters/costrict.js +27 -0
  70. package/dist/core/command-generation/adapters/crush.d.ts +13 -0
  71. package/dist/core/command-generation/adapters/crush.js +30 -0
  72. package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
  73. package/dist/core/command-generation/adapters/cursor.js +44 -0
  74. package/dist/core/command-generation/adapters/factory.d.ts +13 -0
  75. package/dist/core/command-generation/adapters/factory.js +27 -0
  76. package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
  77. package/dist/core/command-generation/adapters/gemini.js +26 -0
  78. package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
  79. package/dist/core/command-generation/adapters/github-copilot.js +26 -0
  80. package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
  81. package/dist/core/command-generation/adapters/iflow.js +29 -0
  82. package/dist/core/command-generation/adapters/index.d.ts +27 -0
  83. package/dist/core/command-generation/adapters/index.js +27 -0
  84. package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
  85. package/dist/core/command-generation/adapters/kilocode.js +23 -0
  86. package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
  87. package/dist/core/command-generation/adapters/opencode.js +26 -0
  88. package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
  89. package/dist/core/command-generation/adapters/qoder.js +30 -0
  90. package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
  91. package/dist/core/command-generation/adapters/qwen.js +26 -0
  92. package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
  93. package/dist/core/command-generation/adapters/roocode.js +27 -0
  94. package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
  95. package/dist/core/command-generation/adapters/windsurf.js +51 -0
  96. package/dist/core/command-generation/generator.d.ts +21 -0
  97. package/dist/core/command-generation/generator.js +27 -0
  98. package/dist/core/command-generation/index.d.ts +22 -0
  99. package/dist/core/command-generation/index.js +24 -0
  100. package/dist/core/command-generation/registry.d.ts +36 -0
  101. package/dist/core/command-generation/registry.js +88 -0
  102. package/dist/core/command-generation/types.d.ts +55 -0
  103. package/dist/core/command-generation/types.js +8 -0
  104. package/dist/core/completions/command-registry.d.ts +7 -0
  105. package/dist/core/completions/command-registry.js +456 -0
  106. package/dist/core/completions/completion-provider.d.ts +60 -0
  107. package/dist/core/completions/completion-provider.js +102 -0
  108. package/dist/core/completions/factory.d.ts +64 -0
  109. package/dist/core/completions/factory.js +75 -0
  110. package/dist/core/completions/generators/bash-generator.d.ts +32 -0
  111. package/dist/core/completions/generators/bash-generator.js +174 -0
  112. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  113. package/dist/core/completions/generators/fish-generator.js +157 -0
  114. package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
  115. package/dist/core/completions/generators/powershell-generator.js +207 -0
  116. package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
  117. package/dist/core/completions/generators/zsh-generator.js +250 -0
  118. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  119. package/dist/core/completions/installers/bash-installer.js +318 -0
  120. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  121. package/dist/core/completions/installers/fish-installer.js +143 -0
  122. package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
  123. package/dist/core/completions/installers/powershell-installer.js +327 -0
  124. package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
  125. package/dist/core/completions/installers/zsh-installer.js +449 -0
  126. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  127. package/dist/core/completions/templates/bash-templates.js +24 -0
  128. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  129. package/dist/core/completions/templates/fish-templates.js +39 -0
  130. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  131. package/dist/core/completions/templates/powershell-templates.js +25 -0
  132. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  133. package/dist/core/completions/templates/zsh-templates.js +36 -0
  134. package/dist/core/completions/types.d.ts +79 -0
  135. package/dist/core/completions/types.js +2 -0
  136. package/dist/core/config-prompts.d.ts +9 -0
  137. package/dist/core/config-prompts.js +34 -0
  138. package/dist/core/config-schema.d.ts +76 -0
  139. package/dist/core/config-schema.js +200 -0
  140. package/dist/core/config.d.ts +17 -0
  141. package/dist/core/config.js +30 -0
  142. package/dist/core/converters/json-converter.d.ts +6 -0
  143. package/dist/core/converters/json-converter.js +51 -0
  144. package/dist/core/global-config.d.ts +39 -0
  145. package/dist/core/global-config.js +115 -0
  146. package/dist/core/index.d.ts +2 -0
  147. package/dist/core/index.js +3 -0
  148. package/dist/core/init.d.ts +32 -0
  149. package/dist/core/init.js +433 -0
  150. package/dist/core/legacy-cleanup.d.ts +162 -0
  151. package/dist/core/legacy-cleanup.js +501 -0
  152. package/dist/core/list.d.ts +9 -0
  153. package/dist/core/list.js +171 -0
  154. package/dist/core/parsers/change-parser.d.ts +13 -0
  155. package/dist/core/parsers/change-parser.js +193 -0
  156. package/dist/core/parsers/markdown-parser.d.ts +22 -0
  157. package/dist/core/parsers/markdown-parser.js +187 -0
  158. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  159. package/dist/core/parsers/requirement-blocks.js +201 -0
  160. package/dist/core/project-config.d.ts +64 -0
  161. package/dist/core/project-config.js +223 -0
  162. package/dist/core/schemas/base.schema.d.ts +13 -0
  163. package/dist/core/schemas/base.schema.js +13 -0
  164. package/dist/core/schemas/change.schema.d.ts +73 -0
  165. package/dist/core/schemas/change.schema.js +31 -0
  166. package/dist/core/schemas/index.d.ts +4 -0
  167. package/dist/core/schemas/index.js +4 -0
  168. package/dist/core/schemas/spec.schema.d.ts +18 -0
  169. package/dist/core/schemas/spec.schema.js +15 -0
  170. package/dist/core/shared/index.d.ts +8 -0
  171. package/dist/core/shared/index.js +8 -0
  172. package/dist/core/shared/skill-generation.d.ts +41 -0
  173. package/dist/core/shared/skill-generation.js +74 -0
  174. package/dist/core/shared/tool-detection.d.ts +66 -0
  175. package/dist/core/shared/tool-detection.js +140 -0
  176. package/dist/core/specs-apply.d.ts +73 -0
  177. package/dist/core/specs-apply.js +384 -0
  178. package/dist/core/styles/palette.d.ts +7 -0
  179. package/dist/core/styles/palette.js +8 -0
  180. package/dist/core/templates/index.d.ts +8 -0
  181. package/dist/core/templates/index.js +9 -0
  182. package/dist/core/templates/skill-templates.d.ts +112 -0
  183. package/dist/core/templates/skill-templates.js +2893 -0
  184. package/dist/core/update.d.ts +42 -0
  185. package/dist/core/update.js +306 -0
  186. package/dist/core/validation/constants.d.ts +34 -0
  187. package/dist/core/validation/constants.js +40 -0
  188. package/dist/core/validation/types.d.ts +18 -0
  189. package/dist/core/validation/types.js +2 -0
  190. package/dist/core/validation/validator.d.ts +33 -0
  191. package/dist/core/validation/validator.js +409 -0
  192. package/dist/core/view.d.ts +8 -0
  193. package/dist/core/view.js +168 -0
  194. package/dist/index.d.ts +3 -0
  195. package/dist/index.js +3 -0
  196. package/dist/prompts/searchable-multi-select.d.ts +27 -0
  197. package/dist/prompts/searchable-multi-select.js +149 -0
  198. package/dist/telemetry/config.d.ts +32 -0
  199. package/dist/telemetry/config.js +68 -0
  200. package/dist/telemetry/index.d.ts +31 -0
  201. package/dist/telemetry/index.js +145 -0
  202. package/dist/ui/ascii-patterns.d.ts +16 -0
  203. package/dist/ui/ascii-patterns.js +133 -0
  204. package/dist/ui/welcome-screen.d.ts +10 -0
  205. package/dist/ui/welcome-screen.js +146 -0
  206. package/dist/utils/change-metadata.d.ts +51 -0
  207. package/dist/utils/change-metadata.js +147 -0
  208. package/dist/utils/change-utils.d.ts +62 -0
  209. package/dist/utils/change-utils.js +121 -0
  210. package/dist/utils/file-system.d.ts +36 -0
  211. package/dist/utils/file-system.js +281 -0
  212. package/dist/utils/index.d.ts +5 -0
  213. package/dist/utils/index.js +7 -0
  214. package/dist/utils/interactive.d.ts +18 -0
  215. package/dist/utils/interactive.js +21 -0
  216. package/dist/utils/item-discovery.d.ts +4 -0
  217. package/dist/utils/item-discovery.js +72 -0
  218. package/dist/utils/match.d.ts +3 -0
  219. package/dist/utils/match.js +22 -0
  220. package/dist/utils/shell-detection.d.ts +20 -0
  221. package/dist/utils/shell-detection.js +41 -0
  222. package/dist/utils/task-progress.d.ts +8 -0
  223. package/dist/utils/task-progress.js +36 -0
  224. package/package.json +84 -0
  225. package/schemas/spec-driven/schema.yaml +148 -0
  226. package/schemas/spec-driven/templates/design.md +19 -0
  227. package/schemas/spec-driven/templates/proposal.md +23 -0
  228. package/schemas/spec-driven/templates/spec.md +8 -0
  229. package/schemas/spec-driven/templates/tasks.md +9 -0
  230. package/schemas/tdd/schema.yaml +213 -0
  231. package/schemas/tdd/templates/docs.md +15 -0
  232. package/schemas/tdd/templates/implementation.md +11 -0
  233. package/schemas/tdd/templates/spec.md +11 -0
  234. package/schemas/tdd/templates/test.md +11 -0
  235. package/scripts/postinstall.js +147 -0
@@ -0,0 +1,149 @@
1
+ import chalk from 'chalk';
2
+ /**
3
+ * Create the searchable multi-select prompt.
4
+ * Uses dynamic import to prevent pre-commit hook hangs (see #367).
5
+ */
6
+ async function createSearchableMultiSelect() {
7
+ const { createPrompt, useState, useKeypress, useMemo, usePrefix, isEnterKey, isBackspaceKey, isUpKey, isDownKey, } = await import('@inquirer/core');
8
+ return createPrompt((config, done) => {
9
+ const { message, choices, pageSize = 15, validate } = config;
10
+ const [searchText, setSearchText] = useState('');
11
+ const [selectedValues, setSelectedValues] = useState(() => choices.filter(c => c.preSelected).map(c => c.value));
12
+ const [cursor, setCursor] = useState(0);
13
+ const [status, setStatus] = useState('idle');
14
+ const [error, setError] = useState(null);
15
+ const prefix = usePrefix({ status });
16
+ // Filter choices by search
17
+ const filteredChoices = useMemo(() => {
18
+ if (!searchText.trim())
19
+ return choices;
20
+ const term = searchText.toLowerCase();
21
+ return choices.filter((c) => c.name.toLowerCase().includes(term) ||
22
+ c.value.toLowerCase().includes(term));
23
+ }, [searchText, choices]);
24
+ const selectedSet = useMemo(() => new Set(selectedValues), [selectedValues]);
25
+ const choiceMap = useMemo(() => new Map(choices.map((c) => [c.value, c])), [choices]);
26
+ useKeypress((key) => {
27
+ if (status === 'done')
28
+ return;
29
+ // Tab to confirm
30
+ if (key.name === 'tab') {
31
+ if (validate) {
32
+ const result = validate(selectedValues);
33
+ if (result !== true) {
34
+ setError(typeof result === 'string' ? result : 'Invalid');
35
+ return;
36
+ }
37
+ }
38
+ setStatus('done');
39
+ done(selectedValues);
40
+ return;
41
+ }
42
+ // Enter to add item
43
+ if (isEnterKey(key)) {
44
+ const choice = filteredChoices[cursor];
45
+ if (choice && !selectedSet.has(choice.value)) {
46
+ setSelectedValues([...selectedValues, choice.value]);
47
+ setSearchText('');
48
+ setCursor(0);
49
+ }
50
+ return;
51
+ }
52
+ // Backspace to remove or delete search char
53
+ if (isBackspaceKey(key)) {
54
+ if (searchText === '' && selectedValues.length > 0) {
55
+ setSelectedValues(selectedValues.slice(0, -1));
56
+ }
57
+ else {
58
+ setSearchText(searchText.slice(0, -1));
59
+ setCursor(0);
60
+ }
61
+ return;
62
+ }
63
+ // Navigation
64
+ if (isUpKey(key)) {
65
+ setCursor(Math.max(0, cursor - 1));
66
+ return;
67
+ }
68
+ if (isDownKey(key)) {
69
+ setCursor(Math.min(filteredChoices.length - 1, cursor + 1));
70
+ return;
71
+ }
72
+ // Character input - handle printable characters
73
+ if (key.name && key.name.length === 1 && !key.ctrl) {
74
+ setSearchText(searchText + key.name);
75
+ setCursor(0);
76
+ }
77
+ });
78
+ // Render done state
79
+ if (status === 'done') {
80
+ const names = selectedValues
81
+ .map((v) => choiceMap.get(v)?.name ?? v)
82
+ .join(', ');
83
+ return `${prefix} ${chalk.bold(message)} ${chalk.cyan(names || '(none)')}`;
84
+ }
85
+ // Render active state
86
+ const lines = [];
87
+ lines.push(`${prefix} ${chalk.bold(message)}`);
88
+ // Selected chips
89
+ const chips = selectedValues.length > 0
90
+ ? selectedValues
91
+ .map((v) => chalk.bgCyan.black(` ${choiceMap.get(v)?.name} `))
92
+ .join(' ')
93
+ : chalk.dim('(none selected)');
94
+ lines.push(` Selected: ${chips}`);
95
+ // Search box
96
+ lines.push(` Search: ${chalk.yellow('[')}${searchText || chalk.dim('type to filter')}${chalk.yellow(']')}`);
97
+ // Instructions
98
+ lines.push(` ${chalk.cyan('↑↓')} navigate • ${chalk.cyan('Enter')} add • ${chalk.cyan('Backspace')} remove • ${chalk.cyan('Tab')} confirm`);
99
+ // List
100
+ if (filteredChoices.length === 0) {
101
+ lines.push(chalk.yellow(' No matches'));
102
+ }
103
+ else {
104
+ // Calculate pagination
105
+ const startIndex = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), filteredChoices.length - pageSize));
106
+ const endIndex = Math.min(startIndex + pageSize, filteredChoices.length);
107
+ const visibleChoices = filteredChoices.slice(startIndex, endIndex);
108
+ for (let i = 0; i < visibleChoices.length; i++) {
109
+ const item = visibleChoices[i];
110
+ const actualIndex = startIndex + i;
111
+ const isActive = actualIndex === cursor;
112
+ const selected = selectedSet.has(item.value);
113
+ const icon = selected ? chalk.green('◉') : chalk.dim('○');
114
+ const arrow = isActive ? chalk.cyan('›') : ' ';
115
+ const name = isActive ? chalk.cyan(item.name) : item.name;
116
+ const isRefresh = selected && item.configured;
117
+ const suffix = selected
118
+ ? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)')
119
+ : '';
120
+ lines.push(` ${arrow} ${icon} ${name}${suffix}`);
121
+ }
122
+ // Show pagination indicator if needed
123
+ if (filteredChoices.length > pageSize) {
124
+ const currentPage = Math.floor(cursor / pageSize) + 1;
125
+ const totalPages = Math.ceil(filteredChoices.length / pageSize);
126
+ lines.push(chalk.dim(` (${currentPage}/${totalPages})`));
127
+ }
128
+ }
129
+ if (error)
130
+ lines.push(chalk.red(` ${error}`));
131
+ return lines.join('\n');
132
+ });
133
+ }
134
+ /**
135
+ * A searchable multi-select prompt with visible search box,
136
+ * selected items display, and intuitive keyboard navigation.
137
+ *
138
+ * - Type to filter choices
139
+ * - ↑↓ to navigate
140
+ * - Enter to add highlighted item
141
+ * - Backspace to remove last selected item (or delete search char)
142
+ * - Tab to confirm selections
143
+ */
144
+ export async function searchableMultiSelect(config) {
145
+ const prompt = await createSearchableMultiSelect();
146
+ return prompt(config);
147
+ }
148
+ export default searchableMultiSelect;
149
+ //# sourceMappingURL=searchable-multi-select.js.map
@@ -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,16 @@
1
+ /**
2
+ * ASCII art animation patterns for the welcome screen.
3
+ * OpenSpec logo animation - diamond/rhombus shape with hollow center "O".
4
+ */
5
+ /**
6
+ * Welcome animation frames - OpenSpec logo building from center
7
+ * 7 rows × 6 columns diamond with hollow center "O"
8
+ * Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
9
+ * Each frame is an array of strings (lines of ASCII art)
10
+ * Grid: 6 cols × 2 chars = 12 chars wide
11
+ */
12
+ export declare const WELCOME_ANIMATION: {
13
+ interval: number;
14
+ frames: string[][];
15
+ };
16
+ //# sourceMappingURL=ascii-patterns.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * ASCII art animation patterns for the welcome screen.
3
+ * OpenSpec logo animation - diamond/rhombus shape with hollow center "O".
4
+ */
5
+ // Detect if full Unicode is supported
6
+ const supportsUnicode = process.platform !== 'win32' ||
7
+ !!process.env.WT_SESSION || // Windows Terminal
8
+ !!process.env.TERM_PROGRAM; // Modern terminal
9
+ // Character set based on Unicode support
10
+ // Block characters for pixel-art aesthetic
11
+ const CHARS = supportsUnicode
12
+ ? { full: '██', dim: '░░', empty: ' ' }
13
+ : { full: '##', dim: '++', empty: ' ' };
14
+ const _ = CHARS.empty;
15
+ const F = CHARS.full;
16
+ const D = CHARS.dim;
17
+ /**
18
+ * Welcome animation frames - OpenSpec logo building from center
19
+ * 7 rows × 6 columns diamond with hollow center "O"
20
+ * Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
21
+ * Each frame is an array of strings (lines of ASCII art)
22
+ * Grid: 6 cols × 2 chars = 12 chars wide
23
+ */
24
+ export const WELCOME_ANIMATION = {
25
+ interval: 120,
26
+ frames: [
27
+ // Frame 1: Empty
28
+ [
29
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
30
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
31
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
32
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
33
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
34
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
35
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
36
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
37
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
38
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
39
+ ],
40
+ // Frame 2: Center blocks appear (dim) - 2x3 center bar
41
+ [
42
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
43
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
44
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
45
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
46
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
47
+ `${_}${_}${_}${_}${D}${D}${_}${_}`,
48
+ `${_}${_}${_}${_}${D}${D}${_}${_}`,
49
+ `${_}${_}${_}${_}${D}${D}${_}${_}`,
50
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
51
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
52
+ ],
53
+ // Frame 3: Center blocks solidify
54
+ [
55
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
56
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
57
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
58
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
59
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
60
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
61
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
62
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
63
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
64
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
65
+ ],
66
+ // Frame 4: Top and bottom points appear
67
+ [
68
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
69
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
70
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
71
+ `${_}${_}${_}${_}${D}${D}${_}${_}`,
72
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
73
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
74
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
75
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
76
+ `${_}${_}${_}${_}${_}${_}${_}${_}`,
77
+ `${_}${_}${_}${_}${D}${D}${_}${_}`,
78
+ ],
79
+ // Frame 5: Inner ring forming
80
+ [
81
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
82
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
83
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
84
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
85
+ `${_}${_}${_}${D}${_}${_}${D}${_}`,
86
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
87
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
88
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
89
+ `${_}${_}${_}${D}${_}${_}${D}${_}`,
90
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
91
+ ],
92
+ // Frame 6: Outer ring appearing
93
+ [
94
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
95
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
96
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
97
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
98
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
99
+ `${_}${_}${D}${_}${F}${F}${_}${D}`,
100
+ `${_}${_}${D}${_}${F}${F}${_}${D}`,
101
+ `${_}${_}${D}${_}${F}${F}${_}${D}`,
102
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
103
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
104
+ ],
105
+ // Frame 7: Full logo
106
+ [
107
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
108
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
109
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
110
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
111
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
112
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
113
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
114
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
115
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
116
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
117
+ ],
118
+ // Frame 8: Hold complete logo
119
+ [
120
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
121
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
122
+ `${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
123
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
124
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
125
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
126
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
127
+ `${_}${_}${F}${_}${F}${F}${_}${F}`,
128
+ `${_}${_}${_}${F}${_}${_}${F}${_}`,
129
+ `${_}${_}${_}${_}${F}${F}${_}${_}`,
130
+ ],
131
+ ],
132
+ };
133
+ //# sourceMappingURL=ascii-patterns.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Animated welcome screen for the experimental artifact workflow setup.
3
+ * Shows side-by-side layout with animated ASCII art on left and welcome text on right.
4
+ */
5
+ /**
6
+ * Shows the animated welcome screen.
7
+ * Returns when user presses Enter.
8
+ */
9
+ export declare function showWelcomeScreen(): Promise<void>;
10
+ //# sourceMappingURL=welcome-screen.d.ts.map