@metasession.co/devaudit-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.
package/dist/index.js ADDED
@@ -0,0 +1,2446 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { createConsola } from 'consola';
4
+ import { execa } from 'execa';
5
+ import { join, resolve, basename, dirname, relative } from 'path';
6
+ import { promises } from 'fs';
7
+ import envPaths from 'env-paths';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+ import { validateManifest } from '@metasession.co/devaudit-plugin-sdk';
10
+ import * as clack2 from '@clack/prompts';
11
+
12
+ var DEFAULT_OPTIONS = { json: false, verbose: false, noColor: false };
13
+ var currentLogger = build(DEFAULT_OPTIONS);
14
+ var jsonModeActive = false;
15
+ function build(opts) {
16
+ const level = opts.verbose ? 5 : 3;
17
+ if (opts.json) {
18
+ return createConsola({
19
+ level,
20
+ reporters: [
21
+ {
22
+ log: (logObj) => {
23
+ const record2 = {
24
+ level: logObj.type,
25
+ tag: logObj.tag ?? null,
26
+ args: logObj.args,
27
+ date: logObj.date
28
+ };
29
+ process.stdout.write(JSON.stringify(record2) + "\n");
30
+ }
31
+ }
32
+ ]
33
+ });
34
+ }
35
+ if (opts.noColor) {
36
+ process.env["NO_COLOR"] = "1";
37
+ }
38
+ return createConsola({ level });
39
+ }
40
+ function configureLogger(opts) {
41
+ const merged = { ...DEFAULT_OPTIONS, ...opts };
42
+ jsonModeActive = merged.json;
43
+ currentLogger = build(merged);
44
+ }
45
+ function logger() {
46
+ return currentLogger;
47
+ }
48
+ function isJsonMode() {
49
+ return jsonModeActive;
50
+ }
51
+ function emitJsonResult(payload) {
52
+ process.stdout.write(JSON.stringify(payload) + "\n");
53
+ }
54
+
55
+ // src/lib/version.ts
56
+ var CLI_VERSION = "0.0.1";
57
+ var paths = envPaths("devaudit", { suffix: "" });
58
+ var CONFIG_DIR = paths.config;
59
+ var AUTH_FILE = join(CONFIG_DIR, "auth.json");
60
+ join(CONFIG_DIR, "config.json");
61
+ var PLUGINS_DIR = join(CONFIG_DIR, "plugins");
62
+ function toFileUrl(absPath) {
63
+ return pathToFileURL(absPath).href.replace(/%7E/g, "~");
64
+ }
65
+ async function loadPluginFromDir(dir) {
66
+ const pkgPath = join(dir, "package.json");
67
+ const raw = await promises.readFile(pkgPath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ const result = validateManifest(parsed);
70
+ if (!result.valid) {
71
+ throw new Error(`Invalid manifest in ${pkgPath}: ${result.errors.join("; ")}`);
72
+ }
73
+ const mainPath = resolve(dir, result.main);
74
+ const mod = await import(toFileUrl(mainPath));
75
+ if (!mod.default || typeof mod.default !== "object") {
76
+ throw new Error(`Plugin main module ${mainPath} did not default-export a Plugin object.`);
77
+ }
78
+ if (mod.default.apiVersion !== "1") {
79
+ throw new Error(
80
+ `Plugin ${result.packageName} declares apiVersion '${mod.default.apiVersion}' at runtime, expected '1'.`
81
+ );
82
+ }
83
+ if (typeof mod.default.name !== "string" || mod.default.name.length === 0) {
84
+ throw new Error(`Plugin main module ${mainPath} did not export a non-empty 'name'.`);
85
+ }
86
+ return {
87
+ dir,
88
+ packageName: result.packageName,
89
+ packageVersion: result.packageVersion,
90
+ manifest: result.manifest,
91
+ plugin: mod.default
92
+ };
93
+ }
94
+
95
+ // src/lib/plugin/discover.ts
96
+ async function listSubdirs(root) {
97
+ try {
98
+ const entries = await promises.readdir(root, { withFileTypes: true });
99
+ return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
100
+ } catch (err) {
101
+ if (err.code === "ENOENT") return [];
102
+ throw err;
103
+ }
104
+ }
105
+ async function discoverPlugins(root = PLUGINS_DIR) {
106
+ const dirs = await listSubdirs(root);
107
+ const loaded = [];
108
+ const failures = [];
109
+ for (const dir of dirs) {
110
+ try {
111
+ loaded.push(await loadPluginFromDir(dir));
112
+ } catch (err) {
113
+ failures.push({ dir, reason: err.message });
114
+ }
115
+ }
116
+ return { loaded, failures };
117
+ }
118
+ async function readSdlcConfig(projectPath) {
119
+ const configPath = join(resolve(projectPath), "sdlc-config.json");
120
+ try {
121
+ const raw = await promises.readFile(configPath, "utf-8");
122
+ return JSON.parse(raw);
123
+ } catch (err) {
124
+ if (err.code === "ENOENT") return null;
125
+ throw err;
126
+ }
127
+ }
128
+ async function checkFrameworkFiles(projectPath, files) {
129
+ const checks = await Promise.all(
130
+ files.map(async (rel) => {
131
+ try {
132
+ await promises.access(join(projectPath, rel));
133
+ return { path: rel, present: true };
134
+ } catch {
135
+ return { path: rel, present: false };
136
+ }
137
+ })
138
+ );
139
+ return checks;
140
+ }
141
+
142
+ // src/lib/plugin/context.ts
143
+ async function buildPluginContext(opts) {
144
+ const cfg = await readSdlcConfig(opts.projectPath);
145
+ const sdlcConfig = cfg ?? {
146
+ project_slug: ""
147
+ };
148
+ return {
149
+ projectPath: opts.projectPath,
150
+ sdlcConfig,
151
+ logger: pluginLogger(),
152
+ apiVersion: "1",
153
+ emit: (event) => {
154
+ opts.events?.push(event);
155
+ }
156
+ };
157
+ }
158
+ function pluginLogger() {
159
+ const log = logger();
160
+ return {
161
+ info: (m) => log.info(`[plugin] ${m}`),
162
+ warn: (m) => log.warn(`[plugin] ${m}`),
163
+ error: (m) => log.error(`[plugin] ${m}`),
164
+ debug: (m) => log.debug(`[plugin] ${m}`)
165
+ };
166
+ }
167
+
168
+ // src/lib/plugin/hooks.ts
169
+ async function runHook(plugins, hook, ctx) {
170
+ const log = logger();
171
+ const results = [];
172
+ for (const p of plugins) {
173
+ const fn = p.plugin.hooks?.[hook];
174
+ if (!fn) {
175
+ results.push({ plugin: p.packageName, hook, status: "skipped" });
176
+ continue;
177
+ }
178
+ try {
179
+ await fn(ctx);
180
+ results.push({ plugin: p.packageName, hook, status: "ok" });
181
+ } catch (err) {
182
+ const message = err.message;
183
+ log.warn(`Plugin '${p.packageName}' hook '${hook}' threw: ${message}`);
184
+ results.push({ plugin: p.packageName, hook, status: "error", message });
185
+ }
186
+ }
187
+ return results;
188
+ }
189
+ function registerPluginCommands(program, plugins) {
190
+ for (const p of plugins) {
191
+ const namespace = pluginNamespace(p.packageName);
192
+ const manifestCommands = p.manifest.commands ?? [];
193
+ if (manifestCommands.length === 0) continue;
194
+ const group2 = program.command(namespace).description(p.manifest.displayName ?? p.packageName);
195
+ for (const c of manifestCommands) {
196
+ const impl = p.plugin.commands?.[c.name];
197
+ if (!impl) continue;
198
+ group2.command(`${c.name} [args...]`).description(c.description).action(async (args) => {
199
+ const log = logger();
200
+ const projectPath = resolve(process.cwd());
201
+ const ctx = await buildPluginContext({ projectPath });
202
+ try {
203
+ await impl(ctx, args);
204
+ } catch (err) {
205
+ log.error(`Plugin '${p.packageName}' command '${c.name}' failed: ${err.message}`);
206
+ process.exit(1);
207
+ }
208
+ });
209
+ }
210
+ }
211
+ }
212
+ function pluginNamespace(packageName) {
213
+ return packageName.replace(/^@[^/]+\//, "").replace(/^devaudit-plugin-/, "");
214
+ }
215
+
216
+ // src/commands/doctor.ts
217
+ async function checkCommand(name, args) {
218
+ try {
219
+ const result = await execa(name, args, { reject: false });
220
+ const ok2 = result.exitCode === 0;
221
+ const firstLine = result.stdout.split("\n")[0] ?? "";
222
+ return { name, ok: ok2, detail: ok2 ? firstLine : `exited ${result.exitCode}` };
223
+ } catch (err) {
224
+ const message = err instanceof Error ? err.message : String(err);
225
+ return { name, ok: false, detail: message };
226
+ }
227
+ }
228
+ async function checkNodeVersion() {
229
+ const version = process.versions.node;
230
+ const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
231
+ const ok2 = major >= 22;
232
+ return { name: "node", ok: ok2, detail: `v${version} (require >=22)` };
233
+ }
234
+ async function runDoctor(options = {}) {
235
+ const log = logger();
236
+ log.info("Running devaudit doctor \u2014 checking required tools...\n");
237
+ const checks = [
238
+ await checkNodeVersion(),
239
+ await checkCommand("git", ["--version"]),
240
+ await checkCommand("gh", ["--version"]),
241
+ await checkCommand("jq", ["--version"]),
242
+ await checkCommand("curl", ["--version"])
243
+ ];
244
+ let allOk = true;
245
+ for (const check of checks) {
246
+ const marker = check.ok ? "\u2713" : "\u2717";
247
+ if (!check.ok) allOk = false;
248
+ log.log(` ${marker} ${check.name.padEnd(8)} ${check.detail}`);
249
+ }
250
+ log.log("");
251
+ const plugins = options.plugins ?? (await discoverPlugins()).loaded;
252
+ if (plugins.length > 0) {
253
+ const ctx = await buildPluginContext({ projectPath: resolve(process.cwd()) });
254
+ await runHook(plugins, "onDoctor", ctx);
255
+ }
256
+ if (allOk) {
257
+ log.success("All required tools present.");
258
+ process.exit(0);
259
+ } else {
260
+ log.error("One or more required tools are missing. Install them and re-run `devaudit doctor`.");
261
+ process.exit(6);
262
+ }
263
+ }
264
+ var DEFAULT_BASE_URL = "https://devaudit.metasession.co";
265
+ async function readAuth() {
266
+ try {
267
+ const raw = await promises.readFile(AUTH_FILE, "utf-8");
268
+ const parsed = JSON.parse(raw);
269
+ if (parsed.version !== 1 || typeof parsed.token !== "string") {
270
+ return null;
271
+ }
272
+ return parsed;
273
+ } catch (err) {
274
+ if (err.code === "ENOENT") return null;
275
+ throw err;
276
+ }
277
+ }
278
+ async function writeAuth(token, baseUrl = DEFAULT_BASE_URL) {
279
+ await promises.mkdir(dirname(AUTH_FILE), { recursive: true, mode: 448 });
280
+ const record2 = { version: 1, token, base_url: baseUrl };
281
+ await promises.writeFile(AUTH_FILE, JSON.stringify(record2, null, 2) + "\n", { mode: 384 });
282
+ }
283
+ async function deleteAuth() {
284
+ try {
285
+ await promises.unlink(AUTH_FILE);
286
+ return true;
287
+ } catch (err) {
288
+ if (err.code === "ENOENT") return false;
289
+ throw err;
290
+ }
291
+ }
292
+ async function resolveToken() {
293
+ const envToken = process.env["DEVAUDIT_USER_TOKEN"];
294
+ if (envToken) {
295
+ const envBase = process.env["DEVAUDIT_BASE_URL"] ?? DEFAULT_BASE_URL;
296
+ return { token: envToken, baseUrl: envBase, source: "env" };
297
+ }
298
+ const record2 = await readAuth();
299
+ if (record2) return { token: record2.token, baseUrl: record2.base_url, source: "file" };
300
+ return null;
301
+ }
302
+
303
+ // src/lib/devaudit-api.ts
304
+ var DevAuditApiError = class extends Error {
305
+ constructor(message, status, body) {
306
+ super(message);
307
+ this.status = status;
308
+ this.body = body;
309
+ this.name = "DevAuditApiError";
310
+ }
311
+ status;
312
+ body;
313
+ };
314
+ var DevAuditClient = class {
315
+ token;
316
+ baseUrl;
317
+ constructor(opts) {
318
+ this.token = opts.token;
319
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
320
+ }
321
+ async listProjects() {
322
+ const res = await this.request("GET", "/api/projects");
323
+ const json = await res.json();
324
+ if (Array.isArray(json)) return json;
325
+ return json.projects ?? [];
326
+ }
327
+ async getProjectBySlug(slug) {
328
+ const list = await this.listProjects();
329
+ return list.find((p) => p.slug === slug) ?? null;
330
+ }
331
+ async createProject(slug, name) {
332
+ const res = await this.request("POST", "/api/projects", { slug, name });
333
+ return await res.json();
334
+ }
335
+ async listApiKeys(projectId) {
336
+ const res = await this.request("GET", `/api/projects/${projectId}/api-keys`);
337
+ const json = await res.json();
338
+ if (Array.isArray(json)) return json;
339
+ return json.keys ?? [];
340
+ }
341
+ async issueApiKey(projectId, name) {
342
+ const res = await this.request("POST", `/api/projects/${projectId}/api-keys`, {
343
+ name,
344
+ role: "uploader"
345
+ });
346
+ return await res.json();
347
+ }
348
+ async request(method, path, body) {
349
+ const url = `${this.baseUrl}${path}`;
350
+ const headers = { "x-devaudit-token": this.token };
351
+ let payload;
352
+ if (body !== void 0) {
353
+ headers["content-type"] = "application/json";
354
+ payload = JSON.stringify(body);
355
+ }
356
+ const res = await fetch(url, { method, headers, body: payload });
357
+ if (!res.ok) {
358
+ const text2 = await res.text();
359
+ throw new DevAuditApiError(`${method} ${path} \u2192 HTTP ${res.status}`, res.status, text2);
360
+ }
361
+ return res;
362
+ }
363
+ };
364
+
365
+ // src/commands/auth/login.ts
366
+ var DEFAULT_BASE_URL2 = "https://devaudit.metasession.co";
367
+ async function runAuthLogin(options) {
368
+ const log = logger();
369
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL2;
370
+ let token = options.token ?? process.env["DEVAUDIT_USER_TOKEN"];
371
+ if (!token) {
372
+ log.info(`Open ${baseUrl}/settings/tokens in your browser and issue a Personal Access Token.`);
373
+ log.info("Paste the `mctok_...` value below; it never leaves this machine.");
374
+ const result = await clack2.password({
375
+ message: "Paste your DevAudit Personal Access Token (mctok_...):",
376
+ validate: (val) => {
377
+ if (!val) return "Token is required.";
378
+ if (!val.startsWith("mctok_")) return "Token should start with 'mctok_'.";
379
+ return void 0;
380
+ }
381
+ });
382
+ if (clack2.isCancel(result)) {
383
+ log.warn("Cancelled.");
384
+ process.exit(0);
385
+ }
386
+ token = result;
387
+ }
388
+ log.info("Validating token against portal...");
389
+ try {
390
+ const client = new DevAuditClient({ token, baseUrl });
391
+ await client.listProjects();
392
+ } catch (err) {
393
+ if (err instanceof DevAuditApiError && (err.status === 401 || err.status === 403)) {
394
+ log.error("Token rejected by portal (HTTP " + err.status + "). Check it was copied correctly + is not revoked.");
395
+ process.exit(3);
396
+ }
397
+ throw err;
398
+ }
399
+ await writeAuth(token, baseUrl);
400
+ log.success("Logged in. Token cached at ~/.config/devaudit/auth.json (mode 0600).");
401
+ }
402
+
403
+ // src/commands/auth/logout.ts
404
+ async function runAuthLogout() {
405
+ const log = logger();
406
+ const existed = await deleteAuth();
407
+ if (existed) {
408
+ log.success(`Removed cached token at ${AUTH_FILE}.`);
409
+ } else {
410
+ log.info("No cached token to remove.");
411
+ }
412
+ }
413
+
414
+ // src/commands/auth/status.ts
415
+ async function runAuthStatus() {
416
+ const log = logger();
417
+ const resolved = await resolveToken();
418
+ if (!resolved) {
419
+ if (isJsonMode()) emitJsonResult({ ok: false, reason: "not_logged_in" });
420
+ else log.warn("Not logged in. Run `devaudit auth login` or set DEVAUDIT_USER_TOKEN.");
421
+ process.exit(3);
422
+ return;
423
+ }
424
+ if (!isJsonMode()) {
425
+ log.info(`Token source: ${resolved.source === "env" ? "DEVAUDIT_USER_TOKEN env var" : "~/.config/devaudit/auth.json"}`);
426
+ log.info(`Portal: ${resolved.baseUrl}`);
427
+ log.info("Verifying token against portal...");
428
+ }
429
+ try {
430
+ const client = new DevAuditClient({ token: resolved.token, baseUrl: resolved.baseUrl });
431
+ const projects = await client.listProjects();
432
+ if (isJsonMode()) {
433
+ emitJsonResult({
434
+ ok: true,
435
+ source: resolved.source,
436
+ baseUrl: resolved.baseUrl,
437
+ projects: projects.map((p) => p.slug)
438
+ });
439
+ return;
440
+ }
441
+ log.success(`Token is valid. Accessible projects: ${projects.length}`);
442
+ for (const p of projects.slice(0, 10)) {
443
+ log.log(` \u2022 ${p.slug}`);
444
+ }
445
+ if (projects.length > 10) log.log(` ... and ${projects.length - 10} more`);
446
+ } catch (err) {
447
+ if (err instanceof DevAuditApiError) {
448
+ if (isJsonMode()) emitJsonResult({ ok: false, reason: "portal_rejected", status: err.status });
449
+ else log.error(`Portal rejected the token (HTTP ${err.status}). Re-run \`devaudit auth login\`.`);
450
+ process.exit(3);
451
+ return;
452
+ }
453
+ if (isJsonMode())
454
+ emitJsonResult({ ok: false, reason: "unexpected", message: err instanceof Error ? err.message : String(err) });
455
+ else log.error(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
456
+ process.exit(1);
457
+ }
458
+ }
459
+ var FRAMEWORK_FILES = [
460
+ "INSTRUCTIONS.md",
461
+ "CLAUDE.md",
462
+ ".cursorrules",
463
+ ".windsurfrules",
464
+ "GEMINI.md",
465
+ "SDLC/0-project-setup.md",
466
+ "SDLC/5-deploy-main.md",
467
+ "scripts/upload-evidence.sh",
468
+ "compliance/RTM.md",
469
+ ".github/workflows/ci.yml"
470
+ ];
471
+ async function runStatus(options) {
472
+ const log = logger();
473
+ const projectPath = resolve(options.path ?? process.cwd());
474
+ const config = await readSdlcConfig(projectPath);
475
+ if (!config) {
476
+ if (isJsonMode()) {
477
+ emitJsonResult({ ok: false, reason: "not_onboarded", projectPath });
478
+ } else {
479
+ log.info(`Inspecting ${projectPath}`);
480
+ log.warn("No sdlc-config.json found here. This project is not onboarded to DevAudit.");
481
+ log.info("Run `devaudit install` to onboard.");
482
+ }
483
+ process.exit(7);
484
+ return;
485
+ }
486
+ const files = await checkFrameworkFiles(projectPath, FRAMEWORK_FILES);
487
+ const presentFiles = files.filter((f) => f.present).map((f) => f.path);
488
+ const missingFiles = files.filter((f) => !f.present).map((f) => f.path);
489
+ if (isJsonMode()) {
490
+ emitJsonResult({
491
+ ok: true,
492
+ projectPath,
493
+ project_slug: config.project_slug,
494
+ stack: config.stack ?? null,
495
+ host: config.host ?? null,
496
+ node_version: config.node_version ?? null,
497
+ python_version: config.python_version ?? null,
498
+ working_directory: config.working_directory ?? null,
499
+ source_dirs: config.source_dirs ?? null,
500
+ devaudit_base_url: config.devaudit?.base_url ?? null,
501
+ uat_enabled: config.uat?.enabled ?? false,
502
+ approval_mode: config.approval?.mode ?? null,
503
+ files_present: presentFiles,
504
+ files_missing: missingFiles
505
+ });
506
+ return;
507
+ }
508
+ log.info(`Inspecting ${projectPath}`);
509
+ log.success("sdlc-config.json found.");
510
+ log.log("");
511
+ log.log(` Project slug: ${config.project_slug}`);
512
+ log.log(` Stack: ${config.stack ?? "(unset)"}`);
513
+ log.log(` Host: ${config.host ?? "(unset)"}`);
514
+ if (config.node_version) log.log(` Node version: ${config.node_version}`);
515
+ if (config.python_version) log.log(` Python version: ${config.python_version}`);
516
+ if (config.working_directory) log.log(` Working dir: ${config.working_directory}`);
517
+ if (config.source_dirs) log.log(` Source dirs: ${config.source_dirs}`);
518
+ log.log(` DevAudit URL: ${config.devaudit?.base_url ?? "(unset)"}`);
519
+ log.log(` UAT enabled: ${config.uat?.enabled ?? false}`);
520
+ log.log(` Approval mode: ${config.approval?.mode ?? "(unset)"}`);
521
+ log.log("");
522
+ log.info("Framework files present?");
523
+ for (const f of files) {
524
+ const marker = f.present ? "\u2713" : "\u2717";
525
+ log.log(` ${marker} ${f.path}`);
526
+ }
527
+ log.log("");
528
+ if (missingFiles.length === 0) {
529
+ log.success("All checked framework files are present.");
530
+ } else {
531
+ log.warn(`${missingFiles.length} framework file(s) missing. Re-sync via DevAudit-Installer's sync-sdlc.sh to refresh.`);
532
+ }
533
+ }
534
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
535
+ var MAX_ATTEMPTS = 3;
536
+ var INITIAL_BACKOFF_MS = 1e3;
537
+ async function collectFiles(filePath) {
538
+ const stat = await promises.stat(filePath);
539
+ if (stat.isFile()) return [filePath];
540
+ if (stat.isDirectory()) {
541
+ const entries = await promises.readdir(filePath, { withFileTypes: true });
542
+ const files = [];
543
+ for (const entry of entries) {
544
+ if (entry.isFile()) files.push(join(filePath, entry.name));
545
+ }
546
+ return files;
547
+ }
548
+ throw new Error(`${filePath} is neither a file nor a directory.`);
549
+ }
550
+ function delay(ms) {
551
+ return new Promise((res) => setTimeout(res, ms));
552
+ }
553
+ async function uploadOne(file, opts) {
554
+ const form = new FormData();
555
+ const buf = await promises.readFile(file);
556
+ const blob = new Blob([new Uint8Array(buf)]);
557
+ form.set("file", blob, basename(file));
558
+ form.set("projectSlug", opts.projectSlug);
559
+ form.set("requirementId", opts.requirementId);
560
+ form.set("evidenceType", opts.evidenceType);
561
+ form.set("metadata", JSON.stringify(opts.metadata ?? {}));
562
+ if (opts.releaseVersion) form.set("releaseVersion", opts.releaseVersion);
563
+ if (opts.createReleaseIfMissing) form.set("createReleaseIfMissing", "true");
564
+ if (opts.environment) form.set("environment", opts.environment);
565
+ if (opts.evidenceCategory) form.set("evidenceCategory", opts.evidenceCategory);
566
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/api/evidence/upload`;
567
+ let attempt = 1;
568
+ let backoff = INITIAL_BACKOFF_MS;
569
+ while (attempt <= MAX_ATTEMPTS) {
570
+ const res = await fetch(url, {
571
+ method: "POST",
572
+ headers: { authorization: `Bearer ${opts.apiKey}` },
573
+ body: form
574
+ });
575
+ if (res.ok) {
576
+ const body = await res.json().catch(() => null);
577
+ return { file, ok: true, status: res.status, body };
578
+ }
579
+ if (RETRYABLE_STATUSES.has(res.status) && attempt < MAX_ATTEMPTS) {
580
+ const retryAfter = Number.parseInt(res.headers.get("retry-after") ?? "", 10);
581
+ const wait = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : backoff;
582
+ await delay(wait);
583
+ backoff *= 2;
584
+ attempt += 1;
585
+ continue;
586
+ }
587
+ const errText = await res.text().catch(() => "(no body)");
588
+ return { file, ok: false, status: res.status, error: errText };
589
+ }
590
+ return { file, ok: false, status: 0, error: "max retries exhausted" };
591
+ }
592
+ async function uploadEvidence(opts) {
593
+ const files = await collectFiles(opts.filePath);
594
+ if (files.length === 0) {
595
+ throw new DevAuditApiError(`No files at ${opts.filePath}`, 0, "");
596
+ }
597
+ const results = [];
598
+ for (const file of files) {
599
+ results.push(await uploadOne(file, opts));
600
+ }
601
+ return results;
602
+ }
603
+
604
+ // src/commands/push.ts
605
+ var DEFAULT_BASE_URL3 = "https://devaudit.metasession.co";
606
+ function buildMetadata(options) {
607
+ const metadata = {};
608
+ if (options.gitSha) metadata["gitSha"] = options.gitSha;
609
+ if (options.ciRunId) metadata["ciRunId"] = options.ciRunId;
610
+ if (options.branch) metadata["branch"] = options.branch;
611
+ return metadata;
612
+ }
613
+ async function runDryRun(options, baseUrl) {
614
+ const log = logger();
615
+ const files = await collectFiles(options.filePath);
616
+ const planned = {
617
+ dryRun: true,
618
+ projectSlug: options.projectSlug,
619
+ requirementId: options.requirementId,
620
+ evidenceType: options.evidenceType,
621
+ baseUrl,
622
+ files: files.map((f) => ({ path: f })),
623
+ metadata: buildMetadata(options),
624
+ ...options.release !== void 0 ? { release: options.release } : {},
625
+ ...options.environment !== void 0 ? { environment: options.environment } : {},
626
+ ...options.category !== void 0 ? { category: options.category } : {}
627
+ };
628
+ if (isJsonMode()) {
629
+ emitJsonResult(planned);
630
+ return;
631
+ }
632
+ log.info(
633
+ `[dry-run] Would upload ${files.length} file(s) for ${options.projectSlug}/${options.requirementId} (${options.evidenceType}) \u2192 ${baseUrl}`
634
+ );
635
+ for (const f of files) log.log(` \xB7 ${f}`);
636
+ }
637
+ async function runPush(options) {
638
+ const log = logger();
639
+ const projectPath = resolve(process.cwd());
640
+ const baseUrl = options.baseUrl ?? process.env["DEVAUDIT_BASE_URL"] ?? DEFAULT_BASE_URL3;
641
+ if (options.dryRun) {
642
+ await runDryRun(options, baseUrl);
643
+ return;
644
+ }
645
+ const apiKey = options.apiKey ?? process.env["DEVAUDIT_API_KEY"];
646
+ if (!apiKey) {
647
+ if (isJsonMode()) emitJsonResult({ ok: false, reason: "missing_api_key" });
648
+ else {
649
+ log.error("DEVAUDIT_API_KEY env var is required (or pass --api-key).");
650
+ log.info("Issue a project API key at: <portal>/projects/<slug>/settings \u2192 API Key Management.");
651
+ }
652
+ process.exit(3);
653
+ }
654
+ log.info(
655
+ `Uploading ${options.filePath} (project=${options.projectSlug} req=${options.requirementId} type=${options.evidenceType}) \u2192 ${baseUrl}`
656
+ );
657
+ const plugins = options.plugins ?? (await discoverPlugins()).loaded;
658
+ if (plugins.length > 0) {
659
+ const ctx = await buildPluginContext({ projectPath });
660
+ await runHook(plugins, "beforePush", ctx);
661
+ }
662
+ const metadata = buildMetadata(options);
663
+ const results = await uploadEvidence({
664
+ projectSlug: options.projectSlug,
665
+ requirementId: options.requirementId,
666
+ evidenceType: options.evidenceType,
667
+ filePath: options.filePath,
668
+ apiKey,
669
+ baseUrl,
670
+ ...options.release !== void 0 ? { releaseVersion: options.release } : {},
671
+ ...options.createReleaseIfMissing !== void 0 ? { createReleaseIfMissing: options.createReleaseIfMissing } : {},
672
+ ...options.environment !== void 0 ? { environment: options.environment } : {},
673
+ ...options.category !== void 0 ? { evidenceCategory: options.category } : {},
674
+ metadata
675
+ });
676
+ let okCount = 0;
677
+ let failCount = 0;
678
+ for (const result of results) {
679
+ if (result.ok) {
680
+ okCount++;
681
+ log.success(` \u2713 ${result.file} (HTTP ${result.status})`);
682
+ } else {
683
+ failCount++;
684
+ log.error(` \u2717 ${result.file} (HTTP ${result.status}): ${result.error ?? "(no detail)"}`);
685
+ }
686
+ }
687
+ log.log("");
688
+ log.info(`Uploaded: ${okCount} succeeded, ${failCount} failed.`);
689
+ if (plugins.length > 0) {
690
+ const ctx = await buildPluginContext({ projectPath });
691
+ await runHook(plugins, "afterPush", ctx);
692
+ }
693
+ if (isJsonMode()) {
694
+ emitJsonResult({
695
+ ok: failCount === 0,
696
+ uploaded: okCount,
697
+ failed: failCount,
698
+ results: results.map((r) => ({ file: r.file, ok: r.ok, status: r.status, error: r.error ?? null }))
699
+ });
700
+ }
701
+ if (failCount > 0) process.exit(4);
702
+ }
703
+ async function resolveInstallerRoot() {
704
+ const override = process.env["DEVAUDIT_INSTALLER_ROOT"];
705
+ if (override) return resolve(override);
706
+ const here = dirname(fileURLToPath(import.meta.url));
707
+ const candidate = resolve(here, "..", "..");
708
+ await promises.access(resolve(candidate, "scripts", "sdlc-onboard.sh"));
709
+ return candidate;
710
+ }
711
+ async function ensureDir(dir, mode = 493) {
712
+ await promises.mkdir(dir, { recursive: true, mode });
713
+ }
714
+ async function exists(path) {
715
+ try {
716
+ await promises.access(path);
717
+ return true;
718
+ } catch {
719
+ return false;
720
+ }
721
+ }
722
+ async function isDir(path) {
723
+ try {
724
+ const stat = await promises.stat(path);
725
+ return stat.isDirectory();
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+ async function copyFile(src, dst, mode) {
731
+ await ensureDir(dirname(dst));
732
+ await promises.copyFile(src, dst);
733
+ if (mode !== void 0) await promises.chmod(dst, mode);
734
+ }
735
+ async function copyDir(src, dst, clean = false) {
736
+ if (clean && await exists(dst)) {
737
+ await promises.rm(dst, { recursive: true, force: true });
738
+ }
739
+ await ensureDir(dst);
740
+ let count = 0;
741
+ const entries = await promises.readdir(src, { withFileTypes: true });
742
+ for (const entry of entries) {
743
+ const srcPath = join(src, entry.name);
744
+ const dstPath = join(dst, entry.name);
745
+ if (entry.isDirectory()) {
746
+ count += await copyDir(srcPath, dstPath, false);
747
+ } else if (entry.isFile()) {
748
+ await promises.copyFile(srcPath, dstPath);
749
+ count += 1;
750
+ }
751
+ }
752
+ return count;
753
+ }
754
+ async function listFiles(dir, predicate) {
755
+ if (!await isDir(dir)) return [];
756
+ const entries = await promises.readdir(dir, { withFileTypes: true });
757
+ return entries.filter((e) => e.isFile()).map((e) => e.name).filter((name) => predicate ? predicate(name) : true).map((name) => join(dir, name));
758
+ }
759
+ function fileBasename(path) {
760
+ return basename(path);
761
+ }
762
+ var ghAvailabilityCache = null;
763
+ async function ghAvailable() {
764
+ if (ghAvailabilityCache !== null) return ghAvailabilityCache.available;
765
+ try {
766
+ await execa("gh", ["--version"]);
767
+ ghAvailabilityCache = { available: true };
768
+ return true;
769
+ } catch {
770
+ ghAvailabilityCache = { available: false };
771
+ return false;
772
+ }
773
+ }
774
+ var GitHubProvider = class {
775
+ name = "github";
776
+ preferGhCli;
777
+ token;
778
+ constructor(opts = {}) {
779
+ this.preferGhCli = opts.preferGhCli ?? true;
780
+ this.token = opts.token ?? process.env["GH_TOKEN"] ?? process.env["GITHUB_TOKEN"];
781
+ }
782
+ async getRepoMeta(cwd) {
783
+ if (this.preferGhCli && await ghAvailable()) {
784
+ const res2 = await execa(
785
+ "gh",
786
+ ["repo", "view", "--json", "owner,name,defaultBranchRef", "--jq", "{owner: .owner.login, name: .name, defaultBranch: .defaultBranchRef.name}"],
787
+ { cwd, reject: false }
788
+ );
789
+ if (res2.exitCode === 0 && res2.stdout.trim().length > 0) {
790
+ const parsed = JSON.parse(res2.stdout);
791
+ return parsed;
792
+ }
793
+ }
794
+ const { owner, name } = await parseOriginRemote(cwd);
795
+ if (!this.token) {
796
+ throw new Error(
797
+ "No `gh` CLI on PATH and no GH_TOKEN env var \u2014 cannot resolve repo metadata for GitHub."
798
+ );
799
+ }
800
+ const res = await fetch(`https://api.github.com/repos/${owner}/${name}`, {
801
+ headers: this.authHeaders()
802
+ });
803
+ if (!res.ok) {
804
+ throw new Error(`GitHub REST repo lookup failed: HTTP ${res.status}`);
805
+ }
806
+ const json = await res.json();
807
+ return { owner, name, defaultBranch: json.default_branch };
808
+ }
809
+ async setSecret(cwd, name, value) {
810
+ if (this.preferGhCli && await ghAvailable()) {
811
+ await execa("gh", ["secret", "set", name], { cwd, input: value });
812
+ return;
813
+ }
814
+ throw new Error(
815
+ "Setting a GitHub repo secret without `gh` CLI requires sodium encryption (libsodium) \u2014 not implemented. Install `gh` CLI to use this command."
816
+ );
817
+ }
818
+ async setVariable(cwd, name, value) {
819
+ if (this.preferGhCli && await ghAvailable()) {
820
+ await execa("gh", ["variable", "set", name, "--body", value], { cwd });
821
+ return;
822
+ }
823
+ const { owner, name: repoName } = await this.getRepoMeta(cwd);
824
+ if (!this.token) {
825
+ throw new Error("No `gh` CLI and no GH_TOKEN \u2014 cannot set repo variable.");
826
+ }
827
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repoName}/actions/variables`, {
828
+ method: "POST",
829
+ headers: { ...this.authHeaders(), "content-type": "application/json" },
830
+ body: JSON.stringify({ name, value })
831
+ });
832
+ if (res.status === 409) {
833
+ const update = await fetch(
834
+ `https://api.github.com/repos/${owner}/${repoName}/actions/variables/${name}`,
835
+ {
836
+ method: "PATCH",
837
+ headers: { ...this.authHeaders(), "content-type": "application/json" },
838
+ body: JSON.stringify({ value })
839
+ }
840
+ );
841
+ if (!update.ok) throw new Error(`GitHub REST variable update failed: HTTP ${update.status}`);
842
+ return;
843
+ }
844
+ if (!res.ok) throw new Error(`GitHub REST variable create failed: HTTP ${res.status}`);
845
+ }
846
+ async applyBranchProtection(cwd, branch, requiredChecks) {
847
+ const body = {
848
+ required_status_checks: { strict: false, contexts: requiredChecks },
849
+ enforce_admins: false,
850
+ required_pull_request_reviews: { dismiss_stale_reviews: true, required_approving_review_count: 0 },
851
+ restrictions: null
852
+ };
853
+ if (this.preferGhCli && await ghAvailable()) {
854
+ const meta2 = await this.getRepoMeta(cwd);
855
+ const res2 = await execa(
856
+ "gh",
857
+ ["api", "-X", "PUT", `/repos/${meta2.owner}/${meta2.name}/branches/${branch}/protection`, "--input", "-"],
858
+ { cwd, input: JSON.stringify(body), reject: false }
859
+ );
860
+ if (res2.exitCode === 0) return { applied: true };
861
+ return { applied: false, message: res2.stderr.split("\n")[0] || "gh api call failed" };
862
+ }
863
+ const meta = await this.getRepoMeta(cwd);
864
+ if (!this.token) {
865
+ return { applied: false, message: "No `gh` CLI and no GH_TOKEN \u2014 cannot apply branch protection." };
866
+ }
867
+ const res = await fetch(
868
+ `https://api.github.com/repos/${meta.owner}/${meta.name}/branches/${branch}/protection`,
869
+ {
870
+ method: "PUT",
871
+ headers: { ...this.authHeaders(), "content-type": "application/json" },
872
+ body: JSON.stringify(body)
873
+ }
874
+ );
875
+ if (res.ok) return { applied: true };
876
+ return { applied: false, message: `GitHub REST branch-protection PUT failed: HTTP ${res.status}` };
877
+ }
878
+ async createPullRequest(cwd, opts) {
879
+ if (this.preferGhCli && await ghAvailable()) {
880
+ const res2 = await execa(
881
+ "gh",
882
+ ["pr", "create", "--base", opts.base, "--head", opts.head, "--title", opts.title, "--body", opts.body],
883
+ { cwd }
884
+ );
885
+ return { url: res2.stdout.trim() };
886
+ }
887
+ const meta = await this.getRepoMeta(cwd);
888
+ if (!this.token) {
889
+ throw new Error("No `gh` CLI and no GH_TOKEN \u2014 cannot create pull request.");
890
+ }
891
+ const res = await fetch(`https://api.github.com/repos/${meta.owner}/${meta.name}/pulls`, {
892
+ method: "POST",
893
+ headers: { ...this.authHeaders(), "content-type": "application/json" },
894
+ body: JSON.stringify(opts)
895
+ });
896
+ if (!res.ok) throw new Error(`GitHub REST PR create failed: HTTP ${res.status}`);
897
+ const json = await res.json();
898
+ return { url: json.html_url };
899
+ }
900
+ authHeaders() {
901
+ if (!this.token) return { accept: "application/vnd.github+json" };
902
+ return { accept: "application/vnd.github+json", authorization: `Bearer ${this.token}` };
903
+ }
904
+ };
905
+ async function parseOriginRemote(cwd) {
906
+ const res = await execa("git", ["remote", "get-url", "origin"], { cwd, reject: false });
907
+ if (res.exitCode !== 0) {
908
+ throw new Error("No `origin` git remote configured; cannot determine GitHub repo.");
909
+ }
910
+ const url = res.stdout.trim();
911
+ const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?$/);
912
+ if (!match) {
913
+ throw new Error(`Could not parse GitHub owner/name from remote URL: ${url}`);
914
+ }
915
+ return { owner: match[1] ?? "", name: match[2] ?? "" };
916
+ }
917
+ async function detectProvider(cwd) {
918
+ const res = await execa("git", ["remote", "get-url", "origin"], { cwd, reject: false });
919
+ if (res.exitCode !== 0) {
920
+ throw new Error(
921
+ "No `origin` git remote configured. Initialise the repo and add a remote before running this command."
922
+ );
923
+ }
924
+ const url = res.stdout.trim();
925
+ return classifyRemoteUrl(url);
926
+ }
927
+ function classifyRemoteUrl(url) {
928
+ if (/github\.com[/:]/.test(url)) return { provider: "github", host: "github.com" };
929
+ if (/gitlab\.com[/:]/.test(url)) return { provider: "gitlab", host: "gitlab.com" };
930
+ if (/bitbucket\.org[/:]/.test(url)) return { provider: "bitbucket", host: "bitbucket.org" };
931
+ const hostMatch = url.match(/^(?:https?:\/\/|git@)([^:/]+)/);
932
+ return { provider: "self-hosted", host: hostMatch?.[1] ?? "unknown" };
933
+ }
934
+
935
+ // src/lib/git-provider/index.ts
936
+ async function getGitProvider(cwd) {
937
+ const { provider, host } = await detectProvider(cwd);
938
+ if (provider === "github") return new GitHubProvider();
939
+ throw new Error(
940
+ `Git provider '${provider}' (host: ${host}) is not yet supported. Only GitHub is implemented in workstream C; GitLab/Bitbucket/self-hosted are planned.`
941
+ );
942
+ }
943
+
944
+ // src/install/auth-probe.ts
945
+ async function runAuthProbe(ctx) {
946
+ const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
947
+ try {
948
+ await client.listProjects();
949
+ return { step: "1/11 Authenticate", status: "ok", message: `PAT accepted at ${ctx.baseUrl}` };
950
+ } catch (err) {
951
+ if (err instanceof DevAuditApiError && (err.status === 401 || err.status === 403)) {
952
+ throw new Error(
953
+ `PAT rejected (HTTP ${err.status}). Issue a fresh token at ${ctx.baseUrl}/settings/tokens and retry.`
954
+ );
955
+ }
956
+ throw err;
957
+ }
958
+ }
959
+ var MAX_DEPTH = 3;
960
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".turbo"]);
961
+ async function fileExists(path) {
962
+ try {
963
+ await promises.access(path);
964
+ return true;
965
+ } catch {
966
+ return false;
967
+ }
968
+ }
969
+ async function findPyproject(root, depth, current) {
970
+ if (depth > MAX_DEPTH) return null;
971
+ const entries = await promises.readdir(current, { withFileTypes: true });
972
+ for (const e of entries) {
973
+ if (e.isFile() && e.name === "pyproject.toml") {
974
+ return join(current, e.name);
975
+ }
976
+ }
977
+ for (const e of entries) {
978
+ if (e.isDirectory() && !SKIP_DIRS.has(e.name)) {
979
+ const found = await findPyproject(root, depth + 1, join(current, e.name));
980
+ if (found) return found;
981
+ }
982
+ }
983
+ return null;
984
+ }
985
+ async function detectStack(ctx) {
986
+ const root = ctx.projectPath;
987
+ if (await fileExists(join(root, "pyproject.toml"))) {
988
+ return { result: ok("python", "."), detected: { stack: "python", workingDirectory: "." } };
989
+ }
990
+ const nested = await findPyproject(root, 1, root);
991
+ if (nested) {
992
+ const wd = relative(root, nested).split("/").slice(0, -1).join("/") || ".";
993
+ return { result: ok("python", wd), detected: { stack: "python", workingDirectory: wd } };
994
+ }
995
+ if (await fileExists(join(root, "package.json"))) {
996
+ return { result: ok("node", "."), detected: { stack: "node", workingDirectory: "." } };
997
+ }
998
+ throw new Error(
999
+ "Could not detect stack \u2014 no pyproject.toml or package.json found within 3 directory levels."
1000
+ );
1001
+ }
1002
+ function ok(stack, wd) {
1003
+ return {
1004
+ step: "2/11 Detect stack",
1005
+ status: "ok",
1006
+ message: `stack=${stack} working_directory=${wd} host=railway`,
1007
+ data: { stack, workingDirectory: wd, host: "railway" }
1008
+ };
1009
+ }
1010
+ var NODE_DEFAULTS = { runtimeVersion: "20", sourceDirs: "app/ lib/" };
1011
+ var PYTHON_DEFAULTS = { runtimeVersion: "3.11", sourceDirs: "src/ tests/" };
1012
+ function defaultSlug(projectName) {
1013
+ return projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1014
+ }
1015
+ function prodUrlSecretDefault(slug) {
1016
+ return slug.toUpperCase().replace(/-/g, "_") + "_PROD_URL";
1017
+ }
1018
+ async function collectPlan(ctx, detected) {
1019
+ const defaults = detected.stack === "node" ? NODE_DEFAULTS : PYTHON_DEFAULTS;
1020
+ if (ctx.nonInteractive) {
1021
+ return planFromConfig(ctx, detected, defaults);
1022
+ }
1023
+ return planFromPrompts(ctx, detected, defaults);
1024
+ }
1025
+ async function planFromConfig(ctx, detected, defaults) {
1026
+ const cfg = await readSdlcConfig(ctx.projectPath);
1027
+ if (!cfg && !ctx.dryRun) {
1028
+ throw new Error(
1029
+ "--yes requires an existing sdlc-config.json in the project directory. Run without --yes to create one interactively."
1030
+ );
1031
+ }
1032
+ const slug = cfg?.project_slug ?? defaultSlug(ctx.projectName);
1033
+ const runtimeKey = detected.stack === "node" ? cfg?.node_version : cfg?.python_version;
1034
+ const cfgRaw = cfg;
1035
+ const existingProdUrlSecret = typeof cfgRaw?.["production_url_secret"] === "string" ? cfgRaw["production_url_secret"] : void 0;
1036
+ return {
1037
+ stack: detected.stack,
1038
+ host: "railway",
1039
+ projectSlug: slug,
1040
+ runtimeVersion: String(runtimeKey ?? defaults.runtimeVersion),
1041
+ sourceDirs: cfg?.source_dirs ?? defaults.sourceDirs,
1042
+ workingDirectory: cfg?.working_directory ?? detected.workingDirectory,
1043
+ prodUrlSecretName: existingProdUrlSecret ?? prodUrlSecretDefault(slug),
1044
+ prodUrlValue: ""
1045
+ };
1046
+ }
1047
+ async function planFromPrompts(ctx, detected, defaults) {
1048
+ const slugDefault = defaultSlug(ctx.projectName);
1049
+ const wdInitialDefault = detected.workingDirectory;
1050
+ const answers = await clack2.group(
1051
+ {
1052
+ projectSlug: () => clack2.text({ message: "Project slug", initialValue: slugDefault }),
1053
+ runtimeVersion: () => clack2.text({
1054
+ message: detected.stack === "node" ? "Node version" : "Python version",
1055
+ initialValue: defaults.runtimeVersion
1056
+ }),
1057
+ sourceDirs: () => clack2.text({ message: "Source dirs (space-sep)", initialValue: defaults.sourceDirs }),
1058
+ workingDirectory: () => clack2.text({
1059
+ message: wdInitialDefault === "." ? "Working directory (blank = root)" : "Working directory",
1060
+ initialValue: wdInitialDefault
1061
+ }),
1062
+ prodUrlSecretName: ({ results }) => clack2.text({
1063
+ message: "Production URL secret name",
1064
+ initialValue: prodUrlSecretDefault(String(results.projectSlug ?? slugDefault))
1065
+ }),
1066
+ prodUrlValue: () => clack2.text({ message: "Production URL (https://...) \u2014 blank to set later", initialValue: "" })
1067
+ },
1068
+ {
1069
+ onCancel: () => {
1070
+ process.stderr.write("Cancelled.\n");
1071
+ process.exit(0);
1072
+ }
1073
+ }
1074
+ );
1075
+ const projectSlug = String(answers.projectSlug);
1076
+ const workingDirectory = String(answers.workingDirectory) || ".";
1077
+ return {
1078
+ stack: detected.stack,
1079
+ host: "railway",
1080
+ projectSlug,
1081
+ runtimeVersion: String(answers.runtimeVersion),
1082
+ sourceDirs: String(answers.sourceDirs),
1083
+ workingDirectory,
1084
+ prodUrlSecretName: String(answers.prodUrlSecretName),
1085
+ prodUrlValue: String(answers.prodUrlValue ?? "")
1086
+ };
1087
+ }
1088
+ var NODE_PATHS_IGNORE = [
1089
+ "SDLC/**",
1090
+ "compliance/**",
1091
+ "*.md",
1092
+ ".cursorrules",
1093
+ ".windsurfrules",
1094
+ "sdlc-config.json",
1095
+ "scripts/upload-evidence.sh",
1096
+ "scripts/validate-compliance-artifacts.sh",
1097
+ "scripts/validate-commits.sh",
1098
+ "scripts/check-requirement-jsdoc.sh"
1099
+ ];
1100
+ var PYTHON_PATHS_IGNORE = [
1101
+ "SDLC/**",
1102
+ "compliance/**",
1103
+ "*.md",
1104
+ ".cursorrules",
1105
+ ".windsurfrules",
1106
+ "sdlc-config.json"
1107
+ ];
1108
+ async function writeSdlcConfig(ctx, plan) {
1109
+ const runtimeKey = plan.stack === "node" ? "node_version" : "python_version";
1110
+ const pathsIgnore = plan.stack === "node" ? NODE_PATHS_IGNORE : PYTHON_PATHS_IGNORE;
1111
+ const existing = await readSdlcConfig(ctx.projectPath) ?? null;
1112
+ const defaultedIfNew = {
1113
+ runner: "ubuntu-latest",
1114
+ sast_baseline: 0,
1115
+ accepted_dep_risks: "",
1116
+ database_service: "",
1117
+ database_image: "",
1118
+ database_port: "",
1119
+ database_env: {},
1120
+ app_env: {},
1121
+ build_env: {},
1122
+ e2e_project: "",
1123
+ e2e_start_command: "",
1124
+ paths_ignore: pathsIgnore,
1125
+ uat: { enabled: false, url: "", required_risk_classes: ["payment", "destructive_migration", "realtime"] },
1126
+ approval: { mode: "dual_actor", auto_low_risk_threshold: "LOW" },
1127
+ production_review: { enabled: true, terminal_status: "prod_review" }
1128
+ };
1129
+ const wizardOwned = {
1130
+ stack: plan.stack,
1131
+ host: plan.host,
1132
+ project_slug: plan.projectSlug,
1133
+ production_url_secret: plan.prodUrlSecretName,
1134
+ [runtimeKey]: plan.runtimeVersion,
1135
+ working_directory: plan.workingDirectory,
1136
+ source_dirs: plan.sourceDirs,
1137
+ devaudit: {
1138
+ base_url: ctx.baseUrl,
1139
+ project_slug: plan.projectSlug,
1140
+ api_key_secret: "DEVAUDIT_API_KEY"
1141
+ }
1142
+ };
1143
+ const config = {
1144
+ ...defaultedIfNew,
1145
+ ...existing ?? {},
1146
+ ...wizardOwned
1147
+ };
1148
+ const outPath = join(ctx.projectPath, "sdlc-config.json");
1149
+ if (ctx.dryRun) {
1150
+ const preserved = existing ? `preserves existing customizations (${Object.keys(existing).filter((k) => !(k in wizardOwned)).length} non-wizard fields)` : "fresh config";
1151
+ return {
1152
+ step: "4/11 Write sdlc-config.json",
1153
+ status: "planned",
1154
+ message: `would write ${outPath} (stack=${plan.stack}, slug=${plan.projectSlug}) \u2014 ${preserved}`
1155
+ };
1156
+ }
1157
+ await promises.writeFile(outPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1158
+ return { step: "4/11 Write sdlc-config.json", status: "ok", message: `wrote ${outPath}` };
1159
+ }
1160
+
1161
+ // src/install/project.ts
1162
+ async function findOrCreateProject(ctx, plan) {
1163
+ if (ctx.dryRun) {
1164
+ return {
1165
+ step: "5/11 Find or create DevAudit project",
1166
+ status: "planned",
1167
+ message: `would create or find project slug='${plan.projectSlug}' on ${ctx.baseUrl}`
1168
+ };
1169
+ }
1170
+ const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
1171
+ const existing = await client.getProjectBySlug(plan.projectSlug);
1172
+ if (existing) {
1173
+ plan.projectId = existing.id;
1174
+ return {
1175
+ step: "5/11 Find or create DevAudit project",
1176
+ status: "ok",
1177
+ message: `project '${plan.projectSlug}' already exists (id ${existing.id.slice(0, 8)}\u2026) \u2014 skipping creation`,
1178
+ data: { projectId: existing.id, created: false }
1179
+ };
1180
+ }
1181
+ const created = await client.createProject(plan.projectSlug, plan.projectSlug);
1182
+ plan.projectId = created.id;
1183
+ return {
1184
+ step: "5/11 Find or create DevAudit project",
1185
+ status: "ok",
1186
+ message: `project '${plan.projectSlug}' created (id ${created.id.slice(0, 8)}\u2026)`,
1187
+ data: { projectId: created.id, created: true }
1188
+ };
1189
+ }
1190
+
1191
+ // src/install/api-key.ts
1192
+ var KEY_NAME = "Onboarding-issued";
1193
+ async function issueApiKey(ctx, plan) {
1194
+ if (ctx.dryRun) {
1195
+ return {
1196
+ step: "6/11 Issue project API key",
1197
+ status: "planned",
1198
+ message: `would issue API key named '${KEY_NAME}' on project '${plan.projectSlug}' (if not already present)`
1199
+ };
1200
+ }
1201
+ if (!plan.projectId) {
1202
+ throw new Error("projectId missing from plan \u2014 step 5 must run before step 6.");
1203
+ }
1204
+ const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
1205
+ const existing = await client.listApiKeys(plan.projectId);
1206
+ const live = existing.find((k) => k.name === KEY_NAME && k.revoked_at === null);
1207
+ if (live) {
1208
+ return {
1209
+ step: "6/11 Issue project API key",
1210
+ status: "warn",
1211
+ message: `'${KEY_NAME}' API key already exists \u2014 revoke it in the portal and re-run, or set DEVAUDIT_API_KEY manually`
1212
+ };
1213
+ }
1214
+ const issued = await client.issueApiKey(plan.projectId, KEY_NAME);
1215
+ plan.apiKey = issued.plainTextKey;
1216
+ return {
1217
+ step: "6/11 Issue project API key",
1218
+ status: "ok",
1219
+ message: `issued (will be stored as repo secret DEVAUDIT_API_KEY)`
1220
+ };
1221
+ }
1222
+
1223
+ // src/install/github.ts
1224
+ function buildOperations(ctx, plan) {
1225
+ const operations = [];
1226
+ if (plan.apiKey) operations.push({ kind: "secret", name: "DEVAUDIT_API_KEY", value: plan.apiKey });
1227
+ operations.push({ kind: "secret", name: "DEVAUDIT_USER_TOKEN", value: ctx.token });
1228
+ if (plan.prodUrlValue) {
1229
+ operations.push({ kind: "secret", name: plan.prodUrlSecretName, value: plan.prodUrlValue });
1230
+ }
1231
+ operations.push({ kind: "variable", name: "DEVAUDIT_BASE_URL", value: ctx.baseUrl });
1232
+ return operations;
1233
+ }
1234
+ function buildSkipped(plan) {
1235
+ const skipped = [];
1236
+ if (!plan.apiKey) skipped.push("DEVAUDIT_API_KEY (no new key issued)");
1237
+ if (!plan.prodUrlValue) skipped.push(`${plan.prodUrlSecretName} (no value provided)`);
1238
+ return skipped;
1239
+ }
1240
+ async function setGithubSecrets(ctx, plan, provider) {
1241
+ const operations = buildOperations(ctx, plan);
1242
+ if (ctx.dryRun) {
1243
+ const summary = operations.map((op) => `${op.kind}:${op.name}`).join(", ");
1244
+ return {
1245
+ step: "7/11 Set GitHub secrets and variables",
1246
+ status: "planned",
1247
+ message: `would set ${summary} via ${provider.name} provider`
1248
+ };
1249
+ }
1250
+ for (const op of operations) {
1251
+ if (op.kind === "secret") {
1252
+ await provider.setSecret(ctx.projectPath, op.name, op.value);
1253
+ } else {
1254
+ await provider.setVariable(ctx.projectPath, op.name, op.value);
1255
+ }
1256
+ }
1257
+ const skipped = buildSkipped(plan);
1258
+ const detail = `${operations.length} item(s) set${skipped.length > 0 ? ` (skipped: ${skipped.join("; ")})` : ""}`;
1259
+ return { step: "7/11 Set GitHub secrets and variables", status: "ok", message: detail };
1260
+ }
1261
+ async function commandExists(cmd) {
1262
+ try {
1263
+ await execa(process.platform === "win32" ? "where" : "which", [cmd]);
1264
+ return true;
1265
+ } catch {
1266
+ return false;
1267
+ }
1268
+ }
1269
+ async function dirExists(path) {
1270
+ try {
1271
+ const s = await promises.stat(path);
1272
+ return s.isDirectory();
1273
+ } catch {
1274
+ return false;
1275
+ }
1276
+ }
1277
+ async function bootstrapHooks(ctx, plan) {
1278
+ if (ctx.dryRun) {
1279
+ const action = plan.stack === "python" ? "pre-commit install" : "npx husky init";
1280
+ return {
1281
+ step: "8/11 Bootstrap hook framework",
1282
+ status: "planned",
1283
+ message: `would run \`${action}\` in ${ctx.projectPath}`
1284
+ };
1285
+ }
1286
+ if (plan.stack === "python") return bootstrapPython(ctx);
1287
+ return bootstrapNode(ctx);
1288
+ }
1289
+ async function bootstrapPython(ctx) {
1290
+ if (!await commandExists("pre-commit")) {
1291
+ return {
1292
+ step: "8/11 Bootstrap hook framework",
1293
+ status: "warn",
1294
+ message: "pre-commit not on PATH \u2014 run `pip install pre-commit && pre-commit install` manually"
1295
+ };
1296
+ }
1297
+ await execa("pre-commit", ["install"], { cwd: ctx.projectPath, stdio: "inherit" });
1298
+ await execa("pre-commit", ["install", "--hook-type", "commit-msg"], { cwd: ctx.projectPath, stdio: "inherit" });
1299
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: "pre-commit hooks installed" };
1300
+ }
1301
+ async function bootstrapNode(ctx) {
1302
+ const huskyDir = join(ctx.projectPath, ".husky");
1303
+ if (await dirExists(huskyDir)) {
1304
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ already exists" };
1305
+ }
1306
+ if (!await commandExists("npx")) {
1307
+ return {
1308
+ step: "8/11 Bootstrap hook framework",
1309
+ status: "warn",
1310
+ message: "npx not on PATH \u2014 run `npx husky init` manually"
1311
+ };
1312
+ }
1313
+ await execa("npx", ["husky", "init"], { cwd: ctx.projectPath, stdio: "inherit" });
1314
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ bootstrapped" };
1315
+ }
1316
+
1317
+ // src/install/branch-protection.ts
1318
+ var REQUIRED_CHECKS = [
1319
+ "Compliance Validation",
1320
+ "DevAudit Release Approval",
1321
+ "Quality Gates"
1322
+ ];
1323
+ async function configureBranchProtection(ctx, provider) {
1324
+ let meta;
1325
+ try {
1326
+ meta = await provider.getRepoMeta(ctx.projectPath);
1327
+ } catch (err) {
1328
+ return {
1329
+ step: "9/11 Configure branch protection",
1330
+ status: "warn",
1331
+ message: `could not resolve git repo (${err.message}) \u2014 configure manually`
1332
+ };
1333
+ }
1334
+ const repo = `${meta.owner}/${meta.name}`;
1335
+ if (ctx.dryRun) {
1336
+ return {
1337
+ step: "9/11 Configure branch protection",
1338
+ status: "planned",
1339
+ message: `would apply branch protection on ${repo}:${meta.defaultBranch} with checks=${JSON.stringify(REQUIRED_CHECKS)}`
1340
+ };
1341
+ }
1342
+ const result = await provider.applyBranchProtection(ctx.projectPath, meta.defaultBranch, REQUIRED_CHECKS);
1343
+ if (result.applied) {
1344
+ return {
1345
+ step: "9/11 Configure branch protection",
1346
+ status: "ok",
1347
+ message: `required checks on ${meta.defaultBranch}: ${REQUIRED_CHECKS.join(", ")}`
1348
+ };
1349
+ }
1350
+ return {
1351
+ step: "9/11 Configure branch protection",
1352
+ status: "warn",
1353
+ message: `${result.message ?? "branch-protection apply failed"} \u2014 configure manually`
1354
+ };
1355
+ }
1356
+ async function loadStackAdapter(installerRoot, stack) {
1357
+ const path = join(installerRoot, "sdlc", "files", "stacks", stack, "adapter.json");
1358
+ const raw = await promises.readFile(path, "utf-8");
1359
+ return JSON.parse(raw);
1360
+ }
1361
+ async function listStacks(installerRoot) {
1362
+ const dir = join(installerRoot, "sdlc", "files", "stacks");
1363
+ const entries = await promises.readdir(dir, { withFileTypes: true });
1364
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_")).map((e) => e.name);
1365
+ }
1366
+ async function listHosts(installerRoot) {
1367
+ const dir = join(installerRoot, "sdlc", "files", "hosts");
1368
+ const entries = await promises.readdir(dir, { withFileTypes: true });
1369
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_")).map((e) => e.name);
1370
+ }
1371
+
1372
+ // src/update/resolve-adapters.ts
1373
+ async function resolveAdapters(projectPath, installerRoot) {
1374
+ const configPath = join(projectPath, "sdlc-config.json");
1375
+ let stack = "node";
1376
+ let host = "railway";
1377
+ let deprecatedDefaults = false;
1378
+ if (await exists(configPath)) {
1379
+ const raw = await promises.readFile(configPath, "utf-8");
1380
+ const cfg = JSON.parse(raw);
1381
+ if (cfg.stack) {
1382
+ stack = cfg.stack;
1383
+ } else {
1384
+ deprecatedDefaults = true;
1385
+ }
1386
+ if (cfg.host) {
1387
+ host = cfg.host;
1388
+ } else {
1389
+ deprecatedDefaults = true;
1390
+ }
1391
+ } else {
1392
+ deprecatedDefaults = true;
1393
+ }
1394
+ const stackPath = join(installerRoot, "sdlc", "files", "stacks", stack, "adapter.json");
1395
+ if (!await exists(stackPath)) {
1396
+ const available = await listStacks(installerRoot);
1397
+ throw new Error(`stack adapter not found: stacks/${stack}/adapter.json. Available: ${available.join(", ")}`);
1398
+ }
1399
+ const hostPath = join(installerRoot, "sdlc", "files", "hosts", host, "adapter.json");
1400
+ if (!await exists(hostPath)) {
1401
+ const available = await listHosts(installerRoot);
1402
+ throw new Error(`host adapter not found: hosts/${host}/adapter.json. Available: ${available.join(", ")}`);
1403
+ }
1404
+ return { stack, host, deprecatedDefaults };
1405
+ }
1406
+ async function syncStageDocs(ctx) {
1407
+ const sdlcTarget = join(ctx.projectPath, "SDLC");
1408
+ await ensureDir(sdlcTarget);
1409
+ const commonDir = join(ctx.installerRoot, "sdlc", "files", "_common");
1410
+ const mdFiles = await listFiles(commonDir, (n) => n.endsWith(".md"));
1411
+ for (const src of mdFiles) {
1412
+ await copyFile(src, join(sdlcTarget, fileBasename(src)));
1413
+ }
1414
+ return {
1415
+ name: "_common docs",
1416
+ filesSynced: mdFiles.length,
1417
+ message: "synced to SDLC/"
1418
+ };
1419
+ }
1420
+ var CURSOR_POINTER = `# Cursor Rules
1421
+
1422
+ All project rules, architectural standards, and development workflows are consolidated in:
1423
+ \u{1F449} **[INSTRUCTIONS.md](./INSTRUCTIONS.md)**
1424
+
1425
+ Please refer to \`INSTRUCTIONS.md\` as the **Single Source of Truth** for all development standards in this repository.
1426
+ `;
1427
+ var WINDSURF_POINTER = `# Windsurf Rules
1428
+
1429
+ All project rules, architectural standards, and development workflows are consolidated in:
1430
+ \u{1F449} **[INSTRUCTIONS.md](./INSTRUCTIONS.md)**
1431
+
1432
+ Please refer to \`INSTRUCTIONS.md\` as the **Single Source of Truth** for all development standards in this repository.
1433
+ `;
1434
+ var GEMINI_POINTER = `# GEMINI.md
1435
+
1436
+ This file provides guidance to Gemini CLI when working in this repository.
1437
+
1438
+ ## Context
1439
+
1440
+ **Project Standards:** See **[./INSTRUCTIONS.md](./INSTRUCTIONS.md)** for project rules, architecture, and development standards.
1441
+
1442
+ Please adhere to the instructions in \`INSTRUCTIONS.md\` as the **Single Source of Truth**.
1443
+ `;
1444
+ var CLAUDE_POINTER_TAIL = `
1445
+ ## Project Standards
1446
+
1447
+ All project rules, architectural standards, and development workflows are consolidated in:
1448
+ \u{1F449} **[INSTRUCTIONS.md](./INSTRUCTIONS.md)**
1449
+
1450
+ Please refer to \`INSTRUCTIONS.md\` as the **Single Source of Truth** for:
1451
+ - Tech Stack & Architecture
1452
+ - Code Style & Formatting
1453
+ - Security & Compliance
1454
+ - SDLC Development Process & Quality Gates
1455
+ `;
1456
+ var CLAUDE_NEW = `# CLAUDE.md
1457
+
1458
+ This file provides guidance to Claude Code when working in this repository.
1459
+ ${CLAUDE_POINTER_TAIL}`;
1460
+ var SDLC_HEADER = "## SDLC Compliance Process (MANDATORY)";
1461
+ async function writePointerFile(path, content) {
1462
+ await promises.writeFile(path, content);
1463
+ }
1464
+ async function updateClaudeFile(target) {
1465
+ if (!await exists(target)) {
1466
+ await promises.writeFile(target, CLAUDE_NEW);
1467
+ return;
1468
+ }
1469
+ let body = await promises.readFile(target, "utf-8");
1470
+ const sdlcIdx = body.indexOf(SDLC_HEADER);
1471
+ if (sdlcIdx >= 0) {
1472
+ body = body.slice(0, sdlcIdx).trimEnd() + "\n";
1473
+ }
1474
+ if (!body.includes("INSTRUCTIONS.md")) {
1475
+ body = body.trimEnd() + "\n" + CLAUDE_POINTER_TAIL;
1476
+ }
1477
+ await promises.writeFile(target, body);
1478
+ }
1479
+ async function updateInstructionsFile(target, sdlcContent) {
1480
+ if (!await exists(target)) {
1481
+ const body = "# Project Instructions & Standards (Single Source of Truth)\n\nThis document serves as the primary reference for all development in this repository.\n\n" + sdlcContent;
1482
+ await promises.writeFile(target, body);
1483
+ return;
1484
+ }
1485
+ let existing = await promises.readFile(target, "utf-8");
1486
+ const sdlcIdx = existing.indexOf(SDLC_HEADER);
1487
+ if (sdlcIdx >= 0) {
1488
+ existing = existing.slice(0, sdlcIdx).replace(/\n*$/u, "\n");
1489
+ } else {
1490
+ existing = existing.replace(/\n*$/u, "\n");
1491
+ }
1492
+ const next = existing + "\n" + sdlcContent;
1493
+ await promises.writeFile(target, next);
1494
+ }
1495
+ async function syncAiRules(ctx) {
1496
+ const sdlcSource = join(ctx.installerRoot, "sdlc", "ai-rules", "INSTRUCTIONS-SDLC.md");
1497
+ if (!await exists(sdlcSource)) {
1498
+ return { name: "AI rule pointers + INSTRUCTIONS.md", filesSynced: 0, skipped: true, message: "INSTRUCTIONS-SDLC.md not found" };
1499
+ }
1500
+ const sdlcContent = await promises.readFile(sdlcSource, "utf-8");
1501
+ await writePointerFile(join(ctx.projectPath, ".cursorrules"), CURSOR_POINTER);
1502
+ await writePointerFile(join(ctx.projectPath, ".windsurfrules"), WINDSURF_POINTER);
1503
+ await writePointerFile(join(ctx.projectPath, "GEMINI.md"), GEMINI_POINTER);
1504
+ await updateClaudeFile(join(ctx.projectPath, "CLAUDE.md"));
1505
+ await updateInstructionsFile(join(ctx.projectPath, "INSTRUCTIONS.md"), sdlcContent);
1506
+ return { name: "AI rule pointers + INSTRUCTIONS.md", filesSynced: 5, message: "synced" };
1507
+ }
1508
+ async function syncStackHooks(ctx) {
1509
+ const adapter = await loadStackAdapter(ctx.installerRoot, ctx.stack);
1510
+ const hookInstallDir = adapter.hook_install_dir ?? "";
1511
+ if (!hookInstallDir) {
1512
+ return { name: `${ctx.stack} hooks`, filesSynced: 0, skipped: true, message: "no hook_install_dir declared" };
1513
+ }
1514
+ const targetDir = join(ctx.projectPath, hookInstallDir);
1515
+ if (!await isDir(targetDir)) {
1516
+ return {
1517
+ name: `${ctx.stack} hooks`,
1518
+ filesSynced: 0,
1519
+ skipped: true,
1520
+ message: `${hookInstallDir}/ not found \u2014 bootstrap hook framework first`
1521
+ };
1522
+ }
1523
+ const stackHooksDir = join(ctx.installerRoot, "sdlc", "files", "stacks", ctx.stack, "hooks");
1524
+ if (!await isDir(stackHooksDir)) {
1525
+ return { name: `${ctx.stack} hooks`, filesSynced: 0, skipped: true, message: "stack has no hooks/" };
1526
+ }
1527
+ let count = 0;
1528
+ for (const hook of adapter.hooks ?? []) {
1529
+ const src = join(stackHooksDir, hook);
1530
+ if (await exists(src)) {
1531
+ const dst = join(targetDir, hook);
1532
+ await copyFile(src, dst, 493);
1533
+ count += 1;
1534
+ }
1535
+ }
1536
+ for (const cfg of adapter.hook_config_files ?? []) {
1537
+ const src = join(stackHooksDir, cfg);
1538
+ if (await exists(src)) {
1539
+ const dst = join(ctx.projectPath, cfg);
1540
+ await copyFile(src, dst);
1541
+ count += 1;
1542
+ }
1543
+ }
1544
+ return { name: `${ctx.stack} hooks`, filesSynced: count, message: `synced to ${hookInstallDir}/` };
1545
+ }
1546
+ async function syncStackDeps(ctx) {
1547
+ if (ctx.stack !== "node") {
1548
+ return { name: `${ctx.stack} deps`, filesSynced: 0, skipped: true };
1549
+ }
1550
+ const pkgPath = join(ctx.projectPath, "package.json");
1551
+ if (!await exists(pkgPath)) {
1552
+ return { name: `${ctx.stack} deps`, filesSynced: 0, skipped: true, message: "no package.json" };
1553
+ }
1554
+ const adapter = await loadStackAdapter(ctx.installerRoot, ctx.stack);
1555
+ const required = adapter.required_dev_dependencies ?? [];
1556
+ if (required.length === 0) {
1557
+ return { name: `${ctx.stack} deps`, filesSynced: 0, message: "no required_dev_dependencies declared" };
1558
+ }
1559
+ const raw = await promises.readFile(pkgPath, "utf-8");
1560
+ const pkg = JSON.parse(raw);
1561
+ const installed = new Set(Object.keys(pkg.devDependencies ?? {}));
1562
+ const missing = required.filter((dep) => !installed.has(dep));
1563
+ if (missing.length === 0) {
1564
+ return { name: `${ctx.stack} deps`, filesSynced: 0, message: "all present" };
1565
+ }
1566
+ const args = ["install", "--save-dev", ...missing];
1567
+ const first = await execa("npm", args, { cwd: ctx.projectPath, reject: false, stdio: "inherit" });
1568
+ if (first.exitCode === 0) {
1569
+ return { name: `${ctx.stack} deps`, filesSynced: missing.length, message: `installed ${missing.join(" ")}` };
1570
+ }
1571
+ const legacyArgs = ["install", "--save-dev", "--legacy-peer-deps", ...missing];
1572
+ const second = await execa("npm", legacyArgs, { cwd: ctx.projectPath, reject: false, stdio: "inherit" });
1573
+ if (second.exitCode === 0) {
1574
+ return {
1575
+ name: `${ctx.stack} deps`,
1576
+ filesSynced: missing.length,
1577
+ message: `installed ${missing.join(" ")} (with --legacy-peer-deps)`
1578
+ };
1579
+ }
1580
+ throw new Error(
1581
+ `Failed to install ${ctx.stack} deps. Fix manually: cd ${ctx.projectPath} && npm install --save-dev ${missing.join(" ")}`
1582
+ );
1583
+ }
1584
+ function isTestScript(name) {
1585
+ return name.endsWith(".test.sh");
1586
+ }
1587
+ async function syncScripts(ctx) {
1588
+ const scriptsDst = join(ctx.projectPath, "scripts");
1589
+ if (!await isDir(scriptsDst)) {
1590
+ return { name: "scripts", filesSynced: 0, skipped: true, message: "scripts/ not found" };
1591
+ }
1592
+ let count = 0;
1593
+ const commonScriptsSrc = join(ctx.installerRoot, "sdlc", "files", "_common", "scripts");
1594
+ if (await isDir(commonScriptsSrc)) {
1595
+ const candidates = await listFiles(commonScriptsSrc, (n) => n.endsWith(".sh") && !isTestScript(n));
1596
+ for (const src of candidates) {
1597
+ await copyFile(src, join(scriptsDst, fileBasename(src)), 493);
1598
+ count += 1;
1599
+ }
1600
+ }
1601
+ const adapter = await loadStackAdapter(ctx.installerRoot, ctx.stack);
1602
+ const stackScriptsSrc = join(ctx.installerRoot, "sdlc", "files", "stacks", ctx.stack, "scripts");
1603
+ if (await isDir(stackScriptsSrc) && adapter.stack_scripts) {
1604
+ for (const scriptName of adapter.stack_scripts) {
1605
+ const src = join(stackScriptsSrc, scriptName);
1606
+ if (await exists(src)) {
1607
+ await copyFile(src, join(scriptsDst, scriptName), 493);
1608
+ count += 1;
1609
+ }
1610
+ }
1611
+ }
1612
+ const uploadEvidence2 = join(ctx.installerRoot, "scripts", "upload-evidence.sh");
1613
+ if (await exists(uploadEvidence2)) {
1614
+ await copyFile(uploadEvidence2, join(scriptsDst, "upload-evidence.sh"), 493);
1615
+ count += 1;
1616
+ }
1617
+ return { name: "scripts", filesSynced: count, message: "synced to scripts/" };
1618
+ }
1619
+ async function syncIssueTemplates(ctx) {
1620
+ const src = join(ctx.installerRoot, "sdlc", "files", "_common", "github", "ISSUE_TEMPLATE");
1621
+ if (!await isDir(src)) {
1622
+ return { name: "Issue templates", filesSynced: 0, skipped: true };
1623
+ }
1624
+ const dst = join(ctx.projectPath, ".github", "ISSUE_TEMPLATE");
1625
+ await ensureDir(dst);
1626
+ const files = await listFiles(src, (n) => n.endsWith(".yml"));
1627
+ for (const file of files) {
1628
+ await copyFile(file, join(dst, fileBasename(file)));
1629
+ }
1630
+ return { name: "Issue templates", filesSynced: files.length, message: "synced to .github/ISSUE_TEMPLATE/" };
1631
+ }
1632
+ async function syncSkills(ctx) {
1633
+ const skillDst = join(ctx.projectPath, ".claude", "skills");
1634
+ const commonSkills = join(ctx.installerRoot, "sdlc", "files", "_common", "skills");
1635
+ const stackSkills = join(ctx.installerRoot, "sdlc", "files", "stacks", ctx.stack, "skills");
1636
+ await ensureDir(skillDst);
1637
+ let count = 0;
1638
+ for (const src of [commonSkills, stackSkills]) {
1639
+ if (!await isDir(src)) continue;
1640
+ const entries = await promises.readdir(src, { withFileTypes: true });
1641
+ for (const entry of entries) {
1642
+ if (!entry.isDirectory()) continue;
1643
+ if (entry.name.startsWith("_")) continue;
1644
+ const skillSrc = join(src, entry.name);
1645
+ const skillDstDir = join(skillDst, entry.name);
1646
+ await copyDir(skillSrc, skillDstDir, true);
1647
+ count += 1;
1648
+ }
1649
+ }
1650
+ if (count === 0) {
1651
+ return { name: "Claude Code skills", filesSynced: 0, skipped: true };
1652
+ }
1653
+ return { name: "Claude Code skills", filesSynced: count, message: `${count} synced to .claude/skills/` };
1654
+ }
1655
+ async function syncEvidenceHelper(ctx) {
1656
+ if (ctx.stack !== "node") {
1657
+ return { name: "E2E evidence helper", filesSynced: 0, skipped: true };
1658
+ }
1659
+ const src = join(
1660
+ ctx.installerRoot,
1661
+ "sdlc",
1662
+ "files",
1663
+ "_common",
1664
+ "skills",
1665
+ "e2e-test-engineer",
1666
+ "references",
1667
+ "evidence.ts"
1668
+ );
1669
+ if (!await exists(src)) {
1670
+ return { name: "E2E evidence helper", filesSynced: 0, skipped: true, message: "source not found" };
1671
+ }
1672
+ const dst = join(ctx.projectPath, "e2e", "helpers", "evidence.ts");
1673
+ await copyFile(src, dst);
1674
+ return { name: "E2E evidence helper", filesSynced: 1, message: "synced to e2e/helpers/evidence.ts" };
1675
+ }
1676
+
1677
+ // src/lib/templates.ts
1678
+ function substituteTokens(content, tokens) {
1679
+ let out = content;
1680
+ for (const [key, value] of Object.entries(tokens)) {
1681
+ const needle = `{{${key}}}`;
1682
+ out = out.split(needle).join(value);
1683
+ }
1684
+ return out;
1685
+ }
1686
+ function substituteBlocks(content, blocks) {
1687
+ if (Object.keys(blocks).length === 0) return content;
1688
+ Object.keys(blocks).map((k) => `{{${k}}}`);
1689
+ return content.split("\n").map((line) => {
1690
+ for (const [key, replacement] of Object.entries(blocks)) {
1691
+ const needle = `{{${key}}}`;
1692
+ if (line.includes(needle)) return replacement;
1693
+ }
1694
+ return line;
1695
+ }).join("\n");
1696
+ }
1697
+ function stripServicesBlock(content) {
1698
+ const lines = content.split("\n");
1699
+ const out = [];
1700
+ let inServices = false;
1701
+ for (const line of lines) {
1702
+ if (!inServices && /^ {4}services:\s*$/.test(line)) {
1703
+ inServices = true;
1704
+ continue;
1705
+ }
1706
+ if (inServices) {
1707
+ if (line.trim() === "") {
1708
+ inServices = false;
1709
+ continue;
1710
+ }
1711
+ continue;
1712
+ }
1713
+ out.push(line);
1714
+ }
1715
+ return out.join("\n");
1716
+ }
1717
+
1718
+ // src/update/ci-templates.ts
1719
+ var CI_TEMPLATES = [
1720
+ "ci.yml.template",
1721
+ "ci-status-fallback.yml.template",
1722
+ "compliance-validation.yml.template",
1723
+ "check-release-approval.yml.template",
1724
+ "post-deploy-prod.yml.template",
1725
+ "compliance-evidence.yml.template"
1726
+ ];
1727
+ var OLD_WORKFLOWS_TO_REMOVE = ["test-on-pr.yml", "check-uat-approval.yml"];
1728
+ function indentEnvBlock(env, indent) {
1729
+ const pad = " ".repeat(indent);
1730
+ return Object.entries(env).map(([k, v]) => `${pad}${k}: ${v}`).join("\n");
1731
+ }
1732
+ function buildDbUriStep(dbService, dbPort) {
1733
+ if (dbService !== "mongodb") return "";
1734
+ return [
1735
+ " - name: Set database URI from dynamic port",
1736
+ " run: |",
1737
+ ` DB_PORT="\${{ job.services.${dbService}.ports['${dbPort}'] }}"`,
1738
+ ' echo "MONGODB_WAWAGARDENBAR_APP_URI=mongodb://localhost:${DB_PORT}" >> "$GITHUB_ENV"',
1739
+ ' echo "Database on port: ${DB_PORT}"'
1740
+ ].join("\n");
1741
+ }
1742
+ async function syncCiTemplates(ctx) {
1743
+ const configPath = join(ctx.projectPath, "sdlc-config.json");
1744
+ const workflowsDir = join(ctx.projectPath, ".github", "workflows");
1745
+ if (!await exists(configPath)) {
1746
+ return { name: "CI workflows", filesSynced: 0, skipped: true, message: "no sdlc-config.json" };
1747
+ }
1748
+ if (!await isDir(workflowsDir)) {
1749
+ return { name: "CI workflows", filesSynced: 0, skipped: true, message: ".github/workflows/ not found" };
1750
+ }
1751
+ await ensureDir(workflowsDir);
1752
+ const cfg = JSON.parse(await promises.readFile(configPath, "utf-8"));
1753
+ for (const oldName of OLD_WORKFLOWS_TO_REMOVE) {
1754
+ const oldPath = join(workflowsDir, oldName);
1755
+ if (await exists(oldPath)) await promises.rm(oldPath);
1756
+ }
1757
+ const workingDirectory = cfg.working_directory && cfg.working_directory !== "." ? cfg.working_directory : "";
1758
+ const workingDirPrefix = workingDirectory ? `${workingDirectory.replace(/\/$/, "")}/` : "";
1759
+ const tokens = {
1760
+ PROJECT_SLUG: cfg.project_slug,
1761
+ PRODUCTION_URL_SECRET: cfg.production_url_secret,
1762
+ NODE_VERSION: String(cfg.node_version ?? ""),
1763
+ PYTHON_VERSION: String(cfg.python_version ?? ""),
1764
+ WORKING_DIRECTORY: workingDirectory || ".",
1765
+ WORKING_DIR_PREFIX: workingDirPrefix,
1766
+ RUNNER: cfg.runner,
1767
+ SOURCE_DIRS: cfg.source_dirs,
1768
+ SAST_BASELINE: String(cfg.sast_baseline),
1769
+ ACCEPTED_DEP_RISKS: cfg.accepted_dep_risks,
1770
+ DATABASE_SERVICE: cfg.database_service,
1771
+ DATABASE_IMAGE: cfg.database_image,
1772
+ DATABASE_PORT: cfg.database_port,
1773
+ E2E_PROJECT: cfg.e2e_project,
1774
+ E2E_START_COMMAND: cfg.e2e_start_command
1775
+ };
1776
+ const pathsIgnoreBlock = (cfg.paths_ignore ?? []).map((p) => ` - '${p}'`).join("\n");
1777
+ const blocks = {
1778
+ PATHS_IGNORE: pathsIgnoreBlock,
1779
+ DATABASE_ENV: cfg.database_env ? indentEnvBlock({ ...cfg.database_env }, 6) : "",
1780
+ APP_ENV: cfg.app_env ? indentEnvBlock({ ...cfg.app_env }, 6) : "",
1781
+ BUILD_ENV: cfg.build_env ? indentEnvBlock({ ...cfg.build_env }, 10) : "",
1782
+ DATABASE_URI_STEP: buildDbUriStep(cfg.database_service, cfg.database_port)
1783
+ };
1784
+ let count = 0;
1785
+ for (const tmpl of CI_TEMPLATES) {
1786
+ const stackTmpl = join(ctx.installerRoot, "sdlc", "files", "ci", ctx.stack, tmpl);
1787
+ const defaultTmpl = join(ctx.installerRoot, "sdlc", "files", "ci", tmpl);
1788
+ let tmplPath;
1789
+ if (await exists(stackTmpl)) {
1790
+ tmplPath = stackTmpl;
1791
+ } else if (await exists(defaultTmpl)) {
1792
+ tmplPath = defaultTmpl;
1793
+ } else {
1794
+ continue;
1795
+ }
1796
+ const outputName = tmpl.replace(/\.template$/, "");
1797
+ const outputPath = join(workflowsDir, outputName);
1798
+ let content = await promises.readFile(tmplPath, "utf-8");
1799
+ content = substituteTokens(content, tokens);
1800
+ content = substituteBlocks(content, blocks);
1801
+ if (!cfg.database_service) {
1802
+ content = stripServicesBlock(content);
1803
+ }
1804
+ await promises.writeFile(outputPath, content);
1805
+ count += 1;
1806
+ }
1807
+ return { name: "CI workflows", filesSynced: count, message: `${count} generated` };
1808
+ }
1809
+ var TIER_1_DOCS = ["Test_Policy.md", "Test_Strategy.md", "Test_Architecture.md"];
1810
+ async function runValidation(projectPath) {
1811
+ const warnings = [];
1812
+ const workflowsDir = join(projectPath, ".github", "workflows");
1813
+ if (await isDir(workflowsDir)) {
1814
+ const ymls = await listFiles(workflowsDir, (n) => n.endsWith(".yml"));
1815
+ for (const wf of ymls) {
1816
+ const content = await promises.readFile(wf, "utf-8");
1817
+ const name = fileBasename(wf);
1818
+ if (content.includes("push:") && !content.includes("pull_request:")) {
1819
+ const dead = (content.match(/event_name.*pull_request/g) ?? []).length;
1820
+ if (dead > 0) {
1821
+ warnings.push(`${name} has ${dead} dead 'event_name == pull_request' condition(s) (push-only trigger)`);
1822
+ }
1823
+ }
1824
+ if (/require.*package\.json.*version/i.test(content)) {
1825
+ warnings.push(`${name} uses package.json for version (should be date-based)`);
1826
+ }
1827
+ if (content.includes("raw.githubusercontent.com/metasession-dev/devaudit")) {
1828
+ warnings.push(`${name} downloads from DevAudit at runtime (should use local scripts)`);
1829
+ }
1830
+ }
1831
+ }
1832
+ const sdlcDir = join(projectPath, "SDLC");
1833
+ if (await isDir(sdlcDir)) {
1834
+ for (const doc of TIER_1_DOCS) {
1835
+ if (!await exists(join(sdlcDir, doc))) {
1836
+ warnings.push(`Missing Tier 1 doc: SDLC/${doc}`);
1837
+ }
1838
+ }
1839
+ }
1840
+ return warnings;
1841
+ }
1842
+
1843
+ // src/update/index.ts
1844
+ var SECTION_RUNNERS = [
1845
+ { key: "2a", run: syncStageDocs },
1846
+ { key: "2b", run: syncAiRules },
1847
+ { key: "2c", run: syncStackHooks },
1848
+ { key: "2c-ii", run: syncStackDeps },
1849
+ { key: "2d", run: syncScripts },
1850
+ { key: "2e", run: syncIssueTemplates },
1851
+ { key: "2e-ii", run: syncSkills },
1852
+ { key: "2e-iii", run: syncEvidenceHelper },
1853
+ { key: "2f", run: syncCiTemplates }
1854
+ ];
1855
+ async function syncProject(projectPath) {
1856
+ const absPath = resolve(projectPath);
1857
+ if (!await isDir(absPath)) {
1858
+ throw new Error(`Project path not found: ${absPath}`);
1859
+ }
1860
+ const installerRoot = await resolveInstallerRoot();
1861
+ const log = logger();
1862
+ const projectName = basename(absPath);
1863
+ log.info(`--- Syncing to: ${projectName} (${absPath}) ---`);
1864
+ const { stack, host, deprecatedDefaults } = await resolveAdapters(absPath, installerRoot);
1865
+ log.info(` Stack: ${stack} | Host: ${host}`);
1866
+ if (deprecatedDefaults) {
1867
+ log.warn(` DEPRECATED: stack/host keys missing from sdlc-config.json \u2014 defaulted to ${stack}+${host}.`);
1868
+ }
1869
+ const ctx = { installerRoot, projectPath: absPath, projectName, stack, host };
1870
+ const sections = [];
1871
+ let total = 0;
1872
+ for (const { key, run } of SECTION_RUNNERS) {
1873
+ const result = await run(ctx);
1874
+ sections.push(result);
1875
+ total += result.filesSynced;
1876
+ if (result.skipped) {
1877
+ log.log(` [${key}] ${result.name}: SKIPPED${result.message ? ` (${result.message})` : ""}`);
1878
+ } else {
1879
+ log.log(` [${key}] ${result.name}: ${result.filesSynced} file(s)${result.message ? ` \u2014 ${result.message}` : ""}`);
1880
+ }
1881
+ }
1882
+ log.log("");
1883
+ log.info(` Total: ${total} files synced`);
1884
+ log.log("");
1885
+ log.log(" --- Validation ---");
1886
+ const warnings = await runValidation(absPath);
1887
+ if (warnings.length === 0) {
1888
+ log.success(" All validation checks passed");
1889
+ } else {
1890
+ for (const w of warnings) log.warn(` ${w}`);
1891
+ }
1892
+ log.log("");
1893
+ return { project: projectName, stack, host, sections, totalFilesSynced: total, warnings };
1894
+ }
1895
+ async function syncAll(projectPaths) {
1896
+ const reports = [];
1897
+ for (const p of projectPaths) {
1898
+ try {
1899
+ reports.push(await syncProject(p));
1900
+ } catch (err) {
1901
+ const log = logger();
1902
+ log.error(`ERROR syncing ${p}: ${err instanceof Error ? err.message : String(err)}`);
1903
+ throw err;
1904
+ }
1905
+ }
1906
+ return reports;
1907
+ }
1908
+
1909
+ // src/install/sync-templates.ts
1910
+ async function syncTemplates(ctx) {
1911
+ if (ctx.dryRun) {
1912
+ return {
1913
+ step: "10/11 Sync SDLC templates",
1914
+ status: "planned",
1915
+ message: `would run native syncProject() against ${ctx.projectPath}`
1916
+ };
1917
+ }
1918
+ const report = await syncProject(ctx.projectPath);
1919
+ return {
1920
+ step: "10/11 Sync SDLC templates",
1921
+ status: "ok",
1922
+ message: `synced ${report.totalFilesSynced} files across ${report.sections.length} sections`,
1923
+ data: { totalFilesSynced: report.totalFilesSynced }
1924
+ };
1925
+ }
1926
+
1927
+ // src/install/done-report.ts
1928
+ function doneReport(ctx, plan) {
1929
+ const branch = "feat/sdlc-onboarding";
1930
+ const lines = [
1931
+ "",
1932
+ ` ${ctx.projectName} is onboarded.`,
1933
+ "",
1934
+ " Next steps:",
1935
+ ` cd ${ctx.projectPath}`,
1936
+ " git status # review the diff",
1937
+ ` git checkout -b ${branch}`,
1938
+ " git add -A",
1939
+ ` git commit -m "feat: onboard ${plan.projectSlug} to Metasession SDLC"`,
1940
+ ` git push -u origin ${branch}`,
1941
+ " gh pr create --base main",
1942
+ "",
1943
+ " After the PR merges:",
1944
+ " - Push a compliance/ doc to develop so compliance-evidence.yml",
1945
+ " registers the first release in DevAudit.",
1946
+ " - Then walk REQ-001 through SDLC/0-project-setup.md \u2192 SDLC/5-deploy-main.md.",
1947
+ ""
1948
+ ];
1949
+ return {
1950
+ step: "11/11 Done",
1951
+ status: "ok",
1952
+ message: lines.join("\n"),
1953
+ data: { nextBranch: branch }
1954
+ };
1955
+ }
1956
+
1957
+ // src/install/index.ts
1958
+ async function runInstall(options) {
1959
+ const log = logger();
1960
+ const projectPath = resolve(options.path ?? process.cwd());
1961
+ if (!await isDir(projectPath)) {
1962
+ throw new Error(`Project path not found: ${projectPath}`);
1963
+ }
1964
+ const projectName = basename(projectPath);
1965
+ const auth = await resolveTokenForInstall(options);
1966
+ const installerRoot = await resolveInstallerRoot();
1967
+ const ctx = {
1968
+ projectPath,
1969
+ projectName,
1970
+ installerRoot,
1971
+ token: auth.token,
1972
+ baseUrl: auth.baseUrl,
1973
+ dryRun: Boolean(options.dryRun),
1974
+ nonInteractive: Boolean(options.nonInteractive)
1975
+ };
1976
+ banner(ctx);
1977
+ const steps = [];
1978
+ steps.push(await record(log, runAuthProbe(ctx)));
1979
+ const plugins = options.plugins ?? (await discoverPlugins()).loaded;
1980
+ if (plugins.length > 0 && !ctx.dryRun) {
1981
+ const pluginCtx = await buildPluginContext({ projectPath: ctx.projectPath });
1982
+ await runHook(plugins, "beforeInstall", pluginCtx);
1983
+ }
1984
+ const { result: detectResult, detected } = await detectStack(ctx);
1985
+ steps.push(await record(log, Promise.resolve(detectResult)));
1986
+ const plan = await collectPlan(ctx, detected);
1987
+ const planStep = planSummary(plan);
1988
+ steps.push(planStep);
1989
+ log.success(`[${planStep.step}] ${planStep.message ?? ""}`);
1990
+ steps.push(await record(log, writeSdlcConfig(ctx, plan)));
1991
+ steps.push(await record(log, findOrCreateProject(ctx, plan)));
1992
+ steps.push(await record(log, issueApiKey(ctx, plan)));
1993
+ const providerResolution = await resolveProvider(options, ctx);
1994
+ if (providerResolution.provider) {
1995
+ steps.push(await record(log, setGithubSecrets(ctx, plan, providerResolution.provider)));
1996
+ } else {
1997
+ const skipped = {
1998
+ step: "7/11 Set GitHub secrets and variables",
1999
+ status: "skipped",
2000
+ message: providerResolution.reason ?? "no git provider available"
2001
+ };
2002
+ steps.push(skipped);
2003
+ log.warn(`[${skipped.step}] SKIPPED ${skipped.message}`);
2004
+ }
2005
+ steps.push(await record(log, bootstrapHooks(ctx, plan)));
2006
+ if (providerResolution.provider) {
2007
+ steps.push(await record(log, configureBranchProtection(ctx, providerResolution.provider)));
2008
+ } else {
2009
+ const skipped = {
2010
+ step: "9/11 Configure branch protection",
2011
+ status: "skipped",
2012
+ message: providerResolution.reason ?? "no git provider available"
2013
+ };
2014
+ steps.push(skipped);
2015
+ log.warn(`[${skipped.step}] SKIPPED ${skipped.message}`);
2016
+ }
2017
+ steps.push(await record(log, syncTemplates(ctx)));
2018
+ const done = doneReport(ctx, plan);
2019
+ steps.push(done);
2020
+ log.success(`[${done.step}]`);
2021
+ log.log(done.message ?? "");
2022
+ if (plugins.length > 0 && !ctx.dryRun) {
2023
+ const pluginCtx = await buildPluginContext({ projectPath: ctx.projectPath });
2024
+ await runHook(plugins, "afterInstall", pluginCtx);
2025
+ }
2026
+ return { project: projectName, projectPath, dryRun: ctx.dryRun, steps };
2027
+ }
2028
+ async function resolveProvider(options, ctx) {
2029
+ if (options.provider) return { provider: options.provider };
2030
+ try {
2031
+ return { provider: await getGitProvider(ctx.projectPath) };
2032
+ } catch (err) {
2033
+ return { provider: null, reason: err.message };
2034
+ }
2035
+ }
2036
+ async function resolveTokenForInstall(options) {
2037
+ if (options.token) {
2038
+ return { token: options.token, baseUrl: options.baseUrl ?? "https://devaudit.metasession.co" };
2039
+ }
2040
+ const resolved = await resolveToken();
2041
+ if (!resolved) {
2042
+ throw new Error(
2043
+ "No DevAudit token found. Set DEVAUDIT_USER_TOKEN, pass --token, or run `devaudit auth login` first."
2044
+ );
2045
+ }
2046
+ return { token: resolved.token, baseUrl: options.baseUrl ?? resolved.baseUrl };
2047
+ }
2048
+ async function record(log, p) {
2049
+ const result = await p;
2050
+ const tag = `[${result.step}]`;
2051
+ const msg = result.message ?? "";
2052
+ if (result.status === "ok") log.success(`${tag} ${msg}`);
2053
+ else if (result.status === "warn") log.warn(`${tag} ${msg}`);
2054
+ else if (result.status === "skipped") log.info(`${tag} SKIPPED ${msg}`);
2055
+ else if (result.status === "planned") log.info(`${tag} [dry-run] ${msg}`);
2056
+ else log.error(`${tag} ${msg}`);
2057
+ return result;
2058
+ }
2059
+ function planSummary(plan) {
2060
+ return {
2061
+ step: "3/11 Configure",
2062
+ status: "ok",
2063
+ message: `slug=${plan.projectSlug} runtime=${plan.runtimeVersion}`,
2064
+ data: { ...plan }
2065
+ };
2066
+ }
2067
+ function banner(ctx) {
2068
+ const log = logger();
2069
+ log.log("");
2070
+ log.info(`Metasession SDLC Onboarding`);
2071
+ log.log(` Consumer: ${ctx.projectName}`);
2072
+ log.log(` Path: ${ctx.projectPath}`);
2073
+ log.log(` DevAudit: ${ctx.baseUrl}`);
2074
+ if (ctx.dryRun) log.warn(" DRY RUN \u2014 no mutations will be performed");
2075
+ log.log("");
2076
+ }
2077
+
2078
+ // src/commands/install.ts
2079
+ async function runInstallCommand(options) {
2080
+ const log = logger();
2081
+ try {
2082
+ await runInstall({
2083
+ ...options.path !== void 0 ? { path: options.path } : {},
2084
+ ...options.token !== void 0 ? { token: options.token } : {},
2085
+ ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
2086
+ ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {},
2087
+ ...options.yes !== void 0 ? { nonInteractive: options.yes } : {}
2088
+ });
2089
+ } catch (err) {
2090
+ log.error(err.message);
2091
+ process.exit(1);
2092
+ }
2093
+ }
2094
+
2095
+ // src/commands/update.ts
2096
+ async function runUpdate(options) {
2097
+ const log = logger();
2098
+ if (options.version) {
2099
+ log.info(`Version (informational, no tag created): ${options.version}`);
2100
+ }
2101
+ if (options.paths.length === 0) {
2102
+ log.error("No project paths provided. Usage: devaudit update <version> <path> [path...]");
2103
+ process.exit(2);
2104
+ }
2105
+ const plugins = options.plugins ?? (await discoverPlugins()).loaded;
2106
+ for (const projectPath of options.paths) {
2107
+ if (plugins.length > 0) {
2108
+ const ctx = await buildPluginContext({ projectPath });
2109
+ await runHook(plugins, "beforeSync", ctx);
2110
+ }
2111
+ }
2112
+ await syncAll(options.paths);
2113
+ for (const projectPath of options.paths) {
2114
+ if (plugins.length > 0) {
2115
+ const ctx = await buildPluginContext({ projectPath });
2116
+ await runHook(plugins, "afterSync", ctx);
2117
+ }
2118
+ }
2119
+ log.success("=== Sync Complete ===");
2120
+ log.log("");
2121
+ log.log("Next steps for each consuming project:");
2122
+ log.log(" 1. cd into the project directory");
2123
+ log.log(" 2. Review the diff: git diff");
2124
+ log.log(" 3. Commit: git add -A && git commit -m 'chore: sync SDLC templates from DevAudit'");
2125
+ log.log(" 4. Push to develop");
2126
+ log.log("");
2127
+ log.warn("Do NOT auto-commit \u2014 review the changes first.");
2128
+ }
2129
+
2130
+ // src/commands/stub.ts
2131
+ function makeStub(info) {
2132
+ return async () => {
2133
+ const log = logger();
2134
+ log.warn(`\`devaudit ${info.command}\` is not implemented yet.`);
2135
+ log.info(info.summary);
2136
+ if (info.trackedIn) {
2137
+ log.info(`Tracked in: ${info.trackedIn}`);
2138
+ }
2139
+ log.info("See ./docs/devaudit-cli/build-plan.md for the full implementation plan.");
2140
+ process.exit(1);
2141
+ };
2142
+ }
2143
+
2144
+ // src/commands/plugin/list.ts
2145
+ async function runPluginList(opts = {}) {
2146
+ const log = logger();
2147
+ const root = opts.root ?? PLUGINS_DIR;
2148
+ const result = await discoverPlugins(root);
2149
+ log.info(`Plugin directory: ${root}`);
2150
+ if (result.loaded.length === 0 && result.failures.length === 0) {
2151
+ log.log(" (no plugins installed)");
2152
+ return;
2153
+ }
2154
+ if (result.loaded.length > 0) {
2155
+ log.log("");
2156
+ log.log("Loaded:");
2157
+ for (const p of result.loaded) {
2158
+ const hooks = Object.keys(p.plugin.hooks ?? {});
2159
+ const commands = Object.keys(p.plugin.commands ?? {});
2160
+ const detail = [
2161
+ `${p.packageName}@${p.packageVersion}`,
2162
+ hooks.length > 0 ? `hooks=[${hooks.join(",")}]` : "hooks=[]",
2163
+ commands.length > 0 ? `commands=[${commands.join(",")}]` : "commands=[]"
2164
+ ].join(" ");
2165
+ log.log(` \u2713 ${detail}`);
2166
+ }
2167
+ }
2168
+ if (result.failures.length > 0) {
2169
+ log.log("");
2170
+ log.warn("Failed to load:");
2171
+ for (const f of result.failures) {
2172
+ log.log(` \u2717 ${f.dir} \u2014 ${f.reason}`);
2173
+ }
2174
+ }
2175
+ }
2176
+ function deriveDirName(source) {
2177
+ const last = source.split("/").pop() ?? source;
2178
+ return last.replace(/\.git$/, "");
2179
+ }
2180
+ async function pathExists(path) {
2181
+ try {
2182
+ await promises.access(path);
2183
+ return true;
2184
+ } catch {
2185
+ return false;
2186
+ }
2187
+ }
2188
+ async function runPluginInstall(opts) {
2189
+ const log = logger();
2190
+ const root = opts.root ?? PLUGINS_DIR;
2191
+ const dirName = deriveDirName(opts.source);
2192
+ if (!dirName) {
2193
+ log.error(`Could not derive a directory name from source: ${opts.source}`);
2194
+ process.exit(2);
2195
+ }
2196
+ await promises.mkdir(root, { recursive: true });
2197
+ const target = join(root, dirName);
2198
+ if (await pathExists(target)) {
2199
+ log.error(`Plugin directory already exists: ${target}. Run \`devaudit plugin remove ${dirName}\` first.`);
2200
+ process.exit(2);
2201
+ }
2202
+ log.info(`Cloning ${opts.source} \u2192 ${target}`);
2203
+ try {
2204
+ await execa("git", ["clone", "--depth", "1", opts.source, target], { stdio: "inherit" });
2205
+ } catch (err) {
2206
+ log.error(`git clone failed: ${err.message}`);
2207
+ process.exit(6);
2208
+ }
2209
+ if (await pathExists(join(target, "package.json"))) {
2210
+ log.info("Installing plugin dependencies...");
2211
+ const install = await execa("npm", ["install", "--legacy-peer-deps"], {
2212
+ cwd: target,
2213
+ stdio: "inherit",
2214
+ reject: false
2215
+ });
2216
+ if (install.exitCode !== 0) {
2217
+ log.error("npm install failed \u2014 leaving the plugin dir in place for inspection.");
2218
+ process.exit(5);
2219
+ }
2220
+ }
2221
+ log.info("Validating plugin manifest...");
2222
+ try {
2223
+ const loaded = await loadPluginFromDir(target);
2224
+ log.success(`Installed: ${loaded.packageName}@${loaded.packageVersion} at ${target}`);
2225
+ } catch (err) {
2226
+ log.error(`Plugin validation failed: ${err.message}`);
2227
+ log.warn(`Removing ${target}`);
2228
+ await promises.rm(target, { recursive: true, force: true });
2229
+ process.exit(9);
2230
+ }
2231
+ }
2232
+ async function runPluginRemove(opts) {
2233
+ const log = logger();
2234
+ const root = opts.root ?? PLUGINS_DIR;
2235
+ const discovery = await discoverPlugins(root);
2236
+ const candidates = [
2237
+ ...discovery.loaded.map((p) => ({ packageName: p.packageName, dir: p.dir })),
2238
+ ...discovery.failures.map((f) => ({ packageName: basename(f.dir), dir: f.dir }))
2239
+ ];
2240
+ const match = candidates.find((c) => c.packageName === opts.name || basename(c.dir) === opts.name);
2241
+ if (!match) {
2242
+ log.error(`No plugin found matching '${opts.name}'.`);
2243
+ log.info("Run `devaudit plugin list` to see installed plugins.");
2244
+ process.exit(2);
2245
+ return;
2246
+ }
2247
+ await promises.rm(match.dir, { recursive: true, force: true });
2248
+ log.success(`Removed plugin at ${match.dir}`);
2249
+ }
2250
+ async function pathExists2(path) {
2251
+ try {
2252
+ await promises.access(path);
2253
+ return true;
2254
+ } catch {
2255
+ return false;
2256
+ }
2257
+ }
2258
+ async function updateOne(dir, packageName) {
2259
+ if (!await pathExists2(join(dir, ".git"))) {
2260
+ return { plugin: packageName, status: "not-git" };
2261
+ }
2262
+ const pull = await execa("git", ["pull", "--ff-only"], { cwd: dir, reject: false });
2263
+ if (pull.exitCode !== 0) {
2264
+ return { plugin: packageName, status: "failed", detail: pull.stderr.split("\n")[0] };
2265
+ }
2266
+ const noChanges = /Already up to date/i.test(pull.stdout);
2267
+ if (await pathExists2(join(dir, "package.json"))) {
2268
+ const install = await execa("npm", ["install", "--legacy-peer-deps"], {
2269
+ cwd: dir,
2270
+ reject: false
2271
+ });
2272
+ if (install.exitCode !== 0) {
2273
+ return { plugin: packageName, status: "failed", detail: "npm install failed after pull" };
2274
+ }
2275
+ }
2276
+ return { plugin: packageName, status: noChanges ? "no-changes" : "updated" };
2277
+ }
2278
+ async function runPluginUpdate(opts = {}) {
2279
+ const log = logger();
2280
+ const root = opts.root ?? PLUGINS_DIR;
2281
+ const discovery = await discoverPlugins(root);
2282
+ if (discovery.loaded.length === 0) {
2283
+ log.info("No plugins installed.");
2284
+ return;
2285
+ }
2286
+ log.info(`Updating ${discovery.loaded.length} plugin(s)...`);
2287
+ const results = [];
2288
+ for (const p of discovery.loaded) {
2289
+ results.push(await updateOne(p.dir, p.packageName));
2290
+ }
2291
+ for (const r of results) {
2292
+ if (r.status === "updated") log.success(` \u2713 ${r.plugin} \u2014 updated`);
2293
+ else if (r.status === "no-changes") log.log(` \xB7 ${r.plugin} \u2014 already up to date`);
2294
+ else if (r.status === "not-git") log.warn(` \u26A0 ${r.plugin} \u2014 not a git checkout, skipped`);
2295
+ else log.error(` \u2717 ${r.plugin} \u2014 ${r.detail ?? "failed"}`);
2296
+ }
2297
+ }
2298
+
2299
+ // src/index.ts
2300
+ var TRACKING_ISSUE = "https://github.com/metasession-dev/DevAudit-Installer/issues/1";
2301
+ function applyCommonFlags(program) {
2302
+ program.addOption(new Option("--json", "machine-readable output"));
2303
+ program.addOption(new Option("-y, --yes", "accept all interactive defaults (CI-friendly)"));
2304
+ program.addOption(new Option("--dry-run", "preview, don't mutate"));
2305
+ program.addOption(new Option("-v, --verbose", "extra detail"));
2306
+ program.addOption(new Option("--no-color", "strip ANSI"));
2307
+ program.addOption(new Option("--org <slug>", "override active org context for this invocation"));
2308
+ program.hook("preAction", (cmd) => {
2309
+ const opts = cmd.optsWithGlobals();
2310
+ configureLogger({
2311
+ json: Boolean(opts.json),
2312
+ verbose: Boolean(opts.verbose),
2313
+ noColor: opts.color === false
2314
+ });
2315
+ });
2316
+ }
2317
+ async function main(argv) {
2318
+ const program = new Command();
2319
+ program.name("devaudit").description(
2320
+ "DevAudit CLI \u2014 installs, syncs, and operates the Metasession SDLC across consumer projects."
2321
+ ).version(CLI_VERSION, "-V, --version");
2322
+ applyCommonFlags(program);
2323
+ program.command("install [path]").description("Interactive onboarding for a consumer project (native TS implementation)").option("--token <token>", "PAT to use (otherwise reads DEVAUDIT_USER_TOKEN env or ~/.config/devaudit/auth.json)").option("--base-url <url>", "override portal URL (defaults to DEVAUDIT_BASE_URL env or production)").action(async (path, cmdOpts, cmd) => {
2324
+ const globals = cmd.optsWithGlobals();
2325
+ await runInstallCommand({
2326
+ ...path !== void 0 ? { path } : {},
2327
+ ...cmdOpts.token !== void 0 ? { token: cmdOpts.token } : {},
2328
+ ...cmdOpts.baseUrl !== void 0 ? { baseUrl: cmdOpts.baseUrl } : {},
2329
+ ...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {},
2330
+ ...globals.yes !== void 0 ? { yes: Boolean(globals.yes) } : {}
2331
+ });
2332
+ });
2333
+ program.command("update <version> <paths...>").description("Sync framework templates into existing consumer(s) (native TS implementation)").action(async (version, paths2) => {
2334
+ await runUpdate({ version, paths: paths2 });
2335
+ });
2336
+ program.command("push <project-slug> <requirement-id> <evidence-type> <file>").description("Upload evidence file(s) to DevAudit (port of scripts/upload-evidence.sh)").option("--release <version>", "release version (e.g. v1.0.0)").option("--create-release-if-missing", "auto-create the release as 'draft' if absent").option("--environment <env>", "uat | production").option("--category <cat>", "ci_pipeline | local_dev | planning | test_report | security_scan | release_artifact").option("--git-sha <sha>", "attached to metadata.gitSha").option("--ci-run-id <id>", "attached to metadata.ciRunId").option("--branch <name>", "attached to metadata.branch").option("--base-url <url>", "override portal URL (defaults to DEVAUDIT_BASE_URL env or production)").option("--api-key <key>", "override DEVAUDIT_API_KEY env var").action(
2337
+ async (projectSlug, requirementId, evidenceType, file, opts, cmd) => {
2338
+ const globals = cmd.optsWithGlobals();
2339
+ await runPush({
2340
+ projectSlug,
2341
+ requirementId,
2342
+ evidenceType,
2343
+ filePath: file,
2344
+ ...opts.release !== void 0 ? { release: opts.release } : {},
2345
+ ...opts.createReleaseIfMissing !== void 0 ? { createReleaseIfMissing: opts.createReleaseIfMissing } : {},
2346
+ ...opts.environment !== void 0 ? { environment: opts.environment } : {},
2347
+ ...opts.category !== void 0 ? { category: opts.category } : {},
2348
+ ...opts.gitSha !== void 0 ? { gitSha: opts.gitSha } : {},
2349
+ ...opts.ciRunId !== void 0 ? { ciRunId: opts.ciRunId } : {},
2350
+ ...opts.branch !== void 0 ? { branch: opts.branch } : {},
2351
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {},
2352
+ ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
2353
+ ...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {}
2354
+ });
2355
+ }
2356
+ );
2357
+ program.command("doctor").description("Verify the local install: required tools on PATH, auth state, config validity").action(runDoctor);
2358
+ program.command("status [path]").description("Show the consumer project's framework state").action(async (path) => {
2359
+ await runStatus({ path });
2360
+ });
2361
+ program.command("upgrade").description("Update the devaudit CLI itself to the latest release").action(
2362
+ makeStub({
2363
+ command: "upgrade",
2364
+ summary: "Self-update via npm or the platform package manager. Workstream A milestone 8.",
2365
+ trackedIn: TRACKING_ISSUE
2366
+ })
2367
+ );
2368
+ const authCmd = program.command("auth").description("Authentication management");
2369
+ authCmd.command("login").option("--token <token>", "PAT to use (skips the interactive prompt)").option("--base-url <url>", "override portal base URL", "https://devaudit.metasession.co").description("Sign in via PAT paste; stores token in ~/.config/devaudit/auth.json").action(async (opts) => {
2370
+ await runAuthLogin({ token: opts.token, baseUrl: opts.baseUrl });
2371
+ });
2372
+ authCmd.command("logout").description("Delete the cached token at ~/.config/devaudit/auth.json").action(runAuthLogout);
2373
+ authCmd.command("status").description("Show current auth state and verify the cached token").action(runAuthStatus);
2374
+ const orgCmd = program.command("org").description("Organisation management (workstream B prereq)");
2375
+ orgCmd.command("list").description("List orgs the user belongs to").action(makeStub({ command: "org list", summary: "Needs portal RBAC + org endpoints.", trackedIn: TRACKING_ISSUE }));
2376
+ orgCmd.command("switch <slug>").description("Switch active org context").action(
2377
+ makeStub({ command: "org switch", summary: "Updates ~/.config/devaudit/config.json.", trackedIn: TRACKING_ISSUE })
2378
+ );
2379
+ const orgPolicyCmd = orgCmd.command("policy").description("Org policy management");
2380
+ orgPolicyCmd.command("list").description("Show the active org's policy baselines").action(
2381
+ makeStub({
2382
+ command: "org policy list",
2383
+ summary: "Reads from the portal policy engine (workstream B).",
2384
+ trackedIn: TRACKING_ISSUE
2385
+ })
2386
+ );
2387
+ orgPolicyCmd.command("apply [path]").description("Apply org policy to a project (or all org projects)").action(
2388
+ makeStub({
2389
+ command: "org policy apply",
2390
+ summary: "Evaluates sdlc-config.json + CI evidence against org policy bundle.",
2391
+ trackedIn: TRACKING_ISSUE
2392
+ })
2393
+ );
2394
+ orgCmd.command("report").option("--format <fmt>", "html | json | csv", "html").description("Generate an org-wide compliance report").action(
2395
+ makeStub({
2396
+ command: "org report",
2397
+ summary: "Aggregates per-project compliance state across the org.",
2398
+ trackedIn: TRACKING_ISSUE
2399
+ })
2400
+ );
2401
+ const pluginCmd = program.command("plugin").description("Plugin management");
2402
+ pluginCmd.command("list").description("List installed plugins in ~/.config/devaudit/plugins/").action(runPluginList);
2403
+ pluginCmd.command("install <source>").description("Install a plugin from a Git URL (portal registry resolution is pending)").action(async (source) => {
2404
+ await runPluginInstall({ source });
2405
+ });
2406
+ pluginCmd.command("remove <name>").description("Remove a locally installed plugin by package or directory name").action(async (name) => {
2407
+ await runPluginRemove({ name });
2408
+ });
2409
+ pluginCmd.command("update").description("Update all installed plugins (git pull + npm install)").action(runPluginUpdate);
2410
+ const configCmd = program.command("config").description("CLI configuration (telemetry, default org, etc.)");
2411
+ configCmd.command("get <key>").description("Read a CLI config value").action(
2412
+ makeStub({ command: "config get", summary: "Reads ~/.config/devaudit/config.json.", trackedIn: TRACKING_ISSUE })
2413
+ );
2414
+ configCmd.command("set <key> <value>").description("Write a CLI config value").action(
2415
+ makeStub({
2416
+ command: "config set",
2417
+ summary: "Writes to ~/.config/devaudit/config.json (mode 0600).",
2418
+ trackedIn: TRACKING_ISSUE
2419
+ })
2420
+ );
2421
+ configCmd.command("list").description("Print all CLI config values").action(
2422
+ makeStub({
2423
+ command: "config list",
2424
+ summary: "Lists all CLI config keys with their current values.",
2425
+ trackedIn: TRACKING_ISSUE
2426
+ })
2427
+ );
2428
+ const discovery = await discoverPlugins();
2429
+ if (discovery.failures.length > 0) {
2430
+ const log = logger();
2431
+ for (const f of discovery.failures) {
2432
+ log.warn(`Plugin at ${f.dir} failed to load: ${f.reason}`);
2433
+ }
2434
+ }
2435
+ registerPluginCommands(program, discovery.loaded);
2436
+ program.parse(argv);
2437
+ }
2438
+ main(process.argv).catch((err) => {
2439
+ const log = logger();
2440
+ log.error(err.message);
2441
+ process.exit(1);
2442
+ });
2443
+
2444
+ export { main };
2445
+ //# sourceMappingURL=index.js.map
2446
+ //# sourceMappingURL=index.js.map