@instafy/cli 0.1.7 → 0.1.8-staging.348

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/dist/index.js CHANGED
@@ -2,11 +2,14 @@ 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";
8
+ import { runGitCredentialHelper } from "./git-credential.js";
7
9
  import { projectInit } from "./project.js";
8
- import { runTunnelCommand } from "./tunnel.js";
10
+ import { listTunnelSessions, startTunnelDetached, stopTunnelSession, tailTunnelLogs, runTunnelCommand } from "./tunnel.js";
9
11
  import { requestControllerApi } from "./api.js";
12
+ import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
10
13
  export const program = new Command();
11
14
  const require = createRequire(import.meta.url);
12
15
  const pkg = require("../package.json");
@@ -32,6 +35,114 @@ program
32
35
  .name("instafy")
33
36
  .description("Instafy CLI — run your project locally and connect to Studio")
34
37
  .version(pkg.version ?? "0.1.0");
38
+ program
39
+ .command("login")
40
+ .description("Log in and save an access token for future CLI commands")
41
+ .option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
42
+ .option("--server-url <url>", "Instafy server/controller URL")
43
+ .option("--token <token>", "Provide token directly (skips prompt)")
44
+ .option("--no-git-setup", "Do not configure git credential helper")
45
+ .option("--no-store", "Do not save token to ~/.instafy/config.json")
46
+ .option("--json", "Output JSON")
47
+ .action(async (opts) => {
48
+ try {
49
+ await login({
50
+ controllerUrl: opts.serverUrl,
51
+ studioUrl: opts.studioUrl,
52
+ token: opts.token,
53
+ gitSetup: opts.gitSetup,
54
+ noStore: opts.store === false,
55
+ json: opts.json,
56
+ });
57
+ }
58
+ catch (error) {
59
+ console.error(kleur.red(String(error)));
60
+ process.exit(1);
61
+ }
62
+ });
63
+ program
64
+ .command("logout")
65
+ .description("Clear the saved CLI access token")
66
+ .option("--json", "Output JSON")
67
+ .action(async (opts) => {
68
+ try {
69
+ await logout({ json: opts.json });
70
+ }
71
+ catch (error) {
72
+ console.error(kleur.red(String(error)));
73
+ process.exit(1);
74
+ }
75
+ });
76
+ const configCommand = program.command("config").description("Get/set saved CLI configuration");
77
+ configCommand
78
+ .command("path")
79
+ .description("Print the config file path")
80
+ .option("--json", "Output JSON")
81
+ .action(async (opts) => {
82
+ try {
83
+ configPath({ json: opts.json });
84
+ }
85
+ catch (error) {
86
+ console.error(kleur.red(String(error)));
87
+ process.exit(1);
88
+ }
89
+ });
90
+ configCommand
91
+ .command("list")
92
+ .description("List saved configuration")
93
+ .option("--json", "Output JSON")
94
+ .action(async (opts) => {
95
+ try {
96
+ configList({ json: opts.json });
97
+ }
98
+ catch (error) {
99
+ console.error(kleur.red(String(error)));
100
+ process.exit(1);
101
+ }
102
+ });
103
+ configCommand
104
+ .command("get")
105
+ .description("Get a config value (controller-url, studio-url)")
106
+ .argument("<key>", "Config key")
107
+ .option("--json", "Output JSON")
108
+ .action(async (key, opts) => {
109
+ try {
110
+ configGet({ key, json: opts.json });
111
+ }
112
+ catch (error) {
113
+ console.error(kleur.red(String(error)));
114
+ process.exit(1);
115
+ }
116
+ });
117
+ configCommand
118
+ .command("set")
119
+ .description("Set a config value (controller-url, studio-url)")
120
+ .argument("<key>", "Config key")
121
+ .argument("<value>", "Config value")
122
+ .option("--json", "Output JSON")
123
+ .action(async (key, value, opts) => {
124
+ try {
125
+ configSet({ key, value, json: opts.json });
126
+ }
127
+ catch (error) {
128
+ console.error(kleur.red(String(error)));
129
+ process.exit(1);
130
+ }
131
+ });
132
+ configCommand
133
+ .command("unset")
134
+ .description("Unset a config value (controller-url, studio-url)")
135
+ .argument("<key>", "Config key")
136
+ .option("--json", "Output JSON")
137
+ .action(async (key, opts) => {
138
+ try {
139
+ configUnset({ key, json: opts.json });
140
+ }
141
+ catch (error) {
142
+ console.error(kleur.red(String(error)));
143
+ process.exit(1);
144
+ }
145
+ });
35
146
  const runtimeStartCommand = program
36
147
  .command("runtime:start")
37
148
  .description("Start a local runtime for this project")
@@ -46,6 +157,7 @@ runtimeStartCommand
46
157
  .option("--codex-bin <path>", "Path to codex binary (fallback to PATH)")
47
158
  .option("--proxy-base-url <url>", "Codex proxy base URL")
48
159
  .option("--workspace <path>", "Workspace directory (defaults to ./.instafy/workspace)")
160
+ .option("--runtime-mode <mode>", "Runtime runner (auto|docker|process)", "auto")
49
161
  .option("--origin-id <uuid>", "Origin ID to use (auto-generated if omitted)")
50
162
  .option("--origin-endpoint <url>", "Explicit origin endpoint (skip tunnel setup when provided)")
51
163
  .option("--origin-token <token>", "Runtime/origin access token for Studio registration")
@@ -100,7 +212,7 @@ program
100
212
  const runtimeTokenCommand = program
101
213
  .command("runtime:token")
102
214
  .description("Mint a runtime access token")
103
- .requiredOption("--project <id>", "Project UUID");
215
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
104
216
  addServerUrlOptions(runtimeTokenCommand);
105
217
  addAccessTokenOptions(runtimeTokenCommand, "Instafy access token (required)");
106
218
  runtimeTokenCommand
@@ -109,8 +221,14 @@ runtimeTokenCommand
109
221
  .option("--json", "Output token as JSON")
110
222
  .action(async (opts) => {
111
223
  try {
224
+ const project = typeof opts.project === "string" && opts.project.trim()
225
+ ? opts.project.trim()
226
+ : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
227
+ if (!project) {
228
+ throw new Error("No project configured. Run `instafy project:init` or pass --project.");
229
+ }
112
230
  await runtimeToken({
113
- project: opts.project,
231
+ project,
114
232
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
115
233
  controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
116
234
  runtimeId: opts.runtimeId,
@@ -126,7 +244,7 @@ runtimeTokenCommand
126
244
  const gitTokenCommand = program
127
245
  .command("git:token")
128
246
  .description("Mint a git access token for the project repo")
129
- .requiredOption("--project <id>", "Project UUID");
247
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
130
248
  addServerUrlOptions(gitTokenCommand);
131
249
  addAccessTokenOptions(gitTokenCommand, "Instafy access token (required)");
132
250
  gitTokenCommand
@@ -137,13 +255,19 @@ gitTokenCommand
137
255
  .option("--json", "Output token response as JSON")
138
256
  .action(async (opts) => {
139
257
  try {
258
+ const project = typeof opts.project === "string" && opts.project.trim()
259
+ ? opts.project.trim()
260
+ : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
261
+ if (!project) {
262
+ throw new Error("No project configured. Run `instafy project:init` or pass --project.");
263
+ }
140
264
  const ttlSecondsRaw = typeof opts.ttlSeconds === "string" ? opts.ttlSeconds.trim() : "";
141
265
  const ttlSeconds = ttlSecondsRaw.length > 0 ? Number.parseInt(ttlSecondsRaw, 10) : undefined;
142
266
  if (ttlSecondsRaw.length > 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) {
143
267
  throw new Error("--ttl-seconds must be a positive integer");
144
268
  }
145
269
  await gitToken({
146
- project: opts.project,
270
+ project,
147
271
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
148
272
  controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
149
273
  supabaseAccessToken: opts.supabaseAccessToken,
@@ -158,6 +282,19 @@ gitTokenCommand
158
282
  process.exit(1);
159
283
  }
160
284
  });
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
+ });
161
298
  const projectInitCommand = program
162
299
  .command("project:init")
163
300
  .description("Create an Instafy project and link this folder (.instafy/project.json)")
@@ -192,22 +329,130 @@ projectInitCommand
192
329
  });
193
330
  const tunnelCommand = program
194
331
  .command("tunnel")
195
- .description("Create a shareable tunnel URL for a local port")
196
- .option("--project <id>", "Project UUID (or set PROJECT_ID)");
332
+ .description("Start a shareable tunnel URL for a local port (defaults to detached)")
333
+ .option("--port <port>", "Local port to expose (default 3000)")
334
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
197
335
  addServerUrlOptions(tunnelCommand);
336
+ addAccessTokenOptions(tunnelCommand, "Instafy access token (defaults to saved `instafy login` token)");
198
337
  addServiceTokenOptions(tunnelCommand, "Instafy service token (advanced)");
199
338
  tunnelCommand
200
- .option("--port <port>", "Local port to expose (default 3000)")
339
+ .option("--no-detach", "Run in foreground until interrupted")
201
340
  .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")
202
343
  .action(async (opts) => {
203
344
  try {
204
345
  const port = opts.port ? Number(opts.port) : undefined;
205
- await runTunnelCommand({
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({
206
361
  project: opts.project,
207
362
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
208
- controllerToken: opts.serviceToken ?? opts.controllerToken,
363
+ controllerToken,
209
364
  port,
210
365
  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),
211
456
  });
212
457
  }
213
458
  catch (error) {
@@ -292,22 +537,22 @@ function configureApiCommand(command, method) {
292
537
  }
293
538
  const apiGetCommand = program
294
539
  .command("api:get")
295
- .description("Perform an authenticated GET request to the controller API")
540
+ .description("Advanced: authenticated GET request to the controller API")
296
541
  .argument("<path>", "API path (or full URL), e.g. /conversations/<id>/messages?limit=50");
297
542
  configureApiCommand(apiGetCommand, "GET");
298
543
  const apiPostCommand = program
299
544
  .command("api:post")
300
- .description("Perform an authenticated POST request to the controller API")
545
+ .description("Advanced: authenticated POST request to the controller API")
301
546
  .argument("<path>", "API path (or full URL)");
302
547
  configureApiCommand(apiPostCommand, "POST");
303
548
  const apiPatchCommand = program
304
549
  .command("api:patch")
305
- .description("Perform an authenticated PATCH request to the controller API")
550
+ .description("Advanced: authenticated PATCH request to the controller API")
306
551
  .argument("<path>", "API path (or full URL)");
307
552
  configureApiCommand(apiPatchCommand, "PATCH");
308
553
  const apiDeleteCommand = program
309
554
  .command("api:delete")
310
- .description("Perform an authenticated DELETE request to the controller API")
555
+ .description("Advanced: authenticated DELETE request to the controller API")
311
556
  .argument("<path>", "API path (or full URL)");
312
557
  configureApiCommand(apiDeleteCommand, "DELETE");
313
558
  export async function runCli(argv = process.argv) {
package/dist/org.js CHANGED
@@ -1,24 +1,11 @@
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;
13
- }
2
+ import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
3
+ import { formatAuthRequiredError } from "./errors.js";
14
4
  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"]);
5
+ const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
6
+ const token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
20
7
  if (!token) {
21
- throw new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
8
+ throw formatAuthRequiredError({ retryCommand: "instafy org:list" });
22
9
  }
23
10
  const response = await fetch(`${controllerUrl}/orgs`, {
24
11
  headers: { authorization: `Bearer ${token}` },
package/dist/project.js CHANGED
@@ -1,18 +1,8 @@
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;
15
- }
4
+ import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
5
+ import { formatAuthRequiredError } from "./errors.js";
16
6
  async function fetchOrganizations(controllerUrl, token) {
17
7
  const response = await fetch(`${controllerUrl}/orgs`, {
18
8
  headers: {
@@ -73,13 +63,10 @@ async function resolveOrg(controllerUrl, token, options) {
73
63
  return { orgId, orgName: json.org_name ?? null };
74
64
  }
75
65
  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"]);
66
+ const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
67
+ const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
81
68
  if (!token) {
82
- throw new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
69
+ throw formatAuthRequiredError({ retryCommand: "instafy project:list" });
83
70
  }
84
71
  const orgs = await fetchOrganizations(controllerUrl, token);
85
72
  let targetOrgs = orgs;
@@ -124,13 +111,13 @@ export async function listProjects(options) {
124
111
  }
125
112
  export async function projectInit(options) {
126
113
  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"]);
114
+ const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
115
+ const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
132
116
  if (!token) {
133
- throw new Error("Instafy or Supabase access token is required (--access-token, INSTAFY_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
117
+ throw formatAuthRequiredError({
118
+ retryCommand: "instafy project:init",
119
+ advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
120
+ });
134
121
  }
135
122
  const org = await resolveOrg(controllerUrl, token, options);
136
123
  const body = {