@c4a/context-cli 0.5.38 → 0.5.39-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli.js +6063 -3774
- package/package.json +3 -1
- package/plugin/commands/align.md +17 -10
- package/plugin/commands/capture.md +2 -2
- package/plugin/commands/compile.md +7 -7
- package/plugin/skills/skill-align-workflow/SKILL.md +24 -10
- package/plugin/skills/skill-align-workflow/references/candidate-resolution.md +3 -3
- package/plugin/skills/skill-align-workflow/references/density-profile.md +1 -1
- package/plugin/skills/skill-align-workflow/references/gates.md +1 -1
- package/plugin/skills/skill-compile-draft/SKILL.md +7 -7
- package/plugin/skills/skill-compile-judge/SKILL.md +1 -1
- package/plugin/skills/skill-semantic-reconcile/references/temporal-and-evidence.md +4 -4
- package/scripts/build-aspect-runtime.ts +45 -0
- package/templates/aspect-runtime/aspectRunnerSdk.js +776 -0
- package/templates/aspects/README.md +7 -5
- package/templates/aspect-runtime/aspectRunnerSdk.ts +0 -749
- /package/templates/aspects/code/{prompt.md → README.md} +0 -0
- /package/templates/aspects/design-system/{prompt.md → README.md} +0 -0
- /package/templates/aspects/graphql/{prompt.md → README.md} +0 -0
- /package/templates/aspects/openapi/{prompt.md → README.md} +0 -0
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import * as ts from "typescript";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Aspect plugin runtime SDK.
|
|
8
|
-
*
|
|
9
|
-
* Product-specific aspects export a lifecycle object through defineAspect().
|
|
10
|
-
* The CLI owns workspace I/O, source_ref encoding, JSONL output, hash
|
|
11
|
-
* calculation, raw bucket publishing, and knowledge writes. Aspect code only
|
|
12
|
-
* reads source files through ctx.source and emits deterministic node/Section
|
|
13
|
-
* rows through ctx.emit.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export interface RunnerRuntimeContext {
|
|
17
|
-
protocol?: string;
|
|
18
|
-
helper_module?: string;
|
|
19
|
-
plugin_module?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface CodeIndexPackage {
|
|
23
|
-
package_name?: string;
|
|
24
|
-
package_slug: string;
|
|
25
|
-
node_slug?: string;
|
|
26
|
-
source_id?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface CodeIndexSymbol {
|
|
30
|
-
export_name?: string;
|
|
31
|
-
symbol_name?: string;
|
|
32
|
-
aliases?: string[];
|
|
33
|
-
package_name?: string;
|
|
34
|
-
package_slug?: string;
|
|
35
|
-
node_slug: string;
|
|
36
|
-
source_id?: string;
|
|
37
|
-
kind?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface CodeIndexContext {
|
|
41
|
-
packages?: CodeIndexPackage[];
|
|
42
|
-
symbols?: CodeIndexSymbol[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface AspectHostInput {
|
|
46
|
-
workspace_root: string;
|
|
47
|
-
repo_root: string;
|
|
48
|
-
aspect: string;
|
|
49
|
-
source_slug: string;
|
|
50
|
-
source_id: string;
|
|
51
|
-
snapshot_id: string;
|
|
52
|
-
runtime?: RunnerRuntimeContext;
|
|
53
|
-
code_index?: CodeIndexContext;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SourceFileRef {
|
|
57
|
-
path: string;
|
|
58
|
-
name: string;
|
|
59
|
-
extension: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface SourceGlobOptions {
|
|
63
|
-
root?: string;
|
|
64
|
-
extensions?: readonly string[];
|
|
65
|
-
recursive?: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface SectionProjectionRow {
|
|
69
|
-
node_slug: string;
|
|
70
|
-
kind: "description" | "spec" | "warning" | "principle" | "decision" | "incident" | "example" | "changelog" | "comparison" | "faq";
|
|
71
|
-
summary?: string;
|
|
72
|
-
content: string;
|
|
73
|
-
source?: SourceFileRef | string;
|
|
74
|
-
source_path?: string;
|
|
75
|
-
artifact: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface NodeProjectionRow {
|
|
79
|
-
node_slug: string;
|
|
80
|
-
title: string;
|
|
81
|
-
type?: "domain" | "entity" | "action";
|
|
82
|
-
tags?: readonly string[];
|
|
83
|
-
summary?: string;
|
|
84
|
-
parent_slug?: string;
|
|
85
|
-
code_package?: string;
|
|
86
|
-
language?: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface AspectCaptureContext {
|
|
90
|
-
aspect: {
|
|
91
|
-
name: string;
|
|
92
|
-
source_slug: string;
|
|
93
|
-
source_id: string;
|
|
94
|
-
snapshot_id: string;
|
|
95
|
-
};
|
|
96
|
-
source: {
|
|
97
|
-
glob(options?: SourceGlobOptions): Promise<SourceFileRef[]>;
|
|
98
|
-
immediateFiles(root: string, extensions: readonly string[]): Promise<SourceFileRef[]>;
|
|
99
|
-
exists(source: SourceFileRef | string): Promise<boolean>;
|
|
100
|
-
readText(source: SourceFileRef | string): Promise<string>;
|
|
101
|
-
};
|
|
102
|
-
code: {
|
|
103
|
-
packages: CodeIndexPackage[];
|
|
104
|
-
symbols: CodeIndexSymbol[];
|
|
105
|
-
findPackage(packageSlug: string): CodeIndexPackage | undefined;
|
|
106
|
-
findSymbol(packageSlug: string, symbolName: string): CodeIndexSymbol | undefined;
|
|
107
|
-
};
|
|
108
|
-
emit: {
|
|
109
|
-
node(row: NodeProjectionRow): void;
|
|
110
|
-
section(row: SectionProjectionRow): void;
|
|
111
|
-
warning(message: string, detail?: Record<string, unknown>): void;
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface AspectPlugin {
|
|
116
|
-
setup?(ctx: AspectCaptureContext): void | Promise<void>;
|
|
117
|
-
capture(ctx: AspectCaptureContext): void | Promise<void>;
|
|
118
|
-
teardown?(ctx: AspectCaptureContext): void | Promise<void>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface RunnerResult {
|
|
122
|
-
type: "aspect-plugin-result.v1";
|
|
123
|
-
nodes: PersistedNodeEmit[];
|
|
124
|
-
sections: PersistedSectionEmit[];
|
|
125
|
-
warnings: AspectPluginWarning[];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface PersistedNodeEmit {
|
|
129
|
-
node_slug: string;
|
|
130
|
-
title: string;
|
|
131
|
-
type: "domain" | "entity" | "action";
|
|
132
|
-
tags: string[];
|
|
133
|
-
summary?: string;
|
|
134
|
-
parent_slug?: string;
|
|
135
|
-
code_package?: string;
|
|
136
|
-
language?: string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
interface PersistedSectionEmit {
|
|
140
|
-
node_slug: string;
|
|
141
|
-
kind: SectionProjectionRow["kind"];
|
|
142
|
-
summary?: string;
|
|
143
|
-
content: string;
|
|
144
|
-
source_path: string;
|
|
145
|
-
artifact: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
interface AspectPluginWarning {
|
|
149
|
-
message: string;
|
|
150
|
-
detail?: Record<string, unknown>;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function defineAspect(plugin: AspectPlugin): AspectPlugin {
|
|
154
|
-
return plugin;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function readHostInput(): Promise<AspectHostInput> {
|
|
158
|
-
const chunks: Buffer[] = [];
|
|
159
|
-
for await (const chunk of process.stdin) {
|
|
160
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
161
|
-
}
|
|
162
|
-
const input = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Partial<AspectHostInput>;
|
|
163
|
-
for (const key of ["workspace_root", "repo_root", "aspect", "source_slug", "source_id", "snapshot_id"] as const) {
|
|
164
|
-
if (typeof input[key] !== "string" || input[key]!.length === 0) {
|
|
165
|
-
throw new Error(`aspect host input missing ${key}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return input as AspectHostInput;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function pathExists(absPath: string): Promise<boolean> {
|
|
172
|
-
try {
|
|
173
|
-
await stat(absPath);
|
|
174
|
-
return true;
|
|
175
|
-
} catch (err) {
|
|
176
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
177
|
-
if (code === "ENOENT" || code === "ENOTDIR") return false;
|
|
178
|
-
throw err;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function sourceRefFromInput(source: SourceFileRef | string): string {
|
|
183
|
-
return typeof source === "string" ? source : source.path;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function normalizeSourceRoot(root: string | undefined): string {
|
|
187
|
-
if (root === undefined || root === "" || root === ".") return "";
|
|
188
|
-
const normalized = toPosix(path.posix.normalize(toPosix(root)));
|
|
189
|
-
validateSourcePath(normalized);
|
|
190
|
-
return normalized;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function sourceFileRef(sourcePath: string): SourceFileRef {
|
|
194
|
-
validateSourcePath(sourcePath);
|
|
195
|
-
return {
|
|
196
|
-
path: sourcePath,
|
|
197
|
-
name: path.posix.basename(sourcePath),
|
|
198
|
-
extension: path.posix.extname(sourcePath).toLowerCase(),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function resolveRepoPath(repoRoot: string, sourcePath: string): Promise<string> {
|
|
203
|
-
validateSourcePath(sourcePath);
|
|
204
|
-
const realRepoRoot = await realpath(repoRoot);
|
|
205
|
-
const realSource = await realpath(path.resolve(realRepoRoot, sourcePath));
|
|
206
|
-
const rel = path.relative(realRepoRoot, realSource);
|
|
207
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
208
|
-
throw new Error(`source_path escapes repo_root: ${sourcePath}`);
|
|
209
|
-
}
|
|
210
|
-
return realSource;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function readRepoText(repoRoot: string, source: SourceFileRef | string): Promise<string> {
|
|
214
|
-
return readFile(await resolveRepoPath(repoRoot, sourceRefFromInput(source)), "utf8");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function collectRepoFiles(repoRoot: string, rootRel: string, extensions: readonly string[], recursive: boolean): Promise<SourceFileRef[]> {
|
|
218
|
-
const normalizedRoot = normalizeSourceRoot(rootRel);
|
|
219
|
-
const realRepoRoot = await realpath(repoRoot);
|
|
220
|
-
let root: string;
|
|
221
|
-
try {
|
|
222
|
-
root = normalizedRoot.length > 0 ? await resolveRepoPath(realRepoRoot, normalizedRoot) : realRepoRoot;
|
|
223
|
-
} catch (err) {
|
|
224
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
225
|
-
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
if (!(await pathExists(root))) return [];
|
|
229
|
-
const normalizedExtensions = extensions.map((ext) => ext.toLowerCase());
|
|
230
|
-
const out: SourceFileRef[] = [];
|
|
231
|
-
async function walk(dir: string): Promise<void> {
|
|
232
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
233
|
-
for (const entry of entries) {
|
|
234
|
-
if (entry.name.startsWith(".")) continue;
|
|
235
|
-
const absPath = path.join(dir, entry.name);
|
|
236
|
-
if (entry.isDirectory()) {
|
|
237
|
-
if (recursive) await walk(absPath);
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
if (!entry.isFile()) continue;
|
|
241
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
242
|
-
if (normalizedExtensions.length > 0 && !normalizedExtensions.includes(ext)) continue;
|
|
243
|
-
out.push(sourceFileRef(repoRelative(realRepoRoot, absPath)));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
await walk(root);
|
|
247
|
-
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function toPosix(value: string): string {
|
|
251
|
-
return value.split(path.sep).join("/");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function repoRelative(repoRoot: string, absPath: string): string {
|
|
255
|
-
const rel = toPosix(path.relative(repoRoot, absPath));
|
|
256
|
-
validateSourcePath(rel);
|
|
257
|
-
return rel;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function validateSourcePath(sourcePath: string): void {
|
|
261
|
-
if (sourcePath.length === 0 || sourcePath.includes("\0") || sourcePath.includes("\\")) {
|
|
262
|
-
throw new Error(`invalid source_path: ${sourcePath}`);
|
|
263
|
-
}
|
|
264
|
-
if (path.posix.isAbsolute(sourcePath)) {
|
|
265
|
-
throw new Error(`source_path must be relative: ${sourcePath}`);
|
|
266
|
-
}
|
|
267
|
-
const parts = sourcePath.split("/");
|
|
268
|
-
if (parts.some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
269
|
-
throw new Error(`source_path must be normalized and stay inside repo_root: ${sourcePath}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function encodeSourcePath(sourcePath: string): string {
|
|
274
|
-
validateSourcePath(sourcePath);
|
|
275
|
-
return sourcePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function encodeFragment(fragment: string): string {
|
|
279
|
-
return encodeURIComponent(fragment);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function fileSourceRef(sourcePath: string, artifact: string, hash: string): string {
|
|
283
|
-
return `file:${encodeSourcePath(sourcePath)}#${encodeFragment(artifact)}@${hash}`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function normalizeSection(row: SectionProjectionRow): PersistedSectionEmit {
|
|
287
|
-
const sourcePath = row.source_path ?? (row.source !== undefined ? sourceRefFromInput(row.source) : undefined);
|
|
288
|
-
if (sourcePath === undefined) {
|
|
289
|
-
throw new Error("ctx.emit.section requires source or source_path");
|
|
290
|
-
}
|
|
291
|
-
validateSourcePath(sourcePath);
|
|
292
|
-
if (row.artifact.trim().length === 0) {
|
|
293
|
-
throw new Error("ctx.emit.section requires a non-empty artifact");
|
|
294
|
-
}
|
|
295
|
-
return {
|
|
296
|
-
node_slug: row.node_slug,
|
|
297
|
-
kind: row.kind,
|
|
298
|
-
...(row.summary !== undefined && row.summary.length > 0 ? { summary: row.summary } : {}),
|
|
299
|
-
content: row.content,
|
|
300
|
-
source_path: sourcePath,
|
|
301
|
-
artifact: row.artifact,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function normalizeNode(row: NodeProjectionRow): PersistedNodeEmit {
|
|
306
|
-
if (row.node_slug.trim().length === 0) {
|
|
307
|
-
throw new Error("ctx.emit.node requires a non-empty node_slug");
|
|
308
|
-
}
|
|
309
|
-
if (row.title.trim().length === 0) {
|
|
310
|
-
throw new Error("ctx.emit.node requires a non-empty title");
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
node_slug: row.node_slug,
|
|
314
|
-
title: row.title,
|
|
315
|
-
type: row.type ?? "entity",
|
|
316
|
-
tags: [...(row.tags ?? ["module"])],
|
|
317
|
-
...(row.summary !== undefined && row.summary.length > 0 ? { summary: row.summary } : {}),
|
|
318
|
-
...(row.parent_slug !== undefined && row.parent_slug.length > 0 ? { parent_slug: row.parent_slug } : {}),
|
|
319
|
-
...(row.code_package !== undefined && row.code_package.length > 0 ? { code_package: row.code_package } : {}),
|
|
320
|
-
...(row.language !== undefined && row.language.length > 0 ? { language: row.language } : {}),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function findPackage(packages: readonly CodeIndexPackage[], packageSlug: string): CodeIndexPackage | undefined {
|
|
325
|
-
return packages.find((pkg) => pkg.package_slug === packageSlug);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function findSymbol(symbols: readonly CodeIndexSymbol[], packageSlug: string, symbolName: string): CodeIndexSymbol | undefined {
|
|
329
|
-
const target = normalizeComponentSlug(symbolName);
|
|
330
|
-
return symbols.find((symbol) => {
|
|
331
|
-
if (symbol.package_slug !== undefined && symbol.package_slug !== packageSlug) return false;
|
|
332
|
-
const nodeLeaf = symbol.node_slug.split("/").at(-1);
|
|
333
|
-
const names = [symbol.export_name, symbol.symbol_name, ...(symbol.aliases ?? []), nodeLeaf]
|
|
334
|
-
.filter((name): name is string => typeof name === "string" && name.length > 0);
|
|
335
|
-
return names.some((name) => normalizeComponentSlug(name) === target || name.toLowerCase() === symbolName.toLowerCase());
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function buildContext(
|
|
340
|
-
input: AspectHostInput,
|
|
341
|
-
nodes: PersistedNodeEmit[],
|
|
342
|
-
sections: PersistedSectionEmit[],
|
|
343
|
-
warnings: AspectPluginWarning[],
|
|
344
|
-
): AspectCaptureContext {
|
|
345
|
-
const packages = input.code_index?.packages ?? [];
|
|
346
|
-
const symbols = input.code_index?.symbols ?? [];
|
|
347
|
-
return {
|
|
348
|
-
aspect: {
|
|
349
|
-
name: input.aspect,
|
|
350
|
-
source_slug: input.source_slug,
|
|
351
|
-
source_id: input.source_id,
|
|
352
|
-
snapshot_id: input.snapshot_id,
|
|
353
|
-
},
|
|
354
|
-
source: {
|
|
355
|
-
glob: (options = {}) => collectRepoFiles(
|
|
356
|
-
input.repo_root,
|
|
357
|
-
options.root ?? "",
|
|
358
|
-
options.extensions ?? [],
|
|
359
|
-
options.recursive ?? true,
|
|
360
|
-
),
|
|
361
|
-
immediateFiles: (root, extensions) => collectRepoFiles(input.repo_root, root, extensions, false),
|
|
362
|
-
exists: async (source) => {
|
|
363
|
-
try {
|
|
364
|
-
await resolveRepoPath(input.repo_root, sourceRefFromInput(source));
|
|
365
|
-
return true;
|
|
366
|
-
} catch (err) {
|
|
367
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
368
|
-
if (code === "ENOENT" || code === "ENOTDIR") return false;
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
readText: (source) => readRepoText(input.repo_root, source),
|
|
373
|
-
},
|
|
374
|
-
code: {
|
|
375
|
-
packages,
|
|
376
|
-
symbols,
|
|
377
|
-
findPackage: (packageSlug) => findPackage(packages, packageSlug),
|
|
378
|
-
findSymbol: (packageSlug, symbolName) => findSymbol(symbols, packageSlug, symbolName),
|
|
379
|
-
},
|
|
380
|
-
emit: {
|
|
381
|
-
node: (row) => {
|
|
382
|
-
nodes.push(normalizeNode(row));
|
|
383
|
-
},
|
|
384
|
-
section: (row) => {
|
|
385
|
-
sections.push(normalizeSection(row));
|
|
386
|
-
},
|
|
387
|
-
warning: (message, detail) => {
|
|
388
|
-
warnings.push({
|
|
389
|
-
message,
|
|
390
|
-
...(detail !== undefined ? { detail } : {}),
|
|
391
|
-
});
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export async function runAspectPluginForHost(pluginModulePath: string, input: AspectHostInput): Promise<RunnerResult> {
|
|
398
|
-
const nodes: PersistedNodeEmit[] = [];
|
|
399
|
-
const sections: PersistedSectionEmit[] = [];
|
|
400
|
-
const warnings: AspectPluginWarning[] = [];
|
|
401
|
-
const pluginModule = await import(pathToFileURL(pluginModulePath).href) as {
|
|
402
|
-
default?: AspectPlugin;
|
|
403
|
-
};
|
|
404
|
-
const plugin = pluginModule.default;
|
|
405
|
-
if (plugin === undefined || typeof plugin.capture !== "function") {
|
|
406
|
-
throw new Error(`${pluginModulePath} must export default defineAspect({ capture(ctx) { ... } })`);
|
|
407
|
-
}
|
|
408
|
-
const ctx = buildContext(input, nodes, sections, warnings);
|
|
409
|
-
await plugin.setup?.(ctx);
|
|
410
|
-
try {
|
|
411
|
-
await plugin.capture(ctx);
|
|
412
|
-
} finally {
|
|
413
|
-
await plugin.teardown?.(ctx);
|
|
414
|
-
}
|
|
415
|
-
return {
|
|
416
|
-
type: "aspect-plugin-result.v1",
|
|
417
|
-
nodes,
|
|
418
|
-
sections,
|
|
419
|
-
warnings,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export function slugSegment(raw: string): string {
|
|
424
|
-
const segment = raw
|
|
425
|
-
.trim()
|
|
426
|
-
.replace(/\.[^.]+$/u, "")
|
|
427
|
-
.replace(/\+$/u, "")
|
|
428
|
-
.replace(/([a-z0-9])([A-Z])/gu, "$1-$2")
|
|
429
|
-
.replace(/[^A-Za-z0-9]+/gu, "-")
|
|
430
|
-
.replace(/^-+|-+$/gu, "")
|
|
431
|
-
.toLowerCase();
|
|
432
|
-
return segment.length > 0 ? segment : "untitled";
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
async function runPlugin(pluginModulePath: string): Promise<RunnerResult> {
|
|
436
|
-
return runAspectPluginForHost(pluginModulePath, await readHostInput());
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function main(): Promise<void> {
|
|
440
|
-
const pluginModulePath = process.argv[2];
|
|
441
|
-
if (pluginModulePath === undefined || pluginModulePath.length === 0) {
|
|
442
|
-
throw new Error("usage: aspectRunnerSdk.ts <aspect-plugin-module>");
|
|
443
|
-
}
|
|
444
|
-
const result = await runPlugin(path.resolve(pluginModulePath));
|
|
445
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (process.argv[1] !== undefined && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
449
|
-
main().catch((err) => {
|
|
450
|
-
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
451
|
-
process.stderr.write(`${message}\n`);
|
|
452
|
-
process.exitCode = 1;
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export function markdownFence(language: string, body: string): string {
|
|
457
|
-
const fence = body.includes("```") ? "~~~~" : "```";
|
|
458
|
-
return `${fence}${language}\n${body.trimEnd()}\n${fence}`;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function normalizeSymbolSlug(raw: string): string {
|
|
462
|
-
const cleaned = raw
|
|
463
|
-
.replace(/\.[^.]+$/u, "")
|
|
464
|
-
.replace(/\+$/u, "")
|
|
465
|
-
.replace(/[^A-Za-z0-9]+/gu, "")
|
|
466
|
-
.toLowerCase();
|
|
467
|
-
return cleaned.length === 0 ? "unknown" : cleaned;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function normalizeComponentSlug(raw: string): string {
|
|
471
|
-
return normalizeSymbolSlug(raw);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function inferComponentName(source: string, fallback: string): string {
|
|
475
|
-
const patterns = [
|
|
476
|
-
/\bimport\s*\{[^}]*\b([A-Z][A-Za-z0-9]+)\b[^}]*\}/u,
|
|
477
|
-
/<\s*([A-Z][A-Za-z0-9]+)\b/u,
|
|
478
|
-
/\b([A-Z][A-Za-z0-9]+)\s*\(/u,
|
|
479
|
-
];
|
|
480
|
-
for (const pattern of patterns) {
|
|
481
|
-
const match = source.match(pattern);
|
|
482
|
-
if (match?.[1]) return match[1];
|
|
483
|
-
}
|
|
484
|
-
return fallback;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
export function artifactFromRelPath(sourcePath: string): string {
|
|
488
|
-
return sourcePath.replace(/\.[^.]+$/u, "");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function firstMarkdownHeading(markdown: string, fallback: string): string {
|
|
492
|
-
const heading = markdown.match(/^#{1,6}\s+(.+)$/mu)?.[1]?.trim();
|
|
493
|
-
return heading && heading.length > 0 ? heading : fallback;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export function compactWhitespace(value: string): string {
|
|
497
|
-
return value.replace(/[ \t]+/gu, " ").replace(/\n{3,}/gu, "\n\n").trim();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function sourceFileFor(fileName: string, source: string): ts.SourceFile {
|
|
501
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
502
|
-
const scriptKind =
|
|
503
|
-
ext === ".tsx" ? ts.ScriptKind.TSX :
|
|
504
|
-
ext === ".jsx" ? ts.ScriptKind.JSX :
|
|
505
|
-
ext === ".js" || ext === ".cjs" || ext === ".mjs" ? ts.ScriptKind.JS :
|
|
506
|
-
ts.ScriptKind.TS;
|
|
507
|
-
return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKind);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function propertyNameText(name: ts.PropertyName): string | null {
|
|
511
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
512
|
-
if (
|
|
513
|
-
ts.isComputedPropertyName(name) &&
|
|
514
|
-
ts.isStringLiteralLike(name.expression)
|
|
515
|
-
) {
|
|
516
|
-
return name.expression.text;
|
|
517
|
-
}
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function jsxTagNameText(name: ts.JsxTagNameExpression): string | null {
|
|
522
|
-
if (ts.isIdentifier(name)) return name.text;
|
|
523
|
-
if (ts.isPropertyAccessExpression(name)) return name.name.text;
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function pushUnique(out: string[], seen: Set<string>, value: string | null | undefined): void {
|
|
528
|
-
if (!value || seen.has(value)) return;
|
|
529
|
-
seen.add(value);
|
|
530
|
-
out.push(value);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
export function parseTsxComponentNames(source: string, fileName: string): string[] {
|
|
534
|
-
const sf = sourceFileFor(fileName, source);
|
|
535
|
-
const names: string[] = [];
|
|
536
|
-
const seen = new Set<string>();
|
|
537
|
-
const isComponentName = (value: string): boolean => /^[A-Z][A-Za-z0-9]+$/u.test(value);
|
|
538
|
-
function visit(node: ts.Node): void {
|
|
539
|
-
if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
540
|
-
for (const specifier of node.importClause.namedBindings.elements) {
|
|
541
|
-
const imported = specifier.propertyName?.text ?? specifier.name.text;
|
|
542
|
-
if (isComponentName(imported)) pushUnique(names, seen, imported);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
546
|
-
const tag = jsxTagNameText(node.tagName);
|
|
547
|
-
if (tag && isComponentName(tag)) pushUnique(names, seen, tag);
|
|
548
|
-
}
|
|
549
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && isComponentName(node.expression.text)) {
|
|
550
|
-
pushUnique(names, seen, node.expression.text);
|
|
551
|
-
}
|
|
552
|
-
ts.forEachChild(node, visit);
|
|
553
|
-
}
|
|
554
|
-
visit(sf);
|
|
555
|
-
return names;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function parseJsLikeTokenNames(source: string, fileName: string, category: string): string[] {
|
|
559
|
-
const sf = sourceFileFor(fileName, source);
|
|
560
|
-
const names: string[] = [];
|
|
561
|
-
const seen = new Set<string>();
|
|
562
|
-
function maybePush(value: string | null | undefined): void {
|
|
563
|
-
if (!value) return;
|
|
564
|
-
void category;
|
|
565
|
-
if (!/^[A-Za-z_$-][A-Za-z0-9_$-]*$/u.test(value)) return;
|
|
566
|
-
pushUnique(names, seen, value);
|
|
567
|
-
}
|
|
568
|
-
function visit(node: ts.Node): void {
|
|
569
|
-
if (ts.isPropertyAssignment(node)) {
|
|
570
|
-
const name = propertyNameText(node.name);
|
|
571
|
-
if (name === "cssVarName" && ts.isStringLiteralLike(node.initializer)) {
|
|
572
|
-
maybePush(node.initializer.text);
|
|
573
|
-
} else {
|
|
574
|
-
maybePush(name);
|
|
575
|
-
}
|
|
576
|
-
} else if (ts.isShorthandPropertyAssignment(node)) {
|
|
577
|
-
maybePush(node.name.text);
|
|
578
|
-
} else if (ts.isStringLiteralLike(node) && (/^--[A-Za-z0-9_-]+$/u.test(node.text) || /^\$[A-Za-z0-9_-]+$/u.test(node.text))) {
|
|
579
|
-
maybePush(node.text);
|
|
580
|
-
}
|
|
581
|
-
ts.forEachChild(node, visit);
|
|
582
|
-
}
|
|
583
|
-
visit(sf);
|
|
584
|
-
return names.sort((a, b) => a.localeCompare(b));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
export function parseScssTokenNames(source: string): string[] {
|
|
588
|
-
const names = new Set<string>();
|
|
589
|
-
for (const line of source.split(/\r?\n/u)) {
|
|
590
|
-
const trimmed = line.trim();
|
|
591
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("@import") || trimmed.startsWith("@use")) continue;
|
|
592
|
-
const variable = trimmed.match(/^(\$[A-Za-z0-9_-]+)\s*:/u)?.[1];
|
|
593
|
-
if (variable) names.add(variable);
|
|
594
|
-
const cssVar = trimmed.match(/(--[A-Za-z0-9_-]+)\s*:/u)?.[1];
|
|
595
|
-
if (cssVar) names.add(cssVar);
|
|
596
|
-
const property = trimmed.match(/^([A-Za-z-]+)\s*:/u)?.[1];
|
|
597
|
-
if (property && !["if", "for", "each", "include"].includes(property)) names.add(property);
|
|
598
|
-
}
|
|
599
|
-
return [...names].sort((a, b) => a.localeCompare(b));
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
export interface MdxBlock {
|
|
603
|
-
type: "heading" | "paragraph" | "code";
|
|
604
|
-
depth?: number;
|
|
605
|
-
language?: string;
|
|
606
|
-
text: string;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export interface ParsedMdxDocument {
|
|
610
|
-
title: string;
|
|
611
|
-
markdown: string;
|
|
612
|
-
blocks: MdxBlock[];
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function replaceMdxCodeBlockComponents(raw: string): string {
|
|
616
|
-
return raw.replace(/<CodeBlock\b[\s\S]*?\/>/gu, (match) => {
|
|
617
|
-
const code = match.match(/code=\{`([\s\S]*?)`\}/u)?.[1];
|
|
618
|
-
if (!code) return "";
|
|
619
|
-
return `\n\n\`\`\`tsx\n${code.trim()}\n\`\`\`\n\n`;
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
export function parseMdxDocument(raw: string, fallbackTitle: string): ParsedMdxDocument {
|
|
624
|
-
const source = replaceMdxCodeBlockComponents(raw);
|
|
625
|
-
const cleanedLines: string[] = [];
|
|
626
|
-
let inFence = false;
|
|
627
|
-
let fenceLanguage = "";
|
|
628
|
-
let inFrontmatter = false;
|
|
629
|
-
let jsxTag: string | null = null;
|
|
630
|
-
for (const [idx, line] of source.split(/\r?\n/u).entries()) {
|
|
631
|
-
const trimmed = line.trim();
|
|
632
|
-
if (idx === 0 && trimmed === "---") {
|
|
633
|
-
inFrontmatter = true;
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
if (inFrontmatter) {
|
|
637
|
-
if (trimmed === "---") inFrontmatter = false;
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const fence = trimmed.match(/^(```|~~~)(.*)$/u);
|
|
641
|
-
if (fence) {
|
|
642
|
-
inFence = !inFence;
|
|
643
|
-
fenceLanguage = inFence ? fence[2].trim() : "";
|
|
644
|
-
cleanedLines.push(line);
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
if (!inFence) {
|
|
648
|
-
if (jsxTag !== null) {
|
|
649
|
-
if (
|
|
650
|
-
trimmed.includes(`</${jsxTag}>`) ||
|
|
651
|
-
trimmed.endsWith("/>") ||
|
|
652
|
-
trimmed === `</${jsxTag}>`
|
|
653
|
-
) {
|
|
654
|
-
jsxTag = null;
|
|
655
|
-
}
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
if (/^import\s/u.test(trimmed) || /^export\s/u.test(trimmed)) continue;
|
|
659
|
-
if (/^\{[^}]*\}$/u.test(trimmed)) continue;
|
|
660
|
-
const jsxOpen = trimmed.match(/^<([A-Z][A-Za-z0-9.:-]*|div|span|a|img)\b/u)?.[1];
|
|
661
|
-
if (jsxOpen) {
|
|
662
|
-
if (!trimmed.endsWith("/>") && !trimmed.includes(`</${jsxOpen}>`)) jsxTag = jsxOpen;
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
cleanedLines.push(line
|
|
666
|
-
.replace(/<\/?[A-Z][A-Za-z0-9.:-]*(?:\s+[^>]*)?>/gu, "")
|
|
667
|
-
.replace(/\{`([^`]+)`\}/gu, "`$1`")
|
|
668
|
-
.replace(/\{["'`]([^"'`]+)["'`]\}/gu, "$1"));
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
void fenceLanguage;
|
|
672
|
-
cleanedLines.push(line);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const markdown = compactWhitespace(cleanedLines.join("\n"));
|
|
676
|
-
const blocks: MdxBlock[] = [];
|
|
677
|
-
let paragraph: string[] = [];
|
|
678
|
-
let code: string[] = [];
|
|
679
|
-
let codeLanguage = "";
|
|
680
|
-
let readingCode = false;
|
|
681
|
-
function flushParagraph(): void {
|
|
682
|
-
const text = compactWhitespace(paragraph.join("\n"));
|
|
683
|
-
if (text.length > 0) blocks.push({ type: "paragraph", text });
|
|
684
|
-
paragraph = [];
|
|
685
|
-
}
|
|
686
|
-
for (const line of markdown.split(/\r?\n/u)) {
|
|
687
|
-
const fence = line.trim().match(/^(```|~~~)(.*)$/u);
|
|
688
|
-
if (fence) {
|
|
689
|
-
if (readingCode) {
|
|
690
|
-
blocks.push({ type: "code", language: codeLanguage, text: code.join("\n") });
|
|
691
|
-
code = [];
|
|
692
|
-
codeLanguage = "";
|
|
693
|
-
readingCode = false;
|
|
694
|
-
} else {
|
|
695
|
-
flushParagraph();
|
|
696
|
-
codeLanguage = fence[2].trim();
|
|
697
|
-
readingCode = true;
|
|
698
|
-
}
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
if (readingCode) {
|
|
702
|
-
code.push(line);
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
const heading = line.match(/^(#{1,6})\s+(.+)$/u);
|
|
706
|
-
if (heading) {
|
|
707
|
-
flushParagraph();
|
|
708
|
-
blocks.push({ type: "heading", depth: heading[1].length, text: heading[2].trim() });
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
if (line.trim().length === 0) {
|
|
712
|
-
flushParagraph();
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
paragraph.push(line);
|
|
716
|
-
}
|
|
717
|
-
flushParagraph();
|
|
718
|
-
const title = blocks.find((block) => block.type === "heading")?.text ?? fallbackTitle;
|
|
719
|
-
return { title, markdown, blocks };
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
type CodeLookupInput = AspectCaptureContext | { code_index?: CodeIndexContext };
|
|
723
|
-
|
|
724
|
-
function codePackages(input: CodeLookupInput): readonly CodeIndexPackage[] {
|
|
725
|
-
return "code" in input ? input.code.packages : input.code_index?.packages ?? [];
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function codeSymbols(input: CodeLookupInput): readonly CodeIndexSymbol[] {
|
|
729
|
-
return "code" in input ? input.code.symbols : input.code_index?.symbols ?? [];
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
export function resolvePackageNodeSlug(input: CodeLookupInput, packageSlug: string): string {
|
|
733
|
-
return codePackages(input).find((pkg) => pkg.package_slug === packageSlug)?.node_slug ?? packageSlug;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
export function resolveSymbolNodeSlug(input: CodeLookupInput, packageSlug: string, componentName: string): string {
|
|
737
|
-
const componentSlug = normalizeSymbolSlug(componentName);
|
|
738
|
-
const normalizedExport = componentName.toLowerCase();
|
|
739
|
-
const match = codeSymbols(input).find((symbol) => {
|
|
740
|
-
if (symbol.package_slug !== undefined && symbol.package_slug !== packageSlug) return false;
|
|
741
|
-
const nodeLeaf = symbol.node_slug.split("/").at(-1);
|
|
742
|
-
const names = [symbol.export_name, symbol.symbol_name, ...(symbol.aliases ?? []), nodeLeaf]
|
|
743
|
-
.filter((name): name is string => typeof name === "string" && name.length > 0);
|
|
744
|
-
return names.some((rawName) =>
|
|
745
|
-
normalizeSymbolSlug(rawName) === componentSlug || rawName.toLowerCase() === normalizedExport
|
|
746
|
-
);
|
|
747
|
-
});
|
|
748
|
-
return match?.node_slug ?? `${resolvePackageNodeSlug(input, packageSlug)}/symbol/${componentSlug}`;
|
|
749
|
-
}
|