@instafy/cli 0.1.7 → 0.1.8
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 +2 -0
- package/dist/api.js +3 -1
- package/dist/auth.js +94 -0
- package/dist/config-command.js +104 -0
- package/dist/config.js +111 -0
- package/dist/git.js +3 -1
- package/dist/index.js +137 -12
- package/dist/org.js +6 -17
- package/dist/project.js +9 -23
- package/dist/runtime.js +266 -57
- package/dist/tunnel.js +42 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
|
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
9
|
+
0. Log in once: `instafy login`
|
|
10
|
+
- Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
|
|
9
11
|
1. Link a folder to a project:
|
|
10
12
|
- VS Code: install the Instafy extension and run `Instafy: Link Workspace to Project`, or
|
|
11
13
|
- Terminal: `instafy project:init`
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { resolveConfiguredAccessToken } from "./config.js";
|
|
2
3
|
function normalizeUrl(raw) {
|
|
3
4
|
const value = (raw ?? "").trim();
|
|
4
5
|
if (!value)
|
|
@@ -18,7 +19,8 @@ function resolveBearerToken(options) {
|
|
|
18
19
|
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
19
20
|
normalizeToken(process.env["INSTAFY_SERVICE_TOKEN"]) ??
|
|
20
21
|
normalizeToken(process.env["CONTROLLER_TOKEN"]) ??
|
|
21
|
-
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"])
|
|
22
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
|
|
23
|
+
resolveConfiguredAccessToken());
|
|
22
24
|
}
|
|
23
25
|
function parseKeyValue(raw) {
|
|
24
26
|
const trimmed = raw.trim();
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
|
|
5
|
+
function normalizeUrl(raw) {
|
|
6
|
+
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return trimmed.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
function normalizeToken(raw) {
|
|
13
|
+
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const lowered = trimmed.toLowerCase();
|
|
18
|
+
if (lowered === "null" || lowered === "undefined") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function looksLikeLocalControllerUrl(controllerUrl) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(controllerUrl);
|
|
26
|
+
const host = parsed.hostname.toLowerCase();
|
|
27
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return controllerUrl.includes("127.0.0.1") || controllerUrl.includes("localhost");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function deriveDefaultStudioUrl(controllerUrl) {
|
|
34
|
+
if (looksLikeLocalControllerUrl(controllerUrl)) {
|
|
35
|
+
return "http://localhost:5173";
|
|
36
|
+
}
|
|
37
|
+
return "https://staging.instafy.dev";
|
|
38
|
+
}
|
|
39
|
+
export async function login(options) {
|
|
40
|
+
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
41
|
+
const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
|
|
42
|
+
normalizeUrl(process.env["INSTAFY_STUDIO_URL"] ?? null) ??
|
|
43
|
+
resolveConfiguredStudioUrl() ??
|
|
44
|
+
deriveDefaultStudioUrl(controllerUrl);
|
|
45
|
+
const url = new URL("/cli/login", studioUrl);
|
|
46
|
+
url.searchParams.set("serverUrl", controllerUrl);
|
|
47
|
+
if (options.json) {
|
|
48
|
+
console.log(JSON.stringify({ url: url.toString(), configPath: getInstafyConfigPath() }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(kleur.green("Instafy CLI login"));
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log("1) Open this URL in your browser:");
|
|
54
|
+
console.log(kleur.cyan(url.toString()));
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log("2) After you sign in, copy the token shown on that page.");
|
|
57
|
+
console.log("");
|
|
58
|
+
const provided = normalizeToken(options.token ?? null);
|
|
59
|
+
const existing = resolveUserAccessToken();
|
|
60
|
+
let token = provided;
|
|
61
|
+
if (!token) {
|
|
62
|
+
const rl = createInterface({ input, output });
|
|
63
|
+
try {
|
|
64
|
+
token = normalizeToken(await rl.question("Paste token: "));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
rl.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new Error("No token provided.");
|
|
72
|
+
}
|
|
73
|
+
if (!options.noStore) {
|
|
74
|
+
writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(kleur.green(`Saved token to ${getInstafyConfigPath()}`));
|
|
77
|
+
}
|
|
78
|
+
else if (existing) {
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(kleur.yellow("Token not stored (existing token kept)."));
|
|
81
|
+
}
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log("Next:");
|
|
84
|
+
console.log(`- ${kleur.cyan("instafy project:init")}`);
|
|
85
|
+
console.log(`- ${kleur.cyan("instafy runtime:start")}`);
|
|
86
|
+
}
|
|
87
|
+
export async function logout(options) {
|
|
88
|
+
clearInstafyCliConfig(["accessToken"]);
|
|
89
|
+
if (options?.json) {
|
|
90
|
+
console.log(JSON.stringify({ ok: true }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(kleur.green("Logged out (cleared saved access token)."));
|
|
94
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { clearInstafyCliConfig, getInstafyConfigPath, readInstafyCliConfig, writeInstafyCliConfig, } from "./config.js";
|
|
3
|
+
function normalizeKey(raw) {
|
|
4
|
+
const key = raw.trim().toLowerCase().replace(/_/g, "-");
|
|
5
|
+
if (key === "controller-url" || key === "controllerurl" || key === "server-url") {
|
|
6
|
+
return "controller-url";
|
|
7
|
+
}
|
|
8
|
+
if (key === "studio-url" || key === "studiourl") {
|
|
9
|
+
return "studio-url";
|
|
10
|
+
}
|
|
11
|
+
throw new Error(`Unknown config key: ${raw} (supported: controller-url, studio-url)`);
|
|
12
|
+
}
|
|
13
|
+
function getValue(config, key) {
|
|
14
|
+
if (key === "controller-url")
|
|
15
|
+
return config.controllerUrl ?? null;
|
|
16
|
+
if (key === "studio-url")
|
|
17
|
+
return config.studioUrl ?? null;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function updateConfig(key, value) {
|
|
21
|
+
if (key === "controller-url") {
|
|
22
|
+
return writeInstafyCliConfig({ controllerUrl: value });
|
|
23
|
+
}
|
|
24
|
+
if (key === "studio-url") {
|
|
25
|
+
return writeInstafyCliConfig({ studioUrl: value });
|
|
26
|
+
}
|
|
27
|
+
return writeInstafyCliConfig({});
|
|
28
|
+
}
|
|
29
|
+
function clearConfig(key) {
|
|
30
|
+
if (key === "controller-url") {
|
|
31
|
+
clearInstafyCliConfig(["controllerUrl"]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key === "studio-url") {
|
|
35
|
+
clearInstafyCliConfig(["studioUrl"]);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function configPath(options) {
|
|
40
|
+
const path = getInstafyConfigPath();
|
|
41
|
+
if (options?.json) {
|
|
42
|
+
console.log(JSON.stringify({ path }, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(path);
|
|
46
|
+
}
|
|
47
|
+
export function configList(options) {
|
|
48
|
+
const config = readInstafyCliConfig();
|
|
49
|
+
const payload = {
|
|
50
|
+
path: getInstafyConfigPath(),
|
|
51
|
+
controllerUrl: config.controllerUrl ?? null,
|
|
52
|
+
studioUrl: config.studioUrl ?? null,
|
|
53
|
+
accessTokenSet: Boolean(config.accessToken),
|
|
54
|
+
updatedAt: config.updatedAt ?? null,
|
|
55
|
+
};
|
|
56
|
+
if (options?.json) {
|
|
57
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(kleur.green("Instafy CLI config"));
|
|
61
|
+
console.log(`Path: ${payload.path}`);
|
|
62
|
+
console.log(`controller-url: ${payload.controllerUrl ?? kleur.yellow("(not set)")}`);
|
|
63
|
+
console.log(`studio-url: ${payload.studioUrl ?? kleur.yellow("(not set)")}`);
|
|
64
|
+
console.log(`access-token: ${payload.accessTokenSet ? kleur.green("(set)") : kleur.yellow("(not set)")}`);
|
|
65
|
+
if (payload.updatedAt) {
|
|
66
|
+
console.log(`updated-at: ${payload.updatedAt}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function configGet(params) {
|
|
70
|
+
const key = normalizeKey(params.key);
|
|
71
|
+
const config = readInstafyCliConfig();
|
|
72
|
+
const value = getValue(config, key);
|
|
73
|
+
if (!value) {
|
|
74
|
+
throw new Error(`Config key ${key} is not set. Run \`instafy config set ${key} <value>\`.`);
|
|
75
|
+
}
|
|
76
|
+
if (params.json) {
|
|
77
|
+
console.log(JSON.stringify({ key, value }, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(value);
|
|
81
|
+
}
|
|
82
|
+
export function configSet(params) {
|
|
83
|
+
const key = normalizeKey(params.key);
|
|
84
|
+
const updated = updateConfig(key, params.value);
|
|
85
|
+
const value = getValue(updated, key);
|
|
86
|
+
if (!value) {
|
|
87
|
+
throw new Error(`Failed to set ${key}.`);
|
|
88
|
+
}
|
|
89
|
+
if (params.json) {
|
|
90
|
+
console.log(JSON.stringify({ key, value, path: getInstafyConfigPath() }, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(kleur.green(`Set ${key}`));
|
|
94
|
+
console.log(value);
|
|
95
|
+
}
|
|
96
|
+
export function configUnset(params) {
|
|
97
|
+
const key = normalizeKey(params.key);
|
|
98
|
+
clearConfig(key);
|
|
99
|
+
if (params.json) {
|
|
100
|
+
console.log(JSON.stringify({ ok: true, key }, null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(kleur.green(`Unset ${key}`));
|
|
104
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
5
|
+
const CONFIG_PATH = path.join(INSTAFY_DIR, "config.json");
|
|
6
|
+
function normalizeToken(value) {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const lowered = trimmed.toLowerCase();
|
|
15
|
+
if (lowered === "null" || lowered === "undefined") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
function normalizeUrl(value) {
|
|
21
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return trimmed.replace(/\/$/, "");
|
|
26
|
+
}
|
|
27
|
+
export function getInstafyConfigPath() {
|
|
28
|
+
return CONFIG_PATH;
|
|
29
|
+
}
|
|
30
|
+
export function readInstafyCliConfig() {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!parsed || typeof parsed !== "object") {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const record = parsed;
|
|
38
|
+
return {
|
|
39
|
+
controllerUrl: normalizeUrl(typeof record.controllerUrl === "string" ? record.controllerUrl : null),
|
|
40
|
+
studioUrl: normalizeUrl(typeof record.studioUrl === "string" ? record.studioUrl : null),
|
|
41
|
+
accessToken: normalizeToken(typeof record.accessToken === "string" ? record.accessToken : null),
|
|
42
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writeInstafyCliConfig(update) {
|
|
50
|
+
const existing = readInstafyCliConfig();
|
|
51
|
+
const next = {
|
|
52
|
+
controllerUrl: normalizeUrl(update.controllerUrl ?? existing.controllerUrl ?? null),
|
|
53
|
+
studioUrl: normalizeUrl(update.studioUrl ?? existing.studioUrl ?? null),
|
|
54
|
+
accessToken: normalizeToken(update.accessToken ?? existing.accessToken ?? null),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
fs.mkdirSync(INSTAFY_DIR, { recursive: true });
|
|
58
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2), { encoding: "utf8" });
|
|
59
|
+
try {
|
|
60
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ignore chmod failures (windows / unusual fs)
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
export function clearInstafyCliConfig(keys) {
|
|
68
|
+
if (!keys || keys.length === 0) {
|
|
69
|
+
try {
|
|
70
|
+
fs.rmSync(CONFIG_PATH, { force: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const existing = readInstafyCliConfig();
|
|
78
|
+
const next = { ...existing };
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
next[key] = null;
|
|
81
|
+
}
|
|
82
|
+
writeInstafyCliConfig(next);
|
|
83
|
+
}
|
|
84
|
+
export function resolveConfiguredControllerUrl() {
|
|
85
|
+
const config = readInstafyCliConfig();
|
|
86
|
+
return normalizeUrl(config.controllerUrl ?? null);
|
|
87
|
+
}
|
|
88
|
+
export function resolveConfiguredStudioUrl() {
|
|
89
|
+
const config = readInstafyCliConfig();
|
|
90
|
+
return normalizeUrl(config.studioUrl ?? null);
|
|
91
|
+
}
|
|
92
|
+
export function resolveConfiguredAccessToken() {
|
|
93
|
+
const config = readInstafyCliConfig();
|
|
94
|
+
return normalizeToken(config.accessToken ?? null);
|
|
95
|
+
}
|
|
96
|
+
export function resolveControllerUrl(params) {
|
|
97
|
+
const config = readInstafyCliConfig();
|
|
98
|
+
return (normalizeUrl(params?.controllerUrl ?? null) ??
|
|
99
|
+
normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
|
|
100
|
+
normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
|
|
101
|
+
normalizeUrl(config.controllerUrl ?? null) ??
|
|
102
|
+
"http://127.0.0.1:8788");
|
|
103
|
+
}
|
|
104
|
+
export function resolveUserAccessToken(params) {
|
|
105
|
+
const config = readInstafyCliConfig();
|
|
106
|
+
return (normalizeToken(params?.accessToken ?? null) ??
|
|
107
|
+
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"] ?? null) ??
|
|
108
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"] ?? null) ??
|
|
109
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"] ?? null) ??
|
|
110
|
+
normalizeToken(config.accessToken ?? null));
|
|
111
|
+
}
|
package/dist/git.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
|
+
import { resolveConfiguredAccessToken } from "./config.js";
|
|
4
5
|
function normalizeToken(value) {
|
|
5
6
|
if (typeof value !== "string") {
|
|
6
7
|
return null;
|
|
@@ -27,6 +28,7 @@ function resolveControllerAccessToken(options) {
|
|
|
27
28
|
normalizeToken(options.supabaseAccessToken) ??
|
|
28
29
|
readTokenFromFile(options.supabaseAccessTokenFile) ??
|
|
29
30
|
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
|
|
31
|
+
resolveConfiguredAccessToken() ??
|
|
30
32
|
null);
|
|
31
33
|
}
|
|
32
34
|
export async function mintGitAccessToken(params) {
|
|
@@ -74,7 +76,7 @@ export async function gitToken(options) {
|
|
|
74
76
|
supabaseAccessTokenFile: options.supabaseAccessTokenFile,
|
|
75
77
|
});
|
|
76
78
|
if (!token) {
|
|
77
|
-
throw new Error("
|
|
79
|
+
throw new Error("Login required. Run `instafy login` or pass --access-token / --supabase-access-token.");
|
|
78
80
|
}
|
|
79
81
|
const minted = await mintGitAccessToken({
|
|
80
82
|
controllerUrl,
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,13 @@ import { Command, Option } from "commander";
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import kleur from "kleur";
|
|
5
|
-
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, } from "./runtime.js";
|
|
5
|
+
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, findProjectManifest, } from "./runtime.js";
|
|
6
|
+
import { login, logout } from "./auth.js";
|
|
6
7
|
import { gitToken } from "./git.js";
|
|
7
8
|
import { projectInit } from "./project.js";
|
|
8
9
|
import { runTunnelCommand } from "./tunnel.js";
|
|
9
10
|
import { requestControllerApi } from "./api.js";
|
|
11
|
+
import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
|
|
10
12
|
export const program = new Command();
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
const pkg = require("../package.json");
|
|
@@ -32,6 +34,112 @@ program
|
|
|
32
34
|
.name("instafy")
|
|
33
35
|
.description("Instafy CLI — run your project locally and connect to Studio")
|
|
34
36
|
.version(pkg.version ?? "0.1.0");
|
|
37
|
+
program
|
|
38
|
+
.command("login")
|
|
39
|
+
.description("Log in and save an access token for future CLI commands")
|
|
40
|
+
.option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
|
|
41
|
+
.option("--server-url <url>", "Instafy server/controller URL")
|
|
42
|
+
.option("--token <token>", "Provide token directly (skips prompt)")
|
|
43
|
+
.option("--no-store", "Do not save token to ~/.instafy/config.json")
|
|
44
|
+
.option("--json", "Output JSON")
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
try {
|
|
47
|
+
await login({
|
|
48
|
+
controllerUrl: opts.serverUrl,
|
|
49
|
+
studioUrl: opts.studioUrl,
|
|
50
|
+
token: opts.token,
|
|
51
|
+
noStore: opts.store === false,
|
|
52
|
+
json: opts.json,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error(kleur.red(String(error)));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command("logout")
|
|
62
|
+
.description("Clear the saved CLI access token")
|
|
63
|
+
.option("--json", "Output JSON")
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
try {
|
|
66
|
+
await logout({ json: opts.json });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(kleur.red(String(error)));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const configCommand = program.command("config").description("Get/set saved CLI configuration");
|
|
74
|
+
configCommand
|
|
75
|
+
.command("path")
|
|
76
|
+
.description("Print the config file path")
|
|
77
|
+
.option("--json", "Output JSON")
|
|
78
|
+
.action(async (opts) => {
|
|
79
|
+
try {
|
|
80
|
+
configPath({ json: opts.json });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(kleur.red(String(error)));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
configCommand
|
|
88
|
+
.command("list")
|
|
89
|
+
.description("List saved configuration")
|
|
90
|
+
.option("--json", "Output JSON")
|
|
91
|
+
.action(async (opts) => {
|
|
92
|
+
try {
|
|
93
|
+
configList({ json: opts.json });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(kleur.red(String(error)));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
configCommand
|
|
101
|
+
.command("get")
|
|
102
|
+
.description("Get a config value (controller-url, studio-url)")
|
|
103
|
+
.argument("<key>", "Config key")
|
|
104
|
+
.option("--json", "Output JSON")
|
|
105
|
+
.action(async (key, opts) => {
|
|
106
|
+
try {
|
|
107
|
+
configGet({ key, json: opts.json });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error(kleur.red(String(error)));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
configCommand
|
|
115
|
+
.command("set")
|
|
116
|
+
.description("Set a config value (controller-url, studio-url)")
|
|
117
|
+
.argument("<key>", "Config key")
|
|
118
|
+
.argument("<value>", "Config value")
|
|
119
|
+
.option("--json", "Output JSON")
|
|
120
|
+
.action(async (key, value, opts) => {
|
|
121
|
+
try {
|
|
122
|
+
configSet({ key, value, json: opts.json });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error(kleur.red(String(error)));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
configCommand
|
|
130
|
+
.command("unset")
|
|
131
|
+
.description("Unset a config value (controller-url, studio-url)")
|
|
132
|
+
.argument("<key>", "Config key")
|
|
133
|
+
.option("--json", "Output JSON")
|
|
134
|
+
.action(async (key, opts) => {
|
|
135
|
+
try {
|
|
136
|
+
configUnset({ key, json: opts.json });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error(kleur.red(String(error)));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
35
143
|
const runtimeStartCommand = program
|
|
36
144
|
.command("runtime:start")
|
|
37
145
|
.description("Start a local runtime for this project")
|
|
@@ -46,6 +154,7 @@ runtimeStartCommand
|
|
|
46
154
|
.option("--codex-bin <path>", "Path to codex binary (fallback to PATH)")
|
|
47
155
|
.option("--proxy-base-url <url>", "Codex proxy base URL")
|
|
48
156
|
.option("--workspace <path>", "Workspace directory (defaults to ./.instafy/workspace)")
|
|
157
|
+
.option("--runtime-mode <mode>", "Runtime runner (auto|docker|process)", "auto")
|
|
49
158
|
.option("--origin-id <uuid>", "Origin ID to use (auto-generated if omitted)")
|
|
50
159
|
.option("--origin-endpoint <url>", "Explicit origin endpoint (skip tunnel setup when provided)")
|
|
51
160
|
.option("--origin-token <token>", "Runtime/origin access token for Studio registration")
|
|
@@ -100,7 +209,7 @@ program
|
|
|
100
209
|
const runtimeTokenCommand = program
|
|
101
210
|
.command("runtime:token")
|
|
102
211
|
.description("Mint a runtime access token")
|
|
103
|
-
.
|
|
212
|
+
.option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
|
|
104
213
|
addServerUrlOptions(runtimeTokenCommand);
|
|
105
214
|
addAccessTokenOptions(runtimeTokenCommand, "Instafy access token (required)");
|
|
106
215
|
runtimeTokenCommand
|
|
@@ -109,8 +218,14 @@ runtimeTokenCommand
|
|
|
109
218
|
.option("--json", "Output token as JSON")
|
|
110
219
|
.action(async (opts) => {
|
|
111
220
|
try {
|
|
221
|
+
const project = typeof opts.project === "string" && opts.project.trim()
|
|
222
|
+
? opts.project.trim()
|
|
223
|
+
: findProjectManifest(process.cwd()).manifest?.projectId ?? null;
|
|
224
|
+
if (!project) {
|
|
225
|
+
throw new Error("No project configured. Run `instafy project:init` or pass --project.");
|
|
226
|
+
}
|
|
112
227
|
await runtimeToken({
|
|
113
|
-
project
|
|
228
|
+
project,
|
|
114
229
|
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
115
230
|
controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
|
|
116
231
|
runtimeId: opts.runtimeId,
|
|
@@ -126,7 +241,7 @@ runtimeTokenCommand
|
|
|
126
241
|
const gitTokenCommand = program
|
|
127
242
|
.command("git:token")
|
|
128
243
|
.description("Mint a git access token for the project repo")
|
|
129
|
-
.
|
|
244
|
+
.option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
|
|
130
245
|
addServerUrlOptions(gitTokenCommand);
|
|
131
246
|
addAccessTokenOptions(gitTokenCommand, "Instafy access token (required)");
|
|
132
247
|
gitTokenCommand
|
|
@@ -137,13 +252,19 @@ gitTokenCommand
|
|
|
137
252
|
.option("--json", "Output token response as JSON")
|
|
138
253
|
.action(async (opts) => {
|
|
139
254
|
try {
|
|
255
|
+
const project = typeof opts.project === "string" && opts.project.trim()
|
|
256
|
+
? opts.project.trim()
|
|
257
|
+
: findProjectManifest(process.cwd()).manifest?.projectId ?? null;
|
|
258
|
+
if (!project) {
|
|
259
|
+
throw new Error("No project configured. Run `instafy project:init` or pass --project.");
|
|
260
|
+
}
|
|
140
261
|
const ttlSecondsRaw = typeof opts.ttlSeconds === "string" ? opts.ttlSeconds.trim() : "";
|
|
141
262
|
const ttlSeconds = ttlSecondsRaw.length > 0 ? Number.parseInt(ttlSecondsRaw, 10) : undefined;
|
|
142
263
|
if (ttlSecondsRaw.length > 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) {
|
|
143
264
|
throw new Error("--ttl-seconds must be a positive integer");
|
|
144
265
|
}
|
|
145
266
|
await gitToken({
|
|
146
|
-
project
|
|
267
|
+
project,
|
|
147
268
|
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
148
269
|
controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
|
|
149
270
|
supabaseAccessToken: opts.supabaseAccessToken,
|
|
@@ -193,11 +314,12 @@ projectInitCommand
|
|
|
193
314
|
const tunnelCommand = program
|
|
194
315
|
.command("tunnel")
|
|
195
316
|
.description("Create a shareable tunnel URL for a local port")
|
|
196
|
-
.option("--
|
|
317
|
+
.option("--port <port>", "Local port to expose (default 3000)")
|
|
318
|
+
.option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
|
|
197
319
|
addServerUrlOptions(tunnelCommand);
|
|
320
|
+
addAccessTokenOptions(tunnelCommand, "Instafy access token (defaults to saved `instafy login` token)");
|
|
198
321
|
addServiceTokenOptions(tunnelCommand, "Instafy service token (advanced)");
|
|
199
322
|
tunnelCommand
|
|
200
|
-
.option("--port <port>", "Local port to expose (default 3000)")
|
|
201
323
|
.option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)")
|
|
202
324
|
.action(async (opts) => {
|
|
203
325
|
try {
|
|
@@ -205,7 +327,10 @@ tunnelCommand
|
|
|
205
327
|
await runTunnelCommand({
|
|
206
328
|
project: opts.project,
|
|
207
329
|
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
208
|
-
controllerToken: opts.serviceToken ??
|
|
330
|
+
controllerToken: opts.serviceToken ??
|
|
331
|
+
opts.controllerToken ??
|
|
332
|
+
opts.accessToken ??
|
|
333
|
+
opts.controllerAccessToken,
|
|
209
334
|
port,
|
|
210
335
|
ratholeBin: opts.ratholeBin,
|
|
211
336
|
});
|
|
@@ -292,22 +417,22 @@ function configureApiCommand(command, method) {
|
|
|
292
417
|
}
|
|
293
418
|
const apiGetCommand = program
|
|
294
419
|
.command("api:get")
|
|
295
|
-
.description("
|
|
420
|
+
.description("Advanced: authenticated GET request to the controller API")
|
|
296
421
|
.argument("<path>", "API path (or full URL), e.g. /conversations/<id>/messages?limit=50");
|
|
297
422
|
configureApiCommand(apiGetCommand, "GET");
|
|
298
423
|
const apiPostCommand = program
|
|
299
424
|
.command("api:post")
|
|
300
|
-
.description("
|
|
425
|
+
.description("Advanced: authenticated POST request to the controller API")
|
|
301
426
|
.argument("<path>", "API path (or full URL)");
|
|
302
427
|
configureApiCommand(apiPostCommand, "POST");
|
|
303
428
|
const apiPatchCommand = program
|
|
304
429
|
.command("api:patch")
|
|
305
|
-
.description("
|
|
430
|
+
.description("Advanced: authenticated PATCH request to the controller API")
|
|
306
431
|
.argument("<path>", "API path (or full URL)");
|
|
307
432
|
configureApiCommand(apiPatchCommand, "PATCH");
|
|
308
433
|
const apiDeleteCommand = program
|
|
309
434
|
.command("api:delete")
|
|
310
|
-
.description("
|
|
435
|
+
.description("Advanced: authenticated DELETE request to the controller API")
|
|
311
436
|
.argument("<path>", "API path (or full URL)");
|
|
312
437
|
configureApiCommand(apiDeleteCommand, "DELETE");
|
|
313
438
|
export async function runCli(argv = process.argv) {
|
package/dist/org.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return "http://127.0.0.1:8788";
|
|
6
|
-
return value.replace(/\/$/, "");
|
|
7
|
-
}
|
|
8
|
-
function normalizeToken(raw) {
|
|
9
|
-
if (!raw)
|
|
10
|
-
return null;
|
|
11
|
-
const trimmed = raw.trim();
|
|
12
|
-
return trimmed.length ? trimmed : null;
|
|
2
|
+
import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
3
|
+
function formatAuthRequiredError() {
|
|
4
|
+
return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
13
5
|
}
|
|
14
6
|
export async function listOrganizations(params) {
|
|
15
|
-
const controllerUrl =
|
|
16
|
-
const token =
|
|
17
|
-
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
18
|
-
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
19
|
-
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
7
|
+
const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
|
|
8
|
+
const token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
|
|
20
9
|
if (!token) {
|
|
21
|
-
throw
|
|
10
|
+
throw formatAuthRequiredError();
|
|
22
11
|
}
|
|
23
12
|
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
24
13
|
headers: { authorization: `Bearer ${token}` },
|
package/dist/project.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return "http://127.0.0.1:8788";
|
|
8
|
-
return value.replace(/\/$/, "");
|
|
9
|
-
}
|
|
10
|
-
function normalizeToken(raw) {
|
|
11
|
-
if (!raw)
|
|
12
|
-
return null;
|
|
13
|
-
const trimmed = raw.trim();
|
|
14
|
-
return trimmed.length ? trimmed : null;
|
|
4
|
+
import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
5
|
+
function formatAuthRequiredError() {
|
|
6
|
+
return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
15
7
|
}
|
|
16
8
|
async function fetchOrganizations(controllerUrl, token) {
|
|
17
9
|
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
@@ -73,13 +65,10 @@ async function resolveOrg(controllerUrl, token, options) {
|
|
|
73
65
|
return { orgId, orgName: json.org_name ?? null };
|
|
74
66
|
}
|
|
75
67
|
export async function listProjects(options) {
|
|
76
|
-
const controllerUrl =
|
|
77
|
-
const token =
|
|
78
|
-
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
79
|
-
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
80
|
-
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
68
|
+
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
69
|
+
const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
|
|
81
70
|
if (!token) {
|
|
82
|
-
throw
|
|
71
|
+
throw formatAuthRequiredError();
|
|
83
72
|
}
|
|
84
73
|
const orgs = await fetchOrganizations(controllerUrl, token);
|
|
85
74
|
let targetOrgs = orgs;
|
|
@@ -124,13 +113,10 @@ export async function listProjects(options) {
|
|
|
124
113
|
}
|
|
125
114
|
export async function projectInit(options) {
|
|
126
115
|
const rootDir = path.resolve(options.path ?? process.cwd());
|
|
127
|
-
const controllerUrl =
|
|
128
|
-
const token =
|
|
129
|
-
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
130
|
-
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
131
|
-
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
116
|
+
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
117
|
+
const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
|
|
132
118
|
if (!token) {
|
|
133
|
-
throw
|
|
119
|
+
throw formatAuthRequiredError();
|
|
134
120
|
}
|
|
135
121
|
const org = await resolveOrg(controllerUrl, token, options);
|
|
136
122
|
const body = {
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import kleur from "kleur";
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { ensureRatholeBinary } from "./rathole.js";
|
|
9
|
+
import { resolveConfiguredAccessToken } from "./config.js";
|
|
9
10
|
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
10
11
|
const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
|
|
11
12
|
const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
|
|
@@ -27,7 +28,82 @@ function resolveRuntimeBinary() {
|
|
|
27
28
|
return candidate;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
throw new Error("runtime-agent binary not found.
|
|
31
|
+
throw new Error("runtime-agent binary not found. If you're in the instafy repo, run `cargo build -p runtime-agent`. Otherwise install Docker and rerun `instafy runtime:start`.");
|
|
32
|
+
}
|
|
33
|
+
function tryResolveRuntimeBinary() {
|
|
34
|
+
try {
|
|
35
|
+
return resolveRuntimeBinary();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isDockerAvailable() {
|
|
42
|
+
try {
|
|
43
|
+
const result = spawnSync("docker", ["version"], { stdio: "ignore" });
|
|
44
|
+
return result.status === 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeControllerUrlForDocker(controllerUrl) {
|
|
51
|
+
const trimmed = controllerUrl.trim().replace(/\/$/, "");
|
|
52
|
+
if (!trimmed) {
|
|
53
|
+
return trimmed;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(trimmed);
|
|
57
|
+
const host = url.hostname.toLowerCase();
|
|
58
|
+
if (host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "0.0.0.0") {
|
|
59
|
+
url.hostname = "host.docker.internal";
|
|
60
|
+
}
|
|
61
|
+
return url.toString().replace(/\/$/, "");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return trimmed
|
|
65
|
+
.replace("127.0.0.1", "host.docker.internal")
|
|
66
|
+
.replace("localhost", "host.docker.internal")
|
|
67
|
+
.replace("0.0.0.0", "host.docker.internal")
|
|
68
|
+
.replace(/\/$/, "");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function resolveRuntimeAgentImage() {
|
|
72
|
+
const fromEnv = normalizeToken(process.env["INSTAFY_RUNTIME_AGENT_IMAGE"]) ??
|
|
73
|
+
normalizeToken(process.env["RUNTIME_AGENT_IMAGE"]) ??
|
|
74
|
+
null;
|
|
75
|
+
return fromEnv ?? "ghcr.io/instafy-dev/instafy-runtime-agent:latest";
|
|
76
|
+
}
|
|
77
|
+
function dockerContainerRunning(containerId) {
|
|
78
|
+
const id = containerId.trim();
|
|
79
|
+
if (!id) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const result = spawnSync("docker", ["inspect", "-f", "{{.State.Running}}", id], {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
});
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return String(result.stdout ?? "").trim() === "true";
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function stopDockerContainer(containerId) {
|
|
97
|
+
const id = containerId.trim();
|
|
98
|
+
if (!id) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
spawnSync("docker", ["rm", "-f", id], { stdio: "ignore" });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
31
107
|
}
|
|
32
108
|
function printStatus(label, value) {
|
|
33
109
|
console.log(`${kleur.cyan(label)} ${value}`);
|
|
@@ -131,6 +207,14 @@ function findRatholeOnPath() {
|
|
|
131
207
|
}
|
|
132
208
|
return null;
|
|
133
209
|
}
|
|
210
|
+
function resolveControllerAccessTokenForCli(options, env, supabaseAccessToken) {
|
|
211
|
+
return (normalizeToken(options.controllerAccessToken) ??
|
|
212
|
+
normalizeToken(env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
213
|
+
normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
214
|
+
supabaseAccessToken ??
|
|
215
|
+
normalizeToken(env["SUPABASE_ACCESS_TOKEN"]) ??
|
|
216
|
+
resolveConfiguredAccessToken());
|
|
217
|
+
}
|
|
134
218
|
export async function resolveRatholeBinaryForCli(options) {
|
|
135
219
|
const warn = options.warn ??
|
|
136
220
|
((message) => {
|
|
@@ -212,33 +296,34 @@ function isProcessAlive(pid) {
|
|
|
212
296
|
}
|
|
213
297
|
}
|
|
214
298
|
export async function runtimeStart(options) {
|
|
215
|
-
const bin = resolveRuntimeBinary();
|
|
216
299
|
const env = { ...process.env };
|
|
217
300
|
const existing = readState();
|
|
218
|
-
if (existing
|
|
219
|
-
|
|
301
|
+
if (existing) {
|
|
302
|
+
if (existing.runner === "docker" && existing.containerId && dockerContainerRunning(existing.containerId)) {
|
|
303
|
+
throw new Error(`Runtime already running (docker container ${existing.containerId}) for project ${existing.projectId}. Stop it first.`);
|
|
304
|
+
}
|
|
305
|
+
if ((!existing.runner || existing.runner === "process") && isProcessAlive(existing.pid)) {
|
|
306
|
+
throw new Error(`Runtime already running (pid ${existing.pid}) for project ${existing.projectId}. Stop it first.`);
|
|
307
|
+
}
|
|
220
308
|
}
|
|
221
|
-
const manifestInfo = findProjectManifest(
|
|
309
|
+
const manifestInfo = findProjectManifest(process.cwd());
|
|
222
310
|
const projectId = options.project ?? env["PROJECT_ID"] ?? manifestInfo.manifest?.projectId ?? null;
|
|
223
311
|
if (!projectId) {
|
|
224
|
-
throw new Error("
|
|
312
|
+
throw new Error("No project configured. Run `instafy project:init` in this folder (recommended) or pass --project.");
|
|
225
313
|
}
|
|
226
314
|
env["PROJECT_ID"] = projectId;
|
|
227
315
|
const supabaseAccessToken = resolveSupabaseAccessToken(options, env);
|
|
228
316
|
if (supabaseAccessToken) {
|
|
229
317
|
env["SUPABASE_ACCESS_TOKEN"] = supabaseAccessToken;
|
|
230
318
|
}
|
|
231
|
-
let controllerAccessToken =
|
|
232
|
-
normalizeToken(env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
233
|
-
normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
234
|
-
supabaseAccessToken;
|
|
319
|
+
let controllerAccessToken = resolveControllerAccessTokenForCli(options, env, supabaseAccessToken);
|
|
235
320
|
let runtimeAccessToken = normalizeToken(options.runtimeToken) ?? normalizeToken(env["RUNTIME_ACCESS_TOKEN"]);
|
|
236
321
|
const agentKey = options.controllerToken ??
|
|
237
322
|
env["INSTAFY_SERVICE_TOKEN"] ??
|
|
238
323
|
env["AGENT_LOGIN_KEY"] ??
|
|
239
324
|
env["AGENT_KEY"];
|
|
240
325
|
if (!agentKey && !controllerAccessToken && !runtimeAccessToken) {
|
|
241
|
-
throw new Error("
|
|
326
|
+
throw new Error("Login required. Run `instafy login`, or pass --access-token / --supabase-access-token, or provide --runtime-token.");
|
|
242
327
|
}
|
|
243
328
|
if (agentKey) {
|
|
244
329
|
env["AGENT_LOGIN_KEY"] = agentKey;
|
|
@@ -250,6 +335,7 @@ export async function runtimeStart(options) {
|
|
|
250
335
|
options.controllerUrl ??
|
|
251
336
|
env["INSTAFY_SERVER_URL"] ??
|
|
252
337
|
env["CONTROLLER_BASE_URL"] ??
|
|
338
|
+
manifestInfo.manifest?.controllerUrl ??
|
|
253
339
|
"http://127.0.0.1:8788";
|
|
254
340
|
if (!runtimeAccessToken && controllerAccessToken) {
|
|
255
341
|
runtimeAccessToken = await mintRuntimeAccessToken({
|
|
@@ -320,47 +406,123 @@ export async function runtimeStart(options) {
|
|
|
320
406
|
if (options.runtimeLeaseId && options.runtimeLeaseId.trim()) {
|
|
321
407
|
env["RUNTIME_LEASE_ID"] = options.runtimeLeaseId.trim();
|
|
322
408
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
409
|
+
const runtimeMode = (options.runtimeMode ?? "auto").toLowerCase();
|
|
410
|
+
const runtimeBin = runtimeMode === "docker" ? null : tryResolveRuntimeBinary();
|
|
411
|
+
const useDocker = runtimeMode === "docker" ||
|
|
412
|
+
(runtimeMode === "auto" && !runtimeBin);
|
|
413
|
+
if (!useDocker) {
|
|
414
|
+
if (!env["ORIGIN_ENDPOINT"]) {
|
|
415
|
+
const ratholeResolved = await resolveRatholeBinaryForCli({
|
|
416
|
+
env,
|
|
417
|
+
version: process.env["RATHOLE_VERSION"] ?? null,
|
|
418
|
+
cacheDir: process.env["RATHOLE_CACHE_DIR"] ?? null,
|
|
419
|
+
logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
|
|
420
|
+
warn: (message) => console.warn(kleur.yellow(message)),
|
|
421
|
+
});
|
|
422
|
+
if (!ratholeResolved) {
|
|
423
|
+
throw new Error("Tunnel is required but rathole is unavailable. Set RATHOLE_BIN (recommended) or pass --origin-endpoint to use a reachable URL.");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const bin = runtimeBin ?? resolveRuntimeBinary();
|
|
427
|
+
printStatus("Starting runtime-agent:", bin);
|
|
428
|
+
printStatus("Project:", projectId);
|
|
429
|
+
printStatus("Workspace:", workspace);
|
|
430
|
+
printStatus("Origin:", originId);
|
|
431
|
+
const detached = Boolean(options.detach);
|
|
432
|
+
const logFile = options.logFile?.trim()
|
|
433
|
+
? path.resolve(options.logFile)
|
|
434
|
+
: detached
|
|
435
|
+
? path.join(LOG_DIR, `runtime-${Date.now()}.log`)
|
|
436
|
+
: undefined;
|
|
437
|
+
if (logFile) {
|
|
438
|
+
ensureLogDir();
|
|
439
|
+
}
|
|
440
|
+
let stdio;
|
|
441
|
+
if (!logFile && !detached) {
|
|
442
|
+
stdio = "inherit";
|
|
333
443
|
}
|
|
444
|
+
else {
|
|
445
|
+
const out = logFile ? openSync(logFile, "a") : "ignore";
|
|
446
|
+
stdio = ["ignore", out, out];
|
|
447
|
+
}
|
|
448
|
+
const spawnOptions = { env, stdio, detached };
|
|
449
|
+
const child = spawn(bin, spawnOptions);
|
|
450
|
+
if (detached) {
|
|
451
|
+
child.unref();
|
|
452
|
+
}
|
|
453
|
+
const startedAt = new Date().toISOString();
|
|
454
|
+
writeState({
|
|
455
|
+
runner: "process",
|
|
456
|
+
pid: child.pid ?? -1,
|
|
457
|
+
projectId,
|
|
458
|
+
workspace,
|
|
459
|
+
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
460
|
+
originId,
|
|
461
|
+
runtimeId: env["RUNTIME_ID"] ?? null,
|
|
462
|
+
displayName: env["RUNTIME_DISPLAY_NAME"],
|
|
463
|
+
startedAt,
|
|
464
|
+
logFile,
|
|
465
|
+
detached,
|
|
466
|
+
});
|
|
467
|
+
child.on("exit", (code, signal) => {
|
|
468
|
+
const msg = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
469
|
+
console.log(kleur.yellow(`runtime-agent exited (${msg})`));
|
|
470
|
+
const current = readState();
|
|
471
|
+
if (current && current.pid === child.pid) {
|
|
472
|
+
clearState();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
334
476
|
}
|
|
335
|
-
|
|
477
|
+
if (!isDockerAvailable()) {
|
|
478
|
+
throw new Error("runtime-agent is not available (no local binary and docker is not installed). Install Docker Desktop, or build runtime-agent from source.");
|
|
479
|
+
}
|
|
480
|
+
const image = resolveRuntimeAgentImage();
|
|
481
|
+
const containerName = `instafy-runtime-${originId}`;
|
|
482
|
+
const hostPort = String(options.bindPort ?? env["ORIGIN_BIND_PORT"] ?? 54332);
|
|
483
|
+
const hostBindHost = options.bindHost ?? "127.0.0.1";
|
|
484
|
+
const dockerEnv = { ...env };
|
|
485
|
+
dockerEnv["WORKSPACE_DIR"] = "/workspace";
|
|
486
|
+
dockerEnv["CODEX_HOME"] = "/workspace/.codex";
|
|
487
|
+
dockerEnv["ORIGIN_BIND_HOST"] = "0.0.0.0";
|
|
488
|
+
dockerEnv["CONTROLLER_BASE_URL"] = normalizeControllerUrlForDocker(env["CONTROLLER_BASE_URL"] ?? "");
|
|
489
|
+
const envArgs = [];
|
|
490
|
+
for (const [key, value] of Object.entries(dockerEnv)) {
|
|
491
|
+
if (typeof value !== "string" || value.length === 0)
|
|
492
|
+
continue;
|
|
493
|
+
envArgs.push("-e", `${key}=${value}`);
|
|
494
|
+
}
|
|
495
|
+
const runArgs = [
|
|
496
|
+
"run",
|
|
497
|
+
"--detach",
|
|
498
|
+
"--name",
|
|
499
|
+
containerName,
|
|
500
|
+
"--add-host",
|
|
501
|
+
"host.docker.internal:host-gateway",
|
|
502
|
+
"-p",
|
|
503
|
+
`${hostBindHost}:${hostPort}:${hostPort}`,
|
|
504
|
+
"-v",
|
|
505
|
+
`${workspace}:/workspace`,
|
|
506
|
+
...envArgs,
|
|
507
|
+
image,
|
|
508
|
+
];
|
|
509
|
+
printStatus("Starting runtime-agent (docker):", image);
|
|
336
510
|
printStatus("Project:", projectId);
|
|
337
511
|
printStatus("Workspace:", workspace);
|
|
338
512
|
printStatus("Origin:", originId);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
:
|
|
343
|
-
? path.join(LOG_DIR, `runtime-${Date.now()}.log`)
|
|
344
|
-
: undefined;
|
|
345
|
-
if (logFile) {
|
|
346
|
-
ensureLogDir();
|
|
347
|
-
}
|
|
348
|
-
let stdio;
|
|
349
|
-
if (!logFile && !detached) {
|
|
350
|
-
stdio = "inherit";
|
|
513
|
+
const started = spawnSync("docker", runArgs, { encoding: "utf8" });
|
|
514
|
+
if (started.status !== 0) {
|
|
515
|
+
const stderr = String(started.stderr ?? "").trim();
|
|
516
|
+
throw new Error(`docker run failed: ${stderr || "unknown error"}`);
|
|
351
517
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
const spawnOptions = { env, stdio, detached };
|
|
357
|
-
const child = spawn(bin, spawnOptions);
|
|
358
|
-
if (detached) {
|
|
359
|
-
child.unref();
|
|
518
|
+
const containerId = String(started.stdout ?? "").trim();
|
|
519
|
+
if (!containerId) {
|
|
520
|
+
throw new Error("docker run did not return a container id");
|
|
360
521
|
}
|
|
361
522
|
const startedAt = new Date().toISOString();
|
|
362
523
|
writeState({
|
|
363
|
-
|
|
524
|
+
runner: "docker",
|
|
525
|
+
pid: -1,
|
|
364
526
|
projectId,
|
|
365
527
|
workspace,
|
|
366
528
|
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
@@ -368,22 +530,40 @@ export async function runtimeStart(options) {
|
|
|
368
530
|
runtimeId: env["RUNTIME_ID"] ?? null,
|
|
369
531
|
displayName: env["RUNTIME_DISPLAY_NAME"],
|
|
370
532
|
startedAt,
|
|
371
|
-
|
|
372
|
-
|
|
533
|
+
detached: Boolean(options.detach),
|
|
534
|
+
containerId,
|
|
535
|
+
containerName,
|
|
373
536
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
console.log(kleur.
|
|
537
|
+
if (options.detach) {
|
|
538
|
+
printStatus("Container:", containerId);
|
|
539
|
+
console.log(kleur.gray(`Use: docker logs -f ${containerId}`));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const logs = spawn("docker", ["logs", "-f", containerId], { stdio: "inherit" });
|
|
543
|
+
const handleExit = async () => {
|
|
544
|
+
stopDockerContainer(containerId);
|
|
377
545
|
const current = readState();
|
|
378
|
-
if (current
|
|
546
|
+
if (current?.containerId === containerId) {
|
|
379
547
|
clearState();
|
|
380
548
|
}
|
|
549
|
+
};
|
|
550
|
+
process.once("SIGINT", () => {
|
|
551
|
+
void handleExit();
|
|
552
|
+
});
|
|
553
|
+
process.once("SIGTERM", () => {
|
|
554
|
+
void handleExit();
|
|
555
|
+
});
|
|
556
|
+
logs.on("exit", () => {
|
|
557
|
+
void handleExit();
|
|
381
558
|
});
|
|
382
559
|
}
|
|
383
560
|
function formatStatus(state, running) {
|
|
384
561
|
return {
|
|
385
562
|
running,
|
|
563
|
+
runner: state.runner ?? "process",
|
|
386
564
|
pid: state.pid,
|
|
565
|
+
containerId: state.containerId ?? null,
|
|
566
|
+
containerName: state.containerName ?? null,
|
|
387
567
|
projectId: state.projectId,
|
|
388
568
|
workspace: state.workspace,
|
|
389
569
|
controllerUrl: state.controllerUrl ?? null,
|
|
@@ -404,7 +584,8 @@ async function sendOfflineBeat(state) {
|
|
|
404
584
|
let bearer = directToken;
|
|
405
585
|
if (!bearer) {
|
|
406
586
|
const controllerAccessToken = normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
407
|
-
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"])
|
|
587
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
|
|
588
|
+
resolveConfiguredAccessToken();
|
|
408
589
|
if (controllerAccessToken) {
|
|
409
590
|
try {
|
|
410
591
|
bearer = await mintRuntimeAccessToken({
|
|
@@ -465,14 +646,25 @@ export async function runtimeStatus(options) {
|
|
|
465
646
|
}
|
|
466
647
|
return;
|
|
467
648
|
}
|
|
468
|
-
const alive =
|
|
649
|
+
const alive = state.runner === "docker"
|
|
650
|
+
? Boolean(state.containerId && dockerContainerRunning(state.containerId))
|
|
651
|
+
: isProcessAlive(state.pid);
|
|
469
652
|
const payload = formatStatus(state, alive);
|
|
470
653
|
if (options?.json) {
|
|
471
654
|
console.log(JSON.stringify(payload));
|
|
472
655
|
return;
|
|
473
656
|
}
|
|
474
657
|
console.log(alive ? kleur.green("Runtime is running") : kleur.red("Runtime is not running"));
|
|
475
|
-
printStatus("
|
|
658
|
+
printStatus("Runner:", state.runner ?? "process");
|
|
659
|
+
if (state.runner === "docker") {
|
|
660
|
+
if (state.containerId)
|
|
661
|
+
printStatus("Container:", state.containerId);
|
|
662
|
+
if (state.containerName)
|
|
663
|
+
printStatus("Name:", state.containerName);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
printStatus("PID:", String(state.pid));
|
|
667
|
+
}
|
|
476
668
|
printStatus("Project:", state.projectId);
|
|
477
669
|
printStatus("Workspace:", state.workspace);
|
|
478
670
|
if (state.controllerUrl)
|
|
@@ -482,13 +674,13 @@ export async function runtimeStatus(options) {
|
|
|
482
674
|
if (state.runtimeId)
|
|
483
675
|
printStatus("Runtime:", state.runtimeId);
|
|
484
676
|
if (state.displayName)
|
|
485
|
-
printStatus("
|
|
677
|
+
printStatus("Display:", state.displayName);
|
|
486
678
|
printStatus("Started:", state.startedAt);
|
|
487
679
|
if (state.logFile)
|
|
488
680
|
printStatus("Log:", state.logFile);
|
|
489
681
|
printStatus("Detached:", state.detached ? "yes" : "no");
|
|
490
682
|
if (!alive) {
|
|
491
|
-
console.log(kleur.yellow(
|
|
683
|
+
console.log(kleur.yellow(`State file exists but ${state.runner === "docker" ? "container" : "process"} is not alive.`));
|
|
492
684
|
}
|
|
493
685
|
}
|
|
494
686
|
export async function runtimeStop(options) {
|
|
@@ -502,6 +694,22 @@ export async function runtimeStop(options) {
|
|
|
502
694
|
}
|
|
503
695
|
return;
|
|
504
696
|
}
|
|
697
|
+
if (state.runner === "docker") {
|
|
698
|
+
const running = Boolean(state.containerId && dockerContainerRunning(state.containerId));
|
|
699
|
+
if (state.containerId) {
|
|
700
|
+
stopDockerContainer(state.containerId);
|
|
701
|
+
}
|
|
702
|
+
clearState();
|
|
703
|
+
await sendOfflineBeat(state);
|
|
704
|
+
const result = { stopped: running, containerId: state.containerId ?? null };
|
|
705
|
+
if (options?.json) {
|
|
706
|
+
console.log(JSON.stringify(result));
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
console.log(running ? kleur.green("Runtime stopped.") : kleur.yellow("Runtime not running."));
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
505
713
|
if (!isProcessAlive(state.pid)) {
|
|
506
714
|
clearState();
|
|
507
715
|
if (options?.json) {
|
|
@@ -552,9 +760,10 @@ export async function runtimeToken(options) {
|
|
|
552
760
|
const token = options.controllerAccessToken ??
|
|
553
761
|
process.env["INSTAFY_ACCESS_TOKEN"] ??
|
|
554
762
|
process.env["CONTROLLER_ACCESS_TOKEN"] ??
|
|
555
|
-
process.env["SUPABASE_ACCESS_TOKEN"]
|
|
763
|
+
process.env["SUPABASE_ACCESS_TOKEN"] ??
|
|
764
|
+
resolveConfiguredAccessToken();
|
|
556
765
|
if (!token) {
|
|
557
|
-
throw new Error("
|
|
766
|
+
throw new Error("Login required. Run `instafy login` or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
558
767
|
}
|
|
559
768
|
const minted = await mintRuntimeAccessToken({
|
|
560
769
|
controllerUrl,
|
package/dist/tunnel.js
CHANGED
|
@@ -3,7 +3,8 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import kleur from "kleur";
|
|
6
|
-
import { resolveRatholeBinaryForCli } from "./runtime.js";
|
|
6
|
+
import { findProjectManifest, resolveRatholeBinaryForCli } from "./runtime.js";
|
|
7
|
+
import { resolveConfiguredControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
7
8
|
function cleanUrl(raw) {
|
|
8
9
|
return raw.replace(/\/+$/, "");
|
|
9
10
|
}
|
|
@@ -14,31 +15,59 @@ function readEnv(key) {
|
|
|
14
15
|
return undefined;
|
|
15
16
|
}
|
|
16
17
|
function resolveProject(opts) {
|
|
17
|
-
|
|
18
|
+
const explicit = opts.project?.trim() ||
|
|
18
19
|
readEnv("PROJECT_ID") ||
|
|
19
20
|
readEnv("CONTROLLER_PROJECT_ID") ||
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
null;
|
|
22
|
+
if (explicit) {
|
|
23
|
+
return explicit;
|
|
24
|
+
}
|
|
25
|
+
const manifest = findProjectManifest(process.cwd()).manifest;
|
|
26
|
+
if (manifest?.projectId) {
|
|
27
|
+
return manifest.projectId;
|
|
28
|
+
}
|
|
29
|
+
throw new Error("No project configured. Run `instafy project:init` or pass --project.");
|
|
23
30
|
}
|
|
24
31
|
function resolveControllerUrl(opts) {
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const explicit = opts.controllerUrl?.trim();
|
|
33
|
+
if (explicit) {
|
|
34
|
+
return explicit;
|
|
35
|
+
}
|
|
36
|
+
const envUrl = readEnv("INSTAFY_SERVER_URL") ||
|
|
27
37
|
readEnv("INSTAFY_URL") ||
|
|
28
38
|
readEnv("CONTROLLER_URL") ||
|
|
29
39
|
readEnv("CONTROLLER_BASE_URL") ||
|
|
30
|
-
|
|
40
|
+
null;
|
|
41
|
+
if (envUrl) {
|
|
42
|
+
return envUrl;
|
|
43
|
+
}
|
|
44
|
+
const manifestControllerUrl = findProjectManifest(process.cwd()).manifest?.controllerUrl?.trim() ?? null;
|
|
45
|
+
if (manifestControllerUrl) {
|
|
46
|
+
return manifestControllerUrl;
|
|
47
|
+
}
|
|
48
|
+
return resolveConfiguredControllerUrl() ?? "http://127.0.0.1:8788";
|
|
31
49
|
}
|
|
32
50
|
function resolveControllerToken(opts) {
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const explicit = opts.controllerToken?.trim();
|
|
52
|
+
if (explicit) {
|
|
53
|
+
return explicit;
|
|
54
|
+
}
|
|
55
|
+
const accessToken = resolveUserAccessToken();
|
|
56
|
+
if (accessToken) {
|
|
57
|
+
return accessToken;
|
|
58
|
+
}
|
|
59
|
+
const serviceToken = readEnv("INSTAFY_SERVICE_TOKEN") ||
|
|
35
60
|
readEnv("CONTROLLER_BEARER") ||
|
|
61
|
+
readEnv("CONTROLLER_TOKEN") ||
|
|
36
62
|
readEnv("SERVICE_ROLE_KEY") ||
|
|
63
|
+
readEnv("CONTROLLER_SERVICE_ROLE_KEY") ||
|
|
37
64
|
readEnv("SUPABASE_SERVICE_ROLE_KEY") ||
|
|
38
65
|
readEnv("CONTROLLER_INTERNAL_TOKEN") ||
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
null;
|
|
67
|
+
if (serviceToken) {
|
|
68
|
+
return serviceToken;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Login required. Run `instafy login`, pass --access-token, or provide --service-token / INSTAFY_SERVICE_TOKEN.");
|
|
42
71
|
}
|
|
43
72
|
function resolvePort(opts) {
|
|
44
73
|
const fromEnv = readEnv("WEBHOOK_LOCAL_PORT") ||
|