@instafy/cli 0.1.8-staging.348 → 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
@@ -7,7 +7,6 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
7
7
  ## Quickstart
8
8
 
9
9
  0. Log in once: `instafy login`
10
- - Also enables Git auth (credential helper) for Instafy Git Service. Disable with `instafy login --no-git-setup`.
11
10
  - Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
12
11
  1. Link a folder to a project:
13
12
  - VS Code: install the Instafy extension and run `Instafy: Link Workspace to Project`, or
@@ -22,10 +21,7 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
22
21
  - `instafy runtime:start` — start a local runtime (agent + origin).
23
22
  - `instafy runtime:status` — show health of the last started runtime.
24
23
  - `instafy runtime:stop` — stop the last started runtime.
25
- - `instafy tunnel` — start a detached tunnel for a local port.
26
- - `instafy tunnel:list` — list local tunnels started by the CLI.
27
- - `instafy tunnel:logs <tunnelId> --follow` — tail tunnel logs.
28
- - `instafy tunnel:stop <tunnelId>` — stop + revoke a tunnel.
24
+ - `instafy tunnel` — request a tunnel and forward a local port.
29
25
  - `instafy api:get` — query controller endpoints (conversations, messages, runs, etc).
30
26
 
31
27
  Run `instafy --help` for the full command list and options.
package/dist/auth.js CHANGED
@@ -2,7 +2,6 @@ import kleur from "kleur";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
4
  import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
5
- import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
6
5
  function normalizeUrl(raw) {
7
6
  const trimmed = typeof raw === "string" ? raw.trim() : "";
8
7
  if (!trimmed) {
@@ -75,17 +74,6 @@ export async function login(options) {
75
74
  writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
76
75
  console.log("");
77
76
  console.log(kleur.green(`Saved token to ${getInstafyConfigPath()}`));
78
- if (options.gitSetup !== false) {
79
- try {
80
- const result = installGitCredentialHelper();
81
- if (result.changed) {
82
- console.log(kleur.green("Enabled git auth (credential helper installed)."));
83
- }
84
- }
85
- catch (error) {
86
- console.log(kleur.yellow(`Warning: failed to configure git credential helper: ${error instanceof Error ? error.message : String(error)}`));
87
- }
88
- }
89
77
  }
90
78
  else if (existing) {
91
79
  console.log("");
@@ -98,12 +86,6 @@ export async function login(options) {
98
86
  }
99
87
  export async function logout(options) {
100
88
  clearInstafyCliConfig(["accessToken"]);
101
- try {
102
- uninstallGitCredentialHelper();
103
- }
104
- catch {
105
- // ignore git helper cleanup failures
106
- }
107
89
  if (options?.json) {
108
90
  console.log(JSON.stringify({ ok: true }));
109
91
  return;
package/dist/git.js CHANGED
@@ -43,7 +43,6 @@ export async function mintGitAccessToken(params) {
43
43
  authorization: `Bearer ${params.controllerAccessToken}`,
44
44
  "content-type": "application/json",
45
45
  },
46
- signal: params.signal,
47
46
  body: JSON.stringify({
48
47
  scopes,
49
48
  ttlSeconds: params.ttlSeconds,
package/dist/index.js CHANGED
@@ -5,9 +5,8 @@ import kleur from "kleur";
5
5
  import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, findProjectManifest, } from "./runtime.js";
6
6
  import { login, logout } from "./auth.js";
7
7
  import { gitToken } from "./git.js";
8
- import { runGitCredentialHelper } from "./git-credential.js";
9
8
  import { projectInit } from "./project.js";
10
- import { listTunnelSessions, startTunnelDetached, stopTunnelSession, tailTunnelLogs, runTunnelCommand } from "./tunnel.js";
9
+ import { runTunnelCommand } from "./tunnel.js";
11
10
  import { requestControllerApi } from "./api.js";
12
11
  import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
13
12
  export const program = new Command();
@@ -41,7 +40,6 @@ program
41
40
  .option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
42
41
  .option("--server-url <url>", "Instafy server/controller URL")
43
42
  .option("--token <token>", "Provide token directly (skips prompt)")
44
- .option("--no-git-setup", "Do not configure git credential helper")
45
43
  .option("--no-store", "Do not save token to ~/.instafy/config.json")
46
44
  .option("--json", "Output JSON")
47
45
  .action(async (opts) => {
@@ -50,7 +48,6 @@ program
50
48
  controllerUrl: opts.serverUrl,
51
49
  studioUrl: opts.studioUrl,
52
50
  token: opts.token,
53
- gitSetup: opts.gitSetup,
54
51
  noStore: opts.store === false,
55
52
  json: opts.json,
56
53
  });
@@ -282,19 +279,6 @@ gitTokenCommand
282
279
  process.exit(1);
283
280
  }
284
281
  });
285
- program
286
- .command("git:credential", { hidden: true })
287
- .description("Internal: git credential helper (used by Git when configured)")
288
- .argument("<operation>", "Operation (get|store|erase)")
289
- .action(async (operation) => {
290
- try {
291
- await runGitCredentialHelper(operation);
292
- }
293
- catch (error) {
294
- console.error(String(error));
295
- process.exit(1);
296
- }
297
- });
298
282
  const projectInitCommand = program
299
283
  .command("project:init")
300
284
  .description("Create an Instafy project and link this folder (.instafy/project.json)")
@@ -329,130 +313,26 @@ projectInitCommand
329
313
  });
330
314
  const tunnelCommand = program
331
315
  .command("tunnel")
332
- .description("Start a shareable tunnel URL for a local port (defaults to detached)")
316
+ .description("Create a shareable tunnel URL for a local port")
333
317
  .option("--port <port>", "Local port to expose (default 3000)")
334
318
  .option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
335
319
  addServerUrlOptions(tunnelCommand);
336
320
  addAccessTokenOptions(tunnelCommand, "Instafy access token (defaults to saved `instafy login` token)");
337
321
  addServiceTokenOptions(tunnelCommand, "Instafy service token (advanced)");
338
322
  tunnelCommand
339
- .option("--no-detach", "Run in foreground until interrupted")
340
323
  .option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)")
341
- .option("--log-file <path>", "Write tunnel logs to a file (default: ~/.instafy/cli-tunnel-logs/*)")
342
- .option("--json", "Output JSON")
343
324
  .action(async (opts) => {
344
325
  try {
345
326
  const port = opts.port ? Number(opts.port) : undefined;
346
- const controllerToken = opts.serviceToken ??
347
- opts.controllerToken ??
348
- opts.accessToken ??
349
- opts.controllerAccessToken;
350
- if (opts.detach === false) {
351
- await runTunnelCommand({
352
- project: opts.project,
353
- controllerUrl: opts.serverUrl ?? opts.controllerUrl,
354
- controllerToken,
355
- port,
356
- ratholeBin: opts.ratholeBin,
357
- });
358
- return;
359
- }
360
- const started = await startTunnelDetached({
327
+ await runTunnelCommand({
361
328
  project: opts.project,
362
329
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
363
- controllerToken,
330
+ controllerToken: opts.serviceToken ??
331
+ opts.controllerToken ??
332
+ opts.accessToken ??
333
+ opts.controllerAccessToken,
364
334
  port,
365
335
  ratholeBin: opts.ratholeBin,
366
- logFile: opts.logFile,
367
- });
368
- if (opts.json) {
369
- console.log(JSON.stringify(started, null, 2));
370
- return;
371
- }
372
- console.log(kleur.green(`Tunnel started: ${started.url} (tunnelId=${started.tunnelId})`));
373
- console.log(kleur.gray(`pid=${started.pid} · port=${started.localPort}`));
374
- console.log(kleur.gray(`log: ${started.logFile}`));
375
- console.log("");
376
- console.log("Next:");
377
- console.log(`- ${kleur.cyan(`instafy tunnel:list`)}`);
378
- console.log(`- ${kleur.cyan(`instafy tunnel:logs ${started.tunnelId} --follow`)}`);
379
- console.log(`- ${kleur.cyan(`instafy tunnel:stop ${started.tunnelId}`)}`);
380
- if (process.platform !== "win32") {
381
- console.log(kleur.gray(`(or) tail -n 200 -f ${started.logFile}`));
382
- }
383
- }
384
- catch (error) {
385
- console.error(kleur.red(String(error)));
386
- process.exit(1);
387
- }
388
- });
389
- program
390
- .command("tunnel:list")
391
- .description("List local tunnel sessions started by this CLI")
392
- .option("--all", "Include stopped/stale tunnels")
393
- .option("--json", "Output JSON")
394
- .action(async (opts) => {
395
- try {
396
- const tunnels = listTunnelSessions({ all: Boolean(opts.all), json: Boolean(opts.json) });
397
- if (opts.json) {
398
- console.log(JSON.stringify(tunnels, null, 2));
399
- return;
400
- }
401
- if (tunnels.length === 0) {
402
- console.log(kleur.yellow("No tunnels found."));
403
- return;
404
- }
405
- for (const tunnel of tunnels) {
406
- console.log(`${tunnel.tunnelId} · ${tunnel.url} · port=${tunnel.localPort} · pid=${tunnel.pid}`);
407
- }
408
- }
409
- catch (error) {
410
- console.error(kleur.red(String(error)));
411
- process.exit(1);
412
- }
413
- });
414
- program
415
- .command("tunnel:stop")
416
- .description("Stop a local tunnel session and revoke it")
417
- .argument("[tunnelId]", "Tunnel ID (defaults to the only active tunnel)")
418
- .option("--server-url <url>", "Instafy server URL")
419
- .option("--access-token <token>", "Instafy access token (defaults to saved `instafy login` token)")
420
- .option("--service-token <token>", "Instafy service token (advanced)")
421
- .option("--json", "Output JSON")
422
- .action(async (tunnelId, opts) => {
423
- try {
424
- const result = await stopTunnelSession({
425
- tunnelId,
426
- controllerUrl: opts.serverUrl,
427
- controllerToken: opts.serviceToken ?? opts.accessToken,
428
- json: opts.json,
429
- });
430
- if (opts.json) {
431
- console.log(JSON.stringify(result, null, 2));
432
- return;
433
- }
434
- console.log(kleur.green(`Tunnel stopped: ${result.tunnelId}`));
435
- }
436
- catch (error) {
437
- console.error(kleur.red(String(error)));
438
- process.exit(1);
439
- }
440
- });
441
- program
442
- .command("tunnel:logs")
443
- .description("Show logs for a local tunnel session")
444
- .argument("[tunnelId]", "Tunnel ID (defaults to the only active tunnel)")
445
- .option("--lines <n>", "Number of lines to show", "200")
446
- .option("--follow", "Follow log output (like tail -f)")
447
- .option("--json", "Output JSON")
448
- .action(async (tunnelId, opts) => {
449
- try {
450
- const lines = typeof opts.lines === "string" ? Number(opts.lines) : undefined;
451
- await tailTunnelLogs({
452
- tunnelId,
453
- lines,
454
- follow: Boolean(opts.follow),
455
- json: Boolean(opts.json),
456
336
  });
457
337
  }
458
338
  catch (error) {
package/dist/org.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import kleur from "kleur";
2
2
  import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
3
- import { formatAuthRequiredError } from "./errors.js";
3
+ function formatAuthRequiredError() {
4
+ return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
5
+ }
4
6
  export async function listOrganizations(params) {
5
7
  const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
6
8
  const token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
7
9
  if (!token) {
8
- throw formatAuthRequiredError({ retryCommand: "instafy org:list" });
10
+ throw formatAuthRequiredError();
9
11
  }
10
12
  const response = await fetch(`${controllerUrl}/orgs`, {
11
13
  headers: { authorization: `Bearer ${token}` },
package/dist/project.js CHANGED
@@ -2,7 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
4
  import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
5
- import { formatAuthRequiredError } from "./errors.js";
5
+ function formatAuthRequiredError() {
6
+ return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
7
+ }
6
8
  async function fetchOrganizations(controllerUrl, token) {
7
9
  const response = await fetch(`${controllerUrl}/orgs`, {
8
10
  headers: {
@@ -66,7 +68,7 @@ export async function listProjects(options) {
66
68
  const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
67
69
  const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
68
70
  if (!token) {
69
- throw formatAuthRequiredError({ retryCommand: "instafy project:list" });
71
+ throw formatAuthRequiredError();
70
72
  }
71
73
  const orgs = await fetchOrganizations(controllerUrl, token);
72
74
  let targetOrgs = orgs;
@@ -114,10 +116,7 @@ export async function projectInit(options) {
114
116
  const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
115
117
  const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
116
118
  if (!token) {
117
- throw formatAuthRequiredError({
118
- retryCommand: "instafy project:init",
119
- advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
120
- });
119
+ throw formatAuthRequiredError();
121
120
  }
122
121
  const org = await resolveOrg(controllerUrl, token, options);
123
122
  const body = {
package/dist/tunnel.js CHANGED
@@ -2,15 +2,9 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
- import { randomUUID } from "node:crypto";
6
5
  import kleur from "kleur";
7
6
  import { findProjectManifest, resolveRatholeBinaryForCli } from "./runtime.js";
8
7
  import { resolveConfiguredControllerUrl, resolveUserAccessToken } from "./config.js";
9
- import { formatAuthRequiredError } from "./errors.js";
10
- const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
11
- const TUNNEL_STATE_FILE = path.join(INSTAFY_DIR, "cli-tunnel-state.json");
12
- const TUNNEL_LOG_DIR = path.join(INSTAFY_DIR, "cli-tunnel-logs");
13
- const TUNNEL_WORKDIR_DIR = path.join(INSTAFY_DIR, "cli-tunnel-workdirs");
14
8
  function cleanUrl(raw) {
15
9
  return raw.replace(/\/+$/, "");
16
10
  }
@@ -32,7 +26,7 @@ function resolveProject(opts) {
32
26
  if (manifest?.projectId) {
33
27
  return manifest.projectId;
34
28
  }
35
- throw new Error("No project configured for this folder.\n\nNext:\n- instafy project:init\n\nOr pass --project <uuid> / set PROJECT_ID.");
29
+ throw new Error("No project configured. Run `instafy project:init` or pass --project.");
36
30
  }
37
31
  function resolveControllerUrl(opts) {
38
32
  const explicit = opts.controllerUrl?.trim();
@@ -73,10 +67,7 @@ function resolveControllerToken(opts) {
73
67
  if (serviceToken) {
74
68
  return serviceToken;
75
69
  }
76
- throw formatAuthRequiredError({
77
- retryCommand: "instafy tunnel",
78
- advancedHint: "pass --access-token / --service-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN / INSTAFY_SERVICE_TOKEN",
79
- });
70
+ throw new Error("Login required. Run `instafy login`, pass --access-token, or provide --service-token / INSTAFY_SERVICE_TOKEN.");
80
71
  }
81
72
  function resolvePort(opts) {
82
73
  const fromEnv = readEnv("WEBHOOK_LOCAL_PORT") ||
@@ -85,77 +76,6 @@ function resolvePort(opts) {
85
76
  const parsedEnv = fromEnv ? Number(fromEnv) : NaN;
86
77
  return opts.port ?? (Number.isFinite(parsedEnv) ? parsedEnv : 3000);
87
78
  }
88
- function ensureDir(dir) {
89
- fs.mkdirSync(dir, { recursive: true });
90
- }
91
- function safePathSegment(value) {
92
- const trimmed = value.trim();
93
- if (!trimmed)
94
- return randomUUID();
95
- return trimmed.replace(/[^a-zA-Z0-9._-]/g, "_");
96
- }
97
- function writeStateFile(state) {
98
- ensureDir(INSTAFY_DIR);
99
- fs.writeFileSync(TUNNEL_STATE_FILE, JSON.stringify(state, null, 2), "utf8");
100
- try {
101
- fs.chmodSync(TUNNEL_STATE_FILE, 0o600);
102
- }
103
- catch {
104
- // ignore chmod failures (windows / unusual fs)
105
- }
106
- }
107
- function readStateFile() {
108
- try {
109
- const raw = fs.readFileSync(TUNNEL_STATE_FILE, "utf8");
110
- const parsed = JSON.parse(raw);
111
- if (!parsed || typeof parsed !== "object")
112
- return { version: 1, tunnels: [] };
113
- if (!Array.isArray(parsed.tunnels))
114
- return { version: 1, tunnels: [] };
115
- return {
116
- version: 1,
117
- tunnels: parsed.tunnels.filter(Boolean),
118
- };
119
- }
120
- catch {
121
- return { version: 1, tunnels: [] };
122
- }
123
- }
124
- function isProcessAlive(pid) {
125
- if (!Number.isFinite(pid) || pid <= 0)
126
- return false;
127
- try {
128
- process.kill(pid, 0);
129
- return true;
130
- }
131
- catch (error) {
132
- const code = error instanceof Error ? error.code : null;
133
- // EPERM means the process exists but we don't have permission to signal it.
134
- return code === "EPERM";
135
- }
136
- }
137
- async function waitForProcessExit(pid, timeoutMs) {
138
- const deadline = Date.now() + timeoutMs;
139
- while (Date.now() < deadline) {
140
- if (!isProcessAlive(pid))
141
- return true;
142
- await new Promise((resolve) => setTimeout(resolve, 100));
143
- }
144
- return !isProcessAlive(pid);
145
- }
146
- function upsertTunnelState(entry) {
147
- const state = readStateFile();
148
- const next = state.tunnels.filter((tunnel) => tunnel.tunnelId !== entry.tunnelId);
149
- next.unshift(entry);
150
- writeStateFile({ version: 1, tunnels: next });
151
- }
152
- function removeTunnelState(tunnelId) {
153
- const state = readStateFile();
154
- const existing = state.tunnels.find((tunnel) => tunnel.tunnelId === tunnelId) ?? null;
155
- const next = state.tunnels.filter((tunnel) => tunnel.tunnelId !== tunnelId);
156
- writeStateFile({ version: 1, tunnels: next });
157
- return existing;
158
- }
159
79
  async function requestTunnel(controllerUrl, token, projectId, metadata) {
160
80
  const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/request`;
161
81
  const response = await fetch(target, {
@@ -290,181 +210,3 @@ export async function runTunnelCommand(opts, options) {
290
210
  process.once("SIGTERM", handle);
291
211
  });
292
212
  }
293
- export async function startTunnelDetached(opts) {
294
- const projectId = resolveProject(opts);
295
- const controllerUrl = resolveControllerUrl(opts);
296
- const controllerToken = resolveControllerToken(opts);
297
- const port = resolvePort(opts);
298
- if (opts.ratholeBin) {
299
- process.env.RATHOLE_BIN = opts.ratholeBin;
300
- }
301
- const rathole = await resolveRatholeBinaryForCli({
302
- env: process.env,
303
- version: process.env.RATHOLE_VERSION ?? null,
304
- cacheDir: process.env.RATHOLE_CACHE_DIR ?? null,
305
- logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
306
- warn: (message) => console.warn(kleur.yellow(message)),
307
- });
308
- if (!rathole) {
309
- throw new Error("rathole is required to start a tunnel. Set RATHOLE_BIN or ensure it is on PATH.");
310
- }
311
- const metadata = { localPort: port, source: "instafy-cli" };
312
- const grant = await requestTunnel(controllerUrl, controllerToken, projectId, metadata);
313
- ensureDir(TUNNEL_WORKDIR_DIR);
314
- ensureDir(TUNNEL_LOG_DIR);
315
- const tunnelIdSafe = safePathSegment(grant.tunnelId);
316
- const workdir = path.join(TUNNEL_WORKDIR_DIR, tunnelIdSafe);
317
- ensureDir(workdir);
318
- const creds = grant.credentials && typeof grant.credentials === "object"
319
- ? grant.credentials
320
- : {};
321
- const configBody = buildRatholeConfig(creds, port);
322
- const configPath = path.join(workdir, "rathole.toml");
323
- fs.writeFileSync(configPath, configBody, { encoding: "utf8", mode: 0o600 });
324
- const logFile = opts.logFile?.trim()
325
- ? path.resolve(opts.logFile.trim())
326
- : path.join(TUNNEL_LOG_DIR, `tunnel-${tunnelIdSafe}.log`);
327
- const logFd = fs.openSync(logFile, "a");
328
- const child = spawn(rathole, ["-c", configPath], {
329
- stdio: ["ignore", logFd, logFd],
330
- cwd: workdir,
331
- detached: true,
332
- });
333
- let exitedEarly = false;
334
- let exitMessage = "";
335
- child.on("exit", (code, signal) => {
336
- exitedEarly = true;
337
- exitMessage = code !== null ? `code ${code}` : `signal ${signal}`;
338
- });
339
- // Give rathole a moment to fail fast, so `instafy tunnel` can report errors.
340
- await new Promise((resolve) => setTimeout(resolve, 250));
341
- if (exitedEarly || !isProcessAlive(child.pid ?? -1)) {
342
- const logTail = (() => {
343
- try {
344
- const text = fs.readFileSync(logFile, "utf8");
345
- const lines = text.split(/\r?\n/).filter(Boolean);
346
- return lines.slice(-20).join("\n");
347
- }
348
- catch {
349
- return "";
350
- }
351
- })();
352
- const suffix = logTail ? `\n\nLast logs:\n${logTail}` : "";
353
- throw new Error(`Tunnel process exited (${exitMessage || "unknown"}).${suffix}`);
354
- }
355
- child.unref();
356
- const entry = {
357
- tunnelId: grant.tunnelId,
358
- projectId,
359
- hostname: grant.hostname,
360
- url: grant.url ?? `https://${grant.hostname}`,
361
- localPort: port,
362
- controllerUrl: cleanUrl(controllerUrl),
363
- pid: child.pid ?? -1,
364
- logFile,
365
- workdir,
366
- startedAt: new Date().toISOString(),
367
- };
368
- upsertTunnelState(entry);
369
- return entry;
370
- }
371
- export function listTunnelSessions(options) {
372
- const state = readStateFile();
373
- if (options?.all) {
374
- return state.tunnels;
375
- }
376
- return state.tunnels.filter((entry) => isProcessAlive(entry.pid));
377
- }
378
- export async function stopTunnelSession(opts) {
379
- const tunnelId = opts.tunnelId?.trim() ?? "";
380
- if (!tunnelId) {
381
- const active = listTunnelSessions({ all: false });
382
- if (active.length === 1) {
383
- return stopTunnelSession({ ...opts, tunnelId: active[0]?.tunnelId });
384
- }
385
- throw new Error("Tunnel id is required. Use `instafy tunnel:list` to find it.");
386
- }
387
- const entry = removeTunnelState(tunnelId);
388
- if (!entry) {
389
- throw new Error(`Tunnel not found in local state: ${tunnelId}`);
390
- }
391
- if (isProcessAlive(entry.pid)) {
392
- try {
393
- process.kill(entry.pid, "SIGTERM");
394
- }
395
- catch {
396
- // ignore
397
- }
398
- const stopped = await waitForProcessExit(entry.pid, 4000);
399
- if (!stopped) {
400
- try {
401
- process.kill(entry.pid, "SIGKILL");
402
- }
403
- catch {
404
- // ignore
405
- }
406
- }
407
- }
408
- const controllerToken = resolveControllerToken(opts);
409
- await revokeTunnel(entry.controllerUrl, controllerToken, entry.projectId, entry.tunnelId);
410
- try {
411
- fs.rmSync(entry.workdir, { recursive: true, force: true });
412
- }
413
- catch {
414
- // ignore
415
- }
416
- return { ok: true, tunnelId: entry.tunnelId };
417
- }
418
- export function resolveTunnelLogFile(tunnelId) {
419
- const chosen = tunnelId?.trim() ?? "";
420
- const all = readStateFile().tunnels;
421
- if (chosen) {
422
- const entry = all.find((tunnel) => tunnel.tunnelId === chosen);
423
- if (!entry) {
424
- throw new Error(`Tunnel not found in local state: ${chosen}`);
425
- }
426
- return entry;
427
- }
428
- const active = all.filter((entry) => isProcessAlive(entry.pid));
429
- if (active.length === 1) {
430
- return active[0];
431
- }
432
- throw new Error("Tunnel id is required. Use `instafy tunnel:list` to find it.");
433
- }
434
- export async function tailTunnelLogs(options) {
435
- const entry = resolveTunnelLogFile(options.tunnelId);
436
- const logFile = entry.logFile;
437
- const lines = Number.isFinite(options.lines) ? options.lines : NaN;
438
- const lineCount = Number.isFinite(lines) && lines > 0 ? Math.floor(lines) : 200;
439
- const follow = Boolean(options.follow);
440
- if (options.json) {
441
- console.log(JSON.stringify({ tunnelId: entry.tunnelId, logFile, follow, lines: lineCount }, null, 2));
442
- return;
443
- }
444
- if (!follow) {
445
- const raw = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
446
- const rows = raw.split(/\r?\n/);
447
- const tail = rows.slice(Math.max(0, rows.length - lineCount));
448
- console.log(tail.join("\n"));
449
- return;
450
- }
451
- // Follow mode: prefer system tail tools.
452
- const child = process.platform === "win32"
453
- ? spawn("powershell.exe", [
454
- "-NoProfile",
455
- "-Command",
456
- `Get-Content -LiteralPath '${logFile.replace(/'/g, "''")}' -Tail ${lineCount} -Wait`,
457
- ], { stdio: "inherit" })
458
- : spawn("tail", ["-n", String(lineCount), "-f", logFile], { stdio: "inherit" });
459
- const handleExit = () => {
460
- try {
461
- child.kill("SIGTERM");
462
- }
463
- catch {
464
- // ignore
465
- }
466
- };
467
- process.once("SIGINT", handleExit);
468
- process.once("SIGTERM", handleExit);
469
- await new Promise((resolve) => child.on("exit", () => resolve()));
470
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.8-staging.348",
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": {
@@ -17,7 +17,6 @@
17
17
  "type": "module",
18
18
  "scripts": {
19
19
  "build": "tsc -p tsconfig.json",
20
- "prepack": "tsc -p tsconfig.json",
21
20
  "dev": "ts-node src/index.ts",
22
21
  "test": "pnpm test:unit",
23
22
  "test:unit": "pnpm build && vitest run",
package/dist/errors.js DELETED
@@ -1,10 +0,0 @@
1
- export function formatAuthRequiredError(params) {
2
- const lines = ["Not authenticated. Run `instafy login` first."];
3
- if (params?.retryCommand) {
4
- lines.push("", `Then retry: ${params.retryCommand}`);
5
- }
6
- if (params?.advancedHint) {
7
- lines.push("", `Advanced: ${params.advancedHint}`);
8
- }
9
- return new Error(lines.join("\n"));
10
- }
@@ -1,201 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
3
- import { mintGitAccessToken } from "./git.js";
4
- function parseCredentialRequest(raw) {
5
- const request = {};
6
- for (const line of raw.split(/\r?\n/)) {
7
- if (!line.trim())
8
- continue;
9
- const idx = line.indexOf("=");
10
- if (idx <= 0)
11
- continue;
12
- const key = line.slice(0, idx).trim();
13
- const value = line.slice(idx + 1).trim();
14
- if (!value)
15
- continue;
16
- if (key === "protocol")
17
- request.protocol = value;
18
- if (key === "host")
19
- request.host = value;
20
- if (key === "path")
21
- request.path = value;
22
- if (key === "url")
23
- request.url = value;
24
- if (key === "username")
25
- request.username = value;
26
- }
27
- return request;
28
- }
29
- function normalizeHost(rawHost) {
30
- const host = rawHost.trim();
31
- if (!host)
32
- return null;
33
- const lowered = host.toLowerCase();
34
- const bracketed = lowered.match(/^\[(.+)\](?::\d+)?$/);
35
- if (bracketed) {
36
- return { host: lowered, hostname: bracketed[1] ?? lowered };
37
- }
38
- const lastColon = lowered.lastIndexOf(":");
39
- if (lastColon > 0) {
40
- const possiblePort = lowered.slice(lastColon + 1);
41
- if (/^\d+$/.test(possiblePort)) {
42
- return { host: lowered, hostname: lowered.slice(0, lastColon) };
43
- }
44
- }
45
- return { host: lowered, hostname: lowered };
46
- }
47
- function resolveRequestHost(request) {
48
- if (request.host?.trim())
49
- return request.host.trim();
50
- if (request.url?.trim()) {
51
- try {
52
- const parsed = new URL(request.url.trim());
53
- return parsed.host;
54
- }
55
- catch {
56
- return null;
57
- }
58
- }
59
- return null;
60
- }
61
- function splitCsv(value) {
62
- return (value ?? "")
63
- .split(",")
64
- .map((entry) => entry.trim().toLowerCase())
65
- .filter(Boolean);
66
- }
67
- function isAllowedGitHost(rawHost) {
68
- if (!rawHost)
69
- return false;
70
- const normalized = normalizeHost(rawHost);
71
- if (!normalized)
72
- return false;
73
- const allowHosts = splitCsv(process.env["INSTAFY_GIT_HOSTS"]);
74
- if (allowHosts.includes(normalized.host) || allowHosts.includes(normalized.hostname)) {
75
- return true;
76
- }
77
- // Safe-by-default allow-list: Instafy domains and local dev.
78
- if (normalized.hostname === "localhost" ||
79
- normalized.hostname === "127.0.0.1" ||
80
- normalized.hostname === "::1" ||
81
- normalized.hostname === "host.docker.internal") {
82
- return true;
83
- }
84
- return normalized.hostname.endsWith(".instafy.dev");
85
- }
86
- function normalizeRepoName(raw) {
87
- const trimmed = raw.trim().replace(/^\/+/, "");
88
- if (!trimmed)
89
- return null;
90
- const first = trimmed.split("/")[0] ?? "";
91
- if (!first.endsWith(".git"))
92
- return null;
93
- const withoutSuffix = first.slice(0, -".git".length);
94
- return withoutSuffix || null;
95
- }
96
- function isUuid(value) {
97
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
98
- }
99
- function parseProjectIdFromUrl(raw) {
100
- const trimmed = raw.trim();
101
- if (!trimmed)
102
- return null;
103
- try {
104
- const parsed = new URL(trimmed);
105
- const repo = normalizeRepoName(parsed.pathname);
106
- return repo && isUuid(repo) ? repo : null;
107
- }
108
- catch {
109
- // Not a WHATWG URL (e.g. scp-like).
110
- }
111
- const scpLike = trimmed.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
112
- if (scpLike) {
113
- const repo = normalizeRepoName(scpLike[2] ?? "");
114
- return repo && isUuid(repo) ? repo : null;
115
- }
116
- return null;
117
- }
118
- function resolveProjectIdFromRequest(request) {
119
- const fromPath = request.path ? normalizeRepoName(request.path) : null;
120
- if (fromPath && isUuid(fromPath))
121
- return fromPath;
122
- const fromUrl = request.url ? parseProjectIdFromUrl(request.url) : null;
123
- if (fromUrl)
124
- return fromUrl;
125
- return null;
126
- }
127
- function resolveProjectIdFromGitRemotes(host) {
128
- const result = spawnSync("git", ["config", "--get-regexp", "^remote\\..*\\.url$"], {
129
- encoding: "utf8",
130
- });
131
- if (result.status !== 0) {
132
- return null;
133
- }
134
- const lines = (result.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
135
- for (const line of lines) {
136
- const parts = line.split(/\s+/, 2);
137
- const url = parts[1] ?? "";
138
- if (!url)
139
- continue;
140
- if (host) {
141
- try {
142
- const parsed = new URL(url);
143
- if (parsed.host !== host)
144
- continue;
145
- }
146
- catch {
147
- // ignore non-url remotes when host is known
148
- continue;
149
- }
150
- }
151
- const projectId = parseProjectIdFromUrl(url);
152
- if (projectId)
153
- return projectId;
154
- }
155
- return null;
156
- }
157
- export async function runGitCredentialHelper(operation) {
158
- const normalized = (operation ?? "").trim().toLowerCase();
159
- if (!normalized) {
160
- throw new Error("git credential helper requires an operation (get|store|erase)");
161
- }
162
- // We mint tokens on demand; no persistence needed.
163
- if (normalized === "store" || normalized === "erase") {
164
- return;
165
- }
166
- if (normalized !== "get") {
167
- throw new Error(`unsupported git credential operation: ${operation}`);
168
- }
169
- const stdin = await new Promise((resolve) => {
170
- let buffer = "";
171
- process.stdin.setEncoding("utf8");
172
- process.stdin.on("data", (chunk) => (buffer += chunk));
173
- process.stdin.on("end", () => resolve(buffer));
174
- process.stdin.resume();
175
- });
176
- const request = parseCredentialRequest(stdin);
177
- const requestHost = resolveRequestHost(request);
178
- if (!isAllowedGitHost(requestHost)) {
179
- return;
180
- }
181
- const projectId = resolveProjectIdFromRequest(request) ??
182
- resolveProjectIdFromGitRemotes(requestHost);
183
- if (!projectId) {
184
- return;
185
- }
186
- const controllerUrl = resolveControllerUrl({ controllerUrl: null });
187
- const userAccessToken = resolveUserAccessToken({ accessToken: null });
188
- if (!userAccessToken) {
189
- throw new Error("Not authenticated. Run `instafy login` first.");
190
- }
191
- const abort = new AbortController();
192
- const timeout = setTimeout(() => abort.abort(), 5000);
193
- const minted = await mintGitAccessToken({
194
- controllerUrl,
195
- controllerAccessToken: userAccessToken,
196
- projectId,
197
- scopes: ["git.read", "git.write"],
198
- signal: abort.signal,
199
- }).finally(() => clearTimeout(timeout));
200
- process.stdout.write(`username=instafy\npassword=${minted.token}\n`);
201
- }
package/dist/git-setup.js DELETED
@@ -1,56 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- const INSTAFY_GIT_HELPER_VALUE = "!instafy git:credential";
3
- function isLikelySameHelper(value) {
4
- return value.includes("instafy git:credential");
5
- }
6
- function runGit(args) {
7
- const result = spawnSync("git", args, { encoding: "utf8" });
8
- if (result.error) {
9
- throw result.error;
10
- }
11
- return result;
12
- }
13
- export function isGitAvailable() {
14
- try {
15
- const result = runGit(["--version"]);
16
- return result.status === 0;
17
- }
18
- catch {
19
- return false;
20
- }
21
- }
22
- export function installGitCredentialHelper() {
23
- if (!isGitAvailable()) {
24
- return { changed: false };
25
- }
26
- const existing = runGit(["config", "--global", "--get-all", "credential.helper"]);
27
- const helpers = (existing.stdout ?? "")
28
- .split(/\r?\n/)
29
- .map((line) => line.trim())
30
- .filter(Boolean);
31
- if (helpers.some(isLikelySameHelper)) {
32
- return { changed: false };
33
- }
34
- runGit(["config", "--global", "--add", "credential.helper", INSTAFY_GIT_HELPER_VALUE]);
35
- return { changed: true };
36
- }
37
- export function uninstallGitCredentialHelper() {
38
- if (!isGitAvailable()) {
39
- return { changed: false };
40
- }
41
- const existing = runGit(["config", "--global", "--get-all", "credential.helper"]);
42
- const helpers = (existing.stdout ?? "")
43
- .split(/\r?\n/)
44
- .map((line) => line.trim())
45
- .filter(Boolean);
46
- if (!helpers.some(isLikelySameHelper)) {
47
- return { changed: false };
48
- }
49
- const remaining = helpers.filter((helper) => !isLikelySameHelper(helper));
50
- // Remove all helpers, then re-add the ones we didn't own.
51
- runGit(["config", "--global", "--unset-all", "credential.helper"]);
52
- for (const helper of remaining) {
53
- runGit(["config", "--global", "--add", "credential.helper", helper]);
54
- }
55
- return { changed: true };
56
- }