@linzumi/cli 0.0.1-beta

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 ADDED
@@ -0,0 +1,177 @@
1
+ # @linzumi/cli
2
+
3
+ ```text
4
+ . * . . *
5
+ * _.-"""-._ .
6
+ .-' _ _ '-. *
7
+ / (o)_(o) \
8
+ | .-. | mouse in the grove
9
+ | .-( )-. | listening for Kandan
10
+ \ / '-' \ /
11
+ '-.\__ __/.-'
12
+ /| |\
13
+ _/ | | | \_
14
+ .' |__|__| '.
15
+ / . /___\ . \
16
+ '._.' /_____\ '._.'
17
+ ```
18
+
19
+ Linzumi's CLI package. It installs the `linzumi` executable.
20
+
21
+ The current CLI is intentionally small: it connects this machine to Kandan as a
22
+ local Codex runner.
23
+
24
+ ## Install
25
+
26
+ Install the exact beta version:
27
+
28
+ ```bash
29
+ npm install -g @linzumi/cli@0.0.1-beta
30
+ ```
31
+
32
+ Or install the current beta tag:
33
+
34
+ ```bash
35
+ npm install -g @linzumi/cli@beta
36
+ ```
37
+
38
+ Check that the executable is available:
39
+
40
+ ```bash
41
+ linzumi --version
42
+ ```
43
+
44
+ Expected output for this beta:
45
+
46
+ ```bash
47
+ linzumi 0.0.1-beta
48
+ ```
49
+
50
+ ## Basic Use
51
+
52
+ Run the CLI without arguments to start the default connection command:
53
+
54
+ ```bash
55
+ linzumi
56
+ ```
57
+
58
+ That prints the connection guide and tells you to start the local runner
59
+ connection flow from Kandan.
60
+
61
+ You can print the same guide directly:
62
+
63
+ ```bash
64
+ linzumi connect
65
+ ```
66
+
67
+ `linzumi connect` is the user-facing command. The CLI runs on your local
68
+ computer; Kandan does not execute this binary for you. Kandan shows the command
69
+ or connection details, and you run them locally.
70
+
71
+ ## Connect To The Linzumi Workspace
72
+
73
+ 1. Install the CLI:
74
+
75
+ ```bash
76
+ npm install -g @linzumi/cli@0.0.1-beta
77
+ ```
78
+
79
+ 2. Confirm the CLI is installed:
80
+
81
+ ```bash
82
+ linzumi --version
83
+ ```
84
+
85
+ 3. Open Kandan in your browser and switch to the Linzumi workspace.
86
+
87
+ 4. Start the local runner connection flow in Kandan.
88
+
89
+ 5. Copy the command Kandan shows you and run it in a terminal on this computer.
90
+
91
+ 6. Leave that terminal process running while you use the local runner from
92
+ Kandan.
93
+
94
+ For orientation, running the CLI without arguments prints the local connection
95
+ guide:
96
+
97
+ ```bash
98
+ linzumi
99
+ ```
100
+
101
+ ## Commands
102
+
103
+ Supported commands right now:
104
+
105
+ ```bash
106
+ linzumi
107
+ linzumi connect
108
+ linzumi --help
109
+ linzumi --version
110
+ linzumi connect --help
111
+ linzumi connect --version
112
+ ```
113
+
114
+ `local-codex-runner` is still accepted as a compatibility alias:
115
+
116
+ ```bash
117
+ linzumi local-codex-runner
118
+ ```
119
+
120
+ ## Runner Protocol
121
+
122
+ During the connection flow, Kandan may tell you to run a command shaped like:
123
+
124
+ ```bash
125
+ linzumi connect app-server --listen ws://127.0.0.1:45021
126
+ ```
127
+
128
+ That trailing `app-server --listen <url>` is runner protocol. It is not a
129
+ separate Linzumi feature; it is the Codex app-server mode that lets this local
130
+ process open the websocket Kandan will connect to.
131
+
132
+ ## Configuration
133
+
134
+ The Kandan local runner flow should give you the command or configuration to run
135
+ on your machine. For manual testing, the same configuration can be supplied with
136
+ environment variables:
137
+
138
+ ```bash
139
+ export LINZUMI_LOCAL_CODEX_RUNNER_LINZ_BIN=/path/to/kandan-local-bridge-linz
140
+ export LINZUMI_LOCAL_CODEX_RUNNER_VM_ID=lzrunner123
141
+ export LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_CODEX_BIN=.kandan/tools/codex/0.116.0/bin/codex
142
+ export LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_ENV=.kandan/runtime/agents/13/channels/8/codex_app_server/runtime.env.sh
143
+ ```
144
+
145
+ Or with flags:
146
+
147
+ ```bash
148
+ linzumi connect \
149
+ --linz-bin /path/to/kandan-local-bridge-linz \
150
+ --vm-id lzrunner123 \
151
+ --remote-codex-bin .kandan/tools/codex/0.116.0/bin/codex \
152
+ --remote-env .kandan/runtime/agents/13/channels/8/codex_app_server/runtime.env.sh \
153
+ app-server --listen ws://127.0.0.1:45021
154
+ ```
155
+
156
+ The runner executes:
157
+
158
+ ```bash
159
+ <linz-bin> exec <vm-id> -- sh -lc "exec <remote-kandan-linz> app-server --env-file <remote-env> <remote-codex-bin> <listen-url>"
160
+ ```
161
+
162
+ ## Troubleshooting
163
+
164
+ If `linzumi` is not found, confirm the global npm bin directory is on your
165
+ `PATH`:
166
+
167
+ ```bash
168
+ npm bin -g
169
+ ```
170
+
171
+ If `linzumi connect` reports missing configuration, use the full command or
172
+ configuration shown by Kandan's local runner connection flow. Manual launches
173
+ need the environment variables or flags listed above.
174
+
175
+ If the runner starts but Kandan cannot connect, check that the `--listen` URL
176
+ was supplied by Kandan and that the configured LINZ bridge command can reach the
177
+ runner VM.
package/bin/linzumi.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ const result = runCli({
6
+ argv: process.argv.slice(2),
7
+ env: process.env,
8
+ cwd: process.cwd(),
9
+ stdout: process.stdout,
10
+ stderr: process.stderr,
11
+ });
12
+
13
+ if (result.exitCode !== 0) {
14
+ process.exit(result.exitCode);
15
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@linzumi/cli",
3
+ "version": "0.0.1-beta",
4
+ "description": "Linzumi local Codex runner CLI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "linzumi": "bin/linzumi.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "node ./test/local-codex-runner.test.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=22.0.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "license": "MIT"
24
+ }
package/src/cli.js ADDED
@@ -0,0 +1,240 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const version = "0.0.1-beta";
4
+
5
+ const requiredEnv = {
6
+ linzBin: "LINZUMI_LOCAL_CODEX_RUNNER_LINZ_BIN",
7
+ vmId: "LINZUMI_LOCAL_CODEX_RUNNER_VM_ID",
8
+ remoteCodexBin: "LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_CODEX_BIN",
9
+ remoteEnv: "LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_ENV",
10
+ };
11
+
12
+ const usage = `linzumi connect [OPTIONS]
13
+
14
+ Connects this machine to Kandan as the local Codex runner.
15
+
16
+ This command runs on your local computer. Use the command shown by Kandan's
17
+ local runner connection flow to start the local Codex runner here.
18
+
19
+ Options:
20
+ --linz-bin <path> LINZ CLI or bridge wrapper to execute
21
+ --vm-id <id> LINZ runner VM id
22
+ --remote-codex-bin <path> Remote Codex binary path
23
+ --remote-env <path> Remote env-file path
24
+ --remote-kandan-linz <path> Remote kandan_linz path
25
+ --help Show this help
26
+ --version Show CLI version
27
+
28
+ Environment defaults:
29
+ ${requiredEnv.linzBin}
30
+ ${requiredEnv.vmId}
31
+ ${requiredEnv.remoteCodexBin}
32
+ ${requiredEnv.remoteEnv}
33
+ LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_KANDAN_LINZ
34
+
35
+ Internal runner protocol:
36
+ linzumi connect [OPTIONS] app-server --listen <url>
37
+ `;
38
+
39
+ const defaultRemoteKandanLinz = "/home/linz/.kandan/runner/bin/kandan_linz";
40
+
41
+ const connectGuide = `linzumi connect
42
+
43
+ This command connects this machine to Kandan as a local Codex runner.
44
+
45
+ Open Kandan, start the local runner connection flow, and follow the command it
46
+ shows you. Run that command on this computer.
47
+
48
+ For help:
49
+ linzumi connect --help
50
+ `;
51
+
52
+ const isBlank = (value) => typeof value !== "string" || value.trim() === "";
53
+
54
+ const readOptionValue = (tokens, index, name) => {
55
+ const value = tokens[index + 1];
56
+ if (isBlank(value)) {
57
+ return { ok: false, error: `${name} requires a value` };
58
+ }
59
+
60
+ return { ok: true, value, nextIndex: index + 2 };
61
+ };
62
+
63
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
64
+
65
+ export const buildRemoteCommand = ({ remoteKandanLinz, remoteEnv, remoteCodexBin, listenUrl }) => {
66
+ const rendered = [
67
+ "exec",
68
+ shellQuote(remoteKandanLinz),
69
+ "app-server",
70
+ "--env-file",
71
+ shellQuote(remoteEnv),
72
+ shellQuote(remoteCodexBin),
73
+ shellQuote(listenUrl),
74
+ ];
75
+
76
+ return rendered.join(" ");
77
+ };
78
+
79
+ const normalizeRemotePath = (path) => {
80
+ if (path.startsWith("/")) {
81
+ return path;
82
+ }
83
+
84
+ return `/home/linz/${path}`;
85
+ };
86
+
87
+ export const parseLocalCodexRunnerArgs = ({ argv, env }) => {
88
+ const options = {
89
+ linzBin: env[requiredEnv.linzBin],
90
+ vmId: env[requiredEnv.vmId],
91
+ remoteCodexBin: env[requiredEnv.remoteCodexBin],
92
+ remoteEnv: env[requiredEnv.remoteEnv],
93
+ remoteKandanLinz:
94
+ env.LINZUMI_LOCAL_CODEX_RUNNER_REMOTE_KANDAN_LINZ ?? defaultRemoteKandanLinz,
95
+ };
96
+ const command = [];
97
+
98
+ let index = 0;
99
+ while (index < argv.length) {
100
+ const token = argv[index];
101
+
102
+ switch (token) {
103
+ case "--help":
104
+ case "-h":
105
+ return { ok: true, help: true };
106
+ case "--version":
107
+ return { ok: true, version: true };
108
+ case "--linz-bin": {
109
+ const parsed = readOptionValue(argv, index, "--linz-bin");
110
+ if (!parsed.ok) return parsed;
111
+ options.linzBin = parsed.value;
112
+ index = parsed.nextIndex;
113
+ break;
114
+ }
115
+ case "--vm-id": {
116
+ const parsed = readOptionValue(argv, index, "--vm-id");
117
+ if (!parsed.ok) return parsed;
118
+ options.vmId = parsed.value;
119
+ index = parsed.nextIndex;
120
+ break;
121
+ }
122
+ case "--remote-codex-bin": {
123
+ const parsed = readOptionValue(argv, index, "--remote-codex-bin");
124
+ if (!parsed.ok) return parsed;
125
+ options.remoteCodexBin = parsed.value;
126
+ index = parsed.nextIndex;
127
+ break;
128
+ }
129
+ case "--remote-env": {
130
+ const parsed = readOptionValue(argv, index, "--remote-env");
131
+ if (!parsed.ok) return parsed;
132
+ options.remoteEnv = parsed.value;
133
+ index = parsed.nextIndex;
134
+ break;
135
+ }
136
+ case "--remote-kandan-linz": {
137
+ const parsed = readOptionValue(argv, index, "--remote-kandan-linz");
138
+ if (!parsed.ok) return parsed;
139
+ options.remoteKandanLinz = parsed.value;
140
+ index = parsed.nextIndex;
141
+ break;
142
+ }
143
+ case "--":
144
+ command.push(...argv.slice(index + 1));
145
+ index = argv.length;
146
+ break;
147
+ default:
148
+ command.push(...argv.slice(index));
149
+ index = argv.length;
150
+ break;
151
+ }
152
+ }
153
+
154
+ const [subcommand, listenFlag, listenUrl, ...extra] = command;
155
+ if (subcommand !== "app-server" || listenFlag !== "--listen" || isBlank(listenUrl) || extra.length > 0) {
156
+ return {
157
+ ok: false,
158
+ error: "connect must be launched by Kandan with the internal runner protocol",
159
+ };
160
+ }
161
+
162
+ const missing = Object.entries(options)
163
+ .filter(([, value]) => isBlank(value))
164
+ .map(([name]) => name);
165
+
166
+ if (missing.length > 0) {
167
+ return { ok: false, error: `missing connect option(s): ${missing.join(", ")}` };
168
+ }
169
+
170
+ return {
171
+ ok: true,
172
+ options: {
173
+ ...options,
174
+ remoteCodexBin: normalizeRemotePath(options.remoteCodexBin),
175
+ remoteEnv: normalizeRemotePath(options.remoteEnv),
176
+ remoteKandanLinz: normalizeRemotePath(options.remoteKandanLinz),
177
+ listenUrl,
178
+ },
179
+ };
180
+ };
181
+
182
+ export const runLocalCodexRunner = ({ argv, env, cwd, spawn = spawnSync }) => {
183
+ if (argv.length === 0) {
184
+ return { exitCode: 0, stdout: connectGuide, stderr: "" };
185
+ }
186
+
187
+ const parsed = parseLocalCodexRunnerArgs({ argv, env });
188
+
189
+ if (!parsed.ok) {
190
+ return { exitCode: 64, stdout: "", stderr: `${parsed.error}\n` };
191
+ }
192
+
193
+ if (parsed.help) {
194
+ return { exitCode: 0, stdout: usage, stderr: "" };
195
+ }
196
+
197
+ if (parsed.version) {
198
+ return { exitCode: 0, stdout: `linzumi ${version}\n`, stderr: "" };
199
+ }
200
+
201
+ const { linzBin, vmId, remoteKandanLinz, remoteEnv, remoteCodexBin, listenUrl } = parsed.options;
202
+ const remoteCommand = buildRemoteCommand({ remoteKandanLinz, remoteEnv, remoteCodexBin, listenUrl });
203
+ const result = spawn(linzBin, ["exec", vmId, "--", "sh", "-lc", remoteCommand], {
204
+ cwd,
205
+ encoding: "utf8",
206
+ stdio: ["ignore", "inherit", "inherit"],
207
+ });
208
+
209
+ if (result.error) {
210
+ return { exitCode: 127, stdout: "", stderr: `${result.error.message}\n` };
211
+ }
212
+
213
+ return { exitCode: result.status ?? 1, stdout: "", stderr: "" };
214
+ };
215
+
216
+ export const runCliToResult = ({ argv, env, cwd, spawn = spawnSync }) => {
217
+ const [command, ...rest] = argv;
218
+
219
+ switch (command) {
220
+ case undefined:
221
+ return runLocalCodexRunner({ argv: [], env, cwd, spawn });
222
+ case "connect":
223
+ case "local-codex-runner":
224
+ return runLocalCodexRunner({ argv: rest, env, cwd, spawn });
225
+ case "--help":
226
+ case "-h":
227
+ return { exitCode: 0, stdout: usage, stderr: "" };
228
+ case "--version":
229
+ return { exitCode: 0, stdout: `linzumi ${version}\n`, stderr: "" };
230
+ default:
231
+ return { exitCode: 64, stdout: "", stderr: `unknown linzumi command: ${command}\n${usage}` };
232
+ }
233
+ };
234
+
235
+ export const runCli = ({ argv, env, cwd, stdout, stderr, spawn = spawnSync }) => {
236
+ const result = runCliToResult({ argv, env, cwd, spawn });
237
+ if (result.stdout !== "") stdout.write(result.stdout);
238
+ if (result.stderr !== "") stderr.write(result.stderr);
239
+ return { exitCode: result.exitCode };
240
+ };