@lotics/cli 0.68.0 → 0.70.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 +1 -0
- package/dist/args.d.ts +2 -0
- package/dist/args.js +5 -0
- package/dist/args.test.js +8 -0
- package/dist/cli.js +33 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.js +3 -0
- package/dist/generate_app_workflows_dts.d.ts +3 -1
- package/dist/generate_app_workflows_dts.js +15 -4
- package/dist/generate_app_workflows_dts.test.js +26 -0
- package/dist/src/cli.js +55 -5
- package/dist/starter_template.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ Workspaces live inside the active org. If the org has more than one, select befo
|
|
|
87
87
|
lotics workspace # list workspaces in the active org (marks current)
|
|
88
88
|
lotics workspace select wks_... # set the workspace for the active scope (pin or profile)
|
|
89
89
|
lotics workspace create "Sales" # create a new workspace (admin only)
|
|
90
|
+
lotics workspace delete wks_... --yes # delete a workspace (admin only; soft delete, recoverable)
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
Single-workspace organizations auto-select on first use. The selection is remembered per org, so switching back lands where you left off.
|
package/dist/args.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function parseArgs(argv: string[]): {
|
|
|
30
30
|
message?: string;
|
|
31
31
|
local: boolean;
|
|
32
32
|
all: boolean;
|
|
33
|
+
/** `--yes` (alias `-y`): confirm a destructive command (e.g. `workspace delete`) non-interactively. */
|
|
34
|
+
yes: boolean;
|
|
33
35
|
/** `--print-created` (alias `--report-effects`): print the honest post-run side-effect harvest. */
|
|
34
36
|
printCreated: boolean;
|
|
35
37
|
/** `--cleanup`: also delete the harvested created records (records only). */
|
package/dist/args.js
CHANGED
|
@@ -25,6 +25,7 @@ export function parseArgs(argv) {
|
|
|
25
25
|
message: undefined,
|
|
26
26
|
local: false,
|
|
27
27
|
all: false,
|
|
28
|
+
yes: false,
|
|
28
29
|
printCreated: false,
|
|
29
30
|
cleanup: false,
|
|
30
31
|
version: false,
|
|
@@ -80,6 +81,10 @@ export function parseArgs(argv) {
|
|
|
80
81
|
case "--all":
|
|
81
82
|
flags.all = true;
|
|
82
83
|
break;
|
|
84
|
+
case "--yes":
|
|
85
|
+
case "-y":
|
|
86
|
+
flags.yes = true;
|
|
87
|
+
break;
|
|
83
88
|
case "--print-created":
|
|
84
89
|
case "--report-effects":
|
|
85
90
|
flags.printCreated = true;
|
package/dist/args.test.js
CHANGED
|
@@ -63,6 +63,14 @@ describe("parseArgs", () => {
|
|
|
63
63
|
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.cleanup).toBe(false);
|
|
64
64
|
expect(parseArgs(["app", "workflow", "run", "wf"]).flags.printCreated).toBe(false);
|
|
65
65
|
});
|
|
66
|
+
it("parses --yes and -y as the same boolean confirmation flag (default false)", () => {
|
|
67
|
+
expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).flags.yes).toBe(true);
|
|
68
|
+
expect(parseArgs(["workspace", "delete", "wsp_1", "-y"]).flags.yes).toBe(true);
|
|
69
|
+
const r = parseArgs(["workspace", "delete", "wsp_1"]);
|
|
70
|
+
expect(r.flags.yes).toBe(false);
|
|
71
|
+
// The boolean flag does not consume the id positional.
|
|
72
|
+
expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).toolArgs).toBe("wsp_1");
|
|
73
|
+
});
|
|
66
74
|
it("treats `org use <name>` as command / subcommand / positional", () => {
|
|
67
75
|
const r = parseArgs(["org", "use", "Acme Corp"]);
|
|
68
76
|
expect(r.command).toBe("org");
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,7 @@ COMMANDS
|
|
|
60
60
|
lotics workspace List workspaces in the active org (marks current)
|
|
61
61
|
lotics workspace select <id> Switch to a different workspace
|
|
62
62
|
lotics workspace create <name> Create a new workspace (admin only)
|
|
63
|
+
lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)
|
|
63
64
|
lotics tools List all available tools
|
|
64
65
|
lotics tools <name> Show tool description and input schema
|
|
65
66
|
lotics run <tool> '<json>' Execute a tool
|
|
@@ -688,10 +689,41 @@ async function main() {
|
|
|
688
689
|
}
|
|
689
690
|
return;
|
|
690
691
|
}
|
|
692
|
+
if (subcommand === "delete") {
|
|
693
|
+
const targetId = toolArgs;
|
|
694
|
+
if (!targetId) {
|
|
695
|
+
console.error('Usage: lotics workspace delete <workspace_id> --yes');
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
const target = workspaces.find((ws) => ws.id === targetId);
|
|
699
|
+
if (!target) {
|
|
700
|
+
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
701
|
+
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
// Destructive + irreversible-looking to the user — require an explicit
|
|
705
|
+
// confirmation (this CLI is used non-interactively by agents and scripts).
|
|
706
|
+
if (!flags.yes) {
|
|
707
|
+
console.error(`This deletes workspace "${target.name}" (${target.id}) — every table, app, record, and template inside it becomes inaccessible (soft delete: retained and recoverable by an admin).`);
|
|
708
|
+
console.error(`Re-run with --yes to confirm: lotics workspace delete ${target.id} --yes`);
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
const deleted = await client.deleteWorkspace(target.id);
|
|
712
|
+
if (flags.json) {
|
|
713
|
+
console.log(JSON.stringify(deleted, null, 2));
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
console.error(`Deleted workspace: ${target.name} (${target.id})`);
|
|
717
|
+
if (currentWorkspaceId === target.id) {
|
|
718
|
+
console.error('This was your selected workspace — select another with "lotics workspace select <id>".');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
691
723
|
// Default: list workspaces
|
|
692
724
|
if (subcommand && subcommand !== "list") {
|
|
693
725
|
console.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
694
|
-
console.error('Usage: lotics workspace [list | select <id> | create <name>]');
|
|
726
|
+
console.error('Usage: lotics workspace [list | select <id> | create <name> | delete <id> --yes]');
|
|
695
727
|
process.exit(1);
|
|
696
728
|
}
|
|
697
729
|
if (flags.json) {
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -142,6 +142,9 @@ export class LoticsClient {
|
|
|
142
142
|
async createWorkspace(body) {
|
|
143
143
|
return this.request("POST", "/v1/workspaces", body);
|
|
144
144
|
}
|
|
145
|
+
async deleteWorkspace(id) {
|
|
146
|
+
return this.request("DELETE", `/v1/workspaces/${encodeURIComponent(id)}`);
|
|
147
|
+
}
|
|
145
148
|
async login() {
|
|
146
149
|
return this.request("POST", "/v1/cli/login");
|
|
147
150
|
}
|
|
@@ -26,7 +26,9 @@ export declare function generateAppWorkflowsDts(workflows: Record<string, AppWor
|
|
|
26
26
|
* to `unknown` rather than throwing — the server's deploy-time schema parse
|
|
27
27
|
* catches structurally-invalid declarations before they reach us.
|
|
28
28
|
*/
|
|
29
|
-
export declare function inputsToType(inputs: Record<string, unknown
|
|
29
|
+
export declare function inputsToType(inputs: Record<string, unknown>, opts?: {
|
|
30
|
+
nullableOptional?: boolean;
|
|
31
|
+
}): string;
|
|
30
32
|
/**
|
|
31
33
|
* Map an output `fields` map (also the top-level `outputs`) to a TS object type.
|
|
32
34
|
* Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
|
|
@@ -41,7 +41,7 @@ declare module "@lotics/app-sdk" {
|
|
|
41
41
|
// No typed inputs ⇒ untyped `Record<string, unknown>` callable. An alias with
|
|
42
42
|
// declared `outputs` also gets an `AppWorkflowResults` entry → typed result.data.
|
|
43
43
|
const valueType = declaration.inputs
|
|
44
|
-
? inputsToType(declaration.inputs)
|
|
44
|
+
? inputsToType(declaration.inputs, { nullableOptional: true })
|
|
45
45
|
: "Record<string, unknown>";
|
|
46
46
|
const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
47
47
|
inputLines.push(` ${aliasKey}: ${valueType};`);
|
|
@@ -74,16 +74,27 @@ ${inputLines.join("\n")}
|
|
|
74
74
|
* to `unknown` rather than throwing — the server's deploy-time schema parse
|
|
75
75
|
* catches structurally-invalid declarations before they reach us.
|
|
76
76
|
*/
|
|
77
|
-
export function inputsToType(inputs) {
|
|
77
|
+
export function inputsToType(inputs, opts) {
|
|
78
|
+
// A WORKFLOW optional input also accepts `null` — the diff-write "clear this
|
|
79
|
+
// field" signal (the backend validates it, the body forwards it to
|
|
80
|
+
// `update_records({ set: { fld: null } })`). So `useWorkflow` call sites can
|
|
81
|
+
// pass `null` to unset a field without a loose `Record<string, unknown>` cast.
|
|
82
|
+
// Query params + agent inputs have no "clear", so they stay `T` (the caller
|
|
83
|
+
// omits the flag). Only the TOP level is nullable: a nested object's optional
|
|
84
|
+
// field mirrors the backend's plain `.optional()` (the recursive call below
|
|
85
|
+
// omits the flag), so the type never claims a nested clear the server rejects.
|
|
86
|
+
const nullableOptional = opts?.nullableOptional === true;
|
|
78
87
|
const fields = [];
|
|
79
88
|
for (const [key, decl] of Object.entries(inputs)) {
|
|
80
89
|
if (decl === null || typeof decl !== "object")
|
|
81
90
|
continue;
|
|
82
91
|
const d = decl;
|
|
83
92
|
const tsType = inputDeclToTsType(d);
|
|
84
|
-
const
|
|
93
|
+
const isOptional = d.required === false;
|
|
94
|
+
const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
|
|
95
|
+
const optional = isOptional ? "?" : "";
|
|
85
96
|
const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
86
|
-
fields.push(` ${fieldKey}${optional}: ${
|
|
97
|
+
fields.push(` ${fieldKey}${optional}: ${fieldType};`);
|
|
87
98
|
}
|
|
88
99
|
if (fields.length === 0)
|
|
89
100
|
return "Record<string, never>";
|
|
@@ -58,6 +58,32 @@ describe("generateAppWorkflowsDts", () => {
|
|
|
58
58
|
expect(dts).toContain("note?: string;");
|
|
59
59
|
expect(dts).toContain("ref: string;");
|
|
60
60
|
});
|
|
61
|
+
it("types a TOP-LEVEL optional workflow input as `T | null` (the diff-write clear signal), required stays `T`, nested stays plain optional", () => {
|
|
62
|
+
const dts = generateAppWorkflowsDts({
|
|
63
|
+
updateTask: {
|
|
64
|
+
workflow_id: "wfl_u",
|
|
65
|
+
inputs: {
|
|
66
|
+
record_id: { type: "record_link", table_id: "tbl_x" },
|
|
67
|
+
due: { type: "date", required: false },
|
|
68
|
+
status: { type: "select", required: false, options: [{ label: "Done", value: "opt_done" }] },
|
|
69
|
+
tags: { type: "select", multi: true, required: false, options: [{ label: "A", value: "opt_a" }] },
|
|
70
|
+
meta: {
|
|
71
|
+
type: "object",
|
|
72
|
+
required: false,
|
|
73
|
+
fields: { note: { type: "text", required: false } },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(dts).toContain("record_id: string;"); // required → NOT nullable
|
|
79
|
+
expect(dts).toContain("due?: string | null;"); // optional scalar → clearable
|
|
80
|
+
expect(dts).toContain('status?: "opt_done" | null;');
|
|
81
|
+
expect(dts).toContain('tags?: ReadonlyArray<"opt_a"> | null;'); // optional multi
|
|
82
|
+
// The object itself is a top-level optional → nullable; its NESTED optional
|
|
83
|
+
// field mirrors the backend's plain `.optional()` (no `| null`).
|
|
84
|
+
expect(dts).toContain("note?: string;");
|
|
85
|
+
expect(dts).not.toContain("note?: string | null;");
|
|
86
|
+
});
|
|
61
87
|
it("maps record_link/select outputs the same way as inputs", () => {
|
|
62
88
|
const dts = generateAppWorkflowsDts({
|
|
63
89
|
lookup: {
|
package/dist/src/cli.js
CHANGED
|
@@ -29704,6 +29704,9 @@ var LoticsClient = class {
|
|
|
29704
29704
|
async createWorkspace(body) {
|
|
29705
29705
|
return this.request("POST", "/v1/workspaces", body);
|
|
29706
29706
|
}
|
|
29707
|
+
async deleteWorkspace(id) {
|
|
29708
|
+
return this.request("DELETE", `/v1/workspaces/${encodeURIComponent(id)}`);
|
|
29709
|
+
}
|
|
29707
29710
|
async login() {
|
|
29708
29711
|
return this.request("POST", "/v1/cli/login");
|
|
29709
29712
|
}
|
|
@@ -30638,10 +30641,18 @@ export default defineConfig({
|
|
|
30638
30641
|
// the hooks dispatcher the instant a hook (useAgentRun / useQuery)
|
|
30639
30642
|
// runs in a rendered App tree.
|
|
30640
30643
|
"@lotics/app-sdk",
|
|
30644
|
+
// The /router subpath is a SEPARATE optimizer entry \u2014 without it,
|
|
30645
|
+
// AppRouter's BrowserRouter bundles apart from the app's
|
|
30646
|
+
// react-router hooks and the Router context never matches
|
|
30647
|
+
// ("useNavigate() may be used only in the context of a <Router>").
|
|
30648
|
+
"@lotics/app-sdk/router",
|
|
30641
30649
|
// react-router-dom (App's router) also ships compiled dist JS \u2014 pin it
|
|
30642
30650
|
// into the shared chunk so its hooks don't split the react instance
|
|
30643
30651
|
// ("Invalid hook call") when a test renders the routed App tree.
|
|
30644
30652
|
"react-router-dom",
|
|
30653
|
+
// v7: react-router-dom is a thin wrapper \u2014 the actual context
|
|
30654
|
+
// lives in react-router; both must ride the shared chunk.
|
|
30655
|
+
"react-router",
|
|
30645
30656
|
],
|
|
30646
30657
|
esbuildOptions: {
|
|
30647
30658
|
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|
|
@@ -31748,7 +31759,7 @@ declare module "@lotics/app-sdk" {
|
|
|
31748
31759
|
const inputLines = [];
|
|
31749
31760
|
const resultLines = [];
|
|
31750
31761
|
for (const [alias, declaration] of entries) {
|
|
31751
|
-
const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
|
|
31762
|
+
const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true }) : "Record<string, unknown>";
|
|
31752
31763
|
const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
31753
31764
|
inputLines.push(` ${aliasKey}: ${valueType};`);
|
|
31754
31765
|
if (declaration.outputs) {
|
|
@@ -31767,15 +31778,18 @@ ${inputLines.join("\n")}
|
|
|
31767
31778
|
}
|
|
31768
31779
|
`;
|
|
31769
31780
|
}
|
|
31770
|
-
function inputsToType(inputs) {
|
|
31781
|
+
function inputsToType(inputs, opts) {
|
|
31782
|
+
const nullableOptional = opts?.nullableOptional === true;
|
|
31771
31783
|
const fields = [];
|
|
31772
31784
|
for (const [key, decl] of Object.entries(inputs)) {
|
|
31773
31785
|
if (decl === null || typeof decl !== "object") continue;
|
|
31774
31786
|
const d = decl;
|
|
31775
31787
|
const tsType = inputDeclToTsType(d);
|
|
31776
|
-
const
|
|
31788
|
+
const isOptional = d.required === false;
|
|
31789
|
+
const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
|
|
31790
|
+
const optional = isOptional ? "?" : "";
|
|
31777
31791
|
const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
31778
|
-
fields.push(` ${fieldKey}${optional}: ${
|
|
31792
|
+
fields.push(` ${fieldKey}${optional}: ${fieldType};`);
|
|
31779
31793
|
}
|
|
31780
31794
|
if (fields.length === 0) return "Record<string, never>";
|
|
31781
31795
|
return `{
|
|
@@ -33164,6 +33178,7 @@ function parseArgs(argv) {
|
|
|
33164
33178
|
message: void 0,
|
|
33165
33179
|
local: false,
|
|
33166
33180
|
all: false,
|
|
33181
|
+
yes: false,
|
|
33167
33182
|
printCreated: false,
|
|
33168
33183
|
cleanup: false,
|
|
33169
33184
|
version: false,
|
|
@@ -33219,6 +33234,10 @@ function parseArgs(argv) {
|
|
|
33219
33234
|
case "--all":
|
|
33220
33235
|
flags.all = true;
|
|
33221
33236
|
break;
|
|
33237
|
+
case "--yes":
|
|
33238
|
+
case "-y":
|
|
33239
|
+
flags.yes = true;
|
|
33240
|
+
break;
|
|
33222
33241
|
case "--print-created":
|
|
33223
33242
|
case "--report-effects":
|
|
33224
33243
|
flags.printCreated = true;
|
|
@@ -49681,6 +49700,7 @@ COMMANDS
|
|
|
49681
49700
|
lotics workspace List workspaces in the active org (marks current)
|
|
49682
49701
|
lotics workspace select <id> Switch to a different workspace
|
|
49683
49702
|
lotics workspace create <name> Create a new workspace (admin only)
|
|
49703
|
+
lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)
|
|
49684
49704
|
lotics tools List all available tools
|
|
49685
49705
|
lotics tools <name> Show tool description and input schema
|
|
49686
49706
|
lotics run <tool> '<json>' Execute a tool
|
|
@@ -50261,9 +50281,39 @@ Available workspaces:`);
|
|
|
50261
50281
|
}
|
|
50262
50282
|
return;
|
|
50263
50283
|
}
|
|
50284
|
+
if (subcommand === "delete") {
|
|
50285
|
+
const targetId = toolArgs;
|
|
50286
|
+
if (!targetId) {
|
|
50287
|
+
console.error("Usage: lotics workspace delete <workspace_id> --yes");
|
|
50288
|
+
process.exit(1);
|
|
50289
|
+
}
|
|
50290
|
+
const target = workspaces.find((ws) => ws.id === targetId);
|
|
50291
|
+
if (!target) {
|
|
50292
|
+
console.error(`Workspace not found: ${targetId}
|
|
50293
|
+
|
|
50294
|
+
Available workspaces:`);
|
|
50295
|
+
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
50296
|
+
process.exit(1);
|
|
50297
|
+
}
|
|
50298
|
+
if (!flags.yes) {
|
|
50299
|
+
console.error(`This deletes workspace "${target.name}" (${target.id}) \u2014 every table, app, record, and template inside it becomes inaccessible (soft delete: retained and recoverable by an admin).`);
|
|
50300
|
+
console.error(`Re-run with --yes to confirm: lotics workspace delete ${target.id} --yes`);
|
|
50301
|
+
process.exit(1);
|
|
50302
|
+
}
|
|
50303
|
+
const deleted = await client.deleteWorkspace(target.id);
|
|
50304
|
+
if (flags.json) {
|
|
50305
|
+
console.log(JSON.stringify(deleted, null, 2));
|
|
50306
|
+
} else {
|
|
50307
|
+
console.error(`Deleted workspace: ${target.name} (${target.id})`);
|
|
50308
|
+
if (currentWorkspaceId === target.id) {
|
|
50309
|
+
console.error('This was your selected workspace \u2014 select another with "lotics workspace select <id>".');
|
|
50310
|
+
}
|
|
50311
|
+
}
|
|
50312
|
+
return;
|
|
50313
|
+
}
|
|
50264
50314
|
if (subcommand && subcommand !== "list") {
|
|
50265
50315
|
console.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
50266
|
-
console.error("Usage: lotics workspace [list | select <id> | create <name>]");
|
|
50316
|
+
console.error("Usage: lotics workspace [list | select <id> | create <name> | delete <id> --yes]");
|
|
50267
50317
|
process.exit(1);
|
|
50268
50318
|
}
|
|
50269
50319
|
if (flags.json) {
|
package/dist/starter_template.js
CHANGED
|
@@ -316,10 +316,18 @@ export default defineConfig({
|
|
|
316
316
|
// the hooks dispatcher the instant a hook (useAgentRun / useQuery)
|
|
317
317
|
// runs in a rendered App tree.
|
|
318
318
|
"@lotics/app-sdk",
|
|
319
|
+
// The /router subpath is a SEPARATE optimizer entry — without it,
|
|
320
|
+
// AppRouter's BrowserRouter bundles apart from the app's
|
|
321
|
+
// react-router hooks and the Router context never matches
|
|
322
|
+
// ("useNavigate() may be used only in the context of a <Router>").
|
|
323
|
+
"@lotics/app-sdk/router",
|
|
319
324
|
// react-router-dom (App's router) also ships compiled dist JS — pin it
|
|
320
325
|
// into the shared chunk so its hooks don't split the react instance
|
|
321
326
|
// ("Invalid hook call") when a test renders the routed App tree.
|
|
322
327
|
"react-router-dom",
|
|
328
|
+
// v7: react-router-dom is a thin wrapper — the actual context
|
|
329
|
+
// lives in react-router; both must ride the shared chunk.
|
|
330
|
+
"react-router",
|
|
323
331
|
],
|
|
324
332
|
esbuildOptions: {
|
|
325
333
|
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|