@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.
- package/LICENSE +21 -0
- package/README.md +4401 -0
- package/dist/annotations.d.ts +106 -0
- package/dist/annotations.js +306 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.js +80 -0
- package/dist/changed-scope.d.ts +23 -0
- package/dist/changed-scope.js +31 -0
- package/dist/check.d.ts +2 -0
- package/dist/check.js +117 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +35 -0
- package/dist/extract-cli.d.ts +23 -0
- package/dist/extract-cli.js +192 -0
- package/dist/extraction/breaking-changes.d.ts +66 -0
- package/dist/extraction/breaking-changes.js +352 -0
- package/dist/extraction/example-extractor.d.ts +55 -0
- package/dist/extraction/example-extractor.js +272 -0
- package/dist/extraction/index.d.ts +27 -0
- package/dist/extraction/index.js +31 -0
- package/dist/extraction/jsdoc-parser.d.ts +43 -0
- package/dist/extraction/jsdoc-parser.js +274 -0
- package/dist/extraction/pipeline.d.ts +54 -0
- package/dist/extraction/pipeline.js +526 -0
- package/dist/extraction/readme-sync.d.ts +108 -0
- package/dist/extraction/readme-sync.js +592 -0
- package/dist/extraction/snapshot-store.d.ts +40 -0
- package/dist/extraction/snapshot-store.js +153 -0
- package/dist/extraction/source-linker.d.ts +80 -0
- package/dist/extraction/source-linker.js +316 -0
- package/dist/extraction/test-example-extractor.d.ts +69 -0
- package/dist/extraction/test-example-extractor.js +400 -0
- package/dist/extraction/test-pattern-extractor.d.ts +68 -0
- package/dist/extraction/test-pattern-extractor.js +261 -0
- package/dist/extraction/testing-pyramid.d.ts +44 -0
- package/dist/extraction/testing-pyramid.js +163 -0
- package/dist/extraction/type-extractor.d.ts +34 -0
- package/dist/extraction/type-extractor.js +494 -0
- package/dist/extraction/types.d.ts +401 -0
- package/dist/extraction/types.js +34 -0
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.js +107 -0
- package/dist/llm.d.ts +8 -0
- package/dist/llm.js +75 -0
- package/dist/readme-sync-cli.d.ts +20 -0
- package/dist/readme-sync-cli.js +167 -0
- package/dist/review.d.ts +2 -0
- package/dist/review.js +93 -0
- package/dist/scan.d.ts +29 -0
- package/dist/scan.js +221 -0
- package/dist/schema.d.ts +169 -0
- package/dist/schema.js +70 -0
- package/dist/snapshot-cli.d.ts +45 -0
- package/dist/snapshot-cli.js +217 -0
- package/dist/sync-cli.d.ts +22 -0
- package/dist/sync-cli.js +154 -0
- package/dist/sync-pipeline.d.ts +58 -0
- package/dist/sync-pipeline.js +104 -0
- package/dist/sync.d.ts +2 -0
- package/dist/sync.js +318 -0
- package/dist/validate-cli.d.ts +20 -0
- package/dist/validate-cli.js +144 -0
- package/dist/validate.d.ts +76 -0
- package/dist/validate.js +183 -0
- package/dist/watch-cli.d.ts +21 -0
- package/dist/watch-cli.js +220 -0
- 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 {};
|
package/dist/sync-cli.js
ADDED
|
@@ -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