lineadraw 0.8.0 → 0.10.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 +373 -199
- package/bin/lineadraw.mjs +15 -110
- package/dist/bridge.d.ts +102 -0
- package/dist/bridge.js +16 -0
- package/dist/chunk-L2CWZAKW.js +141 -0
- package/dist/chunk-L2CWZAKW.js.map +1 -0
- package/dist/{chunk-VWED6V6F.js → chunk-YYJAXJOY.js} +4291 -1114
- package/dist/chunk-YYJAXJOY.js.map +1 -0
- package/dist/exportDxf-C4CLQCC3.js +10 -0
- package/dist/exportDxf-C4CLQCC3.js.map +1 -0
- package/dist/sdk.d.ts +705 -253
- package/dist/sdk.js +288 -231
- package/dist/sdk.js.map +1 -1
- package/lineadraw.d.ts +804 -1649
- package/package.json +3 -3
- package/templates/README.md +6 -4
- package/templates/sample.block.ts +2 -2
- package/dist/chunk-VWED6V6F.js.map +0 -1
- package/dist/exportDxf-2VU5475O.js +0 -8
- /package/dist/{exportDxf-2VU5475O.js.map → bridge.js.map} +0 -0
package/bin/lineadraw.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
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";
|
|
14
13
|
import { isDeepStrictEqual } from "node:util";
|
|
15
14
|
import { dirname, join, resolve } from "node:path";
|
|
16
15
|
import {
|
|
@@ -67,130 +66,36 @@ const parseArgs = (argv) => {
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
// --- bridge --------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
: "";
|
|
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.
|
|
151
71
|
|
|
152
72
|
const bridgeCommand = async (cmd, positional, flags) => {
|
|
153
|
-
const
|
|
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;
|
|
154
79
|
switch (cmd) {
|
|
155
80
|
case "status":
|
|
156
|
-
return
|
|
81
|
+
return b.status();
|
|
157
82
|
case "open": {
|
|
158
83
|
const file = positional[0];
|
|
159
84
|
if (!file) throw new Error("open needs a .linea file path");
|
|
160
|
-
return
|
|
161
|
-
path: resolve(file),
|
|
162
|
-
});
|
|
85
|
+
return b.open(file);
|
|
163
86
|
}
|
|
164
87
|
case "close":
|
|
165
|
-
return
|
|
166
|
-
b,
|
|
167
|
-
"POST",
|
|
168
|
-
`/api/linea_close_document${editorQuery(flags)}`,
|
|
169
|
-
{ discard: flags.discard === true },
|
|
170
|
-
);
|
|
88
|
+
return b.editor(editor).close({ discard: flags.discard === true });
|
|
171
89
|
case "call": {
|
|
172
90
|
const [operation, json] = positional;
|
|
173
91
|
if (!operation)
|
|
174
92
|
throw new Error("call needs an operation name (see `lineadraw tools`)");
|
|
175
|
-
|
|
176
|
-
|
|
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",
|
|
93
|
+
return b.call(operation, json === undefined ? {} : JSON.parse(json), {
|
|
94
|
+
editor,
|
|
188
95
|
});
|
|
189
|
-
return (reply.result?.tools ?? []).map((t) => ({
|
|
190
|
-
name: t.name,
|
|
191
|
-
description: t.description,
|
|
192
|
-
}));
|
|
193
96
|
}
|
|
97
|
+
case "tools":
|
|
98
|
+
return b.tools();
|
|
194
99
|
default:
|
|
195
100
|
throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
|
|
196
101
|
}
|
package/dist/bridge.d.ts
ADDED
|
@@ -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
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/bridge.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { isAbsolute, join, resolve } from "path";
|
|
5
|
+
var APP_ID = "com.lineadraw.desktop";
|
|
6
|
+
var DEFAULT_BRIDGE_PORT = 7431;
|
|
7
|
+
var BridgeError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
status;
|
|
10
|
+
/** The rest of the error body (e.g. `candidates` for `ambiguous`). */
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, opts = {}) {
|
|
13
|
+
super(message, { cause: opts.cause });
|
|
14
|
+
this.name = "BridgeError";
|
|
15
|
+
this.code = opts.code;
|
|
16
|
+
this.status = opts.status;
|
|
17
|
+
this.details = opts.details;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var bridgeFilePath = () => {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
if (process.platform === "win32")
|
|
23
|
+
return join(
|
|
24
|
+
process.env.APPDATA ?? join(home, "AppData", "Roaming"),
|
|
25
|
+
APP_ID,
|
|
26
|
+
"bridge.json"
|
|
27
|
+
);
|
|
28
|
+
if (process.platform === "darwin")
|
|
29
|
+
return join(home, "Library", "Application Support", APP_ID, "bridge.json");
|
|
30
|
+
return join(
|
|
31
|
+
process.env.XDG_DATA_HOME ?? join(home, ".local", "share"),
|
|
32
|
+
APP_ID,
|
|
33
|
+
"bridge.json"
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
var readBridgeFile = (path) => {
|
|
37
|
+
if (path === false || !existsSync(path)) return {};
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var connectEditors = async (opts = {}) => {
|
|
45
|
+
const bridge = createBridge(opts);
|
|
46
|
+
await bridge.status();
|
|
47
|
+
return bridge;
|
|
48
|
+
};
|
|
49
|
+
var createBridge = (opts = {}) => {
|
|
50
|
+
const file = readBridgeFile(opts.bridgeFile ?? bridgeFilePath());
|
|
51
|
+
const port = opts.port ?? file.port ?? DEFAULT_BRIDGE_PORT;
|
|
52
|
+
const token = opts.token ?? process.env.LINEA_BRIDGE_TOKEN ?? file.token;
|
|
53
|
+
const doFetch = opts.fetch ?? fetch;
|
|
54
|
+
const url = `http://127.0.0.1:${port}`;
|
|
55
|
+
const headers = {
|
|
56
|
+
"content-type": "application/json"
|
|
57
|
+
};
|
|
58
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
59
|
+
const request = async (method, path, body) => {
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await doFetch(`${url}${path}`, {
|
|
63
|
+
method,
|
|
64
|
+
headers,
|
|
65
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
66
|
+
});
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const c = e?.cause;
|
|
69
|
+
const cause = c?.code ?? c?.message ?? e?.message ?? e;
|
|
70
|
+
throw new BridgeError(
|
|
71
|
+
`Nothing answers on ${url} (${cause}). Is the Linea desktop app running?`,
|
|
72
|
+
{ code: "unreachable", cause: e }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const text = await response.text();
|
|
76
|
+
let parsed = text;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(text);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = parsed?.error;
|
|
83
|
+
const { code, message, ...details } = error ?? {};
|
|
84
|
+
throw new BridgeError(
|
|
85
|
+
typeof message === "string" ? message : `HTTP ${response.status}: ${text.slice(0, 200)}`,
|
|
86
|
+
{
|
|
87
|
+
code: typeof code === "string" ? code : void 0,
|
|
88
|
+
status: response.status,
|
|
89
|
+
details
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
};
|
|
95
|
+
const call = (operation, args = {}, o = {}) => request(
|
|
96
|
+
"POST",
|
|
97
|
+
`/api/${encodeURIComponent(operation)}${o.editor ? `?editor=${encodeURIComponent(o.editor)}` : ""}`,
|
|
98
|
+
args
|
|
99
|
+
);
|
|
100
|
+
const editor = (ref) => ({
|
|
101
|
+
ref: ref ?? "",
|
|
102
|
+
call: (operation, args) => call(operation, args, { editor: ref }),
|
|
103
|
+
runScript: (source, so = {}) => call(
|
|
104
|
+
"linea_run_script",
|
|
105
|
+
{
|
|
106
|
+
source,
|
|
107
|
+
...so.inputs && { inputs: so.inputs },
|
|
108
|
+
...so.params && { params: so.params }
|
|
109
|
+
},
|
|
110
|
+
{ editor: ref }
|
|
111
|
+
),
|
|
112
|
+
close: (co = {}) => call(
|
|
113
|
+
"linea_close_document",
|
|
114
|
+
{ discard: co.discard === true },
|
|
115
|
+
{ editor: ref }
|
|
116
|
+
)
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
url,
|
|
120
|
+
status: () => request("GET", "/status"),
|
|
121
|
+
editors: () => request("GET", "/api/editors"),
|
|
122
|
+
tools: async () => (await request(
|
|
123
|
+
"GET",
|
|
124
|
+
"/api/tools"
|
|
125
|
+
)).tools?.map(({ name, description }) => ({ name, description })) ?? [],
|
|
126
|
+
open: (path) => request("POST", "/api/linea_open_editor", {
|
|
127
|
+
path: isAbsolute(path) ? path : resolve(path)
|
|
128
|
+
}),
|
|
129
|
+
editor,
|
|
130
|
+
call
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
DEFAULT_BRIDGE_PORT,
|
|
136
|
+
BridgeError,
|
|
137
|
+
bridgeFilePath,
|
|
138
|
+
connectEditors,
|
|
139
|
+
createBridge
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=chunk-L2CWZAKW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bridge.ts"],"sourcesContent":["// A client for RUNNING Linea editors: the desktop app serves the bridge on\n// 127.0.0.1:7431 (docs/desktop-bridge.md in the linea repo) and every open\n// window — plus any paired web tab — registers with it. Operations are the\n// shared `linea_*` tool catalog, each one undo step in that editor;\n// `runScript` sends a whole `defineCommand` module, which runs inside the\n// editor against the real Drawing API as ONE step and ONE round trip.\n//\n// Deliberately dependency-free (node: built-ins + fetch): the `lineadraw`\n// CLI loads it on its own (dist/bridge.js) without the rest of the SDK.\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve } from \"node:path\";\n\n/** Where the desktop app keeps its bridge settings (`bridge.json`). */\nconst APP_ID = \"com.lineadraw.desktop\";\nexport const DEFAULT_BRIDGE_PORT = 7431;\n\n/** A running editor as the bridge lists it. */\nexport interface EditorInfo {\n /** The drawing's document id — the stable way to address it. */\n id: string;\n name: string;\n /** The file it is bound to; absent for an unsaved drawing / a web tab. */\n path?: string | null;\n kind: \"desktop\" | \"web\";\n readOnly: boolean;\n /** Changed since the last file save. */\n dirty: boolean;\n}\n\n/** `GET /status`: the app's version, the bridge port, the open editors. */\nexport interface BridgeStatus {\n version: string;\n port: number;\n editors: EditorInfo[];\n}\n\nexport interface BridgeOptions {\n /** Default: bridge.json's port, else 7431. */\n port?: number;\n /** Agent token, when the app requires one. Default: LINEA_BRIDGE_TOKEN,\n * else bridge.json's token. */\n token?: string;\n /** The bridge.json to read, or `false` to read none. Default: the\n * desktop app's own (per-OS app-data folder). */\n bridgeFile?: string | false;\n /** Injected fetch (tests, proxies). Default: the global one. */\n fetch?: typeof fetch;\n}\n\n/** A bridge or editor refusal: `code` is the bridge's (`ambiguous`,\n * `noSuchEditor`, `readOnly`, `validation`, `fileNotFound`…). */\nexport class BridgeError extends Error {\n readonly code?: string;\n readonly status?: number;\n /** The rest of the error body (e.g. `candidates` for `ambiguous`). */\n readonly details?: Record<string, unknown>;\n constructor(\n message: string,\n opts: {\n code?: string;\n status?: number;\n details?: Record<string, unknown>;\n cause?: unknown;\n } = {},\n ) {\n super(message, { cause: opts.cause });\n this.name = \"BridgeError\";\n this.code = opts.code;\n this.status = opts.status;\n this.details = opts.details;\n }\n}\n\n/** The desktop app's bridge.json path on this OS. */\nexport const bridgeFilePath = (): string => {\n const home = homedir();\n if (process.platform === \"win32\")\n return join(\n process.env.APPDATA ?? join(home, \"AppData\", \"Roaming\"),\n APP_ID,\n \"bridge.json\",\n );\n if (process.platform === \"darwin\")\n return join(home, \"Library\", \"Application Support\", APP_ID, \"bridge.json\");\n return join(\n process.env.XDG_DATA_HOME ?? join(home, \".local\", \"share\"),\n APP_ID,\n \"bridge.json\",\n );\n};\n\nconst readBridgeFile = (\n path: string | false,\n): { port?: number; token?: string } => {\n if (path === false || !existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n return {};\n }\n};\n\n/** One editor, addressed by document id, file path or (unique) name. */\nexport interface EditorHandle {\n /** How this handle addresses the editor. */\n readonly ref: string;\n /** Run one `linea_*` operation; resolves its result. */\n call<T = unknown>(\n operation: string,\n args?: Record<string, unknown>,\n ): Promise<T>;\n /**\n * Run a `defineCommand` module inside the editor as ONE undo step —\n * loops, reads and edits against the full Drawing API, one round trip.\n * `inputs` / `params` answer the module's declared ones (the user is\n * never asked).\n */\n runScript<T = unknown>(\n source: string,\n opts?: {\n inputs?: readonly (readonly [number, number])[];\n params?: Record<string, unknown>;\n },\n ): Promise<T>;\n /** Close it like its window's x; a dirty desktop drawing is refused\n * unless `discard`. */\n close(opts?: { discard?: boolean }): Promise<unknown>;\n}\n\n/** The running app: its editors and the operations they offer. */\nexport interface LineaBridge {\n /** The bridge's address, e.g. http://127.0.0.1:7431. */\n readonly url: string;\n status(): Promise<BridgeStatus>;\n /** The open editors. */\n editors(): Promise<EditorInfo[]>;\n /** The operations the bridge offers (name + description). */\n tools(): Promise<{ name: string; description?: string }[]>;\n /** Open a `.linea` file in a window of its own (an absolute path, or one\n * relative to the current directory); resolves once it has loaded. */\n open(path: string): Promise<EditorInfo>;\n /** A handle on one editor (id, path or unique name). Omitted: the only\n * open editor — with several, calls fail as `ambiguous`. */\n editor(ref?: string): EditorHandle;\n /** One operation on an editor (`editor` as for `editor()`). */\n call<T = unknown>(\n operation: string,\n args?: Record<string, unknown>,\n opts?: { editor?: string },\n ): Promise<T>;\n}\n\n/**\n * Connect to the running Linea desktop app's bridge: reads its bridge.json\n * (port, agent token), checks that it answers, and returns the client.\n * Rejects with a BridgeError when nothing answers.\n */\nexport const connectEditors = async (\n opts: BridgeOptions = {},\n): Promise<LineaBridge> => {\n const bridge = createBridge(opts);\n await bridge.status(); // fail fast, with the \"is it running?\" message\n return bridge;\n};\n\n/** The client without the reachability check (the CLI's one-shot calls). */\nexport const createBridge = (opts: BridgeOptions = {}): LineaBridge => {\n const file = readBridgeFile(opts.bridgeFile ?? bridgeFilePath());\n const port = opts.port ?? file.port ?? DEFAULT_BRIDGE_PORT;\n const token = opts.token ?? process.env.LINEA_BRIDGE_TOKEN ?? file.token;\n const doFetch = opts.fetch ?? fetch;\n const url = `http://127.0.0.1:${port}`;\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (token) headers.authorization = `Bearer ${token}`;\n\n const request = async <T>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<T> => {\n let response: Response;\n try {\n response = await doFetch(`${url}${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n } catch (e) {\n const c = (e as { cause?: { code?: string; message?: string } })?.cause;\n const cause = c?.code ?? c?.message ?? (e as Error)?.message ?? e;\n throw new BridgeError(\n `Nothing answers on ${url} (${cause}). Is the Linea desktop app running?`,\n { code: \"unreachable\", cause: e },\n );\n }\n const text = await response.text();\n let parsed: unknown = text;\n try {\n parsed = JSON.parse(text);\n } catch {\n // not JSON: keep the text\n }\n if (!response.ok) {\n const error = (parsed as { error?: Record<string, unknown> })?.error;\n const { code, message, ...details } = error ?? {};\n throw new BridgeError(\n typeof message === \"string\"\n ? message\n : `HTTP ${response.status}: ${text.slice(0, 200)}`,\n {\n code: typeof code === \"string\" ? code : undefined,\n status: response.status,\n details,\n },\n );\n }\n return parsed as T;\n };\n\n const call = <T>(\n operation: string,\n args: Record<string, unknown> = {},\n o: { editor?: string } = {},\n ): Promise<T> =>\n request<T>(\n \"POST\",\n `/api/${encodeURIComponent(operation)}${\n o.editor ? `?editor=${encodeURIComponent(o.editor)}` : \"\"\n }`,\n args,\n );\n\n const editor = (ref?: string): EditorHandle => ({\n ref: ref ?? \"\",\n call: (operation, args) => call(operation, args, { editor: ref }),\n runScript: (source, so = {}) =>\n call(\n \"linea_run_script\",\n {\n source,\n ...(so.inputs && { inputs: so.inputs }),\n ...(so.params && { params: so.params }),\n },\n { editor: ref },\n ),\n close: (co = {}) =>\n call(\n \"linea_close_document\",\n { discard: co.discard === true },\n { editor: ref },\n ),\n });\n\n return {\n url,\n status: () => request<BridgeStatus>(\"GET\", \"/status\"),\n editors: () => request<EditorInfo[]>(\"GET\", \"/api/editors\"),\n tools: async () =>\n (\n await request<{ tools?: { name: string; description?: string }[] }>(\n \"GET\",\n \"/api/tools\",\n )\n ).tools?.map(({ name, description }) => ({ name, description })) ?? [],\n open: (path) =>\n request<EditorInfo>(\"POST\", \"/api/linea_open_editor\", {\n path: isAbsolute(path) ? path : resolve(path),\n }),\n editor,\n call,\n };\n};\n"],"mappings":";AAUA,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY,MAAM,eAAe;AAG1C,IAAM,SAAS;AACR,IAAM,sBAAsB;AAqC5B,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACT,YACE,SACA,OAKI,CAAC,GACL;AACA,UAAM,SAAS,EAAE,OAAO,KAAK,MAAM,CAAC;AACpC,SAAK,OAAO;AACZ,SAAK,OAAO,KAAK;AACjB,SAAK,SAAS,KAAK;AACnB,SAAK,UAAU,KAAK;AAAA,EACtB;AACF;AAGO,IAAM,iBAAiB,MAAc;AAC1C,QAAM,OAAO,QAAQ;AACrB,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,MACL,QAAQ,IAAI,WAAW,KAAK,MAAM,WAAW,SAAS;AAAA,MACtD;AAAA,MACA;AAAA,IACF;AACF,MAAI,QAAQ,aAAa;AACvB,WAAO,KAAK,MAAM,WAAW,uBAAuB,QAAQ,aAAa;AAC3E,SAAO;AAAA,IACL,QAAQ,IAAI,iBAAiB,KAAK,MAAM,UAAU,OAAO;AAAA,IACzD;AAAA,IACA;AAAA,EACF;AACF;AAEA,IAAM,iBAAiB,CACrB,SACsC;AACtC,MAAI,SAAS,SAAS,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AACjD,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAyDO,IAAM,iBAAiB,OAC5B,OAAsB,CAAC,MACE;AACzB,QAAM,SAAS,aAAa,IAAI;AAChC,QAAM,OAAO,OAAO;AACpB,SAAO;AACT;AAGO,IAAM,eAAe,CAAC,OAAsB,CAAC,MAAmB;AACrE,QAAM,OAAO,eAAe,KAAK,cAAc,eAAe,CAAC;AAC/D,QAAM,OAAO,KAAK,QAAQ,KAAK,QAAQ;AACvC,QAAM,QAAQ,KAAK,SAAS,QAAQ,IAAI,sBAAsB,KAAK;AACnE,QAAM,UAAU,KAAK,SAAS;AAC9B,QAAM,MAAM,oBAAoB,IAAI;AACpC,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,EAClB;AACA,MAAI,MAAO,SAAQ,gBAAgB,UAAU,KAAK;AAElD,QAAM,UAAU,OACd,QACA,MACA,SACe;AACf,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,QAAQ,GAAG,GAAG,GAAG,IAAI,IAAI;AAAA,QACxC;AAAA,QACA;AAAA,QACA,MAAM,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAAA,MAC5D,CAAC;AAAA,IACH,SAAS,GAAG;AACV,YAAM,IAAK,GAAuD;AAClE,YAAM,QAAQ,GAAG,QAAQ,GAAG,WAAY,GAAa,WAAW;AAChE,YAAM,IAAI;AAAA,QACR,sBAAsB,GAAG,KAAK,KAAK;AAAA,QACnC,EAAE,MAAM,eAAe,OAAO,EAAE;AAAA,MAClC;AAAA,IACF;AACA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AAAA,IAER;AACA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAS,QAAgD;AAC/D,YAAM,EAAE,MAAM,SAAS,GAAG,QAAQ,IAAI,SAAS,CAAC;AAChD,YAAM,IAAI;AAAA,QACR,OAAO,YAAY,WACf,UACA,QAAQ,SAAS,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QAClD;AAAA,UACE,MAAM,OAAO,SAAS,WAAW,OAAO;AAAA,UACxC,QAAQ,SAAS;AAAA,UACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,CACX,WACA,OAAgC,CAAC,GACjC,IAAyB,CAAC,MAE1B;AAAA,IACE;AAAA,IACA,QAAQ,mBAAmB,SAAS,CAAC,GACnC,EAAE,SAAS,WAAW,mBAAmB,EAAE,MAAM,CAAC,KAAK,EACzD;AAAA,IACA;AAAA,EACF;AAEF,QAAM,SAAS,CAAC,SAAgC;AAAA,IAC9C,KAAK,OAAO;AAAA,IACZ,MAAM,CAAC,WAAW,SAAS,KAAK,WAAW,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChE,WAAW,CAAC,QAAQ,KAAK,CAAC,MACxB;AAAA,MACE;AAAA,MACA;AAAA,QACE;AAAA,QACA,GAAI,GAAG,UAAU,EAAE,QAAQ,GAAG,OAAO;AAAA,QACrC,GAAI,GAAG,UAAU,EAAE,QAAQ,GAAG,OAAO;AAAA,MACvC;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,IACF,OAAO,CAAC,KAAK,CAAC,MACZ;AAAA,MACE;AAAA,MACA,EAAE,SAAS,GAAG,YAAY,KAAK;AAAA,MAC/B,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,MAAM,QAAsB,OAAO,SAAS;AAAA,IACpD,SAAS,MAAM,QAAsB,OAAO,cAAc;AAAA,IAC1D,OAAO,aAEH,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF,GACA,OAAO,IAAI,CAAC,EAAE,MAAM,YAAY,OAAO,EAAE,MAAM,YAAY,EAAE,KAAK,CAAC;AAAA,IACvE,MAAM,CAAC,SACL,QAAoB,QAAQ,0BAA0B;AAAA,MACpD,MAAM,WAAW,IAAI,IAAI,OAAO,QAAQ,IAAI;AAAA,IAC9C,CAAC;AAAA,IACH;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|