lineadraw 0.5.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
@@ -1,13 +1,15 @@
1
1
  # lineadraw
2
2
 
3
- Type definitions and marketplace tooling for [Linea](https://lineadraw.com)
4
- blocks and commands — the one devDependency a block/command repository
5
- needs. Zero runtime: your modules import types from `"lineadraw"`, and the
6
- Linea editor provides the real implementation when it runs them.
3
+ [Linea](https://lineadraw.com) for Node: the headless drawing SDK, the
4
+ typings for blocks and commands, and the marketplace CLI — the one
5
+ dependency a script, an agent, or a block/command repository needs.
7
6
 
8
7
  - **Typings** — `lineadraw.d.ts` makes `import { defineBlock } from
9
8
  "lineadraw"` and `import { … } from "lineadraw/helpers"` type-check in
10
9
  any editor/CI, with parameter types inferred into your `draw` function.
10
+ - **SDK** — `import { openDrawing } from "lineadraw/sdk"`: open, edit,
11
+ render and export `.linea` documents headlessly
12
+ ([below](#headless-sdk-lineadrawsdk)).
11
13
  - **CLI** — `lineadraw manifest | check | init` turns any repository into
12
14
  a Linea **marketplace**: a git repo the editor can browse and install
13
15
  blocks/commands from.
@@ -65,10 +67,40 @@ pull them along); `paramVisibility({ params })` hides property rows;
65
67
  `previewInputs` curates the inputs used for thumbnails. Units are
66
68
  millimeters, Y-up, angles in radians CCW.
67
69
 
70
+ Parameter types (the same rows serve a command's `params`):
71
+
72
+ - `number` — `min`/`max`, `integer`, `step`, `prefix`/`suffix` ("1:",
73
+ "mm"), `presets` (standard values in the field's dropdown).
74
+ - `string` — `placeholder`, `multiline`, `presets`.
75
+ - `boolean` (`display: "toggle"` — a toggle button showing the label,
76
+ compact in a row), `enum` (`options`, each optionally with a `swatch:
77
+ "#rrggbb"` color square; `display: "buttons"` — a segmented toggle group
78
+ for short choices, falling back to a dropdown when it doesn't fit),
79
+ `color` (`"#rrggbb"`, `presets` = a palette).
80
+ - `numberList` (`number[]`, typed Tekla-style: `"0 2*7200"` →
81
+ `[0, 7200, 7200]`) and `stringList` (`string[]`: `'A "B 2"'` →
82
+ `["A", "B 2"]`), both with `presets` and `placeholder` (shown while
83
+ empty).
84
+ - Pickers over the document's resources, valued by id / name:
85
+ `textStyle`, `dimStyle`, `font`, `lineType`, `hatchPattern`.
86
+ - `label` — a section heading (no value); `row` — 2–3 compact params on one
87
+ panel row (`items`, each label a short prefix like "W"); its items are
88
+ ordinary flat params (`params.w`).
89
+
90
+ Every param may carry a `hint` (the label's tooltip). An instance that
91
+ overrides a param shows "Reset" beside it in Properties, back to your
92
+ default. `draw` also receives
93
+ `styles`: `styles.text(id)` / `styles.dim(id)` give a document style (an
94
+ empty or unknown id → the first), for blocks that lay out with real text
95
+ sizes.
96
+
68
97
  ## Writing a command
69
98
 
70
99
  A command is a macro with a single `defineCommand` default export. Its
71
- whole run is one undo step; Esc during a pick reverts cleanly.
100
+ whole run is one undo step; Esc during a pick reverts cleanly. A
101
+ `params` table (the same rows a block's `params` uses) is asked for in a
102
+ dialog before the run and arrives typed as `ctx.params`; headless callers
103
+ pass it by name.
72
104
 
73
105
  ```ts
74
106
  // commands/circle-here.cmd.ts
@@ -95,13 +127,23 @@ helpers) — the editor resolves both when executing your module.
95
127
  lineadraw manifest Validate all modules and write .lineadraw/marketplace.json
96
128
  lineadraw check Validate without writing; fail if the committed manifest is stale
97
129
  lineadraw init Add the marketplace structure to the current npm project
130
+
131
+ lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
132
+ lineadraw open <file.linea> Open a file in a new window
133
+ lineadraw close [--editor <x>] [--discard]
134
+ lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* tool
135
+ lineadraw tools The operations the bridge offers
98
136
  ```
99
137
 
138
+ The bridge commands talk to the running Linea desktop app on
139
+ `127.0.0.1:7431` (the app's `bridge.json` supplies the port and the agent
140
+ token; `--port`, `--token` and `LINEA_BRIDGE_TOKEN` override) — see
141
+ docs/desktop-bridge.md in the linea repo.
142
+
100
143
  The manifest is deterministic (repo-relative paths, no git metadata), so
101
144
  `check` is a byte-for-byte diff — the included CI workflow runs it on every
102
- push and PR. With `@lineadraw/sdk` installed as a devDependency,
103
- `manifest`/`check` additionally **dry-run every block** and render
104
- thumbnails; without it the CLI still fully works.
145
+ push and PR. `manifest`/`check` also **dry-run every block** through
146
+ `lineadraw/sdk` and render thumbnails.
105
147
 
106
148
  Collection name and owner come from your package.json:
107
149
 
@@ -111,14 +153,183 @@ Collection name and owner come from your package.json:
111
153
 
112
154
  ## Using a marketplace in the editor
113
155
 
114
- In [the editor](https://lineadraw.com): **Library → Marketplace tab → +**
115
- and enter `owner/repo` (or `owner/repo@branch`). For a private repository,
156
+ In [the editor](https://lineadraw.com): **Library → Marketplace tab → the
157
+ gear button (Manage marketplaces…) → Add a marketplace** and enter
158
+ `owner/repo` (or `owner/repo@branch`). For a private repository,
116
159
  create a fine-grained personal access token with read-only Contents access
117
160
  to just that repository and paste it in the add dialog — it is stored only
118
161
  on that device. Installing embeds the module (and its declared dependency
119
162
  closure) in the document, so drawings keep working offline and travel with
120
163
  the file.
121
164
 
165
+ ## Headless SDK (`lineadraw/sdk`)
166
+
167
+ A script-friendly API over the Linea CAD kernel: open, create, query, edit,
168
+ render, and import/export `.linea` documents headlessly in Node — no browser,
169
+ no server. Built for agents, CLIs, and batch tooling.
170
+
171
+ ```bash
172
+ npm i lineadraw
173
+ ```
174
+
175
+ ```js
176
+ import { newDrawing, openDrawing } from "lineadraw/sdk";
177
+
178
+ const doc = newDrawing();
179
+ const walls = doc.addLayer({ name: "walls", color: "#e8e8e8", lineWidth: 0.5 });
180
+ doc.add([
181
+ { type: "polyline", layerId: walls, closed: true,
182
+ points: [[0, 0], [6000, 0], [6000, 3000], [0, 3000]] },
183
+ { type: "dimension", points: [[0, 0], [6000, 0]], offset: [0, -500],
184
+ scale: 50 }, // annotations × drawing scale
185
+ ]);
186
+ doc.addLayout("Plan 1:50", { sheet: "A3", viewport: { scale: "1:50" } });
187
+ await doc.exportPng("check.png", { width: 1600 }); // verify by LOOKING at it
188
+ await doc.save("plan.linea");
189
+ await doc.exportPdf("plan.pdf");
190
+ ```
191
+
192
+ ### Conventions (read once, avoid every common bug)
193
+
194
+ - **Units are millimeters, Y-up. Angles are radians CCW** — everywhere in the
195
+ object model and in `rotate()`. Draw 1:1 (a 6 m wall is 6000 mm); paper
196
+ scale lives in layout viewports (`"1:50"` strings via `parseScale`).
197
+ - **Never write `.linea` JSON by hand.** Every mutation goes through
198
+ kernel-validated ops (`add`/`update`/`remove`/transforms); invalid input
199
+ throws with the failing object and field named.
200
+ - **Style by layer**: unset `color`/`lineWidth`/`lineType` on objects inherit
201
+ from the layer. Layer names are unique (`addLayer` throws on duplicates)
202
+ and can be used instead of ids (`layer: "walls"`, query filters). Note:
203
+ `locked` layers are an editor-UI concept — the SDK does not block edits.
204
+ - **Layer `print: false`** (plot flag) keeps a layer visible on screen and in
205
+ SVG/PNG but excludes it from PDF output — construction/guide layers.
206
+ - **Opacity is byLayer too**: `opacity` (0..1) on a layer is the default for
207
+ its objects; an object's own `opacity` overrides it. Paints across canvas,
208
+ SVG, and PDF; DXF/DWG export drops it (no direct mapping).
209
+ - **Annotation scale is per-object**: for a 1:N drawing set the top-level
210
+ `scale: N` on text, dimensions, and hatches, keeping `textHeight` at
211
+ paper size (2.5 body / 3.5 / 5 titles). `scale` is NOT a `styleOverride`
212
+ key; `add`/`update` reject it there.
213
+ - **Reads are detached snapshots**: `doc.document`, `doc.objects`,
214
+ `getObject`, `listObjects()` return deep clones — mutating them does nothing.
215
+ - **New documents are layout-less**; `exportPdf` of a layout-less document
216
+ falls back to one auto-fitted A3 page.
217
+ - `transaction(fn)` groups edits into one undo step. `array(ids, count,
218
+ delta)`: `count` INCLUDES the originals; the returned ids are the copies.
219
+ - An arc with `startAngle === endAngle` renders as a full circle.
220
+ - There is no rectangle type — use a closed 4-point polyline.
221
+
222
+ ### API surface
223
+
224
+ `newDrawing(name?)` / `drawingFromJson(text)` / `openDrawing(path)` → `Drawing`:
225
+
226
+ - **Name**: `drawing.name` is the file name (set on `openDrawing`, updated
227
+ by `save(path)`); `newDrawing(name)` names it until it is saved. The
228
+ `.linea` JSON itself carries no name.
229
+
230
+ - **Read**: `summary()`, `objects`, `layers`, `layouts`, `getObject`,
231
+ `listObjects({ids, type, layer, color, lineType, content, bbox, inside, near, layout})`,
232
+ `measure(id)` (`bboxOf(ids)`, `length(id)`, `area(id)`), `listLayers()`,
233
+ `listLayouts()`, `listTextStyles()`, `listDimStyles()`,
234
+ `listBlockDefinitions()`, `listUserCommands()`, `revision`
235
+ - **Edit**: `add(objects, {layout?})`, `update(ids, patch)` or
236
+ `update([{id, ...patch}])`, `remove(ids)` (→ count), `reorder(ids,
237
+ "front" | "back" | {above} | {below})`, `transaction(fn)` (sync or async;
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)`,
245
+ `await setBlockDefinitions(sources)`, `await setCommands(sources)`
246
+ - **Scripts**: `await runScript(source, inputs?, params?)` runs a
247
+ `defineCommand` module (the same one the editor palette runs) headlessly
248
+ as ONE undo step — its declared `inputs` (points) are supplied up front,
249
+ its declared `params` by name (`{ count: 3 }`; omitted ones take their
250
+ defaults, unknown names and wrong types throw); `runUserCommand(id, inputs?,
251
+ params?)` runs one of the document's own commands. Returns the script's
252
+ return value.
253
+ - **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
254
+ `removeLayer(nameOrId)` (must be empty and not current),
255
+ `reorderLayers(order)`, `setCurrentLayer(nameOrId)`
256
+ - **Assets**: `addAsset(dataUrl) → assetId` (for `image` objects;
257
+ content-keyed), `removeAsset(id)` (refused while referenced)
258
+ - **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
259
+ - **Layouts**: `addLayout(name, {sheet, scale, viewport, index})`,
260
+ `updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
261
+ `reorderLayouts`, `parseScale("1:50")`. `sheet` is `"A3"` (landscape when
262
+ creating, the current orientation when patching), `{ name: "A3",
263
+ orientation: "portrait" }`, or `{ width, height }` in mm.
264
+ - **Export**: `renderSvg`/`exportSvg`, `renderPng`/`exportPng`, `await toDxf()`/`exportDxf`,
265
+ `toDwg`/`exportDwg` (DWG R2000–2018, default AC1027), `renderPdf`/`exportPdf`,
266
+ `toLinea`/`save`
267
+ - **Import (merge)**: `importDxf(path)`/`importDxfText(text)` (with
268
+ `unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
269
+ `importPdfData(arrayBuffer)`
270
+
271
+ Full parameter docs are in the bundled `dist/index.d.ts` JSDoc.
272
+
273
+ ### Object types
274
+
275
+ `line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
276
+ `arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
277
+ `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
278
+ `text` (position, content; **default alignment center/center**; optional
279
+ leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
280
+ radial/diameter; `offset` is a vector from points[0] to the dimension line;
281
+ linear with >2 points = chain) · `block` (instances of script definitions) ·
282
+ `viewport` (layouts only) · `image` (placement of an asset).
283
+
284
+ ### Block scripts
285
+
286
+ `document.blockDefinitions` is an array of ES-module **source strings**
287
+ (`await doc.setBlockDefinitions([...sources])` — async so the script
288
+ transpiler is loaded and block geometry is definitive when it resolves).
289
+ A block is one `defineBlock` call:
290
+
291
+ ```ts
292
+ import { defineBlock } from "lineadraw";
293
+ import { polar, add } from "lineadraw/helpers"; // vector-math helpers
294
+
295
+ export default defineBlock({
296
+ id: "chair", // instances reference this
297
+ name: "Chair",
298
+ params: [{ name: "width", type: "number", default: 450 }],
299
+ draw: ({ params }) => [
300
+ // Plain object DTOs in block-local coordinates; params.width: number.
301
+ { type: "line", a: { x: 0, y: 0 }, b: { x: params.width, y: 0 } },
302
+ ],
303
+ });
304
+ ```
305
+
306
+ Optional members: `place` — an array of point labels (one pick per label)
307
+ or an interactive `({ params, pickPoint, pickObject })` function run once
308
+ at insertion (default is one insertion point) — and
309
+ `paramVisibility({ params })` (hides property rows). `params` may be the
310
+ table literal or a zero-arg function returning it. Imports are limited to
311
+ `"lineadraw"` and `"lineadraw/helpers"`. The instance's first point input
312
+ is its pivot: `draw` sees inputs localized to it, and instance
313
+ `rotation`/`scale` apply about it. (Plain named exports — incl. the
314
+ pre-rename `main`/`defineInput` names — remain evaluatable as the frozen
315
+ wire form.)
316
+
317
+ ### Interchange notes
318
+
319
+ - DXF/DWG export flattens semantics: block instances are exported as their
320
+ evaluated geometry (no `INSERT` reuse) and `image` objects are skipped
321
+ (DXF images reference external files). The export result reports both in
322
+ `skipped`.
323
+ - PDF import extracts vector strokes and text runs (dimensions arrive as
324
+ exploded geometry). pdf.js loads lazily on first use, as do the PNG
325
+ rasterizer (native resvg) and the DWG transcoder (bundled WebAssembly).
326
+ - `renderSvg({ bbox })` culls objects outside the box (they are absent from the
327
+ file, not merely outside the viewBox).
328
+ - Opening a document written by a NEWER Linea version throws instead of
329
+ loading lossily.
330
+ - Dimensions validate their point counts per kind (linear ≥ 2, angular ≥ 3,
331
+ radial/diameter ≥ 2).
332
+
122
333
  ## Ecosystem
123
334
 
124
335
  - [Linea editor](https://lineadraw.com) — the browser CAD app that runs
@@ -126,8 +337,5 @@ the file.
126
337
  - [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
127
338
  public collection: real blocks/commands to learn from, agent skill, and
128
339
  guides.
129
- - [`@lineadraw/sdk`](https://www.npmjs.com/package/@lineadraw/sdk) —
130
- headless Node API over the same kernel (open/edit/render/export
131
- `.linea` documents; also powers the CLI's block dry-runs).
132
340
  - [`@lineadraw/mcp`](https://www.npmjs.com/package/@lineadraw/mcp) — MCP
133
341
  server with the full editor as an in-chat app.
package/bin/lineadraw.mjs CHANGED
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // The `lineadraw` CLI — marketplace tooling for Linea block/command repos.
3
- // lineadraw manifest [--dir <path>] generate .lineadraw/marketplace.json
4
- // lineadraw check [--dir <path>] validate + fail if the manifest is stale
5
- // lineadraw init [--dir <path>] add the marketplace structure here
6
- // See docs/marketplace-devx.md in the linea repo for the design.
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.
7
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
8
14
  import { dirname, join, resolve } from "node:path";
9
15
  import {
10
16
  buildManifest,
@@ -15,22 +21,182 @@ import {
15
21
  } from "../lib/manifest.mjs";
16
22
  import { init, tsconfigLibWarning } from "../lib/init.mjs";
17
23
 
18
- const USAGE = `Usage: lineadraw <manifest|check|init> [--dir <path>]
24
+ const USAGE = `Usage: lineadraw <command> [options]
19
25
 
20
- manifest Validate all modules and write ${MANIFEST_PATH}
21
- check Validate without writing; fail if the committed manifest is stale
22
- init Add the marketplace structure to the current npm project
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
23
30
 
24
- With @lineadraw/sdk installed (devDependency), manifest and check also
25
- dry-run every block and bake preview thumbnails into the catalog — commit
26
- the manifest from an environment with the sdk installed (the CI template
27
- does) so check compares like with like.`;
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.`;
28
44
 
29
45
  const args = process.argv.slice(2);
30
46
  const command = args[0];
31
47
  const dirFlag = args.indexOf("--dir");
32
48
  const dir = resolve(dirFlag === -1 ? "." : (args[dirFlag + 1] ?? "."));
33
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
+
34
200
  const fail = (message) => {
35
201
  console.error(message);
36
202
  process.exit(1);
@@ -41,10 +207,10 @@ const reportProblems = (problems) => {
41
207
  console.error(`REJECTED ${p.file}: ${p.problems.join("; ")}`);
42
208
  };
43
209
 
44
- /** Validate everything (light + optional sdk dry-run); exits on failure. */
210
+ /** Validate everything (light + sdk dry-run); exits on failure. */
45
211
  const validatedManifest = async () => {
46
212
  const { manifest: base, sources, problems } = buildManifest(dir);
47
- const sdkResult = await verifyWithSdk(dir, base, sources);
213
+ const sdkResult = await verifyWithSdk(base, sources);
48
214
  const all = [...problems, ...sdkResult.problems];
49
215
  if (all.length > 0) {
50
216
  reportProblems(all);
@@ -53,11 +219,7 @@ const validatedManifest = async () => {
53
219
  const manifest = withThumbnails(base, sdkResult.thumbnails);
54
220
  if (!sdkResult.ran)
55
221
  console.warn(
56
- sdkResult.reason === "load-failed"
57
- ? `Blocks were not dry-run: @lineadraw/sdk is installed but failed to load (${sdkResult.detail}). Try reinstalling it.`
58
- : sdkResult.reason === "too-old"
59
- ? "Blocks were not dry-run: the installed @lineadraw/sdk is too old for module validation. Update it to enable this check."
60
- : "Blocks were not dry-run because @lineadraw/sdk is not installed. Install it as a devDependency to enable this check.",
222
+ `Blocks were not dry-run: lineadraw/sdk failed to load (${sdkResult.detail}). Try reinstalling lineadraw.`,
61
223
  );
62
224
  return manifest;
63
225
  };
@@ -85,9 +247,7 @@ switch (command) {
85
247
  );
86
248
  const libWarning = tsconfigLibWarning(dir);
87
249
  if (libWarning !== null) console.warn(`warning: ${libWarning}`);
88
- console.log(
89
- `OK: ${manifest.entries.length} entries, manifest is current.`,
90
- );
250
+ console.log(`OK: ${manifest.entries.length} entries, manifest is current.`);
91
251
  break;
92
252
  }
93
253
  case "init": {
@@ -104,6 +264,14 @@ switch (command) {
104
264
  }
105
265
  break;
106
266
  }
107
- default:
108
- fail(USAGE);
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
+ }
109
277
  }