@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/chunk-6KL7LAPE.js +27810 -0
- package/dist/chunk-6ROWQWZM.js +27811 -0
- package/dist/core-5AX3I6XJ.js +5 -0
- package/dist/core-OT3PB6UL.js +4 -0
- package/dist/index.js +2228 -884
- package/guides/index.json +1 -1
- package/package.json +6 -4
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
|
|
5
|
-
import
|
|
1155
|
+
import { Command as Command16 } from "commander";
|
|
1156
|
+
import chalk15 from "chalk";
|
|
6
1157
|
|
|
7
1158
|
// src/commands/setup.ts
|
|
8
|
-
import { Command as
|
|
9
|
-
import
|
|
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
|
-
//
|
|
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
|
-
"
|
|
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
|
-
"#
|
|
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
|
-
"#
|
|
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((
|
|
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
|
|
771
|
-
# Horus
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
-
|
|
779
|
-
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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:
|
|
2000
|
+
memory: 512m
|
|
793
2001
|
reservations:
|
|
794
|
-
memory:
|
|
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
|
-
|
|
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
|
-
- "\${
|
|
2079
|
+
- "\${TEST_PORT_FORGE_REGISTRY:-9270}:8744"
|
|
883
2080
|
volumes:
|
|
884
|
-
- \${
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
2099
|
+
HORUS_UI_SERVICE,
|
|
1001
2100
|
"",
|
|
1002
|
-
|
|
1003
|
-
vaultRouterService,
|
|
1004
|
-
"",
|
|
1005
|
-
vaultMcpService,
|
|
2101
|
+
ANVIL_SERVICE,
|
|
1006
2102
|
"",
|
|
1007
|
-
|
|
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
|
-
|
|
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",
|
|
1438
|
-
forge: { command: npxPath, args: ["mcp-remote",
|
|
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
|
|
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
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
cursorRulesSpinner
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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/
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
2798
|
+
const res = await fetch(cp.replace(/\/$/, "") + "/health", { signal: AbortSignal.timeout(8e3) });
|
|
2799
|
+
reachable = res.ok;
|
|
1692
2800
|
} catch {
|
|
1693
|
-
|
|
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
|
|
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(
|
|
1699
|
-
console.log(
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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(
|
|
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 (
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1820
|
-
message: "
|
|
1821
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
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
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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
|
-
|
|
1929
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
});
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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(
|
|
2055
|
-
|
|
2056
|
-
|
|
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(
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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 (
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
-
|
|
2085
|
-
} catch
|
|
2086
|
-
console.log(
|
|
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(
|
|
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(
|
|
2105
|
-
console.log(
|
|
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
|
-
|
|
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" ?
|
|
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(
|
|
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
|
-
|
|
2132
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
2157
|
-
console.log(
|
|
2158
|
-
console.log(
|
|
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(
|
|
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(
|
|
2166
|
-
console.log(
|
|
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(` ${
|
|
2169
|
-
console.log(` ${
|
|
2170
|
-
console.log(` ${
|
|
2171
|
-
console.log("");
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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(
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
|
2190
|
-
import
|
|
3147
|
+
import { Command as Command4 } from "commander";
|
|
3148
|
+
import chalk4 from "chalk";
|
|
2191
3149
|
import ora3 from "ora";
|
|
2192
|
-
var upCommand = new
|
|
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(
|
|
2195
|
-
console.log(
|
|
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 ${
|
|
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(
|
|
3169
|
+
console.log(chalk4.bold("Pulling latest images..."));
|
|
2212
3170
|
try {
|
|
2213
3171
|
await composeStreaming(runtime, ["pull"]);
|
|
2214
3172
|
console.log("");
|
|
2215
|
-
console.log(
|
|
3173
|
+
console.log(chalk4.green("\u2713 Pull complete"));
|
|
2216
3174
|
} catch {
|
|
2217
3175
|
console.log("");
|
|
2218
|
-
console.log(
|
|
2219
|
-
console.log(
|
|
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(
|
|
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(
|
|
2229
|
-
console.log(
|
|
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(
|
|
3195
|
+
console.log(chalk4.bold("Service Status:"));
|
|
2238
3196
|
for (const s of states) {
|
|
2239
|
-
const color = s.status === "healthy" ?
|
|
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
|
-
|
|
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
|
|
2257
|
-
import
|
|
3214
|
+
import { Command as Command5 } from "commander";
|
|
3215
|
+
import chalk5 from "chalk";
|
|
2258
3216
|
import ora4 from "ora";
|
|
2259
|
-
var downCommand = new
|
|
3217
|
+
var downCommand = new Command5("down").description("Stop the Horus stack").action(async () => {
|
|
2260
3218
|
if (!configExists() || !composeFileExists()) {
|
|
2261
|
-
console.log(
|
|
2262
|
-
console.log(
|
|
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 ${
|
|
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(
|
|
3235
|
+
console.log(chalk5.bold("Stopping Horus services..."));
|
|
2278
3236
|
try {
|
|
2279
3237
|
await composeStreaming(runtime, ["down"]);
|
|
2280
3238
|
} catch (error) {
|
|
2281
|
-
console.log(
|
|
2282
|
-
console.log(
|
|
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(
|
|
2287
|
-
console.log(
|
|
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
|
|
2293
|
-
import
|
|
3250
|
+
import { Command as Command6 } from "commander";
|
|
3251
|
+
import chalk6 from "chalk";
|
|
2294
3252
|
import ora5 from "ora";
|
|
2295
|
-
var statusCommand = new
|
|
3253
|
+
var statusCommand = new Command6("status").description("Show status of Horus services").action(async () => {
|
|
2296
3254
|
if (!configExists() || !composeFileExists()) {
|
|
2297
|
-
console.log(
|
|
2298
|
-
console.log(
|
|
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(
|
|
2321
|
-
console.log(
|
|
2322
|
-
console.log(` ${
|
|
2323
|
-
console.log(` ${
|
|
2324
|
-
console.log(` ${
|
|
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(
|
|
2328
|
-
console.log(
|
|
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(
|
|
2334
|
-
console.log(
|
|
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)} ${
|
|
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
|
|
2360
|
-
if (lower === "starting") return
|
|
2361
|
-
return
|
|
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
|
|
2377
|
-
import
|
|
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
|
|
3337
|
+
var configCommand = new Command7("config").description("View or modify Horus configuration").action(async () => {
|
|
2380
3338
|
if (!configExists()) {
|
|
2381
|
-
console.log(
|
|
2382
|
-
console.log(
|
|
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(
|
|
2388
|
-
console.log(
|
|
2389
|
-
console.log(` ${
|
|
2390
|
-
console.log(` ${
|
|
2391
|
-
console.log(` ${
|
|
2392
|
-
console.log(` ${
|
|
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(` ${
|
|
3352
|
+
console.log(` ${chalk7.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk7.dim("(not set)")}`);
|
|
2395
3353
|
console.log("");
|
|
2396
|
-
console.log(
|
|
2397
|
-
console.log(` ${
|
|
2398
|
-
console.log(` ${
|
|
2399
|
-
console.log(` ${
|
|
2400
|
-
console.log(` ${
|
|
2401
|
-
console.log(` ${
|
|
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(
|
|
2404
|
-
console.log(` ${
|
|
2405
|
-
console.log(` ${
|
|
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(
|
|
3365
|
+
console.log(chalk7.bold(" Vaults:"));
|
|
2408
3366
|
if (Object.keys(config.vaults ?? {}).length === 0) {
|
|
2409
|
-
console.log(
|
|
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 ?
|
|
2413
|
-
console.log(` ${
|
|
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(
|
|
3375
|
+
console.log(chalk7.bold(" GitHub Hosts:"));
|
|
2418
3376
|
if (Object.keys(config.github_hosts ?? {}).length === 0) {
|
|
2419
|
-
console.log(
|
|
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(` ${
|
|
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(
|
|
2427
|
-
console.log(
|
|
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(
|
|
2433
|
-
console.log(
|
|
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(
|
|
2438
|
-
console.log(
|
|
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(
|
|
2448
|
-
console.log(
|
|
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(
|
|
2453
|
-
console.log(
|
|
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(
|
|
3418
|
+
console.log(chalk7.red(error.message));
|
|
2461
3419
|
process.exit(1);
|
|
2462
3420
|
}
|
|
2463
3421
|
saveConfig(config);
|
|
2464
3422
|
writeEnvFile(config);
|
|
2465
|
-
|
|
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(
|
|
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(
|
|
3444
|
+
console.log(chalk7.dim("Run `horus down && horus up` to restart."));
|
|
2486
3445
|
}
|
|
2487
3446
|
} else {
|
|
2488
|
-
console.log(
|
|
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
|
|
2498
|
-
import
|
|
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
|
|
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(
|
|
2568
|
-
console.log(
|
|
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 ${
|
|
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(
|
|
2585
|
-
console.log(
|
|
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: ${
|
|
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(
|
|
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(
|
|
3578
|
+
console.log(chalk8.dim(error.message));
|
|
2620
3579
|
process.exit(1);
|
|
2621
3580
|
}
|
|
2622
3581
|
console.log("");
|
|
2623
|
-
console.log(
|
|
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(
|
|
2628
|
-
console.log(
|
|
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" ?
|
|
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(
|
|
3608
|
+
console.log(chalk8.dim(error.message));
|
|
2650
3609
|
process.exit(1);
|
|
2651
3610
|
}
|
|
2652
3611
|
console.log("");
|
|
2653
|
-
console.log(
|
|
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: ${
|
|
3623
|
+
console.log(` Latest release: ${chalk8.cyan(latestVersion)}`);
|
|
2665
3624
|
} else {
|
|
2666
|
-
console.log(
|
|
3625
|
+
console.log(chalk8.dim(" Could not reach GitHub to check latest version."));
|
|
2667
3626
|
}
|
|
2668
3627
|
console.log("");
|
|
2669
|
-
console.log(
|
|
2670
|
-
console.log(
|
|
2671
|
-
console.log(` ${
|
|
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(
|
|
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: ${
|
|
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(
|
|
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(
|
|
3658
|
+
console.log(chalk8.dim(error.message));
|
|
2700
3659
|
}
|
|
2701
3660
|
console.log("");
|
|
2702
|
-
console.log(
|
|
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(
|
|
2707
|
-
console.log(
|
|
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(
|
|
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(
|
|
2715
|
-
console.log(
|
|
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" ?
|
|
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(
|
|
3696
|
+
console.log(chalk8.dim(error.message));
|
|
2738
3697
|
console.log("");
|
|
2739
|
-
console.log(
|
|
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(
|
|
2744
|
-
console.log(
|
|
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(` ${
|
|
3705
|
+
console.log(` ${chalk8.bold("Version:")} ${latestVersion}`);
|
|
2747
3706
|
}
|
|
2748
3707
|
console.log("");
|
|
2749
|
-
console.log(
|
|
3708
|
+
console.log(chalk8.bold(" Service Status:"));
|
|
2750
3709
|
for (const s of finalStates) {
|
|
2751
|
-
const color = s.status === "healthy" ?
|
|
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(
|
|
2757
|
-
console.log(
|
|
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
|
|
2764
|
-
import
|
|
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
|
|
3730
|
+
return chalk9.green(" \u2713 ");
|
|
2772
3731
|
case "warn":
|
|
2773
|
-
return
|
|
3732
|
+
return chalk9.yellow(" \u26A0 ");
|
|
2774
3733
|
case "fail":
|
|
2775
|
-
return
|
|
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
|
|
3740
|
+
return chalk9.white(msg);
|
|
2782
3741
|
case "warn":
|
|
2783
|
-
return
|
|
3742
|
+
return chalk9.yellow(msg);
|
|
2784
3743
|
case "fail":
|
|
2785
|
-
return
|
|
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
|
|
3969
|
+
var doctorCommand = new Command9("doctor").description("Diagnose common Horus issues").action(async () => {
|
|
3011
3970
|
console.log("");
|
|
3012
|
-
console.log(
|
|
3013
|
-
console.log(
|
|
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(
|
|
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(
|
|
4016
|
+
console.log(chalk9.green(" All checks passed."));
|
|
3058
4017
|
} else {
|
|
3059
4018
|
const parts = [];
|
|
3060
|
-
if (errors.length > 0) parts.push(
|
|
3061
|
-
if (warnings.length > 0) parts.push(
|
|
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" ?
|
|
3068
|
-
console.log(` ${icon} ${
|
|
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
|
|
3080
|
-
import
|
|
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(
|
|
3100
|
-
console.log(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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: ${
|
|
4100
|
+
backupSpinner.succeed(`Archive created: ${chalk10.dim(tarFile)}`);
|
|
3142
4101
|
} catch (error) {
|
|
3143
4102
|
backupSpinner.fail("Failed to create backup archive");
|
|
3144
|
-
console.log(
|
|
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(
|
|
3168
|
-
console.log(
|
|
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(
|
|
3172
|
-
console.log(
|
|
3173
|
-
console.log(` ${
|
|
3174
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
3182
|
-
console.log(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4191
|
+
console.log(chalk10.bold("Starting services..."));
|
|
3233
4192
|
try {
|
|
3234
4193
|
await composeStreaming(runtime, ["start"]);
|
|
3235
4194
|
} catch (error) {
|
|
3236
|
-
console.log(
|
|
3237
|
-
console.log(
|
|
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" ?
|
|
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(
|
|
4217
|
+
console.log(chalk10.dim(error.message));
|
|
3259
4218
|
process.exit(1);
|
|
3260
4219
|
}
|
|
3261
4220
|
console.log("");
|
|
3262
|
-
console.log(
|
|
4221
|
+
console.log(chalk10.bold.green("Restore complete!"));
|
|
3263
4222
|
console.log("");
|
|
3264
4223
|
}
|
|
3265
|
-
var backupCommand = new
|
|
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
|
|
3274
|
-
import
|
|
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
|
|
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 ${
|
|
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(
|
|
3756
|
-
`All ${testCfg.max_slots} slot(s) are in use. Run ${
|
|
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: ${
|
|
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(
|
|
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 ${
|
|
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" ?
|
|
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(
|
|
4811
|
+
console.log(chalk11.bold.green(`\u2713 Slot ${slot} acquired`));
|
|
3848
4812
|
console.log("");
|
|
3849
|
-
console.log(
|
|
3850
|
-
console.log(` Slot: ${
|
|
3851
|
-
console.log(` Project: ${
|
|
3852
|
-
console.log(` Data: ${
|
|
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(
|
|
3855
|
-
console.log(` Anvil: http://localhost:${
|
|
3856
|
-
console.log(` Forge: http://localhost:${
|
|
3857
|
-
console.log(` Vault MCP: http://localhost:${
|
|
3858
|
-
console.log(` Vault Router: http://localhost:${
|
|
3859
|
-
console.log(` Typesense: http://localhost:${
|
|
3860
|
-
console.log(` UI: http://localhost:${
|
|
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(
|
|
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(
|
|
3871
|
-
console.log(` MCP config: ${
|
|
3872
|
-
console.log(` Launch: ${
|
|
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(
|
|
3875
|
-
console.log(
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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(
|
|
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(
|
|
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" ?
|
|
3951
|
-
console.log(` ${
|
|
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: ${
|
|
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(
|
|
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(
|
|
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
|
|
4011
|
-
import
|
|
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(
|
|
4189
|
-
console.log(
|
|
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(` ${
|
|
4193
|
-
console.log(` ${" ".repeat(20)} ${
|
|
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(
|
|
4197
|
-
console.log(
|
|
4198
|
-
console.log(
|
|
4199
|
-
console.log(
|
|
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(
|
|
4202
|
-
console.log(
|
|
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
|
|
4207
|
-
const content = readFileSync7(
|
|
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(
|
|
4213
|
-
console.log(
|
|
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(` ${
|
|
4216
|
-
console.log(` ${" ".repeat(20)} ${
|
|
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
|
-
|
|
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
|
|
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(
|
|
5208
|
+
console.log(chalk12.yellow(`No guide matched "${queryStr}".`));
|
|
4240
5209
|
console.log("");
|
|
4241
|
-
console.log(
|
|
4242
|
-
console.log(
|
|
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(
|
|
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
|
|
4257
|
-
import
|
|
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(
|
|
4297
|
-
console.log(
|
|
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(` ${
|
|
4301
|
-
console.log(` ${" ".repeat(20)} ${
|
|
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(
|
|
4305
|
-
console.log(
|
|
4306
|
-
console.log(
|
|
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(
|
|
5284
|
+
console.log(chalk13.yellow(`Multiple guides matched (tier: ${tier}):`));
|
|
4316
5285
|
console.log("");
|
|
4317
5286
|
for (const m of matches) {
|
|
4318
|
-
console.log(` ${
|
|
5287
|
+
console.log(` ${chalk13.cyan(m.slug.padEnd(20))} ${m.title}`);
|
|
4319
5288
|
}
|
|
4320
5289
|
console.log("");
|
|
4321
|
-
console.log(
|
|
5290
|
+
console.log(chalk13.dim("Pick one: horus guide <slug>"));
|
|
4322
5291
|
console.log("");
|
|
4323
5292
|
}
|
|
4324
|
-
var guideCommand = new
|
|
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(
|
|
5307
|
+
console.log(chalk13.yellow(`No guide matched "${topic}".`));
|
|
4339
5308
|
console.log("");
|
|
4340
|
-
console.log(
|
|
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
|
|
4360
|
-
import
|
|
5328
|
+
import { Command as Command14 } from "commander";
|
|
5329
|
+
import chalk14 from "chalk";
|
|
4361
5330
|
import ora9 from "ora";
|
|
4362
|
-
var
|
|
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(
|
|
4366
|
-
console.log(
|
|
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(
|
|
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(
|
|
4394
|
-
console.error(
|
|
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(
|
|
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(` ${
|
|
4419
|
-
console.log(` ${
|
|
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(` ${
|
|
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
|
|
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(
|
|
5797
|
+
console.error(chalk15.red(`Error: ${error.message}`));
|
|
4454
5798
|
} else {
|
|
4455
|
-
console.error(
|
|
5799
|
+
console.error(chalk15.red("An unexpected error occurred."));
|
|
4456
5800
|
}
|
|
4457
5801
|
process.exit(1);
|
|
4458
5802
|
}
|