lightspec 0.1.1

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 (203) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +435 -0
  3. package/bin/lightspec.js +3 -0
  4. package/dist/cli/index.d.ts +2 -0
  5. package/dist/cli/index.js +361 -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/show.d.ts +14 -0
  15. package/dist/commands/show.js +132 -0
  16. package/dist/commands/spec.d.ts +15 -0
  17. package/dist/commands/spec.js +225 -0
  18. package/dist/commands/validate.d.ts +24 -0
  19. package/dist/commands/validate.js +294 -0
  20. package/dist/core/archive.d.ts +11 -0
  21. package/dist/core/archive.js +280 -0
  22. package/dist/core/completions/command-registry.d.ts +7 -0
  23. package/dist/core/completions/command-registry.js +456 -0
  24. package/dist/core/completions/completion-provider.d.ts +60 -0
  25. package/dist/core/completions/completion-provider.js +102 -0
  26. package/dist/core/completions/factory.d.ts +64 -0
  27. package/dist/core/completions/factory.js +75 -0
  28. package/dist/core/completions/generators/bash-generator.d.ts +32 -0
  29. package/dist/core/completions/generators/bash-generator.js +174 -0
  30. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  31. package/dist/core/completions/generators/fish-generator.js +157 -0
  32. package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
  33. package/dist/core/completions/generators/powershell-generator.js +207 -0
  34. package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
  35. package/dist/core/completions/generators/zsh-generator.js +250 -0
  36. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  37. package/dist/core/completions/installers/bash-installer.js +318 -0
  38. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  39. package/dist/core/completions/installers/fish-installer.js +143 -0
  40. package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
  41. package/dist/core/completions/installers/powershell-installer.js +327 -0
  42. package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
  43. package/dist/core/completions/installers/zsh-installer.js +449 -0
  44. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  45. package/dist/core/completions/templates/bash-templates.js +24 -0
  46. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  47. package/dist/core/completions/templates/fish-templates.js +39 -0
  48. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  49. package/dist/core/completions/templates/powershell-templates.js +25 -0
  50. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  51. package/dist/core/completions/templates/zsh-templates.js +36 -0
  52. package/dist/core/completions/types.d.ts +79 -0
  53. package/dist/core/completions/types.js +2 -0
  54. package/dist/core/config-prompts.d.ts +9 -0
  55. package/dist/core/config-prompts.js +34 -0
  56. package/dist/core/config-schema.d.ts +76 -0
  57. package/dist/core/config-schema.js +200 -0
  58. package/dist/core/config.d.ts +16 -0
  59. package/dist/core/config.js +30 -0
  60. package/dist/core/configurators/agents.d.ts +8 -0
  61. package/dist/core/configurators/agents.js +15 -0
  62. package/dist/core/configurators/base.d.ts +7 -0
  63. package/dist/core/configurators/base.js +2 -0
  64. package/dist/core/configurators/claude.d.ts +8 -0
  65. package/dist/core/configurators/claude.js +15 -0
  66. package/dist/core/configurators/cline.d.ts +8 -0
  67. package/dist/core/configurators/cline.js +15 -0
  68. package/dist/core/configurators/codebuddy.d.ts +8 -0
  69. package/dist/core/configurators/codebuddy.js +15 -0
  70. package/dist/core/configurators/costrict.d.ts +8 -0
  71. package/dist/core/configurators/costrict.js +15 -0
  72. package/dist/core/configurators/iflow.d.ts +8 -0
  73. package/dist/core/configurators/iflow.js +15 -0
  74. package/dist/core/configurators/qoder.d.ts +30 -0
  75. package/dist/core/configurators/qoder.js +42 -0
  76. package/dist/core/configurators/qwen.d.ts +24 -0
  77. package/dist/core/configurators/qwen.js +37 -0
  78. package/dist/core/configurators/registry.d.ts +9 -0
  79. package/dist/core/configurators/registry.js +43 -0
  80. package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
  81. package/dist/core/configurators/slash/amazon-q.js +46 -0
  82. package/dist/core/configurators/slash/antigravity.d.ts +9 -0
  83. package/dist/core/configurators/slash/antigravity.js +23 -0
  84. package/dist/core/configurators/slash/auggie.d.ts +9 -0
  85. package/dist/core/configurators/slash/auggie.js +31 -0
  86. package/dist/core/configurators/slash/base.d.ts +19 -0
  87. package/dist/core/configurators/slash/base.js +69 -0
  88. package/dist/core/configurators/slash/claude.d.ts +9 -0
  89. package/dist/core/configurators/slash/claude.js +37 -0
  90. package/dist/core/configurators/slash/cline.d.ts +9 -0
  91. package/dist/core/configurators/slash/cline.js +23 -0
  92. package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
  93. package/dist/core/configurators/slash/codebuddy.js +34 -0
  94. package/dist/core/configurators/slash/codex.d.ts +14 -0
  95. package/dist/core/configurators/slash/codex.js +109 -0
  96. package/dist/core/configurators/slash/continue.d.ts +9 -0
  97. package/dist/core/configurators/slash/continue.js +46 -0
  98. package/dist/core/configurators/slash/costrict.d.ts +9 -0
  99. package/dist/core/configurators/slash/costrict.js +31 -0
  100. package/dist/core/configurators/slash/crush.d.ts +9 -0
  101. package/dist/core/configurators/slash/crush.js +37 -0
  102. package/dist/core/configurators/slash/cursor.d.ts +9 -0
  103. package/dist/core/configurators/slash/cursor.js +37 -0
  104. package/dist/core/configurators/slash/factory.d.ts +10 -0
  105. package/dist/core/configurators/slash/factory.js +35 -0
  106. package/dist/core/configurators/slash/gemini.d.ts +9 -0
  107. package/dist/core/configurators/slash/gemini.js +22 -0
  108. package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
  109. package/dist/core/configurators/slash/github-copilot.js +34 -0
  110. package/dist/core/configurators/slash/iflow.d.ts +9 -0
  111. package/dist/core/configurators/slash/iflow.js +37 -0
  112. package/dist/core/configurators/slash/kilocode.d.ts +9 -0
  113. package/dist/core/configurators/slash/kilocode.js +17 -0
  114. package/dist/core/configurators/slash/opencode.d.ts +12 -0
  115. package/dist/core/configurators/slash/opencode.js +72 -0
  116. package/dist/core/configurators/slash/qoder.d.ts +35 -0
  117. package/dist/core/configurators/slash/qoder.js +76 -0
  118. package/dist/core/configurators/slash/qwen.d.ts +32 -0
  119. package/dist/core/configurators/slash/qwen.js +49 -0
  120. package/dist/core/configurators/slash/registry.d.ts +8 -0
  121. package/dist/core/configurators/slash/registry.js +78 -0
  122. package/dist/core/configurators/slash/roocode.d.ts +9 -0
  123. package/dist/core/configurators/slash/roocode.js +23 -0
  124. package/dist/core/configurators/slash/toml-base.d.ts +10 -0
  125. package/dist/core/configurators/slash/toml-base.js +53 -0
  126. package/dist/core/configurators/slash/windsurf.d.ts +9 -0
  127. package/dist/core/configurators/slash/windsurf.js +23 -0
  128. package/dist/core/converters/json-converter.d.ts +6 -0
  129. package/dist/core/converters/json-converter.js +51 -0
  130. package/dist/core/global-config.d.ts +39 -0
  131. package/dist/core/global-config.js +115 -0
  132. package/dist/core/index.d.ts +2 -0
  133. package/dist/core/index.js +3 -0
  134. package/dist/core/init.d.ts +52 -0
  135. package/dist/core/init.js +644 -0
  136. package/dist/core/list.d.ts +9 -0
  137. package/dist/core/list.js +171 -0
  138. package/dist/core/parsers/change-parser.d.ts +13 -0
  139. package/dist/core/parsers/change-parser.js +193 -0
  140. package/dist/core/parsers/markdown-parser.d.ts +22 -0
  141. package/dist/core/parsers/markdown-parser.js +187 -0
  142. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  143. package/dist/core/parsers/requirement-blocks.js +201 -0
  144. package/dist/core/project-config.d.ts +64 -0
  145. package/dist/core/project-config.js +223 -0
  146. package/dist/core/schemas/base.schema.d.ts +13 -0
  147. package/dist/core/schemas/base.schema.js +13 -0
  148. package/dist/core/schemas/change.schema.d.ts +73 -0
  149. package/dist/core/schemas/change.schema.js +31 -0
  150. package/dist/core/schemas/index.d.ts +4 -0
  151. package/dist/core/schemas/index.js +4 -0
  152. package/dist/core/schemas/spec.schema.d.ts +18 -0
  153. package/dist/core/schemas/spec.schema.js +15 -0
  154. package/dist/core/specs-apply.d.ts +73 -0
  155. package/dist/core/specs-apply.js +384 -0
  156. package/dist/core/styles/palette.d.ts +7 -0
  157. package/dist/core/styles/palette.js +8 -0
  158. package/dist/core/templates/agents-root-stub.d.ts +2 -0
  159. package/dist/core/templates/agents-root-stub.js +17 -0
  160. package/dist/core/templates/agents-template.d.ts +2 -0
  161. package/dist/core/templates/agents-template.js +458 -0
  162. package/dist/core/templates/claude-template.d.ts +2 -0
  163. package/dist/core/templates/claude-template.js +2 -0
  164. package/dist/core/templates/cline-template.d.ts +2 -0
  165. package/dist/core/templates/cline-template.js +2 -0
  166. package/dist/core/templates/costrict-template.d.ts +2 -0
  167. package/dist/core/templates/costrict-template.js +2 -0
  168. package/dist/core/templates/index.d.ts +17 -0
  169. package/dist/core/templates/index.js +37 -0
  170. package/dist/core/templates/project-template.d.ts +8 -0
  171. package/dist/core/templates/project-template.js +32 -0
  172. package/dist/core/templates/slash-command-templates.d.ts +4 -0
  173. package/dist/core/templates/slash-command-templates.js +49 -0
  174. package/dist/core/update.d.ts +4 -0
  175. package/dist/core/update.js +88 -0
  176. package/dist/core/validation/constants.d.ts +34 -0
  177. package/dist/core/validation/constants.js +40 -0
  178. package/dist/core/validation/types.d.ts +18 -0
  179. package/dist/core/validation/types.js +2 -0
  180. package/dist/core/validation/validator.d.ts +33 -0
  181. package/dist/core/validation/validator.js +409 -0
  182. package/dist/core/view.d.ts +8 -0
  183. package/dist/core/view.js +168 -0
  184. package/dist/index.d.ts +3 -0
  185. package/dist/index.js +3 -0
  186. package/dist/telemetry/config.d.ts +32 -0
  187. package/dist/telemetry/config.js +68 -0
  188. package/dist/telemetry/index.d.ts +31 -0
  189. package/dist/telemetry/index.js +103 -0
  190. package/dist/utils/file-system.d.ts +25 -0
  191. package/dist/utils/file-system.js +218 -0
  192. package/dist/utils/interactive.d.ts +18 -0
  193. package/dist/utils/interactive.js +21 -0
  194. package/dist/utils/item-discovery.d.ts +4 -0
  195. package/dist/utils/item-discovery.js +72 -0
  196. package/dist/utils/match.d.ts +3 -0
  197. package/dist/utils/match.js +22 -0
  198. package/dist/utils/shell-detection.d.ts +20 -0
  199. package/dist/utils/shell-detection.js +41 -0
  200. package/dist/utils/task-progress.d.ts +8 -0
  201. package/dist/utils/task-progress.js +36 -0
  202. package/package.json +82 -0
  203. package/scripts/postinstall.js +147 -0
@@ -0,0 +1,168 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+ import { getTaskProgressForChange } from '../utils/task-progress.js';
5
+ import { MarkdownParser } from './parsers/markdown-parser.js';
6
+ export class ViewCommand {
7
+ async execute(targetPath = '.') {
8
+ const lightspecDir = path.join(targetPath, 'lightspec');
9
+ if (!fs.existsSync(lightspecDir)) {
10
+ console.error(chalk.red('No lightspec directory found'));
11
+ process.exit(1);
12
+ }
13
+ console.log(chalk.bold('\nLightSpec Dashboard\n'));
14
+ console.log('═'.repeat(60));
15
+ // Get changes and specs data
16
+ const changesData = await this.getChangesData(lightspecDir);
17
+ const specsData = await this.getSpecsData(lightspecDir);
18
+ // Display summary metrics
19
+ this.displaySummary(changesData, specsData);
20
+ // Display draft changes
21
+ if (changesData.draft.length > 0) {
22
+ console.log(chalk.bold.gray('\nDraft Changes'));
23
+ console.log('─'.repeat(60));
24
+ changesData.draft.forEach((change) => {
25
+ console.log(` ${chalk.gray('○')} ${change.name}`);
26
+ });
27
+ }
28
+ // Display active changes
29
+ if (changesData.active.length > 0) {
30
+ console.log(chalk.bold.cyan('\nActive Changes'));
31
+ console.log('─'.repeat(60));
32
+ changesData.active.forEach((change) => {
33
+ const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
34
+ const percentage = change.progress.total > 0
35
+ ? Math.round((change.progress.completed / change.progress.total) * 100)
36
+ : 0;
37
+ console.log(` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
38
+ });
39
+ }
40
+ // Display completed changes
41
+ if (changesData.completed.length > 0) {
42
+ console.log(chalk.bold.green('\nCompleted Changes'));
43
+ console.log('─'.repeat(60));
44
+ changesData.completed.forEach((change) => {
45
+ console.log(` ${chalk.green('✓')} ${change.name}`);
46
+ });
47
+ }
48
+ // Display specifications
49
+ if (specsData.length > 0) {
50
+ console.log(chalk.bold.blue('\nSpecifications'));
51
+ console.log('─'.repeat(60));
52
+ // Sort specs by requirement count (descending)
53
+ specsData.sort((a, b) => b.requirementCount - a.requirementCount);
54
+ specsData.forEach(spec => {
55
+ const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements';
56
+ console.log(` ${chalk.blue('▪')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}`);
57
+ });
58
+ }
59
+ console.log('\n' + '═'.repeat(60));
60
+ console.log(chalk.dim(`\nUse ${chalk.white('lightspec list --changes')} or ${chalk.white('lightspec list --specs')} for detailed views`));
61
+ }
62
+ async getChangesData(lightspecDir) {
63
+ const changesDir = path.join(lightspecDir, 'changes');
64
+ if (!fs.existsSync(changesDir)) {
65
+ return { draft: [], active: [], completed: [] };
66
+ }
67
+ const draft = [];
68
+ const active = [];
69
+ const completed = [];
70
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (entry.isDirectory() && entry.name !== 'archive') {
73
+ const progress = await getTaskProgressForChange(changesDir, entry.name);
74
+ if (progress.total === 0) {
75
+ // No tasks defined yet - still in planning/draft phase
76
+ draft.push({ name: entry.name });
77
+ }
78
+ else if (progress.completed === progress.total) {
79
+ // All tasks complete
80
+ completed.push({ name: entry.name });
81
+ }
82
+ else {
83
+ // Has tasks but not all complete
84
+ active.push({ name: entry.name, progress });
85
+ }
86
+ }
87
+ }
88
+ // Sort all categories by name for deterministic ordering
89
+ draft.sort((a, b) => a.name.localeCompare(b.name));
90
+ // Sort active changes by completion percentage (ascending) and then by name
91
+ active.sort((a, b) => {
92
+ const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
93
+ const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
94
+ if (percentageA < percentageB)
95
+ return -1;
96
+ if (percentageA > percentageB)
97
+ return 1;
98
+ return a.name.localeCompare(b.name);
99
+ });
100
+ completed.sort((a, b) => a.name.localeCompare(b.name));
101
+ return { draft, active, completed };
102
+ }
103
+ async getSpecsData(lightspecDir) {
104
+ const specsDir = path.join(lightspecDir, 'specs');
105
+ if (!fs.existsSync(specsDir)) {
106
+ return [];
107
+ }
108
+ const specs = [];
109
+ const entries = fs.readdirSync(specsDir, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ if (entry.isDirectory()) {
112
+ const specFile = path.join(specsDir, entry.name, 'spec.md');
113
+ if (fs.existsSync(specFile)) {
114
+ try {
115
+ const content = fs.readFileSync(specFile, 'utf-8');
116
+ const parser = new MarkdownParser(content);
117
+ const spec = parser.parseSpec(entry.name);
118
+ const requirementCount = spec.requirements.length;
119
+ specs.push({ name: entry.name, requirementCount });
120
+ }
121
+ catch (error) {
122
+ // If spec cannot be parsed, include with 0 count
123
+ specs.push({ name: entry.name, requirementCount: 0 });
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return specs;
129
+ }
130
+ displaySummary(changesData, specsData) {
131
+ const totalChanges = changesData.draft.length + changesData.active.length + changesData.completed.length;
132
+ const totalSpecs = specsData.length;
133
+ const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
134
+ // Calculate total task progress
135
+ let totalTasks = 0;
136
+ let completedTasks = 0;
137
+ changesData.active.forEach((change) => {
138
+ totalTasks += change.progress.total;
139
+ completedTasks += change.progress.completed;
140
+ });
141
+ changesData.completed.forEach(() => {
142
+ // Completed changes count as 100% done (we don't know exact task count)
143
+ // This is a simplification
144
+ });
145
+ console.log(chalk.bold('Summary:'));
146
+ console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
147
+ if (changesData.draft.length > 0) {
148
+ console.log(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`);
149
+ }
150
+ console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
151
+ console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
152
+ if (totalTasks > 0) {
153
+ const overallProgress = Math.round((completedTasks / totalTasks) * 100);
154
+ console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
155
+ }
156
+ }
157
+ createProgressBar(completed, total, width = 20) {
158
+ if (total === 0)
159
+ return chalk.dim('─'.repeat(width));
160
+ const percentage = completed / total;
161
+ const filled = Math.round(percentage * width);
162
+ const empty = width - filled;
163
+ const filledBar = chalk.green('█'.repeat(filled));
164
+ const emptyBar = chalk.dim('░'.repeat(empty));
165
+ return `[${filledBar}${emptyBar}]`;
166
+ }
167
+ }
168
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1,3 @@
1
+ export * from './cli/index.js';
2
+ export * from './core/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './cli/index.js';
2
+ export * from './core/index.js';
3
+ //# sourceMappingURL=index.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/lightspec/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/lightspec/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/lightspec/config.json on all platforms.
11
+ */
12
+ export function getConfigPath() {
13
+ const configDir = path.join(os.homedir(), '.config', 'lightspec');
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
+ * - LIGHTSPEC_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 LightSpec 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 telemetry resources before CLI exit.
28
+ * Call this before CLI exit.
29
+ */
30
+ export declare function shutdown(): Promise<void>;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,103 @@
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 LIGHTSPEC_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 { randomUUID } from 'crypto';
12
+ import { getTelemetryConfig, updateTelemetryConfig } from './config.js';
13
+ let anonymousId = null;
14
+ /**
15
+ * Check if telemetry is enabled.
16
+ *
17
+ * Disabled when:
18
+ * - LIGHTSPEC_TELEMETRY=0
19
+ * - DO_NOT_TRACK=1
20
+ * - CI=true (any CI environment)
21
+ */
22
+ export function isTelemetryEnabled() {
23
+ // Check explicit opt-out
24
+ if (process.env.LIGHTSPEC_TELEMETRY === '0') {
25
+ return false;
26
+ }
27
+ // Respect DO_NOT_TRACK standard
28
+ if (process.env.DO_NOT_TRACK === '1') {
29
+ return false;
30
+ }
31
+ // Auto-disable in CI environments
32
+ if (process.env.CI === 'true') {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ /**
38
+ * Get or create the anonymous user ID.
39
+ * Lazily generates a UUID on first call and persists it.
40
+ */
41
+ export async function getOrCreateAnonymousId() {
42
+ // Return cached value if available
43
+ if (anonymousId) {
44
+ return anonymousId;
45
+ }
46
+ // Try to load from config
47
+ const config = await getTelemetryConfig();
48
+ if (config.anonymousId) {
49
+ anonymousId = config.anonymousId;
50
+ return anonymousId;
51
+ }
52
+ // Generate new UUID and persist
53
+ anonymousId = randomUUID();
54
+ await updateTelemetryConfig({ anonymousId });
55
+ return anonymousId;
56
+ }
57
+ /**
58
+ * Track a command execution.
59
+ *
60
+ * @param commandName - The command name (e.g., 'init', 'change:apply')
61
+ * @param version - The LightSpec version
62
+ */
63
+ export async function trackCommand(_commandName, _version) {
64
+ if (!isTelemetryEnabled()) {
65
+ return;
66
+ }
67
+ try {
68
+ // Keep anonymous ID creation for consistency with existing telemetry state.
69
+ await getOrCreateAnonymousId();
70
+ }
71
+ catch {
72
+ // Silent failure - telemetry should never break CLI
73
+ }
74
+ }
75
+ /**
76
+ * Show first-run telemetry notice if not already seen.
77
+ */
78
+ export async function maybeShowTelemetryNotice() {
79
+ if (!isTelemetryEnabled()) {
80
+ return;
81
+ }
82
+ try {
83
+ const config = await getTelemetryConfig();
84
+ if (config.noticeSeen) {
85
+ return;
86
+ }
87
+ // Display notice
88
+ console.log('Note: LightSpec collects anonymous usage stats. Opt out: LIGHTSPEC_TELEMETRY=0');
89
+ // Mark as seen
90
+ await updateTelemetryConfig({ noticeSeen: true });
91
+ }
92
+ catch {
93
+ // Silent failure - telemetry should never break CLI
94
+ }
95
+ }
96
+ /**
97
+ * Shutdown telemetry resources before CLI exit.
98
+ * Call this before CLI exit.
99
+ */
100
+ export async function shutdown() {
101
+ return Promise.resolve();
102
+ }
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ export declare class FileSystemUtils {
2
+ /**
3
+ * Converts a path to use forward slashes (POSIX style).
4
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
5
+ */
6
+ static toPosixPath(p: string): string;
7
+ private static isWindowsBasePath;
8
+ private static normalizeSegments;
9
+ static joinPath(basePath: string, ...segments: string[]): string;
10
+ static createDirectory(dirPath: string): Promise<void>;
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;
18
+ static canWriteFile(filePath: string): Promise<boolean>;
19
+ static directoryExists(dirPath: string): Promise<boolean>;
20
+ static writeFile(filePath: string, content: string): Promise<void>;
21
+ static readFile(filePath: string): Promise<string>;
22
+ static updateFileWithMarkers(filePath: string, content: string, startMarker: string, endMarker: string): Promise<void>;
23
+ static ensureWritePermissions(dirPath: string): Promise<boolean>;
24
+ }
25
+ //# sourceMappingURL=file-system.d.ts.map
@@ -0,0 +1,218 @@
1
+ import { promises as fs, constants as fsConstants } from 'fs';
2
+ import path from 'path';
3
+ function isMarkerOnOwnLine(content, markerIndex, markerLength) {
4
+ let leftIndex = markerIndex - 1;
5
+ while (leftIndex >= 0 && content[leftIndex] !== '\n') {
6
+ const char = content[leftIndex];
7
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
8
+ return false;
9
+ }
10
+ leftIndex--;
11
+ }
12
+ let rightIndex = markerIndex + markerLength;
13
+ while (rightIndex < content.length && content[rightIndex] !== '\n') {
14
+ const char = content[rightIndex];
15
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
16
+ return false;
17
+ }
18
+ rightIndex++;
19
+ }
20
+ return true;
21
+ }
22
+ function findMarkerIndex(content, marker, fromIndex = 0) {
23
+ let currentIndex = content.indexOf(marker, fromIndex);
24
+ while (currentIndex !== -1) {
25
+ if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
26
+ return currentIndex;
27
+ }
28
+ currentIndex = content.indexOf(marker, currentIndex + marker.length);
29
+ }
30
+ return -1;
31
+ }
32
+ export class FileSystemUtils {
33
+ /**
34
+ * Converts a path to use forward slashes (POSIX style).
35
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
36
+ */
37
+ static toPosixPath(p) {
38
+ return p.replace(/\\/g, '/');
39
+ }
40
+ static isWindowsBasePath(basePath) {
41
+ return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
42
+ }
43
+ static normalizeSegments(segments) {
44
+ return segments
45
+ .flatMap((segment) => segment.split(/[\\/]+/u))
46
+ .filter((part) => part.length > 0);
47
+ }
48
+ static joinPath(basePath, ...segments) {
49
+ const normalizedSegments = this.normalizeSegments(segments);
50
+ if (this.isWindowsBasePath(basePath)) {
51
+ const normalizedBasePath = path.win32.normalize(basePath);
52
+ return normalizedSegments.length
53
+ ? path.win32.join(normalizedBasePath, ...normalizedSegments)
54
+ : normalizedBasePath;
55
+ }
56
+ const posixBasePath = basePath.replace(/\\/g, '/');
57
+ return normalizedSegments.length
58
+ ? path.posix.join(posixBasePath, ...normalizedSegments)
59
+ : path.posix.normalize(posixBasePath);
60
+ }
61
+ static async createDirectory(dirPath) {
62
+ await fs.mkdir(dirPath, { recursive: true });
63
+ }
64
+ static async fileExists(filePath) {
65
+ try {
66
+ await fs.access(filePath);
67
+ return true;
68
+ }
69
+ catch (error) {
70
+ if (error.code !== 'ENOENT') {
71
+ console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
72
+ }
73
+ return false;
74
+ }
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
+ }
111
+ static async canWriteFile(filePath) {
112
+ try {
113
+ const stats = await fs.stat(filePath);
114
+ if (!stats.isFile()) {
115
+ return true;
116
+ }
117
+ // On Windows, stats.mode doesn't reliably indicate write permissions.
118
+ // Use fs.access with W_OK to check actual write permissions cross-platform.
119
+ try {
120
+ await fs.access(filePath, fsConstants.W_OK);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (error.code === 'ENOENT') {
129
+ // File doesn't exist - find first existing parent directory and check its permissions
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
137
+ try {
138
+ await fs.access(existingDir, fsConstants.W_OK);
139
+ return true;
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ }
145
+ console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
146
+ return false;
147
+ }
148
+ }
149
+ static async directoryExists(dirPath) {
150
+ try {
151
+ const stats = await fs.stat(dirPath);
152
+ return stats.isDirectory();
153
+ }
154
+ catch (error) {
155
+ if (error.code !== 'ENOENT') {
156
+ console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
157
+ }
158
+ return false;
159
+ }
160
+ }
161
+ static async writeFile(filePath, content) {
162
+ const dir = path.dirname(filePath);
163
+ await this.createDirectory(dir);
164
+ await fs.writeFile(filePath, content, 'utf-8');
165
+ }
166
+ static async readFile(filePath) {
167
+ return await fs.readFile(filePath, 'utf-8');
168
+ }
169
+ static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
170
+ let existingContent = '';
171
+ if (await this.fileExists(filePath)) {
172
+ existingContent = await this.readFile(filePath);
173
+ const startIndex = findMarkerIndex(existingContent, startMarker);
174
+ const endIndex = startIndex !== -1
175
+ ? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
176
+ : findMarkerIndex(existingContent, endMarker);
177
+ if (startIndex !== -1 && endIndex !== -1) {
178
+ if (endIndex < startIndex) {
179
+ throw new Error(`Invalid marker state in ${filePath}. End marker appears before start marker.`);
180
+ }
181
+ const before = existingContent.substring(0, startIndex);
182
+ const after = existingContent.substring(endIndex + endMarker.length);
183
+ existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
184
+ }
185
+ else if (startIndex === -1 && endIndex === -1) {
186
+ existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
187
+ }
188
+ else {
189
+ throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
190
+ }
191
+ }
192
+ else {
193
+ existingContent = startMarker + '\n' + content + '\n' + endMarker;
194
+ }
195
+ await this.writeFile(filePath, existingContent);
196
+ }
197
+ static async ensureWritePermissions(dirPath) {
198
+ try {
199
+ // If directory doesn't exist, check parent directory permissions
200
+ if (!await this.directoryExists(dirPath)) {
201
+ const parentDir = path.dirname(dirPath);
202
+ if (!await this.directoryExists(parentDir)) {
203
+ await this.createDirectory(parentDir);
204
+ }
205
+ return await this.ensureWritePermissions(parentDir);
206
+ }
207
+ const testFile = path.join(dirPath, '.lightspec-test-' + Date.now());
208
+ await fs.writeFile(testFile, '');
209
+ await fs.unlink(testFile);
210
+ return true;
211
+ }
212
+ catch (error) {
213
+ console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
214
+ return false;
215
+ }
216
+ }
217
+ }
218
+ //# sourceMappingURL=file-system.js.map
@@ -0,0 +1,18 @@
1
+ export type InteractiveOptions = {
2
+ /**
3
+ * Explicit "disable prompts" flag passed by internal callers.
4
+ */
5
+ noInteractive?: boolean;
6
+ /**
7
+ * Commander-style negated option: `--no-interactive` sets this to false.
8
+ */
9
+ interactive?: boolean;
10
+ };
11
+ /**
12
+ * Resolves whether non-interactive mode is requested.
13
+ * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
14
+ * Use this helper instead of manually checking options.noInteractive to avoid bugs.
15
+ */
16
+ export declare function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean;
17
+ export declare function isInteractive(value?: boolean | InteractiveOptions): boolean;
18
+ //# sourceMappingURL=interactive.d.ts.map