@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 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("Access token is required (--access-token/INSTAFY_ACCESS_TOKEN or --supabase-access-token/SUPABASE_ACCESS_TOKEN).");
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
- .requiredOption("--project <id>", "Project UUID");
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: opts.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
- .requiredOption("--project <id>", "Project UUID");
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: opts.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("--project <id>", "Project UUID (or set PROJECT_ID)");
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 ?? opts.controllerToken,
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("Perform an authenticated GET request to the controller API")
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("Perform an authenticated POST request to the controller API")
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("Perform an authenticated PATCH request to the controller API")
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("Perform an authenticated DELETE request to the controller API")
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
- function normalizeUrl(raw) {
3
- const value = (raw ?? "").trim();
4
- if (!value)
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 = normalizeUrl(params.controllerUrl ?? process.env["INSTAFY_SERVER_URL"] ?? process.env["CONTROLLER_BASE_URL"]);
16
- const token = normalizeToken(params.accessToken) ??
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 new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
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
- function normalizeUrl(raw) {
5
- const value = (raw ?? "").trim();
6
- if (!value)
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 = normalizeUrl(options.controllerUrl ?? process.env["INSTAFY_SERVER_URL"] ?? process.env["CONTROLLER_BASE_URL"]);
77
- const token = normalizeToken(options.accessToken) ??
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 new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
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 = normalizeUrl(options.controllerUrl ?? process.env["INSTAFY_SERVER_URL"] ?? process.env["CONTROLLER_BASE_URL"]);
128
- const token = normalizeToken(options.accessToken) ??
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 new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
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. Build it with `cargo build -p runtime-agent`.");
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 && isProcessAlive(existing.pid)) {
219
- throw new Error(`Runtime already running (pid ${existing.pid}) for project ${existing.projectId}. Stop it first.`);
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(options.workspace ?? process.cwd());
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("Project ID is required (--project or PROJECT_ID)");
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 = normalizeToken(options.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("Provide either --runtime-token/RUNTIME_ACCESS_TOKEN, --access-token/INSTAFY_ACCESS_TOKEN, --supabase-access-token/SUPABASE_ACCESS_TOKEN, or --service-token/INSTAFY_SERVICE_TOKEN (advanced).");
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
- if (!env["ORIGIN_ENDPOINT"]) {
324
- const ratholeResolved = await resolveRatholeBinaryForCli({
325
- env,
326
- version: process.env["RATHOLE_VERSION"] ?? null,
327
- cacheDir: process.env["RATHOLE_CACHE_DIR"] ?? null,
328
- logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
329
- warn: (message) => console.warn(kleur.yellow(message)),
330
- });
331
- if (!ratholeResolved) {
332
- throw new Error("Tunnel is required but rathole is unavailable. Set RATHOLE_BIN (recommended) or pass --origin-endpoint to use a reachable URL.");
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
- printStatus("Starting runtime-agent:", bin);
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 detached = Boolean(options.detach);
340
- const logFile = options.logFile?.trim()
341
- ? path.resolve(options.logFile)
342
- : detached
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
- else {
353
- const out = logFile ? openSync(logFile, "a") : "ignore";
354
- stdio = ["ignore", out, out];
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
- pid: child.pid ?? -1,
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
- logFile,
372
- detached,
533
+ detached: Boolean(options.detach),
534
+ containerId,
535
+ containerName,
373
536
  });
374
- child.on("exit", (code, signal) => {
375
- const msg = code !== null ? `code ${code}` : `signal ${signal}`;
376
- console.log(kleur.yellow(`runtime-agent exited (${msg})`));
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 && current.pid === child.pid) {
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 = isProcessAlive(state.pid);
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("PID:", String(state.pid));
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("Name:", state.displayName);
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("State file exists but process is not alive."));
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("Access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
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
- return (opts.project?.trim() ||
18
+ const explicit = opts.project?.trim() ||
18
19
  readEnv("PROJECT_ID") ||
19
20
  readEnv("CONTROLLER_PROJECT_ID") ||
20
- (() => {
21
- throw new Error("Project id is required (--project or PROJECT_ID)");
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
- return (opts.controllerUrl?.trim() ||
26
- readEnv("INSTAFY_SERVER_URL") ||
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
- "http://127.0.0.1:8788");
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
- return (opts.controllerToken?.trim() ||
34
- readEnv("INSTAFY_SERVICE_TOKEN") ||
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
- throw new Error("Service token is required (--service-token, INSTAFY_SERVICE_TOKEN, or SERVICE_ROLE_KEY).");
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") ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {