@instafy/cli 0.1.0-staging.138
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 +66 -0
- package/bin/instafy.js +5 -0
- package/dist/index.js +247 -0
- package/dist/org.js +46 -0
- package/dist/project.js +184 -0
- package/dist/rathole.js +212 -0
- package/dist/runtime.js +709 -0
- package/dist/tunnel.js +180 -0
- package/package.json +34 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import kleur from "kleur";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { ensureRatholeBinary } from "./rathole.js";
|
|
9
|
+
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
10
|
+
const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
|
|
11
|
+
const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
|
|
12
|
+
function resolveRepoRoot() {
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
return path.resolve(__dirname, "../../..");
|
|
15
|
+
}
|
|
16
|
+
function resolveRuntimeBinary() {
|
|
17
|
+
// Reuse existing runtime-agent binary from workspace target (assumes `cargo build` done).
|
|
18
|
+
const repoRoot = resolveRepoRoot();
|
|
19
|
+
const candidates = [
|
|
20
|
+
path.join(repoRoot, "target", "debug", "runtime-agent"),
|
|
21
|
+
path.join(repoRoot, "target", "release", "runtime-agent"),
|
|
22
|
+
path.join(repoRoot, "packages", "runtime-agent", "target", "debug", "runtime-agent"),
|
|
23
|
+
path.join(repoRoot, "packages", "runtime-agent", "target", "release", "runtime-agent"),
|
|
24
|
+
];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (existsSync(candidate)) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error("runtime-agent binary not found. Build it with `cargo build -p runtime-agent`.");
|
|
31
|
+
}
|
|
32
|
+
function printStatus(label, value) {
|
|
33
|
+
console.log(`${kleur.cyan(label)} ${value}`);
|
|
34
|
+
}
|
|
35
|
+
function ensureStateDir() {
|
|
36
|
+
mkdirSync(INSTAFY_DIR, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
function ensureLogDir() {
|
|
39
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
function writeState(state) {
|
|
42
|
+
ensureStateDir();
|
|
43
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
44
|
+
}
|
|
45
|
+
function readState() {
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(STATE_FILE, "utf8");
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function clearState() {
|
|
55
|
+
try {
|
|
56
|
+
rmSync(STATE_FILE);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function normalizeToken(value) {
|
|
63
|
+
if (typeof value !== "string") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
68
|
+
}
|
|
69
|
+
function setSharedFsEnv(env, opts) {
|
|
70
|
+
if (!opts)
|
|
71
|
+
return;
|
|
72
|
+
if (opts.host)
|
|
73
|
+
env["SHARED_FS_HOST"] = opts.host;
|
|
74
|
+
if (opts.port)
|
|
75
|
+
env["SHARED_FS_PORT"] = String(opts.port);
|
|
76
|
+
if (opts.user)
|
|
77
|
+
env["SHARED_FS_USER"] = opts.user;
|
|
78
|
+
if (opts.path)
|
|
79
|
+
env["SHARED_FS_PATH"] = opts.path;
|
|
80
|
+
if (opts.keyB64)
|
|
81
|
+
env["SHARED_FS_KEY_B64"] = opts.keyB64;
|
|
82
|
+
if (opts.keyPath)
|
|
83
|
+
env["SHARED_FS_KEY_PATH"] = opts.keyPath;
|
|
84
|
+
if (opts.options)
|
|
85
|
+
env["SHARED_FS_OPTIONS"] = opts.options;
|
|
86
|
+
if (opts.flat)
|
|
87
|
+
env["RUNTIME_SHARED_FS_FLAT"] = "1";
|
|
88
|
+
env["SHARED_FS_PROTOCOL"] = env["SHARED_FS_PROTOCOL"] ?? "sshfs";
|
|
89
|
+
}
|
|
90
|
+
async function fetchWorkspaceMountToken(params) {
|
|
91
|
+
const base = params.controllerUrl.replace(/\/+$/, "");
|
|
92
|
+
const url = `${base}/projects/${encodeURIComponent(params.projectId)}/workspace/mount-token`;
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
headers: {
|
|
95
|
+
authorization: `Bearer ${params.controllerAccessToken}`,
|
|
96
|
+
accept: "application/json",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const text = await res.text().catch(() => "");
|
|
101
|
+
throw new Error(`Failed to fetch workspace mount token (${res.status} ${res.statusText}): ${text}`);
|
|
102
|
+
}
|
|
103
|
+
const json = (await res.json());
|
|
104
|
+
if (!json?.protocol || !json.host || !json.path) {
|
|
105
|
+
throw new Error("Workspace mount token response missing required fields.");
|
|
106
|
+
}
|
|
107
|
+
return json;
|
|
108
|
+
}
|
|
109
|
+
function prepareTempKey(keyB64) {
|
|
110
|
+
if (!keyB64)
|
|
111
|
+
return null;
|
|
112
|
+
const buf = Buffer.from(keyB64.trim(), "base64");
|
|
113
|
+
const tmpDir = path.join(os.tmpdir(), "instafy-shared-fs-cli");
|
|
114
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
115
|
+
const file = path.join(tmpDir, `sshfs-key-${Date.now()}.pem`);
|
|
116
|
+
writeFileSync(file, buf, { mode: 0o600 });
|
|
117
|
+
return file;
|
|
118
|
+
}
|
|
119
|
+
export async function mountWorkspace(options) {
|
|
120
|
+
const mountDir = path.resolve(options.mountPath);
|
|
121
|
+
mkdirSync(mountDir, { recursive: true });
|
|
122
|
+
const token = await fetchWorkspaceMountToken({
|
|
123
|
+
controllerUrl: options.controllerUrl,
|
|
124
|
+
controllerAccessToken: options.controllerAccessToken,
|
|
125
|
+
projectId: options.project,
|
|
126
|
+
});
|
|
127
|
+
if (token.protocol !== "sshfs") {
|
|
128
|
+
throw new Error(`Unsupported mount protocol: ${token.protocol}`);
|
|
129
|
+
}
|
|
130
|
+
const sshfsBin = options.sshfsBin || process.env.SHARED_FS_BIN || "sshfs";
|
|
131
|
+
assertSshfsAvailable(sshfsBin);
|
|
132
|
+
const keyPath = prepareTempKey(token.key_b64 ?? null);
|
|
133
|
+
const args = [
|
|
134
|
+
`${token.user}@${token.host}:${token.path}`,
|
|
135
|
+
mountDir,
|
|
136
|
+
"-p",
|
|
137
|
+
String(token.port ?? 22),
|
|
138
|
+
"-o",
|
|
139
|
+
"StrictHostKeyChecking=no",
|
|
140
|
+
];
|
|
141
|
+
const mergedOpts = token.options || options.extraOptions;
|
|
142
|
+
if (keyPath) {
|
|
143
|
+
args.push("-o", `IdentityFile=${keyPath}`);
|
|
144
|
+
}
|
|
145
|
+
if (mergedOpts && mergedOpts.trim()) {
|
|
146
|
+
args.push("-o", mergedOpts.trim());
|
|
147
|
+
}
|
|
148
|
+
const result = spawnSync(sshfsBin, args, {
|
|
149
|
+
stdio: "inherit",
|
|
150
|
+
env: process.env,
|
|
151
|
+
});
|
|
152
|
+
if (keyPath) {
|
|
153
|
+
try {
|
|
154
|
+
rmSync(keyPath);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (result.status !== 0) {
|
|
161
|
+
throw new Error(`sshfs mount failed (code ${result.status ?? result.error?.message ?? "unknown"})\n` +
|
|
162
|
+
`Command: ${sshfsBin} ${args.join(" ")}`);
|
|
163
|
+
}
|
|
164
|
+
validateMountHealthy(mountDir);
|
|
165
|
+
return mountDir;
|
|
166
|
+
}
|
|
167
|
+
export async function checkSshfs(binOverride) {
|
|
168
|
+
const bin = binOverride || process.env.SHARED_FS_BIN || "sshfs";
|
|
169
|
+
assertSshfsAvailable(bin);
|
|
170
|
+
}
|
|
171
|
+
function assertSshfsAvailable(bin) {
|
|
172
|
+
const check = spawnSync(bin, ["-V"], { stdio: "ignore" });
|
|
173
|
+
if ((check.status ?? check["code"] ?? 1) === 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const platform = process.platform;
|
|
177
|
+
const hints = {
|
|
178
|
+
darwin: "brew install --cask macfuse && brew install gromgit/fuse/sshfs-mac",
|
|
179
|
+
linux: "sudo apt-get update && sudo apt-get install -y sshfs",
|
|
180
|
+
win32: "Install WinFsp and SSHFS-Win: https://github.com/billziss-gh/sshfs-win",
|
|
181
|
+
};
|
|
182
|
+
const hint = hints[platform] ??
|
|
183
|
+
"Install sshfs for your platform and ensure it is on PATH (or pass --sshfs-bin).";
|
|
184
|
+
throw new Error(`sshfs binary '${bin}' not found or not executable. ${hint}`);
|
|
185
|
+
}
|
|
186
|
+
function validateMountHealthy(mountDir) {
|
|
187
|
+
try {
|
|
188
|
+
// Best-effort sanity check to catch "Transport endpoint not connected" right after mount.
|
|
189
|
+
const entries = spawnSync("ls", ["-la", mountDir], { stdio: "ignore" });
|
|
190
|
+
if ((entries.status ?? entries["code"] ?? 1) !== 0) {
|
|
191
|
+
throw new Error("ls failed");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
throw new Error(`sshfs mount appears unhealthy at ${mountDir}: ${message}. ` +
|
|
197
|
+
"Ensure the mount host is reachable and sshfs/fuse is enabled.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function findProjectManifest(startDir) {
|
|
201
|
+
let current = path.resolve(startDir);
|
|
202
|
+
const root = path.parse(current).root;
|
|
203
|
+
while (true) {
|
|
204
|
+
const candidate = path.join(current, ".instafy", "project.json");
|
|
205
|
+
if (existsSync(candidate)) {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
208
|
+
if (parsed?.projectId) {
|
|
209
|
+
return { manifest: parsed, path: candidate };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore malformed manifest
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (current === root)
|
|
217
|
+
break;
|
|
218
|
+
current = path.dirname(current);
|
|
219
|
+
}
|
|
220
|
+
return { manifest: null, path: null };
|
|
221
|
+
}
|
|
222
|
+
function readTokenFromFile(filePath) {
|
|
223
|
+
const normalized = normalizeToken(filePath);
|
|
224
|
+
if (!normalized) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const resolved = path.resolve(normalized);
|
|
228
|
+
const contents = readFileSync(resolved, "utf8").trim();
|
|
229
|
+
if (!contents) {
|
|
230
|
+
throw new Error(`token file ${resolved} was empty`);
|
|
231
|
+
}
|
|
232
|
+
return contents;
|
|
233
|
+
}
|
|
234
|
+
function resolveSupabaseAccessToken(options, env) {
|
|
235
|
+
const explicit = normalizeToken(options.supabaseAccessToken);
|
|
236
|
+
if (explicit) {
|
|
237
|
+
return explicit;
|
|
238
|
+
}
|
|
239
|
+
const fromFile = readTokenFromFile(options.supabaseAccessTokenFile);
|
|
240
|
+
if (fromFile) {
|
|
241
|
+
return fromFile;
|
|
242
|
+
}
|
|
243
|
+
const fromEnv = normalizeToken(env["SUPABASE_ACCESS_TOKEN"]);
|
|
244
|
+
if (fromEnv) {
|
|
245
|
+
return fromEnv;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function findRatholeOnPath() {
|
|
250
|
+
const names = process.platform === "win32" ? ["rathole.exe", "rathole"] : ["rathole"];
|
|
251
|
+
const pathEntries = (process.env.PATH ?? "")
|
|
252
|
+
.split(path.delimiter)
|
|
253
|
+
.map((entry) => entry.trim())
|
|
254
|
+
.filter(Boolean);
|
|
255
|
+
for (const entry of pathEntries) {
|
|
256
|
+
for (const name of names) {
|
|
257
|
+
const candidate = path.join(entry, name);
|
|
258
|
+
if (existsSync(candidate)) {
|
|
259
|
+
return candidate;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
export async function resolveRatholeBinaryForCli(options) {
|
|
266
|
+
const warn = options.warn ??
|
|
267
|
+
((message) => {
|
|
268
|
+
console.warn(kleur.yellow(message));
|
|
269
|
+
});
|
|
270
|
+
const logger = options.logger ??
|
|
271
|
+
((message) => {
|
|
272
|
+
console.log(kleur.cyan(`[rathole] ${message}`));
|
|
273
|
+
});
|
|
274
|
+
const existing = normalizeToken(options.env["RATHOLE_BIN"]);
|
|
275
|
+
if (existing) {
|
|
276
|
+
options.env["RATHOLE_BIN"] = existing;
|
|
277
|
+
return existing;
|
|
278
|
+
}
|
|
279
|
+
const finder = options.findBinary ?? findRatholeOnPath;
|
|
280
|
+
const detected = finder();
|
|
281
|
+
if (detected) {
|
|
282
|
+
options.env["RATHOLE_BIN"] = detected;
|
|
283
|
+
return detected;
|
|
284
|
+
}
|
|
285
|
+
const download = options.downloadBinary ??
|
|
286
|
+
((params) => ensureRatholeBinary(params));
|
|
287
|
+
try {
|
|
288
|
+
const resolved = await download({
|
|
289
|
+
version: normalizeToken(options.version ?? undefined) ?? undefined,
|
|
290
|
+
cacheDir: normalizeToken(options.cacheDir ?? undefined) ?? undefined,
|
|
291
|
+
logger,
|
|
292
|
+
});
|
|
293
|
+
options.env["RATHOLE_BIN"] = resolved;
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const suffix = error instanceof Error
|
|
298
|
+
? error.message
|
|
299
|
+
: typeof error === "string"
|
|
300
|
+
? error
|
|
301
|
+
: JSON.stringify(error);
|
|
302
|
+
const osHint = process.platform === "darwin"
|
|
303
|
+
? "Install with cargo (`cargo install --locked rathole`) or set RATHOLE_BIN to a downloaded binary."
|
|
304
|
+
: process.platform === "linux"
|
|
305
|
+
? "Download from https://github.com/rapiz1/rathole/releases (matching your arch) or build via `cargo install --locked rathole`."
|
|
306
|
+
: "Download a rathole binary for your OS/arch and set RATHOLE_BIN.";
|
|
307
|
+
warn(`rathole unavailable (set RATHOLE_BIN or install on PATH). ${osHint} ${suffix}`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export async function mintRuntimeAccessToken(params) {
|
|
312
|
+
const url = params.controllerUrl.replace(/\/$/, "");
|
|
313
|
+
const target = `${url}/projects/${encodeURIComponent(params.projectId)}/runtime/token`;
|
|
314
|
+
const response = await fetch(target, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
authorization: `Bearer ${params.controllerAccessToken}`,
|
|
318
|
+
"content-type": "application/json",
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
runtimeId: params.runtimeId,
|
|
322
|
+
scopes: params.scopes,
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const text = await response.text().catch(() => "");
|
|
327
|
+
throw new Error(`Controller rejected runtime token request (${response.status} ${response.statusText}): ${text}`);
|
|
328
|
+
}
|
|
329
|
+
const payload = (await response.json());
|
|
330
|
+
const token = typeof payload.token === "string" ? payload.token.trim() : "";
|
|
331
|
+
if (!token) {
|
|
332
|
+
throw new Error("Controller response missing token field while minting runtime token.");
|
|
333
|
+
}
|
|
334
|
+
return token;
|
|
335
|
+
}
|
|
336
|
+
function isProcessAlive(pid) {
|
|
337
|
+
try {
|
|
338
|
+
process.kill(pid, 0);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
export async function runtimeStart(options) {
|
|
346
|
+
const bin = resolveRuntimeBinary();
|
|
347
|
+
const env = { ...process.env };
|
|
348
|
+
const existing = readState();
|
|
349
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
350
|
+
throw new Error(`Runtime already running (pid ${existing.pid}) for project ${existing.projectId}. Stop it first.`);
|
|
351
|
+
}
|
|
352
|
+
const manifestInfo = findProjectManifest(options.workspace ?? process.cwd());
|
|
353
|
+
const projectId = options.project ?? env["PROJECT_ID"] ?? manifestInfo.manifest?.projectId ?? null;
|
|
354
|
+
if (!projectId) {
|
|
355
|
+
throw new Error("Project ID is required (--project or PROJECT_ID)");
|
|
356
|
+
}
|
|
357
|
+
env["PROJECT_ID"] = projectId;
|
|
358
|
+
const supabaseAccessToken = resolveSupabaseAccessToken(options, env);
|
|
359
|
+
if (supabaseAccessToken) {
|
|
360
|
+
env["SUPABASE_ACCESS_TOKEN"] = supabaseAccessToken;
|
|
361
|
+
}
|
|
362
|
+
let controllerAccessToken = normalizeToken(options.controllerAccessToken) ??
|
|
363
|
+
normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
364
|
+
supabaseAccessToken;
|
|
365
|
+
let runtimeAccessToken = normalizeToken(options.runtimeToken) ?? normalizeToken(env["RUNTIME_ACCESS_TOKEN"]);
|
|
366
|
+
const agentKey = options.controllerToken ?? env["AGENT_LOGIN_KEY"] ?? env["AGENT_KEY"];
|
|
367
|
+
if (!agentKey && !controllerAccessToken && !runtimeAccessToken) {
|
|
368
|
+
throw new Error("Provide either --runtime-token/RUNTIME_ACCESS_TOKEN, --controller-access-token/CONTROLLER_ACCESS_TOKEN, --supabase-access-token/SUPABASE_ACCESS_TOKEN, or --controller-token/AGENT_LOGIN_KEY (legacy)");
|
|
369
|
+
}
|
|
370
|
+
if (agentKey) {
|
|
371
|
+
env["AGENT_LOGIN_KEY"] = agentKey;
|
|
372
|
+
}
|
|
373
|
+
if (controllerAccessToken) {
|
|
374
|
+
env["CONTROLLER_ACCESS_TOKEN"] = controllerAccessToken;
|
|
375
|
+
}
|
|
376
|
+
env["CONTROLLER_BASE_URL"] =
|
|
377
|
+
options.controllerUrl ?? env["CONTROLLER_BASE_URL"] ?? "http://127.0.0.1:8788";
|
|
378
|
+
if (!runtimeAccessToken && controllerAccessToken) {
|
|
379
|
+
runtimeAccessToken = await mintRuntimeAccessToken({
|
|
380
|
+
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
381
|
+
controllerAccessToken,
|
|
382
|
+
projectId,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (runtimeAccessToken) {
|
|
386
|
+
env["RUNTIME_ACCESS_TOKEN"] = runtimeAccessToken;
|
|
387
|
+
}
|
|
388
|
+
// Auto-fetch workspace mount token if requested
|
|
389
|
+
if (options.mountControllerFs && controllerAccessToken && env["CONTROLLER_BASE_URL"]) {
|
|
390
|
+
const token = await fetchWorkspaceMountToken({
|
|
391
|
+
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
392
|
+
controllerAccessToken,
|
|
393
|
+
projectId,
|
|
394
|
+
});
|
|
395
|
+
setSharedFsEnv(env, {
|
|
396
|
+
host: token.host,
|
|
397
|
+
port: token.port,
|
|
398
|
+
user: token.user,
|
|
399
|
+
path: token.path,
|
|
400
|
+
keyB64: token.key_b64 ?? undefined,
|
|
401
|
+
options: token.options ?? undefined,
|
|
402
|
+
flat: true,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// Shared FS mount options (sshfs) from flags/env
|
|
407
|
+
setSharedFsEnv(env, options.sharedFs);
|
|
408
|
+
}
|
|
409
|
+
if (options.codexBin)
|
|
410
|
+
env["CODEX_BIN"] = options.codexBin;
|
|
411
|
+
if (options.proxyBaseUrl)
|
|
412
|
+
env["PROXY_BASE_URL"] = options.proxyBaseUrl;
|
|
413
|
+
if (!env["CODEX_REASONING_EFFORT"]) {
|
|
414
|
+
env["CODEX_REASONING_EFFORT"] = "low";
|
|
415
|
+
}
|
|
416
|
+
const workspace = path.resolve(options.workspace ?? env["WORKSPACE_DIR"] ?? path.join(process.cwd(), ".instafy", "workspace"));
|
|
417
|
+
mkdirSync(workspace, { recursive: true });
|
|
418
|
+
env["WORKSPACE_DIR"] = workspace;
|
|
419
|
+
const originId = options.originId ?? env["ORIGIN_ID"] ?? randomUUID();
|
|
420
|
+
env["ORIGIN_ID"] = originId;
|
|
421
|
+
env["ORIGIN_BIND_HOST"] = options.bindHost ?? env["ORIGIN_BIND_HOST"] ?? "127.0.0.1";
|
|
422
|
+
env["ORIGIN_BIND_PORT"] = String(options.bindPort ?? env["ORIGIN_BIND_PORT"] ?? 54332);
|
|
423
|
+
env["ORIGIN_PROTOCOLS"] = env["ORIGIN_PROTOCOLS"] ?? "http";
|
|
424
|
+
if (options.originEndpoint && options.originEndpoint.trim()) {
|
|
425
|
+
env["ORIGIN_ENDPOINT"] = options.originEndpoint.trim();
|
|
426
|
+
}
|
|
427
|
+
let originToken = normalizeToken(options.originToken) ?? normalizeToken(env["ORIGIN_INTERNAL_TOKEN"]);
|
|
428
|
+
if (!originToken && runtimeAccessToken) {
|
|
429
|
+
originToken = runtimeAccessToken;
|
|
430
|
+
}
|
|
431
|
+
if (!originToken && controllerAccessToken) {
|
|
432
|
+
originToken = await mintRuntimeAccessToken({
|
|
433
|
+
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
434
|
+
controllerAccessToken,
|
|
435
|
+
projectId,
|
|
436
|
+
runtimeId: env["RUNTIME_ID"],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (!originToken) {
|
|
440
|
+
throw new Error("Runtime/origin token is required (--origin-token or ORIGIN_INTERNAL_TOKEN), or provide --runtime-token/--controller-access-token to mint/use one");
|
|
441
|
+
}
|
|
442
|
+
env["ORIGIN_INTERNAL_TOKEN"] = originToken;
|
|
443
|
+
env["ORIGIN_SKIP_AUTH"] = env["ORIGIN_SKIP_AUTH"] ?? "1";
|
|
444
|
+
// Shared FS mount options (sshfs)
|
|
445
|
+
setSharedFsEnv(env, options.sharedFs);
|
|
446
|
+
env["RUNTIME_DISPLAY_NAME"] =
|
|
447
|
+
options.displayName ?? env["RUNTIME_DISPLAY_NAME"] ?? "Instafy CLI Runtime";
|
|
448
|
+
const manifestProvider = manifestInfo.manifest?.orgName?.trim() ||
|
|
449
|
+
manifestInfo.manifest?.orgId?.toString().trim();
|
|
450
|
+
env["RUNTIME_PROVIDER"] =
|
|
451
|
+
options.provider ?? env["RUNTIME_PROVIDER"] ?? manifestProvider ?? "self-hosted";
|
|
452
|
+
env["RUNTIME_VERSION"] = env["RUNTIME_VERSION"] ?? "0.1.0";
|
|
453
|
+
env["RUNTIME_REQUIRE_CODEX_BIN"] = env["RUNTIME_REQUIRE_CODEX_BIN"] || "1";
|
|
454
|
+
env["RUNTIME_CAPABILITIES"] =
|
|
455
|
+
env["RUNTIME_CAPABILITIES"] ||
|
|
456
|
+
JSON.stringify({ fs: true, github: true, supabase: true, runs: true, agent: true, origin: true });
|
|
457
|
+
if (options.runtimeId && options.runtimeId.trim()) {
|
|
458
|
+
env["RUNTIME_ID"] = options.runtimeId.trim();
|
|
459
|
+
}
|
|
460
|
+
if (options.runtimeLeaseId && options.runtimeLeaseId.trim()) {
|
|
461
|
+
env["RUNTIME_LEASE_ID"] = options.runtimeLeaseId.trim();
|
|
462
|
+
}
|
|
463
|
+
if (!env["ORIGIN_ENDPOINT"]) {
|
|
464
|
+
const ratholeResolved = await resolveRatholeBinaryForCli({
|
|
465
|
+
env,
|
|
466
|
+
version: process.env["RATHOLE_VERSION"] ?? null,
|
|
467
|
+
cacheDir: process.env["RATHOLE_CACHE_DIR"] ?? null,
|
|
468
|
+
logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
|
|
469
|
+
warn: (message) => console.warn(kleur.yellow(message)),
|
|
470
|
+
});
|
|
471
|
+
if (!ratholeResolved) {
|
|
472
|
+
throw new Error("Tunnel is required but rathole is unavailable. Set RATHOLE_BIN (recommended) or pass --origin-endpoint to use a reachable URL.");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
printStatus("Starting runtime-agent:", bin);
|
|
476
|
+
printStatus("Project:", projectId);
|
|
477
|
+
printStatus("Workspace:", workspace);
|
|
478
|
+
printStatus("Origin:", originId);
|
|
479
|
+
const detached = Boolean(options.detach);
|
|
480
|
+
const logFile = options.logFile?.trim()
|
|
481
|
+
? path.resolve(options.logFile)
|
|
482
|
+
: detached
|
|
483
|
+
? path.join(LOG_DIR, `runtime-${Date.now()}.log`)
|
|
484
|
+
: undefined;
|
|
485
|
+
if (logFile) {
|
|
486
|
+
ensureLogDir();
|
|
487
|
+
}
|
|
488
|
+
let stdio;
|
|
489
|
+
if (!logFile && !detached) {
|
|
490
|
+
stdio = "inherit";
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const out = logFile ? openSync(logFile, "a") : "ignore";
|
|
494
|
+
stdio = ["ignore", out, out];
|
|
495
|
+
}
|
|
496
|
+
const spawnOptions = { env, stdio, detached };
|
|
497
|
+
const child = spawn(bin, spawnOptions);
|
|
498
|
+
if (detached) {
|
|
499
|
+
child.unref();
|
|
500
|
+
}
|
|
501
|
+
const startedAt = new Date().toISOString();
|
|
502
|
+
writeState({
|
|
503
|
+
pid: child.pid ?? -1,
|
|
504
|
+
projectId,
|
|
505
|
+
workspace,
|
|
506
|
+
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
507
|
+
originId,
|
|
508
|
+
runtimeId: env["RUNTIME_ID"] ?? null,
|
|
509
|
+
displayName: env["RUNTIME_DISPLAY_NAME"],
|
|
510
|
+
startedAt,
|
|
511
|
+
logFile,
|
|
512
|
+
detached,
|
|
513
|
+
});
|
|
514
|
+
child.on("exit", (code, signal) => {
|
|
515
|
+
const msg = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
516
|
+
console.log(kleur.yellow(`runtime-agent exited (${msg})`));
|
|
517
|
+
const current = readState();
|
|
518
|
+
if (current && current.pid === child.pid) {
|
|
519
|
+
clearState();
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function formatStatus(state, running) {
|
|
524
|
+
return {
|
|
525
|
+
running,
|
|
526
|
+
pid: state.pid,
|
|
527
|
+
projectId: state.projectId,
|
|
528
|
+
workspace: state.workspace,
|
|
529
|
+
controllerUrl: state.controllerUrl ?? null,
|
|
530
|
+
originId: state.originId ?? null,
|
|
531
|
+
runtimeId: state.runtimeId ?? null,
|
|
532
|
+
displayName: state.displayName ?? null,
|
|
533
|
+
startedAt: state.startedAt,
|
|
534
|
+
logFile: state.logFile ?? null,
|
|
535
|
+
detached: state.detached ?? false,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async function sendOfflineBeat(state) {
|
|
539
|
+
if (!state.controllerUrl || !state.projectId || !state.originId) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const directToken = normalizeToken(process.env["RUNTIME_ACCESS_TOKEN"]) ??
|
|
543
|
+
normalizeToken(process.env["ORIGIN_INTERNAL_TOKEN"]);
|
|
544
|
+
let bearer = directToken;
|
|
545
|
+
if (!bearer) {
|
|
546
|
+
const controllerAccessToken = normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
547
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
548
|
+
if (controllerAccessToken) {
|
|
549
|
+
try {
|
|
550
|
+
bearer = await mintRuntimeAccessToken({
|
|
551
|
+
controllerUrl: state.controllerUrl,
|
|
552
|
+
controllerAccessToken,
|
|
553
|
+
projectId: state.projectId,
|
|
554
|
+
runtimeId: state.runtimeId ?? undefined,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
const suffix = error instanceof Error ? `: ${error.message}` : error ? `: ${String(error)}` : "";
|
|
559
|
+
console.warn(kleur.yellow(`Could not mint runtime token for offline beat${suffix}`));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!bearer) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const offlinePayload = {
|
|
567
|
+
projectId: state.projectId,
|
|
568
|
+
originId: state.originId,
|
|
569
|
+
status: "offline",
|
|
570
|
+
latencyMs: null,
|
|
571
|
+
region: "iad",
|
|
572
|
+
metadata: { cli: true },
|
|
573
|
+
};
|
|
574
|
+
try {
|
|
575
|
+
await fetch(`${state.controllerUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(state.projectId)}/origin/presence/beat`, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
headers: {
|
|
578
|
+
authorization: `Bearer ${bearer}`,
|
|
579
|
+
"content-type": "application/json",
|
|
580
|
+
},
|
|
581
|
+
body: JSON.stringify(offlinePayload),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// ignore failures
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function waitForExit(pid, timeoutMs) {
|
|
589
|
+
const start = Date.now();
|
|
590
|
+
while (Date.now() - start < timeoutMs) {
|
|
591
|
+
if (!isProcessAlive(pid))
|
|
592
|
+
return true;
|
|
593
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
594
|
+
}
|
|
595
|
+
return !isProcessAlive(pid);
|
|
596
|
+
}
|
|
597
|
+
export async function runtimeStatus(options) {
|
|
598
|
+
const state = readState();
|
|
599
|
+
if (!state) {
|
|
600
|
+
if (options?.json) {
|
|
601
|
+
console.log(JSON.stringify({ running: false }));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
console.log(kleur.yellow("No runtime state found."));
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const alive = isProcessAlive(state.pid);
|
|
609
|
+
const payload = formatStatus(state, alive);
|
|
610
|
+
if (options?.json) {
|
|
611
|
+
console.log(JSON.stringify(payload));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
console.log(alive ? kleur.green("Runtime is running") : kleur.red("Runtime is not running"));
|
|
615
|
+
printStatus("PID:", String(state.pid));
|
|
616
|
+
printStatus("Project:", state.projectId);
|
|
617
|
+
printStatus("Workspace:", state.workspace);
|
|
618
|
+
if (state.controllerUrl)
|
|
619
|
+
printStatus("Controller:", state.controllerUrl);
|
|
620
|
+
if (state.originId)
|
|
621
|
+
printStatus("Origin:", state.originId);
|
|
622
|
+
if (state.runtimeId)
|
|
623
|
+
printStatus("Runtime:", state.runtimeId);
|
|
624
|
+
if (state.displayName)
|
|
625
|
+
printStatus("Name:", state.displayName);
|
|
626
|
+
printStatus("Started:", state.startedAt);
|
|
627
|
+
if (state.logFile)
|
|
628
|
+
printStatus("Log:", state.logFile);
|
|
629
|
+
printStatus("Detached:", state.detached ? "yes" : "no");
|
|
630
|
+
if (!alive) {
|
|
631
|
+
console.log(kleur.yellow("State file exists but process is not alive."));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
export async function runtimeStop(options) {
|
|
635
|
+
const state = readState();
|
|
636
|
+
if (!state) {
|
|
637
|
+
if (options?.json) {
|
|
638
|
+
console.log(JSON.stringify({ stopped: false, reason: "not_running" }));
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
console.log(kleur.yellow("Runtime not running."));
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!isProcessAlive(state.pid)) {
|
|
646
|
+
clearState();
|
|
647
|
+
if (options?.json) {
|
|
648
|
+
console.log(JSON.stringify({ stopped: false, reason: "not_running" }));
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
console.log(kleur.yellow("Runtime process already stopped. State cleared."));
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
process.kill(state.pid, "SIGINT");
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
if (options?.json) {
|
|
660
|
+
console.log(JSON.stringify({ stopped: false, error: String(error) }));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
const exited = await waitForExit(state.pid, 5000);
|
|
666
|
+
if (!exited) {
|
|
667
|
+
try {
|
|
668
|
+
process.kill(state.pid, "SIGTERM");
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// ignore
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const alive = isProcessAlive(state.pid);
|
|
675
|
+
if (!alive) {
|
|
676
|
+
clearState();
|
|
677
|
+
}
|
|
678
|
+
await sendOfflineBeat(state);
|
|
679
|
+
const result = { stopped: !alive, pid: state.pid };
|
|
680
|
+
if (options?.json) {
|
|
681
|
+
console.log(JSON.stringify(result));
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
console.log(!alive ? kleur.green("Runtime stopped.") : kleur.red("Failed to stop runtime."));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
export async function runtimeToken(options) {
|
|
688
|
+
const controllerUrl = options.controllerUrl ?? process.env["CONTROLLER_BASE_URL"] ?? "http://127.0.0.1:8788";
|
|
689
|
+
const token = options.controllerAccessToken ??
|
|
690
|
+
process.env["CONTROLLER_ACCESS_TOKEN"] ??
|
|
691
|
+
process.env["SUPABASE_ACCESS_TOKEN"];
|
|
692
|
+
if (!token) {
|
|
693
|
+
throw new Error("Controller access token is required (--controller-access-token or CONTROLLER_ACCESS_TOKEN)");
|
|
694
|
+
}
|
|
695
|
+
const minted = await mintRuntimeAccessToken({
|
|
696
|
+
controllerUrl,
|
|
697
|
+
controllerAccessToken: token,
|
|
698
|
+
projectId: options.project,
|
|
699
|
+
runtimeId: options.runtimeId,
|
|
700
|
+
scopes: options.scopes,
|
|
701
|
+
});
|
|
702
|
+
if (options.json) {
|
|
703
|
+
console.log(JSON.stringify({ token: minted }));
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
console.log(minted);
|
|
707
|
+
}
|
|
708
|
+
return minted;
|
|
709
|
+
}
|