@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,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/args.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export declare function parseArgs(argv: string[]): {
|
|
|
28
28
|
message?: string;
|
|
29
29
|
local: boolean;
|
|
30
30
|
all: boolean;
|
|
31
|
+
/** `--print-created` (alias `--report-effects`): print the honest post-run side-effect harvest. */
|
|
32
|
+
printCreated: boolean;
|
|
33
|
+
/** `--cleanup`: also delete the harvested created records (records only). */
|
|
34
|
+
cleanup: boolean;
|
|
31
35
|
version: boolean;
|
|
32
36
|
help: boolean;
|
|
33
37
|
};
|
package/dist/args.js
CHANGED
|
@@ -24,6 +24,8 @@ export function parseArgs(argv) {
|
|
|
24
24
|
message: undefined,
|
|
25
25
|
local: false,
|
|
26
26
|
all: false,
|
|
27
|
+
printCreated: false,
|
|
28
|
+
cleanup: false,
|
|
27
29
|
version: false,
|
|
28
30
|
help: false,
|
|
29
31
|
};
|
|
@@ -74,6 +76,13 @@ export function parseArgs(argv) {
|
|
|
74
76
|
case "--all":
|
|
75
77
|
flags.all = true;
|
|
76
78
|
break;
|
|
79
|
+
case "--print-created":
|
|
80
|
+
case "--report-effects":
|
|
81
|
+
flags.printCreated = true;
|
|
82
|
+
break;
|
|
83
|
+
case "--cleanup":
|
|
84
|
+
flags.cleanup = true;
|
|
85
|
+
break;
|
|
77
86
|
case "--version":
|
|
78
87
|
case "-v":
|
|
79
88
|
flags.version = true;
|
package/dist/args.test.js
CHANGED
|
@@ -51,6 +51,18 @@ describe("parseArgs", () => {
|
|
|
51
51
|
expect(r.flags.workspace).toBeUndefined();
|
|
52
52
|
expect(r.flags.all).toBe(false);
|
|
53
53
|
});
|
|
54
|
+
it("parses --print-created and --report-effects to the same boolean flag", () => {
|
|
55
|
+
expect(parseArgs(["app", "workflow", "run", "wf", "--print-created"]).flags.printCreated).toBe(true);
|
|
56
|
+
expect(parseArgs(["app", "workflow", "run", "wf", "--report-effects"]).flags.printCreated).toBe(true);
|
|
57
|
+
// The alias does not consume the next token (it's a boolean flag).
|
|
58
|
+
const r = parseArgs(["app", "workflow", "run", "wf", "--print-created"]);
|
|
59
|
+
expect(r.restArgs).toEqual(["wf"]);
|
|
60
|
+
});
|
|
61
|
+
it("parses --cleanup as a boolean flag (default false)", () => {
|
|
62
|
+
expect(parseArgs(["app", "workflow", "run", "wf", "--cleanup"]).flags.cleanup).toBe(true);
|
|
63
|
+
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.cleanup).toBe(false);
|
|
64
|
+
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.printCreated).toBe(false);
|
|
65
|
+
});
|
|
54
66
|
it("treats `org use <name>` as command / subcommand / positional", () => {
|
|
55
67
|
const r = parseArgs(["org", "use", "Acme Corp"]);
|
|
56
68
|
expect(r.command).toBe("org");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NODE_OPTIONS` for a spawned Node child (npm, vite), forcing IPv4-first DNS +
|
|
3
|
+
* a fast Happy-Eyeballs family timeout. The CLI bin applies the equivalent as
|
|
4
|
+
* runtime API calls (cli.ts top-of-module), but a child Node process is a fresh
|
|
5
|
+
* runtime that won't inherit them — so they must travel via the env. Preserves
|
|
6
|
+
* any existing `NODE_OPTIONS`.
|
|
7
|
+
*
|
|
8
|
+
* Why: WSL2 + Node 24's Happy-Eyeballs races IPv4/IPv6 and intermittently stalls
|
|
9
|
+
* on a dead IPv6 route to api.lotics.ai. IPv4-first + a 2s per-family cap fails a
|
|
10
|
+
* bad IPv6 path fast to IPv4. Its own module so `app_commands` and `dev/server`
|
|
11
|
+
* share it without a circular import.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ipv4ChildEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NODE_OPTIONS` for a spawned Node child (npm, vite), forcing IPv4-first DNS +
|
|
3
|
+
* a fast Happy-Eyeballs family timeout. The CLI bin applies the equivalent as
|
|
4
|
+
* runtime API calls (cli.ts top-of-module), but a child Node process is a fresh
|
|
5
|
+
* runtime that won't inherit them — so they must travel via the env. Preserves
|
|
6
|
+
* any existing `NODE_OPTIONS`.
|
|
7
|
+
*
|
|
8
|
+
* Why: WSL2 + Node 24's Happy-Eyeballs races IPv4/IPv6 and intermittently stalls
|
|
9
|
+
* on a dead IPv6 route to api.lotics.ai. IPv4-first + a 2s per-family cap fails a
|
|
10
|
+
* bad IPv6 path fast to IPv4. Its own module so `app_commands` and `dev/server`
|
|
11
|
+
* share it without a circular import.
|
|
12
|
+
*/
|
|
13
|
+
export function ipv4ChildEnv(env) {
|
|
14
|
+
return {
|
|
15
|
+
...env,
|
|
16
|
+
NODE_OPTIONS: [
|
|
17
|
+
env.NODE_OPTIONS,
|
|
18
|
+
"--dns-result-order=ipv4first",
|
|
19
|
+
"--network-family-autoselection-attempt-timeout=2000",
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" "),
|
|
23
|
+
};
|
|
24
|
+
}
|