airterm 1.0.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 +1 -0
- package/dist/cli.js +504 -0
- package/package.json +30 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import meow from "meow";
|
|
6
|
+
|
|
7
|
+
// src/components/App.tsx
|
|
8
|
+
import { useState as useState5 } from "react";
|
|
9
|
+
|
|
10
|
+
// src/lib/config.ts
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, chmodSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var CONFIG_DIR = join(homedir(), ".airterm");
|
|
15
|
+
var CONFIG_FILE = join(CONFIG_DIR, "connections.json");
|
|
16
|
+
var KEYS_DIR = join(CONFIG_DIR, "keys");
|
|
17
|
+
function ensureDir() {
|
|
18
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
if (!existsSync(KEYS_DIR)) {
|
|
22
|
+
mkdirSync(KEYS_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function loadConfig() {
|
|
26
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
27
|
+
return { connections: [] };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
return { connections: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function saveConfig(config) {
|
|
37
|
+
ensureDir();
|
|
38
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
39
|
+
}
|
|
40
|
+
function getConnections() {
|
|
41
|
+
return loadConfig().connections;
|
|
42
|
+
}
|
|
43
|
+
function addConnection(conn) {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
config.connections = config.connections.filter((c) => c.id !== conn.id);
|
|
46
|
+
config.connections.push(conn);
|
|
47
|
+
if (config.connections.length === 1) {
|
|
48
|
+
config.defaultConnection = conn.id;
|
|
49
|
+
}
|
|
50
|
+
saveConfig(config);
|
|
51
|
+
}
|
|
52
|
+
function removeConnection(id) {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const conn = config.connections.find((c) => c.id === id);
|
|
55
|
+
if (conn) {
|
|
56
|
+
try {
|
|
57
|
+
if (existsSync(conn.keyPath)) {
|
|
58
|
+
rmSync(conn.keyPath);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
config.connections = config.connections.filter((c) => c.id !== id);
|
|
64
|
+
if (config.defaultConnection === id) {
|
|
65
|
+
config.defaultConnection = config.connections[0]?.id;
|
|
66
|
+
}
|
|
67
|
+
saveConfig(config);
|
|
68
|
+
}
|
|
69
|
+
function saveKey(machineId, keyData) {
|
|
70
|
+
ensureDir();
|
|
71
|
+
const keyPath = join(KEYS_DIR, `${machineId}.key`);
|
|
72
|
+
writeFileSync(keyPath, keyData, { mode: 384 });
|
|
73
|
+
chmodSync(keyPath, 384);
|
|
74
|
+
return keyPath;
|
|
75
|
+
}
|
|
76
|
+
function resetAll() {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
const count = config.connections.length;
|
|
79
|
+
if (existsSync(CONFIG_DIR)) {
|
|
80
|
+
rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
return count;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/components/Welcome.tsx
|
|
86
|
+
import { useState } from "react";
|
|
87
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
88
|
+
import TextInput from "ink-text-input";
|
|
89
|
+
|
|
90
|
+
// src/components/Header.tsx
|
|
91
|
+
import { Box, Text } from "ink";
|
|
92
|
+
import { jsx } from "react/jsx-runtime";
|
|
93
|
+
function Header() {
|
|
94
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx(Box, { borderStyle: "round", paddingX: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "AirTerm" }) }) });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/components/Welcome.tsx
|
|
98
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
99
|
+
function Welcome({ onSubmitCode }) {
|
|
100
|
+
const [code, setCode] = useState("");
|
|
101
|
+
return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
|
|
102
|
+
/* @__PURE__ */ jsx2(Header, {}),
|
|
103
|
+
/* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No saved connections." }) }),
|
|
104
|
+
/* @__PURE__ */ jsxs(Box2, { children: [
|
|
105
|
+
/* @__PURE__ */ jsx2(Text2, { children: "Enter your access code: " }),
|
|
106
|
+
/* @__PURE__ */ jsx2(
|
|
107
|
+
TextInput,
|
|
108
|
+
{
|
|
109
|
+
value: code,
|
|
110
|
+
onChange: setCode,
|
|
111
|
+
onSubmit: (value) => {
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (trimmed) {
|
|
114
|
+
onSubmitCode(trimmed);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
] }),
|
|
120
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
|
|
121
|
+
"Don't have one? Text your AirClaw:",
|
|
122
|
+
"\n",
|
|
123
|
+
"\u2192 sms:+14156058331&body=Give%20me%20terminal%20access"
|
|
124
|
+
] }) })
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/components/AddMachine.tsx
|
|
129
|
+
import { useState as useState2 } from "react";
|
|
130
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
131
|
+
import TextInput2 from "ink-text-input";
|
|
132
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
133
|
+
function AddMachine({ onSubmitCode }) {
|
|
134
|
+
const [code, setCode] = useState2("");
|
|
135
|
+
return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
|
|
136
|
+
/* @__PURE__ */ jsx3(Header, {}),
|
|
137
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx3(Text3, { children: "Add a new machine" }) }),
|
|
138
|
+
/* @__PURE__ */ jsxs2(Box3, { children: [
|
|
139
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Enter your access code: " }),
|
|
140
|
+
/* @__PURE__ */ jsx3(
|
|
141
|
+
TextInput2,
|
|
142
|
+
{
|
|
143
|
+
value: code,
|
|
144
|
+
onChange: setCode,
|
|
145
|
+
onSubmit: (value) => {
|
|
146
|
+
const trimmed = value.trim();
|
|
147
|
+
if (trimmed) {
|
|
148
|
+
onSubmitCode(trimmed);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
] })
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/components/SelectMachine.tsx
|
|
158
|
+
import { useState as useState3 } from "react";
|
|
159
|
+
import { Box as Box4, Text as Text4, useApp, useInput } from "ink";
|
|
160
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
161
|
+
function SelectMachine({
|
|
162
|
+
connections,
|
|
163
|
+
onSelect,
|
|
164
|
+
onAddNew,
|
|
165
|
+
onDelete
|
|
166
|
+
}) {
|
|
167
|
+
const [cursor, setCursor] = useState3(0);
|
|
168
|
+
const [confirmDelete, setConfirmDelete] = useState3(null);
|
|
169
|
+
const { exit } = useApp();
|
|
170
|
+
const totalItems = connections.length + 1;
|
|
171
|
+
useInput((input, key) => {
|
|
172
|
+
if (confirmDelete) {
|
|
173
|
+
if (input === "y" || input === "Y") {
|
|
174
|
+
const conn = connections.find((c) => c.id === confirmDelete);
|
|
175
|
+
if (conn) onDelete(conn);
|
|
176
|
+
setConfirmDelete(null);
|
|
177
|
+
} else {
|
|
178
|
+
setConfirmDelete(null);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key.upArrow) {
|
|
183
|
+
setCursor((prev) => prev > 0 ? prev - 1 : totalItems - 1);
|
|
184
|
+
} else if (key.downArrow) {
|
|
185
|
+
setCursor((prev) => prev < totalItems - 1 ? prev + 1 : 0);
|
|
186
|
+
} else if (key.return) {
|
|
187
|
+
if (cursor < connections.length) {
|
|
188
|
+
onSelect(connections[cursor]);
|
|
189
|
+
} else {
|
|
190
|
+
onAddNew();
|
|
191
|
+
}
|
|
192
|
+
} else if ((input === "d" || input === "x") && cursor < connections.length) {
|
|
193
|
+
setConfirmDelete(connections[cursor].id);
|
|
194
|
+
} else if (input === "q") {
|
|
195
|
+
exit();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (confirmDelete) {
|
|
199
|
+
const conn = connections.find((c) => c.id === confirmDelete);
|
|
200
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
|
|
201
|
+
/* @__PURE__ */ jsx4(Header, {}),
|
|
202
|
+
/* @__PURE__ */ jsxs3(Text4, { children: [
|
|
203
|
+
"Delete \u201C",
|
|
204
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: conn?.name }),
|
|
205
|
+
"\u201D? (y/n)"
|
|
206
|
+
] })
|
|
207
|
+
] });
|
|
208
|
+
}
|
|
209
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
|
|
210
|
+
/* @__PURE__ */ jsx4(Header, {}),
|
|
211
|
+
/* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { children: "Select a machine:" }) }),
|
|
212
|
+
connections.map((conn, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs3(Text4, { color: cursor === i ? "cyan" : void 0, children: [
|
|
213
|
+
cursor === i ? "\u203A " : " ",
|
|
214
|
+
/* @__PURE__ */ jsx4(Text4, { bold: cursor === i, children: conn.name }),
|
|
215
|
+
/* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
216
|
+
" (",
|
|
217
|
+
conn.id,
|
|
218
|
+
")"
|
|
219
|
+
] }),
|
|
220
|
+
/* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
221
|
+
" ",
|
|
222
|
+
conn.hostname
|
|
223
|
+
] })
|
|
224
|
+
] }) }, conn.id)),
|
|
225
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 0, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }),
|
|
226
|
+
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs3(Text4, { color: cursor === connections.length ? "cyan" : void 0, children: [
|
|
227
|
+
cursor === connections.length ? "\u203A " : " ",
|
|
228
|
+
/* @__PURE__ */ jsx4(Text4, { bold: cursor === connections.length, children: "+ Add new machine" })
|
|
229
|
+
] }) }),
|
|
230
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Enter: connect \xB7 d: delete \xB7 q: quit" }) })
|
|
231
|
+
] });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/components/Connecting.tsx
|
|
235
|
+
import { useEffect, useState as useState4 } from "react";
|
|
236
|
+
import { Box as Box5, Text as Text5, useApp as useApp2 } from "ink";
|
|
237
|
+
import Spinner from "ink-spinner";
|
|
238
|
+
|
|
239
|
+
// src/lib/api.ts
|
|
240
|
+
var API_BASE = process.env.AIRCLAW_API_URL || "https://app.airclaw.com";
|
|
241
|
+
async function redeemCode(code) {
|
|
242
|
+
const res = await fetch(`${API_BASE}/api/airterm/redeem`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { "Content-Type": "application/json" },
|
|
245
|
+
body: JSON.stringify({ code })
|
|
246
|
+
});
|
|
247
|
+
const data = await res.json();
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
return { error: data.error || `HTTP ${res.status}` };
|
|
250
|
+
}
|
|
251
|
+
return data;
|
|
252
|
+
}
|
|
253
|
+
function isRedeemError(result) {
|
|
254
|
+
return "error" in result;
|
|
255
|
+
}
|
|
256
|
+
async function downloadKey(url) {
|
|
257
|
+
const res = await fetch(url);
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
throw new Error(`Failed to download key: HTTP ${res.status}`);
|
|
260
|
+
}
|
|
261
|
+
return await res.text();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/lib/ssh.ts
|
|
265
|
+
import { spawnSync } from "child_process";
|
|
266
|
+
function connectSSH(conn) {
|
|
267
|
+
const result = spawnSync(
|
|
268
|
+
"ssh",
|
|
269
|
+
[
|
|
270
|
+
"-i",
|
|
271
|
+
conn.keyPath,
|
|
272
|
+
"-p",
|
|
273
|
+
String(conn.port),
|
|
274
|
+
"-o",
|
|
275
|
+
"StrictHostKeyChecking=accept-new",
|
|
276
|
+
"-o",
|
|
277
|
+
"UserKnownHostsFile=~/.airterm/known_hosts",
|
|
278
|
+
`${conn.username}@${conn.hostname}`
|
|
279
|
+
],
|
|
280
|
+
{ stdio: "inherit" }
|
|
281
|
+
);
|
|
282
|
+
return result.status ?? 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/components/Connecting.tsx
|
|
286
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
287
|
+
function Connecting({ code, connection, onError }) {
|
|
288
|
+
const [status, setStatus] = useState4(
|
|
289
|
+
code ? "Redeeming access code..." : "Connecting..."
|
|
290
|
+
);
|
|
291
|
+
const { exit } = useApp2();
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
let cancelled = false;
|
|
294
|
+
async function run() {
|
|
295
|
+
let conn = connection;
|
|
296
|
+
if (code && !conn) {
|
|
297
|
+
const result = await redeemCode(code);
|
|
298
|
+
if (cancelled) return;
|
|
299
|
+
if (isRedeemError(result)) {
|
|
300
|
+
onError(result.error);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
setStatus("Downloading SSH key...");
|
|
304
|
+
let keyData;
|
|
305
|
+
try {
|
|
306
|
+
keyData = await downloadKey(result.keyUrl);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (cancelled) return;
|
|
309
|
+
onError(`Failed to download SSH key: ${err}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (cancelled) return;
|
|
313
|
+
const keyPath = saveKey(result.machineId, keyData);
|
|
314
|
+
conn = {
|
|
315
|
+
id: result.machineId,
|
|
316
|
+
name: result.machineName,
|
|
317
|
+
hostname: result.hostname,
|
|
318
|
+
port: result.port,
|
|
319
|
+
username: result.username,
|
|
320
|
+
keyPath,
|
|
321
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
322
|
+
};
|
|
323
|
+
addConnection(conn);
|
|
324
|
+
setStatus(`Connecting to ${conn.name}...`);
|
|
325
|
+
}
|
|
326
|
+
if (!conn) {
|
|
327
|
+
onError("No connection specified");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
331
|
+
if (cancelled) return;
|
|
332
|
+
exit();
|
|
333
|
+
const exitCode = connectSSH(conn);
|
|
334
|
+
process.exit(exitCode);
|
|
335
|
+
}
|
|
336
|
+
run().catch((err) => {
|
|
337
|
+
if (!cancelled) {
|
|
338
|
+
onError(String(err));
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return () => {
|
|
342
|
+
cancelled = true;
|
|
343
|
+
};
|
|
344
|
+
}, [code, connection, exit, onError]);
|
|
345
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
346
|
+
/* @__PURE__ */ jsx5(Header, {}),
|
|
347
|
+
/* @__PURE__ */ jsxs4(Box5, { children: [
|
|
348
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: /* @__PURE__ */ jsx5(Spinner, { type: "dots" }) }),
|
|
349
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
350
|
+
" ",
|
|
351
|
+
status
|
|
352
|
+
] })
|
|
353
|
+
] })
|
|
354
|
+
] });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/components/ErrorView.tsx
|
|
358
|
+
import React5 from "react";
|
|
359
|
+
import { Box as Box6, Text as Text6, useApp as useApp3 } from "ink";
|
|
360
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
361
|
+
function ErrorView({ message }) {
|
|
362
|
+
const { exit } = useApp3();
|
|
363
|
+
React5.useEffect(() => {
|
|
364
|
+
const timer = setTimeout(() => exit(), 100);
|
|
365
|
+
return () => clearTimeout(timer);
|
|
366
|
+
}, [exit]);
|
|
367
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
368
|
+
/* @__PURE__ */ jsx6(Header, {}),
|
|
369
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs5(Text6, { color: "red", children: [
|
|
370
|
+
"Error: ",
|
|
371
|
+
message
|
|
372
|
+
] }) })
|
|
373
|
+
] });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/components/App.tsx
|
|
377
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
378
|
+
function App({ initialCode: initialCode2, initialScreen: initialScreen2 }) {
|
|
379
|
+
const connections = getConnections();
|
|
380
|
+
let startScreen;
|
|
381
|
+
if (initialCode2) {
|
|
382
|
+
startScreen = { type: "connecting", code: initialCode2 };
|
|
383
|
+
} else if (initialScreen2 === "add") {
|
|
384
|
+
startScreen = { type: "add" };
|
|
385
|
+
} else if (initialScreen2 === "list") {
|
|
386
|
+
startScreen = { type: "select" };
|
|
387
|
+
} else if (connections.length === 0) {
|
|
388
|
+
startScreen = { type: "welcome" };
|
|
389
|
+
} else if (connections.length === 1) {
|
|
390
|
+
startScreen = { type: "connecting", connection: connections[0] };
|
|
391
|
+
} else {
|
|
392
|
+
startScreen = { type: "select" };
|
|
393
|
+
}
|
|
394
|
+
const [screen, setScreen] = useState5(startScreen);
|
|
395
|
+
const handleCode = (code) => {
|
|
396
|
+
setScreen({ type: "connecting", code });
|
|
397
|
+
};
|
|
398
|
+
const handleError = (message) => {
|
|
399
|
+
setScreen({ type: "error", message });
|
|
400
|
+
};
|
|
401
|
+
const handleDelete = (conn) => {
|
|
402
|
+
removeConnection(conn.id);
|
|
403
|
+
const remaining = getConnections();
|
|
404
|
+
if (remaining.length === 0) {
|
|
405
|
+
setScreen({ type: "welcome" });
|
|
406
|
+
} else {
|
|
407
|
+
setScreen({ type: "select" });
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
switch (screen.type) {
|
|
411
|
+
case "welcome":
|
|
412
|
+
return /* @__PURE__ */ jsx7(Welcome, { onSubmitCode: handleCode });
|
|
413
|
+
case "add":
|
|
414
|
+
return /* @__PURE__ */ jsx7(AddMachine, { onSubmitCode: handleCode });
|
|
415
|
+
case "select":
|
|
416
|
+
return /* @__PURE__ */ jsx7(
|
|
417
|
+
SelectMachine,
|
|
418
|
+
{
|
|
419
|
+
connections: getConnections(),
|
|
420
|
+
onSelect: (conn) => setScreen({ type: "connecting", connection: conn }),
|
|
421
|
+
onAddNew: () => setScreen({ type: "add" }),
|
|
422
|
+
onDelete: handleDelete
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
case "connecting":
|
|
426
|
+
return /* @__PURE__ */ jsx7(
|
|
427
|
+
Connecting,
|
|
428
|
+
{
|
|
429
|
+
code: screen.code,
|
|
430
|
+
connection: screen.connection,
|
|
431
|
+
onError: handleError
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
case "error":
|
|
435
|
+
return /* @__PURE__ */ jsx7(ErrorView, { message: screen.message });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/cli.tsx
|
|
440
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
441
|
+
var cli = meow(
|
|
442
|
+
`
|
|
443
|
+
AirTerm \u2014 SSH into your AirClaw machine
|
|
444
|
+
|
|
445
|
+
Usage:
|
|
446
|
+
airterm Connect to your machine
|
|
447
|
+
airterm <code> Redeem an access code and connect
|
|
448
|
+
airterm add [code] Add a machine with an access code
|
|
449
|
+
airterm list Manage saved connections
|
|
450
|
+
airterm reset Remove all saved connections and keys
|
|
451
|
+
|
|
452
|
+
Options:
|
|
453
|
+
-h, --help Show this help
|
|
454
|
+
-v, --version Show version
|
|
455
|
+
|
|
456
|
+
First time? Run \`airterm\` and paste the access code from your AirClaw agent.
|
|
457
|
+
`,
|
|
458
|
+
{
|
|
459
|
+
importMeta: import.meta,
|
|
460
|
+
flags: {}
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
var command = cli.input[0];
|
|
464
|
+
if (command === "reset") {
|
|
465
|
+
const count = resetAll();
|
|
466
|
+
if (count > 0) {
|
|
467
|
+
console.log(
|
|
468
|
+
`Removed ${count} saved connection${count !== 1 ? "s" : ""} and keys.`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
console.log(
|
|
472
|
+
"AirTerm data wiped. Run `airterm add` to set up again."
|
|
473
|
+
);
|
|
474
|
+
process.exit(0);
|
|
475
|
+
}
|
|
476
|
+
if (command === "help") {
|
|
477
|
+
cli.showHelp(0);
|
|
478
|
+
}
|
|
479
|
+
var initialCode;
|
|
480
|
+
var initialScreen;
|
|
481
|
+
if (command === "add") {
|
|
482
|
+
const code = cli.input[1];
|
|
483
|
+
if (code) {
|
|
484
|
+
initialCode = code;
|
|
485
|
+
} else {
|
|
486
|
+
initialScreen = "add";
|
|
487
|
+
}
|
|
488
|
+
} else if (command === "list") {
|
|
489
|
+
const connections = getConnections();
|
|
490
|
+
if (connections.length === 0) {
|
|
491
|
+
console.log("No saved connections. Run `airterm add` to set up.");
|
|
492
|
+
process.exit(0);
|
|
493
|
+
}
|
|
494
|
+
initialScreen = "list";
|
|
495
|
+
} else if (command && !["add", "list", "help", "reset"].includes(command)) {
|
|
496
|
+
if (/^[A-Za-z0-9_-]{10,}$/.test(command)) {
|
|
497
|
+
initialCode = command;
|
|
498
|
+
} else {
|
|
499
|
+
console.error(`Unknown command: ${command}
|
|
500
|
+
`);
|
|
501
|
+
cli.showHelp(1);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
render(/* @__PURE__ */ jsx8(App, { initialCode, initialScreen }));
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "airterm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SSH into your AirClaw machine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"airterm": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/cli.tsx --format esm --dts --outDir dist",
|
|
14
|
+
"dev": "tsup src/cli.tsx --format esm --watch --outDir dist"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"ink": "^5.1.0",
|
|
18
|
+
"ink-select-input": "^6.0.0",
|
|
19
|
+
"ink-spinner": "^5.0.0",
|
|
20
|
+
"ink-text-input": "^6.0.0",
|
|
21
|
+
"meow": "^13.0.0",
|
|
22
|
+
"react": "^18.3.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"@types/react": "^18.3.0",
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
}
|
|
30
|
+
}
|