@instafy/cli 0.1.7 → 0.1.8-staging.350

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,146 @@ 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 projectInitCommand = program
77
+ .command("project:init")
78
+ .description("Create an Instafy project and link this folder (.instafy/project.json)")
79
+ .option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
80
+ addServerUrlOptions(projectInitCommand);
81
+ projectInitCommand
82
+ .option("--access-token <token>", "Instafy or Supabase access token")
83
+ .option("--project-type <type>", "Project type (customer|sandbox)")
84
+ .option("--org-id <uuid>", "Optional organization id")
85
+ .option("--org-name <name>", "Optional organization name")
86
+ .option("--org-slug <slug>", "Optional organization slug")
87
+ .option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
88
+ .option("--json", "Output JSON")
89
+ .action(async (opts) => {
90
+ try {
91
+ await projectInit({
92
+ path: opts.path,
93
+ controllerUrl: opts.serverUrl ?? opts.controllerUrl,
94
+ accessToken: opts.accessToken,
95
+ projectType: opts.projectType,
96
+ orgId: opts.orgId,
97
+ orgName: opts.orgName,
98
+ orgSlug: opts.orgSlug,
99
+ ownerUserId: opts.ownerUserId,
100
+ json: opts.json,
101
+ });
102
+ }
103
+ catch (error) {
104
+ console.error(kleur.red(String(error)));
105
+ process.exit(1);
106
+ }
107
+ });
108
+ const configCommand = program.command("config").description("Get/set saved CLI configuration");
109
+ configCommand
110
+ .command("path")
111
+ .description("Print the config file path")
112
+ .option("--json", "Output JSON")
113
+ .action(async (opts) => {
114
+ try {
115
+ configPath({ json: opts.json });
116
+ }
117
+ catch (error) {
118
+ console.error(kleur.red(String(error)));
119
+ process.exit(1);
120
+ }
121
+ });
122
+ configCommand
123
+ .command("list")
124
+ .description("List saved configuration")
125
+ .option("--json", "Output JSON")
126
+ .action(async (opts) => {
127
+ try {
128
+ configList({ json: opts.json });
129
+ }
130
+ catch (error) {
131
+ console.error(kleur.red(String(error)));
132
+ process.exit(1);
133
+ }
134
+ });
135
+ configCommand
136
+ .command("get")
137
+ .description("Get a config value (controller-url, studio-url)")
138
+ .argument("<key>", "Config key")
139
+ .option("--json", "Output JSON")
140
+ .action(async (key, opts) => {
141
+ try {
142
+ configGet({ key, json: opts.json });
143
+ }
144
+ catch (error) {
145
+ console.error(kleur.red(String(error)));
146
+ process.exit(1);
147
+ }
148
+ });
149
+ configCommand
150
+ .command("set")
151
+ .description("Set a config value (controller-url, studio-url)")
152
+ .argument("<key>", "Config key")
153
+ .argument("<value>", "Config value")
154
+ .option("--json", "Output JSON")
155
+ .action(async (key, value, opts) => {
156
+ try {
157
+ configSet({ key, value, json: opts.json });
158
+ }
159
+ catch (error) {
160
+ console.error(kleur.red(String(error)));
161
+ process.exit(1);
162
+ }
163
+ });
164
+ configCommand
165
+ .command("unset")
166
+ .description("Unset a config value (controller-url, studio-url)")
167
+ .argument("<key>", "Config key")
168
+ .option("--json", "Output JSON")
169
+ .action(async (key, opts) => {
170
+ try {
171
+ configUnset({ key, json: opts.json });
172
+ }
173
+ catch (error) {
174
+ console.error(kleur.red(String(error)));
175
+ process.exit(1);
176
+ }
177
+ });
35
178
  const runtimeStartCommand = program
36
179
  .command("runtime:start")
37
180
  .description("Start a local runtime for this project")
@@ -46,6 +189,7 @@ runtimeStartCommand
46
189
  .option("--codex-bin <path>", "Path to codex binary (fallback to PATH)")
47
190
  .option("--proxy-base-url <url>", "Codex proxy base URL")
48
191
  .option("--workspace <path>", "Workspace directory (defaults to ./.instafy/workspace)")
192
+ .option("--runtime-mode <mode>", "Runtime runner (auto|docker|process)", "auto")
49
193
  .option("--origin-id <uuid>", "Origin ID to use (auto-generated if omitted)")
50
194
  .option("--origin-endpoint <url>", "Explicit origin endpoint (skip tunnel setup when provided)")
51
195
  .option("--origin-token <token>", "Runtime/origin access token for Studio registration")
@@ -100,7 +244,7 @@ program
100
244
  const runtimeTokenCommand = program
101
245
  .command("runtime:token")
102
246
  .description("Mint a runtime access token")
103
- .requiredOption("--project <id>", "Project UUID");
247
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
104
248
  addServerUrlOptions(runtimeTokenCommand);
105
249
  addAccessTokenOptions(runtimeTokenCommand, "Instafy access token (required)");
106
250
  runtimeTokenCommand
@@ -109,8 +253,14 @@ runtimeTokenCommand
109
253
  .option("--json", "Output token as JSON")
110
254
  .action(async (opts) => {
111
255
  try {
256
+ const project = typeof opts.project === "string" && opts.project.trim()
257
+ ? opts.project.trim()
258
+ : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
259
+ if (!project) {
260
+ throw new Error("No project configured. Run `instafy project:init` or pass --project.");
261
+ }
112
262
  await runtimeToken({
113
- project: opts.project,
263
+ project,
114
264
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
115
265
  controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
116
266
  runtimeId: opts.runtimeId,
@@ -126,7 +276,7 @@ runtimeTokenCommand
126
276
  const gitTokenCommand = program
127
277
  .command("git:token")
128
278
  .description("Mint a git access token for the project repo")
129
- .requiredOption("--project <id>", "Project UUID");
279
+ .option("--project <id>", "Project UUID (defaults to .instafy/project.json)");
130
280
  addServerUrlOptions(gitTokenCommand);
131
281
  addAccessTokenOptions(gitTokenCommand, "Instafy access token (required)");
132
282
  gitTokenCommand
@@ -137,13 +287,19 @@ gitTokenCommand
137
287
  .option("--json", "Output token response as JSON")
138
288
  .action(async (opts) => {
139
289
  try {
290
+ const project = typeof opts.project === "string" && opts.project.trim()
291
+ ? opts.project.trim()
292
+ : findProjectManifest(process.cwd()).manifest?.projectId ?? null;
293
+ if (!project) {
294
+ throw new Error("No project configured. Run `instafy project:init` or pass --project.");
295
+ }
140
296
  const ttlSecondsRaw = typeof opts.ttlSeconds === "string" ? opts.ttlSeconds.trim() : "";
141
297
  const ttlSeconds = ttlSecondsRaw.length > 0 ? Number.parseInt(ttlSecondsRaw, 10) : undefined;
142
298
  if (ttlSecondsRaw.length > 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) {
143
299
  throw new Error("--ttl-seconds must be a positive integer");
144
300
  }
145
301
  await gitToken({
146
- project: opts.project,
302
+ project,
147
303
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
148
304
  controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
149
305
  supabaseAccessToken: opts.supabaseAccessToken,
@@ -158,56 +314,145 @@ gitTokenCommand
158
314
  process.exit(1);
159
315
  }
160
316
  });
161
- const projectInitCommand = program
162
- .command("project:init")
163
- .description("Create an Instafy project and link this folder (.instafy/project.json)")
164
- .option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
165
- addServerUrlOptions(projectInitCommand);
166
- projectInitCommand
167
- .option("--access-token <token>", "Instafy or Supabase access token")
168
- .option("--project-type <type>", "Project type (customer|sandbox)")
169
- .option("--org-id <uuid>", "Optional organization id")
170
- .option("--org-name <name>", "Optional organization name")
171
- .option("--org-slug <slug>", "Optional organization slug")
172
- .option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
173
- .option("--json", "Output JSON")
174
- .action(async (opts) => {
317
+ program
318
+ .command("git:credential", { hidden: true })
319
+ .description("Internal: git credential helper (used by Git when configured)")
320
+ .argument("<operation>", "Operation (get|store|erase)")
321
+ .action(async (operation) => {
175
322
  try {
176
- await projectInit({
177
- path: opts.path,
178
- controllerUrl: opts.serverUrl ?? opts.controllerUrl,
179
- accessToken: opts.accessToken,
180
- projectType: opts.projectType,
181
- orgId: opts.orgId,
182
- orgName: opts.orgName,
183
- orgSlug: opts.orgSlug,
184
- ownerUserId: opts.ownerUserId,
185
- json: opts.json,
186
- });
323
+ await runGitCredentialHelper(operation);
187
324
  }
188
325
  catch (error) {
189
- console.error(kleur.red(String(error)));
326
+ console.error(String(error));
190
327
  process.exit(1);
191
328
  }
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 = {