lineadraw 0.4.0 → 0.6.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 +218 -13
- package/bin/lineadraw.mjs +277 -97
- package/dist/chunk-L4FNH35X.js +80 -0
- package/dist/chunk-L4FNH35X.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/embedded-F5ZJYFHC.js +10 -0
- package/dist/embedded-F5ZJYFHC.js.map +1 -0
- package/dist/node-OWPSMNLK.js +255 -0
- package/dist/node-OWPSMNLK.js.map +1 -0
- package/dist/pkg/linea_dwg_bg.wasm +0 -0
- package/dist/sdk.d.ts +5816 -0
- package/dist/sdk.js +15756 -0
- package/dist/sdk.js.map +1 -0
- package/lib/init.mjs +42 -5
- package/lib/manifest.mjs +65 -27
- package/lineadraw.d.ts +1386 -167
- package/package.json +34 -6
- package/templates/README.md +18 -18
- package/templates/tsconfig.json +2 -0
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# lineadraw
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
blocks and commands
|
|
5
|
-
|
|
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,24 @@ 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 Is the Linea desktop app serving agents? port, open editors
|
|
132
|
+
lineadraw editors The open drawings: id, name, path, kind, dirty
|
|
133
|
+
lineadraw open <file.linea> Open a file in a new window
|
|
134
|
+
lineadraw close [--editor <x>] [--discard]
|
|
135
|
+
lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* tool
|
|
136
|
+
lineadraw tools The operations the bridge offers
|
|
98
137
|
```
|
|
99
138
|
|
|
139
|
+
The bridge commands talk to the running Linea desktop app on
|
|
140
|
+
`127.0.0.1:7431` (the app's `bridge.json` supplies the port and the agent
|
|
141
|
+
token; `--port`, `--token` and `LINEA_BRIDGE_TOKEN` override) — see
|
|
142
|
+
docs/desktop-bridge.md in the linea repo.
|
|
143
|
+
|
|
100
144
|
The manifest is deterministic (repo-relative paths, no git metadata), so
|
|
101
145
|
`check` is a byte-for-byte diff — the included CI workflow runs it on every
|
|
102
|
-
push and PR.
|
|
103
|
-
`
|
|
104
|
-
thumbnails; without it the CLI still fully works.
|
|
146
|
+
push and PR. `manifest`/`check` also **dry-run every block** through
|
|
147
|
+
`lineadraw/sdk` and render thumbnails.
|
|
105
148
|
|
|
106
149
|
Collection name and owner come from your package.json:
|
|
107
150
|
|
|
@@ -111,14 +154,179 @@ Collection name and owner come from your package.json:
|
|
|
111
154
|
|
|
112
155
|
## Using a marketplace in the editor
|
|
113
156
|
|
|
114
|
-
In [the editor](https://lineadraw.com): **Library → Marketplace tab →
|
|
115
|
-
|
|
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,
|
|
116
160
|
create a fine-grained personal access token with read-only Contents access
|
|
117
161
|
to just that repository and paste it in the add dialog — it is stored only
|
|
118
162
|
on that device. Installing embeds the module (and its declared dependency
|
|
119
163
|
closure) in the document, so drawings keep working offline and travel with
|
|
120
164
|
the file.
|
|
121
165
|
|
|
166
|
+
## Headless SDK (`lineadraw/sdk`)
|
|
167
|
+
|
|
168
|
+
A script-friendly API over the Linea CAD kernel: open, create, query, edit,
|
|
169
|
+
render, and import/export `.linea` documents headlessly in Node — no browser,
|
|
170
|
+
no server. Built for agents, CLIs, and batch tooling.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm i lineadraw
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { newDrawing, openDrawing } from "lineadraw/sdk";
|
|
178
|
+
|
|
179
|
+
const doc = newDrawing();
|
|
180
|
+
const walls = doc.addLayer({ name: "walls", color: "#e8e8e8", lineWidth: 0.5 });
|
|
181
|
+
doc.add([
|
|
182
|
+
{ type: "polyline", layerId: walls, closed: true,
|
|
183
|
+
points: [[0, 0], [6000, 0], [6000, 3000], [0, 3000]] },
|
|
184
|
+
{ type: "dimension", points: [[0, 0], [6000, 0]], offset: [0, -500],
|
|
185
|
+
scale: 50 }, // annotations × drawing scale
|
|
186
|
+
]);
|
|
187
|
+
doc.createLayout("Plan 1:50", { sheet: "A3", viewport: { scale: "1:50" } });
|
|
188
|
+
await doc.exportPng("check.png", { width: 1600 }); // verify by LOOKING at it
|
|
189
|
+
await doc.save("plan.linea");
|
|
190
|
+
await doc.exportPdf("plan.pdf");
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Conventions (read once, avoid every common bug)
|
|
194
|
+
|
|
195
|
+
- **Units are millimeters, Y-up. Angles are radians CCW** — everywhere in the
|
|
196
|
+
object model and in `rotate()`. Draw 1:1 (a 6 m wall is 6000 mm); paper
|
|
197
|
+
scale lives in layout viewports (`"1:50"` strings via `parseScale`).
|
|
198
|
+
- **Never write `.linea` JSON by hand.** Every mutation goes through
|
|
199
|
+
kernel-validated ops (`add`/`update`/`remove`/transforms); invalid input
|
|
200
|
+
throws with the failing object and field named.
|
|
201
|
+
- **Style by layer**: unset `color`/`lineWidth`/`lineType` on objects inherit
|
|
202
|
+
from the layer. Layer names are unique (`addLayer` throws on duplicates)
|
|
203
|
+
and can be used instead of ids (`layer: "walls"`, query filters). Note:
|
|
204
|
+
`locked` layers are an editor-UI concept — the SDK does not block edits.
|
|
205
|
+
- **Layer `print: false`** (plot flag) keeps a layer visible on screen and in
|
|
206
|
+
SVG/PNG but excludes it from PDF output — construction/guide layers.
|
|
207
|
+
- **Opacity is byLayer too**: `opacity` (0..1) on a layer is the default for
|
|
208
|
+
its objects; an object's own `opacity` overrides it. Paints across canvas,
|
|
209
|
+
SVG, and PDF; DXF/DWG export drops it (no direct mapping).
|
|
210
|
+
- **Annotation scale is per-object**: for a 1:N drawing set the top-level
|
|
211
|
+
`scale: N` on text, dimensions, and hatches, keeping `textHeight` at
|
|
212
|
+
paper size (2.5 body / 3.5 / 5 titles). `scale` is NOT a `styleOverride`
|
|
213
|
+
key; `add`/`update` reject it there.
|
|
214
|
+
- **Reads are detached snapshots**: `doc.document`, `doc.objects`,
|
|
215
|
+
`getObject`, `query()` return deep clones — mutating them does nothing.
|
|
216
|
+
- **New documents are layout-less**; `exportPdf` of a layout-less document
|
|
217
|
+
falls back to one auto-fitted A3 page.
|
|
218
|
+
- `transaction(fn)` groups edits into one undo step. `array(ids, count,
|
|
219
|
+
delta)`: `count` INCLUDES the originals; the returned ids are the copies.
|
|
220
|
+
- An arc with `startAngle === endAngle` renders as a full circle.
|
|
221
|
+
- There is no rectangle type — use a closed 4-point polyline.
|
|
222
|
+
|
|
223
|
+
### API surface
|
|
224
|
+
|
|
225
|
+
`newDrawing(name?)` / `drawingFromJson(text)` / `openDrawing(path)` → `Drawing`:
|
|
226
|
+
|
|
227
|
+
- **Name**: `drawing.name` is the file name (set on `openDrawing`, updated
|
|
228
|
+
by `save(path)`); `newDrawing(name)` names it until it is saved. The
|
|
229
|
+
`.linea` JSON itself carries no name.
|
|
230
|
+
|
|
231
|
+
- **Read**: `summary()`, `objects`, `layers`, `layouts`, `getObject`,
|
|
232
|
+
`query({ids, type, layer, color, lineType, content, bbox, inside, near, layout})`,
|
|
233
|
+
`measure(id)` (`bboxOf(ids)`, `length(id)`, `area(id)`), `listLayers()`,
|
|
234
|
+
`listLayouts()`, `listBlocks()`, `listCommands()`, `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()`, `setDimStyle`,
|
|
239
|
+
`setTextStyle`, `applyOps(ops)`, `ready()`
|
|
240
|
+
- **Modules**: `await installBlocks(sources)` / `removeBlock(id)`,
|
|
241
|
+
`await installCommands(sources)` / `removeCommand(id)`,
|
|
242
|
+
`await setBlockDefinitions(sources)`, `await setCommands(sources)`
|
|
243
|
+
- **Scripts**: `await runScript(source, inputs?, params?)` runs a
|
|
244
|
+
`defineCommand` module (the same one the editor palette runs) headlessly
|
|
245
|
+
as ONE undo step — its declared `inputs` (points) are supplied up front,
|
|
246
|
+
its declared `params` by name (`{ count: 3 }`; omitted ones take their
|
|
247
|
+
defaults, unknown names and wrong types throw); `runCommand(id, inputs?,
|
|
248
|
+
params?)` runs one of the document's own commands. Returns the script's
|
|
249
|
+
return value.
|
|
250
|
+
- **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
|
|
251
|
+
`removeLayer(nameOrId)` (must be empty and not current),
|
|
252
|
+
`reorderLayers(order)`, `setCurrentLayer(nameOrId)`
|
|
253
|
+
- **Assets**: `addAsset(dataUrl) → assetId` (for `image` objects;
|
|
254
|
+
content-keyed), `removeAsset(id)` (refused while referenced)
|
|
255
|
+
- **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
|
|
256
|
+
- **Layouts**: `createLayout(name, {sheet, scale, viewport, index})`,
|
|
257
|
+
`updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
|
|
258
|
+
`reorderLayouts`, `parseScale("1:50")`. `sheet` is `"A3"` (landscape when
|
|
259
|
+
creating, the current orientation when patching), `{ name: "A3",
|
|
260
|
+
orientation: "portrait" }`, or `{ width, height }` in mm.
|
|
261
|
+
- **Export**: `renderSvg`/`exportSvg`, `renderPng`/`exportPng`, `await toDxf()`/`exportDxf`,
|
|
262
|
+
`toDwg`/`exportDwg` (DWG R2000–2018, default AC1027), `renderPdf`/`exportPdf`,
|
|
263
|
+
`toJson`/`save`
|
|
264
|
+
- **Import (merge)**: `importDxf(path)`/`importDxfText(text)` (with
|
|
265
|
+
`unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
|
|
266
|
+
`importPdfData(arrayBuffer)`
|
|
267
|
+
|
|
268
|
+
Full parameter docs are in the bundled `dist/index.d.ts` JSDoc.
|
|
269
|
+
|
|
270
|
+
### Object types
|
|
271
|
+
|
|
272
|
+
`line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
|
|
273
|
+
`arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
|
|
274
|
+
`hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
|
|
275
|
+
`text` (position, content; **default alignment center/center**; optional
|
|
276
|
+
leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
|
|
277
|
+
radial/diameter; `offset` is a vector from points[0] to the dimension line;
|
|
278
|
+
linear with >2 points = chain) · `block` (instances of script definitions) ·
|
|
279
|
+
`viewport` (layouts only) · `image` (placement of an asset).
|
|
280
|
+
|
|
281
|
+
### Block scripts
|
|
282
|
+
|
|
283
|
+
`document.blockDefinitions` is an array of ES-module **source strings**
|
|
284
|
+
(`await doc.setBlockDefinitions([...sources])` — async so the script
|
|
285
|
+
transpiler is loaded and block geometry is definitive when it resolves).
|
|
286
|
+
A block is one `defineBlock` call:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { defineBlock } from "lineadraw";
|
|
290
|
+
import { polar, add } from "lineadraw/helpers"; // vector-math helpers
|
|
291
|
+
|
|
292
|
+
export default defineBlock({
|
|
293
|
+
id: "chair", // instances reference this
|
|
294
|
+
name: "Chair",
|
|
295
|
+
params: [{ name: "width", type: "number", default: 450 }],
|
|
296
|
+
draw: ({ params }) => [
|
|
297
|
+
// Plain object DTOs in block-local coordinates; params.width: number.
|
|
298
|
+
{ type: "line", a: { x: 0, y: 0 }, b: { x: params.width, y: 0 } },
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Optional members: `place` — an array of point labels (one pick per label)
|
|
304
|
+
or an interactive `({ params, pickPoint, pickObject })` function run once
|
|
305
|
+
at insertion (default is one insertion point) — and
|
|
306
|
+
`paramVisibility({ params })` (hides property rows). `params` may be the
|
|
307
|
+
table literal or a zero-arg function returning it. Imports are limited to
|
|
308
|
+
`"lineadraw"` and `"lineadraw/helpers"`. The instance's first point input
|
|
309
|
+
is its pivot: `draw` sees inputs localized to it, and instance
|
|
310
|
+
`rotation`/`scale` apply about it. (Plain named exports — incl. the
|
|
311
|
+
pre-rename `main`/`defineInput` names — remain evaluatable as the frozen
|
|
312
|
+
wire form.)
|
|
313
|
+
|
|
314
|
+
### Interchange notes
|
|
315
|
+
|
|
316
|
+
- DXF/DWG export flattens semantics: block instances are exported as their
|
|
317
|
+
evaluated geometry (no `INSERT` reuse) and `image` objects are skipped
|
|
318
|
+
(DXF images reference external files). The export result reports both in
|
|
319
|
+
`skipped`.
|
|
320
|
+
- PDF import extracts vector strokes and text runs (dimensions arrive as
|
|
321
|
+
exploded geometry). pdf.js loads lazily on first use, as do the PNG
|
|
322
|
+
rasterizer (native resvg) and the DWG transcoder (bundled WebAssembly).
|
|
323
|
+
- `renderSvg({ bbox })` culls objects outside the box (they are absent from the
|
|
324
|
+
file, not merely outside the viewBox).
|
|
325
|
+
- Opening a document written by a NEWER Linea version throws instead of
|
|
326
|
+
loading lossily.
|
|
327
|
+
- Dimensions validate their point counts per kind (linear ≥ 2, angular ≥ 3,
|
|
328
|
+
radial/diameter ≥ 2).
|
|
329
|
+
|
|
122
330
|
## Ecosystem
|
|
123
331
|
|
|
124
332
|
- [Linea editor](https://lineadraw.com) — the browser CAD app that runs
|
|
@@ -126,8 +334,5 @@ the file.
|
|
|
126
334
|
- [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
|
|
127
335
|
public collection: real blocks/commands to learn from, agent skill, and
|
|
128
336
|
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
337
|
- [`@lineadraw/mcp`](https://www.npmjs.com/package/@lineadraw/mcp) — MCP
|
|
133
338
|
server with the full editor as an in-chat app.
|
package/bin/lineadraw.mjs
CHANGED
|
@@ -1,98 +1,278 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The `lineadraw` CLI —
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
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
|
+
}
|