@arkhera30/cli 0.7.1 → 0.8.1

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/dist/index.js CHANGED
@@ -1,12 +1,1163 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __horusCreateRequire } from 'module'; const require = __horusCreateRequire(import.meta.url);
3
+ import {
4
+ __commonJS,
5
+ __require,
6
+ __toESM,
7
+ require_claude_code_strategy,
8
+ require_claude_md_writer,
9
+ require_compiler,
10
+ require_composite_adapter,
11
+ require_core,
12
+ require_cursor_strategy,
13
+ require_errors,
14
+ require_filesystem_adapter,
15
+ require_forge_search_client,
16
+ require_global_claude_code_strategy,
17
+ require_global_config_loader,
18
+ require_http_adapter,
19
+ require_local_repo_state,
20
+ require_mcp_settings_writer,
21
+ require_merge_workspace_configs,
22
+ require_models,
23
+ require_path_utils,
24
+ require_registry,
25
+ require_repo_develop,
26
+ require_repo_errors,
27
+ require_repo_index_query,
28
+ require_repo_index_store,
29
+ require_repo_registry_client,
30
+ require_repo_scanner,
31
+ require_resolver,
32
+ require_session_cleanup,
33
+ require_session_list,
34
+ require_session_store,
35
+ require_url_utils,
36
+ require_workspace_creator,
37
+ require_workspace_lifecycle,
38
+ require_workspace_manager,
39
+ require_workspace_metadata_store
40
+ } from "./chunk-6ROWQWZM.js";
41
+
42
+ // ../forge/packages/core/dist/adapters/git-adapter.js
43
+ var require_git_adapter = __commonJS({
44
+ "../forge/packages/core/dist/adapters/git-adapter.js"(exports) {
45
+ "use strict";
46
+ var __importDefault = exports && exports.__importDefault || function(mod) {
47
+ return mod && mod.__esModule ? mod : { "default": mod };
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.GitAdapter = void 0;
51
+ var child_process_1 = __require("child_process");
52
+ var util_1 = __require("util");
53
+ var crypto_1 = __require("crypto");
54
+ var fs_1 = __require("fs");
55
+ var path_1 = __importDefault(__require("path"));
56
+ var os_1 = __importDefault(__require("os"));
57
+ var filesystem_adapter_js_1 = require_filesystem_adapter();
58
+ var errors_js_1 = require_errors();
59
+ var execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
60
+ var GitAdapter = class {
61
+ url;
62
+ ref;
63
+ registryPath;
64
+ sparse;
65
+ cacheDir;
66
+ tokenEnv;
67
+ delegate = null;
68
+ synced = false;
69
+ constructor(config) {
70
+ this.url = config.url;
71
+ this.ref = config.ref ?? "main";
72
+ this.registryPath = config.registryPath ?? "registry";
73
+ this.sparse = config.sparse;
74
+ this.tokenEnv = config.tokenEnv;
75
+ const baseCache = config.cacheDir ?? path_1.default.join(os_1.default.homedir(), ".forge", "cache", "git");
76
+ const repoHash = (0, crypto_1.createHash)("sha256").update(this.url).digest("hex").slice(0, 12);
77
+ this.cacheDir = path_1.default.join(baseCache, repoHash);
78
+ }
79
+ /**
80
+ * Returns the local cache directory path for this adapter.
81
+ * Useful for debugging and testing.
82
+ */
83
+ getCacheDir() {
84
+ return this.cacheDir;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // DataAdapter interface
88
+ // ---------------------------------------------------------------------------
89
+ async list(type) {
90
+ const delegate = await this.ensureCloned();
91
+ return delegate.list(type);
92
+ }
93
+ async read(type, id) {
94
+ const delegate = await this.ensureCloned();
95
+ return delegate.read(type, id);
96
+ }
97
+ async exists(type, id) {
98
+ const delegate = await this.ensureCloned();
99
+ return delegate.exists(type, id);
100
+ }
101
+ async write(type, id, bundle) {
102
+ const delegate = await this.ensureCloned();
103
+ return delegate.write(type, id, bundle);
104
+ }
105
+ async readResourceFile(type, id, relativePath) {
106
+ const delegate = await this.ensureCloned();
107
+ return delegate.readResourceFile(type, id, relativePath);
108
+ }
109
+ /**
110
+ * List all available semver versions for a specific artifact.
111
+ * Delegates to the underlying FilesystemAdapter's listVersions.
112
+ * Returns versions sorted descending (highest first).
113
+ * Returns empty array for flat layout artifacts or nonexistent artifacts.
114
+ */
115
+ async listVersions(type, id) {
116
+ const delegate = await this.ensureCloned();
117
+ return delegate.listVersions(type, id);
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Git operations
121
+ // ---------------------------------------------------------------------------
122
+ /**
123
+ * Ensure the repo is cloned/updated and return the FilesystemAdapter delegate.
124
+ */
125
+ async ensureCloned() {
126
+ if (this.delegate && this.synced) {
127
+ return this.delegate;
128
+ }
129
+ const exists = await this.cacheExists();
130
+ if (exists) {
131
+ await this.fetchAndCheckout();
132
+ } else {
133
+ await this.cloneRepo();
134
+ }
135
+ const registryRoot = path_1.default.join(this.cacheDir, this.registryPath);
136
+ this.delegate = new filesystem_adapter_js_1.FilesystemAdapter(registryRoot);
137
+ this.synced = true;
138
+ return this.delegate;
139
+ }
140
+ async cacheExists() {
141
+ try {
142
+ await fs_1.promises.access(path_1.default.join(this.cacheDir, ".git"));
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+ resolveUrl() {
149
+ if (!this.tokenEnv)
150
+ return this.url;
151
+ const token = process.env[this.tokenEnv];
152
+ if (!token) {
153
+ console.warn(`[GitAdapter] Token env var '${this.tokenEnv}' is not set. Falling back to unauthenticated access.`);
154
+ return this.url;
155
+ }
156
+ try {
157
+ const parsed = new URL(this.url);
158
+ parsed.username = token;
159
+ return parsed.toString();
160
+ } catch {
161
+ return this.url;
162
+ }
163
+ }
164
+ async cloneRepo() {
165
+ await fs_1.promises.mkdir(this.cacheDir, { recursive: true });
166
+ const cloneUrl = this.resolveUrl();
167
+ const args = ["clone", "--depth", "1", "--branch", this.ref, "--single-branch"];
168
+ if (this.sparse && this.sparse.length > 0) {
169
+ args.push("--sparse");
170
+ }
171
+ args.push(cloneUrl, this.cacheDir);
172
+ try {
173
+ await this.git(args, path_1.default.dirname(this.cacheDir));
174
+ } catch (err) {
175
+ throw new errors_js_1.AdapterError("GitAdapter", `Clone failed for ${this.url}: ${err.message}`, `Check that the URL is correct and you have access. If using HTTPS, set the tokenEnv config.`);
176
+ }
177
+ if (this.sparse && this.sparse.length > 0) {
178
+ try {
179
+ await this.git(["sparse-checkout", "set", ...this.sparse], this.cacheDir);
180
+ } catch (err) {
181
+ throw new errors_js_1.AdapterError("GitAdapter", `Sparse checkout config failed: ${err.message}`, `Check that the sparse paths are valid directories in the repository.`);
182
+ }
183
+ }
184
+ }
185
+ async fetchAndCheckout() {
186
+ try {
187
+ await this.git(["fetch", "--depth", "1", "origin", this.ref], this.cacheDir);
188
+ await this.git(["checkout", "FETCH_HEAD"], this.cacheDir);
189
+ } catch (err) {
190
+ throw new errors_js_1.AdapterError("GitAdapter", `Fetch/checkout failed for ${this.url} ref=${this.ref}: ${err.message}`, `Check that the ref '${this.ref}' exists in the repository and that you have network access.`);
191
+ }
192
+ }
193
+ async git(args, cwd) {
194
+ const { stdout } = await execFileAsync("git", args, {
195
+ cwd,
196
+ timeout: 3e4,
197
+ maxBuffer: 10 * 1024 * 1024
198
+ // 10MB
199
+ });
200
+ return stdout;
201
+ }
202
+ };
203
+ exports.GitAdapter = GitAdapter;
204
+ }
205
+ });
206
+
207
+ // ../forge/packages/core/dist/adapters/index.js
208
+ var require_adapters = __commonJS({
209
+ "../forge/packages/core/dist/adapters/index.js"(exports) {
210
+ "use strict";
211
+ Object.defineProperty(exports, "__esModule", { value: true });
212
+ exports.HttpAdapter = exports.GitAdapter = exports.CompositeAdapter = exports.FilesystemAdapter = exports.ForgeCoreVersionMismatchError = exports.PreUploadValidationError = exports.PublishValidationError = exports.PublishPushError = exports.PublishAuthError = exports.VersionConflictError = exports.AllAdaptersFailedError = exports.AdapterError = exports.UnsupportedTargetError = exports.VersionMismatchError = exports.CircularDependencyError = exports.InvalidMetadataError = exports.ArtifactNotFoundError = exports.ForgeError = void 0;
213
+ var errors_js_1 = require_errors();
214
+ Object.defineProperty(exports, "ForgeError", { enumerable: true, get: function() {
215
+ return errors_js_1.ForgeError;
216
+ } });
217
+ Object.defineProperty(exports, "ArtifactNotFoundError", { enumerable: true, get: function() {
218
+ return errors_js_1.ArtifactNotFoundError;
219
+ } });
220
+ Object.defineProperty(exports, "InvalidMetadataError", { enumerable: true, get: function() {
221
+ return errors_js_1.InvalidMetadataError;
222
+ } });
223
+ Object.defineProperty(exports, "CircularDependencyError", { enumerable: true, get: function() {
224
+ return errors_js_1.CircularDependencyError;
225
+ } });
226
+ Object.defineProperty(exports, "VersionMismatchError", { enumerable: true, get: function() {
227
+ return errors_js_1.VersionMismatchError;
228
+ } });
229
+ Object.defineProperty(exports, "UnsupportedTargetError", { enumerable: true, get: function() {
230
+ return errors_js_1.UnsupportedTargetError;
231
+ } });
232
+ Object.defineProperty(exports, "AdapterError", { enumerable: true, get: function() {
233
+ return errors_js_1.AdapterError;
234
+ } });
235
+ Object.defineProperty(exports, "AllAdaptersFailedError", { enumerable: true, get: function() {
236
+ return errors_js_1.AllAdaptersFailedError;
237
+ } });
238
+ Object.defineProperty(exports, "VersionConflictError", { enumerable: true, get: function() {
239
+ return errors_js_1.VersionConflictError;
240
+ } });
241
+ Object.defineProperty(exports, "PublishAuthError", { enumerable: true, get: function() {
242
+ return errors_js_1.PublishAuthError;
243
+ } });
244
+ Object.defineProperty(exports, "PublishPushError", { enumerable: true, get: function() {
245
+ return errors_js_1.PublishPushError;
246
+ } });
247
+ Object.defineProperty(exports, "PublishValidationError", { enumerable: true, get: function() {
248
+ return errors_js_1.PublishValidationError;
249
+ } });
250
+ Object.defineProperty(exports, "PreUploadValidationError", { enumerable: true, get: function() {
251
+ return errors_js_1.PreUploadValidationError;
252
+ } });
253
+ Object.defineProperty(exports, "ForgeCoreVersionMismatchError", { enumerable: true, get: function() {
254
+ return errors_js_1.ForgeCoreVersionMismatchError;
255
+ } });
256
+ var filesystem_adapter_js_1 = require_filesystem_adapter();
257
+ Object.defineProperty(exports, "FilesystemAdapter", { enumerable: true, get: function() {
258
+ return filesystem_adapter_js_1.FilesystemAdapter;
259
+ } });
260
+ var composite_adapter_js_1 = require_composite_adapter();
261
+ Object.defineProperty(exports, "CompositeAdapter", { enumerable: true, get: function() {
262
+ return composite_adapter_js_1.CompositeAdapter;
263
+ } });
264
+ var git_adapter_js_1 = require_git_adapter();
265
+ Object.defineProperty(exports, "GitAdapter", { enumerable: true, get: function() {
266
+ return git_adapter_js_1.GitAdapter;
267
+ } });
268
+ var http_adapter_js_1 = require_http_adapter();
269
+ Object.defineProperty(exports, "HttpAdapter", { enumerable: true, get: function() {
270
+ return http_adapter_js_1.HttpAdapter;
271
+ } });
272
+ }
273
+ });
274
+
275
+ // ../forge/packages/core/dist/resolver/index.js
276
+ var require_resolver2 = __commonJS({
277
+ "../forge/packages/core/dist/resolver/index.js"(exports) {
278
+ "use strict";
279
+ Object.defineProperty(exports, "__esModule", { value: true });
280
+ exports.mergeWorkspaceConfigs = exports.Resolver = void 0;
281
+ var resolver_js_1 = require_resolver();
282
+ Object.defineProperty(exports, "Resolver", { enumerable: true, get: function() {
283
+ return resolver_js_1.Resolver;
284
+ } });
285
+ var merge_workspace_configs_js_1 = require_merge_workspace_configs();
286
+ Object.defineProperty(exports, "mergeWorkspaceConfigs", { enumerable: true, get: function() {
287
+ return merge_workspace_configs_js_1.mergeWorkspaceConfigs;
288
+ } });
289
+ }
290
+ });
291
+
292
+ // ../forge/packages/core/dist/workspace/workspace-migration.js
293
+ var require_workspace_migration = __commonJS({
294
+ "../forge/packages/core/dist/workspace/workspace-migration.js"(exports) {
295
+ "use strict";
296
+ var __importDefault = exports && exports.__importDefault || function(mod) {
297
+ return mod && mod.__esModule ? mod : { "default": mod };
298
+ };
299
+ Object.defineProperty(exports, "__esModule", { value: true });
300
+ exports.runStartupMigrations = runStartupMigrations;
301
+ var fs_1 = __require("fs");
302
+ var path_1 = __importDefault(__require("path"));
303
+ var global_config_loader_js_1 = require_global_config_loader();
304
+ var path_utils_js_1 = require_path_utils();
305
+ var workspace_metadata_store_js_1 = require_workspace_metadata_store();
306
+ var mcp_settings_writer_js_1 = require_mcp_settings_writer();
307
+ async function runStartupMigrations(globalConfigPath) {
308
+ try {
309
+ await migrateReadGuardHook(globalConfigPath);
310
+ } catch (err) {
311
+ process.stderr.write(JSON.stringify({
312
+ level: "warn",
313
+ message: "[Forge] Startup migration failed",
314
+ error: err?.message ?? String(err),
315
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
316
+ }) + "\n");
317
+ }
318
+ }
319
+ async function migrateReadGuardHook(globalConfigPath) {
320
+ const globalConfig = await (0, global_config_loader_js_1.loadGlobalConfig)(globalConfigPath);
321
+ const mountPath = (0, path_utils_js_1.expandPath)(globalConfig.workspace.mount_path);
322
+ const hostMountPath = globalConfig.workspace.host_workspaces_path ? globalConfig.workspace.host_workspaces_path : mountPath;
323
+ const metaStore = new workspace_metadata_store_js_1.WorkspaceMetadataStore(globalConfig.workspace.store_path);
324
+ const workspaces = await metaStore.list({ status: "active" });
325
+ for (const ws of workspaces) {
326
+ const guardScript = path_1.default.join(ws.path, ".claude", "scripts", "guard-source-reads.sh");
327
+ let missing = false;
328
+ try {
329
+ await fs_1.promises.access(guardScript);
330
+ } catch {
331
+ missing = true;
332
+ }
333
+ if (!missing) {
334
+ continue;
335
+ }
336
+ const hostWorkspacePath = path_1.default.join(hostMountPath, ws.name);
337
+ try {
338
+ await (0, mcp_settings_writer_js_1.emitReadGuardHook)(ws.path, hostWorkspacePath);
339
+ process.stderr.write(JSON.stringify({
340
+ level: "info",
341
+ message: "[Forge] Migration: emitted read guard hook",
342
+ workspace: ws.name,
343
+ workspaceId: ws.id,
344
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
345
+ }) + "\n");
346
+ } catch (err) {
347
+ process.stderr.write(JSON.stringify({
348
+ level: "warn",
349
+ message: "[Forge] Migration: failed to emit read guard hook",
350
+ workspace: ws.name,
351
+ workspaceId: ws.id,
352
+ error: err?.message ?? String(err),
353
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
354
+ }) + "\n");
355
+ }
356
+ }
357
+ }
358
+ }
359
+ });
360
+
361
+ // ../forge/packages/core/dist/workspace/index.js
362
+ var require_workspace = __commonJS({
363
+ "../forge/packages/core/dist/workspace/index.js"(exports) {
364
+ "use strict";
365
+ Object.defineProperty(exports, "__esModule", { value: true });
366
+ exports.runStartupMigrations = exports.WRAPPER_PATH = exports.updateClaudeMcpServers = exports.emitMcpRemoteWrapper = exports.WorkspaceLifecycleManager = exports.generateBranchName = exports.slugify = exports.WorkspaceCreateError = exports.WorkspaceCreator = exports.WORKSPACES_FILE = exports.generateWorkspaceId = exports.WorkspaceMetadataStore = exports.WorkspaceManager = void 0;
367
+ var workspace_manager_js_1 = require_workspace_manager();
368
+ Object.defineProperty(exports, "WorkspaceManager", { enumerable: true, get: function() {
369
+ return workspace_manager_js_1.WorkspaceManager;
370
+ } });
371
+ var workspace_metadata_store_js_1 = require_workspace_metadata_store();
372
+ Object.defineProperty(exports, "WorkspaceMetadataStore", { enumerable: true, get: function() {
373
+ return workspace_metadata_store_js_1.WorkspaceMetadataStore;
374
+ } });
375
+ Object.defineProperty(exports, "generateWorkspaceId", { enumerable: true, get: function() {
376
+ return workspace_metadata_store_js_1.generateWorkspaceId;
377
+ } });
378
+ Object.defineProperty(exports, "WORKSPACES_FILE", { enumerable: true, get: function() {
379
+ return workspace_metadata_store_js_1.WORKSPACES_FILE;
380
+ } });
381
+ var workspace_creator_js_1 = require_workspace_creator();
382
+ Object.defineProperty(exports, "WorkspaceCreator", { enumerable: true, get: function() {
383
+ return workspace_creator_js_1.WorkspaceCreator;
384
+ } });
385
+ Object.defineProperty(exports, "WorkspaceCreateError", { enumerable: true, get: function() {
386
+ return workspace_creator_js_1.WorkspaceCreateError;
387
+ } });
388
+ Object.defineProperty(exports, "slugify", { enumerable: true, get: function() {
389
+ return workspace_creator_js_1.slugify;
390
+ } });
391
+ Object.defineProperty(exports, "generateBranchName", { enumerable: true, get: function() {
392
+ return workspace_creator_js_1.generateBranchName;
393
+ } });
394
+ var workspace_lifecycle_js_1 = require_workspace_lifecycle();
395
+ Object.defineProperty(exports, "WorkspaceLifecycleManager", { enumerable: true, get: function() {
396
+ return workspace_lifecycle_js_1.WorkspaceLifecycleManager;
397
+ } });
398
+ var mcp_settings_writer_js_1 = require_mcp_settings_writer();
399
+ Object.defineProperty(exports, "emitMcpRemoteWrapper", { enumerable: true, get: function() {
400
+ return mcp_settings_writer_js_1.emitMcpRemoteWrapper;
401
+ } });
402
+ Object.defineProperty(exports, "updateClaudeMcpServers", { enumerable: true, get: function() {
403
+ return mcp_settings_writer_js_1.updateClaudeMcpServers;
404
+ } });
405
+ Object.defineProperty(exports, "WRAPPER_PATH", { enumerable: true, get: function() {
406
+ return mcp_settings_writer_js_1.WRAPPER_PATH;
407
+ } });
408
+ var workspace_migration_js_1 = require_workspace_migration();
409
+ Object.defineProperty(exports, "runStartupMigrations", { enumerable: true, get: function() {
410
+ return workspace_migration_js_1.runStartupMigrations;
411
+ } });
412
+ }
413
+ });
414
+
415
+ // ../forge/packages/core/dist/compiler/index.js
416
+ var require_compiler2 = __commonJS({
417
+ "../forge/packages/core/dist/compiler/index.js"(exports) {
418
+ "use strict";
419
+ Object.defineProperty(exports, "__esModule", { value: true });
420
+ exports.removeManagedSection = exports.upsertManagedSection = exports.GlobalClaudeCodeStrategy = exports.CursorStrategy = exports.ClaudeCodeStrategy = exports.Compiler = void 0;
421
+ var compiler_js_1 = require_compiler();
422
+ Object.defineProperty(exports, "Compiler", { enumerable: true, get: function() {
423
+ return compiler_js_1.Compiler;
424
+ } });
425
+ var claude_code_strategy_js_1 = require_claude_code_strategy();
426
+ Object.defineProperty(exports, "ClaudeCodeStrategy", { enumerable: true, get: function() {
427
+ return claude_code_strategy_js_1.ClaudeCodeStrategy;
428
+ } });
429
+ var cursor_strategy_js_1 = require_cursor_strategy();
430
+ Object.defineProperty(exports, "CursorStrategy", { enumerable: true, get: function() {
431
+ return cursor_strategy_js_1.CursorStrategy;
432
+ } });
433
+ var global_claude_code_strategy_js_1 = require_global_claude_code_strategy();
434
+ Object.defineProperty(exports, "GlobalClaudeCodeStrategy", { enumerable: true, get: function() {
435
+ return global_claude_code_strategy_js_1.GlobalClaudeCodeStrategy;
436
+ } });
437
+ var claude_md_writer_js_1 = require_claude_md_writer();
438
+ Object.defineProperty(exports, "upsertManagedSection", { enumerable: true, get: function() {
439
+ return claude_md_writer_js_1.upsertManagedSection;
440
+ } });
441
+ Object.defineProperty(exports, "removeManagedSection", { enumerable: true, get: function() {
442
+ return claude_md_writer_js_1.removeManagedSection;
443
+ } });
444
+ }
445
+ });
446
+
447
+ // ../forge/packages/core/dist/config/index.js
448
+ var require_config = __commonJS({
449
+ "../forge/packages/core/dist/config/index.js"(exports) {
450
+ "use strict";
451
+ Object.defineProperty(exports, "__esModule", { value: true });
452
+ exports.expandPaths = exports.expandPath = exports.GLOBAL_CONFIG_PATH = exports.GLOBAL_CONFIG_DIR = exports.DEFAULT_GLOBAL_REGISTRY = exports.DEFAULT_LOCAL_REGISTRY = exports.selectSharedRepoRegistry = exports.ensureDefaultRegistries = exports.removeGlobalRegistry = exports.addGlobalRegistry = exports.saveGlobalConfig = exports.loadGlobalConfig = void 0;
453
+ var global_config_loader_js_1 = require_global_config_loader();
454
+ Object.defineProperty(exports, "loadGlobalConfig", { enumerable: true, get: function() {
455
+ return global_config_loader_js_1.loadGlobalConfig;
456
+ } });
457
+ Object.defineProperty(exports, "saveGlobalConfig", { enumerable: true, get: function() {
458
+ return global_config_loader_js_1.saveGlobalConfig;
459
+ } });
460
+ Object.defineProperty(exports, "addGlobalRegistry", { enumerable: true, get: function() {
461
+ return global_config_loader_js_1.addGlobalRegistry;
462
+ } });
463
+ Object.defineProperty(exports, "removeGlobalRegistry", { enumerable: true, get: function() {
464
+ return global_config_loader_js_1.removeGlobalRegistry;
465
+ } });
466
+ Object.defineProperty(exports, "ensureDefaultRegistries", { enumerable: true, get: function() {
467
+ return global_config_loader_js_1.ensureDefaultRegistries;
468
+ } });
469
+ Object.defineProperty(exports, "selectSharedRepoRegistry", { enumerable: true, get: function() {
470
+ return global_config_loader_js_1.selectSharedRepoRegistry;
471
+ } });
472
+ Object.defineProperty(exports, "DEFAULT_LOCAL_REGISTRY", { enumerable: true, get: function() {
473
+ return global_config_loader_js_1.DEFAULT_LOCAL_REGISTRY;
474
+ } });
475
+ Object.defineProperty(exports, "DEFAULT_GLOBAL_REGISTRY", { enumerable: true, get: function() {
476
+ return global_config_loader_js_1.DEFAULT_GLOBAL_REGISTRY;
477
+ } });
478
+ Object.defineProperty(exports, "GLOBAL_CONFIG_DIR", { enumerable: true, get: function() {
479
+ return global_config_loader_js_1.GLOBAL_CONFIG_DIR;
480
+ } });
481
+ Object.defineProperty(exports, "GLOBAL_CONFIG_PATH", { enumerable: true, get: function() {
482
+ return global_config_loader_js_1.GLOBAL_CONFIG_PATH;
483
+ } });
484
+ var path_utils_js_1 = require_path_utils();
485
+ Object.defineProperty(exports, "expandPath", { enumerable: true, get: function() {
486
+ return path_utils_js_1.expandPath;
487
+ } });
488
+ Object.defineProperty(exports, "expandPaths", { enumerable: true, get: function() {
489
+ return path_utils_js_1.expandPaths;
490
+ } });
491
+ }
492
+ });
493
+
494
+ // ../forge/packages/core/dist/repo/clone-layout.js
495
+ var require_clone_layout = __commonJS({
496
+ "../forge/packages/core/dist/repo/clone-layout.js"(exports) {
497
+ "use strict";
498
+ Object.defineProperty(exports, "__esModule", { value: true });
499
+ exports.horusDataPath = horusDataPath;
500
+ exports.horusReposRoot = horusReposRoot;
501
+ exports.repoClonePath = repoClonePath;
502
+ exports.repoClonePathFromCoordinate = repoClonePathFromCoordinate;
503
+ exports.repoWorktreesDir = repoWorktreesDir;
504
+ exports.worktreePath = worktreePath;
505
+ exports.ensureHorusIgnored = ensureHorusIgnored;
506
+ var node_os_1 = __require("os");
507
+ var node_path_1 = __require("path");
508
+ var node_fs_1 = __require("fs");
509
+ var url_utils_js_1 = require_url_utils();
510
+ function horusDataPath() {
511
+ return process.env.HORUS_DATA_PATH || (0, node_path_1.join)((0, node_os_1.homedir)(), "Horus", "data");
512
+ }
513
+ function horusReposRoot(dataPath = horusDataPath()) {
514
+ return (0, node_path_1.join)(dataPath, "repos");
515
+ }
516
+ function repoClonePath(url, dataPath = horusDataPath()) {
517
+ const { host, org, name } = (0, url_utils_js_1.deriveRepoCoordinate)(url);
518
+ return (0, node_path_1.join)(horusReposRoot(dataPath), host, org, name);
519
+ }
520
+ function repoClonePathFromCoordinate(coord, dataPath = horusDataPath()) {
521
+ return (0, node_path_1.join)(horusReposRoot(dataPath), coord.host, coord.org, coord.name);
522
+ }
523
+ function repoWorktreesDir(clonePath) {
524
+ return (0, node_path_1.join)(clonePath, ".horus", "worktrees");
525
+ }
526
+ function worktreePath(clonePath, sessionId) {
527
+ return (0, node_path_1.join)(repoWorktreesDir(clonePath), sessionId);
528
+ }
529
+ var HORUS_EXCLUDE_LINE = "/.horus/";
530
+ async function ensureHorusIgnored(clonePath) {
531
+ const excludePath = (0, node_path_1.join)(clonePath, ".git", "info", "exclude");
532
+ let existing = "";
533
+ try {
534
+ existing = await node_fs_1.promises.readFile(excludePath, "utf8");
535
+ } catch (err) {
536
+ if (err.code !== "ENOENT")
537
+ throw err;
538
+ await node_fs_1.promises.mkdir((0, node_path_1.join)(clonePath, ".git", "info"), { recursive: true });
539
+ }
540
+ const lines = existing.split("\n").map((l) => l.trim());
541
+ if (lines.includes(HORUS_EXCLUDE_LINE) || lines.includes(".horus/")) {
542
+ return;
543
+ }
544
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
545
+ await node_fs_1.promises.appendFile(excludePath, `${prefix}# Horus-managed session worktrees (do not commit)
546
+ ${HORUS_EXCLUDE_LINE}
547
+ `);
548
+ }
549
+ }
550
+ });
551
+
552
+ // ../forge/packages/core/dist/repo/local-repo-state-store.js
553
+ var require_local_repo_state_store = __commonJS({
554
+ "../forge/packages/core/dist/repo/local-repo-state-store.js"(exports) {
555
+ "use strict";
556
+ var __importDefault = exports && exports.__importDefault || function(mod) {
557
+ return mod && mod.__esModule ? mod : { "default": mod };
558
+ };
559
+ Object.defineProperty(exports, "__esModule", { value: true });
560
+ exports.LocalRepoStateStoreManager = void 0;
561
+ exports.repoStatePath = repoStatePath;
562
+ var node_fs_1 = __require("fs");
563
+ var node_path_1 = __importDefault(__require("path"));
564
+ var local_repo_state_js_1 = require_local_repo_state();
565
+ var clone_layout_js_1 = require_clone_layout();
566
+ function repoStatePath(dataPath = (0, clone_layout_js_1.horusDataPath)()) {
567
+ return node_path_1.default.join(dataPath, "config", "repo-state.json");
568
+ }
569
+ var LocalRepoStateStoreManager = class {
570
+ storePath;
571
+ constructor(storePath = repoStatePath()) {
572
+ this.storePath = storePath;
573
+ }
574
+ async load() {
575
+ try {
576
+ const raw = await node_fs_1.promises.readFile(this.storePath, "utf8");
577
+ return local_repo_state_js_1.LocalRepoStateStoreSchema.parse(JSON.parse(raw));
578
+ } catch (err) {
579
+ if (err.code === "ENOENT") {
580
+ return { ...local_repo_state_js_1.EMPTY_LOCAL_REPO_STATE, repos: [] };
581
+ }
582
+ throw err;
583
+ }
584
+ }
585
+ async save(store) {
586
+ await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.storePath), { recursive: true });
587
+ await node_fs_1.promises.writeFile(this.storePath, JSON.stringify(store, null, 2), "utf8");
588
+ }
589
+ async get(clonePath) {
590
+ const store = await this.load();
591
+ return store.repos.find((r) => r.clonePath === clonePath) ?? null;
592
+ }
593
+ /** Insert or replace the entry for this clonePath (dedup-safe). */
594
+ async upsert(entry) {
595
+ const store = await this.load();
596
+ const idx = store.repos.findIndex((r) => r.clonePath === entry.clonePath);
597
+ if (idx >= 0)
598
+ store.repos[idx] = entry;
599
+ else
600
+ store.repos.push(entry);
601
+ await this.save(store);
602
+ }
603
+ /** Patch fields on an existing entry, creating it if absent. */
604
+ async patch(seed, patch) {
605
+ const store = await this.load();
606
+ let entry = store.repos.find((r) => r.clonePath === seed.clonePath);
607
+ if (!entry) {
608
+ entry = { ...seed, lastFetchedAt: null, lastUsedAt: null, worktrees: [] };
609
+ store.repos.push(entry);
610
+ }
611
+ Object.assign(entry, patch);
612
+ await this.save(store);
613
+ return entry;
614
+ }
615
+ /** Add a worktree record, deduped by sessionId. */
616
+ async addWorktree(clonePath, wt) {
617
+ const store = await this.load();
618
+ const entry = store.repos.find((r) => r.clonePath === clonePath);
619
+ if (!entry)
620
+ throw new Error(`LocalRepoState: no entry for ${clonePath}`);
621
+ if (!entry.worktrees.some((w) => w.sessionId === wt.sessionId)) {
622
+ entry.worktrees.push(wt);
623
+ }
624
+ await this.save(store);
625
+ }
626
+ };
627
+ exports.LocalRepoStateStoreManager = LocalRepoStateStoreManager;
628
+ }
629
+ });
630
+
631
+ // ../forge/packages/core/dist/repo/clone-semantics.js
632
+ var require_clone_semantics = __commonJS({
633
+ "../forge/packages/core/dist/repo/clone-semantics.js"(exports) {
634
+ "use strict";
635
+ Object.defineProperty(exports, "__esModule", { value: true });
636
+ exports.DEFAULT_CLONE_TTL_MS = void 0;
637
+ exports.cloneTtlMs = cloneTtlMs;
638
+ exports.isCloneStale = isCloneStale;
639
+ exports.ensureClone = ensureClone;
640
+ exports.refreshIfStale = refreshIfStale;
641
+ exports.createSessionWorktree = createSessionWorktree;
642
+ var node_fs_1 = __require("fs");
643
+ var node_path_1 = __require("path");
644
+ var node_child_process_1 = __require("child_process");
645
+ var node_util_1 = __require("util");
646
+ var clone_layout_js_1 = require_clone_layout();
647
+ var url_utils_js_1 = require_url_utils();
648
+ var local_repo_state_store_js_1 = require_local_repo_state_store();
649
+ var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
650
+ exports.DEFAULT_CLONE_TTL_MS = 60 * 60 * 1e3;
651
+ function cloneTtlMs(override) {
652
+ if (typeof override === "number")
653
+ return override;
654
+ const env = process.env.HORUS_CLONE_TTL_MS;
655
+ if (env && Number.isFinite(Number(env)))
656
+ return Number(env);
657
+ return exports.DEFAULT_CLONE_TTL_MS;
658
+ }
659
+ function isCloneStale(lastFetchedAt, ttlMs, now) {
660
+ if (lastFetchedAt === null)
661
+ return true;
662
+ return now.getTime() - new Date(lastFetchedAt).getTime() > ttlMs;
663
+ }
664
+ async function isGitRepo(dir) {
665
+ try {
666
+ await execFileAsync("git", ["rev-parse", "--git-dir"], { cwd: dir });
667
+ return true;
668
+ } catch {
669
+ return false;
670
+ }
671
+ }
672
+ async function ensureClone(url, opts = {}) {
673
+ const dataPath = opts.dataPath ?? (0, clone_layout_js_1.horusDataPath)();
674
+ const clonePath = (0, clone_layout_js_1.repoClonePath)(url, dataPath);
675
+ const coord = (0, url_utils_js_1.deriveRepoCoordinate)(url);
676
+ const store = new local_repo_state_store_js_1.LocalRepoStateStoreManager((0, local_repo_state_store_js_1.repoStatePath)(dataPath));
677
+ if (await isGitRepo(clonePath)) {
678
+ await store.patch({ ...coord, clonePath }, {});
679
+ await (0, clone_layout_js_1.ensureHorusIgnored)(clonePath);
680
+ return clonePath;
681
+ }
682
+ await node_fs_1.promises.rm(clonePath, { recursive: true, force: true }).catch(() => {
683
+ });
684
+ const tmpPath = `${clonePath}.tmp-${process.pid}-${Date.now()}`;
685
+ await node_fs_1.promises.mkdir((0, node_path_1.dirname)(clonePath), { recursive: true });
686
+ try {
687
+ await execFileAsync("git", ["clone", url, tmpPath], {
688
+ timeout: opts.gitTimeoutMs ?? 12e4
689
+ });
690
+ await node_fs_1.promises.rename(tmpPath, clonePath);
691
+ } catch (err) {
692
+ await node_fs_1.promises.rm(tmpPath, { recursive: true, force: true }).catch(() => {
693
+ });
694
+ throw err;
695
+ }
696
+ await (0, clone_layout_js_1.ensureHorusIgnored)(clonePath);
697
+ await store.patch({ ...coord, clonePath }, {});
698
+ return clonePath;
699
+ }
700
+ async function refreshIfStale(url, baseBranch, opts = {}) {
701
+ const dataPath = opts.dataPath ?? (0, clone_layout_js_1.horusDataPath)();
702
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
703
+ const ttl = cloneTtlMs(opts.ttlMs);
704
+ const clonePath = (0, clone_layout_js_1.repoClonePath)(url, dataPath);
705
+ const coord = (0, url_utils_js_1.deriveRepoCoordinate)(url);
706
+ const store = new local_repo_state_store_js_1.LocalRepoStateStoreManager((0, local_repo_state_store_js_1.repoStatePath)(dataPath));
707
+ const entry = await store.get(clonePath);
708
+ if (!isCloneStale(entry?.lastFetchedAt ?? null, ttl, now))
709
+ return false;
710
+ await execFileAsync("git", ["fetch", "origin", baseBranch], {
711
+ cwd: clonePath,
712
+ timeout: opts.gitTimeoutMs ?? 12e4
713
+ });
714
+ await store.patch({ ...coord, clonePath }, { lastFetchedAt: now.toISOString() });
715
+ return true;
716
+ }
717
+ async function createSessionWorktree(url, sessionId, baseBranch, branch, opts = {}) {
718
+ const dataPath = opts.dataPath ?? (0, clone_layout_js_1.horusDataPath)();
719
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
720
+ const clonePath = await ensureClone(url, opts);
721
+ await refreshIfStale(url, baseBranch, opts);
722
+ const wtPath = (0, clone_layout_js_1.worktreePath)(clonePath, sessionId);
723
+ await execFileAsync("git", ["worktree", "add", "-b", branch, wtPath, `origin/${baseBranch}`], { cwd: clonePath, timeout: opts.gitTimeoutMs ?? 12e4 });
724
+ const coord = (0, url_utils_js_1.deriveRepoCoordinate)(url);
725
+ const store = new local_repo_state_store_js_1.LocalRepoStateStoreManager((0, local_repo_state_store_js_1.repoStatePath)(dataPath));
726
+ await store.patch({ ...coord, clonePath }, { lastUsedAt: now.toISOString() });
727
+ await store.addWorktree(clonePath, {
728
+ sessionId,
729
+ path: wtPath,
730
+ branch,
731
+ createdAt: now.toISOString()
732
+ });
733
+ return wtPath;
734
+ }
735
+ }
736
+ });
737
+
738
+ // ../forge/packages/core/dist/session/agent-spawn.js
739
+ var require_agent_spawn = __commonJS({
740
+ "../forge/packages/core/dist/session/agent-spawn.js"(exports) {
741
+ "use strict";
742
+ Object.defineProperty(exports, "__esModule", { value: true });
743
+ exports.buildClaudeArgs = buildClaudeArgs;
744
+ exports.spawnAgentSession = spawnAgentSession;
745
+ var child_process_1 = __require("child_process");
746
+ var util_1 = __require("util");
747
+ var errors_js_1 = require_errors();
748
+ var execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
749
+ var MAX_BUFFER = 64 * 1024 * 1024;
750
+ function resolveClaudeBin(opts) {
751
+ return opts.claudeBin ?? process.env.CLAUDE_BIN ?? "claude";
752
+ }
753
+ function buildClaudeArgs(opts) {
754
+ const args = ["-p", opts.prompt, "--output-format", "json"];
755
+ if (opts.resume)
756
+ args.push("--resume", opts.resume);
757
+ args.push("--permission-mode", opts.permissionMode ?? "acceptEdits");
758
+ if (opts.allowedTools && opts.allowedTools.length > 0) {
759
+ args.push("--allowedTools", opts.allowedTools.join(","));
760
+ }
761
+ if (typeof opts.maxTurns === "number") {
762
+ args.push("--max-turns", String(opts.maxTurns));
763
+ }
764
+ return args;
765
+ }
766
+ async function spawnAgentSession(opts) {
767
+ const bin = resolveClaudeBin(opts);
768
+ const args = buildClaudeArgs(opts);
769
+ let stdout;
770
+ try {
771
+ const res = await execFileAsync(bin, args, {
772
+ cwd: opts.cwd,
773
+ timeout: opts.timeoutMs ?? 0,
774
+ maxBuffer: MAX_BUFFER,
775
+ encoding: "utf8"
776
+ // guarantee string stdout (and string err.stdout on the salvage path)
777
+ });
778
+ stdout = res.stdout;
779
+ } catch (err) {
780
+ if (err?.code === "ENOENT") {
781
+ throw new errors_js_1.ForgeError("AGENT_BIN_NOT_FOUND", `Claude Code binary "${bin}" was not found on PATH.`, `Install Claude Code or set CLAUDE_BIN to its absolute path. The edit-flow spawn requires a non-bare claude executable.`);
782
+ }
783
+ const salvaged = typeof err?.stdout === "string" ? err.stdout : "";
784
+ if (salvaged.trim()) {
785
+ stdout = salvaged;
786
+ } else {
787
+ throw new errors_js_1.ForgeError("AGENT_SPAWN_FAILED", `Headless claude run failed (cwd=${opts.cwd}): ${err?.message ?? "unknown error"}`, `Verify the worktree is valid and the claude session can authenticate (non-bare inherits the logged-in credential).`);
788
+ }
789
+ }
790
+ let parsed;
791
+ try {
792
+ parsed = JSON.parse(stdout);
793
+ } catch {
794
+ throw new errors_js_1.ForgeError("AGENT_OUTPUT_UNPARSEABLE", `Could not parse JSON from "claude --output-format json" (cwd=${opts.cwd}).`, `First 400 chars of output: ${stdout.slice(0, 400)}`);
795
+ }
796
+ const claudeSessionId = parsed["session_id"] ?? parsed["sessionId"];
797
+ if (!claudeSessionId) {
798
+ throw new errors_js_1.ForgeError("AGENT_OUTPUT_MISSING_SESSION_ID", `"claude --output-format json" did not return a session_id (cwd=${opts.cwd}).`, `session_id is required for resume + provenance. Confirm the Claude Code version supports --output-format json.`);
799
+ }
800
+ return {
801
+ claudeSessionId,
802
+ result: typeof parsed["result"] === "string" ? parsed["result"] : "",
803
+ isError: parsed["is_error"] === true,
804
+ costUsd: typeof parsed["total_cost_usd"] === "number" ? parsed["total_cost_usd"] : void 0,
805
+ numTurns: typeof parsed["num_turns"] === "number" ? parsed["num_turns"] : void 0,
806
+ raw: parsed
807
+ };
808
+ }
809
+ }
810
+ });
811
+
812
+ // ../forge/packages/core/dist/session/gc.js
813
+ var require_gc = __commonJS({
814
+ "../forge/packages/core/dist/session/gc.js"(exports) {
815
+ "use strict";
816
+ Object.defineProperty(exports, "__esModule", { value: true });
817
+ exports.isCleanTree = isCleanTree;
818
+ exports.classifyWorktree = classifyWorktree;
819
+ exports.getWorktreeTreeState = getWorktreeTreeState;
820
+ exports.worktreeGc = worktreeGc;
821
+ exports.cloneGc = cloneGc;
822
+ var node_fs_1 = __require("fs");
823
+ var node_child_process_1 = __require("child_process");
824
+ var node_util_1 = __require("util");
825
+ var local_repo_state_store_js_1 = require_local_repo_state_store();
826
+ var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
827
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
828
+ "in_progress",
829
+ "in-progress",
830
+ "in_review",
831
+ "in-review",
832
+ "open",
833
+ "blocked"
834
+ ]);
835
+ function isCleanTree(t) {
836
+ return t.determined && !t.uncommitted && !t.unpushed && !t.stash;
837
+ }
838
+ function classifyWorktree(input2) {
839
+ const reasons = [];
840
+ const active = input2.status !== null && ACTIVE_STATUSES.has(input2.status);
841
+ if (active)
842
+ reasons.push(`work item status '${input2.status}' is active`);
843
+ if (input2.status === null)
844
+ reasons.push("work item status unknown (Anvil) \u2014 treated as active/unsafe");
845
+ const clean = isCleanTree(input2.tree);
846
+ if (!input2.tree.determined)
847
+ reasons.push("tree state could not be determined \u2014 treated as dirty/unsafe");
848
+ else {
849
+ if (input2.tree.uncommitted)
850
+ reasons.push("uncommitted changes");
851
+ if (input2.tree.unpushed)
852
+ reasons.push("unpushed commits");
853
+ if (input2.tree.stash)
854
+ reasons.push("stash entries present");
855
+ }
856
+ const eligible = !active && input2.status !== null && clean;
857
+ if (eligible)
858
+ reasons.push(`inactive + clean (age ${Math.round(input2.ageMs / 864e5)}d, informational only)`);
859
+ return { eligible, reasons };
860
+ }
861
+ async function getWorktreeTreeState(worktreePath) {
862
+ try {
863
+ const { stdout: porcelain } = await execFileAsync("git", ["status", "--porcelain"], {
864
+ cwd: worktreePath,
865
+ timeout: 15e3
866
+ });
867
+ const { stdout: stashList } = await execFileAsync("git", ["stash", "list"], {
868
+ cwd: worktreePath,
869
+ timeout: 1e4
870
+ });
871
+ let unpushed = true;
872
+ try {
873
+ const { stdout: up } = await execFileAsync("git", ["rev-list", "--count", "@{upstream}..HEAD"], { cwd: worktreePath, timeout: 1e4 });
874
+ unpushed = parseInt(up.trim(), 10) > 0;
875
+ } catch {
876
+ unpushed = true;
877
+ }
878
+ return {
879
+ uncommitted: porcelain.trim().length > 0,
880
+ unpushed,
881
+ stash: stashList.trim().length > 0,
882
+ determined: true
883
+ };
884
+ } catch {
885
+ return { uncommitted: true, unpushed: true, stash: true, determined: false };
886
+ }
887
+ }
888
+ async function worktreeGc(opts) {
889
+ const dataPath = opts.dataPath;
890
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
891
+ const store = new local_repo_state_store_js_1.LocalRepoStateStoreManager((0, local_repo_state_store_js_1.repoStatePath)(dataPath));
892
+ const state = await store.load();
893
+ const reclaimed = [];
894
+ const retained = [];
895
+ for (const entry of state.repos) {
896
+ for (const wt of entry.worktrees) {
897
+ const status = await opts.resolveStatus(wt, entry);
898
+ const tree = await getWorktreeTreeState(wt.path);
899
+ const ageMs = now.getTime() - new Date(wt.createdAt).getTime();
900
+ const { eligible, reasons } = classifyWorktree({ status, tree, ageMs });
901
+ const cand = {
902
+ clonePath: entry.clonePath,
903
+ sessionId: wt.sessionId,
904
+ worktreePath: wt.path,
905
+ branch: wt.branch,
906
+ eligible,
907
+ reasons
908
+ };
909
+ if (eligible)
910
+ reclaimed.push(cand);
911
+ else
912
+ retained.push(cand);
913
+ }
914
+ }
915
+ if (opts.apply === true) {
916
+ for (const cand of reclaimed) {
917
+ await execFileAsync("git", ["worktree", "remove", "--force", cand.worktreePath], {
918
+ cwd: cand.clonePath,
919
+ timeout: 15e3
920
+ }).catch(() => {
921
+ });
922
+ await execFileAsync("git", ["worktree", "prune"], {
923
+ cwd: cand.clonePath,
924
+ timeout: 1e4
925
+ }).catch(() => {
926
+ });
927
+ await node_fs_1.promises.rm(cand.worktreePath, { recursive: true, force: true }).catch(() => {
928
+ });
929
+ const entry = state.repos.find((r) => r.clonePath === cand.clonePath);
930
+ entry.worktrees = entry.worktrees.filter((w) => w.sessionId !== cand.sessionId);
931
+ }
932
+ await store.save(state);
933
+ }
934
+ return { applied: opts.apply === true, reclaimed, retained };
935
+ }
936
+ async function cloneGc(opts) {
937
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
938
+ const store = new local_repo_state_store_js_1.LocalRepoStateStoreManager((0, local_repo_state_store_js_1.repoStatePath)(opts.dataPath));
939
+ const state = await store.load();
940
+ const reclaimed = [];
941
+ const retained = [];
942
+ for (const entry of state.repos) {
943
+ const reasons = [];
944
+ let eligible = true;
945
+ if (entry.worktrees.length > 0) {
946
+ eligible = false;
947
+ reasons.push(`${entry.worktrees.length} live worktree(s) \u2014 never reclaim`);
948
+ } else if (entry.lastUsedAt === null) {
949
+ eligible = false;
950
+ reasons.push("never used \u2014 no LRU signal, retained");
951
+ } else {
952
+ const idle = now.getTime() - new Date(entry.lastUsedAt).getTime();
953
+ if (idle <= opts.maxIdleMs) {
954
+ eligible = false;
955
+ reasons.push(`idle ${Math.round(idle / 864e5)}d \u2264 threshold \u2014 retained`);
956
+ } else {
957
+ reasons.push(`idle ${Math.round(idle / 864e5)}d > threshold, no live worktrees`);
958
+ }
959
+ }
960
+ const cand = {
961
+ clonePath: entry.clonePath,
962
+ liveWorktrees: entry.worktrees.length,
963
+ lastUsedAt: entry.lastUsedAt,
964
+ eligible,
965
+ reasons
966
+ };
967
+ if (eligible)
968
+ reclaimed.push(cand);
969
+ else
970
+ retained.push(cand);
971
+ }
972
+ if (opts.apply === true) {
973
+ for (const cand of reclaimed) {
974
+ await node_fs_1.promises.rm(cand.clonePath, { recursive: true, force: true }).catch(() => {
975
+ });
976
+ }
977
+ state.repos = state.repos.filter((r) => !reclaimed.some((c) => c.clonePath === r.clonePath));
978
+ await store.save(state);
979
+ }
980
+ return { applied: opts.apply === true, reclaimed, retained };
981
+ }
982
+ }
983
+ });
984
+
985
+ // ../forge/packages/core/dist/index.js
986
+ var require_dist = __commonJS({
987
+ "../forge/packages/core/dist/index.js"(exports) {
988
+ "use strict";
989
+ var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
990
+ if (k2 === void 0) k2 = k;
991
+ var desc = Object.getOwnPropertyDescriptor(m, k);
992
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
993
+ desc = { enumerable: true, get: function() {
994
+ return m[k];
995
+ } };
996
+ }
997
+ Object.defineProperty(o, k2, desc);
998
+ }) : (function(o, m, k, k2) {
999
+ if (k2 === void 0) k2 = k;
1000
+ o[k2] = m[k];
1001
+ }));
1002
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
1003
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
1004
+ };
1005
+ Object.defineProperty(exports, "__esModule", { value: true });
1006
+ exports.cloneGc = exports.worktreeGc = exports.getWorktreeTreeState = exports.isCleanTree = exports.classifyWorktree = exports.sessionCleanup = exports.sessionList = exports.SessionStoreManager = exports.ForgeSearchClient = exports.RepoAmbiguousError = exports.RepoExistsError = exports.RepoNotFoundError = exports.RepoRegistryClient = exports.buildClaudeArgs = exports.spawnAgentSession = exports.repoDevelop = exports.repoStatePath = exports.LocalRepoStateStoreManager = exports.createSessionWorktree = exports.refreshIfStale = exports.ensureClone = exports.DEFAULT_CLONE_TTL_MS = exports.cloneTtlMs = exports.isCloneStale = exports.ensureHorusIgnored = exports.worktreePath = exports.repoWorktreesDir = exports.repoClonePathFromCoordinate = exports.repoClonePath = exports.horusReposRoot = exports.horusDataPath = exports.deriveRepoCoordinate = exports.normalizeHost = exports.normalizeGitUrl = exports.RepoIndexQuery = exports.loadRepoIndex = exports.saveRepoIndex = exports.scan = exports.Registry = exports.ForgeCore = void 0;
1007
+ var core_js_1 = require_core();
1008
+ Object.defineProperty(exports, "ForgeCore", { enumerable: true, get: function() {
1009
+ return core_js_1.ForgeCore;
1010
+ } });
1011
+ var registry_js_1 = require_registry();
1012
+ Object.defineProperty(exports, "Registry", { enumerable: true, get: function() {
1013
+ return registry_js_1.Registry;
1014
+ } });
1015
+ __exportStar(require_models(), exports);
1016
+ __exportStar(require_adapters(), exports);
1017
+ __exportStar(require_resolver2(), exports);
1018
+ __exportStar(require_workspace(), exports);
1019
+ __exportStar(require_compiler2(), exports);
1020
+ __exportStar(require_config(), exports);
1021
+ var repo_scanner_js_1 = require_repo_scanner();
1022
+ Object.defineProperty(exports, "scan", { enumerable: true, get: function() {
1023
+ return repo_scanner_js_1.scan;
1024
+ } });
1025
+ var repo_index_store_js_1 = require_repo_index_store();
1026
+ Object.defineProperty(exports, "saveRepoIndex", { enumerable: true, get: function() {
1027
+ return repo_index_store_js_1.saveRepoIndex;
1028
+ } });
1029
+ Object.defineProperty(exports, "loadRepoIndex", { enumerable: true, get: function() {
1030
+ return repo_index_store_js_1.loadRepoIndex;
1031
+ } });
1032
+ var repo_index_query_js_1 = require_repo_index_query();
1033
+ Object.defineProperty(exports, "RepoIndexQuery", { enumerable: true, get: function() {
1034
+ return repo_index_query_js_1.RepoIndexQuery;
1035
+ } });
1036
+ var url_utils_js_1 = require_url_utils();
1037
+ Object.defineProperty(exports, "normalizeGitUrl", { enumerable: true, get: function() {
1038
+ return url_utils_js_1.normalizeGitUrl;
1039
+ } });
1040
+ Object.defineProperty(exports, "normalizeHost", { enumerable: true, get: function() {
1041
+ return url_utils_js_1.normalizeHost;
1042
+ } });
1043
+ Object.defineProperty(exports, "deriveRepoCoordinate", { enumerable: true, get: function() {
1044
+ return url_utils_js_1.deriveRepoCoordinate;
1045
+ } });
1046
+ var clone_layout_js_1 = require_clone_layout();
1047
+ Object.defineProperty(exports, "horusDataPath", { enumerable: true, get: function() {
1048
+ return clone_layout_js_1.horusDataPath;
1049
+ } });
1050
+ Object.defineProperty(exports, "horusReposRoot", { enumerable: true, get: function() {
1051
+ return clone_layout_js_1.horusReposRoot;
1052
+ } });
1053
+ Object.defineProperty(exports, "repoClonePath", { enumerable: true, get: function() {
1054
+ return clone_layout_js_1.repoClonePath;
1055
+ } });
1056
+ Object.defineProperty(exports, "repoClonePathFromCoordinate", { enumerable: true, get: function() {
1057
+ return clone_layout_js_1.repoClonePathFromCoordinate;
1058
+ } });
1059
+ Object.defineProperty(exports, "repoWorktreesDir", { enumerable: true, get: function() {
1060
+ return clone_layout_js_1.repoWorktreesDir;
1061
+ } });
1062
+ Object.defineProperty(exports, "worktreePath", { enumerable: true, get: function() {
1063
+ return clone_layout_js_1.worktreePath;
1064
+ } });
1065
+ Object.defineProperty(exports, "ensureHorusIgnored", { enumerable: true, get: function() {
1066
+ return clone_layout_js_1.ensureHorusIgnored;
1067
+ } });
1068
+ var clone_semantics_js_1 = require_clone_semantics();
1069
+ Object.defineProperty(exports, "isCloneStale", { enumerable: true, get: function() {
1070
+ return clone_semantics_js_1.isCloneStale;
1071
+ } });
1072
+ Object.defineProperty(exports, "cloneTtlMs", { enumerable: true, get: function() {
1073
+ return clone_semantics_js_1.cloneTtlMs;
1074
+ } });
1075
+ Object.defineProperty(exports, "DEFAULT_CLONE_TTL_MS", { enumerable: true, get: function() {
1076
+ return clone_semantics_js_1.DEFAULT_CLONE_TTL_MS;
1077
+ } });
1078
+ Object.defineProperty(exports, "ensureClone", { enumerable: true, get: function() {
1079
+ return clone_semantics_js_1.ensureClone;
1080
+ } });
1081
+ Object.defineProperty(exports, "refreshIfStale", { enumerable: true, get: function() {
1082
+ return clone_semantics_js_1.refreshIfStale;
1083
+ } });
1084
+ Object.defineProperty(exports, "createSessionWorktree", { enumerable: true, get: function() {
1085
+ return clone_semantics_js_1.createSessionWorktree;
1086
+ } });
1087
+ var local_repo_state_store_js_1 = require_local_repo_state_store();
1088
+ Object.defineProperty(exports, "LocalRepoStateStoreManager", { enumerable: true, get: function() {
1089
+ return local_repo_state_store_js_1.LocalRepoStateStoreManager;
1090
+ } });
1091
+ Object.defineProperty(exports, "repoStatePath", { enumerable: true, get: function() {
1092
+ return local_repo_state_store_js_1.repoStatePath;
1093
+ } });
1094
+ var repo_develop_js_1 = require_repo_develop();
1095
+ Object.defineProperty(exports, "repoDevelop", { enumerable: true, get: function() {
1096
+ return repo_develop_js_1.repoDevelop;
1097
+ } });
1098
+ var agent_spawn_js_1 = require_agent_spawn();
1099
+ Object.defineProperty(exports, "spawnAgentSession", { enumerable: true, get: function() {
1100
+ return agent_spawn_js_1.spawnAgentSession;
1101
+ } });
1102
+ Object.defineProperty(exports, "buildClaudeArgs", { enumerable: true, get: function() {
1103
+ return agent_spawn_js_1.buildClaudeArgs;
1104
+ } });
1105
+ var repo_registry_client_js_1 = require_repo_registry_client();
1106
+ Object.defineProperty(exports, "RepoRegistryClient", { enumerable: true, get: function() {
1107
+ return repo_registry_client_js_1.RepoRegistryClient;
1108
+ } });
1109
+ var repo_errors_js_1 = require_repo_errors();
1110
+ Object.defineProperty(exports, "RepoNotFoundError", { enumerable: true, get: function() {
1111
+ return repo_errors_js_1.RepoNotFoundError;
1112
+ } });
1113
+ Object.defineProperty(exports, "RepoExistsError", { enumerable: true, get: function() {
1114
+ return repo_errors_js_1.RepoExistsError;
1115
+ } });
1116
+ Object.defineProperty(exports, "RepoAmbiguousError", { enumerable: true, get: function() {
1117
+ return repo_errors_js_1.RepoAmbiguousError;
1118
+ } });
1119
+ var forge_search_client_js_1 = require_forge_search_client();
1120
+ Object.defineProperty(exports, "ForgeSearchClient", { enumerable: true, get: function() {
1121
+ return forge_search_client_js_1.ForgeSearchClient;
1122
+ } });
1123
+ var session_store_js_1 = require_session_store();
1124
+ Object.defineProperty(exports, "SessionStoreManager", { enumerable: true, get: function() {
1125
+ return session_store_js_1.SessionStoreManager;
1126
+ } });
1127
+ var session_list_js_1 = require_session_list();
1128
+ Object.defineProperty(exports, "sessionList", { enumerable: true, get: function() {
1129
+ return session_list_js_1.sessionList;
1130
+ } });
1131
+ var session_cleanup_js_1 = require_session_cleanup();
1132
+ Object.defineProperty(exports, "sessionCleanup", { enumerable: true, get: function() {
1133
+ return session_cleanup_js_1.sessionCleanup;
1134
+ } });
1135
+ var gc_js_1 = require_gc();
1136
+ Object.defineProperty(exports, "classifyWorktree", { enumerable: true, get: function() {
1137
+ return gc_js_1.classifyWorktree;
1138
+ } });
1139
+ Object.defineProperty(exports, "isCleanTree", { enumerable: true, get: function() {
1140
+ return gc_js_1.isCleanTree;
1141
+ } });
1142
+ Object.defineProperty(exports, "getWorktreeTreeState", { enumerable: true, get: function() {
1143
+ return gc_js_1.getWorktreeTreeState;
1144
+ } });
1145
+ Object.defineProperty(exports, "worktreeGc", { enumerable: true, get: function() {
1146
+ return gc_js_1.worktreeGc;
1147
+ } });
1148
+ Object.defineProperty(exports, "cloneGc", { enumerable: true, get: function() {
1149
+ return gc_js_1.cloneGc;
1150
+ } });
1151
+ }
1152
+ });
2
1153
 
3
1154
  // src/index.ts
4
- import { Command as Command14 } from "commander";
5
- import chalk14 from "chalk";
1155
+ import { Command as Command16 } from "commander";
1156
+ import chalk15 from "chalk";
6
1157
 
7
1158
  // src/commands/setup.ts
8
- import { Command as Command2 } from "commander";
9
- import chalk2 from "chalk";
1159
+ import { Command as Command3 } from "commander";
1160
+ import chalk3 from "chalk";
10
1161
  import ora2 from "ora";
11
1162
  import { execSync } from "child_process";
12
1163
  import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
@@ -18,6 +1169,7 @@ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as
18
1169
  import { resolve, join as pathJoin, relative } from "path";
19
1170
  import { homedir as homedir2 } from "os";
20
1171
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1172
+ import { z } from "zod";
21
1173
 
22
1174
  // src/lib/constants.ts
23
1175
  import { homedir } from "os";
@@ -52,7 +1204,7 @@ var DEFAULT_PORTS = {
52
1204
  vault_router: 8050,
53
1205
  // internal routing layer
54
1206
  ui: 8400,
55
- // reader — Horus Reader SPA
1207
+ // horus-ui — Horus unified UI
56
1208
  forge: 8200,
57
1209
  typesense: 8108,
58
1210
  // Typesense search engine
@@ -64,11 +1216,7 @@ var DEFAULT_PORTS = {
64
1216
  var DEFAULT_DATA_DIR = join(homedir(), "Horus", "data");
65
1217
  var SERVICES = [
66
1218
  "anvil",
67
- "vault-router",
68
- // replaces 'vault'
69
- "vault-mcp",
70
- "forge",
71
- "reader",
1219
+ "horus-ui",
72
1220
  "typesense",
73
1221
  "neo4j"
74
1222
  ];
@@ -88,8 +1236,12 @@ function defaultConfig() {
88
1236
  search: {
89
1237
  api_key: "horus-local-key"
90
1238
  },
1239
+ control_plane_url: "",
1240
+ token_provider: { kind: "", config: "" },
91
1241
  ai: {
92
- key: ""
1242
+ key: "",
1243
+ anthropic_api_key: "",
1244
+ model: "claude-sonnet-4-6"
93
1245
  },
94
1246
  vaults: {},
95
1247
  github_hosts: {},
@@ -101,6 +1253,18 @@ function defaultConfig() {
101
1253
  function ensureHorusDir() {
102
1254
  mkdirSync(HORUS_DIR, { recursive: true });
103
1255
  }
1256
+ function ensureFsLayout() {
1257
+ ensureHorusDir();
1258
+ const dirs = {
1259
+ providers: pathJoin(HORUS_DIR, "providers"),
1260
+ logs: pathJoin(HORUS_DIR, "logs"),
1261
+ keys: pathJoin(HORUS_DIR, "keys")
1262
+ };
1263
+ for (const d of Object.values(dirs)) {
1264
+ mkdirSync(d, { recursive: true });
1265
+ }
1266
+ return dirs;
1267
+ }
104
1268
  function configExists() {
105
1269
  return existsSync2(CONFIG_PATH);
106
1270
  }
@@ -155,16 +1319,68 @@ function buildConfigFromParsed(parsed) {
155
1319
  search: {
156
1320
  api_key: parsed.search?.api_key ?? defaults.search.api_key
157
1321
  },
1322
+ control_plane_url: parsed.control_plane_url ?? defaults.control_plane_url,
1323
+ token_provider: {
1324
+ kind: parsed.token_provider?.kind ?? defaults.token_provider.kind,
1325
+ config: parsed.token_provider?.config ?? defaults.token_provider.config
1326
+ },
158
1327
  ai: {
159
- key: parsed.ai?.key ?? defaults.ai.key
1328
+ key: parsed.ai?.key ?? defaults.ai.key,
1329
+ anthropic_api_key: parsed.ai?.anthropic_api_key ?? defaults.ai.anthropic_api_key,
1330
+ model: parsed.ai?.model ?? defaults.ai.model
160
1331
  },
161
1332
  vaults: parsed.vaults ?? defaults.vaults,
162
1333
  github_hosts: parsed.github_hosts ?? defaults.github_hosts,
163
1334
  host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
164
1335
  host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs,
165
- enable_ui: parsed.enable_ui ?? defaults.enable_ui
1336
+ enable_ui: parsed.enable_ui ?? defaults.enable_ui,
1337
+ registry_git_url: parsed.registry_git_url,
1338
+ registry_deploy_key: parsed.registry_deploy_key,
1339
+ enterprise_registry_url: parsed.enterprise_registry_url
166
1340
  };
167
1341
  }
1342
+ var preprovisionedSchema = z.object({
1343
+ version: z.string().optional(),
1344
+ data_dir: z.string().optional(),
1345
+ runtime: z.enum(["docker", "podman"]).optional(),
1346
+ control_plane_url: z.string().optional(),
1347
+ token_provider: z.object({ kind: z.string(), config: z.string() }).optional(),
1348
+ ports: z.record(z.number()).optional(),
1349
+ repos: z.object({ anvil_notes: z.string().optional(), forge_registry: z.string().optional() }).optional(),
1350
+ vaults: z.record(z.any()).optional(),
1351
+ github_hosts: z.record(z.any()).optional(),
1352
+ ai: z.object({
1353
+ key: z.string().optional(),
1354
+ anthropic_api_key: z.string().optional(),
1355
+ model: z.string().optional()
1356
+ }).optional()
1357
+ }).passthrough();
1358
+ function loadPreprovisionedConfig(path2) {
1359
+ if (!existsSync2(path2)) {
1360
+ throw new Error(`Pre-provisioned config not found at ${path2}`);
1361
+ }
1362
+ const raw = readFileSync2(path2, "utf-8");
1363
+ let parsed;
1364
+ try {
1365
+ parsed = parseYaml(raw);
1366
+ } catch (e) {
1367
+ throw new Error(`Pre-provisioned config is not valid YAML: ${e.message}`);
1368
+ }
1369
+ const result = preprovisionedSchema.safeParse(parsed);
1370
+ if (!result.success) {
1371
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1372
+ throw new Error(`Pre-provisioned config failed validation:
1373
+ ${issues}`);
1374
+ }
1375
+ return buildConfigFromParsed(result.data);
1376
+ }
1377
+ function detectPreprovisionedConfig() {
1378
+ const candidates = [
1379
+ pathJoin(process.cwd(), "config.yaml.preprovisioned"),
1380
+ pathJoin(HORUS_DIR, "config.yaml.preprovisioned")
1381
+ ];
1382
+ return candidates.find((p) => existsSync2(p)) ?? null;
1383
+ }
168
1384
  function saveConfig(config) {
169
1385
  ensureHorusDir();
170
1386
  const yaml = stringifyYaml(config, { lineWidth: 0 });
@@ -215,6 +1431,7 @@ function discoverRepoDirs(rootDir, maxDepth = 4) {
215
1431
  }
216
1432
  function generateEnv(config) {
217
1433
  const dataDir = resolvePath(config.data_dir);
1434
+ const providersPath = pathJoin(HORUS_DIR, "providers");
218
1435
  const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
219
1436
  const baseScanPath = "/data/repos";
220
1437
  let forgeScanPaths;
@@ -256,12 +1473,22 @@ function generateEnv(config) {
256
1473
  "# Search",
257
1474
  `TYPESENSE_API_KEY=${config.search.api_key}`,
258
1475
  "",
259
- "# AI (required for NLP agent search in the Reader)",
1476
+ "# Control plane (empty HORUS_CONTROL_PLANE_URL => local-only mode)",
1477
+ `HORUS_CONTROL_PLANE_URL=${config.control_plane_url ?? ""}`,
1478
+ `TOKEN_PROVIDER_KIND=${config.token_provider?.kind ?? ""}`,
1479
+ `TOKEN_PROVIDER_CONFIG=${config.token_provider?.config ?? ""}`,
1480
+ "",
1481
+ "# Agent chat (Anthropic). Chat surface is disabled when the key is empty.",
1482
+ `HORUS_ANTHROPIC_API_KEY=${config.ai.anthropic_api_key}`,
1483
+ `HORUS_AGENT_MODEL=${config.ai.model}`,
1484
+ "# Legacy Cursor key \u2014 retained for back-compat, unused by the alpha client.",
260
1485
  `HORUS_AI_KEY=${config.ai.key}`,
261
1486
  "",
262
- "# Repository URLs (must be HTTPS \u2014 container services do not have SSH keys)",
1487
+ "# Providers mount (read-only secrets/config dir for horus-ui)",
1488
+ `HORUS_PROVIDERS_PATH=${providersPath}`,
1489
+ "",
1490
+ "# Anvil notes repo (must be HTTPS \u2014 container services do not have SSH keys)",
263
1491
  `ANVIL_REPO_URL=${config.repos.anvil_notes}`,
264
- `FORGE_REGISTRY_REPO_URL=${config.repos.forge_registry}`,
265
1492
  ""
266
1493
  ];
267
1494
  return lines.join("\n");
@@ -286,6 +1513,11 @@ var CONFIG_KEYS = [
286
1513
  "repo.forge-registry",
287
1514
  "search.api-key",
288
1515
  "ai.key",
1516
+ "ai.anthropic-key",
1517
+ "ai.model",
1518
+ "control-plane-url",
1519
+ "token-provider-kind",
1520
+ "token-provider-config",
289
1521
  "enable-ui"
290
1522
  ];
291
1523
  function getConfigValue(config, key) {
@@ -318,6 +1550,16 @@ function getConfigValue(config, key) {
318
1550
  return config.search.api_key;
319
1551
  case "ai.key":
320
1552
  return config.ai.key;
1553
+ case "ai.anthropic-key":
1554
+ return config.ai.anthropic_api_key;
1555
+ case "ai.model":
1556
+ return config.ai.model;
1557
+ case "control-plane-url":
1558
+ return config.control_plane_url ?? "";
1559
+ case "token-provider-kind":
1560
+ return config.token_provider?.kind ?? "";
1561
+ case "token-provider-config":
1562
+ return config.token_provider?.config ?? "";
321
1563
  case "enable-ui":
322
1564
  return String(config.enable_ui);
323
1565
  }
@@ -370,6 +1612,21 @@ function setConfigValue(config, key, value) {
370
1612
  case "ai.key":
371
1613
  updated.ai = { ...updated.ai, key: value };
372
1614
  break;
1615
+ case "ai.anthropic-key":
1616
+ updated.ai = { ...updated.ai, anthropic_api_key: value };
1617
+ break;
1618
+ case "ai.model":
1619
+ updated.ai = { ...updated.ai, model: value };
1620
+ break;
1621
+ case "control-plane-url":
1622
+ updated.control_plane_url = value;
1623
+ break;
1624
+ case "token-provider-kind":
1625
+ updated.token_provider = { kind: value, config: updated.token_provider?.config ?? "" };
1626
+ break;
1627
+ case "token-provider-config":
1628
+ updated.token_provider = { kind: updated.token_provider?.kind ?? "", config: value };
1629
+ break;
373
1630
  case "enable-ui":
374
1631
  if (value !== "true" && value !== "false") {
375
1632
  throw new Error(`Invalid value for enable-ui: ${value}. Must be "true" or "false".`);
@@ -596,7 +1853,7 @@ Run '${runtime.name} compose logs <service>' from ~/Horus/ to investigate.`
596
1853
  Run '${runtime.name} compose logs' from ~/Horus/ to investigate.`
597
1854
  );
598
1855
  }
599
- await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
1856
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
600
1857
  }
601
1858
  }
602
1859
 
@@ -653,67 +1910,6 @@ var ANVIL_SERVICE = ` # \u2500\u2500 Anvil \u2500\u2500\u2500\u2500\u2500\u2500
653
1910
  timeout: 5s
654
1911
  start_period: 60s
655
1912
  retries: 3`;
656
- var FORGE_SERVICE = ` # \u2500\u2500 Forge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
657
- # Workspace manager and package registry MCP server.
658
- forge:
659
- image: ghcr.io/arjunkhera/horus/forge:latest
660
- ports:
661
- - "\${FORGE_PORT:-8200}:8200"
662
- volumes:
663
- - \${HORUS_DATA_PATH}/config:/data/config:rw
664
- - \${HORUS_DATA_PATH}/registry:/data/registry:rw
665
- - \${HORUS_DATA_PATH}/workspaces:/data/workspaces:rw
666
- - \${HORUS_DATA_PATH}/repos:/data/horus-repos:rw
667
- - \${HORUS_DATA_PATH}/sessions:/data/sessions:rw
668
- - \${HOST_REPOS_PATH}:/data/repos:ro
669
- environment:
670
- - HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
671
- - FORGE_PORT=8200
672
- - FORGE_HOST=0.0.0.0
673
- - FORGE_REGISTRY_PATH=/data/registry
674
- - FORGE_WORKSPACES_PATH=/data/workspaces
675
- - FORGE_CONFIG_PATH=/data/config
676
- - FORGE_MANAGED_REPOS_PATH=/data/horus-repos
677
- - FORGE_REGISTRY_REPO_URL=\${FORGE_REGISTRY_REPO_URL:-}
678
- - FORGE_SYNC_INTERVAL=\${FORGE_SYNC_INTERVAL:-300}
679
- - FORGE_ANVIL_URL=http://anvil:8100
680
- - FORGE_VAULT_URL=http://vault-mcp:8300
681
- - FORGE_HOST_WORKSPACES_PATH=\${HORUS_DATA_PATH}/workspaces
682
- - FORGE_HOST_MANAGED_REPOS_PATH=\${HORUS_DATA_PATH}/repos
683
- - FORGE_HOST_REPOS_PATH=\${HOST_REPOS_PATH}
684
- - FORGE_HOST_MANAGED_REPOS_PATH=\${HORUS_DATA_PATH}/repos
685
- - FORGE_HOST_ANVIL_URL=http://localhost:\${ANVIL_PORT:-8100}
686
- - FORGE_HOST_VAULT_URL=http://localhost:\${VAULT_MCP_PORT:-8300}
687
- - FORGE_HOST_FORGE_URL=http://localhost:\${FORGE_PORT:-8200}
688
- - FORGE_SCAN_PATHS=\${FORGE_SCAN_PATHS:-/data/repos}
689
- - FORGE_SESSION_TTL_MS=\${FORGE_SESSION_TTL_MS:-1800000}
690
- - GITHUB_TOKEN=\${GITHUB_TOKEN:-}
691
- - TYPESENSE_HOST=typesense
692
- - TYPESENSE_PORT=8108
693
- - TYPESENSE_API_KEY=\${TYPESENSE_API_KEY:-horus-local-key}
694
- depends_on:
695
- anvil:
696
- condition: service_healthy
697
- typesense:
698
- condition: service_healthy
699
- vault-router:
700
- condition: service_healthy
701
- networks:
702
- - horus-net
703
- restart: unless-stopped
704
- stop_grace_period: 15s
705
- deploy:
706
- resources:
707
- limits:
708
- memory: 512m
709
- reservations:
710
- memory: 128m
711
- healthcheck:
712
- test: ["CMD", "curl", "-f", "http://localhost:8200/health"]
713
- interval: 30s
714
- timeout: 5s
715
- start_period: 60s
716
- retries: 3`;
717
1913
  var NEO4J_SERVICE = ` # \u2500\u2500 Neo4j \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
718
1914
  # Graph database for relationship-aware knowledge queries.
719
1915
  neo4j:
@@ -767,21 +1963,33 @@ var TYPESENSE_SERVICE = ` # \u2500\u2500 Typesense \u2500\u2500\u2500\u2500\u25
767
1963
  retries: 3
768
1964
  start_period: 5s
769
1965
  restart: unless-stopped`;
770
- var READER_SERVICE = ` # \u2500\u2500 Reader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
771
- # Horus Reader \u2014 Express server + NLP agent search at port 8400.
772
- # Serves Reader SPA, proxies /api/* to Anvil, hosts POST /api/ai/ask.
773
- reader:
774
- image: ghcr.io/arjunkhera/horus/reader:latest
1966
+ var HORUS_UI_SERVICE = ` # \u2500\u2500 Horus UI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1967
+ # Unified Horus client. Serves the SPA, proxies /api/anvil/* to the local
1968
+ # Anvil and (when connected) vault/forge/admin to the control plane, and hosts
1969
+ # the agent chat at POST /api/ai/ask. Boots first \u2014 no depends_on. Connected
1970
+ # vs local-only mode is config-driven: an empty HORUS_CONTROL_PLANE_URL is
1971
+ # local-only.
1972
+ horus-ui:
1973
+ image: ghcr.io/arjunkhera/horus/ui:latest
775
1974
  ports:
776
1975
  - "\${UI_PORT:-8400}:8400"
777
1976
  environment:
778
- - CURSOR_API_KEY=\${HORUS_AI_KEY}
779
- - HORUS_AGENT_MODEL=\${HORUS_AGENT_MODEL:-composer-2}
1977
+ - HORUS_CONTROL_PLANE_URL=\${HORUS_CONTROL_PLANE_URL:-}
1978
+ - TOKEN_PROVIDER_KIND=\${TOKEN_PROVIDER_KIND:-}
1979
+ - TOKEN_PROVIDER_CONFIG=\${TOKEN_PROVIDER_CONFIG:-}
1980
+ - HORUS_ANTHROPIC_API_KEY=\${HORUS_ANTHROPIC_API_KEY:-}
1981
+ - HORUS_AGENT_MODEL=\${HORUS_AGENT_MODEL:-claude-sonnet-4-6}
780
1982
  - ANVIL_HOST=anvil
781
1983
  - ANVIL_PORT=8100
782
- depends_on:
783
- anvil:
784
- condition: service_healthy
1984
+ - FORGE_CONFIG_PATH=/horus/config
1985
+ - FORGE_SESSIONS_ROOT=/horus/sessions
1986
+ - FORGE_MANAGED_REPOS_PATH=/horus/repos
1987
+ - FORGE_WORKSPACES_PATH=/horus/workspaces
1988
+ volumes:
1989
+ - \${HORUS_PROVIDERS_PATH}:/horus-providers:ro
1990
+ - \${HORUS_DATA_PATH}/sessions:/horus/sessions:rw
1991
+ - \${HORUS_DATA_PATH}/repos:/horus/repos:rw
1992
+ - \${HORUS_DATA_PATH}/config:/horus/config:rw
785
1993
  networks:
786
1994
  - horus-net
787
1995
  restart: unless-stopped
@@ -789,9 +1997,9 @@ var READER_SERVICE = ` # \u2500\u2500 Reader \u2500\u2500\u2500\u2500\u2500\u25
789
1997
  deploy:
790
1998
  resources:
791
1999
  limits:
792
- memory: 256m
2000
+ memory: 512m
793
2001
  reservations:
794
- memory: 128m
2002
+ memory: 256m
795
2003
  healthcheck:
796
2004
  test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8400/health"]
797
2005
  interval: 30s
@@ -854,7 +2062,7 @@ services:
854
2062
  volumes:
855
2063
  - "\${TEST_DATA_PATH:-/tmp/horus-test}/typesense-data:/data"
856
2064
 
857
- reader:
2065
+ horus-ui:
858
2066
  ports:
859
2067
  - "\${TEST_PORT_UI:-9260}:8400"
860
2068
 
@@ -865,152 +2073,37 @@ services:
865
2073
  volumes:
866
2074
  - "\${TEST_DATA_PATH:-/tmp/horus-test}/neo4j-data:/data"
867
2075
  - "\${TEST_DATA_PATH:-/tmp/horus-test}/neo4j-logs:/logs"
868
- `;
869
- }
870
- function generateComposeFile(config, runtime) {
871
- const vaultEntries = Object.entries(config.vaults).sort(([a], [b]) => a.localeCompare(b));
872
- const vaultServices = vaultEntries.map(([name, vault], index) => {
873
- const hostPort = `800${index + 1}`;
874
- const envVarName = `VAULT_REST_PORT_${name.toUpperCase().replace(/-/g, "_")}`;
875
- const githubHost = resolveGitHubHost(vault.repo, config.github_hosts);
876
- const token = githubHost?.token ?? "";
877
- const apiHost = githubHost?.host ?? "github.com";
878
- return ` # \u2500\u2500 Vault: ${name} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
879
- vault-${name}:
880
- image: ghcr.io/arjunkhera/horus/vault:latest
2076
+ ${config.registry_git_url ? `
2077
+ forge-registry:
881
2078
  ports:
882
- - "\${${envVarName}:-${hostPort}}:8000"
2079
+ - "\${TEST_PORT_FORGE_REGISTRY:-9270}:8744"
883
2080
  volumes:
884
- - \${HORUS_DATA_PATH}/vaults/${name}:/data/knowledge-repo:rw
885
- - vault-${name}-workspace:/data/workspace
886
- environment:
887
- - HORUS_RUNTIME=\${HORUS_RUNTIME:-docker}
888
- - KNOWLEDGE_REPO_PATH=/data/knowledge-repo
889
- - WORKSPACE_PATH=/data/workspace
890
- - VAULT_KNOWLEDGE_REPO_URL=${vault.repo}
891
- - SYNC_INTERVAL=\${VAULT_SYNC_INTERVAL:-300}
892
- - VAULT_SYNC_INTERVAL=\${VAULT_SYNC_INTERVAL:-300}
893
- - LOG_LEVEL=\${LOG_LEVEL:-info}
894
- - HOST=0.0.0.0
895
- - PORT=8000
896
- - GITHUB_TOKEN=${token}
897
- - GITHUB_API_HOST=${apiHost}
898
- - TYPESENSE_HOST=typesense
899
- - TYPESENSE_PORT=8108
900
- - TYPESENSE_API_KEY=\${TYPESENSE_API_KEY:-horus-local-key}
901
- - NEO4J_URI=bolt://neo4j:7687
902
- - NEO4J_USER=neo4j
903
- - NEO4J_PASSWORD=horus-neo4j
904
- depends_on:
905
- typesense:
906
- condition: service_healthy
907
- neo4j:
908
- condition: service_healthy
909
- networks:
910
- - horus-net
911
- restart: unless-stopped
912
- deploy:
913
- resources:
914
- limits:
915
- memory: 512m
916
- reservations:
917
- memory: 256m
918
- healthcheck:
919
- test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
920
- interval: 30s
921
- timeout: 10s
922
- start_period: 60s
923
- retries: 3`;
924
- });
925
- const defaultVaultEntry = vaultEntries.find(([, v]) => v.default);
926
- const defaultVaultName = defaultVaultEntry ? defaultVaultEntry[0] : vaultEntries[0]?.[0] ?? "";
927
- const vaultEndpoints = vaultEntries.map(([name]) => `${name}=http://vault-${name}:8000`).join(",");
928
- const vaultRouterDependsOn = vaultEntries.map(([name]) => ` vault-${name}:
929
- condition: service_healthy`).join("\n");
930
- const vaultRouterService = ` # \u2500\u2500 Vault Router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
931
- # Routes requests to the appropriate vault instance by name.
932
- vault-router:
933
- image: ghcr.io/arjunkhera/horus/vault-router:latest
934
- ports:
935
- - "\${VAULT_ROUTER_PORT:-8050}:8400"
936
- environment:
937
- ${vaultEndpoints ? ` - VAULT_ENDPOINTS=${vaultEndpoints}
938
- ` : ""} - VAULT_DEFAULT=${defaultVaultName}
939
- ${vaultRouterDependsOn ? ` depends_on:
940
- ${vaultRouterDependsOn}
941
- ` : ""} networks:
942
- - horus-net
943
- restart: unless-stopped
944
- deploy:
945
- resources:
946
- limits:
947
- memory: 256m
948
- reservations:
949
- memory: 64m
950
- healthcheck:
951
- test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8400/health')"]
952
- interval: 30s
953
- timeout: 10s
954
- start_period: 30s
955
- retries: 3`;
956
- const vaultMcpService = ` # \u2500\u2500 Vault MCP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
957
- # Thin MCP adapter that translates MCP tool calls to Vault REST API calls.
958
- vault-mcp:
959
- image: ghcr.io/arjunkhera/horus/vault-mcp:latest
960
- ports:
961
- - "\${VAULT_MCP_PORT:-8300}:8300"
962
- environment:
963
- - VAULT_MCP_HTTP=true
964
- - VAULT_MCP_PORT=8300
965
- - VAULT_MCP_HOST=0.0.0.0
966
- - KNOWLEDGE_SERVICE_URL=http://vault-router:8400
967
- depends_on:
968
- vault-router:
969
- condition: service_healthy
970
- networks:
971
- - horus-net
972
- restart: unless-stopped
973
- stop_grace_period: 15s
974
- deploy:
975
- resources:
976
- limits:
977
- memory: 256m
978
- reservations:
979
- memory: 64m
980
- healthcheck:
981
- test: ["CMD", "curl", "-f", "http://localhost:8300/health"]
982
- interval: 30s
983
- timeout: 5s
984
- start_period: 30s
985
- retries: 3`;
986
- const vaultVolumeEntries = [
987
- ...vaultEntries.map(([name]) => ` vault-${name}-workspace:`),
988
- " neo4j-data:",
989
- " neo4j-logs:"
990
- ].join("\n");
2081
+ - "\${TEST_DATA_PATH:-/tmp/horus-test}/registry:/data/registry:rw"
2082
+ ` : ""}`;
2083
+ }
2084
+ function generateComposeFile(_config, runtime) {
991
2085
  const sections = [
992
2086
  "# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
993
2087
  "# Horus \u2014 Generated Docker Compose",
994
2088
  "# Managed by @arkhera30/cli. Do not edit manually.",
995
2089
  "# Generated dynamically from ~/Horus/config.yaml by `horus setup`.",
2090
+ "#",
2091
+ "# Alpha client topology (\xA7C): horus-ui, anvil, typesense, neo4j only.",
2092
+ "# Vault and Forge are remote behind the control plane. Connected vs",
2093
+ "# local-only mode is config-driven (empty HORUS_CONTROL_PLANE_URL =",
2094
+ "# local-only), NOT compose-driven \u2014 the service set is identical in both.",
996
2095
  "# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
997
2096
  "",
998
2097
  "services:",
999
2098
  "",
1000
- ANVIL_SERVICE,
2099
+ HORUS_UI_SERVICE,
1001
2100
  "",
1002
- ...vaultServices.map((s) => s + "\n"),
1003
- vaultRouterService,
1004
- "",
1005
- vaultMcpService,
2101
+ ANVIL_SERVICE,
1006
2102
  "",
1007
- FORGE_SERVICE,
2103
+ TYPESENSE_SERVICE,
1008
2104
  "",
1009
2105
  NEO4J_SERVICE,
1010
2106
  "",
1011
- TYPESENSE_SERVICE,
1012
- "",
1013
- ...config.enable_ui !== false ? [READER_SERVICE, ""] : [],
1014
2107
  "# \u2500\u2500 Networks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
1015
2108
  "networks:",
1016
2109
  " horus-net:",
@@ -1018,7 +2111,8 @@ ${vaultRouterDependsOn}
1018
2111
  "",
1019
2112
  "# \u2500\u2500 Volumes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
1020
2113
  "volumes:",
1021
- vaultVolumeEntries
2114
+ " neo4j-data:",
2115
+ " neo4j-logs:"
1022
2116
  ];
1023
2117
  let content = sections.join("\n");
1024
2118
  if (runtime === "podman") {
@@ -1432,10 +2526,13 @@ function buildClaudeDesktopServers(config, host) {
1432
2526
  const npxPath = detectNpxPath();
1433
2527
  const npxDir = npxPath === "npx" ? "/usr/local/bin" : npxPath.substring(0, npxPath.lastIndexOf("/"));
1434
2528
  const envPath = `${npxDir}:/usr/local/bin:/usr/bin:/bin`;
2529
+ const connectedMode = !!(config.control_plane_url && config.control_plane_url.trim());
2530
+ const vaultUrl = connectedMode ? `http://${host}:${config.ports.ui}/vault/mcp` : `http://${host}:${config.ports.vault_mcp}/mcp`;
2531
+ const forgeUrl = connectedMode ? `http://${host}:${config.ports.ui}/forge/mcp` : `http://${host}:${config.ports.forge}/mcp`;
1435
2532
  return {
1436
2533
  anvil: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.anvil}/mcp`], env: { PATH: envPath } },
1437
- vault: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.vault_mcp}/mcp`], env: { PATH: envPath } },
1438
- forge: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.forge}/mcp`], env: { PATH: envPath } }
2534
+ vault: { command: npxPath, args: ["mcp-remote", vaultUrl], env: { PATH: envPath } },
2535
+ forge: { command: npxPath, args: ["mcp-remote", forgeUrl], env: { PATH: envPath } }
1439
2536
  };
1440
2537
  }
1441
2538
  async function isClaudeCliAvailable() {
@@ -1529,7 +2626,12 @@ function printNextSteps(targets) {
1529
2626
  console.log("");
1530
2627
  }
1531
2628
  async function runConnect(config, runtime, targets, host = "localhost") {
1532
- const httpServers = {
2629
+ const connectedMode = !!(config.control_plane_url && config.control_plane_url.trim());
2630
+ const httpServers = connectedMode ? {
2631
+ anvil: { url: `http://${host}:${config.ports.anvil}/mcp` },
2632
+ vault: { url: `http://${host}:${config.ports.ui}/vault/mcp` },
2633
+ forge: { url: `http://${host}:${config.ports.ui}/forge/mcp` }
2634
+ } : {
1533
2635
  anvil: { url: `http://${host}:${config.ports.anvil}/mcp` },
1534
2636
  vault: { url: `http://${host}:${config.ports.vault_mcp}/mcp` },
1535
2637
  forge: { url: `http://${host}:${config.ports.forge}/mcp` }
@@ -1589,23 +2691,32 @@ async function runConnect(config, runtime, targets, host = "localhost") {
1589
2691
  }
1590
2692
  }
1591
2693
  if (targets.includes("claude-code")) {
1592
- const skillsSpinner = ora("Syncing horus-core skills...").start();
1593
- try {
1594
- await syncSkills(runtime);
1595
- skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
1596
- } catch (error) {
1597
- skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
1598
- console.log(chalk.dim(error.message));
2694
+ if (connectedMode) {
2695
+ console.warn(chalk.yellow("[connect] Skipping skills sync \u2014 connected mode has no local forge container."));
2696
+ console.log(chalk.dim(" TODO: fetch skills via the control plane when the skills API is available."));
2697
+ } else {
2698
+ const skillsSpinner = ora("Syncing horus-core skills...").start();
2699
+ try {
2700
+ await syncSkills(runtime);
2701
+ skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
2702
+ } catch (error) {
2703
+ skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
2704
+ console.log(chalk.dim(error.message));
2705
+ }
1599
2706
  }
1600
2707
  }
1601
2708
  if (targets.includes("cursor")) {
1602
- const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor...").start();
1603
- try {
1604
- await syncSkillsForCursor(runtime);
1605
- cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/ and skills to ~/.cursor/skills-cursor/");
1606
- } catch (error) {
1607
- cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
1608
- console.log(chalk.dim(error.message));
2709
+ if (connectedMode) {
2710
+ console.warn(chalk.yellow("[connect] Skipping Cursor rules sync \u2014 connected mode has no local forge container."));
2711
+ } else {
2712
+ const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor...").start();
2713
+ try {
2714
+ await syncSkillsForCursor(runtime);
2715
+ cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/ and skills to ~/.cursor/skills-cursor/");
2716
+ } catch (error) {
2717
+ cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
2718
+ console.log(chalk.dim(error.message));
2719
+ }
1609
2720
  }
1610
2721
  }
1611
2722
  if (configured.length > 0) {
@@ -1674,321 +2785,220 @@ var connectCommand = new Command("connect").description("Configure Claude/Cursor
1674
2785
  await runConnect(config, runtime, targets, opts.host);
1675
2786
  });
1676
2787
 
1677
- // src/commands/setup.ts
1678
- function injectToken(url, token) {
1679
- if (!token) return url;
1680
- try {
1681
- const parsed = new URL(url);
1682
- parsed.username = "oauth2";
1683
- parsed.password = token;
1684
- return parsed.toString();
1685
- } catch {
1686
- return url;
2788
+ // src/commands/login.ts
2789
+ import { Command as Command2 } from "commander";
2790
+ import chalk2 from "chalk";
2791
+ async function runLogin(config) {
2792
+ const cp = (config.control_plane_url ?? "").trim();
2793
+ if (!cp) {
2794
+ return { ok: true, deferred: false, message: "Local-only mode \u2014 no control plane to log into." };
1687
2795
  }
1688
- }
1689
- function extractHostname(url) {
2796
+ let reachable = false;
1690
2797
  try {
1691
- return new URL(url).hostname;
2798
+ const res = await fetch(cp.replace(/\/$/, "") + "/health", { signal: AbortSignal.timeout(8e3) });
2799
+ reachable = res.ok;
1692
2800
  } catch {
1693
- return "github.com";
2801
+ reachable = false;
1694
2802
  }
2803
+ if (!reachable) {
2804
+ return {
2805
+ ok: false,
2806
+ deferred: true,
2807
+ message: `Control plane at ${cp} is not reachable yet. Run \`horus login\` once it is available.`
2808
+ };
2809
+ }
2810
+ const kind = config.token_provider?.kind ?? "";
2811
+ if (kind === "static") {
2812
+ const hasToken = !!(config.token_provider?.config ?? "").trim();
2813
+ return hasToken ? { ok: true, deferred: false, message: "Static principal token configured \u2014 client is authenticated." } : {
2814
+ ok: false,
2815
+ deferred: true,
2816
+ message: "Static token provider selected but no token is set. Run `horus config set token-provider-config <token>`."
2817
+ };
2818
+ }
2819
+ return {
2820
+ ok: false,
2821
+ deferred: true,
2822
+ message: `Interactive login (${kind || "unconfigured"}) completes against the control plane. Run \`horus login\` once it is available.`
2823
+ };
1695
2824
  }
1696
- var setupCommand = new Command2("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-name <name>", "Vault name (can be specified multiple times)").option("--vault-repo <url>", "Vault knowledge-base repository URL (matches positionally with --vault-name)").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos (primary host)").option("--claude-desktop", "Configure Claude Desktop MCP servers during setup (non-interactive opt-in)").action(async (opts) => {
2825
+ var loginCommand = new Command2("login").description("Authenticate the client against the configured control plane").action(async () => {
2826
+ if (!configExists()) {
2827
+ console.log(chalk2.red("Horus is not configured yet. Run `horus setup` first."));
2828
+ process.exit(1);
2829
+ }
2830
+ const result = await runLogin(loadConfig());
2831
+ if (result.ok) {
2832
+ console.log(chalk2.green(result.message));
2833
+ } else {
2834
+ console.log(chalk2.yellow(result.message));
2835
+ }
2836
+ });
2837
+
2838
+ // src/commands/setup.ts
2839
+ var setupCommand = new Command3("setup").description("First-run setup for the Horus client").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--local-only", "Local-only mode: skip the control-plane / login prompts").option("--config <path>", "Provision from a pre-provisioned, zod-validated config.yaml bundle").option("--no-pull", "Skip pulling container images").option("--runtime <runtime>", "Container runtime to use: docker or podman").option("--data-dir <path>", "Data directory path").option("--anvil-repo <url>", "Anvil notes repository URL (HTTPS)").option("--control-plane <url>", "Control-plane base URL (connected mode)").option("--claude-desktop", "Configure Claude Desktop MCP servers during setup").action(async (opts) => {
1697
2840
  console.log("");
1698
- console.log(chalk2.bold("Horus Setup"));
1699
- console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2841
+ console.log(chalk3.bold("Horus Setup"));
2842
+ console.log(chalk3.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1700
2843
  console.log("");
1701
- if (configExists()) {
1702
- if (opts.yes) {
1703
- console.log(chalk2.yellow("Existing configuration found. Merging with existing values in non-interactive mode."));
1704
- } else {
1705
- const proceed = await confirm({
1706
- message: "Horus is already configured. Reconfigure?",
1707
- default: false
1708
- });
1709
- if (!proceed) {
1710
- console.log(chalk2.dim("Setup cancelled."));
1711
- return;
1712
- }
2844
+ const preprovisionedPath = opts.config ?? detectPreprovisionedConfig();
2845
+ const isPreprovisioned = !!preprovisionedPath;
2846
+ const isYes = !!opts.yes;
2847
+ const isLocalOnly = !!opts.localOnly;
2848
+ const interactive = !isPreprovisioned && !isYes;
2849
+ if (configExists() && interactive) {
2850
+ const proceed = await confirm({
2851
+ message: "Horus is already configured. Reconfigure? (existing data is preserved)",
2852
+ default: false
2853
+ });
2854
+ if (!proceed) {
2855
+ console.log(chalk3.dim("Setup cancelled."));
2856
+ return;
1713
2857
  }
1714
2858
  }
1715
2859
  const checkSpinner = ora2("Checking for container runtimes...").start();
1716
- const [hasDocker, hasPodman] = await Promise.all([
1717
- checkRuntime("docker"),
1718
- checkRuntime("podman")
1719
- ]);
2860
+ const [hasDocker, hasPodman] = await Promise.all([checkRuntime("docker"), checkRuntime("podman")]);
1720
2861
  checkSpinner.stop();
1721
2862
  const available = [
1722
2863
  ...hasDocker ? ["docker"] : [],
1723
2864
  ...hasPodman ? ["podman"] : []
1724
2865
  ];
1725
2866
  if (available.length === 0) {
1726
- console.log(chalk2.red("No container runtime found."));
2867
+ console.log(chalk3.red("No container runtime found."));
1727
2868
  console.log("");
1728
2869
  console.log("Horus requires Docker or Podman with the Compose plugin.");
1729
- console.log("");
1730
- console.log("Install one of:");
1731
2870
  console.log(" Docker Desktop: https://www.docker.com/products/docker-desktop/");
1732
2871
  console.log(" Podman Desktop: https://podman-desktop.io/");
1733
2872
  process.exit(1);
1734
2873
  }
2874
+ let config;
1735
2875
  let selectedRuntime;
1736
- if (opts.yes) {
1737
- const requested = opts.runtime;
1738
- if (requested && !available.includes(requested)) {
1739
- console.log(chalk2.red(`Requested runtime "${requested}" is not installed.`));
1740
- console.log(chalk2.dim(`Available: ${available.join(", ")}`));
2876
+ if (isPreprovisioned) {
2877
+ const loadSpinner = ora2(`Loading pre-provisioned config: ${preprovisionedPath}`).start();
2878
+ try {
2879
+ config = loadPreprovisionedConfig(preprovisionedPath);
2880
+ loadSpinner.succeed("Pre-provisioned config validated");
2881
+ } catch (err) {
2882
+ loadSpinner.fail("Pre-provisioned config is invalid");
2883
+ console.log(chalk3.red(err.message));
1741
2884
  process.exit(1);
1742
2885
  }
1743
- selectedRuntime = requested ?? available[0];
1744
- console.log(`Using ${chalk2.cyan(selectedRuntime)}`);
1745
- } else {
1746
- selectedRuntime = await select({
1747
- message: "Which container runtime would you like to use?",
1748
- choices: available.map((r) => ({
1749
- value: r,
1750
- name: r === "docker" ? "Docker" : "Podman"
1751
- }))
1752
- });
1753
- }
1754
- const runtime = await detectRuntime(selectedRuntime);
1755
- let config;
1756
- if (opts.yes) {
2886
+ const requested = opts.runtime ?? config.runtime;
2887
+ selectedRuntime = available.includes(requested) ? requested : available[0];
2888
+ } else if (isYes) {
1757
2889
  const existing = configExists() ? loadConfig() : null;
1758
2890
  const defaults = defaultConfig();
1759
- let vaults;
1760
- if (opts.vaultName || opts.vaultRepo) {
1761
- const vaultNames = opts.vaultName ? Array.isArray(opts.vaultName) ? opts.vaultName : [opts.vaultName] : ["default"];
1762
- const vaultRepos = opts.vaultRepo ? Array.isArray(opts.vaultRepo) ? opts.vaultRepo : [opts.vaultRepo] : [process.env.VAULT_KNOWLEDGE_REPO_URL ?? ""];
1763
- vaults = {};
1764
- vaultNames.forEach((name, i) => {
1765
- vaults[name] = {
1766
- repo: vaultRepos[i] ?? vaultRepos[0] ?? "",
1767
- default: i === 0
1768
- };
1769
- });
1770
- } else {
1771
- vaults = existing?.vaults ?? (process.env.VAULT_KNOWLEDGE_REPO_URL ? { default: { repo: process.env.VAULT_KNOWLEDGE_REPO_URL, default: true } } : defaults.vaults);
1772
- }
1773
- const primaryToken = opts.githubToken || process.env.GITHUB_TOKEN || "";
1774
- const anvilRepo = opts.anvilRepo || process.env.ANVIL_REPO_URL || existing?.repos.anvil_notes || defaults.repos.anvil_notes;
1775
- let github_hosts;
1776
- if (opts.githubToken || !existing || Object.keys(existing.github_hosts).length === 0) {
1777
- const allRepoUrls = [anvilRepo, ...Object.values(vaults).map((v) => v.repo)].filter(Boolean);
1778
- const seenHosts = /* @__PURE__ */ new Set();
1779
- github_hosts = {};
1780
- let hostIndex = 0;
1781
- for (const url of allRepoUrls) {
1782
- const hostname = extractHostname(url);
1783
- if (!seenHosts.has(hostname)) {
1784
- seenHosts.add(hostname);
1785
- const hostKey = hostIndex === 0 ? "default" : hostname;
1786
- github_hosts[hostKey] = {
1787
- host: hostname,
1788
- token: primaryToken
1789
- };
1790
- hostIndex++;
1791
- }
1792
- }
1793
- if (Object.keys(github_hosts).length === 0) {
1794
- github_hosts["default"] = { host: "github.com", token: primaryToken };
1795
- }
1796
- } else {
1797
- github_hosts = existing.github_hosts;
2891
+ const requested = opts.runtime;
2892
+ if (requested && !available.includes(requested)) {
2893
+ console.log(chalk3.red(`Requested runtime "${requested}" is not installed.`));
2894
+ process.exit(1);
1798
2895
  }
2896
+ selectedRuntime = requested ?? existing?.runtime ?? available[0];
2897
+ const controlPlane = opts.controlPlane || process.env.HORUS_CONTROL_PLANE_URL || existing?.control_plane_url || "";
1799
2898
  config = {
1800
2899
  ...defaults,
1801
- // Preserve all existing top-level fields first
1802
2900
  ...existing ?? {},
1803
- // Then apply runtime (always re-detected)
1804
- runtime: runtime.name,
1805
- // Apply field-level overrides: explicit flag > existing value > default
2901
+ runtime: selectedRuntime,
1806
2902
  data_dir: opts.dataDir || existing?.data_dir || DEFAULT_DATA_DIR,
1807
- host_repos_path: opts.reposPath || existing?.host_repos_path || "",
2903
+ control_plane_url: controlPlane,
2904
+ token_provider: {
2905
+ kind: process.env.TOKEN_PROVIDER_KIND || existing?.token_provider?.kind || (controlPlane ? "static" : ""),
2906
+ config: process.env.TOKEN_PROVIDER_CONFIG || existing?.token_provider?.config || ""
2907
+ },
1808
2908
  repos: {
1809
- anvil_notes: anvilRepo,
1810
- forge_registry: opts.forgeRepo || process.env.FORGE_REGISTRY_REPO_URL || existing?.repos.forge_registry || defaults.repos.forge_registry
2909
+ anvil_notes: opts.anvilRepo || process.env.ANVIL_REPO_URL || existing?.repos.anvil_notes || "",
2910
+ forge_registry: existing?.repos.forge_registry || ""
1811
2911
  },
1812
- vaults,
1813
- github_hosts,
1814
2912
  ai: {
1815
- key: process.env.HORUS_AI_KEY || existing?.ai.key || ""
2913
+ key: process.env.HORUS_AI_KEY || existing?.ai.key || "",
2914
+ anthropic_api_key: process.env.HORUS_ANTHROPIC_API_KEY || existing?.ai.anthropic_api_key || "",
2915
+ model: process.env.HORUS_AGENT_MODEL || existing?.ai.model || defaults.ai.model
1816
2916
  }
1817
2917
  };
1818
2918
  } else {
1819
- const data_dir = await input({
1820
- message: "Data directory:",
1821
- default: DEFAULT_DATA_DIR
1822
- });
1823
- const host_repos_path = await input({
1824
- message: "Host repos path (for Forge repo scanning, leave empty to skip):",
1825
- default: ""
1826
- });
1827
- const host_repos_extra_scan_dirs = [];
1828
- const customize_ports = await confirm({
1829
- message: "Customize port assignments?",
1830
- default: false
1831
- });
1832
- let ports = { ...DEFAULT_PORTS };
1833
- if (customize_ports) {
1834
- const anvil = await number({
1835
- message: "Anvil port:",
1836
- default: DEFAULT_PORTS.anvil
1837
- });
1838
- const vault_rest = await number({
1839
- message: "Vault REST port (per-vault instances):",
1840
- default: DEFAULT_PORTS.vault_rest
1841
- });
1842
- const vault_mcp = await number({
1843
- message: "Vault MCP port:",
1844
- default: DEFAULT_PORTS.vault_mcp
1845
- });
1846
- const vault_router = await number({
1847
- message: "Vault Router port:",
1848
- default: DEFAULT_PORTS.vault_router
1849
- });
1850
- const forge = await number({
1851
- message: "Forge port:",
1852
- default: DEFAULT_PORTS.forge
1853
- });
1854
- ports = {
1855
- anvil: anvil ?? DEFAULT_PORTS.anvil,
1856
- vault_rest: vault_rest ?? DEFAULT_PORTS.vault_rest,
1857
- vault_mcp: vault_mcp ?? DEFAULT_PORTS.vault_mcp,
1858
- vault_router: vault_router ?? DEFAULT_PORTS.vault_router,
1859
- ui: DEFAULT_PORTS.ui,
1860
- forge: forge ?? DEFAULT_PORTS.forge,
1861
- typesense: DEFAULT_PORTS.typesense,
1862
- neo4j_http: DEFAULT_PORTS.neo4j_http,
1863
- neo4j_bolt: DEFAULT_PORTS.neo4j_bolt
1864
- };
1865
- }
1866
- console.log("");
1867
- console.log(chalk2.bold("Repository Configuration"));
1868
- console.log(chalk2.dim("Horus stores notes and knowledge in Git repos you own."));
1869
- console.log(chalk2.dim("Create empty repos on your Git server, then paste the URLs below."));
1870
- console.log("");
1871
- console.log(chalk2.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
1872
- console.log(chalk2.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
1873
- console.log("");
1874
- const primaryHost = await input({
1875
- message: "Primary Git server hostname:",
1876
- default: "github.com"
1877
- });
1878
- const example = (repo) => chalk2.dim(` e.g., https://${primaryHost}/<owner>/${repo}`);
1879
- console.log("");
1880
- const anvil_notes = await input({
1881
- message: `Anvil notes repo URL:
1882
- ${example("horus-notes")}
1883
- `,
1884
- validate: (v) => v.trim().length > 0 || "Anvil needs a notes repo to store your data."
1885
- });
1886
- const forge_registry = await input({
1887
- message: `Forge registry repo URL:
1888
- ${example("forge-registry")}
1889
- `,
1890
- validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
2919
+ selectedRuntime = available.length === 1 ? available[0] : await select({
2920
+ message: "Which container runtime would you like to use?",
2921
+ choices: available.map((r) => ({ value: r, name: r === "docker" ? "Docker" : "Podman" }))
1891
2922
  });
1892
- console.log("");
1893
- console.log(chalk2.bold("Vault Configuration"));
1894
- console.log(chalk2.dim("Add one or more knowledge-base vaults. Each vault is a separate Git repo."));
1895
- console.log("");
1896
- const vaults = {};
1897
- let addingVaults = true;
1898
- let isFirstVault = true;
1899
- while (addingVaults) {
1900
- const vaultName = await input({
1901
- message: "Add vault name (e.g., personal):",
1902
- validate: (v) => {
1903
- const trimmed = v.trim();
1904
- if (!trimmed) return "Vault name cannot be empty.";
1905
- if (!/^[a-z0-9-]+$/.test(trimmed)) return "Vault name must be lowercase alphanumeric with hyphens only.";
1906
- if (trimmed in vaults) return `Vault "${trimmed}" already added.`;
1907
- return true;
2923
+ const defaults = defaultConfig();
2924
+ let control_plane_url = "";
2925
+ let token_provider = { kind: "", config: "" };
2926
+ if (!isLocalOnly) {
2927
+ control_plane_url = (await input({
2928
+ message: "Control-plane URL (leave empty for local-only):",
2929
+ default: opts.controlPlane ?? ""
2930
+ })).trim();
2931
+ if (control_plane_url) {
2932
+ const cpSpinner = ora2(`Checking control plane at ${control_plane_url}...`).start();
2933
+ try {
2934
+ const res = await fetch(control_plane_url.replace(/\/$/, "") + "/health", {
2935
+ signal: AbortSignal.timeout(8e3)
2936
+ });
2937
+ if (res.ok) cpSpinner.succeed("Control plane reachable");
2938
+ else cpSpinner.warn(`Control plane responded HTTP ${res.status} \u2014 continuing (login can be deferred)`);
2939
+ } catch {
2940
+ cpSpinner.warn("Control plane unreachable \u2014 continuing; run `horus login` later");
1908
2941
  }
1909
- });
1910
- const vaultRepo = await input({
1911
- message: `Vault repo URL:
1912
- ${example(`${vaultName.trim()}-knowledge`)}
1913
- `,
1914
- validate: (v) => v.trim().length > 0 || "Vault repo URL cannot be empty."
1915
- });
1916
- let isDefault = isFirstVault;
1917
- if (!isFirstVault) {
1918
- isDefault = await confirm({
1919
- message: `Is "${vaultName.trim()}" the default vault?`,
1920
- default: false
2942
+ const kind = await select({
2943
+ message: "Principal token provider:",
2944
+ choices: [
2945
+ { value: "static", name: "Static token (paste a token)" },
2946
+ { value: "oidc", name: "OIDC (issuer URL; login completes against the control plane)" },
2947
+ { value: "none", name: "None (anonymous / configure later)" }
2948
+ ]
1921
2949
  });
1922
- }
1923
- if (isDefault) {
1924
- for (const v of Object.values(vaults)) {
1925
- v.default = false;
2950
+ let providerConfig = "";
2951
+ if (kind === "static") {
2952
+ providerConfig = (await password({ message: "Principal token:", mask: "*" })).trim();
2953
+ } else if (kind === "oidc") {
2954
+ providerConfig = (await input({ message: "OIDC issuer URL:" })).trim();
1926
2955
  }
2956
+ token_provider = { kind, config: providerConfig };
1927
2957
  }
1928
- vaults[vaultName.trim()] = {
1929
- repo: vaultRepo.trim(),
1930
- default: isDefault || isFirstVault
1931
- };
1932
- isFirstVault = false;
1933
- addingVaults = await confirm({
1934
- message: "Add another vault?",
1935
- default: false
1936
- });
1937
- }
1938
- const defaultCount = Object.values(vaults).filter((v) => v.default).length;
1939
- if (defaultCount === 0 && Object.keys(vaults).length > 0) {
1940
- const firstKey = Object.keys(vaults)[0];
1941
- vaults[firstKey].default = true;
2958
+ } else {
2959
+ console.log(chalk3.dim("Local-only mode \u2014 skipping control-plane and login prompts."));
1942
2960
  }
1943
- console.log("");
1944
- console.log(chalk2.bold("Authentication"));
1945
- console.log(chalk2.dim("A personal access token is required per Git server for private repositories."));
1946
- console.log("");
1947
- const allRepoUrls = [anvil_notes.trim(), ...Object.values(vaults).map((v) => v.repo)].filter(Boolean);
1948
- const uniqueHostnames = [...new Set(allRepoUrls.map(extractHostname))];
1949
- const github_hosts = {};
1950
- for (let i = 0; i < uniqueHostnames.length; i++) {
1951
- const hostname = uniqueHostnames[i];
1952
- const token = await password({
1953
- message: `GitHub token for ${chalk2.cyan(hostname)} (leave empty to skip):`,
1954
- mask: "*"
1955
- });
1956
- const hostKey = i === 0 ? "default" : hostname;
1957
- github_hosts[hostKey] = {
1958
- host: hostname,
1959
- token: token.trim()
2961
+ const anvil_notes = (await input({
2962
+ message: "Anvil notes repo URL (HTTPS, optional \u2014 leave empty to start with an empty local notes dir):",
2963
+ default: opts.anvilRepo ?? ""
2964
+ })).trim();
2965
+ const data_dir = await input({ message: "Data directory:", default: opts.dataDir ?? DEFAULT_DATA_DIR });
2966
+ const anthropicKey = (await password({ message: "Anthropic API key for agent chat (leave empty to skip):", mask: "*" })).trim();
2967
+ let ports = { ...DEFAULT_PORTS };
2968
+ const customizePorts = await confirm({ message: "Customize port assignments?", default: false });
2969
+ if (customizePorts) {
2970
+ const anvil = await number({ message: "Anvil port:", default: DEFAULT_PORTS.anvil });
2971
+ const ui = await number({ message: "Horus UI port:", default: DEFAULT_PORTS.ui });
2972
+ const typesense = await number({ message: "Typesense port:", default: DEFAULT_PORTS.typesense });
2973
+ const neo4j_http = await number({ message: "Neo4j HTTP port:", default: DEFAULT_PORTS.neo4j_http });
2974
+ const neo4j_bolt = await number({ message: "Neo4j Bolt port:", default: DEFAULT_PORTS.neo4j_bolt });
2975
+ ports = {
2976
+ ...ports,
2977
+ anvil: anvil ?? DEFAULT_PORTS.anvil,
2978
+ ui: ui ?? DEFAULT_PORTS.ui,
2979
+ typesense: typesense ?? DEFAULT_PORTS.typesense,
2980
+ neo4j_http: neo4j_http ?? DEFAULT_PORTS.neo4j_http,
2981
+ neo4j_bolt: neo4j_bolt ?? DEFAULT_PORTS.neo4j_bolt
1960
2982
  };
1961
2983
  }
1962
- console.log("");
1963
- console.log(chalk2.bold("NLP Agent Search"));
1964
- console.log(chalk2.dim("Required for the \u2726 Ask bar in the Horus Reader."));
1965
- console.log("");
1966
- const aiKey = await password({
1967
- message: "Horus AI key (leave empty to configure later):",
1968
- mask: "*"
1969
- });
1970
2984
  config = {
1971
- ...defaultConfig(),
2985
+ ...defaults,
2986
+ runtime: selectedRuntime,
1972
2987
  data_dir,
1973
- host_repos_path,
1974
- host_repos_extra_scan_dirs,
1975
- runtime: runtime.name,
1976
2988
  ports,
1977
- repos: {
1978
- anvil_notes: anvil_notes.trim(),
1979
- forge_registry: forge_registry.trim()
1980
- },
1981
- vaults,
1982
- github_hosts,
1983
- ai: {
1984
- key: aiKey.trim()
1985
- }
2989
+ control_plane_url,
2990
+ token_provider,
2991
+ repos: { anvil_notes, forge_registry: "" },
2992
+ ai: { key: "", anthropic_api_key: anthropicKey, model: defaults.ai.model }
1986
2993
  };
1987
2994
  }
2995
+ const runtime = await detectRuntime(selectedRuntime);
2996
+ config.runtime = runtime.name;
1988
2997
  const configSpinner = ora2("Saving configuration...").start();
1989
2998
  try {
1990
2999
  saveConfig(config);
1991
- configSpinner.succeed("Configuration saved to ~/Horus/config.yaml");
3000
+ ensureFsLayout();
3001
+ configSpinner.succeed("Configuration saved to ~/Horus/config.yaml (providers/, logs/, keys/ ready)");
1992
3002
  } catch (error) {
1993
3003
  configSpinner.fail("Failed to save configuration");
1994
3004
  console.error(error.message);
@@ -2013,108 +3023,57 @@ ${example(`${vaultName.trim()}-knowledge`)}
2013
3023
  process.exit(1);
2014
3024
  }
2015
3025
  const dataDir = resolvePath(config.data_dir);
2016
- const anvilToken = resolveGitHubHost(config.repos.anvil_notes, config.github_hosts)?.token ?? "";
2017
- const forgeToken = resolveGitHubHost(config.repos.forge_registry, config.github_hosts)?.token ?? "";
2018
- const reposToClone = [
2019
- {
2020
- url: config.repos.anvil_notes,
2021
- dest: join3(dataDir, "notes"),
2022
- label: "Anvil notes",
2023
- token: anvilToken
2024
- },
2025
- {
2026
- url: config.repos.forge_registry,
2027
- dest: join3(dataDir, "registry"),
2028
- label: "Forge registry",
2029
- token: forgeToken
2030
- }
2031
- ].filter((r) => r.url);
2032
- for (const [name, vault] of Object.entries(config.vaults)) {
2033
- if (vault.repo) {
2034
- const vaultToken = resolveGitHubHost(vault.repo, config.github_hosts)?.token ?? "";
2035
- reposToClone.push({
2036
- url: vault.repo,
2037
- dest: join3(dataDir, "vaults", name),
2038
- label: `Vault: ${name}`,
2039
- token: vaultToken
2040
- });
2041
- }
2042
- }
2043
- if (reposToClone.length > 0) {
2044
- console.log("");
2045
- console.log(chalk2.bold("Cloning repositories..."));
2046
- mkdirSync3(dataDir, { recursive: true });
2047
- for (const repo of reposToClone) {
2048
- const spinner = ora2(`Cloning ${repo.label}...`).start();
2049
- if (existsSync5(join3(repo.dest, ".git"))) {
2050
- spinner.succeed(`${repo.label} already cloned`);
2051
- continue;
2052
- }
3026
+ mkdirSync3(dataDir, { recursive: true });
3027
+ const notesDest = join3(dataDir, "notes");
3028
+ if (config.repos.anvil_notes) {
3029
+ const spinner = ora2("Cloning Anvil notes...").start();
3030
+ if (existsSync5(join3(notesDest, ".git"))) {
3031
+ spinner.succeed("Anvil notes already cloned");
3032
+ } else {
2053
3033
  try {
2054
- mkdirSync3(repo.dest, { recursive: true });
2055
- const cloneUrl = injectToken(repo.url, repo.token);
2056
- execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
2057
- stdio: "pipe",
2058
- timeout: 6e4
2059
- });
2060
- spinner.succeed(`${repo.label} cloned`);
3034
+ mkdirSync3(notesDest, { recursive: true });
3035
+ execSync(`git clone "${config.repos.anvil_notes}" "${notesDest}"`, { stdio: "pipe", timeout: 6e4 });
3036
+ spinner.succeed("Anvil notes cloned");
2061
3037
  } catch (error) {
2062
- spinner.fail(`Failed to clone ${repo.label}`);
2063
- const msg = error.message || "";
2064
- if (msg.includes("already exists and is not an empty directory")) {
2065
- console.log(chalk2.dim(" Directory exists but has no .git \u2014 check the path."));
2066
- } else {
2067
- console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
2068
- }
2069
- console.log(chalk2.dim(` URL: ${repo.url}`));
2070
- if (!repo.token) {
2071
- console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
2072
- }
2073
- process.exit(1);
3038
+ spinner.fail("Failed to clone Anvil notes (continuing with an empty notes dir)");
3039
+ console.log(chalk3.dim(` ${(error.message || "").split("\n")[0]}`));
3040
+ console.log(chalk3.dim(` URL: ${config.repos.anvil_notes}`));
3041
+ mkdirSync3(notesDest, { recursive: true });
2074
3042
  }
2075
3043
  }
3044
+ } else {
3045
+ mkdirSync3(notesDest, { recursive: true });
2076
3046
  }
2077
- if (process.platform === "linux") {
2078
- const forgeDirs = ["config", "registry", "workspaces", "sessions"].map((d) => join3(dataDir, d));
2079
- for (const dir of forgeDirs) {
2080
- mkdirSync3(dir, { recursive: true });
2081
- }
2082
- const dirList = forgeDirs.map((d) => `"${d}"`).join(" ");
3047
+ if (opts.pull !== false) {
3048
+ console.log("");
3049
+ console.log(chalk3.bold("Pulling container images..."));
2083
3050
  try {
2084
- execSync(`chown -R 1001:1001 ${dirList}`, { stdio: "pipe" });
2085
- } catch (error) {
2086
- console.log(chalk2.yellow("Warning: could not chown Forge data dirs to UID 1001."));
2087
- console.log(chalk2.dim(" Forge may fail to start if directory ownership is incorrect."));
2088
- console.log(chalk2.dim(` Run manually: sudo chown -R 1001:1001 ${forgeDirs.join(" ")}`));
3051
+ await composeStreaming(runtime, runtime.name === "podman" ? ["pull"] : ["pull", "--ignore-pull-failures"]);
3052
+ } catch {
3053
+ console.log(chalk3.yellow("Some images could not be pulled \u2014 continuing."));
2089
3054
  }
3055
+ } else {
3056
+ console.log(chalk3.dim("Skipping image pull (--no-pull)."));
2090
3057
  }
2091
3058
  console.log("");
2092
- console.log(chalk2.bold("Pulling container images..."));
2093
- try {
2094
- await composeStreaming(runtime, runtime.name === "podman" ? ["pull"] : ["pull", "--ignore-pull-failures"]);
2095
- } catch {
2096
- console.log(chalk2.yellow("Some images could not be pulled."));
2097
- console.log(chalk2.dim("Continuing \u2014 services will be built from source if build contexts are available."));
2098
- }
2099
- console.log("");
2100
- console.log(chalk2.bold("Starting Horus services..."));
3059
+ console.log(chalk3.bold("Starting Horus services..."));
2101
3060
  try {
2102
3061
  await composeStreaming(runtime, ["up", "-d", "--remove-orphans"]);
2103
3062
  } catch (error) {
2104
- console.log(chalk2.red("Failed to start services."));
2105
- console.log(chalk2.dim(error.message));
3063
+ console.log(chalk3.red("Failed to start services."));
3064
+ console.log(chalk3.dim(error.message));
2106
3065
  process.exit(1);
2107
3066
  }
2108
3067
  console.log("");
2109
3068
  const healthSpinner = ora2("Waiting for services to become healthy...").start();
2110
3069
  let lastStates = [];
2111
3070
  try {
2112
- const states = await pollUntilHealthy(
3071
+ lastStates = await pollUntilHealthy(
2113
3072
  runtime,
2114
3073
  (current) => {
2115
3074
  lastStates = current;
2116
3075
  const summary = current.map((s) => {
2117
- const icon = s.status === "healthy" ? chalk2.green("*") : s.status === "starting" ? chalk2.yellow("~") : chalk2.red("x");
3076
+ const icon = s.status === "healthy" ? chalk3.green("*") : s.status === "starting" ? chalk3.yellow("~") : chalk3.red("x");
2118
3077
  return `${icon} ${s.name}`;
2119
3078
  }).join(" ");
2120
3079
  healthSpinner.text = `Waiting for services... ${summary}`;
@@ -2123,76 +3082,75 @@ ${example(`${vaultName.trim()}-knowledge`)}
2123
3082
  5e3
2124
3083
  );
2125
3084
  healthSpinner.succeed("All services are healthy");
2126
- lastStates = states;
2127
3085
  } catch (error) {
2128
3086
  healthSpinner.fail("Some services did not become healthy");
2129
- console.log(chalk2.dim(error.message));
3087
+ console.log(chalk3.dim(error.message));
3088
+ console.log(chalk3.dim("Tip: check logs with `horus status` or `docker compose logs` from ~/Horus/"));
3089
+ }
3090
+ if (config.control_plane_url) {
2130
3091
  console.log("");
2131
- console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from ~/Horus/"));
2132
- process.exit(1);
3092
+ const loginSpinner = ora2("Logging in to the control plane...").start();
3093
+ const result = await runLogin(config);
3094
+ if (result.ok) loginSpinner.succeed(result.message);
3095
+ else loginSpinner.warn(result.message);
2133
3096
  }
2134
3097
  console.log("");
2135
3098
  const detectedClients = detectInstalledClients();
2136
3099
  if (detectedClients.length > 0) {
2137
- console.log(chalk2.bold("Configuring AI clients..."));
3100
+ console.log(chalk3.bold("Configuring AI clients..."));
2138
3101
  let clientsToConnect = [...detectedClients];
2139
3102
  if (clientsToConnect.includes("claude-desktop")) {
2140
3103
  let configureDesktop;
2141
- if (opts.yes) {
3104
+ if (!interactive) {
2142
3105
  configureDesktop = opts.claudeDesktop === true;
2143
- if (!configureDesktop) {
2144
- console.log(chalk2.dim("Skipping Claude Desktop (pass --claude-desktop to configure it)."));
2145
- }
2146
3106
  } else {
2147
3107
  configureDesktop = opts.claudeDesktop === false ? false : await confirm({ message: "Setup for Claude Desktop?", default: true });
2148
3108
  }
2149
- if (!configureDesktop) {
2150
- clientsToConnect = clientsToConnect.filter((c) => c !== "claude-desktop");
2151
- }
3109
+ if (!configureDesktop) clientsToConnect = clientsToConnect.filter((c) => c !== "claude-desktop");
2152
3110
  }
2153
3111
  if (clientsToConnect.length > 0) {
2154
3112
  try {
2155
3113
  await runConnect(config, runtime, clientsToConnect, "localhost");
2156
- } catch (error) {
2157
- console.log(chalk2.yellow("Could not configure AI clients automatically."));
2158
- console.log(chalk2.dim(`Run ${chalk2.cyan("horus connect")} to configure them manually.`));
3114
+ } catch {
3115
+ console.log(chalk3.yellow("Could not configure AI clients automatically."));
3116
+ console.log(chalk3.dim(`Run ${chalk3.cyan("horus connect")} to configure them manually.`));
2159
3117
  }
2160
3118
  }
2161
3119
  } else {
2162
- console.log(chalk2.dim(`No AI clients detected. Run ${chalk2.cyan("horus connect")} after installing Claude Desktop, Claude Code, or Cursor.`));
3120
+ console.log(chalk3.dim(`No AI clients detected. Run ${chalk3.cyan("horus connect")} after installing one.`));
2163
3121
  }
3122
+ const mode = config.control_plane_url ? "connected" : "local-only";
2164
3123
  console.log("");
2165
- console.log(chalk2.bold.green("Setup complete!"));
2166
- console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3124
+ console.log(chalk3.bold.green("Setup complete!"));
3125
+ console.log(chalk3.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2167
3126
  console.log("");
2168
- console.log(` ${chalk2.bold("Runtime:")} ${runtime.name}`);
2169
- console.log(` ${chalk2.bold("Config:")} ~/Horus/config.yaml`);
2170
- console.log(` ${chalk2.bold("Data:")} ${config.data_dir}`);
2171
- console.log("");
2172
- console.log(chalk2.bold(" Service URLs:"));
2173
- console.log(` Anvil: http://localhost:${config.ports.anvil}`);
2174
- console.log(` Vault Router: http://localhost:${config.ports.vault_router}`);
2175
- console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
2176
- console.log(` Forge: http://localhost:${config.ports.forge}`);
3127
+ console.log(` ${chalk3.bold("Mode:")} ${mode}`);
3128
+ console.log(` ${chalk3.bold("Runtime:")} ${runtime.name}`);
3129
+ console.log(` ${chalk3.bold("Config:")} ~/Horus/config.yaml`);
3130
+ console.log(` ${chalk3.bold("Data:")} ${config.data_dir}`);
3131
+ if (config.control_plane_url) {
3132
+ console.log(` ${chalk3.bold("Control plane:")} ${config.control_plane_url}`);
3133
+ }
2177
3134
  console.log("");
2178
- console.log(chalk2.bold(" Vault instances:"));
2179
- Object.entries(config.vaults).sort(([a], [b]) => a.localeCompare(b)).forEach(([name, vault], index) => {
2180
- const port = `800${index + 1}`;
2181
- const defaultLabel = vault.default ? chalk2.dim(" (default)") : "";
2182
- console.log(` ${name}${defaultLabel}: http://localhost:${port}`);
2183
- });
3135
+ console.log(chalk3.bold(" Service URLs:"));
3136
+ console.log(` Horus UI: http://localhost:${config.ports.ui}`);
3137
+ console.log(` Anvil: http://localhost:${config.ports.anvil}`);
2184
3138
  console.log("");
3139
+ if (mode === "local-only") {
3140
+ console.log(chalk3.dim(" Vault, Forge, and Admin require a control plane \u2014 not available in local-only mode."));
3141
+ console.log("");
3142
+ }
2185
3143
  void lastStates;
2186
3144
  });
2187
3145
 
2188
3146
  // src/commands/up.ts
2189
- import { Command as Command3 } from "commander";
2190
- import chalk3 from "chalk";
3147
+ import { Command as Command4 } from "commander";
3148
+ import chalk4 from "chalk";
2191
3149
  import ora3 from "ora";
2192
- var upCommand = new Command3("up").description("Start the Horus stack").option("--no-pull", "Skip pulling latest images before starting").action(async (opts) => {
3150
+ var upCommand = new Command4("up").description("Start the Horus stack").option("--no-pull", "Skip pulling latest images before starting").action(async (opts) => {
2193
3151
  if (!configExists() || !composeFileExists()) {
2194
- console.log(chalk3.red("Horus is not set up yet."));
2195
- console.log(chalk3.dim("Run `horus setup` first."));
3152
+ console.log(chalk4.red("Horus is not set up yet."));
3153
+ console.log(chalk4.dim("Run `horus setup` first."));
2196
3154
  process.exit(1);
2197
3155
  }
2198
3156
  const config = loadConfig();
@@ -2200,7 +3158,7 @@ var upCommand = new Command3("up").description("Start the Horus stack").option("
2200
3158
  let runtime;
2201
3159
  try {
2202
3160
  runtime = await detectRuntime(config.runtime);
2203
- spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
3161
+ spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
2204
3162
  } catch (error) {
2205
3163
  spinner.fail("No container runtime found");
2206
3164
  console.log(error.message);
@@ -2208,25 +3166,25 @@ var upCommand = new Command3("up").description("Start the Horus stack").option("
2208
3166
  }
2209
3167
  if (opts.pull) {
2210
3168
  console.log("");
2211
- console.log(chalk3.bold("Pulling latest images..."));
3169
+ console.log(chalk4.bold("Pulling latest images..."));
2212
3170
  try {
2213
3171
  await composeStreaming(runtime, ["pull"]);
2214
3172
  console.log("");
2215
- console.log(chalk3.green("\u2713 Pull complete"));
3173
+ console.log(chalk4.green("\u2713 Pull complete"));
2216
3174
  } catch {
2217
3175
  console.log("");
2218
- console.log(chalk3.yellow("\u26A0 Warning: failed to pull one or more images \u2014 using cached versions."));
2219
- console.log(chalk3.dim(" Run `docker compose pull` to see which services failed."));
3176
+ console.log(chalk4.yellow("\u26A0 Warning: failed to pull one or more images \u2014 using cached versions."));
3177
+ console.log(chalk4.dim(" Run `docker compose pull` to see which services failed."));
2220
3178
  }
2221
3179
  }
2222
3180
  console.log("");
2223
- console.log(chalk3.bold("Starting Horus services..."));
3181
+ console.log(chalk4.bold("Starting Horus services..."));
2224
3182
  try {
2225
3183
  const upArgs = opts.pull ? ["up", "-d", "--force-recreate", "--remove-orphans"] : ["up", "-d", "--remove-orphans"];
2226
3184
  await composeStreaming(runtime, upArgs);
2227
3185
  } catch (error) {
2228
- console.log(chalk3.red("Failed to start services."));
2229
- console.log(chalk3.dim(error.message));
3186
+ console.log(chalk4.red("Failed to start services."));
3187
+ console.log(chalk4.dim(error.message));
2230
3188
  process.exit(1);
2231
3189
  }
2232
3190
  console.log("");
@@ -2234,16 +3192,16 @@ var upCommand = new Command3("up").description("Start the Horus stack").option("
2234
3192
  try {
2235
3193
  const states = await checkAllHealth(runtime);
2236
3194
  statusSpinner.stop();
2237
- console.log(chalk3.bold("Service Status:"));
3195
+ console.log(chalk4.bold("Service Status:"));
2238
3196
  for (const s of states) {
2239
- const color = s.status === "healthy" ? chalk3.green : s.status === "starting" ? chalk3.yellow : chalk3.red;
3197
+ const color = s.status === "healthy" ? chalk4.green : s.status === "starting" ? chalk4.yellow : chalk4.red;
2240
3198
  console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
2241
3199
  }
2242
3200
  const allHealthy = states.every((s) => s.status === "healthy");
2243
3201
  if (!allHealthy) {
2244
3202
  console.log("");
2245
3203
  console.log(
2246
- chalk3.yellow("Some services are still starting. Run `horus status` to check progress.")
3204
+ chalk4.yellow("Some services are still starting. Run `horus status` to check progress.")
2247
3205
  );
2248
3206
  }
2249
3207
  } catch {
@@ -2253,13 +3211,13 @@ var upCommand = new Command3("up").description("Start the Horus stack").option("
2253
3211
  });
2254
3212
 
2255
3213
  // src/commands/down.ts
2256
- import { Command as Command4 } from "commander";
2257
- import chalk4 from "chalk";
3214
+ import { Command as Command5 } from "commander";
3215
+ import chalk5 from "chalk";
2258
3216
  import ora4 from "ora";
2259
- var downCommand = new Command4("down").description("Stop the Horus stack").action(async () => {
3217
+ var downCommand = new Command5("down").description("Stop the Horus stack").action(async () => {
2260
3218
  if (!configExists() || !composeFileExists()) {
2261
- console.log(chalk4.red("Horus is not set up yet."));
2262
- console.log(chalk4.dim("Run `horus setup` first."));
3219
+ console.log(chalk5.red("Horus is not set up yet."));
3220
+ console.log(chalk5.dim("Run `horus setup` first."));
2263
3221
  process.exit(1);
2264
3222
  }
2265
3223
  const config = loadConfig();
@@ -2267,35 +3225,35 @@ var downCommand = new Command4("down").description("Stop the Horus stack").actio
2267
3225
  let runtime;
2268
3226
  try {
2269
3227
  runtime = await detectRuntime(config.runtime);
2270
- spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
3228
+ spinner.succeed(`Using ${chalk5.cyan(runtime.name)}`);
2271
3229
  } catch (error) {
2272
3230
  spinner.fail("No container runtime found");
2273
3231
  console.log(error.message);
2274
3232
  process.exit(1);
2275
3233
  }
2276
3234
  console.log("");
2277
- console.log(chalk4.bold("Stopping Horus services..."));
3235
+ console.log(chalk5.bold("Stopping Horus services..."));
2278
3236
  try {
2279
3237
  await composeStreaming(runtime, ["down"]);
2280
3238
  } catch (error) {
2281
- console.log(chalk4.red("Failed to stop services."));
2282
- console.log(chalk4.dim(error.message));
3239
+ console.log(chalk5.red("Failed to stop services."));
3240
+ console.log(chalk5.dim(error.message));
2283
3241
  process.exit(1);
2284
3242
  }
2285
3243
  console.log("");
2286
- console.log(chalk4.green("All services stopped."));
2287
- console.log(chalk4.dim("Data volumes have been preserved. Run `horus up` to restart."));
3244
+ console.log(chalk5.green("All services stopped."));
3245
+ console.log(chalk5.dim("Data volumes have been preserved. Run `horus up` to restart."));
2288
3246
  console.log("");
2289
3247
  });
2290
3248
 
2291
3249
  // src/commands/status.ts
2292
- import { Command as Command5 } from "commander";
2293
- import chalk5 from "chalk";
3250
+ import { Command as Command6 } from "commander";
3251
+ import chalk6 from "chalk";
2294
3252
  import ora5 from "ora";
2295
- var statusCommand = new Command5("status").description("Show status of Horus services").action(async () => {
3253
+ var statusCommand = new Command6("status").description("Show status of Horus services").action(async () => {
2296
3254
  if (!configExists() || !composeFileExists()) {
2297
- console.log(chalk5.red("Horus is not set up yet."));
2298
- console.log(chalk5.dim("Run `horus setup` first."));
3255
+ console.log(chalk6.red("Horus is not set up yet."));
3256
+ console.log(chalk6.dim("Run `horus setup` first."));
2299
3257
  process.exit(1);
2300
3258
  }
2301
3259
  const config = loadConfig();
@@ -2317,28 +3275,28 @@ var statusCommand = new Command5("status").description("Show status of Horus ser
2317
3275
  }
2318
3276
  spinner.stop();
2319
3277
  console.log("");
2320
- console.log(chalk5.bold("Horus Status"));
2321
- console.log(chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2322
- console.log(` ${chalk5.bold("Version:")} ${CLI_VERSION}`);
2323
- console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
2324
- console.log(` ${chalk5.bold("Config:")} ~/Horus/config.yaml`);
3278
+ console.log(chalk6.bold("Horus Status"));
3279
+ console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3280
+ console.log(` ${chalk6.bold("Version:")} ${CLI_VERSION}`);
3281
+ console.log(` ${chalk6.bold("Runtime:")} ${runtime.name}`);
3282
+ console.log(` ${chalk6.bold("Config:")} ~/Horus/config.yaml`);
2325
3283
  console.log("");
2326
3284
  if (containers.length === 0) {
2327
- console.log(chalk5.yellow(" No services are running."));
2328
- console.log(chalk5.dim(" Run `horus up` to start the stack."));
3285
+ console.log(chalk6.yellow(" No services are running."));
3286
+ console.log(chalk6.dim(" Run `horus up` to start the stack."));
2329
3287
  console.log("");
2330
3288
  return;
2331
3289
  }
2332
3290
  const header = ` ${pad("SERVICE", 14)} ${pad("STATUS", 12)} ${pad("PORTS", 20)} ${pad("UPTIME", 20)}`;
2333
- console.log(chalk5.bold(header));
2334
- console.log(chalk5.dim(" " + "\u2500".repeat(66)));
3291
+ console.log(chalk6.bold(header));
3292
+ console.log(chalk6.dim(" " + "\u2500".repeat(66)));
2335
3293
  for (const service of SERVICES) {
2336
3294
  const container = containers.find(
2337
3295
  (c) => c.Service === service || c.Name?.includes(service)
2338
3296
  );
2339
3297
  if (!container) {
2340
3298
  console.log(
2341
- ` ${pad(service, 14)} ${chalk5.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
3299
+ ` ${pad(service, 14)} ${chalk6.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
2342
3300
  );
2343
3301
  continue;
2344
3302
  }
@@ -2356,9 +3314,9 @@ function pad(str, width) {
2356
3314
  }
2357
3315
  function getStatusColor(status) {
2358
3316
  const lower = status.toLowerCase();
2359
- if (lower === "healthy" || lower === "running") return chalk5.green;
2360
- if (lower === "starting") return chalk5.yellow;
2361
- return chalk5.red;
3317
+ if (lower === "healthy" || lower === "running") return chalk6.green;
3318
+ if (lower === "starting") return chalk6.yellow;
3319
+ return chalk6.red;
2362
3320
  }
2363
3321
  function formatPorts(publishers) {
2364
3322
  if (!publishers || publishers.length === 0) return "-";
@@ -2373,69 +3331,69 @@ function extractUptime(status) {
2373
3331
  }
2374
3332
 
2375
3333
  // src/commands/config.ts
2376
- import { Command as Command6 } from "commander";
2377
- import chalk6 from "chalk";
3334
+ import { Command as Command7 } from "commander";
3335
+ import chalk7 from "chalk";
2378
3336
  import { confirm as confirm2 } from "@inquirer/prompts";
2379
- var configCommand = new Command6("config").description("View or modify Horus configuration").action(async () => {
3337
+ var configCommand = new Command7("config").description("View or modify Horus configuration").action(async () => {
2380
3338
  if (!configExists()) {
2381
- console.log(chalk6.red("Horus is not configured yet."));
2382
- console.log(chalk6.dim("Run `horus setup` first."));
3339
+ console.log(chalk7.red("Horus is not configured yet."));
3340
+ console.log(chalk7.dim("Run `horus setup` first."));
2383
3341
  process.exit(1);
2384
3342
  }
2385
3343
  const config = loadConfig();
2386
3344
  console.log("");
2387
- console.log(chalk6.bold("Horus Configuration"));
2388
- console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2389
- console.log(` ${chalk6.bold("version:")} ${config.version}`);
2390
- console.log(` ${chalk6.bold("data-dir:")} ${config.data_dir}`);
2391
- console.log(` ${chalk6.bold("runtime:")} ${config.runtime}`);
2392
- console.log(` ${chalk6.bold("host-repos-path:")} ${config.host_repos_path || chalk6.dim("(not set)")}`);
3345
+ console.log(chalk7.bold("Horus Configuration"));
3346
+ console.log(chalk7.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3347
+ console.log(` ${chalk7.bold("version:")} ${config.version}`);
3348
+ console.log(` ${chalk7.bold("data-dir:")} ${config.data_dir}`);
3349
+ console.log(` ${chalk7.bold("runtime:")} ${config.runtime}`);
3350
+ console.log(` ${chalk7.bold("host-repos-path:")} ${config.host_repos_path || chalk7.dim("(not set)")}`);
2393
3351
  const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
2394
- console.log(` ${chalk6.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk6.dim("(not set)")}`);
3352
+ console.log(` ${chalk7.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk7.dim("(not set)")}`);
2395
3353
  console.log("");
2396
- console.log(chalk6.bold(" Ports:"));
2397
- console.log(` ${chalk6.bold("anvil:")} ${config.ports.anvil}`);
2398
- console.log(` ${chalk6.bold("vault-rest:")} ${config.ports.vault_rest}`);
2399
- console.log(` ${chalk6.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
2400
- console.log(` ${chalk6.bold("vault-router:")} ${config.ports.vault_router}`);
2401
- console.log(` ${chalk6.bold("forge:")} ${config.ports.forge}`);
3354
+ console.log(chalk7.bold(" Ports:"));
3355
+ console.log(` ${chalk7.bold("anvil:")} ${config.ports.anvil}`);
3356
+ console.log(` ${chalk7.bold("vault-rest:")} ${config.ports.vault_rest}`);
3357
+ console.log(` ${chalk7.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
3358
+ console.log(` ${chalk7.bold("vault-router:")} ${config.ports.vault_router}`);
3359
+ console.log(` ${chalk7.bold("forge:")} ${config.ports.forge}`);
2402
3360
  console.log("");
2403
- console.log(chalk6.bold(" Repos:"));
2404
- console.log(` ${chalk6.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk6.dim("(not set)")}`);
2405
- console.log(` ${chalk6.bold("forge-registry:")} ${config.repos.forge_registry || chalk6.dim("(not set)")}`);
3361
+ console.log(chalk7.bold(" Repos:"));
3362
+ console.log(` ${chalk7.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk7.dim("(not set)")}`);
3363
+ console.log(` ${chalk7.bold("forge-registry:")} ${config.repos.forge_registry || chalk7.dim("(not set)")}`);
2406
3364
  console.log("");
2407
- console.log(chalk6.bold(" Vaults:"));
3365
+ console.log(chalk7.bold(" Vaults:"));
2408
3366
  if (Object.keys(config.vaults ?? {}).length === 0) {
2409
- console.log(chalk6.dim(" (none configured)"));
3367
+ console.log(chalk7.dim(" (none configured)"));
2410
3368
  } else {
2411
3369
  for (const [name, vault] of Object.entries(config.vaults)) {
2412
- const defaultLabel = vault.default ? chalk6.dim(" (default)") : "";
2413
- console.log(` ${chalk6.bold(name)}${defaultLabel}: ${vault.repo || chalk6.dim("(no repo)")}`);
3370
+ const defaultLabel = vault.default ? chalk7.dim(" (default)") : "";
3371
+ console.log(` ${chalk7.bold(name)}${defaultLabel}: ${vault.repo || chalk7.dim("(no repo)")}`);
2414
3372
  }
2415
3373
  }
2416
3374
  console.log("");
2417
- console.log(chalk6.bold(" GitHub Hosts:"));
3375
+ console.log(chalk7.bold(" GitHub Hosts:"));
2418
3376
  if (Object.keys(config.github_hosts ?? {}).length === 0) {
2419
- console.log(chalk6.dim(" (none configured)"));
3377
+ console.log(chalk7.dim(" (none configured)"));
2420
3378
  } else {
2421
3379
  for (const [key, gh] of Object.entries(config.github_hosts)) {
2422
- console.log(` ${chalk6.bold(key)}: ${gh.host} token: ${gh.token ? maskApiKey(gh.token) : chalk6.dim("(not set)")}`);
3380
+ console.log(` ${chalk7.bold(key)}: ${gh.host} token: ${gh.token ? maskApiKey(gh.token) : chalk7.dim("(not set)")}`);
2423
3381
  }
2424
3382
  }
2425
3383
  console.log("");
2426
- console.log(chalk6.dim(` Config file: ~/Horus/config.yaml`));
2427
- console.log(chalk6.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
3384
+ console.log(chalk7.dim(` Config file: ~/Horus/config.yaml`));
3385
+ console.log(chalk7.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
2428
3386
  console.log("");
2429
3387
  });
2430
3388
  configCommand.command("get <key>").description("Get a configuration value").action(async (key) => {
2431
3389
  if (!configExists()) {
2432
- console.log(chalk6.red("Horus is not configured yet."));
2433
- console.log(chalk6.dim("Run `horus setup` first."));
3390
+ console.log(chalk7.red("Horus is not configured yet."));
3391
+ console.log(chalk7.dim("Run `horus setup` first."));
2434
3392
  process.exit(1);
2435
3393
  }
2436
3394
  if (!isValidKey(key)) {
2437
- console.log(chalk6.red(`Unknown config key: ${key}`));
2438
- console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
3395
+ console.log(chalk7.red(`Unknown config key: ${key}`));
3396
+ console.log(chalk7.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
2439
3397
  process.exit(1);
2440
3398
  }
2441
3399
  const config = loadConfig();
@@ -2444,25 +3402,26 @@ configCommand.command("get <key>").description("Get a configuration value").acti
2444
3402
  });
2445
3403
  configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
2446
3404
  if (!configExists()) {
2447
- console.log(chalk6.red("Horus is not configured yet."));
2448
- console.log(chalk6.dim("Run `horus setup` first."));
3405
+ console.log(chalk7.red("Horus is not configured yet."));
3406
+ console.log(chalk7.dim("Run `horus setup` first."));
2449
3407
  process.exit(1);
2450
3408
  }
2451
3409
  if (!isValidKey(key)) {
2452
- console.log(chalk6.red(`Unknown config key: ${key}`));
2453
- console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
3410
+ console.log(chalk7.red(`Unknown config key: ${key}`));
3411
+ console.log(chalk7.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
2454
3412
  process.exit(1);
2455
3413
  }
2456
3414
  let config = loadConfig();
2457
3415
  try {
2458
3416
  config = setConfigValue(config, key, value);
2459
3417
  } catch (error) {
2460
- console.log(chalk6.red(error.message));
3418
+ console.log(chalk7.red(error.message));
2461
3419
  process.exit(1);
2462
3420
  }
2463
3421
  saveConfig(config);
2464
3422
  writeEnvFile(config);
2465
- console.log(chalk6.green(`Set ${key} and regenerated .env file.`));
3423
+ installComposeFile(config, config.runtime === "podman" ? "podman" : "docker");
3424
+ console.log(chalk7.green(`Set ${key} and regenerated .env + docker-compose.yml.`));
2466
3425
  const needsRestart = [
2467
3426
  "data-dir",
2468
3427
  "host-repos-path",
@@ -2475,17 +3434,17 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
2475
3434
  "port.forge"
2476
3435
  ];
2477
3436
  if (needsRestart.includes(key)) {
2478
- console.log(chalk6.yellow("Restart required for changes to take effect."));
3437
+ console.log(chalk7.yellow("Restart required for changes to take effect."));
2479
3438
  if (process.stdin.isTTY) {
2480
3439
  const restart = await confirm2({
2481
3440
  message: "Restart Horus now?",
2482
3441
  default: false
2483
3442
  });
2484
3443
  if (restart) {
2485
- console.log(chalk6.dim("Run `horus down && horus up` to restart."));
3444
+ console.log(chalk7.dim("Run `horus down && horus up` to restart."));
2486
3445
  }
2487
3446
  } else {
2488
- console.log(chalk6.dim("Run `horus down && horus up` to restart."));
3447
+ console.log(chalk7.dim("Run `horus down && horus up` to restart."));
2489
3448
  }
2490
3449
  }
2491
3450
  });
@@ -2494,8 +3453,8 @@ function isValidKey(key) {
2494
3453
  }
2495
3454
 
2496
3455
  // src/commands/update.ts
2497
- import { Command as Command7 } from "commander";
2498
- import chalk7 from "chalk";
3456
+ import { Command as Command8 } from "commander";
3457
+ import chalk8 from "chalk";
2499
3458
  import ora6 from "ora";
2500
3459
  import { select as select2, confirm as confirm3 } from "@inquirer/prompts";
2501
3460
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
@@ -2562,17 +3521,17 @@ async function fetchLatestVersion() {
2562
3521
  return null;
2563
3522
  }
2564
3523
  }
2565
- var updateCommand = new Command7("update").description("Update Horus to the latest version").option("--rollback", "Roll back to the previous version").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
3524
+ var updateCommand = new Command8("update").description("Update Horus to the latest version").option("--rollback", "Roll back to the previous version").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
2566
3525
  console.log("");
2567
- console.log(chalk7.bold(opts.rollback ? "Horus Rollback" : "Horus Update"));
2568
- console.log(chalk7.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3526
+ console.log(chalk8.bold(opts.rollback ? "Horus Rollback" : "Horus Update"));
3527
+ console.log(chalk8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2569
3528
  console.log("");
2570
3529
  const config = loadConfig();
2571
3530
  const runtimeSpinner = ora6("Detecting runtime...").start();
2572
3531
  let runtime;
2573
3532
  try {
2574
3533
  runtime = await detectRuntime(config.runtime);
2575
- runtimeSpinner.succeed(`Using ${chalk7.cyan(runtime.name)}`);
3534
+ runtimeSpinner.succeed(`Using ${chalk8.cyan(runtime.name)}`);
2576
3535
  } catch (error) {
2577
3536
  runtimeSpinner.fail("No container runtime found");
2578
3537
  console.log(error.message);
@@ -2581,14 +3540,14 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2581
3540
  if (opts.rollback) {
2582
3541
  const snapshots = listSnapshots();
2583
3542
  if (snapshots.length === 0) {
2584
- console.log(chalk7.red("No snapshots found. Cannot roll back."));
2585
- console.log(chalk7.dim(`Snapshots are stored in ${SNAPSHOTS_DIR}`));
3543
+ console.log(chalk8.red("No snapshots found. Cannot roll back."));
3544
+ console.log(chalk8.dim(`Snapshots are stored in ${SNAPSHOTS_DIR}`));
2586
3545
  process.exit(1);
2587
3546
  }
2588
3547
  let snapshotToRestore;
2589
3548
  if (opts.yes) {
2590
3549
  snapshotToRestore = snapshots[0].snapshot;
2591
- console.log(`Using most recent snapshot: ${chalk7.cyan(snapshotToRestore.timestamp)}`);
3550
+ console.log(`Using most recent snapshot: ${chalk8.cyan(snapshotToRestore.timestamp)}`);
2592
3551
  } else {
2593
3552
  const choices = snapshots.map(({ snapshot }, i) => ({
2594
3553
  name: `${snapshot.timestamp} (images: ${Object.keys(snapshot.images).length})`,
@@ -2606,7 +3565,7 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2606
3565
  default: false
2607
3566
  });
2608
3567
  if (!confirmed) {
2609
- console.log(chalk7.dim("Rollback cancelled."));
3568
+ console.log(chalk8.dim("Rollback cancelled."));
2610
3569
  return;
2611
3570
  }
2612
3571
  }
@@ -2616,16 +3575,16 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2616
3575
  stopSpinner.succeed("Services stopped");
2617
3576
  } catch (error) {
2618
3577
  stopSpinner.fail("Failed to stop services");
2619
- console.log(chalk7.dim(error.message));
3578
+ console.log(chalk8.dim(error.message));
2620
3579
  process.exit(1);
2621
3580
  }
2622
3581
  console.log("");
2623
- console.log(chalk7.bold("Restarting from snapshot (using cached images)..."));
3582
+ console.log(chalk8.bold("Restarting from snapshot (using cached images)..."));
2624
3583
  try {
2625
3584
  await composeStreaming(runtime, ["up", "-d", "--remove-orphans"]);
2626
3585
  } catch (error) {
2627
- console.log(chalk7.red("Failed to restart services."));
2628
- console.log(chalk7.dim(error.message));
3586
+ console.log(chalk8.red("Failed to restart services."));
3587
+ console.log(chalk8.dim(error.message));
2629
3588
  process.exit(1);
2630
3589
  }
2631
3590
  console.log("");
@@ -2635,7 +3594,7 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2635
3594
  runtime,
2636
3595
  (current) => {
2637
3596
  const summary = current.map((s) => {
2638
- const icon = s.status === "healthy" ? chalk7.green("*") : s.status === "starting" ? chalk7.yellow("~") : chalk7.red("x");
3597
+ const icon = s.status === "healthy" ? chalk8.green("*") : s.status === "starting" ? chalk8.yellow("~") : chalk8.red("x");
2639
3598
  return `${icon} ${s.name}`;
2640
3599
  }).join(" ");
2641
3600
  healthSpinner2.text = `Waiting... ${summary}`;
@@ -2646,11 +3605,11 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2646
3605
  healthSpinner2.succeed("All services healthy after rollback");
2647
3606
  } catch (error) {
2648
3607
  healthSpinner2.fail("Some services did not become healthy");
2649
- console.log(chalk7.dim(error.message));
3608
+ console.log(chalk8.dim(error.message));
2650
3609
  process.exit(1);
2651
3610
  }
2652
3611
  console.log("");
2653
- console.log(chalk7.bold.green("Rollback complete!"));
3612
+ console.log(chalk8.bold.green("Rollback complete!"));
2654
3613
  console.log("");
2655
3614
  return;
2656
3615
  }
@@ -2661,14 +3620,14 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2661
3620
  ]);
2662
3621
  versionSpinner.stop();
2663
3622
  if (latestVersion) {
2664
- console.log(` Latest release: ${chalk7.cyan(latestVersion)}`);
3623
+ console.log(` Latest release: ${chalk8.cyan(latestVersion)}`);
2665
3624
  } else {
2666
- console.log(chalk7.dim(" Could not reach GitHub to check latest version."));
3625
+ console.log(chalk8.dim(" Could not reach GitHub to check latest version."));
2667
3626
  }
2668
3627
  console.log("");
2669
- console.log(chalk7.dim(" Note: this updates the Horus container services only."));
2670
- console.log(chalk7.dim(" To update the Horus CLI itself, run:"));
2671
- console.log(` ${chalk7.cyan("npm install -g @arkhera30/cli@latest")}`);
3628
+ console.log(chalk8.dim(" Note: this updates the Horus container services only."));
3629
+ console.log(chalk8.dim(" To update the Horus CLI itself, run:"));
3630
+ console.log(` ${chalk8.cyan("npm install -g @arkhera30/cli@latest")}`);
2672
3631
  console.log("");
2673
3632
  if (!opts.yes) {
2674
3633
  const confirmed = await confirm3({
@@ -2676,7 +3635,7 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2676
3635
  default: true
2677
3636
  });
2678
3637
  if (!confirmed) {
2679
- console.log(chalk7.dim("Update cancelled."));
3638
+ console.log(chalk8.dim("Update cancelled."));
2680
3639
  return;
2681
3640
  }
2682
3641
  }
@@ -2684,10 +3643,10 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2684
3643
  let snapshotPath = "";
2685
3644
  try {
2686
3645
  snapshotPath = saveSnapshot(currentImages);
2687
- snapshotSpinner.succeed(`Snapshot saved: ${chalk7.dim(snapshotPath)}`);
3646
+ snapshotSpinner.succeed(`Snapshot saved: ${chalk8.dim(snapshotPath)}`);
2688
3647
  } catch (error) {
2689
3648
  snapshotSpinner.warn("Could not save snapshot (update will proceed)");
2690
- console.log(chalk7.dim(error.message));
3649
+ console.log(chalk8.dim(error.message));
2691
3650
  }
2692
3651
  const composeSpinner = ora6("Updating compose configuration...").start();
2693
3652
  try {
@@ -2696,23 +3655,23 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2696
3655
  composeSpinner.succeed("Compose configuration updated");
2697
3656
  } catch (error) {
2698
3657
  composeSpinner.warn("Could not update compose configuration \u2014 using existing file");
2699
- console.log(chalk7.dim(error.message));
3658
+ console.log(chalk8.dim(error.message));
2700
3659
  }
2701
3660
  console.log("");
2702
- console.log(chalk7.bold("Pulling latest images..."));
3661
+ console.log(chalk8.bold("Pulling latest images..."));
2703
3662
  try {
2704
3663
  await composeStreaming(runtime, runtime.name === "podman" ? ["pull"] : ["pull", "--ignore-pull-failures"]);
2705
3664
  } catch {
2706
- console.log(chalk7.yellow("Some images could not be pulled."));
2707
- console.log(chalk7.dim("Continuing \u2014 services will be built from source if build contexts are available."));
3665
+ console.log(chalk8.yellow("Some images could not be pulled."));
3666
+ console.log(chalk8.dim("Continuing \u2014 services will be built from source if build contexts are available."));
2708
3667
  }
2709
3668
  console.log("");
2710
- console.log(chalk7.bold("Restarting services..."));
3669
+ console.log(chalk8.bold("Restarting services..."));
2711
3670
  try {
2712
3671
  await composeStreaming(runtime, ["up", "-d", "--force-recreate", "--remove-orphans"]);
2713
3672
  } catch (error) {
2714
- console.log(chalk7.red("Failed to restart services."));
2715
- console.log(chalk7.dim(error.message));
3673
+ console.log(chalk8.red("Failed to restart services."));
3674
+ console.log(chalk8.dim(error.message));
2716
3675
  process.exit(1);
2717
3676
  }
2718
3677
  console.log("");
@@ -2723,7 +3682,7 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2723
3682
  runtime,
2724
3683
  (current) => {
2725
3684
  const summary = current.map((s) => {
2726
- const icon = s.status === "healthy" ? chalk7.green("*") : s.status === "starting" ? chalk7.yellow("~") : chalk7.red("x");
3685
+ const icon = s.status === "healthy" ? chalk8.green("*") : s.status === "starting" ? chalk8.yellow("~") : chalk8.red("x");
2727
3686
  return `${icon} ${s.name}`;
2728
3687
  }).join(" ");
2729
3688
  healthSpinner.text = `Waiting for services... ${summary}`;
@@ -2734,55 +3693,55 @@ var updateCommand = new Command7("update").description("Update Horus to the late
2734
3693
  healthSpinner.succeed("All services healthy");
2735
3694
  } catch (error) {
2736
3695
  healthSpinner.fail("Some services did not become healthy");
2737
- console.log(chalk7.dim(error.message));
3696
+ console.log(chalk8.dim(error.message));
2738
3697
  console.log("");
2739
- console.log(chalk7.dim(`Tip: Roll back with \`horus update --rollback\``));
3698
+ console.log(chalk8.dim(`Tip: Roll back with \`horus update --rollback\``));
2740
3699
  process.exit(1);
2741
3700
  }
2742
3701
  console.log("");
2743
- console.log(chalk7.bold.green("Update complete!"));
2744
- console.log(chalk7.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3702
+ console.log(chalk8.bold.green("Update complete!"));
3703
+ console.log(chalk8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2745
3704
  if (latestVersion) {
2746
- console.log(` ${chalk7.bold("Version:")} ${latestVersion}`);
3705
+ console.log(` ${chalk8.bold("Version:")} ${latestVersion}`);
2747
3706
  }
2748
3707
  console.log("");
2749
- console.log(chalk7.bold(" Service Status:"));
3708
+ console.log(chalk8.bold(" Service Status:"));
2750
3709
  for (const s of finalStates) {
2751
- const color = s.status === "healthy" ? chalk7.green : s.status === "starting" ? chalk7.yellow : chalk7.red;
3710
+ const color = s.status === "healthy" ? chalk8.green : s.status === "starting" ? chalk8.yellow : chalk8.red;
2752
3711
  console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
2753
3712
  }
2754
3713
  if (snapshotPath) {
2755
3714
  console.log("");
2756
- console.log(chalk7.dim(` Snapshot saved for rollback: ${snapshotPath}`));
2757
- console.log(chalk7.dim(" Run `horus update --rollback` to revert if needed."));
3715
+ console.log(chalk8.dim(` Snapshot saved for rollback: ${snapshotPath}`));
3716
+ console.log(chalk8.dim(" Run `horus update --rollback` to revert if needed."));
2758
3717
  }
2759
3718
  console.log("");
2760
3719
  });
2761
3720
 
2762
3721
  // src/commands/doctor.ts
2763
- import { Command as Command8 } from "commander";
2764
- import chalk8 from "chalk";
3722
+ import { Command as Command9 } from "commander";
3723
+ import chalk9 from "chalk";
2765
3724
  import { execSync as execSync2 } from "child_process";
2766
3725
  import { existsSync as existsSync7, accessSync, statfsSync, constants } from "fs";
2767
3726
  import { join as join5 } from "path";
2768
3727
  function symbol(status) {
2769
3728
  switch (status) {
2770
3729
  case "pass":
2771
- return chalk8.green(" \u2713 ");
3730
+ return chalk9.green(" \u2713 ");
2772
3731
  case "warn":
2773
- return chalk8.yellow(" \u26A0 ");
3732
+ return chalk9.yellow(" \u26A0 ");
2774
3733
  case "fail":
2775
- return chalk8.red(" \u2717 ");
3734
+ return chalk9.red(" \u2717 ");
2776
3735
  }
2777
3736
  }
2778
3737
  function colorMessage(status, msg) {
2779
3738
  switch (status) {
2780
3739
  case "pass":
2781
- return chalk8.white(msg);
3740
+ return chalk9.white(msg);
2782
3741
  case "warn":
2783
- return chalk8.yellow(msg);
3742
+ return chalk9.yellow(msg);
2784
3743
  case "fail":
2785
- return chalk8.red(msg);
3744
+ return chalk9.red(msg);
2786
3745
  }
2787
3746
  }
2788
3747
  async function checkRuntimeAvailability(preferred) {
@@ -3007,10 +3966,10 @@ async function checkSyncHealth(serviceName, ports) {
3007
3966
  };
3008
3967
  }
3009
3968
  }
3010
- var doctorCommand = new Command8("doctor").description("Diagnose common Horus issues").action(async () => {
3969
+ var doctorCommand = new Command9("doctor").description("Diagnose common Horus issues").action(async () => {
3011
3970
  console.log("");
3012
- console.log(chalk8.bold("Horus Doctor"));
3013
- console.log(chalk8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3971
+ console.log(chalk9.bold("Horus Doctor"));
3972
+ console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3014
3973
  const allResults = [];
3015
3974
  const config = configExists() ? loadConfig() : null;
3016
3975
  allResults.push(await checkRuntimeAvailability(config?.runtime));
@@ -3052,20 +4011,20 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
3052
4011
  }
3053
4012
  const errors = allResults.filter((r) => r.status === "fail");
3054
4013
  const warnings = allResults.filter((r) => r.status === "warn");
3055
- console.log(chalk8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4014
+ console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3056
4015
  if (errors.length === 0 && warnings.length === 0) {
3057
- console.log(chalk8.green(" All checks passed."));
4016
+ console.log(chalk9.green(" All checks passed."));
3058
4017
  } else {
3059
4018
  const parts = [];
3060
- if (errors.length > 0) parts.push(chalk8.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`));
3061
- if (warnings.length > 0) parts.push(chalk8.yellow(`${warnings.length} warning${warnings.length > 1 ? "s" : ""}`));
4019
+ if (errors.length > 0) parts.push(chalk9.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`));
4020
+ if (warnings.length > 0) parts.push(chalk9.yellow(`${warnings.length} warning${warnings.length > 1 ? "s" : ""}`));
3062
4021
  console.log(` ${parts.join(", ")}`);
3063
4022
  const withHints = [...errors, ...warnings].filter((r) => r.hint);
3064
4023
  if (withHints.length > 0) {
3065
4024
  console.log("");
3066
4025
  for (const r of withHints) {
3067
- const icon = r.status === "fail" ? chalk8.red("\u2717") : chalk8.yellow("\u26A0");
3068
- console.log(` ${icon} ${chalk8.dim(r.hint)}`);
4026
+ const icon = r.status === "fail" ? chalk9.red("\u2717") : chalk9.yellow("\u26A0");
4027
+ console.log(` ${icon} ${chalk9.dim(r.hint)}`);
3069
4028
  }
3070
4029
  }
3071
4030
  }
@@ -3076,8 +4035,8 @@ var doctorCommand = new Command8("doctor").description("Diagnose common Horus is
3076
4035
  });
3077
4036
 
3078
4037
  // src/commands/backup.ts
3079
- import { Command as Command9 } from "commander";
3080
- import chalk9 from "chalk";
4038
+ import { Command as Command10 } from "commander";
4039
+ import chalk10 from "chalk";
3081
4040
  import ora7 from "ora";
3082
4041
  import { confirm as confirm4 } from "@inquirer/prompts";
3083
4042
  import { mkdirSync as mkdirSync5, statSync as statSync2, existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
@@ -3096,15 +4055,15 @@ function formatBytes(bytes) {
3096
4055
  }
3097
4056
  async function createBackup(yes) {
3098
4057
  console.log("");
3099
- console.log(chalk9.bold("Horus Backup"));
3100
- console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4058
+ console.log(chalk10.bold("Horus Backup"));
4059
+ console.log(chalk10.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3101
4060
  console.log("");
3102
4061
  const config = loadConfig();
3103
4062
  const runtimeSpinner = ora7("Detecting runtime...").start();
3104
4063
  let runtime;
3105
4064
  try {
3106
4065
  runtime = await detectRuntime(config.runtime);
3107
- runtimeSpinner.succeed(`Using ${chalk9.cyan(runtime.name)}`);
4066
+ runtimeSpinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
3108
4067
  } catch (error) {
3109
4068
  runtimeSpinner.fail("No container runtime found");
3110
4069
  console.log(error.message);
@@ -3116,7 +4075,7 @@ async function createBackup(yes) {
3116
4075
  default: true
3117
4076
  });
3118
4077
  if (!confirmed) {
3119
- console.log(chalk9.dim("Backup cancelled."));
4078
+ console.log(chalk10.dim("Backup cancelled."));
3120
4079
  return;
3121
4080
  }
3122
4081
  }
@@ -3126,7 +4085,7 @@ async function createBackup(yes) {
3126
4085
  stopSpinner.succeed("Services stopped");
3127
4086
  } catch (error) {
3128
4087
  stopSpinner.fail("Failed to stop services");
3129
- console.log(chalk9.dim(error.message));
4088
+ console.log(chalk10.dim(error.message));
3130
4089
  process.exit(1);
3131
4090
  }
3132
4091
  ensureBackupsDir();
@@ -3138,10 +4097,10 @@ async function createBackup(yes) {
3138
4097
  execSync3(`tar -czf "${tarFile}" -C "${HORUS_DIR}" data/`, {
3139
4098
  stdio: "pipe"
3140
4099
  });
3141
- backupSpinner.succeed(`Archive created: ${chalk9.dim(tarFile)}`);
4100
+ backupSpinner.succeed(`Archive created: ${chalk10.dim(tarFile)}`);
3142
4101
  } catch (error) {
3143
4102
  backupSpinner.fail("Failed to create backup archive");
3144
- console.log(chalk9.dim(error.message));
4103
+ console.log(chalk10.dim(error.message));
3145
4104
  await composeStreaming(runtime, ["start"]).catch(() => {
3146
4105
  });
3147
4106
  process.exit(1);
@@ -3164,25 +4123,25 @@ async function createBackup(yes) {
3164
4123
  startSpinner.succeed("Services restarted");
3165
4124
  } catch (error) {
3166
4125
  startSpinner.fail("Failed to restart services");
3167
- console.log(chalk9.dim(error.message));
3168
- console.log(chalk9.yellow("Run `horus up` to restart services manually."));
4126
+ console.log(chalk10.dim(error.message));
4127
+ console.log(chalk10.yellow("Run `horus up` to restart services manually."));
3169
4128
  }
3170
4129
  console.log("");
3171
- console.log(chalk9.bold.green("Backup complete!"));
3172
- console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3173
- console.log(` ${chalk9.bold("File:")} ${tarFile}`);
3174
- console.log(` ${chalk9.bold("Size:")} ${formatBytes(sizeBytes)}`);
4130
+ console.log(chalk10.bold.green("Backup complete!"));
4131
+ console.log(chalk10.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4132
+ console.log(` ${chalk10.bold("File:")} ${tarFile}`);
4133
+ console.log(` ${chalk10.bold("Size:")} ${formatBytes(sizeBytes)}`);
3175
4134
  console.log("");
3176
- console.log(chalk9.dim(" Restore with: horus backup restore <file>"));
4135
+ console.log(chalk10.dim(" Restore with: horus backup restore <file>"));
3177
4136
  console.log("");
3178
4137
  }
3179
4138
  async function restoreBackup(file, yes) {
3180
4139
  console.log("");
3181
- console.log(chalk9.bold("Horus Restore"));
3182
- console.log(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4140
+ console.log(chalk10.bold("Horus Restore"));
4141
+ console.log(chalk10.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3183
4142
  console.log("");
3184
4143
  if (!existsSync8(file)) {
3185
- console.log(chalk9.red(`Backup file not found: ${file}`));
4144
+ console.log(chalk10.red(`Backup file not found: ${file}`));
3186
4145
  process.exit(1);
3187
4146
  }
3188
4147
  const config = loadConfig();
@@ -3190,21 +4149,21 @@ async function restoreBackup(file, yes) {
3190
4149
  let runtime;
3191
4150
  try {
3192
4151
  runtime = await detectRuntime(config.runtime);
3193
- runtimeSpinner.succeed(`Using ${chalk9.cyan(runtime.name)}`);
4152
+ runtimeSpinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
3194
4153
  } catch (error) {
3195
4154
  runtimeSpinner.fail("No container runtime found");
3196
4155
  console.log(error.message);
3197
4156
  process.exit(1);
3198
4157
  }
3199
4158
  if (!yes) {
3200
- console.log(chalk9.yellow(` Warning: This will overwrite current data in ${config.data_dir}`));
4159
+ console.log(chalk10.yellow(` Warning: This will overwrite current data in ${config.data_dir}`));
3201
4160
  console.log("");
3202
4161
  const confirmed = await confirm4({
3203
4162
  message: `Restore from ${basename(file)}? Current data will be overwritten.`,
3204
4163
  default: false
3205
4164
  });
3206
4165
  if (!confirmed) {
3207
- console.log(chalk9.dim("Restore cancelled."));
4166
+ console.log(chalk10.dim("Restore cancelled."));
3208
4167
  return;
3209
4168
  }
3210
4169
  }
@@ -3214,7 +4173,7 @@ async function restoreBackup(file, yes) {
3214
4173
  stopSpinner.succeed("Services stopped");
3215
4174
  } catch (error) {
3216
4175
  stopSpinner.fail("Failed to stop services");
3217
- console.log(chalk9.dim(error.message));
4176
+ console.log(chalk10.dim(error.message));
3218
4177
  process.exit(1);
3219
4178
  }
3220
4179
  const extractSpinner = ora7("Extracting backup...").start();
@@ -3223,18 +4182,18 @@ async function restoreBackup(file, yes) {
3223
4182
  extractSpinner.succeed("Backup extracted");
3224
4183
  } catch (error) {
3225
4184
  extractSpinner.fail("Failed to extract backup");
3226
- console.log(chalk9.dim(error.message));
4185
+ console.log(chalk10.dim(error.message));
3227
4186
  await composeStreaming(runtime, ["start"]).catch(() => {
3228
4187
  });
3229
4188
  process.exit(1);
3230
4189
  }
3231
4190
  console.log("");
3232
- console.log(chalk9.bold("Starting services..."));
4191
+ console.log(chalk10.bold("Starting services..."));
3233
4192
  try {
3234
4193
  await composeStreaming(runtime, ["start"]);
3235
4194
  } catch (error) {
3236
- console.log(chalk9.red("Failed to start services."));
3237
- console.log(chalk9.dim(error.message));
4195
+ console.log(chalk10.red("Failed to start services."));
4196
+ console.log(chalk10.dim(error.message));
3238
4197
  process.exit(1);
3239
4198
  }
3240
4199
  console.log("");
@@ -3244,7 +4203,7 @@ async function restoreBackup(file, yes) {
3244
4203
  runtime,
3245
4204
  (current) => {
3246
4205
  const summary = current.map((s) => {
3247
- const icon = s.status === "healthy" ? chalk9.green("*") : s.status === "starting" ? chalk9.yellow("~") : chalk9.red("x");
4206
+ const icon = s.status === "healthy" ? chalk10.green("*") : s.status === "starting" ? chalk10.yellow("~") : chalk10.red("x");
3248
4207
  return `${icon} ${s.name}`;
3249
4208
  }).join(" ");
3250
4209
  healthSpinner.text = `Waiting for services... ${summary}`;
@@ -3255,14 +4214,14 @@ async function restoreBackup(file, yes) {
3255
4214
  healthSpinner.succeed("All services healthy");
3256
4215
  } catch (error) {
3257
4216
  healthSpinner.fail("Some services did not become healthy");
3258
- console.log(chalk9.dim(error.message));
4217
+ console.log(chalk10.dim(error.message));
3259
4218
  process.exit(1);
3260
4219
  }
3261
4220
  console.log("");
3262
- console.log(chalk9.bold.green("Restore complete!"));
4221
+ console.log(chalk10.bold.green("Restore complete!"));
3263
4222
  console.log("");
3264
4223
  }
3265
- var backupCommand = new Command9("backup").description("Backup or restore Horus data").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
4224
+ var backupCommand = new Command10("backup").description("Backup or restore Horus data").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
3266
4225
  await createBackup(opts.yes);
3267
4226
  });
3268
4227
  backupCommand.command("restore <file>").description("Restore Horus data from a backup file").option("-y, --yes", "Skip confirmation prompts").action(async (file, opts) => {
@@ -3270,8 +4229,8 @@ backupCommand.command("restore <file>").description("Restore Horus data from a b
3270
4229
  });
3271
4230
 
3272
4231
  // src/commands/test-env.ts
3273
- import { Command as Command10 } from "commander";
3274
- import chalk10 from "chalk";
4232
+ import { Command as Command11 } from "commander";
4233
+ import chalk11 from "chalk";
3275
4234
  import ora8 from "ora";
3276
4235
  import { join as join8 } from "path";
3277
4236
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -3724,7 +4683,7 @@ function projectName(slot) {
3724
4683
  }
3725
4684
 
3726
4685
  // src/commands/test-env.ts
3727
- var testEnvCommand = new Command10("test-env").description("Manage isolated shadow stacks for integration testing");
4686
+ var testEnvCommand = new Command11("test-env").description("Manage isolated shadow stacks for integration testing");
3728
4687
  testEnvCommand.command("acquire").description("Start a shadow stack on alternate ports with isolated data").option("--timeout <seconds>", "Max wait for health checks (default: 120)", "120").option("--image <overrides...>", "Override service images (format: service=image:tag)").option(
3729
4688
  "--standalone",
3730
4689
  "Generate a fully-projected compose file (no overlay-merge). Eliminates port-collision with a live stack. Required on fresh VMs."
@@ -3736,6 +4695,11 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3736
4695
  const mkSpinner = (text) => ora8({ text, stream: jsonMode ? process.stderr : process.stdout });
3737
4696
  const config = loadConfig();
3738
4697
  const dataDir = config.data_dir;
4698
+ if (!config.vaults || Object.keys(config.vaults).length === 0) {
4699
+ config.vaults = {
4700
+ default: { repo: "", default: true }
4701
+ };
4702
+ }
3739
4703
  const testCfg = loadTestEnvConfig(dataDir);
3740
4704
  const vaultNames = Object.keys(config.vaults).sort();
3741
4705
  const defaultVaultEntry = Object.entries(config.vaults).find(([, v]) => v.default);
@@ -3744,7 +4708,7 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3744
4708
  let runtime;
3745
4709
  try {
3746
4710
  runtime = await detectRuntime(config.runtime);
3747
- spinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
4711
+ spinner.succeed(`Using ${chalk11.cyan(runtime.name)}`);
3748
4712
  } catch (error) {
3749
4713
  spinner.fail("No container runtime found");
3750
4714
  console.error(error.message);
@@ -3752,8 +4716,8 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3752
4716
  }
3753
4717
  const slot = findFreeSlot(dataDir, testCfg);
3754
4718
  if (slot === null) {
3755
- console.error(chalk10.red(
3756
- `All ${testCfg.max_slots} slot(s) are in use. Run ${chalk10.bold("horus test-env status")} to see active slots, or ${chalk10.bold("horus test-env release")} to free one.`
4719
+ console.error(chalk11.red(
4720
+ `All ${testCfg.max_slots} slot(s) are in use. Run ${chalk11.bold("horus test-env status")} to see active slots, or ${chalk11.bold("horus test-env release")} to free one.`
3757
4721
  ));
3758
4722
  process.exit(1);
3759
4723
  }
@@ -3762,7 +4726,7 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3762
4726
  const project = projectName(slot);
3763
4727
  const dirSpinner = mkSpinner(`Creating slot-${slot} data directories...`).start();
3764
4728
  createSlotDirs(slotDataPath, vaultNames);
3765
- dirSpinner.succeed(`Data directory: ${chalk10.dim(slotDataPath)}`);
4729
+ dirSpinner.succeed(`Data directory: ${chalk11.dim(slotDataPath)}`);
3766
4730
  const seedSpinner = mkSpinner("Pre-seeding git repos...").start();
3767
4731
  try {
3768
4732
  await preSeedNotesDir(dataDir, slotDataPath);
@@ -3787,7 +4751,7 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3787
4751
  for (const entry of opts.image) {
3788
4752
  const eqIdx = entry.indexOf("=");
3789
4753
  if (eqIdx < 1) {
3790
- console.error(chalk10.red(`Invalid --image format: "${entry}". Expected: service=image:tag`));
4754
+ console.error(chalk11.red(`Invalid --image format: "${entry}". Expected: service=image:tag`));
3791
4755
  process.exit(1);
3792
4756
  }
3793
4757
  imageOverrides[entry.slice(0, eqIdx)] = entry.slice(eqIdx + 1);
@@ -3796,7 +4760,7 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3796
4760
  const standaloneMode = Boolean(opts.standalone);
3797
4761
  const modeLabel = standaloneMode ? "standalone" : "overlay";
3798
4762
  const upSpinner = mkSpinner(
3799
- `Starting shadow stack (project ${chalk10.cyan(project)}, mode: ${chalk10.dim(modeLabel)})...`
4763
+ `Starting shadow stack (project ${chalk11.cyan(project)}, mode: ${chalk11.dim(modeLabel)})...`
3800
4764
  ).start();
3801
4765
  try {
3802
4766
  if (standaloneMode) {
@@ -3816,7 +4780,7 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3816
4780
  const timeoutMs = parseInt(opts.timeout, 10) * 1e3;
3817
4781
  try {
3818
4782
  await waitForShadowStackHealthy(runtime, project, timeoutMs, 3e3, (statuses) => {
3819
- const parts = Object.entries(statuses).map(([svc, s]) => `${svc}:${s === "healthy" ? chalk10.green(s) : chalk10.yellow(s)}`).join(" ");
4783
+ const parts = Object.entries(statuses).map(([svc, s]) => `${svc}:${s === "healthy" ? chalk11.green(s) : chalk11.yellow(s)}`).join(" ");
3820
4784
  healthSpinner.text = `Waiting for services... ${parts}`;
3821
4785
  });
3822
4786
  healthSpinner.succeed("All services healthy");
@@ -3844,22 +4808,22 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3844
4808
  return;
3845
4809
  }
3846
4810
  console.log("");
3847
- console.log(chalk10.bold.green(`\u2713 Slot ${slot} acquired`));
4811
+ console.log(chalk11.bold.green(`\u2713 Slot ${slot} acquired`));
3848
4812
  console.log("");
3849
- console.log(chalk10.bold("Connection info:"));
3850
- console.log(` Slot: ${chalk10.cyan(slot)}`);
3851
- console.log(` Project: ${chalk10.cyan(project)}`);
3852
- console.log(` Data: ${chalk10.dim(slotDataPath)}`);
4813
+ console.log(chalk11.bold("Connection info:"));
4814
+ console.log(` Slot: ${chalk11.cyan(slot)}`);
4815
+ console.log(` Project: ${chalk11.cyan(project)}`);
4816
+ console.log(` Data: ${chalk11.dim(slotDataPath)}`);
3853
4817
  console.log("");
3854
- console.log(chalk10.bold("Ports:"));
3855
- console.log(` Anvil: http://localhost:${chalk10.cyan(ports.anvil)}`);
3856
- console.log(` Forge: http://localhost:${chalk10.cyan(ports.forge)}`);
3857
- console.log(` Vault MCP: http://localhost:${chalk10.cyan(ports.vault_mcp)}`);
3858
- console.log(` Vault Router: http://localhost:${chalk10.cyan(ports.vault_router)}`);
3859
- console.log(` Typesense: http://localhost:${chalk10.cyan(ports.typesense)}`);
3860
- console.log(` UI: http://localhost:${chalk10.cyan(ports.ui)}`);
4818
+ console.log(chalk11.bold("Ports:"));
4819
+ console.log(` Anvil: http://localhost:${chalk11.cyan(ports.anvil)}`);
4820
+ console.log(` Forge: http://localhost:${chalk11.cyan(ports.forge)}`);
4821
+ console.log(` Vault MCP: http://localhost:${chalk11.cyan(ports.vault_mcp)}`);
4822
+ console.log(` Vault Router: http://localhost:${chalk11.cyan(ports.vault_router)}`);
4823
+ console.log(` Typesense: http://localhost:${chalk11.cyan(ports.typesense)}`);
4824
+ console.log(` UI: http://localhost:${chalk11.cyan(ports.ui)}`);
3861
4825
  console.log("");
3862
- console.log(chalk10.bold("Environment:"));
4826
+ console.log(chalk11.bold("Environment:"));
3863
4827
  console.log(` export TEST_SLOT=${slot}`);
3864
4828
  console.log(` export TEST_ANVIL_URL=http://localhost:${ports.anvil}`);
3865
4829
  console.log(` export TEST_FORGE_URL=http://localhost:${ports.forge}`);
@@ -3867,16 +4831,21 @@ testEnvCommand.command("acquire").description("Start a shadow stack on alternate
3867
4831
  console.log(` export TEST_DATA_PATH=${slotDataPath}`);
3868
4832
  console.log("");
3869
4833
  const settingsPath = join8(slotDataPath, "claude-settings.json");
3870
- console.log(chalk10.bold("Agent dev mode:"));
3871
- console.log(` MCP config: ${chalk10.dim(settingsPath)}`);
3872
- console.log(` Launch: ${chalk10.cyan(`claude --mcp-config ${settingsPath}`)}`);
4834
+ console.log(chalk11.bold("Agent dev mode:"));
4835
+ console.log(` MCP config: ${chalk11.dim(settingsPath)}`);
4836
+ console.log(` Launch: ${chalk11.cyan(`claude --mcp-config ${settingsPath}`)}`);
3873
4837
  console.log("");
3874
- console.log(chalk10.dim(`Run ${chalk10.bold(`horus test-env seed --slot ${slot}`)} to populate with fixtures.`));
3875
- console.log(chalk10.dim(`Run ${chalk10.bold(`horus test-env release --slot ${slot}`)} when done.`));
4838
+ console.log(chalk11.dim(`Run ${chalk11.bold(`horus test-env seed --slot ${slot}`)} to populate with fixtures.`));
4839
+ console.log(chalk11.dim(`Run ${chalk11.bold(`horus test-env release --slot ${slot}`)} when done.`));
3876
4840
  });
3877
4841
  testEnvCommand.command("release").description("Tear down a shadow stack and remove its data").option("--slot <n>", "Slot number to release (default: auto-detect acquired slot)").action(async (opts) => {
3878
4842
  const config = loadConfig();
3879
4843
  const dataDir = config.data_dir;
4844
+ if (!config.vaults || Object.keys(config.vaults).length === 0) {
4845
+ config.vaults = {
4846
+ default: { repo: "", default: true }
4847
+ };
4848
+ }
3880
4849
  const testCfg = loadTestEnvConfig(dataDir);
3881
4850
  const vaultNames = Object.keys(config.vaults).sort();
3882
4851
  const defaultVaultEntry = Object.entries(config.vaults).find(([, v]) => v.default);
@@ -3888,7 +4857,7 @@ testEnvCommand.command("release").description("Tear down a shadow stack and remo
3888
4857
  const statuses = getAllSlotStatuses(dataDir, testCfg);
3889
4858
  const acquired = statuses.find((s) => s.state === "acquired" || s.state === "expired");
3890
4859
  if (!acquired) {
3891
- console.log(chalk10.yellow("No active slots found."));
4860
+ console.log(chalk11.yellow("No active slots found."));
3892
4861
  return;
3893
4862
  }
3894
4863
  slot = acquired.slot;
@@ -3901,13 +4870,13 @@ testEnvCommand.command("release").description("Tear down a shadow stack and remo
3901
4870
  let runtime;
3902
4871
  try {
3903
4872
  runtime = await detectRuntime(config.runtime);
3904
- spinner.succeed(`Using ${chalk10.cyan(runtime.name)}`);
4873
+ spinner.succeed(`Using ${chalk11.cyan(runtime.name)}`);
3905
4874
  } catch (error) {
3906
4875
  spinner.fail("No container runtime found");
3907
4876
  console.error(error.message);
3908
4877
  process.exit(1);
3909
4878
  }
3910
- const downSpinner = ora8(`Stopping ${chalk10.cyan(project)}...`).start();
4879
+ const downSpinner = ora8(`Stopping ${chalk11.cyan(project)}...`).start();
3911
4880
  try {
3912
4881
  const { existsSync: existsSync12 } = await import("fs");
3913
4882
  const { join: join11 } = await import("path");
@@ -3926,7 +4895,7 @@ testEnvCommand.command("release").description("Tear down a shadow stack and remo
3926
4895
  removeLock(dataDir, slot);
3927
4896
  cleanSpinner.succeed("Test data removed");
3928
4897
  console.log("");
3929
- console.log(chalk10.bold.green(`\u2713 Slot ${slot} released`));
4898
+ console.log(chalk11.bold.green(`\u2713 Slot ${slot} released`));
3930
4899
  });
3931
4900
  testEnvCommand.command("status").description("Show active shadow stack slots").action(() => {
3932
4901
  const config = loadConfig();
@@ -3935,20 +4904,20 @@ testEnvCommand.command("status").description("Show active shadow stack slots").a
3935
4904
  const statuses = getAllSlotStatuses(dataDir, testCfg);
3936
4905
  const acquiredCount = statuses.filter((s) => s.state === "acquired").length;
3937
4906
  console.log("");
3938
- console.log(chalk10.bold("Test Environment Status"));
4907
+ console.log(chalk11.bold("Test Environment Status"));
3939
4908
  console.log(` Max slots: ${testCfg.max_slots}`);
3940
4909
  console.log(` In use: ${acquiredCount} / ${testCfg.max_slots}`);
3941
4910
  console.log(` Base port: ${testCfg.base_port}`);
3942
4911
  console.log("");
3943
4912
  if (statuses.every((s) => s.state === "free")) {
3944
- console.log(chalk10.dim(" No active slots."));
4913
+ console.log(chalk11.dim(" No active slots."));
3945
4914
  console.log("");
3946
4915
  return;
3947
4916
  }
3948
4917
  for (const s of statuses) {
3949
4918
  if (s.state === "free") continue;
3950
- const stateLabel = s.state === "expired" ? chalk10.yellow("EXPIRED") : chalk10.green("ACTIVE");
3951
- console.log(` ${chalk10.bold(`Slot ${s.slot}`)} ${stateLabel}`);
4919
+ const stateLabel = s.state === "expired" ? chalk11.yellow("EXPIRED") : chalk11.green("ACTIVE");
4920
+ console.log(` ${chalk11.bold(`Slot ${s.slot}`)} ${stateLabel}`);
3952
4921
  if (s.acquiredAt) {
3953
4922
  console.log(` Acquired: ${s.acquiredAt} (${s.elapsedMinutes}m ago)`);
3954
4923
  }
@@ -3956,7 +4925,7 @@ testEnvCommand.command("status").description("Show active shadow stack slots").a
3956
4925
  console.log(` Ports: anvil=${s.ports.anvil} forge=${s.ports.forge} vault-mcp=${s.ports.vault_mcp} typesense=${s.ports.typesense}`);
3957
4926
  }
3958
4927
  if (s.dataPath) {
3959
- console.log(` Data: ${chalk10.dim(s.dataPath)}`);
4928
+ console.log(` Data: ${chalk11.dim(s.dataPath)}`);
3960
4929
  }
3961
4930
  console.log("");
3962
4931
  }
@@ -3972,7 +4941,7 @@ testEnvCommand.command("seed").description("Populate a slot with test fixtures (
3972
4941
  const statuses = getAllSlotStatuses(dataDir, testCfg);
3973
4942
  const acquired = statuses.find((s) => s.state === "acquired");
3974
4943
  if (!acquired) {
3975
- console.error(chalk10.red("No active slot found. Run `horus test-env acquire` first."));
4944
+ console.error(chalk11.red("No active slot found. Run `horus test-env acquire` first."));
3976
4945
  process.exit(1);
3977
4946
  }
3978
4947
  slot = acquired.slot;
@@ -4003,12 +4972,12 @@ testEnvCommand.command("seed").description("Populate a slot with test fixtures (
4003
4972
  }
4004
4973
  }
4005
4974
  console.log("");
4006
- console.log(chalk10.dim("Services will re-index automatically. Allow ~10s before running tests."));
4975
+ console.log(chalk11.dim("Services will re-index automatically. Allow ~10s before running tests."));
4007
4976
  });
4008
4977
 
4009
4978
  // src/commands/help.ts
4010
- import { Command as Command11 } from "commander";
4011
- import chalk11 from "chalk";
4979
+ import { Command as Command12 } from "commander";
4980
+ import chalk12 from "chalk";
4012
4981
  import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
4013
4982
  import { join as join9, dirname as dirname3 } from "path";
4014
4983
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -4185,47 +5154,47 @@ function stripFrontMatter(content) {
4185
5154
  }
4186
5155
  function printTopicIndex(index) {
4187
5156
  console.log("");
4188
- console.log(chalk11.bold("Horus Help \u2014 Available Guides"));
4189
- console.log(chalk11.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5157
+ console.log(chalk12.bold("Horus Help \u2014 Available Guides"));
5158
+ console.log(chalk12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4190
5159
  console.log("");
4191
5160
  for (const g of index.guides) {
4192
- console.log(` ${chalk11.cyan(g.slug.padEnd(20))} ${g.title}`);
4193
- console.log(` ${" ".repeat(20)} ${chalk11.dim(g.description)}`);
5161
+ console.log(` ${chalk12.cyan(g.slug.padEnd(20))} ${g.title}`);
5162
+ console.log(` ${" ".repeat(20)} ${chalk12.dim(g.description)}`);
4194
5163
  console.log("");
4195
5164
  }
4196
- console.log(chalk11.dim("Example queries:"));
4197
- console.log(chalk11.dim(" horus help how do I start"));
4198
- console.log(chalk11.dim(" horus help what is a forge workspace"));
4199
- console.log(chalk11.dim(" horus help create my first anvil note"));
5165
+ console.log(chalk12.dim("Example queries:"));
5166
+ console.log(chalk12.dim(" horus help how do I start"));
5167
+ console.log(chalk12.dim(" horus help what is a forge workspace"));
5168
+ console.log(chalk12.dim(" horus help create my first anvil note"));
4200
5169
  console.log("");
4201
- console.log(chalk11.dim("To print a specific guide directly:"));
4202
- console.log(chalk11.dim(" horus guide <slug>"));
5170
+ console.log(chalk12.dim("To print a specific guide directly:"));
5171
+ console.log(chalk12.dim(" horus guide <slug>"));
4203
5172
  console.log("");
4204
5173
  }
4205
5174
  function printGuideBody(guidesDir, file) {
4206
- const path = join9(guidesDir, file);
4207
- const content = readFileSync7(path, "utf8");
5175
+ const path2 = join9(guidesDir, file);
5176
+ const content = readFileSync7(path2, "utf8");
4208
5177
  console.log(stripFrontMatter(content));
4209
5178
  }
4210
5179
  function printSeeAlso(alternates, guidesDir) {
4211
5180
  if (alternates.length === 0) return;
4212
- console.log(chalk11.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4213
- console.log(chalk11.bold("See also:"));
5181
+ console.log(chalk12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5182
+ console.log(chalk12.bold("See also:"));
4214
5183
  for (const a of alternates) {
4215
- console.log(` ${chalk11.cyan(a.slug.padEnd(20))} ${a.title}`);
4216
- console.log(` ${" ".repeat(20)} ${chalk11.dim(join9(guidesDir, a.file))}`);
5184
+ console.log(` ${chalk12.cyan(a.slug.padEnd(20))} ${a.title}`);
5185
+ console.log(` ${" ".repeat(20)} ${chalk12.dim(join9(guidesDir, a.file))}`);
4217
5186
  }
4218
5187
  console.log("");
4219
5188
  }
4220
5189
  function printFooter() {
4221
5190
  console.log(
4222
- chalk11.dim(
5191
+ chalk12.dim(
4223
5192
  "Run `horus guide <slug>` to print a specific guide without retrieval, or `horus guide` to list all."
4224
5193
  )
4225
5194
  );
4226
5195
  console.log("");
4227
5196
  }
4228
- var helpCommand = new Command11("help").description("Search and print bundled Horus getting-started guides").argument("[query...]", "Natural-language query. Omit to see the topic index.").action((query) => {
5197
+ var helpCommand = new Command12("help").description("Search and print bundled Horus getting-started guides").argument("[query...]", "Natural-language query. Omit to see the topic index.").action((query) => {
4229
5198
  const guidesDir = findGuidesDir();
4230
5199
  const index = loadIndex(guidesDir);
4231
5200
  if (!query || query.length === 0) {
@@ -4236,15 +5205,15 @@ var helpCommand = new Command11("help").description("Search and print bundled Ho
4236
5205
  const result = retrieve(queryStr, index, 3);
4237
5206
  if (!result.primary) {
4238
5207
  console.log("");
4239
- console.log(chalk11.yellow(`No guide matched "${queryStr}".`));
5208
+ console.log(chalk12.yellow(`No guide matched "${queryStr}".`));
4240
5209
  console.log("");
4241
- console.log(chalk11.dim("Try `horus help` with no arguments to see the full topic index,"));
4242
- console.log(chalk11.dim("or pick a slug directly with `horus guide <slug>`."));
5210
+ console.log(chalk12.dim("Try `horus help` with no arguments to see the full topic index,"));
5211
+ console.log(chalk12.dim("or pick a slug directly with `horus guide <slug>`."));
4243
5212
  console.log("");
4244
5213
  return;
4245
5214
  }
4246
5215
  console.log("");
4247
- console.log(chalk11.dim(`# ${result.primary.title} (${result.primary.slug})`));
5216
+ console.log(chalk12.dim(`# ${result.primary.title} (${result.primary.slug})`));
4248
5217
  console.log("");
4249
5218
  printGuideBody(guidesDir, result.primary.file);
4250
5219
  console.log("");
@@ -4253,8 +5222,8 @@ var helpCommand = new Command11("help").description("Search and print bundled Ho
4253
5222
  });
4254
5223
 
4255
5224
  // src/commands/guide.ts
4256
- import { Command as Command12 } from "commander";
4257
- import chalk12 from "chalk";
5225
+ import { Command as Command13 } from "commander";
5226
+ import chalk13 from "chalk";
4258
5227
  import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
4259
5228
  import { join as join10, dirname as dirname4 } from "path";
4260
5229
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -4293,17 +5262,17 @@ function lookupTopic(topic, index) {
4293
5262
  }
4294
5263
  function printGuideList(index) {
4295
5264
  console.log("");
4296
- console.log(chalk12.bold("Bundled Horus Guides"));
4297
- console.log(chalk12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5265
+ console.log(chalk13.bold("Bundled Horus Guides"));
5266
+ console.log(chalk13.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4298
5267
  console.log("");
4299
5268
  for (const g of index.guides) {
4300
- console.log(` ${chalk12.cyan(g.slug.padEnd(20))} ${g.title}`);
4301
- console.log(` ${" ".repeat(20)} ${chalk12.dim(g.description)}`);
5269
+ console.log(` ${chalk13.cyan(g.slug.padEnd(20))} ${g.title}`);
5270
+ console.log(` ${" ".repeat(20)} ${chalk13.dim(g.description)}`);
4302
5271
  console.log("");
4303
5272
  }
4304
- console.log(chalk12.dim("Print a guide: horus guide <slug>"));
4305
- console.log(chalk12.dim("Print a guide file path: horus guide <slug> --path"));
4306
- console.log(chalk12.dim("Print the guides root: horus guide --path"));
5273
+ console.log(chalk13.dim("Print a guide: horus guide <slug>"));
5274
+ console.log(chalk13.dim("Print a guide file path: horus guide <slug> --path"));
5275
+ console.log(chalk13.dim("Print the guides root: horus guide --path"));
4307
5276
  console.log("");
4308
5277
  }
4309
5278
  function printGuideBody2(guidesDir, file) {
@@ -4312,16 +5281,16 @@ function printGuideBody2(guidesDir, file) {
4312
5281
  }
4313
5282
  function printDisambiguation(tier, matches) {
4314
5283
  console.log("");
4315
- console.log(chalk12.yellow(`Multiple guides matched (tier: ${tier}):`));
5284
+ console.log(chalk13.yellow(`Multiple guides matched (tier: ${tier}):`));
4316
5285
  console.log("");
4317
5286
  for (const m of matches) {
4318
- console.log(` ${chalk12.cyan(m.slug.padEnd(20))} ${m.title}`);
5287
+ console.log(` ${chalk13.cyan(m.slug.padEnd(20))} ${m.title}`);
4319
5288
  }
4320
5289
  console.log("");
4321
- console.log(chalk12.dim("Pick one: horus guide <slug>"));
5290
+ console.log(chalk13.dim("Pick one: horus guide <slug>"));
4322
5291
  console.log("");
4323
5292
  }
4324
- var guideCommand = new Command12("guide").description("Print a bundled Horus guide, or list all guides").argument("[topic]", "Slug, slug prefix, or search term. Omit to list all guides.").option("--path", "Print the file path instead of the body (or the guides dir root if no topic)").action((topic, opts) => {
5293
+ var guideCommand = new Command13("guide").description("Print a bundled Horus guide, or list all guides").argument("[topic]", "Slug, slug prefix, or search term. Omit to list all guides.").option("--path", "Print the file path instead of the body (or the guides dir root if no topic)").action((topic, opts) => {
4325
5294
  const guidesDir = findGuidesDir2();
4326
5295
  const index = loadIndex2(guidesDir);
4327
5296
  if (!topic) {
@@ -4335,9 +5304,9 @@ var guideCommand = new Command12("guide").description("Print a bundled Horus gui
4335
5304
  const result = lookupTopic(topic, index);
4336
5305
  if (result.matches.length === 0) {
4337
5306
  console.log("");
4338
- console.log(chalk12.yellow(`No guide matched "${topic}".`));
5307
+ console.log(chalk13.yellow(`No guide matched "${topic}".`));
4339
5308
  console.log("");
4340
- console.log(chalk12.dim("Run `horus guide` to see all available guides."));
5309
+ console.log(chalk13.dim("Run `horus guide` to see all available guides."));
4341
5310
  console.log("");
4342
5311
  process.exitCode = 1;
4343
5312
  return;
@@ -4356,14 +5325,88 @@ var guideCommand = new Command12("guide").description("Print a bundled Horus gui
4356
5325
  });
4357
5326
 
4358
5327
  // src/commands/repo.ts
4359
- import { Command as Command13 } from "commander";
4360
- import chalk13 from "chalk";
5328
+ import { Command as Command14 } from "commander";
5329
+ import chalk14 from "chalk";
4361
5330
  import ora9 from "ora";
4362
- var repoCommand = new Command13("repo").description("Manage the Forge repository index");
5331
+ var import_core2 = __toESM(require_dist(), 1);
5332
+
5333
+ // src/lib/repo-migrate.ts
5334
+ var import_core = __toESM(require_dist(), 1);
5335
+ import { promises as fs } from "fs";
5336
+ import os from "os";
5337
+ import path from "path";
5338
+ function inferOrg(remoteUrl) {
5339
+ const sshMatch = remoteUrl.match(/git@[^:]+:([^/]+)\//);
5340
+ if (sshMatch) return sshMatch[1];
5341
+ const httpsMatch = remoteUrl.match(/https?:\/\/[^/]+\/([^/]+)\//);
5342
+ if (httpsMatch) return httpsMatch[1];
5343
+ return null;
5344
+ }
5345
+ function mapWorkflow(entry) {
5346
+ if (!entry.workflow) return void 0;
5347
+ const wf = entry.workflow;
5348
+ return {
5349
+ type: entry.workflow.type,
5350
+ pushTo: entry.workflow.pushTo,
5351
+ prTarget: entry.workflow.prTarget,
5352
+ branchPattern: entry.workflow.branchPattern,
5353
+ commitFormat: entry.workflow.commitFormat,
5354
+ mergeStrategy: wf["mergeStrategy"]
5355
+ };
5356
+ }
5357
+ async function migrateRepos(opts) {
5358
+ const fromPath = opts.from ? opts.from : path.join(os.homedir(), "Horus", "data", "config", "repos.json");
5359
+ const raw = await fs.readFile(fromPath, "utf8");
5360
+ const index = JSON.parse(raw);
5361
+ const entries = index.repos ?? [];
5362
+ const result = { migrated: [], skipped: [], failed: [] };
5363
+ for (const entry of entries) {
5364
+ if (!entry.remoteUrl) {
5365
+ result.skipped.push({ repo: entry.name, reason: "no remoteUrl" });
5366
+ continue;
5367
+ }
5368
+ const org = inferOrg(entry.remoteUrl);
5369
+ if (!org) {
5370
+ result.skipped.push({ repo: entry.name, reason: "could not infer org from remoteUrl" });
5371
+ continue;
5372
+ }
5373
+ const input2 = {
5374
+ org,
5375
+ name: entry.name,
5376
+ canonicalUrl: entry.remoteUrl,
5377
+ defaultBranch: entry.defaultBranch,
5378
+ language: entry.language ?? void 0,
5379
+ workflow: mapWorkflow(entry)
5380
+ };
5381
+ if (opts.dryRun) {
5382
+ result.migrated.push(`${org}/${entry.name} (dry-run)`);
5383
+ continue;
5384
+ }
5385
+ try {
5386
+ await opts.client.register(input2);
5387
+ result.migrated.push(`${org}/${entry.name}`);
5388
+ } catch (err) {
5389
+ if (err instanceof import_core.RepoExistsError) {
5390
+ result.skipped.push({ repo: `${org}/${entry.name}`, reason: "already registered" });
5391
+ } else {
5392
+ const msg = err.message ?? String(err);
5393
+ if (msg.includes("REPO_EXISTS") || err.code === "REPO_EXISTS") {
5394
+ result.skipped.push({ repo: `${org}/${entry.name}`, reason: "already registered" });
5395
+ } else {
5396
+ result.failed.push({ repo: `${org}/${entry.name}`, error: msg });
5397
+ }
5398
+ }
5399
+ }
5400
+ }
5401
+ return result;
5402
+ }
5403
+
5404
+ // src/commands/repo.ts
5405
+ var repoCommand = new Command14("repo").description("Manage the Forge repository index");
4363
5406
  repoCommand.command("rindex").alias("scan").description("Trigger a full repository index rescan via Forge").action(async () => {
4364
5407
  if (!configExists()) {
4365
- console.log(chalk13.red("Horus is not set up yet."));
4366
- console.log(chalk13.dim("Run `horus setup` first."));
5408
+ console.log(chalk14.red("Horus is not set up yet."));
5409
+ console.log(chalk14.dim("Run `horus setup` first."));
4367
5410
  process.exit(1);
4368
5411
  }
4369
5412
  const config = loadConfig();
@@ -4385,13 +5428,13 @@ repoCommand.command("rindex").alias("scan").description("Trigger a full reposito
4385
5428
  body = await res.text();
4386
5429
  if (!res.ok) {
4387
5430
  spinner.fail(`Forge returned HTTP ${res.status}`);
4388
- console.error(chalk13.red(body));
5431
+ console.error(chalk14.red(body));
4389
5432
  process.exit(1);
4390
5433
  }
4391
5434
  } catch (err) {
4392
5435
  spinner.fail("Could not reach Forge");
4393
- console.error(chalk13.red(`Is Horus running? (horus up)`));
4394
- console.error(chalk13.dim(err.message));
5436
+ console.error(chalk14.red(`Is Horus running? (horus up)`));
5437
+ console.error(chalk14.dim(err.message));
4395
5438
  process.exit(1);
4396
5439
  }
4397
5440
  let parsed;
@@ -4404,7 +5447,7 @@ repoCommand.command("rindex").alias("scan").description("Trigger a full reposito
4404
5447
  }
4405
5448
  if (parsed.error) {
4406
5449
  spinner.fail("Scan failed");
4407
- console.error(chalk13.red(parsed.error.message ?? JSON.stringify(parsed.error)));
5450
+ console.error(chalk14.red(parsed.error.message ?? JSON.stringify(parsed.error)));
4408
5451
  process.exit(1);
4409
5452
  }
4410
5453
  let result = {};
@@ -4415,19 +5458,318 @@ repoCommand.command("rindex").alias("scan").description("Trigger a full reposito
4415
5458
  }
4416
5459
  spinner.succeed("Repository scan complete");
4417
5460
  console.log("");
4418
- console.log(` ${chalk13.bold("Scan paths:")} ${(result.scanPaths ?? []).length}`);
4419
- console.log(` ${chalk13.bold("Repos found:")} ${result.reposFound ?? 0}`);
5461
+ console.log(` ${chalk14.bold("Scan paths:")} ${(result.scanPaths ?? []).length}`);
5462
+ console.log(` ${chalk14.bold("Repos found:")} ${result.reposFound ?? 0}`);
4420
5463
  if (result.repos && result.repos.length > 0) {
4421
5464
  console.log("");
4422
5465
  for (const repo of result.repos) {
4423
- console.log(` ${chalk13.green("\u2713")} ${chalk13.bold(repo.name)} ${chalk13.dim(repo.localPath)}`);
5466
+ console.log(` ${chalk14.green("\u2713")} ${chalk14.bold(repo.name)} ${chalk14.dim(repo.localPath)}`);
4424
5467
  }
4425
5468
  }
4426
5469
  console.log("");
4427
5470
  });
5471
+ repoCommand.command("migrate").description("Import existing repos.json entries into the shared repo registry").option("--from <path>", "Path to repos.json (default: ~/Horus/data/config/repos.json)").option("--registry <url>", "Shared registry base URL (overrides enterprise_registry_url)").option("--dry-run", "Preview what would be migrated without writing to the registry").action(async (opts) => {
5472
+ console.log("");
5473
+ console.log(chalk14.bold("Horus Repo Migrate"));
5474
+ console.log(chalk14.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5475
+ console.log("");
5476
+ if (!configExists()) {
5477
+ console.log(chalk14.red("Horus is not set up yet."));
5478
+ console.log(chalk14.dim("Run `horus setup` first."));
5479
+ process.exit(1);
5480
+ }
5481
+ const config = loadConfig();
5482
+ const registryUrl = opts.registry ?? config.enterprise_registry_url ?? "http://localhost:8744";
5483
+ const client = new import_core2.RepoRegistryClient({ baseUrl: registryUrl });
5484
+ if (opts.dryRun) {
5485
+ console.log(chalk14.yellow(" Dry-run mode \u2014 no changes will be written to the registry."));
5486
+ console.log("");
5487
+ }
5488
+ const spinner = ora9("Reading repos.json...").start();
5489
+ let migrateResult;
5490
+ try {
5491
+ migrateResult = await migrateRepos({
5492
+ from: opts.from,
5493
+ dryRun: opts.dryRun,
5494
+ client
5495
+ });
5496
+ } catch (err) {
5497
+ spinner.fail("Migration failed");
5498
+ console.error(chalk14.red(err.message));
5499
+ process.exit(1);
5500
+ }
5501
+ spinner.stop();
5502
+ const total = migrateResult.migrated.length + migrateResult.skipped.length + migrateResult.failed.length;
5503
+ console.log(
5504
+ `Migrated ${chalk14.bold(String(migrateResult.migrated.length))}/${total} repos:`
5505
+ );
5506
+ console.log("");
5507
+ for (const repo of migrateResult.migrated) {
5508
+ console.log(` ${chalk14.green("\u2705")} ${repo}`);
5509
+ }
5510
+ for (const { repo, reason } of migrateResult.skipped) {
5511
+ console.log(` ${chalk14.yellow("\u26A0\uFE0F ")} ${repo} ${chalk14.dim(`(skipped \u2014 ${reason})`)}`);
5512
+ }
5513
+ for (const { repo, error } of migrateResult.failed) {
5514
+ console.log(` ${chalk14.red("\u274C")} ${repo} ${chalk14.dim(`(failed \u2014 ${error})`)}`);
5515
+ }
5516
+ console.log("");
5517
+ if (migrateResult.failed.length > 0) {
5518
+ process.exit(1);
5519
+ }
5520
+ });
5521
+
5522
+ // src/commands/operator.ts
5523
+ import { writeFileSync as writeFileSync8 } from "fs";
5524
+ import { resolve as resolve2 } from "path";
5525
+ import { Command as Command15 } from "commander";
5526
+ import { execa as execa4 } from "execa";
5527
+
5528
+ // src/lib/bundle.ts
5529
+ import { stringify as stringifyYaml4 } from "yaml";
5530
+ var BUNDLE_VERSION = "1";
5531
+ function buildBundle(input2) {
5532
+ const vaults = {};
5533
+ for (const v of input2.vaults ?? []) {
5534
+ vaults[v.namespace] = v.default ? { endpoint: v.endpoint, default: true } : { endpoint: v.endpoint };
5535
+ }
5536
+ const bundle = {
5537
+ version: input2.version ?? BUNDLE_VERSION,
5538
+ control_plane_url: input2.controlPlaneUrl,
5539
+ token_provider: { kind: input2.tokenProviderKind ?? "static", config: input2.initialToken },
5540
+ vaults
5541
+ };
5542
+ if (input2.dataDir) bundle.data_dir = input2.dataDir;
5543
+ if (input2.runtime) bundle.runtime = input2.runtime;
5544
+ return bundle;
5545
+ }
5546
+ function serializeBundle(bundle) {
5547
+ return stringifyYaml4(bundle);
5548
+ }
5549
+
5550
+ // src/commands/operator.ts
5551
+ async function connect(opts) {
5552
+ const explicit = opts.operatorUrl ?? process.env.HORUS_OPERATOR_URL;
5553
+ if (explicit) {
5554
+ return { baseUrl: explicit.replace(/\/$/, ""), close: async () => {
5555
+ } };
5556
+ }
5557
+ const localPort = Number(opts.port ?? "8090");
5558
+ const ns = opts.namespace ?? "horus-system";
5559
+ const proc = execa4(
5560
+ "kubectl",
5561
+ ["port-forward", "svc/operator-service", `${localPort}:8090`, "-n", ns],
5562
+ { stdio: "ignore" }
5563
+ );
5564
+ await new Promise((r) => setTimeout(r, 1500));
5565
+ return {
5566
+ baseUrl: `http://127.0.0.1:${localPort}`,
5567
+ close: async () => {
5568
+ proc.kill();
5569
+ }
5570
+ };
5571
+ }
5572
+ async function api(baseUrl, method, path2, body) {
5573
+ const res = await fetch(`${baseUrl}${path2}`, {
5574
+ method,
5575
+ headers: { "content-type": "application/json", "x-operator-role": "admin", "x-operator-user": "admin" },
5576
+ body: body === void 0 ? void 0 : JSON.stringify(body)
5577
+ });
5578
+ const text = await res.text();
5579
+ if (!res.ok) {
5580
+ throw new Error(`operator-service ${method} ${path2} \u2192 ${res.status}: ${text}`);
5581
+ }
5582
+ return text ? JSON.parse(text) : {};
5583
+ }
5584
+ function collectVault(value, prev) {
5585
+ const [namespace, endpoint] = value.split("=");
5586
+ if (!namespace || !endpoint) {
5587
+ throw new Error(`--vault must be "<owner/ns>=<endpoint>", got "${value}"`);
5588
+ }
5589
+ return [...prev, { namespace, endpoint }];
5590
+ }
5591
+ var operatorCommand = new Command15("operator").description(
5592
+ "Control Plane admin: users, vaults, provisioning requests"
5593
+ );
5594
+ var userCmd = operatorCommand.command("user").description("User management");
5595
+ userCmd.command("add <userId>").description("Onboard a user and emit a pre-provisioned config bundle").requiredOption("--tenant <tenant>", "tenant the user belongs to").requiredOption("--cp-url <url>", "public Control Plane URL the client connects to").option("--role <role>", "user role", "user").option("--vault <ns=endpoint>", "assign a vault (repeatable)", collectVault, []).option("--ttl <seconds>", "initial token lifetime", "86400").option("--out <path>", "bundle output path").option("--operator-url <url>", "operator-service base URL (skip port-forward)").option("--namespace <ns>", "k8s namespace for port-forward", "horus-system").action(async (userId, opts) => {
5596
+ const vaults = opts.vault;
5597
+ const client = await connect(opts);
5598
+ try {
5599
+ await api(client.baseUrl, "POST", "/requests", {
5600
+ kind: "onboard",
5601
+ tenant: opts.tenant,
5602
+ payload: { userId, role: opts.role, assignedVaults: vaults.map((v) => v.namespace) }
5603
+ });
5604
+ const minted = await api(client.baseUrl, "POST", "/tokens", {
5605
+ userId,
5606
+ ttlSeconds: Number(opts.ttl)
5607
+ });
5608
+ const bundle = buildBundle({
5609
+ controlPlaneUrl: opts.cpUrl,
5610
+ initialToken: minted.token,
5611
+ vaults
5612
+ });
5613
+ const outPath = resolve2(opts.out ?? `./${userId}.bundle.yaml`);
5614
+ writeFileSync8(outPath, serializeBundle(bundle), "utf8");
5615
+ console.log(`Bundle written to ${outPath}`);
5616
+ console.log(`Run: horus setup --config ${outPath}`);
5617
+ } finally {
5618
+ await client.close();
5619
+ }
5620
+ });
5621
+ userCmd.command("list").description("List users").option("--operator-url <url>", "operator-service base URL").action(async (opts) => {
5622
+ const client = await connect(opts);
5623
+ try {
5624
+ console.log(JSON.stringify(await api(client.baseUrl, "GET", "/users"), null, 2));
5625
+ } finally {
5626
+ await client.close();
5627
+ }
5628
+ });
5629
+ var vaultCmd = operatorCommand.command("vault").description("Vault provisioning");
5630
+ function vaultRequest(kind) {
5631
+ return async (opts) => {
5632
+ const client = await connect(opts);
5633
+ try {
5634
+ const out = await api(client.baseUrl, "POST", "/requests", {
5635
+ kind,
5636
+ tenant: opts.tenant,
5637
+ payload: {
5638
+ namespace: opts.namespace,
5639
+ target_router: opts.router,
5640
+ backing_store_adapter: opts.adapter,
5641
+ endpoint: opts.endpoint
5642
+ }
5643
+ });
5644
+ console.log(JSON.stringify(out, null, 2));
5645
+ } finally {
5646
+ await client.close();
5647
+ }
5648
+ };
5649
+ }
5650
+ for (const [sub, kind] of [
5651
+ ["create", "vault_create"],
5652
+ ["attach", "vault_attach"],
5653
+ ["delete", "vault_delete"]
5654
+ ]) {
5655
+ vaultCmd.command(`${sub}`).requiredOption("--namespace <owner/ns>", "vault namespace").requiredOption("--tenant <tenant>", "tenant").option("--router <name>", "target router", "vault-router").option("--adapter <adapter>", "backing-store adapter", "git-subdir").option("--endpoint <url>", "explicit upstream endpoint").option("--operator-url <url>", "operator-service base URL").action(vaultRequest(kind));
5656
+ }
5657
+ var reqCmd = operatorCommand.command("request").description("Provisioning requests");
5658
+ reqCmd.command("list").option("--operator-url <url>", "operator-service base URL").action(async (opts) => {
5659
+ const client = await connect(opts);
5660
+ try {
5661
+ console.log(JSON.stringify(await api(client.baseUrl, "GET", "/requests"), null, 2));
5662
+ } finally {
5663
+ await client.close();
5664
+ }
5665
+ });
5666
+ reqCmd.command("show <id>").option("--operator-url <url>", "operator-service base URL").action(async (id, opts) => {
5667
+ const client = await connect(opts);
5668
+ try {
5669
+ console.log(JSON.stringify(await api(client.baseUrl, "GET", `/requests/${id}`), null, 2));
5670
+ } finally {
5671
+ await client.close();
5672
+ }
5673
+ });
5674
+ for (const decision of ["approve", "reject"]) {
5675
+ reqCmd.command(`${decision} <id>`).option("--operator-url <url>", "operator-service base URL").action(async (id, opts) => {
5676
+ const client = await connect(opts);
5677
+ try {
5678
+ console.log(
5679
+ JSON.stringify(await api(client.baseUrl, "POST", `/requests/${id}/${decision}`), null, 2)
5680
+ );
5681
+ } finally {
5682
+ await client.close();
5683
+ }
5684
+ });
5685
+ }
5686
+ operatorCommand.command("login").description("Authenticate; forces bootstrap-admin credential rotation on first login (\xA7H)").option("--admin <id>", "admin user id", "admin").option("--operator-url <url>", "operator-service base URL").action(async (opts) => {
5687
+ const client = await connect(opts);
5688
+ try {
5689
+ const adminId = opts.admin ?? "admin";
5690
+ const users = await api(
5691
+ client.baseUrl,
5692
+ "GET",
5693
+ "/users"
5694
+ );
5695
+ const admin = users.find((u) => u.userId === adminId);
5696
+ if (admin?.mustRotate) {
5697
+ await api(client.baseUrl, "POST", "/admin/rotate", { adminId });
5698
+ console.log("Bootstrap admin credential rotated (forced on first login).");
5699
+ } else {
5700
+ console.log("Logged in. No rotation required.");
5701
+ }
5702
+ } finally {
5703
+ await client.close();
5704
+ }
5705
+ });
5706
+ function renderPrincipalSecrets(namespace, b) {
5707
+ const clientJwks = JSON.stringify(b.clientJwks);
5708
+ const internalSigningKey = JSON.stringify(b.internalSigningKey);
5709
+ const pubJwk = JSON.stringify(b.internalPublicJwk);
5710
+ return `apiVersion: v1
5711
+ kind: Secret
5712
+ metadata:
5713
+ name: horus-service-secrets
5714
+ namespace: ${namespace}
5715
+ type: Opaque
5716
+ stringData:
5717
+ HORUS_CLIENT_JWKS_JSON: '${clientJwks}'
5718
+ HORUS_INTERNAL_SIGNING_KEY_JSON: '${internalSigningKey}'
5719
+ ---
5720
+ apiVersion: v1
5721
+ kind: Secret
5722
+ metadata:
5723
+ name: horus-principal-pub
5724
+ namespace: ${namespace}
5725
+ type: Opaque
5726
+ stringData:
5727
+ pub.jwk: '${pubJwk}'
5728
+ `;
5729
+ }
5730
+ operatorCommand.command("init").description(
5731
+ "Derive horus-service-secrets + horus-principal-pub from operator-service keys and apply them"
5732
+ ).option("--namespace <ns>", "k8s namespace", "horus-system").option("--operator-url <url>", "operator-service base URL (skip port-forward)").option("--port <port>", "local port for port-forward", "8090").option("--dry-run", "print the Secret manifests instead of applying", false).option("--out <path>", "write the Secret manifests to a file instead of applying").action(async (opts) => {
5733
+ const client = await connect(opts);
5734
+ try {
5735
+ const bundle = await api(client.baseUrl, "GET", "/admin/principal-bundle");
5736
+ const ns = opts.namespace ?? "horus-system";
5737
+ const manifests = renderPrincipalSecrets(ns, bundle);
5738
+ if (opts.out) {
5739
+ writeFileSync8(resolve2(opts.out), manifests, "utf8");
5740
+ console.log(`Principal Secrets written to ${resolve2(opts.out)}`);
5741
+ return;
5742
+ }
5743
+ if (opts.dryRun) {
5744
+ process.stdout.write(manifests);
5745
+ return;
5746
+ }
5747
+ await execa4("kubectl", ["apply", "-n", ns, "-f", "-"], {
5748
+ input: manifests,
5749
+ stdio: ["pipe", "inherit", "inherit"]
5750
+ });
5751
+ console.log(`Applied horus-service-secrets + horus-principal-pub to namespace ${ns}.`);
5752
+ console.log("horus-service can now mint X-Horus-Principal; Vault and forge-registry can verify it.");
5753
+ } finally {
5754
+ await client.close();
5755
+ }
5756
+ });
5757
+ operatorCommand.command("status").option("--operator-url <url>", "operator-service base URL").action(async (opts) => {
5758
+ const client = await connect(opts);
5759
+ try {
5760
+ const health = await api(client.baseUrl, "GET", "/health");
5761
+ const requests = await api(client.baseUrl, "GET", "/requests");
5762
+ const users = await api(client.baseUrl, "GET", "/users");
5763
+ console.log(
5764
+ JSON.stringify({ health, requests: requests.length, users: users.length }, null, 2)
5765
+ );
5766
+ } finally {
5767
+ await client.close();
5768
+ }
5769
+ });
4428
5770
 
4429
5771
  // src/index.ts
4430
- var program = new Command14();
5772
+ var program = new Command16();
4431
5773
  program.name("horus").description("CLI for managing the Horus Docker Compose stack").version(CLI_VERSION);
4432
5774
  program.addCommand(setupCommand);
4433
5775
  program.addCommand(upCommand);
@@ -4435,6 +5777,7 @@ program.addCommand(downCommand);
4435
5777
  program.addCommand(statusCommand);
4436
5778
  program.addCommand(configCommand);
4437
5779
  program.addCommand(connectCommand);
5780
+ program.addCommand(loginCommand);
4438
5781
  program.addCommand(updateCommand);
4439
5782
  program.addCommand(doctorCommand);
4440
5783
  program.addCommand(backupCommand);
@@ -4442,6 +5785,7 @@ program.addCommand(testEnvCommand);
4442
5785
  program.addCommand(helpCommand);
4443
5786
  program.addCommand(guideCommand);
4444
5787
  program.addCommand(repoCommand);
5788
+ program.addCommand(operatorCommand);
4445
5789
  program.exitOverride();
4446
5790
  try {
4447
5791
  await program.parseAsync(process.argv);
@@ -4450,9 +5794,9 @@ try {
4450
5794
  process.exit(0);
4451
5795
  }
4452
5796
  if (error instanceof Error) {
4453
- console.error(chalk14.red(`Error: ${error.message}`));
5797
+ console.error(chalk15.red(`Error: ${error.message}`));
4454
5798
  } else {
4455
- console.error(chalk14.red("An unexpected error occurred."));
5799
+ console.error(chalk15.red("An unexpected error occurred."));
4456
5800
  }
4457
5801
  process.exit(1);
4458
5802
  }