@instafy/cli 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { credentialFreeEnv } from "./detached-env.mjs";
6
7
 
7
8
  export const DEFAULT_CONTROLLER_URL = "http://127.0.0.1:8788";
8
9
 
@@ -145,7 +146,6 @@ export function agentUsesOwnerConversation(config) {
145
146
 
146
147
  /** Credential names the binder itself reads as the owner's login (above). */
147
148
  const BINDER_LOGIN_ENV = ["INSTAFY_ACCESS_TOKEN", "SUPABASE_ACCESS_TOKEN"];
148
- const CREDENTIAL_ENV_RE = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|(^|_)KEY$|_KEY_|API_?KEY)/;
149
149
 
150
150
  /**
151
151
  * Why --agent-executor is refused here, or null. It treats a click from this machine's loopback as the owner's own,
@@ -158,23 +158,12 @@ export function agentExecutorRefusal(env = process.env) {
158
158
  }
159
159
 
160
160
  /**
161
- * The environment of a binder started with `serve --detach`. It outlives the Octo turn that started it and every
162
- * process in a runtime can read its /proc/<pid>/environ, so it keeps no credential it does not need: no runtime job
163
- * token or other CONTROLLER_* secret, no SUPABASE_* service values, no credential-like names (space secrets) except
164
- * names passed with --allow-env, and the owner's login only with `keepLogin` (--agent-executor, the one mode that
161
+ * The environment of a binder started with `serve --detach`: `credentialFreeEnv` (no runtime job token or other
162
+ * CONTROLLER_* secret, no SUPABASE_* service values, no credential-like names such as space secrets), except names
163
+ * passed with --allow-env, and the owner's login only with `keepLogin` (--agent-executor, the one mode that
165
164
  * dispatches Octo turns itself). Everything else (PATH, HOME, the space and conversation ids, plain settings a
166
165
  * handler may declare in `env`) is kept.
167
166
  */
168
167
  export function detachedBinderEnv(env = process.env, allowEnv = [], { keepLogin = false } = {}) {
169
- const out = {};
170
- for (const [name, value] of Object.entries(env)) {
171
- if (value === undefined) continue;
172
- const keep =
173
- (keepLogin && BINDER_LOGIN_ENV.includes(name)) ||
174
- allowEnv.includes(name) ||
175
- name === "CONTROLLER_BASE_URL" ||
176
- (!/^(CONTROLLER|SUPABASE)_/.test(name) && !CREDENTIAL_ENV_RE.test(name));
177
- if (keep) out[name] = value;
178
- }
179
- return out;
168
+ return credentialFreeEnv(env, [...allowEnv, ...(keepLogin ? BINDER_LOGIN_ENV : [])]);
180
169
  }
@@ -0,0 +1,27 @@
1
+ // The environment of a process started to outlive the command (or Octo turn) that started it.
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // Shared by `instafy bind serve --detach` and `instafy preview open --start` (the CLI loads this file from its bundled
5
+ // copy of the binder), so both strip the same credentials.
6
+
7
+ const CREDENTIAL_ENV_RE = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|(^|_)KEY$|_KEY_|API_?KEY)/;
8
+
9
+ /**
10
+ * `env` without credentials a long-lived process does not need. Such a process outlives the job that started it, and
11
+ * every process in a runtime can read its /proc/<pid>/environ, so it keeps no runtime job token or other CONTROLLER_*
12
+ * value (except CONTROLLER_BASE_URL, which is not secret), no SUPABASE_* value and no credential-like name (space
13
+ * secrets, logins). Names in `keep` are kept anyway. Everything else (PATH, HOME, the space and conversation ids,
14
+ * plain settings) is kept.
15
+ */
16
+ export function credentialFreeEnv(env = process.env, keep = []) {
17
+ const out = {};
18
+ for (const [name, value] of Object.entries(env)) {
19
+ if (value === undefined) continue;
20
+ const kept =
21
+ keep.includes(name) ||
22
+ name === "CONTROLLER_BASE_URL" ||
23
+ (!/^(CONTROLLER|SUPABASE)_/.test(name) && !CREDENTIAL_ENV_RE.test(name));
24
+ if (kept) out[name] = value;
25
+ }
26
+ return out;
27
+ }
package/dist/cli.js CHANGED
@@ -1717,7 +1717,7 @@ var init_hardware = __esm({
1717
1717
 
1718
1718
  // src/index.ts
1719
1719
  import { Command, Option } from "commander";
1720
- import { pathToFileURL as pathToFileURL2 } from "node:url";
1720
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
1721
1721
  import { createRequire as createRequire4 } from "node:module";
1722
1722
  import kleur20 from "kleur";
1723
1723
 
@@ -5770,10 +5770,75 @@ async function tailTunnelLogs(options) {
5770
5770
  }
5771
5771
 
5772
5772
  // src/preview.ts
5773
- import fs8 from "node:fs";
5773
+ import { spawn as spawn5 } from "node:child_process";
5774
+ import fs9 from "node:fs";
5774
5775
  import os5 from "node:os";
5775
- import path9 from "node:path";
5776
+ import path10 from "node:path";
5776
5777
  import kleur8 from "kleur";
5778
+
5779
+ // src/bind.ts
5780
+ import { spawn as spawn4 } from "node:child_process";
5781
+ import fs8 from "node:fs";
5782
+ import path9 from "node:path";
5783
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
5784
+ var BIN_REL = path9.join("bin", "instafy-bind.mjs");
5785
+ function resolveBindBin(env = process.env) {
5786
+ const fromEnv = env.INSTAFY_BIND_BIN?.trim();
5787
+ if (fromEnv) return fs8.existsSync(fromEnv) ? path9.resolve(fromEnv) : null;
5788
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
5789
+ for (const candidate of [path9.join(here, "bind", BIN_REL), path9.resolve(here, "..", "..", "instafy-bind", BIN_REL)]) {
5790
+ if (fs8.existsSync(candidate)) return candidate;
5791
+ }
5792
+ return null;
5793
+ }
5794
+ function bundledBindRoot() {
5795
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
5796
+ for (const candidate of [path9.join(here, "bind"), path9.resolve(here, "..", "..", "instafy-bind")]) {
5797
+ if (fs8.existsSync(path9.join(candidate, BIN_REL))) return candidate;
5798
+ }
5799
+ return null;
5800
+ }
5801
+ async function loadCredentialFreeEnv() {
5802
+ const root = bundledBindRoot();
5803
+ const file = root ? path9.join(root, "src", "detached-env.mjs") : null;
5804
+ if (!file || !fs8.existsSync(file)) {
5805
+ throw new Error("the bundled binder (dist/bind) was not found, so a site cannot be started without credentials; rebuild the CLI");
5806
+ }
5807
+ const module = await import(pathToFileURL(file).href);
5808
+ if (typeof module.credentialFreeEnv !== "function") {
5809
+ throw new Error(`${file} does not export credentialFreeEnv; rebuild the CLI`);
5810
+ }
5811
+ return module.credentialFreeEnv;
5812
+ }
5813
+ function runBind(args) {
5814
+ const bin = resolveBindBin();
5815
+ if (!bin) {
5816
+ process.stderr.write(
5817
+ "instafy bind: the bundled binder (dist/bind) was not found; rebuild the CLI, or set INSTAFY_BIND_BIN to packages/instafy-bind/bin/instafy-bind.mjs, or run it directly: node packages/instafy-bind/bin/instafy-bind.mjs <command>\n"
5818
+ );
5819
+ return Promise.resolve(1);
5820
+ }
5821
+ return new Promise((resolve) => {
5822
+ const child = spawn4(process.execPath, [bin, ...args], { stdio: "inherit", env: process.env });
5823
+ const forward = (signal) => {
5824
+ if (!child.killed) child.kill(signal);
5825
+ };
5826
+ process.on("SIGINT", forward);
5827
+ process.on("SIGTERM", forward);
5828
+ child.on("error", (error) => {
5829
+ process.stderr.write(`instafy bind: ${error.message}
5830
+ `);
5831
+ resolve(1);
5832
+ });
5833
+ child.on("exit", (code, signal) => {
5834
+ process.off("SIGINT", forward);
5835
+ process.off("SIGTERM", forward);
5836
+ resolve(code ?? (signal ? 130 : 1));
5837
+ });
5838
+ });
5839
+ }
5840
+
5841
+ // src/preview.ts
5777
5842
  init_controller_fetch();
5778
5843
  init_errors();
5779
5844
 
@@ -5907,19 +5972,19 @@ var MAX_PATH_LENGTH = 512;
5907
5972
  var STATE_LOCK_WAIT_MS = 12e4;
5908
5973
  var STATE_LOCK_STALE_MS = 10 * 6e4;
5909
5974
  function instafyDir() {
5910
- return path9.join(os5.homedir(), ".instafy");
5975
+ return path10.join(os5.homedir(), ".instafy");
5911
5976
  }
5912
5977
  function previewStateFile() {
5913
- return path9.join(instafyDir(), "cli-preview-state.json");
5978
+ return path10.join(instafyDir(), "cli-preview-state.json");
5914
5979
  }
5915
5980
  function previewStateLockFile() {
5916
- return path9.join(instafyDir(), "cli-preview-state.lock");
5981
+ return path10.join(instafyDir(), "cli-preview-state.lock");
5917
5982
  }
5918
5983
  function workdirsRoot() {
5919
- return path9.join(instafyDir(), "cli-preview-workdirs");
5984
+ return path10.join(instafyDir(), "cli-preview-workdirs");
5920
5985
  }
5921
5986
  function workdirFor(previewId) {
5922
- return path9.join(workdirsRoot(), safePathSegment2(previewId));
5987
+ return path10.join(workdirsRoot(), safePathSegment2(previewId));
5923
5988
  }
5924
5989
  var HOST_SUFFIX_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d{1,5})?$/;
5925
5990
  function parseViewerHostSuffix(raw) {
@@ -5972,7 +6037,7 @@ function parsePreviewPath(raw) {
5972
6037
  }
5973
6038
  function readStateFile2() {
5974
6039
  try {
5975
- const parsed = JSON.parse(fs8.readFileSync(previewStateFile(), "utf8"));
6040
+ const parsed = JSON.parse(fs9.readFileSync(previewStateFile(), "utf8"));
5976
6041
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.previews)) {
5977
6042
  return { version: 1, previews: [] };
5978
6043
  }
@@ -5982,8 +6047,8 @@ function readStateFile2() {
5982
6047
  }
5983
6048
  }
5984
6049
  function writeStateFile2(state) {
5985
- fs8.mkdirSync(instafyDir(), { recursive: true });
5986
- fs8.writeFileSync(previewStateFile(), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 384 });
6050
+ fs9.mkdirSync(instafyDir(), { recursive: true });
6051
+ fs9.writeFileSync(previewStateFile(), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 384 });
5987
6052
  }
5988
6053
  function upsertPreviewState(entry) {
5989
6054
  const state = readStateFile2();
@@ -5998,7 +6063,7 @@ function removePreviewState(previewId) {
5998
6063
  return existing;
5999
6064
  }
6000
6065
  function removeWorkdir(previewId) {
6001
- fs8.rmSync(workdirFor(previewId), { recursive: true, force: true });
6066
+ fs9.rmSync(workdirFor(previewId), { recursive: true, force: true });
6002
6067
  }
6003
6068
  function forgetPreview(previewId) {
6004
6069
  const entry = removePreviewState(previewId);
@@ -6010,9 +6075,9 @@ function sleep(ms) {
6010
6075
  }
6011
6076
  function lockHolderIsGone(lockFile) {
6012
6077
  try {
6013
- const stat = fs8.statSync(lockFile);
6078
+ const stat = fs9.statSync(lockFile);
6014
6079
  if (Date.now() - stat.mtimeMs > STATE_LOCK_STALE_MS) return true;
6015
- const pid = Number.parseInt(fs8.readFileSync(lockFile, "utf8").trim(), 10);
6080
+ const pid = Number.parseInt(fs9.readFileSync(lockFile, "utf8").trim(), 10);
6016
6081
  if (!Number.isInteger(pid) || pid <= 0) return Date.now() - stat.mtimeMs > 5e3;
6017
6082
  try {
6018
6083
  process.kill(pid, 0);
@@ -6026,18 +6091,18 @@ function lockHolderIsGone(lockFile) {
6026
6091
  }
6027
6092
  async function withStateLock(fn) {
6028
6093
  const lockFile = previewStateLockFile();
6029
- fs8.mkdirSync(instafyDir(), { recursive: true });
6094
+ fs9.mkdirSync(instafyDir(), { recursive: true });
6030
6095
  const deadline = Date.now() + STATE_LOCK_WAIT_MS;
6031
6096
  for (; ; ) {
6032
6097
  try {
6033
- const fd = fs8.openSync(lockFile, "wx", 384);
6034
- fs8.writeSync(fd, String(process.pid));
6035
- fs8.closeSync(fd);
6098
+ const fd = fs9.openSync(lockFile, "wx", 384);
6099
+ fs9.writeSync(fd, String(process.pid));
6100
+ fs9.closeSync(fd);
6036
6101
  break;
6037
6102
  } catch (error) {
6038
6103
  if (error.code !== "EEXIST") throw error;
6039
6104
  if (lockHolderIsGone(lockFile)) {
6040
- fs8.rmSync(lockFile, { force: true });
6105
+ fs9.rmSync(lockFile, { force: true });
6041
6106
  continue;
6042
6107
  }
6043
6108
  if (Date.now() > deadline) {
@@ -6049,11 +6114,11 @@ async function withStateLock(fn) {
6049
6114
  try {
6050
6115
  return await fn();
6051
6116
  } finally {
6052
- fs8.rmSync(lockFile, { force: true });
6117
+ fs9.rmSync(lockFile, { force: true });
6053
6118
  }
6054
6119
  }
6055
6120
  function configPathFor(workdir) {
6056
- return path9.join(workdir, "rathole.toml");
6121
+ return path10.join(workdir, "rathole.toml");
6057
6122
  }
6058
6123
  async function stopClient(entry) {
6059
6124
  await stopRatholeClient(entry.pid, configPathFor(entry.workdir), entry.binary);
@@ -6185,7 +6250,7 @@ async function resolveRathole(ratholeBin, log) {
6185
6250
  }
6186
6251
  function readFileOrNull(file) {
6187
6252
  try {
6188
- return fs8.readFileSync(file, "utf8");
6253
+ return fs9.readFileSync(file, "utf8");
6189
6254
  } catch {
6190
6255
  return null;
6191
6256
  }
@@ -6293,8 +6358,8 @@ function parseServeCommand(raw) {
6293
6358
  }
6294
6359
  function realDir(dir) {
6295
6360
  try {
6296
- const resolved = fs8.realpathSync(dir);
6297
- return fs8.statSync(resolved).isDirectory() ? resolved : null;
6361
+ const resolved = fs9.realpathSync(dir);
6362
+ return fs9.statSync(resolved).isDirectory() ? resolved : null;
6298
6363
  } catch {
6299
6364
  return null;
6300
6365
  }
@@ -6304,22 +6369,22 @@ function resolveWorkspaceRoot(env, projectId2) {
6304
6369
  if (explicit) return realDir(explicit);
6305
6370
  const base = env["WORKSPACE_DIR"]?.trim();
6306
6371
  if (!base || !projectId2) return null;
6307
- return realDir(path9.join(base, projectId2));
6372
+ return realDir(path10.join(base, projectId2));
6308
6373
  }
6309
6374
  function workspaceRelativeDir(root, dir) {
6310
6375
  const resolved = realDir(dir);
6311
6376
  if (!resolved) return null;
6312
- const relative = path9.relative(root, resolved);
6377
+ const relative = path10.relative(root, resolved);
6313
6378
  if (relative === "") return ".";
6314
- if (relative.startsWith("..") || path9.isAbsolute(relative)) return null;
6315
- return relative.split(path9.sep).join("/");
6379
+ if (relative.startsWith("..") || path10.isAbsolute(relative)) return null;
6380
+ return relative.split(path10.sep).join("/");
6316
6381
  }
6317
6382
  function listeningSocketInodes(procRoot, port) {
6318
6383
  const inodes = /* @__PURE__ */ new Set();
6319
6384
  for (const table of ["net/tcp", "net/tcp6"]) {
6320
6385
  let text2 = "";
6321
6386
  try {
6322
- text2 = fs8.readFileSync(path9.join(procRoot, table), "utf8");
6387
+ text2 = fs9.readFileSync(path10.join(procRoot, table), "utf8");
6323
6388
  } catch {
6324
6389
  continue;
6325
6390
  }
@@ -6340,20 +6405,20 @@ function detectListeningProcess(port, procRoot = "/proc") {
6340
6405
  if (inodes.size === 0) return null;
6341
6406
  let pids = [];
6342
6407
  try {
6343
- pids = fs8.readdirSync(procRoot).filter((name) => /^\d+$/.test(name)).map(Number).sort((a, b) => a - b);
6408
+ pids = fs9.readdirSync(procRoot).filter((name) => /^\d+$/.test(name)).map(Number).sort((a, b) => a - b);
6344
6409
  } catch {
6345
6410
  return null;
6346
6411
  }
6347
6412
  for (const pid of pids) {
6348
6413
  let fds = [];
6349
6414
  try {
6350
- fds = fs8.readdirSync(path9.join(procRoot, String(pid), "fd"));
6415
+ fds = fs9.readdirSync(path10.join(procRoot, String(pid), "fd"));
6351
6416
  } catch {
6352
6417
  continue;
6353
6418
  }
6354
6419
  const holds = fds.some((fd) => {
6355
6420
  try {
6356
- const target = fs8.readlinkSync(path9.join(procRoot, String(pid), "fd", fd));
6421
+ const target = fs9.readlinkSync(path10.join(procRoot, String(pid), "fd", fd));
6357
6422
  const match = /^socket:\[(\d+)\]$/.exec(target);
6358
6423
  return Boolean(match && inodes.has(match[1]));
6359
6424
  } catch {
@@ -6362,9 +6427,9 @@ function detectListeningProcess(port, procRoot = "/proc") {
6362
6427
  });
6363
6428
  if (!holds) continue;
6364
6429
  try {
6365
- const argv = fs8.readFileSync(path9.join(procRoot, String(pid), "cmdline"), "utf8").split("\0");
6430
+ const argv = fs9.readFileSync(path10.join(procRoot, String(pid), "cmdline"), "utf8").split("\0");
6366
6431
  while (argv.length > 0 && argv[argv.length - 1] === "") argv.pop();
6367
- const cwd = fs8.readlinkSync(path9.join(procRoot, String(pid), "cwd"));
6432
+ const cwd = fs9.readlinkSync(path10.join(procRoot, String(pid), "cwd"));
6368
6433
  if (argv.length === 0) continue;
6369
6434
  return { pid, argv, cwd };
6370
6435
  } catch {
@@ -6445,6 +6510,66 @@ function resolvePreviewServe(params) {
6445
6510
  }
6446
6511
  return { serve: { argv: found.argv, cwd } };
6447
6512
  }
6513
+ var START_PORT_WAIT_MS = 6e4;
6514
+ var START_POLL_MS = 300;
6515
+ var START_PROBE_TIMEOUT_MS = 2e3;
6516
+ var START_LOGS_DIR = path10.join(".instafy", "agent", "logs");
6517
+ function displayPath(file, cwd = process.cwd()) {
6518
+ const relative = path10.relative(cwd, file);
6519
+ return relative && !relative.startsWith("..") && !path10.isAbsolute(relative) ? relative : file;
6520
+ }
6521
+ async function startPreviewSite(params) {
6522
+ const { port, sitePath, serve, root } = params;
6523
+ const credentialFreeEnv = await loadCredentialFreeEnv();
6524
+ const env = credentialFreeEnv(params.env ?? process.env);
6525
+ const cwd = path10.join(root, serve.cwd);
6526
+ const logFile = path10.join(root, START_LOGS_DIR, `preview-${port}.log`);
6527
+ fs9.mkdirSync(path10.dirname(logFile), { recursive: true });
6528
+ const fd = fs9.openSync(logFile, "a", 384);
6529
+ let exit = null;
6530
+ let spawnError = null;
6531
+ let pid;
6532
+ try {
6533
+ fs9.writeSync(fd, `
6534
+ [instafy preview open --start] ${(/* @__PURE__ */ new Date()).toISOString()} ${JSON.stringify(serve.argv)} in ${serve.cwd}
6535
+ `);
6536
+ const child = spawn5(serve.argv[0], serve.argv.slice(1), {
6537
+ cwd,
6538
+ env,
6539
+ detached: true,
6540
+ stdio: ["ignore", fd, fd]
6541
+ });
6542
+ child.on("error", (error) => {
6543
+ spawnError = error.code ?? error.message;
6544
+ });
6545
+ child.on("exit", (code, signal) => {
6546
+ exit = signal ? `signal ${signal}` : `exit status ${code}`;
6547
+ });
6548
+ child.unref();
6549
+ pid = child.pid;
6550
+ } finally {
6551
+ fs9.closeSync(fd);
6552
+ }
6553
+ await new Promise((resolve) => setImmediate(resolve));
6554
+ const shownLog = displayPath(logFile);
6555
+ const fail = (why) => new PreviewCommandError(`The site ${why} and did not answer on 127.0.0.1:${port}; log: ${shownLog}`, PREVIEW_EXIT_NOT_LISTENING);
6556
+ const deadline = Date.now() + (params.timeoutMs ?? START_PORT_WAIT_MS);
6557
+ for (; ; ) {
6558
+ if (spawnError || pid === void 0) throw fail(`could not be started (${spawnError ?? "no process"})`);
6559
+ if (exit) throw fail(`started with --serve-cmd exited (${exit})`);
6560
+ const probe = await probeLocalSite(port, sitePath, START_PROBE_TIMEOUT_MS, RATHOLE_LOCAL_HOSTS);
6561
+ if (probe.listening) return { pid, logFile };
6562
+ if (exit) throw fail(`started with --serve-cmd exited (${exit})`);
6563
+ if (Date.now() >= deadline) {
6564
+ try {
6565
+ process.kill(-pid, "SIGTERM");
6566
+ } catch {
6567
+ }
6568
+ throw fail(`started with --serve-cmd was stopped after ${Math.round((params.timeoutMs ?? START_PORT_WAIT_MS) / 1e3)} s`);
6569
+ }
6570
+ await new Promise((resolve) => setTimeout(resolve, START_POLL_MS));
6571
+ }
6572
+ }
6448
6573
  async function createAndStartClient(params) {
6449
6574
  const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve } = params;
6450
6575
  const conversationId = opts.conversationId?.trim() || void 0;
@@ -6472,7 +6597,7 @@ async function createAndStartClient(params) {
6472
6597
  throw new Error("Preview response is missing previewId.");
6473
6598
  }
6474
6599
  const workdir = workdirFor(previewId);
6475
- const logFile = path9.join(instafyDir(), "cli-preview-logs", `preview-${safePathSegment2(previewId)}.log`);
6600
+ const logFile = path10.join(instafyDir(), "cli-preview-logs", `preview-${safePathSegment2(previewId)}.log`);
6476
6601
  const configPath2 = configPathFor(workdir);
6477
6602
  const hostSuffix = parseViewerHostSuffix(grant["viewerHostSuffix"]);
6478
6603
  const studioOrigin = parseStudioOrigin(grant["studioOrigin"]);
@@ -6483,8 +6608,8 @@ async function createAndStartClient(params) {
6483
6608
  try {
6484
6609
  const credentials = grant["credentials"] && typeof grant["credentials"] === "object" ? grant["credentials"] : {};
6485
6610
  const configBody = buildRatholeConfig(credentials, port);
6486
- fs8.mkdirSync(workdir, { recursive: true });
6487
- fs8.mkdirSync(path9.dirname(logFile), { recursive: true });
6611
+ fs9.mkdirSync(workdir, { recursive: true });
6612
+ fs9.mkdirSync(path10.dirname(logFile), { recursive: true });
6488
6613
  if (current && readFileOrNull(configPath2) === configBody && isRatholeClientFor(current.pid, configPath2, current.binary)) {
6489
6614
  pid = current.pid;
6490
6615
  }
@@ -6503,7 +6628,7 @@ async function createAndStartClient(params) {
6503
6628
  }
6504
6629
  if (pid === null) {
6505
6630
  const rathole = await resolveRathole(opts.ratholeBin, log);
6506
- fs8.writeFileSync(configPath2, configBody, { encoding: "utf8", mode: 384 });
6631
+ fs9.writeFileSync(configPath2, configBody, { encoding: "utf8", mode: 384 });
6507
6632
  binary = rathole;
6508
6633
  const started = await spawnRatholeDetached({ rathole, configPath: configPath2, workdir, logFile });
6509
6634
  if (started.exit !== null) {
@@ -6556,7 +6681,24 @@ async function previewOpen(opts) {
6556
6681
  const { auth, controllerUrl } = resolveAuth(opts, retryCommand);
6557
6682
  const serverUrl = cleanUrl(controllerUrl);
6558
6683
  const log = (message) => console.error(kleur8.cyan(message));
6559
- const probe = await probeLocalSite(port, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
6684
+ const explicitServe = opts.start && opts.serveCmd?.trim() ? resolvePreviewServe({ port, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd }).serve : null;
6685
+ let probe = await probeLocalSite(port, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
6686
+ let started = null;
6687
+ if (!probe.listening && opts.start && probe.status === null && !probe.timedOut) {
6688
+ const ipv6 = await probeLocalSite(port, sitePath, void 0, ["::1"]);
6689
+ if (ipv6.status === null && !ipv6.timedOut) {
6690
+ if (!explicitServe) {
6691
+ throw new PreviewCommandError(
6692
+ `Nothing answers on 127.0.0.1:${port}, and --start needs --serve-cmd "<command>" to start the site.`,
6693
+ PREVIEW_EXIT_NOT_LISTENING
6694
+ );
6695
+ }
6696
+ const root = resolveWorkspaceRoot(process.env, projectId2);
6697
+ started = await startPreviewSite({ port, sitePath, serve: explicitServe, root, timeoutMs: opts.startTimeoutMs });
6698
+ log(`Started the site (pid ${started.pid}); log: ${displayPath(started.logFile)}`);
6699
+ probe = await probeLocalSite(port, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
6700
+ }
6701
+ }
6560
6702
  if (!probe.listening) {
6561
6703
  throw await notListeningError(port, sitePath, probe);
6562
6704
  }
@@ -6595,7 +6737,8 @@ async function previewOpen(opts) {
6595
6737
  messageId: stringField(readyBody, "messageId"),
6596
6738
  viewerHostSuffix: parseViewerHostSuffix(grant["viewerHostSuffix"]) ?? null,
6597
6739
  restartable,
6598
- ...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" }
6740
+ ...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" },
6741
+ ...opts.start ? { startedPid: started?.pid ?? null, logFile: started?.logFile ?? null } : {}
6599
6742
  };
6600
6743
  }
6601
6744
  function toPreviewSummary(raw) {
@@ -6608,7 +6751,8 @@ function toPreviewSummary(raw) {
6608
6751
  title: stringField(record2, "title") ?? "",
6609
6752
  port: numberField(record2, "port", "localPort") ?? 0,
6610
6753
  path: stringField(record2, "path") ?? "/",
6611
- status: stringField(record2, "status") ?? "unknown"
6754
+ status: stringField(record2, "status") ?? "unknown",
6755
+ ...typeof record2["restartable"] === "boolean" ? { restartable: record2["restartable"] } : {}
6612
6756
  };
6613
6757
  }
6614
6758
  async function previewList(opts) {
@@ -6656,10 +6800,19 @@ function printPreviewOpen(result, json) {
6656
6800
  }
6657
6801
  const label = result.status === "live" ? kleur8.green("Ready to preview") : result.status === "not_responding" ? kleur8.yellow("Not responding yet") : kleur8.yellow("Preview opened (the server did not report whether the site answers)");
6658
6802
  console.log(`${label}: ${result.title} (port ${result.port}). Open it from the card in the conversation.`);
6803
+ if (result.startedPid) {
6804
+ console.log(kleur8.gray(`Started the site in the background (pid ${result.startedPid}); log: ${displayPath(result.logFile ?? "")}`));
6805
+ } else if (result.startedPid === null) {
6806
+ console.log(kleur8.gray("The site was already running; nothing was started."));
6807
+ }
6659
6808
  if (result.restartable) {
6660
- console.log(kleur8.gray("Open starts the site again after the space sleeps."));
6809
+ console.log(kleur8.gray("Open starts the site again when the process that started it exits, or the space sleeps."));
6661
6810
  } else {
6662
- console.log(kleur8.yellow(`After the space sleeps, Open cannot start the site by itself: ${result.restartNote}.`));
6811
+ console.log(
6812
+ kleur8.yellow(
6813
+ `When the process that started it exits, or the space sleeps, Open cannot start the site by itself: ${result.restartNote}.`
6814
+ )
6815
+ );
6663
6816
  }
6664
6817
  console.log(kleur8.gray(`previewId=${result.previewId}`));
6665
6818
  if (result.httpStatus !== null && result.httpStatus >= 400) {
@@ -6678,15 +6831,23 @@ function printPreviewList(previews, json) {
6678
6831
  console.log(JSON.stringify(previews, null, 2));
6679
6832
  return;
6680
6833
  }
6834
+ const stoppedNote = "A preview whose machine stopped or restarted is not listed; its card reads Stopped, and Open on a restartable card starts the site again.";
6681
6835
  if (previews.length === 0) {
6682
- console.log(kleur8.yellow("No previews found."));
6836
+ console.log(kleur8.yellow("No open previews."));
6837
+ console.log(kleur8.gray(stoppedNote));
6683
6838
  return;
6684
6839
  }
6685
6840
  for (const preview of previews) {
6841
+ const restart = preview.restartable === void 0 ? "" : preview.restartable ? " \xB7 restartable" : " \xB7 not restartable";
6686
6842
  console.log(
6687
- `${preview.previewId} \xB7 ${preview.title || "(untitled)"} \xB7 port=${preview.port}${preview.path === "/" ? "" : ` \xB7 path=${preview.path}`} \xB7 ${preview.status}`
6843
+ `${preview.previewId} \xB7 ${preview.title || "(untitled)"} \xB7 port=${preview.port}${preview.path === "/" ? "" : ` \xB7 path=${preview.path}`} \xB7 preview ${preview.status}${restart}`
6688
6844
  );
6689
6845
  }
6846
+ console.log(
6847
+ kleur8.gray(
6848
+ `This lists open previews; it does not check whether a site answers (${kleur8.cyan("instafy preview check --port <port>")} does). ${stoppedNote}`
6849
+ )
6850
+ );
6690
6851
  }
6691
6852
 
6692
6853
  // src/config-command.ts
@@ -6810,27 +6971,27 @@ init_config();
6810
6971
  // src/git-wrapper.ts
6811
6972
  import { spawnSync as spawnSync4 } from "node:child_process";
6812
6973
  import { randomUUID as randomUUID2 } from "node:crypto";
6813
- import fs9 from "node:fs";
6814
- import path10 from "node:path";
6974
+ import fs10 from "node:fs";
6975
+ import path11 from "node:path";
6815
6976
  import kleur10 from "kleur";
6816
6977
  function pathExists(candidate) {
6817
6978
  try {
6818
- fs9.statSync(candidate);
6979
+ fs10.statSync(candidate);
6819
6980
  return true;
6820
6981
  } catch {
6821
6982
  return false;
6822
6983
  }
6823
6984
  }
6824
6985
  function findInstafyGitContext(startDir) {
6825
- let current = path10.resolve(startDir);
6826
- const root = path10.parse(current).root;
6986
+ let current = path11.resolve(startDir);
6987
+ const root = path11.parse(current).root;
6827
6988
  while (true) {
6828
- const gitDir = path10.join(current, ".instafy", ".git");
6989
+ const gitDir = path11.join(current, ".instafy", ".git");
6829
6990
  if (pathExists(gitDir)) {
6830
6991
  return { workTree: current, gitDir };
6831
6992
  }
6832
6993
  if (current === root) break;
6833
- current = path10.dirname(current);
6994
+ current = path11.dirname(current);
6834
6995
  }
6835
6996
  return null;
6836
6997
  }
@@ -6986,7 +7147,7 @@ function collectDirtyPaths(context, options) {
6986
7147
  return unique;
6987
7148
  }
6988
7149
  function findEmbeddedGitDirs(context, pathSpecs) {
6989
- const instafyGitDir = path10.resolve(context.gitDir);
7150
+ const instafyGitDir = path11.resolve(context.gitDir);
6990
7151
  const seen = /* @__PURE__ */ new Set();
6991
7152
  for (const rawPath of pathSpecs) {
6992
7153
  const normalized = normalizePathSpec(rawPath).replace(/\/+$/, "");
@@ -6995,11 +7156,11 @@ function findEmbeddedGitDirs(context, pathSpecs) {
6995
7156
  }
6996
7157
  let cursor = normalized;
6997
7158
  while (cursor && cursor !== ".") {
6998
- const candidate = path10.join(context.workTree, ...cursor.split("/"), ".git");
6999
- if (pathExists(candidate) && path10.resolve(candidate) !== instafyGitDir) {
7000
- seen.add(path10.resolve(candidate));
7159
+ const candidate = path11.join(context.workTree, ...cursor.split("/"), ".git");
7160
+ if (pathExists(candidate) && path11.resolve(candidate) !== instafyGitDir) {
7161
+ seen.add(path11.resolve(candidate));
7001
7162
  }
7002
- const next = path10.posix.dirname(cursor);
7163
+ const next = path11.posix.dirname(cursor);
7003
7164
  if (next === "." || next === cursor) {
7004
7165
  break;
7005
7166
  }
@@ -7008,8 +7169,8 @@ function findEmbeddedGitDirs(context, pathSpecs) {
7008
7169
  }
7009
7170
  const candidates = Array.from(seen);
7010
7171
  candidates.sort((a, b) => {
7011
- const depthA = a.split(path10.sep).length;
7012
- const depthB = b.split(path10.sep).length;
7172
+ const depthA = a.split(path11.sep).length;
7173
+ const depthB = b.split(path11.sep).length;
7013
7174
  return depthB - depthA;
7014
7175
  });
7015
7176
  return candidates;
@@ -7019,16 +7180,16 @@ function hideEmbeddedGitDirs(context, pathSpecs) {
7019
7180
  if (candidates.length === 0) {
7020
7181
  return [];
7021
7182
  }
7022
- const stagingBase = path10.join(context.workTree, ".instafy", "origin-staging", "embedded-git");
7023
- fs9.mkdirSync(stagingBase, { recursive: true });
7183
+ const stagingBase = path11.join(context.workTree, ".instafy", "origin-staging", "embedded-git");
7184
+ fs10.mkdirSync(stagingBase, { recursive: true });
7024
7185
  const renames = [];
7025
7186
  for (const candidate of candidates) {
7026
- let hidden = path10.join(stagingBase, randomUUID2());
7187
+ let hidden = path11.join(stagingBase, randomUUID2());
7027
7188
  for (let attempt = 0; attempt < 8; attempt += 1) {
7028
7189
  if (!pathExists(hidden)) break;
7029
- hidden = path10.join(stagingBase, randomUUID2());
7190
+ hidden = path11.join(stagingBase, randomUUID2());
7030
7191
  }
7031
- fs9.renameSync(candidate, hidden);
7192
+ fs10.renameSync(candidate, hidden);
7032
7193
  renames.push({ original: candidate, hidden });
7033
7194
  }
7034
7195
  return renames;
@@ -7038,7 +7199,7 @@ function restoreEmbeddedGitDirs(renames) {
7038
7199
  const rename = renames[index];
7039
7200
  if (!rename) continue;
7040
7201
  try {
7041
- fs9.renameSync(rename.hidden, rename.original);
7202
+ fs10.renameSync(rename.hidden, rename.original);
7042
7203
  } catch {
7043
7204
  }
7044
7205
  }
@@ -7597,49 +7758,6 @@ function saveReportJson(report, notSaved) {
7597
7758
  };
7598
7759
  }
7599
7760
 
7600
- // src/bind.ts
7601
- import { spawn as spawn4 } from "node:child_process";
7602
- import fs10 from "node:fs";
7603
- import path11 from "node:path";
7604
- import { fileURLToPath as fileURLToPath2 } from "node:url";
7605
- var BIN_REL = path11.join("bin", "instafy-bind.mjs");
7606
- function resolveBindBin(env = process.env) {
7607
- const fromEnv = env.INSTAFY_BIND_BIN?.trim();
7608
- if (fromEnv) return fs10.existsSync(fromEnv) ? path11.resolve(fromEnv) : null;
7609
- const here = path11.dirname(fileURLToPath2(import.meta.url));
7610
- for (const candidate of [path11.join(here, "bind", BIN_REL), path11.resolve(here, "..", "..", "instafy-bind", BIN_REL)]) {
7611
- if (fs10.existsSync(candidate)) return candidate;
7612
- }
7613
- return null;
7614
- }
7615
- function runBind(args) {
7616
- const bin = resolveBindBin();
7617
- if (!bin) {
7618
- process.stderr.write(
7619
- "instafy bind: the bundled binder (dist/bind) was not found; rebuild the CLI, or set INSTAFY_BIND_BIN to packages/instafy-bind/bin/instafy-bind.mjs, or run it directly: node packages/instafy-bind/bin/instafy-bind.mjs <command>\n"
7620
- );
7621
- return Promise.resolve(1);
7622
- }
7623
- return new Promise((resolve) => {
7624
- const child = spawn4(process.execPath, [bin, ...args], { stdio: "inherit", env: process.env });
7625
- const forward = (signal) => {
7626
- if (!child.killed) child.kill(signal);
7627
- };
7628
- process.on("SIGINT", forward);
7629
- process.on("SIGTERM", forward);
7630
- child.on("error", (error) => {
7631
- process.stderr.write(`instafy bind: ${error.message}
7632
- `);
7633
- resolve(1);
7634
- });
7635
- child.on("exit", (code, signal) => {
7636
- process.off("SIGINT", forward);
7637
- process.off("SIGTERM", forward);
7638
- resolve(code ?? (signal ? 130 : 1));
7639
- });
7640
- });
7641
- }
7642
-
7643
7761
  // src/site-check.ts
7644
7762
  import { spawnSync as spawnSync5 } from "node:child_process";
7645
7763
  import { once } from "node:events";
@@ -10990,7 +11108,7 @@ async function teamRevokeLink(options) {
10990
11108
  init_project_manifest();
10991
11109
  import fs15 from "node:fs";
10992
11110
  import path16 from "node:path";
10993
- import { pathToFileURL } from "node:url";
11111
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
10994
11112
  import kleur16 from "kleur";
10995
11113
  function resolveBindingContext(startDir) {
10996
11114
  const lookup = findProjectManifest(path16.resolve(startDir));
@@ -11110,7 +11228,7 @@ function grantProviderBinding(options) {
11110
11228
  const record2 = {
11111
11229
  providerId,
11112
11230
  projectId: context.projectId,
11113
- rootUri: pathToFileURL(context.projectRoot).toString(),
11231
+ rootUri: pathToFileURL2(context.projectRoot).toString(),
11114
11232
  grantedCapabilities: capabilities,
11115
11233
  grantedPrefix: options.prefix?.trim() || `.instafy/providers/${providerId}/`,
11116
11234
  purpose: options.purpose?.trim() || null,
@@ -13489,7 +13607,10 @@ addServerUrlOptions(previewOpenCommand);
13489
13607
  addAccessTokenOptions(previewOpenCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
13490
13608
  previewOpenCommand.option("--conversation <id>", "Conversation for the card (only needed without a job token)").option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)").option(
13491
13609
  "--serve-cmd <command>",
13492
- "How to start the site again after the space sleeps, run from the current directory (one command, no shell); default: read from the process listening on the port"
13610
+ "How to start the site again when the process that started it exits, or the space sleeps, run from the current directory (one command, no shell); default: read from the process listening on the port"
13611
+ ).option(
13612
+ "--start",
13613
+ "Start --serve-cmd in the background, in its own session, without Instafy credentials, logging to .instafy/agent/logs, and wait for the port (nothing is started when the port already answers)"
13493
13614
  ).option("--json", "Output JSON").action(async (opts) => {
13494
13615
  try {
13495
13616
  const result = await previewOpen({
@@ -13501,14 +13622,15 @@ previewOpenCommand.option("--conversation <id>", "Conversation for the card (onl
13501
13622
  title: opts.title,
13502
13623
  path: opts.path,
13503
13624
  ratholeBin: opts.ratholeBin,
13504
- serveCmd: opts.serveCmd
13625
+ serveCmd: opts.serveCmd,
13626
+ start: Boolean(opts.start)
13505
13627
  });
13506
13628
  printPreviewOpen(result, Boolean(opts.json));
13507
13629
  } catch (error) {
13508
13630
  exitWithPreviewError(error);
13509
13631
  }
13510
13632
  });
13511
- var previewListCommand = previewCommand.command("list").description("List this space's previews");
13633
+ var previewListCommand = previewCommand.command("list").description("List this space's open previews (it does not check the sites: `instafy preview check` does)");
13512
13634
  addSpaceOption(previewListCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
13513
13635
  addServerUrlOptions(previewListCommand);
13514
13636
  addAccessTokenOptions(previewListCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
@@ -13956,7 +14078,7 @@ async function runCli(argv = process.argv) {
13956
14078
  }
13957
14079
  await program.parseAsync(argv);
13958
14080
  }
13959
- if (import.meta.url === pathToFileURL2(process.argv[1] ?? "").href) {
14081
+ if (import.meta.url === pathToFileURL3(process.argv[1] ?? "").href) {
13960
14082
  void runCli(process.argv);
13961
14083
  }
13962
14084
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "description": "Run Instafy spaces locally, link folders to Studio, and share previews/webhooks via tunnels.",
6
6
  "private": false,