@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.
Files changed (55) hide show
  1. package/README.md +9 -7
  2. package/dist/bin/codex-orchestrator.js +214 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  5. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  6. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  7. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  8. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  9. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  10. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  11. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  12. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  13. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  14. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  15. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  16. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  17. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  18. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  19. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  20. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  21. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  22. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  23. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  24. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  25. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  26. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  27. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  28. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  29. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  30. package/dist/packages/orchestrator/src/index.js +1 -0
  31. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  32. package/dist/packages/shared/streams/stdio.js +2 -112
  33. package/dist/packages/shared/utils/strings.js +17 -0
  34. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  35. package/dist/scripts/design/pipeline/context.js +5 -5
  36. package/dist/scripts/design/pipeline/extract.js +9 -6
  37. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  38. package/dist/scripts/design/pipeline/permit.js +59 -0
  39. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  40. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  41. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  42. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  43. package/dist/scripts/lib/cli-args.js +53 -0
  44. package/dist/scripts/lib/docs-helpers.js +111 -0
  45. package/dist/scripts/lib/npm-pack.js +20 -0
  46. package/dist/scripts/lib/run-manifests.js +160 -0
  47. package/package.json +5 -2
  48. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  49. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  50. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  51. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  52. package/dist/orchestrator/src/control-plane/index.js +0 -3
  53. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  54. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  55. 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 '../../../shared/streams/stdio.js';
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 { mkdir, rename, writeFile } from 'node:fs/promises';
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
- 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
+ // 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 './optionalDeps.js';
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 = process.env.CODEX_ORCHESTRATOR_REPO_ROOT ?? process.cwd();
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 runId = process.env.CODEX_ORCHESTRATOR_RUN_ID ?? 'run-local';
12
- const runDir = process.env.CODEX_ORCHESTRATOR_RUN_DIR ?? join(runsRoot, taskId, sanitizeRunId(runId));
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 { loadPlaywright } from './optionalDeps.js';
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
- .toLowerCase()
237
- .replace(/[^a-z0-9]+/g, '-')
238
- .replace(/^-+|-+$/g, '')
239
- .slice(0, 60) || 'capture';
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
- const candidate = error;
8
- if (!candidate) {
7
+ if (!error) {
9
8
  return false;
10
9
  }
11
- const message = candidate.message ?? '';
12
- return (candidate.code === 'ERR_MODULE_NOT_FOUND' ||
13
- candidate.code === 'MODULE_NOT_FOUND' ||
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(`[design-tools] Missing optional dependency "${specifier}". ${DESIGN_SETUP_HINT}`);
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 (await import(toModuleUrl(resolved)));
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
- let playwrightPromise = null;
81
- let pngPromise = null;
82
- let pixelmatchPromise = null;
83
- let cheerioPromise = null;
84
- export async function loadPlaywright() {
85
- if (!playwrightPromise) {
86
- playwrightPromise = loadOptionalDependency('playwright');
87
- }
88
- return playwrightPromise;
89
- }
90
- export async function loadPngjs() {
91
- if (!pngPromise) {
92
- pngPromise = loadOptionalDependency('pngjs');
93
- }
94
- return pngPromise;
95
- }
96
- export async function loadPixelmatch() {
97
- if (!pixelmatchPromise) {
98
- pixelmatchPromise = loadOptionalDependency('pixelmatch');
99
- }
100
- return pixelmatchPromise;
101
- }
102
- export async function loadCheerio() {
103
- if (!cheerioPromise) {
104
- cheerioPromise = loadOptionalDependency('cheerio');
105
- }
106
- return cheerioPromise;
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 { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
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 permitPath = join(repoRoot, 'compliance', 'permit.json');
5
- try {
6
- const raw = await readFile(permitPath, 'utf8');
7
- return JSON.parse(raw);
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
- catch (error) {
10
- const nodeError = error;
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 = new Set((permit.allowedSources ?? [])
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
- .toLowerCase()
69
- .replace(/[^a-z0-9-_]+/g, '-')
70
- .replace(/^-+|-+$/g, '')
71
- .slice(0, 48);
72
- if (normalized.length > 0) {
73
- return normalized;
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 '../optionalDeps.js';
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 '../optionalDeps.js';
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 { access, mkdir, readFile, writeFile } from 'node:fs/promises';
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'], {