@lotics/cli 0.88.0 → 0.88.1

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.
Files changed (87) hide show
  1. package/dist/render_page.js +100 -100
  2. package/dist/src/cli.js +100 -100
  3. package/dist/{client.d.ts → src/client.d.ts} +1 -1
  4. package/dist/src/package_content_types.d.ts +70 -0
  5. package/dist/src/package_content_types.js +12 -0
  6. package/package.json +2 -2
  7. package/dist/app_commands.d.ts +0 -401
  8. package/dist/app_commands.js +0 -1520
  9. package/dist/app_commands.test.d.ts +0 -1
  10. package/dist/app_commands.test.js +0 -898
  11. package/dist/app_workflow_check.d.ts +0 -77
  12. package/dist/app_workflow_check.js +0 -169
  13. package/dist/app_workflow_check.test.d.ts +0 -1
  14. package/dist/app_workflow_check.test.js +0 -166
  15. package/dist/args.d.ts +0 -42
  16. package/dist/args.js +0 -121
  17. package/dist/args.test.d.ts +0 -1
  18. package/dist/args.test.js +0 -88
  19. package/dist/child_env.d.ts +0 -13
  20. package/dist/child_env.js +0 -24
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.js +0 -1095
  23. package/dist/cli_dispatch.test.d.ts +0 -1
  24. package/dist/cli_dispatch.test.js +0 -89
  25. package/dist/client.test.d.ts +0 -1
  26. package/dist/client.test.js +0 -47
  27. package/dist/config.d.ts +0 -124
  28. package/dist/config.js +0 -315
  29. package/dist/config.test.d.ts +0 -1
  30. package/dist/config.test.js +0 -319
  31. package/dist/dev/file_relay.d.ts +0 -39
  32. package/dist/dev/file_relay.js +0 -87
  33. package/dist/dev/file_relay.test.d.ts +0 -1
  34. package/dist/dev/file_relay.test.js +0 -87
  35. package/dist/dev/rpc_handler.d.ts +0 -21
  36. package/dist/dev/rpc_handler.js +0 -166
  37. package/dist/dev/rpc_handler.test.d.ts +0 -1
  38. package/dist/dev/rpc_handler.test.js +0 -107
  39. package/dist/dev/server.d.ts +0 -75
  40. package/dist/dev/server.js +0 -443
  41. package/dist/dev/upload_relay.d.ts +0 -36
  42. package/dist/dev/upload_relay.js +0 -61
  43. package/dist/dev/upload_relay.test.d.ts +0 -1
  44. package/dist/dev/upload_relay.test.js +0 -76
  45. package/dist/dev/wrapper_page.d.ts +0 -40
  46. package/dist/dev/wrapper_page.js +0 -335
  47. package/dist/docx.d.ts +0 -2
  48. package/dist/docx.js +0 -341
  49. package/dist/docx.test.d.ts +0 -1
  50. package/dist/docx.test.js +0 -145
  51. package/dist/file_command_io.d.ts +0 -12
  52. package/dist/file_command_io.js +0 -34
  53. package/dist/generate_app_agents_dts.d.ts +0 -16
  54. package/dist/generate_app_agents_dts.js +0 -62
  55. package/dist/generate_app_agents_dts.test.d.ts +0 -1
  56. package/dist/generate_app_agents_dts.test.js +0 -45
  57. package/dist/generate_app_fields.d.ts +0 -56
  58. package/dist/generate_app_fields.js +0 -148
  59. package/dist/generate_app_fields.test.d.ts +0 -1
  60. package/dist/generate_app_fields.test.js +0 -108
  61. package/dist/generate_app_queries_dts.d.ts +0 -21
  62. package/dist/generate_app_queries_dts.js +0 -57
  63. package/dist/generate_app_workflows_dts.d.ts +0 -39
  64. package/dist/generate_app_workflows_dts.js +0 -229
  65. package/dist/generate_app_workflows_dts.test.d.ts +0 -1
  66. package/dist/generate_app_workflows_dts.test.js +0 -109
  67. package/dist/generate_package_fields.d.ts +0 -49
  68. package/dist/generate_package_fields.js +0 -157
  69. package/dist/generate_package_fields.test.d.ts +0 -1
  70. package/dist/generate_package_fields.test.js +0 -59
  71. package/dist/inputs.d.ts +0 -38
  72. package/dist/inputs.js +0 -50
  73. package/dist/inputs.test.d.ts +0 -1
  74. package/dist/inputs.test.js +0 -89
  75. package/dist/preview.d.ts +0 -3
  76. package/dist/preview.js +0 -233
  77. package/dist/starter_template.d.ts +0 -64
  78. package/dist/starter_template.js +0 -753
  79. package/dist/starter_template.test.d.ts +0 -1
  80. package/dist/starter_template.test.js +0 -115
  81. package/dist/version.d.ts +0 -1
  82. package/dist/version.js +0 -6
  83. package/dist/xlsx.d.ts +0 -2
  84. package/dist/xlsx.js +0 -489
  85. package/dist/xlsx.test.d.ts +0 -1
  86. package/dist/xlsx.test.js +0 -131
  87. /package/dist/{client.js → src/client.js} +0 -0
@@ -1,335 +0,0 @@
1
- /**
2
- * Builds the wrapper HTML for `lotics app dev`.
3
- *
4
- * The wrapper page is served at http://localhost:<port>/ and embeds the
5
- * project's Vite dev server in an iframe with the same
6
- * sandbox="allow-scripts allow-same-origin allow-downloads" attribute
7
- * production uses — safe because the Vite iframe is cross-origin to this
8
- * wrapper (different port), so the app gets its own real origin (and Web
9
- * Storage) without being able to reach the wrapper. `allow-downloads` lets
10
- * apps trigger file downloads (generated xlsx / pdf etc.) — Chrome blocks
11
- * every download path from sandboxed iframes without it. `allow="...; geolocation;
12
- * web-share"` delegates the host's location permission (geofenced actions read the
13
- * device position) and grants the Web Share API (apps call `navigator.share({ files })`
14
- * to push e.g. container photos to the OS share sheet / gallery) — matching
15
- * production. The iframe sends postMessage RPCs to
16
- * this wrapper, which forwards them to the local /_rpc endpoint, which
17
- * dispatches to api.lotics.ai with the CLI's API key.
18
- *
19
- * postMessage is origin-locked both ways — the wrapper passes its origin
20
- * via `?lotics_host=` and talks only to the Vite origin.
21
- *
22
- * Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
23
- * iframe → wrapper: { id: number, op: string, payload: unknown }
24
- * wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
25
- * streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
26
- * the iframe aborts with { id, type: "abort" }.
27
- * urlState (useUrlState): the iframe reads/writes the wrapper's address bar via
28
- * urlState.get/set; set writes in place (replaceState), and browser
29
- * back/forward broadcast { type: "url-state", params } back to the iframe.
30
- * (In-app routing isn't here — the app owns the iframe's own url; see
31
- * @lotics/app-sdk/router.)
32
- */
33
- export function buildWrapperPage(args) {
34
- const { app_name, app_id, workspace_id, vite_url, api_url } = args;
35
- return `<!doctype html>
36
- <html lang="en">
37
- <head>
38
- <meta charset="UTF-8" />
39
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
40
- <title>${escapeHtml(app_name)} — lotics app dev</title>
41
- <style>
42
- html, body { margin: 0; height: 100%; }
43
- body {
44
- display: flex; flex-direction: column;
45
- font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
46
- background: #f4f4f5;
47
- }
48
- header {
49
- padding: 8px 14px;
50
- background: #18181b;
51
- color: #fafafa;
52
- font-size: 12px;
53
- display: flex; align-items: center; gap: 12px;
54
- flex-shrink: 0;
55
- }
56
- header strong { font-size: 13px; }
57
- header code {
58
- font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
59
- font-size: 11px;
60
- background: rgba(255,255,255,0.08);
61
- padding: 1px 6px; border-radius: 3px;
62
- }
63
- header span { color: #a1a1aa; }
64
- iframe { border: 0; flex: 1; width: 100%; background: #fff; }
65
- </style>
66
- </head>
67
- <body>
68
- <header>
69
- <strong>${escapeHtml(app_name)}</strong>
70
- <span>app_id <code>${escapeHtml(app_id)}</code></span>
71
- <span>workspace <code>${escapeHtml(workspace_id)}</code></span>
72
- <span>RPC → <code>${escapeHtml(api_url)}</code></span>
73
- </header>
74
- <iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write; geolocation; web-share"></iframe>
75
- <script>
76
- (function () {
77
- const APP_ID = ${JSON.stringify(app_id)};
78
- const VITE_URL = ${JSON.stringify(vite_url)};
79
- const VITE_ORIGIN = new URL(VITE_URL).origin;
80
- const iframe = document.getElementById("app");
81
-
82
- // Pass the wrapper's own origin to the app via ?lotics_host= so the app
83
- // SDK can origin-lock its postMessage bridge, and bake the saved screen
84
- // (the wrapper url's _loc, kept current by AppRouter's mirror) into the src
85
- // path so a refresh boots the app at that screen — mirrors the production
86
- // host. _loc is honoured only when it resolves same-origin to the Vite
87
- // server (a url param flowing into an iframe src is a redirect vector);
88
- // anything else falls back to the app root.
89
- const appSrc = new URL(VITE_URL);
90
- const savedLoc = new URLSearchParams(window.location.search).get("_loc");
91
- if (savedLoc) {
92
- try {
93
- const resolved = new URL(savedLoc, VITE_ORIGIN);
94
- if (resolved.origin === VITE_ORIGIN) {
95
- appSrc.pathname = resolved.pathname;
96
- appSrc.search = resolved.search;
97
- appSrc.hash = resolved.hash;
98
- }
99
- } catch (e) {
100
- // malformed _loc -> app root
101
- }
102
- }
103
- appSrc.searchParams.set("lotics_host", window.location.origin);
104
- iframe.src = appSrc.toString();
105
-
106
- async function rpc(op, payload) {
107
- const res = await fetch("/_rpc", {
108
- method: "POST",
109
- headers: { "content-type": "application/json" },
110
- body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
111
- });
112
- const text = await res.text();
113
- if (!res.ok) {
114
- let detail = text;
115
- try { detail = JSON.parse(text).message ?? text; } catch (_) {}
116
- throw new Error(detail || ("HTTP " + res.status));
117
- }
118
- return JSON.parse(text);
119
- }
120
-
121
- // The iframe SDK sends one "upload" op carrying a File. A File can't
122
- // cross the JSON /_rpc hop, so the upload runs here in the browser:
123
- // mint an upload URL, PUT the bytes, then finalize.
124
- //
125
- // In dev the minted URL is same-origin (/_upload/<file_id>) — the dev
126
- // server relays the bytes to storage on our behalf, because the prod
127
- // bucket's CORS does not admit a localhost origin. Production PUTs the
128
- // presigned storage URL directly. Same three lines either way: the page
129
- // PUTs wherever the mint points.
130
- //
131
- // Retry policy parity with the production SDK
132
- // (packages/app-sdk/src/upload/transport.ts): 3 PUT attempts with
133
- // 1s/2s/4s backoff on 5xx + network errors. 4xx is terminal. Image
134
- // compression is intentionally NOT inlined here — dev networks are
135
- // stable enough, and end-user mobile cases are handled by the SDK
136
- // and bridged host directly.
137
- async function putWithRetry(url, file) {
138
- const MAX_ATTEMPTS = 3;
139
- let lastError;
140
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
141
- try {
142
- const res = await fetch(url, {
143
- method: "PUT",
144
- body: file,
145
- headers: { "Content-Type": file.type },
146
- });
147
- if (res.ok) return res;
148
- // 4xx is terminal — retrying a malformed/expired URL is pointless.
149
- if (res.status >= 400 && res.status < 500) return res;
150
- lastError = new Error("Storage PUT returned " + res.status);
151
- } catch (err) {
152
- lastError = err;
153
- }
154
- if (attempt < MAX_ATTEMPTS) {
155
- const delayMs = 1000 * Math.pow(2, attempt - 1);
156
- await new Promise(function (r) { setTimeout(r, delayMs); });
157
- }
158
- }
159
- throw lastError || new Error("Storage PUT failed");
160
- }
161
-
162
- async function handleUpload(payload) {
163
- const file = payload && payload.file;
164
- if (!(file instanceof File)) {
165
- throw new Error("upload payload must include a File");
166
- }
167
- const init = await rpc("upload_url", {
168
- filename: file.name,
169
- mime_type: file.type,
170
- file_size: file.size,
171
- });
172
- const putRes = await putWithRetry(init.upload_url, file);
173
- if (!putRes.ok) {
174
- throw new Error("Storage upload failed (" + putRes.status + ")");
175
- }
176
- const completed = await rpc("upload_complete", {
177
- file_id: init.file_id,
178
- file_storage_key: init.file_storage_key,
179
- filename: file.name,
180
- });
181
- return completed.file;
182
- }
183
-
184
- // openExternal — the wrapper page is the top frame (un-sandboxed), so it
185
- // opens the link directly; NOT forwarded to the RPC endpoint (no server
186
- // op). Re-validate the scheme; never trust the iframe's payload.
187
- function handleOpenExternal(payload) {
188
- var url = payload && payload.url;
189
- if (typeof url !== "string") throw new Error("openExternal requires a url string");
190
- var parsed = new URL(url);
191
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
192
- throw new Error('openExternal: unsupported URL scheme "' + parsed.protocol + '"');
193
- }
194
- window.open(parsed.href, "_blank", "noopener,noreferrer");
195
- return undefined;
196
- }
197
-
198
- // urlState.get/set — useUrlState keeps app view-state (filters, search) in
199
- // THIS wrapper page's address bar so it survives refresh and is shareable,
200
- // mirroring the production host. Handled locally (the URL is the store; no
201
- // API hop). The wrapper page is its own top-level page, so its query string
202
- // is entirely the app's. Repeated keys become arrays. Parse/merge MUST
203
- // match @lotics/app-sdk parseSearch/serializeMerge and the production host
204
- // (inline JS here — no module system to share the trivial copy).
205
- function readUrlParams() {
206
- const sp = new URLSearchParams(window.location.search);
207
- const out = {};
208
- sp.forEach(function (_value, key) {
209
- if (key in out) return;
210
- const all = sp.getAll(key);
211
- out[key] = all.length > 1 ? all : all[0];
212
- });
213
- return out;
214
- }
215
- function handleUrlStateSet(payload) {
216
- const params = (payload && payload.params) || {};
217
- const sp = new URLSearchParams(window.location.search);
218
- Object.keys(params).forEach(function (key) {
219
- sp.delete(key);
220
- const value = params[key];
221
- if (value === undefined || value === null) return;
222
- if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
223
- else sp.append(key, value);
224
- });
225
- const qs = sp.toString();
226
- const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
227
- // View-state writes in place — never a history entry. replaceState doesn't
228
- // fire popstate, so no echo. Mirrors the production host.
229
- window.history.replaceState(null, "", url);
230
- return undefined;
231
- }
232
-
233
- // Streaming agent runs (op "agentRun"): the response is a stream, so it
234
- // can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
235
- // body, and forward chunks to the iframe as the SDK's bridged streaming
236
- // protocol expects (stream-chunk* -> stream-end). Mirrors the production
237
- // host in frontend/features/app_ui/app_iframe_host.tsx.
238
- const streamingRuns = {};
239
- async function handleAgentRun(id, payload) {
240
- const controller = new AbortController();
241
- streamingRuns[id] = controller;
242
- try {
243
- const res = await fetch("/_agent_run", {
244
- method: "POST",
245
- headers: { "content-type": "application/json" },
246
- body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
247
- signal: controller.signal,
248
- });
249
- if (!res.ok || !res.body) {
250
- let detail = await res.text().catch(function () { return ""; });
251
- try { detail = JSON.parse(detail).message || detail; } catch (_) {}
252
- throw new Error(detail || ("HTTP " + res.status));
253
- }
254
- const reader = res.body.getReader();
255
- const decoder = new TextDecoder();
256
- for (;;) {
257
- const r = await reader.read();
258
- if (r.done) break;
259
- iframe.contentWindow.postMessage(
260
- { id: id, type: "stream-chunk", chunk: decoder.decode(r.value, { stream: true }) },
261
- VITE_ORIGIN
262
- );
263
- }
264
- const tail = decoder.decode();
265
- if (tail) iframe.contentWindow.postMessage({ id: id, type: "stream-chunk", chunk: tail }, VITE_ORIGIN);
266
- iframe.contentWindow.postMessage({ id: id, type: "stream-end" }, VITE_ORIGIN);
267
- } catch (err) {
268
- if (controller.signal.aborted) return;
269
- const message = err && err.message ? err.message : String(err);
270
- iframe.contentWindow.postMessage({ id: id, type: "error", message: message }, VITE_ORIGIN);
271
- } finally {
272
- delete streamingRuns[id];
273
- }
274
- }
275
-
276
- window.addEventListener("message", async function (event) {
277
- if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
278
- const msg = event.data;
279
- if (!msg || typeof msg.id !== "number") return;
280
- // Abort an in-flight streaming run (control message, carries no op).
281
- if (msg.type === "abort") {
282
- if (streamingRuns[msg.id]) { streamingRuns[msg.id].abort(); delete streamingRuns[msg.id]; }
283
- return;
284
- }
285
- if (typeof msg.op !== "string") return;
286
- // Streaming agent run — many messages back, not a single result.
287
- if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
288
- const startedAt = performance.now();
289
- try {
290
- const data = msg.op === "upload"
291
- ? await handleUpload(msg.payload)
292
- : msg.op === "openExternal"
293
- ? handleOpenExternal(msg.payload)
294
- : msg.op === "urlState.get"
295
- ? readUrlParams()
296
- : msg.op === "urlState.set"
297
- ? handleUrlStateSet(msg.payload)
298
- : await rpc(msg.op, msg.payload);
299
- const ms = Math.round(performance.now() - startedAt);
300
- console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
301
- iframe.contentWindow.postMessage(
302
- { id: msg.id, type: "result", data: data },
303
- VITE_ORIGIN
304
- );
305
- } catch (err) {
306
- const message = err && err.message ? err.message : String(err);
307
- console.error("[lotics-dev] " + msg.op + " failed:", message);
308
- iframe.contentWindow.postMessage(
309
- { id: msg.id, type: "error", message: message },
310
- VITE_ORIGIN
311
- );
312
- }
313
- });
314
-
315
- // Browser back/forward → broadcast the new params so useUrlState re-hydrates
316
- // (the app's own set writes use replaceState — no popstate — so there's no
317
- // echo). Mirrors the production host.
318
- window.addEventListener("popstate", function () {
319
- iframe.contentWindow.postMessage(
320
- { type: "url-state", params: readUrlParams() },
321
- VITE_ORIGIN
322
- );
323
- });
324
- })();
325
- </script>
326
- </body>
327
- </html>
328
- `;
329
- }
330
- function escapeHtml(s) {
331
- return s
332
- .replace(/&/g, "&amp;")
333
- .replace(/</g, "&lt;")
334
- .replace(/>/g, "&gt;");
335
- }
package/dist/docx.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare function printDocxHelp(): void;
2
- export declare function runDocxCommand(subcommand: string | undefined, toolArgs: string | undefined, restArgs: string[]): Promise<void>;
package/dist/docx.js DELETED
@@ -1,341 +0,0 @@
1
- import fs from "node:fs";
2
- import { fail, writeFileAtomic } from "./file_command_io.js";
3
- import { parseDocx, serializeDocx, } from "@lotics/docx";
4
- const DEFAULT_DOC_ATTRS = {
5
- "@_xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
6
- };
7
- async function loadFile(filePath) {
8
- if (!fs.existsSync(filePath))
9
- fail(`File not found: ${filePath}`);
10
- const bytes = fs.readFileSync(filePath);
11
- return parseDocx(new Uint8Array(bytes));
12
- }
13
- async function writeDoc(filePath, doc) {
14
- const bytes = await serializeDocx(doc);
15
- writeFileAtomic(filePath, bytes);
16
- }
17
- // =============================================================================
18
- // Block construction (typed, immutable)
19
- // =============================================================================
20
- function buildParagraph(text, style) {
21
- const properties = style ? [buildPStyle(style)] : null;
22
- const content = [buildRun(text)];
23
- return { kind: "paragraph", properties, content };
24
- }
25
- function buildPStyle(styleId) {
26
- return { "w:pStyle": [], ":@": { "@_w:val": styleId } };
27
- }
28
- function buildRun(text) {
29
- // Split on \n so newlines render as Word line breaks (<w:br/>) instead of
30
- // collapsing into a single line — multi-line agent input is common.
31
- const segments = text.split("\n");
32
- const content = [];
33
- for (let i = 0; i < segments.length; i++) {
34
- const value = segments[i];
35
- if (value !== "") {
36
- content.push({
37
- kind: "text",
38
- value,
39
- preserveSpace: value.startsWith(" ") || value.endsWith(" "),
40
- });
41
- }
42
- if (i < segments.length - 1) {
43
- content.push({ kind: "opaque_run_child", xml: { "w:br": [] } });
44
- }
45
- }
46
- if (content.length === 0)
47
- content.push({ kind: "text", value: "", preserveSpace: false });
48
- return { kind: "run", properties: null, content };
49
- }
50
- function replaceChildren(doc, children) {
51
- return {
52
- parts: doc.parts,
53
- documentAttrs: doc.documentAttrs,
54
- body: { children },
55
- };
56
- }
57
- // =============================================================================
58
- // Read: project body to a simplified shape
59
- // =============================================================================
60
- function readToJson(doc, includeOpaque) {
61
- const blocks = doc.body.children.map((block, index) => projectBlock(block, index, includeOpaque));
62
- return { documentAttrs: doc.documentAttrs, blocks };
63
- }
64
- function projectBlock(block, index, includeOpaque) {
65
- switch (block.kind) {
66
- case "paragraph":
67
- return {
68
- kind: "paragraph",
69
- index,
70
- text: extractText(block),
71
- style: extractStyleId(block.properties),
72
- };
73
- case "body_sect_pr":
74
- return includeOpaque
75
- ? { kind: "body_sect_pr", index, xml: block.xml }
76
- : { kind: "body_sect_pr", index };
77
- case "opaque_block":
78
- return includeOpaque
79
- ? { kind: "opaque_block", index, tag: firstTagName(block.xml), xml: block.xml }
80
- : { kind: "opaque_block", index, tag: firstTagName(block.xml) };
81
- }
82
- }
83
- function extractText(para) {
84
- let out = "";
85
- for (const inline of para.content) {
86
- if (inline.kind !== "run")
87
- continue;
88
- for (const child of inline.content) {
89
- if (child.kind === "text")
90
- out += child.value;
91
- else if (child.kind === "opaque_run_child" && firstTagName(child.xml) === "w:br")
92
- out += "\n";
93
- }
94
- }
95
- return out;
96
- }
97
- function extractStyleId(properties) {
98
- if (!properties)
99
- return undefined;
100
- for (const prop of properties) {
101
- if (firstTagName(prop) !== "w:pStyle")
102
- continue;
103
- const attrs = prop[":@"];
104
- if (attrs?.["@_w:val"])
105
- return attrs["@_w:val"];
106
- }
107
- return undefined;
108
- }
109
- function firstTagName(el) {
110
- for (const key of Object.keys(el)) {
111
- if (key !== ":@" && key !== "#text")
112
- return key;
113
- }
114
- return undefined;
115
- }
116
- // =============================================================================
117
- // Write: build DocxDocument from a SimpleDoc
118
- // =============================================================================
119
- function buildDoc(input) {
120
- if (!input || !Array.isArray(input.blocks))
121
- fail("docx write JSON must include a `blocks` array");
122
- const children = [];
123
- for (let i = 0; i < input.blocks.length; i++) {
124
- const b = input.blocks[i];
125
- if (!b || typeof b !== "object")
126
- fail(`blocks[${i}] must be an object`);
127
- if (b.kind === "paragraph") {
128
- if (typeof b.text !== "string")
129
- fail(`blocks[${i}].text must be a string`);
130
- if (b.style !== undefined && typeof b.style !== "string")
131
- fail(`blocks[${i}].style must be a string`);
132
- children.push(buildParagraph(b.text, b.style));
133
- }
134
- else if (b.kind === "opaque_block") {
135
- fail(`blocks[${i}]: opaque_block cannot be created from JSON; read+modify an existing .docx instead`);
136
- }
137
- else {
138
- const kind = b.kind;
139
- fail(`blocks[${i}]: unknown kind ${JSON.stringify(kind)} (expected "paragraph")`);
140
- }
141
- }
142
- const documentAttrs = { ...DEFAULT_DOC_ATTRS, ...(input.documentAttrs ?? {}) };
143
- return { parts: new Map(), documentAttrs, body: { children } };
144
- }
145
- // =============================================================================
146
- // Subcommand handlers
147
- // =============================================================================
148
- async function docxRead(filePath, rest) {
149
- if (!filePath)
150
- fail("Usage: lotics docx read <file> [--include-opaque]");
151
- const includeOpaque = rest.includes("--include-opaque");
152
- const doc = await loadFile(filePath);
153
- console.log(JSON.stringify(readToJson(doc, includeOpaque), null, 2));
154
- }
155
- async function docxWrite(filePath, json) {
156
- if (!filePath || !json)
157
- fail("Usage: lotics docx write <file> '<json>'");
158
- let parsed;
159
- try {
160
- parsed = JSON.parse(json);
161
- }
162
- catch (e) {
163
- fail(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
164
- }
165
- const doc = buildDoc(parsed);
166
- await writeDoc(filePath, doc);
167
- }
168
- function getStyleFlag(rest) {
169
- for (const arg of rest) {
170
- if (arg.startsWith("--style="))
171
- return arg.slice("--style=".length);
172
- }
173
- return undefined;
174
- }
175
- function getAtFlag(rest) {
176
- for (const arg of rest) {
177
- if (arg.startsWith("--at=")) {
178
- const n = parseInt(arg.slice("--at=".length), 10);
179
- return Number.isFinite(n) ? n : undefined;
180
- }
181
- }
182
- return undefined;
183
- }
184
- function firstPositional(rest) {
185
- return rest.find((a) => !a.startsWith("--"));
186
- }
187
- async function docxAppendParagraph(filePath, rest) {
188
- const text = firstPositional(rest);
189
- if (!filePath || text === undefined)
190
- fail("Usage: lotics docx append-paragraph <file> '<text>' [--style=NAME]");
191
- const style = getStyleFlag(rest);
192
- const doc = await loadFile(filePath);
193
- const para = buildParagraph(text, style);
194
- const newChildren = [...doc.body.children, para];
195
- await writeDoc(filePath, replaceChildren(doc, newChildren));
196
- }
197
- async function docxInsertParagraph(filePath, rest) {
198
- const text = firstPositional(rest);
199
- const at = getAtFlag(rest);
200
- if (!filePath || text === undefined || at === undefined) {
201
- fail("Usage: lotics docx insert-paragraph <file> '<text>' --at=<index> [--style=NAME]");
202
- }
203
- const style = getStyleFlag(rest);
204
- const doc = await loadFile(filePath);
205
- if (at < 0 || at > doc.body.children.length) {
206
- fail(`--at=${at} out of range (0..${doc.body.children.length})`);
207
- }
208
- const para = buildParagraph(text, style);
209
- const newChildren = [...doc.body.children];
210
- newChildren.splice(at, 0, para);
211
- await writeDoc(filePath, replaceChildren(doc, newChildren));
212
- }
213
- async function docxDeleteBlock(filePath, rest) {
214
- const at = getAtFlag(rest);
215
- if (!filePath || at === undefined)
216
- fail("Usage: lotics docx delete-block <file> --at=<index>");
217
- const doc = await loadFile(filePath);
218
- if (at < 0 || at >= doc.body.children.length) {
219
- fail(`--at=${at} out of range (0..${doc.body.children.length - 1})`);
220
- }
221
- const newChildren = doc.body.children.filter((_, i) => i !== at);
222
- await writeDoc(filePath, replaceChildren(doc, newChildren));
223
- }
224
- async function docxReplaceText(filePath, rest) {
225
- const [search, replace] = rest;
226
- if (!filePath || !search || replace === undefined) {
227
- fail("Usage: lotics docx replace-text <file> '<search>' '<replace>'");
228
- }
229
- const doc = await loadFile(filePath);
230
- const newChildren = doc.body.children.map((b) => (b.kind === "paragraph" ? replaceInParagraph(b, search, replace) : b));
231
- await writeDoc(filePath, replaceChildren(doc, newChildren));
232
- }
233
- function replaceInParagraph(p, search, replace) {
234
- const content = p.content.map((inline) => {
235
- if (inline.kind !== "run")
236
- return inline;
237
- const newChildren = inline.content.map((c) => c.kind === "text"
238
- ? { kind: "text", value: c.value.split(search).join(replace), preserveSpace: c.preserveSpace }
239
- : c);
240
- return { kind: "run", properties: inline.properties, content: newChildren };
241
- });
242
- return { kind: "paragraph", properties: p.properties, content };
243
- }
244
- async function docxBatch(filePath, json) {
245
- if (!filePath || !json)
246
- fail("Usage: lotics docx batch <file> '<json-array-of-ops>'");
247
- let ops;
248
- try {
249
- ops = JSON.parse(json);
250
- }
251
- catch (e) {
252
- fail(`Invalid batch JSON: ${e instanceof Error ? e.message : String(e)}`);
253
- }
254
- if (!Array.isArray(ops))
255
- fail("Batch must be a JSON array of ops");
256
- for (let i = 0; i < ops.length; i++) {
257
- const op = ops[i];
258
- if (!op || typeof op !== "object" || typeof op.op !== "string") {
259
- fail(`[op ${i}] each op must be an object with an "op" string field`);
260
- }
261
- }
262
- let doc = await loadFile(filePath);
263
- for (let i = 0; i < ops.length; i++) {
264
- doc = applyBatchOp(doc, ops[i], i);
265
- }
266
- await writeDoc(filePath, doc);
267
- }
268
- function applyBatchOp(doc, op, index) {
269
- switch (op.op) {
270
- case "append-paragraph": {
271
- const para = buildParagraph(op.text, op.style);
272
- return replaceChildren(doc, [...doc.body.children, para]);
273
- }
274
- case "insert-paragraph": {
275
- if (op.at < 0 || op.at > doc.body.children.length) {
276
- fail(`[op ${index}] at=${op.at} out of range`);
277
- }
278
- const next = [...doc.body.children];
279
- next.splice(op.at, 0, buildParagraph(op.text, op.style));
280
- return replaceChildren(doc, next);
281
- }
282
- case "delete-block": {
283
- if (op.at < 0 || op.at >= doc.body.children.length) {
284
- fail(`[op ${index}] at=${op.at} out of range`);
285
- }
286
- return replaceChildren(doc, doc.body.children.filter((_, i) => i !== op.at));
287
- }
288
- case "replace-text": {
289
- const next = doc.body.children.map((b) => (b.kind === "paragraph" ? replaceInParagraph(b, op.search, op.replace) : b));
290
- return replaceChildren(doc, next);
291
- }
292
- default: {
293
- const exhaustive = op;
294
- void exhaustive;
295
- fail(`[op ${index}] Unknown op: ${JSON.stringify(op)}`);
296
- }
297
- }
298
- }
299
- // =============================================================================
300
- // Dispatch + help
301
- // =============================================================================
302
- export function printDocxHelp() {
303
- console.error(`Lotics docx commands — manipulate .docx files in place.
304
- Uses Lotics' own OOXML engine; round-trips faithfully with the Lotics editor and templates.
305
-
306
- lotics docx read <file> [--include-opaque] Dump file as JSON (blocks with text/style)
307
- lotics docx write <file> '<json>' Create .docx from {"blocks":[{"kind":"paragraph","text":...}]}
308
- lotics docx append-paragraph <file> '<text>' [--style=NAME]
309
- Append a paragraph (styles: Title, Heading1..3, Quote, Code)
310
- lotics docx insert-paragraph <file> '<text>' --at=<i> [--style=NAME]
311
- Insert a paragraph at index <i> (0-based)
312
- lotics docx delete-block <file> --at=<i> Remove the block at index <i>
313
- lotics docx replace-text <file> '<search>' '<replace>' Replace text in all paragraph runs
314
- lotics docx batch <file> '<json-array-of-ops>' Apply many ops in one parse/serialize cycle
315
-
316
- Edit ops mutate the file atomically (temp file + rename). Opaque blocks (tables, custom XML) are preserved verbatim.`);
317
- }
318
- export async function runDocxCommand(subcommand, toolArgs, restArgs) {
319
- switch (subcommand) {
320
- case "read":
321
- return docxRead(toolArgs, restArgs);
322
- case "write":
323
- return docxWrite(toolArgs, restArgs[0]);
324
- case "append-paragraph":
325
- return docxAppendParagraph(toolArgs, restArgs);
326
- case "insert-paragraph":
327
- return docxInsertParagraph(toolArgs, restArgs);
328
- case "delete-block":
329
- return docxDeleteBlock(toolArgs, restArgs);
330
- case "replace-text":
331
- return docxReplaceText(toolArgs, restArgs);
332
- case "batch":
333
- return docxBatch(toolArgs, restArgs[0]);
334
- default: {
335
- if (subcommand)
336
- console.error(`Unknown docx subcommand: ${subcommand}\n`);
337
- printDocxHelp();
338
- process.exit(subcommand ? 1 : 0);
339
- }
340
- }
341
- }
@@ -1 +0,0 @@
1
- export {};