swarmdo 1.13.0 → 1.14.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/.claude/helpers/statusline.cjs +1 -1
- package/README.md +2 -1
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/commands/compact.js +3 -2
- package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -3
- package/v3/@swarmdo/cli/dist/src/commands/pack.js +2 -2
- package/v3/@swarmdo/cli/dist/src/commands/redact.js +2 -1
- package/v3/@swarmdo/cli/dist/src/commands/sbom.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/commands/sbom.js +73 -0
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +64 -0
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +130 -0
- package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +12 -0
- package/v3/@swarmdo/cli/dist/src/util/stdout.js +16 -0
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -515,7 +515,7 @@ function getCostFromStdin() {
|
|
|
515
515
|
// misses, the displayed version is meaningful (matches what the user
|
|
516
516
|
// installed), not a stale hard-coded string.
|
|
517
517
|
function getPkgVersion() {
|
|
518
|
-
let ver = '1.
|
|
518
|
+
let ver = '1.14.0';
|
|
519
519
|
try {
|
|
520
520
|
const home = os.homedir();
|
|
521
521
|
const pkgPaths = [
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://swarmdo.com)
|
|
4
4
|
|
|
5
|
-
[](https://img.shields.io/badge/npx%20swarmdo-v1.14.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
|
|
6
6
|
[](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
|
|
7
7
|
[](https://swarmdo.com)
|
|
8
8
|
[](https://github.com/SwarmDo/swarmdo)
|
|
@@ -282,6 +282,7 @@ The recent release train added a full day-to-day operations layer around the swa
|
|
|
282
282
|
| `swarmdo pack` | **Bundle a repo into one AI-context blob** — walk the tree (respects `.gitignore` + glob `--include`/`--exclude`, skips binaries/node_modules), emit markdown/xml/json/plain with a directory tree and per-file + total token estimates. `swarmdo pack --tokens` for a budget breakdown; `--redact` masks secrets first. Deterministic |
|
|
283
283
|
| `swarmdo env` | **Catch env-var drift before deploy** — statically scan code for `process.env.X` / `import.meta.env.X` / `Deno.env.get` / `os.getenv` references and reconcile against `.env` and `.env.example`: reports **missing** (used but undeclared), **unused**, and **undocumented**. `--ci` exits 1 on missing vars. Also an MCP tool (`env_check`). Deterministic |
|
|
284
284
|
| `swarmdo license` | **Audit dependency licenses** — walk `node_modules`, resolve each package's SPDX license, and gate on an allow/deny policy so a GPL or unknown license can't slip into a permissive tree. `--allow MIT,Apache-2.0 --ci` fails the build; distinct from `security` (CVEs). Also an MCP tool (`license_check`). Deterministic |
|
|
285
|
+
| `swarmdo sbom` | **Software Bill of Materials from the lockfile** — emit a CycloneDX (default) or SPDX JSON manifest of every dependency with version, purl, license, and integrity hash, for compliance/vuln tooling. `--spec spdx`, `-o sbom.json`, `--production`. Completes the env/license/sbom supply-chain trio. Deterministic |
|
|
285
286
|
| `swarmdo integrations` (alias `integrate`) | **Use swarmdo from Codex CLI, GitHub Copilot CLI, and pi** — one command wires AGENTS.md + each CLI's MCP config (idempotent, dry-run first, never touches your Claude Code setup) |
|
|
286
287
|
| OpenRouter model pool | **Let swarms pick from any models you configure** — declare tier-mapped OpenRouter models in `swarmdo.config.json`; the router Thompson-samples among them per task and the execution layer dispatches the winner |
|
|
287
288
|
| `swarmdo changelog` (alias `notes`) | **Release notes from conventional commits** — `--out NOTES.md` feeds `gh release create --notes-file` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmdo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Swarmdo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { output } from '../output.js';
|
|
15
15
|
import { compactOutput, formatSavings } from '../compact/compact.js';
|
|
16
|
+
import { writeStdout } from '../util/stdout.js';
|
|
16
17
|
/**
|
|
17
18
|
* Read all of stdin as UTF-8. A stream read (not readFileSync(0)) — the
|
|
18
19
|
* synchronous form throws EAGAIN on a non-blocking pipe on macOS/Linux.
|
|
@@ -61,7 +62,7 @@ async function run(ctx) {
|
|
|
61
62
|
}
|
|
62
63
|
const combined = (r.stdout || '') + (r.stderr || '');
|
|
63
64
|
const { text, stats } = compactOutput(combined, opts);
|
|
64
|
-
|
|
65
|
+
await writeStdout(text);
|
|
65
66
|
if (!quiet)
|
|
66
67
|
process.stderr.write(formatSavings(stats) + '\n');
|
|
67
68
|
// Propagate the wrapped command's exit code verbatim.
|
|
@@ -81,7 +82,7 @@ async function run(ctx) {
|
|
|
81
82
|
else if (!quiet) {
|
|
82
83
|
process.stderr.write(formatSavings(stats) + '\n');
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
+
await writeStdout(text);
|
|
85
86
|
return { success: true, exitCode: 0 };
|
|
86
87
|
}
|
|
87
88
|
export const compactCommand = {
|
|
@@ -35,6 +35,9 @@ const commandLoaders = {
|
|
|
35
35
|
// GPL/unknown licenses in the tree, fail CI on a forbidden one.
|
|
36
36
|
license: () => import('./license.js'),
|
|
37
37
|
licenses: () => import('./license.js'), // alias via loader key
|
|
38
|
+
// Software Bill of Materials from the lockfile (syft/cyclonedx demand) —
|
|
39
|
+
// CycloneDX/SPDX JSON; completes the env/license/sbom supply-chain trio.
|
|
40
|
+
sbom: () => import('./sbom.js'),
|
|
38
41
|
// Queryable exported-symbol index (codegraph demand) — where things are
|
|
39
42
|
// defined without grep+read round-trips.
|
|
40
43
|
codegraph: () => import('./codegraph.js'),
|
|
@@ -268,7 +271,7 @@ export async function getCommandsByCategory() {
|
|
|
268
271
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
269
272
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
270
273
|
// categorized help. Every load now has a named slot.
|
|
271
|
-
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd,] = await Promise.all([
|
|
274
|
+
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd,] = await Promise.all([
|
|
272
275
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
273
276
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
274
277
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -276,7 +279,7 @@ export async function getCommandsByCategory() {
|
|
|
276
279
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
277
280
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
278
281
|
loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
|
|
279
|
-
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'),
|
|
282
|
+
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'),
|
|
280
283
|
]);
|
|
281
284
|
return {
|
|
282
285
|
primary: [
|
|
@@ -292,7 +295,7 @@ export async function getCommandsByCategory() {
|
|
|
292
295
|
utility: [
|
|
293
296
|
configCmd, doctorCmd, daemonCmd, completionsCmd,
|
|
294
297
|
migrateCmd, workflowCmd, demoCmd,
|
|
295
|
-
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, efficiencyCmd,
|
|
298
|
+
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, efficiencyCmd,
|
|
296
299
|
].filter(Boolean),
|
|
297
300
|
analysis: [
|
|
298
301
|
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
@@ -17,6 +17,7 @@ import * as path from 'node:path';
|
|
|
17
17
|
import { output } from '../output.js';
|
|
18
18
|
import { packFiles, makeIgnoreMatcher } from '../pack/pack.js';
|
|
19
19
|
import { redactText } from '../redact/redact.js';
|
|
20
|
+
import { writeStdout } from '../util/stdout.js';
|
|
20
21
|
const SKIP_DIRS = new Set([
|
|
21
22
|
'node_modules', '.git', '.swarm', 'dist', 'dist-standalone', 'build',
|
|
22
23
|
'coverage', '.next', '.turbo', 'out', 'vendor', '.cache',
|
|
@@ -151,8 +152,7 @@ async function run(ctx) {
|
|
|
151
152
|
output.printSuccess(`packed ${stats.files} files (~${stats.tokens} tokens) → ${outFile}`);
|
|
152
153
|
}
|
|
153
154
|
else {
|
|
154
|
-
|
|
155
|
-
if (!process.stdout.isTTY) { /* piped: keep stdout clean */ }
|
|
155
|
+
await writeStdout(bundle);
|
|
156
156
|
process.stderr.write(output.dim(`packed ${stats.files} files (~${stats.tokens} tokens)\n`));
|
|
157
157
|
}
|
|
158
158
|
return { success: true, exitCode: 0 };
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
import { output } from '../output.js';
|
|
17
17
|
import { redactText, scanText, formatFindingsSummary } from '../redact/redact.js';
|
|
18
|
+
import { writeStdout } from '../util/stdout.js';
|
|
18
19
|
function readStdin() {
|
|
19
20
|
return new Promise((resolve) => {
|
|
20
21
|
const chunks = [];
|
|
@@ -93,7 +94,7 @@ async function run(ctx) {
|
|
|
93
94
|
}
|
|
94
95
|
// Redact mode: rewrite and emit.
|
|
95
96
|
const { output: redacted, findings } = redactText(input, opts);
|
|
96
|
-
|
|
97
|
+
await writeStdout(redacted);
|
|
97
98
|
if (asJson) {
|
|
98
99
|
process.stderr.write(JSON.stringify({ count: findings.length, findings }, null, 2) + '\n');
|
|
99
100
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo sbom` — emit a Software Bill of Materials from the npm lockfile.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo sbom # CycloneDX JSON → stdout
|
|
5
|
+
* swarmdo sbom -f spdx -o sbom.json # SPDX 2.3 JSON → file
|
|
6
|
+
* swarmdo sbom --production # exclude dev-only dependencies
|
|
7
|
+
*
|
|
8
|
+
* Completes the supply-chain trio (env / license / sbom). Engine
|
|
9
|
+
* (../sbom/sbom.ts) is pure + tested; this reads package-lock.json.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from '../types.js';
|
|
12
|
+
export declare const sbomCommand: Command;
|
|
13
|
+
export default sbomCommand;
|
|
14
|
+
//# sourceMappingURL=sbom.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo sbom` — emit a Software Bill of Materials from the npm lockfile.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo sbom # CycloneDX JSON → stdout
|
|
5
|
+
* swarmdo sbom -f spdx -o sbom.json # SPDX 2.3 JSON → file
|
|
6
|
+
* swarmdo sbom --production # exclude dev-only dependencies
|
|
7
|
+
*
|
|
8
|
+
* Completes the supply-chain trio (env / license / sbom). Engine
|
|
9
|
+
* (../sbom/sbom.ts) is pure + tested; this reads package-lock.json.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { output } from '../output.js';
|
|
14
|
+
import { componentsFromNpmLock, buildSbom } from '../sbom/sbom.js';
|
|
15
|
+
import { writeStdout } from '../util/stdout.js';
|
|
16
|
+
async function run(ctx) {
|
|
17
|
+
const repoRoot = ctx.cwd || process.cwd();
|
|
18
|
+
const lockPath = path.resolve(repoRoot, ctx.args[0] ?? (typeof ctx.flags.lockfile === 'string' ? ctx.flags.lockfile : 'package-lock.json'));
|
|
19
|
+
let lock;
|
|
20
|
+
try {
|
|
21
|
+
lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
output.printError(`could not read a JSON lockfile at ${path.relative(repoRoot, lockPath) || lockPath} (npm package-lock.json; pnpm/yaml unsupported)`);
|
|
25
|
+
return { success: false, exitCode: 1 };
|
|
26
|
+
}
|
|
27
|
+
if (!lock.packages) {
|
|
28
|
+
output.printError('lockfile has no `packages` map — needs npm lockfileVersion 2 or 3');
|
|
29
|
+
return { success: false, exitCode: 1 };
|
|
30
|
+
}
|
|
31
|
+
// NB: use --spec, not --format — the global --format flag is choices-validated
|
|
32
|
+
// to text|json|table and rejects 'spdx'/'cyclonedx' before this command runs.
|
|
33
|
+
const raw = typeof ctx.flags.spec === 'string' ? ctx.flags.spec : 'cyclonedx';
|
|
34
|
+
if (raw !== 'cyclonedx' && raw !== 'spdx') {
|
|
35
|
+
output.printError(`unknown --spec '${raw}' (use cyclonedx|spdx)`);
|
|
36
|
+
return { success: false, exitCode: 1 };
|
|
37
|
+
}
|
|
38
|
+
const format = raw;
|
|
39
|
+
const components = componentsFromNpmLock(lock, {
|
|
40
|
+
productionOnly: ctx.flags.production === true,
|
|
41
|
+
});
|
|
42
|
+
// Prefer the lockfile's own project name/version; fall back to the repo dir.
|
|
43
|
+
const meta = { name: lock.name ?? path.basename(repoRoot), version: lock.version ?? '0.0.0' };
|
|
44
|
+
const bom = buildSbom(components, meta, format);
|
|
45
|
+
const serialized = JSON.stringify(bom, null, 2);
|
|
46
|
+
const outFile = (typeof ctx.flags.output === 'string' && ctx.flags.output) || (typeof ctx.flags.o === 'string' && ctx.flags.o);
|
|
47
|
+
if (outFile) {
|
|
48
|
+
fs.writeFileSync(path.resolve(repoRoot, outFile), serialized + '\n');
|
|
49
|
+
output.printSuccess(`${format} SBOM: ${components.length} components → ${outFile}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await writeStdout(serialized + '\n');
|
|
53
|
+
process.stderr.write(output.dim(`${format} SBOM: ${components.length} components\n`));
|
|
54
|
+
}
|
|
55
|
+
return { success: true, exitCode: 0 };
|
|
56
|
+
}
|
|
57
|
+
export const sbomCommand = {
|
|
58
|
+
name: 'sbom',
|
|
59
|
+
description: 'Generate a Software Bill of Materials (CycloneDX or SPDX JSON) from the npm lockfile',
|
|
60
|
+
options: [
|
|
61
|
+
{ name: 'spec', description: 'BOM spec: cyclonedx (default) or spdx', type: 'string' },
|
|
62
|
+
{ name: 'output', short: 'o', description: 'write the SBOM to a file instead of stdout', type: 'string' },
|
|
63
|
+
{ name: 'lockfile', description: 'path to the lockfile (default package-lock.json)', type: 'string' },
|
|
64
|
+
{ name: 'production', description: 'exclude dev-only dependencies', type: 'boolean' },
|
|
65
|
+
],
|
|
66
|
+
examples: [
|
|
67
|
+
{ command: 'swarmdo sbom -o sbom.json', description: 'Write a CycloneDX SBOM' },
|
|
68
|
+
{ command: 'swarmdo sbom --spec spdx --production', description: 'SPDX SBOM, production deps only' },
|
|
69
|
+
],
|
|
70
|
+
action: run,
|
|
71
|
+
};
|
|
72
|
+
export default sbomCommand;
|
|
73
|
+
//# sourceMappingURL=sbom.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sbom.ts — generate a Software Bill of Materials from an npm lockfile.
|
|
3
|
+
*
|
|
4
|
+
* Completes the supply-chain trio with `env` (config drift) and `license`
|
|
5
|
+
* (license policy): `sbom` emits the standardized dependency manifest —
|
|
6
|
+
* CycloneDX or SPDX JSON — that compliance, vuln scanners, and regulators
|
|
7
|
+
* (US EO 14028, EU CRA) consume. Distinct from `pack` (repo→AI-context prose)
|
|
8
|
+
* and `license` (policy gate): this is a machine artifact, not a check.
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic (lockfile object → BOM object, no timestamps/IO), so
|
|
11
|
+
* output is golden-testable. The file read lives in ../commands/sbom.ts.
|
|
12
|
+
*/
|
|
13
|
+
export interface Component {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
purl: string;
|
|
17
|
+
license?: string;
|
|
18
|
+
/** hex-encoded integrity, with its algorithm */
|
|
19
|
+
hash?: {
|
|
20
|
+
alg: string;
|
|
21
|
+
content: string;
|
|
22
|
+
};
|
|
23
|
+
dev?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface NpmLockEntry {
|
|
26
|
+
version?: string;
|
|
27
|
+
resolved?: string;
|
|
28
|
+
integrity?: string;
|
|
29
|
+
license?: unknown;
|
|
30
|
+
dev?: boolean;
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface NpmLock {
|
|
34
|
+
name?: string;
|
|
35
|
+
version?: string;
|
|
36
|
+
lockfileVersion?: number;
|
|
37
|
+
packages?: Record<string, NpmLockEntry>;
|
|
38
|
+
}
|
|
39
|
+
/** Package URL for an npm component. Scope `@` is percent-encoded per the purl spec. */
|
|
40
|
+
export declare function purlFor(name: string, version: string): string;
|
|
41
|
+
/** Convert an SRI integrity string (`sha512-<base64>`) to `{alg, content(hex)}`. Deterministic. */
|
|
42
|
+
export declare function integrityToHash(integrity: string): {
|
|
43
|
+
alg: string;
|
|
44
|
+
content: string;
|
|
45
|
+
} | undefined;
|
|
46
|
+
export interface ComponentOptions {
|
|
47
|
+
/** exclude dev-only dependencies */
|
|
48
|
+
productionOnly?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Extract components from a parsed npm lockfile (v2/v3 `packages` map). Pure. */
|
|
51
|
+
export declare function componentsFromNpmLock(lock: NpmLock, opts?: ComponentOptions): Component[];
|
|
52
|
+
export interface BomMeta {
|
|
53
|
+
name: string;
|
|
54
|
+
version: string;
|
|
55
|
+
}
|
|
56
|
+
/** Build a CycloneDX 1.5 BOM object (no timestamp → deterministic). Pure. */
|
|
57
|
+
export declare function buildCycloneDX(components: Component[], meta: BomMeta): Record<string, unknown>;
|
|
58
|
+
/** Build a minimal SPDX 2.3 JSON document (no created timestamp → deterministic). Pure. */
|
|
59
|
+
export declare function buildSpdx(components: Component[], meta: BomMeta): Record<string, unknown>;
|
|
60
|
+
export type SbomFormat = 'cyclonedx' | 'spdx';
|
|
61
|
+
/** Build a BOM in the requested format. Pure. */
|
|
62
|
+
export declare function buildSbom(components: Component[], meta: BomMeta, format: SbomFormat): Record<string, unknown>;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=sbom.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sbom.ts — generate a Software Bill of Materials from an npm lockfile.
|
|
3
|
+
*
|
|
4
|
+
* Completes the supply-chain trio with `env` (config drift) and `license`
|
|
5
|
+
* (license policy): `sbom` emits the standardized dependency manifest —
|
|
6
|
+
* CycloneDX or SPDX JSON — that compliance, vuln scanners, and regulators
|
|
7
|
+
* (US EO 14028, EU CRA) consume. Distinct from `pack` (repo→AI-context prose)
|
|
8
|
+
* and `license` (policy gate): this is a machine artifact, not a check.
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic (lockfile object → BOM object, no timestamps/IO), so
|
|
11
|
+
* output is golden-testable. The file read lives in ../commands/sbom.ts.
|
|
12
|
+
*/
|
|
13
|
+
/** Derive the package name from a lockfile `packages` key (last node_modules segment, scope-aware). */
|
|
14
|
+
function nameFromKey(key) {
|
|
15
|
+
const idx = key.lastIndexOf('node_modules/');
|
|
16
|
+
const tail = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key;
|
|
17
|
+
return tail; // already includes @scope/name because we split on the LAST node_modules/
|
|
18
|
+
}
|
|
19
|
+
/** Package URL for an npm component. Scope `@` is percent-encoded per the purl spec. */
|
|
20
|
+
export function purlFor(name, version) {
|
|
21
|
+
const enc = name.startsWith('@') ? '%40' + name.slice(1) : name;
|
|
22
|
+
return `pkg:npm/${enc}@${version}`;
|
|
23
|
+
}
|
|
24
|
+
/** Convert an SRI integrity string (`sha512-<base64>`) to `{alg, content(hex)}`. Deterministic. */
|
|
25
|
+
export function integrityToHash(integrity) {
|
|
26
|
+
const m = integrity.match(/^(sha\d+)-(.+)$/);
|
|
27
|
+
if (!m)
|
|
28
|
+
return undefined;
|
|
29
|
+
const algMap = { sha1: 'SHA-1', sha256: 'SHA-256', sha384: 'SHA-384', sha512: 'SHA-512' };
|
|
30
|
+
const alg = algMap[m[1]];
|
|
31
|
+
if (!alg)
|
|
32
|
+
return undefined;
|
|
33
|
+
let hex;
|
|
34
|
+
try {
|
|
35
|
+
// base64 → hex, dependency-free and deterministic
|
|
36
|
+
hex = Buffer.from(m[2], 'base64').toString('hex');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return { alg, content: hex };
|
|
42
|
+
}
|
|
43
|
+
function licenseOf(entry) {
|
|
44
|
+
const l = entry.license;
|
|
45
|
+
if (typeof l === 'string' && l.trim())
|
|
46
|
+
return l.trim();
|
|
47
|
+
if (l && typeof l === 'object' && typeof l.type === 'string')
|
|
48
|
+
return l.type;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Extract components from a parsed npm lockfile (v2/v3 `packages` map). Pure. */
|
|
52
|
+
export function componentsFromNpmLock(lock, opts = {}) {
|
|
53
|
+
const packages = lock.packages ?? {};
|
|
54
|
+
const out = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
57
|
+
if (!key)
|
|
58
|
+
continue; // the root project entry
|
|
59
|
+
if (!key.includes('node_modules/'))
|
|
60
|
+
continue;
|
|
61
|
+
if (opts.productionOnly && entry.dev)
|
|
62
|
+
continue;
|
|
63
|
+
const name = nameFromKey(key);
|
|
64
|
+
const version = entry.version ?? '0.0.0';
|
|
65
|
+
const id = `${name}@${version}`;
|
|
66
|
+
if (seen.has(id))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(id);
|
|
69
|
+
const comp = { name, version, purl: purlFor(name, version) };
|
|
70
|
+
const license = licenseOf(entry);
|
|
71
|
+
if (license)
|
|
72
|
+
comp.license = license;
|
|
73
|
+
if (entry.integrity) {
|
|
74
|
+
const h = integrityToHash(entry.integrity);
|
|
75
|
+
if (h)
|
|
76
|
+
comp.hash = h;
|
|
77
|
+
}
|
|
78
|
+
if (entry.dev)
|
|
79
|
+
comp.dev = true;
|
|
80
|
+
out.push(comp);
|
|
81
|
+
}
|
|
82
|
+
out.sort((a, b) => (a.name === b.name ? a.version.localeCompare(b.version) : a.name.localeCompare(b.name)));
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Build a CycloneDX 1.5 BOM object (no timestamp → deterministic). Pure. */
|
|
86
|
+
export function buildCycloneDX(components, meta) {
|
|
87
|
+
return {
|
|
88
|
+
bomFormat: 'CycloneDX',
|
|
89
|
+
specVersion: '1.5',
|
|
90
|
+
version: 1,
|
|
91
|
+
metadata: {
|
|
92
|
+
component: { type: 'application', name: meta.name, version: meta.version },
|
|
93
|
+
},
|
|
94
|
+
components: components.map((c) => {
|
|
95
|
+
const comp = { type: 'library', name: c.name, version: c.version, purl: c.purl };
|
|
96
|
+
if (c.hash)
|
|
97
|
+
comp.hashes = [{ alg: c.hash.alg, content: c.hash.content }];
|
|
98
|
+
if (c.license)
|
|
99
|
+
comp.licenses = [{ license: { id: c.license } }];
|
|
100
|
+
return comp;
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Build a minimal SPDX 2.3 JSON document (no created timestamp → deterministic). Pure. */
|
|
105
|
+
export function buildSpdx(components, meta) {
|
|
106
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9.-]/g, '-');
|
|
107
|
+
return {
|
|
108
|
+
spdxVersion: 'SPDX-2.3',
|
|
109
|
+
dataLicense: 'CC0-1.0',
|
|
110
|
+
SPDXID: 'SPDXRef-DOCUMENT',
|
|
111
|
+
name: meta.name,
|
|
112
|
+
documentNamespace: `https://swarmdo/spdx/${safe(meta.name)}-${meta.version}`,
|
|
113
|
+
packages: [
|
|
114
|
+
{ SPDXID: 'SPDXRef-Package-root', name: meta.name, versionInfo: meta.version, downloadLocation: 'NOASSERTION' },
|
|
115
|
+
...components.map((c) => ({
|
|
116
|
+
SPDXID: `SPDXRef-Package-${safe(c.name)}-${safe(c.version)}`,
|
|
117
|
+
name: c.name,
|
|
118
|
+
versionInfo: c.version,
|
|
119
|
+
downloadLocation: 'NOASSERTION',
|
|
120
|
+
licenseConcluded: c.license ?? 'NOASSERTION',
|
|
121
|
+
externalRefs: [{ referenceCategory: 'PACKAGE-MANAGER', referenceType: 'purl', referenceLocator: c.purl }],
|
|
122
|
+
})),
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Build a BOM in the requested format. Pure. */
|
|
127
|
+
export function buildSbom(components, meta, format) {
|
|
128
|
+
return format === 'spdx' ? buildSpdx(components, meta) : buildCycloneDX(components, meta);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=sbom.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* writeStdout — write to stdout and RESOLVE ONLY WHEN THE CHUNK IS FLUSHED.
|
|
3
|
+
*
|
|
4
|
+
* process.stdout on a pipe is asynchronous: write() buffers in JS and returns
|
|
5
|
+
* immediately. If the command then returns and the CLI harness calls
|
|
6
|
+
* process.exit(), any unflushed bytes are discarded — large piped output
|
|
7
|
+
* (`swarmdo sbom | …`, `pack`, `compact`, `redact`) truncates at the ~64 KiB
|
|
8
|
+
* pipe buffer. Awaiting the write callback guarantees the data reached the OS
|
|
9
|
+
* before we hand control back. For a TTY (synchronous) this is a no-op await.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeStdout(data: string): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=stdout.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* writeStdout — write to stdout and RESOLVE ONLY WHEN THE CHUNK IS FLUSHED.
|
|
3
|
+
*
|
|
4
|
+
* process.stdout on a pipe is asynchronous: write() buffers in JS and returns
|
|
5
|
+
* immediately. If the command then returns and the CLI harness calls
|
|
6
|
+
* process.exit(), any unflushed bytes are discarded — large piped output
|
|
7
|
+
* (`swarmdo sbom | …`, `pack`, `compact`, `redact`) truncates at the ~64 KiB
|
|
8
|
+
* pipe buffer. Awaiting the write callback guarantees the data reached the OS
|
|
9
|
+
* before we hand control back. For a TTY (synchronous) this is a no-op await.
|
|
10
|
+
*/
|
|
11
|
+
export function writeStdout(data) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
process.stdout.write(data, (err) => (err ? reject(err) : resolve()));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=stdout.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmdo/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swarmdo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|