@hoox-sh/hoox-cli 0.11.5 → 0.11.7

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 +46 -5
  2. package/dist/index.js +346 -121
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -6,10 +6,11 @@
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.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.
9
+ **v0.11.x** — monorepo auto-detect + remember (run `hx` from any directory), Linear Rail banner, quieter secrets check, operator security plane (`doctor --security`, `tunnel check`), PYNE (`hoox pyne`), schema validate/generate, wrangler **4.x**.
10
10
 
11
11
  ## Features
12
12
 
13
+ - **Monorepo auto-detect & remember**: On every start the CLI finds the hoox monorepo (`HOOX_REPO` → walk up from cwd → `~/.hoox/config/monorepo.json` → `~/.hoox/repo`), saves the path, and `chdir`s into it so `hx` / `hoox` work from `~/Videos` or any other folder after you have run once from the monorepo.
13
14
  - **One-shot Onboarding** (`hoox onboard`): Full bootstrap from a fresh clone — collects credentials, configures workers, provisions D1/KV, generates keys, pushes secrets, deploys dashboard. The recommended entry point for new users.
14
15
  - **Config & Setup Wizards** (`hoox init`, `hoox setup`): Split into two steps — `init` writes `wrangler.jsonc` and collects integration secrets; `setup` generates keys, applies D1 schema, and pushes secrets to Cloudflare. Run separately when you need fine-grained control.
15
16
  - **Infrastructure as Code**: Manage D1, KV, R2, Queues, Vectorize, and Analytics via `hoox infra`.
@@ -56,6 +57,10 @@ bun run build # produces dist/index.js — required for `hoox` after global in
56
57
  ## Quick Start
57
58
 
58
59
  ```bash
60
+ # From the monorepo root (first time — path is remembered)
61
+ cd /path/to/hoox
62
+ hoox doctor # shows Remembered + Runtime root
63
+
59
64
  # One-shot onboarding (collects credentials, provisions, deploys)
60
65
  hoox onboard
61
66
 
@@ -70,8 +75,42 @@ hoox check health
70
75
  # Deploy to Cloudflare
71
76
  hoox deploy all --auto
72
77
 
73
- # Measure fast-path latency
74
- hoox perf fastpath run --n 50
78
+ # Later: works from any directory (uses remembered monorepo)
79
+ cd ~/Videos
80
+ hx check health
81
+ hx doctor # Source: remembered
82
+ ```
83
+
84
+ ## Monorepo resolution (any cwd)
85
+
86
+ Resolution order at CLI startup:
87
+
88
+ 1. **`HOOX_REPO`** — absolute path override (highest priority)
89
+ 2. **Walk up from cwd** — discovers a local checkout (`packages/cli` + repo markers)
90
+ 3. **Remembered path** — `~/.hoox/config/monorepo.json` (written when discovered via cwd/env)
91
+ 4. **Global clone** — `~/.hoox/repo` (create with `hoox doctor --fix-runtime`)
92
+
93
+ When a root is found, the CLI:
94
+
95
+ - refreshes the remembered path (when source is `cwd` / `env` / `remembered`)
96
+ - sets session `HOOX_REPO` for child processes
97
+ - `chdir`s into the monorepo so relative paths (`wrangler.jsonc`, `workers/`, …) work
98
+
99
+ Markers for a valid monorepo: `packages/cli/package.json` **plus** one of `wrangler.jsonc`, `wrangler.jsonc.example`, `workers/`, or `.gitmodules`.
100
+
101
+ | Env / path | Purpose |
102
+ | ------------------------------ | ------------------------------------------------------ |
103
+ | `HOOX_REPO` | Force monorepo root |
104
+ | `HOOX_HOME` | Override `~/.hoox` (default home for config/data/repo) |
105
+ | `HOOX_CLI_SILENT=1` | Suppress “using monorepo at …” notice |
106
+ | `~/.hoox/config/monorepo.json` | Persisted root `{ "root", "updatedAt" }` |
107
+
108
+ ```bash
109
+ # Inspect layout
110
+ hoox doctor
111
+ # Remembered /home/you/Git/hoox
112
+ # Runtime root /home/you/Git/hoox
113
+ # Source remembered
75
114
  ```
76
115
 
77
116
  ## Available Commands
@@ -358,8 +397,9 @@ hoox perf fastpath report --from 1h
358
397
  ### Operator Security & Private Ingress
359
398
 
360
399
  ```bash
361
- # Paths, TUI entry, and runtime layout
400
+ # Paths, TUI entry, remembered monorepo, runtime layout
362
401
  hoox doctor
402
+ hoox doctor --fix-runtime # clone monorepo into ~/.hoox/repo if needed
363
403
 
364
404
  # Hygiene + optional management /v1/health probes
365
405
  hoox doctor --security
@@ -504,8 +544,9 @@ packages/cli/
504
544
  │ │ ├── perf/ # Percentile, probe-sender, observability-reader
505
545
  │ │ ├── prerequisites/ # Tool version checks
506
546
  │ │ ├── runtime/ # Global ~/.hoox layout
547
+ │ │ ├── workspace/ # Monorepo detect, remember, chdir
507
548
  │ │ └── secrets/ # Secret management
508
- │ ├── ui/ # Interactive menu (clack)
549
+ │ ├── ui/ # Interactive menu + Linear Rail banner
509
550
  │ └── utils/ # Errors, formatters, theme
510
551
  ├── bin/
511
552
  │ └── hoox.js # CLI binary entry point
package/dist/index.js CHANGED
@@ -16200,7 +16200,13 @@ var init_sse = __esm(() => {
16200
16200
  });
16201
16201
 
16202
16202
  // ../shared/src/path-utils.ts
16203
- import { existsSync as existsSync2 } from "fs";
16203
+ import {
16204
+ existsSync as existsSync2,
16205
+ mkdirSync as mkdirSync2,
16206
+ readFileSync as readFileSync2,
16207
+ writeFileSync as writeFileSync2,
16208
+ chmodSync as chmodSync2
16209
+ } from "fs";
16204
16210
  import { homedir as homedir2 } from "os";
16205
16211
  import { dirname, join as join2, resolve } from "path";
16206
16212
  function createHooxPath(path2) {
@@ -16226,7 +16232,55 @@ function isHooxSetupRoot(dir) {
16226
16232
  return false;
16227
16233
  try {
16228
16234
  const root = resolve(dir);
16229
- return existsSync2(join2(root, "wrangler.jsonc")) && existsSync2(join2(root, "packages", "cli", "package.json"));
16235
+ if (!existsSync2(join2(root, "packages", "cli", "package.json"))) {
16236
+ return false;
16237
+ }
16238
+ return existsSync2(join2(root, "wrangler.jsonc")) || existsSync2(join2(root, "wrangler.jsonc.example")) || existsSync2(join2(root, "workers")) || existsSync2(join2(root, ".gitmodules"));
16239
+ } catch {
16240
+ return false;
16241
+ }
16242
+ }
16243
+ function getRememberedMonorepoPath() {
16244
+ return resolveHooxPath("config/monorepo.json");
16245
+ }
16246
+ function getRememberedMonorepoRoot() {
16247
+ try {
16248
+ const path2 = getRememberedMonorepoPath();
16249
+ if (!existsSync2(path2))
16250
+ return null;
16251
+ const raw = readFileSync2(path2, "utf-8");
16252
+ const data = JSON.parse(raw);
16253
+ if (!data.root || typeof data.root !== "string")
16254
+ return null;
16255
+ const resolved = resolve(data.root);
16256
+ return isHooxSetupRoot(resolved) ? resolved : null;
16257
+ } catch {
16258
+ return null;
16259
+ }
16260
+ }
16261
+ function rememberMonorepoRoot(root) {
16262
+ try {
16263
+ const resolved = resolve(root);
16264
+ if (!isHooxSetupRoot(resolved))
16265
+ return false;
16266
+ const configDir = getHooxConfigDir();
16267
+ mkdirSync2(configDir, { recursive: true, mode: 448 });
16268
+ try {
16269
+ chmodSync2(configDir, 448);
16270
+ } catch {}
16271
+ const payload = {
16272
+ root: resolved,
16273
+ updatedAt: new Date().toISOString()
16274
+ };
16275
+ const path2 = getRememberedMonorepoPath();
16276
+ writeFileSync2(path2, JSON.stringify(payload, null, 2) + `
16277
+ `, {
16278
+ mode: 384
16279
+ });
16280
+ try {
16281
+ chmodSync2(path2, 384);
16282
+ } catch {}
16283
+ return true;
16230
16284
  } catch {
16231
16285
  return false;
16232
16286
  }
@@ -16246,6 +16300,7 @@ function resolveHooxRuntimeRoot(options) {
16246
16300
  const env = options?.env ?? process.env;
16247
16301
  const cwd = resolve(options?.cwd ?? process.cwd());
16248
16302
  const globalRepo = getHooxRepoPath();
16303
+ const remembered = options?.skipRemembered ? null : getRememberedMonorepoRoot();
16249
16304
  const envRepo = env.HOOX_REPO?.trim();
16250
16305
  if (envRepo) {
16251
16306
  const resolved = resolve(envRepo);
@@ -16256,6 +16311,7 @@ function resolveHooxRuntimeRoot(options) {
16256
16311
  checked: {
16257
16312
  env: resolved,
16258
16313
  cwd: findHooxSetupRoot(cwd),
16314
+ remembered,
16259
16315
  global: globalRepo
16260
16316
  }
16261
16317
  };
@@ -16266,6 +16322,7 @@ function resolveHooxRuntimeRoot(options) {
16266
16322
  checked: {
16267
16323
  env: resolved,
16268
16324
  cwd: findHooxSetupRoot(cwd),
16325
+ remembered,
16269
16326
  global: globalRepo
16270
16327
  }
16271
16328
  };
@@ -16275,20 +16332,27 @@ function resolveHooxRuntimeRoot(options) {
16275
16332
  return {
16276
16333
  root: local,
16277
16334
  source: "cwd",
16278
- checked: { cwd: local, global: globalRepo }
16335
+ checked: { cwd: local, remembered, global: globalRepo }
16336
+ };
16337
+ }
16338
+ if (remembered) {
16339
+ return {
16340
+ root: remembered,
16341
+ source: "remembered",
16342
+ checked: { cwd: null, remembered, global: globalRepo }
16279
16343
  };
16280
16344
  }
16281
16345
  if (isHooxSetupRoot(globalRepo)) {
16282
16346
  return {
16283
16347
  root: globalRepo,
16284
16348
  source: "global",
16285
- checked: { cwd: null, global: globalRepo }
16349
+ checked: { cwd: null, remembered: null, global: globalRepo }
16286
16350
  };
16287
16351
  }
16288
16352
  return {
16289
16353
  root: null,
16290
16354
  source: "none",
16291
- checked: { cwd: null, global: globalRepo }
16355
+ checked: { cwd: null, remembered: null, global: globalRepo }
16292
16356
  };
16293
16357
  }
16294
16358
  function getTuiEntryCandidates(runtimeRoot) {
@@ -16468,7 +16532,7 @@ function formatRelativeTime2(timestampMs, nowMs) {
16468
16532
  }
16469
16533
 
16470
16534
  // ../shared/src/dashboard-manifest.ts
16471
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
16535
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
16472
16536
  import { join as join3 } from "path";
16473
16537
  function isDashboardSectionFlatKv(sectionId) {
16474
16538
  return !DASHBOARD_SECTIONS_NOT_FLAT_KV.has(sectionId);
@@ -16702,7 +16766,7 @@ function tryReadFile(path2) {
16702
16766
  try {
16703
16767
  if (!existsSync3(path2))
16704
16768
  return null;
16705
- return readFileSync2(path2, "utf-8");
16769
+ return readFileSync3(path2, "utf-8");
16706
16770
  } catch {
16707
16771
  return null;
16708
16772
  }
@@ -21424,7 +21488,8 @@ var init_ui_store = __esm(() => {
21424
21488
  sidebarExpanded: true,
21425
21489
  modal: null,
21426
21490
  commandPaletteOpen: false,
21427
- previousView: null
21491
+ previousView: null,
21492
+ statusErrorExpanded: false
21428
21493
  };
21429
21494
  useUIStore = create()(immer2((set3) => ({
21430
21495
  ...initialState,
@@ -21455,6 +21520,12 @@ var init_ui_store = __esm(() => {
21455
21520
  state.activeView = state.previousView;
21456
21521
  state.previousView = null;
21457
21522
  }
21523
+ }),
21524
+ toggleStatusErrorExpanded: () => set3((state) => {
21525
+ state.statusErrorExpanded = !state.statusErrorExpanded;
21526
+ }),
21527
+ setStatusErrorExpanded: (expanded) => set3((state) => {
21528
+ state.statusErrorExpanded = expanded;
21458
21529
  })
21459
21530
  })));
21460
21531
  });
@@ -21471,13 +21542,28 @@ function ringPush(arr, item, max) {
21471
21542
  const next = [...arr, item];
21472
21543
  return next.length > max ? next.slice(next.length - max) : next;
21473
21544
  }
21545
+ function clearReconnectTimer() {
21546
+ if (reconnectTimer !== null) {
21547
+ clearTimeout(reconnectTimer);
21548
+ reconnectTimer = null;
21549
+ }
21550
+ }
21551
+ function scheduleReconnect(delayMs) {
21552
+ clearReconnectTimer();
21553
+ if (delayMs < 0)
21554
+ return;
21555
+ reconnectTimer = setTimeout(() => {
21556
+ reconnectTimer = null;
21557
+ scheduledReconnectTick();
21558
+ }, delayMs);
21559
+ }
21474
21560
  function getBackoffDelay(retryCount) {
21475
21561
  if (retryCount <= 0)
21476
21562
  return 0;
21477
21563
  const index = Math.min(retryCount - 1, BACKOFF_SEQUENCE.length - 1);
21478
21564
  return BACKOFF_SEQUENCE[index];
21479
21565
  }
21480
- var BACKOFF_SEQUENCE, MAX_RETRIES2 = 5, tradeStreamAbort = null, logStreamAbort = null, fetchWorkersGeneration = 0, MAX_TRADES = 500, MAX_ALERTS = 100, MAX_LOGS = 1000, initialState2, useServiceStore;
21566
+ var BACKOFF_SEQUENCE, MAX_RETRIES2 = 5, tradeStreamAbort = null, logStreamAbort = null, fetchWorkersGeneration = 0, reconnectTimer = null, scheduledReconnectTick = async () => {}, MAX_TRADES = 500, MAX_ALERTS = 100, MAX_LOGS = 1000, initialState2, useServiceStore;
21481
21567
  var init_service_store = __esm(() => {
21482
21568
  init_esm();
21483
21569
  init_immer2();
@@ -21507,6 +21593,7 @@ var init_service_store = __esm(() => {
21507
21593
  const data = await hooxFetch2("/v1/workers");
21508
21594
  if (gen !== fetchWorkersGeneration)
21509
21595
  return;
21596
+ clearReconnectTimer();
21510
21597
  set3((state) => {
21511
21598
  state.workers = data;
21512
21599
  state.lastUpdated = Date.now();
@@ -21532,6 +21619,7 @@ var init_service_store = __esm(() => {
21532
21619
  timestamp: Date.now(),
21533
21620
  acknowledged: false
21534
21621
  });
21622
+ let nextDelay = 0;
21535
21623
  set3((state) => {
21536
21624
  state.lastError = msg;
21537
21625
  state.disconnectedAt = state.disconnectedAt ?? Date.now();
@@ -21540,21 +21628,26 @@ var init_service_store = __esm(() => {
21540
21628
  state.retryCount = state.retryCount + 1;
21541
21629
  if (state.retryCount >= MAX_RETRIES2) {
21542
21630
  state.connectionStatus = "offline";
21543
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
21631
+ state.lastError = `${msg} (gave up after ${MAX_RETRIES2} retries)`;
21632
+ state.reconnectDelay = 0;
21544
21633
  } else {
21545
21634
  state.reconnectDelay = getBackoffDelay(state.retryCount);
21635
+ nextDelay = state.reconnectDelay;
21546
21636
  }
21547
21637
  } else if (state.connectionStatus === "reconnecting") {
21548
21638
  state.retryCount = state.retryCount + 1;
21549
21639
  if (state.retryCount >= MAX_RETRIES2) {
21550
21640
  state.connectionStatus = "offline";
21551
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
21641
+ state.lastError = `${msg} (gave up after ${MAX_RETRIES2} retries)`;
21552
21642
  state.reconnectDelay = 0;
21553
21643
  } else {
21554
21644
  state.reconnectDelay = getBackoffDelay(state.retryCount);
21645
+ nextDelay = state.reconnectDelay;
21555
21646
  }
21556
21647
  }
21557
21648
  });
21649
+ if (nextDelay > 0)
21650
+ scheduleReconnect(nextDelay);
21558
21651
  }
21559
21652
  },
21560
21653
  streamTrades: async () => {
@@ -21616,6 +21709,17 @@ var init_service_store = __esm(() => {
21616
21709
  const merged = [...state.alerts, ...alerts];
21617
21710
  state.alerts = merged.length > MAX_ALERTS ? merged.slice(merged.length - MAX_ALERTS) : merged;
21618
21711
  }),
21712
+ acknowledgeAlert: (id) => set3((state) => {
21713
+ const hit = state.alerts.find((a) => a.id === id);
21714
+ if (hit)
21715
+ hit.acknowledged = true;
21716
+ }),
21717
+ dismissAlert: (id) => set3((state) => {
21718
+ state.alerts = state.alerts.filter((a) => a.id !== id);
21719
+ }),
21720
+ clearLogs: () => set3((state) => {
21721
+ state.logs = [];
21722
+ }),
21619
21723
  setConnectionStatus: (status) => set3((state) => {
21620
21724
  if (status === "connected") {
21621
21725
  state.retryCount = 0;
@@ -21646,49 +21750,69 @@ var init_service_store = __esm(() => {
21646
21750
  state.metrics = metrics;
21647
21751
  state.lastUpdated = metrics.lastUpdated;
21648
21752
  }),
21649
- handleConnectionSuccess: () => set3((state) => {
21650
- state.lastSuccessfulFetch = Date.now();
21651
- state.retryCount = 0;
21652
- state.lastError = null;
21653
- state.lastErrorDetails = null;
21654
- state.reconnectDelay = 0;
21655
- state.disconnectedAt = null;
21656
- state.connectionStatus = "connected";
21657
- }),
21658
- handleConnectionFailure: (errorMessage) => set3((state) => {
21659
- state.lastError = errorMessage;
21660
- state.disconnectedAt = state.disconnectedAt ?? Date.now();
21661
- if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
21662
- state.connectionStatus = "reconnecting";
21663
- state.retryCount = 1;
21664
- state.reconnectDelay = getBackoffDelay(1);
21665
- } else if (state.connectionStatus === "reconnecting") {
21666
- state.retryCount = state.retryCount + 1;
21667
- if (state.retryCount >= MAX_RETRIES2) {
21668
- state.connectionStatus = "offline";
21669
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
21670
- state.reconnectDelay = 0;
21671
- } else {
21672
- state.reconnectDelay = getBackoffDelay(state.retryCount);
21753
+ handleConnectionSuccess: () => {
21754
+ clearReconnectTimer();
21755
+ set3((state) => {
21756
+ state.lastSuccessfulFetch = Date.now();
21757
+ state.retryCount = 0;
21758
+ state.lastError = null;
21759
+ state.lastErrorDetails = null;
21760
+ state.reconnectDelay = 0;
21761
+ state.disconnectedAt = null;
21762
+ state.connectionStatus = "connected";
21763
+ });
21764
+ },
21765
+ handleConnectionFailure: (errorMessage) => {
21766
+ let nextDelay = 0;
21767
+ set3((state) => {
21768
+ state.lastError = errorMessage;
21769
+ state.disconnectedAt = state.disconnectedAt ?? Date.now();
21770
+ if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
21771
+ state.connectionStatus = "reconnecting";
21772
+ state.retryCount = 1;
21773
+ state.reconnectDelay = getBackoffDelay(1);
21774
+ nextDelay = state.reconnectDelay;
21775
+ } else if (state.connectionStatus === "reconnecting") {
21776
+ state.retryCount = state.retryCount + 1;
21777
+ if (state.retryCount >= MAX_RETRIES2) {
21778
+ state.connectionStatus = "offline";
21779
+ state.lastError = `${errorMessage} (gave up after ${MAX_RETRIES2} retries)`;
21780
+ state.reconnectDelay = 0;
21781
+ } else {
21782
+ state.reconnectDelay = getBackoffDelay(state.retryCount);
21783
+ nextDelay = state.reconnectDelay;
21784
+ }
21673
21785
  }
21674
- }
21675
- }),
21676
- resetRetries: () => set3((state) => {
21677
- state.retryCount = 0;
21678
- state.lastError = null;
21679
- state.lastErrorDetails = null;
21680
- state.reconnectDelay = 0;
21681
- state.disconnectedAt = null;
21682
- }),
21683
- forceRetry: () => set3((state) => {
21684
- if (state.connectionStatus === "offline") {
21685
- state.connectionStatus = "polling";
21786
+ });
21787
+ if (nextDelay > 0)
21788
+ scheduleReconnect(nextDelay);
21789
+ },
21790
+ resetRetries: () => {
21791
+ clearReconnectTimer();
21792
+ set3((state) => {
21686
21793
  state.retryCount = 0;
21687
21794
  state.lastError = null;
21688
21795
  state.lastErrorDetails = null;
21689
21796
  state.reconnectDelay = 0;
21797
+ state.disconnectedAt = null;
21798
+ });
21799
+ },
21800
+ forceRetry: () => {
21801
+ clearReconnectTimer();
21802
+ const wasOffline = get2().connectionStatus === "offline";
21803
+ set3((state) => {
21804
+ if (state.connectionStatus === "offline") {
21805
+ state.connectionStatus = "polling";
21806
+ state.retryCount = 0;
21807
+ state.lastError = null;
21808
+ state.lastErrorDetails = null;
21809
+ state.reconnectDelay = 0;
21810
+ }
21811
+ });
21812
+ if (wasOffline) {
21813
+ scheduleReconnect(0);
21690
21814
  }
21691
- }),
21815
+ },
21692
21816
  setLastErrorDetails: (details) => set3((state) => {
21693
21817
  state.lastErrorDetails = details;
21694
21818
  state.lastError = details === null ? null : summarizeCliError(details);
@@ -21711,6 +21835,12 @@ var init_service_store = __esm(() => {
21711
21835
  }, MAX_ALERTS);
21712
21836
  })
21713
21837
  })));
21838
+ scheduledReconnectTick = async () => {
21839
+ const status = useServiceStore.getState().connectionStatus;
21840
+ if (status === "reconnecting" || status === "polling") {
21841
+ await useServiceStore.getState().fetchWorkers();
21842
+ }
21843
+ };
21714
21844
  });
21715
21845
 
21716
21846
  // ../../node_modules/.bun/zustand@5.0.14+158594928fb811b4/node_modules/zustand/esm/middleware.mjs
@@ -21897,39 +22027,69 @@ var init_middleware = __esm(() => {
21897
22027
  });
21898
22028
 
21899
22029
  // ../shared/src/stores/config-store.ts
21900
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
21901
- import { homedir as homedir3 } from "os";
22030
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync3, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
21902
22031
  import { join as join4 } from "path";
22032
+ function getTuiPreferencesPath() {
22033
+ return join4(getHooxHome(), ".tui-state", "preferences.json");
22034
+ }
22035
+ function getLegacyConfigJsonPath() {
22036
+ return join4(getHooxHome(), "config.json");
22037
+ }
22038
+ function extractZustandPrefsPayload(raw) {
22039
+ try {
22040
+ const parsed = JSON.parse(raw);
22041
+ if (parsed && typeof parsed === "object" && parsed.state && typeof parsed.state === "object" && !Array.isArray(parsed.state) && (("theme" in parsed.state) || ("refreshIntervalMs" in parsed.state)) && !("apiToken" in parsed)) {
22042
+ return raw;
22043
+ }
22044
+ } catch {}
22045
+ return null;
22046
+ }
21903
22047
  var bunConfigStorage, DEFAULT_SHORTCUTS, DEFAULT_LOG_FILTER, DEFAULT_NOTIFICATIONS, defaults, useConfigStore;
21904
22048
  var init_config_store = __esm(() => {
21905
22049
  init_esm();
21906
22050
  init_immer2();
21907
22051
  init_middleware();
21908
22052
  init_config();
22053
+ init_path_utils();
21909
22054
  bunConfigStorage = {
21910
22055
  getItem: async (_name) => {
21911
22056
  try {
21912
- const file2 = Bun.file(join4(homedir3(), ".hoox", "config.json"));
21913
- const exists = await file2.exists();
21914
- if (!exists)
21915
- return null;
21916
- return await file2.text();
22057
+ const prefsPath = getTuiPreferencesPath();
22058
+ const prefsFile = Bun.file(prefsPath);
22059
+ if (await prefsFile.exists()) {
22060
+ return await prefsFile.text();
22061
+ }
22062
+ const legacyPath = getLegacyConfigJsonPath();
22063
+ if (existsSync4(legacyPath)) {
22064
+ const legacyRaw = readFileSync4(legacyPath, "utf-8");
22065
+ const envelope = extractZustandPrefsPayload(legacyRaw);
22066
+ if (envelope) {
22067
+ await bunConfigStorage.setItem(_name, envelope);
22068
+ return envelope;
22069
+ }
22070
+ }
22071
+ return null;
21917
22072
  } catch {
21918
22073
  return null;
21919
22074
  }
21920
22075
  },
21921
22076
  setItem: async (_name, value) => {
21922
- const dir = join4(homedir3(), ".hoox");
21923
- if (!existsSync4(dir)) {
21924
- mkdirSync2(dir, { recursive: true, mode: HOOX_DIR_MODE });
22077
+ const home = getHooxHome();
22078
+ const stateDir = join4(home, ".tui-state");
22079
+ if (!existsSync4(home)) {
22080
+ mkdirSync3(home, { recursive: true, mode: HOOX_DIR_MODE });
22081
+ }
22082
+ if (!existsSync4(stateDir)) {
22083
+ mkdirSync3(stateDir, { recursive: true, mode: HOOX_DIR_MODE });
21925
22084
  }
21926
22085
  try {
21927
- chmodSync2(dir, HOOX_DIR_MODE);
22086
+ chmodSync3(home, HOOX_DIR_MODE);
22087
+ chmodSync3(stateDir, HOOX_DIR_MODE);
21928
22088
  } catch {}
21929
- const filePath = join4(dir, "config.json");
22089
+ const filePath = getTuiPreferencesPath();
21930
22090
  await Bun.write(filePath, value);
21931
22091
  try {
21932
- chmodSync2(filePath, HOOX_CONFIG_FILE_MODE);
22092
+ chmodSync3(filePath, HOOX_CONFIG_FILE_MODE);
21933
22093
  } catch {}
21934
22094
  },
21935
22095
  removeItem: async (_name) => {}
@@ -22001,7 +22161,7 @@ var init_config_store = __esm(() => {
22001
22161
  state.notifications[channel] = !state.notifications[channel];
22002
22162
  })
22003
22163
  })), {
22004
- name: "hoox-config",
22164
+ name: "hoox-tui-prefs",
22005
22165
  storage: createJSONStorage(() => bunConfigStorage),
22006
22166
  partialize: (state) => ({
22007
22167
  theme: state.theme,
@@ -22049,6 +22209,7 @@ __export(exports_src, {
22049
22209
  resolveHooxRuntimeRoot: () => resolveHooxRuntimeRoot,
22050
22210
  resolveHooxPath: () => resolveHooxPath,
22051
22211
  resolveDependencies: () => resolveDependencies,
22212
+ rememberMonorepoRoot: () => rememberMonorepoRoot,
22052
22213
  readConfigSync: () => readConfigSync,
22053
22214
  readConfig: () => readConfig,
22054
22215
  parseDashboardManifest: () => parseDashboardManifest,
@@ -22073,6 +22234,8 @@ __export(exports_src, {
22073
22234
  headersToObject: () => headersToObject,
22074
22235
  hasOperatorClientCredentials: () => hasOperatorClientCredentials,
22075
22236
  getTuiEntryCandidates: () => getTuiEntryCandidates,
22237
+ getRememberedMonorepoRoot: () => getRememberedMonorepoRoot,
22238
+ getRememberedMonorepoPath: () => getRememberedMonorepoPath,
22076
22239
  getRelativeHooxPath: () => getRelativeHooxPath,
22077
22240
  getHooxWranglerPath: () => getHooxWranglerPath,
22078
22241
  getHooxStatePath: () => getHooxStatePath,
@@ -25557,15 +25720,15 @@ __export(exports_fs_secure, {
25557
25720
  SECRET_FILE_MODE: () => SECRET_FILE_MODE,
25558
25721
  SECRET_DIR_MODE: () => SECRET_DIR_MODE
25559
25722
  });
25560
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync3 } from "fs";
25723
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync4 } from "fs";
25561
25724
  function secureChmod(path4, mode) {
25562
25725
  try {
25563
- chmodSync3(path4, mode);
25726
+ chmodSync4(path4, mode);
25564
25727
  } catch {}
25565
25728
  }
25566
25729
  function ensureSecretDir(dir) {
25567
25730
  try {
25568
- mkdirSync3(dir, { recursive: true, mode: SECRET_DIR_MODE });
25731
+ mkdirSync4(dir, { recursive: true, mode: SECRET_DIR_MODE });
25569
25732
  } catch {}
25570
25733
  secureChmod(dir, SECRET_DIR_MODE);
25571
25734
  }
@@ -26171,7 +26334,7 @@ var exports_schema_service = {};
26171
26334
  __export(exports_schema_service, {
26172
26335
  SchemaService: () => SchemaService
26173
26336
  });
26174
- import { readFileSync as readFileSync5, existsSync as existsSync11 } from "fs";
26337
+ import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
26175
26338
  import { resolve as resolve4 } from "path";
26176
26339
 
26177
26340
  class SchemaService {
@@ -26207,7 +26370,7 @@ class SchemaService {
26207
26370
  const errors4 = [];
26208
26371
  const isHooxProject = existsSync11(resolve4(this.projectRoot, "wrangler.jsonc")) || existsSync11(resolve4(this.projectRoot, "workers"));
26209
26372
  try {
26210
- const wranglerContent = readFileSync5(workerWranglerPath, "utf-8");
26373
+ const wranglerContent = readFileSync7(workerWranglerPath, "utf-8");
26211
26374
  errors4.push(...validateWranglerJsonc(name, manifest, wranglerContent));
26212
26375
  } catch (e) {
26213
26376
  const isErrnoException = e instanceof Error && "code" in e;
@@ -26221,7 +26384,7 @@ class SchemaService {
26221
26384
  });
26222
26385
  }
26223
26386
  try {
26224
- const rootContent = readFileSync5(rootWranglerPath, "utf-8");
26387
+ const rootContent = readFileSync7(rootWranglerPath, "utf-8");
26225
26388
  errors4.push(...validateRootSecrets(name, manifest, rootContent));
26226
26389
  } catch (e) {
26227
26390
  const errMessage = e instanceof Error ? e.message : String(e);
@@ -26232,7 +26395,7 @@ class SchemaService {
26232
26395
  });
26233
26396
  }
26234
26397
  try {
26235
- const devVarsContent = readFileSync5(devVarsPath, "utf-8");
26398
+ const devVarsContent = readFileSync7(devVarsPath, "utf-8");
26236
26399
  errors4.push(...validateDevVars(name, manifest, devVarsContent));
26237
26400
  } catch (e) {
26238
26401
  const errMessage = e instanceof Error ? e.message : String(e);
@@ -26491,7 +26654,7 @@ var init_trace_service = __esm(() => {
26491
26654
  });
26492
26655
 
26493
26656
  // src/index.ts
26494
- import { readFileSync as readFileSync10 } from "fs";
26657
+ import { readFileSync as readFileSync12 } from "fs";
26495
26658
 
26496
26659
  // ../../node_modules/.bun/commander@15.0.0/node_modules/commander/lib/error.js
26497
26660
  class CommanderError extends Error {
@@ -29621,15 +29784,15 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29621
29784
  }
29622
29785
  // src/services/setup/setup-service.ts
29623
29786
  import {
29624
- chmodSync as chmodSync4,
29787
+ chmodSync as chmodSync5,
29625
29788
  existsSync as existsSync7,
29626
- mkdirSync as mkdirSync4,
29789
+ mkdirSync as mkdirSync5,
29627
29790
  readdirSync as readdirSync3,
29628
- readFileSync as readFileSync3,
29791
+ readFileSync as readFileSync5,
29629
29792
  statSync as statSync2,
29630
- writeFileSync as writeFileSync2
29793
+ writeFileSync as writeFileSync3
29631
29794
  } from "fs";
29632
- import { homedir as homedir4 } from "os";
29795
+ import { homedir as homedir3 } from "os";
29633
29796
  import { join as join7 } from "path";
29634
29797
  var ALL_WORKERS = [
29635
29798
  "d1-worker",
@@ -29699,7 +29862,7 @@ function parseEnvFile(text2) {
29699
29862
  }
29700
29863
  function safeChmod(path4, mode) {
29701
29864
  try {
29702
- chmodSync4(path4, mode);
29865
+ chmodSync5(path4, mode);
29703
29866
  } catch {}
29704
29867
  }
29705
29868
  function loadExistingKeysFromDisk() {
@@ -29708,7 +29871,7 @@ function loadExistingKeysFromDisk() {
29708
29871
  return null;
29709
29872
  let text2;
29710
29873
  try {
29711
- text2 = readFileSync3(setupEnv, "utf-8");
29874
+ text2 = readFileSync5(setupEnv, "utf-8");
29712
29875
  } catch {
29713
29876
  return null;
29714
29877
  }
@@ -29773,9 +29936,9 @@ class SetupService {
29773
29936
  API_SERVICE_KEY_BINDING: internalKey
29774
29937
  };
29775
29938
  if (!existsSync7(KEYS_DIR)) {
29776
- mkdirSync4(KEYS_DIR, { recursive: true });
29939
+ mkdirSync5(KEYS_DIR, { recursive: true });
29777
29940
  } else if (!statSync2(KEYS_DIR).isDirectory()) {
29778
- mkdirSync4(KEYS_DIR, { recursive: true });
29941
+ mkdirSync5(KEYS_DIR, { recursive: true });
29779
29942
  }
29780
29943
  safeChmod(KEYS_DIR, 448);
29781
29944
  const setupEnvPath = join7(KEYS_DIR, "setup.env");
@@ -29798,7 +29961,7 @@ class SetupService {
29798
29961
  const path4 = `workers/${workerFsDir(worker)}/.dev.vars`;
29799
29962
  const meshNames = secretNames;
29800
29963
  const meshSet = new Set(meshNames);
29801
- const existing = existsSync7(path4) ? parseEnvFile(readFileSync3(path4, "utf-8")) : new Map;
29964
+ const existing = existsSync7(path4) ? parseEnvFile(readFileSync5(path4, "utf-8")) : new Map;
29802
29965
  for (const secretName of meshNames) {
29803
29966
  existing.set(secretName, keys[secretName]);
29804
29967
  }
@@ -29958,7 +30121,7 @@ class SetupService {
29958
30121
  const wranglerPath = join7("workers", workerFsDir(worker), "wrangler.jsonc");
29959
30122
  let wranglerContent;
29960
30123
  try {
29961
- wranglerContent = readFileSync3(wranglerPath, "utf-8");
30124
+ wranglerContent = readFileSync5(wranglerPath, "utf-8");
29962
30125
  } catch {
29963
30126
  return { ok: false, error: errMsg };
29964
30127
  }
@@ -29976,7 +30139,7 @@ class SetupService {
29976
30139
  let fixed = wranglerContent.replace(new RegExp(`(\\s*//[^\\n]*\\n)?\\s*"${escapeRegex2(secretName)}"\\s*:\\s*null\\s*,?\\s*\\n?`, "g"), `
29977
30140
  `);
29978
30141
  fixed = fixed.replace(/"vars"\s*:\s*\{\s*\}/, '"vars": {}');
29979
- writeFileSync2(wranglerPath, fixed, "utf-8");
30142
+ writeFileSync3(wranglerPath, fixed, "utf-8");
29980
30143
  this.onProgress({
29981
30144
  type: "info",
29982
30145
  step: "secrets",
@@ -30095,7 +30258,7 @@ class SetupService {
30095
30258
  }
30096
30259
  const fallbacks = [
30097
30260
  "/usr/local/bin/wrangler",
30098
- join7(homedir4(), ".bun", "bin", "wrangler")
30261
+ join7(homedir3(), ".bun", "bin", "wrangler")
30099
30262
  ];
30100
30263
  for (const fb of fallbacks) {
30101
30264
  try {
@@ -30159,7 +30322,7 @@ class SetupService {
30159
30322
  for (const name of workerDirs) {
30160
30323
  const dir = `workers/${workerFsDir(name)}`;
30161
30324
  try {
30162
- const gitEntry = readFileSync3(join7(dir, ".git"), "utf-8").trim();
30325
+ const gitEntry = readFileSync5(join7(dir, ".git"), "utf-8").trim();
30163
30326
  if (gitEntry.startsWith("gitdir:") || gitEntry.length > 0) {
30164
30327
  if (!this.isWorkerTreePopulated(dir, name))
30165
30328
  missing.push(name);
@@ -30308,7 +30471,7 @@ class SetupService {
30308
30471
  const wranglerPath = `workers/${workerFsDir(worker)}/wrangler.jsonc`;
30309
30472
  let content;
30310
30473
  try {
30311
- content = readFileSync3(wranglerPath, "utf-8");
30474
+ content = readFileSync5(wranglerPath, "utf-8");
30312
30475
  } catch {
30313
30476
  continue;
30314
30477
  }
@@ -30340,7 +30503,7 @@ class SetupService {
30340
30503
  }
30341
30504
  if (modified) {
30342
30505
  content = content.replace(/"vars"\s*:\s*\{\s*\}/, '"vars": {}');
30343
- writeFileSync2(wranglerPath, content, "utf-8");
30506
+ writeFileSync3(wranglerPath, content, "utf-8");
30344
30507
  fixed.push(worker);
30345
30508
  }
30346
30509
  }
@@ -30495,7 +30658,7 @@ class SetupService {
30495
30658
  const devVarsPath = `workers/${workerFsDir(worker)}/.dev.vars`;
30496
30659
  let devVarsContent;
30497
30660
  try {
30498
- devVarsContent = readFileSync3(devVarsPath, "utf-8");
30661
+ devVarsContent = readFileSync5(devVarsPath, "utf-8");
30499
30662
  } catch {
30500
30663
  missing.push(`${worker}:.dev.vars (missing file)`);
30501
30664
  continue;
@@ -31300,8 +31463,8 @@ init_errors4();
31300
31463
  import {
31301
31464
  statSync as statSync3,
31302
31465
  existsSync as existsSync8,
31303
- readFileSync as readFileSync4,
31304
- writeFileSync as writeFileSync3,
31466
+ readFileSync as readFileSync6,
31467
+ writeFileSync as writeFileSync4,
31305
31468
  renameSync
31306
31469
  } from "fs";
31307
31470
  import { resolve as resolve2 } from "path";
@@ -32082,7 +32245,7 @@ async function doUpdateInternalUrls(fmt) {
32082
32245
  process.exitCode = 1 /* ERROR */;
32083
32246
  return;
32084
32247
  }
32085
- const content = readFileSync4(filePath, "utf-8");
32248
+ const content = readFileSync6(filePath, "utf-8");
32086
32249
  const errors4 = [];
32087
32250
  const parsed = parse6(content, errors4);
32088
32251
  if (errors4.length > 0) {
@@ -32108,12 +32271,12 @@ async function doUpdateInternalUrls(fmt) {
32108
32271
  formatSuccess("All service URLs already up to date.", fmt);
32109
32272
  return;
32110
32273
  }
32111
- const fresh = readFileSync4(filePath, "utf-8");
32274
+ const fresh = readFileSync6(filePath, "utf-8");
32112
32275
  const edits = modify(fresh, ["vars"], vars, {
32113
32276
  formattingOptions: { tabSize: 2, insertSpaces: true }
32114
32277
  });
32115
32278
  const tmpPath = `${filePath}.tmp-${process.pid}`;
32116
- writeFileSync3(tmpPath, applyEdits(fresh, edits), "utf-8");
32279
+ writeFileSync4(tmpPath, applyEdits(fresh, edits), "utf-8");
32117
32280
  renameSync(tmpPath, filePath);
32118
32281
  formatSuccess(`Updated ${changesCount} service URL(s) in dashboard wrangler.jsonc`, fmt);
32119
32282
  } catch (err) {
@@ -37752,13 +37915,13 @@ import { createRequire } from "module";
37752
37915
  import { resolve as resolve6, dirname as dirname3, join as join10 } from "path";
37753
37916
  import { fileURLToPath } from "url";
37754
37917
  import { existsSync as existsSync12, realpathSync } from "fs";
37755
- import { homedir as homedir5 } from "os";
37918
+ import { homedir as homedir4 } from "os";
37756
37919
 
37757
37920
  // src/services/perf/endpoint-resolver.ts
37758
- import { readFileSync as readFileSync6 } from "fs";
37921
+ import { readFileSync as readFileSync8 } from "fs";
37759
37922
  function readSubdomainPrefix() {
37760
37923
  try {
37761
- const raw = readFileSync6("wrangler.jsonc", "utf8");
37924
+ const raw = readFileSync8("wrangler.jsonc", "utf8");
37762
37925
  const stripped = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
37763
37926
  const parsed = JSON.parse(stripped);
37764
37927
  const prefix = parsed.global?.subdomain_prefix;
@@ -37805,7 +37968,7 @@ function collectTuiCandidates() {
37805
37968
  const cwdRequire = createRequire(join10(process.cwd(), "package.json"));
37806
37969
  pushPackageEntries(candidates, cwdRequire);
37807
37970
  } catch {}
37808
- const bunGlobal = join10(homedir5(), ".bun", "install", "global", "node_modules", TUI_PKG);
37971
+ const bunGlobal = join10(homedir4(), ".bun", "install", "global", "node_modules", TUI_PKG);
37809
37972
  pushPackageRootEntries(candidates, bunGlobal);
37810
37973
  const npmGlobal = process.env.npm_config_prefix?.trim();
37811
37974
  if (npmGlobal) {
@@ -38058,7 +38221,7 @@ TUI exited with code ${code}
38058
38221
  init_dist4();
38059
38222
  init_src();
38060
38223
  import { join as join12 } from "path";
38061
- import { homedir as homedir6 } from "os";
38224
+ import { homedir as homedir5 } from "os";
38062
38225
 
38063
38226
  // src/services/runtime/runtime-service.ts
38064
38227
  init_src();
@@ -38163,6 +38326,62 @@ ${install.stderr || install.stdout}`.trim());
38163
38326
  const tuiEntry = firstExisting(getTuiEntryCandidates(repoPath));
38164
38327
  return { repoPath, cloned, installed, tuiEntry };
38165
38328
  }
38329
+ // src/commands/doctor/doctor-command.ts
38330
+ init_src();
38331
+
38332
+ // src/services/workspace/workspace-context.ts
38333
+ init_src();
38334
+ import { resolve as resolve7 } from "path";
38335
+ var lastWorkspaceContext = null;
38336
+ function getLastWorkspaceContext() {
38337
+ return lastWorkspaceContext;
38338
+ }
38339
+ function ensureWorkspaceContext(options) {
38340
+ const previousCwd = resolve7(options?.cwd ?? process.cwd());
38341
+ const inTest = process.env.BUN_TEST === "1" || false || typeof process.env.BUN_TEST !== "undefined";
38342
+ const allowChdir = options?.force === true || !inTest;
38343
+ const runtime = resolveHooxRuntimeRoot({ cwd: previousCwd });
38344
+ const quiet = options?.quiet === true || process.env.HOOX_CLI_SILENT === "1" || process.argv.includes("--json") || process.argv.includes("--quiet");
38345
+ if (!runtime.root) {
38346
+ const empty = {
38347
+ root: null,
38348
+ source: runtime.source,
38349
+ chdir: false,
38350
+ previousCwd,
38351
+ runtime
38352
+ };
38353
+ lastWorkspaceContext = empty;
38354
+ return empty;
38355
+ }
38356
+ if (runtime.source === "cwd" || runtime.source === "env") {
38357
+ rememberMonorepoRoot(runtime.root);
38358
+ } else if (runtime.source === "remembered") {
38359
+ rememberMonorepoRoot(runtime.root);
38360
+ }
38361
+ if (!process.env.HOOX_REPO) {
38362
+ process.env.HOOX_REPO = runtime.root;
38363
+ }
38364
+ let didChdir = false;
38365
+ const target = resolve7(runtime.root);
38366
+ if (allowChdir && resolve7(process.cwd()) !== target) {
38367
+ process.chdir(target);
38368
+ didChdir = true;
38369
+ if (!quiet) {
38370
+ const label = runtime.source === "remembered" ? "remembered monorepo" : runtime.source === "global" ? "global runtime" : runtime.source === "env" ? "HOOX_REPO" : "monorepo";
38371
+ process.stderr.write(`hoox: using ${label} at ${target}
38372
+ `);
38373
+ }
38374
+ }
38375
+ const ctx = {
38376
+ root: target,
38377
+ source: runtime.source,
38378
+ chdir: didChdir,
38379
+ previousCwd,
38380
+ runtime
38381
+ };
38382
+ lastWorkspaceContext = ctx;
38383
+ return ctx;
38384
+ }
38166
38385
  // src/services/operator-security/operator-security-service.ts
38167
38386
  init_src();
38168
38387
  function collectSecurityHygiene(env = process.env, options) {
@@ -38413,14 +38632,19 @@ function printCheckIcon(severity) {
38413
38632
  function printStatus() {
38414
38633
  const status = getRuntimeStatus();
38415
38634
  const { runtime } = status;
38635
+ const remembered = getRememberedMonorepoRoot();
38636
+ const session = getLastWorkspaceContext();
38637
+ const sourceLabel = session?.source ?? runtime.source;
38638
+ const rootLabel = session?.root ?? runtime.root;
38416
38639
  process.stdout.write(theme.heading(`
38417
38640
  Hoox doctor
38418
38641
 
38419
38642
  `));
38420
38643
  process.stdout.write(`${theme.dim("HOOX_HOME")} ${status.hooxHome}
38421
38644
  ` + `${theme.dim("Global repo")} ${status.repoPath}
38422
- ` + `${theme.dim("Runtime root")} ${runtime.root ?? theme.warning("(none)")}
38423
- ` + `${theme.dim("Source")} ${runtime.source}
38645
+ ` + `${theme.dim("Remembered")} ${remembered ?? theme.dim("(none \u2014 run hx once from the monorepo)")}
38646
+ ` + `${theme.dim("Runtime root")} ${rootLabel ?? theme.warning("(none)")}
38647
+ ` + `${theme.dim("Source")} ${sourceLabel}
38424
38648
  ` + `${theme.dim("TUI entry")} ${status.tuiEntry ?? theme.warning("(not found)")}
38425
38649
 
38426
38650
  `);
@@ -38439,10 +38663,10 @@ Hoox doctor
38439
38663
  detail: status.isSetupRoot ? "monorepo markers present" : status.repoPresent ? "path exists but is not a setup monorepo" : runtime.root ? "optional (runtime resolved elsewhere)" : "missing \u2014 run: hoox doctor --fix-runtime"
38440
38664
  },
38441
38665
  {
38442
- ok: runtime.root !== null,
38666
+ ok: rootLabel !== null,
38443
38667
  required: true,
38444
38668
  label: "Resolved runtime root",
38445
- detail: runtime.root === null ? "set HOOX_REPO, cd into the hoox monorepo, or fix-runtime" : `${runtime.source}: ${runtime.root}`
38669
+ detail: rootLabel === null ? "set HOOX_REPO, run hx once inside the monorepo (remembers path), or: hoox doctor --fix-runtime" : `${sourceLabel}: ${rootLabel}`
38446
38670
  },
38447
38671
  {
38448
38672
  ok: status.tuiEntry !== null,
@@ -38478,7 +38702,7 @@ Hoox doctor
38478
38702
  return failed === 0 ? 0 /* SUCCESS */ : 1 /* ERROR */;
38479
38703
  }
38480
38704
  function printSecurityHygieneSection(env = process.env) {
38481
- const configPath = join12(homedir6(), ".hoox", "config.json");
38705
+ const configPath = join12(homedir5(), ".hoox", "config.json");
38482
38706
  const lines = collectSecurityHygiene(env, { configPath });
38483
38707
  process.stdout.write(theme.heading(`Security hygiene
38484
38708
  `));
@@ -38496,7 +38720,7 @@ async function runSecurityDoctor(options) {
38496
38720
  env.HOOX_API_URL = options.apiUrl;
38497
38721
  }
38498
38722
  const profile = resolveOperatorTransportProfile(env);
38499
- const configPath = join12(homedir6(), ".hoox", "config.json");
38723
+ const configPath = join12(homedir5(), ".hoox", "config.json");
38500
38724
  const hygiene = collectSecurityHygiene(env, { configPath });
38501
38725
  const isLocal = profile.apiBase.includes("localhost") || profile.apiBase.includes("127.0.0.1");
38502
38726
  const shouldProbe = options.probe === true || options.probe !== false && !isLocal;
@@ -38741,7 +38965,7 @@ function registerDisclaimerCommand(program2) {
38741
38965
  }));
38742
38966
  }
38743
38967
  // src/commands/agent/agent-command.ts
38744
- import { readFileSync as readFileSync7 } from "fs";
38968
+ import { readFileSync as readFileSync9 } from "fs";
38745
38969
  init_errors4();
38746
38970
  var PROVIDERS = [
38747
38971
  {
@@ -38775,7 +38999,7 @@ function checkDevVars() {
38775
38999
  const candidates = ["workers/agent-worker/.dev.vars", ".dev.vars"];
38776
39000
  for (const filePath of candidates) {
38777
39001
  try {
38778
- const content = readFileSync7(filePath, "utf-8");
39002
+ const content = readFileSync9(filePath, "utf-8");
38779
39003
  for (const line of content.split(`
38780
39004
  `)) {
38781
39005
  const trimmed = line.trim();
@@ -39306,12 +39530,12 @@ EXAMPLES:
39306
39530
  }
39307
39531
  // src/commands/pyne/pyne-command.ts
39308
39532
  init_config2();
39309
- import { existsSync as existsSync14, readFileSync as readFileSync8 } from "fs";
39310
- import { resolve as resolve7 } from "path";
39533
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
39534
+ import { resolve as resolve8 } from "path";
39311
39535
  init_errors4();
39312
39536
  var DEFAULT_TIMEOUT_MS = 30000;
39313
39537
  function getPyneWorkerDir() {
39314
- return resolve7(process.cwd(), "workers/pyne-worker");
39538
+ return resolve8(process.cwd(), "workers/pyne-worker");
39315
39539
  }
39316
39540
  function requirePyneDir() {
39317
39541
  const dir = getPyneWorkerDir();
@@ -39325,14 +39549,14 @@ function requirePyneDir() {
39325
39549
  function loadDevVars(workerDir) {
39326
39550
  const out = {};
39327
39551
  const candidates = [
39328
- resolve7(workerDir, ".dev.vars"),
39329
- resolve7(process.cwd(), ".dev.vars")
39552
+ resolve8(workerDir, ".dev.vars"),
39553
+ resolve8(process.cwd(), ".dev.vars")
39330
39554
  ];
39331
39555
  for (const filePath of candidates) {
39332
39556
  try {
39333
39557
  if (!existsSync14(filePath))
39334
39558
  continue;
39335
- const content = readFileSync8(filePath, "utf-8");
39559
+ const content = readFileSync10(filePath, "utf-8");
39336
39560
  for (const line of content.split(`
39337
39561
  `)) {
39338
39562
  const trimmed = line.trim();
@@ -39547,11 +39771,11 @@ EXAMPLES:
39547
39771
  } else if (scriptPath === "-") {
39548
39772
  body.script = await new Response(Bun.stdin).text();
39549
39773
  } else {
39550
- const abs = resolve7(process.cwd(), scriptPath);
39774
+ const abs = resolve8(process.cwd(), scriptPath);
39551
39775
  if (!existsSync14(abs)) {
39552
39776
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39553
39777
  }
39554
- body.script = readFileSync8(abs, "utf-8");
39778
+ body.script = readFileSync10(abs, "utf-8");
39555
39779
  }
39556
39780
  const res = await pyneFetch(baseUrl, "/run", {
39557
39781
  method: "POST",
@@ -39598,11 +39822,11 @@ EXAMPLES:
39598
39822
  if (!apiKey) {
39599
39823
  throw new CLIError("API key required for authenticated endpoints", 2 /* INVALID_USAGE */);
39600
39824
  }
39601
- const abs = resolve7(process.cwd(), scriptPath);
39825
+ const abs = resolve8(process.cwd(), scriptPath);
39602
39826
  if (!existsSync14(abs)) {
39603
39827
  throw new CLIError(`Script not found: ${abs}`, 1 /* ERROR */);
39604
39828
  }
39605
- const script = readFileSync8(abs, "utf-8");
39829
+ const script = readFileSync10(abs, "utf-8");
39606
39830
  const baseName = abs.split(/[/\\]/).pop() ?? "script.pine";
39607
39831
  const scriptId = opts.id ?? baseName.replace(/\.pine$/i, "").replace(/[^a-zA-Z0-9_-]/g, "_");
39608
39832
  const res = await pyneFetch(baseUrl, "/scripts", {
@@ -39706,7 +39930,7 @@ Pass script flags after -- :
39706
39930
  pyneCmd.command("sync-vendor").summary("Sync pynescript into python_modules/ for Cloudflare deploy").description(`Runs workers/pyne-worker/scripts/sync_vendor.sh.
39707
39931
  Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorHandling(async () => {
39708
39932
  const dir = requirePyneDir();
39709
- const script = resolve7(dir, "scripts/sync_vendor.sh");
39933
+ const script = resolve8(dir, "scripts/sync_vendor.sh");
39710
39934
  if (!existsSync14(script)) {
39711
39935
  throw new CLIError(`sync_vendor.sh not found at ${script}`, 1 /* ERROR */);
39712
39936
  }
@@ -39715,7 +39939,7 @@ Required before wrangler deploy after pulling new PYNE APIs.`).action(withErrorH
39715
39939
  pyneCmd.command("deploy").summary("sync-vendor then wrangler deploy pyne-worker").option("--skip-sync", "Skip scripts/sync_vendor.sh").action(withErrorHandling(async (opts) => {
39716
39940
  const dir = requirePyneDir();
39717
39941
  if (!opts.skipSync) {
39718
- const script = resolve7(dir, "scripts/sync_vendor.sh");
39942
+ const script = resolve8(dir, "scripts/sync_vendor.sh");
39719
39943
  if (existsSync14(script)) {
39720
39944
  const code = await spawnInPyneDir(["bash", script], dir);
39721
39945
  if (code !== 0) {
@@ -40277,7 +40501,7 @@ ${theme.dim("Live tail duration reached. Exiting.")}
40277
40501
  `);
40278
40502
  }
40279
40503
  } catch {}
40280
- await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
40504
+ await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
40281
40505
  await poll();
40282
40506
  };
40283
40507
  await poll();
@@ -41265,7 +41489,7 @@ function registerCompletionCommand(program2) {
41265
41489
  // src/ui/banner.ts
41266
41490
  init_ansis();
41267
41491
  init_theme();
41268
- import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
41492
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
41269
41493
  import { fileURLToPath as fileURLToPath2 } from "url";
41270
41494
  import { dirname as dirname4, join as join13 } from "path";
41271
41495
  var ORANGE = ansis_default.hex("#ff7f2a");
@@ -41283,7 +41507,7 @@ function findCliVersion() {
41283
41507
  const pkgPath = join13(dir, "package.json");
41284
41508
  if (existsSync15(pkgPath)) {
41285
41509
  try {
41286
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
41510
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
41287
41511
  if (pkg.name === PKG_NAME)
41288
41512
  return pkg.version;
41289
41513
  } catch {}
@@ -41918,7 +42142,7 @@ if (typeof Bun === "undefined") {
41918
42142
  `);
41919
42143
  process.exit(1);
41920
42144
  }
41921
- var pkgVersion = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf-8")).version;
42145
+ var pkgVersion = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf-8")).version;
41922
42146
  var program2 = new Command;
41923
42147
  program2.name("hoox").description("Hoox CLI \u2014 manage Cloudflare Workers, infrastructure, secrets, and deployments").version(`${pkgVersion}
41924
42148
 
@@ -42069,6 +42293,7 @@ registerPerfCommand(program2);
42069
42293
  registerCompletionCommand(program2);
42070
42294
  async function main() {
42071
42295
  try {
42296
+ ensureWorkspaceContext();
42072
42297
  const hasArgs = process.argv.slice(2).length > 0;
42073
42298
  if (hasArgs) {
42074
42299
  await program2.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoox-sh/hoox-cli",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
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.1",
29
+ "@hoox-sh/hoox-shared": "^1.3.0",
30
30
  "@types/node": "^26.1.1",
31
31
  "bun-types": "^1.3.14",
32
32
  "typescript": "^6.0.3"