@agent-flow-board/bridge 0.2.0 → 0.3.0
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 +16 -0
- package/dist/checks/appdir.js +105 -0
- package/dist/checks/approve.js +213 -0
- package/dist/checks/audit.js +3 -2
- package/dist/checks/bind.js +3 -2
- package/dist/checks/broker.js +439 -0
- package/dist/checks/browser.js +137 -32
- package/dist/checks/browserpool.js +221 -0
- package/dist/checks/cache.js +710 -0
- package/dist/checks/command.js +75 -88
- package/dist/checks/commands.js +219 -0
- package/dist/checks/connections.js +4 -3
- package/dist/checks/copy.js +320 -0
- package/dist/checks/dbpool.js +710 -0
- package/dist/checks/doctor.js +649 -0
- package/dist/checks/envPolicy.js +50 -1
- package/dist/checks/envrules.js +322 -0
- package/dist/checks/evidence.js +114 -0
- package/dist/checks/fsretry.js +29 -0
- package/dist/checks/importlegacy.js +300 -0
- package/dist/checks/install.js +150 -0
- package/dist/checks/live.js +76 -0
- package/dist/checks/lock.js +28 -4
- package/dist/checks/login.js +203 -0
- package/dist/checks/mailguard-hook.cjs +216 -0
- package/dist/checks/mailguard.js +98 -0
- package/dist/checks/paths.js +24 -0
- package/dist/checks/preview.js +418 -191
- package/dist/checks/profiles.js +110 -0
- package/dist/checks/provision.js +1 -1
- package/dist/checks/recipe.js +19 -5
- package/dist/checks/registry.js +548 -0
- package/dist/checks/repoenv.js +162 -0
- package/dist/checks/roots.js +144 -0
- package/dist/checks/routing.js +68 -0
- package/dist/checks/run.js +243 -53
- package/dist/checks/sandbox.js +32 -10
- package/dist/checks/scheduler.js +383 -0
- package/dist/checks/scope.js +119 -30
- package/dist/checks/sealedNames.js +39 -0
- package/dist/checks/secrets.js +452 -0
- package/dist/checks/secretscmd.js +80 -0
- package/dist/checks/service.js +589 -0
- package/dist/checks/signin.js +7 -1
- package/dist/checks/singleflight.js +307 -0
- package/dist/checks/store.js +92 -0
- package/dist/checks/surface.js +124 -0
- package/dist/checks/warm.js +342 -0
- package/dist/checks/watch.js +190 -63
- package/dist/checks/wire.js +87 -0
- package/package.json +1 -1
package/dist/checks/command.js
CHANGED
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
// -- DEP-MODULES -- //
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
8
8
|
// -- MODULES -- //
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { ensureBrowser, playwrightRuntimeDir } from './browser.js';
|
|
9
|
+
import { bindFolder } from './bind.js';
|
|
10
|
+
import { ensureBrowser } from './browser.js';
|
|
12
11
|
import { connectChecks, loadRunnerToken, runnerAccount } from './connect.js';
|
|
13
|
-
import { forgetConnection,
|
|
14
|
-
import { ORG_SLUG,
|
|
12
|
+
import { forgetConnection, recordConnection } from './connections.js';
|
|
13
|
+
import { ORG_SLUG, scriptPath, scriptsDir } from './paths.js';
|
|
15
14
|
import { pinFiles, resolveProjectConfig } from './recipe.js';
|
|
16
15
|
import { runCheck } from './run.js';
|
|
17
|
-
import { clearRuns, sweepOrphans } from './scope.js';
|
|
18
|
-
import { permissionFlag } from './sandbox.js';
|
|
19
16
|
import { watchChecks } from './watch.js';
|
|
17
|
+
import { approveCommand, projectCommand, rootsCommand } from './commands.js';
|
|
18
|
+
import { clearCommand, healthCommand } from './doctor.js';
|
|
19
|
+
import { importLegacyCommand } from './importlegacy.js';
|
|
20
|
+
import { loginCommand } from './login.js';
|
|
21
|
+
import { secretsCommand } from './secretscmd.js';
|
|
22
|
+
import { serviceCommand } from './service.js';
|
|
20
23
|
// -- CONSTANTS -- //
|
|
21
24
|
export const CHECK_USAGE = [
|
|
22
25
|
'Usage: agent-flow-board check <command>',
|
|
@@ -25,14 +28,36 @@ export const CHECK_USAGE = [
|
|
|
25
28
|
' pull <item> <key> --server <url> --org <slug> [--accept-hash <sha256>] read the board copy of a script; accept it by hash',
|
|
26
29
|
' bind --org <slug> --project <KEY> [--dir <repo>] remember the folder of a board project for "check watch"',
|
|
27
30
|
' watch [--parallel N] [--once] take queued runs from the boards this machine is connected to',
|
|
28
|
-
'
|
|
31
|
+
' run ... also takes [--registration NAME] [--file <script inside the scripts folder>] [--headed]',
|
|
32
|
+
' project <NAME> --org <slug> (--repo <dir> [--app-dir web] [--board-project KEY] [--recipe] | --url <address> [--extra-hosts a,b]) [--adopt-env --adopt-env-approval <code>] [--upload-pictures --pictures-approval <code>] [--approval <code>] | project list|forget|pin',
|
|
33
|
+
' approve --action <roots|register|secrets|adopt-env|pictures> --target <name> (the owner, on a terminal) | roots add|list|remove <dir> [--approval <code>]',
|
|
34
|
+
' secrets import|list|forget --org <slug> --project <NAME> [--from <file> --keys A,B --test-ref <ref> --test-inbox <email> --owner-said "<sentence>" --allow-service-role --approval <code>]',
|
|
35
|
+
' login <NAME> --org <slug> --seeded | service install|uninstall|status [--no-start] | import-legacy --from <afb-checks folder> --org <slug> [--dry-run]',
|
|
36
|
+
' status [--json] | doctor [--fix] [--json] | clear [runs|profiles|auth|dbs|previews|all] [--all] [--yes] | setup | disconnect --server <url> --org <slug>'
|
|
29
37
|
].join('\n');
|
|
30
38
|
// -- FUNCTIONS -- //
|
|
39
|
+
/** The value of --parallel: undefined when the flag is absent (the machine decides), else a whole number from 1 up. */
|
|
40
|
+
export function parallelFlag(raw) {
|
|
41
|
+
if (raw === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const n = Number(raw);
|
|
44
|
+
if (!Number.isInteger(n) || n < 1)
|
|
45
|
+
throw new Error(`--parallel takes a whole number of 1 or more, not "${raw.slice(0, 20)}".`);
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
31
48
|
/** Runs one check command and returns the exit code: 0 done, 1 a check failed or a problem was listed, 2 could not run. */
|
|
32
49
|
export async function checkMain(ctx) {
|
|
33
50
|
const { args, log, out } = ctx;
|
|
34
51
|
const sub = args[0];
|
|
35
|
-
|
|
52
|
+
// A flag that is given takes the next word as its value; a missing value, or another flag in its place (--step --json), is an error and never a silent default
|
|
53
|
+
const flag = (name) => {
|
|
54
|
+
if (!args.includes(name))
|
|
55
|
+
return undefined;
|
|
56
|
+
const v = args[args.indexOf(name) + 1];
|
|
57
|
+
if (v === undefined || v.startsWith('--'))
|
|
58
|
+
throw new Error(`Say ${name} <value>.`);
|
|
59
|
+
return v;
|
|
60
|
+
};
|
|
36
61
|
const has = (name) => args.includes(name);
|
|
37
62
|
const json = has('--json');
|
|
38
63
|
const need = (name) => {
|
|
@@ -83,12 +108,27 @@ export async function checkMain(ctx) {
|
|
|
83
108
|
return 0;
|
|
84
109
|
}
|
|
85
110
|
if (sub === 'run') {
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
// `--file <script>` names a script inside the scripts folder (<org>/<item>/<key>.mjs) instead of the item and the key
|
|
112
|
+
let item = args[1];
|
|
113
|
+
let key = args[2];
|
|
114
|
+
let fileOrg = null;
|
|
115
|
+
const fileFlag = flag('--file');
|
|
116
|
+
if (fileFlag) {
|
|
117
|
+
const rel = relative(scriptsDir(), resolve(fileFlag)).split(/[\\/]/);
|
|
118
|
+
const base = rel[2]?.replace(/\.mjs$/, '');
|
|
119
|
+
if (rel.length !== 3 || rel.includes('..') || !base)
|
|
120
|
+
throw new Error(`--file must be a script inside ${scriptsDir()}, in the form <org>/<item>/<key>.mjs.`);
|
|
121
|
+
fileOrg = rel[0] ?? null;
|
|
122
|
+
item = rel[1];
|
|
123
|
+
key = base;
|
|
124
|
+
}
|
|
88
125
|
if (!item || !key || item.startsWith('--') || key.startsWith('--'))
|
|
89
|
-
throw new Error('Usage: agent-flow-board check run <item> <key> --server <url> --org <slug>');
|
|
126
|
+
throw new Error('Usage: agent-flow-board check run <item> <key> --server <url> --org <slug> (or --file <script>)');
|
|
90
127
|
const s = server();
|
|
91
128
|
const o = org();
|
|
129
|
+
// The folder of a script names its organisation: a script of one organisation is never run as another's
|
|
130
|
+
if (fileOrg !== null && fileOrg !== o)
|
|
131
|
+
throw new Error(`--file is a script of the organisation ${fileOrg.slice(0, 40)}, but --org says ${o}. Use the same organisation in both.`);
|
|
92
132
|
const token = loadRunnerToken(ctx.store, s, o);
|
|
93
133
|
if (!token)
|
|
94
134
|
throw new Error(`This machine is not connected to ${o} on ${s}. Run: agent-flow-board check connect --server ${s} --org ${o} --json`);
|
|
@@ -100,6 +140,7 @@ export async function checkMain(ctx) {
|
|
|
100
140
|
dir: resolve(flag('--dir') ?? process.cwd()),
|
|
101
141
|
step: flag('--step'),
|
|
102
142
|
runId: flag('--run'),
|
|
143
|
+
registration: flag('--registration'),
|
|
103
144
|
runnerToken: token,
|
|
104
145
|
personalToken: ctx.personalToken(s, o),
|
|
105
146
|
fetch: ctx.fetch,
|
|
@@ -162,6 +203,25 @@ export async function checkMain(ctx) {
|
|
|
162
203
|
}));
|
|
163
204
|
return 0;
|
|
164
205
|
}
|
|
206
|
+
// The registration, approval, sign in and service commands live in their own modules; each takes the arguments after its word
|
|
207
|
+
if (sub === 'project')
|
|
208
|
+
return await projectCommand(args.slice(1), { log, out, json });
|
|
209
|
+
if (sub === 'approve')
|
|
210
|
+
return await approveCommand(args.slice(1), { log, out, json });
|
|
211
|
+
if (sub === 'roots')
|
|
212
|
+
return await rootsCommand(args.slice(1), { log, out, json });
|
|
213
|
+
if (sub === 'login')
|
|
214
|
+
return await loginCommand(args.slice(1), { log, out, json });
|
|
215
|
+
if (sub === 'service')
|
|
216
|
+
return await serviceCommand(args.slice(1), { log, out, json });
|
|
217
|
+
if (sub === 'secrets')
|
|
218
|
+
return await secretsCommand(args.slice(1), { log, out, json });
|
|
219
|
+
if (sub === 'import-legacy')
|
|
220
|
+
return await importLegacyCommand(args.slice(1), { log, out, json });
|
|
221
|
+
if (sub === 'clear')
|
|
222
|
+
return await clearCommand(args.slice(1), { log, out, json, store: ctx.store });
|
|
223
|
+
if (sub === 'status' || sub === 'doctor')
|
|
224
|
+
return await healthCommand(args, { log, out, json, store: ctx.store });
|
|
165
225
|
if (sub === 'bind') {
|
|
166
226
|
const o = org();
|
|
167
227
|
const project = need('--project');
|
|
@@ -180,7 +240,8 @@ export async function checkMain(ctx) {
|
|
|
180
240
|
store: ctx.store,
|
|
181
241
|
fetch: ctx.fetch,
|
|
182
242
|
log,
|
|
183
|
-
|
|
243
|
+
// Without --parallel the slots come from the machine (CPUs and free memory); a value that is not a whole number above 0 is refused, never turned into 3
|
|
244
|
+
parallel: parallelFlag(flag('--parallel')),
|
|
184
245
|
once: has('--once'),
|
|
185
246
|
signal: ac.signal
|
|
186
247
|
});
|
|
@@ -192,13 +253,6 @@ export async function checkMain(ctx) {
|
|
|
192
253
|
log('The browser is ready.');
|
|
193
254
|
return 0;
|
|
194
255
|
}
|
|
195
|
-
if (sub === 'status' || sub === 'doctor')
|
|
196
|
-
return await statusOrDoctor(ctx, sub === 'doctor', has('--fix'), json);
|
|
197
|
-
if (sub === 'clear') {
|
|
198
|
-
const r = clearRuns({ all: has('--all') });
|
|
199
|
-
log(`Removed ${r.removed} finished run folder(s).`);
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
256
|
log(CHECK_USAGE);
|
|
203
257
|
return 2;
|
|
204
258
|
}
|
|
@@ -211,70 +265,3 @@ export async function checkMain(ctx) {
|
|
|
211
265
|
return 2;
|
|
212
266
|
}
|
|
213
267
|
}
|
|
214
|
-
async function statusOrDoctor(ctx, doctor, fix, json) {
|
|
215
|
-
const problems = [];
|
|
216
|
-
const audit = verifyAudit();
|
|
217
|
-
if (!audit.ok)
|
|
218
|
-
problems.push({
|
|
219
|
-
problem: 'The audit log does not verify (it was edited or cut).',
|
|
220
|
-
next: 'Do not edit it. Move checks/audit.log aside to start a new chain.'
|
|
221
|
-
});
|
|
222
|
-
if (!permissionFlag())
|
|
223
|
-
problems.push({
|
|
224
|
-
problem: 'This Node has no permission model, so check scripts cannot be sandboxed.',
|
|
225
|
-
next: 'Install Node 20.18 or later (or any Node 22).'
|
|
226
|
-
});
|
|
227
|
-
if (!playwrightRuntimeDir()) {
|
|
228
|
-
if (doctor && fix)
|
|
229
|
-
await ensureBrowser(ctx.log);
|
|
230
|
-
else
|
|
231
|
-
problems.push({
|
|
232
|
-
problem: 'The browser is not installed.',
|
|
233
|
-
next: 'Run: agent-flow-board check setup'
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
const swept = doctor && fix ? await sweepOrphans(ctx.log) : 0;
|
|
237
|
-
const connections = listConnections().map((c) => ({
|
|
238
|
-
...c,
|
|
239
|
-
connected: Boolean(loadRunnerToken(ctx.store, c.server, c.org))
|
|
240
|
-
}));
|
|
241
|
-
for (const c of connections)
|
|
242
|
-
if (!c.connected)
|
|
243
|
-
problems.push({
|
|
244
|
-
problem: `${c.org} on ${c.server} has no runner token.`,
|
|
245
|
-
next: `Run: agent-flow-board check connect --server ${c.server} --org ${c.org} --json`
|
|
246
|
-
});
|
|
247
|
-
const bindings = listBindings().map((b) => ({
|
|
248
|
-
org: b.org,
|
|
249
|
-
project: b.projectKey,
|
|
250
|
-
folder: b.folder,
|
|
251
|
-
pin: pinStatus(b.org, b.projectKey)
|
|
252
|
-
}));
|
|
253
|
-
for (const b of bindings)
|
|
254
|
-
if (b.pin !== 'ok')
|
|
255
|
-
problems.push({
|
|
256
|
-
problem: `${b.org}/${b.project}: the files that decide what runs changed in ${b.folder}.`,
|
|
257
|
-
next: `In that folder run a check once: agent-flow-board check run <item> <key> --org ${b.org}`
|
|
258
|
-
});
|
|
259
|
-
const state = {
|
|
260
|
-
home: checksHome(),
|
|
261
|
-
connections,
|
|
262
|
-
bindings,
|
|
263
|
-
audit: { ok: audit.ok, lines: audit.lines },
|
|
264
|
-
browser: Boolean(playwrightRuntimeDir()),
|
|
265
|
-
swept,
|
|
266
|
-
problems
|
|
267
|
-
};
|
|
268
|
-
if (json)
|
|
269
|
-
ctx.out(JSON.stringify(state));
|
|
270
|
-
else {
|
|
271
|
-
ctx.log(`State folder: ${state.home}`);
|
|
272
|
-
ctx.log(`Connections: ${connections.map((c) => `${c.org}@${c.server}${c.connected ? '' : ' (no token)'}`).join(', ') || 'none'}`);
|
|
273
|
-
ctx.log(`Bound folders: ${bindings.map((b) => `${b.org}/${b.project} -> ${b.folder} (${b.pin})`).join('; ') || 'none'}`);
|
|
274
|
-
for (const p of problems)
|
|
275
|
-
ctx.log(`Problem: ${p.problem} Next: ${p.next}`);
|
|
276
|
-
if (!problems.length)
|
|
277
|
-
ctx.log('Nothing is wrong.');
|
|
278
|
-
}
|
|
279
|
-
return doctor && problems.length ? 1 : 0;
|
|
280
|
-
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// THE COMMANDS `check project`, `check approve` AND `check roots`. Plain handlers: they take the arguments after the command word and a small context, print through it and return
|
|
2
|
+
// an exit code (0 done, 1 refused with the problems listed, 2 could not run). Nothing here reads a terminal. `check approve` only asks whether it IS on a terminal, because a code is
|
|
3
|
+
// made by the owner and never by an agent.
|
|
4
|
+
// -- DEP-MODULES -- //
|
|
5
|
+
import { isatty } from 'node:tty';
|
|
6
|
+
// -- MODULES -- //
|
|
7
|
+
import { APPROVAL_ACTIONS, Refusal, UsageError, isApprovalAction, mintApproval } from './approve.js';
|
|
8
|
+
import { forgetRegistration, hasState, pinRegistration, pinState, readRegistrations, registerProject } from './registry.js';
|
|
9
|
+
import { addRoot, listRoots, readRoots, removeRoot } from './roots.js';
|
|
10
|
+
// -- CONSTANTS -- //
|
|
11
|
+
const BOOLEAN_FLAGS = new Set([
|
|
12
|
+
'--recipe',
|
|
13
|
+
'--remove',
|
|
14
|
+
'--adopt-env',
|
|
15
|
+
'--upload-pictures',
|
|
16
|
+
'--json'
|
|
17
|
+
]);
|
|
18
|
+
// -- FUNCTIONS -- //
|
|
19
|
+
function parse(args) {
|
|
20
|
+
const pos = [];
|
|
21
|
+
const flags = new Map();
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const a = args[i] ?? '';
|
|
24
|
+
if (!a.startsWith('--')) {
|
|
25
|
+
pos.push(a);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const eq = a.indexOf('=');
|
|
29
|
+
if (eq > 0) {
|
|
30
|
+
flags.set(a.slice(0, eq), a.slice(eq + 1));
|
|
31
|
+
}
|
|
32
|
+
else if (BOOLEAN_FLAGS.has(a)) {
|
|
33
|
+
flags.set(a, true);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const next = args[i + 1];
|
|
37
|
+
if (next === undefined || next.startsWith('--'))
|
|
38
|
+
throw new UsageError(`${a} needs a value.`);
|
|
39
|
+
flags.set(a, next);
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { pos, flags };
|
|
44
|
+
}
|
|
45
|
+
const text = (p, name) => {
|
|
46
|
+
const v = p.flags.get(name);
|
|
47
|
+
return typeof v === 'string' ? v : undefined;
|
|
48
|
+
};
|
|
49
|
+
function need(p, name) {
|
|
50
|
+
const v = text(p, name);
|
|
51
|
+
if (!v)
|
|
52
|
+
throw new UsageError(`${name} is needed.`);
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
/** Runs a handler body and turns what it throws into an exit code and one plain line. */
|
|
56
|
+
async function guarded(ctx, body) {
|
|
57
|
+
try {
|
|
58
|
+
return await body();
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
62
|
+
ctx.log(message);
|
|
63
|
+
return e instanceof Refusal ? 1 : 2;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const show = (ctx, value, lines) => {
|
|
67
|
+
if (ctx.json)
|
|
68
|
+
ctx.out(JSON.stringify(value));
|
|
69
|
+
else
|
|
70
|
+
for (const l of lines)
|
|
71
|
+
ctx.out(l);
|
|
72
|
+
};
|
|
73
|
+
export const PROJECT_USAGE = [
|
|
74
|
+
' project <NAME> --org <slug> --repo <dir> [--app-dir web] [--board-project KEY] [--recipe] [--extra-hosts a,b]',
|
|
75
|
+
' project <NAME> --org <slug> --url https://site [--board-project KEY] [--extra-hosts a,b] --approval <code>',
|
|
76
|
+
' project list [--org <slug>] | project forget <NAME> --org <slug> [--remove] | project pin <NAME> --org <slug>'
|
|
77
|
+
].join('\n');
|
|
78
|
+
/** `check project ...`: register, list, forget or pin a registration. */
|
|
79
|
+
export async function projectCommand(args, ctx) {
|
|
80
|
+
return guarded(ctx, async () => {
|
|
81
|
+
const p = parse(args);
|
|
82
|
+
const json = ctx.json || p.flags.has('--json');
|
|
83
|
+
const c = { ...ctx, json };
|
|
84
|
+
const sub = p.pos[0];
|
|
85
|
+
if (!sub)
|
|
86
|
+
throw new UsageError(`Say what to do.\n${PROJECT_USAGE}`);
|
|
87
|
+
if (sub === 'list') {
|
|
88
|
+
const org = text(p, '--org');
|
|
89
|
+
const { registrations, problems } = readRegistrations(org);
|
|
90
|
+
const rows = registrations.map((r) => ({
|
|
91
|
+
org: r.org,
|
|
92
|
+
name: r.name,
|
|
93
|
+
boardProject: r.boardProject,
|
|
94
|
+
kind: r.url ? 'site' : 'repo',
|
|
95
|
+
repo: r.repo ?? null,
|
|
96
|
+
appDir: r.appDir ?? null,
|
|
97
|
+
url: r.url ?? null,
|
|
98
|
+
pin: pinState(r),
|
|
99
|
+
adoptEnv: r.adoptEnv,
|
|
100
|
+
state: hasState(r.org, r.name)
|
|
101
|
+
}));
|
|
102
|
+
show(c, { registrations: rows, problems }, [
|
|
103
|
+
...rows.map((r) => `${r.org}/${r.name} serves ${r.boardProject} ${r.url ?? `${r.repo}${r.appDir ? ` (${r.appDir})` : ''}`} pin ${r.pin}${r.adoptEnv ? ' adopts .env' : ''}${r.state ? ' holds state' : ''}`),
|
|
104
|
+
...(rows.length ? [] : ['No registrations.']),
|
|
105
|
+
...problems
|
|
106
|
+
]);
|
|
107
|
+
return problems.length ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
if (sub === 'forget') {
|
|
110
|
+
const name = p.pos[1];
|
|
111
|
+
if (!name)
|
|
112
|
+
throw new UsageError('forget needs a registration name.');
|
|
113
|
+
const r = forgetRegistration(need(p, '--org'), name, { remove: p.flags.has('--remove') });
|
|
114
|
+
show(c, r, [
|
|
115
|
+
`${r.removed ? 'Removed' : 'Forgot the saved state of'} ${r.name}${r.hadState ? '' : ' (it held none)'}.`
|
|
116
|
+
]);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (sub === 'pin') {
|
|
120
|
+
const name = p.pos[1];
|
|
121
|
+
if (!name)
|
|
122
|
+
throw new UsageError('pin needs a registration name.');
|
|
123
|
+
const r = pinRegistration(need(p, '--org'), name);
|
|
124
|
+
show(c, { ...r, before: r.before?.slice(0, 12) ?? null, after: r.after.slice(0, 12) }, [
|
|
125
|
+
r.changed
|
|
126
|
+
? `Pinned ${r.name}: ${r.before ? r.before.slice(0, 12) : 'none'} -> ${r.after.slice(0, 12)} (${r.files.join(', ') || 'no files'}).`
|
|
127
|
+
: `${r.name} is already pinned (${r.after.slice(0, 12)}).`
|
|
128
|
+
]);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
if (p.pos.length > 1)
|
|
132
|
+
throw new UsageError(`Unexpected "${p.pos[1]}".\n${PROJECT_USAGE}`);
|
|
133
|
+
const hosts = text(p, '--extra-hosts');
|
|
134
|
+
const input = {
|
|
135
|
+
org: need(p, '--org'),
|
|
136
|
+
name: sub,
|
|
137
|
+
...(text(p, '--board-project') ? { boardProject: text(p, '--board-project') } : {}),
|
|
138
|
+
...(text(p, '--repo') ? { repo: text(p, '--repo') } : {}),
|
|
139
|
+
...(text(p, '--app-dir') ? { appDir: text(p, '--app-dir') } : {}),
|
|
140
|
+
...(text(p, '--url') ? { url: text(p, '--url') } : {}),
|
|
141
|
+
...(hosts
|
|
142
|
+
? {
|
|
143
|
+
extraHosts: hosts
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((h) => h.trim())
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
recipe: p.flags.has('--recipe'),
|
|
150
|
+
adoptEnv: p.flags.has('--adopt-env'),
|
|
151
|
+
uploadSignedInPictures: p.flags.has('--upload-pictures')
|
|
152
|
+
};
|
|
153
|
+
const approvals = {
|
|
154
|
+
...(text(p, '--approval') ? { register: text(p, '--approval') } : {}),
|
|
155
|
+
...(text(p, '--adopt-env-approval')
|
|
156
|
+
? { adoptEnv: text(p, '--adopt-env-approval') }
|
|
157
|
+
: {}),
|
|
158
|
+
...(text(p, '--pictures-approval')
|
|
159
|
+
? { pictures: text(p, '--pictures-approval') }
|
|
160
|
+
: {})
|
|
161
|
+
};
|
|
162
|
+
const reg = registerProject(input, approvals);
|
|
163
|
+
show(c, { org: reg.org, name: reg.name, boardProject: reg.boardProject, database: reg.database }, [
|
|
164
|
+
`Registered ${reg.org}/${reg.name} for ${reg.boardProject}${reg.url ? ` (live site ${new URL(reg.url).hostname})` : ''}. The files that decide what runs are pinned by "check project pin ${reg.name} --org ${reg.org}" or the first run in the folder.`
|
|
165
|
+
]);
|
|
166
|
+
return 0;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `check approve --action <register|secrets|adopt-env|roots|pictures> --target <name or folder>`: the owner's single-use code. Refused without an interactive terminal.
|
|
171
|
+
* `env.isTTY` is for the tests; the command itself passes the real flag of the terminal.
|
|
172
|
+
*/
|
|
173
|
+
export async function approveCommand(args, ctx, env = {}) {
|
|
174
|
+
return guarded(ctx, () => {
|
|
175
|
+
const p = parse(args);
|
|
176
|
+
const action = need(p, '--action');
|
|
177
|
+
if (!isApprovalAction(action))
|
|
178
|
+
throw new UsageError(`--action is one of ${APPROVAL_ACTIONS.join(', ')}.`);
|
|
179
|
+
const target = need(p, '--target');
|
|
180
|
+
const isTTY = env.isTTY ?? (isatty(0) && isatty(1));
|
|
181
|
+
const made = mintApproval(action, target, { isTTY });
|
|
182
|
+
show(ctx, { action, target, code: made.code, expiresAt: made.expiresAt }, [
|
|
183
|
+
`Approval code for ${action} on ${target}: ${made.code}`,
|
|
184
|
+
`It works once and expires at ${made.expiresAt}. Give it to the command that asked for it, with --approval.`
|
|
185
|
+
]);
|
|
186
|
+
return 0;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** `check roots add <dir> --approval <code> | list | remove <dir> --approval <code>`. */
|
|
190
|
+
export async function rootsCommand(args, ctx) {
|
|
191
|
+
return guarded(ctx, () => {
|
|
192
|
+
const p = parse(args);
|
|
193
|
+
const sub = p.pos[0] ?? 'list';
|
|
194
|
+
if (sub === 'list') {
|
|
195
|
+
const { roots, problem } = readRoots();
|
|
196
|
+
show(ctx, { roots, problem }, [
|
|
197
|
+
...(roots.length
|
|
198
|
+
? roots
|
|
199
|
+
: ['No roots. The owner adds one: check roots add <folder> --approval <code>.']),
|
|
200
|
+
...(problem ? [problem] : [])
|
|
201
|
+
]);
|
|
202
|
+
return problem ? 1 : 0;
|
|
203
|
+
}
|
|
204
|
+
const dir = p.pos[1];
|
|
205
|
+
if (!dir)
|
|
206
|
+
throw new UsageError(`${sub} needs a folder.`);
|
|
207
|
+
if (sub === 'add') {
|
|
208
|
+
const real = addRoot(dir, text(p, '--approval'));
|
|
209
|
+
show(ctx, { root: real, roots: listRoots() }, [`Root added: ${real}`]);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (sub === 'remove') {
|
|
213
|
+
const real = removeRoot(dir, text(p, '--approval'));
|
|
214
|
+
show(ctx, { root: real, roots: listRoots() }, [`Root removed: ${real}`]);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
throw new UsageError('Use: roots add <dir> --approval <code> | roots list | roots remove <dir> --approval <code>');
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// THE BOARDS THIS MACHINE IS CONNECTED TO FOR CHECKS: SERVER AND ORGANISATION ONLY, NEVER A TOKEN (THE TOKEN IS IN THE TOKEN STORE)
|
|
2
2
|
// `check watch` re-reads this list on every loop, so a `check connect` for a second organisation is picked up without a restart.
|
|
3
3
|
// -- DEP-MODULES -- //
|
|
4
|
-
import { existsSync, readFileSync,
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
// -- MODULES -- //
|
|
7
7
|
import { checksHome, ensureDir, ORG_SLUG } from './paths.js';
|
|
8
|
+
import { renameRetry } from './fsretry.js';
|
|
8
9
|
// -- FUNCTIONS -- //
|
|
9
10
|
const file = () => join(checksHome(), 'connections.json');
|
|
10
11
|
export function listConnections() {
|
|
@@ -30,12 +31,12 @@ export function recordConnection(c) {
|
|
|
30
31
|
ensureDir(checksHome());
|
|
31
32
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
32
33
|
writeFileSync(tmp, JSON.stringify(next, null, 1), { mode: 0o600 });
|
|
33
|
-
|
|
34
|
+
renameRetry(tmp, file());
|
|
34
35
|
}
|
|
35
36
|
export function forgetConnection(server, org) {
|
|
36
37
|
const next = listConnections().filter((x) => !(x.server === server && x.org === org));
|
|
37
38
|
ensureDir(checksHome());
|
|
38
39
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
39
40
|
writeFileSync(tmp, JSON.stringify(next, null, 1), { mode: 0o600 });
|
|
40
|
-
|
|
41
|
+
renameRetry(tmp, file());
|
|
41
42
|
}
|