@kbediako/codex-orchestrator 0.1.1 → 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 (58) hide show
  1. package/README.md +11 -8
  2. package/dist/bin/codex-orchestrator.js +245 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/devtoolsSetup.js +66 -0
  5. package/dist/orchestrator/src/cli/doctor.js +46 -21
  6. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  7. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  8. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  9. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  10. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  11. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  12. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  13. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  14. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  15. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  16. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  17. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  18. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  19. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  20. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  21. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  22. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  23. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  24. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  25. package/dist/orchestrator/src/cli/utils/devtools.js +178 -0
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  27. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  28. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  29. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  30. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  31. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  32. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  33. package/dist/packages/orchestrator/src/index.js +1 -0
  34. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  35. package/dist/packages/shared/streams/stdio.js +2 -112
  36. package/dist/packages/shared/utils/strings.js +17 -0
  37. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  38. package/dist/scripts/design/pipeline/context.js +5 -5
  39. package/dist/scripts/design/pipeline/extract.js +9 -6
  40. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  41. package/dist/scripts/design/pipeline/permit.js +59 -0
  42. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  43. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  44. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  45. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  46. package/dist/scripts/lib/cli-args.js +53 -0
  47. package/dist/scripts/lib/docs-helpers.js +111 -0
  48. package/dist/scripts/lib/npm-pack.js +20 -0
  49. package/dist/scripts/lib/run-manifests.js +160 -0
  50. package/package.json +17 -6
  51. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  52. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  53. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  54. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  55. package/dist/orchestrator/src/control-plane/index.js +0 -3
  56. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  57. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  58. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -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'], {
@@ -0,0 +1,53 @@
1
+ export function parseArgs(argv) {
2
+ const args = {};
3
+ const positionals = [];
4
+ const entries = [];
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const raw = argv[index];
7
+ if (!raw) {
8
+ continue;
9
+ }
10
+ if (raw === '--') {
11
+ positionals.push(...argv.slice(index + 1));
12
+ break;
13
+ }
14
+ if (!raw.startsWith('-')) {
15
+ positionals.push(raw);
16
+ continue;
17
+ }
18
+ const cleaned = raw.replace(/^--?/, '');
19
+ if (!cleaned) {
20
+ continue;
21
+ }
22
+ const [key, inlineValue] = cleaned.split('=');
23
+ if (inlineValue !== undefined) {
24
+ args[key] = inlineValue;
25
+ entries.push({ key, value: inlineValue });
26
+ continue;
27
+ }
28
+ const next = argv[index + 1];
29
+ if (next && !next.startsWith('-')) {
30
+ args[key] = next;
31
+ entries.push({ key, value: next });
32
+ index += 1;
33
+ }
34
+ else {
35
+ args[key] = true;
36
+ entries.push({ key, value: true });
37
+ }
38
+ }
39
+ return { args, positionals, entries };
40
+ }
41
+ export function hasFlag(args, key) {
42
+ const value = args[key];
43
+ if (value === undefined) {
44
+ return false;
45
+ }
46
+ if (value === false) {
47
+ return false;
48
+ }
49
+ if (value === 'false' || value === '0') {
50
+ return false;
51
+ }
52
+ return true;
53
+ }
@@ -0,0 +1,111 @@
1
+ import { access, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const DOC_ROOTS = ['.agent', '.ai-dev-tasks', 'docs', 'tasks'];
4
+ const DOC_ROOT_FILES = ['README.md', 'AGENTS.md'];
5
+ const EXCLUDED_DIR_NAMES = new Set(['.runs', 'out', 'archives', 'node_modules', 'dist']);
6
+ export async function pathExists(target, options = {}) {
7
+ const { allowMissingOnly = false } = options;
8
+ try {
9
+ await access(target);
10
+ return true;
11
+ }
12
+ catch (error) {
13
+ if (allowMissingOnly && error?.code !== 'ENOENT') {
14
+ throw error;
15
+ }
16
+ return false;
17
+ }
18
+ }
19
+ export function toPosixPath(value) {
20
+ return value.split(path.sep).join('/');
21
+ }
22
+ export async function collectMarkdownFiles(repoRoot, relativeDir) {
23
+ const absDir = path.join(repoRoot, relativeDir);
24
+ const entries = await readdir(absDir, { withFileTypes: true });
25
+ const results = [];
26
+ for (const entry of entries) {
27
+ const relPath = path.join(relativeDir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (EXCLUDED_DIR_NAMES.has(entry.name)) {
30
+ continue;
31
+ }
32
+ results.push(...(await collectMarkdownFiles(repoRoot, relPath)));
33
+ continue;
34
+ }
35
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
36
+ results.push(toPosixPath(relPath));
37
+ }
38
+ }
39
+ return results;
40
+ }
41
+ export async function collectDocFiles(repoRoot) {
42
+ const results = [];
43
+ for (const file of DOC_ROOT_FILES) {
44
+ const abs = path.join(repoRoot, file);
45
+ if (await pathExists(abs)) {
46
+ results.push(toPosixPath(file));
47
+ }
48
+ }
49
+ for (const dir of DOC_ROOTS) {
50
+ const abs = path.join(repoRoot, dir);
51
+ if (await pathExists(abs)) {
52
+ results.push(...(await collectMarkdownFiles(repoRoot, dir)));
53
+ }
54
+ }
55
+ results.sort();
56
+ return results;
57
+ }
58
+ export function normalizeTaskKey(item) {
59
+ if (!item || typeof item !== 'object') {
60
+ return null;
61
+ }
62
+ const id = typeof item.id === 'string' ? item.id.trim() : '';
63
+ const slug = typeof item.slug === 'string' ? item.slug.trim() : '';
64
+ if (slug && id && slug.startsWith(`${id}-`)) {
65
+ return slug;
66
+ }
67
+ if (id && slug) {
68
+ return `${id}-${slug}`;
69
+ }
70
+ if (slug) {
71
+ return slug;
72
+ }
73
+ if (id) {
74
+ return id;
75
+ }
76
+ return null;
77
+ }
78
+ export function parseDateString(value) {
79
+ if (typeof value !== 'string') {
80
+ return null;
81
+ }
82
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
83
+ if (!match) {
84
+ return null;
85
+ }
86
+ return value;
87
+ }
88
+ export function parseIsoDate(raw) {
89
+ const normalized = parseDateString(raw);
90
+ if (!normalized) {
91
+ return null;
92
+ }
93
+ const [yearStr, monthStr, dayStr] = normalized.split('-');
94
+ const year = Number(yearStr);
95
+ const month = Number(monthStr) - 1;
96
+ const day = Number(dayStr);
97
+ const date = new Date(Date.UTC(year, month, day));
98
+ if (Number.isNaN(date.getTime())) {
99
+ return null;
100
+ }
101
+ if (date.getUTCFullYear() !== year ||
102
+ date.getUTCMonth() !== month ||
103
+ date.getUTCDate() !== day) {
104
+ return null;
105
+ }
106
+ return date;
107
+ }
108
+ export function computeAgeInDays(from, to) {
109
+ const msPerDay = 24 * 60 * 60 * 1000;
110
+ return Math.floor((to.getTime() - from.getTime()) / msPerDay);
111
+ }
@@ -0,0 +1,20 @@
1
+ import { execFile } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { promisify } from 'node:util';
4
+ const execFileAsync = promisify(execFile);
5
+ export async function runPack() {
6
+ const { stdout } = await execFileAsync('npm', ['pack', '--json', '--ignore-scripts'], {
7
+ env: { ...process.env, npm_config_ignore_scripts: 'true' },
8
+ maxBuffer: 10 * 1024 * 1024
9
+ });
10
+ const trimmed = String(stdout ?? '').trim();
11
+ if (!trimmed) {
12
+ throw new Error('npm pack produced no output');
13
+ }
14
+ const parsed = JSON.parse(trimmed);
15
+ const record = Array.isArray(parsed) ? parsed[0] : parsed;
16
+ if (!record || typeof record !== 'object') {
17
+ throw new Error('npm pack output did not include a record');
18
+ }
19
+ return record;
20
+ }