lineadraw 0.6.0 → 0.8.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/bin/lineadraw.mjs CHANGED
@@ -1,278 +1,292 @@
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 { isDeepStrictEqual } from "node:util";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import {
17
+ buildManifest,
18
+ manifestJson,
19
+ MANIFEST_PATH,
20
+ verifyWithSdk,
21
+ withThumbnails,
22
+ } from "../lib/manifest.mjs";
23
+ import { init, tsconfigLibWarning } from "../lib/init.mjs";
24
+
25
+ const USAGE = `Usage: lineadraw <command> [options]
26
+
27
+ Marketplace repos (block / command collections):
28
+ manifest [--dir <path>] Validate all modules and write ${MANIFEST_PATH}
29
+ check [--dir <path>] Validate without writing; fail if the committed manifest is stale
30
+ init [--dir <path>] Add the marketplace structure to the current npm project
31
+
32
+ manifest and check also dry-run every block through lineadraw/sdk and bake
33
+ preview thumbnails into the catalog.
34
+
35
+ Running editors (the Linea desktop app on 127.0.0.1:7431):
36
+ status app version, port, 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 "open": {
158
+ const file = positional[0];
159
+ 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
+ });
163
+ }
164
+ case "close":
165
+ return request(
166
+ b,
167
+ "POST",
168
+ `/api/linea_close_document${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", "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
+ // Compared as parsed JSON, not bytes: a formatter, a web edit or a CRLF
243
+ // checkout must not fail a manifest whose content is current.
244
+ let committed = null;
245
+ if (existsSync(committedPath)) {
246
+ try {
247
+ committed = JSON.parse(readFileSync(committedPath, "utf8"));
248
+ } catch {
249
+ committed = "unreadable";
250
+ }
251
+ }
252
+ if (!isDeepStrictEqual(committed, JSON.parse(manifestJson(manifest)))) {
253
+ const why =
254
+ committed === null
255
+ ? "missing"
256
+ : committed === "unreadable"
257
+ ? "not valid JSON"
258
+ : "stale";
259
+ fail(
260
+ `${MANIFEST_PATH} is ${why} — run \`npx lineadraw manifest\` and commit the result.`,
261
+ );
262
+ }
263
+ const libWarning = tsconfigLibWarning(dir);
264
+ if (libWarning !== null) console.warn(`warning: ${libWarning}`);
265
+ console.log(`OK: ${manifest.entries.length} entries, manifest is current.`);
266
+ break;
267
+ }
268
+ case "init": {
269
+ try {
270
+ const { created, skipped, warnings } = await init(dir);
271
+ for (const f of created) console.log(`created ${f}`);
272
+ for (const f of skipped) console.log(`kept ${f}`);
273
+ for (const w of warnings) console.warn(`warning ${w}`);
274
+ console.log(
275
+ "\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
276
+ );
277
+ } catch (e) {
278
+ fail(e instanceof Error ? e.message : String(e));
279
+ }
280
+ break;
281
+ }
282
+ default: {
283
+ if (!BRIDGE_COMMANDS.includes(command)) fail(USAGE);
284
+ const { positional, flags } = parseArgs(args.slice(1));
285
+ try {
286
+ const result = await bridgeCommand(command, positional, flags);
287
+ console.log(JSON.stringify(result, null, 2));
288
+ } catch (e) {
289
+ fail(e instanceof Error ? e.message : String(e));
290
+ }
291
+ }
292
+ }