@macroscope/cli 0.0.0 → 0.2.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/dist/cli.js +2918 -397
- package/dist/cli.js.map +1 -1
- package/dist/core/index.d.ts +91 -4
- package/dist/core/index.js +505 -6
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +458 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/assets/index-DNLqogGA.css +1 -0
- package/dist/ui/assets/index-EeHIpual.js +45 -0
- package/dist/ui/assets/index-EeHIpual.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +22 -17
- package/dist/ui/assets/index-ByDfVdzb.css +0 -1
- package/dist/ui/assets/index-D3mfLpRq.js +0 -45
- package/dist/ui/assets/index-D3mfLpRq.js.map +0 -1
- /package/dist/{index-BTDioymD.d.ts → version-BTDioymD.d.ts} +0 -0
package/dist/core/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { H as Handler, B as Block } from '../
|
|
2
|
-
export { a as BlockManifest, b as BuildResult, R as RenderResult, T as TestResult, V as VERSION, i as isHandler } from '../
|
|
1
|
+
import { H as Handler, B as Block } from '../version-BTDioymD.js';
|
|
2
|
+
export { a as BlockManifest, b as BuildResult, R as RenderResult, T as TestResult, V as VERSION, i as isHandler } from '../version-BTDioymD.js';
|
|
3
3
|
import * as zod from 'zod';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { BlockManifest } from '@macroscope/contracts';
|
|
5
|
+
import { BlockManifest, UploadPayload } from '@macroscope/contracts';
|
|
6
|
+
import ts from 'typescript';
|
|
6
7
|
|
|
7
8
|
declare const manifestSchema: zod.ZodObject<{
|
|
8
9
|
kind: zod.ZodString;
|
|
@@ -106,9 +107,23 @@ interface BlueprintLoadResult {
|
|
|
106
107
|
*/
|
|
107
108
|
declare function loadBlueprints(project: Project): Promise<BlueprintLoadResult>;
|
|
108
109
|
|
|
110
|
+
interface ResolvedPaths {
|
|
111
|
+
source?: string;
|
|
112
|
+
docs?: string;
|
|
113
|
+
preview?: string;
|
|
114
|
+
tests?: string[];
|
|
115
|
+
}
|
|
109
116
|
interface ScannedBlock extends Block {
|
|
110
117
|
/** SHA-256 over the manifest text. Used later by the index for change detection. */
|
|
111
118
|
contentHash: string;
|
|
119
|
+
/**
|
|
120
|
+
* Absolute paths to manifest-referenced files, when present and inside the project.
|
|
121
|
+
* If `manifest.source` is set but the file is missing or outside the project,
|
|
122
|
+
* `resolvedPaths.source` is absent and a corresponding `io` or `validation`
|
|
123
|
+
* ScanError is in `errors[]`. T5 (extractor) should treat a missing
|
|
124
|
+
* `resolvedPaths.source` as a no-op, not an error.
|
|
125
|
+
*/
|
|
126
|
+
resolvedPaths?: ResolvedPaths;
|
|
112
127
|
}
|
|
113
128
|
interface ScanError {
|
|
114
129
|
file: string;
|
|
@@ -122,4 +137,76 @@ interface ScanResult {
|
|
|
122
137
|
}
|
|
123
138
|
declare function scan(project: Project): Promise<ScanResult>;
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
interface ExtractorOptions {
|
|
141
|
+
/** Cap on README excerpt characters. Default 500. */
|
|
142
|
+
readmeExcerptMax?: number;
|
|
143
|
+
/** Test seam — inject a TS Program factory. Defaults to the real ts.createProgram. */
|
|
144
|
+
programFactory?: (sourceFiles: string[]) => ts.Program;
|
|
145
|
+
}
|
|
146
|
+
interface ExtractionError {
|
|
147
|
+
blockId: string;
|
|
148
|
+
kind: 'parse' | 'io' | 'symbol';
|
|
149
|
+
message: string;
|
|
150
|
+
}
|
|
151
|
+
interface ExtractionResult {
|
|
152
|
+
/** One payload per block that had a valid `kind` (the only required manifest field). */
|
|
153
|
+
payloads: UploadPayload[];
|
|
154
|
+
/** Errors collected during extraction. NEVER thrown — bad blocks are skipped. */
|
|
155
|
+
errors: ExtractionError[];
|
|
156
|
+
}
|
|
157
|
+
declare function extractAll(blocks: ScannedBlock[], opts?: ExtractorOptions): Promise<ExtractionResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Turn raw markdown/text into a bounded excerpt safe for embedding similarity:
|
|
160
|
+
* collapse whitespace, strip the markdown formatting that confuses semantic search
|
|
161
|
+
* (`#`, `*`, backticks), and truncate at a word boundary.
|
|
162
|
+
*/
|
|
163
|
+
declare function formatReadmeExcerpt(raw: string, max: number): string;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Deterministic SHA-256 over the canonical-JSON serialisation of the payload.
|
|
167
|
+
* Returns 64-character lowercase hex.
|
|
168
|
+
*/
|
|
169
|
+
declare function hashPayload(payload: UploadPayload): string;
|
|
170
|
+
interface HasherOptions {
|
|
171
|
+
/** Override the workspace root for tests. Defaults to `process.cwd()`. */
|
|
172
|
+
workspaceRoot?: string;
|
|
173
|
+
}
|
|
174
|
+
interface CacheEntry {
|
|
175
|
+
/** SHA-256 hex of the canonical-JSON serialisation of the UploadPayload. */
|
|
176
|
+
hash: string;
|
|
177
|
+
/** Max(mtime ms) of the block's manifest + source + docs at hash time. */
|
|
178
|
+
mtime: number;
|
|
179
|
+
}
|
|
180
|
+
type HashCache = Record<string, CacheEntry>;
|
|
181
|
+
/**
|
|
182
|
+
* Load the cache from disk. NEVER throws: missing file or malformed JSON
|
|
183
|
+
* return `{}` so the watcher can recover by re-hashing everything. Transparently
|
|
184
|
+
* handles both the wrapped `{ schemaVersion, entries }` shape and the legacy
|
|
185
|
+
* flat shape, returning entries either way.
|
|
186
|
+
*/
|
|
187
|
+
declare function loadHashCache(opts?: HasherOptions): Promise<HashCache>;
|
|
188
|
+
/**
|
|
189
|
+
* Atomically write the cache to disk: write to a sibling `.tmp` file then
|
|
190
|
+
* rename. Same-directory rename is atomic on POSIX filesystems. Throws on
|
|
191
|
+
* real I/O failure — callers (watcher loop) decide whether to surface or
|
|
192
|
+
* swallow the error.
|
|
193
|
+
*
|
|
194
|
+
* The on-disk shape is the wrapped envelope (`schemaVersion` + `entries`);
|
|
195
|
+
* `schemaVersion` defaults to the current `SCHEMA_VERSION` from
|
|
196
|
+
* `@macroscope/contracts` so T14's existing callsites transparently persist it.
|
|
197
|
+
*/
|
|
198
|
+
declare function saveHashCache(cache: HashCache, opts?: HasherOptions & {
|
|
199
|
+
schemaVersion?: number;
|
|
200
|
+
}): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Return the subset of blocks whose manifest, source, or docs files have an
|
|
203
|
+
* mtime strictly greater than the cached entry's recorded mtime. Blocks
|
|
204
|
+
* with no cache entry are always returned (first-time extraction needs them).
|
|
205
|
+
*
|
|
206
|
+
* Missing files (e.g. deleted between scan and stat) are treated as "changed"
|
|
207
|
+
* — safer to re-process than silently drop. The watcher reconciles deletions
|
|
208
|
+
* separately via "in cache but not in scan" diffing.
|
|
209
|
+
*/
|
|
210
|
+
declare function filterChangedBlocks(blocks: ScannedBlock[], cache: HashCache): Promise<ScannedBlock[]>;
|
|
211
|
+
|
|
212
|
+
export { Block, type BlueprintLoadCause, type BlueprintLoadError, type BlueprintLoadResult, type CacheEntry, type ExtractionError, type ExtractionResult, type ExtractorOptions, Handler, type HandlerCapability, type HashCache, type HasherOptions, type LoadedBlueprint, type Manifest, type Project, type ProjectConfig, ProjectNotFoundError, type ResolvedPaths, type ScanError, type ScanResult, type ScannedBlock, extractAll, filterChangedBlocks, findProject, formatReadmeExcerpt, hashPayload, loadBlueprints, loadHashCache, manifestSchema, projectConfigSchema, saveHashCache, scan };
|
package/dist/core/index.js
CHANGED
|
@@ -7,6 +7,30 @@ import { z as z2 } from "zod";
|
|
|
7
7
|
import { z as z3 } from "zod";
|
|
8
8
|
import { z as z4 } from "zod";
|
|
9
9
|
import { z as z5 } from "zod";
|
|
10
|
+
import { z as z6 } from "zod";
|
|
11
|
+
var SCHEMA_VERSION = 1;
|
|
12
|
+
var FILESYSTEM_LAYOUT = {
|
|
13
|
+
user: {
|
|
14
|
+
/** `~/.macroscope/` */
|
|
15
|
+
rootDir: ".macroscope",
|
|
16
|
+
/** `~/.macroscope/credentials` — OAuth token store, mode 0600. */
|
|
17
|
+
credentialsFile: ".macroscope/credentials",
|
|
18
|
+
/** `~/.macroscope/machine-id` — stable per-install identifier. */
|
|
19
|
+
machineIdFile: ".macroscope/machine-id"
|
|
20
|
+
},
|
|
21
|
+
project: {
|
|
22
|
+
/** `<project>/.macroscope/` */
|
|
23
|
+
rootDir: ".macroscope",
|
|
24
|
+
/** `<project>/.macroscope/blueprints/` — one folder per kind. */
|
|
25
|
+
blueprintsDir: ".macroscope/blueprints",
|
|
26
|
+
/** `<project>/.macroscope/cache/` */
|
|
27
|
+
cacheDir: ".macroscope/cache",
|
|
28
|
+
/** `<project>/.macroscope/cache/hashes.json` — content-hash cache used by catch-up scan. */
|
|
29
|
+
hashesFile: ".macroscope/cache/hashes.json",
|
|
30
|
+
/** `<project>/.macroscope/cache/queue.json` — watcher push queue, survives restart. */
|
|
31
|
+
queueFile: ".macroscope/cache/queue.json"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
10
34
|
var BlockManifestSchema = z.object({
|
|
11
35
|
/** Must reference a registered blueprint's `kind`. */
|
|
12
36
|
kind: z.string().min(1),
|
|
@@ -126,6 +150,23 @@ var ErrorEnvelopeSchema = z5.object({
|
|
|
126
150
|
requestId: z5.string().optional()
|
|
127
151
|
})
|
|
128
152
|
});
|
|
153
|
+
var ProviderSchema = z6.enum(["github", "gitlab"]);
|
|
154
|
+
var OAuthCredentialsSchema = z6.object({
|
|
155
|
+
provider: ProviderSchema,
|
|
156
|
+
accessToken: z6.string().min(1),
|
|
157
|
+
refreshToken: z6.string().min(1).optional(),
|
|
158
|
+
/** Unix epoch milliseconds at which `accessToken` expires. */
|
|
159
|
+
expiresAt: z6.number().int().nonnegative().optional(),
|
|
160
|
+
/** Provider-side user id (e.g. GitHub `id`, GitLab `id`). Optional — useful
|
|
161
|
+
* for UI display ("logged in as alexspdlr on github") and for the CLI to
|
|
162
|
+
* short-circuit redundant `/user` lookups. */
|
|
163
|
+
providerUserId: z6.string().min(1).optional()
|
|
164
|
+
});
|
|
165
|
+
var UserContextSchema = z6.object({
|
|
166
|
+
userId: z6.string().min(1),
|
|
167
|
+
indexName: z6.string().min(1),
|
|
168
|
+
provider: ProviderSchema
|
|
169
|
+
});
|
|
129
170
|
|
|
130
171
|
// src/core/manifest.ts
|
|
131
172
|
var manifestSchema = BlockManifestSchema;
|
|
@@ -190,10 +231,10 @@ import { existsSync } from "fs";
|
|
|
190
231
|
import { readFile } from "fs/promises";
|
|
191
232
|
import { dirname, join, resolve } from "path";
|
|
192
233
|
import { parse as parseYaml } from "yaml";
|
|
193
|
-
import { z as
|
|
194
|
-
var projectConfigSchema =
|
|
195
|
-
scanRoots:
|
|
196
|
-
ignore:
|
|
234
|
+
import { z as z7 } from "zod";
|
|
235
|
+
var projectConfigSchema = z7.object({
|
|
236
|
+
scanRoots: z7.array(z7.string()).default(["."]),
|
|
237
|
+
ignore: z7.array(z7.string()).default(["node_modules", ".git", "dist", ".macroscope"])
|
|
197
238
|
});
|
|
198
239
|
var ProjectNotFoundError = class extends Error {
|
|
199
240
|
constructor(startedFrom) {
|
|
@@ -361,7 +402,7 @@ function extractDefault(mod) {
|
|
|
361
402
|
import { createHash } from "crypto";
|
|
362
403
|
import { existsSync as existsSync3 } from "fs";
|
|
363
404
|
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
364
|
-
import { dirname as dirname2, join as join3, relative, sep } from "path";
|
|
405
|
+
import { dirname as dirname2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
365
406
|
import { parse as parseYaml2 } from "yaml";
|
|
366
407
|
async function scan(project) {
|
|
367
408
|
const blocks = [];
|
|
@@ -374,6 +415,7 @@ async function scan(project) {
|
|
|
374
415
|
for await (const manifestPath of findManifests(start, ignore)) {
|
|
375
416
|
const result = await parseBlock(manifestPath, project.root);
|
|
376
417
|
if (result.kind === "ok") {
|
|
418
|
+
errors.push(...result.errors);
|
|
377
419
|
if (seenIds.has(result.block.id)) {
|
|
378
420
|
errors.push({
|
|
379
421
|
file: manifestPath,
|
|
@@ -407,6 +449,7 @@ async function* findManifests(dir, ignore) {
|
|
|
407
449
|
}
|
|
408
450
|
for (const entry of entries) {
|
|
409
451
|
if (!entry.isDirectory()) continue;
|
|
452
|
+
if (entry.isSymbolicLink()) continue;
|
|
410
453
|
if (ignore.has(entry.name)) continue;
|
|
411
454
|
yield* findManifests(join3(dir, entry.name), ignore);
|
|
412
455
|
}
|
|
@@ -456,11 +499,85 @@ async function parseBlock(manifestPath, projectRoot) {
|
|
|
456
499
|
const defaultId = toPosix(relative(projectRoot, blockDir));
|
|
457
500
|
const id = manifest.id ?? defaultId;
|
|
458
501
|
const contentHash = createHash("sha256").update(raw).digest("hex");
|
|
502
|
+
const fileErrors = [];
|
|
503
|
+
const resolvedPaths = resolveManifestPaths(
|
|
504
|
+
manifest,
|
|
505
|
+
blockDir,
|
|
506
|
+
projectRoot,
|
|
507
|
+
manifestPath,
|
|
508
|
+
fileErrors
|
|
509
|
+
);
|
|
459
510
|
return {
|
|
460
511
|
kind: "ok",
|
|
461
|
-
block: { id, path: blockDir, manifest, contentHash }
|
|
512
|
+
block: { id, path: blockDir, manifest, contentHash, resolvedPaths },
|
|
513
|
+
errors: fileErrors
|
|
462
514
|
};
|
|
463
515
|
}
|
|
516
|
+
var FILE_FIELDS = ["source", "docs", "preview"];
|
|
517
|
+
function resolveManifestPaths(manifest, blockDir, projectRoot, manifestPath, errors) {
|
|
518
|
+
const testsRaw = manifest.tests;
|
|
519
|
+
const testsArray = testsRaw == null ? void 0 : typeof testsRaw === "string" ? [testsRaw] : testsRaw;
|
|
520
|
+
const hasAnyField = FILE_FIELDS.some((f) => manifest[f] != null) || testsArray != null;
|
|
521
|
+
if (!hasAnyField) return void 0;
|
|
522
|
+
const resolved = {};
|
|
523
|
+
for (const field of FILE_FIELDS) {
|
|
524
|
+
const value = manifest[field];
|
|
525
|
+
if (value == null) continue;
|
|
526
|
+
const abs = resolve2(blockDir, value);
|
|
527
|
+
if (!isInsideProject(abs, projectRoot)) {
|
|
528
|
+
errors.push({
|
|
529
|
+
file: manifestPath,
|
|
530
|
+
kind: "validation",
|
|
531
|
+
field,
|
|
532
|
+
message: `Field \`${field}\` in ${manifestPath} resolves to ${abs}, which is outside the project root ${projectRoot}.`
|
|
533
|
+
});
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (!existsSync3(abs)) {
|
|
537
|
+
errors.push({
|
|
538
|
+
file: manifestPath,
|
|
539
|
+
kind: "io",
|
|
540
|
+
field,
|
|
541
|
+
message: `Field \`${field}\` in ${manifestPath} references ${abs}, which does not exist.`
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
resolved[field] = abs;
|
|
546
|
+
}
|
|
547
|
+
if (testsArray) {
|
|
548
|
+
const resolvedTests = [];
|
|
549
|
+
for (const t of testsArray) {
|
|
550
|
+
const abs = resolve2(blockDir, t);
|
|
551
|
+
if (!isInsideProject(abs, projectRoot)) {
|
|
552
|
+
errors.push({
|
|
553
|
+
file: manifestPath,
|
|
554
|
+
kind: "validation",
|
|
555
|
+
field: "tests",
|
|
556
|
+
message: `Field \`tests\` in ${manifestPath} resolves to ${abs}, which is outside the project root ${projectRoot}.`
|
|
557
|
+
});
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (!existsSync3(abs)) {
|
|
561
|
+
errors.push({
|
|
562
|
+
file: manifestPath,
|
|
563
|
+
kind: "io",
|
|
564
|
+
field: "tests",
|
|
565
|
+
message: `Field \`tests\` in ${manifestPath} references ${abs}, which does not exist.`
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
resolvedTests.push(abs);
|
|
570
|
+
}
|
|
571
|
+
if (resolvedTests.length > 0) {
|
|
572
|
+
resolved.tests = resolvedTests;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
576
|
+
}
|
|
577
|
+
function isInsideProject(absPath, projectRoot) {
|
|
578
|
+
const normalized = resolve2(absPath);
|
|
579
|
+
return normalized === projectRoot || normalized.startsWith(projectRoot + sep);
|
|
580
|
+
}
|
|
464
581
|
function firstZodIssue(err) {
|
|
465
582
|
const issue = err.issues[0];
|
|
466
583
|
if (!issue) return { fieldPath: "(root)", message: "unknown validation error" };
|
|
@@ -470,14 +587,396 @@ function firstZodIssue(err) {
|
|
|
470
587
|
function toPosix(p) {
|
|
471
588
|
return p.split(sep).join("/");
|
|
472
589
|
}
|
|
590
|
+
|
|
591
|
+
// src/core/extractor.ts
|
|
592
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
593
|
+
import ts from "typescript";
|
|
594
|
+
var DEFAULT_README_EXCERPT_MAX = 500;
|
|
595
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
596
|
+
var DEFAULT_COMPILER_OPTIONS = {
|
|
597
|
+
target: ts.ScriptTarget.ES2022,
|
|
598
|
+
module: ts.ModuleKind.ESNext,
|
|
599
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
600
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
601
|
+
allowJs: false,
|
|
602
|
+
noEmit: true,
|
|
603
|
+
skipLibCheck: true,
|
|
604
|
+
esModuleInterop: true,
|
|
605
|
+
allowSyntheticDefaultImports: true,
|
|
606
|
+
strict: false,
|
|
607
|
+
isolatedModules: true
|
|
608
|
+
};
|
|
609
|
+
function defaultProgramFactory(rootFiles) {
|
|
610
|
+
return ts.createProgram(rootFiles, DEFAULT_COMPILER_OPTIONS);
|
|
611
|
+
}
|
|
612
|
+
async function extractAll(blocks, opts = {}) {
|
|
613
|
+
const readmeMax = opts.readmeExcerptMax ?? DEFAULT_README_EXCERPT_MAX;
|
|
614
|
+
const payloads = [];
|
|
615
|
+
const errors = [];
|
|
616
|
+
const sourcesToBlocks = /* @__PURE__ */ new Map();
|
|
617
|
+
for (const block of blocks) {
|
|
618
|
+
if (!block.manifest?.kind) continue;
|
|
619
|
+
const src = block.resolvedPaths?.source;
|
|
620
|
+
if (src && isTsSource(src)) {
|
|
621
|
+
sourcesToBlocks.set(src, block);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
let program;
|
|
625
|
+
let checker;
|
|
626
|
+
if (sourcesToBlocks.size > 0) {
|
|
627
|
+
const rootFiles = Array.from(sourcesToBlocks.keys());
|
|
628
|
+
const factory = opts.programFactory ?? defaultProgramFactory;
|
|
629
|
+
program = factory(rootFiles);
|
|
630
|
+
checker = program.getTypeChecker();
|
|
631
|
+
}
|
|
632
|
+
for (const block of blocks) {
|
|
633
|
+
if (!block.manifest?.kind) continue;
|
|
634
|
+
const payload = {
|
|
635
|
+
blockId: block.id,
|
|
636
|
+
kind: block.manifest.kind,
|
|
637
|
+
symbols: []
|
|
638
|
+
};
|
|
639
|
+
const { name, description, tags } = block.manifest;
|
|
640
|
+
if (typeof name === "string") payload.name = name;
|
|
641
|
+
if (typeof description === "string") payload.description = description;
|
|
642
|
+
if (Array.isArray(tags)) payload.tags = tags;
|
|
643
|
+
const sourcePath = block.resolvedPaths?.source;
|
|
644
|
+
if (sourcePath && isTsSource(sourcePath) && program && checker) {
|
|
645
|
+
const sourceFile = program.getSourceFile(sourcePath);
|
|
646
|
+
if (!sourceFile) {
|
|
647
|
+
errors.push({
|
|
648
|
+
blockId: block.id,
|
|
649
|
+
kind: "parse",
|
|
650
|
+
message: `TS Program did not load ${sourcePath} (parse error or missing file)`
|
|
651
|
+
});
|
|
652
|
+
} else if (hasFatalSyntaxErrors(program, sourceFile)) {
|
|
653
|
+
errors.push({
|
|
654
|
+
blockId: block.id,
|
|
655
|
+
kind: "parse",
|
|
656
|
+
message: `Source file ${sourcePath} has syntax errors; skipping symbol extraction`
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
payload.symbols = extractSymbols(sourceFile, checker);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const docsPath = block.resolvedPaths?.docs;
|
|
663
|
+
if (docsPath) {
|
|
664
|
+
try {
|
|
665
|
+
const raw = await readFile3(docsPath, "utf8");
|
|
666
|
+
payload.readmeExcerpt = formatReadmeExcerpt(raw, readmeMax);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
errors.push({
|
|
669
|
+
blockId: block.id,
|
|
670
|
+
kind: "io",
|
|
671
|
+
message: `Failed to read docs at ${docsPath}: ${err.message}`
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
payloads.push(payload);
|
|
676
|
+
}
|
|
677
|
+
return { payloads, errors };
|
|
678
|
+
}
|
|
679
|
+
function isTsSource(path) {
|
|
680
|
+
const dot = path.lastIndexOf(".");
|
|
681
|
+
if (dot < 0) return false;
|
|
682
|
+
return TS_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
683
|
+
}
|
|
684
|
+
function hasFatalSyntaxErrors(program, sourceFile) {
|
|
685
|
+
const diagnostics = program.getSyntacticDiagnostics(sourceFile);
|
|
686
|
+
return diagnostics.length > 0;
|
|
687
|
+
}
|
|
688
|
+
function extractSymbols(sourceFile, checker) {
|
|
689
|
+
const symbols = [];
|
|
690
|
+
for (const statement of sourceFile.statements) {
|
|
691
|
+
visitTopLevel(statement, sourceFile, checker, symbols);
|
|
692
|
+
}
|
|
693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
694
|
+
return symbols.filter((s) => {
|
|
695
|
+
const key = `${s.kind}\0${s.name}`;
|
|
696
|
+
if (seen.has(key)) return false;
|
|
697
|
+
seen.add(key);
|
|
698
|
+
return true;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function visitTopLevel(node, sourceFile, checker, out) {
|
|
702
|
+
if (ts.isExportAssignment(node)) {
|
|
703
|
+
out.push(extractDefaultExport(node, sourceFile, checker));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (ts.isExportDeclaration(node)) {
|
|
707
|
+
extractExportDeclaration(node, sourceFile, checker, out);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (!hasExportModifier(node)) return;
|
|
711
|
+
const isDefault = hasDefaultModifier(node);
|
|
712
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
713
|
+
const name = node.name?.text ?? (isDefault ? "default" : void 0);
|
|
714
|
+
if (!name) return;
|
|
715
|
+
out.push({
|
|
716
|
+
name,
|
|
717
|
+
kind: "function",
|
|
718
|
+
signature: renderFunctionSignature(node, name, sourceFile, checker)
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (ts.isClassDeclaration(node)) {
|
|
723
|
+
const name = node.name?.text ?? (isDefault ? "default" : void 0);
|
|
724
|
+
if (!name) return;
|
|
725
|
+
out.push({ name, kind: "class", signature: `class ${name}` });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
729
|
+
out.push({
|
|
730
|
+
name: node.name.text,
|
|
731
|
+
kind: "interface",
|
|
732
|
+
signature: oneLineDeclaration(node, sourceFile, `interface ${node.name.text}`)
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
737
|
+
out.push({
|
|
738
|
+
name: node.name.text,
|
|
739
|
+
kind: "type",
|
|
740
|
+
signature: oneLineDeclaration(node, sourceFile, `type ${node.name.text}`)
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (ts.isEnumDeclaration(node)) {
|
|
745
|
+
out.push({ name: node.name.text, kind: "enum", signature: `enum ${node.name.text}` });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (ts.isVariableStatement(node)) {
|
|
749
|
+
for (const decl of node.declarationList.declarations) {
|
|
750
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
751
|
+
const name = decl.name.text;
|
|
752
|
+
out.push({
|
|
753
|
+
name,
|
|
754
|
+
kind: "const",
|
|
755
|
+
signature: renderVariableSignature(decl, name, checker)
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function extractDefaultExport(node, sourceFile, checker) {
|
|
762
|
+
const expr = node.expression;
|
|
763
|
+
if (ts.isIdentifier(expr)) {
|
|
764
|
+
const sym = checker.getSymbolAtLocation(expr);
|
|
765
|
+
const type = sym ? checker.getTypeOfSymbolAtLocation(sym, expr) : void 0;
|
|
766
|
+
const signature = type ? checker.typeToString(type) : "default";
|
|
767
|
+
return { name: "default", kind: "const", signature };
|
|
768
|
+
}
|
|
769
|
+
return { name: "default", kind: "const", signature: expr.getText(sourceFile) };
|
|
770
|
+
}
|
|
771
|
+
function extractExportDeclaration(node, sourceFile, checker, out) {
|
|
772
|
+
if (!node.exportClause && node.moduleSpecifier) {
|
|
773
|
+
const moduleSymbol = checker.getSymbolAtLocation(node.moduleSpecifier);
|
|
774
|
+
if (moduleSymbol) {
|
|
775
|
+
for (const exp of checker.getExportsOfModule(moduleSymbol)) {
|
|
776
|
+
out.push(renderExportedSymbol(exp.name, exp, checker, sourceFile));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
782
|
+
for (const spec of node.exportClause.elements) {
|
|
783
|
+
const name = spec.name.text;
|
|
784
|
+
const sym = checker.getSymbolAtLocation(spec.name);
|
|
785
|
+
out.push(renderExportedSymbol(name, sym, checker, sourceFile));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function renderExportedSymbol(name, symbol, checker, sourceFile) {
|
|
790
|
+
if (!symbol) return { name, kind: "const", signature: name };
|
|
791
|
+
const aliased = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
|
|
792
|
+
const decl = aliased.declarations?.[0];
|
|
793
|
+
if (decl) {
|
|
794
|
+
if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) {
|
|
795
|
+
return {
|
|
796
|
+
name,
|
|
797
|
+
kind: "function",
|
|
798
|
+
signature: renderFunctionSignature(decl, name, sourceFile, checker)
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (ts.isClassDeclaration(decl)) return { name, kind: "class", signature: `class ${name}` };
|
|
802
|
+
if (ts.isInterfaceDeclaration(decl)) {
|
|
803
|
+
return {
|
|
804
|
+
name,
|
|
805
|
+
kind: "interface",
|
|
806
|
+
signature: oneLineDeclaration(decl, decl.getSourceFile(), `interface ${name}`)
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
810
|
+
return {
|
|
811
|
+
name,
|
|
812
|
+
kind: "type",
|
|
813
|
+
signature: oneLineDeclaration(decl, decl.getSourceFile(), `type ${name}`)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (ts.isEnumDeclaration(decl)) return { name, kind: "enum", signature: `enum ${name}` };
|
|
817
|
+
}
|
|
818
|
+
const type = checker.getTypeOfSymbolAtLocation(aliased, sourceFile);
|
|
819
|
+
return { name, kind: "const", signature: checker.typeToString(type) };
|
|
820
|
+
}
|
|
821
|
+
function renderFunctionSignature(node, name, _sourceFile, checker) {
|
|
822
|
+
const signature = checker.getSignatureFromDeclaration(node);
|
|
823
|
+
if (!signature) return `function ${name}()`;
|
|
824
|
+
return `function ${name}${checker.signatureToString(signature)}`;
|
|
825
|
+
}
|
|
826
|
+
function renderVariableSignature(decl, name, checker) {
|
|
827
|
+
const type = checker.getTypeAtLocation(decl);
|
|
828
|
+
return `const ${name}: ${checker.typeToString(type)}`;
|
|
829
|
+
}
|
|
830
|
+
function oneLineDeclaration(node, sourceFile, fallback) {
|
|
831
|
+
try {
|
|
832
|
+
const text = node.getText(sourceFile);
|
|
833
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
834
|
+
return collapsed.length > 0 ? collapsed : fallback;
|
|
835
|
+
} catch {
|
|
836
|
+
return fallback;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function hasExportModifier(node) {
|
|
840
|
+
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
|
|
841
|
+
}
|
|
842
|
+
function hasDefaultModifier(node) {
|
|
843
|
+
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Default) !== 0;
|
|
844
|
+
}
|
|
845
|
+
function formatReadmeExcerpt(raw, max) {
|
|
846
|
+
const stripped = raw.replace(/[#*`_~>]/g, "");
|
|
847
|
+
const collapsed = stripped.replace(/\s+/g, " ").trim();
|
|
848
|
+
if (collapsed.length <= max) return collapsed;
|
|
849
|
+
const slice = collapsed.slice(0, max);
|
|
850
|
+
const nextChar = collapsed.charAt(max);
|
|
851
|
+
if (nextChar === "" || /\s/.test(nextChar)) return slice;
|
|
852
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
853
|
+
if (lastSpace > 0) return slice.slice(0, lastSpace);
|
|
854
|
+
return slice;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/core/hasher.ts
|
|
858
|
+
import { createHash as createHash2 } from "crypto";
|
|
859
|
+
import { mkdir, readFile as readFile4, rename, stat, writeFile } from "fs/promises";
|
|
860
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
861
|
+
function canonicalJSON(value) {
|
|
862
|
+
return stringify(value);
|
|
863
|
+
}
|
|
864
|
+
function stringify(value) {
|
|
865
|
+
if (value === null) return "null";
|
|
866
|
+
if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
|
|
867
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
868
|
+
if (Array.isArray(value)) {
|
|
869
|
+
const parts = value.map((v) => v === void 0 ? "null" : stringify(v));
|
|
870
|
+
return `[${parts.join(",")}]`;
|
|
871
|
+
}
|
|
872
|
+
if (typeof value === "object") {
|
|
873
|
+
const obj = value;
|
|
874
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
875
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${stringify(obj[k])}`);
|
|
876
|
+
return `{${parts.join(",")}}`;
|
|
877
|
+
}
|
|
878
|
+
throw new TypeError(`canonicalJSON: unsupported value type ${typeof value}`);
|
|
879
|
+
}
|
|
880
|
+
function hashPayload(payload) {
|
|
881
|
+
const canonical = canonicalJSON(payload);
|
|
882
|
+
return createHash2("sha256").update(canonical, "utf8").digest("hex");
|
|
883
|
+
}
|
|
884
|
+
function resolveHashesPath(opts) {
|
|
885
|
+
const root = opts?.workspaceRoot ?? process.cwd();
|
|
886
|
+
return join4(root, FILESYSTEM_LAYOUT.project.hashesFile);
|
|
887
|
+
}
|
|
888
|
+
async function readCacheFile(opts) {
|
|
889
|
+
const path = resolveHashesPath(opts);
|
|
890
|
+
let raw;
|
|
891
|
+
try {
|
|
892
|
+
raw = await readFile4(path, "utf8");
|
|
893
|
+
} catch (err) {
|
|
894
|
+
const code = err.code;
|
|
895
|
+
if (code !== "ENOENT") {
|
|
896
|
+
console.warn(`[macroscope] failed to read hash cache at ${path}: ${err.message}`);
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
let parsed;
|
|
901
|
+
try {
|
|
902
|
+
parsed = JSON.parse(raw);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.warn(`[macroscope] hash cache at ${path} is not valid JSON: ${err.message}`);
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
908
|
+
console.warn(`[macroscope] hash cache at ${path} has unexpected shape; ignoring`);
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
const obj = parsed;
|
|
912
|
+
if (typeof obj.schemaVersion === "number" && obj.entries && typeof obj.entries === "object" && !Array.isArray(obj.entries)) {
|
|
913
|
+
return { schemaVersion: obj.schemaVersion, entries: obj.entries };
|
|
914
|
+
}
|
|
915
|
+
return { schemaVersion: void 0, entries: obj };
|
|
916
|
+
}
|
|
917
|
+
async function loadHashCache(opts) {
|
|
918
|
+
const file = await readCacheFile(opts);
|
|
919
|
+
return file?.entries ?? {};
|
|
920
|
+
}
|
|
921
|
+
async function saveHashCache(cache, opts) {
|
|
922
|
+
const path = resolveHashesPath(opts);
|
|
923
|
+
const dir = dirname3(path);
|
|
924
|
+
await mkdir(dir, { recursive: true });
|
|
925
|
+
const tmp = `${path}.tmp`;
|
|
926
|
+
const envelope = {
|
|
927
|
+
schemaVersion: opts?.schemaVersion ?? SCHEMA_VERSION,
|
|
928
|
+
entries: cache
|
|
929
|
+
};
|
|
930
|
+
const body = `${JSON.stringify(envelope, null, 2)}
|
|
931
|
+
`;
|
|
932
|
+
await writeFile(tmp, body, "utf8");
|
|
933
|
+
await rename(tmp, path);
|
|
934
|
+
}
|
|
935
|
+
async function filterChangedBlocks(blocks, cache) {
|
|
936
|
+
const changed = [];
|
|
937
|
+
for (const block of blocks) {
|
|
938
|
+
const cached = cache[block.id];
|
|
939
|
+
if (!cached) {
|
|
940
|
+
changed.push(block);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const maxMtime = await maxFileMtime(block);
|
|
944
|
+
if (maxMtime === void 0 || maxMtime > cached.mtime) {
|
|
945
|
+
changed.push(block);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return changed;
|
|
949
|
+
}
|
|
950
|
+
async function maxFileMtime(block) {
|
|
951
|
+
const paths = [join4(block.path, "macroscope.yaml")];
|
|
952
|
+
if (block.resolvedPaths?.source) paths.push(block.resolvedPaths.source);
|
|
953
|
+
if (block.resolvedPaths?.docs) paths.push(block.resolvedPaths.docs);
|
|
954
|
+
let max;
|
|
955
|
+
for (const p of paths) {
|
|
956
|
+
try {
|
|
957
|
+
const s = await stat(p);
|
|
958
|
+
const m = s.mtimeMs;
|
|
959
|
+
if (max === void 0 || m > max) max = m;
|
|
960
|
+
} catch {
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return max;
|
|
965
|
+
}
|
|
473
966
|
export {
|
|
474
967
|
ProjectNotFoundError,
|
|
475
968
|
VERSION,
|
|
969
|
+
extractAll,
|
|
970
|
+
filterChangedBlocks,
|
|
476
971
|
findProject,
|
|
972
|
+
formatReadmeExcerpt,
|
|
973
|
+
hashPayload,
|
|
477
974
|
isHandler,
|
|
478
975
|
loadBlueprints,
|
|
976
|
+
loadHashCache,
|
|
479
977
|
manifestSchema,
|
|
480
978
|
projectConfigSchema,
|
|
979
|
+
saveHashCache,
|
|
481
980
|
scan
|
|
482
981
|
};
|
|
483
982
|
//# sourceMappingURL=index.js.map
|