@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 +177 -0
- package/bin/linzumi.js +15 -0
- package/package.json +24 -0
- package/src/cli.js +240 -0
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
|
+
};
|