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/README.md +43 -18
- package/bin/lineadraw.mjs +291 -277
- package/dist/chunk-VWED6V6F.js +10020 -0
- package/dist/chunk-VWED6V6F.js.map +1 -0
- package/dist/exportDxf-2VU5475O.js +8 -0
- package/dist/exportDxf-2VU5475O.js.map +1 -0
- package/dist/{node-OWPSMNLK.js → node-PLO3I6OZ.js} +39 -1
- package/dist/node-PLO3I6OZ.js.map +1 -0
- package/dist/pkg/linea_dwg_bg.wasm +0 -0
- package/dist/sdk.d.ts +1656 -1036
- package/dist/sdk.js +3984 -9694
- package/dist/sdk.js.map +1 -1
- package/lineadraw.d.ts +577 -53
- package/package.json +2 -2
- package/templates/README.md +18 -18
- package/dist/node-OWPSMNLK.js.map +0 -1
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`, `
|
|
10
|
-
// `
|
|
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 {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 "
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
case "close":
|
|
165
|
-
return request(
|
|
166
|
-
b,
|
|
167
|
-
"POST",
|
|
168
|
-
`/api/
|
|
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", "
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|