agentic-workflow-manager 9.5.0 → 9.6.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/src/commands/preflight/checks.js +2 -2
- package/dist/src/commands/sensors/bootstrap.js +171 -0
- package/dist/src/commands/sensors/compatibility/contract.js +18 -15
- package/dist/src/commands/sensors/compatibility/manifest.js +29 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +20 -10
- package/dist/src/commands/sensors/detection.js +31 -0
- package/dist/src/commands/sensors/exec.js +116 -11
- package/dist/src/commands/sensors/index.js +51 -10
- package/dist/src/commands/sensors/init.js +17 -170
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/run.js +2 -2
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/secure-fs/native-bridge.js +209 -0
- package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +67 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
- package/dist/tests/commands/sensors/exec.test.js +86 -0
- package/dist/tests/commands/sensors/index.test.js +54 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
- package/dist/tests/commands/sensors/init.test.js +56 -490
- package/dist/tests/commands/sensors/migrate.test.js +513 -0
- package/dist/tests/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
- package/dist/tests/structural/native-release-artifacts.test.js +22 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
- package/dist/tests/structural/release-registry-provenance.test.js +4 -0
- package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
- package/package.json +6 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -11,7 +11,9 @@ const contract_1 = require("./contract");
|
|
|
11
11
|
const contract_2 = require("../compatibility/contract");
|
|
12
12
|
const manifest_1 = require("../compatibility/manifest");
|
|
13
13
|
const pack_source_1 = require("../compatibility/pack-source");
|
|
14
|
+
const source_1 = require("../compatibility/source");
|
|
14
15
|
const safe_file_1 = require("../compatibility/safe-file");
|
|
16
|
+
const registries_1 = require("../../../core/registries");
|
|
15
17
|
function readFailure(file, error) {
|
|
16
18
|
return new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
19
|
}
|
|
@@ -82,11 +84,19 @@ function resolveCoverageInputs(cwd) {
|
|
|
82
84
|
return { kind: 'not_configured' };
|
|
83
85
|
const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
84
86
|
const manifest = (0, manifest_1.parseSensorManifest)(readBoundedJson(manifestPath), manifestPath);
|
|
85
|
-
if (manifest.kind === 'v3')
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (manifest.kind === 'v3' && manifest.pack.mode !== 'project-sensors')
|
|
88
|
+
return { kind: 'not_configured' };
|
|
89
|
+
const coverageManifest = manifest;
|
|
90
|
+
const source = coverageManifest.kind === 'v3'
|
|
91
|
+
? (() => {
|
|
92
|
+
const resolution = (0, source_1.resolveSensorSource)(coverageManifest, { registries: (0, registries_1.listRegistries)() });
|
|
93
|
+
if (!('source' in resolution))
|
|
94
|
+
throw new Error(`${resolution.kind}: ${resolution.remedy}`);
|
|
95
|
+
return resolution.source;
|
|
96
|
+
})()
|
|
97
|
+
: coverageManifest.kind === 'v2' && coverageManifest.pack.registryRoot !== undefined
|
|
98
|
+
? (0, pack_source_1.resolvePackSource)(coverageManifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: coverageManifest.pack.registryRoot }] })
|
|
99
|
+
: (0, pack_source_1.resolvePackSource)(coverageManifest.pack.pack);
|
|
90
100
|
let sourceJson;
|
|
91
101
|
try {
|
|
92
102
|
sourceJson = JSON.parse(source.content);
|
|
@@ -95,14 +105,14 @@ function resolveCoverageInputs(cwd) {
|
|
|
95
105
|
throw new Error(`Invalid JSON at ${source.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
106
|
}
|
|
97
107
|
const parsedPack = (0, contract_2.parseSensorPack)(sourceJson, source.path);
|
|
98
|
-
if (parsedPack.pack.name !==
|
|
99
|
-
throw new Error(`Invalid pack at ${source.path}: name must equal '${
|
|
108
|
+
if (parsedPack.pack.name !== coverageManifest.pack.pack) {
|
|
109
|
+
throw new Error(`Invalid pack at ${source.path}: name must equal '${coverageManifest.pack.pack}'`);
|
|
100
110
|
}
|
|
101
111
|
const { coverage } = parsedPack.pack;
|
|
102
112
|
if (coverage === undefined)
|
|
103
|
-
return { kind: 'no_reference', projectRoot, pack:
|
|
113
|
+
return { kind: 'no_reference', projectRoot, pack: coverageManifest.pack.pack, registry: source.registry.name, registryRoot: source.registry.contentRoot, manifest: coverageManifest };
|
|
104
114
|
return {
|
|
105
|
-
kind: 'ready', projectRoot, pack:
|
|
106
|
-
manifest, contract: (0, contract_1.parseCoverageContract)(coverage, source.path),
|
|
115
|
+
kind: 'ready', projectRoot, pack: coverageManifest.pack.pack, registry: source.registry.name,
|
|
116
|
+
registryRoot: source.registry.contentRoot, manifest: coverageManifest, contract: (0, contract_1.parseCoverageContract)(coverage, source.path),
|
|
107
117
|
};
|
|
108
118
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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.detectStack = detectStack;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const DETECTORS = [
|
|
10
|
+
{ pack: 'js-ts', files: ['package.json'] },
|
|
11
|
+
{ pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
|
|
12
|
+
];
|
|
13
|
+
/** Read-only stack classification shared by run, bootstrap, and legacy init. */
|
|
14
|
+
function detectStack(cwd) {
|
|
15
|
+
for (const { pack, files } of DETECTORS) {
|
|
16
|
+
const indicators = files.filter(file => fs_1.default.existsSync(path_1.default.join(cwd, file)));
|
|
17
|
+
if (indicators.length > 0)
|
|
18
|
+
return { pack, indicators };
|
|
19
|
+
}
|
|
20
|
+
for (const directory of ['.', 'scripts']) {
|
|
21
|
+
const absolute = path_1.default.join(cwd, directory);
|
|
22
|
+
if (!fs_1.default.existsSync(absolute) || !fs_1.default.statSync(absolute).isDirectory())
|
|
23
|
+
continue;
|
|
24
|
+
const indicators = fs_1.default.readdirSync(absolute, { withFileTypes: true })
|
|
25
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.sh'))
|
|
26
|
+
.map(entry => directory === '.' ? entry.name : path_1.default.join(directory, entry.name));
|
|
27
|
+
if (indicators.length > 0)
|
|
28
|
+
return { pack: 'shell', indicators };
|
|
29
|
+
}
|
|
30
|
+
return { pack: 'generic', indicators: [] };
|
|
31
|
+
}
|
|
@@ -7,12 +7,42 @@ exports.runStructuredCommand = runStructuredCommand;
|
|
|
7
7
|
exports.runCommand = runCommand;
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const paths_1 = require("../../core/paths");
|
|
12
13
|
const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
13
14
|
const DEFAULT_KILL_GRACE_MS = 2_000;
|
|
14
15
|
/** After SIGKILL, resolve regardless. A sensor must never hang the gate. */
|
|
15
16
|
const POST_KILL_GRACE_MS = 1_000;
|
|
17
|
+
const PRLIMIT_PATHS = ['/usr/bin/prlimit', '/bin/prlimit'];
|
|
18
|
+
/** Decode a byte-bounded capture without allowing replacement characters to
|
|
19
|
+
* expand it. Invalid input bytes become one-byte `?` markers. */
|
|
20
|
+
function decodeBoundedOutput(buffer) {
|
|
21
|
+
const decoded = Buffer.allocUnsafe(buffer.length);
|
|
22
|
+
let read = 0;
|
|
23
|
+
let written = 0;
|
|
24
|
+
const continuation = (index) => index < buffer.length && (buffer[index] & 0b1100_0000) === 0b1000_0000;
|
|
25
|
+
while (read < buffer.length) {
|
|
26
|
+
const first = buffer[read];
|
|
27
|
+
let length = 1;
|
|
28
|
+
if (first >= 0xc2 && first <= 0xdf && continuation(read + 1))
|
|
29
|
+
length = 2;
|
|
30
|
+
else if (first >= 0xe0 && first <= 0xef && continuation(read + 1) && continuation(read + 2)
|
|
31
|
+
&& !(first === 0xe0 && buffer[read + 1] < 0xa0) && !(first === 0xed && buffer[read + 1] > 0x9f))
|
|
32
|
+
length = 3;
|
|
33
|
+
else if (first >= 0xf0 && first <= 0xf4 && continuation(read + 1) && continuation(read + 2) && continuation(read + 3)
|
|
34
|
+
&& !(first === 0xf0 && buffer[read + 1] < 0x90) && !(first === 0xf4 && buffer[read + 1] > 0x8f))
|
|
35
|
+
length = 4;
|
|
36
|
+
else if (first >= 0x80)
|
|
37
|
+
decoded[written++] = 0x3f;
|
|
38
|
+
if (length > 1 || first < 0x80) {
|
|
39
|
+
buffer.copy(decoded, written, read, read + length);
|
|
40
|
+
written += length;
|
|
41
|
+
}
|
|
42
|
+
read += length;
|
|
43
|
+
}
|
|
44
|
+
return decoded.subarray(0, written).toString('utf8');
|
|
45
|
+
}
|
|
16
46
|
/**
|
|
17
47
|
* Kill an entire process tree, not just its root.
|
|
18
48
|
*
|
|
@@ -76,10 +106,59 @@ function collectSpawn(input, opts) {
|
|
|
76
106
|
const startedAt = process.hrtime.bigint();
|
|
77
107
|
let stdout = '';
|
|
78
108
|
let stderr = '';
|
|
109
|
+
const stdoutChunks = [];
|
|
110
|
+
const stderrChunks = [];
|
|
111
|
+
let capturedBytes = 0;
|
|
79
112
|
let timedOut = false;
|
|
80
113
|
let overflowed = false;
|
|
81
114
|
let settled = false;
|
|
82
115
|
const timers = [];
|
|
116
|
+
const prlimit = !input.shell && process.platform === 'linux'
|
|
117
|
+
? PRLIMIT_PATHS.find(candidate => regularFile(candidate))
|
|
118
|
+
: undefined;
|
|
119
|
+
const streamMaxBuffer = maxBuffer;
|
|
120
|
+
// Some structured Node CLIs call process.exit() after writing their
|
|
121
|
+
// report. Node drops an asynchronous pipe write in that case, but a
|
|
122
|
+
// regular file descriptor is flushed before exit. Keep the report on
|
|
123
|
+
// disk only until settlement. Each channel is hard-capped at maxBuffer
|
|
124
|
+
// while a shared decode budget caps the returned combined output.
|
|
125
|
+
// Linux enforces the per-file cap before either child stream can grow
|
|
126
|
+
// unbounded; polling alone can only discover excess after it happens.
|
|
127
|
+
const captureDir = prlimit ? fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensor-output-')) : undefined;
|
|
128
|
+
const stdoutPath = captureDir ? path_1.default.join(captureDir, 'stdout') : undefined;
|
|
129
|
+
const stderrPath = captureDir ? path_1.default.join(captureDir, 'stderr') : undefined;
|
|
130
|
+
const stdoutFd = stdoutPath ? fs_1.default.openSync(stdoutPath, 'w') : undefined;
|
|
131
|
+
const stderrFd = stderrPath ? fs_1.default.openSync(stderrPath, 'w') : undefined;
|
|
132
|
+
const readCaptured = (file, fd, budget) => {
|
|
133
|
+
fs_1.default.closeSync(fd);
|
|
134
|
+
const size = fs_1.default.statSync(file).size;
|
|
135
|
+
if (size >= streamMaxBuffer)
|
|
136
|
+
overflowed = true;
|
|
137
|
+
if (size > budget)
|
|
138
|
+
overflowed = true;
|
|
139
|
+
const bytes = Math.min(size, budget);
|
|
140
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
141
|
+
if (buffer.length > 0) {
|
|
142
|
+
const readFd = fs_1.default.openSync(file, 'r');
|
|
143
|
+
try {
|
|
144
|
+
fs_1.default.readSync(readFd, buffer, 0, buffer.length, 0);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
fs_1.default.closeSync(readFd);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { output: decodeBoundedOutput(buffer), bytes };
|
|
151
|
+
};
|
|
152
|
+
const captureSize = (file) => {
|
|
153
|
+
if (!file)
|
|
154
|
+
return 0;
|
|
155
|
+
try {
|
|
156
|
+
return fs_1.default.statSync(file).size;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
83
162
|
const child = input.shell
|
|
84
163
|
? (0, child_process_1.spawn)(input.executable, {
|
|
85
164
|
shell: true,
|
|
@@ -87,14 +166,16 @@ function collectSpawn(input, opts) {
|
|
|
87
166
|
detached: !(0, paths_1.isWindowsNative)(),
|
|
88
167
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
168
|
})
|
|
90
|
-
: (0, child_process_1.spawn)(input.executable,
|
|
169
|
+
: (0, child_process_1.spawn)(prlimit ?? input.executable, prlimit
|
|
170
|
+
? [`--fsize=${streamMaxBuffer}`, '--', input.executable, ...input.args]
|
|
171
|
+
: input.args, {
|
|
91
172
|
shell: false,
|
|
92
173
|
cwd: opts.cwd,
|
|
93
174
|
detached: !(0, paths_1.isWindowsNative)(),
|
|
94
175
|
...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
|
|
95
176
|
// stdin closed: a sensor must never block waiting for input, and the
|
|
96
177
|
// EOF also tells watch-mode-capable tools (vitest, jest) to run once.
|
|
97
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
+
stdio: prlimit ? ['ignore', stdoutFd, stderrFd] : ['ignore', 'pipe', 'pipe'],
|
|
98
179
|
});
|
|
99
180
|
const later = (fn, ms) => {
|
|
100
181
|
const t = setTimeout(fn, ms);
|
|
@@ -107,6 +188,20 @@ function collectSpawn(input, opts) {
|
|
|
107
188
|
return;
|
|
108
189
|
settled = true;
|
|
109
190
|
timers.forEach(clearTimeout);
|
|
191
|
+
if (stdoutPath && stderrPath && stdoutFd !== undefined && stderrFd !== undefined) {
|
|
192
|
+
try {
|
|
193
|
+
const capturedStdout = readCaptured(stdoutPath, stdoutFd, maxBuffer);
|
|
194
|
+
stdout = capturedStdout.output;
|
|
195
|
+
stderr = readCaptured(stderrPath, stderrFd, maxBuffer - capturedStdout.bytes).output;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
fs_1.default.rmSync(captureDir, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
stdout = decodeBoundedOutput(Buffer.concat(stdoutChunks));
|
|
203
|
+
stderr = decodeBoundedOutput(Buffer.concat(stderrChunks));
|
|
204
|
+
}
|
|
110
205
|
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
|
|
111
206
|
resolve({ stdout, stderr, code: null, signal: null, timedOut, overflowed, elapsedMs, ...extra });
|
|
112
207
|
};
|
|
@@ -124,15 +219,13 @@ function collectSpawn(input, opts) {
|
|
|
124
219
|
const collect = (into) => (chunk) => {
|
|
125
220
|
if (settled || overflowed)
|
|
126
221
|
return;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
stderr = next;
|
|
135
|
-
if (next.length >= maxBuffer) {
|
|
222
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
223
|
+
const room = maxBuffer - capturedBytes;
|
|
224
|
+
const kept = bytes.subarray(0, Math.max(0, room));
|
|
225
|
+
if (kept.length > 0)
|
|
226
|
+
(into === 'out' ? stdoutChunks : stderrChunks).push(kept);
|
|
227
|
+
capturedBytes += kept.length;
|
|
228
|
+
if (bytes.length > kept.length || capturedBytes >= maxBuffer) {
|
|
136
229
|
overflowed = true;
|
|
137
230
|
cutShort();
|
|
138
231
|
}
|
|
@@ -143,6 +236,18 @@ function collectSpawn(input, opts) {
|
|
|
143
236
|
child.stderr?.on('error', () => { });
|
|
144
237
|
child.on('error', (err) => finish({ spawnError: err }));
|
|
145
238
|
child.on('close', (code, signal) => finish({ code, signal }));
|
|
239
|
+
if (stdoutPath || stderrPath) {
|
|
240
|
+
const outputWatch = setInterval(() => {
|
|
241
|
+
const stdoutSize = captureSize(stdoutPath);
|
|
242
|
+
const stderrSize = captureSize(stderrPath);
|
|
243
|
+
if (!overflowed && (stdoutSize >= streamMaxBuffer || stderrSize >= streamMaxBuffer || stdoutSize + stderrSize >= maxBuffer)) {
|
|
244
|
+
overflowed = true;
|
|
245
|
+
cutShort();
|
|
246
|
+
}
|
|
247
|
+
}, 25);
|
|
248
|
+
outputWatch.unref?.();
|
|
249
|
+
timers.push(outputWatch);
|
|
250
|
+
}
|
|
146
251
|
later(() => { timedOut = true; cutShort(); }, opts.timeout);
|
|
147
252
|
});
|
|
148
253
|
}
|
|
@@ -8,7 +8,6 @@ exports.registerSensorsCommand = registerSensorsCommand;
|
|
|
8
8
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
9
|
const prompts_1 = require("@clack/prompts");
|
|
10
10
|
const run_1 = require("./run");
|
|
11
|
-
const init_1 = require("./init");
|
|
12
11
|
const status_1 = require("./status");
|
|
13
12
|
const install_1 = require("./install");
|
|
14
13
|
const baseline_1 = require("./baseline");
|
|
@@ -16,6 +15,7 @@ const coverage_1 = require("./coverage");
|
|
|
16
15
|
const render_1 = require("./coverage/render");
|
|
17
16
|
const registries_1 = require("../../core/registries");
|
|
18
17
|
const verdict_1 = require("./verdict");
|
|
18
|
+
const bootstrap_1 = require("./bootstrap");
|
|
19
19
|
/** Commander coercion for coverage recurrence emphasis. It deliberately runs
|
|
20
20
|
* before the action, so an invalid value cannot trigger ledger I/O. */
|
|
21
21
|
function parsePositiveSafeInteger(value) {
|
|
@@ -71,23 +71,64 @@ function registerSensorsCommand(program) {
|
|
|
71
71
|
.action(async (opts) => {
|
|
72
72
|
const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
|
|
73
73
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: 'project-sensors', registryRoot, configure: opts.configure, pack: opts.pack, packageRoot: opts.packageRoot });
|
|
75
|
+
if (plan.kind === 'blocked')
|
|
76
|
+
throw new Error(`${plan.reason}: ${plan.remedy}`);
|
|
77
|
+
if (plan.kind === 'noop') {
|
|
78
|
+
prompts_1.log.info('already-configured');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (plan.kind === 'migrate')
|
|
82
|
+
throw new Error('sensors init does not migrate an existing v2 manifest; run awm sensors bootstrap');
|
|
83
|
+
const result = (0, bootstrap_1.applySensorBootstrap)(plan);
|
|
84
|
+
const detected = plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : 'none';
|
|
85
|
+
prompts_1.log.success(`Detected: ${detected}`);
|
|
76
86
|
// Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
|
|
77
87
|
// reported as written is not the one the detection implied.
|
|
78
|
-
|
|
79
|
-
prompts_1.log.warn(`No '${result.unavailablePack}' sensor-pack in the registry — wrote the `
|
|
80
|
-
+ `'${result.manifest.pack}' pack instead (${Object.keys(result.manifest.sensors).length} sensors). `
|
|
81
|
-
+ 'Run `awm update`, or add a registry that ships it, then re-run `awm sensors init`.');
|
|
82
|
-
}
|
|
83
|
-
prompts_1.log.success('Wrote .awm/sensors.json');
|
|
84
|
-
result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
|
|
88
|
+
prompts_1.log.success(`${result} .awm/sensors.json`);
|
|
85
89
|
}
|
|
86
90
|
catch (e) {
|
|
87
91
|
prompts_1.log.error(e instanceof Error ? e.message : String(e));
|
|
88
92
|
process.exit(1);
|
|
89
93
|
}
|
|
90
94
|
});
|
|
95
|
+
sensors
|
|
96
|
+
.command('bootstrap')
|
|
97
|
+
.description('explicitly create or migrate portable project sensor configuration')
|
|
98
|
+
.option('--mode <mode>', 'project-sensors, native-gate, or opt-out')
|
|
99
|
+
.option('--reason <text>', 'versioned reason required by native-gate and opt-out')
|
|
100
|
+
.option('--dry-run', 'report exact project changes without writing files')
|
|
101
|
+
.action(async (opts) => {
|
|
102
|
+
try {
|
|
103
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: opts.mode, reason: opts.reason, dryRun: opts.dryRun });
|
|
104
|
+
if (plan.kind === 'blocked') {
|
|
105
|
+
prompts_1.log.error(`${plan.reason}: ${plan.remedy}`);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (plan.kind === 'noop') {
|
|
110
|
+
prompts_1.log.info('already-configured');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const summary = plan.changes.map(change => change.path).join(', ');
|
|
114
|
+
if (plan.dryRun) {
|
|
115
|
+
const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors'
|
|
116
|
+
? `; pack=${plan.manifest.pack}`
|
|
117
|
+
: '';
|
|
118
|
+
prompts_1.log.info(`dry-run: ${plan.kind}${pack}; ${summary}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const result = (0, bootstrap_1.applySensorBootstrap)(plan);
|
|
122
|
+
const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors'
|
|
123
|
+
? `; pack=${plan.manifest.pack}`
|
|
124
|
+
: '';
|
|
125
|
+
prompts_1.log.success(`${result}${pack}: ${summary}`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
prompts_1.log.error(error instanceof Error ? error.message : String(error));
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
91
132
|
sensors
|
|
92
133
|
.command('baseline')
|
|
93
134
|
.description('snapshot current findings as accepted — sensors then fail only on NEW ones')
|
|
@@ -9,11 +9,8 @@ exports.buildManifest = buildManifest;
|
|
|
9
9
|
exports.initSensors = initSensors;
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const contract_1 = require("./compatibility/contract");
|
|
13
|
-
const materialize_1 = require("./compatibility/materialize");
|
|
14
|
-
const live_1 = require("./compatibility/live");
|
|
15
|
-
const manifest_1 = require("./compatibility/manifest");
|
|
16
12
|
const pack_source_1 = require("./compatibility/pack-source");
|
|
13
|
+
const bootstrap_1 = require("./bootstrap");
|
|
17
14
|
/** Read a pack only through the shared source resolver. Init is a registry
|
|
18
15
|
* boundary: a pack path that is absent is harmless, but a symlink or escaping
|
|
19
16
|
* claimed source is never silently downgraded to an empty manifest. */
|
|
@@ -30,20 +27,6 @@ function readResolvedPack(pack, registryRoot) {
|
|
|
30
27
|
throw error;
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
|
-
function readV2Pack(pack, registryRoot) {
|
|
34
|
-
const source = readResolvedPack(pack, registryRoot);
|
|
35
|
-
if (!source)
|
|
36
|
-
return null;
|
|
37
|
-
let raw;
|
|
38
|
-
try {
|
|
39
|
-
raw = JSON.parse(source.content);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
throw new Error(`cannot parse sensor pack ${source.path}`);
|
|
43
|
-
}
|
|
44
|
-
const parsed = (0, contract_1.parseSensorPack)(raw, source.path);
|
|
45
|
-
return parsed.kind === 'v2' ? { pack: parsed.pack, packRoot: path_1.default.dirname(source.path) } : null;
|
|
46
|
-
}
|
|
47
30
|
const STACK_DETECTORS = [
|
|
48
31
|
{ pack: 'js-ts', files: ['package.json'] },
|
|
49
32
|
{ pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
|
|
@@ -165,159 +148,23 @@ function availablePacks(registryRoot) {
|
|
|
165
148
|
.map(e => e.name)
|
|
166
149
|
.sort();
|
|
167
150
|
}
|
|
168
|
-
/** The last-resort pack, used when the detected one is absent from the registry. */
|
|
169
|
-
const FALLBACK_PACK = 'generic';
|
|
170
|
-
/**
|
|
171
|
-
* Resolve the detected pack against what the registry actually ships.
|
|
172
|
-
*
|
|
173
|
-
* `--pack <name>` has always thrown here (`assertPackExists`), but auto-detection had
|
|
174
|
-
* no such check: it wrote `{"pack":"python","sensors":{}}` and said nothing, so the
|
|
175
|
-
* operator learned the gate was empty from an unrelated command days later. The two
|
|
176
|
-
* paths now reach the same conclusion; only the remedy differs, because an explicit
|
|
177
|
-
* `--pack` is a typo to correct while a detection is a fact about the tree that the
|
|
178
|
-
* registry simply cannot serve yet.
|
|
179
|
-
*
|
|
180
|
-
* Falling back to `generic` keeps the gate measuring *something* real rather than
|
|
181
|
-
* nothing. When the registry has no `generic` either, the detected pack is kept: the
|
|
182
|
-
* manifest is then honestly empty, and preflight's `manifest`/`tools` checks both fail
|
|
183
|
-
* on `total === 0` with a remedy pointing at the registry.
|
|
184
|
-
*/
|
|
185
|
-
function resolvePack(detected, registryRoot) {
|
|
186
|
-
if (!registryRoot)
|
|
187
|
-
return { pack: detected }; // nothing to validate against
|
|
188
|
-
const available = availablePacks(registryRoot);
|
|
189
|
-
if (available.length === 0 || available.includes(detected))
|
|
190
|
-
return { pack: detected };
|
|
191
|
-
return {
|
|
192
|
-
pack: available.includes(FALLBACK_PACK) ? FALLBACK_PACK : detected,
|
|
193
|
-
unavailablePack: detected,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Validate that `pack` exists as a directory under `<registryRoot>/sensor-packs/`.
|
|
198
|
-
* Throws (not a swallow-and-return) so `awm sensors init --pack bogus` actually stops
|
|
199
|
-
* instead of silently writing a manifest for a pack that doesn't exist. Lists every
|
|
200
|
-
* pack directory actually present, sorted, so the user immediately sees valid options.
|
|
201
|
-
*/
|
|
202
|
-
function assertPackExists(pack, registryRoot) {
|
|
203
|
-
const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
|
|
204
|
-
if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory()) {
|
|
205
|
-
throw new Error('registry has no sensor-packs directory');
|
|
206
|
-
}
|
|
207
|
-
const available = availablePacks(registryRoot);
|
|
208
|
-
if (!available.includes(pack)) {
|
|
209
|
-
throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
151
|
async function initSensors(opts = {}) {
|
|
213
152
|
const cwd = opts.cwd ?? process.cwd();
|
|
214
|
-
const configure = opts.configure ?? true; // configure (copy pack config files) by default
|
|
215
|
-
const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
|
|
216
|
-
// Monorepo support: detection and evidence both need to see the real project,
|
|
217
|
-
// not the (possibly unrelated) directory the manifest lives in. The manifest
|
|
218
|
-
// write itself stays pinned to cwd regardless — see manifestPath above.
|
|
219
153
|
const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let raw;
|
|
237
|
-
try {
|
|
238
|
-
raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
throw new Error(`cannot parse existing sensor manifest ${manifestPath}`);
|
|
242
|
-
}
|
|
243
|
-
// Never overwrite a corrupt or future-version manifest during init. Legacy
|
|
244
|
-
// manifests remain a supported migration input; v2 is validated before a new
|
|
245
|
-
// materialized selection becomes the commit point.
|
|
246
|
-
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
247
|
-
if (parsed.kind === 'legacy')
|
|
248
|
-
existing = parsed.pack;
|
|
249
|
-
else if (parsed.kind === 'v2')
|
|
250
|
-
existingV2 = parsed.pack;
|
|
251
|
-
else
|
|
252
|
-
throw new Error('schemaVersion 3 sensor manifests require the project resolver');
|
|
253
|
-
}
|
|
254
|
-
// `detection` keeps saying what the tree IS; `pack` is what the registry can serve
|
|
255
|
-
// for it. They diverge only when the registry lacks the detected pack.
|
|
256
|
-
const { pack: resolvedPack, unavailablePack } = resolvePack(detection.pack, opts.registryRoot);
|
|
257
|
-
// v2 packs own the executable contract. Legacy packs deliberately retain the
|
|
258
|
-
// old string-command path below until a registry author publishes a v2 contract.
|
|
259
|
-
if (opts.registryRoot) {
|
|
260
|
-
const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
|
|
261
|
-
if (resolvedV2) {
|
|
262
|
-
const packSelection = opts.pack ? 'explicit' : undefined;
|
|
263
|
-
const live = await (0, live_1.resolveParsedPackCompatibility)(detectionCwd, resolvedV2.pack, { packSelection });
|
|
264
|
-
const compatibility = live.sensors;
|
|
265
|
-
const sensors = {};
|
|
266
|
-
for (const [name, sensor] of Object.entries(live.pack.sensors)) {
|
|
267
|
-
const resolved = compatibility[name];
|
|
268
|
-
const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
|
|
269
|
-
// Unresolved states do not receive an arbitrary command. They remain
|
|
270
|
-
// represented in the manifest so status/coverage can explain them,
|
|
271
|
-
// but run cannot accidentally dispatch a mismatched executable.
|
|
272
|
-
if (variant) {
|
|
273
|
-
const prior = existing?.sensors[name] ?? existingV2?.sensors[name];
|
|
274
|
-
// Legacy string commands cannot be translated into a shell-free
|
|
275
|
-
// structured command without guessing. Preserve the operator's
|
|
276
|
-
// enabled/fast intent but make that migration non-certified.
|
|
277
|
-
const migratedOverride = existing?.sensors[name]?.cmd !== undefined;
|
|
278
|
-
sensors[name] = {
|
|
279
|
-
enabled: prior?.enabled ?? true,
|
|
280
|
-
fast: prior?.fast ?? sensor.fast ?? false,
|
|
281
|
-
...(prior?.timeout !== undefined ? { timeout: prior.timeout } : {}),
|
|
282
|
-
variantId: variant.id,
|
|
283
|
-
command: variant.command,
|
|
284
|
-
assets: variant.assets,
|
|
285
|
-
...(variant.policyRef ? { policyRef: variant.policyRef } : {}),
|
|
286
|
-
initializedCompatibility: migratedOverride
|
|
287
|
-
? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
|
|
288
|
-
: resolved,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const materialized = (0, materialize_1.materializeResolvedSensors)({
|
|
293
|
-
projectRoot: cwd, packRoot: resolvedV2.packRoot,
|
|
294
|
-
pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot,
|
|
295
|
-
...(opts.packageRoot ? { packageRoot: opts.packageRoot } : {}), sensors, configure,
|
|
296
|
-
});
|
|
297
|
-
return { detection, ...materialized,
|
|
298
|
-
compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
|
|
302
|
-
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
303
|
-
const tmpPath = manifestPath + '.tmp';
|
|
304
|
-
fs_1.default.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
305
|
-
fs_1.default.renameSync(tmpPath, manifestPath);
|
|
306
|
-
const configured = [];
|
|
307
|
-
if (configure && opts.registryRoot) {
|
|
308
|
-
// The pack whose defaults the manifest was actually built from — copying the
|
|
309
|
-
// detected pack's config files here would drop files for sensors that are not
|
|
310
|
-
// in the manifest, and miss the ones that are.
|
|
311
|
-
const packDir = path_1.default.join(opts.registryRoot, 'sensor-packs', resolvedPack);
|
|
312
|
-
if (fs_1.default.existsSync(packDir)) {
|
|
313
|
-
for (const file of fs_1.default.readdirSync(packDir).filter(f => f !== 'pack.json')) {
|
|
314
|
-
const dst = path_1.default.join(cwd, file);
|
|
315
|
-
if (!fs_1.default.existsSync(dst)) {
|
|
316
|
-
fs_1.default.copyFileSync(path_1.default.join(packDir, file), dst);
|
|
317
|
-
configured.push(file);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return { manifest, detection, configured, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
154
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(cwd, {
|
|
155
|
+
mode: 'project-sensors', registryRoot: opts.registryRoot, configure: opts.configure,
|
|
156
|
+
pack: opts.pack, packageRoot: opts.packageRoot,
|
|
157
|
+
});
|
|
158
|
+
if (plan.kind === 'blocked')
|
|
159
|
+
throw new Error(`${plan.reason}: ${plan.remedy}`);
|
|
160
|
+
if (plan.kind === 'migrate')
|
|
161
|
+
throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
|
|
162
|
+
if (plan.kind === 'noop')
|
|
163
|
+
return {
|
|
164
|
+
// No manifest is written or synthesized here. The compatibility API only
|
|
165
|
+
// retains its historical result shape for callers that inspect detection.
|
|
166
|
+
manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
|
|
167
|
+
};
|
|
168
|
+
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
169
|
+
return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created' };
|
|
323
170
|
}
|