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,644 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
6
|
+
import { TemplateManager } from './templates/index.js';
|
|
7
|
+
import { ToolRegistry } from './configurators/registry.js';
|
|
8
|
+
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
9
|
+
import { AI_TOOLS, LIGHTSPEC_DIR_NAME, LIGHTSPEC_MARKERS, } from './config.js';
|
|
10
|
+
import { PALETTE } from './styles/palette.js';
|
|
11
|
+
const PROGRESS_SPINNER = {
|
|
12
|
+
interval: 80,
|
|
13
|
+
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
14
|
+
};
|
|
15
|
+
const LETTER_MAP = {
|
|
16
|
+
O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
|
|
17
|
+
P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
|
|
18
|
+
E: ['██████', '██ ', '█████ ', '██ ', '██████'],
|
|
19
|
+
N: ['██ ██', '███ ██', '██ ███', '██ ██', '██ ██'],
|
|
20
|
+
S: [' █████', '██ ', ' ████ ', ' ██', '█████ '],
|
|
21
|
+
C: [' █████', '██ ', '██ ', '██ ', ' █████'],
|
|
22
|
+
' ': [' ', ' ', ' ', ' ', ' '],
|
|
23
|
+
};
|
|
24
|
+
const sanitizeToolLabel = (raw) => raw.replace(/✅/gu, '✔').trim();
|
|
25
|
+
const parseToolLabel = (raw) => {
|
|
26
|
+
const sanitized = sanitizeToolLabel(raw);
|
|
27
|
+
const match = sanitized.match(/^(.*?)\s*\((.+)\)$/u);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { primary: sanitized };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
primary: match[1].trim(),
|
|
33
|
+
annotation: match[2].trim(),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const isSelectableChoice = (choice) => choice.selectable;
|
|
37
|
+
const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
|
|
38
|
+
const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
|
|
39
|
+
const LIST_SPACER_VALUE = '__list-spacer__';
|
|
40
|
+
const toolSelectionWizard = createPrompt((config, done) => {
|
|
41
|
+
const totalSteps = 3;
|
|
42
|
+
const [step, setStep] = useState('intro');
|
|
43
|
+
const selectableChoices = config.choices.filter(isSelectableChoice);
|
|
44
|
+
const initialCursorIndex = config.choices.findIndex((choice) => choice.selectable);
|
|
45
|
+
const [cursor, setCursor] = useState(initialCursorIndex === -1 ? 0 : initialCursorIndex);
|
|
46
|
+
const [selected, setSelected] = useState(() => {
|
|
47
|
+
const initial = new Set((config.initialSelected ?? []).filter((value) => selectableChoices.some((choice) => choice.value === value)));
|
|
48
|
+
return selectableChoices
|
|
49
|
+
.map((choice) => choice.value)
|
|
50
|
+
.filter((value) => initial.has(value));
|
|
51
|
+
});
|
|
52
|
+
const [error, setError] = useState(null);
|
|
53
|
+
const selectedSet = new Set(selected);
|
|
54
|
+
const pageSize = Math.max(config.choices.length, 1);
|
|
55
|
+
const updateSelected = (next) => {
|
|
56
|
+
const ordered = selectableChoices
|
|
57
|
+
.map((choice) => choice.value)
|
|
58
|
+
.filter((value) => next.has(value));
|
|
59
|
+
setSelected(ordered);
|
|
60
|
+
};
|
|
61
|
+
const page = usePagination({
|
|
62
|
+
items: config.choices,
|
|
63
|
+
active: cursor,
|
|
64
|
+
pageSize,
|
|
65
|
+
loop: false,
|
|
66
|
+
renderItem: ({ item, isActive }) => {
|
|
67
|
+
if (!item.selectable) {
|
|
68
|
+
const prefix = item.kind === 'info' ? ' ' : '';
|
|
69
|
+
const textColor = item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
|
|
70
|
+
return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(`${prefix}${item.label.primary}`)}`;
|
|
71
|
+
}
|
|
72
|
+
const isSelected = selectedSet.has(item.value);
|
|
73
|
+
const cursorSymbol = isActive
|
|
74
|
+
? PALETTE.white('›')
|
|
75
|
+
: PALETTE.midGray(' ');
|
|
76
|
+
const indicator = isSelected
|
|
77
|
+
? PALETTE.white('◉')
|
|
78
|
+
: PALETTE.midGray('○');
|
|
79
|
+
const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
|
|
80
|
+
const annotation = item.label.annotation
|
|
81
|
+
? PALETTE.midGray(` (${item.label.annotation})`)
|
|
82
|
+
: '';
|
|
83
|
+
const configuredNote = item.configured
|
|
84
|
+
? PALETTE.midGray(' (already configured)')
|
|
85
|
+
: '';
|
|
86
|
+
const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
|
|
87
|
+
return `${cursorSymbol} ${indicator} ${label}`;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const moveCursor = (direction) => {
|
|
91
|
+
if (selectableChoices.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let nextIndex = cursor;
|
|
95
|
+
while (true) {
|
|
96
|
+
nextIndex = nextIndex + direction;
|
|
97
|
+
if (nextIndex < 0 || nextIndex >= config.choices.length) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (config.choices[nextIndex]?.selectable) {
|
|
101
|
+
setCursor(nextIndex);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
useKeypress((key) => {
|
|
107
|
+
if (step === 'intro') {
|
|
108
|
+
if (isEnterKey(key)) {
|
|
109
|
+
setStep('select');
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (step === 'select') {
|
|
114
|
+
if (isUpKey(key)) {
|
|
115
|
+
moveCursor(-1);
|
|
116
|
+
setError(null);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (isDownKey(key)) {
|
|
120
|
+
moveCursor(1);
|
|
121
|
+
setError(null);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isSpaceKey(key)) {
|
|
125
|
+
const current = config.choices[cursor];
|
|
126
|
+
if (!current || !current.selectable)
|
|
127
|
+
return;
|
|
128
|
+
const next = new Set(selected);
|
|
129
|
+
if (next.has(current.value)) {
|
|
130
|
+
next.delete(current.value);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
next.add(current.value);
|
|
134
|
+
}
|
|
135
|
+
updateSelected(next);
|
|
136
|
+
setError(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (isEnterKey(key)) {
|
|
140
|
+
const current = config.choices[cursor];
|
|
141
|
+
if (current &&
|
|
142
|
+
current.selectable &&
|
|
143
|
+
!selectedSet.has(current.value)) {
|
|
144
|
+
const next = new Set(selected);
|
|
145
|
+
next.add(current.value);
|
|
146
|
+
updateSelected(next);
|
|
147
|
+
}
|
|
148
|
+
setStep('review');
|
|
149
|
+
setError(null);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.name === 'escape') {
|
|
153
|
+
const next = new Set();
|
|
154
|
+
updateSelected(next);
|
|
155
|
+
setError(null);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (step === 'review') {
|
|
160
|
+
if (isEnterKey(key)) {
|
|
161
|
+
const finalSelection = config.choices
|
|
162
|
+
.map((choice) => choice.value)
|
|
163
|
+
.filter((value) => selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE);
|
|
164
|
+
done(finalSelection);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (isBackspaceKey(key) || key.name === 'escape') {
|
|
168
|
+
setStep('select');
|
|
169
|
+
setError(null);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const rootStubChoice = selectableChoices.find((choice) => choice.value === ROOT_STUB_CHOICE_VALUE);
|
|
174
|
+
const rootStubSelected = rootStubChoice
|
|
175
|
+
? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
|
|
176
|
+
: false;
|
|
177
|
+
const nativeChoices = selectableChoices.filter((choice) => choice.value !== ROOT_STUB_CHOICE_VALUE);
|
|
178
|
+
const selectedNativeChoices = nativeChoices.filter((choice) => selectedSet.has(choice.value));
|
|
179
|
+
const formatSummaryLabel = (choice) => {
|
|
180
|
+
const annotation = choice.label.annotation
|
|
181
|
+
? PALETTE.midGray(` (${choice.label.annotation})`)
|
|
182
|
+
: '';
|
|
183
|
+
const configuredNote = choice.configured
|
|
184
|
+
? PALETTE.midGray(' (already configured)')
|
|
185
|
+
: '';
|
|
186
|
+
return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
|
|
187
|
+
};
|
|
188
|
+
const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
|
|
189
|
+
const lines = [];
|
|
190
|
+
lines.push(PALETTE.midGray(`Step ${stepIndex}/${totalSteps}`));
|
|
191
|
+
lines.push('');
|
|
192
|
+
if (step === 'intro') {
|
|
193
|
+
const introHeadline = config.extendMode
|
|
194
|
+
? 'Extend your LightSpec tooling'
|
|
195
|
+
: 'Configure your LightSpec tooling';
|
|
196
|
+
const introBody = config.extendMode
|
|
197
|
+
? 'We detected an existing setup. We will help you refresh or add integrations.'
|
|
198
|
+
: "Let's get your AI assistants connected so they understand LightSpec.";
|
|
199
|
+
lines.push(PALETTE.white(introHeadline));
|
|
200
|
+
lines.push(PALETTE.midGray(introBody));
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push(PALETTE.midGray('Press Enter to continue.'));
|
|
203
|
+
}
|
|
204
|
+
else if (step === 'select') {
|
|
205
|
+
lines.push(PALETTE.white(config.baseMessage));
|
|
206
|
+
lines.push(PALETTE.midGray('Use ↑/↓ to move · Space to toggle · Enter selects highlighted tool and reviews.'));
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(page);
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push(PALETTE.midGray('Selected configuration:'));
|
|
211
|
+
if (rootStubSelected && rootStubChoice) {
|
|
212
|
+
lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`);
|
|
213
|
+
}
|
|
214
|
+
if (selectedNativeChoices.length === 0) {
|
|
215
|
+
lines.push(` ${PALETTE.midGray('- No natively supported providers selected')}`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
selectedNativeChoices.forEach((choice) => {
|
|
219
|
+
lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
lines.push(PALETTE.white('Review selections'));
|
|
225
|
+
lines.push(PALETTE.midGray('Press Enter to confirm or Backspace to adjust.'));
|
|
226
|
+
lines.push('');
|
|
227
|
+
if (rootStubSelected && rootStubChoice) {
|
|
228
|
+
lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(rootStubChoice)}`);
|
|
229
|
+
}
|
|
230
|
+
if (selectedNativeChoices.length === 0) {
|
|
231
|
+
lines.push(PALETTE.midGray('No natively supported providers selected. Universal instructions will still be applied.'));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
selectedNativeChoices.forEach((choice) => {
|
|
235
|
+
lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (error) {
|
|
240
|
+
return [lines.join('\n'), chalk.red(error)];
|
|
241
|
+
}
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
});
|
|
244
|
+
export class InitCommand {
|
|
245
|
+
prompt;
|
|
246
|
+
toolsArg;
|
|
247
|
+
constructor(options = {}) {
|
|
248
|
+
this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
|
|
249
|
+
this.toolsArg = options.tools;
|
|
250
|
+
}
|
|
251
|
+
async execute(targetPath) {
|
|
252
|
+
const projectPath = path.resolve(targetPath);
|
|
253
|
+
const lightspecDir = LIGHTSPEC_DIR_NAME;
|
|
254
|
+
const lightspecPath = path.join(projectPath, lightspecDir);
|
|
255
|
+
// Validation happens silently in the background
|
|
256
|
+
const extendMode = await this.validate(projectPath, lightspecPath);
|
|
257
|
+
const existingToolStates = await this.getExistingToolStates(projectPath, extendMode);
|
|
258
|
+
this.renderBanner(extendMode);
|
|
259
|
+
// Get configuration (after validation to avoid prompts if validation fails)
|
|
260
|
+
const config = await this.getConfiguration(existingToolStates, extendMode);
|
|
261
|
+
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
262
|
+
const selectedIds = new Set(config.aiTools);
|
|
263
|
+
const selectedTools = availableTools.filter((tool) => selectedIds.has(tool.value));
|
|
264
|
+
const created = selectedTools.filter((tool) => !existingToolStates[tool.value]);
|
|
265
|
+
const refreshed = selectedTools.filter((tool) => existingToolStates[tool.value]);
|
|
266
|
+
const skippedExisting = availableTools.filter((tool) => !selectedIds.has(tool.value) && existingToolStates[tool.value]);
|
|
267
|
+
const skipped = availableTools.filter((tool) => !selectedIds.has(tool.value) && !existingToolStates[tool.value]);
|
|
268
|
+
// Step 1: Create directory structure
|
|
269
|
+
if (!extendMode) {
|
|
270
|
+
const structureSpinner = this.startSpinner('Creating LightSpec structure...');
|
|
271
|
+
await this.createDirectoryStructure(lightspecPath);
|
|
272
|
+
await this.generateFiles(lightspecPath, config);
|
|
273
|
+
structureSpinner.stopAndPersist({
|
|
274
|
+
symbol: PALETTE.white('▌'),
|
|
275
|
+
text: PALETTE.white('LightSpec structure created'),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
ora({ stream: process.stdout }).info(PALETTE.midGray('ℹ LightSpec already initialized. Checking for missing files...'));
|
|
280
|
+
await this.createDirectoryStructure(lightspecPath);
|
|
281
|
+
await this.ensureTemplateFiles(lightspecPath, config);
|
|
282
|
+
}
|
|
283
|
+
// Step 2: Configure AI tools
|
|
284
|
+
const toolSpinner = this.startSpinner('Configuring AI tools...');
|
|
285
|
+
const rootStubStatus = await this.configureAITools(projectPath, lightspecDir, config.aiTools);
|
|
286
|
+
toolSpinner.stopAndPersist({
|
|
287
|
+
symbol: PALETTE.white('▌'),
|
|
288
|
+
text: PALETTE.white('AI tools configured'),
|
|
289
|
+
});
|
|
290
|
+
// Success message
|
|
291
|
+
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
|
|
292
|
+
}
|
|
293
|
+
async validate(projectPath, _lightspecPath) {
|
|
294
|
+
const extendMode = await FileSystemUtils.directoryExists(_lightspecPath);
|
|
295
|
+
// Check write permissions
|
|
296
|
+
if (!(await FileSystemUtils.ensureWritePermissions(projectPath))) {
|
|
297
|
+
throw new Error(`Insufficient permissions to write to ${projectPath}`);
|
|
298
|
+
}
|
|
299
|
+
return extendMode;
|
|
300
|
+
}
|
|
301
|
+
async getConfiguration(existingTools, extendMode) {
|
|
302
|
+
const selectedTools = await this.getSelectedTools(existingTools, extendMode);
|
|
303
|
+
return { aiTools: selectedTools };
|
|
304
|
+
}
|
|
305
|
+
async getSelectedTools(existingTools, extendMode) {
|
|
306
|
+
const nonInteractiveSelection = this.resolveToolsArg();
|
|
307
|
+
if (nonInteractiveSelection !== null) {
|
|
308
|
+
return nonInteractiveSelection;
|
|
309
|
+
}
|
|
310
|
+
// Fall back to interactive mode
|
|
311
|
+
return this.promptForAITools(existingTools, extendMode);
|
|
312
|
+
}
|
|
313
|
+
resolveToolsArg() {
|
|
314
|
+
if (typeof this.toolsArg === 'undefined') {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const raw = this.toolsArg.trim();
|
|
318
|
+
if (raw.length === 0) {
|
|
319
|
+
throw new Error('The --tools option requires a value. Use "all", "none", or a comma-separated list of tool IDs.');
|
|
320
|
+
}
|
|
321
|
+
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
322
|
+
const availableValues = availableTools.map((tool) => tool.value);
|
|
323
|
+
const availableSet = new Set(availableValues);
|
|
324
|
+
const availableList = ['all', 'none', ...availableValues].join(', ');
|
|
325
|
+
const lowerRaw = raw.toLowerCase();
|
|
326
|
+
if (lowerRaw === 'all') {
|
|
327
|
+
return availableValues;
|
|
328
|
+
}
|
|
329
|
+
if (lowerRaw === 'none') {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
const tokens = raw
|
|
333
|
+
.split(',')
|
|
334
|
+
.map((token) => token.trim())
|
|
335
|
+
.filter((token) => token.length > 0);
|
|
336
|
+
if (tokens.length === 0) {
|
|
337
|
+
throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
|
|
338
|
+
}
|
|
339
|
+
const normalizedTokens = tokens.map((token) => token.toLowerCase());
|
|
340
|
+
if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
|
|
341
|
+
throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
|
|
342
|
+
}
|
|
343
|
+
const invalidTokens = tokens.filter((_token, index) => !availableSet.has(normalizedTokens[index]));
|
|
344
|
+
if (invalidTokens.length > 0) {
|
|
345
|
+
throw new Error(`Invalid tool(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`);
|
|
346
|
+
}
|
|
347
|
+
const deduped = [];
|
|
348
|
+
for (const token of normalizedTokens) {
|
|
349
|
+
if (!deduped.includes(token)) {
|
|
350
|
+
deduped.push(token);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return deduped;
|
|
354
|
+
}
|
|
355
|
+
async promptForAITools(existingTools, extendMode) {
|
|
356
|
+
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
357
|
+
const baseMessage = extendMode
|
|
358
|
+
? 'Which natively supported AI tools would you like to add or refresh?'
|
|
359
|
+
: 'Which natively supported AI tools do you use?';
|
|
360
|
+
const initialNativeSelection = extendMode
|
|
361
|
+
? availableTools
|
|
362
|
+
.filter((tool) => existingTools[tool.value])
|
|
363
|
+
.map((tool) => tool.value)
|
|
364
|
+
: [];
|
|
365
|
+
const initialSelected = Array.from(new Set(initialNativeSelection));
|
|
366
|
+
const choices = [
|
|
367
|
+
{
|
|
368
|
+
kind: 'heading',
|
|
369
|
+
value: '__heading-native__',
|
|
370
|
+
label: {
|
|
371
|
+
primary: 'Natively supported providers (✔ LightSpec custom slash commands available)',
|
|
372
|
+
},
|
|
373
|
+
selectable: false,
|
|
374
|
+
},
|
|
375
|
+
...availableTools.map((tool) => ({
|
|
376
|
+
kind: 'option',
|
|
377
|
+
value: tool.value,
|
|
378
|
+
label: parseToolLabel(tool.name),
|
|
379
|
+
configured: Boolean(existingTools[tool.value]),
|
|
380
|
+
selectable: true,
|
|
381
|
+
})),
|
|
382
|
+
...(availableTools.length
|
|
383
|
+
? [
|
|
384
|
+
{
|
|
385
|
+
kind: 'info',
|
|
386
|
+
value: LIST_SPACER_VALUE,
|
|
387
|
+
label: { primary: '' },
|
|
388
|
+
selectable: false,
|
|
389
|
+
},
|
|
390
|
+
]
|
|
391
|
+
: []),
|
|
392
|
+
{
|
|
393
|
+
kind: 'heading',
|
|
394
|
+
value: OTHER_TOOLS_HEADING_VALUE,
|
|
395
|
+
label: {
|
|
396
|
+
primary: 'Other tools (use Universal AGENTS.md for Amp, VS Code, GitHub Copilot, …)',
|
|
397
|
+
},
|
|
398
|
+
selectable: false,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
kind: 'option',
|
|
402
|
+
value: ROOT_STUB_CHOICE_VALUE,
|
|
403
|
+
label: {
|
|
404
|
+
primary: 'Universal AGENTS.md',
|
|
405
|
+
annotation: 'always available',
|
|
406
|
+
},
|
|
407
|
+
configured: extendMode,
|
|
408
|
+
selectable: true,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
return this.prompt({
|
|
412
|
+
extendMode,
|
|
413
|
+
baseMessage,
|
|
414
|
+
choices,
|
|
415
|
+
initialSelected,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async getExistingToolStates(projectPath, extendMode) {
|
|
419
|
+
// Fresh initialization - no tools configured yet
|
|
420
|
+
if (!extendMode) {
|
|
421
|
+
return Object.fromEntries(AI_TOOLS.map(t => [t.value, false]));
|
|
422
|
+
}
|
|
423
|
+
// Extend mode - check all tools in parallel for better performance
|
|
424
|
+
const entries = await Promise.all(AI_TOOLS.map(async (t) => [t.value, await this.isToolConfigured(projectPath, t.value)]));
|
|
425
|
+
return Object.fromEntries(entries);
|
|
426
|
+
}
|
|
427
|
+
async isToolConfigured(projectPath, toolId) {
|
|
428
|
+
// A tool is only considered "configured by LightSpec" if its files contain LightSpec markers.
|
|
429
|
+
// For tools with both config files and slash commands, BOTH must have markers.
|
|
430
|
+
// For slash commands, at least one file with markers is sufficient (not all required).
|
|
431
|
+
// Helper to check if a file exists and contains LightSpec markers
|
|
432
|
+
const fileHasMarkers = async (absolutePath) => {
|
|
433
|
+
try {
|
|
434
|
+
const content = await FileSystemUtils.readFile(absolutePath);
|
|
435
|
+
return content.includes(LIGHTSPEC_MARKERS.start) && content.includes(LIGHTSPEC_MARKERS.end);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
let hasConfigFile = false;
|
|
442
|
+
let hasSlashCommands = false;
|
|
443
|
+
// Check if the tool has a config file with LightSpec markers
|
|
444
|
+
const configFile = ToolRegistry.get(toolId)?.configFileName;
|
|
445
|
+
if (configFile) {
|
|
446
|
+
const configPath = path.join(projectPath, configFile);
|
|
447
|
+
hasConfigFile = (await FileSystemUtils.fileExists(configPath)) && (await fileHasMarkers(configPath));
|
|
448
|
+
}
|
|
449
|
+
// Check if any slash command file exists with LightSpec markers
|
|
450
|
+
const slashConfigurator = SlashCommandRegistry.get(toolId);
|
|
451
|
+
if (slashConfigurator) {
|
|
452
|
+
for (const target of slashConfigurator.getTargets()) {
|
|
453
|
+
const absolute = slashConfigurator.resolveAbsolutePath(projectPath, target.id);
|
|
454
|
+
if ((await FileSystemUtils.fileExists(absolute)) && (await fileHasMarkers(absolute))) {
|
|
455
|
+
hasSlashCommands = true;
|
|
456
|
+
break; // At least one file with markers is sufficient
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Tool is only configured if BOTH exist with markers
|
|
461
|
+
// OR if the tool has no config file requirement (slash commands only)
|
|
462
|
+
// OR if the tool has no slash commands requirement (config file only)
|
|
463
|
+
const hasConfigFileRequirement = configFile !== undefined;
|
|
464
|
+
const hasSlashCommandRequirement = slashConfigurator !== undefined;
|
|
465
|
+
if (hasConfigFileRequirement && hasSlashCommandRequirement) {
|
|
466
|
+
// Both are required - both must be present with markers
|
|
467
|
+
return hasConfigFile && hasSlashCommands;
|
|
468
|
+
}
|
|
469
|
+
else if (hasConfigFileRequirement) {
|
|
470
|
+
// Only config file required
|
|
471
|
+
return hasConfigFile;
|
|
472
|
+
}
|
|
473
|
+
else if (hasSlashCommandRequirement) {
|
|
474
|
+
// Only slash commands required
|
|
475
|
+
return hasSlashCommands;
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
async createDirectoryStructure(lightspecPath) {
|
|
480
|
+
const directories = [
|
|
481
|
+
lightspecPath,
|
|
482
|
+
path.join(lightspecPath, 'specs'),
|
|
483
|
+
path.join(lightspecPath, 'changes'),
|
|
484
|
+
path.join(lightspecPath, 'changes', 'archive'),
|
|
485
|
+
];
|
|
486
|
+
for (const dir of directories) {
|
|
487
|
+
await FileSystemUtils.createDirectory(dir);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async generateFiles(lightspecPath, config) {
|
|
491
|
+
await this.writeTemplateFiles(lightspecPath, config, false);
|
|
492
|
+
}
|
|
493
|
+
async ensureTemplateFiles(lightspecPath, config) {
|
|
494
|
+
await this.writeTemplateFiles(lightspecPath, config, true);
|
|
495
|
+
}
|
|
496
|
+
async writeTemplateFiles(lightspecPath, config, skipExisting) {
|
|
497
|
+
const context = {
|
|
498
|
+
// Could be enhanced with prompts for project details
|
|
499
|
+
};
|
|
500
|
+
const templates = TemplateManager.getTemplates(context);
|
|
501
|
+
for (const template of templates) {
|
|
502
|
+
const filePath = path.join(lightspecPath, template.path);
|
|
503
|
+
// Skip if file exists and we're in skipExisting mode
|
|
504
|
+
if (skipExisting && (await FileSystemUtils.fileExists(filePath))) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const content = typeof template.content === 'function'
|
|
508
|
+
? template.content(context)
|
|
509
|
+
: template.content;
|
|
510
|
+
await FileSystemUtils.writeFile(filePath, content);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async configureAITools(projectPath, lightspecDir, toolIds) {
|
|
514
|
+
const rootStubStatus = await this.configureRootAgentsStub(projectPath, lightspecDir);
|
|
515
|
+
for (const toolId of toolIds) {
|
|
516
|
+
const configurator = ToolRegistry.get(toolId);
|
|
517
|
+
if (configurator && configurator.isAvailable) {
|
|
518
|
+
await configurator.configure(projectPath, lightspecDir);
|
|
519
|
+
}
|
|
520
|
+
const slashConfigurator = SlashCommandRegistry.get(toolId);
|
|
521
|
+
if (slashConfigurator && slashConfigurator.isAvailable) {
|
|
522
|
+
await slashConfigurator.generateAll(projectPath, lightspecDir);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return rootStubStatus;
|
|
526
|
+
}
|
|
527
|
+
async configureRootAgentsStub(projectPath, lightspecDir) {
|
|
528
|
+
const configurator = ToolRegistry.get('agents');
|
|
529
|
+
if (!configurator || !configurator.isAvailable) {
|
|
530
|
+
return 'skipped';
|
|
531
|
+
}
|
|
532
|
+
const stubPath = path.join(projectPath, configurator.configFileName);
|
|
533
|
+
const existed = await FileSystemUtils.fileExists(stubPath);
|
|
534
|
+
await configurator.configure(projectPath, lightspecDir);
|
|
535
|
+
return existed ? 'updated' : 'created';
|
|
536
|
+
}
|
|
537
|
+
displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus) {
|
|
538
|
+
console.log(); // Empty line for spacing
|
|
539
|
+
const successHeadline = extendMode
|
|
540
|
+
? 'LightSpec tool configuration updated!'
|
|
541
|
+
: 'LightSpec initialized successfully!';
|
|
542
|
+
ora().succeed(PALETTE.white(successHeadline));
|
|
543
|
+
console.log();
|
|
544
|
+
console.log(PALETTE.lightGray('Tool summary:'));
|
|
545
|
+
const summaryLines = [
|
|
546
|
+
rootStubStatus === 'created'
|
|
547
|
+
? `${PALETTE.white('▌')} ${PALETTE.white('Root AGENTS.md stub created for other assistants')}`
|
|
548
|
+
: null,
|
|
549
|
+
rootStubStatus === 'updated'
|
|
550
|
+
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('Root AGENTS.md stub refreshed for other assistants')}`
|
|
551
|
+
: null,
|
|
552
|
+
created.length
|
|
553
|
+
? `${PALETTE.white('▌')} ${PALETTE.white('Created:')} ${this.formatToolNames(created)}`
|
|
554
|
+
: null,
|
|
555
|
+
refreshed.length
|
|
556
|
+
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('Refreshed:')} ${this.formatToolNames(refreshed)}`
|
|
557
|
+
: null,
|
|
558
|
+
skippedExisting.length
|
|
559
|
+
? `${PALETTE.midGray('▌')} ${PALETTE.midGray('Skipped (already configured):')} ${this.formatToolNames(skippedExisting)}`
|
|
560
|
+
: null,
|
|
561
|
+
skipped.length
|
|
562
|
+
? `${PALETTE.darkGray('▌')} ${PALETTE.darkGray('Skipped:')} ${this.formatToolNames(skipped)}`
|
|
563
|
+
: null,
|
|
564
|
+
].filter((line) => Boolean(line));
|
|
565
|
+
for (const line of summaryLines) {
|
|
566
|
+
console.log(line);
|
|
567
|
+
}
|
|
568
|
+
console.log();
|
|
569
|
+
console.log(PALETTE.midGray('Use `lightspec update` to refresh shared LightSpec instructions in the future.'));
|
|
570
|
+
// Show restart instruction if any tools were configured
|
|
571
|
+
if (created.length > 0 || refreshed.length > 0) {
|
|
572
|
+
console.log();
|
|
573
|
+
console.log(PALETTE.white('Important: Restart your IDE'));
|
|
574
|
+
console.log(PALETTE.midGray('Slash commands are loaded at startup. Please restart your coding assistant'));
|
|
575
|
+
console.log(PALETTE.midGray('to ensure the new /lightspec commands appear in your command palette.'));
|
|
576
|
+
}
|
|
577
|
+
// Get the selected tool name(s) for display
|
|
578
|
+
const toolName = this.formatToolNames(selectedTools);
|
|
579
|
+
console.log();
|
|
580
|
+
console.log(`Next steps - Copy these prompts to ${toolName}:`);
|
|
581
|
+
console.log(chalk.gray('────────────────────────────────────────────────────────────'));
|
|
582
|
+
console.log(PALETTE.white('1. Populate your project context:'));
|
|
583
|
+
console.log(PALETTE.lightGray(' "Please read lightspec/project.md and help me fill it out'));
|
|
584
|
+
console.log(PALETTE.lightGray(' with details about my project, tech stack, and conventions"\n'));
|
|
585
|
+
console.log(PALETTE.white('2. Create your first change proposal:'));
|
|
586
|
+
console.log(PALETTE.lightGray(' "I want to add [YOUR FEATURE HERE]. Please create an'));
|
|
587
|
+
console.log(PALETTE.lightGray(' LightSpec change proposal for this feature"\n'));
|
|
588
|
+
console.log(PALETTE.white('3. Learn the LightSpec workflow:'));
|
|
589
|
+
console.log(PALETTE.lightGray(' "Please explain the LightSpec workflow from lightspec/AGENTS.md'));
|
|
590
|
+
console.log(PALETTE.lightGray(' and how I should work with you on this project"'));
|
|
591
|
+
console.log(PALETTE.darkGray('────────────────────────────────────────────────────────────\n'));
|
|
592
|
+
// Codex heads-up: prompts installed globally
|
|
593
|
+
const selectedToolIds = new Set(selectedTools.map((t) => t.value));
|
|
594
|
+
if (selectedToolIds.has('codex')) {
|
|
595
|
+
console.log(PALETTE.white('Codex setup note'));
|
|
596
|
+
console.log(PALETTE.midGray('Prompts installed to ~/.codex/prompts (or $CODEX_HOME/prompts).'));
|
|
597
|
+
console.log();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
formatToolNames(tools) {
|
|
601
|
+
const names = tools
|
|
602
|
+
.map((tool) => tool.successLabel ?? tool.name)
|
|
603
|
+
.filter((name) => Boolean(name));
|
|
604
|
+
if (names.length === 0)
|
|
605
|
+
return PALETTE.lightGray('your AGENTS.md-compatible assistant');
|
|
606
|
+
if (names.length === 1)
|
|
607
|
+
return PALETTE.white(names[0]);
|
|
608
|
+
const base = names.slice(0, -1).map((name) => PALETTE.white(name));
|
|
609
|
+
const last = PALETTE.white(names[names.length - 1]);
|
|
610
|
+
return `${base.join(PALETTE.midGray(', '))}${base.length ? PALETTE.midGray(', and ') : ''}${last}`;
|
|
611
|
+
}
|
|
612
|
+
renderBanner(_extendMode) {
|
|
613
|
+
const rows = ['', '', '', '', ''];
|
|
614
|
+
for (const char of 'LIGHTSPEC') {
|
|
615
|
+
const glyph = LETTER_MAP[char] ?? LETTER_MAP[' '];
|
|
616
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
617
|
+
rows[i] += `${glyph[i]} `;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const rowStyles = [
|
|
621
|
+
PALETTE.white,
|
|
622
|
+
PALETTE.lightGray,
|
|
623
|
+
PALETTE.midGray,
|
|
624
|
+
PALETTE.lightGray,
|
|
625
|
+
PALETTE.white,
|
|
626
|
+
];
|
|
627
|
+
console.log();
|
|
628
|
+
rows.forEach((row, index) => {
|
|
629
|
+
console.log(rowStyles[index](row.replace(/\s+$/u, '')));
|
|
630
|
+
});
|
|
631
|
+
console.log();
|
|
632
|
+
console.log(PALETTE.white('Welcome to LightSpec!'));
|
|
633
|
+
console.log();
|
|
634
|
+
}
|
|
635
|
+
startSpinner(text) {
|
|
636
|
+
return ora({
|
|
637
|
+
text,
|
|
638
|
+
stream: process.stdout,
|
|
639
|
+
color: 'gray',
|
|
640
|
+
spinner: PROGRESS_SPINNER,
|
|
641
|
+
}).start();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
//# sourceMappingURL=init.js.map
|