@lotics/cli 0.60.1 → 0.62.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/README.md +3 -2
- package/dist/app_commands.d.ts +41 -0
- package/dist/app_commands.js +199 -10
- package/dist/app_commands.test.js +69 -1
- 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/cli.js +13 -1
- package/dist/src/cli.js +435 -233
- package/dist/starter_template.js +25 -41
- package/dist/starter_template.test.js +14 -10
- package/package.json +2 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lotics app workflow check [alias]` — local TypeScript type check of editable
|
|
3
|
+
* workflow bodies, ONE isolated program per bound alias (GAP-59 fix).
|
|
4
|
+
*
|
|
5
|
+
* The bug it replaces: a single tsc program over `src/workflows/*.ts` +
|
|
6
|
+
* `.lotics/workflows/**` compiles ALL aliases' ambient globals together. Each
|
|
7
|
+
* `.globals.d.ts` declares its OWN `declare const trigger: AppWorkflowTrigger`
|
|
8
|
+
* (where `app_workflow.inputs` is THAT alias's input shape) at top level — so N
|
|
9
|
+
* aliases means N colliding ambient `trigger` declarations. tsc resolves ONE, and
|
|
10
|
+
* every body then type-checks `trigger.app_workflow.inputs` against the WRONG
|
|
11
|
+
* alias's inputs (a 22-workflow app emitted ~237 false TS2339 errors).
|
|
12
|
+
*
|
|
13
|
+
* The fix mirrors the SERVER, which verifies one body at a time and never
|
|
14
|
+
* collides: build a SEPARATE `ts.Program` per alias from exactly that alias's
|
|
15
|
+
* `{body, globals}` pair, so its ambient `trigger` is unambiguous. All aliases
|
|
16
|
+
* run in ONE node process (N in-memory programs, not N `tsc` spawns).
|
|
17
|
+
*
|
|
18
|
+
* `typescript` is resolved from the APP's own `node_modules` (it ships `tsc` as a
|
|
19
|
+
* devDependency), not bundled into the CLI bin: the verdict then tracks the exact
|
|
20
|
+
* compiler version the app's own `npm run typecheck` / CI uses, and the bin stays
|
|
21
|
+
* lean (the compiler is ~9MB). This is the one place the CLI loads a project-local
|
|
22
|
+
* peer tool — the path is only knowable at runtime and the version must match the
|
|
23
|
+
* app's, so there is no static-import alternative.
|
|
24
|
+
*/
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { createRequire } from "node:module";
|
|
28
|
+
/**
|
|
29
|
+
* The compiler options the SERVER uses at set-time verify — a copy of
|
|
30
|
+
* `backend/features/workflows/typecheck_with_typescript.ts:COMPILER_OPTIONS`,
|
|
31
|
+
* expressed against the project-resolved `ts` namespace so the local verdict
|
|
32
|
+
* matches the server's. The CLI can't import backend code, so this is duplicated
|
|
33
|
+
* and pinned to the same literal by tests on BOTH sides (this package's
|
|
34
|
+
* `app_workflow_check.test.ts` + the backend's `typecheck_with_typescript.test.ts`
|
|
35
|
+
* — same pattern as the `__workflow` envelope). If the server changes a flag, both
|
|
36
|
+
* pins break and force this to follow. lib `es2022` with NO DOM (a body runs on
|
|
37
|
+
* the server, not a browser); `types: []` so no `@types/*` ambient leaks in;
|
|
38
|
+
* `skipLibCheck` keeps lib-typecheck off the hot path.
|
|
39
|
+
*/
|
|
40
|
+
export function workflowCheckCompilerOptions(tsApi) {
|
|
41
|
+
return {
|
|
42
|
+
target: tsApi.ScriptTarget.ES2022,
|
|
43
|
+
module: tsApi.ModuleKind.ESNext,
|
|
44
|
+
lib: ["lib.es2022.d.ts"],
|
|
45
|
+
strict: true,
|
|
46
|
+
noImplicitAny: true,
|
|
47
|
+
strictNullChecks: true,
|
|
48
|
+
noImplicitReturns: false,
|
|
49
|
+
noEmit: true,
|
|
50
|
+
allowJs: false,
|
|
51
|
+
isolatedModules: false,
|
|
52
|
+
skipLibCheck: true,
|
|
53
|
+
moduleResolution: tsApi.ModuleResolutionKind.NodeNext,
|
|
54
|
+
types: [],
|
|
55
|
+
declaration: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Count the lines the envelope prefix adds ABOVE the author's body, so a
|
|
60
|
+
* diagnostic on body line K reports as the author's line K, not K+offset. The
|
|
61
|
+
* wrapper opener (`async function __workflow(...) {`) and the header above it
|
|
62
|
+
* (the `/// <reference>`, the `//` comments, the `export {};` marker, blank
|
|
63
|
+
* lines) all sit before the first author line. The author's body is everything
|
|
64
|
+
* between the wrapper opener line and the trailing `}` — so the offset is the
|
|
65
|
+
* count of lines up to and including the opener.
|
|
66
|
+
*
|
|
67
|
+
* Mirrors the body shape `writeWorkflowFile` produces; recognized structurally
|
|
68
|
+
* (the `__workflow` opener) so a future header tweak can't silently desync the
|
|
69
|
+
* line mapping. A file with no recognizable opener (degraded) maps with a zero
|
|
70
|
+
* offset rather than guessing.
|
|
71
|
+
*/
|
|
72
|
+
export function bodyLineOffset(wrappedSource) {
|
|
73
|
+
const lines = wrappedSource.split("\n");
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
if (/^\s*async\s+function\s+__workflow\s*\(/.test(lines[i]))
|
|
76
|
+
return i + 1;
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the app's own `typescript` from `projectDir`. Loud, actionable error
|
|
82
|
+
* when the app has no compiler installed (never a silent skip — a skipped check
|
|
83
|
+
* reads as a clean check). The dynamic import is the boundary adapter the file
|
|
84
|
+
* header explains: the only place we load a project-local peer tool.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadProjectTypescript(projectDir) {
|
|
87
|
+
const requireFromProject = createRequire(path.join(projectDir, "package.json"));
|
|
88
|
+
let resolved;
|
|
89
|
+
try {
|
|
90
|
+
resolved = requireFromProject.resolve("typescript");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error(`Could not resolve 'typescript' from ${projectDir}. ` +
|
|
94
|
+
`'lotics app workflow check' type-checks bodies with the app's own compiler — ` +
|
|
95
|
+
`run 'npm install' (the scaffold ships typescript as a devDependency).`);
|
|
96
|
+
}
|
|
97
|
+
const mod = (await import(pathToImportUrl(resolved)));
|
|
98
|
+
// The CJS typescript module loads under an ESM `import()` as `{ default: ts }`;
|
|
99
|
+
// a future ESM build would expose the namespace directly. Accept both.
|
|
100
|
+
return mod.default ?? mod;
|
|
101
|
+
}
|
|
102
|
+
/** A filesystem path → a `file://` URL `import()` accepts on every platform. */
|
|
103
|
+
function pathToImportUrl(p) {
|
|
104
|
+
const resolved = path.resolve(p);
|
|
105
|
+
const prefixed = resolved.startsWith("/") ? resolved : `/${resolved}`;
|
|
106
|
+
return `file://${prefixed.split(path.sep).join("/")}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Type-check one alias's body in an ISOLATED program built from exactly that
|
|
110
|
+
* alias's `{body, globals}` pair — so the ambient `trigger` is unambiguous and
|
|
111
|
+
* `trigger.app_workflow.inputs` is checked against THIS alias's inputs. Returns
|
|
112
|
+
* every diagnostic that lands in the body file, mapped back to the author's
|
|
113
|
+
* coordinates.
|
|
114
|
+
*/
|
|
115
|
+
export function checkOneWorkflowBody(tsApi, input) {
|
|
116
|
+
const wrapped = fs.readFileSync(input.bodyPath, "utf-8");
|
|
117
|
+
const offset = bodyLineOffset(wrapped);
|
|
118
|
+
const program = tsApi.createProgram({
|
|
119
|
+
rootNames: [input.globalsPath, input.bodyPath],
|
|
120
|
+
options: workflowCheckCompilerOptions(tsApi),
|
|
121
|
+
});
|
|
122
|
+
const bodyFile = program.getSourceFile(input.bodyPath);
|
|
123
|
+
if (!bodyFile) {
|
|
124
|
+
// The program failed to ingest the body file — surface loudly rather than
|
|
125
|
+
// returning a false "clean" result.
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
line: 1,
|
|
129
|
+
col: 1,
|
|
130
|
+
code: "INTERNAL",
|
|
131
|
+
message: `workflow check: body file was not loaded (${input.bodyPath})`,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
const issues = [];
|
|
136
|
+
for (const d of tsApi.getPreEmitDiagnostics(program)) {
|
|
137
|
+
if (d.file !== bodyFile)
|
|
138
|
+
continue;
|
|
139
|
+
let line = 1;
|
|
140
|
+
let col = 1;
|
|
141
|
+
if (typeof d.start === "number") {
|
|
142
|
+
const lc = bodyFile.getLineAndCharacterOfPosition(d.start);
|
|
143
|
+
// Map the wrapped-source line back to the author's body line. A diagnostic
|
|
144
|
+
// that lands on a synthetic wrapper/header line (rare) clamps to line 1.
|
|
145
|
+
line = Math.max(1, lc.line + 1 - offset);
|
|
146
|
+
col = lc.character + 1;
|
|
147
|
+
}
|
|
148
|
+
issues.push({
|
|
149
|
+
line,
|
|
150
|
+
col,
|
|
151
|
+
code: `TS${d.code}`,
|
|
152
|
+
message: tsApi.flattenDiagnosticMessageText(d.messageText, "\n"),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return issues;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Type-check every requested alias in ONE process (one isolated program each).
|
|
159
|
+
* Pure over its inputs (the resolved `ts` + the `{body, globals}` paths) so it's
|
|
160
|
+
* unit-testable without the CLI/manifest plumbing. Aliases are returned in the
|
|
161
|
+
* order given.
|
|
162
|
+
*/
|
|
163
|
+
export function checkWorkflowBodies(tsApi, aliases) {
|
|
164
|
+
return aliases.map(({ alias, input }) => ({
|
|
165
|
+
alias,
|
|
166
|
+
bodyPath: input.bodyPath,
|
|
167
|
+
issues: checkOneWorkflowBody(tsApi, input),
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { bodyLineOffset, checkOneWorkflowBody, checkWorkflowBodies, workflowCheckCompilerOptions, } from "./app_workflow_check.js";
|
|
7
|
+
import { writeWorkflowFile, writeWorkflowGlobals, FALLBACK_ENVELOPE_PREFIX, FALLBACK_ENVELOPE_SUFFIX, } from "./app_commands.js";
|
|
8
|
+
/**
|
|
9
|
+
* The collision regression (GAP-59 fix). The shipped GAP-59 attempt compiled
|
|
10
|
+
* every alias's body + per-alias ambient globals into ONE tsc program. Each
|
|
11
|
+
* `<alias>.globals.d.ts` declares its OWN top-level `declare const trigger:
|
|
12
|
+
* AppWorkflowTrigger` whose `app_workflow.inputs` is THAT alias's input shape —
|
|
13
|
+
* so N aliases collided on the ambient `trigger`, tsc resolved one, and every
|
|
14
|
+
* body checked `trigger.app_workflow.inputs` against the wrong alias's inputs.
|
|
15
|
+
*
|
|
16
|
+
* `checkWorkflowBodies` builds an ISOLATED program per alias, so each body's
|
|
17
|
+
* `trigger` is unambiguous. The decisive case: alias A's globals type
|
|
18
|
+
* `inputs.foo`, alias B's type `inputs.bar`; A's body reads `.foo` (clean), B's
|
|
19
|
+
* reads `.bar` (clean) — both pass ONLY because each program sees only its own
|
|
20
|
+
* globals. A body reading a field that isn't in its own inputs is an error for
|
|
21
|
+
* THAT alias only.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* A faithful per-alias ambient globals `.d.ts`, matching the shape
|
|
25
|
+
* `generateWorkspaceDts` emits for an `app_workflow` trigger: a top-level
|
|
26
|
+
* `interface AppWorkflowTrigger` whose `app_workflow.inputs` carries this alias's
|
|
27
|
+
* typed inputs, `declare const trigger`, and the `__WorkflowReturn` alias the
|
|
28
|
+
* envelope's return type references.
|
|
29
|
+
*/
|
|
30
|
+
function globalsDts(inputFields) {
|
|
31
|
+
const fields = Object.entries(inputFields)
|
|
32
|
+
.map(([k, t]) => ` readonly ${k}: ${t};`)
|
|
33
|
+
.join("\n");
|
|
34
|
+
return [
|
|
35
|
+
"interface AppWorkflowTrigger {",
|
|
36
|
+
' readonly type: "app_workflow";',
|
|
37
|
+
" readonly app_workflow: {",
|
|
38
|
+
" readonly inputs: {",
|
|
39
|
+
fields,
|
|
40
|
+
" };",
|
|
41
|
+
" };",
|
|
42
|
+
"}",
|
|
43
|
+
"declare const trigger: AppWorkflowTrigger;",
|
|
44
|
+
"type __WorkflowReturn = { data?: unknown; message?: string; field_errors?: unknown };",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
describe("checkWorkflowBodies — isolated program per alias (GAP-59 collision regression)", () => {
|
|
48
|
+
let workDir;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
workDir = fs.mkdtempSync(path.join(tmpdir(), "lotics-wf-check-test-"));
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
/** Write one alias's body (wrapped via the real writer) + its globals; return the check input. */
|
|
56
|
+
function writeAlias(alias, body, inputs) {
|
|
57
|
+
const bodyPath = writeWorkflowFile(workDir, alias, body);
|
|
58
|
+
const globalsPath = writeWorkflowGlobals(workDir, alias, globalsDts(inputs));
|
|
59
|
+
return { alias, input: { bodyPath, globalsPath } };
|
|
60
|
+
}
|
|
61
|
+
it("each alias type-checks against its OWN inputs — A.foo clean, B.bar clean, no cross-talk", () => {
|
|
62
|
+
const a = writeAlias("aliasA", "const x: string = trigger.app_workflow.inputs.foo;\nreturn({ data: { x } });", { foo: "string" });
|
|
63
|
+
const b = writeAlias("aliasB", "const y: string = trigger.app_workflow.inputs.bar;\nreturn({ data: { y } });", { bar: "string" });
|
|
64
|
+
const results = checkWorkflowBodies(ts, [a, b]);
|
|
65
|
+
const byAlias = Object.fromEntries(results.map((r) => [r.alias, r]));
|
|
66
|
+
// Both clean — proving each program saw ONLY its own globals. Under the old
|
|
67
|
+
// all-in-one program, one of A/B would read a `.foo`/`.bar` that the OTHER
|
|
68
|
+
// alias's surviving ambient `trigger` doesn't declare → a false TS2339.
|
|
69
|
+
expect(byAlias.aliasA.issues).toEqual([]);
|
|
70
|
+
expect(byAlias.aliasB.issues).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
it("a body reading an input that isn't in its OWN schema is an error for THAT alias only", () => {
|
|
73
|
+
// aliasA's inputs = { foo }, but its body reads `.bar` (which only aliasB has).
|
|
74
|
+
// With isolated programs, aliasA must fail (it has no `bar`) while aliasB,
|
|
75
|
+
// reading its own `.bar`, stays clean.
|
|
76
|
+
const a = writeAlias("aliasA", "const x: string = trigger.app_workflow.inputs.bar;\nreturn({ data: { x } });", { foo: "string" });
|
|
77
|
+
const b = writeAlias("aliasB", "const y: string = trigger.app_workflow.inputs.bar;\nreturn({ data: { y } });", { bar: "string" });
|
|
78
|
+
const results = checkWorkflowBodies(ts, [a, b]);
|
|
79
|
+
const byAlias = Object.fromEntries(results.map((r) => [r.alias, r]));
|
|
80
|
+
expect(byAlias.aliasA.issues.length).toBeGreaterThan(0);
|
|
81
|
+
expect(byAlias.aliasA.issues[0].code).toBe("TS2339"); // Property 'bar' does not exist
|
|
82
|
+
expect(byAlias.aliasA.issues[0].message).toContain("bar");
|
|
83
|
+
// aliasB is unaffected — the error is scoped to the offending alias.
|
|
84
|
+
expect(byAlias.aliasB.issues).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
it("an isolated program proves a single combined program WOULD collide", () => {
|
|
87
|
+
// Build ONE program over BOTH aliases' bodies + globals (the OLD approach) and
|
|
88
|
+
// show it reports a false error that the per-alias programs do NOT — pinning
|
|
89
|
+
// exactly the bug the fix removes.
|
|
90
|
+
const a = writeAlias("aliasA", "const x: string = trigger.app_workflow.inputs.foo;\nreturn({ data: { x } });", { foo: "string" });
|
|
91
|
+
const b = writeAlias("aliasB", "const y: string = trigger.app_workflow.inputs.bar;\nreturn({ data: { y } });", { bar: "string" });
|
|
92
|
+
// Per-alias: both clean.
|
|
93
|
+
const perAlias = checkWorkflowBodies(ts, [a, b]);
|
|
94
|
+
expect(perAlias.every((r) => r.issues.length === 0)).toBe(true);
|
|
95
|
+
// All-in-one: one program over all four files. The two ambient `trigger`
|
|
96
|
+
// declarations + two `AppWorkflowTrigger`/`__WorkflowReturn` interfaces
|
|
97
|
+
// collide, so the combined program reports errors the isolated ones don't.
|
|
98
|
+
const combined = ts.createProgram({
|
|
99
|
+
rootNames: [a.input.globalsPath, a.input.bodyPath, b.input.globalsPath, b.input.bodyPath],
|
|
100
|
+
options: workflowCheckCompilerOptions(ts),
|
|
101
|
+
});
|
|
102
|
+
const combinedDiagnostics = ts.getPreEmitDiagnostics(combined);
|
|
103
|
+
// The collision is a TS2339 on a BODY file — the alias whose ambient `trigger`
|
|
104
|
+
// lost the merge reads a field that isn't on the surviving alias's inputs.
|
|
105
|
+
// Assert THAT specific diagnostic, not a bare count (an unrelated options-level
|
|
106
|
+
// TS5110, which has no `.file`, would also satisfy length > 0 — a false green).
|
|
107
|
+
const collisions = combinedDiagnostics.filter((d) => d.file !== undefined && d.code === 2339);
|
|
108
|
+
expect(collisions.length).toBeGreaterThan(0);
|
|
109
|
+
const text = collisions.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n")).join("\n");
|
|
110
|
+
expect(text).toMatch(/\b(foo|bar)\b/);
|
|
111
|
+
});
|
|
112
|
+
it("maps a diagnostic line back to the author's body (envelope offset removed)", () => {
|
|
113
|
+
// Body line 2 has the error; the report must point at line 2, not the wrapped
|
|
114
|
+
// line (the envelope + header push it down several lines).
|
|
115
|
+
const a = writeAlias("aliasA", "const ok: string = trigger.app_workflow.inputs.foo;\nconst bad: number = trigger.app_workflow.inputs.foo;\nreturn({ data: { ok, bad } });", { foo: "string" });
|
|
116
|
+
const issues = checkOneWorkflowBody(ts, a.input);
|
|
117
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
118
|
+
// The string→number assignment is on the author's line 2.
|
|
119
|
+
expect(issues[0].line).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("bodyLineOffset", () => {
|
|
123
|
+
it("counts the lines up to and including the __workflow opener", () => {
|
|
124
|
+
// Two header lines, then the opener on line 3 → offset 3 (author's line 1
|
|
125
|
+
// is the next line).
|
|
126
|
+
const wrapped = ["// header a", "// header b", FALLBACK_ENVELOPE_PREFIX.trimEnd(), "return({});", FALLBACK_ENVELOPE_SUFFIX.trim()].join("\n");
|
|
127
|
+
expect(bodyLineOffset(wrapped)).toBe(3);
|
|
128
|
+
});
|
|
129
|
+
it("returns 0 when there is no recognizable wrapper opener", () => {
|
|
130
|
+
expect(bodyLineOffset("return({});\n")).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Pin the local compiler options to the SERVER's set-time options
|
|
135
|
+
* (`COMPILER_OPTIONS` in typecheck_with_typescript.ts). The check command's
|
|
136
|
+
* verdict only matches set-time if these stay in lockstep — if the server
|
|
137
|
+
* changes a flag, this breaks and forces the local options to follow.
|
|
138
|
+
*/
|
|
139
|
+
describe("workflowCheckCompilerOptions pins the server's set-time options", () => {
|
|
140
|
+
it("matches the server's FULL pinned literal — every flag, so no flag can silently drift", () => {
|
|
141
|
+
expect(workflowCheckCompilerOptions(ts)).toEqual({
|
|
142
|
+
target: ts.ScriptTarget.ES2022,
|
|
143
|
+
module: ts.ModuleKind.ESNext,
|
|
144
|
+
lib: ["lib.es2022.d.ts"],
|
|
145
|
+
strict: true,
|
|
146
|
+
noImplicitAny: true,
|
|
147
|
+
strictNullChecks: true,
|
|
148
|
+
noImplicitReturns: false,
|
|
149
|
+
noEmit: true,
|
|
150
|
+
allowJs: false,
|
|
151
|
+
isolatedModules: false,
|
|
152
|
+
skipLibCheck: true,
|
|
153
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
154
|
+
types: [],
|
|
155
|
+
declaration: false,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it("uses the SAME __workflow envelope the server compiles bodies within", () => {
|
|
159
|
+
// The body writer wraps in this envelope; the server compiles within the
|
|
160
|
+
// identical one (pinned to the backend's SOURCE_PREFIX/SUFFIX by
|
|
161
|
+
// app_commands.test.ts + apps.test.ts). If they diverge, the local line
|
|
162
|
+
// mapping and the parse of top-level await / return({...}) break.
|
|
163
|
+
expect(FALLBACK_ENVELOPE_PREFIX).toBe("async function __workflow(): Promise<__WorkflowReturn | void> {\n");
|
|
164
|
+
expect(FALLBACK_ENVELOPE_SUFFIX).toBe("\n}");
|
|
165
|
+
});
|
|
166
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ net.setDefaultAutoSelectFamilyAttemptTimeout(2000);
|
|
|
13
13
|
import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
14
14
|
import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
|
|
15
15
|
import { VERSION } from "./version.js";
|
|
16
|
-
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appUiLink, } from "./app_commands.js";
|
|
16
|
+
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appUiLink, } from "./app_commands.js";
|
|
17
17
|
import { parseArgs } from "./args.js";
|
|
18
18
|
import { ingestJsonArgs } from "./inputs.js";
|
|
19
19
|
import { runXlsxCommand } from "./xlsx.js";
|
|
@@ -84,6 +84,8 @@ COMMANDS
|
|
|
84
84
|
lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body
|
|
85
85
|
through set_app_workflow (server verifies)
|
|
86
86
|
lotics app workflow pull Rewrite src/workflows/*.ts from the server
|
|
87
|
+
lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
|
|
88
|
+
isolated program per alias; the app's own tsc)
|
|
87
89
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
88
90
|
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
89
91
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
@@ -500,6 +502,14 @@ async function main() {
|
|
|
500
502
|
console.error("Usage: lotics ui link <component> [--remove]");
|
|
501
503
|
process.exit(1);
|
|
502
504
|
}
|
|
505
|
+
// --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
|
|
506
|
+
// Reads src/workflows/*.ts + .lotics/workflows/*.globals.d.ts and the app's own
|
|
507
|
+
// typescript; builds one isolated program per alias. Handled before the auth
|
|
508
|
+
// gate (like `ui link`) since it never touches the API.
|
|
509
|
+
if (command === "app" && subcommand === "workflow" && toolArgs === "check") {
|
|
510
|
+
await appWorkflowCheck({ alias: restArgs[0] });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
503
513
|
// --- lotics app codegen [path] — regenerate .lotics/* without a deploy ---
|
|
504
514
|
// The .d.ts companions need no auth; app_fields.ts needs a workspace, resolved
|
|
505
515
|
// when credentials are available (offline/unauth still does the .d.ts work).
|
|
@@ -590,6 +600,7 @@ async function main() {
|
|
|
590
600
|
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
|
|
591
601
|
console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
|
|
592
602
|
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
603
|
+
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
593
604
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
594
605
|
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
595
606
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
@@ -748,6 +759,7 @@ async function main() {
|
|
|
748
759
|
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow");
|
|
749
760
|
console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
|
|
750
761
|
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
762
|
+
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
751
763
|
process.exit(1);
|
|
752
764
|
};
|
|
753
765
|
if (action === "run") {
|