@pithyjs/codex 0.1.0-beta.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4401 -0
  3. package/dist/annotations.d.ts +106 -0
  4. package/dist/annotations.js +306 -0
  5. package/dist/apply.d.ts +2 -0
  6. package/dist/apply.js +80 -0
  7. package/dist/changed-scope.d.ts +23 -0
  8. package/dist/changed-scope.js +31 -0
  9. package/dist/check.d.ts +2 -0
  10. package/dist/check.js +117 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/env.d.ts +5 -0
  14. package/dist/env.js +35 -0
  15. package/dist/extract-cli.d.ts +23 -0
  16. package/dist/extract-cli.js +192 -0
  17. package/dist/extraction/breaking-changes.d.ts +66 -0
  18. package/dist/extraction/breaking-changes.js +352 -0
  19. package/dist/extraction/example-extractor.d.ts +55 -0
  20. package/dist/extraction/example-extractor.js +272 -0
  21. package/dist/extraction/index.d.ts +27 -0
  22. package/dist/extraction/index.js +31 -0
  23. package/dist/extraction/jsdoc-parser.d.ts +43 -0
  24. package/dist/extraction/jsdoc-parser.js +274 -0
  25. package/dist/extraction/pipeline.d.ts +54 -0
  26. package/dist/extraction/pipeline.js +526 -0
  27. package/dist/extraction/readme-sync.d.ts +108 -0
  28. package/dist/extraction/readme-sync.js +592 -0
  29. package/dist/extraction/snapshot-store.d.ts +40 -0
  30. package/dist/extraction/snapshot-store.js +153 -0
  31. package/dist/extraction/source-linker.d.ts +80 -0
  32. package/dist/extraction/source-linker.js +316 -0
  33. package/dist/extraction/test-example-extractor.d.ts +69 -0
  34. package/dist/extraction/test-example-extractor.js +400 -0
  35. package/dist/extraction/test-pattern-extractor.d.ts +68 -0
  36. package/dist/extraction/test-pattern-extractor.js +261 -0
  37. package/dist/extraction/testing-pyramid.d.ts +44 -0
  38. package/dist/extraction/testing-pyramid.js +163 -0
  39. package/dist/extraction/type-extractor.d.ts +34 -0
  40. package/dist/extraction/type-extractor.js +494 -0
  41. package/dist/extraction/types.d.ts +401 -0
  42. package/dist/extraction/types.js +34 -0
  43. package/dist/indexer.d.ts +1 -0
  44. package/dist/indexer.js +107 -0
  45. package/dist/llm.d.ts +8 -0
  46. package/dist/llm.js +75 -0
  47. package/dist/readme-sync-cli.d.ts +20 -0
  48. package/dist/readme-sync-cli.js +167 -0
  49. package/dist/review.d.ts +2 -0
  50. package/dist/review.js +93 -0
  51. package/dist/scan.d.ts +29 -0
  52. package/dist/scan.js +221 -0
  53. package/dist/schema.d.ts +169 -0
  54. package/dist/schema.js +70 -0
  55. package/dist/snapshot-cli.d.ts +45 -0
  56. package/dist/snapshot-cli.js +217 -0
  57. package/dist/sync-cli.d.ts +22 -0
  58. package/dist/sync-cli.js +154 -0
  59. package/dist/sync-pipeline.d.ts +58 -0
  60. package/dist/sync-pipeline.js +104 -0
  61. package/dist/sync.d.ts +2 -0
  62. package/dist/sync.js +318 -0
  63. package/dist/validate-cli.d.ts +20 -0
  64. package/dist/validate-cli.js +144 -0
  65. package/dist/validate.d.ts +76 -0
  66. package/dist/validate.js +183 -0
  67. package/dist/watch-cli.d.ts +21 -0
  68. package/dist/watch-cli.js +220 -0
  69. package/package.json +62 -0
package/dist/schema.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.schema",
5
+ * "title": "Entry Schema",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Zod validation schemas for codex entries.
10
+ */
11
+ import { z } from 'zod';
12
+ /** Zod schema for a requirement ID (RID) with text and criticality flag. */
13
+ const Rid = z.object({
14
+ id: z.string().min(1), // e.g. RID-FOR-001
15
+ text: z.string().min(1), // human readable requirement
16
+ critical: z.boolean().default(false), // CI may require coverage for critical ones
17
+ });
18
+ /** Zod schema for expected test coverage across testing levels. */
19
+ const TestsExpected = z.object({
20
+ unit: z.array(z.string()).default([]), // list of RID ids
21
+ integration: z.array(z.string()).default([]),
22
+ e2e: z.array(z.string()).default([]),
23
+ property: z.array(z.string()).default([]),
24
+ contract: z.array(z.string()).default([]),
25
+ });
26
+ /** Zod schema for an API signature entry with name, stability, and optional signature.
27
+ * @codexApi {"parent":"pithy.codex.schema","name":"ApiSig","stability":"stable","signature":"z.ZodObject<{ name: string; stability: string; signature?: string }>"} */
28
+ export const ApiSig = z.object({
29
+ name: z.string(),
30
+ // 'internal' marks exported-but-not-public APIs (e.g. helpers not in the
31
+ // package barrel). Without it here, the indexer's schema validation silently
32
+ // drops any atom whose API uses stability:"internal" from codex.index.json.
33
+ stability: z
34
+ .enum(['internal', 'experimental', 'stable', 'deprecated'])
35
+ .default('stable'),
36
+ signature: z.string().optional(),
37
+ });
38
+ /** Zod schema defining the full structure of a codex entry.
39
+ * @codexApi {"parent":"pithy.codex.schema","name":"codexSchema","stability":"stable","signature":"z.ZodObject<CodexEntry>"} */
40
+ export const codexSchema = z.object({
41
+ id: z.string().min(1),
42
+ title: z.string().min(1),
43
+ category: z.string().min(1), // e.g. "feature", "directive", "runtime"
44
+ tags: z.array(z.string()).default([]),
45
+ updated: z.string().datetime(), // keep your field
46
+ // additions for docs/testing
47
+ public: z.boolean().default(true),
48
+ status: z
49
+ .enum(['draft', 'experimental', 'stable', 'deprecated'])
50
+ .default('draft'),
51
+ version: z.string().default('1.0'),
52
+ owner: z.string().optional(), // e.g. "core@pithy"
53
+ related: z.array(z.string()).default([]), // other entry ids
54
+ dependsOn: z.array(z.string()).default([]), // stronger relation for impact analysis
55
+ apis: z.array(ApiSig).default([]),
56
+ // testing & traceability
57
+ rids: z.array(Rid).default([]),
58
+ acceptance: z.array(z.string()).default([]),
59
+ tests_expected: TestsExpected.default({}),
60
+ // optional free-form description (if you don't store content in md yet)
61
+ summary: z.string().optional(),
62
+ // future: point to .md body (front-matter) if you switch to md
63
+ bodyPath: z.string().optional(),
64
+ // change detection metadata
65
+ meta: z
66
+ .object({
67
+ hash: z.string().optional(), // content hash for change detection
68
+ })
69
+ .optional(),
70
+ });
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.snapshot-cli",
6
+ * "title": "Snapshot CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * CLI for managing API version snapshots.
11
+ *
12
+ * Usage:
13
+ * pithy-codex-snapshot <command> [options]
14
+ *
15
+ * Commands:
16
+ * save Create and save a snapshot for the current version
17
+ * list List all stored snapshot versions
18
+ * diff Diff two snapshot versions
19
+ */
20
+ export interface CliOptions {
21
+ command: 'save' | 'list' | 'diff' | 'help';
22
+ root: string;
23
+ versionsDir: string;
24
+ version?: string;
25
+ fromVersion?: string;
26
+ toVersion?: string;
27
+ verbose: boolean;
28
+ }
29
+ /**
30
+ * Consume the next argument as a value for an option flag.
31
+ * Throws if the value is missing or looks like another flag.
32
+ *
33
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"consumeValue","stability":"stable","signature":"(args: string[], index: number, flag: string) => string"}
34
+ */
35
+ export declare function consumeValue(args: string[], index: number, flag: string): string;
36
+ /**
37
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"parseArgs","stability":"stable","signature":"(args: string[]) => CliOptions"}
38
+ */
39
+ export declare function parseArgs(args: string[]): CliOptions;
40
+ /**
41
+ * Validates and normalizes a path to prevent path traversal attacks
42
+ *
43
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"validatePath","stability":"stable","signature":"(basePath: string, inputPath: string) => string"}
44
+ */
45
+ export declare function validatePath(basePath: string, inputPath: string): string;
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.snapshot-cli",
6
+ * "title": "Snapshot CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * CLI for managing API version snapshots.
11
+ *
12
+ * Usage:
13
+ * pithy-codex-snapshot <command> [options]
14
+ *
15
+ * Commands:
16
+ * save Create and save a snapshot for the current version
17
+ * list List all stored snapshot versions
18
+ * diff Diff two snapshot versions
19
+ */
20
+ import { resolve, relative, isAbsolute, sep } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { runExtractionPipeline, createApiSnapshot, diffSnapshots, detectBreakingChanges, generateChangelog, generateMigrationGuide, } from './extraction/index.js';
23
+ import { saveSnapshot, loadSnapshot, listSnapshots, } from './extraction/snapshot-store.js';
24
+ const DEFAULT_VERSIONS_DIR = 'codex.versions';
25
+ /**
26
+ * Consume the next argument as a value for an option flag.
27
+ * Throws if the value is missing or looks like another flag.
28
+ *
29
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"consumeValue","stability":"stable","signature":"(args: string[], index: number, flag: string) => string"}
30
+ */
31
+ export function consumeValue(args, index, flag) {
32
+ const value = args[index];
33
+ if (value === undefined || value.startsWith('-')) {
34
+ throw new Error(`Missing value for ${flag}`);
35
+ }
36
+ return value;
37
+ }
38
+ /**
39
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"parseArgs","stability":"stable","signature":"(args: string[]) => CliOptions"}
40
+ */
41
+ export function parseArgs(args) {
42
+ const options = {
43
+ command: 'help',
44
+ root: process.cwd(),
45
+ versionsDir: DEFAULT_VERSIONS_DIR,
46
+ verbose: false,
47
+ };
48
+ const VALID_COMMANDS = new Set(['save', 'list', 'diff', 'help']);
49
+ if (args.length > 0 && !args[0].startsWith('-')) {
50
+ if (VALID_COMMANDS.has(args[0])) {
51
+ options.command = args[0];
52
+ }
53
+ }
54
+ for (let i = 0; i < args.length; i++) {
55
+ const arg = args[i];
56
+ switch (arg) {
57
+ case '--root':
58
+ options.root = consumeValue(args, ++i, '--root');
59
+ break;
60
+ case '--dir':
61
+ options.versionsDir = consumeValue(args, ++i, '--dir');
62
+ break;
63
+ case '--version':
64
+ options.version = consumeValue(args, ++i, '--version');
65
+ break;
66
+ case '--from':
67
+ options.fromVersion = consumeValue(args, ++i, '--from');
68
+ break;
69
+ case '--to':
70
+ options.toVersion = consumeValue(args, ++i, '--to');
71
+ break;
72
+ case '--verbose':
73
+ case '-v':
74
+ options.verbose = true;
75
+ break;
76
+ case '--help':
77
+ case '-h':
78
+ options.command = 'help';
79
+ break;
80
+ }
81
+ }
82
+ return options;
83
+ }
84
+ function printHelp() {
85
+ console.log(`
86
+ Codex API Snapshot Manager
87
+
88
+ Usage:
89
+ pithy-codex-snapshot <command> [options]
90
+
91
+ Commands:
92
+ save Save a snapshot for the current version
93
+ list List all stored snapshots
94
+ diff Compare two snapshot versions
95
+
96
+ Options:
97
+ --root <path> Project root directory (default: current working directory)
98
+ --dir <path> Versions directory (default: codex.versions)
99
+ --version <ver> Version to save (required for 'save')
100
+ --from <ver> From version for diff
101
+ --to <ver> To version for diff
102
+ --verbose, -v Show detailed progress
103
+ --help, -h Show this help message
104
+
105
+ Examples:
106
+ # Save a snapshot
107
+ pithy-codex-snapshot save --version 1.0.0
108
+
109
+ # List all snapshots
110
+ pithy-codex-snapshot list
111
+
112
+ # Diff two versions
113
+ pithy-codex-snapshot diff --from 1.0.0 --to 2.0.0
114
+ `);
115
+ }
116
+ /**
117
+ * Validates and normalizes a path to prevent path traversal attacks
118
+ *
119
+ * @codexApi {"parent":"pithy.codex.snapshot-cli","name":"validatePath","stability":"stable","signature":"(basePath: string, inputPath: string) => string"}
120
+ */
121
+ export function validatePath(basePath, inputPath) {
122
+ const resolvedBase = resolve(basePath);
123
+ const resolvedPath = isAbsolute(inputPath)
124
+ ? resolve(inputPath)
125
+ : resolve(basePath, inputPath);
126
+ const relativePath = relative(resolvedBase, resolvedPath);
127
+ if (relativePath.startsWith('..' + sep) ||
128
+ relativePath === '..' ||
129
+ isAbsolute(relativePath)) {
130
+ throw new Error(`Security error: Path "${inputPath}" escapes the project root directory`);
131
+ }
132
+ return resolvedPath;
133
+ }
134
+ async function main() {
135
+ const args = process.argv.slice(2);
136
+ const options = parseArgs(args);
137
+ if (options.command === 'help') {
138
+ printHelp();
139
+ return;
140
+ }
141
+ options.root = resolve(options.root);
142
+ const versionsDir = validatePath(options.root, options.versionsDir);
143
+ switch (options.command) {
144
+ case 'save': {
145
+ if (!options.version) {
146
+ console.error("āŒ Error: --version is required for 'save' command");
147
+ process.exit(1);
148
+ }
149
+ if (options.verbose) {
150
+ console.log(`šŸ“ø Creating API snapshot for version ${options.version}...`);
151
+ }
152
+ const result = await runExtractionPipeline({ root: options.root });
153
+ const snapshot = createApiSnapshot(result, options.version);
154
+ await saveSnapshot(snapshot, versionsDir);
155
+ console.log(`āœ… Snapshot saved: ${versionsDir}/${options.version}.json (${snapshot.entries.length} entries)`);
156
+ break;
157
+ }
158
+ case 'list': {
159
+ const versions = await listSnapshots(versionsDir);
160
+ if (versions.length === 0) {
161
+ console.log('No snapshots found.');
162
+ }
163
+ else {
164
+ console.log(`šŸ“¦ ${versions.length} snapshot(s):`);
165
+ for (const v of versions) {
166
+ const snap = await loadSnapshot(v, versionsDir);
167
+ const entryCount = snap?.entries.length ?? '?';
168
+ console.log(` - ${v} (${entryCount} entries)`);
169
+ }
170
+ }
171
+ break;
172
+ }
173
+ case 'diff': {
174
+ if (!options.fromVersion || !options.toVersion) {
175
+ console.error("āŒ Error: --from and --to are required for 'diff' command");
176
+ process.exit(1);
177
+ }
178
+ const fromSnap = await loadSnapshot(options.fromVersion, versionsDir);
179
+ const toSnap = await loadSnapshot(options.toVersion, versionsDir);
180
+ if (!fromSnap) {
181
+ console.error(`āŒ Snapshot not found: ${options.fromVersion}`);
182
+ process.exit(1);
183
+ }
184
+ if (!toSnap) {
185
+ console.error(`āŒ Snapshot not found: ${options.toVersion}`);
186
+ process.exit(1);
187
+ }
188
+ const diff = diffSnapshots(fromSnap, toSnap);
189
+ const breaking = detectBreakingChanges(diff);
190
+ if (diff.added.length === 0 &&
191
+ diff.removed.length === 0 &&
192
+ diff.changed.length === 0) {
193
+ console.log('No API changes detected.');
194
+ return;
195
+ }
196
+ const changelog = generateChangelog(diff);
197
+ console.log(changelog);
198
+ if (breaking.length > 0) {
199
+ console.log('');
200
+ const guide = generateMigrationGuide(breaking);
201
+ console.log(guide);
202
+ }
203
+ break;
204
+ }
205
+ default:
206
+ console.error(`āŒ Unknown command: ${options.command}`);
207
+ printHelp();
208
+ process.exit(1);
209
+ }
210
+ }
211
+ if (process.argv[1] &&
212
+ resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
213
+ main().catch(err => {
214
+ console.error('Fatal error:', err);
215
+ process.exit(1);
216
+ });
217
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.sync-cli",
6
+ * "title": "Sync CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * Unified CLI for running the codex sync pipeline (extraction + README sync).
11
+ *
12
+ * Usage:
13
+ * pnpm codex sync [options]
14
+ *
15
+ * Options:
16
+ * --root <path> Project root directory (default: cwd)
17
+ * --dry-run Report changes without writing files
18
+ * --skip-readme Skip README marker sync
19
+ * --skip-tests Skip test file analysis
20
+ * --verbose Show detailed progress
21
+ */
22
+ export {};
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.sync-cli",
6
+ * "title": "Sync CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * Unified CLI for running the codex sync pipeline (extraction + README sync).
11
+ *
12
+ * Usage:
13
+ * pnpm codex sync [options]
14
+ *
15
+ * Options:
16
+ * --root <path> Project root directory (default: cwd)
17
+ * --dry-run Report changes without writing files
18
+ * --skip-readme Skip README marker sync
19
+ * --skip-tests Skip test file analysis
20
+ * --verbose Show detailed progress
21
+ */
22
+ import { resolve } from 'node:path';
23
+ import fs from 'node:fs/promises';
24
+ import { runSyncPipeline } from './sync-pipeline.js';
25
+ function parseArgs(args) {
26
+ const options = {
27
+ root: process.cwd(),
28
+ dryRun: false,
29
+ skipReadmeSync: false,
30
+ skipTests: false,
31
+ verbose: false,
32
+ };
33
+ for (let i = 0; i < args.length; i++) {
34
+ const arg = args[i];
35
+ if (arg.startsWith('--root=')) {
36
+ options.root = arg.slice('--root='.length);
37
+ continue;
38
+ }
39
+ switch (arg) {
40
+ case '--root': {
41
+ const next = args[i + 1];
42
+ if (!next || next.startsWith('-')) {
43
+ console.error('Error: --root option requires a path argument.');
44
+ process.exit(1);
45
+ }
46
+ options.root = next;
47
+ i++;
48
+ break;
49
+ }
50
+ case '--dry-run':
51
+ options.dryRun = true;
52
+ break;
53
+ case '--skip-readme':
54
+ options.skipReadmeSync = true;
55
+ break;
56
+ case '--skip-tests':
57
+ options.skipTests = true;
58
+ break;
59
+ case '--verbose':
60
+ case '-v':
61
+ options.verbose = true;
62
+ break;
63
+ case '--help':
64
+ case '-h':
65
+ printHelp();
66
+ process.exit(0);
67
+ break;
68
+ default:
69
+ if (arg.startsWith('-')) {
70
+ console.warn(`Warning: unknown option "${arg}" (ignored)`);
71
+ }
72
+ }
73
+ }
74
+ return options;
75
+ }
76
+ function printHelp() {
77
+ console.log(`
78
+ Codex Sync Pipeline
79
+
80
+ Runs the full codex sync: extraction pipeline → README marker sync.
81
+ Equivalent to running 'codex extract' followed by 'codex readme-sync'.
82
+
83
+ Usage:
84
+ pithy-codex-sync [options]
85
+ pnpm --filter @pithyjs/codex sync-pipeline [-- options]
86
+
87
+ Options:
88
+ --root <path> Project root directory (default: current directory)
89
+ --dry-run Report changes without writing files
90
+ --skip-readme Skip README marker sync step
91
+ --skip-tests Skip test file analysis (faster)
92
+ --verbose, -v Show detailed progress
93
+ --help, -h Show this help message
94
+
95
+ Examples:
96
+ # Full sync from project root
97
+ pithy-codex-sync --root ../..
98
+
99
+ # Preview changes without writing
100
+ pithy-codex-sync --root ../.. --dry-run --verbose
101
+
102
+ # Extract only (no README sync)
103
+ pithy-codex-sync --root ../.. --skip-readme
104
+ `);
105
+ }
106
+ async function main() {
107
+ const args = process.argv.slice(2);
108
+ const options = parseArgs(args);
109
+ options.root = resolve(options.root);
110
+ // Verify root exists
111
+ try {
112
+ const stat = await fs.stat(options.root);
113
+ if (!stat.isDirectory()) {
114
+ console.error(`āŒ Error: "${options.root}" is not a directory`);
115
+ process.exit(1);
116
+ }
117
+ }
118
+ catch {
119
+ console.error(`āŒ Error: Root directory "${options.root}" does not exist`);
120
+ process.exit(1);
121
+ }
122
+ const logger = {
123
+ log: msg => console.log(msg),
124
+ warn: msg => console.warn(msg),
125
+ error: msg => console.error(msg),
126
+ };
127
+ try {
128
+ const result = await runSyncPipeline({
129
+ root: options.root,
130
+ dryRun: options.dryRun,
131
+ skipReadmeSync: options.skipReadmeSync,
132
+ skipTests: options.skipTests,
133
+ verbose: options.verbose,
134
+ }, logger);
135
+ // Always show summary
136
+ console.log('');
137
+ console.log(`✨ Extracted ${result.extraction.stats.componentsFound} components with ${result.extraction.stats.apisExtracted} APIs`);
138
+ if (!options.skipReadmeSync) {
139
+ const changed = result.readmeSync.filter(r => r.changed);
140
+ if (changed.length > 0 && options.dryRun) {
141
+ console.log('');
142
+ console.log('(dry-run mode — no files were written)');
143
+ }
144
+ }
145
+ }
146
+ catch (error) {
147
+ console.error('āŒ Sync failed:', error instanceof Error ? error.message : error);
148
+ process.exit(1);
149
+ }
150
+ }
151
+ main().catch(err => {
152
+ console.error('Fatal error:', err);
153
+ process.exit(1);
154
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.sync-pipeline",
5
+ * "title": "Sync Pipeline",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Unified sync pipeline that orchestrates extraction, README sync, and validation.
10
+ * Used by the `codex sync` CLI command and CI integration.
11
+ */
12
+ import type { ExtendedExtractionResult, ReadmeSyncResult, CodexLogger } from './extraction/types.js';
13
+ /**
14
+ * Options for the unified sync pipeline
15
+ */
16
+ export interface SyncPipelineOptions {
17
+ /** Project root directory */
18
+ root: string;
19
+ /** Whether to perform a dry run (no file writes) */
20
+ dryRun?: boolean;
21
+ /** Whether to show verbose output */
22
+ verbose?: boolean;
23
+ /** Skip README sync step */
24
+ skipReadmeSync?: boolean;
25
+ /** Skip test analysis */
26
+ skipTests?: boolean;
27
+ }
28
+ /**
29
+ * Result of the unified sync pipeline
30
+ */
31
+ export interface SyncPipelineResult {
32
+ /** Extraction pipeline result */
33
+ extraction: ExtendedExtractionResult;
34
+ /** README sync results (empty if skipped) */
35
+ readmeSync: ReadmeSyncResult[];
36
+ /** Total time in milliseconds */
37
+ totalTimeMs: number;
38
+ }
39
+ /**
40
+ * Validates and normalizes a path to prevent path traversal attacks
41
+ * @param basePath - The base directory that paths must stay within
42
+ * @param inputPath - The user-provided path to validate
43
+ * @returns Normalized absolute path
44
+ * @throws Error if path escapes the base directory
45
+ * @codexApi {"parent":"pithy.codex.sync-pipeline","name":"validatePath","stability":"stable","signature":"(basePath: string, inputPath: string) => string"}
46
+ */
47
+ export declare function validatePath(basePath: string, inputPath: string): string;
48
+ /**
49
+ * @codexApi {"parent":"pithy.codex.sync-pipeline","name":"runSyncPipeline","stability":"stable","signature":"(options: SyncPipelineOptions, logger?: CodexLogger) => Promise<SyncPipelineResult>"}
50
+ *
51
+ * Runs the unified sync pipeline: extraction → README sync.
52
+ * This is the main entry point used by `pnpm codex sync` and CI.
53
+ *
54
+ * @param options - Pipeline configuration
55
+ * @param logger - Optional logger for output (silent by default)
56
+ * @returns Pipeline result with extraction data and sync results
57
+ */
58
+ export declare function runSyncPipeline(options: SyncPipelineOptions, logger?: CodexLogger): Promise<SyncPipelineResult>;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.sync-pipeline",
5
+ * "title": "Sync Pipeline",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Unified sync pipeline that orchestrates extraction, README sync, and validation.
10
+ * Used by the `codex sync` CLI command and CI integration.
11
+ */
12
+ import { resolve, isAbsolute, relative, sep } from 'node:path';
13
+ import fs from 'node:fs/promises';
14
+ import { runExtractionPipeline } from './extraction/pipeline.js';
15
+ import { syncAllReadmes } from './extraction/readme-sync.js';
16
+ const silentLogger = {
17
+ log: () => { },
18
+ warn: () => { },
19
+ error: () => { },
20
+ };
21
+ /**
22
+ * Validates and normalizes a path to prevent path traversal attacks
23
+ * @param basePath - The base directory that paths must stay within
24
+ * @param inputPath - The user-provided path to validate
25
+ * @returns Normalized absolute path
26
+ * @throws Error if path escapes the base directory
27
+ * @codexApi {"parent":"pithy.codex.sync-pipeline","name":"validatePath","stability":"stable","signature":"(basePath: string, inputPath: string) => string"}
28
+ */
29
+ export function validatePath(basePath, inputPath) {
30
+ const resolvedBase = resolve(basePath);
31
+ const resolvedPath = isAbsolute(inputPath)
32
+ ? resolve(inputPath)
33
+ : resolve(basePath, inputPath);
34
+ const relativePath = relative(resolvedBase, resolvedPath);
35
+ if (relativePath === '..' ||
36
+ relativePath.startsWith('..' + sep) ||
37
+ isAbsolute(relativePath)) {
38
+ throw new Error(`Security error: Path "${inputPath}" escapes the project root directory`);
39
+ }
40
+ return resolvedPath;
41
+ }
42
+ /**
43
+ * @codexApi {"parent":"pithy.codex.sync-pipeline","name":"runSyncPipeline","stability":"stable","signature":"(options: SyncPipelineOptions, logger?: CodexLogger) => Promise<SyncPipelineResult>"}
44
+ *
45
+ * Runs the unified sync pipeline: extraction → README sync.
46
+ * This is the main entry point used by `pnpm codex sync` and CI.
47
+ *
48
+ * @param options - Pipeline configuration
49
+ * @param logger - Optional logger for output (silent by default)
50
+ * @returns Pipeline result with extraction data and sync results
51
+ */
52
+ export async function runSyncPipeline(options, logger = silentLogger) {
53
+ const startTime = performance.now();
54
+ const root = resolve(options.root);
55
+ // Verify root directory exists
56
+ const stat = await fs.stat(root).catch(() => null);
57
+ if (!stat || !stat.isDirectory()) {
58
+ throw new Error(`Root directory "${root}" does not exist or is not a directory`);
59
+ }
60
+ // Step 1: Extraction pipeline
61
+ logger.log('šŸ“‚ Running extraction pipeline...');
62
+ const extraction = await runExtractionPipeline({
63
+ root,
64
+ analyzeTests: !options.skipTests,
65
+ linkReadmes: true,
66
+ extractRunnableExamples: true,
67
+ });
68
+ logger.log(` Found ${extraction.stats.componentsFound} components with ${extraction.stats.apisExtracted} APIs`);
69
+ // Step 2: README sync
70
+ let readmeSync = [];
71
+ if (!options.skipReadmeSync) {
72
+ logger.log('šŸ“ Syncing README files...');
73
+ const syncConfig = {
74
+ root,
75
+ dryRun: options.dryRun,
76
+ verbose: options.verbose,
77
+ };
78
+ readmeSync = await syncAllReadmes(extraction, syncConfig);
79
+ const changed = readmeSync.filter(r => r.changed);
80
+ const totalMarkers = readmeSync.reduce((sum, r) => sum + r.markersProcessed, 0);
81
+ const totalUpdated = readmeSync.reduce((sum, r) => sum + r.markersUpdated, 0);
82
+ logger.log(` Files scanned: ${readmeSync.length}`);
83
+ logger.log(` Files changed: ${changed.length}`);
84
+ logger.log(` Markers processed: ${totalMarkers}`);
85
+ logger.log(` Markers updated: ${totalUpdated}`);
86
+ // Report warnings
87
+ const allWarnings = readmeSync.flatMap(r => r.warnings);
88
+ if (allWarnings.length > 0) {
89
+ for (const w of allWarnings) {
90
+ logger.warn(` ⚠ ${w}`);
91
+ }
92
+ }
93
+ }
94
+ else {
95
+ logger.log('ā­ Skipping README sync');
96
+ }
97
+ const totalTimeMs = performance.now() - startTime;
98
+ logger.log(`\nāœ… Sync complete in ${(totalTimeMs / 1000).toFixed(2)}s`);
99
+ return {
100
+ extraction,
101
+ readmeSync,
102
+ totalTimeMs,
103
+ };
104
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};