sentinelayer-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +996 -0
  2. package/bin/create-sentinelayer.js +5 -0
  3. package/bin/sentinelayer-cli.js +5 -0
  4. package/bin/sl.js +5 -0
  5. package/package.json +54 -0
  6. package/src/agents/jules/config/definition.js +209 -0
  7. package/src/agents/jules/config/system-prompt.js +175 -0
  8. package/src/agents/jules/error-intake.js +51 -0
  9. package/src/agents/jules/fix-cycle.js +377 -0
  10. package/src/agents/jules/loop.js +367 -0
  11. package/src/agents/jules/pulse.js +319 -0
  12. package/src/agents/jules/stream.js +186 -0
  13. package/src/agents/jules/swarm/file-scanner.js +74 -0
  14. package/src/agents/jules/swarm/index.js +11 -0
  15. package/src/agents/jules/swarm/orchestrator.js +362 -0
  16. package/src/agents/jules/swarm/pattern-hunter.js +123 -0
  17. package/src/agents/jules/swarm/sub-agent.js +308 -0
  18. package/src/agents/jules/tools/auth-audit.js +222 -0
  19. package/src/agents/jules/tools/dispatch.js +327 -0
  20. package/src/agents/jules/tools/file-edit.js +180 -0
  21. package/src/agents/jules/tools/file-read.js +100 -0
  22. package/src/agents/jules/tools/frontend-analyze.js +570 -0
  23. package/src/agents/jules/tools/glob.js +168 -0
  24. package/src/agents/jules/tools/grep.js +228 -0
  25. package/src/agents/jules/tools/index.js +29 -0
  26. package/src/agents/jules/tools/path-guards.js +161 -0
  27. package/src/agents/jules/tools/runtime-audit.js +409 -0
  28. package/src/agents/jules/tools/shell.js +383 -0
  29. package/src/ai/aidenid.js +945 -0
  30. package/src/ai/client.js +508 -0
  31. package/src/ai/domain-target-store.js +268 -0
  32. package/src/ai/identity-store.js +270 -0
  33. package/src/ai/site-store.js +145 -0
  34. package/src/audit/agents/architecture.js +180 -0
  35. package/src/audit/agents/compliance.js +179 -0
  36. package/src/audit/agents/documentation.js +165 -0
  37. package/src/audit/agents/performance.js +145 -0
  38. package/src/audit/agents/security.js +215 -0
  39. package/src/audit/agents/testing.js +172 -0
  40. package/src/audit/orchestrator.js +557 -0
  41. package/src/audit/package.js +204 -0
  42. package/src/audit/registry.js +284 -0
  43. package/src/audit/replay.js +103 -0
  44. package/src/auth/http.js +113 -0
  45. package/src/auth/service.js +848 -0
  46. package/src/auth/session-store.js +345 -0
  47. package/src/cli.js +244 -0
  48. package/src/commands/ai/identity-lifecycle.js +1337 -0
  49. package/src/commands/ai/provision-governance.js +1246 -0
  50. package/src/commands/ai/shared.js +147 -0
  51. package/src/commands/ai.js +11 -0
  52. package/src/commands/apply.js +19 -0
  53. package/src/commands/audit.js +1147 -0
  54. package/src/commands/auth.js +366 -0
  55. package/src/commands/chat.js +191 -0
  56. package/src/commands/config.js +184 -0
  57. package/src/commands/cost.js +311 -0
  58. package/src/commands/daemon/core.js +850 -0
  59. package/src/commands/daemon/extended.js +1048 -0
  60. package/src/commands/daemon/shared.js +213 -0
  61. package/src/commands/daemon.js +11 -0
  62. package/src/commands/guide.js +174 -0
  63. package/src/commands/ingest.js +58 -0
  64. package/src/commands/init.js +55 -0
  65. package/src/commands/legacy-args.js +30 -0
  66. package/src/commands/mcp.js +404 -0
  67. package/src/commands/omargate.js +21 -0
  68. package/src/commands/persona.js +27 -0
  69. package/src/commands/plugin.js +260 -0
  70. package/src/commands/policy.js +132 -0
  71. package/src/commands/prompt.js +238 -0
  72. package/src/commands/review.js +704 -0
  73. package/src/commands/scan.js +788 -0
  74. package/src/commands/spec.js +716 -0
  75. package/src/commands/swarm.js +651 -0
  76. package/src/commands/telemetry.js +202 -0
  77. package/src/commands/watch.js +510 -0
  78. package/src/config/agent-dictionary.js +182 -0
  79. package/src/config/io.js +56 -0
  80. package/src/config/paths.js +18 -0
  81. package/src/config/schema.js +55 -0
  82. package/src/config/service.js +184 -0
  83. package/src/cost/budget.js +235 -0
  84. package/src/cost/history.js +188 -0
  85. package/src/cost/tracker.js +171 -0
  86. package/src/daemon/artifact-lineage.js +534 -0
  87. package/src/daemon/assignment-ledger.js +770 -0
  88. package/src/daemon/ast-parser-layer.js +258 -0
  89. package/src/daemon/budget-governor.js +633 -0
  90. package/src/daemon/callgraph-overlay.js +646 -0
  91. package/src/daemon/error-worker.js +626 -0
  92. package/src/daemon/hybrid-mapper.js +929 -0
  93. package/src/daemon/jira-lifecycle.js +632 -0
  94. package/src/daemon/operator-control.js +657 -0
  95. package/src/daemon/reliability-lane.js +471 -0
  96. package/src/daemon/watchdog.js +971 -0
  97. package/src/guide/generator.js +316 -0
  98. package/src/ingest/engine.js +918 -0
  99. package/src/legacy-cli.js +2435 -0
  100. package/src/mcp/registry.js +695 -0
  101. package/src/memory/blackboard.js +301 -0
  102. package/src/memory/retrieval.js +581 -0
  103. package/src/plugin/manifest.js +553 -0
  104. package/src/policy/packs.js +144 -0
  105. package/src/prompt/generator.js +106 -0
  106. package/src/review/ai-review.js +669 -0
  107. package/src/review/local-review.js +1284 -0
  108. package/src/review/replay.js +235 -0
  109. package/src/review/report.js +664 -0
  110. package/src/review/spec-binding.js +487 -0
  111. package/src/scan/generator.js +351 -0
  112. package/src/spec/generator.js +519 -0
  113. package/src/spec/regenerate.js +237 -0
  114. package/src/spec/templates.js +91 -0
  115. package/src/swarm/dashboard.js +247 -0
  116. package/src/swarm/factory.js +363 -0
  117. package/src/swarm/pentest.js +934 -0
  118. package/src/swarm/registry.js +419 -0
  119. package/src/swarm/report.js +158 -0
  120. package/src/swarm/runtime.js +576 -0
  121. package/src/swarm/scenario-dsl.js +272 -0
  122. package/src/telemetry/ledger.js +302 -0
  123. package/src/ui/markdown.js +220 -0
  124. package/src/ui/progress.js +100 -0
@@ -0,0 +1,345 @@
1
+ import crypto from "node:crypto";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fsp from "node:fs/promises";
5
+ import process from "node:process";
6
+
7
+ const CREDENTIALS_VERSION = 1;
8
+ const KEYRING_SERVICE = "sentinelayer-cli";
9
+
10
+ function nowIso() {
11
+ return new Date().toISOString();
12
+ }
13
+
14
+ function resolveHomeDir(homeDir) {
15
+ return path.resolve(String(homeDir || os.homedir()));
16
+ }
17
+
18
+ /**
19
+ * Resolve the deterministic credentials metadata file path for the current user/home override.
20
+ *
21
+ * @param {{ homeDir?: string }} [options]
22
+ * @returns {string}
23
+ */
24
+ export function resolveCredentialsFilePath({ homeDir } = {}) {
25
+ const resolvedHome = resolveHomeDir(homeDir);
26
+ return path.join(resolvedHome, ".sentinelayer", "credentials.json");
27
+ }
28
+
29
+ function buildKeyringAccountName(apiUrl) {
30
+ const digest = crypto
31
+ .createHash("sha256")
32
+ .update(String(apiUrl || "").trim().toLowerCase())
33
+ .digest("hex")
34
+ .slice(0, 16);
35
+ return `default-${digest}`;
36
+ }
37
+
38
+ function normalizeUser(user = {}) {
39
+ return {
40
+ id: String(user.id || "").trim(),
41
+ githubUsername: String(user.githubUsername || user.github_username || "").trim(),
42
+ email: String(user.email || "").trim(),
43
+ avatarUrl: String(user.avatarUrl || user.avatar_url || "").trim(),
44
+ isAdmin: Boolean(user.isAdmin || user.is_admin),
45
+ };
46
+ }
47
+
48
+ function normalizeMetadata(raw = {}) {
49
+ return {
50
+ version: Number(raw.version || CREDENTIALS_VERSION),
51
+ apiUrl: String(raw.apiUrl || "").trim(),
52
+ storage: String(raw.storage || "file").trim(),
53
+ keyringService: String(raw.keyringService || KEYRING_SERVICE).trim(),
54
+ keyringAccount: String(raw.keyringAccount || "").trim(),
55
+ tokenId: String(raw.tokenId || "").trim() || null,
56
+ tokenPrefix: String(raw.tokenPrefix || "").trim() || null,
57
+ tokenExpiresAt: String(raw.tokenExpiresAt || "").trim() || null,
58
+ createdAt: String(raw.createdAt || "").trim() || nowIso(),
59
+ updatedAt: String(raw.updatedAt || "").trim() || nowIso(),
60
+ user: normalizeUser(raw.user),
61
+ token: String(raw.token || "").trim() || null,
62
+ };
63
+ }
64
+
65
+ async function loadKeytarClient() {
66
+ const disableKeyring = String(process.env.SENTINELAYER_DISABLE_KEYRING || "")
67
+ .trim()
68
+ .toLowerCase();
69
+ if (disableKeyring === "1" || disableKeyring === "true" || disableKeyring === "yes" || disableKeyring === "on") {
70
+ return null;
71
+ }
72
+ try {
73
+ const mod = await import("keytar");
74
+ const client = mod && typeof mod === "object" ? mod.default || mod : null;
75
+ if (!client) {
76
+ return null;
77
+ }
78
+ if (
79
+ typeof client.getPassword !== "function" ||
80
+ typeof client.setPassword !== "function" ||
81
+ typeof client.deletePassword !== "function"
82
+ ) {
83
+ return null;
84
+ }
85
+ return client;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ async function readMetadata({ homeDir } = {}) {
92
+ const filePath = resolveCredentialsFilePath({ homeDir });
93
+ try {
94
+ const raw = await fsp.readFile(filePath, "utf-8");
95
+ const parsed = JSON.parse(raw);
96
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
97
+ return { filePath, metadata: null };
98
+ }
99
+ return { filePath, metadata: normalizeMetadata(parsed) };
100
+ } catch (error) {
101
+ if (error && typeof error === "object" && error.code === "ENOENT") {
102
+ return { filePath, metadata: null };
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ async function writeMetadata(filePath, metadata) {
109
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
110
+ await fsp.writeFile(filePath, `${JSON.stringify(metadata, null, 2)}\n`, {
111
+ encoding: "utf-8",
112
+ mode: 0o600,
113
+ });
114
+ try {
115
+ await fsp.chmod(filePath, 0o600);
116
+ } catch {
117
+ // Windows does not reliably support POSIX chmod semantics.
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Load the active stored session, resolving keyring-backed tokens when configured.
123
+ *
124
+ * @param {{ homeDir?: string }} [options]
125
+ * @returns {Promise<null | {
126
+ * version: number,
127
+ * apiUrl: string,
128
+ * storage: "file" | "keyring",
129
+ * keyringService: string,
130
+ * keyringAccount: string,
131
+ * tokenId: string | null,
132
+ * tokenPrefix: string | null,
133
+ * tokenExpiresAt: string | null,
134
+ * createdAt: string,
135
+ * updatedAt: string,
136
+ * user: {
137
+ * id: string,
138
+ * githubUsername: string,
139
+ * email: string,
140
+ * avatarUrl: string,
141
+ * isAdmin: boolean
142
+ * },
143
+ * token: string,
144
+ * filePath: string
145
+ * }>}
146
+ */
147
+ export async function readStoredSession({ homeDir } = {}) {
148
+ const { filePath, metadata } = await readMetadata({ homeDir });
149
+ if (!metadata) {
150
+ return null;
151
+ }
152
+
153
+ if (metadata.storage === "keyring") {
154
+ const keytar = await loadKeytarClient();
155
+ if (!keytar || !metadata.keyringAccount) {
156
+ return null;
157
+ }
158
+ const token = await keytar.getPassword(
159
+ metadata.keyringService || KEYRING_SERVICE,
160
+ metadata.keyringAccount
161
+ );
162
+ if (!token) {
163
+ return null;
164
+ }
165
+ return {
166
+ ...metadata,
167
+ filePath,
168
+ token,
169
+ storage: "keyring",
170
+ };
171
+ }
172
+
173
+ if (!metadata.token) {
174
+ return null;
175
+ }
176
+ return {
177
+ ...metadata,
178
+ filePath,
179
+ token: metadata.token,
180
+ storage: "file",
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Read persisted session metadata without returning secret token material.
186
+ *
187
+ * @param {{ homeDir?: string }} [options]
188
+ * @returns {Promise<null | {
189
+ * version: number,
190
+ * apiUrl: string,
191
+ * storage: string,
192
+ * keyringService: string,
193
+ * keyringAccount: string,
194
+ * tokenId: string | null,
195
+ * tokenPrefix: string | null,
196
+ * tokenExpiresAt: string | null,
197
+ * createdAt: string,
198
+ * updatedAt: string,
199
+ * user: {
200
+ * id: string,
201
+ * githubUsername: string,
202
+ * email: string,
203
+ * avatarUrl: string,
204
+ * isAdmin: boolean
205
+ * },
206
+ * filePath: string,
207
+ * token: null
208
+ * }>}
209
+ */
210
+ export async function readStoredSessionMetadata({ homeDir } = {}) {
211
+ const { filePath, metadata } = await readMetadata({ homeDir });
212
+ if (!metadata) {
213
+ return null;
214
+ }
215
+ return {
216
+ ...metadata,
217
+ filePath,
218
+ token: null,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Persist a new session token and metadata using keyring storage when available.
224
+ *
225
+ * @param {{
226
+ * apiUrl: string,
227
+ * token: string,
228
+ * tokenId?: string | null,
229
+ * tokenPrefix?: string | null,
230
+ * tokenExpiresAt?: string | null,
231
+ * user?: Record<string, unknown>
232
+ * }} [session]
233
+ * @param {{ homeDir?: string }} [options]
234
+ * @returns {Promise<{
235
+ * version: number,
236
+ * apiUrl: string,
237
+ * storage: "file" | "keyring",
238
+ * keyringService: string,
239
+ * keyringAccount: string,
240
+ * tokenId: string | null,
241
+ * tokenPrefix: string | null,
242
+ * tokenExpiresAt: string | null,
243
+ * createdAt: string,
244
+ * updatedAt: string,
245
+ * user: {
246
+ * id: string,
247
+ * githubUsername: string,
248
+ * email: string,
249
+ * avatarUrl: string,
250
+ * isAdmin: boolean
251
+ * },
252
+ * filePath: string,
253
+ * token: string
254
+ * }>}
255
+ */
256
+ export async function writeStoredSession(
257
+ {
258
+ apiUrl,
259
+ token,
260
+ tokenId = null,
261
+ tokenPrefix = null,
262
+ tokenExpiresAt = null,
263
+ user = {},
264
+ } = {},
265
+ { homeDir } = {}
266
+ ) {
267
+ const normalizedApiUrl = String(apiUrl || "").trim();
268
+ const normalizedToken = String(token || "").trim();
269
+ if (!normalizedApiUrl) {
270
+ throw new Error("apiUrl is required to persist CLI auth session.");
271
+ }
272
+ if (!normalizedToken) {
273
+ throw new Error("token is required to persist CLI auth session.");
274
+ }
275
+
276
+ const { filePath, metadata: existingMetadata } = await readMetadata({ homeDir });
277
+ const keytar = await loadKeytarClient();
278
+ const keyringAccount = buildKeyringAccountName(normalizedApiUrl);
279
+ const updatedAt = nowIso();
280
+
281
+ const nextMetadata = normalizeMetadata({
282
+ version: CREDENTIALS_VERSION,
283
+ apiUrl: normalizedApiUrl,
284
+ tokenId,
285
+ tokenPrefix,
286
+ tokenExpiresAt,
287
+ user: normalizeUser(user),
288
+ createdAt: existingMetadata?.createdAt || updatedAt,
289
+ updatedAt,
290
+ });
291
+
292
+ if (keytar) {
293
+ const previousKeyringAccount = String(existingMetadata?.keyringAccount || "").trim();
294
+ const previousStorage = String(existingMetadata?.storage || "").trim();
295
+ if (previousStorage === "keyring" && previousKeyringAccount && previousKeyringAccount !== keyringAccount) {
296
+ await keytar.deletePassword(KEYRING_SERVICE, previousKeyringAccount);
297
+ }
298
+
299
+ await keytar.setPassword(KEYRING_SERVICE, keyringAccount, normalizedToken);
300
+ nextMetadata.storage = "keyring";
301
+ nextMetadata.keyringService = KEYRING_SERVICE;
302
+ nextMetadata.keyringAccount = keyringAccount;
303
+ nextMetadata.token = null;
304
+ } else {
305
+ nextMetadata.storage = "file";
306
+ nextMetadata.keyringService = KEYRING_SERVICE;
307
+ nextMetadata.keyringAccount = "";
308
+ nextMetadata.token = normalizedToken;
309
+ }
310
+
311
+ await writeMetadata(filePath, nextMetadata);
312
+
313
+ return {
314
+ ...nextMetadata,
315
+ filePath,
316
+ token: normalizedToken,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Remove local session metadata and keyring credentials for the active account.
322
+ *
323
+ * @param {{ homeDir?: string }} [options]
324
+ * @returns {Promise<{ filePath: string, hadSession: boolean }>}
325
+ */
326
+ export async function clearStoredSession({ homeDir } = {}) {
327
+ const { filePath, metadata } = await readMetadata({ homeDir });
328
+ if (metadata && metadata.storage === "keyring") {
329
+ const keytar = await loadKeytarClient();
330
+ if (keytar && metadata.keyringAccount) {
331
+ await keytar.deletePassword(metadata.keyringService || KEYRING_SERVICE, metadata.keyringAccount);
332
+ }
333
+ }
334
+
335
+ try {
336
+ await fsp.rm(filePath, { force: true });
337
+ } catch {
338
+ // Ignore cleanup errors.
339
+ }
340
+
341
+ return {
342
+ filePath,
343
+ hadSession: Boolean(metadata),
344
+ };
345
+ }
package/src/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ import process from "node:process";
2
+ import { Command } from "commander";
3
+
4
+ import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
5
+
6
+ const COMMAND_REGISTRARS = {
7
+ init: {
8
+ loader: () => import("./commands/init.js"),
9
+ exportName: "registerInitCommand",
10
+ needsLegacy: true,
11
+ },
12
+ omargate: {
13
+ loader: () => import("./commands/omargate.js"),
14
+ exportName: "registerOmarGateCommand",
15
+ needsLegacy: true,
16
+ },
17
+ audit: {
18
+ loader: () => import("./commands/audit.js"),
19
+ exportName: "registerAuditCommand",
20
+ needsLegacy: true,
21
+ },
22
+ persona: {
23
+ loader: () => import("./commands/persona.js"),
24
+ exportName: "registerPersonaCommand",
25
+ needsLegacy: true,
26
+ },
27
+ apply: {
28
+ loader: () => import("./commands/apply.js"),
29
+ exportName: "registerApplyCommand",
30
+ needsLegacy: true,
31
+ },
32
+ config: {
33
+ loader: () => import("./commands/config.js"),
34
+ exportName: "registerConfigCommand",
35
+ needsLegacy: false,
36
+ },
37
+ ingest: {
38
+ loader: () => import("./commands/ingest.js"),
39
+ exportName: "registerIngestCommand",
40
+ needsLegacy: false,
41
+ },
42
+ spec: {
43
+ loader: () => import("./commands/spec.js"),
44
+ exportName: "registerSpecCommand",
45
+ needsLegacy: false,
46
+ },
47
+ prompt: {
48
+ loader: () => import("./commands/prompt.js"),
49
+ exportName: "registerPromptCommand",
50
+ needsLegacy: false,
51
+ },
52
+ scan: {
53
+ loader: () => import("./commands/scan.js"),
54
+ exportName: "registerScanCommand",
55
+ needsLegacy: false,
56
+ },
57
+ guide: {
58
+ loader: () => import("./commands/guide.js"),
59
+ exportName: "registerGuideCommand",
60
+ needsLegacy: false,
61
+ },
62
+ cost: {
63
+ loader: () => import("./commands/cost.js"),
64
+ exportName: "registerCostCommand",
65
+ needsLegacy: false,
66
+ },
67
+ telemetry: {
68
+ loader: () => import("./commands/telemetry.js"),
69
+ exportName: "registerTelemetryCommand",
70
+ needsLegacy: false,
71
+ },
72
+ auth: {
73
+ loader: () => import("./commands/auth.js"),
74
+ exportName: "registerAuthCommand",
75
+ needsLegacy: false,
76
+ },
77
+ watch: {
78
+ loader: () => import("./commands/watch.js"),
79
+ exportName: "registerWatchCommand",
80
+ needsLegacy: false,
81
+ },
82
+ mcp: {
83
+ loader: () => import("./commands/mcp.js"),
84
+ exportName: "registerMcpCommand",
85
+ needsLegacy: false,
86
+ },
87
+ plugin: {
88
+ loader: () => import("./commands/plugin.js"),
89
+ exportName: "registerPluginCommand",
90
+ needsLegacy: false,
91
+ },
92
+ ai: {
93
+ loader: () => import("./commands/ai.js"),
94
+ exportName: "registerAiCommand",
95
+ needsLegacy: false,
96
+ },
97
+ review: {
98
+ loader: () => import("./commands/review.js"),
99
+ exportName: "registerReviewCommand",
100
+ needsLegacy: false,
101
+ },
102
+ chat: {
103
+ loader: () => import("./commands/chat.js"),
104
+ exportName: "registerChatCommand",
105
+ needsLegacy: false,
106
+ },
107
+ policy: {
108
+ loader: () => import("./commands/policy.js"),
109
+ exportName: "registerPolicyCommand",
110
+ needsLegacy: false,
111
+ },
112
+ swarm: {
113
+ loader: () => import("./commands/swarm.js"),
114
+ exportName: "registerSwarmCommand",
115
+ needsLegacy: false,
116
+ },
117
+ daemon: {
118
+ loader: () => import("./commands/daemon.js"),
119
+ exportName: "registerDaemonCommand",
120
+ needsLegacy: false,
121
+ },
122
+ };
123
+
124
+ const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
125
+
126
+ // Map slash-prefixed commands to their Commander equivalents.
127
+ // /omargate → omargate, /audit → audit local, etc.
128
+ // Only remap /omargate to Commander. The others (/audit, /persona, /apply)
129
+ // stay on the legacy path for backward compatibility (different output format).
130
+ const SLASH_TO_COMMANDER = {
131
+ "/omargate": "omargate",
132
+ };
133
+
134
+ function normalizeSlashArgs(rawArgs) {
135
+ if (!Array.isArray(rawArgs) || rawArgs.length === 0) return rawArgs;
136
+ const first = String(rawArgs[0] || "").trim();
137
+
138
+ // Direct slash match: /omargate → omargate
139
+ const mapped = SLASH_TO_COMMANDER[first];
140
+ if (mapped) {
141
+ return [mapped, ...rawArgs.slice(1)];
142
+ }
143
+
144
+ // Windows Git Bash path mangling fix: /omargate gets converted to
145
+ // "C:/Program Files/Git/omargate" by MSYS. Detect and recover.
146
+ for (const [slash, cmd] of Object.entries(SLASH_TO_COMMANDER)) {
147
+ const suffix = slash.slice(1); // "omargate" from "/omargate"
148
+ if (first.endsWith("/" + suffix) || first.endsWith("\\" + suffix)) {
149
+ return [cmd, ...rawArgs.slice(1)];
150
+ }
151
+ }
152
+
153
+ return rawArgs;
154
+ }
155
+
156
+ function shouldBypassCommander(rawArgs) {
157
+ if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
158
+ return true;
159
+ }
160
+
161
+ const first = String(rawArgs[0] || "").trim();
162
+ if (!first) {
163
+ return true;
164
+ }
165
+
166
+ // Slash commands are now handled by normalizeSlashArgs before this check
167
+ if (first.startsWith("/") && !SLASH_TO_COMMANDER[first]) {
168
+ return true;
169
+ }
170
+
171
+ if (first === "--help" || first === "-h" || first === "--version" || first === "-v") {
172
+ return true;
173
+ }
174
+
175
+ if (first.startsWith("-")) {
176
+ return true;
177
+ }
178
+
179
+ const resolved = SLASH_TO_COMMANDER[first] || first;
180
+ return !COMMAND_SET.has(resolved);
181
+ }
182
+
183
+ async function registerCommands(program, { invokeLegacy, onlyCommand } = {}) {
184
+ const commandNames =
185
+ onlyCommand && COMMAND_REGISTRARS[onlyCommand]
186
+ ? [onlyCommand]
187
+ : Object.keys(COMMAND_REGISTRARS);
188
+
189
+ for (const commandName of commandNames) {
190
+ const descriptor = COMMAND_REGISTRARS[commandName];
191
+ const loaded = await descriptor.loader();
192
+ const registerFn = loaded[descriptor.exportName];
193
+ if (typeof registerFn !== "function") {
194
+ throw new Error(
195
+ `Command registrar '${descriptor.exportName}' was not exported by '${commandName}' loader.`
196
+ );
197
+ }
198
+ if (descriptor.needsLegacy) {
199
+ registerFn(program, invokeLegacy);
200
+ } else {
201
+ registerFn(program);
202
+ }
203
+ }
204
+ }
205
+
206
+ export async function buildCliProgram({
207
+ invokeLegacy = runLegacyCliWithErrorHandling,
208
+ onlyCommand = null,
209
+ } = {}) {
210
+ const program = new Command();
211
+
212
+ program
213
+ .name("sentinelayer-cli")
214
+ .description("Sentinelayer CLI")
215
+ .version(CLI_VERSION)
216
+ .option("--verbose", "Verbose execution logs")
217
+ .option("--quiet", "Suppress progress indicators and terminal notifications")
218
+ .option("--json", "Emit machine-readable output when supported")
219
+ .showHelpAfterError();
220
+
221
+ await registerCommands(program, {
222
+ invokeLegacy,
223
+ onlyCommand: onlyCommand && COMMAND_SET.has(onlyCommand) ? onlyCommand : null,
224
+ });
225
+
226
+ return program;
227
+ }
228
+
229
+ export async function runCli(rawArgs = process.argv.slice(2)) {
230
+ // Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
231
+ const normalizedArgs = normalizeSlashArgs(rawArgs);
232
+
233
+ if (shouldBypassCommander(normalizedArgs)) {
234
+ await runLegacyCliWithErrorHandling(normalizedArgs);
235
+ return;
236
+ }
237
+
238
+ const program = await buildCliProgram({
239
+ invokeLegacy: runLegacyCliWithErrorHandling,
240
+ onlyCommand: normalizedArgs[0],
241
+ });
242
+ await program.parseAsync(["node", "sentinelayer-cli", ...normalizedArgs]);
243
+ }
244
+