@macroscope/cli 0.0.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/README.md +14 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1473 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/index.d.ts +125 -0
- package/dist/core/index.js +483 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-BTDioymD.d.ts +177 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/assets/index-ByDfVdzb.css +1 -0
- package/dist/ui/assets/index-D3mfLpRq.js +45 -0
- package/dist/ui/assets/index-D3mfLpRq.js.map +1 -0
- package/dist/ui/index.html +13 -0
- package/package.json +75 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/core/handler.ts
|
|
13
|
+
function isHandler(value) {
|
|
14
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15
|
+
const v = value;
|
|
16
|
+
if (typeof v.kind !== "string" || v.kind.length === 0) return false;
|
|
17
|
+
for (const method of ["render", "test", "build", "scaffold", "describe"]) {
|
|
18
|
+
if (v[method] !== void 0 && typeof v[method] !== "function") return false;
|
|
19
|
+
}
|
|
20
|
+
if (v.views !== void 0) {
|
|
21
|
+
if (typeof v.views !== "object" || v.views === null) return false;
|
|
22
|
+
for (const [, spec] of Object.entries(v.views)) {
|
|
23
|
+
if (!isViewSpec(spec)) return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function isViewSpec(value) {
|
|
29
|
+
if (typeof value !== "object" || value === null) return false;
|
|
30
|
+
const v = value;
|
|
31
|
+
switch (v.type) {
|
|
32
|
+
case "iframe":
|
|
33
|
+
if (typeof v.url !== "string" && typeof v.url !== "function") return false;
|
|
34
|
+
if (v.service !== void 0 && !isServiceSpec(v.service)) return false;
|
|
35
|
+
return true;
|
|
36
|
+
case "terminal":
|
|
37
|
+
return typeof v.command === "function" || Array.isArray(v.command) && v.command.every((c) => typeof c === "string");
|
|
38
|
+
case "hybrid": {
|
|
39
|
+
const urlOk = typeof v.url === "string" || typeof v.url === "function";
|
|
40
|
+
const commandOk = typeof v.command === "function" || Array.isArray(v.command) && v.command.every((c) => typeof c === "string");
|
|
41
|
+
if (!urlOk || !commandOk) return false;
|
|
42
|
+
for (const key of ["runLabel", "codeLabel", "terminalLabel"]) {
|
|
43
|
+
if (v[key] !== void 0 && typeof v[key] !== "string") return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isServiceSpec(value) {
|
|
52
|
+
if (typeof value !== "object" || value === null) return false;
|
|
53
|
+
const v = value;
|
|
54
|
+
const commandOk = typeof v.command === "function" || Array.isArray(v.command) && v.command.every((c) => typeof c === "string");
|
|
55
|
+
if (!commandOk) return false;
|
|
56
|
+
if (typeof v.readyWhen !== "object" || v.readyWhen === null) return false;
|
|
57
|
+
const ready = v.readyWhen;
|
|
58
|
+
if (typeof ready.url !== "string") return false;
|
|
59
|
+
if (v.name !== void 0 && typeof v.name !== "string") return false;
|
|
60
|
+
if (v.cwd !== void 0 && typeof v.cwd !== "string" && typeof v.cwd !== "function") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (v.startupTimeoutMs !== void 0 && typeof v.startupTimeoutMs !== "number") return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
var init_handler = __esm({
|
|
67
|
+
"src/core/handler.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// src/core/blueprints.ts
|
|
73
|
+
import { existsSync } from "fs";
|
|
74
|
+
import { readdir } from "fs/promises";
|
|
75
|
+
import { join } from "path";
|
|
76
|
+
import { createJiti } from "jiti";
|
|
77
|
+
async function loadBlueprints(project) {
|
|
78
|
+
const blueprintsDir = join(project.root, ".macroscope", "blueprints");
|
|
79
|
+
if (!existsSync(blueprintsDir)) {
|
|
80
|
+
return { blueprints: [], errors: [] };
|
|
81
|
+
}
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await readdir(blueprintsDir, { withFileTypes: true, encoding: "utf8" });
|
|
85
|
+
} catch {
|
|
86
|
+
return { blueprints: [], errors: [] };
|
|
87
|
+
}
|
|
88
|
+
const blueprints = [];
|
|
89
|
+
const errors = [];
|
|
90
|
+
const jiti = createJiti(import.meta.url);
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isDirectory()) continue;
|
|
93
|
+
const folder = join(blueprintsDir, entry.name);
|
|
94
|
+
const kind = entry.name.normalize("NFC");
|
|
95
|
+
const result = await loadOne(folder, kind, jiti);
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
blueprints.push(result.blueprint);
|
|
98
|
+
} else {
|
|
99
|
+
errors.push(result.error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
blueprints.sort((a, b) => a.kind < b.kind ? -1 : 1);
|
|
103
|
+
errors.sort((a, b) => a.kind < b.kind ? -1 : 1);
|
|
104
|
+
return { blueprints, errors };
|
|
105
|
+
}
|
|
106
|
+
async function loadOne(folder, kind, jiti) {
|
|
107
|
+
let file = null;
|
|
108
|
+
for (const filename of HANDLER_FILENAMES) {
|
|
109
|
+
const candidate = join(folder, filename);
|
|
110
|
+
if (existsSync(candidate)) {
|
|
111
|
+
file = candidate;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!file) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: {
|
|
119
|
+
folder,
|
|
120
|
+
kind,
|
|
121
|
+
cause: "no-handler-file",
|
|
122
|
+
message: `Blueprint folder ${folder} has no handler file. Expected one of: ${HANDLER_FILENAMES.join(", ")}.`
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let mod;
|
|
127
|
+
try {
|
|
128
|
+
mod = await jiti.import(file);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: {
|
|
133
|
+
folder,
|
|
134
|
+
kind,
|
|
135
|
+
cause: "import-failed",
|
|
136
|
+
message: `Failed to import ${file}: ${err.message}`
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const def = extractDefault(mod);
|
|
141
|
+
if (def === void 0) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: {
|
|
145
|
+
folder,
|
|
146
|
+
kind,
|
|
147
|
+
cause: "no-default-export",
|
|
148
|
+
message: `Blueprint at ${file} has no default export. Use \`export default { kind: '${kind}', ... } satisfies Handler\`.`
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!isHandler(def)) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: {
|
|
156
|
+
folder,
|
|
157
|
+
kind,
|
|
158
|
+
cause: "invalid-handler-shape",
|
|
159
|
+
message: `Default export of ${file} is not a valid Handler. It must be an object with a non-empty string \`kind\` field and optional async methods (render, test, build, scaffold, describe).`
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (def.kind !== kind) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: {
|
|
167
|
+
folder,
|
|
168
|
+
kind,
|
|
169
|
+
cause: "kind-mismatch",
|
|
170
|
+
message: `Blueprint at ${file} declares \`kind: '${def.kind}'\` but lives in folder \`${kind}/\`. The kind field must match the folder name.`
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const handlerRecord = def;
|
|
175
|
+
const capabilities = OPERATION_NAMES.filter((op) => typeof handlerRecord[op] === "function");
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
blueprint: { kind: def.kind, folder, file, handler: def, capabilities }
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function extractDefault(mod) {
|
|
182
|
+
if (mod === null || mod === void 0) return void 0;
|
|
183
|
+
if (typeof mod !== "object") return mod;
|
|
184
|
+
if (!Object.prototype.hasOwnProperty.call(mod, "default")) return void 0;
|
|
185
|
+
return mod.default;
|
|
186
|
+
}
|
|
187
|
+
var HANDLER_FILENAMES, OPERATION_NAMES;
|
|
188
|
+
var init_blueprints = __esm({
|
|
189
|
+
"src/core/blueprints.ts"() {
|
|
190
|
+
"use strict";
|
|
191
|
+
init_handler();
|
|
192
|
+
HANDLER_FILENAMES = ["handler.ts", "handler.mjs", "handler.js"];
|
|
193
|
+
OPERATION_NAMES = ["render", "test", "build", "scaffold", "describe"];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// src/core/project.ts
|
|
198
|
+
import { existsSync as existsSync2 } from "fs";
|
|
199
|
+
import { readFile } from "fs/promises";
|
|
200
|
+
import { dirname, join as join2, resolve } from "path";
|
|
201
|
+
import { parse as parseYaml } from "yaml";
|
|
202
|
+
import { z } from "zod";
|
|
203
|
+
async function findProject(cwd = process.cwd()) {
|
|
204
|
+
const startedFrom = resolve(cwd);
|
|
205
|
+
let current = startedFrom;
|
|
206
|
+
while (true) {
|
|
207
|
+
if (existsSync2(join2(current, ".macroscope"))) {
|
|
208
|
+
return { root: current, config: await loadConfig(current) };
|
|
209
|
+
}
|
|
210
|
+
const parent = dirname(current);
|
|
211
|
+
if (parent === current) {
|
|
212
|
+
throw new ProjectNotFoundError(startedFrom);
|
|
213
|
+
}
|
|
214
|
+
current = parent;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function loadConfig(root) {
|
|
218
|
+
const configPath = join2(root, ".macroscope", "config.yaml");
|
|
219
|
+
if (!existsSync2(configPath)) {
|
|
220
|
+
return projectConfigSchema.parse({});
|
|
221
|
+
}
|
|
222
|
+
const raw = await readFile(configPath, "utf8");
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = parseYaml(raw) ?? {};
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
228
|
+
throw new Error(`Failed to parse ${configPath}: ${msg}`);
|
|
229
|
+
}
|
|
230
|
+
const result = projectConfigSchema.safeParse(parsed);
|
|
231
|
+
if (!result.success) {
|
|
232
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
233
|
+
throw new Error(`Invalid project config at ${configPath}:
|
|
234
|
+
${issues}`);
|
|
235
|
+
}
|
|
236
|
+
return result.data;
|
|
237
|
+
}
|
|
238
|
+
var projectConfigSchema, ProjectNotFoundError;
|
|
239
|
+
var init_project = __esm({
|
|
240
|
+
"src/core/project.ts"() {
|
|
241
|
+
"use strict";
|
|
242
|
+
projectConfigSchema = z.object({
|
|
243
|
+
scanRoots: z.array(z.string()).default(["."]),
|
|
244
|
+
ignore: z.array(z.string()).default(["node_modules", ".git", "dist", ".macroscope"])
|
|
245
|
+
});
|
|
246
|
+
ProjectNotFoundError = class extends Error {
|
|
247
|
+
constructor(startedFrom) {
|
|
248
|
+
super(
|
|
249
|
+
`No macroscope project found. Walked up from ${startedFrom} looking for a .macroscope/ directory but reached the filesystem root. Run \`npm create macroscope\` to scaffold one, or cd into an existing project.`
|
|
250
|
+
);
|
|
251
|
+
this.name = "ProjectNotFoundError";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ../contracts/dist/index.js
|
|
258
|
+
import { z as z2 } from "zod";
|
|
259
|
+
import { z as z22 } from "zod";
|
|
260
|
+
import { z as z3 } from "zod";
|
|
261
|
+
import { z as z4 } from "zod";
|
|
262
|
+
import { z as z5 } from "zod";
|
|
263
|
+
var BlockManifestSchema, SymbolSignatureSchema, UploadPayloadSchema, MeilisearchDocSchema, BlockHitSchema, CodeMatchSchema, IndexBatchRequestSchema, IndexBatchResponseSchema, SearchRequestSchema, SearchResponseSchema, DeleteIndexResponseSchema, ScopeTouchRequestSchema, ScopeTouchResponseSchema, HealthResponseSchema, ErrorEnvelopeSchema;
|
|
264
|
+
var init_dist = __esm({
|
|
265
|
+
"../contracts/dist/index.js"() {
|
|
266
|
+
"use strict";
|
|
267
|
+
BlockManifestSchema = z2.object({
|
|
268
|
+
/** Must reference a registered blueprint's `kind`. */
|
|
269
|
+
kind: z2.string().min(1),
|
|
270
|
+
/** Optional override for the auto-derived block id (POSIX relative path). */
|
|
271
|
+
id: z2.string().optional(),
|
|
272
|
+
name: z2.string().optional(),
|
|
273
|
+
description: z2.string().optional(),
|
|
274
|
+
tags: z2.array(z2.string()).optional(),
|
|
275
|
+
source: z2.string().optional(),
|
|
276
|
+
preview: z2.string().optional(),
|
|
277
|
+
docs: z2.string().optional(),
|
|
278
|
+
tests: z2.union([z2.string(), z2.array(z2.string())]).optional()
|
|
279
|
+
}).passthrough();
|
|
280
|
+
SymbolSignatureSchema = z22.object({
|
|
281
|
+
name: z22.string().min(1),
|
|
282
|
+
kind: z22.string().min(1),
|
|
283
|
+
signature: z22.string()
|
|
284
|
+
});
|
|
285
|
+
UploadPayloadSchema = z22.object({
|
|
286
|
+
/** Stable block id (POSIX relative path or manifest `id` override). */
|
|
287
|
+
blockId: z22.string().min(1),
|
|
288
|
+
kind: z22.string().min(1),
|
|
289
|
+
name: z22.string().optional(),
|
|
290
|
+
description: z22.string().optional(),
|
|
291
|
+
tags: z22.array(z22.string()).optional(),
|
|
292
|
+
/** Exported symbols extracted from the block's source files. */
|
|
293
|
+
symbols: z22.array(SymbolSignatureSchema),
|
|
294
|
+
/** First ~500 chars of the README, if any. */
|
|
295
|
+
readmeExcerpt: z22.string().optional()
|
|
296
|
+
});
|
|
297
|
+
MeilisearchDocSchema = UploadPayloadSchema.extend({
|
|
298
|
+
/** SHA-256 over the canonical JSON of the UploadPayload (see T6). */
|
|
299
|
+
contentHash: z22.string().min(1),
|
|
300
|
+
/** Git remote URL (normalised) or local repo name when no remote exists. */
|
|
301
|
+
repo: z22.string().min(1),
|
|
302
|
+
/** hash(machineUuid + absolutePath) — see ARCHITECTURE.md. */
|
|
303
|
+
worktreeId: z22.string().min(1),
|
|
304
|
+
/** POSIX path from the worktree root to the block folder. */
|
|
305
|
+
path: z22.string().min(1),
|
|
306
|
+
/** Unix epoch milliseconds. Refreshed on every push and by `/scope/touch`. */
|
|
307
|
+
lastActiveAt: z22.number().int().nonnegative()
|
|
308
|
+
});
|
|
309
|
+
BlockHitSchema = z3.object({
|
|
310
|
+
blockId: z3.string().min(1),
|
|
311
|
+
kind: z3.string().min(1),
|
|
312
|
+
name: z3.string().optional(),
|
|
313
|
+
description: z3.string().optional(),
|
|
314
|
+
repo: z3.string().min(1),
|
|
315
|
+
worktreeId: z3.string().min(1),
|
|
316
|
+
path: z3.string().min(1),
|
|
317
|
+
/** Relevance score from Meilisearch hybrid ranking. Larger = more relevant. */
|
|
318
|
+
score: z3.number()
|
|
319
|
+
});
|
|
320
|
+
CodeMatchSchema = z3.object({
|
|
321
|
+
/** POSIX path from the worktree root. */
|
|
322
|
+
file: z3.string().min(1),
|
|
323
|
+
/** 1-based line number. */
|
|
324
|
+
line: z3.number().int().positive(),
|
|
325
|
+
/** 1-based column number, when available. */
|
|
326
|
+
column: z3.number().int().positive().optional(),
|
|
327
|
+
/** A single line of source text containing the match. */
|
|
328
|
+
preview: z3.string()
|
|
329
|
+
});
|
|
330
|
+
IndexBatchRequestSchema = z4.object({
|
|
331
|
+
/** Per-block docs to upsert. The cloud routes these to the user's Meilisearch index. */
|
|
332
|
+
upserts: z4.array(MeilisearchDocSchema),
|
|
333
|
+
/** Block ids to delete. Empty array is valid (upsert-only batch). */
|
|
334
|
+
deletes: z4.array(z4.string().min(1))
|
|
335
|
+
});
|
|
336
|
+
IndexBatchResponseSchema = z4.object({
|
|
337
|
+
/** Unix epoch ms of server-side acknowledgement. */
|
|
338
|
+
acceptedAt: z4.number().int().nonnegative(),
|
|
339
|
+
/** Number of upserts accepted (deletes excluded). */
|
|
340
|
+
accepted: z4.number().int().nonnegative()
|
|
341
|
+
});
|
|
342
|
+
SearchRequestSchema = z4.object({
|
|
343
|
+
q: z4.string().min(1),
|
|
344
|
+
/** Restrict to one worktree. Omit to search across all of the user's worktrees. */
|
|
345
|
+
worktreeId: z4.string().optional(),
|
|
346
|
+
/** Restrict to one repo. */
|
|
347
|
+
repo: z4.string().optional(),
|
|
348
|
+
/** Restrict to one block kind. */
|
|
349
|
+
kind: z4.string().optional(),
|
|
350
|
+
/** Cap on returned hits. Server enforces an upper bound. */
|
|
351
|
+
limit: z4.number().int().positive().optional()
|
|
352
|
+
});
|
|
353
|
+
SearchResponseSchema = z4.object({
|
|
354
|
+
hits: z4.array(BlockHitSchema),
|
|
355
|
+
/** Approximate total match count from Meilisearch (estimated, not exact). */
|
|
356
|
+
totalEstimated: z4.number().int().nonnegative()
|
|
357
|
+
});
|
|
358
|
+
DeleteIndexResponseSchema = z4.object({
|
|
359
|
+
deletedAt: z4.number().int().nonnegative()
|
|
360
|
+
});
|
|
361
|
+
ScopeTouchRequestSchema = z4.object({
|
|
362
|
+
worktreeId: z4.string().min(1),
|
|
363
|
+
repo: z4.string().min(1)
|
|
364
|
+
});
|
|
365
|
+
ScopeTouchResponseSchema = z4.object({
|
|
366
|
+
touchedAt: z4.number().int().nonnegative()
|
|
367
|
+
});
|
|
368
|
+
HealthResponseSchema = z4.object({
|
|
369
|
+
status: z4.literal("ok"),
|
|
370
|
+
schemaVersion: z4.number().int().positive()
|
|
371
|
+
});
|
|
372
|
+
ErrorEnvelopeSchema = z5.object({
|
|
373
|
+
error: z5.object({
|
|
374
|
+
/** Stable machine-readable code (e.g. `unauthorized`, `validation_failed`). */
|
|
375
|
+
code: z5.string().min(1),
|
|
376
|
+
/** Human-readable message. */
|
|
377
|
+
message: z5.string().min(1),
|
|
378
|
+
/** Source file path when the error originates from parsing a manifest or other on-disk artifact. Set by the CLI side (see `ScanError`) and by the cloud when validating uploaded manifests. */
|
|
379
|
+
file: z5.string().optional(),
|
|
380
|
+
/** Path of the offending field when the error is validation-related. */
|
|
381
|
+
field: z5.string().optional(),
|
|
382
|
+
/** Server-assigned request id for log correlation. */
|
|
383
|
+
requestId: z5.string().optional()
|
|
384
|
+
})
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// src/core/manifest.ts
|
|
390
|
+
var manifestSchema;
|
|
391
|
+
var init_manifest = __esm({
|
|
392
|
+
"src/core/manifest.ts"() {
|
|
393
|
+
"use strict";
|
|
394
|
+
init_dist();
|
|
395
|
+
manifestSchema = BlockManifestSchema;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/core/scanner.ts
|
|
400
|
+
import { createHash } from "crypto";
|
|
401
|
+
import { existsSync as existsSync3 } from "fs";
|
|
402
|
+
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
403
|
+
import { dirname as dirname2, join as join3, relative, sep } from "path";
|
|
404
|
+
import { parse as parseYaml2 } from "yaml";
|
|
405
|
+
async function scan(project) {
|
|
406
|
+
const blocks = [];
|
|
407
|
+
const errors = [];
|
|
408
|
+
const ignore = new Set(project.config.ignore);
|
|
409
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
410
|
+
for (const root of project.config.scanRoots) {
|
|
411
|
+
const start = join3(project.root, root);
|
|
412
|
+
if (!existsSync3(start)) continue;
|
|
413
|
+
for await (const manifestPath of findManifests(start, ignore)) {
|
|
414
|
+
const result = await parseBlock(manifestPath, project.root);
|
|
415
|
+
if (result.kind === "ok") {
|
|
416
|
+
if (seenIds.has(result.block.id)) {
|
|
417
|
+
errors.push({
|
|
418
|
+
file: manifestPath,
|
|
419
|
+
kind: "validation",
|
|
420
|
+
message: `Duplicate block id "${result.block.id}" at ${manifestPath}. Set an explicit \`id:\` field to disambiguate.`,
|
|
421
|
+
field: "id"
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
seenIds.add(result.block.id);
|
|
425
|
+
blocks.push(result.block);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
errors.push(result.error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
blocks.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
433
|
+
return { blocks, errors };
|
|
434
|
+
}
|
|
435
|
+
async function* findManifests(dir, ignore) {
|
|
436
|
+
let entries;
|
|
437
|
+
try {
|
|
438
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
439
|
+
} catch {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
if (entry.isFile() && entry.name === "macroscope.yaml") {
|
|
444
|
+
yield join3(dir, entry.name);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (!entry.isDirectory()) continue;
|
|
449
|
+
if (ignore.has(entry.name)) continue;
|
|
450
|
+
yield* findManifests(join3(dir, entry.name), ignore);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function parseBlock(manifestPath, projectRoot) {
|
|
454
|
+
let raw;
|
|
455
|
+
try {
|
|
456
|
+
raw = await readFile2(manifestPath, "utf8");
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return {
|
|
459
|
+
kind: "err",
|
|
460
|
+
error: {
|
|
461
|
+
file: manifestPath,
|
|
462
|
+
kind: "io",
|
|
463
|
+
message: `Failed to read manifest at ${manifestPath}: ${err.message}`
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
let yamlValue;
|
|
468
|
+
try {
|
|
469
|
+
yamlValue = parseYaml2(raw);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
return {
|
|
472
|
+
kind: "err",
|
|
473
|
+
error: {
|
|
474
|
+
file: manifestPath,
|
|
475
|
+
kind: "parse",
|
|
476
|
+
message: `Invalid YAML in ${manifestPath}: ${err.message}`
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const parsed = manifestSchema.safeParse(yamlValue);
|
|
481
|
+
if (!parsed.success) {
|
|
482
|
+
const first = firstZodIssue(parsed.error);
|
|
483
|
+
return {
|
|
484
|
+
kind: "err",
|
|
485
|
+
error: {
|
|
486
|
+
file: manifestPath,
|
|
487
|
+
kind: "validation",
|
|
488
|
+
message: `Manifest at ${manifestPath} is invalid: field \`${first.fieldPath}\` ${first.message}.`,
|
|
489
|
+
field: first.fieldPath
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const manifest = parsed.data;
|
|
494
|
+
const blockDir = dirname2(manifestPath);
|
|
495
|
+
const defaultId = toPosix(relative(projectRoot, blockDir));
|
|
496
|
+
const id = manifest.id ?? defaultId;
|
|
497
|
+
const contentHash = createHash("sha256").update(raw).digest("hex");
|
|
498
|
+
return {
|
|
499
|
+
kind: "ok",
|
|
500
|
+
block: { id, path: blockDir, manifest, contentHash }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function firstZodIssue(err) {
|
|
504
|
+
const issue = err.issues[0];
|
|
505
|
+
if (!issue) return { fieldPath: "(root)", message: "unknown validation error" };
|
|
506
|
+
const fieldPath = issue.path.length ? issue.path.join(".") : "(root)";
|
|
507
|
+
return { fieldPath, message: issue.message.toLowerCase() };
|
|
508
|
+
}
|
|
509
|
+
function toPosix(p) {
|
|
510
|
+
return p.split(sep).join("/");
|
|
511
|
+
}
|
|
512
|
+
var init_scanner = __esm({
|
|
513
|
+
"src/core/scanner.ts"() {
|
|
514
|
+
"use strict";
|
|
515
|
+
init_manifest();
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// src/ui/api/terminal-resolver.ts
|
|
520
|
+
async function resolveTerminalCommand(cwd, blockId, viewKey) {
|
|
521
|
+
let project;
|
|
522
|
+
try {
|
|
523
|
+
project = await findProject(cwd);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err instanceof ProjectNotFoundError) {
|
|
526
|
+
return { ok: false, error: err.message };
|
|
527
|
+
}
|
|
528
|
+
return { ok: false, error: err.message };
|
|
529
|
+
}
|
|
530
|
+
const [scanResult, blueprintResult] = await Promise.all([scan(project), loadBlueprints(project)]);
|
|
531
|
+
const block = scanResult.blocks.find((b) => b.id === blockId);
|
|
532
|
+
if (!block) {
|
|
533
|
+
return { ok: false, error: `No block found with id "${blockId}".` };
|
|
534
|
+
}
|
|
535
|
+
const blueprint = blueprintResult.blueprints.find((bp) => bp.kind === block.manifest.kind);
|
|
536
|
+
if (!blueprint) {
|
|
537
|
+
return { ok: false, error: `No blueprint registered for kind "${block.manifest.kind}".` };
|
|
538
|
+
}
|
|
539
|
+
const spec = blueprint.handler.views?.[viewKey];
|
|
540
|
+
if (!spec) {
|
|
541
|
+
return { ok: false, error: `Blueprint "${blueprint.kind}" has no view named "${viewKey}".` };
|
|
542
|
+
}
|
|
543
|
+
if (spec.type !== "terminal" && spec.type !== "hybrid") {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: `View "${viewKey}" on "${blueprint.kind}" has no terminal command.`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const blockInput = { id: block.id, path: block.path, manifest: block.manifest };
|
|
550
|
+
let command;
|
|
551
|
+
if (Array.isArray(spec.command)) {
|
|
552
|
+
command = spec.command;
|
|
553
|
+
} else {
|
|
554
|
+
try {
|
|
555
|
+
command = await withTimeout2(spec.command(blockInput), COMMAND_RESOLVE_TIMEOUT_MS);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
return { ok: false, error: `Command resolver failed: ${err.message}` };
|
|
558
|
+
}
|
|
559
|
+
if (!Array.isArray(command) || !command.every((c) => typeof c === "string")) {
|
|
560
|
+
return { ok: false, error: "Command resolver did not return a string[]." };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return { ok: true, command, cwd: project.root };
|
|
564
|
+
}
|
|
565
|
+
function withTimeout2(p, ms) {
|
|
566
|
+
return Promise.race([
|
|
567
|
+
p,
|
|
568
|
+
new Promise(
|
|
569
|
+
(_, reject) => setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms)
|
|
570
|
+
)
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
var COMMAND_RESOLVE_TIMEOUT_MS;
|
|
574
|
+
var init_terminal_resolver = __esm({
|
|
575
|
+
"src/ui/api/terminal-resolver.ts"() {
|
|
576
|
+
"use strict";
|
|
577
|
+
init_blueprints();
|
|
578
|
+
init_project();
|
|
579
|
+
init_scanner();
|
|
580
|
+
COMMAND_RESOLVE_TIMEOUT_MS = 5e3;
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// src/ui/api/terminal-ws.ts
|
|
585
|
+
var terminal_ws_exports = {};
|
|
586
|
+
__export(terminal_ws_exports, {
|
|
587
|
+
attachTerminalWs: () => attachTerminalWs,
|
|
588
|
+
setSpawnOverride: () => setSpawnOverride
|
|
589
|
+
});
|
|
590
|
+
import { spawn } from "child_process";
|
|
591
|
+
import { WebSocketServer } from "ws";
|
|
592
|
+
function setSpawnOverride(override) {
|
|
593
|
+
spawnOverride = override;
|
|
594
|
+
}
|
|
595
|
+
function attachTerminalWs(httpServer, defaultCwd) {
|
|
596
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
597
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
598
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
599
|
+
if (url.pathname !== "/api/terminal") {
|
|
600
|
+
socket.destroy();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
604
|
+
handleConnection(ws, req, defaultCwd);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async function handleConnection(ws, req, defaultCwd) {
|
|
609
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
610
|
+
const blockId = url.searchParams.get("blockId") ?? "";
|
|
611
|
+
const viewKey = url.searchParams.get("viewKey") ?? "";
|
|
612
|
+
let command;
|
|
613
|
+
let cwd;
|
|
614
|
+
if (spawnOverride) {
|
|
615
|
+
command = spawnOverride.command;
|
|
616
|
+
cwd = spawnOverride.cwd ?? defaultCwd;
|
|
617
|
+
} else {
|
|
618
|
+
const resolved = await resolveTerminalCommand(defaultCwd, blockId, viewKey);
|
|
619
|
+
if (!resolved.ok) {
|
|
620
|
+
send(ws, { type: "error", message: resolved.error });
|
|
621
|
+
ws.close();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
command = resolved.command;
|
|
625
|
+
cwd = resolved.cwd;
|
|
626
|
+
}
|
|
627
|
+
if (command.length === 0) {
|
|
628
|
+
send(ws, { type: "error", message: "empty command" });
|
|
629
|
+
ws.close();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
send(ws, { type: "open", command });
|
|
633
|
+
const [bin, ...args] = command;
|
|
634
|
+
let child;
|
|
635
|
+
try {
|
|
636
|
+
child = spawn(bin, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
637
|
+
} catch (err) {
|
|
638
|
+
send(ws, { type: "error", message: `failed to spawn: ${err.message}` });
|
|
639
|
+
ws.close();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
child.stdout?.on("data", (chunk) => send(ws, { type: "out", data: chunk.toString() }));
|
|
643
|
+
child.stderr?.on("data", (chunk) => send(ws, { type: "err", data: chunk.toString() }));
|
|
644
|
+
child.on("exit", (code, signal) => {
|
|
645
|
+
send(ws, { type: "close", code, signal });
|
|
646
|
+
ws.close();
|
|
647
|
+
});
|
|
648
|
+
child.on("error", (err) => {
|
|
649
|
+
send(ws, { type: "error", message: err.message });
|
|
650
|
+
ws.close();
|
|
651
|
+
});
|
|
652
|
+
ws.on("message", (raw) => {
|
|
653
|
+
let frame;
|
|
654
|
+
try {
|
|
655
|
+
frame = JSON.parse(raw.toString());
|
|
656
|
+
} catch {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (frame.type === "in" && typeof frame.data === "string") {
|
|
660
|
+
child.stdin?.write(frame.data);
|
|
661
|
+
} else if (frame.type === "eof") {
|
|
662
|
+
child.stdin?.end();
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
ws.on("close", () => {
|
|
666
|
+
if (!child.killed) child.kill();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function send(ws, frame) {
|
|
670
|
+
try {
|
|
671
|
+
ws.send(JSON.stringify(frame));
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
var spawnOverride;
|
|
676
|
+
var init_terminal_ws = __esm({
|
|
677
|
+
"src/ui/api/terminal-ws.ts"() {
|
|
678
|
+
"use strict";
|
|
679
|
+
init_terminal_resolver();
|
|
680
|
+
spawnOverride = null;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// src/cli.ts
|
|
685
|
+
init_blueprints();
|
|
686
|
+
init_project();
|
|
687
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
688
|
+
import { fileURLToPath } from "url";
|
|
689
|
+
import { Command } from "commander";
|
|
690
|
+
import importLocal from "import-local";
|
|
691
|
+
|
|
692
|
+
// src/core/version.ts
|
|
693
|
+
var VERSION = "0.0.0";
|
|
694
|
+
|
|
695
|
+
// src/ui/ui-server.ts
|
|
696
|
+
import { existsSync as existsSync4 } from "fs";
|
|
697
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
698
|
+
import { createServer } from "http";
|
|
699
|
+
import { extname as extname2, join as join4, normalize, resolve as resolve2 } from "path";
|
|
700
|
+
|
|
701
|
+
// src/ui/api/blocks-handler.ts
|
|
702
|
+
init_blueprints();
|
|
703
|
+
init_project();
|
|
704
|
+
init_scanner();
|
|
705
|
+
var URL_RESOLVE_TIMEOUT_MS = 5e3;
|
|
706
|
+
async function handleBlocksRequest(cwd) {
|
|
707
|
+
try {
|
|
708
|
+
const project = await findProject(cwd);
|
|
709
|
+
const [scanResult, blueprintResult] = await Promise.all([
|
|
710
|
+
scan(project),
|
|
711
|
+
loadBlueprints(project)
|
|
712
|
+
]);
|
|
713
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
714
|
+
for (const bp of blueprintResult.blueprints) byKind.set(bp.kind, bp);
|
|
715
|
+
const blocks = await Promise.all(
|
|
716
|
+
scanResult.blocks.map(async (block) => ({
|
|
717
|
+
id: block.id,
|
|
718
|
+
path: block.path,
|
|
719
|
+
manifest: block.manifest,
|
|
720
|
+
contentHash: block.contentHash,
|
|
721
|
+
views: await resolveViewsWithTimeout(
|
|
722
|
+
block,
|
|
723
|
+
byKind.get(block.manifest.kind)?.handler,
|
|
724
|
+
URL_RESOLVE_TIMEOUT_MS
|
|
725
|
+
)
|
|
726
|
+
}))
|
|
727
|
+
);
|
|
728
|
+
return {
|
|
729
|
+
status: 200,
|
|
730
|
+
body: {
|
|
731
|
+
blocks,
|
|
732
|
+
errors: scanResult.errors,
|
|
733
|
+
blueprintErrors: blueprintResult.errors
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
} catch (err) {
|
|
737
|
+
if (err instanceof ProjectNotFoundError) {
|
|
738
|
+
return { status: 404, body: { error: err.message } };
|
|
739
|
+
}
|
|
740
|
+
return { status: 500, body: { error: err.message } };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function resolveViewsWithTimeout(block, handler, timeoutMs = URL_RESOLVE_TIMEOUT_MS) {
|
|
744
|
+
if (!handler?.views) return [];
|
|
745
|
+
const blockInput = { id: block.id, path: block.path, manifest: block.manifest };
|
|
746
|
+
const resolved = [];
|
|
747
|
+
for (const [key, spec] of Object.entries(handler.views)) {
|
|
748
|
+
if (spec.type === "iframe") {
|
|
749
|
+
const service = spec.service ? { name: spec.service.name ?? firstArg(spec.service.command) } : null;
|
|
750
|
+
if (typeof spec.url === "string") {
|
|
751
|
+
resolved.push({ type: "iframe", key, label: titleize(key), url: spec.url, service });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const url = await withTimeout(spec.url(blockInput), timeoutMs);
|
|
756
|
+
if (typeof url !== "string") continue;
|
|
757
|
+
resolved.push({ type: "iframe", key, label: titleize(key), url, service });
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
} else if (spec.type === "terminal") {
|
|
761
|
+
resolved.push({ type: "terminal", key, label: titleize(key) });
|
|
762
|
+
} else if (spec.type === "hybrid") {
|
|
763
|
+
const runLabel = spec.runLabel ?? "Run";
|
|
764
|
+
const codeLabel = spec.codeLabel ?? "Code";
|
|
765
|
+
const terminalLabel = spec.terminalLabel ?? "Terminal";
|
|
766
|
+
if (typeof spec.url === "string") {
|
|
767
|
+
resolved.push({
|
|
768
|
+
type: "hybrid",
|
|
769
|
+
key,
|
|
770
|
+
label: titleize(key),
|
|
771
|
+
url: spec.url,
|
|
772
|
+
runLabel,
|
|
773
|
+
codeLabel,
|
|
774
|
+
terminalLabel
|
|
775
|
+
});
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const url = await withTimeout(spec.url(blockInput), timeoutMs);
|
|
780
|
+
if (typeof url !== "string") continue;
|
|
781
|
+
resolved.push({
|
|
782
|
+
type: "hybrid",
|
|
783
|
+
key,
|
|
784
|
+
label: titleize(key),
|
|
785
|
+
url,
|
|
786
|
+
runLabel,
|
|
787
|
+
codeLabel,
|
|
788
|
+
terminalLabel
|
|
789
|
+
});
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return resolved;
|
|
795
|
+
}
|
|
796
|
+
function withTimeout(promise, ms) {
|
|
797
|
+
return Promise.race([
|
|
798
|
+
promise,
|
|
799
|
+
new Promise(
|
|
800
|
+
(_, reject) => setTimeout(() => reject(new Error(`url resolver timed out after ${ms}ms`)), ms)
|
|
801
|
+
)
|
|
802
|
+
]);
|
|
803
|
+
}
|
|
804
|
+
function titleize(key) {
|
|
805
|
+
return key.charAt(0).toUpperCase() + key.slice(1).replace(/-/g, " ");
|
|
806
|
+
}
|
|
807
|
+
function firstArg(command) {
|
|
808
|
+
if (Array.isArray(command)) return command[0] ?? "service";
|
|
809
|
+
return "service";
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/ui/api/blueprints-handler.ts
|
|
813
|
+
init_blueprints();
|
|
814
|
+
init_project();
|
|
815
|
+
async function handleBlueprintsRequest(cwd) {
|
|
816
|
+
try {
|
|
817
|
+
const project = await findProject(cwd);
|
|
818
|
+
const result = await loadBlueprints(project);
|
|
819
|
+
return {
|
|
820
|
+
status: 200,
|
|
821
|
+
body: {
|
|
822
|
+
blueprints: result.blueprints.map((b) => ({
|
|
823
|
+
kind: b.kind,
|
|
824
|
+
folder: b.folder,
|
|
825
|
+
file: b.file,
|
|
826
|
+
capabilities: b.capabilities
|
|
827
|
+
})),
|
|
828
|
+
errors: result.errors
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (err instanceof ProjectNotFoundError) {
|
|
833
|
+
return { status: 404, body: { error: err.message } };
|
|
834
|
+
}
|
|
835
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
836
|
+
return { status: 500, body: { error: message } };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/ui/api/code-handler.ts
|
|
841
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
842
|
+
import { extname } from "path";
|
|
843
|
+
import { codeToHtml } from "shiki";
|
|
844
|
+
var EXT_TO_LANG = {
|
|
845
|
+
".ts": "typescript",
|
|
846
|
+
".tsx": "tsx",
|
|
847
|
+
".js": "javascript",
|
|
848
|
+
".jsx": "jsx",
|
|
849
|
+
".mjs": "javascript",
|
|
850
|
+
".cjs": "javascript",
|
|
851
|
+
".json": "json",
|
|
852
|
+
".css": "css",
|
|
853
|
+
".html": "html",
|
|
854
|
+
".md": "markdown",
|
|
855
|
+
".yaml": "yaml",
|
|
856
|
+
".yml": "yaml",
|
|
857
|
+
".sh": "shell",
|
|
858
|
+
".py": "python",
|
|
859
|
+
".rs": "rust",
|
|
860
|
+
".go": "go"
|
|
861
|
+
};
|
|
862
|
+
async function handleCodeRequest(absolutePath, language) {
|
|
863
|
+
let raw;
|
|
864
|
+
try {
|
|
865
|
+
raw = await readFile3(absolutePath, "utf8");
|
|
866
|
+
} catch {
|
|
867
|
+
return errorResponse(404, `File not found: ${absolutePath}`);
|
|
868
|
+
}
|
|
869
|
+
const lang = language ?? EXT_TO_LANG[extname(absolutePath).toLowerCase()] ?? "text";
|
|
870
|
+
try {
|
|
871
|
+
const inner = await codeToHtml(raw, { lang, theme: "github-dark" });
|
|
872
|
+
return {
|
|
873
|
+
status: 200,
|
|
874
|
+
contentType: "text/html; charset=utf-8",
|
|
875
|
+
body: wrap(inner)
|
|
876
|
+
};
|
|
877
|
+
} catch {
|
|
878
|
+
const fallback = `<pre>${escapeHtml(raw)}</pre>`;
|
|
879
|
+
return {
|
|
880
|
+
status: 200,
|
|
881
|
+
contentType: "text/html; charset=utf-8",
|
|
882
|
+
body: wrap(fallback)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function wrap(inner) {
|
|
887
|
+
return `<!doctype html>
|
|
888
|
+
<html>
|
|
889
|
+
<head>
|
|
890
|
+
<meta charset="utf-8" />
|
|
891
|
+
<title>macroscope \xB7 code</title>
|
|
892
|
+
<style>
|
|
893
|
+
body { margin: 0; padding: 0; font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
894
|
+
pre { margin: 0; padding: 1rem; overflow-x: auto; line-height: 1.5; font-size: 13px; }
|
|
895
|
+
</style>
|
|
896
|
+
</head>
|
|
897
|
+
<body>${inner}</body>
|
|
898
|
+
</html>`;
|
|
899
|
+
}
|
|
900
|
+
function errorResponse(status, message) {
|
|
901
|
+
return {
|
|
902
|
+
status,
|
|
903
|
+
contentType: "text/html; charset=utf-8",
|
|
904
|
+
body: `<!doctype html><body><pre>${escapeHtml(message)}</pre></body>`
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function escapeHtml(s) {
|
|
908
|
+
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c] ?? c);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/ui/api/markdown-handler.ts
|
|
912
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
913
|
+
import { marked } from "marked";
|
|
914
|
+
async function handleMarkdownRequest(absolutePath) {
|
|
915
|
+
let raw;
|
|
916
|
+
try {
|
|
917
|
+
raw = await readFile4(absolutePath, "utf8");
|
|
918
|
+
} catch {
|
|
919
|
+
return errorResponse2(404, `File not found: ${absolutePath}`);
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
const inner = await marked.parse(raw);
|
|
923
|
+
return {
|
|
924
|
+
status: 200,
|
|
925
|
+
contentType: "text/html; charset=utf-8",
|
|
926
|
+
body: wrap2(inner)
|
|
927
|
+
};
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return errorResponse2(500, `Failed to render markdown: ${err.message}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function wrap2(inner) {
|
|
933
|
+
return `<!doctype html>
|
|
934
|
+
<html>
|
|
935
|
+
<head>
|
|
936
|
+
<meta charset="utf-8" />
|
|
937
|
+
<title>macroscope \xB7 markdown</title>
|
|
938
|
+
<style>
|
|
939
|
+
body { font-family: ui-sans-serif, system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #18181b; }
|
|
940
|
+
h1, h2, h3 { margin-top: 1.6em; }
|
|
941
|
+
code { background: #f4f4f5; padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.9em; }
|
|
942
|
+
pre { background: #18181b; color: #fafafa; padding: 1em; border-radius: 6px; overflow-x: auto; }
|
|
943
|
+
pre code { background: transparent; padding: 0; color: inherit; }
|
|
944
|
+
a { color: #2563eb; }
|
|
945
|
+
</style>
|
|
946
|
+
</head>
|
|
947
|
+
<body>${inner}</body>
|
|
948
|
+
</html>`;
|
|
949
|
+
}
|
|
950
|
+
function errorResponse2(status, message) {
|
|
951
|
+
return {
|
|
952
|
+
status,
|
|
953
|
+
contentType: "text/html; charset=utf-8",
|
|
954
|
+
body: `<!doctype html><body><pre>${escapeHtml2(message)}</pre></body>`
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function escapeHtml2(s) {
|
|
958
|
+
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c] ?? c);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/ui/api/service-manager.ts
|
|
962
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
963
|
+
|
|
964
|
+
// src/ui/api/service-readiness.ts
|
|
965
|
+
async function pollReadiness(url, opts = {}) {
|
|
966
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
967
|
+
const intervalMs = opts.intervalMs ?? 250;
|
|
968
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
969
|
+
const deadline = Date.now() + timeoutMs;
|
|
970
|
+
let lastError = "";
|
|
971
|
+
while (Date.now() < deadline) {
|
|
972
|
+
if (opts.signal?.aborted) return { ready: false, aborted: true };
|
|
973
|
+
try {
|
|
974
|
+
const fetchPromise = fetchImpl(url, { signal: opts.signal });
|
|
975
|
+
let onAbort = null;
|
|
976
|
+
const signal = opts.signal;
|
|
977
|
+
const abortPromise = signal ? new Promise((_, reject) => {
|
|
978
|
+
onAbort = () => {
|
|
979
|
+
reject(new AbortError());
|
|
980
|
+
};
|
|
981
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
982
|
+
}) : new Promise(() => {
|
|
983
|
+
});
|
|
984
|
+
try {
|
|
985
|
+
const res = await Promise.race([fetchPromise, abortPromise]);
|
|
986
|
+
if (res.status < 500) return { ready: true };
|
|
987
|
+
lastError = `status ${res.status}`;
|
|
988
|
+
} finally {
|
|
989
|
+
if (onAbort && opts.signal) opts.signal.removeEventListener("abort", onAbort);
|
|
990
|
+
}
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (opts.signal?.aborted || err instanceof AbortError) return { ready: false, aborted: true };
|
|
993
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
994
|
+
}
|
|
995
|
+
const result = await sleep(intervalMs, opts.signal);
|
|
996
|
+
if (result.aborted) return { ready: false, aborted: true };
|
|
997
|
+
}
|
|
998
|
+
return { ready: false, lastError };
|
|
999
|
+
}
|
|
1000
|
+
var AbortError = class extends Error {
|
|
1001
|
+
constructor() {
|
|
1002
|
+
super("aborted");
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
function sleep(ms, signal) {
|
|
1006
|
+
return new Promise((resolve4) => {
|
|
1007
|
+
let onAbort = null;
|
|
1008
|
+
const t = setTimeout(() => {
|
|
1009
|
+
if (onAbort && signal) signal.removeEventListener("abort", onAbort);
|
|
1010
|
+
resolve4({ aborted: false });
|
|
1011
|
+
}, ms);
|
|
1012
|
+
if (signal?.aborted) {
|
|
1013
|
+
clearTimeout(t);
|
|
1014
|
+
resolve4({ aborted: true });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
onAbort = () => {
|
|
1018
|
+
clearTimeout(t);
|
|
1019
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
1020
|
+
resolve4({ aborted: true });
|
|
1021
|
+
};
|
|
1022
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/ui/api/service-manager.ts
|
|
1027
|
+
var LOG_BUFFER_BYTES = 4096;
|
|
1028
|
+
var ServiceManager = class {
|
|
1029
|
+
spawn;
|
|
1030
|
+
poll;
|
|
1031
|
+
services = /* @__PURE__ */ new Map();
|
|
1032
|
+
constructor(deps = {}) {
|
|
1033
|
+
this.spawn = deps.spawn ?? nodeSpawn;
|
|
1034
|
+
this.poll = deps.pollReadiness ?? pollReadiness;
|
|
1035
|
+
}
|
|
1036
|
+
keyFor(input) {
|
|
1037
|
+
return JSON.stringify({ command: input.command, cwd: input.cwd });
|
|
1038
|
+
}
|
|
1039
|
+
get(key) {
|
|
1040
|
+
return this.services.get(key)?.state;
|
|
1041
|
+
}
|
|
1042
|
+
async ensure(input) {
|
|
1043
|
+
const key = this.keyFor(input);
|
|
1044
|
+
const existing = this.services.get(key);
|
|
1045
|
+
if (existing) return existing.state;
|
|
1046
|
+
if (input.probeFirst) {
|
|
1047
|
+
const probe = await this.poll(input.readyUrl, { timeoutMs: 500, intervalMs: 100 });
|
|
1048
|
+
if (probe.ready) {
|
|
1049
|
+
const state2 = { status: "ready", logs: "", adopted: true };
|
|
1050
|
+
this.services.set(key, { state: state2, abort: new AbortController() });
|
|
1051
|
+
return state2;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const [bin, ...args] = input.command;
|
|
1055
|
+
if (!bin) return { status: "failed", logs: "", message: "empty command" };
|
|
1056
|
+
const abort = new AbortController();
|
|
1057
|
+
const state = { status: "starting", logs: "" };
|
|
1058
|
+
let child;
|
|
1059
|
+
try {
|
|
1060
|
+
child = this.spawn(bin, args, { cwd: input.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
return { status: "failed", logs: "", message: `spawn failed: ${err.message}` };
|
|
1063
|
+
}
|
|
1064
|
+
this.services.set(key, { state, child, abort });
|
|
1065
|
+
const appendLog = (chunk) => {
|
|
1066
|
+
state.logs = (state.logs + chunk.toString()).slice(-LOG_BUFFER_BYTES);
|
|
1067
|
+
};
|
|
1068
|
+
child.stdout?.on("data", appendLog);
|
|
1069
|
+
child.stderr?.on("data", appendLog);
|
|
1070
|
+
child.on("exit", (code) => {
|
|
1071
|
+
if (state.status === "starting" || state.status === "ready") {
|
|
1072
|
+
state.status = "failed";
|
|
1073
|
+
state.message = `process exited with code ${code}`;
|
|
1074
|
+
abort.abort();
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
child.on("error", (err) => {
|
|
1078
|
+
if (state.status === "starting") {
|
|
1079
|
+
state.status = "failed";
|
|
1080
|
+
state.message = err.message;
|
|
1081
|
+
abort.abort();
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
void this.poll(input.readyUrl, {
|
|
1085
|
+
timeoutMs: input.startupTimeoutMs ?? 3e4,
|
|
1086
|
+
signal: abort.signal
|
|
1087
|
+
}).then((result) => {
|
|
1088
|
+
if (state.status !== "starting") return;
|
|
1089
|
+
if (result.ready) {
|
|
1090
|
+
state.status = "ready";
|
|
1091
|
+
} else if (!result.aborted) {
|
|
1092
|
+
state.status = "failed";
|
|
1093
|
+
state.message = `readiness check failed: ${result.lastError ?? "timeout"}`;
|
|
1094
|
+
if (!child.killed) child.kill();
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
return state;
|
|
1098
|
+
}
|
|
1099
|
+
shutdown() {
|
|
1100
|
+
for (const entry of this.services.values()) {
|
|
1101
|
+
entry.abort.abort();
|
|
1102
|
+
if (entry.child && !entry.child.killed) entry.child.kill();
|
|
1103
|
+
}
|
|
1104
|
+
this.services.clear();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/ui/api/services-handler.ts
|
|
1109
|
+
init_blueprints();
|
|
1110
|
+
init_project();
|
|
1111
|
+
init_scanner();
|
|
1112
|
+
async function handleServiceEnsureRequest(cwd, manager, input) {
|
|
1113
|
+
if (!input.blockId || !input.viewKey) {
|
|
1114
|
+
return { status: 400, body: { error: "blockId and viewKey are required" } };
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
const project = await findProject(cwd);
|
|
1118
|
+
const [scanResult, blueprintResult] = await Promise.all([
|
|
1119
|
+
scan(project),
|
|
1120
|
+
loadBlueprints(project)
|
|
1121
|
+
]);
|
|
1122
|
+
const block = scanResult.blocks.find((b) => b.id === input.blockId);
|
|
1123
|
+
if (!block) return { status: 404, body: { error: `unknown block: ${input.blockId}` } };
|
|
1124
|
+
const bp = blueprintResult.blueprints.find((b) => b.kind === block.manifest.kind);
|
|
1125
|
+
const viewSpec = bp?.handler.views?.[input.viewKey];
|
|
1126
|
+
if (!viewSpec || viewSpec.type !== "iframe" || !viewSpec.service) {
|
|
1127
|
+
return { status: 404, body: { error: `view ${input.viewKey} has no service` } };
|
|
1128
|
+
}
|
|
1129
|
+
const svc = viewSpec.service;
|
|
1130
|
+
const blockInput = { id: block.id, path: block.path, manifest: block.manifest };
|
|
1131
|
+
const command = typeof svc.command === "function" ? await svc.command(blockInput) : svc.command;
|
|
1132
|
+
const cwdResolved = typeof svc.cwd === "function" ? await svc.cwd(blockInput) : svc.cwd ?? project.root;
|
|
1133
|
+
const state = await manager.ensure({
|
|
1134
|
+
command,
|
|
1135
|
+
cwd: cwdResolved,
|
|
1136
|
+
readyUrl: svc.readyWhen.url,
|
|
1137
|
+
probeFirst: true,
|
|
1138
|
+
startupTimeoutMs: svc.startupTimeoutMs
|
|
1139
|
+
});
|
|
1140
|
+
return {
|
|
1141
|
+
status: 200,
|
|
1142
|
+
body: {
|
|
1143
|
+
status: state.status,
|
|
1144
|
+
message: state.message,
|
|
1145
|
+
logs: state.status === "failed" ? state.logs : void 0
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
if (err instanceof ProjectNotFoundError) return { status: 404, body: { error: err.message } };
|
|
1150
|
+
return { status: 500, body: { error: err.message } };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/ui/ui-server.ts
|
|
1155
|
+
var MIME_TYPES = {
|
|
1156
|
+
".html": "text/html; charset=utf-8",
|
|
1157
|
+
".htm": "text/html; charset=utf-8",
|
|
1158
|
+
".js": "application/javascript; charset=utf-8",
|
|
1159
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1160
|
+
".css": "text/css; charset=utf-8",
|
|
1161
|
+
".json": "application/json; charset=utf-8",
|
|
1162
|
+
".svg": "image/svg+xml",
|
|
1163
|
+
".png": "image/png",
|
|
1164
|
+
".jpg": "image/jpeg",
|
|
1165
|
+
".jpeg": "image/jpeg",
|
|
1166
|
+
".gif": "image/gif",
|
|
1167
|
+
".ico": "image/x-icon",
|
|
1168
|
+
".woff": "font/woff",
|
|
1169
|
+
".woff2": "font/woff2",
|
|
1170
|
+
".map": "application/json; charset=utf-8"
|
|
1171
|
+
};
|
|
1172
|
+
async function startUiServer(opts) {
|
|
1173
|
+
const staticDir = resolve2(opts.staticDir);
|
|
1174
|
+
if (!existsSync4(staticDir)) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Macroscope UI build is missing. Expected files at ${staticDir}. Run \`pnpm build\` first.`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const indexFile = join4(staticDir, "index.html");
|
|
1180
|
+
if (!existsSync4(indexFile)) {
|
|
1181
|
+
throw new Error(`Macroscope UI build is incomplete: ${indexFile} not found.`);
|
|
1182
|
+
}
|
|
1183
|
+
const serviceManager = new ServiceManager();
|
|
1184
|
+
const server = createServer((req, res) => {
|
|
1185
|
+
void handleRequest(req, res, staticDir, opts.cwd, serviceManager).catch((err) => {
|
|
1186
|
+
respondError(res, 500, err instanceof Error ? err.message : String(err));
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
const port = await new Promise((resolveListen, reject) => {
|
|
1190
|
+
server.once("error", reject);
|
|
1191
|
+
server.listen(opts.port ?? 0, "127.0.0.1", () => {
|
|
1192
|
+
const addr = server.address();
|
|
1193
|
+
if (!addr || typeof addr === "string") {
|
|
1194
|
+
reject(new Error("Server did not start with an address"));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
resolveListen(addr.port);
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
const { attachTerminalWs: attachTerminalWs2 } = await Promise.resolve().then(() => (init_terminal_ws(), terminal_ws_exports));
|
|
1201
|
+
attachTerminalWs2(server, opts.cwd);
|
|
1202
|
+
const url = `http://localhost:${port}`;
|
|
1203
|
+
if (opts.open !== false) {
|
|
1204
|
+
try {
|
|
1205
|
+
const openModule = await import("open");
|
|
1206
|
+
const openFn = openModule.default ?? openModule;
|
|
1207
|
+
await openFn(url);
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const onSignal = () => serviceManager.shutdown();
|
|
1212
|
+
process.once("SIGINT", onSignal);
|
|
1213
|
+
process.once("SIGTERM", onSignal);
|
|
1214
|
+
return {
|
|
1215
|
+
url,
|
|
1216
|
+
port,
|
|
1217
|
+
close: () => new Promise((resolveClose, reject) => {
|
|
1218
|
+
serviceManager.shutdown();
|
|
1219
|
+
process.off("SIGINT", onSignal);
|
|
1220
|
+
process.off("SIGTERM", onSignal);
|
|
1221
|
+
server.close((err) => err ? reject(err) : resolveClose());
|
|
1222
|
+
})
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
async function handleRequest(req, res, staticDir, cwd, serviceManager) {
|
|
1226
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1227
|
+
if (url.pathname === "/api/blueprints") {
|
|
1228
|
+
const result = await handleBlueprintsRequest(cwd);
|
|
1229
|
+
res.statusCode = result.status;
|
|
1230
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1231
|
+
res.end(JSON.stringify(result.body));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (url.pathname === "/api/blocks") {
|
|
1235
|
+
const result = await handleBlocksRequest(cwd);
|
|
1236
|
+
res.statusCode = result.status;
|
|
1237
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1238
|
+
res.end(JSON.stringify(result.body));
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (url.pathname === "/api/markdown") {
|
|
1242
|
+
const requestedPath = url.searchParams.get("path");
|
|
1243
|
+
if (!requestedPath) {
|
|
1244
|
+
res.statusCode = 400;
|
|
1245
|
+
res.setHeader("content-type", "text/plain");
|
|
1246
|
+
res.end("Missing ?path");
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const absolute = resolve2(cwd, requestedPath);
|
|
1250
|
+
if (!absolute.startsWith(resolve2(cwd))) {
|
|
1251
|
+
res.statusCode = 403;
|
|
1252
|
+
res.setHeader("content-type", "text/plain");
|
|
1253
|
+
res.end("Forbidden");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const result = await handleMarkdownRequest(absolute);
|
|
1257
|
+
res.statusCode = result.status;
|
|
1258
|
+
res.setHeader("content-type", result.contentType);
|
|
1259
|
+
res.end(result.body);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (url.pathname === "/api/code") {
|
|
1263
|
+
const requestedPath = url.searchParams.get("path");
|
|
1264
|
+
const lang = url.searchParams.get("lang") ?? void 0;
|
|
1265
|
+
if (!requestedPath) {
|
|
1266
|
+
res.statusCode = 400;
|
|
1267
|
+
res.setHeader("content-type", "text/plain");
|
|
1268
|
+
res.end("Missing ?path");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const absolute = resolve2(cwd, requestedPath);
|
|
1272
|
+
if (!absolute.startsWith(resolve2(cwd))) {
|
|
1273
|
+
res.statusCode = 403;
|
|
1274
|
+
res.setHeader("content-type", "text/plain");
|
|
1275
|
+
res.end("Forbidden");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const result = await handleCodeRequest(absolute, lang);
|
|
1279
|
+
res.statusCode = result.status;
|
|
1280
|
+
res.setHeader("content-type", result.contentType);
|
|
1281
|
+
res.end(result.body);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (url.pathname === "/api/services/ensure" && req.method === "POST") {
|
|
1285
|
+
const raw = await readBody(req);
|
|
1286
|
+
let parsed;
|
|
1287
|
+
try {
|
|
1288
|
+
parsed = JSON.parse(raw || "{}");
|
|
1289
|
+
} catch {
|
|
1290
|
+
res.statusCode = 400;
|
|
1291
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1292
|
+
res.end(JSON.stringify({ error: "invalid json" }));
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const result = await handleServiceEnsureRequest(cwd, serviceManager, {
|
|
1296
|
+
blockId: parsed.blockId ?? "",
|
|
1297
|
+
viewKey: parsed.viewKey ?? ""
|
|
1298
|
+
});
|
|
1299
|
+
res.statusCode = result.status;
|
|
1300
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1301
|
+
res.end(JSON.stringify(result.body));
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const requested = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
1305
|
+
const absolutePath = normalize(join4(staticDir, requested));
|
|
1306
|
+
if (!absolutePath.startsWith(staticDir)) {
|
|
1307
|
+
respondError(res, 403, "Forbidden");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
const content = await readFile5(absolutePath);
|
|
1312
|
+
res.setHeader("content-type", MIME_TYPES[extname2(absolutePath)] ?? "application/octet-stream");
|
|
1313
|
+
res.end(content);
|
|
1314
|
+
return;
|
|
1315
|
+
} catch {
|
|
1316
|
+
try {
|
|
1317
|
+
const indexContent = await readFile5(join4(staticDir, "index.html"));
|
|
1318
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1319
|
+
res.end(indexContent);
|
|
1320
|
+
} catch {
|
|
1321
|
+
respondError(res, 404, "Not found");
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function respondError(res, status, message) {
|
|
1326
|
+
res.statusCode = status;
|
|
1327
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
1328
|
+
res.end(message);
|
|
1329
|
+
}
|
|
1330
|
+
function readBody(req) {
|
|
1331
|
+
return new Promise((resolveBody, reject) => {
|
|
1332
|
+
const chunks = [];
|
|
1333
|
+
req.on("data", (c) => chunks.push(c));
|
|
1334
|
+
req.on("end", () => resolveBody(Buffer.concat(chunks).toString()));
|
|
1335
|
+
req.on("error", reject);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/cli.ts
|
|
1340
|
+
if (importLocal(import.meta.url)) {
|
|
1341
|
+
} else {
|
|
1342
|
+
main();
|
|
1343
|
+
}
|
|
1344
|
+
function main() {
|
|
1345
|
+
const program = new Command();
|
|
1346
|
+
program.name("macroscope").description("Catalogue tooling for AI-collaborative software development").version(VERSION, "-v, --version").option("--json", "output machine-readable JSON");
|
|
1347
|
+
program.command("hello").description("Placeholder \u2014 proves the CLI wiring works").action((_opts, cmd) => {
|
|
1348
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1349
|
+
if (globalOpts.json) {
|
|
1350
|
+
process.stdout.write(`${JSON.stringify({ ok: true, message: "hello from macroscope" })}
|
|
1351
|
+
`);
|
|
1352
|
+
} else {
|
|
1353
|
+
process.stdout.write("hello from macroscope\n");
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
program.command("blueprints").description("List the unit blueprints defined in this project (.macroscope/blueprints/)").option("--cwd <path>", "project search starts here (defaults to current dir)").action(async (cmdOpts, cmd) => {
|
|
1357
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1358
|
+
await runBlueprints({ cwd: cmdOpts.cwd, json: !!globalOpts.json });
|
|
1359
|
+
});
|
|
1360
|
+
program.command("ui").description("Open the macroscope web UI in your browser").option("--cwd <path>", "project search starts here (defaults to current dir)").option(
|
|
1361
|
+
"--port <n>",
|
|
1362
|
+
"port to listen on (default: auto-pick a free port)",
|
|
1363
|
+
(v) => Number.parseInt(v, 10)
|
|
1364
|
+
).option("--no-open", "don't automatically open a browser").action(async (cmdOpts) => {
|
|
1365
|
+
await runUi({
|
|
1366
|
+
cwd: cmdOpts.cwd,
|
|
1367
|
+
port: cmdOpts.port,
|
|
1368
|
+
open: cmdOpts.open !== false
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
program.parse(process.argv);
|
|
1372
|
+
}
|
|
1373
|
+
async function runBlueprints(opts) {
|
|
1374
|
+
let project;
|
|
1375
|
+
try {
|
|
1376
|
+
project = await findProject(opts.cwd ?? process.cwd());
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
if (err instanceof ProjectNotFoundError) {
|
|
1379
|
+
writeError(err.message, opts.json);
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
writeError(`${err.message}`, opts.json);
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
const result = await loadBlueprints(project);
|
|
1386
|
+
if (opts.json) {
|
|
1387
|
+
writeJson(result);
|
|
1388
|
+
} else {
|
|
1389
|
+
writeHuman(result);
|
|
1390
|
+
}
|
|
1391
|
+
if (result.errors.length > 0) {
|
|
1392
|
+
process.exitCode = 1;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function writeHuman(result) {
|
|
1396
|
+
if (result.blueprints.length === 0 && result.errors.length === 0) {
|
|
1397
|
+
process.stdout.write(
|
|
1398
|
+
"No blueprints found. Create one at .macroscope/blueprints/<kind>/handler.ts.\n"
|
|
1399
|
+
);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (result.blueprints.length > 0) {
|
|
1403
|
+
for (const bp of result.blueprints) {
|
|
1404
|
+
const caps = bp.capabilities.length > 0 ? bp.capabilities.join(", ") : "(no operations)";
|
|
1405
|
+
process.stdout.write(`${bp.kind.padEnd(24)} ${caps}
|
|
1406
|
+
`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (result.errors.length > 0) {
|
|
1410
|
+
process.stderr.write(`
|
|
1411
|
+
${result.errors.length} blueprint error(s):
|
|
1412
|
+
`);
|
|
1413
|
+
for (const e of result.errors) {
|
|
1414
|
+
process.stderr.write(` [${e.kind}] ${e.message}
|
|
1415
|
+
`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function writeJson(result) {
|
|
1420
|
+
const out = {
|
|
1421
|
+
blueprints: result.blueprints.map((b) => ({
|
|
1422
|
+
kind: b.kind,
|
|
1423
|
+
folder: b.folder,
|
|
1424
|
+
file: b.file,
|
|
1425
|
+
capabilities: b.capabilities
|
|
1426
|
+
})),
|
|
1427
|
+
errors: result.errors
|
|
1428
|
+
};
|
|
1429
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}
|
|
1430
|
+
`);
|
|
1431
|
+
}
|
|
1432
|
+
async function runUi(opts) {
|
|
1433
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1434
|
+
try {
|
|
1435
|
+
await findProject(cwd);
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
if (err instanceof ProjectNotFoundError) {
|
|
1438
|
+
process.stderr.write(`${err.message}
|
|
1439
|
+
`);
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
throw err;
|
|
1443
|
+
}
|
|
1444
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
1445
|
+
const staticDir = resolve3(here, "ui");
|
|
1446
|
+
let server;
|
|
1447
|
+
try {
|
|
1448
|
+
server = await startUiServer({ cwd, staticDir, port: opts.port, open: opts.open });
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
process.stderr.write(`macroscope ui: ${err.message}
|
|
1451
|
+
`);
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
process.stdout.write(`macroscope ui running at ${server.url}
|
|
1455
|
+
`);
|
|
1456
|
+
process.stdout.write("Press Ctrl+C to stop.\n");
|
|
1457
|
+
const shutdown = async () => {
|
|
1458
|
+
await server.close();
|
|
1459
|
+
process.exit(0);
|
|
1460
|
+
};
|
|
1461
|
+
process.on("SIGINT", () => void shutdown());
|
|
1462
|
+
process.on("SIGTERM", () => void shutdown());
|
|
1463
|
+
}
|
|
1464
|
+
function writeError(message, json) {
|
|
1465
|
+
if (json) {
|
|
1466
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
|
|
1467
|
+
`);
|
|
1468
|
+
} else {
|
|
1469
|
+
process.stderr.write(`${message}
|
|
1470
|
+
`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
//# sourceMappingURL=cli.js.map
|