agentic-workflow-manager 6.5.2 → 8.0.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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +261 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +59 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +150 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +92 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
- package/dist/tests/commands/sensors/exec.test.js +67 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +132 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverProjectEvidence = discoverProjectEvidence;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const semver_1 = __importDefault(require("semver"));
|
|
10
|
+
const paths_1 = require("../../../core/paths");
|
|
11
|
+
const LOCKFILES = { 'package-lock.json': 'npm', 'npm-shrinkwrap.json': 'npm', 'pnpm-lock.yaml': 'pnpm', 'yarn.lock': 'yarn', 'bun.lockb': 'bun', 'bun.lock': 'bun' };
|
|
12
|
+
const COMMON_CONFIGS = ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc', '.eslintrc.json', 'tsconfig.json', 'semgrep.yml', 'semgrep.yaml', '.semgrep.yml', 'pyproject.toml', 'ruff.toml', 'mypy.ini', 'setup.cfg'];
|
|
13
|
+
function safeFile(root, relative) {
|
|
14
|
+
if (!relative || path_1.default.isAbsolute(relative) || relative.includes('\\') || relative.split('/').some(part => part === '.' || part === '..' || part === ''))
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs_1.default.lstatSync(path_1.default.join(root, relative));
|
|
18
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function safeParts(parts) {
|
|
25
|
+
return parts.length > 0 && parts.every(part => Boolean(part) && part !== '.' && part !== '..' && !part.includes('/') && !part.includes('\\'));
|
|
26
|
+
}
|
|
27
|
+
/** Refuse every symlink in a local environment path; evidence must never escape cwd. */
|
|
28
|
+
function containedEntry(root, parts, expect) {
|
|
29
|
+
if (!safeParts(parts))
|
|
30
|
+
return null;
|
|
31
|
+
let candidate = root;
|
|
32
|
+
try {
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
candidate = path_1.default.join(candidate, part);
|
|
35
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
40
|
+
return (expect === 'file' ? stat.isFile() : stat.isDirectory()) ? candidate : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readBoundedLocalFile(root, parts) {
|
|
47
|
+
const file = containedEntry(root, parts, 'file');
|
|
48
|
+
if (!file)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const stat = fs_1.default.statSync(file);
|
|
52
|
+
return stat.size <= 64 * 1024 ? fs_1.default.readFileSync(file, 'utf8') : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function exactVersion(value) {
|
|
59
|
+
return typeof value === 'string' && semver_1.default.valid(value.trim()) !== null ? semver_1.default.clean(value.trim()) : null;
|
|
60
|
+
}
|
|
61
|
+
function pythonEnvironment(root) {
|
|
62
|
+
for (const name of ['.venv', 'venv']) {
|
|
63
|
+
if (!containedEntry(root, [name], 'directory'))
|
|
64
|
+
continue;
|
|
65
|
+
const config = readBoundedLocalFile(root, [name, 'pyvenv.cfg']);
|
|
66
|
+
const version = config?.match(/^version\s*=\s*([^\r\n#]+)\s*$/mi)?.[1] ?? null;
|
|
67
|
+
return { rootParts: [name], runtimeVersion: exactVersion(version) };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function pythonSitePackages(root, environment, target) {
|
|
72
|
+
const candidates = target === 'win32'
|
|
73
|
+
? [[...environment, 'Lib', 'site-packages']]
|
|
74
|
+
: [];
|
|
75
|
+
if (target !== 'win32') {
|
|
76
|
+
const lib = containedEntry(root, [...environment, 'lib'], 'directory');
|
|
77
|
+
if (lib) {
|
|
78
|
+
try {
|
|
79
|
+
for (const entry of fs_1.default.readdirSync(lib, { withFileTypes: true }).slice(0, 128)) {
|
|
80
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && /^python\d+\.\d+$/.test(entry.name))
|
|
81
|
+
candidates.push([...environment, 'lib', entry.name, 'site-packages']);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { /* unreadable environments are not evidence */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return candidates.filter(parts => containedEntry(root, parts, 'directory') !== null);
|
|
88
|
+
}
|
|
89
|
+
function pythonToolVersion(root, sitePackages, tool) {
|
|
90
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(tool))
|
|
91
|
+
return null;
|
|
92
|
+
const normalized = tool.replace(/[._-]+/g, '-').toLowerCase();
|
|
93
|
+
for (const directory of sitePackages) {
|
|
94
|
+
const folder = containedEntry(root, directory, 'directory');
|
|
95
|
+
if (!folder)
|
|
96
|
+
continue;
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = fs_1.default.readdirSync(folder, { withFileTypes: true }).slice(0, 512);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !entry.name.endsWith('.dist-info'))
|
|
106
|
+
continue;
|
|
107
|
+
const distribution = entry.name.slice(0, -'.dist-info'.length);
|
|
108
|
+
if (!distribution.toLowerCase().startsWith(normalized + '-'))
|
|
109
|
+
continue;
|
|
110
|
+
const metadata = readBoundedLocalFile(root, [...directory, entry.name, 'METADATA']);
|
|
111
|
+
if (!metadata)
|
|
112
|
+
continue;
|
|
113
|
+
const name = metadata.match(/^Name:\s*([^\r\n]+)\s*$/mi)?.[1]?.trim().replace(/[._-]+/g, '-').toLowerCase();
|
|
114
|
+
const version = exactVersion(metadata.match(/^Version:\s*([^\r\n]+)\s*$/mi)?.[1]);
|
|
115
|
+
if (name === normalized && version)
|
|
116
|
+
return version;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function packageJson(root) {
|
|
122
|
+
if (!safeFile(root, 'package.json'))
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8'));
|
|
126
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function stringMap(value) {
|
|
133
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
134
|
+
return {};
|
|
135
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
|
|
136
|
+
}
|
|
137
|
+
function installedPackageVersion(root, tool) {
|
|
138
|
+
if (!tool || tool.split('/').some(part => !part || part === '.' || part === '..' || part.includes('\\')))
|
|
139
|
+
return null;
|
|
140
|
+
const packageFile = path_1.default.join(root, 'node_modules', ...tool.split('/'), 'package.json');
|
|
141
|
+
try {
|
|
142
|
+
const modules = fs_1.default.realpathSync(path_1.default.join(root, 'node_modules'));
|
|
143
|
+
const fileStat = fs_1.default.lstatSync(packageFile);
|
|
144
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink())
|
|
145
|
+
return null;
|
|
146
|
+
const real = fs_1.default.realpathSync(packageFile);
|
|
147
|
+
if (!real.startsWith(modules + path_1.default.sep))
|
|
148
|
+
return null;
|
|
149
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(real, 'utf8'));
|
|
150
|
+
const version = typeof parsed.version === 'string' ? parsed.version : null;
|
|
151
|
+
return version !== null && semver_1.default.valid(version) !== null ? version : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Read bounded project metadata only; it never shells out, downloads, or mutates. */
|
|
158
|
+
function discoverProjectEvidence(cwd, pack, dependencies = {}) {
|
|
159
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
160
|
+
throw new Error('cwd must be a non-empty path');
|
|
161
|
+
let root;
|
|
162
|
+
try {
|
|
163
|
+
root = fs_1.default.realpathSync(cwd);
|
|
164
|
+
if (!fs_1.default.statSync(root).isDirectory())
|
|
165
|
+
throw new Error();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
throw new Error(`cwd is not a readable project directory: ${cwd}`);
|
|
169
|
+
}
|
|
170
|
+
if (!pack || typeof pack !== 'object' || typeof pack.name !== 'string')
|
|
171
|
+
throw new Error('pack must be a parsed sensor pack');
|
|
172
|
+
const targetPlatform = (dependencies.platform ?? paths_1.platform)();
|
|
173
|
+
const pkg = packageJson(root);
|
|
174
|
+
const locks = Object.keys(LOCKFILES).filter(file => safeFile(root, file));
|
|
175
|
+
const lockManagers = new Set(locks.map(file => LOCKFILES[file]));
|
|
176
|
+
const declaredManager = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
|
|
177
|
+
if (declaredManager)
|
|
178
|
+
lockManagers.add(declaredManager);
|
|
179
|
+
const configCandidates = new Set(COMMON_CONFIGS);
|
|
180
|
+
if ('schemaVersion' in pack) {
|
|
181
|
+
for (const marker of pack.detects)
|
|
182
|
+
configCandidates.add(marker);
|
|
183
|
+
for (const sensor of Object.values(pack.sensors)) {
|
|
184
|
+
for (const marker of [...(sensor.applicability.allFiles ?? []), ...(sensor.applicability.anyFiles ?? [])])
|
|
185
|
+
configCandidates.add(marker);
|
|
186
|
+
for (const variant of sensor.variants)
|
|
187
|
+
for (const config of variant.requirements.configFiles ?? [])
|
|
188
|
+
configCandidates.add(config);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const configFiles = [...configCandidates].filter(file => safeFile(root, file)).sort();
|
|
192
|
+
const scripts = Object.keys(stringMap(pkg?.scripts)).sort();
|
|
193
|
+
const declaredToolRanges = { ...stringMap(pkg?.dependencies), ...stringMap(pkg?.devDependencies), ...stringMap(pkg?.peerDependencies) };
|
|
194
|
+
const tools = new Set();
|
|
195
|
+
if ('schemaVersion' in pack)
|
|
196
|
+
for (const sensor of Object.values(pack.sensors))
|
|
197
|
+
for (const variant of sensor.variants)
|
|
198
|
+
tools.add(variant.requirements.tool);
|
|
199
|
+
const environment = pythonEnvironment(root);
|
|
200
|
+
const sitePackages = environment ? pythonSitePackages(root, environment.rootParts, targetPlatform) : [];
|
|
201
|
+
const toolVersions = Object.fromEntries([...tools].sort().map(tool => [tool, pythonToolVersion(root, sitePackages, tool) ?? installedPackageVersion(root, tool)]));
|
|
202
|
+
return {
|
|
203
|
+
cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, declaredToolRanges, toolVersions,
|
|
204
|
+
packageManager: declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null), packageManagerConflict: lockManagers.size > 1,
|
|
205
|
+
scripts, configFiles, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveLiveCompatibility = resolveLiveCompatibility;
|
|
7
|
+
exports.resolveParsedPackCompatibility = resolveParsedPackCompatibility;
|
|
8
|
+
const pack_source_1 = require("./pack-source");
|
|
9
|
+
const contract_1 = require("./contract");
|
|
10
|
+
const discovery_1 = require("./discovery");
|
|
11
|
+
const resolve_1 = require("./resolve");
|
|
12
|
+
const probe_1 = require("./probe");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
/**
|
|
15
|
+
* Re-resolve a v2 pack from the configured registry and current project evidence.
|
|
16
|
+
* Manifest evidence is intentionally not an input: it is an init-time trace, not a
|
|
17
|
+
* live certification source. Probes are bounded and structured through the shared
|
|
18
|
+
* compatibility probe runner.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveLiveCompatibility(cwd, packName, registryRoot) {
|
|
21
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
22
|
+
throw new Error('cwd must be a non-empty path');
|
|
23
|
+
if (typeof packName !== 'string' || !/^[a-z][a-z0-9-]*$/.test(packName))
|
|
24
|
+
throw new Error('pack name must be a stable lowercase id');
|
|
25
|
+
if (registryRoot !== undefined && (typeof registryRoot !== 'string' || !path_1.default.isAbsolute(registryRoot) || path_1.default.normalize(registryRoot) !== registryRoot || /[\0\r\n]/.test(registryRoot))) {
|
|
26
|
+
throw new Error('registry root provenance must be an absolute normalized path');
|
|
27
|
+
}
|
|
28
|
+
const source = registryRoot === undefined
|
|
29
|
+
? (0, pack_source_1.resolvePackSource)(packName)
|
|
30
|
+
: (0, pack_source_1.resolvePackSource)(packName, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: registryRoot }] });
|
|
31
|
+
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
32
|
+
if (parsed.kind !== 'v2')
|
|
33
|
+
throw new Error(`sensor pack "${packName}" does not provide a v2 compatibility contract`);
|
|
34
|
+
return resolveParsedPackCompatibility(cwd, parsed.pack);
|
|
35
|
+
}
|
|
36
|
+
/** Resolve a pre-parsed v2 pack. Init uses this when given an explicit registry root. */
|
|
37
|
+
async function resolveParsedPackCompatibility(cwd, pack) {
|
|
38
|
+
if (typeof cwd !== 'string' || cwd.trim() === '')
|
|
39
|
+
throw new Error('cwd must be a non-empty path');
|
|
40
|
+
if (!pack || typeof pack !== 'object' || pack.schemaVersion !== 2)
|
|
41
|
+
throw new Error('pack must be a parsed v2 sensor pack');
|
|
42
|
+
const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, pack);
|
|
43
|
+
const initial = (0, resolve_1.resolveProjectCompatibility)(pack, evidence).sensors;
|
|
44
|
+
const sensors = {};
|
|
45
|
+
for (const [name, sensor] of Object.entries(pack.sensors)) {
|
|
46
|
+
const base = initial[name];
|
|
47
|
+
const variant = base.variantId === null ? null : sensor.variants.find(candidate => candidate.id === base.variantId) ?? null;
|
|
48
|
+
const probe = variant
|
|
49
|
+
? await (0, probe_1.runCompatibilityProbe)(variant.probe, {
|
|
50
|
+
cwd,
|
|
51
|
+
toolExecutable: variant.command.executable,
|
|
52
|
+
configFiles: evidence.configFiles,
|
|
53
|
+
scripts: evidence.scripts,
|
|
54
|
+
})
|
|
55
|
+
: null;
|
|
56
|
+
sensors[name] = (0, resolve_1.resolveProjectCompatibility)({ ...pack, sensors: { [name]: sensor } }, { ...evidence, probe: probe ?? undefined }).sensors[name];
|
|
57
|
+
}
|
|
58
|
+
return { pack, sensors };
|
|
59
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.legacyCompatibility = legacyCompatibility;
|
|
7
|
+
exports.parseSensorManifest = parseSensorManifest;
|
|
8
|
+
exports.serializeManifestV2 = serializeManifestV2;
|
|
9
|
+
const contract_1 = require("./contract");
|
|
10
|
+
const semver_1 = __importDefault(require("semver"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function sourceSuffix(source) {
|
|
16
|
+
return typeof source === 'string' && source.length > 0 && !/[\0\r\n]/.test(source) ? ` in ${source}` : ' in <unknown source>';
|
|
17
|
+
}
|
|
18
|
+
function invalid(source, message) {
|
|
19
|
+
throw new Error(`Invalid sensor manifest${sourceSuffix(source)}: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
function record(value, source, location) {
|
|
22
|
+
if (!isRecord(value))
|
|
23
|
+
invalid(source, `${location} must be an object`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function fields(value, allowed, source, location) {
|
|
27
|
+
for (const key of Object.keys(value)) {
|
|
28
|
+
if (!allowed.includes(key))
|
|
29
|
+
invalid(source, `${location} has unknown field "${key}"`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function text(value, source, location) {
|
|
33
|
+
if (typeof value !== 'string' || value.trim().length === 0 || /[\0\r\n]/.test(value)) {
|
|
34
|
+
invalid(source, `${location} must be a nonempty single-line string without NUL`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function id(value, source, location) {
|
|
39
|
+
const parsed = text(value, source, location);
|
|
40
|
+
if (!/^[a-z][a-z0-9-]*$/.test(parsed))
|
|
41
|
+
invalid(source, `${location} must be a stable lowercase id`);
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
function stringArray(value, source, location, allowEmpty) {
|
|
45
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
|
46
|
+
invalid(source, `${location} must be ${allowEmpty ? 'an array' : 'a nonempty array'}`);
|
|
47
|
+
}
|
|
48
|
+
return value.map((item, index) => text(item, source, `${location}[${index}]`));
|
|
49
|
+
}
|
|
50
|
+
function asset(value, source, location) {
|
|
51
|
+
const parsed = text(value, source, location);
|
|
52
|
+
if (parsed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(parsed) || parsed.startsWith('\\\\') || parsed.includes('\\') || parsed.split('/').some(part => part === '' || part === '.' || part === '..'))
|
|
53
|
+
invalid(source, `${location} must be a contained relative asset path`);
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
const STATES = new Set(['certified', 'compatible-unverified', 'incompatible', 'missing-tool', 'unverifiable', 'not-applicable']);
|
|
57
|
+
function nullableText(value, source, location) {
|
|
58
|
+
return value === null ? null : text(value, source, location);
|
|
59
|
+
}
|
|
60
|
+
function parseCompatibilityEvidence(input, source, location) {
|
|
61
|
+
const value = record(input, source, location);
|
|
62
|
+
fields(value, ['state', 'reason', 'variantId', 'toolVersion', 'runtimeVersion', 'certifiedRange', 'evidence'], source, location);
|
|
63
|
+
if (typeof value.state !== 'string' || !STATES.has(value.state))
|
|
64
|
+
invalid(source, `${location}.state must be a supported state`);
|
|
65
|
+
if (!Array.isArray(value.evidence))
|
|
66
|
+
invalid(source, `${location}.evidence must be an array`);
|
|
67
|
+
const evidence = { state: value.state, reason: text(value.reason, source, `${location}.reason`), variantId: nullableText(value.variantId, source, `${location}.variantId`), toolVersion: nullableText(value.toolVersion, source, `${location}.toolVersion`), runtimeVersion: nullableText(value.runtimeVersion, source, `${location}.runtimeVersion`), certifiedRange: nullableText(value.certifiedRange, source, `${location}.certifiedRange`), evidence: value.evidence.map((entry, index) => { const item = record(entry, source, `${location}.evidence[${index}]`); fields(item, ['kind', 'status', 'path'], source, `${location}.evidence[${index}]`); return { kind: text(item.kind, source, `${location}.evidence[${index}].kind`), status: text(item.status, source, `${location}.evidence[${index}].status`), ...('path' in item ? { path: asset(item.path, source, `${location}.evidence[${index}].path`) } : {}) }; }) };
|
|
68
|
+
if (evidence.toolVersion !== null && semver_1.default.valid(evidence.toolVersion) === null)
|
|
69
|
+
invalid(source, `${location}.toolVersion must be a valid semver version`);
|
|
70
|
+
if (evidence.runtimeVersion !== null && semver_1.default.valid(evidence.runtimeVersion) === null)
|
|
71
|
+
invalid(source, `${location}.runtimeVersion must be a valid semver version`);
|
|
72
|
+
if (evidence.certifiedRange !== null && semver_1.default.validRange(evidence.certifiedRange) === null)
|
|
73
|
+
invalid(source, `${location}.certifiedRange must be a valid semver range`);
|
|
74
|
+
return evidence;
|
|
75
|
+
}
|
|
76
|
+
function legacyCompatibility(reason = 'legacy manifest without schemaVersion') {
|
|
77
|
+
if (typeof reason !== 'string' || reason.trim().length === 0 || /[\0\r\n]/.test(reason)) {
|
|
78
|
+
throw new Error('legacy compatibility reason must be a nonempty single-line string without NUL');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
state: 'compatible-unverified',
|
|
82
|
+
reason,
|
|
83
|
+
variantId: null,
|
|
84
|
+
toolVersion: null,
|
|
85
|
+
runtimeVersion: null,
|
|
86
|
+
certifiedRange: null,
|
|
87
|
+
evidence: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseLegacySensor(input, source, location) {
|
|
91
|
+
if (typeof input === 'string')
|
|
92
|
+
return { cmd: text(input, source, `${location}.cmd`) };
|
|
93
|
+
const value = record(input, source, location);
|
|
94
|
+
fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
|
|
95
|
+
const sensor = {};
|
|
96
|
+
if ('cmd' in value)
|
|
97
|
+
sensor.cmd = text(value.cmd, source, `${location}.cmd`);
|
|
98
|
+
if ('fast' in value) {
|
|
99
|
+
if (typeof value.fast !== 'boolean')
|
|
100
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
101
|
+
sensor.fast = value.fast;
|
|
102
|
+
}
|
|
103
|
+
if ('enabled' in value) {
|
|
104
|
+
if (typeof value.enabled !== 'boolean')
|
|
105
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
106
|
+
sensor.enabled = value.enabled;
|
|
107
|
+
}
|
|
108
|
+
if ('timeout' in value) {
|
|
109
|
+
if (typeof value.timeout !== 'number' || !Number.isSafeInteger(value.timeout) || value.timeout <= 0)
|
|
110
|
+
invalid(source, `${location}.timeout must be a positive safe integer`);
|
|
111
|
+
sensor.timeout = value.timeout;
|
|
112
|
+
}
|
|
113
|
+
if ('changedCmd' in value)
|
|
114
|
+
sensor.changedCmd = text(value.changedCmd, source, `${location}.changedCmd`);
|
|
115
|
+
if ('changedExtensions' in value)
|
|
116
|
+
sensor.changedExtensions = stringArray(value.changedExtensions, source, `${location}.changedExtensions`, true);
|
|
117
|
+
if ('formatter' in value)
|
|
118
|
+
sensor.formatter = text(value.formatter, source, `${location}.formatter`);
|
|
119
|
+
return sensor;
|
|
120
|
+
}
|
|
121
|
+
function parseLegacyManifest(value, source) {
|
|
122
|
+
fields(value, ['pack', 'sensors', 'concurrency'], source, 'root');
|
|
123
|
+
const pack = id(value.pack, source, 'pack');
|
|
124
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
125
|
+
const sensors = {};
|
|
126
|
+
for (const name of Object.keys(sensorsInput))
|
|
127
|
+
sensors[id(name, source, 'sensor id')] = parseLegacySensor(sensorsInput[name], source, `sensors.${name}`);
|
|
128
|
+
const manifest = { pack, sensors, compatibility: legacyCompatibility() };
|
|
129
|
+
if ('concurrency' in value) {
|
|
130
|
+
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
131
|
+
invalid(source, 'concurrency must be a positive safe integer');
|
|
132
|
+
manifest.concurrency = value.concurrency;
|
|
133
|
+
}
|
|
134
|
+
return manifest;
|
|
135
|
+
}
|
|
136
|
+
function parseV2Sensor(input, source, location) {
|
|
137
|
+
const value = record(input, source, location);
|
|
138
|
+
fields(value, ['enabled', 'fast', 'variantId', 'command', 'assets', 'initializedCompatibility'], source, location);
|
|
139
|
+
if (typeof value.enabled !== 'boolean')
|
|
140
|
+
invalid(source, `${location}.enabled must be a boolean`);
|
|
141
|
+
const sensor = {
|
|
142
|
+
enabled: value.enabled, variantId: id(value.variantId, source, `${location}.variantId`),
|
|
143
|
+
command: (0, contract_1.parseStructuredCommand)(value.command, source),
|
|
144
|
+
initializedCompatibility: parseCompatibilityEvidence(value.initializedCompatibility, source, `${location}.initializedCompatibility`),
|
|
145
|
+
};
|
|
146
|
+
if (sensor.initializedCompatibility.variantId !== null && sensor.initializedCompatibility.variantId !== sensor.variantId)
|
|
147
|
+
invalid(source, `${location}.initializedCompatibility.variantId must match variantId`);
|
|
148
|
+
if (['certified', 'compatible-unverified', 'incompatible'].includes(sensor.initializedCompatibility.state) && sensor.initializedCompatibility.variantId === null)
|
|
149
|
+
invalid(source, `${location}.initializedCompatibility.variantId is required for ${sensor.initializedCompatibility.state}`);
|
|
150
|
+
if (sensor.initializedCompatibility.state === 'certified' && (sensor.initializedCompatibility.toolVersion === null || sensor.initializedCompatibility.runtimeVersion === null || sensor.initializedCompatibility.certifiedRange === null))
|
|
151
|
+
invalid(source, `${location}.initializedCompatibility certified evidence is incomplete`);
|
|
152
|
+
if (sensor.initializedCompatibility.state === 'certified' && !semver_1.default.satisfies(sensor.initializedCompatibility.toolVersion, sensor.initializedCompatibility.certifiedRange))
|
|
153
|
+
invalid(source, `${location}.initializedCompatibility.toolVersion must satisfy certifiedRange`);
|
|
154
|
+
if ('assets' in value)
|
|
155
|
+
sensor.assets = stringArray(value.assets, source, `${location}.assets`, false).map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
|
|
156
|
+
if ('fast' in value) {
|
|
157
|
+
if (typeof value.fast !== 'boolean')
|
|
158
|
+
invalid(source, `${location}.fast must be a boolean`);
|
|
159
|
+
sensor.fast = value.fast;
|
|
160
|
+
}
|
|
161
|
+
return sensor;
|
|
162
|
+
}
|
|
163
|
+
function provenanceRoot(value, source) {
|
|
164
|
+
const parsed = text(value, source, 'registryRoot');
|
|
165
|
+
if (!path_1.default.isAbsolute(parsed) || path_1.default.normalize(parsed) !== parsed)
|
|
166
|
+
invalid(source, 'registryRoot must be an absolute normalized path');
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
function parseV2Manifest(value, source) {
|
|
170
|
+
fields(value, ['schemaVersion', 'pack', 'registryRoot', 'sensors', 'concurrency'], source, 'root');
|
|
171
|
+
if (value.schemaVersion !== 2)
|
|
172
|
+
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the manifest`);
|
|
173
|
+
const pack = id(value.pack, source, 'pack');
|
|
174
|
+
const sensorsInput = record(value.sensors, source, 'sensors');
|
|
175
|
+
const sensors = {};
|
|
176
|
+
for (const name of Object.keys(sensorsInput))
|
|
177
|
+
sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
|
|
178
|
+
const manifest = { schemaVersion: 2, pack, sensors };
|
|
179
|
+
if ('registryRoot' in value)
|
|
180
|
+
manifest.registryRoot = provenanceRoot(value.registryRoot, source);
|
|
181
|
+
if ('concurrency' in value) {
|
|
182
|
+
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
183
|
+
invalid(source, 'concurrency must be a positive safe integer');
|
|
184
|
+
manifest.concurrency = value.concurrency;
|
|
185
|
+
}
|
|
186
|
+
return manifest;
|
|
187
|
+
}
|
|
188
|
+
function parseSensorManifest(input, source) {
|
|
189
|
+
const value = record(input, source, 'root');
|
|
190
|
+
return 'schemaVersion' in value ? { kind: 'v2', pack: parseV2Manifest(value, source) } : { kind: 'legacy', pack: parseLegacyManifest(value, source) };
|
|
191
|
+
}
|
|
192
|
+
function serializeManifestV2(input) {
|
|
193
|
+
const parsed = parseSensorManifest(input, 'manifest serialization');
|
|
194
|
+
if (parsed.kind !== 'v2')
|
|
195
|
+
throw new Error('Cannot serialize a legacy sensor manifest as v2');
|
|
196
|
+
return JSON.stringify(parsed.pack, null, 2);
|
|
197
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.materializeResolvedSensors = materializeResolvedSensors;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const manifest_1 = require("./manifest");
|
|
10
|
+
function stableId(value, label) {
|
|
11
|
+
if (typeof value !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value))
|
|
12
|
+
throw new Error(`${label} must be a stable lowercase id`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function containedAsset(value, label) {
|
|
16
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('\\') || path_1.default.isAbsolute(value) || value.split('/').some(part => !part || part === '.' || part === '..'))
|
|
17
|
+
throw new Error(`${label} must be a contained relative asset path`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function root(value, label) {
|
|
21
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
22
|
+
throw new Error(`${label} must be a non-empty path`);
|
|
23
|
+
const resolved = path_1.default.resolve(value);
|
|
24
|
+
try {
|
|
25
|
+
if (!fs_1.default.statSync(resolved).isDirectory())
|
|
26
|
+
throw new Error();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`${label} must be an existing directory`);
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function atomicWrite(destination, content) {
|
|
34
|
+
const directory = path_1.default.dirname(destination);
|
|
35
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
36
|
+
const temporary = path_1.default.join(directory, `.${path_1.default.basename(destination)}.${process.pid}.${Date.now()}.tmp`);
|
|
37
|
+
try {
|
|
38
|
+
fs_1.default.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
39
|
+
fs_1.default.renameSync(temporary, destination);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
try {
|
|
43
|
+
if (fs_1.default.existsSync(temporary))
|
|
44
|
+
fs_1.default.unlinkSync(temporary);
|
|
45
|
+
}
|
|
46
|
+
catch { /* best effort cleanup */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function priorAssets(projectRoot) {
|
|
50
|
+
const manifest = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
51
|
+
if (!fs_1.default.existsSync(manifest))
|
|
52
|
+
return [];
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(fs_1.default.readFileSync(manifest, 'utf8'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error(`cannot materialize over invalid sensor manifest ${manifest}`);
|
|
59
|
+
}
|
|
60
|
+
const parsedManifest = (0, manifest_1.parseSensorManifest)(parsed, manifest);
|
|
61
|
+
if (parsedManifest.kind !== 'v2')
|
|
62
|
+
return [];
|
|
63
|
+
return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
|
|
64
|
+
}
|
|
65
|
+
/** Write a selected, v2-only sensor configuration without overwriting user files.
|
|
66
|
+
* The manifest is the commit point: created assets are removed best-effort if writing it fails. */
|
|
67
|
+
function materializeResolvedSensors(input) {
|
|
68
|
+
if (!input || typeof input !== 'object')
|
|
69
|
+
throw new Error('materialization input is required');
|
|
70
|
+
const projectRoot = root(input.projectRoot, 'projectRoot');
|
|
71
|
+
const packRoot = root(input.packRoot, 'packRoot');
|
|
72
|
+
const pack = stableId(input.pack, 'pack');
|
|
73
|
+
if (!input.sensors || typeof input.sensors !== 'object' || Array.isArray(input.sensors))
|
|
74
|
+
throw new Error('sensors must be an object');
|
|
75
|
+
const sensors = {};
|
|
76
|
+
for (const [name, sensor] of Object.entries(input.sensors)) {
|
|
77
|
+
stableId(name, 'sensor id');
|
|
78
|
+
// Reuse the durable parser as the single schema boundary before persisting.
|
|
79
|
+
const parsed = (0, manifest_1.parseSensorManifest)({ schemaVersion: 2, pack, sensors: { [name]: sensor } }, 'materialized sensor');
|
|
80
|
+
if (parsed.kind !== 'v2')
|
|
81
|
+
throw new Error('materialized sensor must be v2');
|
|
82
|
+
sensors[name] = parsed.pack.sensors[name];
|
|
83
|
+
}
|
|
84
|
+
const manifest = { schemaVersion: 2, pack, sensors, ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}) };
|
|
85
|
+
const selected = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
|
|
86
|
+
const prior = priorAssets(projectRoot);
|
|
87
|
+
const configured = [];
|
|
88
|
+
const preserved = [];
|
|
89
|
+
const created = [];
|
|
90
|
+
try {
|
|
91
|
+
if (input.configure !== false) {
|
|
92
|
+
for (const asset of selected) {
|
|
93
|
+
const source = path_1.default.join(packRoot, ...asset.split('/'));
|
|
94
|
+
const destination = path_1.default.join(projectRoot, ...asset.split('/'));
|
|
95
|
+
let stat;
|
|
96
|
+
try {
|
|
97
|
+
stat = fs_1.default.lstatSync(source);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error(`selected asset is missing from pack: ${asset}`);
|
|
101
|
+
}
|
|
102
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
103
|
+
throw new Error(`selected asset must be a regular file: ${asset}`);
|
|
104
|
+
if (fs_1.default.existsSync(destination)) {
|
|
105
|
+
preserved.push(asset);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
109
|
+
atomicWrite(destination, fs_1.default.readFileSync(source, 'utf8'));
|
|
110
|
+
created.push(destination);
|
|
111
|
+
configured.push(asset);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
atomicWrite(path_1.default.join(projectRoot, '.awm', 'sensors.json'), (0, manifest_1.serializeManifestV2)(manifest));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
for (const file of created.reverse()) {
|
|
118
|
+
try {
|
|
119
|
+
fs_1.default.unlinkSync(file);
|
|
120
|
+
}
|
|
121
|
+
catch { /* best effort transaction rollback */ }
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return { manifest, configured, preserved, orphaned: prior.filter(asset => !selected.includes(asset)).sort() };
|
|
126
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolvePackSource = resolvePackSource;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const registries_1 = require("../../../core/registries");
|
|
10
|
+
const MAX_PACK_BYTES = 1024 * 1024;
|
|
11
|
+
function validPackName(pack) {
|
|
12
|
+
return typeof pack === 'string' && /^[a-z][a-z0-9-]*$/.test(pack);
|
|
13
|
+
}
|
|
14
|
+
function contained(root, candidate) {
|
|
15
|
+
const prefix = root.endsWith(path_1.default.sep) ? root : root + path_1.default.sep;
|
|
16
|
+
return candidate.startsWith(prefix);
|
|
17
|
+
}
|
|
18
|
+
/** Walk each component under contentRoot with lstat: realpath alone would hide an
|
|
19
|
+
* internal symlink whose target happens to remain inside the registry. */
|
|
20
|
+
function inspectContainedPack(root, pack) {
|
|
21
|
+
const components = ['sensor-packs', pack, 'pack.json'];
|
|
22
|
+
let candidate = root;
|
|
23
|
+
for (let index = 0; index < components.length; index++) {
|
|
24
|
+
candidate = path_1.default.join(candidate, components[index]);
|
|
25
|
+
let stat;
|
|
26
|
+
try {
|
|
27
|
+
stat = fs_1.default.lstatSync(candidate);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT')
|
|
31
|
+
return undefined;
|
|
32
|
+
if (error.code === 'ENOTDIR')
|
|
33
|
+
throw new Error(`pack source ${candidate} has a non-regular or symbolic parent`);
|
|
34
|
+
throw new Error(`cannot inspect pack source ${candidate}`);
|
|
35
|
+
}
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
throw new Error(`pack source ${candidate} must not contain symbolic links`);
|
|
38
|
+
if (index < components.length - 1 && !stat.isDirectory())
|
|
39
|
+
throw new Error(`pack source ${candidate} has a non-regular parent`);
|
|
40
|
+
if (index === components.length - 1)
|
|
41
|
+
return { candidate, stat };
|
|
42
|
+
}
|
|
43
|
+
throw new Error('pack source component walk did not reach pack.json');
|
|
44
|
+
}
|
|
45
|
+
/** Resolve only an exact, regular pack.json beneath the first configured registry.
|
|
46
|
+
* Registry order is authority; an unsafe claimed source is a hard failure, not a fallback. */
|
|
47
|
+
function resolvePackSource(pack, options = {}) {
|
|
48
|
+
if (!validPackName(pack))
|
|
49
|
+
throw new Error('pack name must be a stable lowercase id');
|
|
50
|
+
const registries = options.registries ?? (0, registries_1.listRegistries)();
|
|
51
|
+
if (!Array.isArray(registries))
|
|
52
|
+
throw new Error('registries must be an array');
|
|
53
|
+
for (const registry of registries) {
|
|
54
|
+
if (!registry || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '')
|
|
55
|
+
throw new Error('registry has an invalid content root');
|
|
56
|
+
const inspected = inspectContainedPack(registry.contentRoot, pack);
|
|
57
|
+
if (!inspected)
|
|
58
|
+
continue;
|
|
59
|
+
const { candidate, stat } = inspected;
|
|
60
|
+
if (!stat.isFile())
|
|
61
|
+
throw new Error(`pack source ${candidate} must be a contained regular file, never a symbolic link`);
|
|
62
|
+
if (stat.size > MAX_PACK_BYTES)
|
|
63
|
+
throw new Error(`pack source ${candidate} exceeds the 1 MiB limit`);
|
|
64
|
+
let root;
|
|
65
|
+
let real;
|
|
66
|
+
try {
|
|
67
|
+
root = fs_1.default.realpathSync(registry.contentRoot);
|
|
68
|
+
real = fs_1.default.realpathSync(candidate);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`cannot canonicalize pack source ${candidate}`);
|
|
72
|
+
}
|
|
73
|
+
if (!contained(root, real))
|
|
74
|
+
throw new Error(`pack source ${candidate} escapes its registry root`);
|
|
75
|
+
return { path: real, content: fs_1.default.readFileSync(real, 'utf8'), registry };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`sensor pack "${pack}" was not found in configured registries`);
|
|
78
|
+
}
|