cawdev-cli 0.9.0 → 1.0.0-beta
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/README.md +4 -171
- package/lib/run-plugin.mjs +40 -0
- package/lib/usage-report.mjs +27 -0
- package/lib/version.mjs +48 -0
- package/package.json +1 -1
- package/runner/README.md +56 -0
- package/runner/attach.mjs +442 -1
- package/runner/banner.mjs +11 -1
- package/runner/bootstrap.mjs +70 -35
- package/runner/cawdev.mjs +215 -19
- package/runner/configure.mjs +412 -0
- package/runner/paths.mjs +254 -0
- package/runner/runner.mjs +829 -131
- package/runner/token-store.mjs +14 -2
package/runner/bootstrap.mjs
CHANGED
|
@@ -30,8 +30,9 @@ import { spawn } from 'node:child_process';
|
|
|
30
30
|
import { access, chmod, mkdir, writeFile } from 'node:fs/promises';
|
|
31
31
|
import { createInterface } from 'node:readline/promises';
|
|
32
32
|
import { homedir, hostname } from 'node:os';
|
|
33
|
-
import { dirname, join
|
|
33
|
+
import { dirname, join } from 'node:path';
|
|
34
34
|
import { painter } from '../lib/ansi.mjs';
|
|
35
|
+
import { absolute } from './paths.mjs';
|
|
35
36
|
import { Select, pickFromLine, pickManyFromLine, plainLines } from './select.mjs';
|
|
36
37
|
import { signInThroughBrowser, storedSession } from './sign-in.mjs';
|
|
37
38
|
import { saveToken, tokenFile } from './token-store.mjs';
|
|
@@ -106,7 +107,9 @@ export function checkoutFor(root, slug) {
|
|
|
106
107
|
// the root the person named. Everything else is already inside it, because
|
|
107
108
|
// the separator is the character the line above removes.
|
|
108
109
|
.replace(/^\.+$/, '-');
|
|
109
|
-
|
|
110
|
+
// `absolute` and not `resolve`: `~/code` through resolve alone was
|
|
111
|
+
// `<cwd>/~/code`, which is the bug R289 started from.
|
|
112
|
+
return join(absolute(root), safe);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
async function exists(path) {
|
|
@@ -160,7 +163,11 @@ export async function mintRunnerToken(session, slugs, name) {
|
|
|
160
163
|
if (!minted?.secret) {
|
|
161
164
|
throw new Error('The platform minted a token but did not return it.');
|
|
162
165
|
}
|
|
163
|
-
|
|
166
|
+
// R283: the id travels alongside the secret from here on, so a later
|
|
167
|
+
// `cawdev config add-project` can widen THIS token's grants
|
|
168
|
+
// (`PUT /api/tokens/{id}/grants`) instead of minting a new one and leaving
|
|
169
|
+
// the old one live on the Tokens page for somebody to notice and revoke.
|
|
170
|
+
return { secret: minted.secret, id: minted.token?.id ?? null };
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
/**
|
|
@@ -185,19 +192,26 @@ export function cloneInto(gitUrl, path) {
|
|
|
185
192
|
});
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
/** Whether
|
|
189
|
-
export function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
/** Whether the agent is on the PATH at all — a cheaper question than "is it signed in". */
|
|
196
|
+
export async function findAgents(commands = ['agy', 'claude']) {
|
|
197
|
+
const found = [];
|
|
198
|
+
for (const command of Array.isArray(commands) ? commands : [commands]) {
|
|
199
|
+
const where = await new Promise((done) => {
|
|
200
|
+
const child = spawn(process.platform === 'win32' ? 'where' : 'which', [command], {
|
|
201
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
202
|
+
});
|
|
203
|
+
let out = '';
|
|
204
|
+
child.stdout.on('data', (chunk) => {
|
|
205
|
+
out += chunk;
|
|
206
|
+
});
|
|
207
|
+
child.on('error', () => done(null));
|
|
208
|
+
child.on('close', (code) => done(code === 0 && out.trim() ? out.trim().split('\n')[0] : null));
|
|
193
209
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
child.on('close', (code) => done(code === 0 && out.trim() ? out.trim().split('\n')[0] : null));
|
|
200
|
-
});
|
|
210
|
+
if (where) {
|
|
211
|
+
found.push({ command, where });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return found;
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
/**
|
|
@@ -285,7 +299,7 @@ export async function setUpThisMachine({
|
|
|
285
299
|
configPath = runnerConfigPath(),
|
|
286
300
|
signIn = signInThroughBrowser,
|
|
287
301
|
session: given = null,
|
|
288
|
-
agent =
|
|
302
|
+
agent = findAgents,
|
|
289
303
|
} = {}) {
|
|
290
304
|
const session = given ?? (await storedSession(url));
|
|
291
305
|
|
|
@@ -360,7 +374,7 @@ export async function setUpThisMachine({
|
|
|
360
374
|
say(` ${ink.muted(`Skipping ${project.slug}.`)}`);
|
|
361
375
|
continue;
|
|
362
376
|
}
|
|
363
|
-
entries.push({ slug: project.slug, path:
|
|
377
|
+
entries.push({ slug: project.slug, path: absolute(typed) });
|
|
364
378
|
continue;
|
|
365
379
|
}
|
|
366
380
|
say('');
|
|
@@ -377,25 +391,46 @@ export async function setUpThisMachine({
|
|
|
377
391
|
// R93's one question this cannot answer for itself. Asked BEFORE the token is
|
|
378
392
|
// minted, so somebody who has to go and log in elsewhere has not left a
|
|
379
393
|
// credential behind them.
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
} else {
|
|
385
|
-
say(` ${ink.warn('!')} ${ink.muted('No')} ${ink.text('claude')} ${ink.muted('on this PATH. Install Claude Code before a run can start.')}`);
|
|
394
|
+
const agentInfos = await agent();
|
|
395
|
+
if (agentInfos.length === 0) {
|
|
396
|
+
say(` ${ink.warn('!')} ${ink.muted('Neither Claude Code nor Antigravity CLI found on PATH. Install at least one before a run can start.')}`);
|
|
397
|
+
throw new Error('No supported coding agent found.');
|
|
386
398
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
'
|
|
392
|
-
|
|
393
|
-
|
|
399
|
+
|
|
400
|
+
const chosenAgents = agentInfos.length === 1
|
|
401
|
+
? agentInfos
|
|
402
|
+
: (await askMany(ask, say, new Select({
|
|
403
|
+
title: 'Which coding agents do you want to use on this machine?',
|
|
404
|
+
rows: agentInfos.map((a) => ({
|
|
405
|
+
id: a.command,
|
|
406
|
+
label: a.command === 'agy' ? 'Antigravity CLI' : 'Claude Code',
|
|
407
|
+
hint: a.where,
|
|
408
|
+
})),
|
|
409
|
+
}))).map((row) => agentInfos.find((a) => a.command === row.id));
|
|
410
|
+
|
|
411
|
+
if (!chosenAgents.length) {
|
|
412
|
+
throw new Error('You must select at least one coding agent to run.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
say('');
|
|
416
|
+
for (const a of chosenAgents) {
|
|
417
|
+
const agentDisplayName = a.command === 'agy' ? 'Antigravity CLI' : 'Claude Code';
|
|
418
|
+
say(` ${ink.muted('This machine spawns')} ${ink.text(a.command)} ${ink.muted(`(${a.where}) for every run.`)}`);
|
|
419
|
+
if (!await confirm(ask, `Is ${agentDisplayName} signed in on this machine?`)) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Sign in first — run \`${a.command}\` once in a terminal and follow it — then run cawdev again.\n`
|
|
422
|
+
+ ' Nothing has been created, so there is nothing to undo.',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
394
425
|
}
|
|
395
426
|
|
|
396
427
|
const name = defaultRunnerName();
|
|
397
|
-
const
|
|
398
|
-
const config = configFor({ url, token, name, entries });
|
|
428
|
+
const minted = await mintRunnerToken(session, entries.map((entry) => entry.slug), name);
|
|
429
|
+
const config = configFor({ url, token: minted.secret, name, entries });
|
|
430
|
+
config.agentCommands = chosenAgents.map(a => a.command);
|
|
431
|
+
// R283: kept beside the secret so a later `cawdev config add-project` can
|
|
432
|
+
// widen this token in place rather than minting a second one.
|
|
433
|
+
config.tokenId = minted.id;
|
|
399
434
|
await write(configPath, config);
|
|
400
435
|
|
|
401
436
|
say('');
|
|
@@ -477,13 +512,13 @@ export async function mintForThisMachine({
|
|
|
477
512
|
say(` ${ink.success('✓')} ${ink.muted('Signed in as')} ${ink.text(session.email)}`);
|
|
478
513
|
|
|
479
514
|
const name = config.name ?? defaultRunnerName();
|
|
480
|
-
const
|
|
515
|
+
const minted = await mintRunnerToken(session, slugs, name);
|
|
481
516
|
|
|
482
517
|
// Filed under the name the DAEMON will look for, which is not always the door
|
|
483
518
|
// a person came through — in development the console is on `:4200` and the
|
|
484
519
|
// API it proxies to is on `:8091`, and both are this one cawdev. Storing it
|
|
485
520
|
// under the sign-in URL puts a working credential somewhere nothing reads.
|
|
486
|
-
await store(storeUrl,
|
|
521
|
+
await store(storeUrl, minted.secret, { name, id: minted.id });
|
|
487
522
|
|
|
488
523
|
say(` ${ink.success('✓')} ${ink.muted('Minted a')} ${ink.text('runner:operate')} `
|
|
489
524
|
+ `${ink.muted(`token for ${slugs.length} project${slugs.length === 1 ? '' : 's'}`)}`);
|
|
@@ -497,5 +532,5 @@ export async function mintForThisMachine({
|
|
|
497
532
|
}
|
|
498
533
|
say('');
|
|
499
534
|
|
|
500
|
-
return { token, slugs, name };
|
|
535
|
+
return { token: minted.secret, tokenId: minted.id, slugs, name };
|
|
501
536
|
}
|
package/runner/cawdev.mjs
CHANGED
|
@@ -26,11 +26,18 @@ import { homedir } from 'node:os';
|
|
|
26
26
|
import { dirname, join, resolve } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import { attach, urlFrom, valueOf } from './attach.mjs';
|
|
29
|
-
import { asker, mintForThisMachine, setUpThisMachine } from './bootstrap.mjs';
|
|
29
|
+
import { asker, askOne, mintForThisMachine, setUpThisMachine } from './bootstrap.mjs';
|
|
30
30
|
import { liveSockets, probeSocket, socketPathFor } from './control.mjs';
|
|
31
|
+
import {
|
|
32
|
+
addProject, addWorkspace, readConfigFile, reloadIfRunning, setAcceptsRulesFromConsole,
|
|
33
|
+
setAgentEnabled,
|
|
34
|
+
} from './configure.mjs';
|
|
35
|
+
import { Select } from './select.mjs';
|
|
36
|
+
import { storedSession } from './sign-in.mjs';
|
|
31
37
|
import { loadToken } from './token-store.mjs';
|
|
32
38
|
import { painter } from '../lib/ansi.mjs';
|
|
33
39
|
import { mark } from './brand.mjs';
|
|
40
|
+
import { cliVersion, isBelow } from '../lib/version.mjs';
|
|
34
41
|
|
|
35
42
|
const HERE = dirname(new URL(import.meta.url).pathname);
|
|
36
43
|
const RUNNER = join(HERE, 'runner.mjs');
|
|
@@ -46,14 +53,26 @@ const USAGE = `
|
|
|
46
53
|
cawdev --no-start attach only; never launch a daemon
|
|
47
54
|
cawdev --watch-only do not sign in; watch without being able to act
|
|
48
55
|
cawdev --leave-running leave the daemon running when you quit
|
|
56
|
+
cawdev --version this CLI's version, and the platform's
|
|
49
57
|
cawdev --help
|
|
50
58
|
|
|
59
|
+
cawdev config a menu: add a project, add a
|
|
60
|
+
workspace, agents, console rules
|
|
61
|
+
cawdev config add-project serve one more project you can write to
|
|
62
|
+
cawdev config add-workspace another checkout for a project you
|
|
63
|
+
already serve — R47's concurrency
|
|
64
|
+
cawdev config agent enable|disable <claude|agy>
|
|
65
|
+
cawdev config rules on|off accept permission rules from the console
|
|
66
|
+
cawdev config show this machine's config, token redacted
|
|
67
|
+
|
|
51
68
|
On a machine with no config, cawdev sets one up: it signs you in through the
|
|
52
69
|
browser, asks which projects this machine should run agents for, clones them,
|
|
53
|
-
and mints its own runner token. No token is ever typed.
|
|
70
|
+
and mints its own runner token. No token is ever typed. \`cawdev config\` is
|
|
71
|
+
the same walk, one change at a time, on a machine already set up.
|
|
54
72
|
|
|
55
73
|
Inside: enter prompts the session you are watching, / takes a command
|
|
56
|
-
(/help lists them), L lists the runs,
|
|
74
|
+
(/help lists them), L lists the runs, c configures this machine (the same
|
|
75
|
+
menu as \`cawdev config\`, on screen), and q stops the machine and leaves.
|
|
57
76
|
It asks twice while sessions are running. --leave-running keeps it up.
|
|
58
77
|
`;
|
|
59
78
|
|
|
@@ -280,22 +299,10 @@ export function daemonUrl(file, env = process.env) {
|
|
|
280
299
|
return String(env.CAWDEV_URL ?? file?.url ?? DEFAULT_URL).replace(/\/+$/, '');
|
|
281
300
|
}
|
|
282
301
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
* make — it names the file and the parse error — and swallowing it here to
|
|
288
|
-
* offer a setup walk would replace a precise message with a wrong guess about
|
|
289
|
-
* what somebody wants.
|
|
290
|
-
*/
|
|
291
|
-
export async function readConfigFile(path) {
|
|
292
|
-
try {
|
|
293
|
-
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
294
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
295
|
-
} catch {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
302
|
+
// `readConfigFile` lives in configure.mjs since R288 — the attached UI reads
|
|
303
|
+
// the file too, and this module imports attach.mjs. Re-exported so nothing
|
|
304
|
+
// that imported it from here has to move.
|
|
305
|
+
export { readConfigFile };
|
|
299
306
|
|
|
300
307
|
/**
|
|
301
308
|
* The setup walk, with the terminal handed to it and taken back.
|
|
@@ -337,6 +344,181 @@ async function runMint(configPath, file, argv, ink) {
|
|
|
337
344
|
});
|
|
338
345
|
}
|
|
339
346
|
|
|
347
|
+
/**
|
|
348
|
+
* This CLI's version against the platform's — R283.
|
|
349
|
+
*
|
|
350
|
+
* Best-effort and silent on the happy path, matching `acceptsConsoleRules`'
|
|
351
|
+
* own philosophy (R126's comment on the daemon side): a mismatch is worth a
|
|
352
|
+
* line, agreement is worth nothing. Never thrown from here — a network hiccup
|
|
353
|
+
* is not evidence of a stale CLI, and `cawdev` has an attach to get on with.
|
|
354
|
+
*/
|
|
355
|
+
async function warnIfVersionMismatched(url, ink) {
|
|
356
|
+
try {
|
|
357
|
+
const mine = await cliVersion();
|
|
358
|
+
const response = await fetch(`${url}/api/health`);
|
|
359
|
+
if (!response.ok) return;
|
|
360
|
+
const health = await response.json();
|
|
361
|
+
if (health.version && health.version !== mine && health.version !== 'unknown') {
|
|
362
|
+
console.log(` ${ink.warn('!')} ${ink.muted(`This CLI is ${mine}; ${url} is on ${health.version}.`)}`);
|
|
363
|
+
console.log(` ${ink.muted('Update this machine\'s cawdev if things look wrong.')}`);
|
|
364
|
+
}
|
|
365
|
+
if (isBelow(mine, health.minimumCliVersion)) {
|
|
366
|
+
console.log(` ${ink.danger('!')} ${ink.text(`This CLI (${mine}) is below what ${url} requires `
|
|
367
|
+
+ `(${health.minimumCliVersion}) — the daemon will refuse to start until it is updated.`)}`);
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// Best-effort. readConfig's own health check, before registering, is
|
|
371
|
+
// where a genuinely unreachable platform actually matters.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* `cawdev --version`: this CLI's own version, and the platform's.
|
|
377
|
+
*
|
|
378
|
+
* Works with no config and no sign-in — `/api/health` needs neither — so it
|
|
379
|
+
* answers on a machine that has not been set up at all yet.
|
|
380
|
+
*/
|
|
381
|
+
async function runVersion(argv, ink) {
|
|
382
|
+
const mine = await cliVersion();
|
|
383
|
+
console.log(` ${ink.muted('cawdev')} ${ink.text(mine)}`);
|
|
384
|
+
const url = urlFrom(argv);
|
|
385
|
+
try {
|
|
386
|
+
const response = await fetch(`${url}/api/health`);
|
|
387
|
+
const health = response.ok ? await response.json() : null;
|
|
388
|
+
if (health) {
|
|
389
|
+
console.log(` ${ink.muted(url)} ${ink.text(health.version)}`);
|
|
390
|
+
if (isBelow(mine, health.minimumCliVersion)) {
|
|
391
|
+
console.log(` ${ink.danger('This CLI is below the minimum that platform requires')} `
|
|
392
|
+
+ `(${health.minimumCliVersion}). Update it.`);
|
|
393
|
+
} else if (health.version !== mine && health.version !== 'unknown') {
|
|
394
|
+
console.log(` ${ink.muted('These differ — update whichever is behind.')}`);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
console.log(` ${ink.muted(url)} ${ink.warn('did not answer')}`);
|
|
398
|
+
}
|
|
399
|
+
} catch (failure) {
|
|
400
|
+
console.log(` ${ink.muted(url)} ${ink.warn(`unreachable (${failure.message})`)}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* `cawdev config` — R283's granular half of the setup walk, on a machine
|
|
406
|
+
* already configured. A direct subcommand (`add-project`, `add-workspace`,
|
|
407
|
+
* `agent enable|disable <command>`, `rules on|off`, `show`) or, with none, a
|
|
408
|
+
* menu — the same `Select` widget everything that offers a choice uses.
|
|
409
|
+
*/
|
|
410
|
+
async function runConfig(args, ink) {
|
|
411
|
+
const configPath = await findConfig(args);
|
|
412
|
+
if (!configPath) {
|
|
413
|
+
throw new Error('No config on this machine yet. Run `cawdev --setup` first.');
|
|
414
|
+
}
|
|
415
|
+
const file = await readConfigFile(configPath);
|
|
416
|
+
if (!file) {
|
|
417
|
+
throw new Error(`Could not read ${configPath}.`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (args[0] === 'show') {
|
|
421
|
+
printConfig(file, ink, (line) => console.log(line));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const url = daemonUrl(file);
|
|
426
|
+
const session = await storedSession(url);
|
|
427
|
+
if (!session.signedIn) {
|
|
428
|
+
throw new Error('Not signed in on this machine. Run `cawdev --setup` first.');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const ask = asker();
|
|
432
|
+
const say = (line) => console.log(line);
|
|
433
|
+
try {
|
|
434
|
+
const action = args.length ? args : await pickAction(ask, say);
|
|
435
|
+
await runAction(action, { configPath, file, url, session, ask, say, ink });
|
|
436
|
+
} finally {
|
|
437
|
+
ask.close();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function pickAction(ask, say, pick = askOne) {
|
|
442
|
+
const picked = await pick(ask, say, new Select({
|
|
443
|
+
title: 'What do you want to change on this machine?',
|
|
444
|
+
rows: [
|
|
445
|
+
{ id: 'add-project', label: 'Add a project' },
|
|
446
|
+
{ id: 'add-workspace', label: 'Add a workspace to a project' },
|
|
447
|
+
{ id: 'agent-enable', label: 'Enable an agent (claude / agy)' },
|
|
448
|
+
{ id: 'agent-disable', label: 'Disable an agent' },
|
|
449
|
+
{ id: 'rules-on', label: 'Accept permission rules from the console' },
|
|
450
|
+
{ id: 'rules-off', label: 'Stop accepting rules from the console' },
|
|
451
|
+
{ id: 'show', label: 'Show this machine\'s config' },
|
|
452
|
+
],
|
|
453
|
+
}));
|
|
454
|
+
const id = picked.row.id;
|
|
455
|
+
if (id === 'agent-enable' || id === 'agent-disable') {
|
|
456
|
+
const command = (await ask.line(' Which agent — claude or agy? ')).trim();
|
|
457
|
+
return ['agent', id === 'agent-enable' ? 'enable' : 'disable', command];
|
|
458
|
+
}
|
|
459
|
+
if (id === 'rules-on') return ['rules', 'on'];
|
|
460
|
+
if (id === 'rules-off') return ['rules', 'off'];
|
|
461
|
+
return [id];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function runAction([verb, ...rest], { configPath, file, url, session, ask, say, ink }) {
|
|
465
|
+
if (verb === 'show') {
|
|
466
|
+
printConfig(file, ink, say);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (verb === 'add-project') {
|
|
470
|
+
await addProject({ configPath, file, url, session, ask, say, ink });
|
|
471
|
+
await afterProjectsChanged(file, say, ink);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (verb === 'add-workspace') {
|
|
475
|
+
await addWorkspace({ configPath, file, url, session, ask, say, ink });
|
|
476
|
+
await afterProjectsChanged(file, say, ink);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (verb === 'agent') {
|
|
480
|
+
const [sub, command] = rest;
|
|
481
|
+
if (!['claude', 'agy'].includes(command)) {
|
|
482
|
+
throw new Error('Name an agent: cawdev config agent enable|disable claude|agy');
|
|
483
|
+
}
|
|
484
|
+
await setAgentEnabled({
|
|
485
|
+
file, configPath, ask, say, ink, command, enabled: sub === 'enable',
|
|
486
|
+
});
|
|
487
|
+
await reloadIfRunning(file.name, say, ink);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (verb === 'rules') {
|
|
491
|
+
const [sub] = rest;
|
|
492
|
+
await setAcceptsRulesFromConsole({ file, configPath, ask, say, ink, enabled: sub === 'on' });
|
|
493
|
+
await reloadIfRunning(file.name, say, ink);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
throw new Error(`Not a cawdev config command: ${verb}. See cawdev --help.`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Adding a project or a workspace is NOT hot-swappable — `config.projects` is
|
|
501
|
+
* read once at boot and threaded everywhere by value, unlike the three
|
|
502
|
+
* settings runner.mjs's SIGHUP handler re-reads. So this only says why
|
|
503
|
+
* nothing happened yet, on the daemon this config would actually restart as.
|
|
504
|
+
*/
|
|
505
|
+
export async function afterProjectsChanged(file, say, ink, { probe = probeSocket } = {}) {
|
|
506
|
+
if (await probe(socketPathFor(file.name))) {
|
|
507
|
+
say(` ${ink.muted('A runner is already running here, on the config it booted with.')}`);
|
|
508
|
+
say(` ${ink.muted('Attach and quit to stop it, and the next cawdev starts one on what you just set up.')}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** The config as somebody would want to read it back — never the secret. */
|
|
513
|
+
export function printConfig(file, ink, say) {
|
|
514
|
+
const shown = { ...file };
|
|
515
|
+
if (shown.token) shown.token = `${String(shown.token).slice(0, 10)}…`;
|
|
516
|
+
say(JSON.stringify(shown, null, 2)
|
|
517
|
+
.split('\n')
|
|
518
|
+
.map((line) => ` ${ink.text(line)}`)
|
|
519
|
+
.join('\n'));
|
|
520
|
+
}
|
|
521
|
+
|
|
340
522
|
async function main() {
|
|
341
523
|
const argv = process.argv.slice(2);
|
|
342
524
|
const ink = painter();
|
|
@@ -346,6 +528,16 @@ async function main() {
|
|
|
346
528
|
return;
|
|
347
529
|
}
|
|
348
530
|
|
|
531
|
+
if (argv[0] === 'config') {
|
|
532
|
+
await runConfig(argv.slice(1), ink);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (argv.includes('--version')) {
|
|
537
|
+
await runVersion(argv, ink);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
349
541
|
// The mark, once, before anything else happens. It is the only decoration in
|
|
350
542
|
// the program and it is here because this is the moment somebody is waiting:
|
|
351
543
|
// a browser about to open, or a daemon about to boot.
|
|
@@ -353,6 +545,10 @@ async function main() {
|
|
|
353
545
|
console.log(`\n${line}\n`);
|
|
354
546
|
}
|
|
355
547
|
|
|
548
|
+
// R283. Best-effort, silent when the two agree — see the function's own
|
|
549
|
+
// comment for why this never throws.
|
|
550
|
+
await warnIfVersionMismatched(urlFrom(argv), ink);
|
|
551
|
+
|
|
356
552
|
// Asked for by name, the walk runs even where one has been done before —
|
|
357
553
|
// that is what "again" means, and adding a project to this machine is the
|
|
358
554
|
// ordinary reason. A daemon already running keeps the config it booted with,
|