hadara 0.3.3 → 0.3.4-rc.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/README.md +17 -9
- package/dist/cli/dashboard.js +13 -6
- package/dist/cli/evidence.js +42 -0
- package/dist/cli/handoff.js +13 -0
- package/dist/cli/init.js +20 -2
- package/dist/cli/main.js +3 -0
- package/dist/cli/package-smoke.js +33 -1
- package/dist/cli/release-closeout.js +22 -0
- package/dist/context/context-pack.js +91 -7
- package/dist/context/session-start.js +39 -0
- package/dist/core/schema.js +8 -0
- package/dist/handoff/handoff-stale-problems.js +150 -0
- package/dist/schemas/context-pack.schema.json +23 -0
- package/dist/schemas/evidence-summary.schema.json +73 -0
- package/dist/schemas/handoff-stale-problems.schema.json +64 -0
- package/dist/schemas/package-recycle.schema.json +150 -0
- package/dist/schemas/release-closeout.schema.json +39 -0
- package/dist/schemas/schema-index.json +28 -0
- package/dist/schemas/session-start.schema.json +30 -0
- package/dist/schemas/smoke-evidence-summary.schema.json +1 -1
- package/dist/services/capability-registry.js +115 -0
- package/dist/services/dashboard-bootstrap.js +3 -3
- package/dist/services/evidence-summary.js +66 -0
- package/dist/services/package-recycle.js +709 -0
- package/dist/services/release-closeout.js +136 -0
- package/dist/task/task-finalize.js +36 -5
- package/dist/task/task-lifecycle.js +6 -6
- package/package.json +1 -1
|
@@ -0,0 +1,709 @@
|
|
|
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.createPackageRecycleReport = createPackageRecycleReport;
|
|
7
|
+
exports.createPackageRecycleDryRunReport = createPackageRecycleDryRunReport;
|
|
8
|
+
exports.createPackageRecycleExecuteReport = createPackageRecycleExecuteReport;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const schema_1 = require("../core/schema");
|
|
14
|
+
const smoke_evidence_1 = require("./smoke-evidence");
|
|
15
|
+
const DEFAULT_PACKAGE_SPECIFIER = 'hadara@latest';
|
|
16
|
+
function createPackageRecycleReport(options) {
|
|
17
|
+
return options.execute ? createPackageRecycleExecuteReport(options) : createPackageRecycleDryRunReport(options);
|
|
18
|
+
}
|
|
19
|
+
function createPackageRecycleDryRunReport(options) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
validateOptions(options, issues);
|
|
22
|
+
const packageInfo = createPackageInfo(options.packageSpecifier, options.expectedVersion);
|
|
23
|
+
const report = {
|
|
24
|
+
schemaVersion: 'hadara.packageRecycle.v1',
|
|
25
|
+
command: 'package.recycle',
|
|
26
|
+
ok: issues.every((issue) => issue.severity !== 'error'),
|
|
27
|
+
mode: 'dry-run',
|
|
28
|
+
readOnly: true,
|
|
29
|
+
package: packageInfo,
|
|
30
|
+
networkPolicy: createNetworkPolicy(),
|
|
31
|
+
execution: createExecutionFlags(),
|
|
32
|
+
workspace: createWorkspace(options.workspace, options.keepTemp === true),
|
|
33
|
+
steps: createPlannedSteps(packageInfo, options),
|
|
34
|
+
artifacts: createArtifacts(options),
|
|
35
|
+
privacy: createPrivacy(),
|
|
36
|
+
issues
|
|
37
|
+
};
|
|
38
|
+
(0, schema_1.assertSchema)('hadara.packageRecycle.v1', report);
|
|
39
|
+
return report;
|
|
40
|
+
}
|
|
41
|
+
function createPackageRecycleExecuteReport(options) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
validateOptions(options, issues);
|
|
44
|
+
const packageInfo = createPackageInfo(options.packageSpecifier, options.expectedVersion);
|
|
45
|
+
const workspaceSetup = prepareWorkspace(options.paths.projectRoot, options.workspace, options.keepTemp === true, issues);
|
|
46
|
+
const steps = [
|
|
47
|
+
{
|
|
48
|
+
id: 'plan-workspace',
|
|
49
|
+
label: 'Prepare disposable consumer workspace',
|
|
50
|
+
status: workspaceSetup.ok ? 'passed' : 'failed',
|
|
51
|
+
summary: workspaceSetup.ok
|
|
52
|
+
? 'Disposable consumer workspace was prepared outside the project source.'
|
|
53
|
+
: 'Disposable consumer workspace could not be prepared safely.'
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
const execution = createExecutionFlags();
|
|
57
|
+
const artifacts = createArtifacts(options);
|
|
58
|
+
const runner = options.runner ?? runCommand;
|
|
59
|
+
const timeoutMs = (options.timeoutSeconds ?? 180) * 1000;
|
|
60
|
+
try {
|
|
61
|
+
if (issues.some((issue) => issue.severity === 'error') || !workspaceSetup.ok) {
|
|
62
|
+
steps.push(...createSkippedExecutionSteps());
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
execution.npmViewExecuted = true;
|
|
66
|
+
const npmView = runner(npmCommand(), ['view', packageInfo.specifier, 'version', '--json'], {
|
|
67
|
+
cwd: workspaceSetup.path,
|
|
68
|
+
timeoutMs
|
|
69
|
+
});
|
|
70
|
+
const viewStep = commandStep('npm-view-version', 'Verify registry package version', `npm view ${packageInfo.specifier} version --json`, npmView);
|
|
71
|
+
const observedVersion = parseJsonString(npmView.stdout);
|
|
72
|
+
packageInfo.observedVersion = observedVersion;
|
|
73
|
+
if (npmView.status !== 0 || !observedVersion) {
|
|
74
|
+
failStep(viewStep, npmView.timedOut ? 'Registry version lookup timed out.' : 'Registry version lookup failed or returned no version.');
|
|
75
|
+
issues.push({
|
|
76
|
+
severity: 'error',
|
|
77
|
+
code: npmView.timedOut ? 'PACKAGE_RECYCLE_NPM_VIEW_TIMEOUT' : 'PACKAGE_RECYCLE_NPM_VIEW_FAILED',
|
|
78
|
+
message: npmView.timedOut ? 'npm view timed out during installed-package recycle.' : 'npm view failed or did not return a package version.',
|
|
79
|
+
stepId: viewStep.id
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (packageInfo.expectedVersion && packageInfo.expectedVersion !== observedVersion) {
|
|
83
|
+
failStep(viewStep, `Registry version ${observedVersion} did not match expected ${packageInfo.expectedVersion}.`);
|
|
84
|
+
issues.push({
|
|
85
|
+
severity: 'error',
|
|
86
|
+
code: 'PACKAGE_RECYCLE_VERSION_MISMATCH',
|
|
87
|
+
message: 'Registry package version did not match the expected version.',
|
|
88
|
+
stepId: viewStep.id
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
viewStep.summary = `Registry resolved package version ${observedVersion}.`;
|
|
93
|
+
}
|
|
94
|
+
steps.push(viewStep);
|
|
95
|
+
execution.npmDistTagExecuted = true;
|
|
96
|
+
const distTags = runner(npmCommand(), ['dist-tag', 'ls', packageInfo.name], {
|
|
97
|
+
cwd: workspaceSetup.path,
|
|
98
|
+
timeoutMs
|
|
99
|
+
});
|
|
100
|
+
const distTagStep = commandStep('npm-dist-tags', 'Verify registry dist-tags', `npm dist-tag ls ${packageInfo.name}`, distTags);
|
|
101
|
+
if (distTags.status !== 0) {
|
|
102
|
+
failStep(distTagStep, distTags.timedOut ? 'Registry dist-tag lookup timed out.' : 'Registry dist-tag lookup failed.');
|
|
103
|
+
issues.push({
|
|
104
|
+
severity: 'error',
|
|
105
|
+
code: distTags.timedOut ? 'PACKAGE_RECYCLE_DIST_TAG_TIMEOUT' : 'PACKAGE_RECYCLE_DIST_TAG_FAILED',
|
|
106
|
+
message: distTags.timedOut ? 'npm dist-tag lookup timed out.' : 'npm dist-tag lookup failed.',
|
|
107
|
+
stepId: distTagStep.id
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
packageInfo.distTags = parseDistTags(distTags.stdout);
|
|
112
|
+
packageInfo.latestVersion = packageInfo.distTags.latest ?? null;
|
|
113
|
+
distTagStep.summary = packageInfo.latestVersion
|
|
114
|
+
? `Registry latest dist-tag points to ${packageInfo.latestVersion}.`
|
|
115
|
+
: 'Registry dist-tags were read; no latest tag was reported.';
|
|
116
|
+
}
|
|
117
|
+
steps.push(distTagStep);
|
|
118
|
+
if (!hasError(issues)) {
|
|
119
|
+
execution.packageInstallExecuted = true;
|
|
120
|
+
const prefix = node_path_1.default.join(workspaceSetup.path, 'prefix');
|
|
121
|
+
node_fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
122
|
+
const install = runner(npmCommand(), ['install', '-g', '--prefix', prefix, '--no-audit', '--no-fund', packageInfo.specifier], {
|
|
123
|
+
cwd: workspaceSetup.path,
|
|
124
|
+
timeoutMs
|
|
125
|
+
});
|
|
126
|
+
const installStep = commandStep('install-package', 'Install package from registry into isolated prefix', `npm install -g --prefix <redacted-prefix> ${packageInfo.specifier}`, install);
|
|
127
|
+
if (install.status !== 0) {
|
|
128
|
+
failStep(installStep, install.timedOut ? 'Isolated registry package install timed out.' : 'Isolated registry package install failed.');
|
|
129
|
+
issues.push({
|
|
130
|
+
severity: 'error',
|
|
131
|
+
code: install.timedOut ? 'PACKAGE_RECYCLE_INSTALL_TIMEOUT' : 'PACKAGE_RECYCLE_INSTALL_FAILED',
|
|
132
|
+
message: install.timedOut ? 'Installed-package recycle install timed out.' : 'Installed-package recycle install failed.',
|
|
133
|
+
stepId: installStep.id
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
installStep.summary = 'Package installed into an isolated temporary prefix from the registry.';
|
|
138
|
+
artifacts.push({
|
|
139
|
+
kind: 'install-tree',
|
|
140
|
+
visibility: 'temporary',
|
|
141
|
+
relativePath: 'prefix',
|
|
142
|
+
pathRedacted: true,
|
|
143
|
+
rawContentIncluded: false
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
steps.push(installStep);
|
|
147
|
+
if (install.status === 0) {
|
|
148
|
+
runInstalledSmokes({
|
|
149
|
+
runner,
|
|
150
|
+
installedBin: installedHadaraCommand(prefix),
|
|
151
|
+
installPrefix: prefix,
|
|
152
|
+
projectRoot: options.paths.projectRoot,
|
|
153
|
+
workspacePath: workspaceSetup.path,
|
|
154
|
+
timeoutMs,
|
|
155
|
+
packageInfo,
|
|
156
|
+
execution,
|
|
157
|
+
steps,
|
|
158
|
+
issues
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
steps.push(...createSkippedInstalledSteps());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
steps.push({
|
|
167
|
+
id: 'install-package',
|
|
168
|
+
label: 'Install package from registry into isolated prefix',
|
|
169
|
+
command: `npm install -g --prefix <redacted-prefix> ${packageInfo.specifier}`,
|
|
170
|
+
status: 'skipped',
|
|
171
|
+
summary: 'Skipped because registry metadata checks failed.'
|
|
172
|
+
}, ...createSkippedInstalledSteps());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
const cleanup = cleanupWorkspace(workspaceSetup, options.keepTemp === true);
|
|
178
|
+
steps.push({
|
|
179
|
+
id: 'cleanup',
|
|
180
|
+
label: 'Cleanup disposable workspace',
|
|
181
|
+
status: cleanup.ok ? 'passed' : 'failed',
|
|
182
|
+
summary: cleanup.summary
|
|
183
|
+
});
|
|
184
|
+
if (!cleanup.ok) {
|
|
185
|
+
issues.push({
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
code: 'PACKAGE_RECYCLE_CLEANUP_FAILED',
|
|
188
|
+
message: 'Installed-package recycle cleanup could not remove the disposable workspace.',
|
|
189
|
+
stepId: 'cleanup'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const report = {
|
|
194
|
+
schemaVersion: 'hadara.packageRecycle.v1',
|
|
195
|
+
command: 'package.recycle',
|
|
196
|
+
ok: issues.every((issue) => issue.severity !== 'error') && steps.every((step) => step.status !== 'failed'),
|
|
197
|
+
mode: 'execute',
|
|
198
|
+
readOnly: false,
|
|
199
|
+
package: packageInfo,
|
|
200
|
+
networkPolicy: createNetworkPolicy(),
|
|
201
|
+
execution,
|
|
202
|
+
workspace: {
|
|
203
|
+
kind: 'disposable',
|
|
204
|
+
displayPath: workspaceSetup.displayPath,
|
|
205
|
+
pathRedacted: true,
|
|
206
|
+
retention: options.keepTemp === true ? 'kept-temporary' : 'deleted'
|
|
207
|
+
},
|
|
208
|
+
steps,
|
|
209
|
+
artifacts,
|
|
210
|
+
privacy: createPrivacy(),
|
|
211
|
+
issues
|
|
212
|
+
};
|
|
213
|
+
if (options.attachEvidence === true && options.taskId && options.noEvidence !== true) {
|
|
214
|
+
const evidence = (0, smoke_evidence_1.attachReducedSmokeEvidence)({
|
|
215
|
+
projectRoot: options.paths.projectRoot,
|
|
216
|
+
taskId: options.taskId,
|
|
217
|
+
category: 'package-recycle',
|
|
218
|
+
kind: 'command-log',
|
|
219
|
+
summary: `Installed-package recycle ${report.ok ? 'passed' : 'failed'} with reduced public evidence.`,
|
|
220
|
+
result: report.ok ? 'passed' : 'failed',
|
|
221
|
+
report
|
|
222
|
+
});
|
|
223
|
+
report.artifacts.push(evidence.artifact);
|
|
224
|
+
report.steps.push({
|
|
225
|
+
id: 'evidence',
|
|
226
|
+
label: 'Attach reduced public evidence',
|
|
227
|
+
status: 'passed',
|
|
228
|
+
summary: 'Reduced installed-package recycle summary was attached as public evidence after redaction checks.'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
(0, schema_1.assertSchema)('hadara.packageRecycle.v1', report);
|
|
232
|
+
return report;
|
|
233
|
+
}
|
|
234
|
+
function runInstalledSmokes(input) {
|
|
235
|
+
const env = installPathEnv(input.installPrefix, input.projectRoot);
|
|
236
|
+
input.execution.installedVersionExecuted = true;
|
|
237
|
+
const version = input.runner(input.installedBin, ['version', '--json'], {
|
|
238
|
+
cwd: input.workspacePath,
|
|
239
|
+
timeoutMs: input.timeoutMs,
|
|
240
|
+
env
|
|
241
|
+
});
|
|
242
|
+
const versionStep = commandStep('installed-version', 'Verify installed CLI version', 'hadara version --json', version);
|
|
243
|
+
const packageVersion = parsePackageVersion(version.stdout);
|
|
244
|
+
if (version.status !== 0 || !packageVersion) {
|
|
245
|
+
failStep(versionStep, version.timedOut ? 'Installed CLI version check timed out.' : 'Installed CLI version check failed or returned no packageVersion.');
|
|
246
|
+
input.issues.push({
|
|
247
|
+
severity: 'error',
|
|
248
|
+
code: version.timedOut ? 'PACKAGE_RECYCLE_INSTALLED_VERSION_TIMEOUT' : 'PACKAGE_RECYCLE_INSTALLED_VERSION_FAILED',
|
|
249
|
+
message: version.timedOut ? 'Installed hadara version check timed out.' : 'Installed hadara version check failed or returned no packageVersion.',
|
|
250
|
+
stepId: versionStep.id
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (input.packageInfo.expectedVersion && input.packageInfo.expectedVersion !== packageVersion) {
|
|
254
|
+
failStep(versionStep, `Installed CLI version ${packageVersion} did not match expected ${input.packageInfo.expectedVersion}.`);
|
|
255
|
+
input.issues.push({
|
|
256
|
+
severity: 'error',
|
|
257
|
+
code: 'PACKAGE_RECYCLE_INSTALLED_VERSION_MISMATCH',
|
|
258
|
+
message: 'Installed package version did not match the expected version.',
|
|
259
|
+
stepId: versionStep.id
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
versionStep.summary = `Installed CLI reports packageVersion ${packageVersion}.`;
|
|
264
|
+
}
|
|
265
|
+
input.steps.push(versionStep);
|
|
266
|
+
input.execution.lifecycleHelpExecuted = true;
|
|
267
|
+
pushJsonSmokeStep(input, {
|
|
268
|
+
id: 'help-lifecycle',
|
|
269
|
+
label: 'Verify lifecycle help from installed CLI',
|
|
270
|
+
command: 'hadara help lifecycle --json',
|
|
271
|
+
args: ['help', 'lifecycle', '--json']
|
|
272
|
+
});
|
|
273
|
+
const disposableProject = node_path_1.default.join(input.workspacePath, 'consumer-project');
|
|
274
|
+
node_fs_1.default.mkdirSync(disposableProject, { recursive: true });
|
|
275
|
+
input.execution.initExecuted = true;
|
|
276
|
+
pushJsonSmokeStep(input, {
|
|
277
|
+
id: 'init-project',
|
|
278
|
+
label: 'Initialize disposable project with installed CLI',
|
|
279
|
+
command: 'hadara init --json',
|
|
280
|
+
args: ['init', '--json'],
|
|
281
|
+
cwd: disposableProject
|
|
282
|
+
});
|
|
283
|
+
input.execution.taskLifecycleExecuted = true;
|
|
284
|
+
const createTask = input.runner(input.installedBin, ['task', 'create', 'Installed package recycle smoke', '--json'], {
|
|
285
|
+
cwd: disposableProject,
|
|
286
|
+
timeoutMs: input.timeoutMs,
|
|
287
|
+
env
|
|
288
|
+
});
|
|
289
|
+
const createTaskStep = commandStep('task-create', 'Create disposable task with installed CLI', 'hadara task create <title> --json', createTask);
|
|
290
|
+
const taskId = parseTaskId(createTask.stdout);
|
|
291
|
+
if (!isOkJson(createTask) || !taskId) {
|
|
292
|
+
failStep(createTaskStep, createTask.timedOut ? 'Installed task create timed out.' : 'Installed task create failed or returned no task id.');
|
|
293
|
+
input.issues.push({
|
|
294
|
+
severity: 'error',
|
|
295
|
+
code: createTask.timedOut ? 'PACKAGE_RECYCLE_TASK_CREATE_TIMEOUT' : 'PACKAGE_RECYCLE_TASK_CREATE_FAILED',
|
|
296
|
+
message: createTask.timedOut ? 'Installed task create timed out.' : 'Installed task create failed or returned no task id.',
|
|
297
|
+
stepId: createTaskStep.id
|
|
298
|
+
});
|
|
299
|
+
input.steps.push(createTaskStep);
|
|
300
|
+
input.steps.push(skippedStep('task-lifecycle', 'Verify task lifecycle read model', 'hadara task lifecycle --task <task-id> --json', 'Skipped because task creation failed.'));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
createTaskStep.summary = `Disposable task ${taskId} was created.`;
|
|
304
|
+
input.steps.push(createTaskStep);
|
|
305
|
+
pushJsonSmokeStep(input, {
|
|
306
|
+
id: 'task-lifecycle',
|
|
307
|
+
label: 'Verify task lifecycle read model',
|
|
308
|
+
command: 'hadara task lifecycle --task <task-id> --json',
|
|
309
|
+
args: ['task', 'lifecycle', '--task', taskId, '--json'],
|
|
310
|
+
cwd: disposableProject
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
input.execution.contextSmokeExecuted = true;
|
|
314
|
+
pushJsonSmokeStep(input, {
|
|
315
|
+
id: 'context-graph',
|
|
316
|
+
label: 'Verify context graph read model',
|
|
317
|
+
command: 'hadara context graph --json',
|
|
318
|
+
args: ['context', 'graph', '--json'],
|
|
319
|
+
cwd: disposableProject
|
|
320
|
+
});
|
|
321
|
+
pushJsonSmokeStep(input, {
|
|
322
|
+
id: 'context-pack',
|
|
323
|
+
label: 'Verify context pack read model',
|
|
324
|
+
command: 'hadara context pack --json',
|
|
325
|
+
args: ['context', 'pack', '--json'],
|
|
326
|
+
cwd: disposableProject
|
|
327
|
+
});
|
|
328
|
+
pushJsonSmokeStep(input, {
|
|
329
|
+
id: 'context-slice',
|
|
330
|
+
label: 'Verify context slice raw adapter',
|
|
331
|
+
command: 'hadara context slice --path README.md --from 1 --to 20 --json',
|
|
332
|
+
args: ['context', 'slice', '--path', 'README.md', '--from', '1', '--to', '20', '--json'],
|
|
333
|
+
cwd: disposableProject
|
|
334
|
+
});
|
|
335
|
+
pushJsonSmokeStep(input, {
|
|
336
|
+
id: 'session-start',
|
|
337
|
+
label: 'Verify session start read model',
|
|
338
|
+
command: 'hadara session start --task <task-id> --json',
|
|
339
|
+
args: ['session', 'start', ...(taskId ? ['--task', taskId] : []), '--json'],
|
|
340
|
+
cwd: disposableProject
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function pushJsonSmokeStep(input, step) {
|
|
344
|
+
const result = input.runner(input.installedBin, step.args, {
|
|
345
|
+
cwd: step.cwd ?? input.workspacePath,
|
|
346
|
+
timeoutMs: input.timeoutMs,
|
|
347
|
+
env: installPathEnv(input.installPrefix, input.projectRoot)
|
|
348
|
+
});
|
|
349
|
+
const reportStep = commandStep(step.id, step.label, step.command, result);
|
|
350
|
+
if (!isOkJson(result)) {
|
|
351
|
+
failStep(reportStep, result.timedOut ? `${step.label} timed out.` : `${step.label} failed or returned non-ok JSON.`);
|
|
352
|
+
input.issues.push({
|
|
353
|
+
severity: 'error',
|
|
354
|
+
code: `PACKAGE_RECYCLE_${step.id.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_FAILED`,
|
|
355
|
+
message: `${step.label} failed during installed-package recycle.`,
|
|
356
|
+
stepId: step.id
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
reportStep.summary = `${step.label} returned an ok JSON report.`;
|
|
361
|
+
}
|
|
362
|
+
input.steps.push(reportStep);
|
|
363
|
+
}
|
|
364
|
+
function createPackageInfo(packageSpecifier, expectedVersion) {
|
|
365
|
+
const specifier = packageSpecifier ?? DEFAULT_PACKAGE_SPECIFIER;
|
|
366
|
+
return {
|
|
367
|
+
specifier,
|
|
368
|
+
name: packageNameFromSpecifier(specifier),
|
|
369
|
+
expectedVersion: expectedVersion ?? null,
|
|
370
|
+
observedVersion: null,
|
|
371
|
+
latestVersion: null,
|
|
372
|
+
distTags: {}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function createExecutionFlags() {
|
|
376
|
+
return {
|
|
377
|
+
npmViewExecuted: false,
|
|
378
|
+
npmDistTagExecuted: false,
|
|
379
|
+
packageInstallExecuted: false,
|
|
380
|
+
installedVersionExecuted: false,
|
|
381
|
+
lifecycleHelpExecuted: false,
|
|
382
|
+
initExecuted: false,
|
|
383
|
+
taskLifecycleExecuted: false,
|
|
384
|
+
contextSmokeExecuted: false,
|
|
385
|
+
releaseMutationExecuted: false,
|
|
386
|
+
publishExecuted: false
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function createNetworkPolicy() {
|
|
390
|
+
return {
|
|
391
|
+
mode: 'environment-inherited',
|
|
392
|
+
enforced: false,
|
|
393
|
+
notes: ['Installed-package recycle uses npm registry/network access only in explicit --execute mode.']
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function createPrivacy() {
|
|
397
|
+
return {
|
|
398
|
+
rawLogsIncluded: false,
|
|
399
|
+
rawPackageContentsIncluded: false,
|
|
400
|
+
privatePathsIncluded: false,
|
|
401
|
+
environmentSecretsIncluded: false,
|
|
402
|
+
privateStorePathsIncluded: false
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function createWorkspace(workspace, keepTemp) {
|
|
406
|
+
return {
|
|
407
|
+
kind: 'disposable',
|
|
408
|
+
displayPath: workspace ? '<redacted-disposable-workspace>' : '<system-temp>/hadara-package-recycle-*',
|
|
409
|
+
pathRedacted: true,
|
|
410
|
+
retention: keepTemp ? 'kept-temporary' : 'deleted'
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function createArtifacts(options) {
|
|
414
|
+
const artifacts = [
|
|
415
|
+
{
|
|
416
|
+
kind: 'summary',
|
|
417
|
+
visibility: 'temporary',
|
|
418
|
+
pathRedacted: true,
|
|
419
|
+
rawContentIncluded: false
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
if (options.attachEvidence === true && options.taskId && options.noEvidence !== true) {
|
|
423
|
+
artifacts.push({
|
|
424
|
+
kind: 'summary',
|
|
425
|
+
visibility: 'public',
|
|
426
|
+
evidencePath: `tasks/${options.taskId}-*/artifacts/package-recycle/summary.json`,
|
|
427
|
+
rawContentIncluded: false
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return artifacts;
|
|
431
|
+
}
|
|
432
|
+
function createPlannedSteps(packageInfo, options) {
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
id: 'plan-workspace',
|
|
436
|
+
label: 'Plan disposable consumer workspace',
|
|
437
|
+
status: 'planned',
|
|
438
|
+
summary: 'Would create an isolated install prefix and disposable initialized HADARA project.'
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'npm-view-version',
|
|
442
|
+
label: 'Verify registry package version',
|
|
443
|
+
command: `npm view ${packageInfo.specifier} version --json`,
|
|
444
|
+
status: 'planned',
|
|
445
|
+
summary: options.expectedVersion
|
|
446
|
+
? `Would verify registry version equals ${options.expectedVersion}.`
|
|
447
|
+
: 'Would read the registry package version.'
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: 'npm-dist-tags',
|
|
451
|
+
label: 'Verify registry dist-tags',
|
|
452
|
+
command: `npm dist-tag ls ${packageInfo.name}`,
|
|
453
|
+
status: 'planned',
|
|
454
|
+
summary: 'Would read npm dist-tags, including latest.'
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: 'install-package',
|
|
458
|
+
label: 'Install package from registry into isolated prefix',
|
|
459
|
+
command: `npm install -g --prefix <redacted-prefix> ${packageInfo.specifier}`,
|
|
460
|
+
status: 'planned',
|
|
461
|
+
summary: 'Would install the published package into an isolated prefix.'
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: 'installed-version',
|
|
465
|
+
label: 'Verify installed CLI version',
|
|
466
|
+
command: 'hadara version --json',
|
|
467
|
+
status: 'planned',
|
|
468
|
+
summary: 'Would verify the installed CLI reports the expected packageVersion.'
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: 'help-lifecycle',
|
|
472
|
+
label: 'Verify lifecycle help from installed CLI',
|
|
473
|
+
command: 'hadara help lifecycle --json',
|
|
474
|
+
status: 'planned',
|
|
475
|
+
summary: 'Would verify installed lifecycle guidance is available.'
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: 'init-project',
|
|
479
|
+
label: 'Initialize disposable project with installed CLI',
|
|
480
|
+
command: 'hadara init --json',
|
|
481
|
+
status: 'planned',
|
|
482
|
+
summary: 'Would initialize a disposable consumer project.'
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'task-create',
|
|
486
|
+
label: 'Create disposable task with installed CLI',
|
|
487
|
+
command: 'hadara task create <title> --json',
|
|
488
|
+
status: 'planned',
|
|
489
|
+
summary: 'Would create a task capsule in the disposable project.'
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'task-lifecycle',
|
|
493
|
+
label: 'Verify task lifecycle read model',
|
|
494
|
+
command: 'hadara task lifecycle --task <task-id> --json',
|
|
495
|
+
status: 'planned',
|
|
496
|
+
summary: 'Would verify the task lifecycle read model for the disposable task.'
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: 'context-graph',
|
|
500
|
+
label: 'Verify context graph read model',
|
|
501
|
+
command: 'hadara context graph --json',
|
|
502
|
+
status: 'planned',
|
|
503
|
+
summary: 'Would verify context graph on the initialized project.'
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: 'context-pack',
|
|
507
|
+
label: 'Verify context pack read model',
|
|
508
|
+
command: 'hadara context pack --json',
|
|
509
|
+
status: 'planned',
|
|
510
|
+
summary: 'Would verify context pack on the initialized project.'
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: 'context-slice',
|
|
514
|
+
label: 'Verify context slice raw adapter',
|
|
515
|
+
command: 'hadara context slice --path README.md --from 1 --to 20 --json',
|
|
516
|
+
status: 'planned',
|
|
517
|
+
summary: 'Would verify bounded raw context slicing on initialized docs.'
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: 'session-start',
|
|
521
|
+
label: 'Verify session start read model',
|
|
522
|
+
command: 'hadara session start --task <task-id> --json',
|
|
523
|
+
status: 'planned',
|
|
524
|
+
summary: 'Would verify bounded session start on the disposable task.'
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 'cleanup',
|
|
528
|
+
label: 'Cleanup disposable workspace',
|
|
529
|
+
status: 'planned',
|
|
530
|
+
summary: 'Would remove the disposable consumer workspace unless --keep-temp is set.'
|
|
531
|
+
}
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
function createSkippedExecutionSteps() {
|
|
535
|
+
return [
|
|
536
|
+
skippedStep('npm-view-version', 'Verify registry package version', `npm view ${DEFAULT_PACKAGE_SPECIFIER} version --json`, 'Skipped because option validation or workspace preparation failed.'),
|
|
537
|
+
skippedStep('npm-dist-tags', 'Verify registry dist-tags', 'npm dist-tag ls hadara', 'Skipped because option validation or workspace preparation failed.'),
|
|
538
|
+
skippedStep('install-package', 'Install package from registry into isolated prefix', `npm install -g --prefix <redacted-prefix> ${DEFAULT_PACKAGE_SPECIFIER}`, 'Skipped because option validation or workspace preparation failed.'),
|
|
539
|
+
...createSkippedInstalledSteps()
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
function createSkippedInstalledSteps() {
|
|
543
|
+
return [
|
|
544
|
+
skippedStep('installed-version', 'Verify installed CLI version', 'hadara version --json', 'Skipped because package install failed.'),
|
|
545
|
+
skippedStep('help-lifecycle', 'Verify lifecycle help from installed CLI', 'hadara help lifecycle --json', 'Skipped because package install failed.'),
|
|
546
|
+
skippedStep('init-project', 'Initialize disposable project with installed CLI', 'hadara init --json', 'Skipped because package install failed.'),
|
|
547
|
+
skippedStep('task-create', 'Create disposable task with installed CLI', 'hadara task create <title> --json', 'Skipped because package install failed.'),
|
|
548
|
+
skippedStep('task-lifecycle', 'Verify task lifecycle read model', 'hadara task lifecycle --task <task-id> --json', 'Skipped because package install failed.'),
|
|
549
|
+
skippedStep('context-graph', 'Verify context graph read model', 'hadara context graph --json', 'Skipped because package install failed.'),
|
|
550
|
+
skippedStep('context-pack', 'Verify context pack read model', 'hadara context pack --json', 'Skipped because package install failed.'),
|
|
551
|
+
skippedStep('context-slice', 'Verify context slice raw adapter', 'hadara context slice --path README.md --from 1 --to 20 --json', 'Skipped because package install failed.'),
|
|
552
|
+
skippedStep('session-start', 'Verify session start read model', 'hadara session start --task <task-id> --json', 'Skipped because package install failed.')
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
function skippedStep(id, label, command, summary) {
|
|
556
|
+
return { id, label, command, status: 'skipped', summary };
|
|
557
|
+
}
|
|
558
|
+
function prepareWorkspace(projectRoot, workspace, keepTemp, issues) {
|
|
559
|
+
const target = workspace ? node_path_1.default.resolve(projectRoot, workspace) : node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'hadara-package-recycle-'));
|
|
560
|
+
if (workspace) {
|
|
561
|
+
const relative = node_path_1.default.relative(projectRoot, target);
|
|
562
|
+
if (!relative || relative.startsWith('..') || node_path_1.default.isAbsolute(relative)) {
|
|
563
|
+
issues.push({
|
|
564
|
+
severity: 'error',
|
|
565
|
+
code: 'PACKAGE_RECYCLE_WORKSPACE_OUTSIDE_PROJECT',
|
|
566
|
+
message: 'Custom package recycle workspace must stay inside the project root unless the default system temp workspace is used.',
|
|
567
|
+
stepId: 'plan-workspace'
|
|
568
|
+
});
|
|
569
|
+
return { ok: false, path: target, displayPath: '<redacted-disposable-workspace>' };
|
|
570
|
+
}
|
|
571
|
+
node_fs_1.default.mkdirSync(target, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
void keepTemp;
|
|
574
|
+
return { ok: true, path: target, displayPath: workspace ? '<redacted-disposable-workspace>' : '<system-temp>/hadara-package-recycle-*' };
|
|
575
|
+
}
|
|
576
|
+
function cleanupWorkspace(workspace, keepTemp) {
|
|
577
|
+
if (!workspace.ok)
|
|
578
|
+
return { ok: true, summary: 'No disposable workspace cleanup was needed.' };
|
|
579
|
+
if (keepTemp)
|
|
580
|
+
return { ok: true, summary: 'Disposable workspace was kept because --keep-temp was set.' };
|
|
581
|
+
try {
|
|
582
|
+
node_fs_1.default.rmSync(workspace.path, { recursive: true, force: true });
|
|
583
|
+
return { ok: true, summary: 'Disposable workspace was removed.' };
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return { ok: false, summary: 'Disposable workspace cleanup failed.' };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function validateOptions(options, issues) {
|
|
590
|
+
if (options.taskId && !/^T-\d{4}$/.test(options.taskId)) {
|
|
591
|
+
issues.push({
|
|
592
|
+
severity: 'error',
|
|
593
|
+
code: 'PACKAGE_RECYCLE_TASK_ID_INVALID',
|
|
594
|
+
message: 'Task id must use T-XXXX format.'
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (options.timeoutSeconds !== undefined && options.timeoutSeconds < 1) {
|
|
598
|
+
issues.push({
|
|
599
|
+
severity: 'error',
|
|
600
|
+
code: 'PACKAGE_RECYCLE_TIMEOUT_INVALID',
|
|
601
|
+
message: 'Timeout must be at least one second.'
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function commandStep(id, label, command, result) {
|
|
606
|
+
return {
|
|
607
|
+
id,
|
|
608
|
+
label,
|
|
609
|
+
command,
|
|
610
|
+
status: result.status === 0 ? 'passed' : 'failed',
|
|
611
|
+
exitCode: result.status,
|
|
612
|
+
...(result.elapsedMs === undefined ? {} : { elapsedMs: result.elapsedMs }),
|
|
613
|
+
summary: result.status === 0 ? `${label} completed.` : `${label} failed.`
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function failStep(step, summary) {
|
|
617
|
+
step.status = 'failed';
|
|
618
|
+
step.summary = summary;
|
|
619
|
+
}
|
|
620
|
+
function runCommand(command, args, options) {
|
|
621
|
+
const start = Date.now();
|
|
622
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
623
|
+
cwd: options.cwd,
|
|
624
|
+
env: options.env ?? process.env,
|
|
625
|
+
encoding: 'utf8',
|
|
626
|
+
timeout: options.timeoutMs,
|
|
627
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
628
|
+
});
|
|
629
|
+
return {
|
|
630
|
+
status: result.status,
|
|
631
|
+
signal: result.signal,
|
|
632
|
+
stdout: result.stdout ?? '',
|
|
633
|
+
stderr: result.stderr ?? '',
|
|
634
|
+
elapsedMs: Date.now() - start,
|
|
635
|
+
timedOut: result.error?.name === 'Error' && /timed out/i.test(result.error.message)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function npmCommand() {
|
|
639
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
640
|
+
}
|
|
641
|
+
function installedHadaraCommand(prefix) {
|
|
642
|
+
return process.platform === 'win32' ? node_path_1.default.join(prefix, 'hadara.cmd') : node_path_1.default.join(prefix, 'bin', 'hadara');
|
|
643
|
+
}
|
|
644
|
+
function installPathEnv(prefix, projectRoot) {
|
|
645
|
+
const bin = process.platform === 'win32' ? prefix : node_path_1.default.join(prefix, 'bin');
|
|
646
|
+
return {
|
|
647
|
+
...process.env,
|
|
648
|
+
PATH: `${bin}${node_path_1.default.delimiter}${process.env.PATH ?? ''}`,
|
|
649
|
+
HADARA_PROJECT_ROOT: projectRoot
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function packageNameFromSpecifier(specifier) {
|
|
653
|
+
if (specifier.startsWith('@')) {
|
|
654
|
+
const parts = specifier.split('@');
|
|
655
|
+
return `@${parts[1]}`;
|
|
656
|
+
}
|
|
657
|
+
return specifier.split('@')[0] || 'hadara';
|
|
658
|
+
}
|
|
659
|
+
function parseJsonString(stdout) {
|
|
660
|
+
try {
|
|
661
|
+
const parsed = JSON.parse(stdout);
|
|
662
|
+
return typeof parsed === 'string' ? parsed : null;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
const trimmed = stdout.trim();
|
|
666
|
+
return trimmed.length > 0 ? trimmed.replace(/^"|"$/g, '') : null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function parseDistTags(stdout) {
|
|
670
|
+
const tags = {};
|
|
671
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
672
|
+
const match = line.trim().match(/^([^:]+):\s+(.+)$/);
|
|
673
|
+
if (match)
|
|
674
|
+
tags[match[1]] = match[2];
|
|
675
|
+
}
|
|
676
|
+
return tags;
|
|
677
|
+
}
|
|
678
|
+
function parsePackageVersion(stdout) {
|
|
679
|
+
try {
|
|
680
|
+
const parsed = JSON.parse(stdout);
|
|
681
|
+
return typeof parsed?.packageVersion === 'string' ? parsed.packageVersion : null;
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function parseTaskId(stdout) {
|
|
688
|
+
try {
|
|
689
|
+
const parsed = JSON.parse(stdout);
|
|
690
|
+
return typeof parsed?.task?.id === 'string' ? parsed.task.id : typeof parsed?.id === 'string' ? parsed.id : null;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function isOkJson(result) {
|
|
697
|
+
if (result.status !== 0)
|
|
698
|
+
return false;
|
|
699
|
+
try {
|
|
700
|
+
const parsed = JSON.parse(result.stdout);
|
|
701
|
+
return parsed?.ok !== false;
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return result.stdout.trim() === '';
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function hasError(issues) {
|
|
708
|
+
return issues.some((issue) => issue.severity === 'error');
|
|
709
|
+
}
|