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.
- package/LICENSE +22 -0
- package/README.md +435 -0
- package/bin/lightspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +361 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +257 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/core/archive.d.ts +11 -0
- package/dist/core/archive.js +280 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +456 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
- package/dist/core/completions/generators/powershell-generator.js +207 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
- package/dist/core/completions/generators/zsh-generator.js +250 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/completions/types.d.ts +79 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +30 -0
- package/dist/core/configurators/agents.d.ts +8 -0
- package/dist/core/configurators/agents.js +15 -0
- package/dist/core/configurators/base.d.ts +7 -0
- package/dist/core/configurators/base.js +2 -0
- package/dist/core/configurators/claude.d.ts +8 -0
- package/dist/core/configurators/claude.js +15 -0
- package/dist/core/configurators/cline.d.ts +8 -0
- package/dist/core/configurators/cline.js +15 -0
- package/dist/core/configurators/codebuddy.d.ts +8 -0
- package/dist/core/configurators/codebuddy.js +15 -0
- package/dist/core/configurators/costrict.d.ts +8 -0
- package/dist/core/configurators/costrict.js +15 -0
- package/dist/core/configurators/iflow.d.ts +8 -0
- package/dist/core/configurators/iflow.js +15 -0
- package/dist/core/configurators/qoder.d.ts +30 -0
- package/dist/core/configurators/qoder.js +42 -0
- package/dist/core/configurators/qwen.d.ts +24 -0
- package/dist/core/configurators/qwen.js +37 -0
- package/dist/core/configurators/registry.d.ts +9 -0
- package/dist/core/configurators/registry.js +43 -0
- package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
- package/dist/core/configurators/slash/amazon-q.js +46 -0
- package/dist/core/configurators/slash/antigravity.d.ts +9 -0
- package/dist/core/configurators/slash/antigravity.js +23 -0
- package/dist/core/configurators/slash/auggie.d.ts +9 -0
- package/dist/core/configurators/slash/auggie.js +31 -0
- package/dist/core/configurators/slash/base.d.ts +19 -0
- package/dist/core/configurators/slash/base.js +69 -0
- package/dist/core/configurators/slash/claude.d.ts +9 -0
- package/dist/core/configurators/slash/claude.js +37 -0
- package/dist/core/configurators/slash/cline.d.ts +9 -0
- package/dist/core/configurators/slash/cline.js +23 -0
- package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
- package/dist/core/configurators/slash/codebuddy.js +34 -0
- package/dist/core/configurators/slash/codex.d.ts +14 -0
- package/dist/core/configurators/slash/codex.js +109 -0
- package/dist/core/configurators/slash/continue.d.ts +9 -0
- package/dist/core/configurators/slash/continue.js +46 -0
- package/dist/core/configurators/slash/costrict.d.ts +9 -0
- package/dist/core/configurators/slash/costrict.js +31 -0
- package/dist/core/configurators/slash/crush.d.ts +9 -0
- package/dist/core/configurators/slash/crush.js +37 -0
- package/dist/core/configurators/slash/cursor.d.ts +9 -0
- package/dist/core/configurators/slash/cursor.js +37 -0
- package/dist/core/configurators/slash/factory.d.ts +10 -0
- package/dist/core/configurators/slash/factory.js +35 -0
- package/dist/core/configurators/slash/gemini.d.ts +9 -0
- package/dist/core/configurators/slash/gemini.js +22 -0
- package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
- package/dist/core/configurators/slash/github-copilot.js +34 -0
- package/dist/core/configurators/slash/iflow.d.ts +9 -0
- package/dist/core/configurators/slash/iflow.js +37 -0
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/opencode.d.ts +12 -0
- package/dist/core/configurators/slash/opencode.js +72 -0
- package/dist/core/configurators/slash/qoder.d.ts +35 -0
- package/dist/core/configurators/slash/qoder.js +76 -0
- package/dist/core/configurators/slash/qwen.d.ts +32 -0
- package/dist/core/configurators/slash/qwen.js +49 -0
- package/dist/core/configurators/slash/registry.d.ts +8 -0
- package/dist/core/configurators/slash/registry.js +78 -0
- package/dist/core/configurators/slash/roocode.d.ts +9 -0
- package/dist/core/configurators/slash/roocode.js +23 -0
- package/dist/core/configurators/slash/toml-base.d.ts +10 -0
- package/dist/core/configurators/slash/toml-base.js +53 -0
- package/dist/core/configurators/slash/windsurf.d.ts +9 -0
- package/dist/core/configurators/slash/windsurf.js +23 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +39 -0
- package/dist/core/global-config.js +115 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +52 -0
- package/dist/core/init.js +644 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/agents-root-stub.d.ts +2 -0
- package/dist/core/templates/agents-root-stub.js +17 -0
- package/dist/core/templates/agents-template.d.ts +2 -0
- package/dist/core/templates/agents-template.js +458 -0
- package/dist/core/templates/claude-template.d.ts +2 -0
- package/dist/core/templates/claude-template.js +2 -0
- package/dist/core/templates/cline-template.d.ts +2 -0
- package/dist/core/templates/cline-template.js +2 -0
- package/dist/core/templates/costrict-template.d.ts +2 -0
- package/dist/core/templates/costrict-template.js +2 -0
- package/dist/core/templates/index.d.ts +17 -0
- package/dist/core/templates/index.js +37 -0
- package/dist/core/templates/project-template.d.ts +8 -0
- package/dist/core/templates/project-template.js +32 -0
- package/dist/core/templates/slash-command-templates.d.ts +4 -0
- package/dist/core/templates/slash-command-templates.js +49 -0
- package/dist/core/update.d.ts +4 -0
- package/dist/core/update.js +88 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +103 -0
- package/dist/utils/file-system.d.ts +25 -0
- package/dist/utils/file-system.js +218 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +82 -0
- 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
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|