@theholocron/cli 2.0.0-alpha.66 → 2.0.0-alpha.68
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.mjs +180 -118
- package/package.json +3 -1
package/dist/cli.mjs
CHANGED
|
@@ -5,8 +5,10 @@ import yargs from "yargs";
|
|
|
5
5
|
import { hideBin } from "yargs/helpers";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
import { stdin, stdout } from "node:process";
|
|
8
|
-
import { ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
|
|
8
|
+
import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
|
|
9
9
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import chalk from "chalk";
|
|
10
12
|
import { createHash } from "node:crypto";
|
|
11
13
|
import { createGitHubClient } from "@theholocron/github-client";
|
|
12
14
|
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
@@ -221,6 +223,35 @@ function listStoredProviders() {
|
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
//#endregion
|
|
226
|
+
//#region src/ui/progress.ts
|
|
227
|
+
/**
|
|
228
|
+
* Runs `fn`, showing an ora spinner for its duration in TTY environments.
|
|
229
|
+
* In non-TTY environments (CI, pipes, tests) the spinner is skipped entirely.
|
|
230
|
+
*/
|
|
231
|
+
async function withSpinner(label, fn) {
|
|
232
|
+
if (!process.stdout.isTTY) return fn();
|
|
233
|
+
const spinner = ora(label).start();
|
|
234
|
+
try {
|
|
235
|
+
const result = await fn();
|
|
236
|
+
spinner.succeed();
|
|
237
|
+
return result;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
spinner.fail();
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/ui/style.ts
|
|
245
|
+
const style = {
|
|
246
|
+
success: (msg) => `${chalk.green("✓")} ${msg}`,
|
|
247
|
+
warn: (msg) => `${chalk.yellow("⚠")} ${msg}`,
|
|
248
|
+
fail: (msg) => `${chalk.red("✗")} ${msg}`,
|
|
249
|
+
step: (msg) => `${chalk.cyan("→")} ${msg}`,
|
|
250
|
+
hint: (msg) => chalk.dim(msg),
|
|
251
|
+
dim: (msg) => chalk.dim(msg),
|
|
252
|
+
header: (msg) => chalk.bold(msg)
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
224
255
|
//#region src/commands/auth.ts
|
|
225
256
|
/**
|
|
226
257
|
* `holocron auth <subcommand>` — manage bootstrap credentials in the
|
|
@@ -263,11 +294,11 @@ async function runAuthSet(input) {
|
|
|
263
294
|
});
|
|
264
295
|
const packageName = resolvePluginPackage(provider);
|
|
265
296
|
if (!token) {
|
|
266
|
-
print(`no token supplied for \`${provider}\`.`);
|
|
267
|
-
print(` pass as positional arg: holocron auth set ${provider} <token>`);
|
|
268
|
-
print(` or via env: HOLOCRON_${provider.toUpperCase()}_TOKEN / ${provider.toUpperCase()}_TOKEN`);
|
|
297
|
+
print(style.fail(`no token supplied for \`${provider}\`.`));
|
|
298
|
+
print(style.hint(` pass as positional arg: holocron auth set ${provider} <token>`));
|
|
299
|
+
print(style.hint(` or via env: HOLOCRON_${provider.toUpperCase()}_TOKEN / ${provider.toUpperCase()}_TOKEN`));
|
|
269
300
|
const hint = await tryLoadHint(importer, packageName);
|
|
270
|
-
if (hint) print(` hint: ${hint}`);
|
|
301
|
+
if (hint) print(style.hint(` hint: ${hint}`));
|
|
271
302
|
return {
|
|
272
303
|
status: "fail",
|
|
273
304
|
message: "no token supplied"
|
|
@@ -279,27 +310,28 @@ async function runAuthSet(input) {
|
|
|
279
310
|
if (typeof module.verifyToken === "function") {
|
|
280
311
|
const verified = await module.verifyToken(token);
|
|
281
312
|
if (!verified.ok) {
|
|
282
|
-
print(`token rejected by ${provider}: ${verified.message}`);
|
|
283
|
-
if (module.AUTH_HINT) print(` hint: ${module.AUTH_HINT}`);
|
|
313
|
+
print(style.fail(`token rejected by ${provider}: ${verified.message}`));
|
|
314
|
+
if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
|
|
284
315
|
return {
|
|
285
316
|
status: "fail",
|
|
286
317
|
message: verified.message
|
|
287
318
|
};
|
|
288
319
|
}
|
|
289
320
|
subject = verified.subject;
|
|
290
|
-
} else print(
|
|
321
|
+
} else print(style.warn(`${provider} plugin has no verifyToken; storing without verification`));
|
|
291
322
|
} catch (err) {
|
|
292
|
-
|
|
293
|
-
print(`
|
|
323
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
324
|
+
print(style.warn(`cannot verify token — failed to load ${packageName}: ${msg}`));
|
|
325
|
+
print(style.hint(` storing token anyway; run 'holocron auth check ${provider}' once the plugin is installed`));
|
|
294
326
|
}
|
|
295
327
|
if (!setToken(provider, token)) {
|
|
296
|
-
print(`keyring unavailable — token not stored. Use env vars instead.`);
|
|
328
|
+
print(style.fail(`keyring unavailable — token not stored. Use env vars instead.`));
|
|
297
329
|
return {
|
|
298
330
|
status: "fail",
|
|
299
331
|
message: "keyring unavailable"
|
|
300
332
|
};
|
|
301
333
|
}
|
|
302
|
-
print(`stored ${provider} token${subject ? ` (${subject})` : ""}`);
|
|
334
|
+
print(style.success(`stored ${provider} token${subject ? ` (${subject})` : ""}`));
|
|
303
335
|
return {
|
|
304
336
|
status: "ok",
|
|
305
337
|
...subject ? { message: subject } : {}
|
|
@@ -308,10 +340,10 @@ async function runAuthSet(input) {
|
|
|
308
340
|
function runAuthUnset(input) {
|
|
309
341
|
const print = input.print ?? ((l) => console.log(l));
|
|
310
342
|
if (deleteToken(input.provider)) {
|
|
311
|
-
print(`removed ${input.provider} token`);
|
|
343
|
+
print(style.success(`removed ${input.provider} token`));
|
|
312
344
|
return { status: "ok" };
|
|
313
345
|
}
|
|
314
|
-
print(`no stored token for ${input.provider}`);
|
|
346
|
+
print(style.dim(`no stored token for ${input.provider}`));
|
|
315
347
|
return {
|
|
316
348
|
status: "skip",
|
|
317
349
|
message: "nothing to remove"
|
|
@@ -323,7 +355,7 @@ async function runAuthCheck(input) {
|
|
|
323
355
|
const { provider } = input;
|
|
324
356
|
const token = getToken(provider);
|
|
325
357
|
if (!token) {
|
|
326
|
-
print(`no stored token for ${provider}`);
|
|
358
|
+
print(style.dim(`no stored token for ${provider}`));
|
|
327
359
|
return {
|
|
328
360
|
status: "skip",
|
|
329
361
|
message: "no stored token"
|
|
@@ -333,29 +365,30 @@ async function runAuthCheck(input) {
|
|
|
333
365
|
try {
|
|
334
366
|
const module = await importer(packageName);
|
|
335
367
|
if (typeof module.verifyToken !== "function") {
|
|
336
|
-
print(`${provider}: token stored (plugin has no verifyToken; can't confirm validity)`);
|
|
368
|
+
print(style.warn(`${provider}: token stored (plugin has no verifyToken; can't confirm validity)`));
|
|
337
369
|
return {
|
|
338
370
|
status: "ok",
|
|
339
371
|
message: "stored, unverified"
|
|
340
372
|
};
|
|
341
373
|
}
|
|
342
|
-
const
|
|
374
|
+
const verify = () => module.verifyToken(token);
|
|
375
|
+
const verified = input.showSpinner !== false ? await withSpinner(`Verifying ${provider} token…`, verify) : await verify();
|
|
343
376
|
if (verified.ok) {
|
|
344
|
-
print(`${provider}: ok — ${verified.subject}`);
|
|
377
|
+
print(style.success(`${provider}: ok — ${verified.subject}`));
|
|
345
378
|
return {
|
|
346
379
|
status: "ok",
|
|
347
380
|
message: verified.subject
|
|
348
381
|
};
|
|
349
382
|
}
|
|
350
|
-
print(`${provider}: rejected — ${verified.message}`);
|
|
351
|
-
if (module.AUTH_HINT) print(` hint: ${module.AUTH_HINT}`);
|
|
383
|
+
print(style.fail(`${provider}: rejected — ${verified.message}`));
|
|
384
|
+
if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
|
|
352
385
|
return {
|
|
353
386
|
status: "fail",
|
|
354
387
|
message: verified.message
|
|
355
388
|
};
|
|
356
389
|
} catch (err) {
|
|
357
390
|
const msg = err instanceof Error ? err.message : String(err);
|
|
358
|
-
print(`${provider}: cannot verify — ${msg}`);
|
|
391
|
+
print(style.fail(`${provider}: cannot verify — ${msg}`));
|
|
359
392
|
return {
|
|
360
393
|
status: "fail",
|
|
361
394
|
message: msg
|
|
@@ -367,8 +400,8 @@ async function runAuthList(input = {}) {
|
|
|
367
400
|
const importer = input.importer ?? defaultImporter$1;
|
|
368
401
|
const providers = listStoredProviders();
|
|
369
402
|
if (providers.length === 0) {
|
|
370
|
-
print("no stored tokens.");
|
|
371
|
-
print("run: holocron auth set <provider> <token>");
|
|
403
|
+
print(style.dim("no stored tokens."));
|
|
404
|
+
print(style.hint("run: holocron auth set <provider> <token>"));
|
|
372
405
|
return {
|
|
373
406
|
status: "ok",
|
|
374
407
|
message: "none"
|
|
@@ -378,9 +411,13 @@ async function runAuthList(input = {}) {
|
|
|
378
411
|
const check = await runAuthCheck({
|
|
379
412
|
provider,
|
|
380
413
|
importer,
|
|
381
|
-
print: () => {}
|
|
414
|
+
print: () => {},
|
|
415
|
+
showSpinner: false
|
|
382
416
|
});
|
|
383
|
-
|
|
417
|
+
const label = `${provider}${check.message ? ` — ${check.message}` : ""}`;
|
|
418
|
+
if (check.status === "ok") print(` ${style.success(label)}`);
|
|
419
|
+
else if (check.status === "fail") print(` ${style.fail(label)}`);
|
|
420
|
+
else print(` ${style.dim(`· ${label}`)}`);
|
|
384
421
|
}
|
|
385
422
|
return { status: "ok" };
|
|
386
423
|
}
|
|
@@ -492,12 +529,12 @@ async function runDeploy(input) {
|
|
|
492
529
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
493
530
|
await loader.load();
|
|
494
531
|
const dryRun = input.context.dryRun ?? false;
|
|
495
|
-
print(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`);
|
|
532
|
+
print(style.header(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
|
|
496
533
|
if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
|
|
497
534
|
const deploy = loader.get("deployment");
|
|
498
535
|
if (dryRun) {
|
|
499
536
|
const message = `would: ${deploy.providerName}.triggerDeployment(projectId=${input.projectId}, branch=${input.branch}${input.target ? `, target=${input.target}` : ""})`;
|
|
500
|
-
print(`
|
|
537
|
+
print(` ${style.dim(`… ${message}`)}`);
|
|
501
538
|
return {
|
|
502
539
|
deployment: null,
|
|
503
540
|
status: "dry-run",
|
|
@@ -505,19 +542,19 @@ async function runDeploy(input) {
|
|
|
505
542
|
};
|
|
506
543
|
}
|
|
507
544
|
try {
|
|
508
|
-
const record = await deploy.triggerDeployment({
|
|
545
|
+
const record = await withSpinner(`Deploying ${input.branch}${input.target ? ` → ${input.target}` : ""}…`, () => deploy.triggerDeployment({
|
|
509
546
|
projectId: input.projectId,
|
|
510
547
|
branch: input.branch,
|
|
511
548
|
...input.target ? { target: input.target } : {}
|
|
512
|
-
});
|
|
513
|
-
print(`
|
|
549
|
+
}));
|
|
550
|
+
print(` ${style.success(`${record.status} — ${record.url}`)}`);
|
|
514
551
|
return {
|
|
515
552
|
deployment: record,
|
|
516
553
|
status: "ok"
|
|
517
554
|
};
|
|
518
555
|
} catch (err) {
|
|
519
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
520
|
-
print(`
|
|
557
|
+
print(` ${style.fail(message)}`);
|
|
521
558
|
return {
|
|
522
559
|
deployment: null,
|
|
523
560
|
status: "fail",
|
|
@@ -530,11 +567,11 @@ async function runDeploy(input) {
|
|
|
530
567
|
async function runDoctor(input) {
|
|
531
568
|
const print = input.print ?? ((line) => console.log(line));
|
|
532
569
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
533
|
-
await loader.load();
|
|
570
|
+
await withSpinner("Loading plugins…", () => loader.load());
|
|
534
571
|
const rows = [];
|
|
535
572
|
const config = input.loaded.resolved;
|
|
536
|
-
print(`Holocron doctor — ${config.name}`);
|
|
537
|
-
print(` config: ${input.loaded.filepath}`);
|
|
573
|
+
print(style.header(`Holocron doctor — ${config.name}`));
|
|
574
|
+
print(style.dim(` config: ${input.loaded.filepath}`));
|
|
538
575
|
print("");
|
|
539
576
|
for (const key of loader.loadedKeys()) {
|
|
540
577
|
const cardinality = CARDINALITY[key];
|
|
@@ -554,7 +591,12 @@ async function runDoctor(input) {
|
|
|
554
591
|
rows.push(row);
|
|
555
592
|
}
|
|
556
593
|
}
|
|
557
|
-
for (const row of rows)
|
|
594
|
+
for (const row of rows) {
|
|
595
|
+
const label = `${pad(row.capability, 14)} via ${pad(row.provider, 14)} ${row.message}`;
|
|
596
|
+
if (row.status === "ok") print(` ${style.success(label)}`);
|
|
597
|
+
else if (row.status === "fail") print(` ${style.fail(label)}`);
|
|
598
|
+
else print(` ${style.dim(`· ${label}`)}`);
|
|
599
|
+
}
|
|
558
600
|
const summary = rows.reduce((acc, r) => {
|
|
559
601
|
acc[r.status] += 1;
|
|
560
602
|
return acc;
|
|
@@ -563,8 +605,9 @@ async function runDoctor(input) {
|
|
|
563
605
|
fail: 0,
|
|
564
606
|
skip: 0
|
|
565
607
|
});
|
|
608
|
+
const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped`;
|
|
566
609
|
print("");
|
|
567
|
-
print(
|
|
610
|
+
print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
|
|
568
611
|
return {
|
|
569
612
|
rows,
|
|
570
613
|
summary
|
|
@@ -2612,22 +2655,22 @@ function describeScope(scope) {
|
|
|
2612
2655
|
async function runSecretsSync(input) {
|
|
2613
2656
|
const print = input.print ?? ((line) => console.log(line));
|
|
2614
2657
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
2615
|
-
await loader.load();
|
|
2658
|
+
await withSpinner("Loading plugins…", () => loader.load());
|
|
2616
2659
|
const dryRun = input.context.dryRun ?? false;
|
|
2617
2660
|
const targets = input.targets ?? ["production", "preview"];
|
|
2618
|
-
print(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`);
|
|
2619
|
-
print(` vault: ${vaultProviderName(loader)}`);
|
|
2661
|
+
print(style.header(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`));
|
|
2662
|
+
print(style.dim(` vault: ${vaultProviderName(loader)}`));
|
|
2620
2663
|
print("");
|
|
2621
2664
|
const vault = loader.get("vault");
|
|
2622
2665
|
if (!vault.readEnvironment) throw new Error(`vault provider (${vault.providerName}) does not implement readEnvironment — sync needs bulk env reads`);
|
|
2623
|
-
const envVars = await vault.readEnvironment(input.environmentId);
|
|
2666
|
+
const envVars = await withSpinner(`Reading vault environment ${input.environmentId}…`, () => vault.readEnvironment(input.environmentId));
|
|
2624
2667
|
const keys = Object.keys(envVars).sort();
|
|
2625
|
-
print(`
|
|
2668
|
+
print(style.step(`read ${keys.length} keys from vault`));
|
|
2626
2669
|
print("");
|
|
2627
2670
|
const rows = [];
|
|
2628
2671
|
if (loader.has("secrets")) {
|
|
2629
2672
|
const secrets = loader.get("secrets");
|
|
2630
|
-
print("
|
|
2673
|
+
print(style.step("secrets (repo scope)"));
|
|
2631
2674
|
for (const key of keys) {
|
|
2632
2675
|
rows.push(await runRow(`secrets:${secrets.providerName}`, "scope=repo", key, dryRun, async () => {
|
|
2633
2676
|
await secrets.setSecret({ kind: "repo" }, key, envVars[key]);
|
|
@@ -2638,7 +2681,7 @@ async function runSecretsSync(input) {
|
|
|
2638
2681
|
if (loader.has("deployment")) {
|
|
2639
2682
|
const deploy = loader.get("deployment");
|
|
2640
2683
|
if (!input.projectId) {
|
|
2641
|
-
print("
|
|
2684
|
+
print(style.step("deployment"));
|
|
2642
2685
|
const row = {
|
|
2643
2686
|
destination: `deployment:${deploy.providerName}`,
|
|
2644
2687
|
scope: "no projectId",
|
|
@@ -2649,7 +2692,7 @@ async function runSecretsSync(input) {
|
|
|
2649
2692
|
rows.push(row);
|
|
2650
2693
|
print(formatRow(row));
|
|
2651
2694
|
} else for (const target of targets) {
|
|
2652
|
-
print(`
|
|
2695
|
+
print(style.step(`deployment (target=${target})`));
|
|
2653
2696
|
for (const key of keys) {
|
|
2654
2697
|
rows.push(await runRow(`deployment:${deploy.providerName}`, `target=${target}`, key, dryRun, async () => {
|
|
2655
2698
|
await deploy.setEnvVar(input.projectId, target, key, envVars[key]);
|
|
@@ -2671,7 +2714,8 @@ async function runSecretsSync(input) {
|
|
|
2671
2714
|
dryRun: 0
|
|
2672
2715
|
});
|
|
2673
2716
|
print("");
|
|
2674
|
-
|
|
2717
|
+
const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
|
|
2718
|
+
print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
|
|
2675
2719
|
return {
|
|
2676
2720
|
rows,
|
|
2677
2721
|
summary
|
|
@@ -2703,9 +2747,12 @@ async function runRow(destination, scope, key, dryRun, body) {
|
|
|
2703
2747
|
}
|
|
2704
2748
|
}
|
|
2705
2749
|
function formatRow(row) {
|
|
2706
|
-
const
|
|
2707
|
-
const
|
|
2708
|
-
return ` ${
|
|
2750
|
+
const detail = row.message ? style.dim(` (${row.message})`) : "";
|
|
2751
|
+
const label = `${row.key}${detail}`;
|
|
2752
|
+
if (row.status === "ok") return ` ${style.success(label)}`;
|
|
2753
|
+
if (row.status === "fail") return ` ${style.fail(label)}`;
|
|
2754
|
+
if (row.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
2755
|
+
return ` ${style.dim(`· ${label}`)}`;
|
|
2709
2756
|
}
|
|
2710
2757
|
function vaultProviderName(loader) {
|
|
2711
2758
|
if (!loader.has("vault")) return "<missing>";
|
|
@@ -3122,18 +3169,18 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
|
|
|
3122
3169
|
async function runSetup(input) {
|
|
3123
3170
|
const print = input.print ?? ((line) => console.log(line));
|
|
3124
3171
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
3125
|
-
await loader.load();
|
|
3172
|
+
await withSpinner("Loading plugins…", () => loader.load());
|
|
3126
3173
|
const config = input.loaded.resolved;
|
|
3127
3174
|
const dryRun = input.context.dryRun ?? false;
|
|
3128
3175
|
const steps = [];
|
|
3129
3176
|
const repo = config.repo;
|
|
3130
3177
|
const effectivePreset = repo?.protection;
|
|
3131
|
-
print(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`);
|
|
3132
|
-
print(` config: ${input.loaded.filepath}`);
|
|
3178
|
+
print(style.header(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`));
|
|
3179
|
+
print(style.dim(` config: ${input.loaded.filepath}`));
|
|
3133
3180
|
print("");
|
|
3134
3181
|
if (loader.has("source")) {
|
|
3135
3182
|
const source = loader.get("source");
|
|
3136
|
-
print("
|
|
3183
|
+
print(style.step("source"));
|
|
3137
3184
|
for (const method of [
|
|
3138
3185
|
"enableVulnerabilityAlerts",
|
|
3139
3186
|
"enableAutomatedSecurityFixes",
|
|
@@ -3173,7 +3220,7 @@ async function runSetup(input) {
|
|
|
3173
3220
|
const workflows = config.workflows;
|
|
3174
3221
|
if (loader.has("source") && workflows && workflows.length > 0) {
|
|
3175
3222
|
const source = loader.get("source");
|
|
3176
|
-
print("
|
|
3223
|
+
print(style.step("workflows"));
|
|
3177
3224
|
for (const entry of workflows) {
|
|
3178
3225
|
const name = typeof entry === "string" ? entry : entry.name;
|
|
3179
3226
|
const withOverrides = typeof entry === "object" ? entry.with : void 0;
|
|
@@ -3254,7 +3301,7 @@ async function runSetup(input) {
|
|
|
3254
3301
|
}
|
|
3255
3302
|
if (loader.has("environments")) {
|
|
3256
3303
|
const envs = loader.get("environments");
|
|
3257
|
-
print("
|
|
3304
|
+
print(style.step("environments"));
|
|
3258
3305
|
for (const envName of ["staging", "production"]) {
|
|
3259
3306
|
steps.push(await runStep("environments", `upsert ${envName}`, dryRun, async () => {
|
|
3260
3307
|
await envs.upsertEnvironment({ name: envName });
|
|
@@ -3264,7 +3311,7 @@ async function runSetup(input) {
|
|
|
3264
3311
|
}
|
|
3265
3312
|
if (loader.has("deployment")) {
|
|
3266
3313
|
const deploy = loader.get("deployment");
|
|
3267
|
-
print("
|
|
3314
|
+
print(style.step("deployment"));
|
|
3268
3315
|
steps.push(await runStep("deployment", `ensureProject ${config.name}`, dryRun, async () => {
|
|
3269
3316
|
await deploy.ensureProject({ name: config.name });
|
|
3270
3317
|
}));
|
|
@@ -3272,7 +3319,7 @@ async function runSetup(input) {
|
|
|
3272
3319
|
}
|
|
3273
3320
|
if (loader.has("auth")) {
|
|
3274
3321
|
const auth = loader.get("auth");
|
|
3275
|
-
print("
|
|
3322
|
+
print(style.step("auth"));
|
|
3276
3323
|
if (auth.ensureWebhookApp) {
|
|
3277
3324
|
steps.push(await runStep("auth", "ensureWebhookApp", dryRun, async () => {
|
|
3278
3325
|
return `webhook ${(await auth.ensureWebhookApp()).alreadyExists ? "exists" : "created"}`;
|
|
@@ -3290,7 +3337,7 @@ async function runSetup(input) {
|
|
|
3290
3337
|
}
|
|
3291
3338
|
if (loader.has("vault")) {
|
|
3292
3339
|
const vault = loader.get("vault");
|
|
3293
|
-
print("
|
|
3340
|
+
print(style.step("vault"));
|
|
3294
3341
|
if (vault.ensureProject) {
|
|
3295
3342
|
steps.push(await runStep("vault", `ensureProject ${config.name}`, dryRun, async () => {
|
|
3296
3343
|
return `project ${(await vault.ensureProject(config.name)).alreadyExists ? "exists" : "created"}`;
|
|
@@ -3327,7 +3374,7 @@ async function runSetup(input) {
|
|
|
3327
3374
|
}
|
|
3328
3375
|
if (loader.has("tooling")) {
|
|
3329
3376
|
const tools = loader.get("tooling");
|
|
3330
|
-
print("
|
|
3377
|
+
print(style.step("tooling"));
|
|
3331
3378
|
for (const tool of tools) {
|
|
3332
3379
|
steps.push(await runStep("tooling", `${tool.providerName}.sync`, dryRun, async () => {
|
|
3333
3380
|
await tool.sync();
|
|
@@ -3348,23 +3395,24 @@ async function runSetup(input) {
|
|
|
3348
3395
|
dryRun: 0
|
|
3349
3396
|
});
|
|
3350
3397
|
print("");
|
|
3351
|
-
|
|
3398
|
+
const summaryLine = ` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
|
|
3399
|
+
print(summary.fail > 0 ? style.fail(summaryLine.trim()) : style.success(summaryLine.trim()));
|
|
3352
3400
|
if (steps.some((s) => s.reason === "permissions")) {
|
|
3353
3401
|
print("");
|
|
3354
|
-
print("
|
|
3355
|
-
print(" These operations require a temporary fine-grained PAT with the");
|
|
3356
|
-
print(" following repository permissions:");
|
|
3402
|
+
print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
|
|
3403
|
+
print(style.hint(" These operations require a temporary fine-grained PAT with the"));
|
|
3404
|
+
print(style.hint(" following repository permissions:"));
|
|
3357
3405
|
print("");
|
|
3358
|
-
print(" · Administration — read and write");
|
|
3359
|
-
print(" · Code scanning alerts — read and write");
|
|
3360
|
-
print(" · Contents — read and write");
|
|
3361
|
-
print(" · Secret scanning alerts — read and write");
|
|
3362
|
-
print(" · Workflows — read and write");
|
|
3363
|
-
print(" · Metadata — read (added automatically)");
|
|
3406
|
+
print(style.hint(" · Administration — read and write"));
|
|
3407
|
+
print(style.hint(" · Code scanning alerts — read and write"));
|
|
3408
|
+
print(style.hint(" · Contents — read and write"));
|
|
3409
|
+
print(style.hint(" · Secret scanning alerts — read and write"));
|
|
3410
|
+
print(style.hint(" · Workflows — read and write"));
|
|
3411
|
+
print(style.hint(" · Metadata — read (added automatically)"));
|
|
3364
3412
|
print("");
|
|
3365
|
-
print(" Create one at: https://github.com/settings/personal-access-tokens/new");
|
|
3366
|
-
print(" Then re-run: holocron setup --token <your-temp-pat>");
|
|
3367
|
-
print(" You can revoke it immediately after setup completes.");
|
|
3413
|
+
print(style.hint(" Create one at: https://github.com/settings/personal-access-tokens/new"));
|
|
3414
|
+
print(style.hint(" Then re-run: holocron setup --token <your-temp-pat>"));
|
|
3415
|
+
print(style.hint(" You can revoke it immediately after setup completes."));
|
|
3368
3416
|
}
|
|
3369
3417
|
return {
|
|
3370
3418
|
steps,
|
|
@@ -3412,10 +3460,13 @@ function classify403(err) {
|
|
|
3412
3460
|
return "permissions";
|
|
3413
3461
|
}
|
|
3414
3462
|
function formatStep(step) {
|
|
3415
|
-
const icon = step.status === "ok" ? "✓" : step.status === "fail" ? "✗" : step.status === "dry-run" ? "…" : "·";
|
|
3416
3463
|
const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
|
|
3417
|
-
const detail = step.message ? ` (${step.message})` : "";
|
|
3418
|
-
|
|
3464
|
+
const detail = step.message ? style.dim(` (${step.message})`) : "";
|
|
3465
|
+
const label = `${step.step}${tag}${detail}`;
|
|
3466
|
+
if (step.status === "ok") return ` ${style.success(label)}`;
|
|
3467
|
+
if (step.status === "fail") return ` ${style.fail(label)}`;
|
|
3468
|
+
if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
3469
|
+
return ` ${style.dim(`· ${label}`)}`;
|
|
3419
3470
|
}
|
|
3420
3471
|
//#endregion
|
|
3421
3472
|
//#region src/commands/sync.ts
|
|
@@ -3426,14 +3477,20 @@ const SYNC_STEPS = [
|
|
|
3426
3477
|
"keywords",
|
|
3427
3478
|
"description"
|
|
3428
3479
|
];
|
|
3480
|
+
const LOCAL_STEPS = /* @__PURE__ */ new Set(["keywords", "description"]);
|
|
3429
3481
|
async function runSync(input) {
|
|
3430
3482
|
const print = input.print ?? ((line) => console.log(line));
|
|
3431
3483
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
3432
|
-
await loader.load();
|
|
3433
3484
|
const config = input.loaded.resolved;
|
|
3434
3485
|
const dryRun = input.context.dryRun ?? false;
|
|
3435
3486
|
const requestedSteps = input.steps;
|
|
3436
3487
|
const steps = [];
|
|
3488
|
+
if (!requestedSteps || requestedSteps.some((s) => !LOCAL_STEPS.has(s))) await loader.load();
|
|
3489
|
+
else try {
|
|
3490
|
+
await loader.load();
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
if (!(err instanceof AuthError)) throw err;
|
|
3493
|
+
}
|
|
3437
3494
|
print(`Holocron sync — ${config.name}${dryRun ? " (dry-run)" : ""}`);
|
|
3438
3495
|
print(` config: ${input.loaded.filepath}`);
|
|
3439
3496
|
print("");
|
|
@@ -3442,6 +3499,7 @@ async function runSync(input) {
|
|
|
3442
3499
|
print(" → source");
|
|
3443
3500
|
for (const stepName of SYNC_STEPS) {
|
|
3444
3501
|
if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
|
|
3502
|
+
if (LOCAL_STEPS.has(stepName)) continue;
|
|
3445
3503
|
if (stepName === "labels") if (source.syncLabels) {
|
|
3446
3504
|
steps.push(await runSyncStep("source", "sync labels", dryRun, () => source.syncLabels(CANONICAL_LABELS, STALE_LABELS)));
|
|
3447
3505
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
@@ -3500,47 +3558,6 @@ async function runSync(input) {
|
|
|
3500
3558
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
3501
3559
|
}
|
|
3502
3560
|
}
|
|
3503
|
-
if (stepName === "keywords") {
|
|
3504
|
-
const topics = config.repo?.topics ?? [];
|
|
3505
|
-
if (topics.length === 0) {
|
|
3506
|
-
steps.push({
|
|
3507
|
-
capability: "source",
|
|
3508
|
-
step: "sync keywords",
|
|
3509
|
-
status: "skip",
|
|
3510
|
-
message: "no topics configured"
|
|
3511
|
-
});
|
|
3512
|
-
print(formatSyncStep(steps[steps.length - 1]));
|
|
3513
|
-
} else {
|
|
3514
|
-
steps.push(await runSyncStep("source", "sync keywords", dryRun, async () => {
|
|
3515
|
-
return await writePackageJsonField(input.context.repoRoot, "keywords", topics) ? `${topics.length} keywords written` : `${topics.length} topics (no package.json)`;
|
|
3516
|
-
}));
|
|
3517
|
-
print(formatSyncStep(steps[steps.length - 1]));
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
if (stepName === "description") {
|
|
3521
|
-
const description = config.description;
|
|
3522
|
-
if (!description) {
|
|
3523
|
-
steps.push({
|
|
3524
|
-
capability: "source",
|
|
3525
|
-
step: "sync description",
|
|
3526
|
-
status: "skip",
|
|
3527
|
-
message: "no description configured"
|
|
3528
|
-
});
|
|
3529
|
-
print(formatSyncStep(steps[steps.length - 1]));
|
|
3530
|
-
} else {
|
|
3531
|
-
steps.push(await runSyncStep("source", "sync description", dryRun, async () => {
|
|
3532
|
-
const pkgWrote = await writePackageJsonField(input.context.repoRoot, "description", description);
|
|
3533
|
-
const readmeWrote = await updateReadmeDescription(input.context.repoRoot, description);
|
|
3534
|
-
if (source.syncDescription) await source.syncDescription(description);
|
|
3535
|
-
const parts = [];
|
|
3536
|
-
if (pkgWrote) parts.push("package.json");
|
|
3537
|
-
if (readmeWrote) parts.push("README.md");
|
|
3538
|
-
if (source.syncDescription) parts.push("GitHub");
|
|
3539
|
-
return parts.length > 0 ? parts.join(", ") + " updated" : "description synced";
|
|
3540
|
-
}));
|
|
3541
|
-
print(formatSyncStep(steps[steps.length - 1]));
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
3561
|
}
|
|
3545
3562
|
if (requestedSteps) {
|
|
3546
3563
|
for (const name of requestedSteps) if (!SYNC_STEPS.includes(name)) {
|
|
@@ -3554,6 +3571,51 @@ async function runSync(input) {
|
|
|
3554
3571
|
}
|
|
3555
3572
|
}
|
|
3556
3573
|
}
|
|
3574
|
+
for (const stepName of ["keywords", "description"]) {
|
|
3575
|
+
if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
|
|
3576
|
+
if (stepName === "keywords") {
|
|
3577
|
+
const topics = config.repo?.topics ?? [];
|
|
3578
|
+
if (topics.length === 0) {
|
|
3579
|
+
steps.push({
|
|
3580
|
+
capability: "local",
|
|
3581
|
+
step: "sync keywords",
|
|
3582
|
+
status: "skip",
|
|
3583
|
+
message: "no topics configured"
|
|
3584
|
+
});
|
|
3585
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
3586
|
+
} else {
|
|
3587
|
+
steps.push(await runSyncStep("local", "sync keywords", dryRun, async () => {
|
|
3588
|
+
return await writePackageJsonField(input.context.repoRoot, "keywords", topics) ? `${topics.length} keywords written` : `${topics.length} topics (no package.json)`;
|
|
3589
|
+
}));
|
|
3590
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (stepName === "description") {
|
|
3594
|
+
const description = config.description;
|
|
3595
|
+
if (!description) {
|
|
3596
|
+
steps.push({
|
|
3597
|
+
capability: "local",
|
|
3598
|
+
step: "sync description",
|
|
3599
|
+
status: "skip",
|
|
3600
|
+
message: "no description configured"
|
|
3601
|
+
});
|
|
3602
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
3603
|
+
} else {
|
|
3604
|
+
const source = loader.has("source") ? loader.get("source") : null;
|
|
3605
|
+
steps.push(await runSyncStep("local", "sync description", dryRun, async () => {
|
|
3606
|
+
const pkgWrote = await writePackageJsonField(input.context.repoRoot, "description", description);
|
|
3607
|
+
const readmeWrote = await updateReadmeDescription(input.context.repoRoot, description);
|
|
3608
|
+
if (source?.syncDescription) await source.syncDescription(description);
|
|
3609
|
+
const parts = [];
|
|
3610
|
+
if (pkgWrote) parts.push("package.json");
|
|
3611
|
+
if (readmeWrote) parts.push("README.md");
|
|
3612
|
+
if (source?.syncDescription) parts.push("GitHub");
|
|
3613
|
+
return parts.length > 0 ? parts.join(", ") + " updated" : "description synced";
|
|
3614
|
+
}));
|
|
3615
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3557
3619
|
const summary = steps.reduce((acc, s) => {
|
|
3558
3620
|
if (s.status === "ok") acc.ok += 1;
|
|
3559
3621
|
else if (s.status === "fail") acc.fail += 1;
|
|
@@ -3905,10 +3967,10 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
|
|
|
3905
3967
|
dryRun: argv.dryRun,
|
|
3906
3968
|
...argv.otp ? { otp: argv.otp } : {}
|
|
3907
3969
|
})).status === "fail") process.exitCode = 1;
|
|
3908
|
-
}).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync [steps..]", "Sync
|
|
3970
|
+
}).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, topics, keywords, description)", (y) => y.positional("steps", {
|
|
3909
3971
|
type: "string",
|
|
3910
3972
|
array: true,
|
|
3911
|
-
describe: "Steps to run: labels, properties, topics (default: all)"
|
|
3973
|
+
describe: "Steps to run: labels, properties, topics, keywords, description (default: all)"
|
|
3912
3974
|
}).option("repo", {
|
|
3913
3975
|
type: "string",
|
|
3914
3976
|
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.68",
|
|
4
4
|
"description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"@napi-rs/keyring": "^1.3.0",
|
|
37
37
|
"@theholocron/github-client": "^0.11.3",
|
|
38
38
|
"@theholocron/http-client": "^0.11.3",
|
|
39
|
+
"chalk": "^5.4.1",
|
|
40
|
+
"ora": "^8.2.0",
|
|
39
41
|
"tsx": "^4.22.4",
|
|
40
42
|
"yargs": "^18.0.0"
|
|
41
43
|
},
|