@hoox-sh/hoox-cli 0.11.2 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **Runtime requirement:** Bun ≥ 1.2. The bin shebang and bundle target are Bun-only; `npm install -g` will install the package but the CLI will not run under Node.js.
8
8
 
9
- **v0.10.1** — operator security plane: fail-closed `tui --remote`, transport profile (Bearer + Access), `doctor --security`, `tunnel check`, management `/v1/*` contract. **v0.9.x** shipped hop-level `perf fastpath` observability and reliability fixes.
9
+ **v0.11.x** — operator security plane (`doctor --security`, `tunnel check`, transport profile Bearer + Access), PYNE edge ops (`hoox pyne`), schema validate/generate, and wrangler **4.x**. **v0.10.x** hardened fail-closed remote TUI / management `/v1/*`. **v0.9.x** shipped hop-level `perf fastpath` observability.
10
10
 
11
11
  ## Features
12
12
 
@@ -24,7 +24,7 @@
24
24
  - **Repair & Recovery** (`hoox repair`): Comprehensive system check, guided rebuild, per-component repair.
25
25
  - **Top-level Secrets & Keys**: `hoox secrets` and `hoox keys` for Cloudflare Worker secrets and internal auth keys.
26
26
  - **Unified Dashboard** (`hoox dashboard dev | deploy`): Start the dev server or build & deploy the Next.js dashboard.
27
- - **Interactive menu & TUI**: Running `hoox` with no arguments opens the interactive menu; `hoox tui` launches the full OpenTUI operations center.
27
+ - **Interactive menu & TUI**: Running `hoox` with **no arguments**: if `wrangler.jsonc` is missing, auto-runs `onboard` (or `onboard --resume` when `.wizard-state.json` exists); otherwise opens the interactive menu. `hoox tui` launches the full OpenTUI operations center.
28
28
  - **Operator security plane** (`hoox doctor`, `hoox tunnel`): Path/TUI hygiene, fail-closed remote transport (Bearer + Access), and private-ingress checks.
29
29
  - **Modern-minimal output**: Zinc/indigo palette, completion footer with “next step” suggestions, “did you mean …” for typos, custom help formatter, `--no-color` / `NO_COLOR` / `TERM=dumb`, compact `formatNumber` / `formatBytes`. Shared `format*` helpers available to plugin authors.
30
30
  - **Shell Completions**: bash, zsh, and fish completion scripts via `hoox completion <shell>`.
package/bin/hoox.js CHANGED
@@ -24,6 +24,20 @@
24
24
  * when the module is loaded as a side effect from here.
25
25
  */
26
26
 
27
+ // Runtime guard before any dynamic import: Node users (wrong shebang, or
28
+ // `node bin/hoox.js`) get a clear message instead of ESM resolution stacks.
29
+ // The same check lives in src/index.ts for direct source entry; this one
30
+ // covers the bin path when Node loads this file without Bun globals.
31
+ if (typeof globalThis.Bun === "undefined") {
32
+ process.stderr.write(
33
+ "Error: the Hoox CLI requires Bun >= 1.2 to run.\n" +
34
+ " Install Bun: curl -fsSL https://bun.sh | bash\n" +
35
+ " Then run: bunx hoox <command>\n" +
36
+ " (npm install -g / node bin/hoox.js will not work.)\n"
37
+ );
38
+ process.exit(1);
39
+ }
40
+
27
41
  import { existsSync, statSync, readdirSync } from "node:fs";
28
42
  import { fileURLToPath } from "node:url";
29
43
  import { dirname, resolve, join } from "node:path";
package/dist/index.js CHANGED
@@ -25067,7 +25067,7 @@ class ConfigService {
25067
25067
  const e = result.error;
25068
25068
  switch (e.code) {
25069
25069
  case "not-found":
25070
- throw new Error(e.triedFallback ? `Config file not found: ${e.path}. Also tried: ${e.triedFallback}. Run 'hoox config init' to create one.` : `Config file not found: ${e.path}. Run 'hoox config init' to create one.`);
25070
+ throw new Error(e.triedFallback ? `Config file not found: ${e.path}. Also tried: ${e.triedFallback}. Run 'hoox onboard' (or 'hoox init') to create one.` : `Config file not found: ${e.path}. Run 'hoox onboard' (or 'hoox init') to create one.`);
25071
25071
  case "invalid-jsonc":
25072
25072
  throw new Error(`Invalid JSONC in ${e.path}:
25073
25073
  ${e.errors.map((m3) => ` - ${m3}`).join(`
@@ -25548,6 +25548,33 @@ var init_cloudflare_service = __esm(() => {
25548
25548
  init_wrangler_output();
25549
25549
  });
25550
25550
 
25551
+ // src/utils/fs-secure.ts
25552
+ var exports_fs_secure = {};
25553
+ __export(exports_fs_secure, {
25554
+ secureSecretFile: () => secureSecretFile,
25555
+ secureChmod: () => secureChmod,
25556
+ ensureSecretDir: () => ensureSecretDir,
25557
+ SECRET_FILE_MODE: () => SECRET_FILE_MODE,
25558
+ SECRET_DIR_MODE: () => SECRET_DIR_MODE
25559
+ });
25560
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync3 } from "fs";
25561
+ function secureChmod(path4, mode) {
25562
+ try {
25563
+ chmodSync3(path4, mode);
25564
+ } catch {}
25565
+ }
25566
+ function ensureSecretDir(dir) {
25567
+ try {
25568
+ mkdirSync3(dir, { recursive: true, mode: SECRET_DIR_MODE });
25569
+ } catch {}
25570
+ secureChmod(dir, SECRET_DIR_MODE);
25571
+ }
25572
+ function secureSecretFile(path4) {
25573
+ secureChmod(path4, SECRET_FILE_MODE);
25574
+ }
25575
+ var SECRET_FILE_MODE = 384, SECRET_DIR_MODE = 448;
25576
+ var init_fs_secure = () => {};
25577
+
25551
25578
  // src/services/prerequisites/prerequisites-service.ts
25552
25579
  class PrerequisitesService {
25553
25580
  MINIMUM_WRANGLER = "3.88.0";
@@ -25891,6 +25918,7 @@ class UpdateService {
25891
25918
  }
25892
25919
  async checkAndPromptUpdate(options) {
25893
25920
  const silent = options?.silent === true;
25921
+ const yes = options?.yes === true;
25894
25922
  try {
25895
25923
  const versionCheck = await this.prereqs.checkWranglerVersion();
25896
25924
  if (!versionCheck.outdated) {
@@ -25900,6 +25928,9 @@ class UpdateService {
25900
25928
  }
25901
25929
  return { updated: false };
25902
25930
  }
25931
+ if (silent && !yes) {
25932
+ return { updated: false };
25933
+ }
25903
25934
  const current2 = versionCheck.current ?? "unknown";
25904
25935
  const minimum = versionCheck.minimum ?? "unknown";
25905
25936
  const latest2 = await this.checkLatestVersion();
@@ -26140,7 +26171,7 @@ var exports_schema_service = {};
26140
26171
  __export(exports_schema_service, {
26141
26172
  SchemaService: () => SchemaService
26142
26173
  });
26143
- import { readFileSync as readFileSync5, existsSync as existsSync10 } from "fs";
26174
+ import { readFileSync as readFileSync5, existsSync as existsSync11 } from "fs";
26144
26175
  import { resolve as resolve4 } from "path";
26145
26176
 
26146
26177
  class SchemaService {
@@ -26174,7 +26205,7 @@ class SchemaService {
26174
26205
  const workerWranglerPath = resolve4(workersDir, name, "wrangler.jsonc");
26175
26206
  const devVarsPath = resolve4(workersDir, name, ".dev.vars");
26176
26207
  const errors4 = [];
26177
- const isHooxProject = existsSync10(resolve4(this.projectRoot, "wrangler.jsonc")) || existsSync10(resolve4(this.projectRoot, "workers"));
26208
+ const isHooxProject = existsSync11(resolve4(this.projectRoot, "wrangler.jsonc")) || existsSync11(resolve4(this.projectRoot, "workers"));
26178
26209
  try {
26179
26210
  const wranglerContent = readFileSync5(workerWranglerPath, "utf-8");
26180
26211
  errors4.push(...validateWranglerJsonc(name, manifest, wranglerContent));
@@ -28579,6 +28610,10 @@ function formatSuccess(message, opts) {
28579
28610
  function formatError2(error51, opts = {}) {
28580
28611
  const message = typeof error51 === "string" ? error51 : error51.message;
28581
28612
  const cliError = error51 instanceof CLIError ? error51 : null;
28613
+ const write = (s) => {
28614
+ process.stderr.write(s);
28615
+ process.stdout.write(s);
28616
+ };
28582
28617
  if (opts.json) {
28583
28618
  const output = {
28584
28619
  success: false,
@@ -28592,12 +28627,12 @@ function formatError2(error51, opts = {}) {
28592
28627
  if (opts.suggestions && opts.suggestions.length > 0) {
28593
28628
  output.suggestions = opts.suggestions;
28594
28629
  }
28595
- process.stdout.write(JSON.stringify(output) + `
28630
+ write(JSON.stringify(output) + `
28596
28631
  `);
28597
28632
  return;
28598
28633
  }
28599
28634
  if (opts.quiet) {
28600
- process.stdout.write(`${message}
28635
+ write(`${message}
28601
28636
  `);
28602
28637
  return;
28603
28638
  }
@@ -28607,13 +28642,13 @@ function formatError2(error51, opts = {}) {
28607
28642
  titleParts.push(theme.textSubtle(`[${cliError.code}]`));
28608
28643
  }
28609
28644
  titleParts.push(theme.text(message));
28610
- process.stdout.write(`${titleParts.join(" ")}
28645
+ write(`${titleParts.join(" ")}
28611
28646
  `);
28612
28647
  if (cliError?.details) {
28613
28648
  const detailLines = cliError.details.split(`
28614
28649
  `).filter((l) => l.length > 0);
28615
28650
  for (const line of detailLines) {
28616
- process.stdout.write(` ${theme.textMuted(line)}
28651
+ write(` ${theme.textMuted(line)}
28617
28652
  `);
28618
28653
  }
28619
28654
  }
@@ -28621,24 +28656,24 @@ function formatError2(error51, opts = {}) {
28621
28656
  const hasSuggestions = !!opts.suggestions && opts.suggestions.length > 0;
28622
28657
  if (hasHint || hasSuggestions) {
28623
28658
  if (inCard) {
28624
- process.stdout.write(` ${theme.border(icons.pipe)}
28659
+ write(` ${theme.border(icons.pipe)}
28625
28660
  `);
28626
28661
  }
28627
28662
  if (hasHint) {
28628
- process.stdout.write(` ${inCard ? theme.border(icons.pipe) : " "} ${theme.dim("\u21B3 hint:")} ${theme.value(cliError.hint)}
28663
+ write(` ${inCard ? theme.border(icons.pipe) : " "} ${theme.dim("\u21B3 hint:")} ${theme.value(cliError.hint)}
28629
28664
  `);
28630
28665
  }
28631
28666
  if (hasSuggestions) {
28632
28667
  const sugText = opts.suggestions.map((s) => `'${s}'`).join(", ");
28633
- process.stdout.write(` ${inCard ? theme.border(icons.pipe) : " "} ${theme.dim("did you mean:")} ${theme.accent(sugText)} ${theme.dim("?")}
28668
+ write(` ${inCard ? theme.border(icons.pipe) : " "} ${theme.dim("did you mean:")} ${theme.accent(sugText)} ${theme.dim("?")}
28634
28669
  `);
28635
28670
  }
28636
28671
  if (inCard) {
28637
- process.stdout.write(` ${theme.border(icons.pipe)}
28672
+ write(` ${theme.border(icons.pipe)}
28638
28673
  `);
28639
28674
  }
28640
28675
  } else if (inCard && !cliError?.details) {
28641
- process.stdout.write(` ${theme.border(icons.pipe)}
28676
+ write(` ${theme.border(icons.pipe)}
28642
28677
  `);
28643
28678
  }
28644
28679
  }
@@ -28979,7 +29014,12 @@ function captureHelpEvent(cmd, eventName) {
28979
29014
  // src/utils/completion.ts
28980
29015
  var SUGGEST_NEXT = {
28981
29016
  init: { command: "hoox setup", reason: "provision infrastructure" },
28982
- setup: { command: "hoox deploy all", reason: "ship to Cloudflare" },
29017
+ onboard: { command: "hoox check setup", reason: "verify bootstrap" },
29018
+ setup: { command: "hoox check setup", reason: "verify infrastructure" },
29019
+ "check setup": {
29020
+ command: "hoox deploy all",
29021
+ reason: "ship workers to Cloudflare"
29022
+ },
28983
29023
  "deploy all": { command: "hoox check health", reason: "verify the deploy" },
28984
29024
  "check health": {
28985
29025
  command: "hoox monitor trades",
@@ -29014,6 +29054,7 @@ function getCmdPath(cmd) {
29014
29054
  // src/commands/init/init-command.ts
29015
29055
  init_dist4();
29016
29056
  init_src();
29057
+ import * as fs2 from "fs";
29017
29058
 
29018
29059
  // src/services/cloudflare/index.ts
29019
29060
  init_cloudflare_service();
@@ -29080,21 +29121,32 @@ class CLIProvisioner {
29080
29121
 
29081
29122
  // src/commands/init/init-command.ts
29082
29123
  async function verifyRepoRoot() {
29083
- const rootConfig = Bun.file("wrangler.jsonc");
29084
29124
  const cliPackage = Bun.file("packages/cli/package.json");
29085
- const [rootConfigExists, cliPackageExists] = await Promise.all([
29086
- rootConfig.exists(),
29087
- cliPackage.exists()
29125
+ const wranglerExample = Bun.file("wrangler.jsonc.example");
29126
+ const wranglerConfig = Bun.file("wrangler.jsonc");
29127
+ const gitmodules = Bun.file(".gitmodules");
29128
+ const [
29129
+ cliPackageExists,
29130
+ wranglerExampleExists,
29131
+ wranglerConfigExists,
29132
+ gitmodulesExists
29133
+ ] = await Promise.all([
29134
+ cliPackage.exists(),
29135
+ wranglerExample.exists(),
29136
+ wranglerConfig.exists(),
29137
+ gitmodules.exists()
29088
29138
  ]);
29089
- if (rootConfigExists && cliPackageExists)
29139
+ const workersDirExists = fs2.existsSync("workers");
29140
+ const hasRepoSignal = wranglerExampleExists || wranglerConfigExists || workersDirExists || gitmodulesExists;
29141
+ if (cliPackageExists && hasRepoSignal)
29090
29142
  return;
29091
29143
  const reasons = [];
29092
- if (!rootConfigExists) {
29093
- reasons.push("no root `wrangler.jsonc` found in the current directory");
29094
- }
29095
29144
  if (!cliPackageExists) {
29096
29145
  reasons.push("no `packages/cli` workspace found (this is not the hoox monorepo)");
29097
29146
  }
29147
+ if (cliPackageExists && !hasRepoSignal) {
29148
+ reasons.push("missing Hoox repo markers (expected one of: `wrangler.jsonc.example`, `wrangler.jsonc`, `workers/`, or `.gitmodules`)");
29149
+ }
29098
29150
  const cloneHint = " Clone first: git clone --recursive " + "https://github.com/hoox-sh/hoox.git";
29099
29151
  throw new CLIError(`Not inside a Hoox repository root.
29100
29152
  ` + reasons.join(`
@@ -29168,6 +29220,8 @@ async function createDevVars(config2, secrets, opts) {
29168
29220
  const dir = `${worker.path}`;
29169
29221
  try {
29170
29222
  await Bun.write(filePath, content);
29223
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
29224
+ secureSecretFile2(filePath);
29171
29225
  if (!opts?.quiet) {
29172
29226
  formatSuccess(`Created ${filePath}`, opts);
29173
29227
  }
@@ -29175,6 +29229,8 @@ async function createDevVars(config2, secrets, opts) {
29175
29229
  const { mkdir } = await import("fs/promises");
29176
29230
  await mkdir(dir, { recursive: true });
29177
29231
  await Bun.write(filePath, content);
29232
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
29233
+ secureSecretFile2(filePath);
29178
29234
  if (!opts?.quiet) {
29179
29235
  formatSuccess(`Created ${filePath}`, opts);
29180
29236
  }
@@ -29267,7 +29323,10 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29267
29323
  if (error51) {
29268
29324
  formatError2(new CLIError(error51, 1 /* ERROR */), globalOpts);
29269
29325
  process.exitCode = 1 /* ERROR */;
29326
+ return;
29270
29327
  }
29328
+ process.env.CLOUDFLARE_API_TOKEN = token;
29329
+ process.env.CLOUDFLARE_ACCOUNT_ID = account;
29271
29330
  if (!globalOpts.quiet) {
29272
29331
  formatSuccess("Cloudflare API token validated", globalOpts);
29273
29332
  }
@@ -29305,6 +29364,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29305
29364
  if (isCancel(resumed)) {
29306
29365
  cancel("Setup cancelled.");
29307
29366
  process.exitCode = 0;
29367
+ return;
29308
29368
  }
29309
29369
  if (!resumed) {
29310
29370
  engine = null;
@@ -29324,10 +29384,12 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29324
29384
  if (isCancel(accepted)) {
29325
29385
  cancel("Setup cancelled.");
29326
29386
  process.exitCode = 0;
29387
+ return;
29327
29388
  }
29328
29389
  if (!accepted) {
29329
29390
  outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29330
29391
  process.exitCode = 0;
29392
+ return;
29331
29393
  }
29332
29394
  }
29333
29395
  engine.execute({ checksPassed: true });
@@ -29349,6 +29411,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29349
29411
  if (isCancel(apiToken)) {
29350
29412
  cancel("Setup cancelled.");
29351
29413
  process.exitCode = 0;
29414
+ return;
29352
29415
  }
29353
29416
  log.step("Validating Cloudflare API token...");
29354
29417
  const tokenError = await validateApiToken(cf, apiToken);
@@ -29357,6 +29420,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29357
29420
  log.warn("Please check your token and try again.");
29358
29421
  } else {
29359
29422
  tokenValid = true;
29423
+ process.env.CLOUDFLARE_API_TOKEN = apiToken;
29360
29424
  formatSuccess("Cloudflare API token validated", globalOpts);
29361
29425
  }
29362
29426
  }
@@ -29377,6 +29441,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29377
29441
  if (isCancel(accountResult)) {
29378
29442
  cancel("Setup cancelled.");
29379
29443
  process.exitCode = 0;
29444
+ return;
29380
29445
  }
29381
29446
  const secretStoreResult = await text({
29382
29447
  message: "Cloudflare Secret Store ID:",
@@ -29386,6 +29451,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29386
29451
  if (isCancel(secretStoreResult)) {
29387
29452
  cancel("Setup cancelled.");
29388
29453
  process.exitCode = 0;
29454
+ return;
29389
29455
  }
29390
29456
  const prefixResult = await text({
29391
29457
  message: "Subdomain prefix:",
@@ -29400,7 +29466,9 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29400
29466
  if (isCancel(prefixResult)) {
29401
29467
  cancel("Setup cancelled.");
29402
29468
  process.exitCode = 0;
29469
+ return;
29403
29470
  }
29471
+ process.env.CLOUDFLARE_ACCOUNT_ID = accountResult;
29404
29472
  engine.execute({
29405
29473
  apiToken,
29406
29474
  accountId: accountResult,
@@ -29433,6 +29501,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29433
29501
  if (isCancel(presetChoice)) {
29434
29502
  cancel("Setup cancelled.");
29435
29503
  process.exitCode = 0;
29504
+ return;
29436
29505
  }
29437
29506
  engine.execute({ preset: presetChoice });
29438
29507
  await saveState(engine);
@@ -29456,6 +29525,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29456
29525
  if (isCancel(shouldProvision)) {
29457
29526
  cancel("Setup cancelled.");
29458
29527
  process.exitCode = 0;
29528
+ return;
29459
29529
  }
29460
29530
  if (shouldProvision) {
29461
29531
  log.step("Provisioning infrastructure...");
@@ -29505,6 +29575,9 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29505
29575
  process.exitCode = 0;
29506
29576
  }
29507
29577
  });
29578
+ if (isCancel(collected)) {
29579
+ return;
29580
+ }
29508
29581
  collectedSecrets[key] = {};
29509
29582
  for (const [secretName] of secretEntries) {
29510
29583
  const val = collected[secretName];
@@ -29532,6 +29605,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29532
29605
  if (isCancel(shouldDeploy)) {
29533
29606
  cancel("Setup cancelled.");
29534
29607
  process.exitCode = 0;
29608
+ return;
29535
29609
  }
29536
29610
  if (shouldDeploy) {
29537
29611
  log.step("Deploying workers...");
@@ -29547,8 +29621,10 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29547
29621
  }
29548
29622
  // src/services/setup/setup-service.ts
29549
29623
  import {
29550
- existsSync as existsSync6,
29551
- mkdirSync as mkdirSync3,
29624
+ chmodSync as chmodSync4,
29625
+ existsSync as existsSync7,
29626
+ mkdirSync as mkdirSync4,
29627
+ readdirSync as readdirSync3,
29552
29628
  readFileSync as readFileSync3,
29553
29629
  statSync as statSync2,
29554
29630
  writeFileSync as writeFileSync2
@@ -29607,6 +29683,54 @@ var KEYS_DIR = ".keys";
29607
29683
  function escapeRegex2(s) {
29608
29684
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29609
29685
  }
29686
+ function parseEnvFile(text2) {
29687
+ const map2 = new Map;
29688
+ for (const line of text2.split(`
29689
+ `)) {
29690
+ const trimmed = line.trim();
29691
+ if (!trimmed || trimmed.startsWith("#"))
29692
+ continue;
29693
+ const eq = trimmed.indexOf("=");
29694
+ if (eq <= 0)
29695
+ continue;
29696
+ map2.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
29697
+ }
29698
+ return map2;
29699
+ }
29700
+ function safeChmod(path4, mode) {
29701
+ try {
29702
+ chmodSync4(path4, mode);
29703
+ } catch {}
29704
+ }
29705
+ function loadExistingKeysFromDisk() {
29706
+ const setupEnv = join7(KEYS_DIR, "setup.env");
29707
+ if (!existsSync7(setupEnv))
29708
+ return null;
29709
+ let text2;
29710
+ try {
29711
+ text2 = readFileSync3(setupEnv, "utf-8");
29712
+ } catch {
29713
+ return null;
29714
+ }
29715
+ const map2 = parseEnvFile(text2);
29716
+ const required2 = [
29717
+ "INTERNAL_KEY_BINDING",
29718
+ "AGENT_INTERNAL_KEY",
29719
+ "SESSION_SECRET",
29720
+ "WEBHOOK_API_KEY_BINDING",
29721
+ "TELEGRAM_INTERNAL_KEY_BINDING",
29722
+ "TRADE_INTERNAL_KEY",
29723
+ "API_SERVICE_KEY_BINDING"
29724
+ ];
29725
+ const keys = {};
29726
+ for (const name of required2) {
29727
+ const v = map2.get(name);
29728
+ if (!v)
29729
+ return null;
29730
+ keys[name] = v;
29731
+ }
29732
+ return keys;
29733
+ }
29610
29734
 
29611
29735
  class SetupService {
29612
29736
  onProgress;
@@ -29617,7 +29741,20 @@ class SetupService {
29617
29741
  }
29618
29742
  async generateKeys(skip = false) {
29619
29743
  if (skip) {
29620
- this.onProgress({ type: "info", step: "keys", message: "Skipped" });
29744
+ const existing = loadExistingKeysFromDisk();
29745
+ if (existing) {
29746
+ this.onProgress({
29747
+ type: "info",
29748
+ step: "keys",
29749
+ message: "Using existing keys from .keys/setup.env (--skip-keys)"
29750
+ });
29751
+ return existing;
29752
+ }
29753
+ this.onProgress({
29754
+ type: "info",
29755
+ step: "keys",
29756
+ message: "Skipped (no existing .keys/setup.env)"
29757
+ });
29621
29758
  return null;
29622
29759
  }
29623
29760
  this.onProgress({
@@ -29635,35 +29772,53 @@ class SetupService {
29635
29772
  TRADE_INTERNAL_KEY: internalKey,
29636
29773
  API_SERVICE_KEY_BINDING: internalKey
29637
29774
  };
29638
- if (!existsSync6(KEYS_DIR)) {
29639
- mkdirSync3(KEYS_DIR, { recursive: true });
29775
+ if (!existsSync7(KEYS_DIR)) {
29776
+ mkdirSync4(KEYS_DIR, { recursive: true });
29640
29777
  } else if (!statSync2(KEYS_DIR).isDirectory()) {
29641
- mkdirSync3(KEYS_DIR, { recursive: true });
29778
+ mkdirSync4(KEYS_DIR, { recursive: true });
29642
29779
  }
29780
+ safeChmod(KEYS_DIR, 448);
29781
+ const setupEnvPath = join7(KEYS_DIR, "setup.env");
29643
29782
  const envLines = Object.entries(keys).map(([k, v]) => `${k}=${v}`);
29644
- await Bun.write(join7(KEYS_DIR, "setup.env"), `# Hoox Setup \u2014 Auto-Generated Keys
29783
+ await Bun.write(setupEnvPath, `# Hoox Setup \u2014 Auto-Generated Keys
29645
29784
  ` + "# Generated by `hoox setup`. NEVER commit this file.\n" + `# Created: ${new Date().toISOString()}
29646
29785
 
29647
29786
  ` + envLines.join(`
29648
29787
  `) + `
29649
29788
  `);
29789
+ safeChmod(setupEnvPath, 384);
29650
29790
  for (const [name, value] of Object.entries(keys)) {
29651
- await Bun.write(join7(KEYS_DIR, `${name.toLowerCase()}.env`), `${name}=${value}
29791
+ const keyPath = join7(KEYS_DIR, `${name.toLowerCase()}.env`);
29792
+ await Bun.write(keyPath, `${name}=${value}
29652
29793
  `);
29794
+ safeChmod(keyPath, 384);
29653
29795
  }
29654
29796
  let devVarsCount = 0;
29655
29797
  for (const [worker, secretNames] of Object.entries(DEV_VARS_WORKER_KEYS)) {
29656
29798
  const path4 = `workers/${workerFsDir(worker)}/.dev.vars`;
29799
+ const meshNames = secretNames;
29800
+ const meshSet = new Set(meshNames);
29801
+ const existing = existsSync7(path4) ? parseEnvFile(readFileSync3(path4, "utf-8")) : new Map;
29802
+ for (const secretName of meshNames) {
29803
+ existing.set(secretName, keys[secretName]);
29804
+ }
29657
29805
  let content = `# .dev.vars \u2014 local secrets for ${worker}
29658
29806
  `;
29659
29807
  content += `# Generated by \`hoox setup\`. NEVER commit this file.
29660
29808
 
29661
29809
  `;
29662
- for (const secretName of secretNames) {
29663
- content += `${secretName}=${keys[secretName]}
29810
+ for (const secretName of meshNames) {
29811
+ content += `${secretName}=${existing.get(secretName)}
29664
29812
  `;
29665
29813
  }
29814
+ for (const [k, v] of existing) {
29815
+ if (!meshSet.has(k)) {
29816
+ content += `${k}=${v}
29817
+ `;
29818
+ }
29819
+ }
29666
29820
  await Bun.write(path4, content);
29821
+ safeChmod(path4, 384);
29667
29822
  devVarsCount++;
29668
29823
  }
29669
29824
  this.onProgress({
@@ -29992,38 +30147,40 @@ class SetupService {
29992
30147
  join7(dir, "pyproject.toml"),
29993
30148
  join7(dir, "wrangler.jsonc")
29994
30149
  ];
29995
- return markers.some((p3) => existsSync6(p3));
30150
+ return markers.some((p3) => existsSync7(p3));
29996
30151
  }
29997
- async ensureWorkers() {
29998
- this.onProgress({
29999
- type: "step-start",
30000
- step: "workers",
30001
- message: "Checking worker submodules..."
30002
- });
30152
+ listMissingWorkers() {
30003
30153
  const workerDirs = [
30004
30154
  ...ALL_WORKERS,
30005
30155
  ...TOOLING_WORKERS,
30006
30156
  "dashboard"
30007
30157
  ];
30008
30158
  const missing = [];
30009
- const gitmodulesPath = ".gitmodules";
30010
30159
  for (const name of workerDirs) {
30011
30160
  const dir = `workers/${workerFsDir(name)}`;
30012
30161
  try {
30013
30162
  const gitEntry = readFileSync3(join7(dir, ".git"), "utf-8").trim();
30014
30163
  if (gitEntry.startsWith("gitdir:") || gitEntry.length > 0) {
30015
- if (!this.isWorkerTreePopulated(dir, name)) {
30164
+ if (!this.isWorkerTreePopulated(dir, name))
30016
30165
  missing.push(name);
30017
- }
30018
30166
  } else if (!this.isWorkerTreePopulated(dir, name)) {
30019
30167
  missing.push(name);
30020
30168
  }
30021
30169
  } catch {
30022
- if (!this.isWorkerTreePopulated(dir, name)) {
30170
+ if (!this.isWorkerTreePopulated(dir, name))
30023
30171
  missing.push(name);
30024
- }
30025
30172
  }
30026
30173
  }
30174
+ return missing;
30175
+ }
30176
+ async ensureWorkers() {
30177
+ this.onProgress({
30178
+ type: "step-start",
30179
+ step: "workers",
30180
+ message: "Checking worker submodules..."
30181
+ });
30182
+ const missing = this.listMissingWorkers();
30183
+ const gitmodulesPath = ".gitmodules";
30027
30184
  if (missing.length === 0) {
30028
30185
  this.onProgress({
30029
30186
  type: "step-complete",
@@ -30037,7 +30194,7 @@ class SetupService {
30037
30194
  step: "workers",
30038
30195
  message: `${missing.length} worker(s) not cloned: ${missing.join(", ")}`
30039
30196
  });
30040
- if (!existsSync6(gitmodulesPath)) {
30197
+ if (!existsSync7(gitmodulesPath)) {
30041
30198
  this.onProgress({
30042
30199
  type: "step-error",
30043
30200
  step: "workers",
@@ -30078,14 +30235,17 @@ class SetupService {
30078
30235
  ];
30079
30236
  const toBuild = [];
30080
30237
  for (const pkg of packages) {
30238
+ let built = false;
30081
30239
  try {
30082
- const jsFiles = readFileSync3(pkg.dist, "utf-8");
30083
- if (!jsFiles.includes(".js")) {
30084
- toBuild.push(pkg.name);
30240
+ if (existsSync7(pkg.dist) && statSync2(pkg.dist).isDirectory()) {
30241
+ const entries = readdirSync3(pkg.dist);
30242
+ built = entries.some((e) => e.endsWith(".js"));
30085
30243
  }
30086
30244
  } catch {
30087
- toBuild.push(pkg.name);
30245
+ built = false;
30088
30246
  }
30247
+ if (!built)
30248
+ toBuild.push(pkg.name);
30089
30249
  }
30090
30250
  if (toBuild.length === 0) {
30091
30251
  this.onProgress({
@@ -30402,13 +30562,21 @@ class SetupService {
30402
30562
  results.success = false;
30403
30563
  return results;
30404
30564
  }
30405
- const cloned = await this.ensureWorkers();
30406
- if (cloned.length > 0) {
30565
+ const missingAfterClone = await this.ensureWorkers();
30566
+ const stillMissing = this.listMissingWorkers();
30567
+ if (stillMissing.length > 0) {
30568
+ steps.push({
30569
+ step: "workers",
30570
+ success: false,
30571
+ message: `Missing worker trees: ${stillMissing.join(", ")}. Run: git submodule update --init --recursive`
30572
+ });
30407
30573
  this.onProgress({
30408
- type: "warn",
30574
+ type: "step-error",
30409
30575
  step: "workers",
30410
- message: "Run `hoox setup` again after clone completes."
30576
+ message: missingAfterClone.length > 0 ? "Workers still missing after clone attempt \u2014 aborting setup." : "Workers missing \u2014 aborting setup."
30411
30577
  });
30578
+ results.success = false;
30579
+ return results;
30412
30580
  }
30413
30581
  const built = await this.ensurePackages();
30414
30582
  if (!built) {
@@ -30417,6 +30585,8 @@ class SetupService {
30417
30585
  success: false,
30418
30586
  message: "Package build failed \u2014 check dependencies"
30419
30587
  });
30588
+ results.success = false;
30589
+ return results;
30420
30590
  }
30421
30591
  this.fixWranglerConfigs();
30422
30592
  const keys = await this.generateKeys(options.skipKeys);
@@ -30433,7 +30603,7 @@ class SetupService {
30433
30603
  } else {
30434
30604
  steps.push({ step: "d1-schema", success: true, message: "Skipped" });
30435
30605
  }
30436
- if (keys) {
30606
+ if (!options.skipSecrets && keys) {
30437
30607
  const reconcile = await this.reconcileSecrets(keys);
30438
30608
  if (reconcile.warnings.length > 0) {
30439
30609
  for (const w of reconcile.warnings) {
@@ -30441,20 +30611,25 @@ class SetupService {
30441
30611
  }
30442
30612
  }
30443
30613
  }
30444
- const secretResults = await this.setSecrets(keys);
30445
- results.secrets = secretResults;
30446
- const secretOk = secretResults.filter((r2) => r2.ok).length;
30447
- const secretFail = secretResults.filter((r2) => !r2.ok).length;
30448
- steps.push({
30449
- step: "secrets",
30450
- success: secretFail === 0,
30451
- message: `${secretOk} set, ${secretFail} failed`,
30452
- details: {
30453
- total: secretResults.length,
30454
- ok: secretOk,
30455
- failed: secretFail
30456
- }
30457
- });
30614
+ if (options.skipSecrets) {
30615
+ steps.push({ step: "secrets", success: true, message: "Skipped" });
30616
+ results.secrets = [];
30617
+ } else {
30618
+ const secretResults = await this.setSecrets(keys);
30619
+ results.secrets = secretResults;
30620
+ const secretOk = secretResults.filter((r2) => r2.ok).length;
30621
+ const secretFail = secretResults.filter((r2) => !r2.ok).length;
30622
+ steps.push({
30623
+ step: "secrets",
30624
+ success: secretFail === 0,
30625
+ message: `${secretOk} set, ${secretFail} failed`,
30626
+ details: {
30627
+ total: secretResults.length,
30628
+ ok: secretOk,
30629
+ failed: secretFail
30630
+ }
30631
+ });
30632
+ }
30458
30633
  const dashResult = await this.rebuildDashboard(options.skipDashboard);
30459
30634
  steps.push(dashResult);
30460
30635
  if (keys) {
@@ -30559,12 +30734,35 @@ async function runOnboard(options, fmt) {
30559
30734
  acceptRisk: options.acceptRisk
30560
30735
  };
30561
30736
  await runInitCommand(initOpts, { json: fmt.json, quiet: fmt.quiet }, isNonInteractive);
30737
+ if (process.exitCode && process.exitCode !== 0) {
30738
+ return;
30739
+ }
30740
+ const configReady = await Bun.file("wrangler.jsonc").exists();
30741
+ if (!configReady) {
30742
+ if (!quiet) {
30743
+ formatError2(new CLIError("Onboard stopped: workspace config was not written (init cancelled or incomplete).", 1 /* ERROR */, undefined, false, "Re-run `hoox onboard` or `hoox init` to finish configuration, then `hoox setup`."), fmt);
30744
+ }
30745
+ process.exitCode = 1 /* ERROR */;
30746
+ return;
30747
+ }
30748
+ if (options.token) {
30749
+ process.env.CLOUDFLARE_API_TOKEN = options.token;
30750
+ }
30751
+ if (options.account) {
30752
+ process.env.CLOUDFLARE_ACCOUNT_ID = options.account;
30753
+ }
30562
30754
  if (!quiet) {
30563
30755
  process.stdout.write(theme.heading(`
30564
30756
  \u2501\u2501\u2501 Step 2/2: Setup (infrastructure bootstrap) \u2501\u2501\u2501
30565
30757
  `));
30566
30758
  }
30567
30759
  const setupSvc = new SetupService;
30760
+ const authOk = await setupSvc.checkAuth();
30761
+ if (!authOk) {
30762
+ 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."), fmt);
30763
+ process.exitCode = 1 /* ERROR */;
30764
+ return;
30765
+ }
30568
30766
  const setupOpts = {
30569
30767
  skipKeys: Boolean(options.skipKeys),
30570
30768
  skipDb: Boolean(options.skipDb),
@@ -30576,14 +30774,14 @@ async function runOnboard(options, fmt) {
30576
30774
  const result = await setupSvc.runAll(setupOpts);
30577
30775
  if (result.success) {
30578
30776
  if (!quiet) {
30579
- formatSuccess("Bootstrap complete! Run: hoox check setup to verify.", fmt);
30777
+ formatSuccess("Bootstrap complete! Next: `hoox check setup` then `hoox deploy all`", fmt);
30580
30778
  }
30581
30779
  } else {
30582
- formatError2(new CLIError("Bootstrap completed with issues. See logs above.", 1 /* ERROR */), fmt);
30780
+ formatError2(new CLIError("Bootstrap completed with issues. See logs above.", 1 /* ERROR */, undefined, false, "Retry with `hoox setup`, diagnose with `hoox check setup`, or run `hoox repair check`."), fmt);
30583
30781
  process.exitCode = 1 /* ERROR */;
30584
30782
  }
30585
30783
  } catch (e) {
30586
- formatError2(new CLIError(e instanceof Error ? e.message : String(e), 1 /* ERROR */), fmt);
30784
+ formatError2(new CLIError(e instanceof Error ? e.message : String(e), 1 /* ERROR */, undefined, false, "Retry with `hoox setup`, diagnose with `hoox check setup`, or run `hoox repair check`."), fmt);
30587
30785
  process.exitCode = 1 /* ERROR */;
30588
30786
  }
30589
30787
  }
@@ -31101,7 +31299,7 @@ function renderDefaultSummary(results) {
31101
31299
  init_errors4();
31102
31300
  import {
31103
31301
  statSync as statSync3,
31104
- existsSync as existsSync7,
31302
+ existsSync as existsSync8,
31105
31303
  readFileSync as readFileSync4,
31106
31304
  writeFileSync as writeFileSync3,
31107
31305
  renameSync
@@ -31536,7 +31734,7 @@ ${section}:
31536
31734
  init_main();
31537
31735
  function getDashboardBuildInfo(dashboardPath) {
31538
31736
  const workerPath = resolve2(dashboardPath, ".open-next", "worker.js");
31539
- if (!existsSync7(workerPath)) {
31737
+ if (!existsSync8(workerPath)) {
31540
31738
  return { exists: false };
31541
31739
  }
31542
31740
  const stats = statSync3(workerPath);
@@ -31879,7 +32077,7 @@ async function doUpdateInternalUrls(fmt) {
31879
32077
  const prefix = global.subdomain_prefix ?? "hoox";
31880
32078
  const workers = config2.listEnabledWorkers();
31881
32079
  const filePath = resolve2(process.cwd(), "workers", "dashboard", "wrangler.jsonc");
31882
- if (!existsSync7(filePath)) {
32080
+ if (!existsSync8(filePath)) {
31883
32081
  formatError2(new CLIError("Dashboard wrangler.jsonc not found at workers/dashboard/wrangler.jsonc", 1 /* ERROR */), fmt);
31884
32082
  process.exitCode = 1 /* ERROR */;
31885
32083
  return;
@@ -33033,6 +33231,8 @@ async function handleInit(opts) {
33033
33231
  await Bun.write(tmpPath, envContent);
33034
33232
  await Bun.write(".env.local", envContent);
33035
33233
  await Bun.file(tmpPath).delete().catch(() => {});
33234
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
33235
+ secureSecretFile2(".env.local");
33036
33236
  if (!opts.quiet)
33037
33237
  formatSuccess(".env.local written", opts);
33038
33238
  log.step("Writing .dev.vars files...");
@@ -33047,6 +33247,8 @@ async function handleInit(opts) {
33047
33247
  ].join(`
33048
33248
  `);
33049
33249
  await Bun.write(`${workerPath}/.dev.vars`, content);
33250
+ const { secureSecretFile: chmodDev } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
33251
+ chmodDev(`${workerPath}/.dev.vars`);
33050
33252
  if (!opts.quiet)
33051
33253
  formatSuccess(`Created ${workerPath}/.dev.vars`, opts);
33052
33254
  }
@@ -33123,6 +33325,8 @@ async function handleGenerateDevVars(opts) {
33123
33325
  ].join(`
33124
33326
  `);
33125
33327
  await Bun.write(`${workerPath}/.dev.vars`, content);
33328
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
33329
+ secureSecretFile2(`${workerPath}/.dev.vars`);
33126
33330
  count++;
33127
33331
  }
33128
33332
  formatSuccess(`Generated .dev.vars for ${count} worker(s)`, opts);
@@ -33370,7 +33574,7 @@ class SecretsService {
33370
33574
  const path5 = configPath ?? "wrangler.jsonc";
33371
33575
  const file2 = Bun.file(path5);
33372
33576
  if (!await file2.exists()) {
33373
- throw new Error(`Config file not found: ${path5}`);
33577
+ throw new Error(`Config file not found: ${path5}. Run 'hoox onboard' (or 'hoox init') to create one.`);
33374
33578
  }
33375
33579
  const text2 = await file2.text();
33376
33580
  const config2 = parse6(text2);
@@ -33453,6 +33657,8 @@ class SecretsService {
33453
33657
  const devVarsPath = `${worker.path}/.dev.vars`;
33454
33658
  try {
33455
33659
  await Bun.write(devVarsPath, content);
33660
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
33661
+ secureSecretFile2(devVarsPath);
33456
33662
  return { ok: true, value: devVarsPath };
33457
33663
  } catch (err) {
33458
33664
  const message = err instanceof Error ? err.message : String(err);
@@ -33700,6 +33906,8 @@ async function updateDevVars(filePath, key, value) {
33700
33906
  await Bun.write(filePath, `${key}=${value}
33701
33907
  `);
33702
33908
  }
33909
+ const { secureSecretFile: secureSecretFile2 } = await Promise.resolve().then(() => (init_fs_secure(), exports_fs_secure));
33910
+ secureSecretFile2(filePath);
33703
33911
  }
33704
33912
  function registerSecretsSubcommands(secretsCmd, service = "config") {
33705
33913
  secretsCmd.command("list [worker]").summary("List secrets for all workers or a specific worker").description(`List the secrets declared in wrangler.jsonc for workers.
@@ -33773,7 +33981,7 @@ EXAMPLES:
33773
33981
  const svc = await SecretsService.create();
33774
33982
  const declared = svc.listSecrets(workerName);
33775
33983
  if (!declared.includes(secretName) && declared.length > 0) {
33776
- throw new CLIError(`Secret "${secretName}" is not declared for worker "${workerName}". ` + `Declared secrets: ${declared.join(", ")}`, 2 /* INVALID_USAGE */);
33984
+ throw new CLIError(`Secret "${secretName}" is not declared for worker "${workerName}". Declared secrets: ${declared.join(", ")}`, 2 /* INVALID_USAGE */);
33777
33985
  }
33778
33986
  const value = await promptSecret(`Enter value for "${secretName}"`);
33779
33987
  if (!value) {
@@ -33956,7 +34164,8 @@ EXAMPLES:
33956
34164
  }
33957
34165
 
33958
34166
  // src/commands/config/keys-subcommands.ts
33959
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
34167
+ import { existsSync as existsSync9 } from "fs";
34168
+ init_fs_secure();
33960
34169
  init_theme();
33961
34170
  function generateKey(bytes = 32) {
33962
34171
  const buf = new Uint8Array(bytes);
@@ -33982,9 +34191,7 @@ EXAMPLES:
33982
34191
  hoox config keys generate`).action(withErrorHandling(async (_2, cmd) => {
33983
34192
  const opts = getFormatOptions(cmd);
33984
34193
  const keysDir = ".keys";
33985
- if (!existsSync8(keysDir)) {
33986
- mkdirSync4(keysDir, { recursive: true });
33987
- }
34194
+ ensureSecretDir(keysDir);
33988
34195
  const meshKey = generateKey();
33989
34196
  const keys = {
33990
34197
  INTERNAL_KEY_BINDING: meshKey,
@@ -33999,7 +34206,13 @@ EXAMPLES:
33999
34206
  const filePath = `${keysDir}/${name.toLowerCase()}.env`;
34000
34207
  await Bun.write(filePath, `${name}=${value}
34001
34208
  `);
34209
+ secureSecretFile(filePath);
34002
34210
  }
34211
+ await Bun.write(`${keysDir}/setup.env`, `# Generated by hoox keys generate
34212
+ ` + Object.entries(keys).map(([k, v]) => `${k}=${v}`).join(`
34213
+ `) + `
34214
+ `);
34215
+ secureSecretFile(`${keysDir}/setup.env`);
34003
34216
  formatSuccess(`Generated ${Object.keys(keys).length} keys in ${keysDir}/`, opts);
34004
34217
  if (!opts.quiet && !opts.json) {
34005
34218
  process.stdout.write(`${theme.warning("!")} Keep these keys secret. Add ${keysDir}/ to .gitignore.
@@ -34016,7 +34229,7 @@ EXAMPLES:
34016
34229
  hoox config keys list`).action(withErrorHandling(async (_2, cmd) => {
34017
34230
  const opts = getFormatOptions(cmd);
34018
34231
  const keysDir = ".keys";
34019
- if (!existsSync8(keysDir)) {
34232
+ if (!existsSync9(keysDir)) {
34020
34233
  process.stdout.write(`${theme.dim("No .keys/ directory found.")}
34021
34234
  `);
34022
34235
  return;
@@ -34657,7 +34870,7 @@ async function runSecretsChecks(secretsService, configService, cf) {
34657
34870
  async function runDatabaseChecks(cf, configService) {
34658
34871
  const checks3 = [];
34659
34872
  const d1Worker = configService.getWorker("d1-worker");
34660
- const dbName = d1Worker?.vars?.database_name ?? "my-database";
34873
+ const dbName = d1Worker?.vars?.database_name ?? "trade-data-db";
34661
34874
  const d1ListResult = await cf.d1List();
34662
34875
  if (d1ListResult.ok) {
34663
34876
  const hasDb = d1ListResult.value.includes(dbName);
@@ -36282,7 +36495,7 @@ init_dist4();
36282
36495
  // src/services/db/db-service.ts
36283
36496
  init_config2();
36284
36497
  init_cloudflare_service();
36285
- import { existsSync as existsSync9 } from "fs";
36498
+ import { existsSync as existsSync10 } from "fs";
36286
36499
  import { join as join9 } from "path";
36287
36500
  var D1_WRANGLER_CONFIG_CANDIDATES = [
36288
36501
  "workers/d1-worker/wrangler.jsonc",
@@ -36320,7 +36533,7 @@ class DbService {
36320
36533
  if (vars?.database_name)
36321
36534
  return vars.database_name;
36322
36535
  } catch {}
36323
- return "my-database";
36536
+ return "trade-data-db";
36324
36537
  }
36325
36538
  async apply(dbName, remote, schemaPath) {
36326
36539
  const path6 = schemaPath ?? this.resolveDefaultSchemaPath();
@@ -36384,7 +36597,7 @@ class DbService {
36384
36597
  if (this.wranglerConfigPath)
36385
36598
  return this.wranglerConfigPath;
36386
36599
  for (const candidate of D1_WRANGLER_CONFIG_CANDIDATES) {
36387
- if (existsSync9(candidate))
36600
+ if (existsSync10(candidate))
36388
36601
  return candidate;
36389
36602
  }
36390
36603
  return;
@@ -37479,8 +37692,7 @@ function registerSchemaCommand(program2) {
37479
37692
  const svc = new SchemaService;
37480
37693
  const manifest = svc.getManifest(worker);
37481
37694
  if (!manifest) {
37482
- formatError2(new CLIError(`Unknown worker "${worker}"`, 2 /* INVALID_USAGE */), fmt);
37483
- return;
37695
+ throw new CLIError(`Unknown worker "${worker}"`, 2 /* INVALID_USAGE */, undefined, false, `Known workers: ${svc.getWorkerNames().join(", ") || "(none)"}. Run: hoox schema list`);
37484
37696
  }
37485
37697
  const wranglerContent = generateWranglerJsonc(manifest);
37486
37698
  const devVarsContent = generateDevVars(manifest);
@@ -37515,7 +37727,7 @@ import { spawn } from "child_process";
37515
37727
  import { createRequire } from "module";
37516
37728
  import { resolve as resolve6, dirname as dirname3, join as join10 } from "path";
37517
37729
  import { fileURLToPath } from "url";
37518
- import { existsSync as existsSync11, realpathSync } from "fs";
37730
+ import { existsSync as existsSync12, realpathSync } from "fs";
37519
37731
  import { homedir as homedir5 } from "os";
37520
37732
 
37521
37733
  // src/services/perf/endpoint-resolver.ts
@@ -37579,7 +37791,7 @@ function collectTuiCandidates() {
37579
37791
  return [...new Set(candidates)];
37580
37792
  }
37581
37793
  function pushPackageRootEntries(candidates, root) {
37582
- if (!root || !existsSync11(root))
37794
+ if (!root || !existsSync12(root))
37583
37795
  return;
37584
37796
  candidates.push(join10(root, "dist", "main.js"), join10(root, "src", "main.tsx"), join10(root, "bin", "hoox-tui.js"), join10(root, "src", "main.ts"));
37585
37797
  }
@@ -37607,7 +37819,7 @@ function pushFromWhich(candidates, binName) {
37607
37819
  function resolveTUIEntry() {
37608
37820
  const candidates = collectTuiCandidates();
37609
37821
  for (const path6 of candidates) {
37610
- if (existsSync11(path6))
37822
+ if (existsSync12(path6))
37611
37823
  return path6;
37612
37824
  }
37613
37825
  const runtime = resolveHooxRuntimeRoot();
@@ -37826,13 +38038,13 @@ import { homedir as homedir6 } from "os";
37826
38038
 
37827
38039
  // src/services/runtime/runtime-service.ts
37828
38040
  init_src();
37829
- import { existsSync as existsSync12, lstatSync as lstatSync2, readlinkSync, rmSync } from "fs";
38041
+ import { existsSync as existsSync13, lstatSync as lstatSync2, readlinkSync, rmSync } from "fs";
37830
38042
  import { mkdir } from "fs/promises";
37831
38043
  import { join as join11 } from "path";
37832
38044
  var DEFAULT_HOOX_RUNTIME_REPO = "https://github.com/hoox-sh/hoox.git";
37833
38045
  function firstExisting(paths) {
37834
38046
  for (const p3 of paths) {
37835
- if (existsSync12(p3))
38047
+ if (existsSync13(p3))
37836
38048
  return p3;
37837
38049
  }
37838
38050
  return null;
@@ -37846,7 +38058,7 @@ function getRuntimeStatus(cwd = process.cwd()) {
37846
38058
  repoPath,
37847
38059
  runtime,
37848
38060
  tuiEntry,
37849
- repoPresent: existsSync12(repoPath),
38061
+ repoPresent: existsSync13(repoPath),
37850
38062
  isSetupRoot: isHooxSetupRoot(repoPath)
37851
38063
  };
37852
38064
  }
@@ -37868,7 +38080,7 @@ function isBrokenSymlink(path6) {
37868
38080
  try {
37869
38081
  if (!lstatSync2(path6).isSymbolicLink())
37870
38082
  return false;
37871
- return !existsSync12(path6);
38083
+ return !existsSync13(path6);
37872
38084
  } catch {
37873
38085
  return false;
37874
38086
  }
@@ -37881,7 +38093,7 @@ async function ensureGlobalRuntime(options = {}) {
37881
38093
  await mkdir(hooxHome, { recursive: true });
37882
38094
  let cloned = false;
37883
38095
  if (!isHooxSetupRoot(repoPath)) {
37884
- if (existsSync12(repoPath) || isBrokenSymlink(repoPath)) {
38096
+ if (existsSync13(repoPath) || isBrokenSymlink(repoPath)) {
37885
38097
  if (isBrokenSymlink(repoPath)) {
37886
38098
  let target = "";
37887
38099
  try {
@@ -37913,8 +38125,8 @@ ${clone2.stderr || clone2.stdout}`.trim());
37913
38125
  if (!options.skipInstall) {
37914
38126
  const nodeModules = join11(repoPath, "node_modules");
37915
38127
  const tuiPkg = join11(repoPath, "packages", "tui", "package.json");
37916
- const needsInstall = !existsSync12(nodeModules) || existsSync12(tuiPkg) && !existsSync12(join11(repoPath, "packages", "tui", "node_modules")) && !existsSync12(join11(nodeModules, "@opentui"));
37917
- if (cloned || needsInstall || !existsSync12(nodeModules)) {
38128
+ const needsInstall = !existsSync13(nodeModules) || existsSync13(tuiPkg) && !existsSync13(join11(repoPath, "packages", "tui", "node_modules")) && !existsSync13(join11(nodeModules, "@opentui"));
38129
+ if (cloned || needsInstall || !existsSync13(nodeModules)) {
37918
38130
  log2(`Running bun install in ${repoPath}`);
37919
38131
  const install = await runCommand(["bun", "install"], repoPath);
37920
38132
  if (!install.ok) {
@@ -39070,7 +39282,7 @@ EXAMPLES:
39070
39282
  }
39071
39283
  // src/commands/pyne/pyne-command.ts
39072
39284
  init_config2();
39073
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
39285
+ import { existsSync as existsSync14, readFileSync as readFileSync8 } from "fs";
39074
39286
  import { resolve as resolve7 } from "path";
39075
39287
  init_errors4();
39076
39288
  var DEFAULT_TIMEOUT_MS = 30000;
@@ -39079,7 +39291,7 @@ function getPyneWorkerDir() {
39079
39291
  }
39080
39292
  function requirePyneDir() {
39081
39293
  const dir = getPyneWorkerDir();
39082
- if (!existsSync13(dir)) {
39294
+ if (!existsSync14(dir)) {
39083
39295
  throw new CLIError(`pyne-worker directory not found: ${dir}
39084
39296
  ` + ` Clone it with: hoox clone pyne-worker
39085
39297
  ` + ` Or: git submodule update --init workers/pyne-worker`, 1 /* ERROR */);
@@ -39094,7 +39306,7 @@ function loadDevVars(workerDir) {
39094
39306
  ];
39095
39307
  for (const filePath of candidates) {
39096
39308
  try {
39097
- if (!existsSync13(filePath))
39309
+ if (!existsSync14(filePath))
39098
39310
  continue;
39099
39311
  const content = readFileSync8(filePath, "utf-8");
39100
39312
  for (const line of content.split(`
@@ -39312,7 +39524,7 @@ EXAMPLES:
39312
39524
  body.script = await new Response(Bun.stdin).text();
39313
39525
  } else {
39314
39526
  const abs = resolve7(process.cwd(), scriptPath);
39315
- if (!existsSync13(abs)) {
39527
+ if (!existsSync14(abs)) {
39316
39528
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39317
39529
  }
39318
39530
  body.script = readFileSync8(abs, "utf-8");
@@ -39363,7 +39575,7 @@ EXAMPLES:
39363
39575
  throw new CLIError("API key required for authenticated endpoints", 2 /* INVALID_USAGE */);
39364
39576
  }
39365
39577
  const abs = resolve7(process.cwd(), scriptPath);
39366
- if (!existsSync13(abs)) {
39578
+ if (!existsSync14(abs)) {
39367
39579
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39368
39580
  }
39369
39581
  const script = readFileSync8(abs, "utf-8");
@@ -39471,7 +39683,7 @@ Pass script flags after -- :
39471
39683
  Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorHandling(async () => {
39472
39684
  const dir = requirePyneDir();
39473
39685
  const script = resolve7(dir, "scripts/sync_vendor.sh");
39474
- if (!existsSync13(script)) {
39686
+ if (!existsSync14(script)) {
39475
39687
  throw new CLIError(`sync_vendor.sh not found at ${script}`, 1 /* ERROR */);
39476
39688
  }
39477
39689
  process.exitCode = await spawnInPyneDir(["bash", script], dir);
@@ -39480,7 +39692,7 @@ Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorH
39480
39692
  const dir = requirePyneDir();
39481
39693
  if (!opts.skipSync) {
39482
39694
  const script = resolve7(dir, "scripts/sync_vendor.sh");
39483
- if (existsSync13(script)) {
39695
+ if (existsSync14(script)) {
39484
39696
  const code = await spawnInPyneDir(["bash", script], dir);
39485
39697
  if (code !== 0) {
39486
39698
  process.exitCode = code;
@@ -41029,7 +41241,7 @@ function registerCompletionCommand(program2) {
41029
41241
  // src/ui/banner.ts
41030
41242
  init_ansis();
41031
41243
  init_theme();
41032
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
41244
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
41033
41245
  import { fileURLToPath as fileURLToPath2 } from "url";
41034
41246
  import { dirname as dirname4, join as join13 } from "path";
41035
41247
  var ORANGE = ansis_default.hex("#ff7f2a");
@@ -41045,7 +41257,7 @@ function findCliVersion() {
41045
41257
  let dir = dirname4(fileURLToPath2(import.meta.url));
41046
41258
  for (let i2 = 0;i2 < 5; i2++) {
41047
41259
  const pkgPath = join13(dir, "package.json");
41048
- if (existsSync14(pkgPath)) {
41260
+ if (existsSync15(pkgPath)) {
41049
41261
  try {
41050
41262
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
41051
41263
  if (pkg.name === PKG_NAME)
@@ -41684,6 +41896,13 @@ Hoox CLI \u2014 Cloudflare Workers Platform
41684
41896
  styleCommandText: (str) => theme.bold(str),
41685
41897
  styleOptionText: (str) => str,
41686
41898
  styleDescriptionText: (str) => theme.dim(str)
41899
+ }).configureOutput({
41900
+ outputError: (str, write) => {
41901
+ if (/^error: unknown command/i.test(str) || /^error: unknown option/i.test(str) || /^error: missing required argument/i.test(str) || /^error: required option/i.test(str) || /^\(Did you mean /i.test(str)) {
41902
+ return;
41903
+ }
41904
+ write(str);
41905
+ }
41687
41906
  });
41688
41907
  program2.option("--json", "Output in JSON format");
41689
41908
  program2.option("--quiet", "Minimal output");
@@ -41698,10 +41917,16 @@ program2.hook("preAction", async () => {
41698
41917
  return;
41699
41918
  if (process.env.BUN_TEST === "1")
41700
41919
  return;
41920
+ if (process.argv.includes("--json"))
41921
+ return;
41701
41922
  try {
41702
41923
  const { UpdateService: UpdateService2 } = await Promise.resolve().then(() => (init_update(), exports_update));
41703
41924
  const service = new UpdateService2;
41704
- await service.checkAndPromptUpdate({ yes: true, silent: true });
41925
+ const autoUpdate = process.env.HOOX_AUTO_UPDATE_WRANGLER === "1";
41926
+ await service.checkAndPromptUpdate({
41927
+ yes: autoUpdate,
41928
+ silent: true
41929
+ });
41705
41930
  } catch {}
41706
41931
  });
41707
41932
  program2.hook("postAction", (thisCmd) => {
@@ -41713,54 +41938,67 @@ program2.hook("postAction", (thisCmd) => {
41713
41938
  const durationMs = Date.now() - startedAt;
41714
41939
  const path6 = getCmdPath(thisCmd);
41715
41940
  const suggestion = suggestNextCommand(path6);
41716
- formatCompletion("Done", { durationMs, suggestion });
41941
+ const g2 = program2.opts();
41942
+ formatCompletion("Done", {
41943
+ durationMs,
41944
+ suggestion,
41945
+ json: Boolean(g2.json),
41946
+ quiet: Boolean(g2.quiet),
41947
+ noColor: g2.color === false || Boolean(process.env.NO_COLOR)
41948
+ });
41717
41949
  });
41718
41950
  program2.exitOverride((err) => {
41719
41951
  const globalOpts = program2.opts();
41720
41952
  const errFmtOpts = {
41721
41953
  json: Boolean(globalOpts.json),
41722
41954
  quiet: Boolean(globalOpts.quiet),
41723
- noColor: Boolean(globalOpts.noColor)
41955
+ noColor: globalOpts.color === false || Boolean(process.env.NO_COLOR)
41724
41956
  };
41725
41957
  if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
41726
41958
  process.exitCode = 0 /* SUCCESS */;
41727
- return;
41959
+ throw err;
41728
41960
  }
41729
- if (err.code === "commander.missingArgument" || err.message === "(outputHelp)" || err.message?.includes("outputHelp")) {
41961
+ if ((err.message === "(outputHelp)" || err.message?.includes("outputHelp")) && err.code !== "commander.missingArgument") {
41730
41962
  process.exitCode = 0 /* SUCCESS */;
41731
- return;
41963
+ throw err;
41732
41964
  }
41733
41965
  if (err.code === "commander.unknownCommand") {
41734
41966
  const match = err.message.match(/'([^']+)'/);
41735
41967
  const badArg = match?.[1] ?? "";
41736
41968
  const suggestion = suggestForCommand(program2, badArg);
41737
- formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), suggestion ? { ...errFmtOpts, suggestions: [suggestion] } : errFmtOpts);
41969
+ const cleanMsg = err.message.replace(/\n?\(Did you mean[^)]*\)\s*/gi, "").trim();
41970
+ formatError2(new CLIError(cleanMsg, 2 /* INVALID_USAGE */, undefined, false), suggestion ? { ...errFmtOpts, suggestions: [suggestion] } : errFmtOpts);
41738
41971
  process.exitCode = 2 /* INVALID_USAGE */;
41739
- return;
41972
+ throw err;
41740
41973
  }
41741
41974
  if (err.code === "commander.unknownOption") {
41742
41975
  formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), errFmtOpts);
41743
41976
  process.exitCode = 2 /* INVALID_USAGE */;
41744
- return;
41977
+ throw err;
41745
41978
  }
41746
- if (err.code === "commander.missingMandatoryOptionValue" || err.code === "commander.missingArgument") {
41979
+ if (err.code === "commander.missingArgument" || err.code === "commander.missingMandatoryOptionValue") {
41747
41980
  formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), errFmtOpts);
41748
41981
  process.exitCode = 2 /* INVALID_USAGE */;
41749
- return;
41982
+ throw err;
41750
41983
  }
41751
41984
  if (err instanceof CLIError) {
41752
41985
  formatError2(err, errFmtOpts);
41753
41986
  process.exitCode = err.code;
41754
- return;
41987
+ throw err;
41755
41988
  }
41756
41989
  formatError2(new CLIError(err.message, 1 /* ERROR */, undefined, false), errFmtOpts);
41757
41990
  process.exitCode = 1 /* ERROR */;
41991
+ throw err;
41758
41992
  });
41759
41993
  process.on("uncaughtException", (err) => {
41994
+ if (process.exitCode !== undefined && process.exitCode !== 0)
41995
+ return;
41760
41996
  formatError2(err instanceof CLIError ? err : new CLIError(`Uncaught exception: ${err.message}`, 1 /* ERROR */, err.stack, false));
41761
41997
  process.exitCode = 1 /* ERROR */;
41762
41998
  });
41763
41999
  process.on("unhandledRejection", (reason) => {
42000
+ if (process.exitCode !== undefined && process.exitCode !== 0)
42001
+ return;
41764
42002
  const err = reason instanceof Error ? reason : new Error(`Unhandled rejection: ${toError(reason)}`);
41765
42003
  formatError2(new CLIError(err.message, 1 /* ERROR */, err.stack, false));
41766
42004
  process.exitCode = 1 /* ERROR */;
@@ -41804,17 +42042,31 @@ async function main() {
41804
42042
  const configFile = Bun.file("wrangler.jsonc");
41805
42043
  const hasConfig = await configFile.exists();
41806
42044
  const wizardStateFile = Bun.file(WIZARD_STATE_PATH);
41807
- const hasWizardState = await wizardStateFile.exists();
42045
+ const hasWizardState = await wizardStateFile.exists() && (await wizardStateFile.text()).trim().length > 0;
41808
42046
  if (!hasConfig) {
41809
42047
  const args = hasWizardState ? ["onboard", "--resume"] : ["onboard"];
41810
42048
  await program2.parseAsync(args, { from: "user" });
41811
42049
  return;
41812
42050
  }
41813
- if (hasWizardState) {
41814
- await Bun.write(WIZARD_STATE_PATH, "");
42051
+ if (await wizardStateFile.exists()) {
42052
+ try {
42053
+ const { unlinkSync } = await import("fs");
42054
+ unlinkSync(WIZARD_STATE_PATH);
42055
+ } catch {
42056
+ await Bun.write(WIZARD_STATE_PATH, "");
42057
+ }
41815
42058
  }
41816
42059
  await runInteractiveTUI(program2);
41817
42060
  }
42061
+ } catch (err) {
42062
+ if (process.exitCode === undefined) {
42063
+ const message = err instanceof Error ? err.message : `Unhandled error: ${String(err)}`;
42064
+ const isCommander = err instanceof Error && "code" in err && typeof err.code === "string" && String(err.code).startsWith("commander.");
42065
+ if (!isCommander) {
42066
+ formatError2(new CLIError(message, 1 /* ERROR */, undefined, false));
42067
+ }
42068
+ process.exitCode = 1 /* ERROR */;
42069
+ }
41818
42070
  } finally {
41819
42071
  if (typeof process.exit === "function" && process.exitCode !== undefined) {
41820
42072
  process.exit(process.exitCode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoox-sh/hoox-cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Hoox CLI — manage Cloudflare Workers, infrastructure, secrets, and deployments (@hoox-sh/hoox-cli)",
5
5
  "type": "module",
6
6
  "bin": {