uc-config 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { writeFile, mkdir } from "node:fs/promises";
4
+ import { resolve, join } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { Writable } from "node:stream";
7
+ import { CoreClient, resolveSecrets } from "./client.js";
8
+ import { Adapter } from "./adapter.js";
9
+ import { compile, validateConfig } from "./compiler.js";
10
+ import { Engine, PendingSetup } from "./engine.js";
11
+ import { formatPlan, makePlan } from "./planner.js";
12
+ import { inventory, importConfig, generateBindings } from "./inventory.js";
13
+ import { emptyState, } from "./model.js";
14
+ import { readJson, saveJson, redact, withLock, isObject } from "./util.js";
15
+ import { rollbackPlan } from "./recovery.js";
16
+ import { validateRequest } from "./schema.js";
17
+ import { diagnose, formatDiagnosis } from "./diagnose.js";
18
+ import { init } from "./init.js";
19
+ import { readFileSync, readdirSync } from "node:fs";
20
+ const pkgVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
21
+ const program = new Command()
22
+ .name("uc-config")
23
+ .description("TypeScript configuration and provisioning for Unfolded Circle Remote 3")
24
+ .version(pkgVersion)
25
+ .option("--workspace <path>", "project directory", process.cwd());
26
+ // UC_TARGET, else the workspace's only target, else "home".
27
+ function implicitTarget() {
28
+ if (process.env.UC_TARGET)
29
+ return process.env.UC_TARGET;
30
+ const ws = process.argv.indexOf("--workspace");
31
+ const dir = resolve(ws > 0 ? process.argv[ws + 1] : process.cwd());
32
+ try {
33
+ const names = readdirSync(join(dir, ".uc", "targets"))
34
+ .filter((f) => f.endsWith(".json"))
35
+ .map((f) => f.slice(0, -5));
36
+ if (names.length === 1)
37
+ return names[0];
38
+ }
39
+ catch {
40
+ // no targets yet
41
+ }
42
+ return "home";
43
+ }
44
+ const defaultTarget = implicitTarget();
45
+ const root = () => resolve(program.opts().workspace);
46
+ const local = (name) => join(root(), ".uc", name);
47
+ const targetName = (name) => {
48
+ if (!/^[a-zA-Z0-9_-]+$/.test(name))
49
+ throw new Error("Invalid target name");
50
+ return name;
51
+ };
52
+ const targetFile = (name) => local(`targets/${targetName(name)}.json`);
53
+ const stateFile = (name) => local(`state/${targetName(name)}.json`);
54
+ const journalFile = (name) => local(`journals/${targetName(name)}.json`);
55
+ async function load(name) {
56
+ let target;
57
+ try {
58
+ target = await readJson(targetFile(name));
59
+ }
60
+ catch (e) {
61
+ if (e.code === "ENOENT")
62
+ throw new Error(`No target "${name}" in this workspace. Run: uc-config connect ${name} --host http://<REMOTE_IP>`);
63
+ throw e;
64
+ }
65
+ let token = process.env[target.tokenEnv];
66
+ if (!token) {
67
+ try {
68
+ token = (await readJson(local("credentials.json")))[target.identity];
69
+ }
70
+ catch (e) {
71
+ if (e.code !== "ENOENT")
72
+ throw e;
73
+ }
74
+ }
75
+ const client = new CoreClient(target.host, token);
76
+ let state;
77
+ try {
78
+ state = await readJson(stateFile(name));
79
+ }
80
+ catch (e) {
81
+ if (e.code !== "ENOENT")
82
+ throw e;
83
+ state = emptyState(target.identity);
84
+ }
85
+ const adapter = new Adapter(client);
86
+ const engine = new Engine(adapter, {
87
+ save: (s) => saveJson(stateFile(name), s),
88
+ journal: async (j) => {
89
+ await saveJson(local(`journals/history/${targetName(name)}-${j.planDigest}.json`), j);
90
+ await saveJson(journalFile(name), j);
91
+ },
92
+ });
93
+ return { target, client, state, adapter, engine };
94
+ }
95
+ async function hiddenPrompt(label) {
96
+ if (!process.stdin.isTTY)
97
+ throw new Error("Use UC_PIN or run auth in an interactive terminal");
98
+ let muted = false;
99
+ const sink = new Writable({
100
+ write(chunk, _encoding, callback) {
101
+ if (!muted)
102
+ process.stdout.write(chunk);
103
+ callback();
104
+ },
105
+ });
106
+ const rl = createInterface({
107
+ input: process.stdin,
108
+ output: sink,
109
+ terminal: true,
110
+ });
111
+ process.stdout.write(label);
112
+ muted = true;
113
+ try {
114
+ return await rl.question("");
115
+ }
116
+ finally {
117
+ rl.close();
118
+ process.stdout.write("\n");
119
+ }
120
+ }
121
+ program
122
+ .command("connect <name>")
123
+ .requiredOption("--host <url>")
124
+ .option("--token-env <name>", "API key environment variable", "UC_API_KEY")
125
+ .action(async (name, o) => {
126
+ const client = new CoreClient(o.host);
127
+ const version = await client.version();
128
+ if (version.model !== "UCR3" || typeof version.address !== "string")
129
+ throw new Error("Expected an identifiable Remote 3");
130
+ const target = {
131
+ host: client.base.origin,
132
+ identity: version.address,
133
+ version,
134
+ tokenEnv: o.tokenEnv,
135
+ };
136
+ try {
137
+ const old = await readJson(targetFile(name));
138
+ if (old.identity !== target.identity)
139
+ throw new Error("Target name already refers to another remote; use a new name");
140
+ }
141
+ catch (e) {
142
+ if (e.code !== "ENOENT")
143
+ throw e;
144
+ }
145
+ await saveJson(targetFile(name), target);
146
+ console.log(`Connected ${name}: ${version.model}, core ${version.core}, API ${version.api}. No configuration changed.`);
147
+ });
148
+ program
149
+ .command("auth")
150
+ .option("--target <name>", "target", defaultTarget)
151
+ .action(async (o) => {
152
+ const { target } = await load(o.target);
153
+ const pin = process.env.UC_PIN ?? (await hiddenPrompt("Web configurator PIN: "));
154
+ const client = new CoreClient(target.host);
155
+ validateRequest("/auth/api_keys", "POST", {
156
+ name: "uc-config",
157
+ scopes: ["admin"],
158
+ });
159
+ const { data } = await client.request("POST", "/auth/api_keys", { name: "uc-config", scopes: ["admin"] }, {
160
+ Authorization: `Basic ${Buffer.from(`web-configurator:${pin}`).toString("base64")}`,
161
+ });
162
+ if (typeof data.api_key !== "string")
163
+ throw new Error("No API key returned");
164
+ let credentials = {};
165
+ try {
166
+ credentials = await readJson(local("credentials.json"));
167
+ }
168
+ catch (e) {
169
+ if (e.code !== "ENOENT")
170
+ throw e;
171
+ }
172
+ credentials[target.identity] = data.api_key;
173
+ await saveJson(local("credentials.json"), credentials);
174
+ console.log("API key saved in .uc/credentials.json (mode 0600, gitignored). Approve it on the remote if requested, then run doctor.");
175
+ });
176
+ program
177
+ .command("doctor")
178
+ .option("--target <name>", "target", defaultTarget)
179
+ .action(async (o) => {
180
+ const { client, target } = await load(o.target);
181
+ await client.verifyTarget(target);
182
+ console.log(`Identity verified; core ${target.version.core}, reported API ${target.version.api}`);
183
+ for (const path of [
184
+ "/entities",
185
+ "/activities",
186
+ "/intg/drivers",
187
+ "/intg/instances",
188
+ "/intg/setup",
189
+ "/docks",
190
+ "/profiles",
191
+ "/cfg/device/button_layout",
192
+ "/cfg/device/screen_layout",
193
+ ]) {
194
+ try {
195
+ await client.get(path);
196
+ console.log(`OK ${path}`);
197
+ }
198
+ catch (e) {
199
+ console.log(`FAIL ${e.message}`);
200
+ process.exitCode = 1;
201
+ }
202
+ }
203
+ });
204
+ program
205
+ .command("inventory")
206
+ .option("--target <name>", "target", defaultTarget)
207
+ .option("--out <file>", "JSON output", ".uc/inventory.json")
208
+ .option("--bindings <file>", "TypeScript device bindings")
209
+ .action(async (o) => {
210
+ const { client } = await load(o.target);
211
+ const inv = await inventory(client);
212
+ await saveJson(resolve(root(), o.out), redact(inv));
213
+ if (o.bindings)
214
+ await saveText(o.bindings, generateBindings(inv));
215
+ console.log(`Inventory written to ${o.out}${inv.unsupported.length ? `; ${inv.unsupported.length} unavailable endpoints` : ""}`);
216
+ });
217
+ async function saveText(file, contents) {
218
+ const dest = resolve(root(), file);
219
+ await mkdir(resolve(dest, ".."), { recursive: true });
220
+ await writeFile(dest, contents, { mode: 0o600, flag: "wx" });
221
+ }
222
+ program
223
+ .command("import")
224
+ .option("--target <name>", "target", defaultTarget)
225
+ .option("--out <file>", "TypeScript output", "remote.config.ts")
226
+ .action(async (o) => {
227
+ const { client } = await load(o.target);
228
+ const { config, warnings } = await importConfig(client);
229
+ validateConfig(config);
230
+ await saveText(o.out, `import { defineRemote } from 'uc-config';\n\nexport default defineRemote(${JSON.stringify(config, null, 2)});\n`);
231
+ await saveJson(local("import-warnings.json"), warnings);
232
+ console.log(`Wrote ${o.out}; no ownership or remote changes.\n${warnings.map((w) => `- ${w}`).join("\n")}`);
233
+ });
234
+ program
235
+ .command("compile")
236
+ .description("Compile TypeScript configuration into a local JSON snapshot")
237
+ .option("--config <file>", "TypeScript source", "remote.config.ts")
238
+ .option("--out <file>", "IR output", ".uc/build.json")
239
+ .action(async (o) => {
240
+ const config = await compile(resolve(root(), o.config));
241
+ await saveJson(resolve(root(), o.out), config);
242
+ console.log(`Compiled ${Object.keys(config.resources).length} resources to ${o.out}`);
243
+ });
244
+ program
245
+ .command("plan")
246
+ .option("--target <name>", "target", defaultTarget)
247
+ .option("--build <file>", "compiled IR", ".uc/build.json")
248
+ .option("--out <file>", "saved plan", ".uc/plan.json")
249
+ .option("--overwrite-drift", "explicitly replace edits to managed fields")
250
+ .option("--prune", "delete removed, previously managed resources after checking references")
251
+ .action(async (o) => {
252
+ const { target, state, adapter } = await load(o.target);
253
+ const config = await readJson(resolve(root(), o.build));
254
+ const plan = await makePlan(config, target, state, adapter, {
255
+ overwriteDrift: !!o.overwriteDrift,
256
+ prune: !!o.prune,
257
+ });
258
+ await saveJson(resolve(root(), o.out), plan);
259
+ console.log(formatPlan(plan));
260
+ if (plan.conflicts.length)
261
+ process.exitCode = 2;
262
+ });
263
+ program
264
+ .command("apply <plan>")
265
+ .option("--target <name>", "target", defaultTarget)
266
+ .option("--adopt-only", "record existing configuration locally; refuse any remote mutations")
267
+ .action(async (file, o) => {
268
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
269
+ const { target, state, engine } = await load(o.target);
270
+ const plan = await readJson(resolve(root(), file));
271
+ if (o.adoptOnly && plan.operations.some((op) => op.action !== "adopt"))
272
+ throw new Error("Plan is not adoption-only; no changes made");
273
+ if (plan.target.identity !== target.identity ||
274
+ plan.target.host !== target.host)
275
+ throw new Error("Plan target differs from selected target");
276
+ try {
277
+ const journal = await readJson(journalFile(o.target));
278
+ if (journal.status === "failed" || journal.status === "running")
279
+ throw new Error("Previous apply is unresolved; run resume and inspect journal before applying again");
280
+ }
281
+ catch (e) {
282
+ if (e.code !== "ENOENT")
283
+ throw e;
284
+ }
285
+ console.log(formatPlan(plan));
286
+ await engine.apply(plan, state);
287
+ console.log(`Applied and verified. ${plan.deferred.length ? "Replan deferred resources." : "Run check to confirm convergence."}`);
288
+ });
289
+ });
290
+ program
291
+ .command("check")
292
+ .option("--target <name>", "target", defaultTarget)
293
+ .option("--build <file>", "compiled IR", ".uc/build.json")
294
+ .action(async (o) => {
295
+ const { target, state, adapter } = await load(o.target);
296
+ const plan = await makePlan(await readJson(resolve(root(), o.build)), target, state, adapter);
297
+ console.log(formatPlan(plan));
298
+ if (plan.operations.length || plan.conflicts.length || plan.deferred.length)
299
+ process.exitCode = 2;
300
+ });
301
+ program
302
+ .command("resume")
303
+ .option("--target <name>", "target", defaultTarget)
304
+ .action(async (o) => {
305
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
306
+ const { target, state, client, engine } = await load(o.target);
307
+ await client.verifyTarget(target);
308
+ console.log((await engine.resume(state)).join("\n") ||
309
+ "No pending setup processes.");
310
+ let journal;
311
+ try {
312
+ journal = await readJson(journalFile(o.target));
313
+ }
314
+ catch (e) {
315
+ if (e.code === "ENOENT") {
316
+ console.log("Compile and plan the next phase.");
317
+ return;
318
+ }
319
+ throw e;
320
+ }
321
+ const uncertain = journal.entries.filter((e) => (e.status === "intent" || e.status === "created") &&
322
+ !state.bindings[e.key] &&
323
+ !state.setups[e.key]);
324
+ if (uncertain.length)
325
+ throw new Error(`Uncertain writes: ${uncertain.map((e) => e.key).join(", ")}. Inspect inventory and use state adopt with the exact resulting id; no automatic retry.`);
326
+ journal.status = Object.keys(state.setups).length ? "paused" : "complete";
327
+ await saveJson(journalFile(o.target), journal);
328
+ console.log("Saved progress retained. Compile and plan to reconcile remaining configuration.");
329
+ });
330
+ });
331
+ const setup = program.command("setup");
332
+ setup
333
+ .command("status <key>")
334
+ .option("--target <name>", "target", defaultTarget)
335
+ .action(async (key, o) => {
336
+ const { state, client } = await load(o.target);
337
+ const s = state.setups[key];
338
+ if (!s)
339
+ throw new Error("No pending setup for this key");
340
+ const info = await client.get(`/${s.kind === "integration" ? "intg" : "docks"}/setup/${encodeURIComponent(s.id)}`);
341
+ console.log(JSON.stringify(redact(info), null, 2));
342
+ });
343
+ setup
344
+ .command("respond <key>")
345
+ .option("--target <name>", "target", defaultTarget)
346
+ .option("--input <file>", "JSON request body; use secret references")
347
+ .option("--confirm", "confirm requested physical action")
348
+ .action(async (key, o) => {
349
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
350
+ const { state, client, target } = await load(o.target);
351
+ await client.verifyTarget(target);
352
+ const s = state.setups[key];
353
+ if (!s)
354
+ throw new Error("No pending setup");
355
+ if (!!o.input === !!o.confirm)
356
+ throw new Error("Provide exactly one of --input or --confirm");
357
+ const body = o.confirm
358
+ ? { confirm: true }
359
+ : await resolveSecrets(await readJson(resolve(root(), o.input)));
360
+ const template = s.kind === "integration"
361
+ ? "/intg/setup/{driverId}"
362
+ : "/docks/setup/{dockId}";
363
+ validateRequest(template, "PUT", body);
364
+ await client.request("PUT", `/${s.kind === "integration" ? "intg" : "docks"}/setup/${encodeURIComponent(s.id)}`, body);
365
+ console.log("Response submitted. Run resume.");
366
+ });
367
+ });
368
+ const pair = program.command("pairing");
369
+ pair
370
+ .command("status <remoteId>")
371
+ .option("--target <name>", "target", defaultTarget)
372
+ .action(async (id, o) => {
373
+ const { client } = await load(o.target);
374
+ console.log(JSON.stringify(redact(await client.get(`/remotes/${encodeURIComponent(id)}/bt/pairing`)), null, 2));
375
+ });
376
+ pair
377
+ .command("respond <remoteId>")
378
+ .option("--target <name>", "target", defaultTarget)
379
+ .requiredOption("--input <file>", "Pairing response JSON; passkey can use secret() JSON reference")
380
+ .action(async (id, o) => {
381
+ const { client, target } = await load(o.target);
382
+ await client.verifyTarget(target);
383
+ const body = await resolveSecrets(await readJson(resolve(root(), o.input)));
384
+ validateRequest("/remotes/{entityId}/bt/pairing", "POST", body);
385
+ await client.request("POST", `/remotes/${encodeURIComponent(id)}/bt/pairing`, body);
386
+ console.log("Pairing response submitted. Replan to verify pairing.");
387
+ });
388
+ const stateCmd = program.command("state");
389
+ stateCmd
390
+ .command("adopt <key> <id>")
391
+ .option("--target <name>", "target", defaultTarget)
392
+ .option("--build <file>", "compiled IR", ".uc/build.json")
393
+ .action(async (key, id, o) => {
394
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
395
+ const { state, adapter, target } = await load(o.target);
396
+ await adapter.client.verifyTarget(target);
397
+ if (state.bindings[key])
398
+ throw new Error("Key already bound; use state move or forget first");
399
+ const config = await readJson(resolve(root(), o.build));
400
+ const r = config.resources[key];
401
+ if (!r)
402
+ throw new Error("Unknown configuration key");
403
+ const obs = await adapter.observe({ ...r, id }, state, key);
404
+ if (!obs.value)
405
+ throw new Error("No such remote resource");
406
+ state.bindings[key] = { id, resource: r, baseline: {} };
407
+ delete state.setups[key];
408
+ state.revision++;
409
+ await saveJson(stateFile(o.target), state);
410
+ console.log("Identity adopted; no remote changes. Resume, then plan field ownership.");
411
+ });
412
+ });
413
+ stateCmd
414
+ .command("forget <key>")
415
+ .option("--target <name>", "target", defaultTarget)
416
+ .action(async (key, o) => {
417
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
418
+ const { state } = await load(o.target);
419
+ if (!state.bindings[key])
420
+ throw new Error("Unknown managed key");
421
+ delete state.bindings[key];
422
+ state.revision++;
423
+ await saveJson(stateFile(o.target), state);
424
+ console.log("Ownership removed. Remote resource preserved.");
425
+ });
426
+ });
427
+ stateCmd
428
+ .command("move <from> <to>")
429
+ .option("--target <name>", "target", defaultTarget)
430
+ .action(async (from, to, o) => {
431
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
432
+ const { state } = await load(o.target);
433
+ if (!state.bindings[from] ||
434
+ state.bindings[to] ||
435
+ !/^\w[\w.-]*$/.test(to))
436
+ throw new Error("Invalid source/destination key");
437
+ state.bindings[to] = state.bindings[from];
438
+ delete state.bindings[from];
439
+ const rewrite = (x) => Array.isArray(x)
440
+ ? x.map(rewrite)
441
+ : isObject(x)
442
+ ? Object.fromEntries(Object.entries(x).map(([k, v]) => [
443
+ k,
444
+ k === "$ref" && v === from ? to : rewrite(v),
445
+ ]))
446
+ : x;
447
+ state.bindings = rewrite(state.bindings);
448
+ state.revision++;
449
+ await saveJson(stateFile(o.target), state);
450
+ console.log("State key moved; update source references and recompile.");
451
+ });
452
+ });
453
+ program
454
+ .command("rollback")
455
+ .option("--target <name>", "target", defaultTarget)
456
+ .option("--journal <file>", "journal to compensate; defaults to latest")
457
+ .option("--out <file>", "rollback plan", ".uc/rollback-plan.json")
458
+ .action(async (o) => {
459
+ const { target, state, adapter } = await load(o.target);
460
+ const journal = await readJson(o.journal ? resolve(root(), o.journal) : journalFile(o.target));
461
+ const plan = await rollbackPlan(journal, target, state, adapter);
462
+ await saveJson(resolve(root(), o.out), plan);
463
+ console.log(formatPlan(plan));
464
+ console.log("Rollback plan saved; review and apply explicitly. Source must also be reverted to retain the rollback.");
465
+ if (plan.conflicts.length)
466
+ process.exitCode = 2;
467
+ });
468
+ program
469
+ .command("backup")
470
+ .option("--target <name>", "target", defaultTarget)
471
+ .requiredOption("--out <file>", "native backup archive")
472
+ .action(async (o) => {
473
+ await withLock(local(`locks/${targetName(o.target)}.lock`), async () => {
474
+ const { client, target } = await load(o.target);
475
+ await client.verifyTarget(target);
476
+ console.log("Exporting native backup: the remote temporarily stops integrations and docks. Keep the archive private; it may contain credentials.");
477
+ const bytes = await client.download("/system/backup/export");
478
+ const file = resolve(root(), o.out);
479
+ await mkdir(resolve(file, ".."), { recursive: true });
480
+ await writeFile(file, bytes, { mode: 0o600, flag: "wx" });
481
+ console.log(`Backup saved to ${o.out}`);
482
+ });
483
+ });
484
+ const ir = program.command("ir");
485
+ ir.command("learn <emitterId>")
486
+ .option("--target <name>", "target", defaultTarget)
487
+ .option("--timeout <seconds>", "learning window", "60")
488
+ .action(async (id, o) => {
489
+ const seconds = Number(o.timeout);
490
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
491
+ throw new Error("Timeout must be 1–300 seconds");
492
+ const { client, target } = await load(o.target);
493
+ await client.verifyTarget(target);
494
+ await client.request("PUT", `/ir/emitters/${encodeURIComponent(id)}/learn?timeout=${seconds}`);
495
+ console.log("Learning started. Press the original remote button, then run ir capture.");
496
+ });
497
+ ir.command("capture <emitterId>")
498
+ .option("--target <name>", "target", defaultTarget)
499
+ .requiredOption("--out <file>", "learned code JSON")
500
+ .action(async (id, o) => {
501
+ const { client } = await load(o.target);
502
+ const codes = await client.get(`/ir/emitters/${encodeURIComponent(id)}/learn`);
503
+ await saveJson(resolve(root(), o.out), codes);
504
+ console.log("Captured learning status/codes. Assign chosen codes to irCode() resources.");
505
+ });
506
+ program
507
+ .command("init")
508
+ .description("Scaffold a private config workspace (package.json, tsconfig, .gitignore, AGENTS.md)")
509
+ .action(async () => {
510
+ const r = await init(root(), pkgVersion);
511
+ for (const f of r.written)
512
+ console.log(`created ${f}`);
513
+ if (r.packageJsonUpdated)
514
+ console.log("added uc-config to package.json");
515
+ for (const f of r.skipped)
516
+ console.log(`kept existing ${f}`);
517
+ console.log("Next: npm install, then start your coding agent here and ask it to set up your Remote 3.");
518
+ });
519
+ program
520
+ .command("diagnose")
521
+ .description("Read-only health check: orphaned entity references and disconnected integrations")
522
+ .option("--target <name>", "target", defaultTarget)
523
+ .option("--json", "machine-readable output")
524
+ .action(async (o) => {
525
+ const { client, target } = await load(o.target);
526
+ await client.verifyTarget(target);
527
+ const result = await diagnose(client);
528
+ console.log(o.json ? JSON.stringify(result, null, 2) : formatDiagnosis(result));
529
+ if (result.orphans.length || result.problems.length)
530
+ process.exitCode = 2;
531
+ });
532
+ program
533
+ .command("api <method> <path>")
534
+ .description("Raw authenticated Core API request (path relative to /api). Non-GET requires --write.")
535
+ .option("--target <name>", "target", defaultTarget)
536
+ .option("--data <json>", "JSON request body")
537
+ .option("--write", "allow a non-GET request")
538
+ .action(async (method, path, o) => {
539
+ const verb = method.toUpperCase();
540
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(verb))
541
+ throw new Error(`Unsupported method ${method}`);
542
+ if (verb !== "GET" && !o.write)
543
+ throw new Error(`${verb} changes the remote; rerun with --write once intended`);
544
+ const { client, target } = await load(o.target);
545
+ await client.verifyTarget(target);
546
+ const body = o.data === undefined ? undefined : JSON.parse(o.data);
547
+ const { data } = await client.request(verb, path, body);
548
+ console.log(JSON.stringify(redact(data), null, 2));
549
+ });
550
+ program.parseAsync().catch((e) => {
551
+ console.error(e instanceof PendingSetup
552
+ ? `Paused: ${e.message}`
553
+ : `Error: ${e.message}`);
554
+ process.exitCode = e instanceof PendingSetup ? 3 : 1;
555
+ });
@@ -0,0 +1,25 @@
1
+ import type { ObjectValue, Target, Version } from "./model.js";
2
+ export declare class ApiError extends Error {
3
+ status: number;
4
+ method: string;
5
+ path: string;
6
+ constructor(status: number, method: string, path: string);
7
+ }
8
+ export declare class CoreClient {
9
+ private token?;
10
+ private transport;
11
+ private timeout;
12
+ readonly base: URL;
13
+ constructor(host: string, token?: string | undefined, transport?: typeof fetch, timeout?: number);
14
+ request<T = ObjectValue>(method: string, path: string, body?: unknown, extra?: Record<string, string>): Promise<{
15
+ data: T;
16
+ headers: Headers;
17
+ }>;
18
+ get<T = ObjectValue>(path: string): Promise<T>;
19
+ maybe(path: string): Promise<ObjectValue | null>;
20
+ list(path: string): Promise<ObjectValue[]>;
21
+ download(path: string): Promise<Uint8Array>;
22
+ version(): Promise<Version>;
23
+ verifyTarget(target: Target): Promise<void>;
24
+ }
25
+ export declare function resolveSecrets<T>(v: T): Promise<T>;