hadara 0.1.0-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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/agent/evidence.js +50 -0
- package/dist/agent/loop.js +124 -0
- package/dist/cli/args.js +70 -0
- package/dist/cli/dashboard.js +185 -0
- package/dist/cli/debt.js +41 -0
- package/dist/cli/doctor.js +68 -0
- package/dist/cli/errors.js +58 -0
- package/dist/cli/evidence-json.js +75 -0
- package/dist/cli/evidence.js +80 -0
- package/dist/cli/handoff.js +16 -0
- package/dist/cli/harness.js +57 -0
- package/dist/cli/hermes-json.js +31 -0
- package/dist/cli/hermes.js +28 -0
- package/dist/cli/init.js +142 -0
- package/dist/cli/install.js +34 -0
- package/dist/cli/main.js +216 -0
- package/dist/cli/mcp.js +15 -0
- package/dist/cli/package-smoke.js +37 -0
- package/dist/cli/policy-json.js +22 -0
- package/dist/cli/policy.js +43 -0
- package/dist/cli/release-artifact.js +47 -0
- package/dist/cli/release-dry-run.js +24 -0
- package/dist/cli/release-gate.js +28 -0
- package/dist/cli/release-publish.js +41 -0
- package/dist/cli/run-scaffold.js +68 -0
- package/dist/cli/run-state.js +41 -0
- package/dist/cli/run.js +191 -0
- package/dist/cli/smoke.js +58 -0
- package/dist/cli/status-json.js +6 -0
- package/dist/cli/status.js +26 -0
- package/dist/cli/task-json.js +8 -0
- package/dist/cli/task.js +64 -0
- package/dist/cli/tools.js +25 -0
- package/dist/cli/tui.js +72 -0
- package/dist/cli/write-preflight.js +27 -0
- package/dist/core/audit.js +41 -0
- package/dist/core/events.js +63 -0
- package/dist/core/fs.js +44 -0
- package/dist/core/paths.js +59 -0
- package/dist/core/redaction.js +178 -0
- package/dist/core/schema.js +253 -0
- package/dist/core/workspace.js +47 -0
- package/dist/evidence/evidence.js +170 -0
- package/dist/evidence/private-manifest.js +101 -0
- package/dist/handoff/handoff.js +49 -0
- package/dist/harness/replay.js +200 -0
- package/dist/harness/validate.js +465 -0
- package/dist/hermes/context-export.js +104 -0
- package/dist/index.js +29 -0
- package/dist/mcp/server.js +104 -0
- package/dist/mcp/tool-dispatch.js +159 -0
- package/dist/mcp/tool-registry.js +150 -0
- package/dist/mcp/tool-schemas.js +18 -0
- package/dist/policy/command-risk.js +39 -0
- package/dist/policy/permission-matrix.js +42 -0
- package/dist/policy/policy.js +20 -0
- package/dist/policy/preflight.js +47 -0
- package/dist/policy/presets.js +24 -0
- package/dist/policy/tokenizer.js +53 -0
- package/dist/providers/fallback-executor.js +46 -0
- package/dist/providers/mock-provider.js +49 -0
- package/dist/providers/provider-contract.js +2 -0
- package/dist/providers/provider-preparation.js +220 -0
- package/dist/providers/scripted-provider.js +69 -0
- package/dist/schemas/active-run-projection.schema.json +73 -0
- package/dist/schemas/active-run-resume.schema.json +68 -0
- package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
- package/dist/schemas/context-export.schema.json +35 -0
- package/dist/schemas/event.schema.json +17 -0
- package/dist/schemas/evidence-list.schema.json +49 -0
- package/dist/schemas/feature-smoke.schema.json +67 -0
- package/dist/schemas/install-plan.schema.json +93 -0
- package/dist/schemas/package-smoke.schema.json +130 -0
- package/dist/schemas/private-evidence.schema.json +48 -0
- package/dist/schemas/provider-call.schema.json +42 -0
- package/dist/schemas/provider-config.schema.json +43 -0
- package/dist/schemas/release-artifact-manifest.schema.json +55 -0
- package/dist/schemas/release-artifact.schema.json +140 -0
- package/dist/schemas/release-dry-run.schema.json +141 -0
- package/dist/schemas/release-gate.schema.json +42 -0
- package/dist/schemas/release-publish.schema.json +114 -0
- package/dist/schemas/schema-index.json +145 -0
- package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
- package/dist/schemas/tools-list.schema.json +78 -0
- package/dist/schemas/write-preflight.schema.json +47 -0
- package/dist/services/active-run-state.js +215 -0
- package/dist/services/capability-registry.js +540 -0
- package/dist/services/clean-checkout-smoke.js +393 -0
- package/dist/services/evidence-list.js +136 -0
- package/dist/services/feature-smoke.js +155 -0
- package/dist/services/harness-service.js +7 -0
- package/dist/services/install-plan.js +233 -0
- package/dist/services/operational-debt.js +767 -0
- package/dist/services/operations-status-service.js +195 -0
- package/dist/services/package-smoke.js +676 -0
- package/dist/services/policy-service.js +25 -0
- package/dist/services/project-read-model.js +101 -0
- package/dist/services/release-artifact-evidence.js +77 -0
- package/dist/services/release-artifact.js +351 -0
- package/dist/services/release-dry-run.js +253 -0
- package/dist/services/release-evidence.js +138 -0
- package/dist/services/release-publish.js +163 -0
- package/dist/services/smoke-evidence.js +104 -0
- package/dist/services/task-read-model.js +125 -0
- package/dist/services/tools-list.js +26 -0
- package/dist/services/write-preflight.js +240 -0
- package/dist/task/task-capsule.js +121 -0
- package/dist/tools/fake-shell.js +56 -0
- package/dist/tui/cache.js +341 -0
- package/dist/tui/constants.js +44 -0
- package/dist/tui/layout.js +140 -0
- package/dist/tui/markdown.js +238 -0
- package/dist/tui/read-model-worker.js +24 -0
- package/dist/tui/read-model.js +502 -0
- package/dist/tui/snapshot.js +434 -0
- package/dist/tui/state.js +229 -0
- package/dist/tui/terminal.js +475 -0
- package/dist/tui/theme.js +86 -0
- package/package.json +16 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPolicyCheckReport = createPolicyCheckReport;
|
|
4
|
+
exports.createPolicyEvaluateReport = createPolicyEvaluateReport;
|
|
5
|
+
const preflight_1 = require("../policy/preflight");
|
|
6
|
+
const policy_1 = require("../policy/policy");
|
|
7
|
+
function createPolicyCheckReport(command, mode = 'assisted') {
|
|
8
|
+
const normalizedMode = (0, policy_1.parsePermissionMode)(mode);
|
|
9
|
+
const shell = (0, policy_1.tokenizeShellCommand)(command);
|
|
10
|
+
const decision = (0, policy_1.classifyShellCommand)(command, normalizedMode);
|
|
11
|
+
return {
|
|
12
|
+
schemaVersion: 'hadara.policy.check-shell.v1',
|
|
13
|
+
command: 'policy.check-shell',
|
|
14
|
+
ok: decision.action !== 'deny',
|
|
15
|
+
input: {
|
|
16
|
+
mode: normalizedMode,
|
|
17
|
+
command
|
|
18
|
+
},
|
|
19
|
+
shell,
|
|
20
|
+
decision
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function createPolicyEvaluateReport(command, mode = 'assisted') {
|
|
24
|
+
return (0, preflight_1.createShellExecutionPreflight)(command, (0, policy_1.parsePermissionMode)(mode));
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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.readProjectFile = readProjectFile;
|
|
7
|
+
exports.readProjectSources = readProjectSources;
|
|
8
|
+
exports.createHandoffReadReport = createHandoffReadReport;
|
|
9
|
+
exports.createProjectStateReadReport = createProjectStateReadReport;
|
|
10
|
+
exports.tailLines = tailLines;
|
|
11
|
+
exports.extractSection = extractSection;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
function readProjectFile(projectRoot, relativePath) {
|
|
15
|
+
const filePath = node_path_1.default.join(projectRoot, relativePath);
|
|
16
|
+
const exists = node_fs_1.default.existsSync(filePath);
|
|
17
|
+
return {
|
|
18
|
+
path: relativePath,
|
|
19
|
+
exists,
|
|
20
|
+
content: exists ? node_fs_1.default.readFileSync(filePath, 'utf8') : ''
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function readProjectSources(projectRoot) {
|
|
24
|
+
return {
|
|
25
|
+
projectState: readProjectFile(projectRoot, 'docs/PROJECT_STATE.md'),
|
|
26
|
+
handoff: readProjectFile(projectRoot, 'docs/AGENT_HANDOFF.md'),
|
|
27
|
+
taskBoard: readProjectFile(projectRoot, 'docs/TASK_BOARD.md'),
|
|
28
|
+
developmentSlices: readProjectFile(projectRoot, 'docs/DEVELOPMENT_SLICES.md'),
|
|
29
|
+
validationHistory: readProjectFile(projectRoot, 'docs/VALIDATION_HISTORY.md')
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createHandoffReadReport(projectRoot, options) {
|
|
33
|
+
const sources = readProjectSources(projectRoot);
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: 'hadara.handoff.read.v1',
|
|
36
|
+
command: 'handoff.read',
|
|
37
|
+
ok: true,
|
|
38
|
+
handoff: {
|
|
39
|
+
current: sources.handoff.content,
|
|
40
|
+
history: options.includeHistory ? tailLines(readProjectFile(projectRoot, 'docs/HANDOFF_HISTORY.md').content, options.historyLimit) : null,
|
|
41
|
+
validationHistory: options.includeHistory ? tailLines(sources.validationHistory.content, options.historyLimit) : null
|
|
42
|
+
},
|
|
43
|
+
issues: []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createProjectStateReadReport(projectRoot, options) {
|
|
47
|
+
const sources = readProjectSources(projectRoot);
|
|
48
|
+
if (options.summaryOnly) {
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 'hadara.project.state.read.v1',
|
|
51
|
+
command: 'project.state.read',
|
|
52
|
+
ok: true,
|
|
53
|
+
summary: {
|
|
54
|
+
projectState: extractSection(sources.projectState.content, '## Current Status'),
|
|
55
|
+
taskBoardTail: tailLines(sources.taskBoard.content, 20),
|
|
56
|
+
developmentSlicesTail: tailLines(sources.developmentSlices.content, 12)
|
|
57
|
+
},
|
|
58
|
+
issues: []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!options.includeDocuments) {
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: 'hadara.project.state.read.v1',
|
|
64
|
+
command: 'project.state.read',
|
|
65
|
+
ok: true,
|
|
66
|
+
documents: [
|
|
67
|
+
{ path: sources.projectState.path, included: false },
|
|
68
|
+
{ path: sources.taskBoard.path, included: false },
|
|
69
|
+
{ path: sources.developmentSlices.path, included: false }
|
|
70
|
+
],
|
|
71
|
+
issues: []
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: 'hadara.project.state.read.v1',
|
|
76
|
+
command: 'project.state.read',
|
|
77
|
+
ok: true,
|
|
78
|
+
projectState: sources.projectState.content,
|
|
79
|
+
taskBoard: sources.taskBoard.content,
|
|
80
|
+
developmentSlices: sources.developmentSlices.content,
|
|
81
|
+
issues: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function tailLines(content, limit) {
|
|
85
|
+
return content
|
|
86
|
+
.split(/\r?\n/)
|
|
87
|
+
.filter((line) => line.trim().length > 0)
|
|
88
|
+
.slice(-limit)
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
|
91
|
+
function extractSection(content, heading) {
|
|
92
|
+
const match = new RegExp(`^${escapeRegExp(heading)}\\s*$`, 'm').exec(content);
|
|
93
|
+
if (!match || match.index === undefined)
|
|
94
|
+
return '';
|
|
95
|
+
const afterHeading = content.slice(match.index + match[0].length);
|
|
96
|
+
const nextHeading = afterHeading.search(/\n##\s+/);
|
|
97
|
+
return (nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading).trim();
|
|
98
|
+
}
|
|
99
|
+
function escapeRegExp(value) {
|
|
100
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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.attachReleaseArtifactEvidence = attachReleaseArtifactEvidence;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const evidence_1 = require("../evidence/evidence");
|
|
10
|
+
const fs_1 = require("../core/fs");
|
|
11
|
+
const redaction_1 = require("../core/redaction");
|
|
12
|
+
const release_dry_run_1 = require("./release-dry-run");
|
|
13
|
+
function attachReleaseArtifactEvidence(input) {
|
|
14
|
+
const taskDir = findTaskDir(input.projectRoot, input.taskId);
|
|
15
|
+
if (!taskDir)
|
|
16
|
+
throw new Error(`Task capsule not found: ${input.taskId}`);
|
|
17
|
+
const time = new Date().toISOString();
|
|
18
|
+
const reportArtifact = {
|
|
19
|
+
...input.report,
|
|
20
|
+
evidence: {
|
|
21
|
+
time,
|
|
22
|
+
taskId: input.taskId,
|
|
23
|
+
gitCommit: (0, release_dry_run_1.readCurrentGitCommit)(input.projectRoot)
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const content = JSON.stringify(reportArtifact, null, 2);
|
|
27
|
+
const policy = (0, evidence_1.createPublicEvidenceArtifactPolicyReport)(content);
|
|
28
|
+
if (policy.blocking) {
|
|
29
|
+
throw new evidence_1.EvidenceArtifactPolicyError('PUBLIC_ARTIFACT_SECRET_DETECTED', 'Public release artifact evidence contains secret-like content; reduce the report before attaching.', policy.redaction);
|
|
30
|
+
}
|
|
31
|
+
const artifactDir = node_path_1.default.join(taskDir, 'artifacts', 'release-artifact');
|
|
32
|
+
(0, fs_1.ensureDir)(artifactDir);
|
|
33
|
+
const targetPath = node_path_1.default.join(artifactDir, `${safeFilePart(time)}-report.json`);
|
|
34
|
+
node_fs_1.default.writeFileSync(targetPath, content, 'utf8');
|
|
35
|
+
const evidencePath = toPortablePath(node_path_1.default.relative(taskDir, targetPath));
|
|
36
|
+
const evidence = {
|
|
37
|
+
schemaVersion: 'hadara.evidence.v1',
|
|
38
|
+
time,
|
|
39
|
+
taskId: input.taskId,
|
|
40
|
+
kind: 'command-log',
|
|
41
|
+
summary: (0, redaction_1.redactSecrets)(input.summary.replace(/\|/g, '/')),
|
|
42
|
+
result: input.report.ok ? 'passed' : 'failed',
|
|
43
|
+
visibility: 'public',
|
|
44
|
+
evidencePath
|
|
45
|
+
};
|
|
46
|
+
appendEvidenceMarkdown(taskDir, evidence);
|
|
47
|
+
node_fs_1.default.appendFileSync(node_path_1.default.join(taskDir, 'evidence.jsonl'), `${JSON.stringify(evidence)}\n`, 'utf8');
|
|
48
|
+
return {
|
|
49
|
+
evidence,
|
|
50
|
+
artifact: {
|
|
51
|
+
kind: 'report',
|
|
52
|
+
visibility: 'public',
|
|
53
|
+
evidencePath: toPortablePath(node_path_1.default.join('tasks', node_path_1.default.basename(taskDir), evidencePath)),
|
|
54
|
+
rawContentIncluded: false
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function appendEvidenceMarkdown(taskDir, evidence) {
|
|
59
|
+
const markdownPath = node_path_1.default.join(taskDir, 'EVIDENCE.md');
|
|
60
|
+
if (!node_fs_1.default.existsSync(markdownPath)) {
|
|
61
|
+
node_fs_1.default.writeFileSync(markdownPath, '# Evidence\n\n| Time | Kind | Summary | Result |\n|---|---|---|---|\n', 'utf8');
|
|
62
|
+
}
|
|
63
|
+
node_fs_1.default.appendFileSync(markdownPath, `| ${evidence.time} | ${evidence.kind} | ${evidence.summary} (${evidence.evidencePath}) | ${evidence.result} |\n`, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
function findTaskDir(projectRoot, taskId) {
|
|
66
|
+
const tasksDir = node_path_1.default.join(projectRoot, 'tasks');
|
|
67
|
+
if (!node_fs_1.default.existsSync(tasksDir))
|
|
68
|
+
return null;
|
|
69
|
+
const entry = node_fs_1.default.readdirSync(tasksDir).find((name) => name.startsWith(`${taskId}-`));
|
|
70
|
+
return entry ? node_path_1.default.join(tasksDir, entry) : null;
|
|
71
|
+
}
|
|
72
|
+
function safeFilePart(value) {
|
|
73
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'artifact';
|
|
74
|
+
}
|
|
75
|
+
function toPortablePath(value) {
|
|
76
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
77
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
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.createReleaseArtifactReport = createReleaseArtifactReport;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
13
|
+
const schema_1 = require("../core/schema");
|
|
14
|
+
const requiredFiles = ['package.json', 'README.md', 'LICENSE', 'dist/cli/main.js'];
|
|
15
|
+
const allowedRoots = ['dist/', 'README.md', 'LICENSE', 'package.json'];
|
|
16
|
+
function createReleaseArtifactReport(options) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
validateTimeout(options.timeoutSeconds, issues);
|
|
19
|
+
if (options.execute !== true) {
|
|
20
|
+
issues.push({
|
|
21
|
+
severity: 'error',
|
|
22
|
+
code: 'RELEASE_ARTIFACT_EXECUTION_REQUIRED',
|
|
23
|
+
message: 'Release artifact building requires explicit --execute because it runs npm pack and writes artifacts.'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const packageMetadata = readPackageMetadata(options.paths.projectRoot, issues);
|
|
27
|
+
const output = prepareOutput(options.paths.projectRoot, options.output, options.keepTemp === true, issues);
|
|
28
|
+
const staging = prepareStaging(options.paths.projectRoot, issues);
|
|
29
|
+
const runner = options.runner ?? runCommand;
|
|
30
|
+
const timeoutMs = (options.timeoutSeconds ?? 120) * 1000;
|
|
31
|
+
const execution = {
|
|
32
|
+
stagingCreated: false,
|
|
33
|
+
npmPackExecuted: false,
|
|
34
|
+
checksumGenerated: false,
|
|
35
|
+
manifestGenerated: false,
|
|
36
|
+
packageContentsVerified: false,
|
|
37
|
+
publishExecuted: false,
|
|
38
|
+
githubReleaseCreated: false,
|
|
39
|
+
dockerImageBuilt: false
|
|
40
|
+
};
|
|
41
|
+
const artifacts = [];
|
|
42
|
+
const packageContents = {
|
|
43
|
+
verified: false,
|
|
44
|
+
fileCount: 0,
|
|
45
|
+
allowedRoots,
|
|
46
|
+
requiredFiles,
|
|
47
|
+
forbiddenMatches: []
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
if (!issues.some((issue) => issue.severity === 'error')) {
|
|
51
|
+
copyWhitelistedPackage(options.paths.projectRoot, staging.path, packageMetadata, issues);
|
|
52
|
+
execution.stagingCreated = !issues.some((issue) => issue.stepId === 'stage-package');
|
|
53
|
+
if (execution.stagingCreated) {
|
|
54
|
+
execution.npmPackExecuted = true;
|
|
55
|
+
const pack = runner(npmCommand(), ['pack', '--json', '--pack-destination', output.path], {
|
|
56
|
+
cwd: staging.path,
|
|
57
|
+
timeoutMs
|
|
58
|
+
});
|
|
59
|
+
const parsed = parsePackResult(pack.stdout);
|
|
60
|
+
if (pack.status !== 0 || !parsed) {
|
|
61
|
+
issues.push({
|
|
62
|
+
severity: 'error',
|
|
63
|
+
code: pack.timedOut ? 'RELEASE_ARTIFACT_NPM_PACK_TIMEOUT' : 'RELEASE_ARTIFACT_NPM_PACK_FAILED',
|
|
64
|
+
message: pack.timedOut ? 'npm pack timed out while building release artifacts.' : 'npm pack failed or did not return a reduced package result.',
|
|
65
|
+
stepId: 'npm-pack'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const verify = verifyPackageContents(parsed.files.map((file) => file.path));
|
|
70
|
+
packageContents.verified = verify.ok;
|
|
71
|
+
packageContents.fileCount = parsed.files.length;
|
|
72
|
+
packageContents.forbiddenMatches = verify.forbidden;
|
|
73
|
+
execution.packageContentsVerified = verify.ok;
|
|
74
|
+
for (const missing of verify.missingRequired) {
|
|
75
|
+
issues.push({
|
|
76
|
+
severity: 'error',
|
|
77
|
+
code: 'RELEASE_ARTIFACT_REQUIRED_FILE_MISSING',
|
|
78
|
+
message: `Release artifact package is missing required file ${missing}.`,
|
|
79
|
+
stepId: 'verify-contents'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const forbidden of verify.forbidden) {
|
|
83
|
+
issues.push({
|
|
84
|
+
severity: 'error',
|
|
85
|
+
code: 'RELEASE_ARTIFACT_FORBIDDEN_FILE_INCLUDED',
|
|
86
|
+
message: `Release artifact package includes file outside the whitelist: ${forbidden}.`,
|
|
87
|
+
stepId: 'verify-contents'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const tarballPath = node_path_1.default.join(output.path, parsed.filename);
|
|
91
|
+
const tarballHash = hashFile(tarballPath);
|
|
92
|
+
artifacts.push(createArtifact('tarball', output, parsed.filename, tarballPath, tarballHash));
|
|
93
|
+
const checksumFileName = `${parsed.filename}.sha256`;
|
|
94
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(output.path, checksumFileName), `${tarballHash} ${parsed.filename}\n`, 'utf8');
|
|
95
|
+
execution.checksumGenerated = true;
|
|
96
|
+
artifacts.push(createArtifact('checksum', output, checksumFileName, node_path_1.default.join(output.path, checksumFileName), hashFile(node_path_1.default.join(output.path, checksumFileName))));
|
|
97
|
+
const manifestFileName = `${parsed.filename}.manifest.json`;
|
|
98
|
+
const manifest = createManifest(packageMetadata, parsed, tarballHash);
|
|
99
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(output.path, manifestFileName), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
100
|
+
execution.manifestGenerated = true;
|
|
101
|
+
artifacts.push(createArtifact('manifest', output, manifestFileName, node_path_1.default.join(output.path, manifestFileName), hashFile(node_path_1.default.join(output.path, manifestFileName))));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
cleanupDirectory(staging.path, true);
|
|
108
|
+
if (output.cleanupAllowed)
|
|
109
|
+
cleanupDirectory(output.path, false);
|
|
110
|
+
}
|
|
111
|
+
const report = {
|
|
112
|
+
schemaVersion: 'hadara.releaseArtifact.v1',
|
|
113
|
+
command: 'release.artifact',
|
|
114
|
+
ok: issues.every((issue) => issue.severity !== 'error'),
|
|
115
|
+
mode: 'execute',
|
|
116
|
+
execution,
|
|
117
|
+
output: {
|
|
118
|
+
kind: output.kind,
|
|
119
|
+
displayPath: output.displayPath,
|
|
120
|
+
pathRedacted: true,
|
|
121
|
+
...(output.relativePath ? { relativePath: output.relativePath } : {}),
|
|
122
|
+
retention: output.retention
|
|
123
|
+
},
|
|
124
|
+
package: {
|
|
125
|
+
name: packageMetadata.name,
|
|
126
|
+
version: packageMetadata.version,
|
|
127
|
+
private: packageMetadata.private,
|
|
128
|
+
filesWhitelistApplied: true
|
|
129
|
+
},
|
|
130
|
+
artifacts,
|
|
131
|
+
packageContents,
|
|
132
|
+
privacy: {
|
|
133
|
+
rawLogsIncluded: false,
|
|
134
|
+
packageContentsIncluded: false,
|
|
135
|
+
privatePathsIncluded: false,
|
|
136
|
+
environmentSecretsIncluded: false,
|
|
137
|
+
privateStorePathsIncluded: false
|
|
138
|
+
},
|
|
139
|
+
issues
|
|
140
|
+
};
|
|
141
|
+
(0, schema_1.assertSchema)('hadara.releaseArtifact.v1', report);
|
|
142
|
+
return report;
|
|
143
|
+
}
|
|
144
|
+
function readPackageMetadata(projectRoot, issues) {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(projectRoot, 'package.json'), 'utf8'));
|
|
147
|
+
const bin = isRecord(parsed.bin) ? parsed.bin : {};
|
|
148
|
+
if (parsed.name !== 'hadara') {
|
|
149
|
+
issues.push({ severity: 'error', code: 'RELEASE_ARTIFACT_PACKAGE_NAME_INVALID', message: 'Release artifact package name must be hadara.' });
|
|
150
|
+
}
|
|
151
|
+
if (bin.hadara !== './dist/cli/main.js') {
|
|
152
|
+
issues.push({ severity: 'error', code: 'RELEASE_ARTIFACT_BIN_MISSING', message: 'Release artifact package must expose bin.hadara at ./dist/cli/main.js.' });
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
name: typeof parsed.name === 'string' ? parsed.name : 'unknown',
|
|
156
|
+
version: typeof parsed.version === 'string' ? parsed.version : '0.0.0',
|
|
157
|
+
private: parsed.private === true,
|
|
158
|
+
...(typeof parsed.license === 'string' ? { license: parsed.license } : {})
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
issues.push({
|
|
163
|
+
severity: 'error',
|
|
164
|
+
code: 'RELEASE_ARTIFACT_PACKAGE_JSON_MISSING',
|
|
165
|
+
message: 'Release artifact builder requires readable package.json metadata.'
|
|
166
|
+
});
|
|
167
|
+
return { name: 'unknown', version: '0.0.0', private: true };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function prepareOutput(projectRoot, output, keepTemp, issues) {
|
|
171
|
+
const outputPath = output ? node_path_1.default.resolve(projectRoot, output) : node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'hadara-release-artifact-'));
|
|
172
|
+
try {
|
|
173
|
+
(0, fs_1.ensureDir)(outputPath);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
issues.push({
|
|
177
|
+
severity: 'error',
|
|
178
|
+
code: 'RELEASE_ARTIFACT_OUTPUT_CREATE_FAILED',
|
|
179
|
+
message: 'Release artifact output directory could not be created.'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const relativePath = safeRelativePath(output);
|
|
183
|
+
return {
|
|
184
|
+
kind: output ? 'explicit' : 'disposable',
|
|
185
|
+
path: outputPath,
|
|
186
|
+
displayPath: relativePath ? `./${relativePath}` : '<redacted-release-artifact-output>',
|
|
187
|
+
...(relativePath ? { relativePath } : {}),
|
|
188
|
+
retention: output ? 'explicit-output' : keepTemp ? 'kept-temporary' : 'deleted',
|
|
189
|
+
cleanupAllowed: !output && !keepTemp
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function prepareStaging(projectRoot, issues) {
|
|
193
|
+
const staging = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'hadara-release-artifact-stage-'));
|
|
194
|
+
for (const required of ['dist/cli/main.js', 'README.md', 'LICENSE']) {
|
|
195
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, required))) {
|
|
196
|
+
issues.push({
|
|
197
|
+
severity: 'error',
|
|
198
|
+
code: 'RELEASE_ARTIFACT_REQUIRED_SOURCE_MISSING',
|
|
199
|
+
message: `Release artifact source is missing required file ${required}.`,
|
|
200
|
+
stepId: 'stage-package'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { path: staging };
|
|
205
|
+
}
|
|
206
|
+
function copyWhitelistedPackage(projectRoot, staging, metadata, issues) {
|
|
207
|
+
try {
|
|
208
|
+
node_fs_1.default.cpSync(node_path_1.default.join(projectRoot, 'dist'), node_path_1.default.join(staging, 'dist'), { recursive: true });
|
|
209
|
+
node_fs_1.default.copyFileSync(node_path_1.default.join(projectRoot, 'README.md'), node_path_1.default.join(staging, 'README.md'));
|
|
210
|
+
node_fs_1.default.copyFileSync(node_path_1.default.join(projectRoot, 'LICENSE'), node_path_1.default.join(staging, 'LICENSE'));
|
|
211
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(staging, 'package.json'), `${JSON.stringify({
|
|
212
|
+
name: metadata.name,
|
|
213
|
+
version: metadata.version,
|
|
214
|
+
private: metadata.private,
|
|
215
|
+
...(metadata.license ? { license: metadata.license } : {}),
|
|
216
|
+
description: 'HADARA: portable agentic development workbench',
|
|
217
|
+
bin: { hadara: './dist/cli/main.js' },
|
|
218
|
+
files: ['dist/', 'README.md', 'LICENSE', 'package.json']
|
|
219
|
+
}, null, 2)}\n`, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
issues.push({
|
|
223
|
+
severity: 'error',
|
|
224
|
+
code: 'RELEASE_ARTIFACT_STAGE_FAILED',
|
|
225
|
+
message: 'Release artifact staging package could not be created.',
|
|
226
|
+
stepId: 'stage-package'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function parsePackResult(stdout) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(stdout);
|
|
233
|
+
if (!Array.isArray(parsed))
|
|
234
|
+
return undefined;
|
|
235
|
+
const first = parsed[0];
|
|
236
|
+
if (!first || typeof first.filename !== 'string' || first.filename.includes('/') || first.filename.includes('\\'))
|
|
237
|
+
return undefined;
|
|
238
|
+
const files = Array.isArray(first.files)
|
|
239
|
+
? first.files
|
|
240
|
+
.map((file) => {
|
|
241
|
+
if (!isRecord(file) || typeof file.path !== 'string')
|
|
242
|
+
return undefined;
|
|
243
|
+
return {
|
|
244
|
+
path: file.path,
|
|
245
|
+
...(typeof file.size === 'number' ? { size: file.size } : {})
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
.filter((file) => file !== undefined)
|
|
249
|
+
: [];
|
|
250
|
+
return { filename: first.filename, files };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function verifyPackageContents(files) {
|
|
257
|
+
const forbidden = files.filter((file) => !allowedRoots.some((root) => file === root || (root.endsWith('/') && file.startsWith(root))));
|
|
258
|
+
const missingRequired = requiredFiles.filter((required) => !files.includes(required));
|
|
259
|
+
return {
|
|
260
|
+
ok: forbidden.length === 0 && missingRequired.length === 0,
|
|
261
|
+
forbidden,
|
|
262
|
+
missingRequired
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function createManifest(metadata, pack, tarballHash) {
|
|
266
|
+
return {
|
|
267
|
+
schemaVersion: 'hadara.releaseArtifact.manifest.v1',
|
|
268
|
+
package: metadata,
|
|
269
|
+
tarball: {
|
|
270
|
+
fileName: pack.filename,
|
|
271
|
+
hash: `sha256:${tarballHash}`
|
|
272
|
+
},
|
|
273
|
+
files: pack.files.map((file) => ({
|
|
274
|
+
path: file.path,
|
|
275
|
+
...(file.size === undefined ? {} : { byteLength: file.size })
|
|
276
|
+
})),
|
|
277
|
+
releaseMutationExecuted: false,
|
|
278
|
+
publishExecuted: false,
|
|
279
|
+
githubReleaseCreated: false
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function createArtifact(kind, output, fileName, filePath, hash) {
|
|
283
|
+
return {
|
|
284
|
+
kind,
|
|
285
|
+
visibility: output.kind === 'explicit' ? 'local' : 'temporary',
|
|
286
|
+
fileName,
|
|
287
|
+
...(output.relativePath ? { relativePath: `${output.relativePath}/${fileName}` } : {}),
|
|
288
|
+
pathRedacted: true,
|
|
289
|
+
byteLength: safeFileSize(filePath),
|
|
290
|
+
hash: `sha256:${hash}`,
|
|
291
|
+
rawContentIncluded: false
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function hashFile(filePath) {
|
|
295
|
+
const hash = node_crypto_1.default.createHash('sha256');
|
|
296
|
+
hash.update(node_fs_1.default.readFileSync(filePath));
|
|
297
|
+
return hash.digest('hex');
|
|
298
|
+
}
|
|
299
|
+
function safeFileSize(filePath) {
|
|
300
|
+
try {
|
|
301
|
+
return node_fs_1.default.statSync(filePath).size;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function cleanupDirectory(directory, cleanup) {
|
|
308
|
+
if (cleanup)
|
|
309
|
+
node_fs_1.default.rmSync(directory, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
function runCommand(command, args, options) {
|
|
312
|
+
const started = Date.now();
|
|
313
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
314
|
+
cwd: options.cwd,
|
|
315
|
+
encoding: 'utf8',
|
|
316
|
+
timeout: options.timeoutMs,
|
|
317
|
+
maxBuffer: 1024 * 1024 * 4
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
status: typeof result.status === 'number' ? result.status : null,
|
|
321
|
+
signal: result.signal,
|
|
322
|
+
stdout: result.stdout ?? '',
|
|
323
|
+
stderr: result.stderr ?? '',
|
|
324
|
+
elapsedMs: Date.now() - started,
|
|
325
|
+
timedOut: result.error?.name === 'TimeoutError' || result.signal === 'SIGTERM'
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function validateTimeout(timeoutSeconds, issues) {
|
|
329
|
+
if (timeoutSeconds !== undefined && (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1)) {
|
|
330
|
+
issues.push({
|
|
331
|
+
severity: 'error',
|
|
332
|
+
code: 'RELEASE_ARTIFACT_TIMEOUT_INVALID',
|
|
333
|
+
message: 'Release artifact timeout must be a positive integer number of seconds.'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function npmCommand() {
|
|
338
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
339
|
+
}
|
|
340
|
+
function safeRelativePath(value) {
|
|
341
|
+
if (!value || node_path_1.default.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('~') || value.startsWith('<') || value.includes('%')) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
const normalized = value.split(/[\\/]+/).filter(Boolean).join('/');
|
|
345
|
+
if (!normalized || normalized === '.' || normalized.startsWith('..'))
|
|
346
|
+
return undefined;
|
|
347
|
+
return normalized;
|
|
348
|
+
}
|
|
349
|
+
function isRecord(value) {
|
|
350
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
351
|
+
}
|