lineadraw 0.5.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/bin/lineadraw.mjs CHANGED
@@ -1,109 +1,278 @@
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.
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { dirname, join, resolve } from "node:path";
9
- import {
10
- buildManifest,
11
- manifestJson,
12
- MANIFEST_PATH,
13
- verifyWithSdk,
14
- withThumbnails,
15
- } from "../lib/manifest.mjs";
16
- import { init, tsconfigLibWarning } from "../lib/init.mjs";
17
-
18
- const USAGE = `Usage: lineadraw <manifest|check|init> [--dir <path>]
19
-
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
23
-
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.`;
28
-
29
- const args = process.argv.slice(2);
30
- const command = args[0];
31
- const dirFlag = args.indexOf("--dir");
32
- const dir = resolve(dirFlag === -1 ? "." : (args[dirFlag + 1] ?? "."));
33
-
34
- const fail = (message) => {
35
- console.error(message);
36
- process.exit(1);
37
- };
38
-
39
- const reportProblems = (problems) => {
40
- for (const p of problems)
41
- console.error(`REJECTED ${p.file}: ${p.problems.join("; ")}`);
42
- };
43
-
44
- /** Validate everything (light + optional sdk dry-run); exits on failure. */
45
- const validatedManifest = async () => {
46
- const { manifest: base, sources, problems } = buildManifest(dir);
47
- const sdkResult = await verifyWithSdk(dir, base, sources);
48
- const all = [...problems, ...sdkResult.problems];
49
- if (all.length > 0) {
50
- reportProblems(all);
51
- fail(`${all.length} problem(s) — nothing written.`);
52
- }
53
- const manifest = withThumbnails(base, sdkResult.thumbnails);
54
- if (!sdkResult.ran)
55
- 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.",
61
- );
62
- return manifest;
63
- };
64
-
65
- switch (command) {
66
- case "manifest": {
67
- const manifest = await validatedManifest();
68
- const outPath = join(dir, MANIFEST_PATH);
69
- mkdirSync(dirname(outPath), { recursive: true });
70
- writeFileSync(outPath, manifestJson(manifest));
71
- console.log(`${MANIFEST_PATH}: ${manifest.entries.length} entries`);
72
- for (const e of manifest.entries)
73
- console.log(` ${e.kind} ${e.id} v${e.version}`);
74
- break;
75
- }
76
- case "check": {
77
- const manifest = await validatedManifest();
78
- const committedPath = join(dir, MANIFEST_PATH);
79
- const committed = existsSync(committedPath)
80
- ? readFileSync(committedPath, "utf8")
81
- : null;
82
- if (committed !== manifestJson(manifest))
83
- fail(
84
- `${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
85
- );
86
- const libWarning = tsconfigLibWarning(dir);
87
- if (libWarning !== null) console.warn(`warning: ${libWarning}`);
88
- console.log(
89
- `OK: ${manifest.entries.length} entries, manifest is current.`,
90
- );
91
- break;
92
- }
93
- case "init": {
94
- try {
95
- const { created, skipped, warnings } = await init(dir);
96
- for (const f of created) console.log(`created ${f}`);
97
- for (const f of skipped) console.log(`kept ${f}`);
98
- for (const w of warnings) console.warn(`warning ${w}`);
99
- console.log(
100
- "\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
101
- );
102
- } catch (e) {
103
- fail(e instanceof Error ? e.message : String(e));
104
- }
105
- break;
106
- }
107
- default:
108
- fail(USAGE);
109
- }
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
+ }