@hoox-sh/hoox-cli 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +211 -60
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -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/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();
@@ -29255,18 +29315,20 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29255
29315
  engine = new WizardEngine;
29256
29316
  }
29257
29317
  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;
29318
+ if (engine.getCurrentStep().id === "PREREQUISITES") {
29319
+ if (!skipRiskWarning) {
29320
+ const accepted = await confirm({
29321
+ 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?",
29322
+ initialValue: false
29323
+ });
29324
+ if (isCancel(accepted)) {
29325
+ cancel("Setup cancelled.");
29326
+ process.exitCode = 0;
29327
+ }
29328
+ if (!accepted) {
29329
+ outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29330
+ process.exitCode = 0;
29331
+ }
29270
29332
  }
29271
29333
  engine.execute({ checksPassed: true });
29272
29334
  await saveState(engine);
@@ -33256,6 +33318,35 @@ init_dist4();
33256
33318
  // src/services/secrets/secrets-service.ts
33257
33319
  init_main();
33258
33320
  init_wrangler_output();
33321
+ import { join as join8, resolve as resolvePath } from "path";
33322
+ async function resolveWorkerWranglerConfig(workerPath) {
33323
+ const abs = resolvePath(workerPath);
33324
+ const candidates = [
33325
+ join8(abs, "wrangler.jsonc"),
33326
+ join8(abs, "wrangler.toml"),
33327
+ join8(abs, "wrangler.jsonc.example")
33328
+ ];
33329
+ for (const configPath of candidates) {
33330
+ if (!await Bun.file(configPath).exists())
33331
+ continue;
33332
+ let deployName = null;
33333
+ try {
33334
+ const text2 = await Bun.file(configPath).text();
33335
+ if (configPath.endsWith(".toml")) {
33336
+ const m3 = text2.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
33337
+ if (m3?.[1])
33338
+ deployName = m3[1];
33339
+ } else {
33340
+ const parsed = parse6(text2);
33341
+ if (typeof parsed?.name === "string" && parsed.name.trim()) {
33342
+ deployName = parsed.name.trim();
33343
+ }
33344
+ }
33345
+ } catch {}
33346
+ return { configPath, deployName };
33347
+ }
33348
+ throw new Error(`No wrangler.jsonc (or wrangler.jsonc.example) in ${workerPath}. ` + `Copy the example config into wrangler.jsonc and fill account resources, ` + `then re-run secrets sync.`);
33349
+ }
33259
33350
  var SYSTEM_SECRET_NAMES = [
33260
33351
  "INTERNAL_KEY_BINDING",
33261
33352
  "AGENT_INTERNAL_KEY",
@@ -33400,7 +33491,7 @@ class SecretsService {
33400
33491
  try {
33401
33492
  const value = existingValues.get(secret);
33402
33493
  if (value !== undefined && !this.isPlaceholder(value)) {
33403
- await this.execWranglerSecretPut(worker.path, secret, value);
33494
+ await this.execWranglerSecretPut(worker.path, secret, value, workerName);
33404
33495
  synced.push(secret);
33405
33496
  items.push({ name: secret, status: "synced" });
33406
33497
  } else {
@@ -33456,9 +33547,15 @@ class SecretsService {
33456
33547
  const v = value.trim();
33457
33548
  return v === "" || v.startsWith("placeholder_") || v.startsWith("your_") || v.startsWith("generate_") || v.startsWith("<") || v.includes("generate-with");
33458
33549
  }
33459
- async execWranglerSecretPut(workerPath, name, value) {
33460
- const proc = Bun.spawn(["wrangler", "secret", "put", name], {
33461
- cwd: workerPath,
33550
+ async execWranglerSecretPut(workerPath, name, value, logicalWorkerName) {
33551
+ const { configPath, deployName } = await resolveWorkerWranglerConfig(workerPath);
33552
+ const workerName = deployName ?? logicalWorkerName ?? null;
33553
+ const args = ["wrangler", "secret", "put", name, "-c", configPath];
33554
+ if (workerName) {
33555
+ args.push("--name", workerName);
33556
+ }
33557
+ const proc = Bun.spawn(args, {
33558
+ cwd: resolvePath(workerPath),
33462
33559
  stdin: "pipe",
33463
33560
  stdout: "pipe",
33464
33561
  stderr: "pipe"
@@ -33725,7 +33822,18 @@ EXAMPLES:
33725
33822
  throw new CLIError(`Secret "${secretName}" is not declared for worker "${workerName}".`, 2 /* INVALID_USAGE */);
33726
33823
  }
33727
33824
  const workerPath = svc.getWorkerPath(workerName) ?? `workers/${workerName}`;
33728
- const proc = Bun.spawn(["wrangler", "secret", "delete", secretName], {
33825
+ const { configPath, deployName } = await resolveWorkerWranglerConfig(workerPath);
33826
+ const deploy = deployName ?? workerName;
33827
+ const proc = Bun.spawn([
33828
+ "wrangler",
33829
+ "secret",
33830
+ "delete",
33831
+ secretName,
33832
+ "-c",
33833
+ configPath,
33834
+ "--name",
33835
+ deploy
33836
+ ], {
33729
33837
  cwd: workerPath,
33730
33838
  stdout: "pipe",
33731
33839
  stderr: "pipe"
@@ -36175,7 +36283,7 @@ init_dist4();
36175
36283
  init_config2();
36176
36284
  init_cloudflare_service();
36177
36285
  import { existsSync as existsSync9 } from "fs";
36178
- import { join as join8 } from "path";
36286
+ import { join as join9 } from "path";
36179
36287
  var D1_WRANGLER_CONFIG_CANDIDATES = [
36180
36288
  "workers/d1-worker/wrangler.jsonc",
36181
36289
  "workers/trade-worker/wrangler.jsonc"
@@ -36192,13 +36300,13 @@ class DbService {
36192
36300
  }
36193
36301
  resolveDefaultSchemaPath() {
36194
36302
  if (this.homeDir) {
36195
- return join8(this.homeDir, ".hoox", "workers", "trade-worker", "schema.sql");
36303
+ return join9(this.homeDir, ".hoox", "workers", "trade-worker", "schema.sql");
36196
36304
  }
36197
36305
  return "workers/trade-worker/schema.sql";
36198
36306
  }
36199
36307
  resolveMigrationScriptPath() {
36200
36308
  if (this.homeDir) {
36201
- return join8(this.homeDir, ".hoox", "scripts", "migrate-tracking.sh");
36309
+ return join9(this.homeDir, ".hoox", "scripts", "migrate-tracking.sh");
36202
36310
  }
36203
36311
  return "scripts/migrate-tracking.sh";
36204
36312
  }
@@ -37405,7 +37513,7 @@ init_theme();
37405
37513
  init_errors4();
37406
37514
  import { spawn } from "child_process";
37407
37515
  import { createRequire } from "module";
37408
- import { resolve as resolve6, dirname as dirname3, join as join9 } from "path";
37516
+ import { resolve as resolve6, dirname as dirname3, join as join10 } from "path";
37409
37517
  import { fileURLToPath } from "url";
37410
37518
  import { existsSync as existsSync11, realpathSync } from "fs";
37411
37519
  import { homedir as homedir5 } from "os";
@@ -37458,14 +37566,14 @@ function collectTuiCandidates() {
37458
37566
  candidates.push(resolve6(__dirname2, "../../../../tui/src/main.tsx"), resolve6(__dirname2, "../../../../tui/dist/main.js"), resolve6(__dirname2, "../../../tui/src/main.tsx"), resolve6(__dirname2, "../../../tui/dist/main.js"), resolve6(process.cwd(), "packages/tui/src/main.tsx"), resolve6(process.cwd(), "packages/tui/dist/main.js"), resolve6(process.cwd(), "../tui/src/main.tsx"), resolve6(process.cwd(), "../tui/dist/main.js"));
37459
37567
  pushPackageEntries(candidates, require2);
37460
37568
  try {
37461
- const cwdRequire = createRequire(join9(process.cwd(), "package.json"));
37569
+ const cwdRequire = createRequire(join10(process.cwd(), "package.json"));
37462
37570
  pushPackageEntries(candidates, cwdRequire);
37463
37571
  } catch {}
37464
- const bunGlobal = join9(homedir5(), ".bun", "install", "global", "node_modules", TUI_PKG);
37572
+ const bunGlobal = join10(homedir5(), ".bun", "install", "global", "node_modules", TUI_PKG);
37465
37573
  pushPackageRootEntries(candidates, bunGlobal);
37466
37574
  const npmGlobal = process.env.npm_config_prefix?.trim();
37467
37575
  if (npmGlobal) {
37468
- pushPackageRootEntries(candidates, join9(npmGlobal, "lib", "node_modules", TUI_PKG));
37576
+ pushPackageRootEntries(candidates, join10(npmGlobal, "lib", "node_modules", TUI_PKG));
37469
37577
  }
37470
37578
  pushFromWhich(candidates, "hoox-tui");
37471
37579
  return [...new Set(candidates)];
@@ -37473,7 +37581,7 @@ function collectTuiCandidates() {
37473
37581
  function pushPackageRootEntries(candidates, root) {
37474
37582
  if (!root || !existsSync11(root))
37475
37583
  return;
37476
- candidates.push(join9(root, "dist", "main.js"), join9(root, "src", "main.tsx"), join9(root, "bin", "hoox-tui.js"), join9(root, "src", "main.ts"));
37584
+ candidates.push(join10(root, "dist", "main.js"), join10(root, "src", "main.tsx"), join10(root, "bin", "hoox-tui.js"), join10(root, "src", "main.ts"));
37477
37585
  }
37478
37586
  function pushPackageEntries(candidates, req) {
37479
37587
  try {
@@ -37713,14 +37821,14 @@ TUI exited with code ${code}
37713
37821
  // src/commands/doctor/doctor-command.ts
37714
37822
  init_dist4();
37715
37823
  init_src();
37716
- import { join as join11 } from "path";
37824
+ import { join as join12 } from "path";
37717
37825
  import { homedir as homedir6 } from "os";
37718
37826
 
37719
37827
  // src/services/runtime/runtime-service.ts
37720
37828
  init_src();
37721
37829
  import { existsSync as existsSync12, lstatSync as lstatSync2, readlinkSync, rmSync } from "fs";
37722
37830
  import { mkdir } from "fs/promises";
37723
- import { join as join10 } from "path";
37831
+ import { join as join11 } from "path";
37724
37832
  var DEFAULT_HOOX_RUNTIME_REPO = "https://github.com/hoox-sh/hoox.git";
37725
37833
  function firstExisting(paths) {
37726
37834
  for (const p3 of paths) {
@@ -37803,9 +37911,9 @@ ${clone2.stderr || clone2.stdout}`.trim());
37803
37911
  }
37804
37912
  let installed = false;
37805
37913
  if (!options.skipInstall) {
37806
- const nodeModules = join10(repoPath, "node_modules");
37807
- const tuiPkg = join10(repoPath, "packages", "tui", "package.json");
37808
- const needsInstall = !existsSync12(nodeModules) || existsSync12(tuiPkg) && !existsSync12(join10(repoPath, "packages", "tui", "node_modules")) && !existsSync12(join10(nodeModules, "@opentui"));
37914
+ const nodeModules = join11(repoPath, "node_modules");
37915
+ 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"));
37809
37917
  if (cloned || needsInstall || !existsSync12(nodeModules)) {
37810
37918
  log2(`Running bun install in ${repoPath}`);
37811
37919
  const install = await runCommand(["bun", "install"], repoPath);
@@ -38134,7 +38242,7 @@ Hoox doctor
38134
38242
  return failed === 0 ? 0 /* SUCCESS */ : 1 /* ERROR */;
38135
38243
  }
38136
38244
  function printSecurityHygieneSection(env = process.env) {
38137
- const configPath = join11(homedir6(), ".hoox", "config.json");
38245
+ const configPath = join12(homedir6(), ".hoox", "config.json");
38138
38246
  const lines = collectSecurityHygiene(env, { configPath });
38139
38247
  process.stdout.write(theme.heading(`Security hygiene
38140
38248
  `));
@@ -38152,7 +38260,7 @@ async function runSecurityDoctor(options) {
38152
38260
  env.HOOX_API_URL = options.apiUrl;
38153
38261
  }
38154
38262
  const profile = resolveOperatorTransportProfile(env);
38155
- const configPath = join11(homedir6(), ".hoox", "config.json");
38263
+ const configPath = join12(homedir6(), ".hoox", "config.json");
38156
38264
  const hygiene = collectSecurityHygiene(env, { configPath });
38157
38265
  const isLocal = profile.apiBase.includes("localhost") || profile.apiBase.includes("127.0.0.1");
38158
38266
  const shouldProbe = options.probe === true || options.probe !== false && !isLocal;
@@ -38483,7 +38591,31 @@ function sanitizeErrorMessage(message, secrets) {
38483
38591
  out = out.replace(/([?&]key=)[^&\s]+/gi, "$1[redacted]");
38484
38592
  return out;
38485
38593
  }
38594
+ var PROBE_ALLOWED_HOSTS = new Set([
38595
+ "api.cloudflare.com",
38596
+ "api.openai.com",
38597
+ "api.anthropic.com",
38598
+ "generativelanguage.googleapis.com",
38599
+ "api.cognitive.microsoft.com"
38600
+ ]);
38601
+ function assertProbeUrl(url2) {
38602
+ let parsed;
38603
+ try {
38604
+ parsed = new URL(url2);
38605
+ } catch {
38606
+ throw new Error(`Invalid probe URL: ${url2}`);
38607
+ }
38608
+ if (parsed.protocol !== "https:") {
38609
+ throw new Error(`Probe URL must be https: ${url2}`);
38610
+ }
38611
+ const host = parsed.hostname.toLowerCase();
38612
+ const allowed = PROBE_ALLOWED_HOSTS.has(host) || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
38613
+ if (!allowed) {
38614
+ throw new Error(`Probe URL host not allowlisted: ${host}`);
38615
+ }
38616
+ }
38486
38617
  async function timedFetch(url2, init, timeoutMs) {
38618
+ assertProbeUrl(url2);
38487
38619
  const start = performance.now();
38488
38620
  const response = await fetch(url2, {
38489
38621
  ...init,
@@ -38592,9 +38724,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38592
38724
  return { status: "online", latencyMs };
38593
38725
  }
38594
38726
  if (response.status === 404) {
38595
- const start = performance.now();
38596
38727
  try {
38597
- const msgRes = await fetch("https://api.anthropic.com/v1/messages", {
38728
+ const { response: msgRes, latencyMs: lat } = await timedFetch("https://api.anthropic.com/v1/messages", {
38598
38729
  method: "POST",
38599
38730
  headers: {
38600
38731
  "x-api-key": apiKey,
@@ -38605,10 +38736,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38605
38736
  model: "claude-sonnet-4-20250514",
38606
38737
  max_tokens: 1,
38607
38738
  messages: [{ role: "user", content: "ping" }]
38608
- }),
38609
- signal: AbortSignal.timeout(timeoutMs)
38610
- });
38611
- const lat = Math.round(performance.now() - start);
38739
+ })
38740
+ }, timeoutMs);
38612
38741
  if (msgRes.ok)
38613
38742
  return { status: "online", latencyMs: lat };
38614
38743
  if (msgRes.status === 401 || msgRes.status === 403) {
@@ -38684,7 +38813,9 @@ async function probeAzure(apiKey, timeoutMs) {
38684
38813
  error: "AZURE_ENDPOINT missing"
38685
38814
  };
38686
38815
  }
38687
- const base = endpoint.replace(/\/+$/, "");
38816
+ let base = endpoint;
38817
+ while (base.endsWith("/"))
38818
+ base = base.slice(0, -1);
38688
38819
  const url2 = `${base}/openai/models?api-version=2024-02-01`;
38689
38820
  try {
38690
38821
  const { response, latencyMs } = await timedFetch(url2, {
@@ -38942,15 +39073,18 @@ init_config2();
38942
39073
  import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
38943
39074
  import { resolve as resolve7 } from "path";
38944
39075
  init_errors4();
38945
- var PYNE_WORKER_DIR = resolve7(process.cwd(), "workers/pyne-worker");
38946
39076
  var DEFAULT_TIMEOUT_MS = 30000;
39077
+ function getPyneWorkerDir() {
39078
+ return resolve7(process.cwd(), "workers/pyne-worker");
39079
+ }
38947
39080
  function requirePyneDir() {
38948
- if (!existsSync13(PYNE_WORKER_DIR)) {
38949
- throw new CLIError(`pyne-worker directory not found: ${PYNE_WORKER_DIR}
39081
+ const dir = getPyneWorkerDir();
39082
+ if (!existsSync13(dir)) {
39083
+ throw new CLIError(`pyne-worker directory not found: ${dir}
38950
39084
  ` + ` Clone it with: hoox clone pyne-worker
38951
39085
  ` + ` Or: git submodule update --init workers/pyne-worker`, 1 /* ERROR */);
38952
39086
  }
38953
- return PYNE_WORKER_DIR;
39087
+ return dir;
38954
39088
  }
38955
39089
  function loadDevVars(workerDir) {
38956
39090
  const out = {};
@@ -39018,11 +39152,33 @@ function resolveApiKey(explicit, workerDir) {
39018
39152
  }
39019
39153
  return null;
39020
39154
  }
39155
+ function normalizeBaseUrl(url2) {
39156
+ let end = url2.length;
39157
+ while (end > 0 && url2.charCodeAt(end - 1) === 47) {
39158
+ end -= 1;
39159
+ }
39160
+ return end === url2.length ? url2 : url2.slice(0, end);
39161
+ }
39162
+ function buildPyneRequestUrl(baseUrl, path6) {
39163
+ const base = normalizeBaseUrl(baseUrl);
39164
+ let parsed;
39165
+ try {
39166
+ parsed = new URL(base);
39167
+ } catch {
39168
+ throw new CLIError(`Invalid pyne-worker URL: ${baseUrl}`, 1 /* ERROR */);
39169
+ }
39170
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
39171
+ throw new CLIError(`pyne-worker URL must be http(s): ${baseUrl}`, 1 /* ERROR */);
39172
+ }
39173
+ const suffix = path6.startsWith("/") ? path6 : `/${path6}`;
39174
+ return `${parsed.origin}${parsed.pathname === "/" ? "" : parsed.pathname}${suffix}`;
39175
+ }
39021
39176
  async function pyneFetch(baseUrl, path6, options = {}) {
39022
39177
  const method = options.method ?? "GET";
39023
39178
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39024
39179
  const controller = new AbortController;
39025
39180
  const timer = setTimeout(() => controller.abort(), timeoutMs);
39181
+ const requestUrl = buildPyneRequestUrl(baseUrl, path6);
39026
39182
  const headers = {
39027
39183
  Accept: "application/json"
39028
39184
  };
@@ -39035,7 +39191,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39035
39191
  body = JSON.stringify(options.body);
39036
39192
  }
39037
39193
  try {
39038
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}${path6}`, {
39194
+ const res = await fetch(requestUrl, {
39039
39195
  method,
39040
39196
  headers,
39041
39197
  body,
@@ -39055,8 +39211,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39055
39211
  }
39056
39212
  function printResult(data, fmt) {
39057
39213
  if (fmt.json) {
39058
- process.stdout.write(formatJson(data) + `
39059
- `);
39214
+ formatJson(data, fmt);
39060
39215
  return;
39061
39216
  }
39062
39217
  if (typeof data === "string") {
@@ -39065,8 +39220,7 @@ function printResult(data, fmt) {
39065
39220
  `));
39066
39221
  return;
39067
39222
  }
39068
- process.stdout.write(formatJson(data) + `
39069
- `);
39223
+ formatJson(data, fmt);
39070
39224
  }
39071
39225
  async function spawnInPyneDir(cmd, cwd) {
39072
39226
  const proc = Bun.spawn(cmd, {
@@ -39118,27 +39272,24 @@ EXAMPLES:
39118
39272
  if (!res.ok)
39119
39273
  process.exitCode = 1 /* ERROR */;
39120
39274
  if (fmt.json) {
39121
- process.stdout.write(formatJson(payload) + `
39122
- `);
39275
+ formatJson(payload, fmt);
39123
39276
  } else {
39124
39277
  const icon = res.ok ? "ok" : "fail";
39125
39278
  formatSuccess(`pyne-worker ${icon} \u2014 HTTP ${res.status} in ${latencyMs}ms (${baseUrl})`, fmt);
39126
39279
  if (res.json) {
39127
- process.stdout.write(formatJson(res.json) + `
39128
- `);
39280
+ formatJson(res.json, fmt);
39129
39281
  }
39130
39282
  }
39131
39283
  } catch (err) {
39132
39284
  process.exitCode = 1 /* ERROR */;
39133
39285
  const message = err instanceof Error ? err.message : String(err);
39134
39286
  if (fmt.json) {
39135
- process.stdout.write(formatJson({
39287
+ formatJson({
39136
39288
  worker: "pyne-worker",
39137
39289
  url: `${baseUrl}/health`,
39138
39290
  status: "down",
39139
39291
  error: message
39140
- }) + `
39141
- `);
39292
+ }, fmt);
39142
39293
  } else {
39143
39294
  throw new CLIError(`pyne-worker health failed: ${message}`, 1 /* ERROR */);
39144
39295
  }
@@ -40880,7 +41031,7 @@ init_ansis();
40880
41031
  init_theme();
40881
41032
  import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
40882
41033
  import { fileURLToPath as fileURLToPath2 } from "url";
40883
- import { dirname as dirname4, join as join12 } from "path";
41034
+ import { dirname as dirname4, join as join13 } from "path";
40884
41035
  var ORANGE = ansis_default.hex("#ff7f2a");
40885
41036
  var AMBER = ansis_default.hex("#ffb722");
40886
41037
  var INDIGO = ansis_default.hex("#818cf8");
@@ -40893,7 +41044,7 @@ function findCliVersion() {
40893
41044
  const PKG_NAME = "@hoox-sh/hoox-cli";
40894
41045
  let dir = dirname4(fileURLToPath2(import.meta.url));
40895
41046
  for (let i2 = 0;i2 < 5; i2++) {
40896
- const pkgPath = join12(dir, "package.json");
41047
+ const pkgPath = join13(dir, "package.json");
40897
41048
  if (existsSync14(pkgPath)) {
40898
41049
  try {
40899
41050
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoox-sh/hoox-cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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"