@jango-blockchained/hoox-cli 0.7.0 → 0.7.2

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.
Files changed (3) hide show
  1. package/README.md +42 -26
  2. package/dist/index.js +550 -196
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -21388,18 +21388,15 @@ class KvSyncService {
21388
21388
  return stdout2.trim() || null;
21389
21389
  }
21390
21390
  async set(namespaceId, key, value) {
21391
- const proc = Bun.spawn([
21392
- "wrangler",
21393
- "kv",
21394
- "key",
21395
- "put",
21396
- "--namespace-id",
21397
- namespaceId,
21398
- key,
21399
- value
21400
- ], { stdout: "pipe", stderr: "pipe" });
21391
+ const proc = Bun.spawn(["wrangler", "kv", "key", "put", "--namespace-id", namespaceId, key], { stdout: "pipe", stderr: "pipe", stdin: "pipe" });
21392
+ proc.stdin.write(value + `
21393
+ `);
21394
+ proc.stdin.end();
21395
+ const [, stderr] = await Promise.all([
21396
+ new Response(proc.stdout).text(),
21397
+ new Response(proc.stderr).text()
21398
+ ]);
21401
21399
  const exitCode = await proc.exited;
21402
- const stderr = await new Response(proc.stderr).text();
21403
21400
  if (exitCode !== 0) {
21404
21401
  throw new Error(`Failed to set key "${key}": ${stderr.trim() || `exit ${exitCode}`}`);
21405
21402
  }
@@ -21531,7 +21528,7 @@ var exports_schema_service = {};
21531
21528
  __export(exports_schema_service, {
21532
21529
  SchemaService: () => SchemaService
21533
21530
  });
21534
- import { readFileSync as readFileSync4 } from "fs";
21531
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
21535
21532
  import { resolve as resolve5 } from "path";
21536
21533
 
21537
21534
  class SchemaService {
@@ -21565,34 +21562,41 @@ class SchemaService {
21565
21562
  const workerWranglerPath = resolve5(workersDir, name, "wrangler.jsonc");
21566
21563
  const devVarsPath = resolve5(workersDir, name, ".dev.vars");
21567
21564
  const errors4 = [];
21565
+ const isHooxProject = existsSync5(resolve5(this.projectRoot, "wrangler.jsonc")) || existsSync5(resolve5(this.projectRoot, "workers"));
21568
21566
  try {
21569
21567
  const wranglerContent = readFileSync4(workerWranglerPath, "utf-8");
21570
21568
  errors4.push(...validateWranglerJsonc(name, manifest, wranglerContent));
21571
21569
  } catch (e) {
21570
+ const isErrnoException = e instanceof Error && "code" in e;
21571
+ const isEnoent = isErrnoException && e.code === "ENOENT";
21572
+ const errMessage = e instanceof Error ? e.message : String(e);
21573
+ const message = isEnoent && !isHooxProject ? `Cannot read ${workerWranglerPath}: File not found. Are you in the Hoox project root directory?` : `Cannot read ${workerWranglerPath}: ${errMessage}`;
21572
21574
  errors4.push({
21573
21575
  worker: name,
21574
21576
  severity: "error",
21575
- message: `Cannot read ${workerWranglerPath}: ${e.message}`
21577
+ message
21576
21578
  });
21577
21579
  }
21578
21580
  try {
21579
21581
  const rootContent = readFileSync4(rootWranglerPath, "utf-8");
21580
21582
  errors4.push(...validateRootSecrets(name, manifest, rootContent));
21581
21583
  } catch (e) {
21584
+ const errMessage = e instanceof Error ? e.message : String(e);
21582
21585
  errors4.push({
21583
21586
  worker: name,
21584
21587
  severity: "warning",
21585
- message: `Cannot read root wrangler.jsonc: ${e.message}`
21588
+ message: `Cannot read root wrangler.jsonc: ${errMessage}`
21586
21589
  });
21587
21590
  }
21588
21591
  try {
21589
21592
  const devVarsContent = readFileSync4(devVarsPath, "utf-8");
21590
21593
  errors4.push(...validateDevVars(name, manifest, devVarsContent));
21591
21594
  } catch (e) {
21595
+ const errMessage = e instanceof Error ? e.message : String(e);
21592
21596
  errors4.push({
21593
21597
  worker: name,
21594
21598
  severity: "warning",
21595
- message: `Cannot read ${devVarsPath}: ${e.message}`
21599
+ message: `Cannot read ${devVarsPath}: ${errMessage}`
21596
21600
  });
21597
21601
  }
21598
21602
  return {
@@ -23674,11 +23678,13 @@ class CLIError extends Error {
23674
23678
  code;
23675
23679
  details;
23676
23680
  recoverable;
23677
- constructor(message, code = 1 /* ERROR */, details, recoverable = false) {
23681
+ hint;
23682
+ constructor(message, code = 1 /* ERROR */, details, recoverable = false, hint) {
23678
23683
  super(message);
23679
23684
  this.code = code;
23680
23685
  this.details = details;
23681
23686
  this.recoverable = recoverable;
23687
+ this.hint = hint;
23682
23688
  this.name = "CLIError";
23683
23689
  }
23684
23690
  }
@@ -23746,7 +23752,63 @@ function stripAnsi(str) {
23746
23752
  return ansis_default.strip(str);
23747
23753
  }
23748
23754
 
23755
+ // src/utils/timer.ts
23756
+ function formatDuration2(ms) {
23757
+ if (!Number.isFinite(ms) || ms < 0)
23758
+ return "0ms";
23759
+ if (ms < 1000)
23760
+ return `${Math.round(ms)}ms`;
23761
+ if (ms < 60000)
23762
+ return `${(ms / 1000).toFixed(1)}s`;
23763
+ if (ms < 3600000) {
23764
+ const m2 = Math.floor(ms / 60000);
23765
+ const s = Math.floor(ms % 60000 / 1000);
23766
+ return `${m2}m ${String(s).padStart(2, "0")}s`;
23767
+ }
23768
+ const h = Math.floor(ms / 3600000);
23769
+ const m = Math.floor(ms % 3600000 / 60000);
23770
+ return `${h}h ${String(m).padStart(2, "0")}m`;
23771
+ }
23772
+ function startTimer() {
23773
+ const startedAt = Date.now();
23774
+ return {
23775
+ ms: () => Date.now() - startedAt,
23776
+ format: () => formatDuration2(Date.now() - startedAt)
23777
+ };
23778
+ }
23779
+
23780
+ // src/utils/format-mode.ts
23781
+ function isRichMode(opts) {
23782
+ if (opts?.json || opts?.quiet)
23783
+ return false;
23784
+ return Boolean(process.stdout.isTTY);
23785
+ }
23786
+
23749
23787
  // src/utils/formatters.ts
23788
+ var formatDuration3 = formatDuration2;
23789
+ var BADGE_STYLE = {
23790
+ ok: { bg: ansis_default.bgGreen, fg: ansis_default.black },
23791
+ warn: { bg: ansis_default.bgYellow, fg: ansis_default.black },
23792
+ err: { bg: ansis_default.bgRed, fg: ansis_default.white },
23793
+ info: { bg: ansis_default.bgBlue, fg: ansis_default.white }
23794
+ };
23795
+ function formatBadge(level, text) {
23796
+ const content = (text ?? defaultBadgeLabel(level)).padEnd(4, " ");
23797
+ const { bg, fg } = BADGE_STYLE[level];
23798
+ return bg(fg(` ${content} `));
23799
+ }
23800
+ function defaultBadgeLabel(level) {
23801
+ switch (level) {
23802
+ case "ok":
23803
+ return "OK";
23804
+ case "warn":
23805
+ return "WARN";
23806
+ case "err":
23807
+ return "FAIL";
23808
+ case "info":
23809
+ return "INFO";
23810
+ }
23811
+ }
23750
23812
  function formatSuccess(message, opts) {
23751
23813
  if (opts?.quiet)
23752
23814
  return;
@@ -23770,6 +23832,9 @@ function formatError2(error51, opts) {
23770
23832
  if (cliError?.details) {
23771
23833
  output.details = cliError.details;
23772
23834
  }
23835
+ if (cliError?.hint) {
23836
+ output.hint = cliError.hint;
23837
+ }
23773
23838
  process.stdout.write(JSON.stringify(output) + `
23774
23839
  `);
23775
23840
  return;
@@ -23783,6 +23848,10 @@ function formatError2(error51, opts) {
23783
23848
  `);
23784
23849
  if (cliError?.details) {
23785
23850
  process.stdout.write(` ${theme.dim(cliError.details)}
23851
+ `);
23852
+ }
23853
+ if (cliError?.hint) {
23854
+ process.stdout.write(`${theme.dim("\u21B3 hint:")} ${theme.value(cliError.hint)}
23786
23855
  `);
23787
23856
  }
23788
23857
  }
@@ -25455,6 +25524,16 @@ ${l2}
25455
25524
  }).prompt();
25456
25525
  };
25457
25526
  var i = `${styleText2("gray", S_BAR)} `;
25527
+ var tasks = async (o2, e) => {
25528
+ for (const t2 of o2) {
25529
+ if (t2.enabled === false)
25530
+ continue;
25531
+ const s = spinner(e);
25532
+ s.start(t2.title);
25533
+ const n2 = await t2.task(s.message);
25534
+ s.stop(n2 || t2.title);
25535
+ }
25536
+ };
25458
25537
  var text = (t2) => new n({
25459
25538
  validate: t2.validate,
25460
25539
  placeholder: t2.placeholder,
@@ -25540,7 +25619,12 @@ class CloudflareService {
25540
25619
  };
25541
25620
  } catch (err) {
25542
25621
  const message = err instanceof Error ? err.message : String(err);
25543
- return { ok: false, error: `Failed to spawn wrangler: ${message}` };
25622
+ const hint = /ENOENT|not found/i.test(message) ? "Install wrangler with `bun add -g wrangler` (or `npm i -g wrangler`), then run `wrangler login`." : undefined;
25623
+ return {
25624
+ ok: false,
25625
+ error: hint ? `Failed to spawn wrangler: ${message}
25626
+ \u21B3 hint: ${hint}` : `Failed to spawn wrangler: ${message}`
25627
+ };
25544
25628
  }
25545
25629
  }
25546
25630
  async whoami() {
@@ -25752,7 +25836,32 @@ class CloudflareService {
25752
25836
  ]);
25753
25837
  }
25754
25838
  async zonesList() {
25755
- return this.runWrangler(["zones", "list"]);
25839
+ const token = process.env.CLOUDFLARE_API_TOKEN;
25840
+ if (!token) {
25841
+ return {
25842
+ ok: false,
25843
+ error: "CLOUDFLARE_API_TOKEN environment variable is not set. Set it or run `wrangler login`."
25844
+ };
25845
+ }
25846
+ try {
25847
+ const response = await fetch("https://api.cloudflare.com/client/v4/zones?per_page=50", {
25848
+ headers: {
25849
+ Authorization: `Bearer ${token}`,
25850
+ "Content-Type": "application/json"
25851
+ }
25852
+ });
25853
+ const json2 = await response.json();
25854
+ if (!response.ok || !json2.success) {
25855
+ const errorMsg = json2.errors?.map((e) => e.message).join("; ") || `HTTP ${response.status}`;
25856
+ return { ok: false, error: `Failed to list zones: ${errorMsg}` };
25857
+ }
25858
+ const lines = json2.result.map((z2) => `${z2.name} (${z2.id})`);
25859
+ return { ok: true, value: lines.join(`
25860
+ `) };
25861
+ } catch (err) {
25862
+ const message = err instanceof Error ? err.message : String(err);
25863
+ return { ok: false, error: `Cloudflare API request failed: ${message}` };
25864
+ }
25756
25865
  }
25757
25866
  extractUrl(stdout2) {
25758
25867
  for (const line of stdout2.split(`
@@ -25976,8 +26085,8 @@ OPTIONS:
25976
26085
 
25977
26086
  EXAMPLES:
25978
26087
  hoox init Interactive wizard
25979
- hoox init --token cfut_xxx --account xxx Non-interactive`).option("--token <token>", "Cloudflare API token (non-interactive)").option("--account <id>", "Cloudflare Account ID (non-interactive)").option("--secret-store <id>", "Secret Store ID (non-interactive)").option("--prefix <prefix>", "Subdomain prefix (non-interactive, default: cryptolinx)").option("--preset <name>", "Worker preset (minimal, standard, full)").option("--resume", "Resume from saved wizard state").option("--accept-risk", "Skip the risk acknowledgment confirmation").action(withErrorHandling(async (options) => {
25980
- const globalOpts = getFormatOptions(program2);
26088
+ hoox init --token cfut_xxx --account xxx Non-interactive`).option("--token <token>", "Cloudflare API token (non-interactive)").option("--account <id>", "Cloudflare Account ID (non-interactive)").option("--secret-store <id>", "Secret Store ID (non-interactive)").option("--prefix <prefix>", "Subdomain prefix (non-interactive, default: cryptolinx)").option("--preset <name>", "Worker preset (minimal, standard, full)").option("--resume", "Resume from saved wizard state").option("--accept-risk", "Skip the risk acknowledgment confirmation").action(withErrorHandling(async (options, cmd) => {
26089
+ const globalOpts = getFormatOptions(cmd);
25981
26090
  const isNonInteractive = Boolean(options.token && options.account);
25982
26091
  await runInitCommand(options, globalOpts, isNonInteractive);
25983
26092
  }, { service: "init" }));
@@ -26642,11 +26751,53 @@ class DockerService {
26642
26751
  }
26643
26752
  isCommandAvailable(cmd) {
26644
26753
  return new Promise((resolve2) => {
26645
- const proc = Bun.spawn(["which", cmd.split(" ")[0]], {
26646
- stdout: "pipe",
26647
- stderr: "pipe"
26754
+ const parts = cmd.split(/\s+/).filter(Boolean);
26755
+ const binary = parts[0];
26756
+ const hasSubcommand = parts.length > 1;
26757
+ let proc;
26758
+ try {
26759
+ proc = Bun.spawn(["which", binary], {
26760
+ stdout: "pipe",
26761
+ stderr: "pipe"
26762
+ });
26763
+ } catch {
26764
+ resolve2(false);
26765
+ return;
26766
+ }
26767
+ const timer = setTimeout(() => {
26768
+ try {
26769
+ proc.kill();
26770
+ } catch {}
26771
+ resolve2(false);
26772
+ }, 5000);
26773
+ proc.exited.then((code) => {
26774
+ clearTimeout(timer);
26775
+ if (code !== 0) {
26776
+ resolve2(false);
26777
+ return;
26778
+ }
26779
+ if (!hasSubcommand) {
26780
+ resolve2(true);
26781
+ return;
26782
+ }
26783
+ try {
26784
+ const subproc = Bun.spawn([binary, ...parts.slice(1), "version"], {
26785
+ stdout: "pipe",
26786
+ stderr: "pipe"
26787
+ });
26788
+ subproc.exited.then((subcode) => {
26789
+ try {
26790
+ subproc.kill();
26791
+ } catch {}
26792
+ resolve2(subcode === 0);
26793
+ }).catch(() => resolve2(true));
26794
+ } catch {
26795
+ resolve2(true);
26796
+ }
26797
+ }).catch(() => {
26798
+ clearTimeout(timer);
26799
+ resolve2(false);
26648
26800
  });
26649
- proc.exited.then((code) => resolve2(code === 0)).catch(() => resolve2(false));
26650
26801
  });
26651
26802
  }
26652
26803
  }
@@ -26684,8 +26835,8 @@ OPTIONS:
26684
26835
  EXAMPLES:
26685
26836
  hoox dev start
26686
26837
  hoox dev start --runtime native
26687
- hoox dev start --runtime docker`).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async (options) => {
26688
- const fmt = getFormatOptions(program2);
26838
+ hoox dev start --runtime docker`).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async (options, cmd) => {
26839
+ const fmt = getFormatOptions(cmd);
26689
26840
  const configService = new ConfigService;
26690
26841
  const prereqs = new PrerequisitesService;
26691
26842
  const docker = new DockerService;
@@ -26803,8 +26954,8 @@ OPTIONS:
26803
26954
  EXAMPLES:
26804
26955
  hoox dev worker trade-worker
26805
26956
  hoox dev worker agent-worker --port 8788
26806
- hoox dev worker hoox --runtime docker`).option("--port <port>", "Dev server port (default: 8787)", (v) => parseInt(v, 10)).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async (name, options) => {
26807
- const fmt = getFormatOptions(program2);
26957
+ hoox dev worker hoox --runtime docker`).option("--port <port>", "Dev server port (default: 8787)", (v) => parseInt(v, 10)).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async (name, options, cmd) => {
26958
+ const fmt = getFormatOptions(cmd);
26808
26959
  const configService = new ConfigService;
26809
26960
  await configService.load();
26810
26961
  const worker = configService.getWorker(name);
@@ -26839,8 +26990,8 @@ OPTIONS:
26839
26990
 
26840
26991
  EXAMPLES:
26841
26992
  hoox dev dashboard
26842
- hoox dev dashboard --runtime docker`).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async () => {
26843
- const fmt = getFormatOptions(program2);
26993
+ hoox dev dashboard --runtime docker`).option("--runtime <native|docker>", "Choose dev runtime (overrides saved preference)").action(withErrorHandling(async (_options, cmd) => {
26994
+ const fmt = getFormatOptions(cmd);
26844
26995
  const dashboardPath = path3.resolve(process.cwd(), "workers/dashboard");
26845
26996
  const dashboardDir = Bun.file(dashboardPath);
26846
26997
  if (!await dashboardDir.exists()) {
@@ -26859,6 +27010,131 @@ EXAMPLES:
26859
27010
  }
26860
27011
  // src/commands/deploy/deploy-command.ts
26861
27012
  init_config2();
27013
+
27014
+ // src/utils/rich.ts
27015
+ async function runRichTasks(tasks2, options = {}) {
27016
+ const rich = isRichMode(options.format);
27017
+ const results = [];
27018
+ if (tasks2.length === 0)
27019
+ return results;
27020
+ const silent = options.format?.json === true;
27021
+ if (rich && !silent) {
27022
+ if (options.title)
27023
+ log.step(theme.heading(options.title));
27024
+ await tasks(tasks2.map((t2) => ({
27025
+ title: t2.title,
27026
+ task: async (message) => {
27027
+ const timer = startTimer();
27028
+ try {
27029
+ const value = await t2.run();
27030
+ const result = {
27031
+ title: t2.title,
27032
+ ok: true,
27033
+ ms: timer.ms(),
27034
+ value
27035
+ };
27036
+ if (t2.details) {
27037
+ result.details = await t2.details(value);
27038
+ }
27039
+ results.push(result);
27040
+ const dur = formatDuration2(result.ms);
27041
+ if (result.details) {
27042
+ const summary = Object.entries(result.details).map(([k, v]) => `${theme.dim(k)}=${theme.value(v)}`).join(" ");
27043
+ message(`${theme.success(icons.success)} ${dur} ${summary}`);
27044
+ } else {
27045
+ message(`${theme.success(icons.success)} ${dur}`);
27046
+ }
27047
+ } catch (err) {
27048
+ const message2 = err instanceof CLIError ? err.message : err instanceof Error ? err.message : String(err);
27049
+ const result = {
27050
+ title: t2.title,
27051
+ ok: false,
27052
+ ms: timer.ms(),
27053
+ error: message2
27054
+ };
27055
+ results.push(result);
27056
+ message(`${theme.error(icons.error)} ${formatDuration2(result.ms)} ${theme.dim(message2)}`);
27057
+ }
27058
+ }
27059
+ })));
27060
+ } else if (silent) {
27061
+ for (const t2 of tasks2) {
27062
+ const timer = startTimer();
27063
+ try {
27064
+ const value = await t2.run();
27065
+ const result = {
27066
+ title: t2.title,
27067
+ ok: true,
27068
+ ms: timer.ms(),
27069
+ value
27070
+ };
27071
+ if (t2.details) {
27072
+ result.details = await t2.details(value);
27073
+ }
27074
+ results.push(result);
27075
+ } catch (err) {
27076
+ const message2 = err instanceof CLIError ? err.message : err instanceof Error ? err.message : String(err);
27077
+ const result = {
27078
+ title: t2.title,
27079
+ ok: false,
27080
+ ms: timer.ms(),
27081
+ error: message2
27082
+ };
27083
+ results.push(result);
27084
+ }
27085
+ }
27086
+ } else {
27087
+ for (const t2 of tasks2) {
27088
+ log.step(t2.title);
27089
+ const timer = startTimer();
27090
+ try {
27091
+ const value = await t2.run();
27092
+ const result = {
27093
+ title: t2.title,
27094
+ ok: true,
27095
+ ms: timer.ms(),
27096
+ value
27097
+ };
27098
+ if (t2.details) {
27099
+ result.details = await t2.details(value);
27100
+ }
27101
+ results.push(result);
27102
+ log.success(`${t2.title} ${theme.dim(formatDuration2(result.ms))}`);
27103
+ } catch (err) {
27104
+ const message2 = err instanceof CLIError ? err.message : err instanceof Error ? err.message : String(err);
27105
+ const result = {
27106
+ title: t2.title,
27107
+ ok: false,
27108
+ ms: timer.ms(),
27109
+ error: message2
27110
+ };
27111
+ results.push(result);
27112
+ log.error(`${t2.title} ${theme.dim(message2)}`);
27113
+ }
27114
+ }
27115
+ }
27116
+ if (results.some((r2) => !r2.ok)) {
27117
+ process.exitCode = 1;
27118
+ }
27119
+ if (options.onSummary) {
27120
+ options.onSummary(results);
27121
+ } else if (!options.format?.json && !options.format?.quiet) {
27122
+ renderDefaultSummary(results);
27123
+ }
27124
+ return results;
27125
+ }
27126
+ function renderDefaultSummary(results) {
27127
+ if (results.length === 0)
27128
+ return;
27129
+ const rows = results.map((r2) => ({
27130
+ Task: r2.title,
27131
+ Status: r2.ok ? theme.success(icons.success) : theme.error(icons.error),
27132
+ Duration: formatDuration2(r2.ms)
27133
+ }));
27134
+ formatTable(rows, {});
27135
+ }
27136
+
27137
+ // src/commands/deploy/deploy-command.ts
26862
27138
  import { statSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
26863
27139
  import { resolve as resolve2 } from "path";
26864
27140
 
@@ -26937,13 +27213,6 @@ class EnvService {
26937
27213
  default: "cryptolinx",
26938
27214
  hint: "Prefix for worker URLs"
26939
27215
  },
26940
- {
26941
- name: "INTERNAL_KEY_BINDING",
26942
- required: true,
26943
- secret: true,
26944
- section: "Internal Auth",
26945
- hint: "Internal key for D1 worker auth"
26946
- },
26947
27216
  {
26948
27217
  name: "TRADE_INTERNAL_KEY",
26949
27218
  required: true,
@@ -27340,62 +27609,61 @@ var DEPLOY_ORDER = [
27340
27609
  "hoox",
27341
27610
  "dashboard"
27342
27611
  ];
27343
- async function deployAll(configService, cf, env, forceRebuildDashboard = false, autoMode = false) {
27612
+ async function deployAll(configService, cf, env, forceRebuildDashboard = false, autoMode = false, format3 = {}) {
27344
27613
  const enabled = configService.listEnabledWorkers();
27345
27614
  const workers = DEPLOY_ORDER.filter((w) => w !== "dashboard" && enabled.includes(w));
27346
27615
  const unknown2 = enabled.filter((w) => w !== "dashboard" && !DEPLOY_ORDER.includes(w));
27347
27616
  const allItems = [...workers, ...unknown2, "dashboard"];
27348
- const results = [];
27349
27617
  if (allItems.length === 0) {
27350
- return results;
27618
+ return [];
27351
27619
  }
27352
- const s = spinner();
27353
- s.start(`Deploying ${allItems.length} item(s)...`);
27354
- for (let i2 = 0;i2 < allItems.length; i2++) {
27355
- const name = allItems[i2];
27356
- const isDashboard = name === "dashboard";
27357
- s.message(`[${i2 + 1}/${allItems.length}] ${name}...`);
27358
- let result;
27359
- if (isDashboard) {
27360
- result = await deployDashboard(cf, forceRebuildDashboard, true, autoMode);
27361
- } else {
27362
- result = await deploySingle(configService, cf, name, env);
27363
- }
27364
- results.push(result);
27365
- if (result.success) {
27366
- s.stop(`${theme.success(icons.success)} ${name} deployed`);
27367
- } else {
27368
- s.stop(`${theme.error(icons.error)} ${name} failed`);
27369
- }
27370
- if (result.success) {
27371
- if (result.url) {
27372
- log.step(` ${theme.dim("URL:")} ${result.url}`);
27373
- }
27374
- if (result.size) {
27375
- log.step(` ${theme.dim("Size:")} ${result.size}`);
27376
- }
27377
- if (result.startupTime) {
27378
- log.step(` ${theme.dim("Startup:")} ${result.startupTime}`);
27620
+ const tasks2 = allItems.map((name) => ({
27621
+ title: name,
27622
+ run: async () => {
27623
+ if (name === "dashboard") {
27624
+ return await deployDashboard(cf, forceRebuildDashboard, true, autoMode);
27379
27625
  }
27380
- if (result.versionId) {
27381
- log.step(` ${theme.dim("Version:")} ${result.versionId.slice(0, 8)}...`);
27382
- }
27383
- if (!result.url && !result.size && !result.startupTime && !result.versionId && result.rawOutput) {
27384
- log.step(` ${theme.dim("Output:")} ${result.rawOutput.slice(0, 80)}`);
27385
- }
27386
- } else if (result.error) {
27387
- log.warn(` ${theme.error("Error:")} ${result.error}`);
27388
- }
27389
- if (i2 < allItems.length - 1) {
27390
- s.start(`Deploying ${allItems.length} item(s)...`);
27626
+ return await deploySingle(configService, cf, name, env);
27627
+ },
27628
+ details: (r2) => {
27629
+ if (!r2.success)
27630
+ return { error: r2.error ?? "unknown" };
27631
+ const d = {};
27632
+ if (r2.url)
27633
+ d.URL = r2.url;
27634
+ if (r2.size)
27635
+ d.Size = r2.size;
27636
+ if (r2.startupTime)
27637
+ d.Startup = r2.startupTime;
27638
+ if (r2.versionId)
27639
+ d.Version = r2.versionId.slice(0, 8) + "\u2026";
27640
+ if (Object.keys(d).length === 0 && r2.rawOutput) {
27641
+ d.Output = r2.rawOutput.slice(0, 80);
27642
+ }
27643
+ return d;
27391
27644
  }
27392
- }
27393
- const succeeded = results.filter((r2) => r2.success).length;
27394
- const failed = results.filter((r2) => !r2.success).length;
27395
- if (failed > 0) {
27396
- log.warn(`Summary: ${succeeded}/${allItems.length} deployed (${failed} failed)`);
27397
- }
27398
- return results;
27645
+ }));
27646
+ const richResults = await runRichTasks(tasks2, {
27647
+ title: `Deploying ${allItems.length} item(s)`,
27648
+ format: format3,
27649
+ onSummary: (results) => renderDeploySummary(results, format3)
27650
+ });
27651
+ return richResults.map((r2) => r2.value).filter(Boolean);
27652
+ }
27653
+ function renderDeploySummary(results, format3) {
27654
+ if (format3.json || format3.quiet)
27655
+ return;
27656
+ const rows = results.map((r2) => {
27657
+ const d = r2.value;
27658
+ return {
27659
+ Worker: r2.title,
27660
+ Status: r2.ok ? formatBadge("ok", "DEPLOYED") : formatBadge("err", "FAILED"),
27661
+ URL: d?.url ?? theme.dim("\u2014"),
27662
+ Size: d?.size ?? theme.dim("\u2014"),
27663
+ Duration: formatDuration3(r2.ms)
27664
+ };
27665
+ });
27666
+ formatTable(rows, {});
27399
27667
  }
27400
27668
  async function deployDashboard(_cf, forceRebuild = false, silentMode = false, autoMode = false) {
27401
27669
  const dashboardPath = "workers/dashboard";
@@ -27803,9 +28071,10 @@ EXAMPLES:
27803
28071
  hoox deploy all
27804
28072
  hoox deploy all --env production
27805
28073
  hoox deploy all --rebuild
27806
- hoox deploy all --auto`).option("--env <env>", "Cloudflare environment (e.g. production, staging)").option("--rebuild", "Force rebuild of dashboard before deploying").option("--auto", "Skip dashboard rebuild prompt, use existing build if available").option("--dry-run", "Preview deployment plan without executing").action(withErrorHandling(async (options) => {
28074
+ hoox deploy all --auto`).option("--env <env>", "Cloudflare environment (e.g. production, staging)").option("--rebuild", "Force rebuild of dashboard before deploying").option("--auto", "Skip dashboard rebuild prompt, use existing build if available").option("--dry-run", "Preview deployment plan without executing").action(withErrorHandling(async (options, cmd) => {
27807
28075
  const configService = new ConfigService;
27808
28076
  await configService.load();
28077
+ const format3 = getFormatOptions(cmd);
27809
28078
  if (options.dryRun) {
27810
28079
  const enabled = configService.listEnabledWorkers();
27811
28080
  const workers = DEPLOY_ORDER.filter((w) => w !== "dashboard" && enabled.includes(w));
@@ -27838,7 +28107,7 @@ ${theme.dim("Run without --dry-run to deploy.")}
27838
28107
  return;
27839
28108
  }
27840
28109
  const cf = new CloudflareService;
27841
- await deployAll(configService, cf, options.env, options.rebuild ?? false, options.auto ?? false);
28110
+ await deployAll(configService, cf, options.env, options.rebuild ?? false, options.auto ?? false, format3);
27842
28111
  }, { service: "deploy" }));
27843
28112
  deployCmd.command("workers").summary("Deploy all enabled workers to Cloudflare").description(`Deploy all enabled workers to Cloudflare Workers.
27844
28113
 
@@ -27927,8 +28196,8 @@ OPTIONS:
27927
28196
 
27928
28197
  EXAMPLES:
27929
28198
  hoox deploy worker trade-worker
27930
- hoox deploy worker agent-worker --env production`).option("--env <env>", "Cloudflare environment (e.g. production, staging)").action(withErrorHandling(async (name, options) => {
27931
- const fmt = getFormatOptions(program2);
28199
+ hoox deploy worker agent-worker --env production`).option("--env <env>", "Cloudflare environment (e.g. production, staging)").action(withErrorHandling(async (name, options, cmd) => {
28200
+ const fmt = getFormatOptions(cmd);
27932
28201
  const configService = new ConfigService;
27933
28202
  await configService.load();
27934
28203
  const cf = new CloudflareService;
@@ -27950,8 +28219,8 @@ OPTIONS:
27950
28219
 
27951
28220
  EXAMPLES:
27952
28221
  hoox deploy dashboard Interactive: choose to rebuild or use existing
27953
- hoox deploy dashboard --rebuild Force rebuild before deploying`).option("--rebuild", "Force rebuild of dashboard before deploying").action(withErrorHandling(async (options) => {
27954
- const fmt = getFormatOptions(program2);
28222
+ hoox deploy dashboard --rebuild Force rebuild before deploying`).option("--rebuild", "Force rebuild of dashboard before deploying").action(withErrorHandling(async (options, cmd) => {
28223
+ const fmt = getFormatOptions(cmd);
27955
28224
  const cf = new CloudflareService;
27956
28225
  const result = await deployDashboard(cf, options.rebuild);
27957
28226
  if (result.success) {
@@ -27977,8 +28246,8 @@ Use --token and --secret-token to override.
27977
28246
  EXAMPLES:
27978
28247
  hoox deploy telegram-webhook
27979
28248
  hoox deploy telegram-webhook --token 123456:ABC-DEF1234
27980
- hoox deploy telegram-webhook --subdomain myapp`).option("--token <token>", "Telegram bot token (from @BotFather)").option("--secret-token <secret>", "Telegram webhook secret token").option("--subdomain <prefix>", "Worker subdomain prefix (default: from config)").action(withErrorHandling(async (options) => {
27981
- const fmt = getFormatOptions(program2);
28249
+ hoox deploy telegram-webhook --subdomain myapp`).option("--token <token>", "Telegram bot token (from @BotFather)").option("--secret-token <secret>", "Telegram webhook secret token").option("--subdomain <prefix>", "Worker subdomain prefix (default: from config)").action(withErrorHandling(async (options, cmd) => {
28250
+ const fmt = getFormatOptions(cmd);
27982
28251
  await doTelegramWebhook(fmt, options.token, options.secretToken, options.subdomain);
27983
28252
  }, { service: "deploy" }));
27984
28253
  deployCmd.command("update-internal-urls").summary("Update dashboard wrangler.jsonc with current service URLs").description(`Update the dashboard's wrangler.jsonc with the current service URLs.
@@ -27987,8 +28256,8 @@ This is a post-deployment step that ensures the dashboard has correct
27987
28256
  service binding URLs for all workers.
27988
28257
 
27989
28258
  EXAMPLES:
27990
- hoox deploy update-internal-urls`).action(withErrorHandling(async () => {
27991
- const fmt = getFormatOptions(program2);
28259
+ hoox deploy update-internal-urls`).action(withErrorHandling(async (_options, cmd) => {
28260
+ const fmt = getFormatOptions(cmd);
27992
28261
  await doUpdateInternalUrls(fmt);
27993
28262
  }, { service: "deploy" }));
27994
28263
  deployCmd.command("kv-config").summary("Apply KV manifest keys post-deployment").description(`Apply the KV manifest key-value pairs after deploying workers.
@@ -27997,8 +28266,8 @@ Sets all KV keys from the manifest to their default values.
27997
28266
  This post-deployment step initializes the CONFIG_KV namespace.
27998
28267
 
27999
28268
  EXAMPLES:
28000
- hoox deploy kv-config`).action(withErrorHandling(async () => {
28001
- const fmt = getFormatOptions(program2);
28269
+ hoox deploy kv-config`).action(withErrorHandling(async (_options, cmd) => {
28270
+ const fmt = getFormatOptions(cmd);
28002
28271
  await doKvConfig(fmt);
28003
28272
  }, { service: "deploy" }));
28004
28273
  deployCmd.command("history <worker>").summary("Show deployment version history for a worker").description(`Display the deployment version history for a Cloudflare Worker.
@@ -28010,8 +28279,8 @@ ARGUMENTS:
28010
28279
 
28011
28280
  EXAMPLES:
28012
28281
  hoox deploy history trade-worker
28013
- hoox deploy history hoox --json`).action(withErrorHandling(async (worker) => {
28014
- const fmt = getFormatOptions(program2);
28282
+ hoox deploy history hoox --json`).action(withErrorHandling(async (worker, _options, cmd) => {
28283
+ const fmt = getFormatOptions(cmd);
28015
28284
  await doVersionHistory(worker, fmt);
28016
28285
  }, { service: "deploy" }));
28017
28286
  deployCmd.command("rollback <worker>").argument("[version]", "Version ID to rollback to (if omitted, prompts to select)").summary("Rollback a worker to a previous version").description(`Rollback a Cloudflare Worker to a previous version.
@@ -28029,8 +28298,8 @@ OPTIONS:
28029
28298
  EXAMPLES:
28030
28299
  hoox deploy rollback trade-worker
28031
28300
  hoox deploy rollback trade-worker <version-id>
28032
- hoox deploy rollback trade-worker <version-id> --yes`).option("--yes", "Skip confirmation prompt").action(withErrorHandling(async (worker, version2, options) => {
28033
- const fmt = getFormatOptions(program2);
28301
+ hoox deploy rollback trade-worker <version-id> --yes`).option("--yes", "Skip confirmation prompt").action(withErrorHandling(async (worker, version2, options, cmd) => {
28302
+ const fmt = getFormatOptions(cmd);
28034
28303
  await doVersionRollback(worker, version2, fmt, options.yes ?? false);
28035
28304
  }, { service: "deploy" }));
28036
28305
  }
@@ -30101,27 +30370,40 @@ ${summary.failed > 0 ? theme.error(icons.error) : theme.success(icons.success)}
30101
30370
  `);
30102
30371
  }
30103
30372
  async function handleSetup(opts) {
30104
- const s = spinner();
30105
30373
  try {
30106
30374
  const configService = new ConfigService;
30107
- s.start("Loading config...");
30108
30375
  await configService.load();
30109
- s.stop("Config loaded");
30110
30376
  const cf = new CloudflareService;
30111
30377
  const secretsService = await SecretsService.create();
30112
30378
  const categories = [];
30113
- s.start("Validating config...");
30114
- categories.push(await runConfigChecks(configService));
30115
- s.stop("Config validation complete");
30116
- s.start("Checking infrastructure (D1, KV, R2, Queues)...");
30117
- categories.push(await runInfraChecks(cf));
30118
- s.stop("Infrastructure checks complete");
30119
- s.start("Checking secrets...");
30120
- categories.push(await runSecretsChecks(secretsService, configService, cf));
30121
- s.stop("Secrets checks complete");
30122
- s.start("Checking database...");
30123
- categories.push(await runDatabaseChecks(cf, configService));
30124
- s.stop("Database checks complete");
30379
+ const tasks2 = [
30380
+ {
30381
+ title: "Validating config",
30382
+ run: async () => await runConfigChecks(configService)
30383
+ },
30384
+ {
30385
+ title: "Checking infrastructure (D1, KV, R2, Queues)",
30386
+ run: async () => await runInfraChecks(cf)
30387
+ },
30388
+ {
30389
+ title: "Checking secrets",
30390
+ run: async () => await runSecretsChecks(secretsService, configService, cf)
30391
+ },
30392
+ {
30393
+ title: "Checking database",
30394
+ run: async () => await runDatabaseChecks(cf, configService)
30395
+ }
30396
+ ];
30397
+ const results = await runRichTasks(tasks2, {
30398
+ title: "Running diagnostics",
30399
+ format: opts,
30400
+ onSummary: (rows) => {
30401
+ for (const r2 of rows) {
30402
+ if (r2.ok && r2.value)
30403
+ categories.push(r2.value);
30404
+ }
30405
+ }
30406
+ });
30125
30407
  const report = buildReport(categories);
30126
30408
  if (opts.json) {
30127
30409
  process.stdout.write(JSON.stringify(report, null, 2) + `
@@ -30132,6 +30414,9 @@ async function handleSetup(opts) {
30132
30414
  if (!report.success) {
30133
30415
  process.exitCode = 1 /* ERROR */;
30134
30416
  }
30417
+ if (results.some((r2) => !r2.ok)) {
30418
+ process.exitCode = 1 /* ERROR */;
30419
+ }
30135
30420
  } catch (err) {
30136
30421
  const message = err instanceof Error ? err.message : String(err);
30137
30422
  formatError2(message, opts);
@@ -30139,7 +30424,6 @@ async function handleSetup(opts) {
30139
30424
  }
30140
30425
  }
30141
30426
  async function handleHealth(opts, autoFix) {
30142
- const s = spinner();
30143
30427
  const results = [];
30144
30428
  try {
30145
30429
  const configService = new ConfigService;
@@ -30150,29 +30434,37 @@ async function handleHealth(opts, autoFix) {
30150
30434
  formatSuccess("No enabled workers to check", opts);
30151
30435
  return;
30152
30436
  }
30153
- s.start(`Health-checking ${enabledWorkers.length} worker(s)...`);
30154
- for (const workerName of enabledWorkers) {
30155
- s.message(`Probing ${workerName}...`);
30156
- try {
30157
- const tailResult = await cf.tail(workerName);
30158
- const healthy = tailResult.ok;
30159
- results.push({
30160
- worker: workerName,
30161
- status: healthy ? "healthy" : "degraded",
30162
- connectivity: healthy,
30163
- error: tailResult.ok ? undefined : tailResult.error ?? "Unknown error"
30164
- });
30165
- } catch (err) {
30166
- const message = err instanceof Error ? err.message : String(err);
30167
- results.push({
30168
- worker: workerName,
30169
- status: "down",
30170
- connectivity: false,
30171
- error: message
30172
- });
30437
+ const tasks2 = enabledWorkers.map((workerName) => ({
30438
+ title: `Probe ${workerName}`,
30439
+ run: async () => {
30440
+ try {
30441
+ const tailResult = await cf.tail(workerName);
30442
+ const healthy = tailResult.ok;
30443
+ return {
30444
+ worker: workerName,
30445
+ status: healthy ? "healthy" : "degraded",
30446
+ connectivity: healthy,
30447
+ error: tailResult.ok ? undefined : tailResult.error ?? "Unknown error"
30448
+ };
30449
+ } catch (err) {
30450
+ const message = err instanceof Error ? err.message : String(err);
30451
+ return {
30452
+ worker: workerName,
30453
+ status: "down",
30454
+ connectivity: false,
30455
+ error: message
30456
+ };
30457
+ }
30173
30458
  }
30459
+ }));
30460
+ const taskResults = await runRichTasks(tasks2, {
30461
+ title: `Health-checking ${enabledWorkers.length} worker(s)`,
30462
+ format: opts
30463
+ });
30464
+ for (const r2 of taskResults) {
30465
+ if (r2.value)
30466
+ results.push(r2.value);
30174
30467
  }
30175
- s.stop("Health check complete");
30176
30468
  if (opts.json) {
30177
30469
  process.stdout.write(JSON.stringify(results, null, 2) + `
30178
30470
  `);
@@ -30197,7 +30489,6 @@ ${theme.warning(icons.warning)} Auto-fix flag set but health issues require manu
30197
30489
  process.exitCode = 1 /* ERROR */;
30198
30490
  }
30199
30491
  } catch (err) {
30200
- s.stop("Health check failed");
30201
30492
  const message = err instanceof Error ? err.message : String(err);
30202
30493
  formatError2(message, opts);
30203
30494
  process.exitCode = 1 /* ERROR */;
@@ -30771,8 +31062,8 @@ async function tailAllWorkers(opts, fmt) {
30771
31062
  }
30772
31063
  function registerLogsCommand(program2) {
30773
31064
  const logsCmd = program2.command("logs").description("Tail and view Cloudflare Worker logs in real-time");
30774
- logsCmd.command("worker <name>").description("Tail logs for a specific Cloudflare Worker").option("--level <level>", "Filter by log level (log, info, warn, error, debug, all)", "all").option("--follow", "Continuously stream logs (default)", true).option("--no-follow", "Show recent logs and exit after 5 seconds").option("--json", "Output logs in JSON format").action(async (name, options) => {
30775
- const fmt = getFormatOptions(program2);
31065
+ logsCmd.command("worker <name>").description("Tail logs for a specific Cloudflare Worker").option("--level <level>", "Filter by log level (log, info, warn, error, debug, all)", "all").option("--follow", "Continuously stream logs (default)", true).option("--no-follow", "Show recent logs and exit after 5 seconds").option("--json", "Output logs in JSON format").action(async (name, options, cmd) => {
31066
+ const fmt = getFormatOptions(cmd);
30776
31067
  const workerOpts = {
30777
31068
  level: normalizeLevel(options.level ?? "all"),
30778
31069
  follow: options.follow !== false,
@@ -30786,8 +31077,8 @@ function registerLogsCommand(program2) {
30786
31077
  process.exitCode = 1 /* ERROR */;
30787
31078
  }
30788
31079
  });
30789
- logsCmd.command("all").description("Tail logs from all enabled workers concurrently").option("--level <level>", "Filter by log level (log, info, warn, error, debug, all)", "all").option("--follow", "Continuously stream logs (default)", true).option("--no-follow", "Show recent logs and exit after 5 seconds").option("--json", "Output logs in JSON format").action(async (options) => {
30790
- const fmt = getFormatOptions(program2);
31080
+ logsCmd.command("all").description("Tail logs from all enabled workers concurrently").option("--level <level>", "Filter by log level (log, info, warn, error, debug, all)", "all").option("--follow", "Continuously stream logs (default)", true).option("--no-follow", "Show recent logs and exit after 5 seconds").option("--json", "Output logs in JSON format").action(async (options, cmd) => {
31081
+ const fmt = getFormatOptions(cmd);
30791
31082
  const workerOpts = {
30792
31083
  level: normalizeLevel(options.level ?? "all"),
30793
31084
  follow: options.follow !== false,
@@ -30896,21 +31187,22 @@ var PIPELINE_STEPS = [
30896
31187
  ];
30897
31188
  function registerTestCommand(program2) {
30898
31189
  const testCmd = program2.command("test").description("Run tests and CI pipeline");
30899
- testCmd.command("all").description("Run full CI pipeline: lint \u2192 typecheck \u2192 unit tests \u2192 integration tests").option("--json", "Output results as JSON").action(withErrorHandling(async (options) => {
30900
- const fmt = getFormatOptions(program2);
31190
+ testCmd.command("all").description("Run full CI pipeline: lint \u2192 typecheck \u2192 unit tests \u2192 integration tests").option("--json", "Output results as JSON").action(withErrorHandling(async (options, cmd) => {
31191
+ const fmt = getFormatOptions(cmd);
30901
31192
  const useJson = options.json || fmt.json;
30902
31193
  const opts = { json: useJson, quiet: fmt.quiet };
30903
31194
  const results = [];
30904
31195
  const s = spinner();
31196
+ const pipelineTimer = startTimer();
30905
31197
  s.start("Running CI pipeline...");
30906
31198
  for (const step of PIPELINE_STEPS) {
30907
31199
  s.message(`${theme.info(icons.info)} ${step.label}...`);
30908
31200
  const result = await runStep(step.args, process.cwd());
30909
31201
  results.push({ ...result, step: step.label });
30910
31202
  if (result.success) {
30911
- s.message(` ${theme.success(icons.success)} ${step.label} passed (${result.duration}ms)`);
31203
+ s.message(` ${theme.success(icons.success)} ${step.label} passed (${formatDuration3(result.duration)})`);
30912
31204
  } else {
30913
- s.message(` ${theme.error(icons.error)} ${step.label} failed (${result.duration}ms)`);
31205
+ s.message(` ${theme.error(icons.error)} ${step.label} failed (${formatDuration3(result.duration)})`);
30914
31206
  if (result.error) {
30915
31207
  s.message(theme.dim(result.error.slice(0, 500)));
30916
31208
  }
@@ -30923,14 +31215,14 @@ function registerTestCommand(program2) {
30923
31215
  failed: results.filter((r2) => !r2.success).length,
30924
31216
  results
30925
31217
  };
30926
- s.stop(summary.failed > 0 ? `Pipeline complete: ${summary.passed} passed, ${summary.failed} failed` : "Pipeline complete");
31218
+ s.stop(summary.failed > 0 ? `Pipeline complete: ${summary.passed} passed, ${summary.failed} failed (${formatDuration3(pipelineTimer.ms())})` : `Pipeline complete (${formatDuration3(pipelineTimer.ms())})`);
30927
31219
  printSummary(summary, opts);
30928
31220
  if (summary.failed > 0) {
30929
31221
  process.exitCode = 1 /* ERROR */;
30930
31222
  }
30931
31223
  }, { service: "test" }));
30932
- testCmd.command("unit").description("Run unit tests with bun test").option("--coverage", "Run with coverage reporting").action(async (options) => {
30933
- const fmt = getFormatOptions(program2);
31224
+ testCmd.command("unit").description("Run unit tests with bun test").option("--coverage", "Run with coverage reporting").action(async (options, cmd) => {
31225
+ const fmt = getFormatOptions(cmd);
30934
31226
  const args = ["bun", "test"];
30935
31227
  if (options.coverage)
30936
31228
  args.push("--coverage");
@@ -30942,8 +31234,8 @@ function registerTestCommand(program2) {
30942
31234
  process.exitCode = 1 /* ERROR */;
30943
31235
  }
30944
31236
  });
30945
- testCmd.command("integration").description("Run integration tests with vitest").option("--coverage", "Run with coverage reporting").action(async (options) => {
30946
- const fmt = getFormatOptions(program2);
31237
+ testCmd.command("integration").description("Run integration tests with vitest").option("--coverage", "Run with coverage reporting").action(async (options, cmd) => {
31238
+ const fmt = getFormatOptions(cmd);
30947
31239
  const args = ["vitest", "run", "--config", "vitest.config.ts"];
30948
31240
  if (options.coverage)
30949
31241
  args.push("--coverage");
@@ -30955,8 +31247,8 @@ function registerTestCommand(program2) {
30955
31247
  process.exitCode = 1 /* ERROR */;
30956
31248
  }
30957
31249
  });
30958
- testCmd.command("worker <name>").description("Run tests for a specific worker by name").option("--coverage", "Run with coverage reporting").action(withErrorHandling(async (name, options) => {
30959
- const fmt = getFormatOptions(program2);
31250
+ testCmd.command("worker <name>").description("Run tests for a specific worker by name").option("--coverage", "Run with coverage reporting").action(withErrorHandling(async (name, options, cmd) => {
31251
+ const fmt = getFormatOptions(cmd);
30960
31252
  const configService = new ConfigService;
30961
31253
  await configService.load();
30962
31254
  const workerConfig = configService.getWorker(name);
@@ -30979,14 +31271,16 @@ function registerTestCommand(program2) {
30979
31271
  }, { service: "test" }));
30980
31272
  testCmd.command("live").description("Run live Cloudflare service integration tests (no mocks)").option("--service <name>", "Test a specific service: d1, kv, r2, queues, ai, api, secrets, durable-objects").action(withErrorHandling(async (options) => {
30981
31273
  const s = spinner();
31274
+ const liveTimer = startTimer();
30982
31275
  let filePattern = options.service ? `tests/live/${options.service}.test.ts` : "tests/live/";
30983
31276
  s.start(`Running live tests${options.service ? ` for ${options.service}` : ""}...`);
30984
31277
  const args = ["bun", "test", filePattern, "--jobs", "1"];
30985
31278
  const result = await runWithInherit(args, process.cwd());
31279
+ const dur = formatDuration3(liveTimer.ms());
30986
31280
  if (result.success) {
30987
- s.stop("Live tests complete");
31281
+ s.stop(`Live tests complete (${dur})`);
30988
31282
  } else {
30989
- s.stop(`Live tests: some failures (exit code ${result.exitCode})`);
31283
+ s.stop(`Live tests: some failures (exit code ${result.exitCode}, ${dur})`);
30990
31284
  process.exitCode = 1 /* ERROR */;
30991
31285
  }
30992
31286
  }, { service: "test" }));
@@ -31344,8 +31638,8 @@ EXAMPLES:
31344
31638
  hoox clone --all --home Clone all workers to $HOME/.hoox/workers
31345
31639
  hoox clone trade-worker Clone specific worker
31346
31640
  hoox clone trade-worker --home Clone specific worker to $HOME/.hoox/workers
31347
- hoox clone --org my-org Clone from specific org`).option("--all", "Clone all worker repositories").option("--home", "Clone into $HOME/.hoox/workers instead of default location").option("--org <org>", "GitHub organization (derived from git remote by default)").argument("[name]", "Worker name to clone (omit to list status)").action(withErrorHandling(async (name, options) => {
31348
- const fmt = getFormatOptions(program2);
31641
+ hoox clone --org my-org Clone from specific org`).option("--all", "Clone all worker repositories").option("--home", "Clone into $HOME/.hoox/workers instead of default location").option("--org <org>", "GitHub organization (derived from git remote by default)").argument("[name]", "Worker name to clone (omit to list status)").action(withErrorHandling(async (name, options, cmd) => {
31642
+ const fmt = getFormatOptions(cmd);
31349
31643
  const configService = new ConfigService;
31350
31644
  await configService.load();
31351
31645
  const workers = configService.listWorkers();
@@ -31380,6 +31674,7 @@ EXAMPLES:
31380
31674
  }
31381
31675
  const repoBase = options.org ? `https://github.com/${options.org}` : await resolveRepoBase();
31382
31676
  const s = spinner();
31677
+ const cloneTimer = startTimer();
31383
31678
  const location = options.home ? "$HOME/.hoox/workers" : "workers/";
31384
31679
  s.start(`Cloning ${workers.length} worker(s) to ${location}...`);
31385
31680
  const results = [];
@@ -31419,10 +31714,10 @@ EXAMPLES:
31419
31714
  const succeeded = results.filter((r2) => r2.cloned).length;
31420
31715
  const failed = results.filter((r2) => !r2.cloned).length;
31421
31716
  if (failed > 0) {
31422
- s.stop(`Clone complete: ${succeeded} succeeded, ${failed} failed`);
31717
+ s.stop(`Clone complete: ${succeeded} succeeded, ${failed} failed (${formatDuration3(cloneTimer.ms())})`);
31423
31718
  process.exitCode = 1 /* ERROR */;
31424
31719
  } else {
31425
- s.stop(`All ${succeeded} worker(s) cloned successfully`);
31720
+ s.stop(`All ${succeeded} worker(s) cloned successfully (${formatDuration3(cloneTimer.ms())})`);
31426
31721
  }
31427
31722
  if (!fmt.quiet) {
31428
31723
  const rows = results.map((r2) => ({
@@ -31450,6 +31745,7 @@ EXAMPLES:
31450
31745
  }
31451
31746
  const repoBase = options.org ? `https://github.com/${options.org}` : await resolveRepoBase();
31452
31747
  const s = spinner();
31748
+ const singleTimer = startTimer();
31453
31749
  const location = options.home ? "$HOME/.hoox/workers" : "workers/";
31454
31750
  s.start(`Cloning ${name} to ${location}...`);
31455
31751
  const result = await cloneWorker(getRepoUrl(repoBase, name), workerConfig.path, name, process.cwd(), homeDir);
@@ -31458,11 +31754,11 @@ EXAMPLES:
31458
31754
  s.message("Updating submodules...");
31459
31755
  await updateSubmodules(process.cwd());
31460
31756
  } catch {}
31461
- s.stop(`Successfully cloned ${name}`);
31757
+ s.stop(`Successfully cloned ${name} (${formatDuration3(singleTimer.ms())})`);
31462
31758
  const locationText = options.home ? " at $HOME/.hoox/workers" : "";
31463
31759
  formatSuccess(`Cloned ${name}${locationText} from ${result.repo}`, fmt);
31464
31760
  } else {
31465
- s.stop(`Failed to clone ${name}`);
31761
+ s.stop(`Failed to clone ${name} (${formatDuration3(singleTimer.ms())})`);
31466
31762
  formatError2(new CLIError(`Failed to clone "${name}": ${result.error}`, 1 /* ERROR */), fmt);
31467
31763
  process.exitCode = 1 /* ERROR */;
31468
31764
  }
@@ -31650,16 +31946,23 @@ class DbService {
31650
31946
  return stdout2.trim();
31651
31947
  }
31652
31948
  async readMigrationSql() {
31949
+ const path5 = this.resolveMigrationScriptPath();
31950
+ let file2;
31653
31951
  try {
31654
- const file2 = Bun.file(this.resolveMigrationScriptPath());
31655
- if (await file2.exists()) {
31656
- const content = await file2.text();
31657
- const match = content.match(/d1\s+execute\s+\S+\s+--command=["'](.+?)["']/s);
31658
- if (match)
31659
- return match[1];
31660
- }
31661
- } catch {}
31662
- return "SELECT 1";
31952
+ file2 = Bun.file(path5);
31953
+ } catch (e) {
31954
+ const message = e instanceof Error ? e.message : String(e);
31955
+ throw new Error(`Migration script "${path5}" could not be read: ${message}. ` + `Refusing to run no-op migration.`, { cause: e });
31956
+ }
31957
+ if (!await file2.exists()) {
31958
+ throw new Error(`Migration script "${path5}" not found. ` + `Refusing to run no-op migration. Create the script or run \`hoox setup\` first.`);
31959
+ }
31960
+ const content = await file2.text();
31961
+ const match = content.match(/d1\s+execute\s+\S+\s+--command=["'](.+?)["']/s);
31962
+ if (!match) {
31963
+ throw new Error(`Migration script "${path5}" was found but no \`d1 execute \u2026 --command="\u2026"\` line could be extracted. ` + `Refusing to fall back to a no-op migration \u2014 please check the script format.`);
31964
+ }
31965
+ return match[1];
31663
31966
  }
31664
31967
  }
31665
31968
  // src/commands/db/db-command.ts
@@ -31670,34 +31973,36 @@ async function resolveDb(cmd, svc) {
31670
31973
  async function handleApply(opts, dbName, remote, file2) {
31671
31974
  const svc = new DbService;
31672
31975
  const s = spinner();
31976
+ const t2 = startTimer();
31673
31977
  s.start("Applying schema...");
31674
31978
  try {
31675
31979
  const output = await svc.apply(dbName, remote, file2);
31676
- s.stop("Schema applied");
31980
+ s.stop(`Schema applied (${formatDuration3(t2.ms())})`);
31677
31981
  formatSuccess(`Schema applied to ${dbName}${remote ? " (remote)" : " (local)"}`, opts);
31678
31982
  if (!opts.quiet && output) {
31679
31983
  process.stdout.write(`${output}
31680
31984
  `);
31681
31985
  }
31682
31986
  } catch (err) {
31683
- s.stop("Schema apply failed");
31987
+ s.stop(`Schema apply failed (${formatDuration3(t2.ms())})`);
31684
31988
  throw err;
31685
31989
  }
31686
31990
  }
31687
31991
  async function handleMigrate(opts, dbName, remote) {
31688
31992
  const svc = new DbService;
31689
31993
  const s = spinner();
31994
+ const t2 = startTimer();
31690
31995
  s.start("Running migrations...");
31691
31996
  try {
31692
31997
  const output = await svc.migrate(dbName, remote);
31693
- s.stop("Migrations complete");
31998
+ s.stop(`Migrations complete (${formatDuration3(t2.ms())})`);
31694
31999
  formatSuccess(`Migrations applied to ${dbName}${remote ? " (remote)" : " (local)"}`, opts);
31695
32000
  if (!opts.quiet && output) {
31696
32001
  process.stdout.write(`${output}
31697
32002
  `);
31698
32003
  }
31699
32004
  } catch (err) {
31700
- s.stop("Migrations failed");
32005
+ s.stop(`Migrations failed (${formatDuration3(t2.ms())})`);
31701
32006
  throw err;
31702
32007
  }
31703
32008
  }
@@ -31716,7 +32021,37 @@ async function handleList2(opts, dbName, remote) {
31716
32021
  formatTable(rows, opts);
31717
32022
  }
31718
32023
  }
31719
- async function handleQuery(opts, dbName, sql, remote) {
32024
+ var DESTRUCTIVE_SQL_KEYWORDS = [
32025
+ "DROP",
32026
+ "TRUNCATE",
32027
+ "ALTER",
32028
+ "DELETE",
32029
+ "UPDATE",
32030
+ "REPLACE",
32031
+ "INSERT",
32032
+ "CREATE",
32033
+ "ATTACH",
32034
+ "DETACH",
32035
+ "REINDEX",
32036
+ "VACUUM"
32037
+ ];
32038
+ function findDestructiveKeywords(sql) {
32039
+ const upper = sql.toUpperCase();
32040
+ const found = [];
32041
+ for (const kw of DESTRUCTIVE_SQL_KEYWORDS) {
32042
+ const re = new RegExp(`(^|[^A-Z0-9_])${kw}([^A-Z0-9_]|$)`, "g");
32043
+ if (re.test(upper))
32044
+ found.push(kw);
32045
+ }
32046
+ return found;
32047
+ }
32048
+ async function handleQuery(opts, dbName, sql, remote, allowDestructive) {
32049
+ if (!allowDestructive) {
32050
+ const matches = findDestructiveKeywords(sql);
32051
+ if (matches.length > 0) {
32052
+ throw new Error(`Destructive SQL keywords detected: ${matches.join(", ")}. ` + `Re-run with --allow-destructive to confirm. ` + `(Keywords: ${DESTRUCTIVE_SQL_KEYWORDS.join(", ")})`);
32053
+ }
32054
+ }
31720
32055
  const svc = new DbService;
31721
32056
  const output = await svc.query(dbName, sql, remote);
31722
32057
  if (opts.json) {
@@ -31735,13 +32070,14 @@ async function handleQuery(opts, dbName, sql, remote) {
31735
32070
  async function handleExport(opts, dbName, outputPath) {
31736
32071
  const svc = new DbService;
31737
32072
  const s = spinner();
32073
+ const t2 = startTimer();
31738
32074
  s.start("Exporting database...");
31739
32075
  try {
31740
32076
  const path5 = await svc.export(dbName, outputPath);
31741
- s.stop("Database exported");
32077
+ s.stop(`Database exported (${formatDuration3(t2.ms())})`);
31742
32078
  formatSuccess(`Database exported to ${path5}`, opts);
31743
32079
  } catch (err) {
31744
- s.stop("Export failed");
32080
+ s.stop(`Export failed (${formatDuration3(t2.ms())})`);
31745
32081
  throw err;
31746
32082
  }
31747
32083
  }
@@ -31758,17 +32094,18 @@ async function handleReset(opts, dbName, confirmed) {
31758
32094
  }
31759
32095
  const svc = new DbService;
31760
32096
  const s = spinner();
32097
+ const t2 = startTimer();
31761
32098
  s.start("Resetting database...");
31762
32099
  try {
31763
32100
  const output = await svc.reset(dbName);
31764
- s.stop("Database reset");
32101
+ s.stop(`Database reset (${formatDuration3(t2.ms())})`);
31765
32102
  formatSuccess(`Database "${dbName}" has been recreated`, opts);
31766
32103
  if (!opts.quiet && output) {
31767
32104
  process.stdout.write(`${output}
31768
32105
  `);
31769
32106
  }
31770
32107
  } catch (err) {
31771
- s.stop("Reset failed");
32108
+ s.stop(`Reset failed (${formatDuration3(t2.ms())})`);
31772
32109
  throw err;
31773
32110
  }
31774
32111
  }
@@ -31816,12 +32153,13 @@ EXAMPLES:
31816
32153
  const remote = Boolean(cmd.optsWithGlobals().remote);
31817
32154
  await handleList2(opts, dbName, remote);
31818
32155
  }, { service: "db" }));
31819
- dbCmd.command("query <sql>").description("Execute a SQL query").action(withErrorHandling(async (sql, _2, cmd) => {
32156
+ dbCmd.command("query <sql>").description("Execute a SQL query").option("--allow-destructive", "Allow destructive keywords (DROP, DELETE, TRUNCATE, ALTER, UPDATE)").action(withErrorHandling(async (sql, options, cmd) => {
31820
32157
  const opts = getFormatOptions(cmd);
31821
32158
  const svc = new DbService;
31822
32159
  const dbName = await resolveDb(cmd, svc);
31823
32160
  const remote = Boolean(cmd.optsWithGlobals().remote);
31824
- await handleQuery(opts, dbName, sql, remote);
32161
+ const allowDestructive = Boolean(options.allowDestructive);
32162
+ await handleQuery(opts, dbName, sql, remote, allowDestructive);
31825
32163
  }, { service: "db" }));
31826
32164
  dbCmd.command("export").description("Export database to .sql file").option("--output <path>", "Output file path").action(withErrorHandling(async (options, cmd) => {
31827
32165
  const opts = getFormatOptions(cmd);
@@ -31916,11 +32254,20 @@ ${theme.heading("Summary:")} ${result.healthyCount} healthy, ${result.degradedCo
31916
32254
  process.exitCode = 1 /* ERROR */;
31917
32255
  }
31918
32256
  }
32257
+ function assertSafeInteger(value, name, max) {
32258
+ if (!Number.isInteger(value) || value < 0 || value > max || !Number.isSafeInteger(value)) {
32259
+ throw new Error(`Invalid ${name}: ${value} (must be a non-negative integer \u2264 ${max})`);
32260
+ }
32261
+ }
32262
+ function escapeSqlString(value) {
32263
+ return value.replace(/'/g, "''");
32264
+ }
31919
32265
  async function doMonitorTrades(limit, fmt) {
31920
32266
  try {
32267
+ assertSafeInteger(limit, "limit", 100);
31921
32268
  const db = new DbService;
31922
32269
  const dbName = await db.resolveDbName();
31923
- const sql = `SELECT * FROM trades ORDER BY timestamp DESC LIMIT ${Math.min(limit, 100)}`;
32270
+ const sql = `SELECT * FROM trades ORDER BY timestamp DESC LIMIT ${limit}`;
31924
32271
  const output = await db.query(dbName, sql, true);
31925
32272
  process.stdout.write(output + `
31926
32273
  `);
@@ -31938,7 +32285,7 @@ async function doMonitorLogs(workerName, fmt) {
31938
32285
  if (!/^[a-zA-Z0-9_-]+$/.test(workerName)) {
31939
32286
  throw new Error(`Invalid worker name: "${workerName}"`);
31940
32287
  }
31941
- sql = `SELECT * FROM system_logs WHERE worker = '${workerName}' ORDER BY timestamp DESC LIMIT 20`;
32288
+ sql = `SELECT * FROM system_logs WHERE worker = '${escapeSqlString(workerName)}' ORDER BY timestamp DESC LIMIT 20`;
31942
32289
  } else {
31943
32290
  sql = "SELECT * FROM system_logs ORDER BY timestamp DESC LIMIT 20";
31944
32291
  }
@@ -32052,6 +32399,7 @@ async function doMonitorAnalyticsSummary(fmt) {
32052
32399
  }
32053
32400
  async function doMonitorAnalyticsErrors(hours, fmt) {
32054
32401
  try {
32402
+ assertSafeInteger(hours, "hours", 8760);
32055
32403
  const db = new DbService;
32056
32404
  const dbName = await db.resolveDbName();
32057
32405
  const sql = `SELECT level, COUNT(*) as count FROM system_logs WHERE level IN ('error', 'warn') AND timestamp > unixepoch('now', '-${hours} hours') GROUP BY level`;
@@ -32713,14 +33061,18 @@ EXAMPLES:
32713
33061
  throw new CLIError(`"${submodulePath}" is not a git submodule \u2014 nothing to update`, 2 /* INVALID_USAGE */);
32714
33062
  }
32715
33063
  const s = spinner();
33064
+ const t2 = startTimer();
32716
33065
  s.start(`Updating ${target}...`);
32717
33066
  const output = await gitSubmoduleUpdate(cwd, submodulePath);
32718
- s.stop(output ? theme.success(`Updated ${target}`) : theme.muted(`${target} already up to date`));
33067
+ const dur = formatDuration3(t2.ms());
33068
+ s.stop(output ? theme.success(`Updated ${target} (${dur})`) : theme.muted(`${target} already up to date (${dur})`));
32719
33069
  } else {
32720
33070
  const s = spinner();
33071
+ const t2 = startTimer();
32721
33072
  s.start("Pulling latest from remote...");
32722
33073
  const output = await gitPull(cwd);
32723
- s.stop(output ? theme.success("Repository up to date") : theme.muted("Already up to date"));
33074
+ const dur = formatDuration3(t2.ms());
33075
+ s.stop(output ? theme.success(`Repository up to date (${dur})`) : theme.muted(`Already up to date (${dur})`));
32724
33076
  }
32725
33077
  }, { service: "update" }));
32726
33078
  }
@@ -32807,7 +33159,7 @@ function registerSchemaCommand(program2) {
32807
33159
  import { spawn } from "child_process";
32808
33160
  import { resolve as resolve7, dirname } from "path";
32809
33161
  import { fileURLToPath } from "url";
32810
- import { existsSync as existsSync5 } from "fs";
33162
+ import { existsSync as existsSync6 } from "fs";
32811
33163
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
32812
33164
  function resolveTUIEntry() {
32813
33165
  const candidates = [
@@ -32816,7 +33168,7 @@ function resolveTUIEntry() {
32816
33168
  resolve7(process.cwd(), "../tui/src/main.tsx")
32817
33169
  ];
32818
33170
  for (const path5 of candidates) {
32819
- if (existsSync5(path5))
33171
+ if (existsSync6(path5))
32820
33172
  return path5;
32821
33173
  }
32822
33174
  throw new CLIError("Could not find TUI entry point. Ensure packages/tui/src/main.tsx exists.", 1 /* ERROR */);
@@ -33005,12 +33357,13 @@ EXAMPLES:
33005
33357
  }
33006
33358
  // src/services/setup/setup-service.ts
33007
33359
  import {
33008
- existsSync as existsSync6,
33360
+ existsSync as existsSync7,
33009
33361
  mkdirSync as mkdirSync3,
33010
33362
  readFileSync as readFileSync6,
33011
33363
  statSync as statSync2,
33012
33364
  writeFileSync as writeFileSync4
33013
33365
  } from "fs";
33366
+ import { homedir as homedir4 } from "os";
33014
33367
  import { join as join6 } from "path";
33015
33368
  var ALL_WORKERS = [
33016
33369
  "d1-worker",
@@ -33072,7 +33425,7 @@ class SetupService {
33072
33425
  WEBHOOK_API_KEY_BINDING: this._randomHex(32),
33073
33426
  TELEGRAM_INTERNAL_KEY_BINDING: internalKey
33074
33427
  };
33075
- if (!existsSync6(KEYS_DIR)) {
33428
+ if (!existsSync7(KEYS_DIR)) {
33076
33429
  mkdirSync3(KEYS_DIR, { recursive: true });
33077
33430
  } else if (!statSync2(KEYS_DIR).isDirectory()) {
33078
33431
  mkdirSync3(KEYS_DIR, { recursive: true });
@@ -33377,8 +33730,7 @@ class SetupService {
33377
33730
  }
33378
33731
  const fallbacks = [
33379
33732
  "/usr/local/bin/wrangler",
33380
- "/home/jango/.bun/install/global/bin/wrangler",
33381
- join6(process.env.HOME || "~", ".bun", "bin", "wrangler")
33733
+ join6(homedir4(), ".bun", "bin", "wrangler")
33382
33734
  ];
33383
33735
  for (const fb of fallbacks) {
33384
33736
  try {
@@ -33451,7 +33803,7 @@ class SetupService {
33451
33803
  step: "workers",
33452
33804
  message: `${missing.length} worker(s) not cloned: ${missing.join(", ")}`
33453
33805
  });
33454
- if (!existsSync6(gitmodulesPath)) {
33806
+ if (!existsSync7(gitmodulesPath)) {
33455
33807
  this.onProgress({
33456
33808
  type: "step-error",
33457
33809
  step: "workers",
@@ -34052,7 +34404,7 @@ EXAMPLES:
34052
34404
  const authOk = await tempSvc.checkAuth();
34053
34405
  if (!authOk) {
34054
34406
  authCheck.stop("Authentication failed");
34055
- formatError2(new CLIError("Not authenticated with Cloudflare. Run 'wrangler login' first.", 1 /* ERROR */), globalOpts);
34407
+ formatError2(new CLIError("Not authenticated with Cloudflare. Run 'wrangler login' first.", 1 /* ERROR */, undefined, false, "Run `wrangler login` interactively, or set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables for CI / non-interactive use."), globalOpts);
34056
34408
  outro(theme.error("Setup aborted"));
34057
34409
  process.exitCode = 1 /* ERROR */;
34058
34410
  return;
@@ -34091,12 +34443,14 @@ EXAMPLES:
34091
34443
  currentSpinner.stop(msg);
34092
34444
  currentSpinner = null;
34093
34445
  }));
34446
+ const totalTimer = startTimer();
34094
34447
  const result = await setupSvc.runAll(opts);
34448
+ const totalMs = totalTimer.ms();
34095
34449
  if (isInteractive) {
34096
- log.step("\u2500\u2500 Summary \u2500\u2500");
34450
+ log.step(`\u2500\u2500 Summary (total ${formatDuration3(totalMs)}) \u2500\u2500`);
34097
34451
  const rows = result.steps.map((s) => ({
34098
34452
  Step: s.step,
34099
- Status: s.success ? theme.success("\u2713") : theme.error("\u2717"),
34453
+ Status: s.success ? formatBadge("ok", "DONE") : formatBadge("err", "FAIL"),
34100
34454
  Message: s.message
34101
34455
  }));
34102
34456
  formatTable(rows, globalOpts);