@kbediako/codex-orchestrator 0.1.2 → 0.1.3
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 +9 -7
- package/dist/bin/codex-orchestrator.js +214 -121
- package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
- package/dist/orchestrator/src/cli/exec/context.js +5 -2
- package/dist/orchestrator/src/cli/exec/learning.js +5 -3
- package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
- package/dist/orchestrator/src/cli/exec/summary.js +1 -1
- package/dist/orchestrator/src/cli/orchestrator.js +16 -7
- package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
- package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
- package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
- package/dist/orchestrator/src/cli/rlm/types.js +1 -0
- package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
- package/dist/orchestrator/src/cli/run/environment.js +4 -11
- package/dist/orchestrator/src/cli/run/manifest.js +7 -1
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
- package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
- package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
- package/dist/orchestrator/src/cli/utils/strings.js +8 -6
- package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
- package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
- package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
- package/dist/packages/orchestrator/src/index.js +1 -0
- package/dist/packages/shared/design-artifacts/writer.js +4 -14
- package/dist/packages/shared/streams/stdio.js +2 -112
- package/dist/packages/shared/utils/strings.js +17 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
- package/dist/scripts/design/pipeline/context.js +5 -5
- package/dist/scripts/design/pipeline/extract.js +9 -6
- package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
- package/dist/scripts/design/pipeline/permit.js +59 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
- package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
- package/dist/scripts/design/pipeline/visual-regression.js +2 -11
- package/dist/scripts/lib/cli-args.js +53 -0
- package/dist/scripts/lib/docs-helpers.js +111 -0
- package/dist/scripts/lib/npm-pack.js +20 -0
- package/dist/scripts/lib/run-manifests.js +160 -0
- package/package.json +5 -2
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
- package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
- package/dist/orchestrator/src/control-plane/index.js +0 -3
- package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
- package/dist/orchestrator/src/scheduler/index.js +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a tracker that sequences stdout/stderr chunks while maintaining
|
|
4
|
+
* bounded in-memory buffers for each stream.
|
|
5
|
+
*/
|
|
6
|
+
export function createStdioTracker(options = {}) {
|
|
7
|
+
const encoding = options.encoding ?? 'utf-8';
|
|
8
|
+
const maxBufferBytes = options.maxBufferBytes ?? 64 * 1024;
|
|
9
|
+
const now = options.now ?? (() => new Date());
|
|
10
|
+
let sequence = Math.max(0, options.startSequence ?? 0);
|
|
11
|
+
const buffers = {
|
|
12
|
+
stdout: createInternalBuffer(),
|
|
13
|
+
stderr: createInternalBuffer()
|
|
14
|
+
};
|
|
15
|
+
const push = (stream, chunk) => {
|
|
16
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
17
|
+
const target = buffers[stream];
|
|
18
|
+
appendWithLimit(target, buffer, maxBufferBytes);
|
|
19
|
+
const chunkRecord = {
|
|
20
|
+
sequence: ++sequence,
|
|
21
|
+
stream,
|
|
22
|
+
bytes: buffer.byteLength,
|
|
23
|
+
data: buffer.toString(encoding),
|
|
24
|
+
timestamp: now().toISOString()
|
|
25
|
+
};
|
|
26
|
+
return chunkRecord;
|
|
27
|
+
};
|
|
28
|
+
const getBuffered = (stream) => {
|
|
29
|
+
const target = buffers[stream];
|
|
30
|
+
if (target.byteLength === 0) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
const activeChunks = snapshotChunks(target);
|
|
34
|
+
if (activeChunks.length === 0) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
if (activeChunks.length === 1) {
|
|
38
|
+
return activeChunks[0].toString(encoding);
|
|
39
|
+
}
|
|
40
|
+
return Buffer.concat(activeChunks, target.byteLength).toString(encoding);
|
|
41
|
+
};
|
|
42
|
+
const getBufferedBytes = (stream) => buffers[stream].byteLength;
|
|
43
|
+
const reset = () => {
|
|
44
|
+
buffers.stdout = createInternalBuffer();
|
|
45
|
+
buffers.stderr = createInternalBuffer();
|
|
46
|
+
sequence = Math.max(0, options.startSequence ?? 0);
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
push,
|
|
50
|
+
getBuffered,
|
|
51
|
+
getBufferedBytes,
|
|
52
|
+
reset
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createInternalBuffer() {
|
|
56
|
+
return { chunks: [], start: 0, byteLength: 0 };
|
|
57
|
+
}
|
|
58
|
+
function snapshotChunks(buffer) {
|
|
59
|
+
if (buffer.start === 0) {
|
|
60
|
+
return buffer.chunks;
|
|
61
|
+
}
|
|
62
|
+
return buffer.chunks.slice(buffer.start);
|
|
63
|
+
}
|
|
64
|
+
function appendWithLimit(target, incoming, limit) {
|
|
65
|
+
if (limit <= 0) {
|
|
66
|
+
target.chunks = [];
|
|
67
|
+
target.start = 0;
|
|
68
|
+
target.byteLength = 0;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!incoming || incoming.byteLength === 0) {
|
|
72
|
+
trimExcess(target, limit);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (incoming.byteLength >= limit) {
|
|
76
|
+
const trimmed = Buffer.from(incoming.subarray(incoming.byteLength - limit));
|
|
77
|
+
target.chunks = [trimmed];
|
|
78
|
+
target.start = 0;
|
|
79
|
+
target.byteLength = trimmed.byteLength;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
target.chunks.push(incoming);
|
|
83
|
+
target.byteLength += incoming.byteLength;
|
|
84
|
+
trimExcess(target, limit);
|
|
85
|
+
}
|
|
86
|
+
function trimExcess(target, limit) {
|
|
87
|
+
if (target.byteLength <= limit) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let startIndex = target.start;
|
|
91
|
+
const chunks = target.chunks;
|
|
92
|
+
while (target.byteLength > limit && startIndex < chunks.length) {
|
|
93
|
+
const head = chunks[startIndex];
|
|
94
|
+
if (!head) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const overflow = target.byteLength - limit;
|
|
98
|
+
if (head.byteLength <= overflow) {
|
|
99
|
+
target.byteLength -= head.byteLength;
|
|
100
|
+
startIndex += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
chunks[startIndex] = head.subarray(overflow);
|
|
104
|
+
target.byteLength -= overflow;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
target.start = startIndex;
|
|
108
|
+
if (target.start > 0 && target.start * 2 >= chunks.length) {
|
|
109
|
+
target.chunks = chunks.slice(target.start);
|
|
110
|
+
target.start = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { ToolInvocationFailedError } from '../tool-orchestrator.js';
|
|
4
|
-
import { createStdioTracker } from '
|
|
4
|
+
import { createStdioTracker } from './stdio.js';
|
|
5
5
|
export class UnifiedExecRunner {
|
|
6
6
|
orchestrator;
|
|
7
7
|
sessionManager;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ToolOrchestrator, SandboxRetryableError, ApprovalRequiredError, ApprovalDeniedError, ToolInvocationFailedError } from './tool-orchestrator.js';
|
|
2
2
|
export { ExecSessionManager } from './exec/session-manager.js';
|
|
3
3
|
export { UnifiedExecRunner } from './exec/unified-exec.js';
|
|
4
|
+
export { RemoteExecHandleService } from './exec/handle-service.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join, relative as relativePath } from 'node:path';
|
|
1
|
+
import { join, relative as relativePath } from 'node:path';
|
|
3
2
|
import { persistDesignManifest } from '../manifest/writer.js';
|
|
3
|
+
import { writeJsonAtomic } from '../../../orchestrator/src/cli/utils/fs.js';
|
|
4
|
+
import { sanitizeTaskId } from '../../../orchestrator/src/persistence/sanitizeTaskId.js';
|
|
5
|
+
import { sanitizeRunId } from '../../../orchestrator/src/persistence/sanitizeRunId.js';
|
|
4
6
|
export async function writeDesignSummary(options) {
|
|
5
7
|
const now = options.now ?? new Date();
|
|
6
8
|
const context = options.context;
|
|
@@ -207,15 +209,3 @@ function accumulateNumericTotals(target, incoming) {
|
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
|
-
function sanitizeTaskId(value) {
|
|
211
|
-
return value.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase();
|
|
212
|
-
}
|
|
213
|
-
function sanitizeRunId(value) {
|
|
214
|
-
return value.replace(/[:]/g, '-');
|
|
215
|
-
}
|
|
216
|
-
async function writeJsonAtomic(targetPath, payload) {
|
|
217
|
-
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
218
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
219
|
-
await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
220
|
-
await rename(tmpPath, targetPath);
|
|
221
|
-
}
|
|
@@ -1,112 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Creates a tracker that sequences stdout/stderr chunks while maintaining
|
|
4
|
-
* bounded in-memory buffers for each stream.
|
|
5
|
-
*/
|
|
6
|
-
export function createStdioTracker(options = {}) {
|
|
7
|
-
const encoding = options.encoding ?? 'utf-8';
|
|
8
|
-
const maxBufferBytes = options.maxBufferBytes ?? 64 * 1024;
|
|
9
|
-
const now = options.now ?? (() => new Date());
|
|
10
|
-
let sequence = Math.max(0, options.startSequence ?? 0);
|
|
11
|
-
const buffers = {
|
|
12
|
-
stdout: createInternalBuffer(),
|
|
13
|
-
stderr: createInternalBuffer()
|
|
14
|
-
};
|
|
15
|
-
const push = (stream, chunk) => {
|
|
16
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
17
|
-
const target = buffers[stream];
|
|
18
|
-
appendWithLimit(target, buffer, maxBufferBytes);
|
|
19
|
-
const chunkRecord = {
|
|
20
|
-
sequence: ++sequence,
|
|
21
|
-
stream,
|
|
22
|
-
bytes: buffer.byteLength,
|
|
23
|
-
data: buffer.toString(encoding),
|
|
24
|
-
timestamp: now().toISOString()
|
|
25
|
-
};
|
|
26
|
-
return chunkRecord;
|
|
27
|
-
};
|
|
28
|
-
const getBuffered = (stream) => {
|
|
29
|
-
const target = buffers[stream];
|
|
30
|
-
if (target.byteLength === 0) {
|
|
31
|
-
return '';
|
|
32
|
-
}
|
|
33
|
-
const activeChunks = snapshotChunks(target);
|
|
34
|
-
if (activeChunks.length === 0) {
|
|
35
|
-
return '';
|
|
36
|
-
}
|
|
37
|
-
if (activeChunks.length === 1) {
|
|
38
|
-
return activeChunks[0].toString(encoding);
|
|
39
|
-
}
|
|
40
|
-
return Buffer.concat(activeChunks, target.byteLength).toString(encoding);
|
|
41
|
-
};
|
|
42
|
-
const getBufferedBytes = (stream) => buffers[stream].byteLength;
|
|
43
|
-
const reset = () => {
|
|
44
|
-
buffers.stdout = createInternalBuffer();
|
|
45
|
-
buffers.stderr = createInternalBuffer();
|
|
46
|
-
sequence = Math.max(0, options.startSequence ?? 0);
|
|
47
|
-
};
|
|
48
|
-
return {
|
|
49
|
-
push,
|
|
50
|
-
getBuffered,
|
|
51
|
-
getBufferedBytes,
|
|
52
|
-
reset
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function createInternalBuffer() {
|
|
56
|
-
return { chunks: [], start: 0, byteLength: 0 };
|
|
57
|
-
}
|
|
58
|
-
function snapshotChunks(buffer) {
|
|
59
|
-
if (buffer.start === 0) {
|
|
60
|
-
return buffer.chunks;
|
|
61
|
-
}
|
|
62
|
-
return buffer.chunks.slice(buffer.start);
|
|
63
|
-
}
|
|
64
|
-
function appendWithLimit(target, incoming, limit) {
|
|
65
|
-
if (limit <= 0) {
|
|
66
|
-
target.chunks = [];
|
|
67
|
-
target.start = 0;
|
|
68
|
-
target.byteLength = 0;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (!incoming || incoming.byteLength === 0) {
|
|
72
|
-
trimExcess(target, limit);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (incoming.byteLength >= limit) {
|
|
76
|
-
const trimmed = Buffer.from(incoming.subarray(incoming.byteLength - limit));
|
|
77
|
-
target.chunks = [trimmed];
|
|
78
|
-
target.start = 0;
|
|
79
|
-
target.byteLength = trimmed.byteLength;
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
target.chunks.push(incoming);
|
|
83
|
-
target.byteLength += incoming.byteLength;
|
|
84
|
-
trimExcess(target, limit);
|
|
85
|
-
}
|
|
86
|
-
function trimExcess(target, limit) {
|
|
87
|
-
if (target.byteLength <= limit) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
let startIndex = target.start;
|
|
91
|
-
const chunks = target.chunks;
|
|
92
|
-
while (target.byteLength > limit && startIndex < chunks.length) {
|
|
93
|
-
const head = chunks[startIndex];
|
|
94
|
-
if (!head) {
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
const overflow = target.byteLength - limit;
|
|
98
|
-
if (head.byteLength <= overflow) {
|
|
99
|
-
target.byteLength -= head.byteLength;
|
|
100
|
-
startIndex += 1;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
chunks[startIndex] = head.subarray(overflow);
|
|
104
|
-
target.byteLength -= overflow;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
target.start = startIndex;
|
|
108
|
-
if (target.start > 0 && target.start * 2 >= chunks.length) {
|
|
109
|
-
target.chunks = chunks.slice(target.start);
|
|
110
|
-
target.start = 0;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
1
|
+
// Deprecated shim: keep exports stable while stdio tracking moves into packages/orchestrator.
|
|
2
|
+
export * from '../../orchestrator/src/exec/stdio.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function slugify(value, options = {}) {
|
|
2
|
+
const fallback = typeof options.fallback === 'string' ? options.fallback : 'command';
|
|
3
|
+
const maxLength = Number.isFinite(options.maxLength)
|
|
4
|
+
? Math.max(1, Math.floor(options.maxLength))
|
|
5
|
+
: 80;
|
|
6
|
+
const lowercase = options.lowercase ?? false;
|
|
7
|
+
const pattern = options.pattern ?? /[^a-zA-Z0-9]+/g;
|
|
8
|
+
const collapseDashes = options.collapseDashes ?? true;
|
|
9
|
+
const base = lowercase ? value.toLowerCase() : value;
|
|
10
|
+
const cleaned = base.trim().replace(pattern, '-');
|
|
11
|
+
const collapsed = collapseDashes ? cleaned.replace(/-+/g, '-') : cleaned;
|
|
12
|
+
const normalized = collapsed.replace(/^-+|-+$/g, '');
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
return normalized.slice(0, maxLength);
|
|
17
|
+
}
|
|
@@ -8,7 +8,7 @@ import { loadDesignContext } from './context.js';
|
|
|
8
8
|
import { appendApprovals, appendArtifacts, ensureToolkitState, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
|
|
9
9
|
import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
10
10
|
import { runDefaultInteractions } from './toolkit/snapshot.js';
|
|
11
|
-
import { loadPlaywright } from './
|
|
11
|
+
import { loadPlaywright } from './optional-deps.js';
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const MOTION_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
14
14
|
const DEFAULT_CAPTURE_SECONDS = 12;
|
|
@@ -3,13 +3,13 @@ import { mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { loadDesignConfig, designPipelineId } from '../../../packages/shared/config/index.js';
|
|
4
4
|
import { sanitizeTaskId } from '../../../orchestrator/src/persistence/sanitizeTaskId.js';
|
|
5
5
|
import { sanitizeRunId } from '../../../orchestrator/src/persistence/sanitizeRunId.js';
|
|
6
|
+
import { resolveEnvironmentPaths } from '../../lib/run-manifests.js';
|
|
6
7
|
export async function loadDesignContext() {
|
|
7
|
-
const repoRoot
|
|
8
|
-
const runsRoot = process.env.CODEX_ORCHESTRATOR_RUNS_DIR ?? join(repoRoot, '.runs');
|
|
9
|
-
const outRoot = process.env.CODEX_ORCHESTRATOR_OUT_DIR ?? join(repoRoot, 'out');
|
|
8
|
+
const { repoRoot, runsRoot, outRoot } = resolveEnvironmentPaths();
|
|
10
9
|
const taskId = sanitizeTaskId(process.env.CODEX_ORCHESTRATOR_TASK_ID ?? process.env.MCP_RUNNER_TASK_ID ?? 'unknown-task');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const rawRunId = process.env.CODEX_ORCHESTRATOR_RUN_ID ?? 'run-local';
|
|
11
|
+
const runId = sanitizeRunId(rawRunId);
|
|
12
|
+
const runDir = process.env.CODEX_ORCHESTRATOR_RUN_DIR ?? join(runsRoot, taskId, runId);
|
|
13
13
|
const manifestPath = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH ?? join(runDir, 'manifest.json');
|
|
14
14
|
const designConfigPath = process.env.DESIGN_CONFIG_PATH ?? join(repoRoot, 'design.config.yaml');
|
|
15
15
|
const config = await loadDesignConfig({ rootDir: repoRoot, filePath: designConfigPath });
|
|
@@ -4,7 +4,8 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { loadDesignContext } from './context.js';
|
|
5
5
|
import { appendArtifacts, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
|
|
6
6
|
import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
7
|
-
import {
|
|
7
|
+
import { slugify as sharedSlugify } from '../../../packages/shared/utils/strings.js';
|
|
8
|
+
import { loadPlaywright } from './optional-deps.js';
|
|
8
9
|
async function main() {
|
|
9
10
|
const context = await loadDesignContext();
|
|
10
11
|
const state = await loadDesignRunState(context.statePath);
|
|
@@ -232,11 +233,13 @@ function defaultBreakpoints() {
|
|
|
232
233
|
];
|
|
233
234
|
}
|
|
234
235
|
function slugify(value) {
|
|
235
|
-
return value
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
return sharedSlugify(value, {
|
|
237
|
+
fallback: 'capture',
|
|
238
|
+
maxLength: 60,
|
|
239
|
+
lowercase: true,
|
|
240
|
+
pattern: /[^a-z0-9]+/g,
|
|
241
|
+
collapseDashes: true
|
|
242
|
+
});
|
|
240
243
|
}
|
|
241
244
|
function sanitizeSegment(value) {
|
|
242
245
|
const slug = slugify(value);
|
|
@@ -4,18 +4,17 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
const DESIGN_SETUP_HINT = 'Run "npm run setup:design-tools" and "npx playwright install" to enable design tooling.';
|
|
6
6
|
function isModuleNotFound(error) {
|
|
7
|
-
|
|
8
|
-
if (!candidate) {
|
|
7
|
+
if (!error) {
|
|
9
8
|
return false;
|
|
10
9
|
}
|
|
11
|
-
const message =
|
|
12
|
-
return (
|
|
13
|
-
|
|
10
|
+
const message = error.message ?? '';
|
|
11
|
+
return (error.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
12
|
+
error.code === 'MODULE_NOT_FOUND' ||
|
|
14
13
|
message.includes('Cannot find package') ||
|
|
15
14
|
message.includes('Cannot find module'));
|
|
16
15
|
}
|
|
17
|
-
function missingDependency(specifier) {
|
|
18
|
-
return new Error(`[
|
|
16
|
+
function missingDependency(specifier, label, hint) {
|
|
17
|
+
return new Error(`[${label}] Missing optional dependency "${specifier}". ${hint}`);
|
|
19
18
|
}
|
|
20
19
|
function resolveWithRequire(specifier, base) {
|
|
21
20
|
try {
|
|
@@ -62,46 +61,58 @@ function toModuleUrl(resolved) {
|
|
|
62
61
|
}
|
|
63
62
|
return pathToFileURL(resolved).href;
|
|
64
63
|
}
|
|
65
|
-
async function loadOptionalDependency(specifier) {
|
|
64
|
+
async function loadOptionalDependency(specifier, label, hint) {
|
|
66
65
|
const resolved = resolveOptionalDependency(specifier);
|
|
67
66
|
if (!resolved) {
|
|
68
|
-
throw missingDependency(specifier);
|
|
67
|
+
throw missingDependency(specifier, label, hint);
|
|
69
68
|
}
|
|
70
69
|
try {
|
|
71
|
-
return
|
|
70
|
+
return await import(toModuleUrl(resolved));
|
|
72
71
|
}
|
|
73
72
|
catch (error) {
|
|
74
73
|
if (isModuleNotFound(error)) {
|
|
75
|
-
throw missingDependency(specifier);
|
|
74
|
+
throw missingDependency(specifier, label, hint);
|
|
76
75
|
}
|
|
77
76
|
throw error;
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
79
|
+
export function createOptionalDependencyLoader({ label, hint }) {
|
|
80
|
+
let playwrightPromise = null;
|
|
81
|
+
let pngPromise = null;
|
|
82
|
+
let pixelmatchPromise = null;
|
|
83
|
+
let cheerioPromise = null;
|
|
84
|
+
return {
|
|
85
|
+
async loadPlaywright() {
|
|
86
|
+
if (!playwrightPromise) {
|
|
87
|
+
playwrightPromise = loadOptionalDependency('playwright', label, hint);
|
|
88
|
+
}
|
|
89
|
+
return playwrightPromise;
|
|
90
|
+
},
|
|
91
|
+
async loadPngjs() {
|
|
92
|
+
if (!pngPromise) {
|
|
93
|
+
pngPromise = loadOptionalDependency('pngjs', label, hint);
|
|
94
|
+
}
|
|
95
|
+
return pngPromise;
|
|
96
|
+
},
|
|
97
|
+
async loadPixelmatch() {
|
|
98
|
+
if (!pixelmatchPromise) {
|
|
99
|
+
pixelmatchPromise = loadOptionalDependency('pixelmatch', label, hint);
|
|
100
|
+
}
|
|
101
|
+
return pixelmatchPromise;
|
|
102
|
+
},
|
|
103
|
+
async loadCheerio() {
|
|
104
|
+
if (!cheerioPromise) {
|
|
105
|
+
cheerioPromise = loadOptionalDependency('cheerio', label, hint);
|
|
106
|
+
}
|
|
107
|
+
return cheerioPromise;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
107
110
|
}
|
|
111
|
+
const designLoader = createOptionalDependencyLoader({
|
|
112
|
+
label: 'design-tools',
|
|
113
|
+
hint: DESIGN_SETUP_HINT
|
|
114
|
+
});
|
|
115
|
+
export const loadPlaywright = designLoader.loadPlaywright;
|
|
116
|
+
export const loadPngjs = designLoader.loadPngjs;
|
|
117
|
+
export const loadPixelmatch = designLoader.loadPixelmatch;
|
|
118
|
+
export const loadCheerio = designLoader.loadCheerio;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function loadPermitFile(repoRoot) {
|
|
4
|
+
const permitPath = join(repoRoot, 'compliance', 'permit.json');
|
|
5
|
+
try {
|
|
6
|
+
const raw = await readFile(permitPath, 'utf8');
|
|
7
|
+
return { status: 'found', permit: JSON.parse(raw), path: permitPath, error: null };
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error?.code === 'ENOENT') {
|
|
11
|
+
return { status: 'missing', permit: { allowedSources: [] }, path: permitPath, error: null };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
status: 'error',
|
|
15
|
+
permit: null,
|
|
16
|
+
path: permitPath,
|
|
17
|
+
error: error?.message ?? String(error)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function buildAllowedOriginSet(permit) {
|
|
22
|
+
const allowed = new Set();
|
|
23
|
+
const sources = Array.isArray(permit?.allowedSources) ? permit.allowedSources : [];
|
|
24
|
+
for (const entry of sources) {
|
|
25
|
+
if (!entry || typeof entry.origin !== 'string') {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
allowed.add(new URL(entry.origin).origin);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return allowed;
|
|
36
|
+
}
|
|
37
|
+
export function findPermitEntry(permit, origin) {
|
|
38
|
+
const sources = Array.isArray(permit?.allowedSources) ? permit.allowedSources : [];
|
|
39
|
+
let originKey = origin;
|
|
40
|
+
try {
|
|
41
|
+
originKey = new URL(origin).origin;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore invalid origin
|
|
45
|
+
}
|
|
46
|
+
return (sources.find((entry) => entry?.origin === originKey) ??
|
|
47
|
+
sources.find((entry) => {
|
|
48
|
+
if (!entry?.origin) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return new URL(entry.origin).origin === originKey;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}) ??
|
|
58
|
+
null);
|
|
59
|
+
}
|
|
@@ -1,31 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { buildAllowedOriginSet, loadPermitFile } from '../permit.js';
|
|
2
|
+
import { slugify as sharedSlugify } from '../../../../packages/shared/utils/strings.js';
|
|
3
3
|
export async function loadToolkitPermit(repoRoot) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return
|
|
4
|
+
const permitResult = await loadPermitFile(repoRoot);
|
|
5
|
+
if (permitResult.status === 'missing') {
|
|
6
|
+
console.warn('[design-toolkit] compliance/permit.json not found; proceeding without permit enforcement');
|
|
7
|
+
return { allowedSources: [] };
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (nodeError?.code === 'ENOENT') {
|
|
12
|
-
console.warn('[design-toolkit] compliance/permit.json not found; proceeding without permit enforcement');
|
|
13
|
-
return { allowedSources: [] };
|
|
14
|
-
}
|
|
15
|
-
throw error;
|
|
9
|
+
if (permitResult.status === 'error') {
|
|
10
|
+
throw new Error(permitResult.error ?? 'Unable to read compliance/permit.json');
|
|
16
11
|
}
|
|
12
|
+
return permitResult.permit ?? { allowedSources: [] };
|
|
17
13
|
}
|
|
18
14
|
export function ensureSourcePermitted(url, permit) {
|
|
19
|
-
const allowed =
|
|
20
|
-
.map((entry) => {
|
|
21
|
-
try {
|
|
22
|
-
return new URL(entry.origin).origin;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
.filter((origin) => Boolean(origin)));
|
|
15
|
+
const allowed = buildAllowedOriginSet(permit);
|
|
29
16
|
const origin = new URL(url).origin;
|
|
30
17
|
if (allowed.size === 0 || allowed.has(origin)) {
|
|
31
18
|
return true;
|
|
@@ -64,15 +51,14 @@ export function buildRetentionMetadata(retention, now) {
|
|
|
64
51
|
};
|
|
65
52
|
}
|
|
66
53
|
export function slugifyToolkitValue(value, index) {
|
|
67
|
-
const normalized = value
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
return `source-${index + 1}`;
|
|
54
|
+
const normalized = sharedSlugify(value, {
|
|
55
|
+
fallback: '',
|
|
56
|
+
maxLength: 48,
|
|
57
|
+
lowercase: true,
|
|
58
|
+
pattern: /[^a-z0-9-_]+/g,
|
|
59
|
+
collapseDashes: false
|
|
60
|
+
});
|
|
61
|
+
return normalized.length > 0 ? normalized : `source-${index + 1}`;
|
|
76
62
|
}
|
|
77
63
|
function defaultBreakpoints() {
|
|
78
64
|
return [
|
|
@@ -2,7 +2,7 @@ import { access, cp, mkdir, readFile, readdir, symlink, writeFile } from 'node:f
|
|
|
2
2
|
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { loadPixelmatch, loadPlaywright, loadPngjs } from '../
|
|
5
|
+
import { loadPixelmatch, loadPlaywright, loadPngjs } from '../optional-deps.js';
|
|
6
6
|
import { loadDesignContext } from '../context.js';
|
|
7
7
|
import { appendApprovals, appendToolkitArtifacts, ensureToolkitState, loadDesignRunState, saveDesignRunState, upsertStage, upsertToolkitContext } from '../state.js';
|
|
8
8
|
import { stageArtifacts } from '../../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { loadCheerio, loadPlaywright } from '../
|
|
2
|
+
import { loadCheerio, loadPlaywright } from '../optional-deps.js';
|
|
3
3
|
const DEFAULT_MAX_STYLESHEETS = 24;
|
|
4
4
|
const DEFAULT_VIEWPORT = { width: 1440, height: 900 };
|
|
5
5
|
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { constants } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { join, relative } from 'node:path';
|
|
5
4
|
import { tmpdir } from 'node:os';
|
|
6
5
|
import { loadDesignContext } from './context.js';
|
|
7
6
|
import { appendArtifacts, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
|
|
8
7
|
import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
8
|
+
import { pathExists } from '../../lib/docs-helpers.js';
|
|
9
9
|
const DESIGN_SYSTEM_DIR = 'packages/design-system';
|
|
10
10
|
const SUMMARY_FILE = join(DESIGN_SYSTEM_DIR, '.codex', 'visual-regression-summary.json');
|
|
11
11
|
async function main() {
|
|
@@ -75,15 +75,6 @@ async function main() {
|
|
|
75
75
|
const statusText = exitCode === 0 ? 'passed' : 'failed';
|
|
76
76
|
console.log(`[design-visual-regression] ${statusText}; summary staged at ${staged.path}`);
|
|
77
77
|
}
|
|
78
|
-
async function pathExists(path) {
|
|
79
|
-
try {
|
|
80
|
-
await access(path, constants.F_OK);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
78
|
async function runVisualRegression() {
|
|
88
79
|
return new Promise((resolve) => {
|
|
89
80
|
const child = spawn('npm', ['--prefix', DESIGN_SYSTEM_DIR, 'run', 'test:visual'], {
|