@cod3vil/trunk 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 +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +69 -0
- package/dist/commands/clone.d.ts +6 -0
- package/dist/commands/clone.js +114 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +235 -0
- package/dist/commands/new.d.ts +6 -0
- package/dist/commands/new.js +357 -0
- package/dist/core/adopt.d.ts +14 -0
- package/dist/core/adopt.js +157 -0
- package/dist/core/agents.d.ts +30 -0
- package/dist/core/agents.js +31 -0
- package/dist/core/arguments.d.ts +92 -0
- package/dist/core/arguments.js +93 -0
- package/dist/core/detect.d.ts +28 -0
- package/dist/core/detect.js +169 -0
- package/dist/core/diff.d.ts +37 -0
- package/dist/core/diff.js +140 -0
- package/dist/core/env.d.ts +63 -0
- package/dist/core/env.js +140 -0
- package/dist/core/generate/aliases.d.ts +7 -0
- package/dist/core/generate/aliases.js +45 -0
- package/dist/core/generate/header.d.ts +2 -0
- package/dist/core/generate/header.js +81 -0
- package/dist/core/generate/index.d.ts +8 -0
- package/dist/core/generate/index.js +74 -0
- package/dist/core/generate/proxy.d.ts +8 -0
- package/dist/core/generate/proxy.js +56 -0
- package/dist/core/generate/steps.d.ts +7 -0
- package/dist/core/generate/steps.js +88 -0
- package/dist/core/generate/tmux.d.ts +4 -0
- package/dist/core/generate/tmux.js +98 -0
- package/dist/core/generate/toml.d.ts +16 -0
- package/dist/core/generate/toml.js +68 -0
- package/dist/core/gh.d.ts +60 -0
- package/dist/core/gh.js +101 -0
- package/dist/core/git.d.ts +79 -0
- package/dist/core/git.js +211 -0
- package/dist/core/journal.d.ts +54 -0
- package/dist/core/journal.js +147 -0
- package/dist/core/log.d.ts +8 -0
- package/dist/core/log.js +38 -0
- package/dist/core/pipeline.d.ts +119 -0
- package/dist/core/pipeline.js +473 -0
- package/dist/core/platform.d.ts +10 -0
- package/dist/core/platform.js +26 -0
- package/dist/core/prefix.d.ts +25 -0
- package/dist/core/prefix.js +59 -0
- package/dist/core/process.d.ts +27 -0
- package/dist/core/process.js +43 -0
- package/dist/core/repo.d.ts +79 -0
- package/dist/core/repo.js +294 -0
- package/dist/core/resolve.d.ts +127 -0
- package/dist/core/resolve.js +488 -0
- package/dist/core/result.d.ts +27 -0
- package/dist/core/result.js +32 -0
- package/dist/core/settings.d.ts +51 -0
- package/dist/core/settings.js +83 -0
- package/dist/core/tmuxRename.d.ts +36 -0
- package/dist/core/tmuxRename.js +79 -0
- package/dist/core/validate.d.ts +22 -0
- package/dist/core/validate.js +111 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.js +35 -0
- package/dist/core/words.d.ts +16 -0
- package/dist/core/words.js +198 -0
- package/dist/core/wt.d.ts +41 -0
- package/dist/core/wt.js +59 -0
- package/dist/ui/SetupForm.d.ts +55 -0
- package/dist/ui/SetupForm.js +354 -0
- package/dist/ui/Summary.d.ts +15 -0
- package/dist/ui/Summary.js +74 -0
- package/dist/ui/fields/MultiSelect.d.ts +17 -0
- package/dist/ui/fields/MultiSelect.js +66 -0
- package/dist/ui/fields/Select.d.ts +17 -0
- package/dist/ui/fields/Select.js +37 -0
- package/dist/ui/fields/TextInput.d.ts +13 -0
- package/dist/ui/fields/TextInput.js +50 -0
- package/package.json +76 -0
- package/readme.md +147 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns flags, adopted values, environment detection and built-in defaults into
|
|
3
|
+
* the one Settings object consumed by the generator. This module is deliberately
|
|
4
|
+
* synchronous and headless so scripted and interactive setup share the same path.
|
|
5
|
+
*/
|
|
6
|
+
import { agentCommands, agentIds, isAgentId, maximumAgents, } from './agents.js';
|
|
7
|
+
import { initials, validatePrefix } from './prefix.js';
|
|
8
|
+
import { assertSettings, settingsDefaults } from './settings.js';
|
|
9
|
+
export const setupFieldOrder = Object.freeze([
|
|
10
|
+
'prefix',
|
|
11
|
+
'pm',
|
|
12
|
+
'tmux',
|
|
13
|
+
'agents',
|
|
14
|
+
'copyIgnored',
|
|
15
|
+
'server',
|
|
16
|
+
'caddy',
|
|
17
|
+
'mcAlias',
|
|
18
|
+
]);
|
|
19
|
+
const packageManagers = Object.freeze(['npm', 'pnpm', 'bun']);
|
|
20
|
+
const caddyInstallUrl = 'https://caddyserver.com/docs/install';
|
|
21
|
+
/** Resolve a setup without reading the terminal or filesystem. */
|
|
22
|
+
export function resolve(options) {
|
|
23
|
+
const warnings = (options.packageDetection?.warnings ?? []).map(message => Object.freeze({ message }));
|
|
24
|
+
const flags = parseFlags(options.flags ?? {});
|
|
25
|
+
const answers = validateValues(options.answers ?? {}, 'answer');
|
|
26
|
+
const existing = sanitizeExisting(options.existing ?? {}, warnings);
|
|
27
|
+
const detected = detectionValues(options);
|
|
28
|
+
const defaults = builtInValues(options);
|
|
29
|
+
const selected = selectValues({
|
|
30
|
+
flags,
|
|
31
|
+
answers,
|
|
32
|
+
existing,
|
|
33
|
+
detected,
|
|
34
|
+
defaults,
|
|
35
|
+
});
|
|
36
|
+
normalizeDependencies(selected, flags);
|
|
37
|
+
warnAboutPackageManagerMismatch(existing, options.packageDetection, warnings);
|
|
38
|
+
const questions = options.acceptDefaults
|
|
39
|
+
? []
|
|
40
|
+
: setupFieldOrder
|
|
41
|
+
.filter(field => shouldAsk(field, selected))
|
|
42
|
+
.map(field => questionFor(field, selected, options));
|
|
43
|
+
const draft = createSettings(options, selected.values);
|
|
44
|
+
const sources = Object.freeze({ ...selected.sources });
|
|
45
|
+
const frozenWarnings = Object.freeze(warnings);
|
|
46
|
+
if (questions.length > 0) {
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
kind: 'questions',
|
|
49
|
+
draft,
|
|
50
|
+
questions: Object.freeze(questions),
|
|
51
|
+
sources,
|
|
52
|
+
warnings: frozenWarnings,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
assertSettings(draft);
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: 'complete',
|
|
58
|
+
settings: draft,
|
|
59
|
+
draft,
|
|
60
|
+
questions: Object.freeze([]),
|
|
61
|
+
sources,
|
|
62
|
+
warnings: frozenWarnings,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Maps Meow's names onto the names used by Settings. */
|
|
66
|
+
export function setupFlagsFromCli(flags) {
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
prefix: flags.prefix,
|
|
69
|
+
pm: flags.pm,
|
|
70
|
+
tmux: flags.tmux,
|
|
71
|
+
agents: flags.agents,
|
|
72
|
+
copyIgnored: flags.copyIgnored,
|
|
73
|
+
server: flags.server,
|
|
74
|
+
caddy: flags.caddy,
|
|
75
|
+
mcAlias: flags.mc,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Makes a non-interactive rerun explicit and reproducible instead of merely
|
|
80
|
+
* suggesting `--yes`, whose defaults could change with the machine.
|
|
81
|
+
*/
|
|
82
|
+
export function buildRerunCommand(executable, originalArguments, resolution) {
|
|
83
|
+
const separator = originalArguments.indexOf('--');
|
|
84
|
+
const beforeSeparator = separator === -1
|
|
85
|
+
? [...originalArguments]
|
|
86
|
+
: originalArguments.slice(0, separator);
|
|
87
|
+
const afterSeparator = separator === -1 ? [] : originalArguments.slice(separator);
|
|
88
|
+
const added = resolution.questions.map(question => questionArgument(question));
|
|
89
|
+
const arguments_ = Object.freeze([
|
|
90
|
+
...beforeSeparator,
|
|
91
|
+
...added,
|
|
92
|
+
...afterSeparator,
|
|
93
|
+
]);
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
arguments: arguments_,
|
|
96
|
+
display: [executable, ...arguments_]
|
|
97
|
+
.map(argument => shellQuote(argument))
|
|
98
|
+
.join(' '),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function builtInValues(options) {
|
|
102
|
+
const values = {
|
|
103
|
+
prefix: initials(options.fixed.repoName),
|
|
104
|
+
pm: 'npm',
|
|
105
|
+
tmux: settingsDefaults.tmux,
|
|
106
|
+
agents: settingsDefaults.agents,
|
|
107
|
+
copyIgnored: settingsDefaults.copyIgnored,
|
|
108
|
+
server: settingsDefaults.server,
|
|
109
|
+
caddy: settingsDefaults.caddy,
|
|
110
|
+
mcAlias: settingsDefaults.mcAlias,
|
|
111
|
+
...options.defaults,
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
...values,
|
|
115
|
+
pm: parsePackageManager(values.pm, 'default package manager'),
|
|
116
|
+
agents: validateAgents(values.agents),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function detectionValues(options) {
|
|
120
|
+
const detected = {};
|
|
121
|
+
if (options.packageDetection) {
|
|
122
|
+
detected.pm = options.packageDetection.packageManager;
|
|
123
|
+
}
|
|
124
|
+
if (options.tools) {
|
|
125
|
+
detected.agents = Object.freeze(agentIds
|
|
126
|
+
.filter(id => options.tools?.agents[id].path !== undefined)
|
|
127
|
+
.slice(0, maximumAgents));
|
|
128
|
+
detected.caddy = options.tools.caddy.path !== undefined;
|
|
129
|
+
}
|
|
130
|
+
return detected;
|
|
131
|
+
}
|
|
132
|
+
function selectValues({ flags, answers, existing, detected, defaults, }) {
|
|
133
|
+
const values = { ...defaults };
|
|
134
|
+
const sources = {
|
|
135
|
+
prefix: 'built-in',
|
|
136
|
+
pm: 'built-in',
|
|
137
|
+
tmux: 'built-in',
|
|
138
|
+
agents: 'built-in',
|
|
139
|
+
copyIgnored: 'built-in',
|
|
140
|
+
server: 'built-in',
|
|
141
|
+
caddy: 'built-in',
|
|
142
|
+
mcAlias: 'built-in',
|
|
143
|
+
};
|
|
144
|
+
const answered = {
|
|
145
|
+
prefix: false,
|
|
146
|
+
pm: false,
|
|
147
|
+
tmux: false,
|
|
148
|
+
agents: false,
|
|
149
|
+
copyIgnored: false,
|
|
150
|
+
server: false,
|
|
151
|
+
caddy: false,
|
|
152
|
+
mcAlias: false,
|
|
153
|
+
};
|
|
154
|
+
for (const field of setupFieldOrder) {
|
|
155
|
+
if (hasValue(flags, field)) {
|
|
156
|
+
setSelected(field, flags[field], 'flag', true);
|
|
157
|
+
}
|
|
158
|
+
else if (hasValue(answers, field)) {
|
|
159
|
+
setSelected(field, answers[field], 'answer', true);
|
|
160
|
+
}
|
|
161
|
+
else if (hasValue(existing, field)) {
|
|
162
|
+
setSelected(field, existing[field], 'existing', false);
|
|
163
|
+
}
|
|
164
|
+
else if (hasValue(detected, field)) {
|
|
165
|
+
setSelected(field, detected[field], 'detection', false);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
setSelected(field, defaults[field], 'built-in', false);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { values, sources, answered };
|
|
172
|
+
function setSelected(field, value, source, isAnswered) {
|
|
173
|
+
values[field] = value;
|
|
174
|
+
sources[field] = source;
|
|
175
|
+
answered[field] = isAnswered;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function normalizeDependencies(selected, flags) {
|
|
179
|
+
if (!selected.values.tmux) {
|
|
180
|
+
if (hasValue(flags, 'agents') && flags.agents.length > 0) {
|
|
181
|
+
throw new TypeError('Agents require --tmux; remove --no-tmux or use --agents=.');
|
|
182
|
+
}
|
|
183
|
+
selected.values.agents = Object.freeze([]);
|
|
184
|
+
selected.sources.agents = 'dependency';
|
|
185
|
+
selected.answered.agents = true;
|
|
186
|
+
}
|
|
187
|
+
if (!selected.values.server) {
|
|
188
|
+
if (hasValue(flags, 'caddy') && flags.caddy) {
|
|
189
|
+
throw new TypeError('Caddy requires --server; remove --no-server or use --no-caddy.');
|
|
190
|
+
}
|
|
191
|
+
selected.values.caddy = false;
|
|
192
|
+
selected.sources.caddy = 'dependency';
|
|
193
|
+
selected.answered.caddy = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function shouldAsk(field, selected) {
|
|
197
|
+
if (selected.answered[field]) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (field === 'agents') {
|
|
201
|
+
return selected.values.tmux;
|
|
202
|
+
}
|
|
203
|
+
if (field === 'caddy') {
|
|
204
|
+
return selected.values.server;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
function questionFor(field, selected, options) {
|
|
209
|
+
const source = selected.sources[field];
|
|
210
|
+
switch (field) {
|
|
211
|
+
case 'prefix': {
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
field,
|
|
214
|
+
kind: 'text',
|
|
215
|
+
label: 'prefix',
|
|
216
|
+
flag: '--prefix',
|
|
217
|
+
defaultValue: selected.values.prefix,
|
|
218
|
+
source,
|
|
219
|
+
help: 'keep it unique across your repos',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
case 'pm': {
|
|
223
|
+
return Object.freeze({
|
|
224
|
+
field,
|
|
225
|
+
kind: 'select',
|
|
226
|
+
label: 'package manager',
|
|
227
|
+
flag: '--pm',
|
|
228
|
+
defaultValue: selected.values.pm,
|
|
229
|
+
source,
|
|
230
|
+
options: packageManagers,
|
|
231
|
+
needsConfirmation: options.packageDetection?.needsConfirmation ?? false,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
case 'tmux': {
|
|
235
|
+
return Object.freeze({
|
|
236
|
+
field,
|
|
237
|
+
kind: 'boolean',
|
|
238
|
+
label: 'tmux session',
|
|
239
|
+
flag: '--tmux/--no-tmux',
|
|
240
|
+
defaultValue: selected.values.tmux,
|
|
241
|
+
source,
|
|
242
|
+
installed: options.tools?.tmux.path !== undefined,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
case 'agents': {
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
field,
|
|
248
|
+
kind: 'multi-select',
|
|
249
|
+
label: 'agents',
|
|
250
|
+
flag: '--agents',
|
|
251
|
+
defaultValue: Object.freeze([...selected.values.agents]),
|
|
252
|
+
source,
|
|
253
|
+
maximum: maximumAgents,
|
|
254
|
+
options: Object.freeze(agentIds.map(id => Object.freeze({
|
|
255
|
+
id,
|
|
256
|
+
command: agentCommands[id],
|
|
257
|
+
installed: options.tools?.agents[id].path !== undefined,
|
|
258
|
+
}))),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
case 'copyIgnored': {
|
|
262
|
+
return booleanQuestion({
|
|
263
|
+
field,
|
|
264
|
+
label: 'copy-ignored',
|
|
265
|
+
flag: '--copy/--no-copy',
|
|
266
|
+
defaultValue: selected.values.copyIgnored,
|
|
267
|
+
source,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
case 'server': {
|
|
271
|
+
return booleanQuestion({
|
|
272
|
+
field,
|
|
273
|
+
label: 'dev server',
|
|
274
|
+
flag: '--server/--no-server',
|
|
275
|
+
defaultValue: selected.values.server,
|
|
276
|
+
source,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
case 'caddy': {
|
|
280
|
+
return Object.freeze({
|
|
281
|
+
field,
|
|
282
|
+
kind: 'boolean',
|
|
283
|
+
label: options.tools?.caddy.path === undefined
|
|
284
|
+
? 'include Caddy anyway'
|
|
285
|
+
: 'Caddy route',
|
|
286
|
+
flag: '--caddy/--no-caddy',
|
|
287
|
+
defaultValue: selected.values.caddy,
|
|
288
|
+
source,
|
|
289
|
+
availability: caddyAvailability(options.tools),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
case 'mcAlias': {
|
|
293
|
+
return booleanQuestion({
|
|
294
|
+
field,
|
|
295
|
+
label: 'mc alias',
|
|
296
|
+
flag: '--mc/--no-mc',
|
|
297
|
+
defaultValue: selected.values.mcAlias,
|
|
298
|
+
source,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function booleanQuestion(properties) {
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
...properties,
|
|
306
|
+
kind: 'boolean',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function caddyAvailability(tools) {
|
|
310
|
+
if (tools?.caddy.path) {
|
|
311
|
+
return Object.freeze({ kind: 'installed' });
|
|
312
|
+
}
|
|
313
|
+
if (tools?.brew.path) {
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
kind: 'brew-installable',
|
|
316
|
+
executable: tools.brew.path,
|
|
317
|
+
arguments: Object.freeze(['install', 'caddy']),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return Object.freeze({ kind: 'missing', installUrl: caddyInstallUrl });
|
|
321
|
+
}
|
|
322
|
+
function createSettings(options, values) {
|
|
323
|
+
const appDirectory = options.fixed.appDir ?? options.packageDetection?.appDir;
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
trunkVersion: options.fixed.trunkVersion,
|
|
326
|
+
generatedOn: options.fixed.generatedOn,
|
|
327
|
+
repoName: options.fixed.repoName,
|
|
328
|
+
hostLabel: options.fixed.hostLabel,
|
|
329
|
+
prefix: values.prefix,
|
|
330
|
+
pm: values.pm,
|
|
331
|
+
...(appDirectory ? { appDir: appDirectory } : {}),
|
|
332
|
+
tmux: values.tmux,
|
|
333
|
+
agents: Object.freeze([...values.agents]),
|
|
334
|
+
editorWindow: true,
|
|
335
|
+
copyIgnored: values.copyIgnored,
|
|
336
|
+
server: values.server,
|
|
337
|
+
caddy: values.caddy,
|
|
338
|
+
mcAlias: values.mcAlias,
|
|
339
|
+
devScript: options.fixed.devScript ?? settingsDefaults.devScript,
|
|
340
|
+
scripts: Object.freeze([
|
|
341
|
+
...(options.fixed.scripts ?? options.packageDetection?.scripts ?? []),
|
|
342
|
+
]),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function parseFlags(flags) {
|
|
346
|
+
const values = {};
|
|
347
|
+
if (flags.prefix !== undefined) {
|
|
348
|
+
values.prefix = flags.prefix;
|
|
349
|
+
}
|
|
350
|
+
if (flags.pm !== undefined) {
|
|
351
|
+
values.pm = parsePackageManager(flags.pm, '--pm');
|
|
352
|
+
}
|
|
353
|
+
if (flags.tmux !== undefined) {
|
|
354
|
+
values.tmux = flags.tmux;
|
|
355
|
+
}
|
|
356
|
+
if (flags.agents !== undefined) {
|
|
357
|
+
values.agents = parseAgents(flags.agents);
|
|
358
|
+
}
|
|
359
|
+
if (flags.copyIgnored !== undefined) {
|
|
360
|
+
values.copyIgnored = flags.copyIgnored;
|
|
361
|
+
}
|
|
362
|
+
if (flags.server !== undefined) {
|
|
363
|
+
values.server = flags.server;
|
|
364
|
+
}
|
|
365
|
+
if (flags.caddy !== undefined) {
|
|
366
|
+
values.caddy = flags.caddy;
|
|
367
|
+
}
|
|
368
|
+
if (flags.mcAlias !== undefined) {
|
|
369
|
+
values.mcAlias = flags.mcAlias;
|
|
370
|
+
}
|
|
371
|
+
return validateValues(values, 'flag');
|
|
372
|
+
}
|
|
373
|
+
function validateValues(values, source) {
|
|
374
|
+
const validated = { ...values };
|
|
375
|
+
if (values.prefix !== undefined) {
|
|
376
|
+
const result = validatePrefix(values.prefix);
|
|
377
|
+
if (!result.valid) {
|
|
378
|
+
throw new TypeError(result.reason);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (values.pm !== undefined) {
|
|
382
|
+
validated.pm = parsePackageManager(values.pm, `${source} package manager`);
|
|
383
|
+
}
|
|
384
|
+
if (values.agents !== undefined) {
|
|
385
|
+
validated.agents = validateAgents(values.agents);
|
|
386
|
+
}
|
|
387
|
+
return validated;
|
|
388
|
+
}
|
|
389
|
+
function sanitizeExisting(values, warnings) {
|
|
390
|
+
const valid = {};
|
|
391
|
+
for (const field of setupFieldOrder) {
|
|
392
|
+
if (!hasValue(values, field)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
Object.assign(valid, validateValues({ [field]: values[field] }, 'default'));
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
warnings.push(Object.freeze({
|
|
400
|
+
field,
|
|
401
|
+
message: `Ignoring invalid existing ${field}: ${errorMessage(error)}`,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return valid;
|
|
406
|
+
}
|
|
407
|
+
function parsePackageManager(value, name) {
|
|
408
|
+
if (packageManagers.includes(value)) {
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
throw new TypeError(`${name} must be npm, pnpm, or bun.`);
|
|
412
|
+
}
|
|
413
|
+
function parseAgents(value) {
|
|
414
|
+
if (value === '') {
|
|
415
|
+
return Object.freeze([]);
|
|
416
|
+
}
|
|
417
|
+
const values = value.split(',').map(agent => agent.trim());
|
|
418
|
+
if (values.some(agent => agent.length === 0)) {
|
|
419
|
+
throw new TypeError('--agents must be a comma-separated list without empty entries.');
|
|
420
|
+
}
|
|
421
|
+
return validateAgents(values);
|
|
422
|
+
}
|
|
423
|
+
function validateAgents(values) {
|
|
424
|
+
for (const value of values) {
|
|
425
|
+
if (!isAgentId(value)) {
|
|
426
|
+
throw new TypeError(`Unknown agent "${value}"; choose from ${agentIds.join(',')}.`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (new Set(values).size !== values.length) {
|
|
430
|
+
throw new TypeError('Agents must not contain duplicates.');
|
|
431
|
+
}
|
|
432
|
+
if (values.length > maximumAgents) {
|
|
433
|
+
throw new TypeError(`max ${maximumAgents} (2×2 grid)`);
|
|
434
|
+
}
|
|
435
|
+
const selected = new Set(values);
|
|
436
|
+
return Object.freeze(agentIds.filter(id => selected.has(id)));
|
|
437
|
+
}
|
|
438
|
+
function warnAboutPackageManagerMismatch(existing, detection, warnings) {
|
|
439
|
+
if (existing.pm && detection && existing.pm !== detection.packageManager) {
|
|
440
|
+
warnings.push(Object.freeze({
|
|
441
|
+
field: 'pm',
|
|
442
|
+
message: `Existing config uses ${existing.pm}, but lockfile detection found ${detection.packageManager}.`,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function questionArgument(question) {
|
|
447
|
+
switch (question.field) {
|
|
448
|
+
case 'prefix': {
|
|
449
|
+
return `--prefix=${question.defaultValue}`;
|
|
450
|
+
}
|
|
451
|
+
case 'pm': {
|
|
452
|
+
return `--pm=${question.defaultValue}`;
|
|
453
|
+
}
|
|
454
|
+
case 'agents': {
|
|
455
|
+
return `--agents=${question.defaultValue.join(',')}`;
|
|
456
|
+
}
|
|
457
|
+
case 'tmux': {
|
|
458
|
+
return question.defaultValue ? '--tmux' : '--no-tmux';
|
|
459
|
+
}
|
|
460
|
+
case 'copyIgnored': {
|
|
461
|
+
return question.defaultValue ? '--copy' : '--no-copy';
|
|
462
|
+
}
|
|
463
|
+
case 'server': {
|
|
464
|
+
return question.defaultValue ? '--server' : '--no-server';
|
|
465
|
+
}
|
|
466
|
+
case 'caddy': {
|
|
467
|
+
return question.defaultValue ? '--caddy' : '--no-caddy';
|
|
468
|
+
}
|
|
469
|
+
case 'mcAlias': {
|
|
470
|
+
return question.defaultValue ? '--mc' : '--no-mc';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function shellQuote(value) {
|
|
475
|
+
return /^[\w@%+=:,./-]+$/.test(value)
|
|
476
|
+
? value
|
|
477
|
+
: `'${value.replaceAll("'", `'\\''`)}'`;
|
|
478
|
+
}
|
|
479
|
+
function hasValue(value, key) {
|
|
480
|
+
if (typeof value !== 'object' || value === null) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
return (Object.hasOwn(value, key) &&
|
|
484
|
+
value[key] !== undefined);
|
|
485
|
+
}
|
|
486
|
+
function errorMessage(error) {
|
|
487
|
+
return error instanceof Error ? error.message : String(error);
|
|
488
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How every command reports back. Commands return an Outcome instead of exiting
|
|
3
|
+
* themselves, so `cli.tsx` stays the one place that touches `process.exit` and
|
|
4
|
+
* the exit code of any run is easy to follow.
|
|
5
|
+
*/
|
|
6
|
+
/** Scripts depend on these numbers, so treat them as part of the public API. */
|
|
7
|
+
export declare const exitCodes: {
|
|
8
|
+
readonly success: 0;
|
|
9
|
+
/** An external operation failed: git, wt or gh returned an error. */
|
|
10
|
+
readonly operationFailed: 1;
|
|
11
|
+
/** The command line itself was wrong: unknown command, missing argument. */
|
|
12
|
+
readonly badUsage: 2;
|
|
13
|
+
/** The machine cannot run trunk: Windows, or git/wt missing. */
|
|
14
|
+
readonly unsupportedEnvironment: 3;
|
|
15
|
+
/** The user declined a prompt or interrupted the run. */
|
|
16
|
+
readonly userAborted: 4;
|
|
17
|
+
};
|
|
18
|
+
export type ExitCode = (typeof exitCodes)[keyof typeof exitCodes];
|
|
19
|
+
export type Outcome = {
|
|
20
|
+
code: ExitCode;
|
|
21
|
+
message?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function succeed(message?: string): Outcome;
|
|
24
|
+
export declare function operationFailed(message: string): Outcome;
|
|
25
|
+
export declare function badUsage(message: string): Outcome;
|
|
26
|
+
export declare function unsupportedEnvironment(message: string): Outcome;
|
|
27
|
+
export declare function userAborted(message?: string): Outcome;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How every command reports back. Commands return an Outcome instead of exiting
|
|
3
|
+
* themselves, so `cli.tsx` stays the one place that touches `process.exit` and
|
|
4
|
+
* the exit code of any run is easy to follow.
|
|
5
|
+
*/
|
|
6
|
+
/** Scripts depend on these numbers, so treat them as part of the public API. */
|
|
7
|
+
export const exitCodes = {
|
|
8
|
+
success: 0,
|
|
9
|
+
/** An external operation failed: git, wt or gh returned an error. */
|
|
10
|
+
operationFailed: 1,
|
|
11
|
+
/** The command line itself was wrong: unknown command, missing argument. */
|
|
12
|
+
badUsage: 2,
|
|
13
|
+
/** The machine cannot run trunk: Windows, or git/wt missing. */
|
|
14
|
+
unsupportedEnvironment: 3,
|
|
15
|
+
/** The user declined a prompt or interrupted the run. */
|
|
16
|
+
userAborted: 4,
|
|
17
|
+
};
|
|
18
|
+
export function succeed(message) {
|
|
19
|
+
return { code: exitCodes.success, message };
|
|
20
|
+
}
|
|
21
|
+
export function operationFailed(message) {
|
|
22
|
+
return { code: exitCodes.operationFailed, message };
|
|
23
|
+
}
|
|
24
|
+
export function badUsage(message) {
|
|
25
|
+
return { code: exitCodes.badUsage, message };
|
|
26
|
+
}
|
|
27
|
+
export function unsupportedEnvironment(message) {
|
|
28
|
+
return { code: exitCodes.unsupportedEnvironment, message };
|
|
29
|
+
}
|
|
30
|
+
export function userAborted(message = 'setup aborted') {
|
|
31
|
+
return { code: exitCodes.userAborted, message };
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The complete input to the wt.toml generator. Keeping runtime detection and
|
|
3
|
+
* clocks outside this type makes composition deterministic and easy to test.
|
|
4
|
+
*/
|
|
5
|
+
import { type AgentId } from './agents.js';
|
|
6
|
+
import type { PackageManager } from './detect.js';
|
|
7
|
+
export type Settings = Readonly<{
|
|
8
|
+
trunkVersion: string;
|
|
9
|
+
generatedOn: string;
|
|
10
|
+
repoName: string;
|
|
11
|
+
hostLabel: string;
|
|
12
|
+
prefix: string;
|
|
13
|
+
pm: PackageManager;
|
|
14
|
+
appDir?: string;
|
|
15
|
+
tmux: boolean;
|
|
16
|
+
agents: readonly AgentId[];
|
|
17
|
+
editorWindow: true;
|
|
18
|
+
copyIgnored: boolean;
|
|
19
|
+
server: boolean;
|
|
20
|
+
caddy: boolean;
|
|
21
|
+
mcAlias: boolean;
|
|
22
|
+
devScript: string;
|
|
23
|
+
scripts: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Extra paths for `[step.copy-ignored]`. trunk never proposes these; they are
|
|
26
|
+
* carried over verbatim when adopting a hand-written config, so a repository
|
|
27
|
+
* that already excludes something does not lose it on overwrite.
|
|
28
|
+
*/
|
|
29
|
+
copyIgnoredExclude?: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* A project created before its remote exists. `remote_repo` renders empty
|
|
32
|
+
* without an origin, and every URL-bearing template is built from it, so the
|
|
33
|
+
* project name is written out literally instead.
|
|
34
|
+
*/
|
|
35
|
+
noRemote?: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const settingsDefaults: Readonly<{
|
|
38
|
+
tmux: true;
|
|
39
|
+
agents: readonly AgentId[];
|
|
40
|
+
editorWindow: true;
|
|
41
|
+
copyIgnored: true;
|
|
42
|
+
server: true;
|
|
43
|
+
caddy: true;
|
|
44
|
+
mcAlias: true;
|
|
45
|
+
devScript: "dev";
|
|
46
|
+
scripts: readonly string[];
|
|
47
|
+
}>;
|
|
48
|
+
/** Rejects values that cannot be represented safely by the generated config. */
|
|
49
|
+
export declare function assertSettings(settings: Settings): void;
|
|
50
|
+
/** Caddy has nothing to route when the development server is disabled. */
|
|
51
|
+
export declare function usesCaddy(settings: Settings): boolean;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The complete input to the wt.toml generator. Keeping runtime detection and
|
|
3
|
+
* clocks outside this type makes composition deterministic and easy to test.
|
|
4
|
+
*/
|
|
5
|
+
import { isAgentId, maximumAgents } from './agents.js';
|
|
6
|
+
import { validatePrefix } from './prefix.js';
|
|
7
|
+
export const settingsDefaults = Object.freeze({
|
|
8
|
+
tmux: true,
|
|
9
|
+
agents: Object.freeze([]),
|
|
10
|
+
editorWindow: true,
|
|
11
|
+
copyIgnored: true,
|
|
12
|
+
server: true,
|
|
13
|
+
caddy: true,
|
|
14
|
+
mcAlias: true,
|
|
15
|
+
devScript: 'dev',
|
|
16
|
+
scripts: Object.freeze([]),
|
|
17
|
+
});
|
|
18
|
+
/** Rejects values that cannot be represented safely by the generated config. */
|
|
19
|
+
export function assertSettings(settings) {
|
|
20
|
+
assertSingleLine('trunkVersion', settings.trunkVersion);
|
|
21
|
+
assertDate(settings.generatedOn);
|
|
22
|
+
assertSingleLine('repoName', settings.repoName);
|
|
23
|
+
assertSingleLine('hostLabel', settings.hostLabel);
|
|
24
|
+
assertSingleLine('devScript', settings.devScript);
|
|
25
|
+
if (settings.hostLabel !== settings.repoName.toLowerCase()) {
|
|
26
|
+
throw new TypeError('hostLabel must be the lowercase repoName.');
|
|
27
|
+
}
|
|
28
|
+
const prefix = validatePrefix(settings.prefix);
|
|
29
|
+
if (!prefix.valid) {
|
|
30
|
+
throw new TypeError(prefix.reason);
|
|
31
|
+
}
|
|
32
|
+
if (settings.agents.length > maximumAgents) {
|
|
33
|
+
throw new TypeError(`At most ${maximumAgents} agents can be generated.`);
|
|
34
|
+
}
|
|
35
|
+
if (!settings.tmux && settings.agents.length > 0) {
|
|
36
|
+
throw new TypeError('Agents require the tmux session.');
|
|
37
|
+
}
|
|
38
|
+
if (new Set(settings.agents).size !== settings.agents.length) {
|
|
39
|
+
throw new TypeError('Agent ids must be unique.');
|
|
40
|
+
}
|
|
41
|
+
for (const agent of settings.agents) {
|
|
42
|
+
const value = agent;
|
|
43
|
+
if (!isAgentId(value)) {
|
|
44
|
+
throw new TypeError(`Unknown agent id: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (settings.appDir) {
|
|
48
|
+
assertRelativePath(settings.appDir);
|
|
49
|
+
}
|
|
50
|
+
for (const script of settings.scripts) {
|
|
51
|
+
assertSingleLine('script name', script);
|
|
52
|
+
}
|
|
53
|
+
for (const exclude of settings.copyIgnoredExclude ?? []) {
|
|
54
|
+
assertSingleLine('copy-ignored exclude', exclude);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Caddy has nothing to route when the development server is disabled. */
|
|
58
|
+
export function usesCaddy(settings) {
|
|
59
|
+
return settings.server && settings.caddy;
|
|
60
|
+
}
|
|
61
|
+
function assertDate(value) {
|
|
62
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
63
|
+
throw new TypeError('generatedOn must use YYYY-MM-DD.');
|
|
64
|
+
}
|
|
65
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
66
|
+
if (Number.isNaN(parsed.valueOf()) ||
|
|
67
|
+
!parsed.toISOString().startsWith(value)) {
|
|
68
|
+
throw new TypeError('generatedOn must be a real calendar date.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function assertSingleLine(name, value) {
|
|
72
|
+
if (!value || /[\0\r\n]/.test(value)) {
|
|
73
|
+
throw new TypeError(`${name} must be a non-empty single line.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function assertRelativePath(value) {
|
|
77
|
+
assertSingleLine('appDir', value);
|
|
78
|
+
if (value.startsWith('/') ||
|
|
79
|
+
value.startsWith('~') ||
|
|
80
|
+
value.split(/[\\/]/).includes('..')) {
|
|
81
|
+
throw new TypeError('appDir must be a relative path inside the worktree.');
|
|
82
|
+
}
|
|
83
|
+
}
|