lineadraw 0.6.0 → 0.7.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 CHANGED
@@ -128,8 +128,7 @@ lineadraw manifest Validate all modules and write .lineadraw/marketplace.json
128
128
  lineadraw check Validate without writing; fail if the committed manifest is stale
129
129
  lineadraw init Add the marketplace structure to the current npm project
130
130
 
131
- lineadraw status Is the Linea desktop app serving agents? port, open editors
132
- lineadraw editors The open drawings: id, name, path, kind, dirty
131
+ lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
133
132
  lineadraw open <file.linea> Open a file in a new window
134
133
  lineadraw close [--editor <x>] [--discard]
135
134
  lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* tool
@@ -184,7 +183,7 @@ doc.add([
184
183
  { type: "dimension", points: [[0, 0], [6000, 0]], offset: [0, -500],
185
184
  scale: 50 }, // annotations × drawing scale
186
185
  ]);
187
- doc.createLayout("Plan 1:50", { sheet: "A3", viewport: { scale: "1:50" } });
186
+ doc.addLayout("Plan 1:50", { sheet: "A3", viewport: { scale: "1:50" } });
188
187
  await doc.exportPng("check.png", { width: 1600 }); // verify by LOOKING at it
189
188
  await doc.save("plan.linea");
190
189
  await doc.exportPdf("plan.pdf");
@@ -212,7 +211,7 @@ await doc.exportPdf("plan.pdf");
212
211
  paper size (2.5 body / 3.5 / 5 titles). `scale` is NOT a `styleOverride`
213
212
  key; `add`/`update` reject it there.
214
213
  - **Reads are detached snapshots**: `doc.document`, `doc.objects`,
215
- `getObject`, `query()` return deep clones — mutating them does nothing.
214
+ `getObject`, `listObjects()` return deep clones — mutating them does nothing.
216
215
  - **New documents are layout-less**; `exportPdf` of a layout-less document
217
216
  falls back to one auto-fitted A3 page.
218
217
  - `transaction(fn)` groups edits into one undo step. `array(ids, count,
@@ -229,22 +228,26 @@ await doc.exportPdf("plan.pdf");
229
228
  `.linea` JSON itself carries no name.
230
229
 
231
230
  - **Read**: `summary()`, `objects`, `layers`, `layouts`, `getObject`,
232
- `query({ids, type, layer, color, lineType, content, bbox, inside, near, layout})`,
231
+ `listObjects({ids, type, layer, color, lineType, content, bbox, inside, near, layout})`,
233
232
  `measure(id)` (`bboxOf(ids)`, `length(id)`, `area(id)`), `listLayers()`,
234
- `listLayouts()`, `listBlocks()`, `listCommands()`, `revision`
233
+ `listLayouts()`, `listTextStyles()`, `listDimStyles()`,
234
+ `listBlockDefinitions()`, `listUserCommands()`, `revision`
235
235
  - **Edit**: `add(objects, {layout?})`, `update(ids, patch)` or
236
236
  `update([{id, ...patch}])`, `remove(ids)` (→ count), `reorder(ids,
237
237
  "front" | "back" | {above} | {below})`, `transaction(fn)` (sync or async;
238
- a throw rolls back), `undo()`, `redo()`, `setDimStyle`,
239
- `setTextStyle`, `applyOps(ops)`, `ready()`
240
- - **Modules**: `await installBlocks(sources)` / `removeBlock(id)`,
241
- `await installCommands(sources)` / `removeCommand(id)`,
238
+ a throw rolls back), `undo()`, `redo()`, `applyOps(ops)`, `ready()`
239
+ - **Styles** (by name or id): `listTextStyles()`, `addTextStyle({name, …})`,
240
+ `updateTextStyle(nameOrId, patch)`, `removeTextStyle(nameOrId)` (refused
241
+ while objects use it) — the same four for `DimStyle`; `setTextStyle` /
242
+ `setDimStyle` remain as deprecated aliases of update
243
+ - **Modules**: `await addBlockDefinitions(sources)` / `removeBlockDefinition(id)`,
244
+ `await addUserCommands(sources)` / `removeUserCommand(id)`,
242
245
  `await setBlockDefinitions(sources)`, `await setCommands(sources)`
243
246
  - **Scripts**: `await runScript(source, inputs?, params?)` runs a
244
247
  `defineCommand` module (the same one the editor palette runs) headlessly
245
248
  as ONE undo step — its declared `inputs` (points) are supplied up front,
246
249
  its declared `params` by name (`{ count: 3 }`; omitted ones take their
247
- defaults, unknown names and wrong types throw); `runCommand(id, inputs?,
250
+ defaults, unknown names and wrong types throw); `runUserCommand(id, inputs?,
248
251
  params?)` runs one of the document's own commands. Returns the script's
249
252
  return value.
250
253
  - **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
@@ -253,14 +256,14 @@ await doc.exportPdf("plan.pdf");
253
256
  - **Assets**: `addAsset(dataUrl) → assetId` (for `image` objects;
254
257
  content-keyed), `removeAsset(id)` (refused while referenced)
255
258
  - **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
256
- - **Layouts**: `createLayout(name, {sheet, scale, viewport, index})`,
259
+ - **Layouts**: `addLayout(name, {sheet, scale, viewport, index})`,
257
260
  `updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
258
261
  `reorderLayouts`, `parseScale("1:50")`. `sheet` is `"A3"` (landscape when
259
262
  creating, the current orientation when patching), `{ name: "A3",
260
263
  orientation: "portrait" }`, or `{ width, height }` in mm.
261
264
  - **Export**: `renderSvg`/`exportSvg`, `renderPng`/`exportPng`, `await toDxf()`/`exportDxf`,
262
265
  `toDwg`/`exportDwg` (DWG R2000–2018, default AC1027), `renderPdf`/`exportPdf`,
263
- `toJson`/`save`
266
+ `toLinea`/`save`
264
267
  - **Import (merge)**: `importDxf(path)`/`importDxfText(text)` (with
265
268
  `unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
266
269
  `importPdfData(arrayBuffer)`
package/bin/lineadraw.mjs CHANGED
@@ -1,278 +1,277 @@
1
1
  #!/usr/bin/env node
2
- // The `lineadraw` CLI — two halves:
3
- // * marketplace tooling for Linea block/command repos
4
- // (docs/marketplace-devx.md in the linea repo):
5
- // lineadraw manifest [--dir <path>] generate .lineadraw/marketplace.json
6
- // lineadraw check [--dir <path>] validate + fail if the manifest is stale
7
- // lineadraw init [--dir <path>] add the marketplace structure here
8
- // * the BRIDGE: what the running Linea desktop app serves on
9
- // 127.0.0.1:7431 (docs/desktop-bridge.md) — `status`, `editors`, `open`,
10
- // `close`, `call`, `tools`. Reads the desktop's bridge.json for the port
11
- // and the agent token; `--port` / `--token` / LINEA_BRIDGE_TOKEN override.
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { homedir } from "node:os";
14
- import { dirname, join, resolve } from "node:path";
15
- import {
16
- buildManifest,
17
- manifestJson,
18
- MANIFEST_PATH,
19
- verifyWithSdk,
20
- withThumbnails,
21
- } from "../lib/manifest.mjs";
22
- import { init, tsconfigLibWarning } from "../lib/init.mjs";
23
-
24
- const USAGE = `Usage: lineadraw <command> [options]
25
-
26
- Marketplace repos (block / command collections):
27
- manifest [--dir <path>] Validate all modules and write ${MANIFEST_PATH}
28
- check [--dir <path>] Validate without writing; fail if the committed manifest is stale
29
- init [--dir <path>] Add the marketplace structure to the current npm project
30
-
31
- manifest and check also dry-run every block through lineadraw/sdk and bake
32
- preview thumbnails into the catalog.
33
-
34
- Running editors (the Linea desktop app on 127.0.0.1:7431):
35
- status is anything serving? port, editors
36
- editors the open drawings: id, name, path, kind, dirty
37
- open <file.linea> open a file in a window of its own (returns its editor)
38
- close [--editor <x>] [--discard] close an editor like its window's x
39
- call <operation> ['<json>'] [--editor <x>]
40
- run one linea_* operation (see tools)
41
- tools the operations the bridge offers
42
-
43
- The desktop's bridge.json (port, agent token) is read automatically;
44
- LINEA_BRIDGE_TOKEN or --token override the token, --port the port.`;
45
-
46
- const args = process.argv.slice(2);
47
- const command = args[0];
48
- const dirFlag = args.indexOf("--dir");
49
- const dir = resolve(dirFlag === -1 ? "." : (args[dirFlag + 1] ?? "."));
50
-
51
- /** `--key value` / `--flag` after the command; the rest are positional. */
52
- const parseArgs = (argv) => {
53
- const positional = [];
54
- const flags = {};
55
- for (let i = 0; i < argv.length; i++) {
56
- const a = argv[i];
57
- if (a.startsWith("--")) {
58
- const key = a.slice(2);
59
- const next = argv[i + 1];
60
- if (next !== undefined && !next.startsWith("--")) {
61
- flags[key] = next;
62
- i++;
63
- } else flags[key] = true;
64
- } else positional.push(a);
65
- }
66
- return { positional, flags };
67
- };
68
-
69
- // --- bridge --------------------------------------------------------------
70
-
71
- const APP_ID = "com.lineadraw.desktop";
72
- const DEFAULT_PORT = 7431;
73
-
74
- const bridgeFilePath = () => {
75
- const home = homedir();
76
- if (process.platform === "win32")
77
- return join(
78
- process.env.APPDATA ?? join(home, "AppData", "Roaming"),
79
- APP_ID,
80
- "bridge.json",
81
- );
82
- if (process.platform === "darwin")
83
- return join(home, "Library", "Application Support", APP_ID, "bridge.json");
84
- return join(
85
- process.env.XDG_DATA_HOME ?? join(home, ".local", "share"),
86
- APP_ID,
87
- "bridge.json",
88
- );
89
- };
90
-
91
- const readBridgeFile = () => {
92
- const path = bridgeFilePath();
93
- if (!existsSync(path)) return {};
94
- try {
95
- return JSON.parse(readFileSync(path, "utf8"));
96
- } catch {
97
- return {};
98
- }
99
- };
100
-
101
- const bridge = (flags) => {
102
- const file = readBridgeFile();
103
- const port =
104
- typeof flags.port === "string"
105
- ? Number(flags.port)
106
- : file.port || DEFAULT_PORT;
107
- const token =
108
- (typeof flags.token === "string" ? flags.token : undefined) ??
109
- process.env.LINEA_BRIDGE_TOKEN ??
110
- file.token;
111
- const headers = { "content-type": "application/json" };
112
- if (token) headers.authorization = `Bearer ${token}`;
113
- return { base: `http://127.0.0.1:${port}`, headers };
114
- };
115
-
116
- const request = async (b, method, path, body) => {
117
- let response;
118
- try {
119
- response = await fetch(`${b.base}${path}`, {
120
- method,
121
- headers: b.headers,
122
- body: body === undefined ? undefined : JSON.stringify(body),
123
- });
124
- } catch (e) {
125
- const cause = e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e;
126
- throw new Error(
127
- `Nothing answers on ${b.base} (${cause}). Is the Linea desktop app running?`,
128
- { cause: e },
129
- );
130
- }
131
- const text = await response.text();
132
- let parsed = text;
133
- try {
134
- parsed = JSON.parse(text);
135
- } catch {
136
- // not JSON: keep the text
137
- }
138
- if (!response.ok) {
139
- const error = parsed?.error;
140
- throw new Error(
141
- error?.message ?? `HTTP ${response.status}: ${text.slice(0, 200)}`,
142
- );
143
- }
144
- return parsed;
145
- };
146
-
147
- const editorQuery = (flags) =>
148
- typeof flags.editor === "string"
149
- ? `?editor=${encodeURIComponent(flags.editor)}`
150
- : "";
151
-
152
- const bridgeCommand = async (cmd, positional, flags) => {
153
- const b = bridge(flags);
154
- switch (cmd) {
155
- case "status":
156
- return request(b, "GET", "/status");
157
- case "editors":
158
- return request(b, "GET", "/api/editors");
159
- case "open": {
160
- const file = positional[0];
161
- if (!file) throw new Error("open needs a .linea file path");
162
- return request(b, "POST", "/api/openEditor", { path: resolve(file) });
163
- }
164
- case "close":
165
- return request(
166
- b,
167
- "POST",
168
- `/api/linea_close_editor${editorQuery(flags)}`,
169
- { discard: flags.discard === true },
170
- );
171
- case "call": {
172
- const [operation, json] = positional;
173
- if (!operation)
174
- throw new Error("call needs an operation name (see `lineadraw tools`)");
175
- const payload = json === undefined ? {} : JSON.parse(json);
176
- return request(
177
- b,
178
- "POST",
179
- `/api/${encodeURIComponent(operation)}${editorQuery(flags)}`,
180
- payload,
181
- );
182
- }
183
- case "tools": {
184
- const reply = await request(b, "POST", "/mcp", {
185
- jsonrpc: "2.0",
186
- id: 1,
187
- method: "tools/list",
188
- });
189
- return (reply.result?.tools ?? []).map((t) => ({
190
- name: t.name,
191
- description: t.description,
192
- }));
193
- }
194
- default:
195
- throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
196
- }
197
- };
198
-
199
- const BRIDGE_COMMANDS = ["status", "editors", "open", "close", "call", "tools"];
200
-
201
- const fail = (message) => {
202
- console.error(message);
203
- process.exit(1);
204
- };
205
-
206
- const reportProblems = (problems) => {
207
- for (const p of problems)
208
- console.error(`REJECTED ${p.file}: ${p.problems.join("; ")}`);
209
- };
210
-
211
- /** Validate everything (light + sdk dry-run); exits on failure. */
212
- const validatedManifest = async () => {
213
- const { manifest: base, sources, problems } = buildManifest(dir);
214
- const sdkResult = await verifyWithSdk(base, sources);
215
- const all = [...problems, ...sdkResult.problems];
216
- if (all.length > 0) {
217
- reportProblems(all);
218
- fail(`${all.length} problem(s) — nothing written.`);
219
- }
220
- const manifest = withThumbnails(base, sdkResult.thumbnails);
221
- if (!sdkResult.ran)
222
- console.warn(
223
- `Blocks were not dry-run: lineadraw/sdk failed to load (${sdkResult.detail}). Try reinstalling lineadraw.`,
224
- );
225
- return manifest;
226
- };
227
-
228
- switch (command) {
229
- case "manifest": {
230
- const manifest = await validatedManifest();
231
- const outPath = join(dir, MANIFEST_PATH);
232
- mkdirSync(dirname(outPath), { recursive: true });
233
- writeFileSync(outPath, manifestJson(manifest));
234
- console.log(`${MANIFEST_PATH}: ${manifest.entries.length} entries`);
235
- for (const e of manifest.entries)
236
- console.log(` ${e.kind} ${e.id} v${e.version}`);
237
- break;
238
- }
239
- case "check": {
240
- const manifest = await validatedManifest();
241
- const committedPath = join(dir, MANIFEST_PATH);
242
- const committed = existsSync(committedPath)
243
- ? readFileSync(committedPath, "utf8")
244
- : null;
245
- if (committed !== manifestJson(manifest))
246
- fail(
247
- `${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
248
- );
249
- const libWarning = tsconfigLibWarning(dir);
250
- if (libWarning !== null) console.warn(`warning: ${libWarning}`);
251
- console.log(`OK: ${manifest.entries.length} entries, manifest is current.`);
252
- break;
253
- }
254
- case "init": {
255
- try {
256
- const { created, skipped, warnings } = await init(dir);
257
- for (const f of created) console.log(`created ${f}`);
258
- for (const f of skipped) console.log(`kept ${f}`);
259
- for (const w of warnings) console.warn(`warning ${w}`);
260
- console.log(
261
- "\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
262
- );
263
- } catch (e) {
264
- fail(e instanceof Error ? e.message : String(e));
265
- }
266
- break;
267
- }
268
- default: {
269
- if (!BRIDGE_COMMANDS.includes(command)) fail(USAGE);
270
- const { positional, flags } = parseArgs(args.slice(1));
271
- try {
272
- const result = await bridgeCommand(command, positional, flags);
273
- console.log(JSON.stringify(result, null, 2));
274
- } catch (e) {
275
- fail(e instanceof Error ? e.message : String(e));
276
- }
277
- }
278
- }
2
+ // The `lineadraw` CLI — two halves:
3
+ // * marketplace tooling for Linea block/command repos
4
+ // (docs/marketplace-devx.md in the linea repo):
5
+ // lineadraw manifest [--dir <path>] generate .lineadraw/marketplace.json
6
+ // lineadraw check [--dir <path>] validate + fail if the manifest is stale
7
+ // lineadraw init [--dir <path>] add the marketplace structure here
8
+ // * the BRIDGE: what the running Linea desktop app serves on
9
+ // 127.0.0.1:7431 (docs/desktop-bridge.md) — `status`, `open`, `close`,
10
+ // `call`, `tools`. Reads the desktop's bridge.json for the port
11
+ // and the agent token; `--port` / `--token` / LINEA_BRIDGE_TOKEN override.
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import {
16
+ buildManifest,
17
+ manifestJson,
18
+ MANIFEST_PATH,
19
+ verifyWithSdk,
20
+ withThumbnails,
21
+ } from "../lib/manifest.mjs";
22
+ import { init, tsconfigLibWarning } from "../lib/init.mjs";
23
+
24
+ const USAGE = `Usage: lineadraw <command> [options]
25
+
26
+ Marketplace repos (block / command collections):
27
+ manifest [--dir <path>] Validate all modules and write ${MANIFEST_PATH}
28
+ check [--dir <path>] Validate without writing; fail if the committed manifest is stale
29
+ init [--dir <path>] Add the marketplace structure to the current npm project
30
+
31
+ manifest and check also dry-run every block through lineadraw/sdk and bake
32
+ preview thumbnails into the catalog.
33
+
34
+ Running editors (the Linea desktop app on 127.0.0.1:7431):
35
+ status app version, port, the open drawings (id, name, path, kind, dirty)
36
+ open <file.linea> open a file in a window of its own (returns its editor)
37
+ close [--editor <x>] [--discard] close an editor like its window's x
38
+ call <operation> ['<json>'] [--editor <x>]
39
+ run one linea_* operation (see tools)
40
+ tools the operations the bridge offers
41
+
42
+ The desktop's bridge.json (port, agent token) is read automatically;
43
+ LINEA_BRIDGE_TOKEN or --token override the token, --port the port.`;
44
+
45
+ const args = process.argv.slice(2);
46
+ const command = args[0];
47
+ const dirFlag = args.indexOf("--dir");
48
+ const dir = resolve(dirFlag === -1 ? "." : (args[dirFlag + 1] ?? "."));
49
+
50
+ /** `--key value` / `--flag` after the command; the rest are positional. */
51
+ const parseArgs = (argv) => {
52
+ const positional = [];
53
+ const flags = {};
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a.startsWith("--")) {
57
+ const key = a.slice(2);
58
+ const next = argv[i + 1];
59
+ if (next !== undefined && !next.startsWith("--")) {
60
+ flags[key] = next;
61
+ i++;
62
+ } else flags[key] = true;
63
+ } else positional.push(a);
64
+ }
65
+ return { positional, flags };
66
+ };
67
+
68
+ // --- bridge --------------------------------------------------------------
69
+
70
+ const APP_ID = "com.lineadraw.desktop";
71
+ const DEFAULT_PORT = 7431;
72
+
73
+ const bridgeFilePath = () => {
74
+ const home = homedir();
75
+ if (process.platform === "win32")
76
+ return join(
77
+ process.env.APPDATA ?? join(home, "AppData", "Roaming"),
78
+ APP_ID,
79
+ "bridge.json",
80
+ );
81
+ if (process.platform === "darwin")
82
+ return join(home, "Library", "Application Support", APP_ID, "bridge.json");
83
+ return join(
84
+ process.env.XDG_DATA_HOME ?? join(home, ".local", "share"),
85
+ APP_ID,
86
+ "bridge.json",
87
+ );
88
+ };
89
+
90
+ const readBridgeFile = () => {
91
+ const path = bridgeFilePath();
92
+ if (!existsSync(path)) return {};
93
+ try {
94
+ return JSON.parse(readFileSync(path, "utf8"));
95
+ } catch {
96
+ return {};
97
+ }
98
+ };
99
+
100
+ const bridge = (flags) => {
101
+ const file = readBridgeFile();
102
+ const port =
103
+ typeof flags.port === "string"
104
+ ? Number(flags.port)
105
+ : file.port || DEFAULT_PORT;
106
+ const token =
107
+ (typeof flags.token === "string" ? flags.token : undefined) ??
108
+ process.env.LINEA_BRIDGE_TOKEN ??
109
+ file.token;
110
+ const headers = { "content-type": "application/json" };
111
+ if (token) headers.authorization = `Bearer ${token}`;
112
+ return { base: `http://127.0.0.1:${port}`, headers };
113
+ };
114
+
115
+ const request = async (b, method, path, body) => {
116
+ let response;
117
+ try {
118
+ response = await fetch(`${b.base}${path}`, {
119
+ method,
120
+ headers: b.headers,
121
+ body: body === undefined ? undefined : JSON.stringify(body),
122
+ });
123
+ } catch (e) {
124
+ const cause = e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e;
125
+ throw new Error(
126
+ `Nothing answers on ${b.base} (${cause}). Is the Linea desktop app running?`,
127
+ { cause: e },
128
+ );
129
+ }
130
+ const text = await response.text();
131
+ let parsed = text;
132
+ try {
133
+ parsed = JSON.parse(text);
134
+ } catch {
135
+ // not JSON: keep the text
136
+ }
137
+ if (!response.ok) {
138
+ const error = parsed?.error;
139
+ throw new Error(
140
+ error?.message ?? `HTTP ${response.status}: ${text.slice(0, 200)}`,
141
+ );
142
+ }
143
+ return parsed;
144
+ };
145
+
146
+ const editorQuery = (flags) =>
147
+ typeof flags.editor === "string"
148
+ ? `?editor=${encodeURIComponent(flags.editor)}`
149
+ : "";
150
+
151
+ const bridgeCommand = async (cmd, positional, flags) => {
152
+ const b = bridge(flags);
153
+ switch (cmd) {
154
+ case "status":
155
+ return request(b, "GET", "/status");
156
+ case "open": {
157
+ const file = positional[0];
158
+ if (!file) throw new Error("open needs a .linea file path");
159
+ return request(b, "POST", "/api/linea_open_editor", {
160
+ path: resolve(file),
161
+ });
162
+ }
163
+ case "close":
164
+ return request(
165
+ b,
166
+ "POST",
167
+ `/api/linea_close_document${editorQuery(flags)}`,
168
+ { discard: flags.discard === true },
169
+ );
170
+ case "call": {
171
+ const [operation, json] = positional;
172
+ if (!operation)
173
+ throw new Error("call needs an operation name (see `lineadraw tools`)");
174
+ const payload = json === undefined ? {} : JSON.parse(json);
175
+ return request(
176
+ b,
177
+ "POST",
178
+ `/api/${encodeURIComponent(operation)}${editorQuery(flags)}`,
179
+ payload,
180
+ );
181
+ }
182
+ case "tools": {
183
+ const reply = await request(b, "POST", "/mcp", {
184
+ jsonrpc: "2.0",
185
+ id: 1,
186
+ method: "tools/list",
187
+ });
188
+ return (reply.result?.tools ?? []).map((t) => ({
189
+ name: t.name,
190
+ description: t.description,
191
+ }));
192
+ }
193
+ default:
194
+ throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
195
+ }
196
+ };
197
+
198
+ const BRIDGE_COMMANDS = ["status", "open", "close", "call", "tools"];
199
+
200
+ const fail = (message) => {
201
+ console.error(message);
202
+ process.exit(1);
203
+ };
204
+
205
+ const reportProblems = (problems) => {
206
+ for (const p of problems)
207
+ console.error(`REJECTED ${p.file}: ${p.problems.join("; ")}`);
208
+ };
209
+
210
+ /** Validate everything (light + sdk dry-run); exits on failure. */
211
+ const validatedManifest = async () => {
212
+ const { manifest: base, sources, problems } = buildManifest(dir);
213
+ const sdkResult = await verifyWithSdk(base, sources);
214
+ const all = [...problems, ...sdkResult.problems];
215
+ if (all.length > 0) {
216
+ reportProblems(all);
217
+ fail(`${all.length} problem(s) — nothing written.`);
218
+ }
219
+ const manifest = withThumbnails(base, sdkResult.thumbnails);
220
+ if (!sdkResult.ran)
221
+ console.warn(
222
+ `Blocks were not dry-run: lineadraw/sdk failed to load (${sdkResult.detail}). Try reinstalling lineadraw.`,
223
+ );
224
+ return manifest;
225
+ };
226
+
227
+ switch (command) {
228
+ case "manifest": {
229
+ const manifest = await validatedManifest();
230
+ const outPath = join(dir, MANIFEST_PATH);
231
+ mkdirSync(dirname(outPath), { recursive: true });
232
+ writeFileSync(outPath, manifestJson(manifest));
233
+ console.log(`${MANIFEST_PATH}: ${manifest.entries.length} entries`);
234
+ for (const e of manifest.entries)
235
+ console.log(` ${e.kind} ${e.id} v${e.version}`);
236
+ break;
237
+ }
238
+ case "check": {
239
+ const manifest = await validatedManifest();
240
+ const committedPath = join(dir, MANIFEST_PATH);
241
+ const committed = existsSync(committedPath)
242
+ ? readFileSync(committedPath, "utf8")
243
+ : null;
244
+ if (committed !== manifestJson(manifest))
245
+ fail(
246
+ `${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
247
+ );
248
+ const libWarning = tsconfigLibWarning(dir);
249
+ if (libWarning !== null) console.warn(`warning: ${libWarning}`);
250
+ console.log(`OK: ${manifest.entries.length} entries, manifest is current.`);
251
+ break;
252
+ }
253
+ case "init": {
254
+ try {
255
+ const { created, skipped, warnings } = await init(dir);
256
+ for (const f of created) console.log(`created ${f}`);
257
+ for (const f of skipped) console.log(`kept ${f}`);
258
+ for (const w of warnings) console.warn(`warning ${w}`);
259
+ console.log(
260
+ "\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
261
+ );
262
+ } catch (e) {
263
+ fail(e instanceof Error ? e.message : String(e));
264
+ }
265
+ break;
266
+ }
267
+ default: {
268
+ if (!BRIDGE_COMMANDS.includes(command)) fail(USAGE);
269
+ const { positional, flags } = parseArgs(args.slice(1));
270
+ try {
271
+ const result = await bridgeCommand(command, positional, flags);
272
+ console.log(JSON.stringify(result, null, 2));
273
+ } catch (e) {
274
+ fail(e instanceof Error ? e.message : String(e));
275
+ }
276
+ }
277
+ }