lineadraw 0.8.0 → 0.9.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,178 +1,29 @@
1
1
  # lineadraw
2
2
 
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.
3
+ [Linea](https://lineadraw.com) for Node — one package, four jobs:
6
4
 
7
- - **Typings** — `lineadraw.d.ts` makes `import { defineBlock } from
8
- "lineadraw"` and `import { … } from "lineadraw/helpers"` type-check in
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)).
13
- - **CLI** — `lineadraw manifest | check | init` turns any repository into
14
- a Linea **marketplace**: a git repo the editor can browse and install
15
- blocks/commands from.
16
-
17
- ## Quick start
18
-
19
- Scaffold a fresh marketplace repository:
20
-
21
- ```bash
22
- npm create lineadraw my-blocks
23
- ```
24
-
25
- Or add the structure to an existing project:
5
+ | Job | What you get | Entry point |
6
+ | --- | --- | --- |
7
+ | [**Drawing SDK**](#1-drawing-sdk) | Open, create, edit, render and export `.linea` / DXF / DWG / PDF / SVG / PNG headlessly — for scripts, agents and batch tools. Also drives drawings open in the desktop app. | `import { openDrawing } from "lineadraw/sdk"` |
8
+ | [**Extension development**](#2-extension-development) | Typings for parametric **blocks** and macro **commands**: `defineBlock` / `defineCommand` type-check in any editor, with parameter types inferred. | `import { defineBlock } from "lineadraw"` |
9
+ | [**Marketplace management**](#3-marketplace-management) | Turn a git repo into a Linea **marketplace** the editor can browse and install from: scaffold it, validate every module, generate the catalog, check it in CI. | `lineadraw init` · `manifest` · `check` |
10
+ | [**CLI for the desktop app**](#4-cli-for-the-running-desktop-app) | Talk to the running Linea desktop app from a terminal: list open drawings, open and close files, run any `linea_*` operation. | `lineadraw status` · `open` · `close` · `call` · `tools` |
26
11
 
27
12
  ```bash
28
- npm i -D lineadraw
29
- npx lineadraw init
30
- ```
31
-
32
- Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
33
- wired to the typings, a sample block, a CI workflow, and a generated
34
- catalog (`.lineadraw/marketplace.json`) — the repo is addable in the
35
- editor from the first push.
36
-
37
- ## Writing a block
38
-
39
- A block is one module with a single `defineBlock` default export:
40
- typed parameters in, geometry out.
41
-
42
- ```ts
43
- // blocks/surface.block.ts
44
- import { defineBlock } from "lineadraw";
45
-
46
- export default defineBlock({
47
- // The id is the block's identity everywhere: @<collection>/<slug>.
48
- // Re-publishing the same id updates installed copies in place.
49
- id: "@my-blocks/surface",
50
- name: "Surface",
51
- version: "1.0.0",
52
- params: [
53
- { name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
54
- ],
55
- place: ["Start point", "End point"], // one point pick per label, in order
56
- draw: ({ params, inputs: [start, end] }) => {
57
- params.kind; // "rock" | "ground" — inferred from the params table
58
- return [{ type: "line", a: start, b: end }];
59
- },
60
- });
13
+ npm i lineadraw # the SDK in a script or tool
14
+ npm i -D lineadraw # typings + CLI in a block/command repository
15
+ npm create lineadraw my-blocks # a fresh marketplace repository
61
16
  ```
62
17
 
63
- Notable optional members: `place` may instead be an interactive
64
- `({ params, pickPoint, pickObject })` function run once at insertion;
65
- `dependencies` lists ids of blocks your `draw` emits instances of (installs
66
- pull them along); `paramVisibility({ params })` hides property rows;
67
- `previewInputs` curates the inputs used for thumbnails. Units are
68
- millimeters, Y-up, angles in radians CCW.
69
-
70
- Parameter types (the same rows serve a command's `params`):
18
+ Requires Node.js >= 20. Units everywhere are millimeters, Y-up, angles in
19
+ radians CCW.
71
20
 
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
-
97
- ## Writing a command
98
-
99
- A command is a macro with a single `defineCommand` default export. Its
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.
104
-
105
- ```ts
106
- // commands/circle-here.cmd.ts
107
- import { defineCommand } from "lineadraw";
108
-
109
- export default defineCommand({
110
- id: "@my-blocks/circle-here",
111
- name: "Circle here",
112
- run: async ({ document, pickPoint, prompt, showToast }) => {
113
- const center = await pickPoint("Center point");
114
- const r = await prompt("Radius");
115
- document.add([{ type: "circle", center, radius: Number.parseFloat(r) }]);
116
- showToast("Done", "success");
117
- },
118
- });
119
- ```
120
-
121
- Imports are limited to `"lineadraw"` and `"lineadraw/helpers"` (geometry
122
- helpers) — the editor resolves both when executing your module.
123
-
124
- ## The CLI
125
-
126
- ```
127
- lineadraw manifest Validate all modules and write .lineadraw/marketplace.json
128
- lineadraw check Validate without writing; fail if the committed manifest is stale
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
136
- ```
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
-
143
- The manifest is deterministic (repo-relative paths, no git metadata), so
144
- `check` compares the parsed JSON (formatting, a trailing newline or CRLF
145
- line endings never make it stale) — the included CI workflow runs it on every
146
- push and PR. `manifest`/`check` also **dry-run every block** through
147
- `lineadraw/sdk` and render thumbnails.
148
-
149
- Collection name and owner come from your package.json:
150
-
151
- ```json
152
- { "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
153
- ```
154
-
155
- ## Using a marketplace in the editor
156
-
157
- In [the editor](https://lineadraw.com): **Library → Marketplace tab → the
158
- gear button (Manage marketplaces…) → Add a marketplace** and enter
159
- `owner/repo` (or `owner/repo@branch`). For a private repository,
160
- create a fine-grained personal access token with read-only Contents access
161
- to just that repository and paste it in the add dialog — it is stored only
162
- on that device. Installing embeds the module (and its declared dependency
163
- closure) in the document, so drawings keep working offline and travel with
164
- the file.
165
-
166
- ## Headless SDK (`lineadraw/sdk`)
21
+ ## 1. Drawing SDK
167
22
 
168
23
  A script-friendly API over the Linea CAD kernel: open, create, query, edit,
169
24
  render, and import/export `.linea` documents headlessly in Node — no browser,
170
25
  no server. Built for agents, CLIs, and batch tooling.
171
26
 
172
- ```bash
173
- npm i lineadraw
174
- ```
175
-
176
27
  ```js
177
28
  import { newDrawing, openDrawing } from "lineadraw/sdk";
178
29
 
@@ -243,8 +94,7 @@ appended when missing):
243
94
  a throw rolls back), `undo()`, `redo()`, `applyOps(ops)`, `ready()`
244
95
  - **Styles** (by name or id): `listTextStyles()`, `addTextStyle({name, …})`,
245
96
  `updateTextStyle(nameOrId, patch)`, `removeTextStyle(nameOrId)` (refused
246
- while objects use it) — the same four for `DimStyle`; `setTextStyle` /
247
- `setDimStyle` remain as deprecated aliases of update
97
+ while objects use it) — the same four for `DimStyle`
248
98
  - **Line types / hatch patterns** (the document's own definitions, by
249
99
  name, resolved ahead of the built-in catalogue): `listLineTypes()`,
250
100
  `addLineTypes([{name, dashesMm}])`, `updateLineType(name, patch)`,
@@ -271,8 +121,10 @@ appended when missing):
271
121
  - **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
272
122
  `removeLayer(nameOrId)` (must be empty and not current),
273
123
  `reorderLayers(order)`, `setCurrentLayer(nameOrId)`
274
- - **Assets**: `addAsset(dataUrl) → assetId` (for `image` objects;
275
- content-keyed), `removeAsset(id)` (refused while referenced)
124
+ - **Assets**: `addAsset(dataUrl) → assetId` (content-keyed; an embedded
125
+ reference names it), `listAssets()`, `removeAsset(id)` (refused while
126
+ referenced)
127
+ - **Settings**: `updateSettings(patch)` (document settings, e.g. units)
276
128
  - **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
277
129
  - **Layouts**: `addLayout(name, {sheet, scale, viewport, index})`,
278
130
  `updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
@@ -286,13 +138,13 @@ appended when missing):
286
138
  `unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
287
139
  `importPdfData(arrayBuffer)`
288
140
 
289
- Full parameter docs are in the bundled `dist/index.d.ts` JSDoc.
141
+ Full parameter docs are in the bundled `dist/sdk.d.ts` JSDoc.
290
142
 
291
143
  ### Object types
292
144
 
293
145
  `line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
294
146
  `arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
295
- `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
147
+ `point` (p) · `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
296
148
  `text` (position, content; **default alignment center/center**; optional
297
149
  leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
298
150
  radial/diameter; linear: `offset` is the signed distance of the dimension
@@ -301,46 +153,70 @@ line from points[0] (left of the measuring direction positive) and
301
153
  signed arc radius; `labels[i]` place and word each segment's text —
302
154
  `along`/`across` in mm, unset = automatic, `content` with `{value}` for the
303
155
  measurement; linear with >2 points = chain) · `block` (instances of script definitions) ·
304
- `viewport` (layouts only) · `image` (placement of an asset).
156
+ `table` (columns, rows, cells) · `comment` (a review bubble, never
157
+ printed) · `reference` (a linked or embedded file: `.linea`, DXF, DWG,
158
+ PDF, PNG/JPEG — a picture is a reference whose `source` is
159
+ `{ kind: "asset", assetId }`) · `viewport` (layouts only).
160
+
161
+ ### Block definitions in a document
305
162
 
306
- ### Block scripts
163
+ `document.blockDefinitions` is an array of ES-module **source strings** —
164
+ the same `defineBlock` modules described under
165
+ [Extension development](#2-extension-development). Load them with
166
+ `await doc.setBlockDefinitions([...sources])` or `addBlockDefinitions`
167
+ (async, so the script transpiler is loaded and block geometry is
168
+ definitive when it resolves), then place instances with
169
+ `add([{ type: "block", definitionId }])`. An instance's first point input
170
+ is its pivot: `draw` sees inputs localized to it, and the instance's
171
+ `rotation`/`scale` apply about it.
307
172
 
308
- `document.blockDefinitions` is an array of ES-module **source strings**
309
- (`await doc.setBlockDefinitions([...sources])` — async so the script
310
- transpiler is loaded and block geometry is definitive when it resolves).
311
- A block is one `defineBlock` call:
173
+ ### Running editors (the desktop bridge)
174
+
175
+ `Drawing` works on a document in your own process. To work on a drawing
176
+ that is OPEN in the Linea desktop app (or a web tab paired with it), connect
177
+ to the app's bridge on `127.0.0.1:7431`:
312
178
 
313
179
  ```ts
314
- import { defineBlock } from "lineadraw";
315
- import { polar, add } from "lineadraw/helpers"; // vector-math helpers
180
+ import { connectEditors } from "lineadraw/sdk";
316
181
 
317
- export default defineBlock({
318
- id: "chair", // instances reference this
319
- name: "Chair",
320
- params: [{ name: "width", type: "number", default: 450 }],
321
- draw: ({ params }) => [
322
- // Plain object DTOs in block-local coordinates; params.width: number.
323
- { type: "line", a: { x: 0, y: 0 }, b: { x: params.width, y: 0 } },
324
- ],
182
+ const linea = await connectEditors(); // port + agent token from the app's bridge.json
183
+ const [first] = await linea.editors(); // { id, name, path, kind, readOnly, dirty }
184
+ const plan = linea.editor(first.id); // by document id, file path or unique name
185
+
186
+ // One linea_* operation = one undo step in that editor.
187
+ await plan.call("linea_add_objects", {
188
+ objects: [{ type: "circle", center: [0, 0], radius: 500 }],
325
189
  });
190
+
191
+ // A whole command module, run INSIDE the editor against the full Drawing
192
+ // API: one round trip, one undo step. `inputs` / `params` answer the
193
+ // module's declared ones.
194
+ const result = await plan.runScript(
195
+ `import { defineCommand } from "lineadraw";
196
+ export default defineCommand({
197
+ id: "count-lines",
198
+ name: "Count lines",
199
+ run: ({ document }) => document.listObjects({ type: "line" }).length,
200
+ });`,
201
+ );
202
+
203
+ const other = await linea.open("details.linea"); // a window of its own
204
+ await linea.editor(other.id).close({ discard: true });
326
205
  ```
327
206
 
328
- Optional members: `place` — an array of point labels (one pick per label)
329
- or an interactive `({ params, pickPoint, pickObject })` function run once
330
- at insertion (default is one insertion point) — and
331
- `paramVisibility({ params })` (hides property rows). `params` may be the
332
- table literal or a zero-arg function returning it. Imports are limited to
333
- `"lineadraw"` and `"lineadraw/helpers"`. The instance's first point input
334
- is its pivot: `draw` sees inputs localized to it, and instance
335
- `rotation`/`scale` apply about it. (Plain named exports — incl. the
336
- pre-rename `main`/`defineInput` names — remain evaluatable as the frozen
337
- wire form.)
207
+ The operations are the same `linea_*` catalog the MCP server and the CLI
208
+ offer (`await linea.tools()` lists them). Failures throw a `BridgeError`
209
+ whose `code` says why: `unreachable` (the app isn't running),
210
+ `ambiguous` (several editors are open and none was named — `details`
211
+ lists them), `noSuchEditor`, `readOnly`, `validation`… The connection
212
+ options (`port`, `token`, `bridgeFile`) override what bridge.json says;
213
+ `LINEA_BRIDGE_TOKEN` supplies a token too.
338
214
 
339
215
  ### Interchange notes
340
216
 
341
217
  - DXF/DWG export flattens semantics: block instances are exported as their
342
- evaluated geometry (no `INSERT` reuse) and `image` objects are skipped
343
- (DXF images reference external files). The export result reports both in
218
+ evaluated geometry (no `INSERT` reuse), and embedded references are
219
+ skipped (DXF can't embed a file). The export result reports both in
344
220
  `skipped`.
345
221
  - PDF import extracts vector strokes and text runs (dimensions arrive as
346
222
  exploded geometry). pdf.js loads lazily on first use, as do the PNG
@@ -352,10 +228,185 @@ wire form.)
352
228
  - Dimensions validate their point counts per kind (linear ≥ 2, angular ≥ 3,
353
229
  radial/diameter ≥ 2).
354
230
 
231
+ ## 2. Extension development
232
+
233
+ Blocks and commands are plain TypeScript (or JavaScript) modules. The
234
+ package's `lineadraw.d.ts` types the two module names a script may import —
235
+ `"lineadraw"` (`defineBlock`, `defineCommand`) and `"lineadraw/helpers"`
236
+ (geometry helpers) — so authoring type-checks in VS Code, any other
237
+ TypeScript-aware editor, and CI. The editor resolves both names when it
238
+ runs your module; nothing else can be imported. `lineadraw init` writes a
239
+ `tsconfig.json` wired to the typings.
240
+
241
+ A module works on its own too: drop a `.block.ts` / `.cmd.ts` file onto the
242
+ editor, or exercise it headlessly with the SDK
243
+ (`await doc.addBlockDefinitions([source])`, `await doc.runScript(source)`).
244
+
245
+ ### Writing a block
246
+
247
+ A block is one module with a single `defineBlock` default export:
248
+ typed parameters in, geometry out.
249
+
250
+ ```ts
251
+ // blocks/surface.block.ts
252
+ import { defineBlock } from "lineadraw";
253
+
254
+ export default defineBlock({
255
+ // The id is the block's identity everywhere: @<collection>/<slug>.
256
+ // Re-publishing the same id updates installed copies in place.
257
+ id: "@my-blocks/surface",
258
+ name: "Surface",
259
+ version: "1.0.0",
260
+ params: [
261
+ { name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
262
+ ],
263
+ place: ["Start point", "End point"], // one point pick per label, in order
264
+ draw: ({ params, inputs: [start, end] }) => {
265
+ params.kind; // "rock" | "ground" — inferred from the params table
266
+ return [{ type: "line", a: start, b: end }];
267
+ },
268
+ });
269
+ ```
270
+
271
+ Notable optional members: `place` may instead be an interactive
272
+ `({ params, pickPoint, pickObject })` function run once at insertion;
273
+ `dependencies` lists ids of blocks your `draw` emits instances of (installs
274
+ pull them along); `paramVisibility({ params })` hides property rows;
275
+ `previewInputs` curates the inputs used for thumbnails. Units are
276
+ millimeters, Y-up, angles in radians CCW.
277
+
278
+ Parameter types (the same rows serve a command's `params`):
279
+
280
+ - `number` — `min`/`max`, `integer`, `step`, `prefix`/`suffix` ("1:",
281
+ "mm"), `presets` (standard values in the field's dropdown).
282
+ - `string` — `placeholder`, `multiline`, `presets`.
283
+ - `boolean` (`display: "toggle"` — a toggle button showing the label,
284
+ compact in a row), `enum` (`options`, each optionally with a `swatch:
285
+ "#rrggbb"` color square; `display: "buttons"` — a segmented toggle group
286
+ for short choices, falling back to a dropdown when it doesn't fit),
287
+ `color` (`"#rrggbb"`, `presets` = a palette).
288
+ - `numberList` (`number[]`, typed Tekla-style: `"0 2*7200"` →
289
+ `[0, 7200, 7200]`) and `stringList` (`string[]`: `'A "B 2"'` →
290
+ `["A", "B 2"]`), both with `presets` and `placeholder` (shown while
291
+ empty).
292
+ - Pickers over the document's resources, valued by id / name:
293
+ `textStyle`, `dimStyle`, `font`, `lineType`, `hatchPattern`.
294
+ - `label` — a section heading (no value); `row` — 2–3 compact params on one
295
+ panel row (`items`, each label a short prefix like "W"); its items are
296
+ ordinary flat params (`params.w`).
297
+
298
+ Every param may carry a `hint` (the label's tooltip). An instance that
299
+ overrides a param shows "Reset" beside it in Properties, back to your
300
+ default. `draw` also receives
301
+ `styles`: `styles.text(id)` / `styles.dim(id)` give a document style (an
302
+ empty or unknown id → the first), for blocks that lay out with real text
303
+ sizes.
304
+
305
+ ### Writing a command
306
+
307
+ A command is a macro with a single `defineCommand` default export. Its
308
+ whole run is one undo step; Esc during a pick reverts cleanly. A
309
+ `params` table (the same rows a block's `params` uses) is asked for in a
310
+ dialog before the run and arrives typed as `ctx.params`; headless callers
311
+ pass it by name.
312
+
313
+ ```ts
314
+ // commands/circle-here.cmd.ts
315
+ import { defineCommand } from "lineadraw";
316
+
317
+ export default defineCommand({
318
+ id: "@my-blocks/circle-here",
319
+ name: "Circle here",
320
+ inputs: ["Center point"],
321
+ params: [{ name: "radius", type: "number", default: 10, min: 0.1 }],
322
+ // `inputs` are picked in the editor (or passed by a headless caller),
323
+ // `params` asked for in a dialog; `editor` is absent when headless.
324
+ run: async ({ document, editor, inputs: [center], params }) => {
325
+ document.add([{ type: "circle", center, radius: params.radius }]);
326
+ editor?.toast("Done", "success");
327
+ },
328
+ });
329
+ ```
330
+
331
+ ## 3. Marketplace management
332
+
333
+ A marketplace is a git repository of blocks and commands plus a generated
334
+ catalog (`.lineadraw/marketplace.json`) that the editor reads.
335
+
336
+ Scaffold a fresh one:
337
+
338
+ ```bash
339
+ npm create lineadraw my-blocks
340
+ ```
341
+
342
+ Or add the structure to an existing npm project:
343
+
344
+ ```bash
345
+ npm i -D lineadraw
346
+ npx lineadraw init
347
+ ```
348
+
349
+ Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
350
+ wired to the typings, a sample block, a CI workflow, the catalog, and
351
+ `npm run manifest` / `npm run check` scripts — the repo is addable in the
352
+ editor from the first push.
353
+
354
+ ```
355
+ lineadraw manifest [--dir <path>] Validate all modules and write .lineadraw/marketplace.json
356
+ lineadraw check [--dir <path>] Validate without writing; fail if the committed manifest is stale
357
+ lineadraw init [--dir <path>] Add the marketplace structure to the current npm project
358
+ ```
359
+
360
+ `manifest` and `check` **dry-run every block** through `lineadraw/sdk` and
361
+ bake preview thumbnails into the catalog. The manifest is deterministic
362
+ (repo-relative paths, no git metadata), so `check` compares the parsed JSON
363
+ — formatting, a trailing newline or CRLF line endings never make it stale.
364
+ The included CI workflow runs it on every push and PR.
365
+
366
+ Collection name and owner come from your package.json:
367
+
368
+ ```json
369
+ { "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
370
+ ```
371
+
372
+ ### Using a marketplace in the editor
373
+
374
+ In [the editor](https://lineadraw.com): **Library → Marketplace tab → the
375
+ gear button (Manage marketplaces…) → Add a marketplace** and enter
376
+ `owner/repo` (or `owner/repo@branch`). For a private repository,
377
+ create a fine-grained personal access token with read-only Contents access
378
+ to just that repository and paste it in the add dialog — it is stored only
379
+ on that device. Installing embeds the module (and its declared dependency
380
+ closure) in the document, so drawings keep working offline and travel with
381
+ the file.
382
+
383
+ ## 4. CLI for the running desktop app
384
+
385
+ ```
386
+ lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
387
+ lineadraw open <file.linea> Open a file in a new window
388
+ lineadraw close [--editor <x>] [--discard]
389
+ lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* operation
390
+ lineadraw tools The operations the bridge offers
391
+ ```
392
+
393
+ These commands talk to the running Linea desktop app on `127.0.0.1:7431`
394
+ (the app's `bridge.json` supplies the port and the agent token; `--port`,
395
+ `--token` and `LINEA_BRIDGE_TOKEN` override). `--editor` names a drawing by
396
+ document id, file path or unique name, and may be left out while only one
397
+ is open. Each `call` is one undo step in that editor. From code, use the
398
+ SDK's [`connectEditors()`](#running-editors-the-desktop-bridge).
399
+
400
+ ```bash
401
+ npx lineadraw call linea_add_objects '{"objects":[{"type":"circle","center":[0,0],"radius":500}]}'
402
+ ```
403
+
355
404
  ## Ecosystem
356
405
 
357
- - [Linea editor](https://lineadraw.com) — the browser CAD app that runs
358
- these modules.
406
+ - [Linea](https://lineadraw.com) — the CAD editor, on the desktop and on
407
+ the web, that runs these modules.
408
+ - [`create-lineadraw`](https://www.npmjs.com/package/create-lineadraw) —
409
+ `npm create lineadraw`, the marketplace scaffolder.
359
410
  - [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
360
411
  public collection: real blocks/commands to learn from, agent skill, and
361
412
  guides.
package/bin/lineadraw.mjs CHANGED
@@ -10,7 +10,6 @@
10
10
  // `call`, `tools`. Reads the desktop's bridge.json for the port
11
11
  // and the agent token; `--port` / `--token` / LINEA_BRIDGE_TOKEN override.
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { homedir } from "node:os";
14
13
  import { isDeepStrictEqual } from "node:util";
15
14
  import { dirname, join, resolve } from "node:path";
16
15
  import {
@@ -67,130 +66,36 @@ const parseArgs = (argv) => {
67
66
  };
68
67
 
69
68
  // --- 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
- : "";
69
+ // The client is lineadraw/sdk's (src/bridge.ts), built on its own as
70
+ // dist/bridge.js so these commands never load the kernel.
151
71
 
152
72
  const bridgeCommand = async (cmd, positional, flags) => {
153
- const b = bridge(flags);
73
+ const { createBridge } = await import("../dist/bridge.js");
74
+ const b = createBridge({
75
+ port: typeof flags.port === "string" ? Number(flags.port) : undefined,
76
+ token: typeof flags.token === "string" ? flags.token : undefined,
77
+ });
78
+ const editor = typeof flags.editor === "string" ? flags.editor : undefined;
154
79
  switch (cmd) {
155
80
  case "status":
156
- return request(b, "GET", "/status");
81
+ return b.status();
157
82
  case "open": {
158
83
  const file = positional[0];
159
84
  if (!file) throw new Error("open needs a .linea file path");
160
- return request(b, "POST", "/api/linea_open_editor", {
161
- path: resolve(file),
162
- });
85
+ return b.open(file);
163
86
  }
164
87
  case "close":
165
- return request(
166
- b,
167
- "POST",
168
- `/api/linea_close_document${editorQuery(flags)}`,
169
- { discard: flags.discard === true },
170
- );
88
+ return b.editor(editor).close({ discard: flags.discard === true });
171
89
  case "call": {
172
90
  const [operation, json] = positional;
173
91
  if (!operation)
174
92
  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",
93
+ return b.call(operation, json === undefined ? {} : JSON.parse(json), {
94
+ editor,
188
95
  });
189
- return (reply.result?.tools ?? []).map((t) => ({
190
- name: t.name,
191
- description: t.description,
192
- }));
193
96
  }
97
+ case "tools":
98
+ return b.tools();
194
99
  default:
195
100
  throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
196
101
  }