@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.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +100 -100
- package/dist/{client.d.ts → src/client.d.ts} +1 -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
package/dist/docx.test.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { parseDocx } from "@lotics/docx";
|
|
6
|
-
import { runDocxCommand } from "./docx.js";
|
|
7
|
-
import { CliError } from "./file_command_io.js";
|
|
8
|
-
let tmpDir;
|
|
9
|
-
let file;
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "docx-test-"));
|
|
12
|
-
file = path.join(tmpDir, "test.docx");
|
|
13
|
-
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
14
|
-
});
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
async function writeFresh(json) {
|
|
20
|
-
await runDocxCommand("write", file, [json]);
|
|
21
|
-
}
|
|
22
|
-
async function readBlocks() {
|
|
23
|
-
const stdout = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
24
|
-
try {
|
|
25
|
-
await runDocxCommand("read", file, []);
|
|
26
|
-
const printed = stdout.mock.calls[0]?.[0];
|
|
27
|
-
const out = JSON.parse(printed);
|
|
28
|
-
return out.blocks
|
|
29
|
-
.filter((b) => b.kind === "paragraph")
|
|
30
|
-
.map((b) => ({ text: b.text ?? "", style: b.style }));
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
stdout.mockRestore();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
describe("docx write + read", () => {
|
|
37
|
-
it("round-trips paragraphs with styles", async () => {
|
|
38
|
-
await writeFresh(JSON.stringify({
|
|
39
|
-
blocks: [
|
|
40
|
-
{ kind: "paragraph", text: "Title", style: "Heading1" },
|
|
41
|
-
{ kind: "paragraph", text: "Body" },
|
|
42
|
-
],
|
|
43
|
-
}));
|
|
44
|
-
expect(await readBlocks()).toEqual([
|
|
45
|
-
{ text: "Title", style: "Heading1" },
|
|
46
|
-
{ text: "Body", style: undefined },
|
|
47
|
-
]);
|
|
48
|
-
});
|
|
49
|
-
it("renders multi-line text as <w:br/> line breaks and round-trips through read", async () => {
|
|
50
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "L1\nL2\nL3" }] }));
|
|
51
|
-
expect(await readBlocks()).toEqual([{ text: "L1\nL2\nL3", style: undefined }]);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
describe("docx single-op mutations", () => {
|
|
55
|
-
beforeEach(async () => {
|
|
56
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "first" }] }));
|
|
57
|
-
});
|
|
58
|
-
it("append-paragraph adds at end", async () => {
|
|
59
|
-
await runDocxCommand("append-paragraph", file, ["second"]);
|
|
60
|
-
expect((await readBlocks()).map((b) => b.text)).toEqual(["first", "second"]);
|
|
61
|
-
});
|
|
62
|
-
it("append-paragraph carries a style flag", async () => {
|
|
63
|
-
await runDocxCommand("append-paragraph", file, ["heading", "--style=Heading2"]);
|
|
64
|
-
expect((await readBlocks())[1]).toEqual({ text: "heading", style: "Heading2" });
|
|
65
|
-
});
|
|
66
|
-
it("append-paragraph does NOT consume --style as positional text", async () => {
|
|
67
|
-
// Regression: an earlier draft picked rest[0] without filtering flags,
|
|
68
|
-
// so `--style=X` became the paragraph text.
|
|
69
|
-
await expect(runDocxCommand("append-paragraph", file, ["--style=Heading1"])).rejects.toBeInstanceOf(CliError);
|
|
70
|
-
});
|
|
71
|
-
it("insert-paragraph splices at index", async () => {
|
|
72
|
-
await runDocxCommand("insert-paragraph", file, ["front", "--at=0"]);
|
|
73
|
-
expect((await readBlocks()).map((b) => b.text)).toEqual(["front", "first"]);
|
|
74
|
-
});
|
|
75
|
-
it("delete-block removes by index", async () => {
|
|
76
|
-
await runDocxCommand("append-paragraph", file, ["second"]);
|
|
77
|
-
await runDocxCommand("delete-block", file, ["--at=0"]);
|
|
78
|
-
expect((await readBlocks()).map((b) => b.text)).toEqual(["second"]);
|
|
79
|
-
});
|
|
80
|
-
it("replace-text rewrites all paragraph runs", async () => {
|
|
81
|
-
await runDocxCommand("append-paragraph", file, ["the {{name}}"]);
|
|
82
|
-
await runDocxCommand("replace-text", file, ["{{name}}", "Acme"]);
|
|
83
|
-
expect((await readBlocks()).map((b) => b.text)).toEqual(["first", "the Acme"]);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("docx batch", () => {
|
|
87
|
-
it("applies many ops in one parse/serialize cycle", async () => {
|
|
88
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "a" }] }));
|
|
89
|
-
const ops = [
|
|
90
|
-
{ op: "append-paragraph", text: "b" },
|
|
91
|
-
{ op: "insert-paragraph", text: "front", at: 0 },
|
|
92
|
-
{ op: "replace-text", search: "b", replace: "B" },
|
|
93
|
-
];
|
|
94
|
-
await runDocxCommand("batch", file, [JSON.stringify(ops)]);
|
|
95
|
-
expect((await readBlocks()).map((b) => b.text)).toEqual(["front", "a", "B"]);
|
|
96
|
-
});
|
|
97
|
-
it("rejects batch op without an 'op' string", async () => {
|
|
98
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "x" }] }));
|
|
99
|
-
await expect(runDocxCommand("batch", file, [JSON.stringify([{ text: "y" }])])).rejects.toBeInstanceOf(CliError);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("docx round-trip preserves opaque blocks", () => {
|
|
103
|
-
it("a table (parsed as opaque_block) survives replace-text + write back", async () => {
|
|
104
|
-
// Build a docx by hand with a paragraph + a synthetic table cell.
|
|
105
|
-
// Use the live parse/serialize path to confirm opaque content survives.
|
|
106
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "before" }] }));
|
|
107
|
-
// We can't construct an opaque_block via write JSON, so just verify
|
|
108
|
-
// that the round-trip of a doc containing only paragraphs is stable
|
|
109
|
-
// and that error path for opaque_block is hit on write.
|
|
110
|
-
await expect(runDocxCommand("write", file, [JSON.stringify({
|
|
111
|
-
blocks: [{ kind: "opaque_block" }],
|
|
112
|
-
})])).rejects.toBeInstanceOf(CliError);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
describe("docx error paths", () => {
|
|
116
|
-
it("fails on missing file", async () => {
|
|
117
|
-
await expect(runDocxCommand("append-paragraph", path.join(tmpDir, "no.docx"), ["x"])).rejects.toBeInstanceOf(CliError);
|
|
118
|
-
});
|
|
119
|
-
it("fails on invalid write JSON", async () => {
|
|
120
|
-
await expect(runDocxCommand("write", file, ["{not json"])).rejects.toBeInstanceOf(CliError);
|
|
121
|
-
});
|
|
122
|
-
it("fails on missing blocks array", async () => {
|
|
123
|
-
await expect(runDocxCommand("write", file, [JSON.stringify({})])).rejects.toBeInstanceOf(CliError);
|
|
124
|
-
});
|
|
125
|
-
it("fails on out-of-range --at on insert-paragraph", async () => {
|
|
126
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "only" }] }));
|
|
127
|
-
await expect(runDocxCommand("insert-paragraph", file, ["x", "--at=99"])).rejects.toBeInstanceOf(CliError);
|
|
128
|
-
});
|
|
129
|
-
it("fails on out-of-range --at on delete-block", async () => {
|
|
130
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "only" }] }));
|
|
131
|
-
await expect(runDocxCommand("delete-block", file, ["--at=5"])).rejects.toBeInstanceOf(CliError);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
// Verify parseDocx is callable from this test (sanity that the barrel import
|
|
135
|
-
// works under tsgo + vitest) — write a doc, parse with the library directly,
|
|
136
|
-
// confirm at least one paragraph block.
|
|
137
|
-
describe("docx barrel sanity", () => {
|
|
138
|
-
it("parseDocx is reachable from @lotics/docx", async () => {
|
|
139
|
-
await writeFresh(JSON.stringify({ blocks: [{ kind: "paragraph", text: "hi" }] }));
|
|
140
|
-
const bytes = fs.readFileSync(file);
|
|
141
|
-
const doc = await parseDocx(new Uint8Array(bytes));
|
|
142
|
-
const paragraphs = doc.body.children.filter((b) => b.kind === "paragraph");
|
|
143
|
-
expect(paragraphs.length).toBeGreaterThanOrEqual(1);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Throw a tagged user-facing error from a CLI subcommand.
|
|
3
|
-
*
|
|
4
|
-
* The top-level CLI catch (`main().catch` in cli.ts) prints `message` to
|
|
5
|
-
* stderr and exits 1, so callers get the same UX as `process.exit(1)` but
|
|
6
|
-
* tests can catch the throw without killing the test runner.
|
|
7
|
-
*/
|
|
8
|
-
export declare class CliError extends Error {
|
|
9
|
-
constructor(message: string);
|
|
10
|
-
}
|
|
11
|
-
export declare function fail(message: string): never;
|
|
12
|
-
export declare function writeFileAtomic(filePath: string, bytes: Uint8Array): void;
|
package/dist/file_command_io.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Throw a tagged user-facing error from a CLI subcommand.
|
|
5
|
-
*
|
|
6
|
-
* The top-level CLI catch (`main().catch` in cli.ts) prints `message` to
|
|
7
|
-
* stderr and exits 1, so callers get the same UX as `process.exit(1)` but
|
|
8
|
-
* tests can catch the throw without killing the test runner.
|
|
9
|
-
*/
|
|
10
|
-
export class CliError extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "CliError";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function fail(message) {
|
|
17
|
-
throw new CliError(message);
|
|
18
|
-
}
|
|
19
|
-
export function writeFileAtomic(filePath, bytes) {
|
|
20
|
-
const dir = path.dirname(path.resolve(filePath));
|
|
21
|
-
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
22
|
-
fs.writeFileSync(tmp, bytes);
|
|
23
|
-
try {
|
|
24
|
-
fs.renameSync(tmp, filePath);
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
try {
|
|
28
|
-
fs.unlinkSync(tmp);
|
|
29
|
-
}
|
|
30
|
-
catch { /* best-effort cleanup */ }
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
console.error(`Wrote ${filePath}`);
|
|
34
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codegen: emit `.lotics/app_agents.d.ts` from the app's manifest.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `generate_app_workflows_dts` — agents reuse the exact same typed
|
|
5
|
-
* input/output vocabulary, so the `inputsToType` / `objectFieldsToType` mappers
|
|
6
|
-
* are shared. The augmentation adds a typed entry to `AppAgents` for every alias
|
|
7
|
-
* declared in `package.json#lotics.agents`:
|
|
8
|
-
*
|
|
9
|
-
* - No declared `inputs` → `alias: Record<string, unknown>` (untyped payload)
|
|
10
|
-
* - Declared `inputs` → `alias: { …declared shape… }`
|
|
11
|
-
* - Declared `outputs` → an `AppAgentResults[alias]` entry → typed `output`
|
|
12
|
-
*
|
|
13
|
-
* Pure function. Same inputs → same output. Idempotent.
|
|
14
|
-
*/
|
|
15
|
-
import type { AppAgentDeclaration } from "./app_commands.js";
|
|
16
|
-
export declare function generateAppAgentsDts(agents: Record<string, AppAgentDeclaration> | undefined): string;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codegen: emit `.lotics/app_agents.d.ts` from the app's manifest.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `generate_app_workflows_dts` — agents reuse the exact same typed
|
|
5
|
-
* input/output vocabulary, so the `inputsToType` / `objectFieldsToType` mappers
|
|
6
|
-
* are shared. The augmentation adds a typed entry to `AppAgents` for every alias
|
|
7
|
-
* declared in `package.json#lotics.agents`:
|
|
8
|
-
*
|
|
9
|
-
* - No declared `inputs` → `alias: Record<string, unknown>` (untyped payload)
|
|
10
|
-
* - Declared `inputs` → `alias: { …declared shape… }`
|
|
11
|
-
* - Declared `outputs` → an `AppAgentResults[alias]` entry → typed `output`
|
|
12
|
-
*
|
|
13
|
-
* Pure function. Same inputs → same output. Idempotent.
|
|
14
|
-
*/
|
|
15
|
-
import { inputsToType, objectFieldsToType, isValidIdentifier } from "./generate_app_workflows_dts.js";
|
|
16
|
-
const HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
|
|
17
|
-
// DO NOT EDIT — regenerated from package.json#lotics.agents.
|
|
18
|
-
//
|
|
19
|
-
// This file gives \`useAgentRun("alias")\` typed input + output at call sites by
|
|
20
|
-
// augmenting the @lotics/app-sdk \`AppAgents\` / \`AppAgentResults\` interfaces.
|
|
21
|
-
|
|
22
|
-
import "@lotics/app-sdk";
|
|
23
|
-
`;
|
|
24
|
-
export function generateAppAgentsDts(agents) {
|
|
25
|
-
const entries = Object.entries(agents ?? {});
|
|
26
|
-
if (entries.length === 0) {
|
|
27
|
-
return `${HEADER}
|
|
28
|
-
// No agents declared in package.json#lotics.agents.
|
|
29
|
-
// Add an entry to enable typed useAgentRun<"alias"> at call sites.
|
|
30
|
-
declare module "@lotics/app-sdk" {
|
|
31
|
-
interface AppAgents {}
|
|
32
|
-
}
|
|
33
|
-
`;
|
|
34
|
-
}
|
|
35
|
-
// Sort alphabetically for deterministic output across regenerations.
|
|
36
|
-
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
37
|
-
const inputLines = [];
|
|
38
|
-
const resultLines = [];
|
|
39
|
-
for (const [alias, declaration] of entries) {
|
|
40
|
-
const valueType = declaration.inputs
|
|
41
|
-
? inputsToType(declaration.inputs)
|
|
42
|
-
: "Record<string, unknown>";
|
|
43
|
-
const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
44
|
-
inputLines.push(` ${aliasKey}: ${valueType};`);
|
|
45
|
-
if (declaration.outputs) {
|
|
46
|
-
resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const resultsBlock = resultLines.length > 0
|
|
50
|
-
? `
|
|
51
|
-
interface AppAgentResults {
|
|
52
|
-
${resultLines.join("\n")}
|
|
53
|
-
}`
|
|
54
|
-
: "";
|
|
55
|
-
return `${HEADER}
|
|
56
|
-
declare module "@lotics/app-sdk" {
|
|
57
|
-
interface AppAgents {
|
|
58
|
-
${inputLines.join("\n")}
|
|
59
|
-
}${resultsBlock}
|
|
60
|
-
}
|
|
61
|
-
`;
|
|
62
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { generateAppAgentsDts } from "./generate_app_agents_dts.js";
|
|
3
|
-
describe("generateAppAgentsDts", () => {
|
|
4
|
-
it("emits an empty AppAgents interface when no agents are declared", () => {
|
|
5
|
-
const out = generateAppAgentsDts(undefined);
|
|
6
|
-
expect(out).toContain("interface AppAgents {}");
|
|
7
|
-
expect(out).not.toContain("interface AppAgentResults");
|
|
8
|
-
});
|
|
9
|
-
it("types the input payload from declared inputs", () => {
|
|
10
|
-
const out = generateAppAgentsDts({
|
|
11
|
-
recognize: { inputs: { photo: { type: "file" }, prompt: { type: "text", required: false } } },
|
|
12
|
-
});
|
|
13
|
-
expect(out).toContain("recognize: {");
|
|
14
|
-
expect(out).toContain("photo: string;");
|
|
15
|
-
expect(out).toContain("prompt?: string;");
|
|
16
|
-
});
|
|
17
|
-
it("leaves an inputs-less agent as an untyped payload", () => {
|
|
18
|
-
const out = generateAppAgentsDts({ summarize: {} });
|
|
19
|
-
expect(out).toContain("summarize: Record<string, unknown>;");
|
|
20
|
-
});
|
|
21
|
-
it("emits AppAgentResults from declared outputs, including nested objects", () => {
|
|
22
|
-
const out = generateAppAgentsDts({
|
|
23
|
-
recognize: {
|
|
24
|
-
inputs: { photo: { type: "file" } },
|
|
25
|
-
outputs: {
|
|
26
|
-
style: { type: "text" },
|
|
27
|
-
dimensions: {
|
|
28
|
-
type: "object",
|
|
29
|
-
fields: { width_mm: { type: "number" }, height_mm: { type: "number" } },
|
|
30
|
-
},
|
|
31
|
-
confidence: { type: "number" },
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
expect(out).toContain("interface AppAgentResults {");
|
|
36
|
-
expect(out).toContain("style: string");
|
|
37
|
-
expect(out).toContain("width_mm: number");
|
|
38
|
-
expect(out).toContain("confidence: number");
|
|
39
|
-
});
|
|
40
|
-
it("is deterministic and sorts aliases", () => {
|
|
41
|
-
const a = generateAppAgentsDts({ zebra: {}, alpha: {} });
|
|
42
|
-
expect(a.indexOf("alpha:")).toBeLessThan(a.indexOf("zebra:"));
|
|
43
|
-
expect(generateAppAgentsDts({ zebra: {}, alpha: {} })).toBe(a);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` from a workspace schema.
|
|
3
|
-
*
|
|
4
|
-
* Unlike the `.d.ts` companions (which only augment types), this is a real
|
|
5
|
-
* `.ts` module: apps import the string VALUES at runtime to address fields and
|
|
6
|
-
* select options by a stable, human-readable alias instead of pasting opaque
|
|
7
|
-
* `fld_…` / `opt_…` ids into their source:
|
|
8
|
-
*
|
|
9
|
-
* import { F, OPT } from "../.lotics/app_fields";
|
|
10
|
-
* record.data[F.SHIPMENTS.status] // "fld_…"
|
|
11
|
-
* if (status === OPT.SHIPMENTS.status.cleared) // "opt_…"
|
|
12
|
-
*
|
|
13
|
-
* Aliases are derived from display names (NFD-stripped, non-alnum → `_`, deduped
|
|
14
|
-
* in stable order), so a rename on the platform re-runs codegen and the app's
|
|
15
|
-
* call sites move with it. Pure function — same schema → same bytes. Idempotent.
|
|
16
|
-
*/
|
|
17
|
-
/** One select option as it appears in a field's metadata. */
|
|
18
|
-
export interface TableFieldOption {
|
|
19
|
-
/** Stable id (`opt_…`) — the value persisted in record data. */
|
|
20
|
-
id: string;
|
|
21
|
-
/** Display label the option alias is slugified from. */
|
|
22
|
-
label: string;
|
|
23
|
-
}
|
|
24
|
-
/** A single table field with the metadata codegen needs. */
|
|
25
|
-
export interface TableFieldSchema {
|
|
26
|
-
/** Stable id (`fld_…`) — the value emitted under `F`. */
|
|
27
|
-
id: string;
|
|
28
|
-
/** Display name the field alias is slugified from. */
|
|
29
|
-
name: string;
|
|
30
|
-
/** Present only for select/multi-select fields — drives the `OPT` map. */
|
|
31
|
-
options?: TableFieldOption[];
|
|
32
|
-
}
|
|
33
|
-
/** The schema of one table, as returned by `client.getWorkspaceSchema`. */
|
|
34
|
-
export interface TableSchema {
|
|
35
|
-
/** Stable id (`tbl_…`). Carried for fidelity; not emitted by name. */
|
|
36
|
-
id: string;
|
|
37
|
-
/** Display name the TABLE alias is slugified from. */
|
|
38
|
-
name: string;
|
|
39
|
-
fields: TableFieldSchema[];
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Slugify a display name to a valid TS identifier. NFD-normalize then strip
|
|
43
|
-
* diacritics so "Lô hàng" and "Lo hang" don't collide on the accent, lowercase,
|
|
44
|
-
* non-alnum → `_`, collapse runs, trim edge `_`. A leading digit (identifiers
|
|
45
|
-
* can't start with one) and the empty result both get a `_` prefix/placeholder.
|
|
46
|
-
* `upper` uppercases the result (TABLE aliases read as constants).
|
|
47
|
-
*/
|
|
48
|
-
export declare function slugifyAlias(name: string, upper: boolean): string;
|
|
49
|
-
/** A property key for an object literal — bare when a valid identifier, else quoted. */
|
|
50
|
-
export declare function propKey(alias: string): string;
|
|
51
|
-
/**
|
|
52
|
-
* Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
|
|
53
|
-
* workspace schema (the subset the app touches). An empty list yields valid,
|
|
54
|
-
* empty `F`/`OPT` maps so the file always compiles and imports resolve.
|
|
55
|
-
*/
|
|
56
|
-
export declare function generateAppFields(tables: TableSchema[]): string;
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` from a workspace schema.
|
|
3
|
-
*
|
|
4
|
-
* Unlike the `.d.ts` companions (which only augment types), this is a real
|
|
5
|
-
* `.ts` module: apps import the string VALUES at runtime to address fields and
|
|
6
|
-
* select options by a stable, human-readable alias instead of pasting opaque
|
|
7
|
-
* `fld_…` / `opt_…` ids into their source:
|
|
8
|
-
*
|
|
9
|
-
* import { F, OPT } from "../.lotics/app_fields";
|
|
10
|
-
* record.data[F.SHIPMENTS.status] // "fld_…"
|
|
11
|
-
* if (status === OPT.SHIPMENTS.status.cleared) // "opt_…"
|
|
12
|
-
*
|
|
13
|
-
* Aliases are derived from display names (NFD-stripped, non-alnum → `_`, deduped
|
|
14
|
-
* in stable order), so a rename on the platform re-runs codegen and the app's
|
|
15
|
-
* call sites move with it. Pure function — same schema → same bytes. Idempotent.
|
|
16
|
-
*/
|
|
17
|
-
import { isValidIdentifier } from "./generate_app_workflows_dts.js";
|
|
18
|
-
const HEADER = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
|
|
19
|
-
// DO NOT EDIT — regenerated from the workspace schema.
|
|
20
|
-
//
|
|
21
|
-
// Runtime field + option ids addressed by stable display-name aliases:
|
|
22
|
-
// record.data[F.<TABLE>.<field>] → "fld_…"
|
|
23
|
-
// value === OPT.<TABLE>.<field>.<option> → "opt_…"
|
|
24
|
-
// A rename on the platform re-runs codegen and moves these in lockstep.
|
|
25
|
-
`;
|
|
26
|
-
/**
|
|
27
|
-
* Slugify a display name to a valid TS identifier. NFD-normalize then strip
|
|
28
|
-
* diacritics so "Lô hàng" and "Lo hang" don't collide on the accent, lowercase,
|
|
29
|
-
* non-alnum → `_`, collapse runs, trim edge `_`. A leading digit (identifiers
|
|
30
|
-
* can't start with one) and the empty result both get a `_` prefix/placeholder.
|
|
31
|
-
* `upper` uppercases the result (TABLE aliases read as constants).
|
|
32
|
-
*/
|
|
33
|
-
export function slugifyAlias(name, upper) {
|
|
34
|
-
const stripped = name
|
|
35
|
-
.normalize("NFD")
|
|
36
|
-
.replace(/[̀-ͯ]/g, "") // combining diacritical marks
|
|
37
|
-
.replace(/đ/g, "d")
|
|
38
|
-
.replace(/Đ/g, "D"); // not a combining mark — map explicitly
|
|
39
|
-
const cased = upper ? stripped.toUpperCase() : stripped.toLowerCase();
|
|
40
|
-
const slug = cased
|
|
41
|
-
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
42
|
-
.replace(/^_+|_+$/g, "");
|
|
43
|
-
if (slug === "")
|
|
44
|
-
return "_";
|
|
45
|
-
return /^[0-9]/.test(slug) ? `_${slug}` : slug;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Assign each input a unique alias, preserving input order. The first claim on
|
|
49
|
-
* a slug keeps it; later collisions get `_2`, `_3`, … so the mapping is stable
|
|
50
|
-
* across regenerations (order is the schema's field/option order). Returns the
|
|
51
|
-
* aliases positionally aligned with `names`.
|
|
52
|
-
*/
|
|
53
|
-
function dedupeAliases(names, upper) {
|
|
54
|
-
const used = new Map();
|
|
55
|
-
return names.map((name) => {
|
|
56
|
-
const base = slugifyAlias(name, upper);
|
|
57
|
-
const seen = used.get(base);
|
|
58
|
-
if (seen === undefined) {
|
|
59
|
-
used.set(base, 1);
|
|
60
|
-
return base;
|
|
61
|
-
}
|
|
62
|
-
let n = seen + 1;
|
|
63
|
-
while (used.has(`${base}_${n}`))
|
|
64
|
-
n++;
|
|
65
|
-
used.set(base, n);
|
|
66
|
-
used.set(`${base}_${n}`, 1);
|
|
67
|
-
return `${base}_${n}`;
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
/** A property key for an object literal — bare when a valid identifier, else quoted. */
|
|
71
|
-
export function propKey(alias) {
|
|
72
|
-
return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
73
|
-
}
|
|
74
|
-
/** Resolve table + field aliases once, so the `F`/`OPT` maps and the union types agree. */
|
|
75
|
-
function aliasTables(tables) {
|
|
76
|
-
const tableAliases = dedupeAliases(tables.map((t) => t.name), true);
|
|
77
|
-
return tables.map((table, i) => {
|
|
78
|
-
const fieldAliases = dedupeAliases(table.fields.map((f) => f.name), false);
|
|
79
|
-
return {
|
|
80
|
-
alias: tableAliases[i],
|
|
81
|
-
table,
|
|
82
|
-
fields: table.fields.map((field, j) => ({ alias: fieldAliases[j], field })),
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
/** Emit the `F` map: `{ TABLE: { field: "fld_…" } }` plus its literal-union type. */
|
|
87
|
-
function emitFieldMap(aliased) {
|
|
88
|
-
const tableBlocks = aliased.map(({ alias, fields }) => {
|
|
89
|
-
const fieldLines = fields.map(({ alias: fieldAlias, field }) => ` ${propKey(fieldAlias)}: ${JSON.stringify(field.id)},`);
|
|
90
|
-
return ` ${propKey(alias)}: {\n${fieldLines.join("\n")}\n },`;
|
|
91
|
-
});
|
|
92
|
-
return `export const F = {\n${tableBlocks.join("\n")}\n} as const;`;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Emit the `OPT` map: `{ TABLE: { selectField: { option: "opt_…" } } }`. Only
|
|
96
|
-
* fields that carry options appear. Tables and fields with no options are
|
|
97
|
-
* omitted entirely (so `OPT.SHIPMENTS` may be absent — that's by design).
|
|
98
|
-
*/
|
|
99
|
-
function emitOptionMap(aliased) {
|
|
100
|
-
const tableBlocks = [];
|
|
101
|
-
for (const { alias, fields } of aliased) {
|
|
102
|
-
const fieldBlocks = [];
|
|
103
|
-
for (const { alias: fieldAlias, field } of fields) {
|
|
104
|
-
const options = field.options ?? [];
|
|
105
|
-
if (options.length === 0)
|
|
106
|
-
continue;
|
|
107
|
-
const optionAliases = dedupeAliases(options.map((o) => o.label), false);
|
|
108
|
-
const optionLines = options.map((option, i) => ` ${propKey(optionAliases[i])}: ${JSON.stringify(option.id)},`);
|
|
109
|
-
fieldBlocks.push(` ${propKey(fieldAlias)}: {\n${optionLines.join("\n")}\n },`);
|
|
110
|
-
}
|
|
111
|
-
if (fieldBlocks.length === 0)
|
|
112
|
-
continue;
|
|
113
|
-
tableBlocks.push(` ${propKey(alias)}: {\n${fieldBlocks.join("\n")}\n },`);
|
|
114
|
-
}
|
|
115
|
-
if (tableBlocks.length === 0)
|
|
116
|
-
return `export const OPT = {} as const;`;
|
|
117
|
-
return `export const OPT = {\n${tableBlocks.join("\n")}\n} as const;`;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
|
|
121
|
-
* workspace schema (the subset the app touches). An empty list yields valid,
|
|
122
|
-
* empty `F`/`OPT` maps so the file always compiles and imports resolve.
|
|
123
|
-
*/
|
|
124
|
-
export function generateAppFields(tables) {
|
|
125
|
-
if (tables.length === 0) {
|
|
126
|
-
return `${HEADER}
|
|
127
|
-
export const F = {} as const;
|
|
128
|
-
|
|
129
|
-
export const OPT = {} as const;
|
|
130
|
-
|
|
131
|
-
/** Field-id alias map (empty — no tables in scope). */
|
|
132
|
-
export type AppFields = typeof F;
|
|
133
|
-
/** Select-option alias map (empty — no tables in scope). */
|
|
134
|
-
export type AppOptions = typeof OPT;
|
|
135
|
-
`;
|
|
136
|
-
}
|
|
137
|
-
const aliased = aliasTables(tables);
|
|
138
|
-
return `${HEADER}
|
|
139
|
-
${emitFieldMap(aliased)}
|
|
140
|
-
|
|
141
|
-
${emitOptionMap(aliased)}
|
|
142
|
-
|
|
143
|
-
/** Field-id alias map: \`F[<TABLE>][<field>]\` is the \`fld_…\` id (literal-typed). */
|
|
144
|
-
export type AppFields = typeof F;
|
|
145
|
-
/** Select-option alias map: \`OPT[<TABLE>][<field>][<option>]\` is the \`opt_…\` id. */
|
|
146
|
-
export type AppOptions = typeof OPT;
|
|
147
|
-
`;
|
|
148
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { generateAppFields, slugifyAlias } from "./generate_app_fields.js";
|
|
3
|
-
describe("slugifyAlias", () => {
|
|
4
|
-
it("strips Vietnamese diacritics so accented + unaccented names don't collide on the accent", () => {
|
|
5
|
-
expect(slugifyAlias("Lô hàng", true)).toBe("LO_HANG");
|
|
6
|
-
expect(slugifyAlias("Đơn vị", true)).toBe("DON_VI");
|
|
7
|
-
expect(slugifyAlias("trạng thái", false)).toBe("trang_thai");
|
|
8
|
-
});
|
|
9
|
-
it("collapses non-alnum runs to a single underscore and trims edges", () => {
|
|
10
|
-
expect(slugifyAlias(" Báo giá / CTA ", true)).toBe("BAO_GIA_CTA");
|
|
11
|
-
expect(slugifyAlias("a---b", false)).toBe("a_b");
|
|
12
|
-
});
|
|
13
|
-
it("prefixes a leading digit (identifiers can't start with one)", () => {
|
|
14
|
-
expect(slugifyAlias("40HC cont", false)).toBe("_40hc_cont");
|
|
15
|
-
});
|
|
16
|
-
it("falls back to '_' for an all-symbol or empty name", () => {
|
|
17
|
-
expect(slugifyAlias("***", false)).toBe("_");
|
|
18
|
-
expect(slugifyAlias("", true)).toBe("_");
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
describe("generateAppFields", () => {
|
|
22
|
-
it("emits empty F/OPT maps that still compile when no tables are in scope", () => {
|
|
23
|
-
const out = generateAppFields([]);
|
|
24
|
-
expect(out).toContain("export const F = {} as const;");
|
|
25
|
-
expect(out).toContain("export const OPT = {} as const;");
|
|
26
|
-
});
|
|
27
|
-
it("emits F entries keyed by slugified table + field names", () => {
|
|
28
|
-
const tables = [
|
|
29
|
-
{
|
|
30
|
-
id: "tbl_1",
|
|
31
|
-
name: "Lô hàng",
|
|
32
|
-
fields: [
|
|
33
|
-
{ id: "fld_status", name: "Trạng thái" },
|
|
34
|
-
{ id: "fld_eta", name: "ETA" },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
const out = generateAppFields(tables);
|
|
39
|
-
expect(out).toContain("LO_HANG: {");
|
|
40
|
-
expect(out).toContain('trang_thai: "fld_status"');
|
|
41
|
-
expect(out).toContain('eta: "fld_eta"');
|
|
42
|
-
});
|
|
43
|
-
it("emits OPT only for fields that carry options, keyed by option label", () => {
|
|
44
|
-
const tables = [
|
|
45
|
-
{
|
|
46
|
-
id: "tbl_1",
|
|
47
|
-
name: "Shipments",
|
|
48
|
-
fields: [
|
|
49
|
-
{
|
|
50
|
-
id: "fld_status",
|
|
51
|
-
name: "Status",
|
|
52
|
-
options: [
|
|
53
|
-
{ id: "opt_open", label: "Open" },
|
|
54
|
-
{ id: "opt_cleared", label: "Cleared" },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
{ id: "fld_eta", name: "ETA" },
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
];
|
|
61
|
-
const out = generateAppFields(tables);
|
|
62
|
-
expect(out).toContain("SHIPMENTS: {");
|
|
63
|
-
expect(out).toContain('open: "opt_open"');
|
|
64
|
-
expect(out).toContain('cleared: "opt_cleared"');
|
|
65
|
-
// No options on ETA → it must not appear in the OPT block.
|
|
66
|
-
const optBlock = out.slice(out.indexOf("export const OPT"));
|
|
67
|
-
expect(optBlock).not.toContain("eta:");
|
|
68
|
-
});
|
|
69
|
-
it("omits a table from OPT entirely when none of its fields have options", () => {
|
|
70
|
-
const tables = [
|
|
71
|
-
{ id: "tbl_1", name: "Plain", fields: [{ id: "fld_a", name: "A" }] },
|
|
72
|
-
];
|
|
73
|
-
const out = generateAppFields(tables);
|
|
74
|
-
expect(out).toContain("export const OPT = {} as const;");
|
|
75
|
-
});
|
|
76
|
-
it("dedupes collided aliases with _2/_3 in stable field order", () => {
|
|
77
|
-
const tables = [
|
|
78
|
-
{
|
|
79
|
-
id: "tbl_1",
|
|
80
|
-
name: "T",
|
|
81
|
-
fields: [
|
|
82
|
-
{ id: "fld_a", name: "Ngày" },
|
|
83
|
-
{ id: "fld_b", name: "ngày" }, // slugifies to the same `ngay`
|
|
84
|
-
{ id: "fld_c", name: "Ngày!" }, // also `ngay`
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
const out = generateAppFields(tables);
|
|
89
|
-
expect(out).toContain('ngay: "fld_a"');
|
|
90
|
-
expect(out).toContain('ngay_2: "fld_b"');
|
|
91
|
-
expect(out).toContain('ngay_3: "fld_c"');
|
|
92
|
-
});
|
|
93
|
-
it("quotes a non-identifier alias only when slugify could not produce one", () => {
|
|
94
|
-
// An all-symbol name slugifies to "_", a valid identifier — so it's bare.
|
|
95
|
-
const tables = [
|
|
96
|
-
{ id: "tbl_1", name: "***", fields: [{ id: "fld_a", name: "@@@" }] },
|
|
97
|
-
];
|
|
98
|
-
const out = generateAppFields(tables);
|
|
99
|
-
expect(out).toContain("_: {");
|
|
100
|
-
expect(out).toContain('_: "fld_a"');
|
|
101
|
-
});
|
|
102
|
-
it("is deterministic — same schema → byte-identical output", () => {
|
|
103
|
-
const tables = [
|
|
104
|
-
{ id: "tbl_1", name: "A", fields: [{ id: "fld_a", name: "X" }] },
|
|
105
|
-
];
|
|
106
|
-
expect(generateAppFields(tables)).toBe(generateAppFields(tables));
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codegen: emit `.lotics/app_queries.d.ts` from the app's manifest.
|
|
3
|
-
*
|
|
4
|
-
* The starter tsconfig.json includes `.lotics/` in its `include` list, so this
|
|
5
|
-
* file is auto-discovered by `tsc --noEmit` and the bundler. The augmentation
|
|
6
|
-
* adds a typed entry to `AppQueries` for every alias declared in
|
|
7
|
-
* `package.json#lotics.queries`, mapping each to its declared param shape:
|
|
8
|
-
*
|
|
9
|
-
* - `params` declared → `alias: { …declared shape… }`
|
|
10
|
-
* - `params` omitted → `alias: Record<string, never>` (no params)
|
|
11
|
-
*
|
|
12
|
-
* Result: `useQuery("alias", params)` is typed against the manifest, and an
|
|
13
|
-
* undeclared alias is a compile-time error.
|
|
14
|
-
*
|
|
15
|
-
* The typed-input → TS-type mappers are shared with the workflow codegen —
|
|
16
|
-
* both surfaces use the same `AppWorkflowInput` vocabulary.
|
|
17
|
-
*
|
|
18
|
-
* Pure function. Same inputs → same output. Idempotent.
|
|
19
|
-
*/
|
|
20
|
-
import type { AppQueryDeclaration } from "./app_commands.js";
|
|
21
|
-
export declare function generateAppQueriesDts(queries: Record<string, AppQueryDeclaration> | undefined): string;
|