@aitty/cli 0.1.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/dist/cli.d.ts +25 -0
- package/dist/cli.js +269 -0
- package/package.json +48 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AittyRuntimeDependencies } from "@aitty/server";
|
|
2
|
+
|
|
3
|
+
//#region src/cli.d.ts
|
|
4
|
+
interface CliRuntimeDependencies extends AittyRuntimeDependencies {
|
|
5
|
+
openBrowser?: (url: string, options?: {
|
|
6
|
+
wait?: boolean;
|
|
7
|
+
}) => Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface CliRunOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
loadRuntimeDependencies?: () => Promise<CliRuntimeDependencies>;
|
|
13
|
+
registerSignalHandlers?: boolean;
|
|
14
|
+
stderr?: {
|
|
15
|
+
write(chunk: string): boolean;
|
|
16
|
+
};
|
|
17
|
+
stdout?: {
|
|
18
|
+
write(chunk: string): boolean;
|
|
19
|
+
};
|
|
20
|
+
version?: string;
|
|
21
|
+
}
|
|
22
|
+
declare function runCli(argv?: readonly string[], options?: CliRunOptions): Promise<number>;
|
|
23
|
+
declare function main(argv?: readonly string[], options?: CliRunOptions): Promise<number>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { main, runCli };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { constants, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { access, stat } from "node:fs/promises";
|
|
4
|
+
import { delimiter, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createAittyServer, loadRuntimeDependencies } from "@aitty/server";
|
|
7
|
+
import { createStructuredLogger } from "@aitty/server/logging";
|
|
8
|
+
//#region src/cli.ts
|
|
9
|
+
var CliUsageError = class extends Error {
|
|
10
|
+
exitCode = 2;
|
|
11
|
+
};
|
|
12
|
+
var CliRuntimeError = class extends Error {
|
|
13
|
+
exitCode;
|
|
14
|
+
constructor(message, exitCode = 1) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const HELP_TEXT = [
|
|
20
|
+
"Usage: aitty exec [options] [--] command [args...]",
|
|
21
|
+
"",
|
|
22
|
+
"Options:",
|
|
23
|
+
" --cwd <dir> Working directory for the spawned child",
|
|
24
|
+
" --port <port> Listen on a specific loopback port",
|
|
25
|
+
" --host <host> Listen on a specific loopback host",
|
|
26
|
+
" --buffer-size <bytes> Replay buffer size in bytes",
|
|
27
|
+
" --theme <name> Set the initial shell data-theme attribute",
|
|
28
|
+
" --no-open Do not auto-open the browser",
|
|
29
|
+
" --verbose Enable DEBUG logging on stderr",
|
|
30
|
+
" --help Show this help message",
|
|
31
|
+
" --version Show the package version",
|
|
32
|
+
"",
|
|
33
|
+
"Examples:",
|
|
34
|
+
" aitty exec -- codex",
|
|
35
|
+
" aitty exec -- claude",
|
|
36
|
+
" aitty exec -- droid -r",
|
|
37
|
+
""
|
|
38
|
+
].join("\n");
|
|
39
|
+
let cachedPackageVersion;
|
|
40
|
+
async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
41
|
+
const stdout = options.stdout ?? process.stdout;
|
|
42
|
+
const stderr = options.stderr ?? process.stderr;
|
|
43
|
+
const env = options.env ?? process.env;
|
|
44
|
+
const currentWorkingDirectory = options.cwd ?? process.cwd();
|
|
45
|
+
try {
|
|
46
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
47
|
+
stdout.write(HELP_TEXT);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
51
|
+
stdout.write(`${resolvePackageVersion(options.version)}\n`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const config = await prepareConfig(parseCliArgs(argv, currentWorkingDirectory), env, currentWorkingDirectory);
|
|
55
|
+
const dependencies = await (options.loadRuntimeDependencies ?? loadRuntimeDependencies)();
|
|
56
|
+
const session = await createAittyServer({
|
|
57
|
+
args: config.args,
|
|
58
|
+
bufferSize: config.bufferSize,
|
|
59
|
+
command: config.command,
|
|
60
|
+
cwd: config.cwd,
|
|
61
|
+
env,
|
|
62
|
+
host: config.host,
|
|
63
|
+
port: config.port,
|
|
64
|
+
theme: config.theme,
|
|
65
|
+
verbose: config.verbose
|
|
66
|
+
}, dependencies, {
|
|
67
|
+
registerSignalHandlers: options.registerSignalHandlers,
|
|
68
|
+
stderr
|
|
69
|
+
});
|
|
70
|
+
stdout.write(`${session.url}\n`);
|
|
71
|
+
openCliBrowser(session.url, session.token, config, dependencies, stderr);
|
|
72
|
+
return session.closed;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
stderr.write(formatCliError(error));
|
|
75
|
+
return resolveCliExitCode(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function openCliBrowser(url, token, config, dependencies, stderr) {
|
|
79
|
+
if (config.noOpen || !dependencies.openBrowser) return;
|
|
80
|
+
const logger = createStructuredLogger({
|
|
81
|
+
token,
|
|
82
|
+
verbose: config.verbose,
|
|
83
|
+
writer: stderr
|
|
84
|
+
});
|
|
85
|
+
dependencies.openBrowser(url, { wait: false }).catch((error) => {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
logger.error(`Failed to open browser: ${message}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function main(argv = process.argv.slice(2), options = {}) {
|
|
91
|
+
return runCli(argv, options);
|
|
92
|
+
}
|
|
93
|
+
function parseCliArgs(argv, currentWorkingDirectory) {
|
|
94
|
+
if (argv[0] !== "exec") throw new CliUsageError("Missing command. Use: aitty exec -- <command> [args...]");
|
|
95
|
+
let bufferSize;
|
|
96
|
+
let cwd = currentWorkingDirectory;
|
|
97
|
+
let host = "127.0.0.1";
|
|
98
|
+
let noOpen = false;
|
|
99
|
+
let port = 0;
|
|
100
|
+
let theme;
|
|
101
|
+
let verbose = false;
|
|
102
|
+
let index = 1;
|
|
103
|
+
while (index < argv.length) {
|
|
104
|
+
const argument = argv[index];
|
|
105
|
+
if (argument === "--") return withCommand(argv.slice(index + 1));
|
|
106
|
+
if (argument === "--cwd") {
|
|
107
|
+
cwd = resolve(currentWorkingDirectory, readRequiredValue(argv, index, "--cwd"));
|
|
108
|
+
index += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (argument === "--port") {
|
|
112
|
+
port = parsePort(readRequiredValue(argv, index, "--port"));
|
|
113
|
+
index += 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (argument === "--host") {
|
|
117
|
+
host = readRequiredValue(argv, index, "--host").trim();
|
|
118
|
+
index += 2;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (argument === "--buffer-size") {
|
|
122
|
+
bufferSize = parsePositiveInteger(readRequiredValue(argv, index, "--buffer-size"), "--buffer-size");
|
|
123
|
+
index += 2;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (argument === "--theme") {
|
|
127
|
+
theme = normalizeThemeOption(readRequiredValue(argv, index, "--theme"));
|
|
128
|
+
index += 2;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (argument === "--no-open") {
|
|
132
|
+
noOpen = true;
|
|
133
|
+
index += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (argument === "--verbose") {
|
|
137
|
+
verbose = true;
|
|
138
|
+
index += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return withCommand(argv.slice(index));
|
|
142
|
+
}
|
|
143
|
+
throw new CliUsageError("Missing command after exec");
|
|
144
|
+
function withCommand(command) {
|
|
145
|
+
if (command.length === 0) throw new CliUsageError("Missing command after exec");
|
|
146
|
+
return {
|
|
147
|
+
args: command.slice(1),
|
|
148
|
+
bufferSize,
|
|
149
|
+
command: command[0],
|
|
150
|
+
cwd,
|
|
151
|
+
host,
|
|
152
|
+
noOpen,
|
|
153
|
+
port,
|
|
154
|
+
theme,
|
|
155
|
+
verbose
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function normalizeThemeOption(value) {
|
|
160
|
+
const theme = value?.trim();
|
|
161
|
+
return theme ? theme : void 0;
|
|
162
|
+
}
|
|
163
|
+
async function prepareConfig(config, env, currentWorkingDirectory) {
|
|
164
|
+
return {
|
|
165
|
+
...config,
|
|
166
|
+
command: await resolveExecutablePath(config.command, env, currentWorkingDirectory),
|
|
167
|
+
cwd: await validateWorkingDirectory(config.cwd)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async function validateWorkingDirectory(cwd) {
|
|
171
|
+
const resolvedDirectory = resolve(cwd);
|
|
172
|
+
if (!(await statPath(resolvedDirectory, `Configured --cwd does not exist: ${cwd}`)).isDirectory()) throw new CliRuntimeError(`Configured --cwd is not a directory: ${cwd}`);
|
|
173
|
+
return resolvedDirectory;
|
|
174
|
+
}
|
|
175
|
+
async function resolveExecutablePath(executable, env, currentWorkingDirectory) {
|
|
176
|
+
if (isExplicitPath(executable)) return validateExecutablePath(resolve(currentWorkingDirectory, executable), executable);
|
|
177
|
+
const resolvedExecutable = await findExecutableOnPath(executable, env.PATH, currentWorkingDirectory);
|
|
178
|
+
if (resolvedExecutable) return resolvedExecutable;
|
|
179
|
+
throw new CliRuntimeError(`Executable not found in PATH: ${executable}`, 127);
|
|
180
|
+
}
|
|
181
|
+
function isExplicitPath(executable) {
|
|
182
|
+
return executable.includes("/") || executable.includes("\\");
|
|
183
|
+
}
|
|
184
|
+
async function validateExecutablePath(resolvedExecutable, displayValue) {
|
|
185
|
+
if (!(await statPath(resolvedExecutable, `Executable does not exist: ${displayValue}`)).isFile()) throw new CliRuntimeError(`Executable is not a regular file: ${displayValue}`);
|
|
186
|
+
try {
|
|
187
|
+
await access(resolvedExecutable, constants.X_OK);
|
|
188
|
+
} catch {
|
|
189
|
+
throw new CliRuntimeError(`Executable is not executable: ${displayValue}`);
|
|
190
|
+
}
|
|
191
|
+
return resolvedExecutable;
|
|
192
|
+
}
|
|
193
|
+
async function findExecutableOnPath(executable, pathValue, currentWorkingDirectory) {
|
|
194
|
+
if (!pathValue) return null;
|
|
195
|
+
for (const entry of pathValue.split(delimiter)) {
|
|
196
|
+
const candidate = resolve(currentWorkingDirectory, entry.length > 0 ? entry : ".", executable);
|
|
197
|
+
try {
|
|
198
|
+
if (!(await stat(candidate)).isFile()) continue;
|
|
199
|
+
await access(candidate, constants.X_OK);
|
|
200
|
+
return candidate;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (isNotFoundError(error) || isAccessError(error)) continue;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
async function statPath(path, missingMessage) {
|
|
209
|
+
try {
|
|
210
|
+
return await stat(path);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (isNotFoundError(error)) throw new CliRuntimeError(missingMessage);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function readRequiredValue(argv, index, flag) {
|
|
217
|
+
const value = argv[index + 1];
|
|
218
|
+
if (!value) throw new CliUsageError(`Missing value for ${flag}`);
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
function parsePort(value) {
|
|
222
|
+
if (!value) return 0;
|
|
223
|
+
return parsePositiveInteger(value, "--port");
|
|
224
|
+
}
|
|
225
|
+
function parsePositiveInteger(value, label) {
|
|
226
|
+
const parsed = Number(value);
|
|
227
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new CliUsageError(`Invalid ${label}: ${value}`);
|
|
228
|
+
return parsed;
|
|
229
|
+
}
|
|
230
|
+
function formatCliError(error) {
|
|
231
|
+
if (error instanceof CliUsageError) return `${error.message}\nTry 'aitty --help' for usage.\n`;
|
|
232
|
+
if (error instanceof Error) return `${error.message}\n`;
|
|
233
|
+
return `${String(error)}\n`;
|
|
234
|
+
}
|
|
235
|
+
function resolveCliExitCode(error) {
|
|
236
|
+
if (error instanceof CliUsageError || error instanceof CliRuntimeError) return error.exitCode;
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
function isAccessError(error) {
|
|
240
|
+
return error instanceof Error && "code" in error && error.code === "EACCES";
|
|
241
|
+
}
|
|
242
|
+
function isNotFoundError(error) {
|
|
243
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
244
|
+
}
|
|
245
|
+
function resolvePackageVersion(override) {
|
|
246
|
+
if (override) return override;
|
|
247
|
+
if (!cachedPackageVersion) {
|
|
248
|
+
const packageJson = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
249
|
+
cachedPackageVersion = JSON.parse(packageJson).version;
|
|
250
|
+
}
|
|
251
|
+
return cachedPackageVersion;
|
|
252
|
+
}
|
|
253
|
+
const entrypoint = process.argv[1];
|
|
254
|
+
if (entrypoint && isCliEntrypoint(entrypoint, import.meta.url)) runCli().then((exitCode) => {
|
|
255
|
+
process.exitCode = exitCode;
|
|
256
|
+
}).catch((error) => {
|
|
257
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
258
|
+
process.stderr.write(`${message}\n`);
|
|
259
|
+
process.exitCode = resolveCliExitCode(error);
|
|
260
|
+
});
|
|
261
|
+
function isCliEntrypoint(entrypoint, moduleUrl) {
|
|
262
|
+
try {
|
|
263
|
+
return realpathSync(entrypoint) === realpathSync(fileURLToPath(moduleUrl));
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
export { main, runCli };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aitty/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line launcher for aitty browser terminal sessions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"browser",
|
|
9
|
+
"cli",
|
|
10
|
+
"pty",
|
|
11
|
+
"terminal"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/kingsword09/aitty#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/kingsword09/aitty/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": "Kingsword kingsword09 <kingsword09@gmail.com>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kingsword09/aitty.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"aitty": "./dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/cli.js",
|
|
29
|
+
"types": "./dist/cli.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/cli.d.ts",
|
|
33
|
+
"default": "./dist/cli.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aitty/server": "workspace:*"
|
|
47
|
+
}
|
|
48
|
+
}
|