@lotics/cli 0.57.0 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/app_commands.d.ts +124 -2
- package/dist/app_commands.js +613 -6
- package/dist/app_commands.test.js +501 -2
- 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 +132 -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 +861 -39
- package/dist/starter_template.js +89 -0
- package/dist/starter_template.test.js +11 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import dns from "node:dns";
|
|
3
|
+
import net from "node:net";
|
|
2
4
|
import fs from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import readline from "node:readline";
|
|
7
|
+
// WSL2 + Node 24's Happy-Eyeballs races IPv4/IPv6 and intermittently stalls on a
|
|
8
|
+
// dead IPv6 route to api.lotics.ai (curl works; node fetch ETIMEDOUTs). Prefer
|
|
9
|
+
// IPv4 and cap the per-family attempt so a bad IPv6 path fails fast to IPv4.
|
|
10
|
+
// Bin-only side effect — client.ts stays pure for SDK consumers.
|
|
11
|
+
dns.setDefaultResultOrder("ipv4first");
|
|
12
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(2000);
|
|
5
13
|
import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
6
14
|
import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
|
|
7
15
|
import { VERSION } from "./version.js";
|
|
8
|
-
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename } from "./app_commands.js";
|
|
16
|
+
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appUiLink, } from "./app_commands.js";
|
|
9
17
|
import { parseArgs } from "./args.js";
|
|
18
|
+
import { ingestJsonArgs } from "./inputs.js";
|
|
10
19
|
import { runXlsxCommand } from "./xlsx.js";
|
|
11
20
|
import { runDocxCommand } from "./docx.js";
|
|
12
21
|
function printHelp() {
|
|
@@ -65,9 +74,21 @@ COMMANDS
|
|
|
65
74
|
lotics app deploy [-m <message>] Build + upload current dir as a new version
|
|
66
75
|
(code + queries only — workflow bindings are
|
|
67
76
|
managed by set_app_workflow / remove_app_workflow)
|
|
77
|
+
lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
|
|
78
|
+
from the manifest + workspace schema — no deploy
|
|
79
|
+
lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
|
|
80
|
+
(inputs: inline JSON, @file, or stdin;
|
|
81
|
+
--print-created reports created records +
|
|
82
|
+
a paste-ready cleanup plan; --cleanup also
|
|
83
|
+
deletes those records — NOT a rollback)
|
|
84
|
+
lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body
|
|
85
|
+
through set_app_workflow (server verifies)
|
|
86
|
+
lotics app workflow pull Rewrite src/workflows/*.ts from the server
|
|
68
87
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
69
88
|
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
70
89
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
90
|
+
lotics ui link <component> [--remove] Dev-link @lotics/ui to the monorepo's
|
|
91
|
+
packages/ui/src for live HMR (monorepo only)
|
|
71
92
|
lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
|
|
72
93
|
(uses the bundled Lotics xlsx engine; prefer over
|
|
73
94
|
npm xlsx/exceljs for round-trip fidelity)
|
|
@@ -461,6 +482,40 @@ async function main() {
|
|
|
461
482
|
await runDocxCommand(subcommand, toolArgs, restArgs);
|
|
462
483
|
return;
|
|
463
484
|
}
|
|
485
|
+
// --- lotics ui link <component> [--remove] — local vite.config edit, no auth ---
|
|
486
|
+
if (command === "ui") {
|
|
487
|
+
if (subcommand === "link") {
|
|
488
|
+
const component = toolArgs;
|
|
489
|
+
if (!component) {
|
|
490
|
+
console.error("Usage: lotics ui link <component> [--remove]");
|
|
491
|
+
console.error("Dev-links @lotics/ui to the monorepo's packages/ui/src for live HMR.");
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
// `--remove` isn't a value-taking flag, so the parser leaves it as a
|
|
495
|
+
// trailing positional (the component took `toolArgs`).
|
|
496
|
+
appUiLink({ component, remove: restArgs.includes("--remove") });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
|
|
500
|
+
console.error("Usage: lotics ui link <component> [--remove]");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
// --- lotics app codegen [path] — regenerate .lotics/* without a deploy ---
|
|
504
|
+
// The .d.ts companions need no auth; app_fields.ts needs a workspace, resolved
|
|
505
|
+
// when credentials are available (offline/unauth still does the .d.ts work).
|
|
506
|
+
if (command === "app" && subcommand === "codegen") {
|
|
507
|
+
const projectDir = toolArgs;
|
|
508
|
+
const ctx = resolveContext(flags);
|
|
509
|
+
if (!ctx) {
|
|
510
|
+
await appCodegen({ projectDir });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
514
|
+
const client = new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId });
|
|
515
|
+
await resolveWorkspace(client, ctx);
|
|
516
|
+
await appCodegen({ projectDir, client });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
464
519
|
// --- lotics org [list | use <name|id> [--local]] — credential store, config-only ---
|
|
465
520
|
if (command === "org") {
|
|
466
521
|
const global = loadGlobalConfig() ?? {};
|
|
@@ -524,11 +579,17 @@ async function main() {
|
|
|
524
579
|
console.error('Run "lotics --help" for usage.');
|
|
525
580
|
process.exit(1);
|
|
526
581
|
}
|
|
582
|
+
// `ui` and `app codegen` are handled above (no-auth / optional-client) and
|
|
583
|
+
// return before this guard — they never reach the requireClient path.
|
|
527
584
|
if (command === "app" && !subcommand) {
|
|
528
585
|
console.error("Usage:");
|
|
529
586
|
console.error(" lotics app create <name> [path] Scaffold a new app locally");
|
|
530
587
|
console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
|
|
531
588
|
console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
|
|
589
|
+
console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) — no deploy");
|
|
590
|
+
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
|
|
591
|
+
console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
|
|
592
|
+
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
532
593
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
533
594
|
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
534
595
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
@@ -677,6 +738,64 @@ async function main() {
|
|
|
677
738
|
await appRename(client, { name: newName });
|
|
678
739
|
return;
|
|
679
740
|
}
|
|
741
|
+
if (subcommand === "workflow") {
|
|
742
|
+
// `lotics app workflow <run|set|pull> …` — disambiguated subcommands so an
|
|
743
|
+
// alias can never collide with the verb. `toolArgs` is the verb; `restArgs`
|
|
744
|
+
// carries the alias (+ inputs for run). No bare `workflow <alias>` form.
|
|
745
|
+
const action = toolArgs;
|
|
746
|
+
const workflowUsage = () => {
|
|
747
|
+
console.error("Usage:");
|
|
748
|
+
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow");
|
|
749
|
+
console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
|
|
750
|
+
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
751
|
+
process.exit(1);
|
|
752
|
+
};
|
|
753
|
+
if (action === "run") {
|
|
754
|
+
// `run <alias> '<json>'` — inputs use the SAME ingestion as `lotics run`
|
|
755
|
+
// (inline / @file / stdin) so bulk inputs bypass ARG_MAX.
|
|
756
|
+
const alias = restArgs[0];
|
|
757
|
+
if (!alias) {
|
|
758
|
+
console.error("Usage: lotics app workflow run <alias> '<json>'");
|
|
759
|
+
console.error(" lotics app workflow run <alias> @inputs.json (read inputs from a file)");
|
|
760
|
+
console.error(" cat inputs.json | lotics app workflow run <alias> (read inputs from stdin)");
|
|
761
|
+
console.error("Flags: --print-created (report created records + cleanup plan + caveat)");
|
|
762
|
+
console.error(" --cleanup (also delete the created records — records only, NOT a rollback)");
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
const ingested = await ingestJsonArgs({
|
|
766
|
+
rawArg: restArgs[1],
|
|
767
|
+
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
768
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
769
|
+
readStdin,
|
|
770
|
+
});
|
|
771
|
+
if (ingested.kind === "error") {
|
|
772
|
+
console.error(ingested.message);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
await appExecuteWorkflow(client, {
|
|
776
|
+
alias,
|
|
777
|
+
inputs: ingested.args,
|
|
778
|
+
printCreated: flags.printCreated,
|
|
779
|
+
cleanup: flags.cleanup,
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (action === "set") {
|
|
784
|
+
const alias = restArgs[0];
|
|
785
|
+
if (!alias) {
|
|
786
|
+
console.error("Usage: lotics app workflow set <alias>");
|
|
787
|
+
console.error("Pushes the edited src/workflows/<alias>.ts body via set_app_workflow.");
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
await appWorkflowSet(client, { alias });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (action === "pull") {
|
|
794
|
+
await appWorkflowPull(client);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
workflowUsage();
|
|
798
|
+
}
|
|
680
799
|
if (subcommand === "dev") {
|
|
681
800
|
// First positional is an optional project path (defaults to cwd).
|
|
682
801
|
// --port and --vite-port can override the wrapper / Vite ports.
|
|
@@ -782,36 +901,19 @@ async function main() {
|
|
|
782
901
|
// lotics run <tool> [json_args]
|
|
783
902
|
if (command === "run") {
|
|
784
903
|
const toolName = subcommand;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
catch (err) {
|
|
797
|
-
console.error(`Cannot read args file "${argsPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
798
|
-
process.exit(1);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
else if (!rawArgs && !process.stdin.isTTY) {
|
|
802
|
-
// Piped/redirected stdin — `… | lotics run <tool>` or `< file.json`.
|
|
803
|
-
rawArgs = await readStdin();
|
|
804
|
-
}
|
|
805
|
-
let args = {};
|
|
806
|
-
if (rawArgs) {
|
|
807
|
-
try {
|
|
808
|
-
args = JSON.parse(rawArgs);
|
|
809
|
-
}
|
|
810
|
-
catch {
|
|
811
|
-
console.error(`Invalid JSON: ${rawArgs}`);
|
|
812
|
-
process.exit(1);
|
|
813
|
-
}
|
|
904
|
+
// `@file` / piped stdin carry payloads too large for an inline arg (the OS
|
|
905
|
+
// caps ARG_MAX). Shared with `lotics app workflow` so both ingest identically.
|
|
906
|
+
const ingested = await ingestJsonArgs({
|
|
907
|
+
rawArg: toolArgs,
|
|
908
|
+
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
909
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
910
|
+
readStdin,
|
|
911
|
+
});
|
|
912
|
+
if (ingested.kind === "error") {
|
|
913
|
+
console.error(ingested.message);
|
|
914
|
+
process.exit(1);
|
|
814
915
|
}
|
|
916
|
+
const args = ingested.args;
|
|
815
917
|
const timeoutMs = flags.timeout ?? 60000;
|
|
816
918
|
// Always request text format so model_output is available; --json only affects CLI output
|
|
817
919
|
const result = await client.execute(toolName, args, { format: "text", timeoutMs });
|
package/dist/client.d.ts
CHANGED
|
@@ -162,6 +162,25 @@ export declare class LoticsClient {
|
|
|
162
162
|
workspace_id: string;
|
|
163
163
|
current_version_id: string | null;
|
|
164
164
|
}>;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the display name + fields (incl. select options) of the given tables
|
|
167
|
+
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
168
|
+
* alias maps. One `get_table` call per id (the tool surface has no batch
|
|
169
|
+
* variant); a missing/inaccessible table is dropped rather than throwing, so a
|
|
170
|
+
* stale id in the scope set never fails codegen.
|
|
171
|
+
*/
|
|
172
|
+
getWorkspaceSchema(tableIds: string[]): Promise<Array<{
|
|
173
|
+
id: string;
|
|
174
|
+
name: string;
|
|
175
|
+
fields: Array<{
|
|
176
|
+
id: string;
|
|
177
|
+
name: string;
|
|
178
|
+
options?: Array<{
|
|
179
|
+
id: string;
|
|
180
|
+
label: string;
|
|
181
|
+
}>;
|
|
182
|
+
}>;
|
|
183
|
+
}>>;
|
|
165
184
|
/**
|
|
166
185
|
* Rename an app's public subdomain — its `<slug>.lotics.app` address.
|
|
167
186
|
* Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
|
|
@@ -224,6 +243,45 @@ export declare class LoticsClient {
|
|
|
224
243
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
225
244
|
*/
|
|
226
245
|
appWorkflow(app_id: string, alias: string, inputs: unknown): Promise<unknown>;
|
|
246
|
+
/**
|
|
247
|
+
* Bind (create or replace) an app workflow by alias via the `set_app_workflow`
|
|
248
|
+
* tool — the SINGLE author of `apps.workflows` + the workflow row. `source` is
|
|
249
|
+
* the verbatim JS-subset body (no `on({...})` trigger). `inputs`/`outputs` are
|
|
250
|
+
* the typed schemas declared in `package.json#lotics.workflows.<alias>`. The
|
|
251
|
+
* server re-verifies the body and echoes the bound `outputs` (declared, else
|
|
252
|
+
* DERIVED from `return({ data })`), so the CLI can show the author what shape
|
|
253
|
+
* `result.data` will carry. Wraps the tool rather than a bespoke endpoint so
|
|
254
|
+
* the file flow stays a convenience over the existing single-author contract.
|
|
255
|
+
*/
|
|
256
|
+
setAppWorkflow(app_id: string, alias: string, body: {
|
|
257
|
+
source: string;
|
|
258
|
+
inputs?: Record<string, unknown>;
|
|
259
|
+
outputs?: Record<string, unknown>;
|
|
260
|
+
name?: string;
|
|
261
|
+
description?: string;
|
|
262
|
+
}): Promise<ToolExecuteResult>;
|
|
263
|
+
/**
|
|
264
|
+
* Fetch one app workflow's faithful source + bound input/output schemas via
|
|
265
|
+
* `get_app_workflow`. `source` is the JS-subset body re-rendered from the
|
|
266
|
+
* persisted step tree (incl. the `return({ data })` clause, opaque field/option
|
|
267
|
+
* keys) — the exact text `lotics app workflow set` would push back. Feeds
|
|
268
|
+
* `lotics app pull`, which writes it to `src/workflows/<alias>.ts`.
|
|
269
|
+
*/
|
|
270
|
+
getAppWorkflow(app_id: string, alias: string): Promise<ToolExecuteResult>;
|
|
271
|
+
/**
|
|
272
|
+
* Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
|
|
273
|
+
* make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
|
|
274
|
+
* The server is the single source of the type model — the CLI never
|
|
275
|
+
* re-implements it. `envelope_prefix`/`envelope_suffix` are the exact
|
|
276
|
+
* `async function __workflow(): …` wrapper the server compiles inside, so the
|
|
277
|
+
* local typecheck mirrors the set-time verdict. Mirrors
|
|
278
|
+
* POST /v1/apps/{app_id}/workflows/{alias}/dts.
|
|
279
|
+
*/
|
|
280
|
+
getAppWorkflowDts(app_id: string, alias: string): Promise<{
|
|
281
|
+
dts: string;
|
|
282
|
+
envelope_prefix: string;
|
|
283
|
+
envelope_suffix: string;
|
|
284
|
+
}>;
|
|
227
285
|
/**
|
|
228
286
|
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
229
287
|
* caller reads `res.body`). Unlike `request`, this does not buffer/parse the
|
package/dist/client.js
CHANGED
|
@@ -187,6 +187,48 @@ export class LoticsClient {
|
|
|
187
187
|
async createApp(body) {
|
|
188
188
|
return this.request("POST", "/v1/apps", body);
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the display name + fields (incl. select options) of the given tables
|
|
192
|
+
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
193
|
+
* alias maps. One `get_table` call per id (the tool surface has no batch
|
|
194
|
+
* variant); a missing/inaccessible table is dropped rather than throwing, so a
|
|
195
|
+
* stale id in the scope set never fails codegen.
|
|
196
|
+
*/
|
|
197
|
+
async getWorkspaceSchema(tableIds) {
|
|
198
|
+
const tables = await Promise.all(tableIds.map(async (table_id) => {
|
|
199
|
+
// JSON (the default) — `res.result` is the structured `get_table` output
|
|
200
|
+
// already; `text` would only also run `toModelOutput` per table for
|
|
201
|
+
// nothing. The field id is `key` and an option's id is `key` / label is
|
|
202
|
+
// `name` (the `TableField` / select-option schema shapes).
|
|
203
|
+
const res = await this.execute("get_table", { table_id });
|
|
204
|
+
if (res.error || res.result === null || typeof res.result !== "object")
|
|
205
|
+
return null;
|
|
206
|
+
const table = res.result;
|
|
207
|
+
if (typeof table.id !== "string" || typeof table.name !== "string" || !Array.isArray(table.fields)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const fields = table.fields.flatMap((field) => {
|
|
211
|
+
if (field === null || typeof field !== "object")
|
|
212
|
+
return [];
|
|
213
|
+
const f = field;
|
|
214
|
+
if (typeof f.key !== "string" || typeof f.name !== "string")
|
|
215
|
+
return [];
|
|
216
|
+
const options = Array.isArray(f.options)
|
|
217
|
+
? f.options.flatMap((opt) => {
|
|
218
|
+
if (opt === null || typeof opt !== "object")
|
|
219
|
+
return [];
|
|
220
|
+
const o = opt;
|
|
221
|
+
return typeof o.key === "string" && typeof o.name === "string"
|
|
222
|
+
? [{ id: o.key, label: o.name }]
|
|
223
|
+
: [];
|
|
224
|
+
})
|
|
225
|
+
: undefined;
|
|
226
|
+
return [{ id: f.key, name: f.name, ...(options && options.length > 0 ? { options } : {}) }];
|
|
227
|
+
});
|
|
228
|
+
return { id: table.id, name: table.name, fields };
|
|
229
|
+
}));
|
|
230
|
+
return tables.filter((t) => t !== null);
|
|
231
|
+
}
|
|
190
232
|
/**
|
|
191
233
|
* Rename an app's public subdomain — its `<slug>.lotics.app` address.
|
|
192
234
|
* Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
|
|
@@ -260,6 +302,49 @@ export class LoticsClient {
|
|
|
260
302
|
return parsed ?? {};
|
|
261
303
|
return { status: "error", message: transportErrorMessage(response.status, parsed) };
|
|
262
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Bind (create or replace) an app workflow by alias via the `set_app_workflow`
|
|
307
|
+
* tool — the SINGLE author of `apps.workflows` + the workflow row. `source` is
|
|
308
|
+
* the verbatim JS-subset body (no `on({...})` trigger). `inputs`/`outputs` are
|
|
309
|
+
* the typed schemas declared in `package.json#lotics.workflows.<alias>`. The
|
|
310
|
+
* server re-verifies the body and echoes the bound `outputs` (declared, else
|
|
311
|
+
* DERIVED from `return({ data })`), so the CLI can show the author what shape
|
|
312
|
+
* `result.data` will carry. Wraps the tool rather than a bespoke endpoint so
|
|
313
|
+
* the file flow stays a convenience over the existing single-author contract.
|
|
314
|
+
*/
|
|
315
|
+
async setAppWorkflow(app_id, alias, body) {
|
|
316
|
+
return this.execute("set_app_workflow", {
|
|
317
|
+
app_id,
|
|
318
|
+
alias,
|
|
319
|
+
source: body.source,
|
|
320
|
+
...(body.inputs ? { inputs: body.inputs } : {}),
|
|
321
|
+
...(body.outputs ? { outputs: body.outputs } : {}),
|
|
322
|
+
...(body.name ? { name: body.name } : {}),
|
|
323
|
+
...(body.description ? { description: body.description } : {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Fetch one app workflow's faithful source + bound input/output schemas via
|
|
328
|
+
* `get_app_workflow`. `source` is the JS-subset body re-rendered from the
|
|
329
|
+
* persisted step tree (incl. the `return({ data })` clause, opaque field/option
|
|
330
|
+
* keys) — the exact text `lotics app workflow set` would push back. Feeds
|
|
331
|
+
* `lotics app pull`, which writes it to `src/workflows/<alias>.ts`.
|
|
332
|
+
*/
|
|
333
|
+
async getAppWorkflow(app_id, alias) {
|
|
334
|
+
return this.execute("get_app_workflow", { app_id, alias });
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
|
|
338
|
+
* make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
|
|
339
|
+
* The server is the single source of the type model — the CLI never
|
|
340
|
+
* re-implements it. `envelope_prefix`/`envelope_suffix` are the exact
|
|
341
|
+
* `async function __workflow(): …` wrapper the server compiles inside, so the
|
|
342
|
+
* local typecheck mirrors the set-time verdict. Mirrors
|
|
343
|
+
* POST /v1/apps/{app_id}/workflows/{alias}/dts.
|
|
344
|
+
*/
|
|
345
|
+
async getAppWorkflowDts(app_id, alias) {
|
|
346
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`);
|
|
347
|
+
}
|
|
263
348
|
/**
|
|
264
349
|
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
265
350
|
* caller reads `res.body`). Unlike `request`, this does not buffer/parse the
|
package/dist/dev/server.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import net from "node:net";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
|
+
import { ipv4ChildEnv } from "../child_env.js";
|
|
17
18
|
import { dispatchRpc } from "./rpc_handler.js";
|
|
18
19
|
import { buildWrapperPage } from "./wrapper_page.js";
|
|
19
20
|
const DEFAULT_PORT = 5174;
|
|
@@ -55,7 +56,7 @@ export async function startDevServer(args) {
|
|
|
55
56
|
const viteChild = spawn("npx", ["vite", "--port", String(vitePort), "--strictPort"], {
|
|
56
57
|
cwd: args.projectDir,
|
|
57
58
|
stdio: [process.stdin.isTTY ? "inherit" : "ignore", "inherit", "inherit"],
|
|
58
|
-
env: { ...process.env, FORCE_COLOR: "1" },
|
|
59
|
+
env: ipv4ChildEnv({ ...process.env, FORCE_COLOR: "1" }),
|
|
59
60
|
});
|
|
60
61
|
let stopped = false;
|
|
61
62
|
let stoppingResolve = null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` from a workspace schema.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the `.d.ts` companions (which only augment types), this is a real
|
|
5
|
+
* `.ts` module: apps import the string VALUES at runtime to address fields and
|
|
6
|
+
* select options by a stable, human-readable alias instead of pasting opaque
|
|
7
|
+
* `fld_…` / `opt_…` ids into their source:
|
|
8
|
+
*
|
|
9
|
+
* import { F, OPT } from "../.lotics/app_fields";
|
|
10
|
+
* record.data[F.SHIPMENTS.status] // "fld_…"
|
|
11
|
+
* if (status === OPT.SHIPMENTS.status.cleared) // "opt_…"
|
|
12
|
+
*
|
|
13
|
+
* Aliases are derived from display names (NFD-stripped, non-alnum → `_`, deduped
|
|
14
|
+
* in stable order), so a rename on the platform re-runs codegen and the app's
|
|
15
|
+
* call sites move with it. Pure function — same schema → same bytes. Idempotent.
|
|
16
|
+
*/
|
|
17
|
+
/** One select option as it appears in a field's metadata. */
|
|
18
|
+
export interface TableFieldOption {
|
|
19
|
+
/** Stable id (`opt_…`) — the value persisted in record data. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Display label the option alias is slugified from. */
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
/** A single table field with the metadata codegen needs. */
|
|
25
|
+
export interface TableFieldSchema {
|
|
26
|
+
/** Stable id (`fld_…`) — the value emitted under `F`. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Display name the field alias is slugified from. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Present only for select/multi-select fields — drives the `OPT` map. */
|
|
31
|
+
options?: TableFieldOption[];
|
|
32
|
+
}
|
|
33
|
+
/** The schema of one table, as returned by `client.getWorkspaceSchema`. */
|
|
34
|
+
export interface TableSchema {
|
|
35
|
+
/** Stable id (`tbl_…`). Carried for fidelity; not emitted by name. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Display name the TABLE alias is slugified from. */
|
|
38
|
+
name: string;
|
|
39
|
+
fields: TableFieldSchema[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Slugify a display name to a valid TS identifier. NFD-normalize then strip
|
|
43
|
+
* diacritics so "Lô hàng" and "Lo hang" don't collide on the accent, lowercase,
|
|
44
|
+
* non-alnum → `_`, collapse runs, trim edge `_`. A leading digit (identifiers
|
|
45
|
+
* can't start with one) and the empty result both get a `_` prefix/placeholder.
|
|
46
|
+
* `upper` uppercases the result (TABLE aliases read as constants).
|
|
47
|
+
*/
|
|
48
|
+
export declare function slugifyAlias(name: string, upper: boolean): string;
|
|
49
|
+
/**
|
|
50
|
+
* Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
|
|
51
|
+
* workspace schema (the subset the app touches). An empty list yields valid,
|
|
52
|
+
* empty `F`/`OPT` maps so the file always compiles and imports resolve.
|
|
53
|
+
*/
|
|
54
|
+
export declare function generateAppFields(tables: TableSchema[]): string;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` from a workspace schema.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the `.d.ts` companions (which only augment types), this is a real
|
|
5
|
+
* `.ts` module: apps import the string VALUES at runtime to address fields and
|
|
6
|
+
* select options by a stable, human-readable alias instead of pasting opaque
|
|
7
|
+
* `fld_…` / `opt_…` ids into their source:
|
|
8
|
+
*
|
|
9
|
+
* import { F, OPT } from "../.lotics/app_fields";
|
|
10
|
+
* record.data[F.SHIPMENTS.status] // "fld_…"
|
|
11
|
+
* if (status === OPT.SHIPMENTS.status.cleared) // "opt_…"
|
|
12
|
+
*
|
|
13
|
+
* Aliases are derived from display names (NFD-stripped, non-alnum → `_`, deduped
|
|
14
|
+
* in stable order), so a rename on the platform re-runs codegen and the app's
|
|
15
|
+
* call sites move with it. Pure function — same schema → same bytes. Idempotent.
|
|
16
|
+
*/
|
|
17
|
+
import { isValidIdentifier } from "./generate_app_workflows_dts.js";
|
|
18
|
+
const HEADER = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
|
|
19
|
+
// DO NOT EDIT — regenerated from the workspace schema.
|
|
20
|
+
//
|
|
21
|
+
// Runtime field + option ids addressed by stable display-name aliases:
|
|
22
|
+
// record.data[F.<TABLE>.<field>] → "fld_…"
|
|
23
|
+
// value === OPT.<TABLE>.<field>.<option> → "opt_…"
|
|
24
|
+
// A rename on the platform re-runs codegen and moves these in lockstep.
|
|
25
|
+
`;
|
|
26
|
+
/**
|
|
27
|
+
* Slugify a display name to a valid TS identifier. NFD-normalize then strip
|
|
28
|
+
* diacritics so "Lô hàng" and "Lo hang" don't collide on the accent, lowercase,
|
|
29
|
+
* non-alnum → `_`, collapse runs, trim edge `_`. A leading digit (identifiers
|
|
30
|
+
* can't start with one) and the empty result both get a `_` prefix/placeholder.
|
|
31
|
+
* `upper` uppercases the result (TABLE aliases read as constants).
|
|
32
|
+
*/
|
|
33
|
+
export function slugifyAlias(name, upper) {
|
|
34
|
+
const stripped = name
|
|
35
|
+
.normalize("NFD")
|
|
36
|
+
.replace(/[̀-ͯ]/g, "") // combining diacritical marks
|
|
37
|
+
.replace(/đ/g, "d")
|
|
38
|
+
.replace(/Đ/g, "D"); // not a combining mark — map explicitly
|
|
39
|
+
const cased = upper ? stripped.toUpperCase() : stripped.toLowerCase();
|
|
40
|
+
const slug = cased
|
|
41
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
42
|
+
.replace(/^_+|_+$/g, "");
|
|
43
|
+
if (slug === "")
|
|
44
|
+
return "_";
|
|
45
|
+
return /^[0-9]/.test(slug) ? `_${slug}` : slug;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Assign each input a unique alias, preserving input order. The first claim on
|
|
49
|
+
* a slug keeps it; later collisions get `_2`, `_3`, … so the mapping is stable
|
|
50
|
+
* across regenerations (order is the schema's field/option order). Returns the
|
|
51
|
+
* aliases positionally aligned with `names`.
|
|
52
|
+
*/
|
|
53
|
+
function dedupeAliases(names, upper) {
|
|
54
|
+
const used = new Map();
|
|
55
|
+
return names.map((name) => {
|
|
56
|
+
const base = slugifyAlias(name, upper);
|
|
57
|
+
const seen = used.get(base);
|
|
58
|
+
if (seen === undefined) {
|
|
59
|
+
used.set(base, 1);
|
|
60
|
+
return base;
|
|
61
|
+
}
|
|
62
|
+
let n = seen + 1;
|
|
63
|
+
while (used.has(`${base}_${n}`))
|
|
64
|
+
n++;
|
|
65
|
+
used.set(base, n);
|
|
66
|
+
used.set(`${base}_${n}`, 1);
|
|
67
|
+
return `${base}_${n}`;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** A property key for an object literal — bare when a valid identifier, else quoted. */
|
|
71
|
+
function propKey(alias) {
|
|
72
|
+
return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
73
|
+
}
|
|
74
|
+
/** Resolve table + field aliases once, so the `F`/`OPT` maps and the union types agree. */
|
|
75
|
+
function aliasTables(tables) {
|
|
76
|
+
const tableAliases = dedupeAliases(tables.map((t) => t.name), true);
|
|
77
|
+
return tables.map((table, i) => {
|
|
78
|
+
const fieldAliases = dedupeAliases(table.fields.map((f) => f.name), false);
|
|
79
|
+
return {
|
|
80
|
+
alias: tableAliases[i],
|
|
81
|
+
table,
|
|
82
|
+
fields: table.fields.map((field, j) => ({ alias: fieldAliases[j], field })),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Emit the `F` map: `{ TABLE: { field: "fld_…" } }` plus its literal-union type. */
|
|
87
|
+
function emitFieldMap(aliased) {
|
|
88
|
+
const tableBlocks = aliased.map(({ alias, fields }) => {
|
|
89
|
+
const fieldLines = fields.map(({ alias: fieldAlias, field }) => ` ${propKey(fieldAlias)}: ${JSON.stringify(field.id)},`);
|
|
90
|
+
return ` ${propKey(alias)}: {\n${fieldLines.join("\n")}\n },`;
|
|
91
|
+
});
|
|
92
|
+
return `export const F = {\n${tableBlocks.join("\n")}\n} as const;`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit the `OPT` map: `{ TABLE: { selectField: { option: "opt_…" } } }`. Only
|
|
96
|
+
* fields that carry options appear. Tables and fields with no options are
|
|
97
|
+
* omitted entirely (so `OPT.SHIPMENTS` may be absent — that's by design).
|
|
98
|
+
*/
|
|
99
|
+
function emitOptionMap(aliased) {
|
|
100
|
+
const tableBlocks = [];
|
|
101
|
+
for (const { alias, fields } of aliased) {
|
|
102
|
+
const fieldBlocks = [];
|
|
103
|
+
for (const { alias: fieldAlias, field } of fields) {
|
|
104
|
+
const options = field.options ?? [];
|
|
105
|
+
if (options.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const optionAliases = dedupeAliases(options.map((o) => o.label), false);
|
|
108
|
+
const optionLines = options.map((option, i) => ` ${propKey(optionAliases[i])}: ${JSON.stringify(option.id)},`);
|
|
109
|
+
fieldBlocks.push(` ${propKey(fieldAlias)}: {\n${optionLines.join("\n")}\n },`);
|
|
110
|
+
}
|
|
111
|
+
if (fieldBlocks.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
tableBlocks.push(` ${propKey(alias)}: {\n${fieldBlocks.join("\n")}\n },`);
|
|
114
|
+
}
|
|
115
|
+
if (tableBlocks.length === 0)
|
|
116
|
+
return `export const OPT = {} as const;`;
|
|
117
|
+
return `export const OPT = {\n${tableBlocks.join("\n")}\n} as const;`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
|
|
121
|
+
* workspace schema (the subset the app touches). An empty list yields valid,
|
|
122
|
+
* empty `F`/`OPT` maps so the file always compiles and imports resolve.
|
|
123
|
+
*/
|
|
124
|
+
export function generateAppFields(tables) {
|
|
125
|
+
if (tables.length === 0) {
|
|
126
|
+
return `${HEADER}
|
|
127
|
+
export const F = {} as const;
|
|
128
|
+
|
|
129
|
+
export const OPT = {} as const;
|
|
130
|
+
|
|
131
|
+
/** Field-id alias map (empty — no tables in scope). */
|
|
132
|
+
export type AppFields = typeof F;
|
|
133
|
+
/** Select-option alias map (empty — no tables in scope). */
|
|
134
|
+
export type AppOptions = typeof OPT;
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
const aliased = aliasTables(tables);
|
|
138
|
+
return `${HEADER}
|
|
139
|
+
${emitFieldMap(aliased)}
|
|
140
|
+
|
|
141
|
+
${emitOptionMap(aliased)}
|
|
142
|
+
|
|
143
|
+
/** Field-id alias map: \`F[<TABLE>][<field>]\` is the \`fld_…\` id (literal-typed). */
|
|
144
|
+
export type AppFields = typeof F;
|
|
145
|
+
/** Select-option alias map: \`OPT[<TABLE>][<field>][<option>]\` is the \`opt_…\` id. */
|
|
146
|
+
export type AppOptions = typeof OPT;
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|