@hoox-sh/hoox-cli 0.11.1 → 0.11.3

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>`.
@@ -455,7 +455,7 @@ The CI pipeline runs in this order:
455
455
 
456
456
  1. `bun run lint` — ESLint check
457
457
  2. `bun run typecheck` — TypeScript check (`tsc --noEmit`)
458
- 3. `bun test packages/cli --coverage` — Run tests with coverage (80% threshold)
458
+ 3. `bun test packages/cli --coverage` — Run tests with coverage (~80%+ overall; `src/` ~95%)
459
459
  4. `bun run build` — TypeScript build check
460
460
 
461
461
  ## Project Structure
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
@@ -14719,6 +14719,7 @@ var init_types = __esm(() => {
14719
14719
  price: exports_external.number().positive().finite().optional(),
14720
14720
  leverage: exports_external.number().int().positive().max(125).optional(),
14721
14721
  test: exports_external.boolean().optional(),
14722
+ idempotencyKey: exports_external.string().min(1).max(512).optional(),
14722
14723
  queuedAt: exports_external.string().min(1).max(64)
14723
14724
  }).strict();
14724
14725
  TradeSignalSchema = exports_external.object({
@@ -14753,7 +14754,7 @@ var init_types = __esm(() => {
14753
14754
  });
14754
14755
 
14755
14756
  // ../shared/src/kvKeys.ts
14756
- var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage", KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size", KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT = "trade:max_daily_drawdown_percent", KV_TRADE_TRAILING_STOP_PERCENT = "trade:trailing_stop_percent", KV_TRADE_KILL_SWITCH = "trade:kill_switch", KV_TRADE_ROUTING = "trade:routing", KV_BOT_ENABLED = "bot:enabled", KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id", KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution", KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error", KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled", KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips", KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode", KV_EMAIL_SCAN_SUBJECT = "email:scan_subject", KV_EMAIL_USE_IMAP = "email:use_imap", KV_EMAIL_COIN_PATTERN = "email:coin_pattern", KV_EMAIL_ACTION_PATTERN = "email:action_pattern", KV_EMAIL_QUANTITY_MULTIPLIER = "email:quantity_multiplier", KV_AGENT_CONFIG = "agent:config", KV_AGENT_OPENAI_KEY = "agent:openai_key", KV_AGENT_ANTHROPIC_KEY = "agent:anthropic_key", KV_AGENT_GOOGLE_KEY = "agent:google_key", KV_DASHBOARD_AI_HEALTH_SUMMARY = "dashboard:ai_health_summary", KV_HEALTH_CHECK = "health_check", KV_HOUSEKEEPING_LAST_CHECK = "housekeeping:last_check", QUEUE_MODE_FAILOVER = "queue_failover", QUEUE_MODE_EVERYWHERE = "queue_everywhere", QUEUE_MODE_DISABLED = "queue_disabled", KVKeys;
14757
+ var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage", KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size", KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT = "trade:max_daily_drawdown_percent", KV_TRADE_TRAILING_STOP_PERCENT = "trade:trailing_stop_percent", KV_TRADE_KILL_SWITCH = "trade:kill_switch", KV_TRADE_ROUTING = "trade:routing", KV_BOT_ENABLED = "bot:enabled", KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id", KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution", KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error", KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled", KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips", KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode", KV_EMAIL_SCAN_SUBJECT = "email:scan_subject", KV_EMAIL_USE_IMAP = "email:use_imap", KV_EMAIL_COIN_PATTERN = "email:coin_pattern", KV_EMAIL_ACTION_PATTERN = "email:action_pattern", KV_EMAIL_QUANTITY_MULTIPLIER = "email:quantity_multiplier", KV_AGENT_CONFIG = "agent:config", KV_AGENT_OPENAI_KEY = "agent:openai_key", KV_AGENT_ANTHROPIC_KEY = "agent:anthropic_key", KV_AGENT_GOOGLE_KEY = "agent:google_key", KV_DASHBOARD_AI_HEALTH_SUMMARY = "dashboard:ai_health_summary", KV_GLOBAL_KILL_SWITCH = "global:kill_switch", KV_HEALTH_CHECK = "health_check", KV_HOUSEKEEPING_LAST_CHECK = "housekeeping:last_check", QUEUE_MODE_FAILOVER = "queue_failover", QUEUE_MODE_EVERYWHERE = "queue_everywhere", QUEUE_MODE_DISABLED = "queue_disabled", KVKeys;
14757
14758
  var init_kvKeys = __esm(() => {
14758
14759
  KVKeys = {
14759
14760
  KV_TRADE_DEFAULT_LEVERAGE,
@@ -14779,6 +14780,7 @@ var init_kvKeys = __esm(() => {
14779
14780
  KV_AGENT_ANTHROPIC_KEY,
14780
14781
  KV_AGENT_GOOGLE_KEY,
14781
14782
  KV_DASHBOARD_AI_HEALTH_SUMMARY,
14783
+ KV_GLOBAL_KILL_SWITCH,
14782
14784
  KV_HEALTH_CHECK,
14783
14785
  KV_HOUSEKEEPING_LAST_CHECK,
14784
14786
  QUEUE_MODE_FAILOVER,
@@ -14787,6 +14789,54 @@ var init_kvKeys = __esm(() => {
14787
14789
  };
14788
14790
  });
14789
14791
 
14792
+ // ../shared/src/kill-switch.ts
14793
+ function isTruthyKillSwitchFlag(value) {
14794
+ if (value == null)
14795
+ return false;
14796
+ const v = value.trim().toLowerCase();
14797
+ return v === "true" || v === "1" || v === "yes" || v === "on";
14798
+ }
14799
+ async function checkKillSwitch(kv, options) {
14800
+ const onMissingKv = options?.onMissingKv ?? "open";
14801
+ const onReadError = options?.onReadError ?? "closed";
14802
+ if (!kv) {
14803
+ if (onMissingKv === "closed") {
14804
+ return { enabled: true, source: "missing_kv" };
14805
+ }
14806
+ return { enabled: false };
14807
+ }
14808
+ try {
14809
+ const values = await Promise.all(KILL_SWITCH_KEYS.map(async (key) => ({
14810
+ key,
14811
+ value: await kv.get(key)
14812
+ })));
14813
+ for (const { key, value } of values) {
14814
+ if (isTruthyKillSwitchFlag(value)) {
14815
+ return { enabled: true, source: key };
14816
+ }
14817
+ }
14818
+ return { enabled: false };
14819
+ } catch (error51) {
14820
+ const errorStr = String(error51);
14821
+ if (onReadError === "closed") {
14822
+ return { enabled: true, source: "read_error", error: errorStr };
14823
+ }
14824
+ return { enabled: false, error: errorStr };
14825
+ }
14826
+ }
14827
+ async function isTradingPaused(kv, options) {
14828
+ const result = await checkKillSwitch(kv, options);
14829
+ return result.enabled;
14830
+ }
14831
+ var KILL_SWITCH_ACTIVE_PREFIX = "KILL_SWITCH_ACTIVE", KILL_SWITCH_KEYS;
14832
+ var init_kill_switch = __esm(() => {
14833
+ init_kvKeys();
14834
+ KILL_SWITCH_KEYS = [
14835
+ KV_TRADE_KILL_SWITCH,
14836
+ KV_GLOBAL_KILL_SWITCH
14837
+ ];
14838
+ });
14839
+
14790
14840
  // ../shared/src/errors.ts
14791
14841
  function toError(err, fallback = "Unknown error") {
14792
14842
  if (err instanceof Error)
@@ -15908,7 +15958,11 @@ var init_config = __esm(() => {
15908
15958
 
15909
15959
  // ../shared/src/operator-transport.ts
15910
15960
  function stripTrailingSlashes(url2) {
15911
- return url2.replace(/\/+$/, "");
15961
+ let end = url2.length;
15962
+ while (end > 0 && url2.charCodeAt(end - 1) === 47) {
15963
+ end -= 1;
15964
+ }
15965
+ return end === url2.length ? url2 : url2.slice(0, end);
15912
15966
  }
15913
15967
  function parseTransport(raw) {
15914
15968
  if (!raw)
@@ -15955,7 +16009,7 @@ function hasOperatorClientCredentials(profile) {
15955
16009
  return Boolean(profile.accessClientId && profile.accessClientSecret);
15956
16010
  }
15957
16011
  function operatorUrl(profile, path2) {
15958
- const base = profile.apiBase.replace(/\/+$/, "");
16012
+ const base = stripTrailingSlashes(profile.apiBase);
15959
16013
  const p = path2.startsWith("/") ? path2 : `/${path2}`;
15960
16014
  return `${base}${p}`;
15961
16015
  }
@@ -22006,6 +22060,8 @@ __export(exports_src, {
22006
22060
  kvManifestFromDashboardManifests: () => kvManifestFromDashboardManifests,
22007
22061
  isWithinHooxHome: () => isWithinHooxHome,
22008
22062
  isUnixModeGroupOrWorldAccessible: () => isUnixModeGroupOrWorldAccessible,
22063
+ isTruthyKillSwitchFlag: () => isTruthyKillSwitchFlag,
22064
+ isTradingPaused: () => isTradingPaused,
22009
22065
  isHooxSetupRoot: () => isHooxSetupRoot,
22010
22066
  isDashboardSectionFlatKv: () => isDashboardSectionFlatKv,
22011
22067
  isDashboardSectionEditable: () => isDashboardSectionEditable,
@@ -22064,6 +22120,7 @@ __export(exports_src, {
22064
22120
  createJsonResponse: () => createJsonResponse,
22065
22121
  createErrorResponse: () => createErrorResponse,
22066
22122
  createCronHandler: () => createCronHandler,
22123
+ checkKillSwitch: () => checkKillSwitch,
22067
22124
  buildOperatorAuthHeaders: () => buildOperatorAuthHeaders,
22068
22125
  buildDashboardKvKey: () => buildDashboardKvKey,
22069
22126
  authenticatedServiceFetch: () => authenticatedServiceFetch,
@@ -22090,6 +22147,8 @@ __export(exports_src, {
22090
22147
  PRESETS: () => PRESETS,
22091
22148
  LogLevelColor: () => LogLevelColor,
22092
22149
  KVKeys: () => KVKeys,
22150
+ KILL_SWITCH_KEYS: () => KILL_SWITCH_KEYS,
22151
+ KILL_SWITCH_ACTIVE_PREFIX: () => KILL_SWITCH_ACTIVE_PREFIX,
22093
22152
  InternalAuthKeyFields: () => InternalAuthKeyFields,
22094
22153
  INTEGRATIONS: () => INTEGRATIONS,
22095
22154
  HOOX_DIR_MODE: () => HOOX_DIR_MODE,
@@ -22127,6 +22186,7 @@ var init_src = __esm(() => {
22127
22186
  init_legal();
22128
22187
  init_types();
22129
22188
  init_kvKeys();
22189
+ init_kill_switch();
22130
22190
  init_errors3();
22131
22191
  init_exchanges();
22132
22192
  init_kvUtils();
@@ -25007,7 +25067,7 @@ class ConfigService {
25007
25067
  const e = result.error;
25008
25068
  switch (e.code) {
25009
25069
  case "not-found":
25010
- 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.`);
25011
25071
  case "invalid-jsonc":
25012
25072
  throw new Error(`Invalid JSONC in ${e.path}:
25013
25073
  ${e.errors.map((m3) => ` - ${m3}`).join(`
@@ -25831,6 +25891,7 @@ class UpdateService {
25831
25891
  }
25832
25892
  async checkAndPromptUpdate(options) {
25833
25893
  const silent = options?.silent === true;
25894
+ const yes = options?.yes === true;
25834
25895
  try {
25835
25896
  const versionCheck = await this.prereqs.checkWranglerVersion();
25836
25897
  if (!versionCheck.outdated) {
@@ -25840,6 +25901,9 @@ class UpdateService {
25840
25901
  }
25841
25902
  return { updated: false };
25842
25903
  }
25904
+ if (silent && !yes) {
25905
+ return { updated: false };
25906
+ }
25843
25907
  const current2 = versionCheck.current ?? "unknown";
25844
25908
  const minimum = versionCheck.minimum ?? "unknown";
25845
25909
  const latest2 = await this.checkLatestVersion();
@@ -26080,7 +26144,7 @@ var exports_schema_service = {};
26080
26144
  __export(exports_schema_service, {
26081
26145
  SchemaService: () => SchemaService
26082
26146
  });
26083
- import { readFileSync as readFileSync5, existsSync as existsSync10 } from "fs";
26147
+ import { readFileSync as readFileSync5, existsSync as existsSync11 } from "fs";
26084
26148
  import { resolve as resolve4 } from "path";
26085
26149
 
26086
26150
  class SchemaService {
@@ -26114,7 +26178,7 @@ class SchemaService {
26114
26178
  const workerWranglerPath = resolve4(workersDir, name, "wrangler.jsonc");
26115
26179
  const devVarsPath = resolve4(workersDir, name, ".dev.vars");
26116
26180
  const errors4 = [];
26117
- const isHooxProject = existsSync10(resolve4(this.projectRoot, "wrangler.jsonc")) || existsSync10(resolve4(this.projectRoot, "workers"));
26181
+ const isHooxProject = existsSync11(resolve4(this.projectRoot, "wrangler.jsonc")) || existsSync11(resolve4(this.projectRoot, "workers"));
26118
26182
  try {
26119
26183
  const wranglerContent = readFileSync5(workerWranglerPath, "utf-8");
26120
26184
  errors4.push(...validateWranglerJsonc(name, manifest, wranglerContent));
@@ -28919,7 +28983,12 @@ function captureHelpEvent(cmd, eventName) {
28919
28983
  // src/utils/completion.ts
28920
28984
  var SUGGEST_NEXT = {
28921
28985
  init: { command: "hoox setup", reason: "provision infrastructure" },
28922
- setup: { command: "hoox deploy all", reason: "ship to Cloudflare" },
28986
+ onboard: { command: "hoox check setup", reason: "verify bootstrap" },
28987
+ setup: { command: "hoox check setup", reason: "verify infrastructure" },
28988
+ "check setup": {
28989
+ command: "hoox deploy all",
28990
+ reason: "ship workers to Cloudflare"
28991
+ },
28923
28992
  "deploy all": { command: "hoox check health", reason: "verify the deploy" },
28924
28993
  "check health": {
28925
28994
  command: "hoox monitor trades",
@@ -28954,6 +29023,7 @@ function getCmdPath(cmd) {
28954
29023
  // src/commands/init/init-command.ts
28955
29024
  init_dist4();
28956
29025
  init_src();
29026
+ import * as fs2 from "fs";
28957
29027
 
28958
29028
  // src/services/cloudflare/index.ts
28959
29029
  init_cloudflare_service();
@@ -29020,21 +29090,32 @@ class CLIProvisioner {
29020
29090
 
29021
29091
  // src/commands/init/init-command.ts
29022
29092
  async function verifyRepoRoot() {
29023
- const rootConfig = Bun.file("wrangler.jsonc");
29024
29093
  const cliPackage = Bun.file("packages/cli/package.json");
29025
- const [rootConfigExists, cliPackageExists] = await Promise.all([
29026
- rootConfig.exists(),
29027
- cliPackage.exists()
29094
+ const wranglerExample = Bun.file("wrangler.jsonc.example");
29095
+ const wranglerConfig = Bun.file("wrangler.jsonc");
29096
+ const gitmodules = Bun.file(".gitmodules");
29097
+ const [
29098
+ cliPackageExists,
29099
+ wranglerExampleExists,
29100
+ wranglerConfigExists,
29101
+ gitmodulesExists
29102
+ ] = await Promise.all([
29103
+ cliPackage.exists(),
29104
+ wranglerExample.exists(),
29105
+ wranglerConfig.exists(),
29106
+ gitmodules.exists()
29028
29107
  ]);
29029
- if (rootConfigExists && cliPackageExists)
29108
+ const workersDirExists = fs2.existsSync("workers");
29109
+ const hasRepoSignal = wranglerExampleExists || wranglerConfigExists || workersDirExists || gitmodulesExists;
29110
+ if (cliPackageExists && hasRepoSignal)
29030
29111
  return;
29031
29112
  const reasons = [];
29032
- if (!rootConfigExists) {
29033
- reasons.push("no root `wrangler.jsonc` found in the current directory");
29034
- }
29035
29113
  if (!cliPackageExists) {
29036
29114
  reasons.push("no `packages/cli` workspace found (this is not the hoox monorepo)");
29037
29115
  }
29116
+ if (cliPackageExists && !hasRepoSignal) {
29117
+ reasons.push("missing Hoox repo markers (expected one of: `wrangler.jsonc.example`, `wrangler.jsonc`, `workers/`, or `.gitmodules`)");
29118
+ }
29038
29119
  const cloneHint = " Clone first: git clone --recursive " + "https://github.com/hoox-sh/hoox.git";
29039
29120
  throw new CLIError(`Not inside a Hoox repository root.
29040
29121
  ` + reasons.join(`
@@ -29207,7 +29288,10 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29207
29288
  if (error51) {
29208
29289
  formatError2(new CLIError(error51, 1 /* ERROR */), globalOpts);
29209
29290
  process.exitCode = 1 /* ERROR */;
29291
+ return;
29210
29292
  }
29293
+ process.env.CLOUDFLARE_API_TOKEN = token;
29294
+ process.env.CLOUDFLARE_ACCOUNT_ID = account;
29211
29295
  if (!globalOpts.quiet) {
29212
29296
  formatSuccess("Cloudflare API token validated", globalOpts);
29213
29297
  }
@@ -29245,6 +29329,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29245
29329
  if (isCancel(resumed)) {
29246
29330
  cancel("Setup cancelled.");
29247
29331
  process.exitCode = 0;
29332
+ return;
29248
29333
  }
29249
29334
  if (!resumed) {
29250
29335
  engine = null;
@@ -29255,18 +29340,22 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29255
29340
  engine = new WizardEngine;
29256
29341
  }
29257
29342
  const skipRiskWarning = options.acceptRisk;
29258
- if (!skipRiskWarning && engine.getCurrentStep().id === "PREREQUISITES") {
29259
- const accepted = await confirm({
29260
- message: "Hoox connects to live trading exchanges and can execute real trades with real money. By continuing, you acknowledge that you are solely responsible for all trading activity and accept the risk of financial loss. Do you accept these terms?",
29261
- initialValue: false
29262
- });
29263
- if (isCancel(accepted)) {
29264
- cancel("Setup cancelled.");
29265
- process.exitCode = 0;
29266
- }
29267
- if (!accepted) {
29268
- outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29269
- process.exitCode = 0;
29343
+ if (engine.getCurrentStep().id === "PREREQUISITES") {
29344
+ if (!skipRiskWarning) {
29345
+ const accepted = await confirm({
29346
+ message: "Hoox connects to live trading exchanges and can execute real trades with real money. By continuing, you acknowledge that you are solely responsible for all trading activity and accept the risk of financial loss. Do you accept these terms?",
29347
+ initialValue: false
29348
+ });
29349
+ if (isCancel(accepted)) {
29350
+ cancel("Setup cancelled.");
29351
+ process.exitCode = 0;
29352
+ return;
29353
+ }
29354
+ if (!accepted) {
29355
+ outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29356
+ process.exitCode = 0;
29357
+ return;
29358
+ }
29270
29359
  }
29271
29360
  engine.execute({ checksPassed: true });
29272
29361
  await saveState(engine);
@@ -29287,6 +29376,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29287
29376
  if (isCancel(apiToken)) {
29288
29377
  cancel("Setup cancelled.");
29289
29378
  process.exitCode = 0;
29379
+ return;
29290
29380
  }
29291
29381
  log.step("Validating Cloudflare API token...");
29292
29382
  const tokenError = await validateApiToken(cf, apiToken);
@@ -29295,6 +29385,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29295
29385
  log.warn("Please check your token and try again.");
29296
29386
  } else {
29297
29387
  tokenValid = true;
29388
+ process.env.CLOUDFLARE_API_TOKEN = apiToken;
29298
29389
  formatSuccess("Cloudflare API token validated", globalOpts);
29299
29390
  }
29300
29391
  }
@@ -29315,6 +29406,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29315
29406
  if (isCancel(accountResult)) {
29316
29407
  cancel("Setup cancelled.");
29317
29408
  process.exitCode = 0;
29409
+ return;
29318
29410
  }
29319
29411
  const secretStoreResult = await text({
29320
29412
  message: "Cloudflare Secret Store ID:",
@@ -29324,6 +29416,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29324
29416
  if (isCancel(secretStoreResult)) {
29325
29417
  cancel("Setup cancelled.");
29326
29418
  process.exitCode = 0;
29419
+ return;
29327
29420
  }
29328
29421
  const prefixResult = await text({
29329
29422
  message: "Subdomain prefix:",
@@ -29338,7 +29431,9 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29338
29431
  if (isCancel(prefixResult)) {
29339
29432
  cancel("Setup cancelled.");
29340
29433
  process.exitCode = 0;
29434
+ return;
29341
29435
  }
29436
+ process.env.CLOUDFLARE_ACCOUNT_ID = accountResult;
29342
29437
  engine.execute({
29343
29438
  apiToken,
29344
29439
  accountId: accountResult,
@@ -29371,6 +29466,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29371
29466
  if (isCancel(presetChoice)) {
29372
29467
  cancel("Setup cancelled.");
29373
29468
  process.exitCode = 0;
29469
+ return;
29374
29470
  }
29375
29471
  engine.execute({ preset: presetChoice });
29376
29472
  await saveState(engine);
@@ -29394,6 +29490,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29394
29490
  if (isCancel(shouldProvision)) {
29395
29491
  cancel("Setup cancelled.");
29396
29492
  process.exitCode = 0;
29493
+ return;
29397
29494
  }
29398
29495
  if (shouldProvision) {
29399
29496
  log.step("Provisioning infrastructure...");
@@ -29443,6 +29540,9 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29443
29540
  process.exitCode = 0;
29444
29541
  }
29445
29542
  });
29543
+ if (isCancel(collected)) {
29544
+ return;
29545
+ }
29446
29546
  collectedSecrets[key] = {};
29447
29547
  for (const [secretName] of secretEntries) {
29448
29548
  const val = collected[secretName];
@@ -29470,6 +29570,7 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29470
29570
  if (isCancel(shouldDeploy)) {
29471
29571
  cancel("Setup cancelled.");
29472
29572
  process.exitCode = 0;
29573
+ return;
29473
29574
  }
29474
29575
  if (shouldDeploy) {
29475
29576
  log.step("Deploying workers...");
@@ -29485,7 +29586,8 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29485
29586
  }
29486
29587
  // src/services/setup/setup-service.ts
29487
29588
  import {
29488
- existsSync as existsSync6,
29589
+ chmodSync as chmodSync3,
29590
+ existsSync as existsSync7,
29489
29591
  mkdirSync as mkdirSync3,
29490
29592
  readFileSync as readFileSync3,
29491
29593
  statSync as statSync2,
@@ -29545,6 +29647,25 @@ var KEYS_DIR = ".keys";
29545
29647
  function escapeRegex2(s) {
29546
29648
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29547
29649
  }
29650
+ function parseEnvFile(text2) {
29651
+ const map2 = new Map;
29652
+ for (const line of text2.split(`
29653
+ `)) {
29654
+ const trimmed = line.trim();
29655
+ if (!trimmed || trimmed.startsWith("#"))
29656
+ continue;
29657
+ const eq = trimmed.indexOf("=");
29658
+ if (eq <= 0)
29659
+ continue;
29660
+ map2.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
29661
+ }
29662
+ return map2;
29663
+ }
29664
+ function safeChmod(path4, mode) {
29665
+ try {
29666
+ chmodSync3(path4, mode);
29667
+ } catch {}
29668
+ }
29548
29669
 
29549
29670
  class SetupService {
29550
29671
  onProgress;
@@ -29573,35 +29694,53 @@ class SetupService {
29573
29694
  TRADE_INTERNAL_KEY: internalKey,
29574
29695
  API_SERVICE_KEY_BINDING: internalKey
29575
29696
  };
29576
- if (!existsSync6(KEYS_DIR)) {
29697
+ if (!existsSync7(KEYS_DIR)) {
29577
29698
  mkdirSync3(KEYS_DIR, { recursive: true });
29578
29699
  } else if (!statSync2(KEYS_DIR).isDirectory()) {
29579
29700
  mkdirSync3(KEYS_DIR, { recursive: true });
29580
29701
  }
29702
+ safeChmod(KEYS_DIR, 448);
29703
+ const setupEnvPath = join7(KEYS_DIR, "setup.env");
29581
29704
  const envLines = Object.entries(keys).map(([k, v]) => `${k}=${v}`);
29582
- await Bun.write(join7(KEYS_DIR, "setup.env"), `# Hoox Setup \u2014 Auto-Generated Keys
29705
+ await Bun.write(setupEnvPath, `# Hoox Setup \u2014 Auto-Generated Keys
29583
29706
  ` + "# Generated by `hoox setup`. NEVER commit this file.\n" + `# Created: ${new Date().toISOString()}
29584
29707
 
29585
29708
  ` + envLines.join(`
29586
29709
  `) + `
29587
29710
  `);
29711
+ safeChmod(setupEnvPath, 384);
29588
29712
  for (const [name, value] of Object.entries(keys)) {
29589
- await Bun.write(join7(KEYS_DIR, `${name.toLowerCase()}.env`), `${name}=${value}
29713
+ const keyPath = join7(KEYS_DIR, `${name.toLowerCase()}.env`);
29714
+ await Bun.write(keyPath, `${name}=${value}
29590
29715
  `);
29716
+ safeChmod(keyPath, 384);
29591
29717
  }
29592
29718
  let devVarsCount = 0;
29593
29719
  for (const [worker, secretNames] of Object.entries(DEV_VARS_WORKER_KEYS)) {
29594
29720
  const path4 = `workers/${workerFsDir(worker)}/.dev.vars`;
29721
+ const meshNames = secretNames;
29722
+ const meshSet = new Set(meshNames);
29723
+ const existing = existsSync7(path4) ? parseEnvFile(readFileSync3(path4, "utf-8")) : new Map;
29724
+ for (const secretName of meshNames) {
29725
+ existing.set(secretName, keys[secretName]);
29726
+ }
29595
29727
  let content = `# .dev.vars \u2014 local secrets for ${worker}
29596
29728
  `;
29597
29729
  content += `# Generated by \`hoox setup\`. NEVER commit this file.
29598
29730
 
29599
29731
  `;
29600
- for (const secretName of secretNames) {
29601
- content += `${secretName}=${keys[secretName]}
29732
+ for (const secretName of meshNames) {
29733
+ content += `${secretName}=${existing.get(secretName)}
29734
+ `;
29735
+ }
29736
+ for (const [k, v] of existing) {
29737
+ if (!meshSet.has(k)) {
29738
+ content += `${k}=${v}
29602
29739
  `;
29740
+ }
29603
29741
  }
29604
29742
  await Bun.write(path4, content);
29743
+ safeChmod(path4, 384);
29605
29744
  devVarsCount++;
29606
29745
  }
29607
29746
  this.onProgress({
@@ -29930,7 +30069,7 @@ class SetupService {
29930
30069
  join7(dir, "pyproject.toml"),
29931
30070
  join7(dir, "wrangler.jsonc")
29932
30071
  ];
29933
- return markers.some((p3) => existsSync6(p3));
30072
+ return markers.some((p3) => existsSync7(p3));
29934
30073
  }
29935
30074
  async ensureWorkers() {
29936
30075
  this.onProgress({
@@ -29975,7 +30114,7 @@ class SetupService {
29975
30114
  step: "workers",
29976
30115
  message: `${missing.length} worker(s) not cloned: ${missing.join(", ")}`
29977
30116
  });
29978
- if (!existsSync6(gitmodulesPath)) {
30117
+ if (!existsSync7(gitmodulesPath)) {
29979
30118
  this.onProgress({
29980
30119
  type: "step-error",
29981
30120
  step: "workers",
@@ -30371,7 +30510,7 @@ class SetupService {
30371
30510
  } else {
30372
30511
  steps.push({ step: "d1-schema", success: true, message: "Skipped" });
30373
30512
  }
30374
- if (keys) {
30513
+ if (!options.skipSecrets && keys) {
30375
30514
  const reconcile = await this.reconcileSecrets(keys);
30376
30515
  if (reconcile.warnings.length > 0) {
30377
30516
  for (const w of reconcile.warnings) {
@@ -30379,20 +30518,25 @@ class SetupService {
30379
30518
  }
30380
30519
  }
30381
30520
  }
30382
- const secretResults = await this.setSecrets(keys);
30383
- results.secrets = secretResults;
30384
- const secretOk = secretResults.filter((r2) => r2.ok).length;
30385
- const secretFail = secretResults.filter((r2) => !r2.ok).length;
30386
- steps.push({
30387
- step: "secrets",
30388
- success: secretFail === 0,
30389
- message: `${secretOk} set, ${secretFail} failed`,
30390
- details: {
30391
- total: secretResults.length,
30392
- ok: secretOk,
30393
- failed: secretFail
30394
- }
30395
- });
30521
+ if (options.skipSecrets) {
30522
+ steps.push({ step: "secrets", success: true, message: "Skipped" });
30523
+ results.secrets = [];
30524
+ } else {
30525
+ const secretResults = await this.setSecrets(keys);
30526
+ results.secrets = secretResults;
30527
+ const secretOk = secretResults.filter((r2) => r2.ok).length;
30528
+ const secretFail = secretResults.filter((r2) => !r2.ok).length;
30529
+ steps.push({
30530
+ step: "secrets",
30531
+ success: secretFail === 0,
30532
+ message: `${secretOk} set, ${secretFail} failed`,
30533
+ details: {
30534
+ total: secretResults.length,
30535
+ ok: secretOk,
30536
+ failed: secretFail
30537
+ }
30538
+ });
30539
+ }
30396
30540
  const dashResult = await this.rebuildDashboard(options.skipDashboard);
30397
30541
  steps.push(dashResult);
30398
30542
  if (keys) {
@@ -30497,12 +30641,27 @@ async function runOnboard(options, fmt) {
30497
30641
  acceptRisk: options.acceptRisk
30498
30642
  };
30499
30643
  await runInitCommand(initOpts, { json: fmt.json, quiet: fmt.quiet }, isNonInteractive);
30644
+ if (process.exitCode && process.exitCode !== 0) {
30645
+ return;
30646
+ }
30647
+ if (options.token) {
30648
+ process.env.CLOUDFLARE_API_TOKEN = options.token;
30649
+ }
30650
+ if (options.account) {
30651
+ process.env.CLOUDFLARE_ACCOUNT_ID = options.account;
30652
+ }
30500
30653
  if (!quiet) {
30501
30654
  process.stdout.write(theme.heading(`
30502
30655
  \u2501\u2501\u2501 Step 2/2: Setup (infrastructure bootstrap) \u2501\u2501\u2501
30503
30656
  `));
30504
30657
  }
30505
30658
  const setupSvc = new SetupService;
30659
+ const authOk = await setupSvc.checkAuth();
30660
+ if (!authOk) {
30661
+ 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);
30662
+ process.exitCode = 1 /* ERROR */;
30663
+ return;
30664
+ }
30506
30665
  const setupOpts = {
30507
30666
  skipKeys: Boolean(options.skipKeys),
30508
30667
  skipDb: Boolean(options.skipDb),
@@ -30514,14 +30673,14 @@ async function runOnboard(options, fmt) {
30514
30673
  const result = await setupSvc.runAll(setupOpts);
30515
30674
  if (result.success) {
30516
30675
  if (!quiet) {
30517
- formatSuccess("Bootstrap complete! Run: hoox check setup to verify.", fmt);
30676
+ formatSuccess("Bootstrap complete! Next: `hoox check setup` then `hoox deploy all`", fmt);
30518
30677
  }
30519
30678
  } else {
30520
- formatError2(new CLIError("Bootstrap completed with issues. See logs above.", 1 /* ERROR */), fmt);
30679
+ 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);
30521
30680
  process.exitCode = 1 /* ERROR */;
30522
30681
  }
30523
30682
  } catch (e) {
30524
- formatError2(new CLIError(e instanceof Error ? e.message : String(e), 1 /* ERROR */), fmt);
30683
+ 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);
30525
30684
  process.exitCode = 1 /* ERROR */;
30526
30685
  }
30527
30686
  }
@@ -31039,7 +31198,7 @@ function renderDefaultSummary(results) {
31039
31198
  init_errors4();
31040
31199
  import {
31041
31200
  statSync as statSync3,
31042
- existsSync as existsSync7,
31201
+ existsSync as existsSync8,
31043
31202
  readFileSync as readFileSync4,
31044
31203
  writeFileSync as writeFileSync3,
31045
31204
  renameSync
@@ -31474,7 +31633,7 @@ ${section}:
31474
31633
  init_main();
31475
31634
  function getDashboardBuildInfo(dashboardPath) {
31476
31635
  const workerPath = resolve2(dashboardPath, ".open-next", "worker.js");
31477
- if (!existsSync7(workerPath)) {
31636
+ if (!existsSync8(workerPath)) {
31478
31637
  return { exists: false };
31479
31638
  }
31480
31639
  const stats = statSync3(workerPath);
@@ -31817,7 +31976,7 @@ async function doUpdateInternalUrls(fmt) {
31817
31976
  const prefix = global.subdomain_prefix ?? "hoox";
31818
31977
  const workers = config2.listEnabledWorkers();
31819
31978
  const filePath = resolve2(process.cwd(), "workers", "dashboard", "wrangler.jsonc");
31820
- if (!existsSync7(filePath)) {
31979
+ if (!existsSync8(filePath)) {
31821
31980
  formatError2(new CLIError("Dashboard wrangler.jsonc not found at workers/dashboard/wrangler.jsonc", 1 /* ERROR */), fmt);
31822
31981
  process.exitCode = 1 /* ERROR */;
31823
31982
  return;
@@ -33894,7 +34053,7 @@ EXAMPLES:
33894
34053
  }
33895
34054
 
33896
34055
  // src/commands/config/keys-subcommands.ts
33897
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
34056
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
33898
34057
  init_theme();
33899
34058
  function generateKey(bytes = 32) {
33900
34059
  const buf = new Uint8Array(bytes);
@@ -33920,7 +34079,7 @@ EXAMPLES:
33920
34079
  hoox config keys generate`).action(withErrorHandling(async (_2, cmd) => {
33921
34080
  const opts = getFormatOptions(cmd);
33922
34081
  const keysDir = ".keys";
33923
- if (!existsSync8(keysDir)) {
34082
+ if (!existsSync9(keysDir)) {
33924
34083
  mkdirSync4(keysDir, { recursive: true });
33925
34084
  }
33926
34085
  const meshKey = generateKey();
@@ -33954,7 +34113,7 @@ EXAMPLES:
33954
34113
  hoox config keys list`).action(withErrorHandling(async (_2, cmd) => {
33955
34114
  const opts = getFormatOptions(cmd);
33956
34115
  const keysDir = ".keys";
33957
- if (!existsSync8(keysDir)) {
34116
+ if (!existsSync9(keysDir)) {
33958
34117
  process.stdout.write(`${theme.dim("No .keys/ directory found.")}
33959
34118
  `);
33960
34119
  return;
@@ -34595,7 +34754,7 @@ async function runSecretsChecks(secretsService, configService, cf) {
34595
34754
  async function runDatabaseChecks(cf, configService) {
34596
34755
  const checks3 = [];
34597
34756
  const d1Worker = configService.getWorker("d1-worker");
34598
- const dbName = d1Worker?.vars?.database_name ?? "my-database";
34757
+ const dbName = d1Worker?.vars?.database_name ?? "trade-data-db";
34599
34758
  const d1ListResult = await cf.d1List();
34600
34759
  if (d1ListResult.ok) {
34601
34760
  const hasDb = d1ListResult.value.includes(dbName);
@@ -36220,7 +36379,7 @@ init_dist4();
36220
36379
  // src/services/db/db-service.ts
36221
36380
  init_config2();
36222
36381
  init_cloudflare_service();
36223
- import { existsSync as existsSync9 } from "fs";
36382
+ import { existsSync as existsSync10 } from "fs";
36224
36383
  import { join as join9 } from "path";
36225
36384
  var D1_WRANGLER_CONFIG_CANDIDATES = [
36226
36385
  "workers/d1-worker/wrangler.jsonc",
@@ -36258,7 +36417,7 @@ class DbService {
36258
36417
  if (vars?.database_name)
36259
36418
  return vars.database_name;
36260
36419
  } catch {}
36261
- return "my-database";
36420
+ return "trade-data-db";
36262
36421
  }
36263
36422
  async apply(dbName, remote, schemaPath) {
36264
36423
  const path6 = schemaPath ?? this.resolveDefaultSchemaPath();
@@ -36322,7 +36481,7 @@ class DbService {
36322
36481
  if (this.wranglerConfigPath)
36323
36482
  return this.wranglerConfigPath;
36324
36483
  for (const candidate of D1_WRANGLER_CONFIG_CANDIDATES) {
36325
- if (existsSync9(candidate))
36484
+ if (existsSync10(candidate))
36326
36485
  return candidate;
36327
36486
  }
36328
36487
  return;
@@ -37417,8 +37576,7 @@ function registerSchemaCommand(program2) {
37417
37576
  const svc = new SchemaService;
37418
37577
  const manifest = svc.getManifest(worker);
37419
37578
  if (!manifest) {
37420
- formatError2(new CLIError(`Unknown worker "${worker}"`, 2 /* INVALID_USAGE */), fmt);
37421
- return;
37579
+ throw new CLIError(`Unknown worker "${worker}"`, 2 /* INVALID_USAGE */, undefined, false, `Known workers: ${svc.getWorkerNames().join(", ") || "(none)"}. Run: hoox schema list`);
37422
37580
  }
37423
37581
  const wranglerContent = generateWranglerJsonc(manifest);
37424
37582
  const devVarsContent = generateDevVars(manifest);
@@ -37453,7 +37611,7 @@ import { spawn } from "child_process";
37453
37611
  import { createRequire } from "module";
37454
37612
  import { resolve as resolve6, dirname as dirname3, join as join10 } from "path";
37455
37613
  import { fileURLToPath } from "url";
37456
- import { existsSync as existsSync11, realpathSync } from "fs";
37614
+ import { existsSync as existsSync12, realpathSync } from "fs";
37457
37615
  import { homedir as homedir5 } from "os";
37458
37616
 
37459
37617
  // src/services/perf/endpoint-resolver.ts
@@ -37517,7 +37675,7 @@ function collectTuiCandidates() {
37517
37675
  return [...new Set(candidates)];
37518
37676
  }
37519
37677
  function pushPackageRootEntries(candidates, root) {
37520
- if (!root || !existsSync11(root))
37678
+ if (!root || !existsSync12(root))
37521
37679
  return;
37522
37680
  candidates.push(join10(root, "dist", "main.js"), join10(root, "src", "main.tsx"), join10(root, "bin", "hoox-tui.js"), join10(root, "src", "main.ts"));
37523
37681
  }
@@ -37545,7 +37703,7 @@ function pushFromWhich(candidates, binName) {
37545
37703
  function resolveTUIEntry() {
37546
37704
  const candidates = collectTuiCandidates();
37547
37705
  for (const path6 of candidates) {
37548
- if (existsSync11(path6))
37706
+ if (existsSync12(path6))
37549
37707
  return path6;
37550
37708
  }
37551
37709
  const runtime = resolveHooxRuntimeRoot();
@@ -37764,13 +37922,13 @@ import { homedir as homedir6 } from "os";
37764
37922
 
37765
37923
  // src/services/runtime/runtime-service.ts
37766
37924
  init_src();
37767
- import { existsSync as existsSync12, lstatSync as lstatSync2, readlinkSync, rmSync } from "fs";
37925
+ import { existsSync as existsSync13, lstatSync as lstatSync2, readlinkSync, rmSync } from "fs";
37768
37926
  import { mkdir } from "fs/promises";
37769
37927
  import { join as join11 } from "path";
37770
37928
  var DEFAULT_HOOX_RUNTIME_REPO = "https://github.com/hoox-sh/hoox.git";
37771
37929
  function firstExisting(paths) {
37772
37930
  for (const p3 of paths) {
37773
- if (existsSync12(p3))
37931
+ if (existsSync13(p3))
37774
37932
  return p3;
37775
37933
  }
37776
37934
  return null;
@@ -37784,7 +37942,7 @@ function getRuntimeStatus(cwd = process.cwd()) {
37784
37942
  repoPath,
37785
37943
  runtime,
37786
37944
  tuiEntry,
37787
- repoPresent: existsSync12(repoPath),
37945
+ repoPresent: existsSync13(repoPath),
37788
37946
  isSetupRoot: isHooxSetupRoot(repoPath)
37789
37947
  };
37790
37948
  }
@@ -37806,7 +37964,7 @@ function isBrokenSymlink(path6) {
37806
37964
  try {
37807
37965
  if (!lstatSync2(path6).isSymbolicLink())
37808
37966
  return false;
37809
- return !existsSync12(path6);
37967
+ return !existsSync13(path6);
37810
37968
  } catch {
37811
37969
  return false;
37812
37970
  }
@@ -37819,7 +37977,7 @@ async function ensureGlobalRuntime(options = {}) {
37819
37977
  await mkdir(hooxHome, { recursive: true });
37820
37978
  let cloned = false;
37821
37979
  if (!isHooxSetupRoot(repoPath)) {
37822
- if (existsSync12(repoPath) || isBrokenSymlink(repoPath)) {
37980
+ if (existsSync13(repoPath) || isBrokenSymlink(repoPath)) {
37823
37981
  if (isBrokenSymlink(repoPath)) {
37824
37982
  let target = "";
37825
37983
  try {
@@ -37851,8 +38009,8 @@ ${clone2.stderr || clone2.stdout}`.trim());
37851
38009
  if (!options.skipInstall) {
37852
38010
  const nodeModules = join11(repoPath, "node_modules");
37853
38011
  const tuiPkg = join11(repoPath, "packages", "tui", "package.json");
37854
- const needsInstall = !existsSync12(nodeModules) || existsSync12(tuiPkg) && !existsSync12(join11(repoPath, "packages", "tui", "node_modules")) && !existsSync12(join11(nodeModules, "@opentui"));
37855
- if (cloned || needsInstall || !existsSync12(nodeModules)) {
38012
+ const needsInstall = !existsSync13(nodeModules) || existsSync13(tuiPkg) && !existsSync13(join11(repoPath, "packages", "tui", "node_modules")) && !existsSync13(join11(nodeModules, "@opentui"));
38013
+ if (cloned || needsInstall || !existsSync13(nodeModules)) {
37856
38014
  log2(`Running bun install in ${repoPath}`);
37857
38015
  const install = await runCommand(["bun", "install"], repoPath);
37858
38016
  if (!install.ok) {
@@ -38529,7 +38687,31 @@ function sanitizeErrorMessage(message, secrets) {
38529
38687
  out = out.replace(/([?&]key=)[^&\s]+/gi, "$1[redacted]");
38530
38688
  return out;
38531
38689
  }
38690
+ var PROBE_ALLOWED_HOSTS = new Set([
38691
+ "api.cloudflare.com",
38692
+ "api.openai.com",
38693
+ "api.anthropic.com",
38694
+ "generativelanguage.googleapis.com",
38695
+ "api.cognitive.microsoft.com"
38696
+ ]);
38697
+ function assertProbeUrl(url2) {
38698
+ let parsed;
38699
+ try {
38700
+ parsed = new URL(url2);
38701
+ } catch {
38702
+ throw new Error(`Invalid probe URL: ${url2}`);
38703
+ }
38704
+ if (parsed.protocol !== "https:") {
38705
+ throw new Error(`Probe URL must be https: ${url2}`);
38706
+ }
38707
+ const host = parsed.hostname.toLowerCase();
38708
+ const allowed = PROBE_ALLOWED_HOSTS.has(host) || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
38709
+ if (!allowed) {
38710
+ throw new Error(`Probe URL host not allowlisted: ${host}`);
38711
+ }
38712
+ }
38532
38713
  async function timedFetch(url2, init, timeoutMs) {
38714
+ assertProbeUrl(url2);
38533
38715
  const start = performance.now();
38534
38716
  const response = await fetch(url2, {
38535
38717
  ...init,
@@ -38638,9 +38820,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38638
38820
  return { status: "online", latencyMs };
38639
38821
  }
38640
38822
  if (response.status === 404) {
38641
- const start = performance.now();
38642
38823
  try {
38643
- const msgRes = await fetch("https://api.anthropic.com/v1/messages", {
38824
+ const { response: msgRes, latencyMs: lat } = await timedFetch("https://api.anthropic.com/v1/messages", {
38644
38825
  method: "POST",
38645
38826
  headers: {
38646
38827
  "x-api-key": apiKey,
@@ -38651,10 +38832,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38651
38832
  model: "claude-sonnet-4-20250514",
38652
38833
  max_tokens: 1,
38653
38834
  messages: [{ role: "user", content: "ping" }]
38654
- }),
38655
- signal: AbortSignal.timeout(timeoutMs)
38656
- });
38657
- const lat = Math.round(performance.now() - start);
38835
+ })
38836
+ }, timeoutMs);
38658
38837
  if (msgRes.ok)
38659
38838
  return { status: "online", latencyMs: lat };
38660
38839
  if (msgRes.status === 401 || msgRes.status === 403) {
@@ -38730,7 +38909,9 @@ async function probeAzure(apiKey, timeoutMs) {
38730
38909
  error: "AZURE_ENDPOINT missing"
38731
38910
  };
38732
38911
  }
38733
- const base = endpoint.replace(/\/+$/, "");
38912
+ let base = endpoint;
38913
+ while (base.endsWith("/"))
38914
+ base = base.slice(0, -1);
38734
38915
  const url2 = `${base}/openai/models?api-version=2024-02-01`;
38735
38916
  try {
38736
38917
  const { response, latencyMs } = await timedFetch(url2, {
@@ -38985,18 +39166,21 @@ EXAMPLES:
38985
39166
  }
38986
39167
  // src/commands/pyne/pyne-command.ts
38987
39168
  init_config2();
38988
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
39169
+ import { existsSync as existsSync14, readFileSync as readFileSync8 } from "fs";
38989
39170
  import { resolve as resolve7 } from "path";
38990
39171
  init_errors4();
38991
- var PYNE_WORKER_DIR = resolve7(process.cwd(), "workers/pyne-worker");
38992
39172
  var DEFAULT_TIMEOUT_MS = 30000;
39173
+ function getPyneWorkerDir() {
39174
+ return resolve7(process.cwd(), "workers/pyne-worker");
39175
+ }
38993
39176
  function requirePyneDir() {
38994
- if (!existsSync13(PYNE_WORKER_DIR)) {
38995
- throw new CLIError(`pyne-worker directory not found: ${PYNE_WORKER_DIR}
39177
+ const dir = getPyneWorkerDir();
39178
+ if (!existsSync14(dir)) {
39179
+ throw new CLIError(`pyne-worker directory not found: ${dir}
38996
39180
  ` + ` Clone it with: hoox clone pyne-worker
38997
39181
  ` + ` Or: git submodule update --init workers/pyne-worker`, 1 /* ERROR */);
38998
39182
  }
38999
- return PYNE_WORKER_DIR;
39183
+ return dir;
39000
39184
  }
39001
39185
  function loadDevVars(workerDir) {
39002
39186
  const out = {};
@@ -39006,7 +39190,7 @@ function loadDevVars(workerDir) {
39006
39190
  ];
39007
39191
  for (const filePath of candidates) {
39008
39192
  try {
39009
- if (!existsSync13(filePath))
39193
+ if (!existsSync14(filePath))
39010
39194
  continue;
39011
39195
  const content = readFileSync8(filePath, "utf-8");
39012
39196
  for (const line of content.split(`
@@ -39064,11 +39248,33 @@ function resolveApiKey(explicit, workerDir) {
39064
39248
  }
39065
39249
  return null;
39066
39250
  }
39251
+ function normalizeBaseUrl(url2) {
39252
+ let end = url2.length;
39253
+ while (end > 0 && url2.charCodeAt(end - 1) === 47) {
39254
+ end -= 1;
39255
+ }
39256
+ return end === url2.length ? url2 : url2.slice(0, end);
39257
+ }
39258
+ function buildPyneRequestUrl(baseUrl, path6) {
39259
+ const base = normalizeBaseUrl(baseUrl);
39260
+ let parsed;
39261
+ try {
39262
+ parsed = new URL(base);
39263
+ } catch {
39264
+ throw new CLIError(`Invalid pyne-worker URL: ${baseUrl}`, 1 /* ERROR */);
39265
+ }
39266
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
39267
+ throw new CLIError(`pyne-worker URL must be http(s): ${baseUrl}`, 1 /* ERROR */);
39268
+ }
39269
+ const suffix = path6.startsWith("/") ? path6 : `/${path6}`;
39270
+ return `${parsed.origin}${parsed.pathname === "/" ? "" : parsed.pathname}${suffix}`;
39271
+ }
39067
39272
  async function pyneFetch(baseUrl, path6, options = {}) {
39068
39273
  const method = options.method ?? "GET";
39069
39274
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39070
39275
  const controller = new AbortController;
39071
39276
  const timer = setTimeout(() => controller.abort(), timeoutMs);
39277
+ const requestUrl = buildPyneRequestUrl(baseUrl, path6);
39072
39278
  const headers = {
39073
39279
  Accept: "application/json"
39074
39280
  };
@@ -39081,7 +39287,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39081
39287
  body = JSON.stringify(options.body);
39082
39288
  }
39083
39289
  try {
39084
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}${path6}`, {
39290
+ const res = await fetch(requestUrl, {
39085
39291
  method,
39086
39292
  headers,
39087
39293
  body,
@@ -39101,8 +39307,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39101
39307
  }
39102
39308
  function printResult(data, fmt) {
39103
39309
  if (fmt.json) {
39104
- process.stdout.write(formatJson(data) + `
39105
- `);
39310
+ formatJson(data, fmt);
39106
39311
  return;
39107
39312
  }
39108
39313
  if (typeof data === "string") {
@@ -39111,8 +39316,7 @@ function printResult(data, fmt) {
39111
39316
  `));
39112
39317
  return;
39113
39318
  }
39114
- process.stdout.write(formatJson(data) + `
39115
- `);
39319
+ formatJson(data, fmt);
39116
39320
  }
39117
39321
  async function spawnInPyneDir(cmd, cwd) {
39118
39322
  const proc = Bun.spawn(cmd, {
@@ -39164,27 +39368,24 @@ EXAMPLES:
39164
39368
  if (!res.ok)
39165
39369
  process.exitCode = 1 /* ERROR */;
39166
39370
  if (fmt.json) {
39167
- process.stdout.write(formatJson(payload) + `
39168
- `);
39371
+ formatJson(payload, fmt);
39169
39372
  } else {
39170
39373
  const icon = res.ok ? "ok" : "fail";
39171
39374
  formatSuccess(`pyne-worker ${icon} \u2014 HTTP ${res.status} in ${latencyMs}ms (${baseUrl})`, fmt);
39172
39375
  if (res.json) {
39173
- process.stdout.write(formatJson(res.json) + `
39174
- `);
39376
+ formatJson(res.json, fmt);
39175
39377
  }
39176
39378
  }
39177
39379
  } catch (err) {
39178
39380
  process.exitCode = 1 /* ERROR */;
39179
39381
  const message = err instanceof Error ? err.message : String(err);
39180
39382
  if (fmt.json) {
39181
- process.stdout.write(formatJson({
39383
+ formatJson({
39182
39384
  worker: "pyne-worker",
39183
39385
  url: `${baseUrl}/health`,
39184
39386
  status: "down",
39185
39387
  error: message
39186
- }) + `
39187
- `);
39388
+ }, fmt);
39188
39389
  } else {
39189
39390
  throw new CLIError(`pyne-worker health failed: ${message}`, 1 /* ERROR */);
39190
39391
  }
@@ -39207,7 +39408,7 @@ EXAMPLES:
39207
39408
  body.script = await new Response(Bun.stdin).text();
39208
39409
  } else {
39209
39410
  const abs = resolve7(process.cwd(), scriptPath);
39210
- if (!existsSync13(abs)) {
39411
+ if (!existsSync14(abs)) {
39211
39412
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39212
39413
  }
39213
39414
  body.script = readFileSync8(abs, "utf-8");
@@ -39258,7 +39459,7 @@ EXAMPLES:
39258
39459
  throw new CLIError("API key required for authenticated endpoints", 2 /* INVALID_USAGE */);
39259
39460
  }
39260
39461
  const abs = resolve7(process.cwd(), scriptPath);
39261
- if (!existsSync13(abs)) {
39462
+ if (!existsSync14(abs)) {
39262
39463
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39263
39464
  }
39264
39465
  const script = readFileSync8(abs, "utf-8");
@@ -39366,7 +39567,7 @@ Pass script flags after -- :
39366
39567
  Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorHandling(async () => {
39367
39568
  const dir = requirePyneDir();
39368
39569
  const script = resolve7(dir, "scripts/sync_vendor.sh");
39369
- if (!existsSync13(script)) {
39570
+ if (!existsSync14(script)) {
39370
39571
  throw new CLIError(`sync_vendor.sh not found at ${script}`, 1 /* ERROR */);
39371
39572
  }
39372
39573
  process.exitCode = await spawnInPyneDir(["bash", script], dir);
@@ -39375,7 +39576,7 @@ Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorH
39375
39576
  const dir = requirePyneDir();
39376
39577
  if (!opts.skipSync) {
39377
39578
  const script = resolve7(dir, "scripts/sync_vendor.sh");
39378
- if (existsSync13(script)) {
39579
+ if (existsSync14(script)) {
39379
39580
  const code = await spawnInPyneDir(["bash", script], dir);
39380
39581
  if (code !== 0) {
39381
39582
  process.exitCode = code;
@@ -40924,7 +41125,7 @@ function registerCompletionCommand(program2) {
40924
41125
  // src/ui/banner.ts
40925
41126
  init_ansis();
40926
41127
  init_theme();
40927
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
41128
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
40928
41129
  import { fileURLToPath as fileURLToPath2 } from "url";
40929
41130
  import { dirname as dirname4, join as join13 } from "path";
40930
41131
  var ORANGE = ansis_default.hex("#ff7f2a");
@@ -40940,7 +41141,7 @@ function findCliVersion() {
40940
41141
  let dir = dirname4(fileURLToPath2(import.meta.url));
40941
41142
  for (let i2 = 0;i2 < 5; i2++) {
40942
41143
  const pkgPath = join13(dir, "package.json");
40943
- if (existsSync14(pkgPath)) {
41144
+ if (existsSync15(pkgPath)) {
40944
41145
  try {
40945
41146
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
40946
41147
  if (pkg.name === PKG_NAME)
@@ -41579,6 +41780,13 @@ Hoox CLI \u2014 Cloudflare Workers Platform
41579
41780
  styleCommandText: (str) => theme.bold(str),
41580
41781
  styleOptionText: (str) => str,
41581
41782
  styleDescriptionText: (str) => theme.dim(str)
41783
+ }).configureOutput({
41784
+ outputError: (str, write) => {
41785
+ 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)) {
41786
+ return;
41787
+ }
41788
+ write(str);
41789
+ }
41582
41790
  });
41583
41791
  program2.option("--json", "Output in JSON format");
41584
41792
  program2.option("--quiet", "Minimal output");
@@ -41593,10 +41801,16 @@ program2.hook("preAction", async () => {
41593
41801
  return;
41594
41802
  if (process.env.BUN_TEST === "1")
41595
41803
  return;
41804
+ if (process.argv.includes("--json"))
41805
+ return;
41596
41806
  try {
41597
41807
  const { UpdateService: UpdateService2 } = await Promise.resolve().then(() => (init_update(), exports_update));
41598
41808
  const service = new UpdateService2;
41599
- await service.checkAndPromptUpdate({ yes: true, silent: true });
41809
+ const autoUpdate = process.env.HOOX_AUTO_UPDATE_WRANGLER === "1";
41810
+ await service.checkAndPromptUpdate({
41811
+ yes: autoUpdate,
41812
+ silent: true
41813
+ });
41600
41814
  } catch {}
41601
41815
  });
41602
41816
  program2.hook("postAction", (thisCmd) => {
@@ -41608,22 +41822,29 @@ program2.hook("postAction", (thisCmd) => {
41608
41822
  const durationMs = Date.now() - startedAt;
41609
41823
  const path6 = getCmdPath(thisCmd);
41610
41824
  const suggestion = suggestNextCommand(path6);
41611
- formatCompletion("Done", { durationMs, suggestion });
41825
+ const g2 = program2.opts();
41826
+ formatCompletion("Done", {
41827
+ durationMs,
41828
+ suggestion,
41829
+ json: Boolean(g2.json),
41830
+ quiet: Boolean(g2.quiet),
41831
+ noColor: g2.color === false || Boolean(process.env.NO_COLOR)
41832
+ });
41612
41833
  });
41613
41834
  program2.exitOverride((err) => {
41614
41835
  const globalOpts = program2.opts();
41615
41836
  const errFmtOpts = {
41616
41837
  json: Boolean(globalOpts.json),
41617
41838
  quiet: Boolean(globalOpts.quiet),
41618
- noColor: Boolean(globalOpts.noColor)
41839
+ noColor: globalOpts.color === false || Boolean(process.env.NO_COLOR)
41619
41840
  };
41620
41841
  if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
41621
41842
  process.exitCode = 0 /* SUCCESS */;
41622
- return;
41843
+ throw err;
41623
41844
  }
41624
- if (err.code === "commander.missingArgument" || err.message === "(outputHelp)" || err.message?.includes("outputHelp")) {
41845
+ if ((err.message === "(outputHelp)" || err.message?.includes("outputHelp")) && err.code !== "commander.missingArgument") {
41625
41846
  process.exitCode = 0 /* SUCCESS */;
41626
- return;
41847
+ throw err;
41627
41848
  }
41628
41849
  if (err.code === "commander.unknownCommand") {
41629
41850
  const match = err.message.match(/'([^']+)'/);
@@ -41631,31 +41852,36 @@ program2.exitOverride((err) => {
41631
41852
  const suggestion = suggestForCommand(program2, badArg);
41632
41853
  formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), suggestion ? { ...errFmtOpts, suggestions: [suggestion] } : errFmtOpts);
41633
41854
  process.exitCode = 2 /* INVALID_USAGE */;
41634
- return;
41855
+ throw err;
41635
41856
  }
41636
41857
  if (err.code === "commander.unknownOption") {
41637
41858
  formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), errFmtOpts);
41638
41859
  process.exitCode = 2 /* INVALID_USAGE */;
41639
- return;
41860
+ throw err;
41640
41861
  }
41641
- if (err.code === "commander.missingMandatoryOptionValue" || err.code === "commander.missingArgument") {
41862
+ if (err.code === "commander.missingArgument" || err.code === "commander.missingMandatoryOptionValue") {
41642
41863
  formatError2(new CLIError(err.message, 2 /* INVALID_USAGE */, undefined, false), errFmtOpts);
41643
41864
  process.exitCode = 2 /* INVALID_USAGE */;
41644
- return;
41865
+ throw err;
41645
41866
  }
41646
41867
  if (err instanceof CLIError) {
41647
41868
  formatError2(err, errFmtOpts);
41648
41869
  process.exitCode = err.code;
41649
- return;
41870
+ throw err;
41650
41871
  }
41651
41872
  formatError2(new CLIError(err.message, 1 /* ERROR */, undefined, false), errFmtOpts);
41652
41873
  process.exitCode = 1 /* ERROR */;
41874
+ throw err;
41653
41875
  });
41654
41876
  process.on("uncaughtException", (err) => {
41877
+ if (process.exitCode !== undefined && process.exitCode !== 0)
41878
+ return;
41655
41879
  formatError2(err instanceof CLIError ? err : new CLIError(`Uncaught exception: ${err.message}`, 1 /* ERROR */, err.stack, false));
41656
41880
  process.exitCode = 1 /* ERROR */;
41657
41881
  });
41658
41882
  process.on("unhandledRejection", (reason) => {
41883
+ if (process.exitCode !== undefined && process.exitCode !== 0)
41884
+ return;
41659
41885
  const err = reason instanceof Error ? reason : new Error(`Unhandled rejection: ${toError(reason)}`);
41660
41886
  formatError2(new CLIError(err.message, 1 /* ERROR */, err.stack, false));
41661
41887
  process.exitCode = 1 /* ERROR */;
@@ -41699,17 +41925,31 @@ async function main() {
41699
41925
  const configFile = Bun.file("wrangler.jsonc");
41700
41926
  const hasConfig = await configFile.exists();
41701
41927
  const wizardStateFile = Bun.file(WIZARD_STATE_PATH);
41702
- const hasWizardState = await wizardStateFile.exists();
41928
+ const hasWizardState = await wizardStateFile.exists() && (await wizardStateFile.text()).trim().length > 0;
41703
41929
  if (!hasConfig) {
41704
41930
  const args = hasWizardState ? ["onboard", "--resume"] : ["onboard"];
41705
41931
  await program2.parseAsync(args, { from: "user" });
41706
41932
  return;
41707
41933
  }
41708
- if (hasWizardState) {
41709
- await Bun.write(WIZARD_STATE_PATH, "");
41934
+ if (await wizardStateFile.exists()) {
41935
+ try {
41936
+ const { unlinkSync } = await import("fs");
41937
+ unlinkSync(WIZARD_STATE_PATH);
41938
+ } catch {
41939
+ await Bun.write(WIZARD_STATE_PATH, "");
41940
+ }
41710
41941
  }
41711
41942
  await runInteractiveTUI(program2);
41712
41943
  }
41944
+ } catch (err) {
41945
+ if (process.exitCode === undefined) {
41946
+ const message = err instanceof Error ? err.message : `Unhandled error: ${String(err)}`;
41947
+ const isCommander = err instanceof Error && "code" in err && typeof err.code === "string" && String(err.code).startsWith("commander.");
41948
+ if (!isCommander) {
41949
+ formatError2(new CLIError(message, 1 /* ERROR */, undefined, false));
41950
+ }
41951
+ process.exitCode = 1 /* ERROR */;
41952
+ }
41713
41953
  } finally {
41714
41954
  if (typeof process.exit === "function" && process.exitCode !== undefined) {
41715
41955
  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.1",
3
+ "version": "0.11.3",
4
4
  "description": "Hoox CLI — manage Cloudflare Workers, infrastructure, secrets, and deployments (@hoox-sh/hoox-cli)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "@hoox-sh/hoox-tui": "workspace:*"
27
27
  },
28
28
  "devDependencies": {
29
- "@hoox-sh/hoox-shared": "^1.2.0",
29
+ "@hoox-sh/hoox-shared": "^1.2.1",
30
30
  "@types/node": "^26.1.1",
31
31
  "bun-types": "^1.3.14",
32
32
  "typescript": "^6.0.3"