@lotics/cli 0.29.0 → 0.30.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/src/args.d.ts +32 -0
- package/dist/src/args.js +94 -0
- package/dist/src/args.test.d.ts +1 -0
- package/dist/src/args.test.js +37 -0
- package/dist/src/cli.js +6 -80
- package/dist/src/dev/wrapper_page.d.ts +10 -5
- package/dist/src/dev/wrapper_page.js +24 -12
- package/dist/src/generate_app_workflows_dts.js +2 -0
- package/dist/src/starter_template.js +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
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 (`--force-workflow-sync`, `--json`, …) toggle. Anything not matching a
|
|
8
|
+
* known flag is 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
|
+
name?: string;
|
|
25
|
+
timezone?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
forceWorkflowSync: boolean;
|
|
28
|
+
local: boolean;
|
|
29
|
+
version: boolean;
|
|
30
|
+
help: boolean;
|
|
31
|
+
};
|
|
32
|
+
};
|
package/dist/src/args.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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 (`--force-workflow-sync`, `--json`, …) toggle. Anything not matching a
|
|
8
|
+
* known flag is 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
|
+
name: undefined,
|
|
21
|
+
timezone: undefined,
|
|
22
|
+
message: undefined,
|
|
23
|
+
forceWorkflowSync: false,
|
|
24
|
+
local: false,
|
|
25
|
+
version: false,
|
|
26
|
+
help: false,
|
|
27
|
+
};
|
|
28
|
+
let command;
|
|
29
|
+
let subcommand;
|
|
30
|
+
let toolArgs;
|
|
31
|
+
const restArgs = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < argv.length) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case "--json":
|
|
37
|
+
flags.json = true;
|
|
38
|
+
break;
|
|
39
|
+
case "--timeout":
|
|
40
|
+
flags.timeout = parseInt(argv[++i], 10);
|
|
41
|
+
break;
|
|
42
|
+
case "--output":
|
|
43
|
+
case "-o":
|
|
44
|
+
flags.output = argv[++i];
|
|
45
|
+
break;
|
|
46
|
+
case "--as":
|
|
47
|
+
flags.as = argv[++i];
|
|
48
|
+
break;
|
|
49
|
+
case "--api-key":
|
|
50
|
+
flags.apiKey = argv[++i];
|
|
51
|
+
break;
|
|
52
|
+
case "--name":
|
|
53
|
+
flags.name = argv[++i];
|
|
54
|
+
break;
|
|
55
|
+
case "--timezone":
|
|
56
|
+
flags.timezone = argv[++i];
|
|
57
|
+
break;
|
|
58
|
+
case "-m":
|
|
59
|
+
case "--message":
|
|
60
|
+
flags.message = argv[++i];
|
|
61
|
+
break;
|
|
62
|
+
case "--force-workflow-sync":
|
|
63
|
+
flags.forceWorkflowSync = true;
|
|
64
|
+
break;
|
|
65
|
+
case "--local":
|
|
66
|
+
flags.local = true;
|
|
67
|
+
break;
|
|
68
|
+
case "--version":
|
|
69
|
+
case "-v":
|
|
70
|
+
flags.version = true;
|
|
71
|
+
break;
|
|
72
|
+
case "--help":
|
|
73
|
+
case "-h":
|
|
74
|
+
flags.help = true;
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
if (!command) {
|
|
78
|
+
command = arg;
|
|
79
|
+
}
|
|
80
|
+
else if (!subcommand) {
|
|
81
|
+
subcommand = arg;
|
|
82
|
+
}
|
|
83
|
+
else if (!toolArgs) {
|
|
84
|
+
toolArgs = arg;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
restArgs.push(arg);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
return { command, subcommand, toolArgs, restArgs, flags };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
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` and `--force-workflow-sync` must be parsed
|
|
12
|
+
// as flags, not silently 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("parses --force-workflow-sync as a boolean flag", () => {
|
|
23
|
+
const r = parseArgs(["app", "deploy", "msg", "--force-workflow-sync"]);
|
|
24
|
+
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
25
|
+
expect(r.toolArgs).toBe("msg");
|
|
26
|
+
});
|
|
27
|
+
it("parses --force-workflow-sync before -m without eating the message", () => {
|
|
28
|
+
const r = parseArgs(["app", "deploy", "--force-workflow-sync", "-m", "msg"]);
|
|
29
|
+
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
30
|
+
expect(r.flags.message).toBe("msg");
|
|
31
|
+
});
|
|
32
|
+
it("defaults forceWorkflowSync to false and message to undefined", () => {
|
|
33
|
+
const r = parseArgs(["app", "deploy"]);
|
|
34
|
+
expect(r.flags.forceWorkflowSync).toBe(false);
|
|
35
|
+
expect(r.flags.message).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
package/dist/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
|
6
6
|
import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
|
|
7
7
|
import { VERSION } from "./version.js";
|
|
8
8
|
import { appCreate, appPull, appDeploy, appDev } from "./app_commands.js";
|
|
9
|
+
import { parseArgs } from "./args.js";
|
|
9
10
|
function printHelp() {
|
|
10
11
|
console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
|
|
11
12
|
|
|
@@ -114,79 +115,6 @@ else ~/.lotics/config.json. A per-directory config pins a project or worktree to
|
|
|
114
115
|
its own account and workspace; --local creates one. Note: an exported
|
|
115
116
|
LOTICS_API_KEY env var overrides the config file's key.`);
|
|
116
117
|
}
|
|
117
|
-
function parseArgs(argv) {
|
|
118
|
-
const flags = {
|
|
119
|
-
json: false,
|
|
120
|
-
timeout: undefined,
|
|
121
|
-
output: undefined,
|
|
122
|
-
as: undefined,
|
|
123
|
-
apiKey: undefined,
|
|
124
|
-
name: undefined,
|
|
125
|
-
timezone: undefined,
|
|
126
|
-
local: false,
|
|
127
|
-
version: false,
|
|
128
|
-
help: false,
|
|
129
|
-
};
|
|
130
|
-
let command;
|
|
131
|
-
let subcommand;
|
|
132
|
-
let toolArgs;
|
|
133
|
-
const restArgs = [];
|
|
134
|
-
let i = 0;
|
|
135
|
-
while (i < argv.length) {
|
|
136
|
-
const arg = argv[i];
|
|
137
|
-
switch (arg) {
|
|
138
|
-
case "--json":
|
|
139
|
-
flags.json = true;
|
|
140
|
-
break;
|
|
141
|
-
case "--timeout":
|
|
142
|
-
flags.timeout = parseInt(argv[++i], 10);
|
|
143
|
-
break;
|
|
144
|
-
case "--output":
|
|
145
|
-
case "-o":
|
|
146
|
-
flags.output = argv[++i];
|
|
147
|
-
break;
|
|
148
|
-
case "--as":
|
|
149
|
-
flags.as = argv[++i];
|
|
150
|
-
break;
|
|
151
|
-
case "--api-key":
|
|
152
|
-
flags.apiKey = argv[++i];
|
|
153
|
-
break;
|
|
154
|
-
case "--name":
|
|
155
|
-
flags.name = argv[++i];
|
|
156
|
-
break;
|
|
157
|
-
case "--timezone":
|
|
158
|
-
flags.timezone = argv[++i];
|
|
159
|
-
break;
|
|
160
|
-
case "--local":
|
|
161
|
-
flags.local = true;
|
|
162
|
-
break;
|
|
163
|
-
case "--version":
|
|
164
|
-
case "-v":
|
|
165
|
-
flags.version = true;
|
|
166
|
-
break;
|
|
167
|
-
case "--help":
|
|
168
|
-
case "-h":
|
|
169
|
-
flags.help = true;
|
|
170
|
-
break;
|
|
171
|
-
default:
|
|
172
|
-
if (!command) {
|
|
173
|
-
command = arg;
|
|
174
|
-
}
|
|
175
|
-
else if (!subcommand) {
|
|
176
|
-
subcommand = arg;
|
|
177
|
-
}
|
|
178
|
-
else if (!toolArgs) {
|
|
179
|
-
toolArgs = arg;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
restArgs.push(arg);
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
i++;
|
|
187
|
-
}
|
|
188
|
-
return { command, subcommand, toolArgs, restArgs, flags };
|
|
189
|
-
}
|
|
190
118
|
function readStdin() {
|
|
191
119
|
return new Promise((resolve, reject) => {
|
|
192
120
|
const chunks = [];
|
|
@@ -546,13 +474,11 @@ async function main() {
|
|
|
546
474
|
return;
|
|
547
475
|
}
|
|
548
476
|
if (subcommand === "deploy") {
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const forceWorkflowSync = restArgs.includes("--force-workflow-sync");
|
|
555
|
-
await appDeploy(client, { message, forceWorkflowSync });
|
|
477
|
+
// The message is either `-m <message>` or a bare positional arg after
|
|
478
|
+
// `deploy`. `--force-workflow-sync` is a parsed boolean flag, valid in
|
|
479
|
+
// any position.
|
|
480
|
+
const message = flags.message ?? toolArgs;
|
|
481
|
+
await appDeploy(client, { message, forceWorkflowSync: flags.forceWorkflowSync });
|
|
556
482
|
return;
|
|
557
483
|
}
|
|
558
484
|
if (subcommand === "dev") {
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Builds the wrapper HTML for `lotics app dev`.
|
|
3
3
|
*
|
|
4
4
|
* The wrapper page is served at http://localhost:<port>/ and embeds the
|
|
5
|
-
* project's Vite dev server in
|
|
6
|
-
* sandbox="allow-scripts" attribute production uses
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* project's Vite dev server in an iframe with the same
|
|
6
|
+
* sandbox="allow-scripts allow-same-origin" attribute production uses —
|
|
7
|
+
* safe because the Vite iframe is cross-origin to this wrapper (different
|
|
8
|
+
* port), so the app gets its own real origin (and Web Storage) without
|
|
9
|
+
* being able to reach the wrapper. The iframe sends postMessage RPCs to
|
|
10
|
+
* this wrapper, which forwards them to the local /_rpc endpoint, which
|
|
11
|
+
* dispatches to api.lotics.ai with the CLI's API key.
|
|
12
|
+
*
|
|
13
|
+
* postMessage is origin-locked both ways — the wrapper passes its origin
|
|
14
|
+
* via `?lotics_host=` and talks only to the Vite origin.
|
|
10
15
|
*
|
|
11
16
|
* Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
|
|
12
17
|
* iframe → wrapper: { id: number, op: string, payload: unknown }
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Builds the wrapper HTML for `lotics app dev`.
|
|
3
3
|
*
|
|
4
4
|
* The wrapper page is served at http://localhost:<port>/ and embeds the
|
|
5
|
-
* project's Vite dev server in
|
|
6
|
-
* sandbox="allow-scripts" attribute production uses
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* project's Vite dev server in an iframe with the same
|
|
6
|
+
* sandbox="allow-scripts allow-same-origin" attribute production uses —
|
|
7
|
+
* safe because the Vite iframe is cross-origin to this wrapper (different
|
|
8
|
+
* port), so the app gets its own real origin (and Web Storage) without
|
|
9
|
+
* being able to reach the wrapper. The iframe sends postMessage RPCs to
|
|
10
|
+
* this wrapper, which forwards them to the local /_rpc endpoint, which
|
|
11
|
+
* dispatches to api.lotics.ai with the CLI's API key.
|
|
12
|
+
*
|
|
13
|
+
* postMessage is origin-locked both ways — the wrapper passes its origin
|
|
14
|
+
* via `?lotics_host=` and talks only to the Vite origin.
|
|
10
15
|
*
|
|
11
16
|
* Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
|
|
12
17
|
* iframe → wrapper: { id: number, op: string, payload: unknown }
|
|
@@ -53,12 +58,19 @@ export function buildWrapperPage(args) {
|
|
|
53
58
|
<span>workspace <code>${escapeHtml(workspace_id)}</code></span>
|
|
54
59
|
<span>RPC → <code>${escapeHtml(api_url)}</code></span>
|
|
55
60
|
</header>
|
|
56
|
-
<iframe id="app" sandbox="allow-scripts" allow="clipboard-write"
|
|
61
|
+
<iframe id="app" sandbox="allow-scripts allow-same-origin" allow="clipboard-write"></iframe>
|
|
57
62
|
<script>
|
|
58
63
|
(function () {
|
|
59
64
|
const APP_ID = ${JSON.stringify(app_id)};
|
|
65
|
+
const VITE_URL = ${JSON.stringify(vite_url)};
|
|
66
|
+
const VITE_ORIGIN = new URL(VITE_URL).origin;
|
|
60
67
|
const iframe = document.getElementById("app");
|
|
61
68
|
|
|
69
|
+
// Pass the wrapper's own origin to the app via ?lotics_host= so the app
|
|
70
|
+
// SDK can origin-lock its postMessage bridge.
|
|
71
|
+
iframe.src = VITE_URL + (VITE_URL.indexOf("?") >= 0 ? "&" : "?")
|
|
72
|
+
+ "lotics_host=" + encodeURIComponent(window.location.origin);
|
|
73
|
+
|
|
62
74
|
async function rpc(op, payload) {
|
|
63
75
|
const res = await fetch("/_rpc", {
|
|
64
76
|
method: "POST",
|
|
@@ -104,7 +116,7 @@ export function buildWrapperPage(args) {
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
window.addEventListener("message", async function (event) {
|
|
107
|
-
if (event.source !== iframe.contentWindow) return;
|
|
119
|
+
if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
|
|
108
120
|
const msg = event.data;
|
|
109
121
|
if (!msg || typeof msg.id !== "number" || typeof msg.op !== "string") return;
|
|
110
122
|
const startedAt = performance.now();
|
|
@@ -114,13 +126,16 @@ export function buildWrapperPage(args) {
|
|
|
114
126
|
: await rpc(msg.op, msg.payload);
|
|
115
127
|
const ms = Math.round(performance.now() - startedAt);
|
|
116
128
|
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
117
|
-
iframe.contentWindow.postMessage(
|
|
129
|
+
iframe.contentWindow.postMessage(
|
|
130
|
+
{ id: msg.id, type: "result", data: data },
|
|
131
|
+
VITE_ORIGIN
|
|
132
|
+
);
|
|
118
133
|
} catch (err) {
|
|
119
134
|
const message = err && err.message ? err.message : String(err);
|
|
120
135
|
console.error("[lotics-dev] " + msg.op + " failed:", message);
|
|
121
136
|
iframe.contentWindow.postMessage(
|
|
122
137
|
{ id: msg.id, type: "error", message: message },
|
|
123
|
-
|
|
138
|
+
VITE_ORIGIN
|
|
124
139
|
);
|
|
125
140
|
}
|
|
126
141
|
});
|
|
@@ -136,6 +151,3 @@ function escapeHtml(s) {
|
|
|
136
151
|
.replace(/</g, "<")
|
|
137
152
|
.replace(/>/g, ">");
|
|
138
153
|
}
|
|
139
|
-
function escapeAttr(s) {
|
|
140
|
-
return escapeHtml(s).replace(/"/g, """);
|
|
141
|
-
}
|
|
@@ -50,7 +50,7 @@ export function buildStarterTemplate(args) {
|
|
|
50
50
|
test: "vitest run",
|
|
51
51
|
},
|
|
52
52
|
dependencies: {
|
|
53
|
-
"@lotics/app-sdk": "^0.
|
|
53
|
+
"@lotics/app-sdk": "^0.7.0",
|
|
54
54
|
"@lotics/ui": "^1.3.0",
|
|
55
55
|
"@react-native-picker/picker": "^2.7.0",
|
|
56
56
|
"expo-image": "~3.0.9",
|
|
@@ -155,6 +155,11 @@ export default defineConfig({
|
|
|
155
155
|
// iframe loads modules from api.lotics.ai which already permits null
|
|
156
156
|
// origin via CORS.
|
|
157
157
|
cors: { origin: "*" },
|
|
158
|
+
// @lotics/ui/fonts.css references the API's /iframe/fonts/*.woff2 files
|
|
159
|
+
// by root-relative URL. A deployed app is served from the API origin so
|
|
160
|
+
// they resolve directly; under \`lotics app dev\` the app runs on
|
|
161
|
+
// localhost, so proxy /iframe to the API to load the real fonts.
|
|
162
|
+
proxy: { "/iframe": { target: "https://api.lotics.ai", changeOrigin: true } },
|
|
158
163
|
},
|
|
159
164
|
test: {
|
|
160
165
|
environment: "jsdom",
|