@lotics/cli 0.57.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/app_commands.d.ts +165 -2
- package/dist/app_commands.js +803 -7
- package/dist/app_commands.test.js +569 -2
- package/dist/app_workflow_check.d.ts +77 -0
- package/dist/app_workflow_check.js +169 -0
- package/dist/app_workflow_check.test.d.ts +1 -0
- package/dist/app_workflow_check.test.js +166 -0
- package/dist/args.d.ts +4 -0
- package/dist/args.js +9 -0
- package/dist/args.test.js +12 -0
- package/dist/child_env.d.ts +13 -0
- package/dist/child_env.js +24 -0
- package/dist/cli.js +144 -30
- package/dist/client.d.ts +58 -0
- package/dist/client.js +85 -0
- package/dist/dev/server.js +2 -1
- package/dist/generate_app_fields.d.ts +54 -0
- package/dist/generate_app_fields.js +148 -0
- package/dist/generate_app_fields.test.d.ts +1 -0
- package/dist/generate_app_fields.test.js +108 -0
- package/dist/inputs.d.ts +38 -0
- package/dist/inputs.js +50 -0
- package/dist/inputs.test.d.ts +1 -0
- package/dist/inputs.test.js +89 -0
- package/dist/src/cli.js +1208 -187
- package/dist/starter_template.js +72 -2
- package/dist/starter_template.test.js +15 -0
- package/package.json +3 -1
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
});
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
});
|