@lotics/cli 0.88.0 → 0.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +162 -100
- package/dist/{client.d.ts → src/client.d.ts} +9 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /package/dist/{client.js → src/client.js} +0 -0
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type ts from "typescript";
|
|
2
|
-
/**
|
|
3
|
-
* The compiler options the SERVER uses at set-time verify — a copy of
|
|
4
|
-
* `backend/features/workflows/typecheck_with_typescript.ts:COMPILER_OPTIONS`,
|
|
5
|
-
* expressed against the project-resolved `ts` namespace so the local verdict
|
|
6
|
-
* matches the server's. The CLI can't import backend code, so this is duplicated
|
|
7
|
-
* and pinned to the same literal by tests on BOTH sides (this package's
|
|
8
|
-
* `app_workflow_check.test.ts` + the backend's `typecheck_with_typescript.test.ts`
|
|
9
|
-
* — same pattern as the `__workflow` envelope). If the server changes a flag, both
|
|
10
|
-
* pins break and force this to follow. lib `es2022` with NO DOM (a body runs on
|
|
11
|
-
* the server, not a browser); `types: []` so no `@types/*` ambient leaks in;
|
|
12
|
-
* `skipLibCheck` keeps lib-typecheck off the hot path.
|
|
13
|
-
*/
|
|
14
|
-
export declare function workflowCheckCompilerOptions(tsApi: typeof ts): ts.CompilerOptions;
|
|
15
|
-
/** One type error, mapped back to the author's body coordinates. */
|
|
16
|
-
export interface WorkflowCheckIssue {
|
|
17
|
-
/** 1-indexed line in the author's body (the envelope-prefix offset removed). */
|
|
18
|
-
line: number;
|
|
19
|
-
/** 1-indexed column in the author's body. */
|
|
20
|
-
col: number;
|
|
21
|
-
/** TS-prefixed diagnostic code, e.g. "TS2339". */
|
|
22
|
-
code: string;
|
|
23
|
-
/** Human-readable message; multi-line TS messages joined with `\n`. */
|
|
24
|
-
message: string;
|
|
25
|
-
}
|
|
26
|
-
/** The verdict for one alias's body. `issues: []` ⇒ clean. */
|
|
27
|
-
export interface WorkflowCheckAliasResult {
|
|
28
|
-
alias: string;
|
|
29
|
-
bodyPath: string;
|
|
30
|
-
issues: WorkflowCheckIssue[];
|
|
31
|
-
}
|
|
32
|
-
export interface WorkflowCheckInput {
|
|
33
|
-
/** Absolute path to the wrapped body file (`src/workflows/<alias>.ts`). */
|
|
34
|
-
bodyPath: string;
|
|
35
|
-
/** Absolute path to the per-alias ambient globals (`.lotics/workflows/<alias>.globals.d.ts`). */
|
|
36
|
-
globalsPath: string;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Count the lines the envelope prefix adds ABOVE the author's body, so a
|
|
40
|
-
* diagnostic on body line K reports as the author's line K, not K+offset. The
|
|
41
|
-
* wrapper opener (`async function __workflow(...) {`) and the header above it
|
|
42
|
-
* (the `/// <reference>`, the `//` comments, the `export {};` marker, blank
|
|
43
|
-
* lines) all sit before the first author line. The author's body is everything
|
|
44
|
-
* between the wrapper opener line and the trailing `}` — so the offset is the
|
|
45
|
-
* count of lines up to and including the opener.
|
|
46
|
-
*
|
|
47
|
-
* Mirrors the body shape `writeWorkflowFile` produces; recognized structurally
|
|
48
|
-
* (the `__workflow` opener) so a future header tweak can't silently desync the
|
|
49
|
-
* line mapping. A file with no recognizable opener (degraded) maps with a zero
|
|
50
|
-
* offset rather than guessing.
|
|
51
|
-
*/
|
|
52
|
-
export declare function bodyLineOffset(wrappedSource: string): number;
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the app's own `typescript` from `projectDir`. Loud, actionable error
|
|
55
|
-
* when the app has no compiler installed (never a silent skip — a skipped check
|
|
56
|
-
* reads as a clean check). The dynamic import is the boundary adapter the file
|
|
57
|
-
* header explains: the only place we load a project-local peer tool.
|
|
58
|
-
*/
|
|
59
|
-
export declare function loadProjectTypescript(projectDir: string): Promise<typeof ts>;
|
|
60
|
-
/**
|
|
61
|
-
* Type-check one alias's body in an ISOLATED program built from exactly that
|
|
62
|
-
* alias's `{body, globals}` pair — so the ambient `trigger` is unambiguous and
|
|
63
|
-
* `trigger.app_workflow.inputs` is checked against THIS alias's inputs. Returns
|
|
64
|
-
* every diagnostic that lands in the body file, mapped back to the author's
|
|
65
|
-
* coordinates.
|
|
66
|
-
*/
|
|
67
|
-
export declare function checkOneWorkflowBody(tsApi: typeof ts, input: WorkflowCheckInput): WorkflowCheckIssue[];
|
|
68
|
-
/**
|
|
69
|
-
* Type-check every requested alias in ONE process (one isolated program each).
|
|
70
|
-
* Pure over its inputs (the resolved `ts` + the `{body, globals}` paths) so it's
|
|
71
|
-
* unit-testable without the CLI/manifest plumbing. Aliases are returned in the
|
|
72
|
-
* order given.
|
|
73
|
-
*/
|
|
74
|
-
export declare function checkWorkflowBodies(tsApi: typeof ts, aliases: {
|
|
75
|
-
alias: string;
|
|
76
|
-
input: WorkflowCheckInput;
|
|
77
|
-
}[]): WorkflowCheckAliasResult[];
|
|
@@ -1,169 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,166 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI argument parser.
|
|
3
|
-
*
|
|
4
|
-
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
|
-
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
|
-
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
-
* flags (`--json`, `--local`, …) toggle. Anything not matching a known flag is
|
|
8
|
-
* positional.
|
|
9
|
-
*
|
|
10
|
-
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
|
-
* on import, so importing the parser from there would execute the CLI.
|
|
12
|
-
*/
|
|
13
|
-
export declare function parseArgs(argv: string[]): {
|
|
14
|
-
command?: string;
|
|
15
|
-
subcommand?: string;
|
|
16
|
-
toolArgs?: string;
|
|
17
|
-
restArgs: string[];
|
|
18
|
-
flags: {
|
|
19
|
-
json: boolean;
|
|
20
|
-
timeout?: number;
|
|
21
|
-
output?: string;
|
|
22
|
-
as?: string;
|
|
23
|
-
apiKey?: string;
|
|
24
|
-
workspace?: string;
|
|
25
|
-
viewAs?: string;
|
|
26
|
-
/** `--ui-src <path>`: absolute packages/ui/src for `ui link` in an external app. */
|
|
27
|
-
uiSrc?: string;
|
|
28
|
-
name?: string;
|
|
29
|
-
timezone?: string;
|
|
30
|
-
message?: string;
|
|
31
|
-
local: boolean;
|
|
32
|
-
all: boolean;
|
|
33
|
-
/** `--yes` (alias `-y`): confirm a destructive command (e.g. `workspace delete`) non-interactively. */
|
|
34
|
-
yes: boolean;
|
|
35
|
-
/** `--print-created` (alias `--report-effects`): print the honest post-run side-effect harvest. */
|
|
36
|
-
printCreated: boolean;
|
|
37
|
-
/** `--cleanup`: also delete the harvested created records (records only). */
|
|
38
|
-
cleanup: boolean;
|
|
39
|
-
version: boolean;
|
|
40
|
-
help: boolean;
|
|
41
|
-
};
|
|
42
|
-
};
|
package/dist/args.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI argument parser.
|
|
3
|
-
*
|
|
4
|
-
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
|
-
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
|
-
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
-
* flags (`--json`, `--local`, …) toggle. Anything not matching a known flag is
|
|
8
|
-
* positional.
|
|
9
|
-
*
|
|
10
|
-
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
|
-
* on import, so importing the parser from there would execute the CLI.
|
|
12
|
-
*/
|
|
13
|
-
export function parseArgs(argv) {
|
|
14
|
-
const flags = {
|
|
15
|
-
json: false,
|
|
16
|
-
timeout: undefined,
|
|
17
|
-
output: undefined,
|
|
18
|
-
as: undefined,
|
|
19
|
-
apiKey: undefined,
|
|
20
|
-
workspace: undefined,
|
|
21
|
-
viewAs: undefined,
|
|
22
|
-
uiSrc: undefined,
|
|
23
|
-
name: undefined,
|
|
24
|
-
timezone: undefined,
|
|
25
|
-
message: undefined,
|
|
26
|
-
local: false,
|
|
27
|
-
all: false,
|
|
28
|
-
yes: false,
|
|
29
|
-
printCreated: false,
|
|
30
|
-
cleanup: false,
|
|
31
|
-
version: false,
|
|
32
|
-
help: false,
|
|
33
|
-
};
|
|
34
|
-
let command;
|
|
35
|
-
let subcommand;
|
|
36
|
-
let toolArgs;
|
|
37
|
-
const restArgs = [];
|
|
38
|
-
let i = 0;
|
|
39
|
-
while (i < argv.length) {
|
|
40
|
-
const arg = argv[i];
|
|
41
|
-
switch (arg) {
|
|
42
|
-
case "--json":
|
|
43
|
-
flags.json = true;
|
|
44
|
-
break;
|
|
45
|
-
case "--timeout":
|
|
46
|
-
flags.timeout = parseInt(argv[++i], 10);
|
|
47
|
-
break;
|
|
48
|
-
case "--output":
|
|
49
|
-
case "-o":
|
|
50
|
-
flags.output = argv[++i];
|
|
51
|
-
break;
|
|
52
|
-
case "--as":
|
|
53
|
-
flags.as = argv[++i];
|
|
54
|
-
break;
|
|
55
|
-
case "--api-key":
|
|
56
|
-
flags.apiKey = argv[++i];
|
|
57
|
-
break;
|
|
58
|
-
case "--workspace":
|
|
59
|
-
case "-w":
|
|
60
|
-
flags.workspace = argv[++i];
|
|
61
|
-
break;
|
|
62
|
-
case "--view-as":
|
|
63
|
-
flags.viewAs = argv[++i];
|
|
64
|
-
break;
|
|
65
|
-
case "--ui-src":
|
|
66
|
-
flags.uiSrc = argv[++i];
|
|
67
|
-
break;
|
|
68
|
-
case "--name":
|
|
69
|
-
flags.name = argv[++i];
|
|
70
|
-
break;
|
|
71
|
-
case "--timezone":
|
|
72
|
-
flags.timezone = argv[++i];
|
|
73
|
-
break;
|
|
74
|
-
case "-m":
|
|
75
|
-
case "--message":
|
|
76
|
-
flags.message = argv[++i];
|
|
77
|
-
break;
|
|
78
|
-
case "--local":
|
|
79
|
-
flags.local = true;
|
|
80
|
-
break;
|
|
81
|
-
case "--all":
|
|
82
|
-
flags.all = true;
|
|
83
|
-
break;
|
|
84
|
-
case "--yes":
|
|
85
|
-
case "-y":
|
|
86
|
-
flags.yes = true;
|
|
87
|
-
break;
|
|
88
|
-
case "--print-created":
|
|
89
|
-
case "--report-effects":
|
|
90
|
-
flags.printCreated = true;
|
|
91
|
-
break;
|
|
92
|
-
case "--cleanup":
|
|
93
|
-
flags.cleanup = true;
|
|
94
|
-
break;
|
|
95
|
-
case "--version":
|
|
96
|
-
case "-v":
|
|
97
|
-
flags.version = true;
|
|
98
|
-
break;
|
|
99
|
-
case "--help":
|
|
100
|
-
case "-h":
|
|
101
|
-
flags.help = true;
|
|
102
|
-
break;
|
|
103
|
-
default:
|
|
104
|
-
if (!command) {
|
|
105
|
-
command = arg;
|
|
106
|
-
}
|
|
107
|
-
else if (!subcommand) {
|
|
108
|
-
subcommand = arg;
|
|
109
|
-
}
|
|
110
|
-
else if (!toolArgs) {
|
|
111
|
-
toolArgs = arg;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
restArgs.push(arg);
|
|
115
|
-
}
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
i++;
|
|
119
|
-
}
|
|
120
|
-
return { command, subcommand, toolArgs, restArgs, flags };
|
|
121
|
-
}
|
package/dist/args.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/args.test.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseArgs } from "./args.js";
|
|
3
|
-
describe("parseArgs", () => {
|
|
4
|
-
it("splits command / subcommand / positional", () => {
|
|
5
|
-
const r = parseArgs(["app", "deploy", "my message"]);
|
|
6
|
-
expect(r.command).toBe("app");
|
|
7
|
-
expect(r.subcommand).toBe("deploy");
|
|
8
|
-
expect(r.toolArgs).toBe("my message");
|
|
9
|
-
expect(r.restArgs).toEqual([]);
|
|
10
|
-
});
|
|
11
|
-
// `app deploy` regression: `-m` must be parsed as a flag, not silently
|
|
12
|
-
// consumed as the positional message.
|
|
13
|
-
it("parses -m as the message flag", () => {
|
|
14
|
-
const r = parseArgs(["app", "deploy", "-m", "a message"]);
|
|
15
|
-
expect(r.flags.message).toBe("a message");
|
|
16
|
-
expect(r.toolArgs).toBeUndefined();
|
|
17
|
-
});
|
|
18
|
-
it("parses --message as the message flag", () => {
|
|
19
|
-
const r = parseArgs(["app", "deploy", "--message", "a message"]);
|
|
20
|
-
expect(r.flags.message).toBe("a message");
|
|
21
|
-
});
|
|
22
|
-
it("defaults message to undefined for a bare deploy", () => {
|
|
23
|
-
const r = parseArgs(["app", "deploy"]);
|
|
24
|
-
expect(r.flags.message).toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
it("treats a bare --version as the boolean CLI-version flag", () => {
|
|
27
|
-
const r = parseArgs(["--version"]);
|
|
28
|
-
expect(r.flags.version).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
it("treats -v as the boolean CLI-version flag", () => {
|
|
31
|
-
const r = parseArgs(["-v"]);
|
|
32
|
-
expect(r.flags.version).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
it("parses --workspace as a value flag", () => {
|
|
35
|
-
const r = parseArgs(["run", "query_tables", "{}", "--workspace", "wsp_123"]);
|
|
36
|
-
expect(r.flags.workspace).toBe("wsp_123");
|
|
37
|
-
expect(r.command).toBe("run");
|
|
38
|
-
expect(r.subcommand).toBe("query_tables");
|
|
39
|
-
expect(r.toolArgs).toBe("{}");
|
|
40
|
-
});
|
|
41
|
-
it("parses -w as the workspace alias", () => {
|
|
42
|
-
const r = parseArgs(["run", "query_tables", "-w", "wsp_123"]);
|
|
43
|
-
expect(r.flags.workspace).toBe("wsp_123");
|
|
44
|
-
});
|
|
45
|
-
it("parses --view-as as a value flag", () => {
|
|
46
|
-
const r = parseArgs(["app", "dev", "--view-as", "mbr_abc"]);
|
|
47
|
-
expect(r.flags.viewAs).toBe("mbr_abc");
|
|
48
|
-
expect(r.command).toBe("app");
|
|
49
|
-
expect(r.subcommand).toBe("dev");
|
|
50
|
-
});
|
|
51
|
-
it("parses --all as a boolean flag", () => {
|
|
52
|
-
const r = parseArgs(["auth", "logout", "--all"]);
|
|
53
|
-
expect(r.flags.all).toBe(true);
|
|
54
|
-
expect(r.command).toBe("auth");
|
|
55
|
-
expect(r.subcommand).toBe("logout");
|
|
56
|
-
});
|
|
57
|
-
it("defaults workspace to undefined and all to false", () => {
|
|
58
|
-
const r = parseArgs(["org"]);
|
|
59
|
-
expect(r.flags.workspace).toBeUndefined();
|
|
60
|
-
expect(r.flags.all).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
it("parses --print-created and --report-effects to the same boolean flag", () => {
|
|
63
|
-
expect(parseArgs(["app", "workflow", "run", "wf", "--print-created"]).flags.printCreated).toBe(true);
|
|
64
|
-
expect(parseArgs(["app", "workflow", "run", "wf", "--report-effects"]).flags.printCreated).toBe(true);
|
|
65
|
-
// The alias does not consume the next token (it's a boolean flag).
|
|
66
|
-
const r = parseArgs(["app", "workflow", "run", "wf", "--print-created"]);
|
|
67
|
-
expect(r.restArgs).toEqual(["wf"]);
|
|
68
|
-
});
|
|
69
|
-
it("parses --cleanup as a boolean flag (default false)", () => {
|
|
70
|
-
expect(parseArgs(["app", "workflow", "run", "wf", "--cleanup"]).flags.cleanup).toBe(true);
|
|
71
|
-
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.cleanup).toBe(false);
|
|
72
|
-
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.printCreated).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
it("parses --yes and -y as the same boolean confirmation flag (default false)", () => {
|
|
75
|
-
expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).flags.yes).toBe(true);
|
|
76
|
-
expect(parseArgs(["workspace", "delete", "wsp_1", "-y"]).flags.yes).toBe(true);
|
|
77
|
-
const r = parseArgs(["workspace", "delete", "wsp_1"]);
|
|
78
|
-
expect(r.flags.yes).toBe(false);
|
|
79
|
-
// The boolean flag does not consume the id positional.
|
|
80
|
-
expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).toolArgs).toBe("wsp_1");
|
|
81
|
-
});
|
|
82
|
-
it("treats `org use <name>` as command / subcommand / positional", () => {
|
|
83
|
-
const r = parseArgs(["org", "use", "Acme Corp"]);
|
|
84
|
-
expect(r.command).toBe("org");
|
|
85
|
-
expect(r.subcommand).toBe("use");
|
|
86
|
-
expect(r.toolArgs).toBe("Acme Corp");
|
|
87
|
-
});
|
|
88
|
-
});
|