@jstn-sdk/ma 0.1.13 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +0 -0
- package/.codex/agents/Architect.toml +1 -0
- package/.codex/agents/Auditor.toml +1 -0
- package/.codex/agents/Builder.toml +1 -0
- package/.codex/agents/Flow.toml +1 -0
- package/.codex/agents/Sage.toml +1 -0
- package/.codex/agents/Vibe.toml +1 -0
- package/.codex/hooks.json +12 -2
- package/.codex/prompts/enforcement.md +0 -0
- package/.codex/prompts/onboarding.md +6 -1
- package/.codex/prompts/release-rules.md +0 -0
- package/.codex/prompts/skill-contract.md +0 -0
- package/COVERAGE.md +7 -7
- package/DEMO.md +15 -6
- package/LICENSE +0 -0
- package/README.md +126 -793
- package/bin/ma.js +585 -97
- package/data/clone-data.ledger.json +0 -0
- package/data/clone-data.proof.json +0 -0
- package/data/clone-data.rvf +0 -0
- package/docs/README.md +8 -2
- package/docs/autonomous-tasks.md +46 -0
- package/docs/codex-integration.md +16 -0
- package/docs/getting-started.md +28 -4
- package/docs/installed-sdk.md +0 -0
- package/docs/mcp-setup.md +30 -1
- package/docs/qa/{release-issue-gates-0.1.13.json → release-issue-gates-0.14.0.json} +77 -77
- package/docs/qa/release-readiness-0.1.5.md +0 -0
- package/docs/qa/{release-readiness-0.1.13.md → release-readiness-0.14.0.md} +12 -12
- package/docs/quality.md +24 -0
- package/docs/reference/native-engineering-patterns.md +0 -0
- package/docs/reference/native-security-playbooks.md +0 -0
- package/docs/reference/native-source-selection.md +0 -0
- package/docs/reference/native-style-and-deslop.md +0 -0
- package/docs/release-spec.md +27 -14
- package/docs/skills.md +1 -1
- package/index.js +73 -0
- package/mcp/collections.json +0 -0
- package/mcp/fallback.json +0 -0
- package/mcp/local/code-intel.js +179 -41
- package/mcp/local/context.js +180 -0
- package/mcp/local/memory.js +17 -6
- package/mcp/local/playbooks.js +0 -0
- package/mcp/local/quality.js +39 -0
- package/mcp/local/state.js +7 -3
- package/mcp/local/team-run.js +8 -3
- package/mcp/local/trace.js +0 -0
- package/mcp/local-capabilities.json +9 -0
- package/mcp/native-playbooks.json +0 -0
- package/mcp/servers.json +0 -0
- package/package.json +26 -4
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +2 -2
- package/plugins/meta-architect/obsidian/main.js +137 -4
- package/plugins/meta-architect/obsidian/manifest.json +1 -1
- package/plugins/meta-architect/obsidian/styles.css +0 -0
- package/plugins/meta-architect/skills/align/SKILL.md +0 -0
- package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +0 -0
- package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/build/SKILL.md +0 -0
- package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
- package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
- package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
- package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +0 -0
- package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/SKILL.md +0 -0
- package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +0 -0
- package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +0 -0
- package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/sage/references/source-selection.md +0 -0
- package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
- package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/references/security-playbooks.md +0 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
- package/schemas/autonomous-task-queue.schema.json +56 -0
- package/schemas/handoff-packet.schema.json +36 -0
- package/schemas/setup-receipt.schema.json +28 -0
- package/schemas/skill-frontmatter.schema.json +20 -0
- package/schemas/support-bundle.schema.json +37 -0
- package/schemas/task-contract.schema.json +31 -0
- package/scripts/active-autonomy-hook.mjs +141 -11
- package/scripts/biome-staged.js +0 -0
- package/scripts/build-linux-packages.mjs +7 -3
- package/scripts/cleanup-test-fixtures.sh +55 -0
- package/scripts/context-hydration-hook.mjs +89 -0
- package/scripts/demo-smoke.js +285 -0
- package/scripts/doctor.js +10 -8
- package/scripts/install.sh +33 -4
- package/scripts/install.sh.sha256 +1 -0
- package/scripts/linux-package-lib.mjs +0 -0
- package/scripts/linux-package-smoke.mjs +2 -2
- package/scripts/package-size-check.mjs +26 -0
- package/scripts/plugin-sync.js +83 -17
- package/scripts/postinstall.js +17 -10
- package/scripts/prepack.js +9 -0
- package/scripts/ralph/prompt.md +0 -0
- package/scripts/release-metadata.js +0 -0
- package/scripts/release-sync.js +72 -7
- package/scripts/release-verify.js +100 -7
- package/scripts/setup-npmrc.js +48 -23
- package/scripts/skills-install.js +9 -2
- package/scripts/skills-manifest.js +2 -21
- package/scripts/skills-pack.js +2 -2
- package/scripts/skills-validate.js +5 -55
- package/skills/align/SKILL.md +0 -0
- package/skills/align/agents/openai.yaml +0 -0
- package/skills/align/references/shared-language.md +0 -0
- package/skills/arch/SKILL.md +0 -0
- package/skills/arch/agents/openai.yaml +0 -0
- package/skills/build/SKILL.md +0 -0
- package/skills/build/agents/openai.yaml +0 -0
- package/skills/cleanup/SKILL.md +0 -0
- package/skills/cleanup/agents/openai.yaml +0 -0
- package/skills/cleanup/references/style-and-deslop.md +0 -0
- package/skills/diagnose/SKILL.md +0 -0
- package/skills/diagnose/agents/openai.yaml +0 -0
- package/skills/flow/SKILL.md +0 -0
- package/skills/flow/agents/openai.yaml +0 -0
- package/skills/index.json +0 -0
- package/skills/maestro/SKILL.md +0 -0
- package/skills/maestro/agents/openai.yaml +0 -0
- package/skills/maestro/references/core-release-rules.md +0 -0
- package/skills/maestro/references/native-ingest-map.md +0 -0
- package/skills/sage/SKILL.md +0 -0
- package/skills/sage/agents/openai.yaml +0 -0
- package/skills/sage/references/source-selection.md +0 -0
- package/skills/tdd/SKILL.md +0 -0
- package/skills/tdd/agents/openai.yaml +0 -0
- package/skills/vet/SKILL.md +0 -0
- package/skills/vet/agents/openai.yaml +0 -0
- package/skills/vet/references/security-playbooks.md +0 -0
- package/skills/vibe/SKILL.md +0 -0
- package/skills/vibe/agents/openai.yaml +0 -0
- package/sprint/00-idea.md +0 -0
- package/sprint/01-architecture.md +0 -0
- package/sprint/02-oss-evidence.md +0 -0
- package/sprint/03-logic.md +0 -0
- package/sprint/04-security.md +0 -0
- package/sprint/05-dx-ux.md +0 -0
- package/sprint/06-build-plan.md +0 -0
- package/sprint/07-release.md +0 -0
- package/src/agents.js +279 -0
- package/src/bootstrap.js +252 -63
- package/src/build-gate.js +0 -0
- package/src/codex-app-server.js +291 -0
- package/src/decision-log.js +0 -0
- package/src/fs-utils.js +175 -4
- package/src/launcher.js +24 -21
- package/src/mcp-config.js +9 -1
- package/src/mcp-live-client.js +140 -7
- package/src/paths.js +6 -1
- package/src/policy.js +2 -2
- package/src/prelaunch.js +189 -0
- package/src/quality/ai-quality-orchestrator.js +328 -0
- package/src/release-issue-gates.js +62 -0
- package/src/release-operations.js +62 -0
- package/src/release-state.js +0 -0
- package/src/runtime/active-autonomy-core.js +0 -0
- package/src/runtime/agent-compat.js +31 -0
- package/src/runtime/alignment-sentinel.js +0 -0
- package/src/runtime/architect-review.js +248 -10
- package/src/runtime/autonomous-tasks.js +408 -0
- package/src/runtime/build-readiness.js +0 -0
- package/src/runtime/code-graph-rehearse.js +0 -0
- package/src/runtime/codeburn-core.js +112 -0
- package/src/runtime/context-authority.js +40 -0
- package/src/runtime/context-economy-core.js +40 -0
- package/src/runtime/continuity-graph.js +201 -0
- package/src/runtime/continuity-notes.js +71 -26
- package/src/runtime/core-source-ingest.js +0 -0
- package/src/runtime/detached-provider.js +82 -14
- package/src/runtime/doctor-report.js +18 -0
- package/src/runtime/environment-awareness-core.js +0 -0
- package/src/runtime/exposure-catalog.js +0 -0
- package/src/runtime/graphify-core.js +167 -0
- package/src/runtime/guidance-stack.js +9 -2
- package/src/runtime/handoff-packets.js +92 -0
- package/src/runtime/headroom-core.js +104 -0
- package/src/runtime/helper-orchestration-core.js +0 -0
- package/src/runtime/hook-profiles.js +20 -0
- package/src/runtime/learning-loop-core.js +110 -4
- package/src/runtime/live-agent-verification.js +122 -0
- package/src/runtime/maestro-events.js +0 -0
- package/src/runtime/maestro-manager.js +0 -0
- package/src/runtime/maestro-output.js +58 -0
- package/src/runtime/maestro-state.js +0 -0
- package/src/runtime/managed-markdown.js +29 -0
- package/src/runtime/mcp-authority.js +147 -0
- package/src/runtime/mcp-policy.js +0 -0
- package/src/runtime/obsidian-integration-core.js +255 -17
- package/src/runtime/obsidian-plugin-bridge.js +344 -38
- package/src/runtime/orchestrator.js +0 -0
- package/src/runtime/pi-maestro-core.js +89 -0
- package/src/runtime/preferences.js +79 -0
- package/src/runtime/project-context.js +636 -0
- package/src/runtime/prompt-strategy-core.js +0 -0
- package/src/runtime/quorum-review.js +0 -0
- package/src/runtime/ralph-execution-core.js +0 -0
- package/src/runtime/redaction-gateway.js +218 -19
- package/src/runtime/runtime-state.js +94 -2
- package/src/runtime/schema-migrations.js +162 -0
- package/src/runtime/semantic-recording-core.js +0 -0
- package/src/runtime/signal-hooks.js +13 -0
- package/src/runtime/skills-registry-export.js +92 -8
- package/src/runtime/startup-path.js +0 -0
- package/src/runtime/task-contracts.js +83 -0
- package/src/runtime/universal-plugin-broker-core.js +373 -50
- package/src/runtime/workspace-intelligence-runtime.js +0 -0
- package/src/runtime/workspace-virtualizer.js +0 -0
- package/src/runtime/workspaces.js +0 -0
- package/src/runtime-artifacts.js +0 -0
- package/src/setup-lifecycle.js +359 -0
- package/src/skill-frontmatter.js +78 -0
- package/src/skill-installer.js +226 -71
- package/src/skills.js +297 -19
- package/src/state-sync.js +0 -0
- package/src/test-fixtures.js +543 -0
- package/src/tui/grid.js +67 -0
- package/src/tui/status-grid.js +23 -0
- package/support-bundle.json +114 -0
- package/templates/AGENTS.md +6 -2
- package/templates/catalog-manifest.json +0 -0
- package/templates/model-instructions/core.md +0 -0
- package/templates/model-instructions/release.md +0 -0
- package/templates/model-instructions/security.md +0 -0
- package/templates/quality/ai-quality-rules.yml +37 -0
- package/docs/assets/image/Screenshot(1).png +0 -0
- package/docs/assets/image/Screenshot(2).png +0 -0
- package/docs/assets/image/Screenshot(3).png +0 -0
- package/docs/assets/image/Screenshot(4).png +0 -0
- package/docs/assets/image/Screenshot(5).png +0 -0
- package/docs/assets/image/Screenshot(6).png +0 -0
- package/docs/assets/image/Screenshot(7).png +0 -0
- package/docs/assets/image/Screenshot(8).png +0 -0
- package/docs/assets/image/Screenshot(9).png +0 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +0 -8
- package/docs/onboarding.md +0 -81
- package/docs/prompt-guidance-contract.md +0 -17
- package/docs/prompt-guidance-fragments/active-autonomy-core.md +0 -7
- package/docs/skills-publishing.md +0 -255
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const TEST_ROOT = path.join(os.tmpdir(), "ma-tests");
|
|
18
|
+
const RETENTION_ROOT = path.join(TEST_ROOT, "retained");
|
|
19
|
+
const EXCLUDED_DIRS = new Set([
|
|
20
|
+
".git",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist",
|
|
23
|
+
".next",
|
|
24
|
+
".ma",
|
|
25
|
+
".codex",
|
|
26
|
+
".omx",
|
|
27
|
+
".active",
|
|
28
|
+
".internal",
|
|
29
|
+
".npm-cache",
|
|
30
|
+
".turbo",
|
|
31
|
+
".vercel",
|
|
32
|
+
"coverage",
|
|
33
|
+
]);
|
|
34
|
+
const generatedRoots = new Set();
|
|
35
|
+
let budgetQueue = Promise.resolve();
|
|
36
|
+
let exitHookInstalled = false;
|
|
37
|
+
|
|
38
|
+
function safeTestName(value) {
|
|
39
|
+
const name = String(value || "fixture")
|
|
40
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
41
|
+
.replace(/^-+|-+$/g, "");
|
|
42
|
+
return name.slice(0, 80) || "fixture";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureExitHook() {
|
|
46
|
+
if (exitHookInstalled) return;
|
|
47
|
+
exitHookInstalled = true;
|
|
48
|
+
process.once("exit", () => {
|
|
49
|
+
for (const root of generatedRoots) {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(root, { recursive: true, force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
// Best-effort cleanup during process shutdown.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createTestNamespace(testName) {
|
|
60
|
+
assertRealTestRootSync(TEST_ROOT);
|
|
61
|
+
mkdirSync(TEST_ROOT, { recursive: true, mode: 0o700 });
|
|
62
|
+
try {
|
|
63
|
+
chmodSync(TEST_ROOT, 0o700);
|
|
64
|
+
} catch {
|
|
65
|
+
// The OS may reject chmod on an unusual temporary filesystem.
|
|
66
|
+
}
|
|
67
|
+
mkdirSync(RETENTION_ROOT, { recursive: true, mode: 0o700 });
|
|
68
|
+
const suffix = `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
69
|
+
const root = mkdtempSync(path.join(TEST_ROOT, `${safeTestName(testName)}-${suffix}-`));
|
|
70
|
+
writeFileSync(path.join(root, ".active"), `${process.pid}\n`, { mode: 0o600 });
|
|
71
|
+
generatedRoots.add(root);
|
|
72
|
+
ensureExitHook();
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function removeTestNamespace(root) {
|
|
77
|
+
const resolved = path.resolve(root);
|
|
78
|
+
assertTestNamespace(resolved, "Fixture root");
|
|
79
|
+
const metadata = await fs.lstat(resolved).catch(() => null);
|
|
80
|
+
if (metadata?.isSymbolicLink()) throw new Error(`Refusing symlinked fixture path: ${resolved}`);
|
|
81
|
+
generatedRoots.delete(root);
|
|
82
|
+
await fs.rm(resolved, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isWithin(root, candidate) {
|
|
86
|
+
const relative = path.relative(root, candidate);
|
|
87
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertTestNamespace(root, label) {
|
|
91
|
+
if (root === TEST_ROOT || !isWithin(TEST_ROOT, root) || isWithin(RETENTION_ROOT, root)) {
|
|
92
|
+
throw new Error(`${label} must be inside ${TEST_ROOT}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertRealTestRootSync(root) {
|
|
97
|
+
const metadata = lstatSync(root, { throwIfNoEntry: false });
|
|
98
|
+
if (metadata?.isSymbolicLink()) throw new Error(`Refusing symlinked test root: ${root}`);
|
|
99
|
+
const retention = lstatSync(RETENTION_ROOT, { throwIfNoEntry: false });
|
|
100
|
+
if (retention?.isSymbolicLink())
|
|
101
|
+
throw new Error(`Refusing symlinked retention root: ${RETENTION_ROOT}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function assertNoSymlinkComponents(root, candidate) {
|
|
105
|
+
let current = root;
|
|
106
|
+
const relative = path.relative(root, candidate);
|
|
107
|
+
for (const part of relative ? relative.split(path.sep) : []) {
|
|
108
|
+
current = path.join(current, part);
|
|
109
|
+
const metadata = await fs.lstat(current).catch((error) => {
|
|
110
|
+
if (error.code === "ENOENT") return null;
|
|
111
|
+
throw error;
|
|
112
|
+
});
|
|
113
|
+
if (metadata?.isSymbolicLink()) {
|
|
114
|
+
throw new Error(`Refusing symlinked fixture path: ${candidate}`);
|
|
115
|
+
}
|
|
116
|
+
if (!metadata) break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertNoSymlinkComponentsSync(root, candidate) {
|
|
121
|
+
let current = root;
|
|
122
|
+
const relative = path.relative(root, candidate);
|
|
123
|
+
for (const part of relative ? relative.split(path.sep) : []) {
|
|
124
|
+
current = path.join(current, part);
|
|
125
|
+
const metadata = lstatSync(current, { throwIfNoEntry: false });
|
|
126
|
+
if (metadata?.isSymbolicLink()) {
|
|
127
|
+
throw new Error(`Refusing symlinked fixture path: ${candidate}`);
|
|
128
|
+
}
|
|
129
|
+
if (!metadata) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function assertRealFixtureRoot(root) {
|
|
134
|
+
const metadata = await fs.lstat(root).catch(() => null);
|
|
135
|
+
if (metadata?.isSymbolicLink()) throw new Error(`Refusing symlinked fixture root: ${root}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function pruneTestFixture(fixtureRoot) {
|
|
139
|
+
const root = path.resolve(fixtureRoot);
|
|
140
|
+
assertTestNamespace(root, "Fixture root");
|
|
141
|
+
await assertRealFixtureRoot(root);
|
|
142
|
+
const generatedPaths = [
|
|
143
|
+
".ma/runtime",
|
|
144
|
+
".ma/context",
|
|
145
|
+
".ma/learning",
|
|
146
|
+
".ma/obsidian",
|
|
147
|
+
".ma/redaction-vault",
|
|
148
|
+
".codex/agents",
|
|
149
|
+
".codex/prompts",
|
|
150
|
+
".codex/skills",
|
|
151
|
+
"node_modules",
|
|
152
|
+
"dist",
|
|
153
|
+
".next",
|
|
154
|
+
".vercel",
|
|
155
|
+
".turbo",
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
await Promise.all(
|
|
159
|
+
generatedPaths.map(async (relative) => {
|
|
160
|
+
const target = path.resolve(root, relative);
|
|
161
|
+
if (isWithin(root, target)) {
|
|
162
|
+
await assertNoSymlinkComponents(root, target);
|
|
163
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const receiptRoots = [path.join(root, "receipts"), path.join(root, ".ma", "hooks", "receipts")];
|
|
169
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
170
|
+
for (const receiptsRoot of receiptRoots) {
|
|
171
|
+
let entries = [];
|
|
172
|
+
try {
|
|
173
|
+
entries = await fs.readdir(receiptsRoot, { withFileTypes: true });
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
await Promise.all(
|
|
178
|
+
entries.map(async (entry) => {
|
|
179
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) return;
|
|
180
|
+
const receipt = path.join(receiptsRoot, entry.name);
|
|
181
|
+
await assertNoSymlinkComponents(root, receipt);
|
|
182
|
+
const metadata = await fs.stat(receipt).catch(() => null);
|
|
183
|
+
if (metadata && metadata.mtimeMs < cutoff) await fs.rm(receipt, { force: true });
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function copyTree(sourceRoot, targetRoot, { strategy = "copy" } = {}) {
|
|
190
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
191
|
+
const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
194
|
+
const source = path.join(sourceRoot, entry.name);
|
|
195
|
+
const target = path.join(targetRoot, entry.name);
|
|
196
|
+
const metadata = await fs.lstat(source).catch(() => null);
|
|
197
|
+
if (!metadata || metadata.isSymbolicLink()) continue;
|
|
198
|
+
if (metadata.isDirectory()) {
|
|
199
|
+
await copyTree(source, target, { strategy });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!metadata.isFile()) continue;
|
|
203
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
204
|
+
if (strategy === "hardlink") {
|
|
205
|
+
await fs.link(source, target);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (strategy === "reflink") {
|
|
209
|
+
await fs.copyFile(source, target, constants.COPYFILE_FICLONE_FORCE);
|
|
210
|
+
} else {
|
|
211
|
+
await fs.copyFile(source, target);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function runCommand(command, args) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const child = spawn(command, args, { stdio: "ignore" });
|
|
219
|
+
child.once("error", reject);
|
|
220
|
+
child.once("close", (code) =>
|
|
221
|
+
code === 0 ? resolve() : reject(new Error(`${command} exited with ${code}`)),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isCopyCapabilityError(error) {
|
|
227
|
+
return ["EOPNOTSUPP", "ENOTSUP", "EXDEV", "EINVAL"].includes(error?.code);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function copyProjectFixtureInternal(sourceRoot, targetRoot, options = {}) {
|
|
231
|
+
const source = path.resolve(sourceRoot);
|
|
232
|
+
const target = path.resolve(targetRoot);
|
|
233
|
+
assertTestNamespace(target, "Fixture target");
|
|
234
|
+
await assertNoSymlinkComponents(TEST_ROOT, target);
|
|
235
|
+
if (source === target || isWithin(source, target))
|
|
236
|
+
throw new Error("Fixture target must be outside the source tree");
|
|
237
|
+
await fs.mkdir(target, { recursive: true });
|
|
238
|
+
const existingEntries = (await fs.readdir(target)).filter((entry) => entry !== ".active");
|
|
239
|
+
if (existingEntries.length > 0) {
|
|
240
|
+
throw new Error("Fixture target must be empty");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (options.useReflink !== false) {
|
|
244
|
+
try {
|
|
245
|
+
await copyTree(source, target, { strategy: "reflink" });
|
|
246
|
+
return await enforceFixtureBudget(target, "reflink");
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (!isCopyCapabilityError(error)) throw error;
|
|
249
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
250
|
+
await fs.mkdir(target, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ponytail: hardlinks are opt-in because writes would mutate the source; reflink/rsync preserve isolation.
|
|
254
|
+
if (options.useHardlinks === true) {
|
|
255
|
+
try {
|
|
256
|
+
await copyTree(source, target, { strategy: "hardlink" });
|
|
257
|
+
return await enforceFixtureBudget(target, "hardlink");
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!isCopyCapabilityError(error)) throw error;
|
|
260
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
261
|
+
await fs.mkdir(target, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (options.useHardlinks === false) {
|
|
265
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
266
|
+
await fs.mkdir(target, { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await runCommand("rsync", [
|
|
270
|
+
"-a",
|
|
271
|
+
"--delete",
|
|
272
|
+
"--exclude=node_modules",
|
|
273
|
+
"--exclude=dist",
|
|
274
|
+
"--exclude=.next",
|
|
275
|
+
"--exclude=.ma",
|
|
276
|
+
"--exclude=.codex",
|
|
277
|
+
"--exclude=.git",
|
|
278
|
+
"--exclude=.omx",
|
|
279
|
+
"--exclude=.internal",
|
|
280
|
+
"--exclude=.npm-cache",
|
|
281
|
+
"--exclude=.turbo",
|
|
282
|
+
"--exclude=.vercel",
|
|
283
|
+
"--exclude=coverage",
|
|
284
|
+
"--exclude=.active",
|
|
285
|
+
"--no-links",
|
|
286
|
+
`${source}${path.sep}`,
|
|
287
|
+
`${target}${path.sep}`,
|
|
288
|
+
]);
|
|
289
|
+
return await enforceFixtureBudget(target, "rsync");
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error?.code !== "ENOENT") throw error;
|
|
292
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
293
|
+
await fs.mkdir(target, { recursive: true });
|
|
294
|
+
await copyTree(source, target, { strategy: "copy" });
|
|
295
|
+
return await enforceFixtureBudget(target, "copy");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function copyProjectFixture(sourceRoot, targetRoot, options = {}) {
|
|
300
|
+
const source = path.resolve(sourceRoot);
|
|
301
|
+
const target = path.resolve(targetRoot);
|
|
302
|
+
assertTestNamespace(target, "Fixture target");
|
|
303
|
+
try {
|
|
304
|
+
return await copyProjectFixtureInternal(sourceRoot, target, options);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (target !== source) {
|
|
307
|
+
for (const entry of await fs.readdir(target).catch(() => [])) {
|
|
308
|
+
if (entry !== ".active") {
|
|
309
|
+
await fs.rm(path.join(target, entry), { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function enforceFixtureBudget(root, strategy) {
|
|
318
|
+
const check = budgetQueue.then(async () => {
|
|
319
|
+
const bytes = await getTestDiskUsage(root);
|
|
320
|
+
if (bytes > TEST_FIXTURE_LIMITS.hardFixtureBytes) {
|
|
321
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
322
|
+
throw new Error(`Fixture exceeds hard disk budget: ${bytes} bytes`);
|
|
323
|
+
}
|
|
324
|
+
// ponytail: periodic aggregate scan avoids O(fixtures^2) setup while retaining a hard cap.
|
|
325
|
+
if (generatedRoots.size % 16 === 0) {
|
|
326
|
+
const total = await getTestDiskUsage(TEST_ROOT);
|
|
327
|
+
if (total > TEST_FIXTURE_LIMITS.maxTotalBytes) {
|
|
328
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
329
|
+
throw new Error(`Test fixture root exceeds disk budget: ${total} bytes`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { strategy, bytes };
|
|
333
|
+
});
|
|
334
|
+
budgetQueue = check.catch(() => {});
|
|
335
|
+
return check;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function runArchive(args) {
|
|
339
|
+
return new Promise((resolve, reject) => {
|
|
340
|
+
const child = spawn("tar", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
341
|
+
let stderr = "";
|
|
342
|
+
child.stderr.on("data", (chunk) => {
|
|
343
|
+
stderr += chunk.toString();
|
|
344
|
+
});
|
|
345
|
+
child.once("error", reject);
|
|
346
|
+
child.once("close", (code) =>
|
|
347
|
+
code === 0 ? resolve() : reject(new Error(stderr || `tar exited with ${code}`)),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function commandAvailable(command) {
|
|
353
|
+
try {
|
|
354
|
+
await runCommand(command, ["--version"]);
|
|
355
|
+
return true;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error?.code === "ENOENT") return false;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function compressMaState(fixtureRoot) {
|
|
363
|
+
const root = path.resolve(fixtureRoot);
|
|
364
|
+
assertTestNamespace(root, "Fixture root");
|
|
365
|
+
await assertRealFixtureRoot(root);
|
|
366
|
+
const maDir = path.join(root, ".ma");
|
|
367
|
+
if (!existsSync(maDir)) return null;
|
|
368
|
+
await assertNoSymlinkComponents(root, maDir);
|
|
369
|
+
const archiveBase = `${path.basename(root)}-${Date.now().toString(36)}`;
|
|
370
|
+
const zstdPath = path.join(RETENTION_ROOT, `${archiveBase}.tar.zst`);
|
|
371
|
+
if (await commandAvailable("zstd")) {
|
|
372
|
+
await runArchive(["--use-compress-program=zstd", "-cf", zstdPath, "-C", maDir, "."]);
|
|
373
|
+
await fs.rm(maDir, { recursive: true, force: true });
|
|
374
|
+
return zstdPath;
|
|
375
|
+
}
|
|
376
|
+
const gzipPath = path.join(RETENTION_ROOT, `${archiveBase}.tar.gz`);
|
|
377
|
+
await runArchive(["-czf", gzipPath, "-C", maDir, "."]);
|
|
378
|
+
await fs.rm(maDir, { recursive: true, force: true });
|
|
379
|
+
return gzipPath;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createMinimalFixture(fixtureRoot, fixture) {
|
|
383
|
+
const root = path.resolve(fixtureRoot);
|
|
384
|
+
assertTestNamespace(root, "Fixture root");
|
|
385
|
+
const created = [];
|
|
386
|
+
return (async () => {
|
|
387
|
+
const files = Object.entries(fixture.files || {});
|
|
388
|
+
const targets = [];
|
|
389
|
+
for (const [relative] of files) {
|
|
390
|
+
const target = path.resolve(root, relative);
|
|
391
|
+
if (!isWithin(root, target)) throw new Error(`Fixture path escapes root: ${relative}`);
|
|
392
|
+
await assertRealFixtureRoot(root);
|
|
393
|
+
await assertNoSymlinkComponents(root, target);
|
|
394
|
+
targets.push(target);
|
|
395
|
+
}
|
|
396
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
397
|
+
const [, content] = files[index];
|
|
398
|
+
const target = targets[index];
|
|
399
|
+
if (!existsSync(target)) created.push(target);
|
|
400
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
401
|
+
await fs.writeFile(target, content);
|
|
402
|
+
}
|
|
403
|
+
if (fixture.maContext) {
|
|
404
|
+
const contextRoot = path.join(root, ".ma", "context");
|
|
405
|
+
await fs.mkdir(contextRoot, { recursive: true });
|
|
406
|
+
const contextPath = path.join(contextRoot, "project-index.json");
|
|
407
|
+
if (!existsSync(contextPath)) created.push(contextPath);
|
|
408
|
+
await fs.writeFile(
|
|
409
|
+
contextPath,
|
|
410
|
+
`${JSON.stringify(
|
|
411
|
+
{
|
|
412
|
+
schemaVersion: "1.0",
|
|
413
|
+
languages: ["typescript"],
|
|
414
|
+
frameworks: [],
|
|
415
|
+
commands: { test: "echo test" },
|
|
416
|
+
generatedDirectories: ["node_modules", "dist"],
|
|
417
|
+
...fixture.maContext,
|
|
418
|
+
},
|
|
419
|
+
null,
|
|
420
|
+
2,
|
|
421
|
+
)}\n`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return enforceFixtureBudget(root, "minimal");
|
|
425
|
+
})().catch(async (error) => {
|
|
426
|
+
await Promise.all(created.map((file) => fs.rm(file, { force: true })));
|
|
427
|
+
throw error;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function runTestWithStreaming(command, args, options) {
|
|
432
|
+
const {
|
|
433
|
+
logPath,
|
|
434
|
+
summaryPath,
|
|
435
|
+
timeoutMs = 120_000,
|
|
436
|
+
maxLogBytes = TEST_FIXTURE_LIMITS.hardFixtureBytes,
|
|
437
|
+
} = options;
|
|
438
|
+
const logFile = path.resolve(logPath);
|
|
439
|
+
const summaryFile = path.resolve(summaryPath);
|
|
440
|
+
assertTestNamespace(path.dirname(logFile), "Log path");
|
|
441
|
+
assertTestNamespace(path.dirname(summaryFile), "Summary path");
|
|
442
|
+
assertNoSymlinkComponentsSync(TEST_ROOT, logFile);
|
|
443
|
+
assertNoSymlinkComponentsSync(TEST_ROOT, summaryFile);
|
|
444
|
+
return new Promise((resolve, reject) => {
|
|
445
|
+
const child = spawn(command, args, { stdio: ["inherit", "pipe", "pipe"] });
|
|
446
|
+
const log = fs.open(logFile, "w");
|
|
447
|
+
const summary = fs.open(summaryFile, "w");
|
|
448
|
+
let summaryBytes = 0;
|
|
449
|
+
let logBytes = 0;
|
|
450
|
+
let writeError = null;
|
|
451
|
+
let settled = false;
|
|
452
|
+
const pendingWrites = new Set();
|
|
453
|
+
let writeQueue = Promise.resolve();
|
|
454
|
+
let forceTimer;
|
|
455
|
+
const terminate = () => {
|
|
456
|
+
if (child.exitCode !== null) return;
|
|
457
|
+
child.kill("SIGTERM");
|
|
458
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), 1_000);
|
|
459
|
+
};
|
|
460
|
+
const writeLine = async (line, includeInSummary) => {
|
|
461
|
+
const [logHandle, summaryHandle] = await Promise.all([log, summary]);
|
|
462
|
+
const value = `${line}\n`;
|
|
463
|
+
const valueBytes = Buffer.byteLength(value);
|
|
464
|
+
if (logBytes + valueBytes > maxLogBytes) {
|
|
465
|
+
throw new Error(`Test log exceeds disk budget: ${maxLogBytes} bytes`);
|
|
466
|
+
}
|
|
467
|
+
await logHandle.write(value);
|
|
468
|
+
logBytes += valueBytes;
|
|
469
|
+
if (includeInSummary && summaryBytes < 1024 * 1024) {
|
|
470
|
+
const remaining = 1024 * 1024 - summaryBytes;
|
|
471
|
+
const bounded = Buffer.from(value).subarray(0, remaining);
|
|
472
|
+
await summaryHandle.write(bounded);
|
|
473
|
+
summaryBytes += bounded.length;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const queueWrite = (line, includeInSummary) => {
|
|
477
|
+
const pending = writeQueue.then(() => writeLine(line, includeInSummary));
|
|
478
|
+
writeQueue = pending.catch(() => {});
|
|
479
|
+
pendingWrites.add(pending);
|
|
480
|
+
void pending.then(
|
|
481
|
+
() => pendingWrites.delete(pending),
|
|
482
|
+
(error) => {
|
|
483
|
+
writeError ||= error;
|
|
484
|
+
pendingWrites.delete(pending);
|
|
485
|
+
terminate();
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
};
|
|
489
|
+
const onData = (chunk, forceSummary) => {
|
|
490
|
+
for (const line of chunk.toString().split(/\r?\n/)) {
|
|
491
|
+
if (line) queueWrite(line, forceSummary || /ERROR|FAIL|receipt/i.test(line));
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
child.stdout.on("data", (chunk) => onData(chunk, false));
|
|
495
|
+
child.stderr.on("data", (chunk) => onData(chunk, true));
|
|
496
|
+
const timer = setTimeout(terminate, timeoutMs);
|
|
497
|
+
const finish = async (code, signal, error = null) => {
|
|
498
|
+
if (settled) return;
|
|
499
|
+
settled = true;
|
|
500
|
+
clearTimeout(timer);
|
|
501
|
+
clearTimeout(forceTimer);
|
|
502
|
+
try {
|
|
503
|
+
await Promise.all([...pendingWrites]);
|
|
504
|
+
const [logHandle, summaryHandle] = await Promise.all([log, summary]);
|
|
505
|
+
await Promise.all([logHandle.close(), summaryHandle.close()]);
|
|
506
|
+
if (error || writeError) reject(error || writeError);
|
|
507
|
+
else resolve({ code: code ?? 1, signal });
|
|
508
|
+
} catch (closeError) {
|
|
509
|
+
reject(closeError);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
child.once("error", (error) => finish(1, null, error));
|
|
513
|
+
child.once("close", async (code, signal) => {
|
|
514
|
+
await finish(code, signal);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function getTestDiskUsage(root = TEST_ROOT) {
|
|
520
|
+
let total = 0;
|
|
521
|
+
async function walk(current) {
|
|
522
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
523
|
+
for (const entry of entries) {
|
|
524
|
+
const target = path.join(current, entry.name);
|
|
525
|
+
if (entry.isDirectory()) await walk(target);
|
|
526
|
+
else if (entry.isFile()) {
|
|
527
|
+
const stat = await fs
|
|
528
|
+
.stat(target)
|
|
529
|
+
.catch((error) => (error?.code === "ENOENT" ? null : Promise.reject(error)));
|
|
530
|
+
if (stat) total += stat.size;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
await walk(root);
|
|
535
|
+
return total;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export const TEST_FIXTURE_ROOT = TEST_ROOT;
|
|
539
|
+
export const TEST_FIXTURE_LIMITS = Object.freeze({
|
|
540
|
+
maxFixtureBytes: 20 * 1024 * 1024,
|
|
541
|
+
hardFixtureBytes: 50 * 1024 * 1024,
|
|
542
|
+
maxTotalBytes: 1024 * 1024 * 1024,
|
|
543
|
+
});
|
package/src/tui/grid.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const ANSI_ESCAPE_PATTERN = `${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`;
|
|
2
|
+
const ANSI_ESCAPE = new RegExp(ANSI_ESCAPE_PATTERN, "g");
|
|
3
|
+
|
|
4
|
+
export function measureWidth(value) {
|
|
5
|
+
return String(value ?? "").replace(ANSI_ESCAPE, "").length;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function padCell(value, width, align = "left") {
|
|
9
|
+
const text = String(value ?? "");
|
|
10
|
+
const visible = measureWidth(text);
|
|
11
|
+
if (visible === width) return text;
|
|
12
|
+
if (visible > width) return truncateVisible(text, width);
|
|
13
|
+
const padding = " ".repeat(width - visible);
|
|
14
|
+
if (align === "right") return padding + text;
|
|
15
|
+
if (align === "center") {
|
|
16
|
+
const left = Math.floor(padding.length / 2);
|
|
17
|
+
return " ".repeat(left) + text + " ".repeat(padding.length - left);
|
|
18
|
+
}
|
|
19
|
+
return text + padding;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function truncateVisible(text, width) {
|
|
23
|
+
const tokens = text.split(new RegExp(`(${ANSI_ESCAPE_PATTERN})`, "g"));
|
|
24
|
+
let output = "";
|
|
25
|
+
let visible = 0;
|
|
26
|
+
for (const token of tokens) {
|
|
27
|
+
if (!token) continue;
|
|
28
|
+
if (token.startsWith(`${String.fromCharCode(27)}[`)) {
|
|
29
|
+
output += token;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const characters = Array.from(token);
|
|
33
|
+
const available = Math.max(0, width - visible);
|
|
34
|
+
output += characters.slice(0, available).join("");
|
|
35
|
+
visible += Math.min(characters.length, available);
|
|
36
|
+
if (characters.length > available) {
|
|
37
|
+
if (
|
|
38
|
+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`).test(output) &&
|
|
39
|
+
!output.endsWith(`${String.fromCharCode(27)}[0m`)
|
|
40
|
+
) {
|
|
41
|
+
output += `${String.fromCharCode(27)}[0m`;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderRow(cells, widths, alignments = []) {
|
|
50
|
+
return `| ${cells.map((cell, index) => padCell(cell, widths[index], alignments[index])).join(" | ")} |`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderSeparatorLine(widths) {
|
|
54
|
+
return `+-${widths.map((width) => "-".repeat(width)).join("-+-")}-+`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderGrid(rows, { headers = true, alignments = [] } = {}) {
|
|
58
|
+
if (!rows.length) return "";
|
|
59
|
+
const widths = rows[0].map((_, index) =>
|
|
60
|
+
Math.max(...rows.map((row) => measureWidth(row[index])), 1),
|
|
61
|
+
);
|
|
62
|
+
const lines = [renderSeparatorLine(widths), renderRow(rows[0], widths, alignments)];
|
|
63
|
+
if (headers) lines.push(renderSeparatorLine(widths));
|
|
64
|
+
lines.push(...rows.slice(1).map((row) => renderRow(row, widths, alignments)));
|
|
65
|
+
lines.push(renderSeparatorLine(widths));
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderGrid } from "./grid.js";
|
|
2
|
+
|
|
3
|
+
export function renderCheckGrid(statuses) {
|
|
4
|
+
return renderGrid([
|
|
5
|
+
["Status", "Check", "Detail"],
|
|
6
|
+
...statuses.map(({ kind, label, detail = "" }) => [kind, label, detail]),
|
|
7
|
+
]);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function renderReleaseGrid(release, nextTriggers = []) {
|
|
11
|
+
const rows = [
|
|
12
|
+
["Gate", "Status"],
|
|
13
|
+
["Idea", release.idea_status],
|
|
14
|
+
["Architecture", release.architecture_status],
|
|
15
|
+
["Evidence", release.evidence_status],
|
|
16
|
+
["Logic", release.logic_status],
|
|
17
|
+
["Security", release.security_status],
|
|
18
|
+
["Experience", release.experience_status],
|
|
19
|
+
["Build", release.build_status],
|
|
20
|
+
];
|
|
21
|
+
if (nextTriggers.length) rows.push(["Next", nextTriggers.join(", ")]);
|
|
22
|
+
return renderGrid(rows);
|
|
23
|
+
}
|