lineadraw 0.7.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/bin/lineadraw.mjs CHANGED
@@ -10,7 +10,7 @@
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";
13
+ import { isDeepStrictEqual } from "node:util";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import {
16
16
  buildManifest,
@@ -66,130 +66,36 @@ const parseArgs = (argv) => {
66
66
  };
67
67
 
68
68
  // --- bridge --------------------------------------------------------------
69
-
70
- const APP_ID = "com.lineadraw.desktop";
71
- const DEFAULT_PORT = 7431;
72
-
73
- const bridgeFilePath = () => {
74
- const home = homedir();
75
- if (process.platform === "win32")
76
- return join(
77
- process.env.APPDATA ?? join(home, "AppData", "Roaming"),
78
- APP_ID,
79
- "bridge.json",
80
- );
81
- if (process.platform === "darwin")
82
- return join(home, "Library", "Application Support", APP_ID, "bridge.json");
83
- return join(
84
- process.env.XDG_DATA_HOME ?? join(home, ".local", "share"),
85
- APP_ID,
86
- "bridge.json",
87
- );
88
- };
89
-
90
- const readBridgeFile = () => {
91
- const path = bridgeFilePath();
92
- if (!existsSync(path)) return {};
93
- try {
94
- return JSON.parse(readFileSync(path, "utf8"));
95
- } catch {
96
- return {};
97
- }
98
- };
99
-
100
- const bridge = (flags) => {
101
- const file = readBridgeFile();
102
- const port =
103
- typeof flags.port === "string"
104
- ? Number(flags.port)
105
- : file.port || DEFAULT_PORT;
106
- const token =
107
- (typeof flags.token === "string" ? flags.token : undefined) ??
108
- process.env.LINEA_BRIDGE_TOKEN ??
109
- file.token;
110
- const headers = { "content-type": "application/json" };
111
- if (token) headers.authorization = `Bearer ${token}`;
112
- return { base: `http://127.0.0.1:${port}`, headers };
113
- };
114
-
115
- const request = async (b, method, path, body) => {
116
- let response;
117
- try {
118
- response = await fetch(`${b.base}${path}`, {
119
- method,
120
- headers: b.headers,
121
- body: body === undefined ? undefined : JSON.stringify(body),
122
- });
123
- } catch (e) {
124
- const cause = e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e;
125
- throw new Error(
126
- `Nothing answers on ${b.base} (${cause}). Is the Linea desktop app running?`,
127
- { cause: e },
128
- );
129
- }
130
- const text = await response.text();
131
- let parsed = text;
132
- try {
133
- parsed = JSON.parse(text);
134
- } catch {
135
- // not JSON: keep the text
136
- }
137
- if (!response.ok) {
138
- const error = parsed?.error;
139
- throw new Error(
140
- error?.message ?? `HTTP ${response.status}: ${text.slice(0, 200)}`,
141
- );
142
- }
143
- return parsed;
144
- };
145
-
146
- const editorQuery = (flags) =>
147
- typeof flags.editor === "string"
148
- ? `?editor=${encodeURIComponent(flags.editor)}`
149
- : "";
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.
150
71
 
151
72
  const bridgeCommand = async (cmd, positional, flags) => {
152
- 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;
153
79
  switch (cmd) {
154
80
  case "status":
155
- return request(b, "GET", "/status");
81
+ return b.status();
156
82
  case "open": {
157
83
  const file = positional[0];
158
84
  if (!file) throw new Error("open needs a .linea file path");
159
- return request(b, "POST", "/api/linea_open_editor", {
160
- path: resolve(file),
161
- });
85
+ return b.open(file);
162
86
  }
163
87
  case "close":
164
- return request(
165
- b,
166
- "POST",
167
- `/api/linea_close_document${editorQuery(flags)}`,
168
- { discard: flags.discard === true },
169
- );
88
+ return b.editor(editor).close({ discard: flags.discard === true });
170
89
  case "call": {
171
90
  const [operation, json] = positional;
172
91
  if (!operation)
173
92
  throw new Error("call needs an operation name (see `lineadraw tools`)");
174
- const payload = json === undefined ? {} : JSON.parse(json);
175
- return request(
176
- b,
177
- "POST",
178
- `/api/${encodeURIComponent(operation)}${editorQuery(flags)}`,
179
- payload,
180
- );
181
- }
182
- case "tools": {
183
- const reply = await request(b, "POST", "/mcp", {
184
- jsonrpc: "2.0",
185
- id: 1,
186
- method: "tools/list",
93
+ return b.call(operation, json === undefined ? {} : JSON.parse(json), {
94
+ editor,
187
95
  });
188
- return (reply.result?.tools ?? []).map((t) => ({
189
- name: t.name,
190
- description: t.description,
191
- }));
192
96
  }
97
+ case "tools":
98
+ return b.tools();
193
99
  default:
194
100
  throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
195
101
  }
@@ -238,13 +144,27 @@ switch (command) {
238
144
  case "check": {
239
145
  const manifest = await validatedManifest();
240
146
  const committedPath = join(dir, MANIFEST_PATH);
241
- const committed = existsSync(committedPath)
242
- ? readFileSync(committedPath, "utf8")
243
- : null;
244
- if (committed !== manifestJson(manifest))
147
+ // Compared as parsed JSON, not bytes: a formatter, a web edit or a CRLF
148
+ // checkout must not fail a manifest whose content is current.
149
+ let committed = null;
150
+ if (existsSync(committedPath)) {
151
+ try {
152
+ committed = JSON.parse(readFileSync(committedPath, "utf8"));
153
+ } catch {
154
+ committed = "unreadable";
155
+ }
156
+ }
157
+ if (!isDeepStrictEqual(committed, JSON.parse(manifestJson(manifest)))) {
158
+ const why =
159
+ committed === null
160
+ ? "missing"
161
+ : committed === "unreadable"
162
+ ? "not valid JSON"
163
+ : "stale";
245
164
  fail(
246
- `${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
165
+ `${MANIFEST_PATH} is ${why} — run \`npx lineadraw manifest\` and commit the result.`,
247
166
  );
167
+ }
248
168
  const libWarning = tsconfigLibWarning(dir);
249
169
  if (libWarning !== null) console.warn(`warning: ${libWarning}`);
250
170
  console.log(`OK: ${manifest.entries.length} entries, manifest is current.`);
@@ -0,0 +1,102 @@
1
+ declare const DEFAULT_BRIDGE_PORT = 7431;
2
+ /** A running editor as the bridge lists it. */
3
+ interface EditorInfo {
4
+ /** The drawing's document id — the stable way to address it. */
5
+ id: string;
6
+ name: string;
7
+ /** The file it is bound to; absent for an unsaved drawing / a web tab. */
8
+ path?: string | null;
9
+ kind: "desktop" | "web";
10
+ readOnly: boolean;
11
+ /** Changed since the last file save. */
12
+ dirty: boolean;
13
+ }
14
+ /** `GET /status`: the app's version, the bridge port, the open editors. */
15
+ interface BridgeStatus {
16
+ version: string;
17
+ port: number;
18
+ editors: EditorInfo[];
19
+ }
20
+ interface BridgeOptions {
21
+ /** Default: bridge.json's port, else 7431. */
22
+ port?: number;
23
+ /** Agent token, when the app requires one. Default: LINEA_BRIDGE_TOKEN,
24
+ * else bridge.json's token. */
25
+ token?: string;
26
+ /** The bridge.json to read, or `false` to read none. Default: the
27
+ * desktop app's own (per-OS app-data folder). */
28
+ bridgeFile?: string | false;
29
+ /** Injected fetch (tests, proxies). Default: the global one. */
30
+ fetch?: typeof fetch;
31
+ }
32
+ /** A bridge or editor refusal: `code` is the bridge's (`ambiguous`,
33
+ * `noSuchEditor`, `readOnly`, `validation`, `fileNotFound`…). */
34
+ declare class BridgeError extends Error {
35
+ readonly code?: string;
36
+ readonly status?: number;
37
+ /** The rest of the error body (e.g. `candidates` for `ambiguous`). */
38
+ readonly details?: Record<string, unknown>;
39
+ constructor(message: string, opts?: {
40
+ code?: string;
41
+ status?: number;
42
+ details?: Record<string, unknown>;
43
+ cause?: unknown;
44
+ });
45
+ }
46
+ /** The desktop app's bridge.json path on this OS. */
47
+ declare const bridgeFilePath: () => string;
48
+ /** One editor, addressed by document id, file path or (unique) name. */
49
+ interface EditorHandle {
50
+ /** How this handle addresses the editor. */
51
+ readonly ref: string;
52
+ /** Run one `linea_*` operation; resolves its result. */
53
+ call<T = unknown>(operation: string, args?: Record<string, unknown>): Promise<T>;
54
+ /**
55
+ * Run a `defineCommand` module inside the editor as ONE undo step —
56
+ * loops, reads and edits against the full Drawing API, one round trip.
57
+ * `inputs` / `params` answer the module's declared ones (the user is
58
+ * never asked).
59
+ */
60
+ runScript<T = unknown>(source: string, opts?: {
61
+ inputs?: readonly (readonly [number, number])[];
62
+ params?: Record<string, unknown>;
63
+ }): Promise<T>;
64
+ /** Close it like its window's x; a dirty desktop drawing is refused
65
+ * unless `discard`. */
66
+ close(opts?: {
67
+ discard?: boolean;
68
+ }): Promise<unknown>;
69
+ }
70
+ /** The running app: its editors and the operations they offer. */
71
+ interface LineaBridge {
72
+ /** The bridge's address, e.g. http://127.0.0.1:7431. */
73
+ readonly url: string;
74
+ status(): Promise<BridgeStatus>;
75
+ /** The open editors. */
76
+ editors(): Promise<EditorInfo[]>;
77
+ /** The operations the bridge offers (name + description). */
78
+ tools(): Promise<{
79
+ name: string;
80
+ description?: string;
81
+ }[]>;
82
+ /** Open a `.linea` file in a window of its own (an absolute path, or one
83
+ * relative to the current directory); resolves once it has loaded. */
84
+ open(path: string): Promise<EditorInfo>;
85
+ /** A handle on one editor (id, path or unique name). Omitted: the only
86
+ * open editor — with several, calls fail as `ambiguous`. */
87
+ editor(ref?: string): EditorHandle;
88
+ /** One operation on an editor (`editor` as for `editor()`). */
89
+ call<T = unknown>(operation: string, args?: Record<string, unknown>, opts?: {
90
+ editor?: string;
91
+ }): Promise<T>;
92
+ }
93
+ /**
94
+ * Connect to the running Linea desktop app's bridge: reads its bridge.json
95
+ * (port, agent token), checks that it answers, and returns the client.
96
+ * Rejects with a BridgeError when nothing answers.
97
+ */
98
+ declare const connectEditors: (opts?: BridgeOptions) => Promise<LineaBridge>;
99
+ /** The client without the reachability check (the CLI's one-shot calls). */
100
+ declare const createBridge: (opts?: BridgeOptions) => LineaBridge;
101
+
102
+ export { BridgeError, type BridgeOptions, type BridgeStatus, DEFAULT_BRIDGE_PORT, type EditorHandle, type EditorInfo, type LineaBridge, bridgeFilePath, connectEditors, createBridge };
package/dist/bridge.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ BridgeError,
3
+ DEFAULT_BRIDGE_PORT,
4
+ bridgeFilePath,
5
+ connectEditors,
6
+ createBridge
7
+ } from "./chunk-L2CWZAKW.js";
8
+ import "./chunk-PZ5AY32C.js";
9
+ export {
10
+ BridgeError,
11
+ DEFAULT_BRIDGE_PORT,
12
+ bridgeFilePath,
13
+ connectEditors,
14
+ createBridge
15
+ };
16
+ //# sourceMappingURL=bridge.js.map