@instafy/cli 0.1.8 → 0.1.9

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
@@ -4,11 +4,13 @@ import { createRequire } from "node:module";
4
4
  import kleur from "kleur";
5
5
  import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, findProjectManifest, } from "./runtime.js";
6
6
  import { login, logout } from "./auth.js";
7
- import { gitToken } from "./git.js";
8
- import { projectInit } from "./project.js";
9
- import { runTunnelCommand } from "./tunnel.js";
7
+ import { runGitCredentialHelper } from "./git-credential.js";
8
+ import { projectInit, projectProfile } from "./project.js";
9
+ import { listTunnelSessions, startTunnelDetached, stopTunnelSession, tailTunnelLogs, runTunnelCommand } from "./tunnel.js";
10
10
  import { requestControllerApi } from "./api.js";
11
11
  import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
12
+ import { getInstafyProfileConfigPath, listInstafyProfileNames, readInstafyProfileConfig } from "./config.js";
13
+ import { runInstafyGit, runInstafyGitSync } from "./git-wrapper.js";
12
14
  export const program = new Command();
13
15
  const require = createRequire(import.meta.url);
14
16
  const pkg = require("../package.json");
@@ -39,7 +41,11 @@ program
39
41
  .description("Log in and save an access token for future CLI commands")
40
42
  .option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
41
43
  .option("--server-url <url>", "Instafy server/controller URL")
44
+ .option("--profile <name>", "Save token under a named profile (multi-account support)")
42
45
  .option("--token <token>", "Provide token directly (skips prompt)")
46
+ .option("--email <email>", "Email for non-interactive login (requires SUPABASE_URL + SUPABASE_ANON_KEY)")
47
+ .option("--password <password>", "Password for non-interactive login (requires SUPABASE_URL + SUPABASE_ANON_KEY)")
48
+ .option("--no-git-setup", "Do not configure git credential helper")
43
49
  .option("--no-store", "Do not save token to ~/.instafy/config.json")
44
50
  .option("--json", "Output JSON")
45
51
  .action(async (opts) => {
@@ -48,7 +54,11 @@ program
48
54
  controllerUrl: opts.serverUrl,
49
55
  studioUrl: opts.studioUrl,
50
56
  token: opts.token,
57
+ email: opts.email,
58
+ password: opts.password,
59
+ gitSetup: opts.gitSetup,
51
60
  noStore: opts.store === false,
61
+ profile: opts.profile,
52
62
  json: opts.json,
53
63
  });
54
64
  }
@@ -60,10 +70,94 @@ program
60
70
  program
61
71
  .command("logout")
62
72
  .description("Clear the saved CLI access token")
73
+ .option("--profile <name>", "Clear the token for a named profile")
63
74
  .option("--json", "Output JSON")
64
75
  .action(async (opts) => {
65
76
  try {
66
- await logout({ json: opts.json });
77
+ await logout({ json: opts.json, profile: opts.profile });
78
+ }
79
+ catch (error) {
80
+ console.error(kleur.red(String(error)));
81
+ process.exit(1);
82
+ }
83
+ });
84
+ const projectCommand = program
85
+ .command("project")
86
+ .description("Create/link and manage Instafy projects");
87
+ projectCommand.action(() => projectCommand.outputHelp());
88
+ const projectInitCommand = projectCommand
89
+ .command("init")
90
+ .description("Create an Instafy project and link this folder (.instafy/project.json)")
91
+ .option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
92
+ addServerUrlOptions(projectInitCommand);
93
+ projectInitCommand
94
+ .option("--access-token <token>", "Instafy or Supabase access token")
95
+ .option("--profile <name>", "CLI profile to use when running commands in this folder")
96
+ .option("--project-type <type>", "Project type (customer|sandbox)")
97
+ .option("--org-id <uuid>", "Optional organization id")
98
+ .option("--org-name <name>", "Optional organization name")
99
+ .option("--org-slug <slug>", "Optional organization slug")
100
+ .option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
101
+ .option("--json", "Output JSON")
102
+ .action(async (opts) => {
103
+ try {
104
+ await projectInit({
105
+ path: opts.path,
106
+ controllerUrl: opts.serverUrl ?? opts.controllerUrl,
107
+ accessToken: opts.accessToken,
108
+ profile: opts.profile,
109
+ projectType: opts.projectType,
110
+ orgId: opts.orgId,
111
+ orgName: opts.orgName,
112
+ orgSlug: opts.orgSlug,
113
+ ownerUserId: opts.ownerUserId,
114
+ json: opts.json,
115
+ });
116
+ }
117
+ catch (error) {
118
+ console.error(kleur.red(String(error)));
119
+ process.exit(1);
120
+ }
121
+ });
122
+ projectCommand
123
+ .command("profile")
124
+ .description("Get/set the CLI profile for this folder (.instafy/project.json)")
125
+ .argument("[profile]", "Profile name to set (omit to print current)")
126
+ .option("--unset", "Clear the configured profile for this folder")
127
+ .option("--path <dir>", "Directory to search for the manifest (default: cwd)")
128
+ .option("--json", "Output JSON")
129
+ .action(async (profile, opts) => {
130
+ try {
131
+ projectProfile({
132
+ profile,
133
+ unset: opts.unset,
134
+ path: opts.path,
135
+ json: opts.json,
136
+ });
137
+ }
138
+ catch (error) {
139
+ console.error(kleur.red(String(error)));
140
+ process.exit(1);
141
+ }
142
+ });
143
+ const projectListCommand = projectCommand
144
+ .command("list")
145
+ .description("List projects for your account")
146
+ .option("--access-token <token>", "Instafy or Supabase access token");
147
+ addServerUrlOptions(projectListCommand);
148
+ projectListCommand
149
+ .option("--org-id <uuid>", "Filter by organization id")
150
+ .option("--org-slug <slug>", "Filter by organization slug")
151
+ .option("--json", "Output JSON")
152
+ .action(async (opts) => {
153
+ try {
154
+ await (await import("./project.js")).listProjects({
155
+ controllerUrl: opts.serverUrl ?? opts.controllerUrl,
156
+ accessToken: opts.accessToken,
157
+ orgId: opts.orgId,
158
+ orgSlug: opts.orgSlug,
159
+ json: opts.json,
160
+ });
67
161
  }
68
162
  catch (error) {
69
163
  console.error(kleur.red(String(error)));
@@ -71,6 +165,7 @@ program
71
165
  }
72
166
  });
73
167
  const configCommand = program.command("config").description("Get/set saved CLI configuration");
168
+ configCommand.action(() => configCommand.outputHelp());
74
169
  configCommand
75
170
  .command("path")
76
171
  .description("Print the config file path")
@@ -140,8 +235,12 @@ configCommand
140
235
  process.exit(1);
141
236
  }
142
237
  });
143
- const runtimeStartCommand = program
144
- .command("runtime:start")
238
+ const runtimeCommand = program
239
+ .command("runtime")
240
+ .description("Start/stop the local Instafy runtime");
241
+ runtimeCommand.action(() => runtimeCommand.outputHelp());
242
+ const runtimeStartCommand = runtimeCommand
243
+ .command("start")
145
244
  .description("Start a local runtime for this project")
146
245
  .option("--project <id>", "Project UUID");
147
246
  addServerUrlOptions(runtimeStartCommand);
@@ -180,8 +279,8 @@ runtimeStartCommand
180
279
  process.exit(1);
181
280
  }
182
281
  });
183
- program
184
- .command("runtime:status")
282
+ runtimeCommand
283
+ .command("status")
185
284
  .description("Show runtime health")
186
285
  .option("--json", "Output status as JSON")
187
286
  .action(async (opts) => {
@@ -193,8 +292,8 @@ program
193
292
  process.exit(1);
194
293
  }
195
294
  });
196
- program
197
- .command("runtime:stop")
295
+ runtimeCommand
296
+ .command("stop")
198
297
  .description("Stop the local Instafy runtime")
199
298
  .option("--json", "Output result as JSON")
200
299
  .action(async (opts) => {
@@ -206,8 +305,8 @@ program
206
305
  process.exit(1);
207
306
  }
208
307
  });
209
- const runtimeTokenCommand = program
210
- .command("runtime:token")
308
+ const runtimeTokenCommand = runtimeCommand
309
+ .command("token")
211
310
  .description("Mint a runtime access token")
212
311
  .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
213
312
  addServerUrlOptions(runtimeTokenCommand);
@@ -222,7 +321,7 @@ runtimeTokenCommand
222
321
  ? opts.project.trim()
223
322
  : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
224
323
  if (!project) {
225
- throw new Error("No project configured. Run `instafy project:init` or pass --project.");
324
+ throw new Error("No project configured. Run `instafy project init` or pass --project.");
226
325
  }
227
326
  await runtimeToken({
228
327
  project,
@@ -238,101 +337,140 @@ runtimeTokenCommand
238
337
  process.exit(1);
239
338
  }
240
339
  });
241
- const gitTokenCommand = program
242
- .command("git:token")
243
- .description("Mint a git access token for the project repo")
244
- .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
245
- addServerUrlOptions(gitTokenCommand);
246
- addAccessTokenOptions(gitTokenCommand, "Instafy access token (required)");
247
- gitTokenCommand
248
- .option("--supabase-access-token <token>", "Supabase session token (alternative to Studio token)")
249
- .option("--supabase-access-token-file <path>", "File containing the Supabase session token")
250
- .option("--scope <scope...>", "Requested scopes (git.read, git.write)")
251
- .option("--ttl-seconds <seconds>", "Token TTL in seconds (default server policy)")
252
- .option("--json", "Output token response as JSON")
340
+ program
341
+ .command("git")
342
+ .description("Run git against the Instafy canonical checkout (.instafy/.git)")
343
+ .allowUnknownOption(true);
344
+ const tunnelCommand = program
345
+ .command("tunnel")
346
+ .description("Create and manage shareable tunnels");
347
+ tunnelCommand.action(() => tunnelCommand.outputHelp());
348
+ const tunnelStartCommand = tunnelCommand
349
+ .command("start")
350
+ .description("Create a shareable tunnel URL for a local port (defaults to detached)")
351
+ .option("--port <port>", "Local port to expose (default 3000)")
352
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
353
+ addServerUrlOptions(tunnelStartCommand);
354
+ addAccessTokenOptions(tunnelStartCommand, "Instafy access token (defaults to saved `instafy login` token)");
355
+ addServiceTokenOptions(tunnelStartCommand, "Instafy service token (advanced)");
356
+ tunnelStartCommand
357
+ .option("--no-detach", "Run in foreground until interrupted")
358
+ .option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)")
359
+ .option("--log-file <path>", "Write tunnel logs to a file (default: ~/.instafy/cli-tunnel-logs/*)")
360
+ .option("--json", "Output JSON")
253
361
  .action(async (opts) => {
254
362
  try {
255
- const project = typeof opts.project === "string" && opts.project.trim()
256
- ? opts.project.trim()
257
- : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
258
- if (!project) {
259
- throw new Error("No project configured. Run `instafy project:init` or pass --project.");
260
- }
261
- const ttlSecondsRaw = typeof opts.ttlSeconds === "string" ? opts.ttlSeconds.trim() : "";
262
- const ttlSeconds = ttlSecondsRaw.length > 0 ? Number.parseInt(ttlSecondsRaw, 10) : undefined;
263
- if (ttlSecondsRaw.length > 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) {
264
- throw new Error("--ttl-seconds must be a positive integer");
363
+ const port = opts.port ? Number(opts.port) : undefined;
364
+ const controllerToken = opts.serviceToken ??
365
+ opts.controllerToken ??
366
+ opts.accessToken ??
367
+ opts.controllerAccessToken;
368
+ if (opts.detach === false) {
369
+ await runTunnelCommand({
370
+ project: opts.project,
371
+ controllerUrl: opts.serverUrl ?? opts.controllerUrl,
372
+ controllerToken,
373
+ port,
374
+ ratholeBin: opts.ratholeBin,
375
+ });
376
+ return;
265
377
  }
266
- await gitToken({
267
- project,
378
+ const started = await startTunnelDetached({
379
+ project: opts.project,
268
380
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
269
- controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
270
- supabaseAccessToken: opts.supabaseAccessToken,
271
- supabaseAccessTokenFile: opts.supabaseAccessTokenFile,
272
- scopes: opts.scope,
273
- ttlSeconds,
274
- json: opts.json,
381
+ controllerToken,
382
+ port,
383
+ ratholeBin: opts.ratholeBin,
384
+ logFile: opts.logFile,
275
385
  });
386
+ if (opts.json) {
387
+ console.log(JSON.stringify(started, null, 2));
388
+ return;
389
+ }
390
+ console.log(kleur.green(`Tunnel started: ${started.url} (tunnelId=${started.tunnelId})`));
391
+ console.log(kleur.gray(`pid=${started.pid} · port=${started.localPort}`));
392
+ console.log(kleur.gray(`log: ${started.logFile}`));
393
+ console.log("");
394
+ console.log("Next:");
395
+ console.log(`- ${kleur.cyan(`instafy tunnel list`)}`);
396
+ console.log(`- ${kleur.cyan(`instafy tunnel logs ${started.tunnelId} --follow`)}`);
397
+ console.log(`- ${kleur.cyan(`instafy tunnel stop ${started.tunnelId}`)}`);
398
+ if (process.platform !== "win32") {
399
+ console.log(kleur.gray(`(or) tail -n 200 -f ${started.logFile}`));
400
+ }
276
401
  }
277
402
  catch (error) {
278
403
  console.error(kleur.red(String(error)));
279
404
  process.exit(1);
280
405
  }
281
406
  });
282
- const projectInitCommand = program
283
- .command("project:init")
284
- .description("Create an Instafy project and link this folder (.instafy/project.json)")
285
- .option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
286
- addServerUrlOptions(projectInitCommand);
287
- projectInitCommand
288
- .option("--access-token <token>", "Instafy or Supabase access token")
289
- .option("--project-type <type>", "Project type (customer|sandbox)")
290
- .option("--org-id <uuid>", "Optional organization id")
291
- .option("--org-name <name>", "Optional organization name")
292
- .option("--org-slug <slug>", "Optional organization slug")
293
- .option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
407
+ tunnelCommand
408
+ .command("list")
409
+ .description("List local tunnel sessions started by this CLI")
410
+ .option("--all", "Include stopped/stale tunnels")
294
411
  .option("--json", "Output JSON")
295
412
  .action(async (opts) => {
296
413
  try {
297
- await projectInit({
298
- path: opts.path,
299
- controllerUrl: opts.serverUrl ?? opts.controllerUrl,
300
- accessToken: opts.accessToken,
301
- projectType: opts.projectType,
302
- orgId: opts.orgId,
303
- orgName: opts.orgName,
304
- orgSlug: opts.orgSlug,
305
- ownerUserId: opts.ownerUserId,
414
+ const tunnels = listTunnelSessions({ all: Boolean(opts.all), json: Boolean(opts.json) });
415
+ if (opts.json) {
416
+ console.log(JSON.stringify(tunnels, null, 2));
417
+ return;
418
+ }
419
+ if (tunnels.length === 0) {
420
+ console.log(kleur.yellow("No tunnels found."));
421
+ return;
422
+ }
423
+ for (const tunnel of tunnels) {
424
+ console.log(`${tunnel.tunnelId} · ${tunnel.url} · port=${tunnel.localPort} · pid=${tunnel.pid}`);
425
+ }
426
+ }
427
+ catch (error) {
428
+ console.error(kleur.red(String(error)));
429
+ process.exit(1);
430
+ }
431
+ });
432
+ tunnelCommand
433
+ .command("stop")
434
+ .description("Stop a local tunnel session and revoke it")
435
+ .argument("[tunnelId]", "Tunnel ID (defaults to the only active tunnel)")
436
+ .option("--server-url <url>", "Instafy server URL")
437
+ .option("--access-token <token>", "Instafy access token (defaults to saved `instafy login` token)")
438
+ .option("--service-token <token>", "Instafy service token (advanced)")
439
+ .option("--json", "Output JSON")
440
+ .action(async (tunnelId, opts) => {
441
+ try {
442
+ const result = await stopTunnelSession({
443
+ tunnelId,
444
+ controllerUrl: opts.serverUrl,
445
+ controllerToken: opts.serviceToken ?? opts.accessToken,
306
446
  json: opts.json,
307
447
  });
448
+ if (opts.json) {
449
+ console.log(JSON.stringify(result, null, 2));
450
+ return;
451
+ }
452
+ console.log(kleur.green(`Tunnel stopped: ${result.tunnelId}`));
308
453
  }
309
454
  catch (error) {
310
455
  console.error(kleur.red(String(error)));
311
456
  process.exit(1);
312
457
  }
313
458
  });
314
- const tunnelCommand = program
315
- .command("tunnel")
316
- .description("Create a shareable tunnel URL for a local port")
317
- .option("--port <port>", "Local port to expose (default 3000)")
318
- .option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
319
- addServerUrlOptions(tunnelCommand);
320
- addAccessTokenOptions(tunnelCommand, "Instafy access token (defaults to saved `instafy login` token)");
321
- addServiceTokenOptions(tunnelCommand, "Instafy service token (advanced)");
322
459
  tunnelCommand
323
- .option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)")
324
- .action(async (opts) => {
460
+ .command("logs")
461
+ .description("Show logs for a local tunnel session")
462
+ .argument("[tunnelId]", "Tunnel ID (defaults to the only active tunnel)")
463
+ .option("--lines <n>", "Number of lines to show", "200")
464
+ .option("--follow", "Follow log output (like tail -f)")
465
+ .option("--json", "Output JSON")
466
+ .action(async (tunnelId, opts) => {
325
467
  try {
326
- const port = opts.port ? Number(opts.port) : undefined;
327
- await runTunnelCommand({
328
- project: opts.project,
329
- controllerUrl: opts.serverUrl ?? opts.controllerUrl,
330
- controllerToken: opts.serviceToken ??
331
- opts.controllerToken ??
332
- opts.accessToken ??
333
- opts.controllerAccessToken,
334
- port,
335
- ratholeBin: opts.ratholeBin,
468
+ const lines = typeof opts.lines === "string" ? Number(opts.lines) : undefined;
469
+ await tailTunnelLogs({
470
+ tunnelId,
471
+ lines,
472
+ follow: Boolean(opts.follow),
473
+ json: Boolean(opts.json),
336
474
  });
337
475
  }
338
476
  catch (error) {
@@ -340,8 +478,12 @@ tunnelCommand
340
478
  process.exit(1);
341
479
  }
342
480
  });
343
- const orgListCommand = program
344
- .command("org:list")
481
+ const orgCommand = program
482
+ .command("org")
483
+ .description("Organization utilities");
484
+ orgCommand.action(() => orgCommand.outputHelp());
485
+ const orgListCommand = orgCommand
486
+ .command("list")
345
487
  .description("List organizations for your account");
346
488
  addServerUrlOptions(orgListCommand);
347
489
  orgListCommand
@@ -360,24 +502,42 @@ orgListCommand
360
502
  process.exit(1);
361
503
  }
362
504
  });
363
- const projectListCommand = program
364
- .command("project:list")
365
- .description("List projects for your account")
366
- .option("--access-token <token>", "Instafy or Supabase access token");
367
- addServerUrlOptions(projectListCommand);
368
- projectListCommand
369
- .option("--org-id <uuid>", "Filter by organization id")
370
- .option("--org-slug <slug>", "Filter by organization slug")
505
+ const profileCommand = program
506
+ .command("profile")
507
+ .description("Manage saved CLI profiles (~/.instafy/profiles)");
508
+ profileCommand.action(() => profileCommand.outputHelp());
509
+ profileCommand
510
+ .command("list")
511
+ .description("List saved CLI profiles")
371
512
  .option("--json", "Output JSON")
372
513
  .action(async (opts) => {
373
514
  try {
374
- await (await import("./project.js")).listProjects({
375
- controllerUrl: opts.serverUrl ?? opts.controllerUrl,
376
- accessToken: opts.accessToken,
377
- orgId: opts.orgId,
378
- orgSlug: opts.orgSlug,
379
- json: opts.json,
515
+ const names = listInstafyProfileNames();
516
+ const profiles = names.map((name) => {
517
+ const config = readInstafyProfileConfig(name);
518
+ return {
519
+ name,
520
+ path: getInstafyProfileConfigPath(name),
521
+ controllerUrl: config.controllerUrl ?? null,
522
+ studioUrl: config.studioUrl ?? null,
523
+ accessTokenSet: Boolean(config.accessToken),
524
+ updatedAt: config.updatedAt ?? null,
525
+ };
380
526
  });
527
+ if (opts.json) {
528
+ console.log(JSON.stringify({ profiles }, null, 2));
529
+ return;
530
+ }
531
+ if (profiles.length === 0) {
532
+ console.log(kleur.yellow("No profiles found."));
533
+ console.log(`Create one with: ${kleur.cyan("instafy login --profile <name>")}`);
534
+ return;
535
+ }
536
+ console.log(kleur.green("Instafy CLI profiles"));
537
+ for (const profile of profiles) {
538
+ const token = profile.accessTokenSet ? kleur.green("token") : kleur.yellow("no-token");
539
+ console.log(`- ${kleur.cyan(profile.name)} (${token})`);
540
+ }
381
541
  }
382
542
  catch (error) {
383
543
  console.error(kleur.red(String(error)));
@@ -415,23 +575,27 @@ function configureApiCommand(command, method) {
415
575
  }
416
576
  });
417
577
  }
418
- const apiGetCommand = program
419
- .command("api:get")
578
+ const apiCommand = program
579
+ .command("api", { hidden: true })
580
+ .description("Advanced: authenticated requests to the controller API");
581
+ apiCommand.action(() => apiCommand.outputHelp());
582
+ const apiGetCommand = apiCommand
583
+ .command("get")
420
584
  .description("Advanced: authenticated GET request to the controller API")
421
585
  .argument("<path>", "API path (or full URL), e.g. /conversations/<id>/messages?limit=50");
422
586
  configureApiCommand(apiGetCommand, "GET");
423
- const apiPostCommand = program
424
- .command("api:post")
587
+ const apiPostCommand = apiCommand
588
+ .command("post")
425
589
  .description("Advanced: authenticated POST request to the controller API")
426
590
  .argument("<path>", "API path (or full URL)");
427
591
  configureApiCommand(apiPostCommand, "POST");
428
- const apiPatchCommand = program
429
- .command("api:patch")
592
+ const apiPatchCommand = apiCommand
593
+ .command("patch")
430
594
  .description("Advanced: authenticated PATCH request to the controller API")
431
595
  .argument("<path>", "API path (or full URL)");
432
596
  configureApiCommand(apiPatchCommand, "PATCH");
433
- const apiDeleteCommand = program
434
- .command("api:delete")
597
+ const apiDeleteCommand = apiCommand
598
+ .command("delete")
435
599
  .description("Advanced: authenticated DELETE request to the controller API")
436
600
  .argument("<path>", "API path (or full URL)");
437
601
  configureApiCommand(apiDeleteCommand, "DELETE");
@@ -440,6 +604,27 @@ export async function runCli(argv = process.argv) {
440
604
  program.outputHelp();
441
605
  return;
442
606
  }
607
+ const args = argv.slice(2);
608
+ if (args[0] === "git") {
609
+ if (args[1] === "credential") {
610
+ try {
611
+ await runGitCredentialHelper(args[2] ?? "");
612
+ }
613
+ catch (error) {
614
+ console.error(String(error));
615
+ process.exitCode = 1;
616
+ }
617
+ return;
618
+ }
619
+ if (args[1] === "sync") {
620
+ const code = await runInstafyGitSync(args.slice(2));
621
+ process.exitCode = code;
622
+ return;
623
+ }
624
+ const code = runInstafyGit(args.slice(1));
625
+ process.exitCode = code;
626
+ return;
627
+ }
443
628
  await program.parseAsync(argv);
444
629
  }
445
630
  if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
package/dist/org.js CHANGED
@@ -1,19 +1,24 @@
1
1
  import kleur from "kleur";
2
2
  import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
3
- function formatAuthRequiredError() {
4
- return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
5
- }
3
+ import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
6
4
  export async function listOrganizations(params) {
7
5
  const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
8
6
  const token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
9
7
  if (!token) {
10
- throw formatAuthRequiredError();
8
+ throw formatAuthRequiredError({ retryCommand: "instafy org list" });
11
9
  }
12
10
  const response = await fetch(`${controllerUrl}/orgs`, {
13
11
  headers: { authorization: `Bearer ${token}` },
14
12
  });
15
13
  if (!response.ok) {
16
14
  const text = await response.text().catch(() => "");
15
+ if (response.status === 401 || response.status === 403) {
16
+ throw formatAuthRejectedError({
17
+ status: response.status,
18
+ responseBody: text,
19
+ retryCommand: "instafy org list",
20
+ });
21
+ }
17
22
  throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
18
23
  }
19
24
  const body = (await response.json());
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ export function findProjectManifest(startDir) {
4
+ let current = path.resolve(startDir);
5
+ const root = path.parse(current).root;
6
+ while (true) {
7
+ const candidate = path.join(current, ".instafy", "project.json");
8
+ if (existsSync(candidate)) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
11
+ if (parsed?.projectId) {
12
+ return { manifest: parsed, path: candidate };
13
+ }
14
+ }
15
+ catch {
16
+ // ignore malformed manifest
17
+ }
18
+ }
19
+ if (current === root)
20
+ break;
21
+ current = path.dirname(current);
22
+ }
23
+ return { manifest: null, path: null };
24
+ }