@lotics/cli 0.88.0 → 0.89.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.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +162 -100
- package/dist/{client.d.ts → src/client.d.ts} +9 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /package/dist/{client.js → src/client.js} +0 -0
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { generatePackageAppFields } from "./generate_package_fields.js";
|
|
3
|
-
// A live installation binding — fully-qualified alias → concrete id. The
|
|
4
|
-
// generator sources the alias SET here (never a contract file); the emitted
|
|
5
|
-
// module resolves the SAME keys through `getAppBinding()` at runtime.
|
|
6
|
-
const binding = {
|
|
7
|
-
fields: {
|
|
8
|
-
"tasks.title": "fld_title",
|
|
9
|
-
"tasks.status": "fld_status",
|
|
10
|
-
"projects.name": "fld_name",
|
|
11
|
-
},
|
|
12
|
-
options: {
|
|
13
|
-
"tasks.status:to_do": "opt_todo",
|
|
14
|
-
"tasks.status:doing": "opt_doing",
|
|
15
|
-
"tasks.status:done": "opt_done",
|
|
16
|
-
},
|
|
17
|
-
roles: { approver: "grp_approver" },
|
|
18
|
-
};
|
|
19
|
-
describe("generatePackageAppFields", () => {
|
|
20
|
-
it("emits runtime-resolved F/OPT/ROLE keyed by the binding's aliases", () => {
|
|
21
|
-
const src = generatePackageAppFields(binding);
|
|
22
|
-
// Resolves through the SDK binding at module load (top-level await).
|
|
23
|
-
expect(src).toContain('import { getAppBinding } from "@lotics/app-sdk";');
|
|
24
|
-
expect(src).toContain("const binding = await getAppBinding();");
|
|
25
|
-
// F: uppercased entity alias, fully-qualified binding keys.
|
|
26
|
-
expect(src).toContain('title: bound(binding.fields, "tasks.title", "field")');
|
|
27
|
-
expect(src).toContain('name: bound(binding.fields, "projects.name", "field")');
|
|
28
|
-
expect(src).toMatch(/TASKS: \{/);
|
|
29
|
-
expect(src).toMatch(/PROJECTS: \{/);
|
|
30
|
-
// OPT: qualified entity.field:opt keys; option-less fields absent.
|
|
31
|
-
expect(src).toContain('to_do: bound(binding.options, "tasks.status:to_do", "option")');
|
|
32
|
-
expect(src).toContain('done: bound(binding.options, "tasks.status:done", "option")');
|
|
33
|
-
expect(src).not.toContain('"tasks.title:');
|
|
34
|
-
// ROLE map from the binding's roles.
|
|
35
|
-
expect(src).toContain('approver: bound(binding.roles, "approver", "role")');
|
|
36
|
-
// No concrete id in any VALUE position — the module is workspace-agnostic.
|
|
37
|
-
// (Doc comments legitimately name the id prefixes, so match quoted ids.)
|
|
38
|
-
expect(src).not.toMatch(/"(fld|opt|tbl|grp)_/);
|
|
39
|
-
});
|
|
40
|
-
it("emits valid empty maps for an empty binding", () => {
|
|
41
|
-
const src = generatePackageAppFields({ fields: {}, options: {}, roles: {} });
|
|
42
|
-
expect(src).toContain("export const F = {} as const;");
|
|
43
|
-
expect(src).toContain("export const OPT = {} as const;");
|
|
44
|
-
expect(src).toContain("export const ROLE = {} as const;");
|
|
45
|
-
// Still imports the SDK so the module shape is uniform.
|
|
46
|
-
expect(src).toContain("const binding = await getAppBinding();");
|
|
47
|
-
});
|
|
48
|
-
it("quotes an alias that is not a valid identifier", () => {
|
|
49
|
-
const src = generatePackageAppFields({
|
|
50
|
-
fields: { "tasks.2nd_field": "fld_x" },
|
|
51
|
-
options: {},
|
|
52
|
-
roles: {},
|
|
53
|
-
});
|
|
54
|
-
expect(src).toContain('"2nd_field": bound(binding.fields, "tasks.2nd_field", "field")');
|
|
55
|
-
});
|
|
56
|
-
it("is deterministic — same binding, same bytes", () => {
|
|
57
|
-
expect(generatePackageAppFields(binding)).toBe(generatePackageAppFields(binding));
|
|
58
|
-
});
|
|
59
|
-
});
|
package/dist/inputs.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared JSON-args ingestion for `lotics run` and `lotics app workflow`.
|
|
3
|
-
*
|
|
4
|
-
* A tool/workflow payload can arrive three ways, all of which bypass the OS
|
|
5
|
-
* `ARG_MAX` limit so bulk inputs (a knowledge-doc `content`, a batch update)
|
|
6
|
-
* don't overflow the inline arg:
|
|
7
|
-
* - inline: `… '<json>'`
|
|
8
|
-
* - `@file`: `… @args.json` (a leading `@` is unambiguous — JSON starts `{`)
|
|
9
|
-
* - stdin: `… < args.json` or `cat args.json | …`
|
|
10
|
-
*
|
|
11
|
-
* Kept apart from `cli.ts` (which runs `main()` on import) so the file/JSON
|
|
12
|
-
* branches are unit-testable; `readStdin` is injected so the tests don't touch
|
|
13
|
-
* the real process stdin.
|
|
14
|
-
*/
|
|
15
|
-
export type IngestResult = {
|
|
16
|
-
kind: "ok";
|
|
17
|
-
args: Record<string, unknown>;
|
|
18
|
-
} | {
|
|
19
|
-
kind: "error";
|
|
20
|
-
message: string;
|
|
21
|
-
};
|
|
22
|
-
export interface IngestOptions {
|
|
23
|
-
/** The positional arg as parsed (`'<json>'`, `@file`, or undefined). */
|
|
24
|
-
rawArg: string | undefined;
|
|
25
|
-
/** Whether stdin is a TTY — when false and no rawArg, read piped stdin. */
|
|
26
|
-
stdinIsTTY: boolean;
|
|
27
|
-
/** Reads a UTF-8 file. Injected for testability. */
|
|
28
|
-
readFile: (path: string) => string;
|
|
29
|
-
/** Reads all of stdin. Injected for testability. */
|
|
30
|
-
readStdin: () => Promise<string>;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Resolve the raw payload string, then JSON.parse it. An absent payload (no
|
|
34
|
-
* arg, no piped stdin) is a valid empty `{}` — some tools/workflows take no
|
|
35
|
-
* inputs. A `@file` read failure or invalid JSON is a typed error the caller
|
|
36
|
-
* surfaces; this function never throws or exits.
|
|
37
|
-
*/
|
|
38
|
-
export declare function ingestJsonArgs(opts: IngestOptions): Promise<IngestResult>;
|
package/dist/inputs.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared JSON-args ingestion for `lotics run` and `lotics app workflow`.
|
|
3
|
-
*
|
|
4
|
-
* A tool/workflow payload can arrive three ways, all of which bypass the OS
|
|
5
|
-
* `ARG_MAX` limit so bulk inputs (a knowledge-doc `content`, a batch update)
|
|
6
|
-
* don't overflow the inline arg:
|
|
7
|
-
* - inline: `… '<json>'`
|
|
8
|
-
* - `@file`: `… @args.json` (a leading `@` is unambiguous — JSON starts `{`)
|
|
9
|
-
* - stdin: `… < args.json` or `cat args.json | …`
|
|
10
|
-
*
|
|
11
|
-
* Kept apart from `cli.ts` (which runs `main()` on import) so the file/JSON
|
|
12
|
-
* branches are unit-testable; `readStdin` is injected so the tests don't touch
|
|
13
|
-
* the real process stdin.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Resolve the raw payload string, then JSON.parse it. An absent payload (no
|
|
17
|
-
* arg, no piped stdin) is a valid empty `{}` — some tools/workflows take no
|
|
18
|
-
* inputs. A `@file` read failure or invalid JSON is a typed error the caller
|
|
19
|
-
* surfaces; this function never throws or exits.
|
|
20
|
-
*/
|
|
21
|
-
export async function ingestJsonArgs(opts) {
|
|
22
|
-
let raw = opts.rawArg;
|
|
23
|
-
if (raw && raw.startsWith("@")) {
|
|
24
|
-
const argsPath = raw.slice(1);
|
|
25
|
-
try {
|
|
26
|
-
raw = opts.readFile(argsPath);
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
return {
|
|
30
|
-
kind: "error",
|
|
31
|
-
message: `Cannot read args file "${argsPath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else if (!raw && !opts.stdinIsTTY) {
|
|
36
|
-
raw = await opts.readStdin();
|
|
37
|
-
}
|
|
38
|
-
if (!raw)
|
|
39
|
-
return { kind: "ok", args: {} };
|
|
40
|
-
try {
|
|
41
|
-
const parsed = JSON.parse(raw);
|
|
42
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
43
|
-
return { kind: "error", message: `JSON args must be an object, got: ${raw}` };
|
|
44
|
-
}
|
|
45
|
-
return { kind: "ok", args: parsed };
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return { kind: "error", message: `Invalid JSON: ${raw}` };
|
|
49
|
-
}
|
|
50
|
-
}
|
package/dist/inputs.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/inputs.test.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { ingestJsonArgs } from "./inputs.js";
|
|
3
|
-
const neverFile = () => {
|
|
4
|
-
throw new Error("readFile should not be called");
|
|
5
|
-
};
|
|
6
|
-
const neverStdin = async () => {
|
|
7
|
-
throw new Error("readStdin should not be called");
|
|
8
|
-
};
|
|
9
|
-
describe("ingestJsonArgs", () => {
|
|
10
|
-
it("parses an inline JSON object arg without touching file/stdin", async () => {
|
|
11
|
-
const r = await ingestJsonArgs({
|
|
12
|
-
rawArg: '{"table_id":"tbl_1"}',
|
|
13
|
-
stdinIsTTY: true,
|
|
14
|
-
readFile: neverFile,
|
|
15
|
-
readStdin: neverStdin,
|
|
16
|
-
});
|
|
17
|
-
expect(r).toEqual({ kind: "ok", args: { table_id: "tbl_1" } });
|
|
18
|
-
});
|
|
19
|
-
it("reads @file args (bypassing ARG_MAX)", async () => {
|
|
20
|
-
const r = await ingestJsonArgs({
|
|
21
|
-
rawArg: "@args.json",
|
|
22
|
-
stdinIsTTY: true,
|
|
23
|
-
readFile: (p) => {
|
|
24
|
-
expect(p).toBe("args.json");
|
|
25
|
-
return '{"big":"payload"}';
|
|
26
|
-
},
|
|
27
|
-
readStdin: neverStdin,
|
|
28
|
-
});
|
|
29
|
-
expect(r).toEqual({ kind: "ok", args: { big: "payload" } });
|
|
30
|
-
});
|
|
31
|
-
it("surfaces a typed error when the @file can't be read", async () => {
|
|
32
|
-
const r = await ingestJsonArgs({
|
|
33
|
-
rawArg: "@missing.json",
|
|
34
|
-
stdinIsTTY: true,
|
|
35
|
-
readFile: () => {
|
|
36
|
-
throw new Error("ENOENT");
|
|
37
|
-
},
|
|
38
|
-
readStdin: neverStdin,
|
|
39
|
-
});
|
|
40
|
-
expect(r.kind).toBe("error");
|
|
41
|
-
if (r.kind === "error")
|
|
42
|
-
expect(r.message).toContain("missing.json");
|
|
43
|
-
});
|
|
44
|
-
it("reads piped stdin when there's no arg and stdin is not a TTY", async () => {
|
|
45
|
-
const r = await ingestJsonArgs({
|
|
46
|
-
rawArg: undefined,
|
|
47
|
-
stdinIsTTY: false,
|
|
48
|
-
readFile: neverFile,
|
|
49
|
-
readStdin: async () => '{"from":"stdin"}',
|
|
50
|
-
});
|
|
51
|
-
expect(r).toEqual({ kind: "ok", args: { from: "stdin" } });
|
|
52
|
-
});
|
|
53
|
-
it("treats an absent payload (no arg, TTY stdin) as empty {}", async () => {
|
|
54
|
-
const r = await ingestJsonArgs({
|
|
55
|
-
rawArg: undefined,
|
|
56
|
-
stdinIsTTY: true,
|
|
57
|
-
readFile: neverFile,
|
|
58
|
-
readStdin: neverStdin,
|
|
59
|
-
});
|
|
60
|
-
expect(r).toEqual({ kind: "ok", args: {} });
|
|
61
|
-
});
|
|
62
|
-
it("errors on invalid JSON", async () => {
|
|
63
|
-
const r = await ingestJsonArgs({
|
|
64
|
-
rawArg: "{not json",
|
|
65
|
-
stdinIsTTY: true,
|
|
66
|
-
readFile: neverFile,
|
|
67
|
-
readStdin: neverStdin,
|
|
68
|
-
});
|
|
69
|
-
expect(r.kind).toBe("error");
|
|
70
|
-
if (r.kind === "error")
|
|
71
|
-
expect(r.message).toContain("Invalid JSON");
|
|
72
|
-
});
|
|
73
|
-
it("rejects a non-object top-level JSON (array / scalar)", async () => {
|
|
74
|
-
const arr = await ingestJsonArgs({
|
|
75
|
-
rawArg: "[1,2,3]",
|
|
76
|
-
stdinIsTTY: true,
|
|
77
|
-
readFile: neverFile,
|
|
78
|
-
readStdin: neverStdin,
|
|
79
|
-
});
|
|
80
|
-
expect(arr.kind).toBe("error");
|
|
81
|
-
const scalar = await ingestJsonArgs({
|
|
82
|
-
rawArg: "42",
|
|
83
|
-
stdinIsTTY: true,
|
|
84
|
-
readFile: neverFile,
|
|
85
|
-
readStdin: neverStdin,
|
|
86
|
-
});
|
|
87
|
-
expect(scalar.kind).toBe("error");
|
|
88
|
-
});
|
|
89
|
-
});
|
package/dist/preview.d.ts
DELETED
package/dist/preview.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
// `lotics preview <file.docx|.xlsx> [--out x.png]` — render a generated document to
|
|
2
|
-
// a PNG using the SAME engines the frontend FilePreview uses. No npm deps: drives a
|
|
3
|
-
// headless Chrome over CDP with Node built-ins (WebSocket/fetch/http/child_process),
|
|
4
|
-
// keeping the CLI a single bundled binary. The render logic lives in the esbuild
|
|
5
|
-
// browser bundle dist/render_page.js (built by build_cli.mjs); this file orchestrates
|
|
6
|
-
// Chrome. Chrome itself is external (system Chrome / Playwright chromium / CHROME_PATH)
|
|
7
|
-
// — inherent to rendering docx/xlsx, which are browser-rendered formats.
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { createServer } from "node:http";
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { join, dirname, resolve, extname, basename } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
15
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
function fail(msg) {
|
|
17
|
-
console.error(msg);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
/** Locate a Chrome/Chromium binary: explicit env → Playwright's install → system. */
|
|
21
|
-
function findChrome() {
|
|
22
|
-
const env = process.env.LOTICS_CHROME || process.env.CHROME_PATH;
|
|
23
|
-
if (env && existsSync(env))
|
|
24
|
-
return env;
|
|
25
|
-
const home = process.env.HOME || "";
|
|
26
|
-
// Playwright installs under ms-playwright/chromium-<rev>/… Enumerate with readdirSync
|
|
27
|
-
// (Node 10+) — NOT node:fs globSync, which is Node 22+; a globSync import would fail to
|
|
28
|
-
// load the whole bundled CLI on the Node 18+ it supports.
|
|
29
|
-
const pwDir = process.platform === "darwin"
|
|
30
|
-
? `${home}/Library/Caches/ms-playwright`
|
|
31
|
-
: `${home}/.cache/ms-playwright`;
|
|
32
|
-
if (existsSync(pwDir)) {
|
|
33
|
-
const rel = process.platform === "darwin"
|
|
34
|
-
? ["chrome-mac/Chromium.app/Contents/MacOS/Chromium"]
|
|
35
|
-
: ["chrome-linux/chrome", "chrome-linux/headless_shell"];
|
|
36
|
-
const revs = readdirSync(pwDir)
|
|
37
|
-
.filter((n) => n.startsWith("chromium-") || n.startsWith("chromium_headless_shell-"))
|
|
38
|
-
.sort()
|
|
39
|
-
.reverse();
|
|
40
|
-
for (const rev of revs) {
|
|
41
|
-
for (const r of rel) {
|
|
42
|
-
const bin = join(pwDir, rev, r);
|
|
43
|
-
if (existsSync(bin))
|
|
44
|
-
return bin;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const systemPaths = [
|
|
49
|
-
"/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium",
|
|
50
|
-
"/usr/bin/chromium-browser", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
51
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
52
|
-
];
|
|
53
|
-
return systemPaths.find((p) => existsSync(p)) ?? null;
|
|
54
|
-
}
|
|
55
|
-
/** Minimal CDP client over the built-in WebSocket. */
|
|
56
|
-
async function cdpConnect(wsUrl) {
|
|
57
|
-
const ws = new WebSocket(wsUrl);
|
|
58
|
-
await new Promise((res, rej) => {
|
|
59
|
-
ws.onopen = () => res();
|
|
60
|
-
ws.onerror = () => rej(new Error("CDP websocket failed to open"));
|
|
61
|
-
});
|
|
62
|
-
let id = 0;
|
|
63
|
-
const pending = new Map();
|
|
64
|
-
// If Chrome exits mid-render the socket closes with no reply — settle every in-flight
|
|
65
|
-
// request so `send()` rejects instead of hanging forever (the finally then cleans up).
|
|
66
|
-
ws.onclose = () => {
|
|
67
|
-
for (const done of pending.values())
|
|
68
|
-
done(Promise.reject(new Error("CDP connection closed (Chrome exited?)")));
|
|
69
|
-
pending.clear();
|
|
70
|
-
};
|
|
71
|
-
ws.onmessage = (e) => {
|
|
72
|
-
const m = JSON.parse(String(e.data));
|
|
73
|
-
if (m.id != null && pending.has(m.id)) {
|
|
74
|
-
const done = pending.get(m.id);
|
|
75
|
-
pending.delete(m.id);
|
|
76
|
-
done(m.error ? Promise.reject(new Error(m.error.message)) : m.result);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const send = (method, params = {}) => new Promise((res) => {
|
|
80
|
-
const i = ++id;
|
|
81
|
-
pending.set(i, (r) => res(r));
|
|
82
|
-
ws.send(JSON.stringify({ id: i, method, params }));
|
|
83
|
-
});
|
|
84
|
-
return { send, close: () => ws.close() };
|
|
85
|
-
}
|
|
86
|
-
export async function runPreviewCommand(filePath, flags) {
|
|
87
|
-
if (!filePath)
|
|
88
|
-
fail("Usage: lotics preview <file.docx|.xlsx> [--out <file.png>]");
|
|
89
|
-
const abs = resolve(filePath);
|
|
90
|
-
if (!existsSync(abs))
|
|
91
|
-
fail(`File not found: ${abs}`);
|
|
92
|
-
const ext = extname(abs).toLowerCase();
|
|
93
|
-
const type = ext === ".docx" ? "docx" : (ext === ".xlsx" || ext === ".xls" || ext === ".csv") ? "xlsx" : null;
|
|
94
|
-
if (!type)
|
|
95
|
-
fail(`Unsupported file type "${ext}" — preview supports .docx and .xlsx/.csv. (PDFs open directly — no preview needed.)`);
|
|
96
|
-
// preview drives Chrome over CDP via the built-in WebSocket (Node 22+). The rest of the
|
|
97
|
-
// CLI supports Node 18, so fail this one command clearly rather than with "WebSocket is
|
|
98
|
-
// not defined" — and before launching Chrome.
|
|
99
|
-
if (typeof WebSocket === "undefined") {
|
|
100
|
-
fail("lotics preview needs Node 22+ (it drives Chrome over CDP via the built-in WebSocket). Upgrade Node and retry.");
|
|
101
|
-
}
|
|
102
|
-
const bundlePath = join(HERE, "..", "render_page.js");
|
|
103
|
-
if (!existsSync(bundlePath))
|
|
104
|
-
fail(`Render bundle missing at ${bundlePath} — reinstall @lotics/cli (build step failed).`);
|
|
105
|
-
const chrome = findChrome();
|
|
106
|
-
if (!chrome) {
|
|
107
|
-
fail("No Chrome/Chromium found. Set CHROME_PATH to a Chrome binary, or install one:\n" +
|
|
108
|
-
" npx playwright install chromium (then it's auto-detected)\n" +
|
|
109
|
-
" or install Google Chrome / Chromium via your package manager.");
|
|
110
|
-
}
|
|
111
|
-
const b64 = readFileSync(abs).toString("base64");
|
|
112
|
-
const bundle = readFileSync(bundlePath, "utf8");
|
|
113
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;background:#fff;font-family:sans-serif}` +
|
|
114
|
-
`#root{padding:20px;max-width:1240px;margin:0 auto}</style></head><body><div id="root"></div>` +
|
|
115
|
-
`<script>window.__LOTICS_RENDER=${JSON.stringify({ type, b64 })}</script>` +
|
|
116
|
-
`<script src="/render_page.js"></script></body></html>`;
|
|
117
|
-
const server = createServer((req, res) => {
|
|
118
|
-
if (req.url === "/render_page.js") {
|
|
119
|
-
res.writeHead(200, { "content-type": "application/javascript" });
|
|
120
|
-
res.end(bundle);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
124
|
-
res.end(html);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
128
|
-
const httpPort = server.address().port;
|
|
129
|
-
const udd = mkdtempSync(join(tmpdir(), "lotics-render-"));
|
|
130
|
-
const child = spawn(chrome, [
|
|
131
|
-
"--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars",
|
|
132
|
-
// Large multi-sheet renders exhaust the (often tiny) /dev/shm in containers/WSL2 and
|
|
133
|
-
// crash the tab; back shared memory with /tmp instead.
|
|
134
|
-
"--disable-dev-shm-usage",
|
|
135
|
-
"--force-device-scale-factor=2", "--remote-debugging-port=0", `--user-data-dir=${udd}`,
|
|
136
|
-
"about:blank",
|
|
137
|
-
], { stdio: "ignore" });
|
|
138
|
-
const cleanup = () => {
|
|
139
|
-
try {
|
|
140
|
-
child.kill();
|
|
141
|
-
}
|
|
142
|
-
catch { /* ignore */ }
|
|
143
|
-
try {
|
|
144
|
-
server.close();
|
|
145
|
-
}
|
|
146
|
-
catch { /* ignore */ }
|
|
147
|
-
try {
|
|
148
|
-
rmSync(udd, { recursive: true, force: true });
|
|
149
|
-
}
|
|
150
|
-
catch { /* ignore */ }
|
|
151
|
-
};
|
|
152
|
-
try {
|
|
153
|
-
// Chrome writes the chosen debug port to DevToolsActivePort (first line).
|
|
154
|
-
let cdpPort = 0;
|
|
155
|
-
const portFile = join(udd, "DevToolsActivePort");
|
|
156
|
-
for (let i = 0; i < 100 && !cdpPort; i++) {
|
|
157
|
-
if (existsSync(portFile)) {
|
|
158
|
-
const p = parseInt(readFileSync(portFile, "utf8").split("\n")[0], 10);
|
|
159
|
-
if (p)
|
|
160
|
-
cdpPort = p;
|
|
161
|
-
}
|
|
162
|
-
if (!cdpPort)
|
|
163
|
-
await sleep(100);
|
|
164
|
-
}
|
|
165
|
-
if (!cdpPort)
|
|
166
|
-
throw new Error("Chrome did not expose a debugging port (launch failed?).");
|
|
167
|
-
let target;
|
|
168
|
-
for (let i = 0; i < 60 && !target?.webSocketDebuggerUrl; i++) {
|
|
169
|
-
try {
|
|
170
|
-
const list = await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json();
|
|
171
|
-
target = list.find((t) => t.type === "page");
|
|
172
|
-
}
|
|
173
|
-
catch { /* not ready */ }
|
|
174
|
-
if (!target?.webSocketDebuggerUrl)
|
|
175
|
-
await sleep(100);
|
|
176
|
-
}
|
|
177
|
-
if (!target?.webSocketDebuggerUrl)
|
|
178
|
-
throw new Error("No Chrome page target available.");
|
|
179
|
-
const cdp = await cdpConnect(target.webSocketDebuggerUrl);
|
|
180
|
-
await cdp.send("Page.enable");
|
|
181
|
-
await cdp.send("Runtime.enable");
|
|
182
|
-
await cdp.send("Page.navigate", { url: `http://127.0.0.1:${httpPort}/` });
|
|
183
|
-
// Poll for the render-done flag the page sets.
|
|
184
|
-
let err;
|
|
185
|
-
let done = false;
|
|
186
|
-
const warnings = [];
|
|
187
|
-
for (let i = 0; i < 200; i++) {
|
|
188
|
-
const r = await cdp.send("Runtime.evaluate", {
|
|
189
|
-
expression: "({done: !!window.__loticsDone, err: window.__loticsError || '', warnings: window.__loticsWarnings || []})",
|
|
190
|
-
returnByValue: true,
|
|
191
|
-
});
|
|
192
|
-
const v = r.result?.value;
|
|
193
|
-
if (v?.done) {
|
|
194
|
-
done = true;
|
|
195
|
-
err = v.err || undefined;
|
|
196
|
-
if (v.warnings?.length)
|
|
197
|
-
warnings.push(...v.warnings);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
await sleep(75);
|
|
201
|
-
}
|
|
202
|
-
if (!done)
|
|
203
|
-
throw new Error("Render timed out (page never signaled completion).");
|
|
204
|
-
if (err)
|
|
205
|
-
throw new Error(`Render engine error: ${err}`);
|
|
206
|
-
// Size the capture to the full rendered content, then screenshot beyond viewport.
|
|
207
|
-
// Clamp to Chrome's ceiling: the PNG is 2× (device scale) the CSS clip, so a CSS side
|
|
208
|
-
// over ~15000px would blow past the ~32767px canvas limit. A very long docx or several
|
|
209
|
-
// huge sheets hit this — clamp explicitly and WARN, never clip in silence.
|
|
210
|
-
const metrics = await cdp.send("Page.getLayoutMetrics");
|
|
211
|
-
const size = metrics.cssContentSize ?? metrics.contentSize ?? { width: 1240, height: 1600 };
|
|
212
|
-
const MAX_CAPTURE = 15000; // CSS px; ×2 device scale → ≤30000px PNG
|
|
213
|
-
const w = Math.ceil(size.width);
|
|
214
|
-
const h = Math.ceil(size.height);
|
|
215
|
-
if (w > MAX_CAPTURE || h > MAX_CAPTURE) {
|
|
216
|
-
warnings.push(`content ${w}×${h}px clipped to ${Math.min(w, MAX_CAPTURE)}×${Math.min(h, MAX_CAPTURE)}px (exceeds the ${MAX_CAPTURE}px capture limit)`);
|
|
217
|
-
}
|
|
218
|
-
const shot = await cdp.send("Page.captureScreenshot", {
|
|
219
|
-
format: "png",
|
|
220
|
-
captureBeyondViewport: true,
|
|
221
|
-
clip: { x: 0, y: 0, width: Math.min(w, MAX_CAPTURE), height: Math.min(h, MAX_CAPTURE), scale: 1 },
|
|
222
|
-
});
|
|
223
|
-
cdp.close();
|
|
224
|
-
const outPath = flags.output ? resolve(flags.output) : join(dirname(abs), basename(abs, ext) + ".png");
|
|
225
|
-
writeFileSync(outPath, Buffer.from(shot.data, "base64"));
|
|
226
|
-
console.log(`Rendered ${basename(abs)} → ${outPath} (${Math.min(w, MAX_CAPTURE)}×${Math.min(h, MAX_CAPTURE)}, via ${basename(chrome)})`);
|
|
227
|
-
for (const warn of warnings)
|
|
228
|
-
console.error(` ⚠ ${warn}`);
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
cleanup();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Starter template for `lotics app create`.
|
|
3
|
-
*
|
|
4
|
-
* The CLI scaffolds these files into the user's target directory, then runs
|
|
5
|
-
* `npm install` + `npm run build` + `lotics app deploy` to populate v1.
|
|
6
|
-
*
|
|
7
|
-
* Embedded as string literals here (rather than packaged as files alongside
|
|
8
|
-
* the CLI) so the CLI binary is self-contained and the starter survives
|
|
9
|
-
* `npm publish` cleanly. Updates to the starter ship in CLI releases.
|
|
10
|
-
*
|
|
11
|
-
* Conventions (locked decisions):
|
|
12
|
-
* - Vite + React + TypeScript (strict mode).
|
|
13
|
-
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
* `mount(<LoticsLocaleProvider locale={vi}><PortalHost><App/></PortalHost></LoticsLocaleProvider>)` —
|
|
15
|
-
* LoticsLocaleProvider sets the kit's strings to Vietnamese (Lotics apps are
|
|
16
|
-
* Vietnamese; swap to `en` for English); PortalHost is the render target
|
|
17
|
-
* @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
18
|
-
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
19
|
-
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
20
|
-
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
21
|
-
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
22
|
-
* Don't switch to `base: "./"` without updating `rewriteAssetPaths`.
|
|
23
|
-
* - react-native aliased to react-native-web so @lotics/ui's RN primitives
|
|
24
|
-
* render in a pure-web environment. `.web.tsx` is prioritized in the
|
|
25
|
-
* resolve.extensions list.
|
|
26
|
-
* - @lotics/app-sdk + @lotics/ui peer-supplied; user installs both via npm.
|
|
27
|
-
* - oxlint + vitest + tsc out of the box, plus a GitHub Actions CI
|
|
28
|
-
* workflow that runs all three on every PR.
|
|
29
|
-
*/
|
|
30
|
-
export interface StarterFile {
|
|
31
|
-
path: string;
|
|
32
|
-
content: string;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Pin defaults for `@lotics/ui` and `@lotics/app-sdk` in the scaffolded
|
|
36
|
-
* package.json. Used when `lotics app create` can't resolve the latest
|
|
37
|
-
* versions from npm (no network, registry blip) — keeps `app create`
|
|
38
|
-
* working offline at the cost of a stale starter pin.
|
|
39
|
-
*
|
|
40
|
-
* Bump these in tandem with new releases of either package. Production
|
|
41
|
-
* scaffolds resolve the live version via `fetchLatestNpmVersion` and only
|
|
42
|
-
* fall back here when the lookup fails.
|
|
43
|
-
*/
|
|
44
|
-
export declare const STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
45
|
-
export declare const STARTER_FALLBACK_SDK_VERSION = "0.51.0";
|
|
46
|
-
/**
|
|
47
|
-
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
|
48
|
-
* an app deep-typechecks `@lotics/ui`'s `.tsx` source against the SAME RN types
|
|
49
|
-
* the package is developed and CI'd against. A mismatch surfaces RN-version-
|
|
50
|
-
* sensitive type errors (e.g. `StyleSheet.absoluteFill`, which `frontend/` CI
|
|
51
|
-
* never sees because `packages/ui` is only typechecked through `frontend/`).
|
|
52
|
-
* RN is aliased to react-native-web at runtime, so this pin is effectively
|
|
53
|
-
* types-only.
|
|
54
|
-
*/
|
|
55
|
-
export declare const STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
56
|
-
export declare function buildStarterTemplate(args: {
|
|
57
|
-
app_name: string;
|
|
58
|
-
app_id: string;
|
|
59
|
-
workspace_id: string;
|
|
60
|
-
/** @lotics/ui version range, e.g. "^1.8.0". Defaults to the fallback pin. */
|
|
61
|
-
ui_version?: string;
|
|
62
|
-
/** @lotics/app-sdk version range, e.g. "^0.7.0". Defaults to the fallback pin. */
|
|
63
|
-
sdk_version?: string;
|
|
64
|
-
}): StarterFile[];
|