@metasession.co/devaudit-cli 0.1.54 → 0.1.56
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/README.md +14 -6
- package/dist/index.js +162 -22
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/sdlc/CLAUDE.md +1 -3
- package/sdlc/files/_common/skills/adr-author/SKILL.md +1 -1
- package/sdlc/files/ci/ci.yml.template +49 -8
- package/sdlc/files/ci/python/ci.yml.template +6 -2
- package/sdlc/article.md +0 -219
package/README.md
CHANGED
|
@@ -6,9 +6,21 @@ This is the source of `@metasession.co/devaudit-cli` (binary name: `devaudit`).
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
`npx` is the canonical zero-install invocation — pulls the latest version on first run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @metasession.co/devaudit-cli@latest --help
|
|
13
|
+
npx @metasession.co/devaudit-cli@latest install ../path/to/your-project
|
|
14
|
+
npx @metasession.co/devaudit-cli@latest update
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Prefer a permanent install? Run once, then the short forms work everywhere:
|
|
18
|
+
|
|
9
19
|
```bash
|
|
10
20
|
npm install -g @metasession.co/devaudit-cli
|
|
11
21
|
devaudit --help
|
|
22
|
+
devaudit install ../path/to/your-project
|
|
23
|
+
devaudit update
|
|
12
24
|
```
|
|
13
25
|
|
|
14
26
|
Requires Node ≥ 22. Native binaries (no Node runtime needed) are on the roadmap.
|
|
@@ -71,15 +83,11 @@ cli/
|
|
|
71
83
|
└── version.ts # CLI version constant
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
Future structure (per [build-plan.md](../docs/devaudit-cli/build-plan.md)): `src/commands/{install,update,push,auth/*,org/*,plugin/*,config/*,status,upgrade}.ts` and `src/lib/{adapter,devaudit-api,sdlc-config,auth,git-provider,policy,plugin,report,prompts,paths,stack-detect}.ts`.
|
|
75
|
-
|
|
76
86
|
## Why a CLI (it replaced the original bash scripts)
|
|
77
87
|
|
|
78
88
|
- Cross-platform native (Linux/macOS/Windows; no WSL requirement)
|
|
79
89
|
- JSON output mode on every command for CI
|
|
80
90
|
- Interactive UX comparable to Vercel/Supabase/Firebase/GH/Railway CLIs
|
|
81
|
-
-
|
|
82
|
-
- Plugin extensibility
|
|
91
|
+
- Plugin extensibility (`@metasession.co/devaudit-plugin-sdk` defines the contract; `@metasession.co/devaudit-plugin-prisma` + `@metasession.co/devaudit-plugin-evidence-export` are first-party reference implementations)
|
|
83
92
|
- Organisation-level features: policy-as-code, RBAC, centralised reporting
|
|
84
|
-
|
|
85
|
-
The full motivation lives in [`../docs/devaudit-cli/README.md`](../docs/devaudit-cli/README.md).
|
|
93
|
+
- Single-binary distribution via Node SEA (no Node runtime required on the user's machine) — on the roadmap
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import envPaths from 'env-paths';
|
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { validateManifest } from '@metasession.co/devaudit-plugin-sdk';
|
|
10
10
|
import * as clack2 from '@clack/prompts';
|
|
11
|
+
import Ajv from 'ajv';
|
|
11
12
|
import { readdir, readFile, writeFile } from 'fs/promises';
|
|
12
13
|
|
|
13
14
|
var DEFAULT_OPTIONS = { json: false, verbose: false, noColor: false };
|
|
@@ -55,7 +56,7 @@ function emitJsonResult(payload) {
|
|
|
55
56
|
|
|
56
57
|
// package.json
|
|
57
58
|
var package_default = {
|
|
58
|
-
version: "0.1.
|
|
59
|
+
version: "0.1.56"};
|
|
59
60
|
|
|
60
61
|
// src/lib/version.ts
|
|
61
62
|
var CLI_VERSION = package_default.version;
|
|
@@ -594,27 +595,62 @@ async function runStatus(options) {
|
|
|
594
595
|
}
|
|
595
596
|
}
|
|
596
597
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
597
|
-
var
|
|
598
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
598
599
|
var INITIAL_BACKOFF_MS = 1e3;
|
|
600
|
+
function maxAttempts() {
|
|
601
|
+
const raw = Number.parseInt(process.env["UPLOAD_MAX_ATTEMPTS"] ?? "", 10);
|
|
602
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_ATTEMPTS;
|
|
603
|
+
}
|
|
599
604
|
async function collectFiles(filePath) {
|
|
600
605
|
const stat = await promises.stat(filePath);
|
|
601
606
|
if (stat.isFile()) return [filePath];
|
|
602
607
|
if (stat.isDirectory()) {
|
|
603
|
-
const entries = await promises.readdir(filePath, { withFileTypes: true });
|
|
604
608
|
const files = [];
|
|
609
|
+
const entries = await promises.readdir(filePath, { withFileTypes: true });
|
|
605
610
|
for (const entry of entries) {
|
|
606
|
-
|
|
611
|
+
const child = join(filePath, entry.name);
|
|
612
|
+
if (entry.isDirectory()) {
|
|
613
|
+
files.push(...await collectFiles(child));
|
|
614
|
+
} else if (entry.isFile()) {
|
|
615
|
+
files.push(child);
|
|
616
|
+
}
|
|
607
617
|
}
|
|
608
618
|
return files;
|
|
609
619
|
}
|
|
610
620
|
throw new Error(`${filePath} is neither a file nor a directory.`);
|
|
611
621
|
}
|
|
622
|
+
var STUB_BANNER = /STARTER TEMPLATE.+REPLACE BEFORE/;
|
|
623
|
+
function isUneditedStub(buf) {
|
|
624
|
+
return STUB_BANNER.test(buf.toString("utf-8"));
|
|
625
|
+
}
|
|
612
626
|
function delay(ms) {
|
|
613
627
|
return new Promise((res) => setTimeout(res, ms));
|
|
614
628
|
}
|
|
615
|
-
async function
|
|
629
|
+
async function probeBaseUrlDrift(baseUrl) {
|
|
630
|
+
try {
|
|
631
|
+
const probeUrl = `${baseUrl.replace(/\/$/, "")}/api/health`;
|
|
632
|
+
const controller = new AbortController();
|
|
633
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
634
|
+
let res;
|
|
635
|
+
try {
|
|
636
|
+
res = await fetch(probeUrl, { method: "HEAD", redirect: "manual", signal: controller.signal });
|
|
637
|
+
} finally {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
}
|
|
640
|
+
if (res.status < 300 || res.status >= 400) return null;
|
|
641
|
+
const location = res.headers.get("location");
|
|
642
|
+
if (!location) return null;
|
|
643
|
+
const oldHost = new URL(baseUrl).host;
|
|
644
|
+
const newHost = new URL(location, baseUrl).host;
|
|
645
|
+
if (!newHost || oldHost === newHost) return null;
|
|
646
|
+
return `DEVAUDIT_BASE_URL host '${oldHost}' redirects to '${newHost}'. Rotate the DEVAUDIT_BASE_URL secret to the new host to avoid silent breakage (uploads still succeed this run). Ref: https://github.com/metasession-dev/DevAudit-Installer/issues/143`;
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function uploadOne(file, buf, opts) {
|
|
652
|
+
const attempts = maxAttempts();
|
|
616
653
|
const form = new FormData();
|
|
617
|
-
const buf = await promises.readFile(file);
|
|
618
654
|
const blob = new Blob([new Uint8Array(buf)]);
|
|
619
655
|
form.set("file", blob, basename(file));
|
|
620
656
|
form.set("projectSlug", opts.projectSlug);
|
|
@@ -625,10 +661,14 @@ async function uploadOne(file, opts) {
|
|
|
625
661
|
if (opts.createReleaseIfMissing) form.set("createReleaseIfMissing", "true");
|
|
626
662
|
if (opts.environment) form.set("environment", opts.environment);
|
|
627
663
|
if (opts.evidenceCategory) form.set("evidenceCategory", opts.evidenceCategory);
|
|
664
|
+
if (opts.releaseBranch) form.set("releaseBranch", opts.releaseBranch);
|
|
665
|
+
if (opts.releaseTitle) form.set("releaseTitle", opts.releaseTitle);
|
|
666
|
+
if (opts.changeType) form.set("changeType", opts.changeType);
|
|
667
|
+
if (opts.gateStatus) form.set("gateStatus", opts.gateStatus);
|
|
628
668
|
const url = `${opts.baseUrl.replace(/\/$/, "")}/api/evidence/upload`;
|
|
629
669
|
let attempt = 1;
|
|
630
670
|
let backoff = INITIAL_BACKOFF_MS;
|
|
631
|
-
while (attempt <=
|
|
671
|
+
while (attempt <= attempts) {
|
|
632
672
|
const res = await fetch(url, {
|
|
633
673
|
method: "POST",
|
|
634
674
|
headers: { authorization: `Bearer ${opts.apiKey}` },
|
|
@@ -638,7 +678,7 @@ async function uploadOne(file, opts) {
|
|
|
638
678
|
const body = await res.json().catch(() => null);
|
|
639
679
|
return { file, ok: true, status: res.status, body };
|
|
640
680
|
}
|
|
641
|
-
if (RETRYABLE_STATUSES.has(res.status) && attempt <
|
|
681
|
+
if (RETRYABLE_STATUSES.has(res.status) && attempt < attempts) {
|
|
642
682
|
const retryAfter = Number.parseInt(res.headers.get("retry-after") ?? "", 10);
|
|
643
683
|
const wait = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : backoff;
|
|
644
684
|
await delay(wait);
|
|
@@ -658,7 +698,12 @@ async function uploadEvidence(opts) {
|
|
|
658
698
|
}
|
|
659
699
|
const results = [];
|
|
660
700
|
for (const file of files) {
|
|
661
|
-
|
|
701
|
+
const buf = await promises.readFile(file);
|
|
702
|
+
if (isUneditedStub(buf)) {
|
|
703
|
+
results.push({ file, ok: true, status: 0, skipped: true });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
results.push(await uploadOne(file, buf, opts));
|
|
662
707
|
}
|
|
663
708
|
return results;
|
|
664
709
|
}
|
|
@@ -670,8 +715,24 @@ function buildMetadata(options) {
|
|
|
670
715
|
if (options.gitSha) metadata["gitSha"] = options.gitSha;
|
|
671
716
|
if (options.ciRunId) metadata["ciRunId"] = options.ciRunId;
|
|
672
717
|
if (options.branch) metadata["branch"] = options.branch;
|
|
718
|
+
for (const kv of options.metaKeys ?? []) {
|
|
719
|
+
const eq = kv.indexOf("=");
|
|
720
|
+
metadata[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
721
|
+
}
|
|
673
722
|
return metadata;
|
|
674
723
|
}
|
|
724
|
+
function validateOptions(options) {
|
|
725
|
+
if (options.environment && !options.release) {
|
|
726
|
+
return "--environment requires --release (evidence without a release is orphaned)";
|
|
727
|
+
}
|
|
728
|
+
if (options.release && !options.category) {
|
|
729
|
+
return "--category is required when --release is specified (gate validation)";
|
|
730
|
+
}
|
|
731
|
+
for (const kv of options.metaKeys ?? []) {
|
|
732
|
+
if (!kv.includes("=")) return `--meta-key requires key=value (got: ${kv})`;
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
675
736
|
async function runDryRun(options, baseUrl) {
|
|
676
737
|
const log = logger();
|
|
677
738
|
const files = await collectFiles(options.filePath);
|
|
@@ -700,6 +761,12 @@ async function runPush(options) {
|
|
|
700
761
|
const log = logger();
|
|
701
762
|
const projectPath = resolve(process.cwd());
|
|
702
763
|
const baseUrl = options.baseUrl ?? process.env["DEVAUDIT_BASE_URL"] ?? DEFAULT_BASE_URL3;
|
|
764
|
+
const validationError = validateOptions(options);
|
|
765
|
+
if (validationError) {
|
|
766
|
+
if (isJsonMode()) emitJsonResult({ ok: false, reason: "invalid_arguments", message: validationError });
|
|
767
|
+
else log.error(validationError);
|
|
768
|
+
process.exit(2);
|
|
769
|
+
}
|
|
703
770
|
if (options.dryRun) {
|
|
704
771
|
await runDryRun(options, baseUrl);
|
|
705
772
|
return;
|
|
@@ -716,6 +783,8 @@ async function runPush(options) {
|
|
|
716
783
|
log.info(
|
|
717
784
|
`Uploading ${options.filePath} (project=${options.projectSlug} req=${options.requirementId} type=${options.evidenceType}) \u2192 ${baseUrl}`
|
|
718
785
|
);
|
|
786
|
+
const driftWarning = await probeBaseUrlDrift(baseUrl);
|
|
787
|
+
if (driftWarning) log.warn(driftWarning);
|
|
719
788
|
const plugins = options.plugins ?? (await discoverPlugins()).loaded;
|
|
720
789
|
if (plugins.length > 0) {
|
|
721
790
|
const ctx = await buildPluginContext({ projectPath });
|
|
@@ -733,12 +802,20 @@ async function runPush(options) {
|
|
|
733
802
|
...options.createReleaseIfMissing !== void 0 ? { createReleaseIfMissing: options.createReleaseIfMissing } : {},
|
|
734
803
|
...options.environment !== void 0 ? { environment: options.environment } : {},
|
|
735
804
|
...options.category !== void 0 ? { evidenceCategory: options.category } : {},
|
|
805
|
+
...options.branch !== void 0 ? { releaseBranch: options.branch } : {},
|
|
806
|
+
...options.releaseTitle !== void 0 ? { releaseTitle: options.releaseTitle } : {},
|
|
807
|
+
...options.changeType !== void 0 ? { changeType: options.changeType } : {},
|
|
808
|
+
...options.gateStatus !== void 0 ? { gateStatus: options.gateStatus } : {},
|
|
736
809
|
metadata
|
|
737
810
|
});
|
|
738
811
|
let okCount = 0;
|
|
739
812
|
let failCount = 0;
|
|
813
|
+
let skippedCount = 0;
|
|
740
814
|
for (const result of results) {
|
|
741
|
-
if (result.
|
|
815
|
+
if (result.skipped) {
|
|
816
|
+
skippedCount++;
|
|
817
|
+
log.warn(` \u2298 ${result.file} SKIPPED \u2014 unedited starter stub (replace the STARTER TEMPLATE banner to upload)`);
|
|
818
|
+
} else if (result.ok) {
|
|
742
819
|
okCount++;
|
|
743
820
|
log.success(` \u2713 ${result.file} (HTTP ${result.status})`);
|
|
744
821
|
} else {
|
|
@@ -747,7 +824,7 @@ async function runPush(options) {
|
|
|
747
824
|
}
|
|
748
825
|
}
|
|
749
826
|
log.log("");
|
|
750
|
-
log.info(`Uploaded: ${okCount} succeeded, ${failCount} failed.`);
|
|
827
|
+
log.info(`Uploaded: ${okCount} succeeded, ${failCount} failed, ${skippedCount} skipped.`);
|
|
751
828
|
if (plugins.length > 0) {
|
|
752
829
|
const ctx = await buildPluginContext({ projectPath });
|
|
753
830
|
await runHook(plugins, "afterPush", ctx);
|
|
@@ -757,7 +834,14 @@ async function runPush(options) {
|
|
|
757
834
|
ok: failCount === 0,
|
|
758
835
|
uploaded: okCount,
|
|
759
836
|
failed: failCount,
|
|
760
|
-
|
|
837
|
+
skipped: skippedCount,
|
|
838
|
+
results: results.map((r) => ({
|
|
839
|
+
file: r.file,
|
|
840
|
+
ok: r.ok,
|
|
841
|
+
status: r.status,
|
|
842
|
+
skipped: r.skipped ?? false,
|
|
843
|
+
error: r.error ?? null
|
|
844
|
+
}))
|
|
761
845
|
});
|
|
762
846
|
}
|
|
763
847
|
if (failCount > 0) process.exit(4);
|
|
@@ -1488,10 +1572,44 @@ async function configureBranchProtection(ctx, provider) {
|
|
|
1488
1572
|
message: `${result.message ?? "branch-protection apply failed"} \u2014 configure manually`
|
|
1489
1573
|
};
|
|
1490
1574
|
}
|
|
1575
|
+
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
1576
|
+
var validatorCache = /* @__PURE__ */ new Map();
|
|
1577
|
+
async function getValidator(schemaPath) {
|
|
1578
|
+
const cached = validatorCache.get(schemaPath);
|
|
1579
|
+
if (cached) return cached;
|
|
1580
|
+
let schema;
|
|
1581
|
+
try {
|
|
1582
|
+
schema = JSON.parse(await promises.readFile(schemaPath, "utf-8"));
|
|
1583
|
+
} catch {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
delete schema["$id"];
|
|
1587
|
+
const validate = ajv.compile(schema);
|
|
1588
|
+
validatorCache.set(schemaPath, validate);
|
|
1589
|
+
return validate;
|
|
1590
|
+
}
|
|
1591
|
+
function formatErrors(validate) {
|
|
1592
|
+
return (validate.errors ?? []).map((e) => ` - ${e.instancePath === "" ? "(root)" : e.instancePath} ${e.message ?? "is invalid"}`).join("\n");
|
|
1593
|
+
}
|
|
1594
|
+
async function loadAdapter(adapterPath, schemaPath, kind) {
|
|
1595
|
+
const raw = await promises.readFile(adapterPath, "utf-8");
|
|
1596
|
+
let parsed;
|
|
1597
|
+
try {
|
|
1598
|
+
parsed = JSON.parse(raw);
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
throw new Error(`Invalid JSON in ${kind} adapter ${adapterPath}: ${err.message}`);
|
|
1601
|
+
}
|
|
1602
|
+
const validate = await getValidator(schemaPath);
|
|
1603
|
+
if (validate && !validate(parsed)) {
|
|
1604
|
+
throw new Error(`${kind} adapter ${adapterPath} failed schema validation:
|
|
1605
|
+
${formatErrors(validate)}`);
|
|
1606
|
+
}
|
|
1607
|
+
return parsed;
|
|
1608
|
+
}
|
|
1491
1609
|
async function loadStackAdapter(installerRoot, stack) {
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
return
|
|
1610
|
+
const adapterPath = join(installerRoot, "sdlc", "files", "stacks", stack, "adapter.json");
|
|
1611
|
+
const schemaPath = join(installerRoot, "sdlc", "files", "stacks", "_schema", "adapter.schema.json");
|
|
1612
|
+
return loadAdapter(adapterPath, schemaPath, "Stack");
|
|
1495
1613
|
}
|
|
1496
1614
|
async function listStacks(installerRoot) {
|
|
1497
1615
|
const dir = join(installerRoot, "sdlc", "files", "stacks");
|
|
@@ -2438,8 +2556,6 @@ async function runJoinCommand(options) {
|
|
|
2438
2556
|
process.exit(1);
|
|
2439
2557
|
}
|
|
2440
2558
|
}
|
|
2441
|
-
|
|
2442
|
-
// src/commands/update.ts
|
|
2443
2559
|
async function runUpdate(options) {
|
|
2444
2560
|
const log = logger();
|
|
2445
2561
|
if (options.version) {
|
|
@@ -2449,6 +2565,14 @@ async function runUpdate(options) {
|
|
|
2449
2565
|
log.error("No project paths provided. Usage: devaudit update <version> <path> [path...]");
|
|
2450
2566
|
process.exit(2);
|
|
2451
2567
|
}
|
|
2568
|
+
if (options.dryRun) {
|
|
2569
|
+
log.warn("DRY RUN \u2014 no files will be written and no plugin hooks will fire");
|
|
2570
|
+
for (const projectPath of options.paths) {
|
|
2571
|
+
log.info(` [dry-run] would sync SDLC templates via syncProject() against ${resolve(projectPath)}`);
|
|
2572
|
+
}
|
|
2573
|
+
log.success("=== Dry run complete (no mutations performed) ===");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2452
2576
|
const plugins = options.plugins ?? (await discoverPlugins()).loaded;
|
|
2453
2577
|
for (const projectPath of options.paths) {
|
|
2454
2578
|
if (plugins.length > 0) {
|
|
@@ -2556,7 +2680,9 @@ function makeStub(info) {
|
|
|
2556
2680
|
if (info.trackedIn) {
|
|
2557
2681
|
log.info(`Tracked in: ${info.trackedIn}`);
|
|
2558
2682
|
}
|
|
2559
|
-
log.info(
|
|
2683
|
+
log.info(
|
|
2684
|
+
"File an issue at https://github.com/metasession-dev/DevAudit-Installer/issues if you need this command."
|
|
2685
|
+
);
|
|
2560
2686
|
process.exit(1);
|
|
2561
2687
|
};
|
|
2562
2688
|
}
|
|
@@ -2773,7 +2899,7 @@ async function main(argv) {
|
|
|
2773
2899
|
});
|
|
2774
2900
|
program.command("update [version] [paths...]").description(
|
|
2775
2901
|
"Sync framework templates into existing consumer(s). Both args are optional: paths default to the current directory, and version is a cosmetic label (defaults to the running CLI version). So a bare `devaudit update` syncs the current project."
|
|
2776
|
-
).action(async (version, paths2) => {
|
|
2902
|
+
).action(async (version, paths2, cmd) => {
|
|
2777
2903
|
let resolvedVersion = version;
|
|
2778
2904
|
let resolvedPaths = paths2 ?? [];
|
|
2779
2905
|
const looksLikeVersion = (s) => /^v?\d+(\.\d+)/.test(s);
|
|
@@ -2782,9 +2908,19 @@ async function main(argv) {
|
|
|
2782
2908
|
resolvedVersion = void 0;
|
|
2783
2909
|
}
|
|
2784
2910
|
if (resolvedPaths.length === 0) resolvedPaths = ["."];
|
|
2785
|
-
|
|
2911
|
+
const globals = cmd.optsWithGlobals();
|
|
2912
|
+
await runUpdate({
|
|
2913
|
+
version: resolvedVersion ?? CLI_VERSION,
|
|
2914
|
+
paths: resolvedPaths,
|
|
2915
|
+
...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {}
|
|
2916
|
+
});
|
|
2786
2917
|
});
|
|
2787
|
-
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>", "
|
|
2918
|
+
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>", "git branch \u2014 sent as releaseBranch + metadata.branch").option("--release-title <text>", "human title for the release row (releaseTitle; portal no-clobbers)").option("--change-type <type>", "conventional-commit prefix for the release row (changeType)").option("--gate-status <status>", "passed | failed | skipped (gateStatus)").option(
|
|
2919
|
+
"--meta-key <pair>",
|
|
2920
|
+
"repeatable key=value merged into the metadata JSON",
|
|
2921
|
+
(val, acc) => [...acc, val],
|
|
2922
|
+
[]
|
|
2923
|
+
).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(
|
|
2788
2924
|
async (projectSlug, requirementId, evidenceType, file, opts, cmd) => {
|
|
2789
2925
|
const globals = cmd.optsWithGlobals();
|
|
2790
2926
|
await runPush({
|
|
@@ -2799,6 +2935,10 @@ async function main(argv) {
|
|
|
2799
2935
|
...opts.gitSha !== void 0 ? { gitSha: opts.gitSha } : {},
|
|
2800
2936
|
...opts.ciRunId !== void 0 ? { ciRunId: opts.ciRunId } : {},
|
|
2801
2937
|
...opts.branch !== void 0 ? { branch: opts.branch } : {},
|
|
2938
|
+
...opts.releaseTitle !== void 0 ? { releaseTitle: opts.releaseTitle } : {},
|
|
2939
|
+
...opts.changeType !== void 0 ? { changeType: opts.changeType } : {},
|
|
2940
|
+
...opts.gateStatus !== void 0 ? { gateStatus: opts.gateStatus } : {},
|
|
2941
|
+
...opts.metaKey !== void 0 && opts.metaKey.length > 0 ? { metaKeys: opts.metaKey } : {},
|
|
2802
2942
|
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {},
|
|
2803
2943
|
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
2804
2944
|
...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {}
|
|
@@ -2811,7 +2951,7 @@ async function main(argv) {
|
|
|
2811
2951
|
const opts = cmd?.optsWithGlobals() ?? {};
|
|
2812
2952
|
await runBootstrapGovernance({ path, dryRun: opts.dryRun });
|
|
2813
2953
|
});
|
|
2814
|
-
program.command("doctor").description("Verify the local install: required tools on PATH,
|
|
2954
|
+
program.command("doctor").description("Verify the local install: required tools on PATH (node>=22, git, gh, jq, curl) + a release close-out drift check").action(runDoctor);
|
|
2815
2955
|
program.command("status [path]").description("Show the consumer project's framework state").action(async (path) => {
|
|
2816
2956
|
await runStatus({ path });
|
|
2817
2957
|
});
|