@lotics/cli 0.64.0 → 0.66.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 +8 -2
- package/dist/app_commands.d.ts +34 -9
- package/dist/app_commands.js +121 -45
- package/dist/app_commands.test.js +51 -13
- package/dist/args.d.ts +2 -0
- package/dist/args.js +4 -0
- package/dist/cli.js +35 -8
- package/dist/client.d.ts +20 -0
- package/dist/client.js +15 -0
- package/dist/generate_app_workflows_dts.js +1 -1
- package/dist/generate_app_workflows_dts.test.js +10 -0
- package/dist/src/cli.js +528 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,8 +181,14 @@ lotics app workflow pull # rewrite src/workflows/*.ts + globa
|
|
|
181
181
|
lotics app workflow check # typecheck every body locally ([alias] for one)
|
|
182
182
|
lotics app workflow set issueInvoice # push the edited src/workflows/issueInvoice.ts
|
|
183
183
|
|
|
184
|
-
#
|
|
185
|
-
|
|
184
|
+
# Iterate on a named query WITHOUT a deploy: push package.json#lotics.queries.<alias>
|
|
185
|
+
# to apps.queries (server-validated like a deploy). apps.queries is manifest-
|
|
186
|
+
# authoritative, so the next `app deploy` re-syncs it — keep the manifest current.
|
|
187
|
+
lotics app query set openInvoices # push package.json#lotics.queries.openInvoices
|
|
188
|
+
|
|
189
|
+
# Dev-link @lotics/ui to packages/ui/src for live HMR (Vite alias; deploy bundles it)
|
|
190
|
+
lotics ui link card # monorepo: packages/ui/src found automatically
|
|
191
|
+
lotics ui link card --ui-src /abs/monorepo/packages/ui/src # external app (e.g. ~/lotics_apps)
|
|
186
192
|
lotics ui link card --remove # finalize: PR + publish, then drop the alias
|
|
187
193
|
```
|
|
188
194
|
|
package/dist/app_commands.d.ts
CHANGED
|
@@ -87,16 +87,23 @@ export declare function writeWorkflowFile(projectDir: string, alias: string, sou
|
|
|
87
87
|
*/
|
|
88
88
|
export declare function stripWorkflowHeader(content: string): string;
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
90
|
+
* Heal a pre-existing app's `tsconfig.json` so its generated types load and its
|
|
91
|
+
* `npm run typecheck` stays honest. Idempotent, run on every pull / codegen /
|
|
92
|
+
* deploy. Two things the current starter ships but an app SCAFFOLDED before those
|
|
93
|
+
* releases (or with a hand-written tsconfig) can lack:
|
|
94
|
+
*
|
|
95
|
+
* 1. `include`: a bare `.lotics` is rewritten to the recursive `LOTICS_INCLUDE_GLOB`.
|
|
96
|
+
* A bare dot-dir is skipped by TypeScript's include-glob walk, so the codegen'd
|
|
97
|
+
* `.d.ts` never enter the program — every `useQuery`/`useWorkflow`/`useAgentRun`
|
|
98
|
+
* param and result silently falls back to `unknown`.
|
|
99
|
+
* 2. `exclude`: the workflow-body globs — the bodies use the app's DOM lib (the
|
|
100
|
+
* server doesn't) and the per-alias ambient globals collide on `trigger`,
|
|
101
|
+
* poisoning typecheck. Checked separately by `lotics app workflow check`.
|
|
102
|
+
*
|
|
103
|
+
* A missing / unparseable tsconfig is a non-fatal warn (the caller still succeeds);
|
|
104
|
+
* the author fixes the config.
|
|
98
105
|
*/
|
|
99
|
-
export declare function
|
|
106
|
+
export declare function ensureAppTsconfig(projectDir: string): void;
|
|
100
107
|
/**
|
|
101
108
|
* `lotics app codegen [path]` — regenerate every `.lotics/` artifact from the
|
|
102
109
|
* manifest + workspace schema, WITHOUT a deploy. The `.d.ts` companions are
|
|
@@ -296,6 +303,23 @@ export declare function appExecuteWorkflow(client: LoticsClient, args: {
|
|
|
296
303
|
export declare function appWorkflowSet(client: LoticsClient, args: {
|
|
297
304
|
alias: string;
|
|
298
305
|
}): Promise<void>;
|
|
306
|
+
/**
|
|
307
|
+
* `lotics app query set <alias>` — push `package.json#lotics.queries.<alias>` to
|
|
308
|
+
* `apps.queries` through `set_app_query`, WITHOUT a deploy. The deploy-free inner
|
|
309
|
+
* loop for named queries, parallel to `lotics app workflow set` for workflows.
|
|
310
|
+
*
|
|
311
|
+
* The declaration (`{ ast, params? }`) is read from the manifest — the same map
|
|
312
|
+
* `useQuery` codegen reads and `lotics app deploy` syncs authoritatively. The
|
|
313
|
+
* server validates it exactly as a deploy does (alias identifier, workspace-only
|
|
314
|
+
* tables, resolvable fields, declared params). Because `apps.queries` is
|
|
315
|
+
* manifest-authoritative, the next `lotics app deploy` overwrites this from the
|
|
316
|
+
* manifest — so keep the manifest as the source of truth; this only skips the
|
|
317
|
+
* build/upload round-trip while iterating. Errors (unbound alias, validation
|
|
318
|
+
* failure) print to stderr and exit non-zero.
|
|
319
|
+
*/
|
|
320
|
+
export declare function appQuerySet(client: LoticsClient, args: {
|
|
321
|
+
alias: string;
|
|
322
|
+
}): Promise<void>;
|
|
299
323
|
/**
|
|
300
324
|
* `lotics app workflow pull` — rewrite every `src/workflows/<alias>.ts` from the
|
|
301
325
|
* server without a full `lotics app pull` (no source archive, no npm install).
|
|
@@ -339,4 +363,5 @@ export declare function appUiLink(args: {
|
|
|
339
363
|
projectDir?: string;
|
|
340
364
|
component: string;
|
|
341
365
|
remove?: boolean;
|
|
366
|
+
uiSrc?: string;
|
|
342
367
|
}): void;
|
package/dist/app_commands.js
CHANGED
|
@@ -64,6 +64,17 @@ const WORKFLOW_GLOBALS_DIR = path.join(".lotics", "workflows");
|
|
|
64
64
|
* with `/` separators — tsconfig globs are POSIX even on Windows.
|
|
65
65
|
*/
|
|
66
66
|
const WORKFLOW_TSCONFIG_EXCLUDES = ["src/workflows", ".lotics/workflows"];
|
|
67
|
+
/**
|
|
68
|
+
* The include glob that actually loads the generated `.lotics/*.d.ts` companions.
|
|
69
|
+
* A BARE `.lotics` entry loads NONE of them: TypeScript's include-glob walk skips
|
|
70
|
+
* dot-directories, so `useQuery` / `useWorkflow` / `useAgentRun` fall back to their
|
|
71
|
+
* untyped string overloads and every param / input / result silently becomes
|
|
72
|
+
* `unknown`. The current starter emits the glob (`starter_template.ts`); apps
|
|
73
|
+
* scaffolded before that fix shipped the bare form and need healing.
|
|
74
|
+
*/
|
|
75
|
+
const LOTICS_INCLUDE_GLOB = ".lotics/**/*";
|
|
76
|
+
/** The bare `.lotics` forms an older starter emitted — all skipped by the glob walk. */
|
|
77
|
+
const STALE_LOTICS_INCLUDES = new Set([".lotics", "./.lotics", ".lotics/"]);
|
|
67
78
|
/**
|
|
68
79
|
* The `async function __workflow(...)` wrapper a workflow body sits inside —
|
|
69
80
|
* the SAME envelope the server compiles the body within at `set_app_workflow`
|
|
@@ -304,20 +315,27 @@ function writeAppMeta(projectDir, meta) {
|
|
|
304
315
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
305
316
|
}
|
|
306
317
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
318
|
+
* Heal a pre-existing app's `tsconfig.json` so its generated types load and its
|
|
319
|
+
* `npm run typecheck` stays honest. Idempotent, run on every pull / codegen /
|
|
320
|
+
* deploy. Two things the current starter ships but an app SCAFFOLDED before those
|
|
321
|
+
* releases (or with a hand-written tsconfig) can lack:
|
|
322
|
+
*
|
|
323
|
+
* 1. `include`: a bare `.lotics` is rewritten to the recursive `LOTICS_INCLUDE_GLOB`.
|
|
324
|
+
* A bare dot-dir is skipped by TypeScript's include-glob walk, so the codegen'd
|
|
325
|
+
* `.d.ts` never enter the program — every `useQuery`/`useWorkflow`/`useAgentRun`
|
|
326
|
+
* param and result silently falls back to `unknown`.
|
|
327
|
+
* 2. `exclude`: the workflow-body globs — the bodies use the app's DOM lib (the
|
|
328
|
+
* server doesn't) and the per-alias ambient globals collide on `trigger`,
|
|
329
|
+
* poisoning typecheck. Checked separately by `lotics app workflow check`.
|
|
330
|
+
*
|
|
331
|
+
* A missing / unparseable tsconfig is a non-fatal warn (the caller still succeeds);
|
|
332
|
+
* the author fixes the config.
|
|
315
333
|
*/
|
|
316
|
-
export function
|
|
334
|
+
export function ensureAppTsconfig(projectDir) {
|
|
317
335
|
const tsconfigPath = path.join(projectDir, "tsconfig.json");
|
|
318
336
|
if (!fs.existsSync(tsconfigPath)) {
|
|
319
|
-
console.error(`⚠ No tsconfig.json at ${projectDir} — could not ensure
|
|
320
|
-
`
|
|
337
|
+
console.error(`⚠ No tsconfig.json at ${projectDir} — could not ensure "include" has "${LOTICS_INCLUDE_GLOB}" ` +
|
|
338
|
+
`(a bare .lotics loads none of the generated types) or "exclude" has ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")}.`);
|
|
321
339
|
return;
|
|
322
340
|
}
|
|
323
341
|
let parsed;
|
|
@@ -326,30 +344,47 @@ export function ensureWorkflowTsconfigExcludes(projectDir) {
|
|
|
326
344
|
}
|
|
327
345
|
catch (err) {
|
|
328
346
|
console.error(`⚠ Could not parse tsconfig.json (${err instanceof Error ? err.message : String(err)}) — ` +
|
|
329
|
-
`
|
|
347
|
+
`ensure "include" has "${LOTICS_INCLUDE_GLOB}" and "exclude" has ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")} manually.`);
|
|
330
348
|
return;
|
|
331
349
|
}
|
|
332
350
|
if (!parsed || typeof parsed !== "object")
|
|
333
351
|
return;
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
352
|
+
const changes = [];
|
|
353
|
+
// 1. Heal a stale bare `.lotics` include so the generated `.d.ts` actually load
|
|
354
|
+
// (a bare dot-dir is invisible to TypeScript's include-glob walk).
|
|
355
|
+
if (Array.isArray(parsed.include)) {
|
|
356
|
+
const inc = parsed.include.filter((e) => typeof e === "string");
|
|
357
|
+
if (inc.some((e) => STALE_LOTICS_INCLUDES.has(e))) {
|
|
358
|
+
const healed = inc.map((e) => (STALE_LOTICS_INCLUDES.has(e) ? LOTICS_INCLUDE_GLOB : e));
|
|
359
|
+
parsed.include = healed.filter((e, i) => healed.indexOf(e) === i); // dedupe if the glob was already present
|
|
360
|
+
changes.push(`rewrote a bare ".lotics" to "${LOTICS_INCLUDE_GLOB}" in "include" (a dot-dir loads zero generated types)`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 2. Ensure the workflow-body excludes. `exclude` is a TOP-LEVEL tsconfig field —
|
|
364
|
+
// tsc ignores `compilerOptions.exclude`, so they must land at the top level.
|
|
365
|
+
const currentEx = Array.isArray(parsed.exclude)
|
|
366
|
+
? parsed.exclude.filter((e) => typeof e === "string")
|
|
367
|
+
: [];
|
|
368
|
+
const toAdd = WORKFLOW_TSCONFIG_EXCLUDES.filter((g) => !currentEx.includes(g));
|
|
369
|
+
if (toAdd.length > 0) {
|
|
370
|
+
parsed.exclude = [...currentEx, ...toAdd];
|
|
371
|
+
changes.push(`added ${toAdd.join(", ")} to "exclude"`);
|
|
372
|
+
}
|
|
373
|
+
if (changes.length === 0)
|
|
341
374
|
return;
|
|
342
|
-
parsed.exclude = [...currentStrings, ...toAdd];
|
|
343
375
|
fs.writeFileSync(tsconfigPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
344
|
-
console.error(`Patched tsconfig.json:
|
|
345
|
-
`(check them with: lotics app workflow check).`);
|
|
376
|
+
console.error(`Patched tsconfig.json: ${changes.join("; ")}.`);
|
|
346
377
|
}
|
|
347
378
|
/**
|
|
348
|
-
* Write
|
|
349
|
-
* manifest's
|
|
350
|
-
* dev / deploy
|
|
351
|
-
*
|
|
352
|
-
*
|
|
379
|
+
* Write the three `.lotics/app_{workflows,queries,agents}.d.ts` companions from
|
|
380
|
+
* the manifest's maps, then heal the app's tsconfig so they actually load. Called
|
|
381
|
+
* from `app create / pull / dev / deploy / codegen`, so the augmented `AppWorkflows`
|
|
382
|
+
* / `AppQueries` / `AppAgents` types stay in sync with the manifest.
|
|
383
|
+
*
|
|
384
|
+
* The heal is at the write boundary on purpose: a `.d.ts` written but not loaded is
|
|
385
|
+
* useless (a bare `.lotics` include is skipped by TypeScript's include-glob walk and
|
|
386
|
+
* loads zero of them), so `ensureAppTsconfig` couples "wrote the types" with "the
|
|
387
|
+
* program can see them" — no caller can do one without the other.
|
|
353
388
|
*/
|
|
354
389
|
function writeAppDts(projectDir, manifest) {
|
|
355
390
|
const dotLotics = path.join(projectDir, ".lotics");
|
|
@@ -361,6 +396,7 @@ function writeAppDts(projectDir, manifest) {
|
|
|
361
396
|
];
|
|
362
397
|
for (const [file, content] of written)
|
|
363
398
|
fs.writeFileSync(file, content);
|
|
399
|
+
ensureAppTsconfig(projectDir);
|
|
364
400
|
return written.map(([file]) => file);
|
|
365
401
|
}
|
|
366
402
|
/**
|
|
@@ -450,10 +486,6 @@ export async function appCodegen(args) {
|
|
|
450
486
|
// it, so the local typecheck tracks the current workspace schema. Aliases
|
|
451
487
|
// never pulled (no body file yet) are skipped — codegen isn't a pull.
|
|
452
488
|
await refreshWorkflowGlobals(args.client, projectDir, meta.app_id, Object.keys(meta.workflows ?? {}));
|
|
453
|
-
// Keep the main tsconfig excluding the workflow-body globs (idempotent) so
|
|
454
|
-
// npm run typecheck never loads the bodies or their colliding per-alias globals.
|
|
455
|
-
if (Object.keys(meta.workflows ?? {}).length > 0)
|
|
456
|
-
ensureWorkflowTsconfigExcludes(projectDir);
|
|
457
489
|
}
|
|
458
490
|
/**
|
|
459
491
|
* For each bound alias that already has a local body file, fetch its current
|
|
@@ -745,9 +777,6 @@ export async function appPull(client, args) {
|
|
|
745
777
|
if (written.length > 0) {
|
|
746
778
|
console.error(`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`);
|
|
747
779
|
}
|
|
748
|
-
// A pre-existing app's tsconfig may predate the workflow-body excludes; pulling
|
|
749
|
-
// bodies into it would break its npm run typecheck. Patch it idempotently.
|
|
750
|
-
ensureWorkflowTsconfigExcludes(targetPath);
|
|
751
780
|
}
|
|
752
781
|
console.error(`Installing npm dependencies...`);
|
|
753
782
|
await runNpm(["install"], targetPath);
|
|
@@ -867,9 +896,13 @@ args) {
|
|
|
867
896
|
// `capabilities` block turns every capability OFF on the next deploy
|
|
868
897
|
// (fail-safe; the declaration is the grant).
|
|
869
898
|
capabilities: meta.capabilities ?? {},
|
|
870
|
-
// Workflow
|
|
871
|
-
// remove_app_workflow own apps.workflows.
|
|
872
|
-
//
|
|
899
|
+
// Workflow BINDINGS are NOT a deploy concern — set_app_workflow /
|
|
900
|
+
// remove_app_workflow own apps.workflows. But the alias KEYS of the
|
|
901
|
+
// manifest's `workflows` map ARE sent (never the bindings): they record
|
|
902
|
+
// which aliases this bundle declares, so remove_app_workflow can refuse
|
|
903
|
+
// to unbind an alias the served version still calls. Drop an alias from
|
|
904
|
+
// the manifest + redeploy to lift that guard before removing its binding.
|
|
905
|
+
workflow_aliases: Object.keys(meta.workflows ?? {}),
|
|
873
906
|
});
|
|
874
907
|
writeAppMeta(projectDir, {
|
|
875
908
|
...meta,
|
|
@@ -1169,6 +1202,37 @@ export async function appWorkflowSet(client, args) {
|
|
|
1169
1202
|
console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
|
|
1170
1203
|
}
|
|
1171
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* `lotics app query set <alias>` — push `package.json#lotics.queries.<alias>` to
|
|
1207
|
+
* `apps.queries` through `set_app_query`, WITHOUT a deploy. The deploy-free inner
|
|
1208
|
+
* loop for named queries, parallel to `lotics app workflow set` for workflows.
|
|
1209
|
+
*
|
|
1210
|
+
* The declaration (`{ ast, params? }`) is read from the manifest — the same map
|
|
1211
|
+
* `useQuery` codegen reads and `lotics app deploy` syncs authoritatively. The
|
|
1212
|
+
* server validates it exactly as a deploy does (alias identifier, workspace-only
|
|
1213
|
+
* tables, resolvable fields, declared params). Because `apps.queries` is
|
|
1214
|
+
* manifest-authoritative, the next `lotics app deploy` overwrites this from the
|
|
1215
|
+
* manifest — so keep the manifest as the source of truth; this only skips the
|
|
1216
|
+
* build/upload round-trip while iterating. Errors (unbound alias, validation
|
|
1217
|
+
* failure) print to stderr and exit non-zero.
|
|
1218
|
+
*/
|
|
1219
|
+
export async function appQuerySet(client, args) {
|
|
1220
|
+
const projectDir = process.cwd();
|
|
1221
|
+
const meta = readAppMeta(projectDir);
|
|
1222
|
+
const declaration = meta.queries?.[args.alias];
|
|
1223
|
+
if (!declaration) {
|
|
1224
|
+
console.error(`No query "${args.alias}" in package.json#lotics.queries. ` +
|
|
1225
|
+
`Declare it there (alias → { ast, params? }) first.`);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
const res = await client.setAppQuery(meta.app_id, args.alias, declaration);
|
|
1229
|
+
if (res.error) {
|
|
1230
|
+
console.error(`Failed to set query "${args.alias}": ${res.error}`);
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
console.error(`Set query "${args.alias}" on ${meta.app_id}. ` +
|
|
1234
|
+
`(apps.queries is manifest-authoritative — the next 'lotics app deploy' re-syncs it.)`);
|
|
1235
|
+
}
|
|
1172
1236
|
/**
|
|
1173
1237
|
* `lotics app workflow pull` — rewrite every `src/workflows/<alias>.ts` from the
|
|
1174
1238
|
* server without a full `lotics app pull` (no source archive, no npm install).
|
|
@@ -1187,9 +1251,10 @@ export async function appWorkflowPull(client) {
|
|
|
1187
1251
|
const written = await writeWorkflowFiles(client, projectDir, meta.app_id, aliases);
|
|
1188
1252
|
console.error(`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` +
|
|
1189
1253
|
(written.length > 0 ? ` (${written.join(", ")})` : ""));
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1254
|
+
// This command writes workflow BODIES, not the `.d.ts` — so it doesn't reach
|
|
1255
|
+
// `writeAppDts`'s heal. Heal here so the bodies land excluded and a stale
|
|
1256
|
+
// `.lotics` include doesn't leave the app's other generated types dead.
|
|
1257
|
+
ensureAppTsconfig(projectDir);
|
|
1193
1258
|
}
|
|
1194
1259
|
/**
|
|
1195
1260
|
* `lotics app workflow check [alias]` — local TypeScript type check of the
|
|
@@ -1328,11 +1393,18 @@ export function appUiLink(args) {
|
|
|
1328
1393
|
if (!fs.existsSync(viteConfigPath)) {
|
|
1329
1394
|
throw new Error(`No vite.config.ts at ${projectDir}. Run inside a 'lotics app' project directory.`);
|
|
1330
1395
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1396
|
+
// Resolve packages/ui/src. An explicit --ui-src / LOTICS_UI_SRC wins — that's how
|
|
1397
|
+
// an EXTERNAL app (one that consumes @lotics/ui from npm, with no monorepo above
|
|
1398
|
+
// it) links the local kit; otherwise walk up for a monorepo checkout.
|
|
1399
|
+
const explicit = args.uiSrc ?? process.env.LOTICS_UI_SRC;
|
|
1400
|
+
const uiSrc = explicit ? path.resolve(explicit) : findUiSrcDir(projectDir);
|
|
1401
|
+
if (!uiSrc || !fs.existsSync(uiSrc) || !fs.statSync(uiSrc).isDirectory()) {
|
|
1402
|
+
throw new Error(explicit
|
|
1403
|
+
? `--ui-src / LOTICS_UI_SRC points at '${explicit}', which is not a directory. ` +
|
|
1404
|
+
`Pass the absolute path to the monorepo's packages/ui/src.`
|
|
1405
|
+
: "Cannot find packages/ui/src by walking up from this directory. For an EXTERNAL app " +
|
|
1406
|
+
"(consuming @lotics/ui from npm), pass --ui-src=<abs path to packages/ui/src> or set " +
|
|
1407
|
+
"LOTICS_UI_SRC; inside a monorepo checkout it is found automatically.");
|
|
1336
1408
|
}
|
|
1337
1409
|
// Validate the named component exists in src so a typo fails loud (the alias
|
|
1338
1410
|
// itself stays package-wide — this is the advisory check the spec calls for).
|
|
@@ -1373,6 +1445,10 @@ export function appUiLink(args) {
|
|
|
1373
1445
|
fs.writeFileSync(viteConfigPath, updated);
|
|
1374
1446
|
console.error(`Dev-linked @lotics/ui → ${uiSrc} in ${viteConfigPath}.`);
|
|
1375
1447
|
console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
|
|
1448
|
+
// The app's tsc still resolves @lotics/ui from node_modules (the published .d.ts) —
|
|
1449
|
+
// the kit `src` can't be typechecked in an app because it's RN-Web (uses
|
|
1450
|
+
// react-native-web types the app resolves as base react-native). Typecheck the kit
|
|
1451
|
+
// in packages/ui; the finalize publish restores the app's own typecheck.
|
|
1376
1452
|
console.error("Finalize: PR the packages/ui change → publish → `lotics ui link <component> --remove` + bump the app's dep.");
|
|
1377
1453
|
}
|
|
1378
1454
|
/** Escape a string for literal use inside a RegExp. */
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { stampPulledManifest, undeclaredCapabilities, appDirName, defaultPullTarget,
|
|
5
|
+
import { stampPulledManifest, undeclaredCapabilities, appDirName, defaultPullTarget, ensureAppTsconfig, appCodegen, appUiLink, appVersions, appWorkflowSet, appWorkflowPull, appExecuteWorkflow, writeWorkflowFile, writeWorkflowGlobals, stripWorkflowHeader, FALLBACK_ENVELOPE_PREFIX, FALLBACK_ENVELOPE_SUFFIX, } from "./app_commands.js";
|
|
6
6
|
/**
|
|
7
7
|
* `appPull` reads workflows from the live App row (server response), NOT from
|
|
8
8
|
* the manifest embedded in the extracted source archive. The frozen archive
|
|
@@ -208,8 +208,9 @@ describe("appCodegen (.d.ts-only path)", () => {
|
|
|
208
208
|
});
|
|
209
209
|
/**
|
|
210
210
|
* `appUiLink` edits the app's vite.config.ts resolve.alias to dev-link
|
|
211
|
-
* @lotics/ui at
|
|
212
|
-
*
|
|
211
|
+
* @lotics/ui at packages/ui/src — auto-found in a monorepo checkout, or given
|
|
212
|
+
* explicitly via --ui-src for an external app. It validates the src + component,
|
|
213
|
+
* fails loud when neither resolves, and insert/remove is idempotent.
|
|
213
214
|
*/
|
|
214
215
|
describe("appUiLink", () => {
|
|
215
216
|
let root;
|
|
@@ -264,16 +265,30 @@ describe("appUiLink", () => {
|
|
|
264
265
|
it("fails loud when the named component does not exist in packages/ui/src", () => {
|
|
265
266
|
expect(() => appUiLink({ projectDir: appDir, component: "nonexistent" })).toThrow(/nonexistent/);
|
|
266
267
|
});
|
|
267
|
-
it("fails loud when there is no
|
|
268
|
+
it("fails loud when there is no packages/ui/src and no --ui-src override", () => {
|
|
268
269
|
const lonely = fs.mkdtempSync(path.join(tmpdir(), "lotics-lonely-app-"));
|
|
269
270
|
fs.writeFileSync(path.join(lonely, "vite.config.ts"), "export default { resolve: { alias: [] } };");
|
|
270
271
|
try {
|
|
271
|
-
expect(() => appUiLink({ projectDir: lonely, component: "card" })).toThrow(/
|
|
272
|
+
expect(() => appUiLink({ projectDir: lonely, component: "card" })).toThrow(/--ui-src/);
|
|
272
273
|
}
|
|
273
274
|
finally {
|
|
274
275
|
fs.rmSync(lonely, { recursive: true, force: true });
|
|
275
276
|
}
|
|
276
277
|
});
|
|
278
|
+
it("links an EXTERNAL app (no monorepo above) via an explicit --ui-src", () => {
|
|
279
|
+
const ext = fs.mkdtempSync(path.join(tmpdir(), "lotics-external-app-"));
|
|
280
|
+
fs.writeFileSync(path.join(ext, "vite.config.ts"), "export default { resolve: { alias: [] } };");
|
|
281
|
+
try {
|
|
282
|
+
appUiLink({ projectDir: ext, component: "card", uiSrc });
|
|
283
|
+
expect(fs.readFileSync(path.join(ext, "vite.config.ts"), "utf-8")).toContain(uiSrc);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
fs.rmSync(ext, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
it("fails loud when --ui-src is not a directory", () => {
|
|
290
|
+
expect(() => appUiLink({ projectDir: appDir, component: "card", uiSrc: path.join(root, "nope") })).toThrow(/not a directory/);
|
|
291
|
+
});
|
|
277
292
|
});
|
|
278
293
|
/**
|
|
279
294
|
* The workflow body files are how AA-1 Option B becomes "open a file → edit →
|
|
@@ -680,7 +695,7 @@ describe("defaultPullTarget", () => {
|
|
|
680
695
|
expect(defaultPullTarget("app_X", "Sales Tracker")).toBe("Sales Tracker");
|
|
681
696
|
});
|
|
682
697
|
});
|
|
683
|
-
describe("
|
|
698
|
+
describe("ensureAppTsconfig", () => {
|
|
684
699
|
let dir;
|
|
685
700
|
beforeEach(() => {
|
|
686
701
|
dir = fs.mkdtempSync(path.join(tmpdir(), "lotics-tsconfig-"));
|
|
@@ -692,37 +707,60 @@ describe("ensureWorkflowTsconfigExcludes", () => {
|
|
|
692
707
|
const read = () => JSON.parse(fs.readFileSync(tsconfigPath(), "utf-8"));
|
|
693
708
|
it("adds both workflow globs to the TOP-LEVEL exclude", () => {
|
|
694
709
|
fs.writeFileSync(tsconfigPath(), JSON.stringify({ compilerOptions: {}, exclude: ["node_modules"] }));
|
|
695
|
-
|
|
710
|
+
ensureAppTsconfig(dir);
|
|
696
711
|
expect(read().exclude).toEqual(["node_modules", "src/workflows", ".lotics/workflows"]);
|
|
697
712
|
});
|
|
698
713
|
it("writes TOP-LEVEL even when a compilerOptions.exclude exists (tsc ignores the nested key)", () => {
|
|
699
714
|
fs.writeFileSync(tsconfigPath(), JSON.stringify({ compilerOptions: { exclude: ["x"] } }));
|
|
700
|
-
|
|
715
|
+
ensureAppTsconfig(dir);
|
|
701
716
|
const cfg = read();
|
|
702
717
|
expect(cfg.exclude).toEqual(["src/workflows", ".lotics/workflows"]);
|
|
703
718
|
expect(cfg.compilerOptions.exclude).toEqual(["x"]); // left untouched (and tsc-ignored)
|
|
704
719
|
});
|
|
705
720
|
it("preserves pre-existing top-level excludes", () => {
|
|
706
721
|
fs.writeFileSync(tsconfigPath(), JSON.stringify({ exclude: ["node_modules", "dist"] }));
|
|
707
|
-
|
|
722
|
+
ensureAppTsconfig(dir);
|
|
708
723
|
expect(read().exclude).toEqual(["node_modules", "dist", "src/workflows", ".lotics/workflows"]);
|
|
709
724
|
});
|
|
710
725
|
it("is idempotent — a second call writes nothing new", () => {
|
|
711
726
|
fs.writeFileSync(tsconfigPath(), JSON.stringify({ exclude: ["node_modules"] }));
|
|
712
|
-
|
|
727
|
+
ensureAppTsconfig(dir);
|
|
713
728
|
const afterFirst = fs.readFileSync(tsconfigPath(), "utf-8");
|
|
714
|
-
|
|
729
|
+
ensureAppTsconfig(dir);
|
|
715
730
|
expect(fs.readFileSync(tsconfigPath(), "utf-8")).toBe(afterFirst);
|
|
716
731
|
});
|
|
717
732
|
it("warns + does not throw or create a file when there is no tsconfig", () => {
|
|
718
|
-
expect(() =>
|
|
733
|
+
expect(() => ensureAppTsconfig(dir)).not.toThrow();
|
|
719
734
|
expect(fs.existsSync(tsconfigPath())).toBe(false);
|
|
720
735
|
});
|
|
721
736
|
it("warns + does not throw on an unparseable tsconfig (left as-is)", () => {
|
|
722
737
|
fs.writeFileSync(tsconfigPath(), "{ not json,, }");
|
|
723
|
-
expect(() =>
|
|
738
|
+
expect(() => ensureAppTsconfig(dir)).not.toThrow();
|
|
724
739
|
expect(fs.readFileSync(tsconfigPath(), "utf-8")).toBe("{ not json,, }");
|
|
725
740
|
});
|
|
741
|
+
it("rewrites a bare .lotics include to the glob (a dot-dir loads zero generated types)", () => {
|
|
742
|
+
fs.writeFileSync(tsconfigPath(), JSON.stringify({ include: ["src", ".lotics"], exclude: ["node_modules"] }));
|
|
743
|
+
ensureAppTsconfig(dir);
|
|
744
|
+
expect(read().include).toEqual(["src", ".lotics/**/*"]);
|
|
745
|
+
});
|
|
746
|
+
it("leaves an already-correct .lotics/**/* include untouched (idempotent)", () => {
|
|
747
|
+
fs.writeFileSync(tsconfigPath(), JSON.stringify({ include: ["src", ".lotics/**/*"], exclude: ["node_modules", "src/workflows", ".lotics/workflows"] }));
|
|
748
|
+
const before = fs.readFileSync(tsconfigPath(), "utf-8");
|
|
749
|
+
ensureAppTsconfig(dir);
|
|
750
|
+
expect(fs.readFileSync(tsconfigPath(), "utf-8")).toBe(before);
|
|
751
|
+
});
|
|
752
|
+
it("dedupes when both a bare .lotics and the glob are present", () => {
|
|
753
|
+
fs.writeFileSync(tsconfigPath(), JSON.stringify({ include: [".lotics", ".lotics/**/*", "src"] }));
|
|
754
|
+
ensureAppTsconfig(dir);
|
|
755
|
+
expect(read().include).toEqual([".lotics/**/*", "src"]);
|
|
756
|
+
});
|
|
757
|
+
it("heals the include AND adds the excludes in a single write", () => {
|
|
758
|
+
fs.writeFileSync(tsconfigPath(), JSON.stringify({ include: ["src", ".lotics"], exclude: ["node_modules"] }));
|
|
759
|
+
ensureAppTsconfig(dir);
|
|
760
|
+
const cfg = read();
|
|
761
|
+
expect(cfg.include).toEqual(["src", ".lotics/**/*"]);
|
|
762
|
+
expect(cfg.exclude).toEqual(["node_modules", "src/workflows", ".lotics/workflows"]);
|
|
763
|
+
});
|
|
726
764
|
});
|
|
727
765
|
describe("appVersions", () => {
|
|
728
766
|
let logLines;
|
package/dist/args.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare function parseArgs(argv: string[]): {
|
|
|
23
23
|
apiKey?: string;
|
|
24
24
|
workspace?: string;
|
|
25
25
|
viewAs?: string;
|
|
26
|
+
/** `--ui-src <path>`: absolute packages/ui/src for `ui link` in an external app. */
|
|
27
|
+
uiSrc?: string;
|
|
26
28
|
name?: string;
|
|
27
29
|
timezone?: string;
|
|
28
30
|
message?: string;
|
package/dist/args.js
CHANGED
|
@@ -19,6 +19,7 @@ export function parseArgs(argv) {
|
|
|
19
19
|
apiKey: undefined,
|
|
20
20
|
workspace: undefined,
|
|
21
21
|
viewAs: undefined,
|
|
22
|
+
uiSrc: undefined,
|
|
22
23
|
name: undefined,
|
|
23
24
|
timezone: undefined,
|
|
24
25
|
message: undefined,
|
|
@@ -60,6 +61,9 @@ export function parseArgs(argv) {
|
|
|
60
61
|
case "--view-as":
|
|
61
62
|
flags.viewAs = argv[++i];
|
|
62
63
|
break;
|
|
64
|
+
case "--ui-src":
|
|
65
|
+
flags.uiSrc = argv[++i];
|
|
66
|
+
break;
|
|
63
67
|
case "--name":
|
|
64
68
|
flags.name = argv[++i];
|
|
65
69
|
break;
|
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, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appUiLink, } from "./app_commands.js";
|
|
16
|
+
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appQuerySet, 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";
|
|
@@ -90,11 +90,16 @@ COMMANDS
|
|
|
90
90
|
lotics app workflow pull Rewrite src/workflows/*.ts from the server
|
|
91
91
|
lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
|
|
92
92
|
isolated program per alias; the app's own tsc)
|
|
93
|
+
lotics app query set <alias> Push package.json#lotics.queries.<alias> to
|
|
94
|
+
apps.queries via set_app_query (no deploy;
|
|
95
|
+
re-synced by the next deploy from the manifest)
|
|
93
96
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
94
97
|
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
95
98
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
96
|
-
lotics ui link <component> [--
|
|
97
|
-
packages/ui/src
|
|
99
|
+
lotics ui link <component> [--ui-src <path>] [--remove]
|
|
100
|
+
Dev-link @lotics/ui to packages/ui/src (Vite alias
|
|
101
|
+
+ tsc paths) for live HMR + typecheck. Monorepo apps
|
|
102
|
+
auto-find it; external apps pass --ui-src / LOTICS_UI_SRC
|
|
98
103
|
lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
|
|
99
104
|
(uses the bundled Lotics xlsx engine; prefer over
|
|
100
105
|
npm xlsx/exceljs for round-trip fidelity)
|
|
@@ -493,17 +498,19 @@ async function main() {
|
|
|
493
498
|
if (subcommand === "link") {
|
|
494
499
|
const component = toolArgs;
|
|
495
500
|
if (!component) {
|
|
496
|
-
console.error("Usage: lotics ui link <component> [--remove]");
|
|
497
|
-
console.error("Dev-links @lotics/ui to
|
|
501
|
+
console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
|
|
502
|
+
console.error("Dev-links @lotics/ui to packages/ui/src (Vite + tsc) for live HMR + typecheck.");
|
|
503
|
+
console.error("Monorepo apps find packages/ui/src automatically; external apps pass --ui-src / LOTICS_UI_SRC.");
|
|
498
504
|
process.exit(1);
|
|
499
505
|
}
|
|
500
506
|
// `--remove` isn't a value-taking flag, so the parser leaves it as a
|
|
501
|
-
// trailing positional (the component took `toolArgs`)
|
|
502
|
-
|
|
507
|
+
// trailing positional (the component took `toolArgs`); `--ui-src` IS a
|
|
508
|
+
// value flag (flags.uiSrc), falling back to LOTICS_UI_SRC inside appUiLink.
|
|
509
|
+
appUiLink({ component, uiSrc: flags.uiSrc, remove: restArgs.includes("--remove") });
|
|
503
510
|
return;
|
|
504
511
|
}
|
|
505
512
|
console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
|
|
506
|
-
console.error("Usage: lotics ui link <component> [--remove]");
|
|
513
|
+
console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
|
|
507
514
|
process.exit(1);
|
|
508
515
|
}
|
|
509
516
|
// --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
|
|
@@ -606,6 +613,7 @@ async function main() {
|
|
|
606
613
|
console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
|
|
607
614
|
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
608
615
|
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
616
|
+
console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
|
|
609
617
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
610
618
|
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
611
619
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
@@ -826,6 +834,25 @@ async function main() {
|
|
|
826
834
|
}
|
|
827
835
|
workflowUsage();
|
|
828
836
|
}
|
|
837
|
+
if (subcommand === "query") {
|
|
838
|
+
// `lotics app query set <alias>` — push package.json#lotics.queries.<alias>
|
|
839
|
+
// to apps.queries via set_app_query, no deploy. `toolArgs` is the verb;
|
|
840
|
+
// `restArgs` carries the alias.
|
|
841
|
+
const action = toolArgs;
|
|
842
|
+
if (action === "set") {
|
|
843
|
+
const alias = restArgs[0];
|
|
844
|
+
if (!alias) {
|
|
845
|
+
console.error("Usage: lotics app query set <alias>");
|
|
846
|
+
console.error("Pushes package.json#lotics.queries.<alias> to apps.queries via set_app_query (no deploy).");
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
await appQuerySet(client, { alias });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
console.error("Usage:");
|
|
853
|
+
console.error(" lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries (no deploy)");
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
829
856
|
if (subcommand === "dev") {
|
|
830
857
|
// First positional is an optional project path (defaults to cwd).
|
|
831
858
|
// --port and --vite-port can override the wrapper / Vite ports.
|
package/dist/client.d.ts
CHANGED
|
@@ -277,6 +277,18 @@ export declare class LoticsClient {
|
|
|
277
277
|
name?: string;
|
|
278
278
|
description?: string;
|
|
279
279
|
}): Promise<ToolExecuteResult>;
|
|
280
|
+
/**
|
|
281
|
+
* Bind (create or replace) an app query by alias via the `set_app_query` tool
|
|
282
|
+
* — the deploy-free authoring path for `apps.queries`, parallel to
|
|
283
|
+
* `setAppWorkflow`. `declaration` is the `{ ast, params? }` from
|
|
284
|
+
* `package.json#lotics.queries.<alias>`. The server validates it exactly as a
|
|
285
|
+
* deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
|
|
286
|
+
* so the next `lotics app deploy` overwrites this from the manifest.
|
|
287
|
+
*/
|
|
288
|
+
setAppQuery(app_id: string, alias: string, declaration: {
|
|
289
|
+
ast: unknown;
|
|
290
|
+
params?: Record<string, unknown>;
|
|
291
|
+
}): Promise<ToolExecuteResult>;
|
|
280
292
|
/**
|
|
281
293
|
* Fetch one app workflow's faithful source + bound input/output schemas via
|
|
282
294
|
* `get_app_workflow`. `source` is the JS-subset body re-rendered from the
|
|
@@ -375,6 +387,14 @@ export declare class LoticsClient {
|
|
|
375
387
|
capabilities?: {
|
|
376
388
|
comments?: boolean;
|
|
377
389
|
};
|
|
390
|
+
/**
|
|
391
|
+
* The manifest's `lotics.workflows` KEYS — the workflow aliases the deployed
|
|
392
|
+
* code declares (NOT the bindings; `set_app_workflow` / `remove_app_workflow`
|
|
393
|
+
* own `apps.workflows`). Recorded on the new version so `remove_app_workflow`
|
|
394
|
+
* refuses to unbind an alias the served version still calls. Always sent
|
|
395
|
+
* (empty array when none declared).
|
|
396
|
+
*/
|
|
397
|
+
workflow_aliases?: string[];
|
|
378
398
|
}): Promise<{
|
|
379
399
|
version_id: string;
|
|
380
400
|
version_number: number;
|