@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/project.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
- import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
5
- function formatAuthRequiredError() {
6
- return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
4
+ import { stdin as input } from "node:process";
5
+ import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, } from "./config.js";
6
+ import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
7
+ import { findProjectManifest } from "./project-manifest.js";
8
+ let promptsModule = null;
9
+ async function loadPrompts() {
10
+ promptsModule ?? (promptsModule = import("@clack/prompts"));
11
+ return promptsModule;
7
12
  }
8
- async function fetchOrganizations(controllerUrl, token) {
13
+ async function fetchOrganizations(controllerUrl, token, retryCommand) {
9
14
  const response = await fetch(`${controllerUrl}/orgs`, {
10
15
  headers: {
11
16
  authorization: `Bearer ${token}`,
@@ -13,12 +18,19 @@ async function fetchOrganizations(controllerUrl, token) {
13
18
  });
14
19
  if (!response.ok) {
15
20
  const text = await response.text().catch(() => "");
21
+ if (response.status === 401 || response.status === 403) {
22
+ throw formatAuthRejectedError({
23
+ status: response.status,
24
+ responseBody: text,
25
+ retryCommand,
26
+ });
27
+ }
16
28
  throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
17
29
  }
18
30
  const body = (await response.json());
19
31
  return Array.isArray(body.orgs) ? body.orgs : [];
20
32
  }
21
- async function fetchOrgProjects(controllerUrl, token, orgId) {
33
+ async function fetchOrgProjects(controllerUrl, token, orgId, retryCommand) {
22
34
  const response = await fetch(`${controllerUrl}/orgs/${encodeURIComponent(orgId)}/projects`, {
23
35
  headers: {
24
36
  authorization: `Bearer ${token}`,
@@ -26,25 +38,19 @@ async function fetchOrgProjects(controllerUrl, token, orgId) {
26
38
  });
27
39
  if (!response.ok) {
28
40
  const text = await response.text().catch(() => "");
41
+ if (response.status === 401 || response.status === 403) {
42
+ throw formatAuthRejectedError({
43
+ status: response.status,
44
+ responseBody: text,
45
+ retryCommand,
46
+ });
47
+ }
29
48
  throw new Error(`Project list failed (${response.status} ${response.statusText}): ${text}`);
30
49
  }
31
50
  const body = (await response.json());
32
51
  return Array.isArray(body.projects) ? body.projects : [];
33
52
  }
34
- async function resolveOrg(controllerUrl, token, options) {
35
- if (options.orgId) {
36
- return { orgId: options.orgId, orgName: options.orgName ?? null };
37
- }
38
- const payload = {};
39
- if (options.orgName) {
40
- payload.orgName = options.orgName;
41
- }
42
- if (options.orgSlug) {
43
- payload.orgSlug = options.orgSlug;
44
- }
45
- if (options.ownerUserId) {
46
- payload.ownerUserId = options.ownerUserId;
47
- }
53
+ async function createOrganization(controllerUrl, token, payload, retryCommand) {
48
54
  const response = await fetch(`${controllerUrl}/orgs`, {
49
55
  method: "POST",
50
56
  headers: {
@@ -55,22 +61,165 @@ async function resolveOrg(controllerUrl, token, options) {
55
61
  });
56
62
  if (!response.ok) {
57
63
  const text = await response.text().catch(() => "");
64
+ if (response.status === 401 || response.status === 403) {
65
+ throw formatAuthRejectedError({
66
+ status: response.status,
67
+ responseBody: text,
68
+ retryCommand,
69
+ });
70
+ }
58
71
  throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
59
72
  }
60
73
  const json = (await response.json());
61
- const orgId = json.org_id;
74
+ const orgId = json.org_id ?? json.orgId ?? json.id;
62
75
  if (!orgId) {
63
- throw new Error("Organization creation response missing org_id");
76
+ throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
64
77
  }
65
- return { orgId, orgName: json.org_name ?? null };
78
+ return { orgId, orgName: json.org_name ?? json.orgName ?? null };
79
+ }
80
+ async function resolveOrg(controllerUrl, token, options, retryCommand) {
81
+ if (options.orgId) {
82
+ return { orgId: options.orgId, orgName: options.orgName ?? null };
83
+ }
84
+ const orgSlug = options.orgSlug?.trim() || null;
85
+ const orgName = options.orgName?.trim() || null;
86
+ const studioUrl = resolveConfiguredStudioUrl({ profile: options.profile ?? null }) ?? "https://staging.instafy.dev";
87
+ const studioOrgUrl = `${studioUrl.replace(/\/$/, "")}/studio?panel=settings`;
88
+ const allowInteractive = Boolean(input.isTTY && process.stdout.isTTY && options.json !== true && process.env.CI !== "true");
89
+ async function promptAndCreateOrg() {
90
+ const { isCancel, text } = await loadPrompts();
91
+ const enteredName = await text({
92
+ message: "Organization name",
93
+ defaultValue: "Personal",
94
+ });
95
+ if (isCancel(enteredName)) {
96
+ throw new Error("Cancelled.");
97
+ }
98
+ const chosenName = String(enteredName).trim() || "Personal";
99
+ const enteredSlug = await text({
100
+ message: "Organization slug (optional)",
101
+ });
102
+ if (isCancel(enteredSlug)) {
103
+ throw new Error("Cancelled.");
104
+ }
105
+ const chosenSlug = String(enteredSlug).trim();
106
+ const payload = {
107
+ orgName: chosenName,
108
+ };
109
+ if (options.ownerUserId) {
110
+ payload.ownerUserId = options.ownerUserId;
111
+ }
112
+ if (chosenSlug) {
113
+ payload.orgSlug = chosenSlug;
114
+ }
115
+ const created = await createOrganization(controllerUrl, token, payload, retryCommand);
116
+ return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
117
+ }
118
+ if (orgSlug) {
119
+ const orgs = await fetchOrganizations(controllerUrl, token, retryCommand);
120
+ const matches = orgs.filter((org) => org.slug === orgSlug);
121
+ if (matches.length === 1) {
122
+ return { orgId: matches[0].id, orgName: matches[0].name ?? null };
123
+ }
124
+ }
125
+ if (!orgSlug && !orgName) {
126
+ const orgs = await fetchOrganizations(controllerUrl, token, retryCommand);
127
+ if (orgs.length === 0) {
128
+ if (allowInteractive) {
129
+ const { confirm, isCancel } = await loadPrompts();
130
+ console.log(kleur.yellow("No organizations found for this account."));
131
+ console.log("");
132
+ console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
133
+ console.log("");
134
+ const shouldCreate = await confirm({
135
+ message: "Create a new organization now?",
136
+ initialValue: true,
137
+ });
138
+ if (isCancel(shouldCreate) || !shouldCreate) {
139
+ throw new Error("No organization selected.");
140
+ }
141
+ return await promptAndCreateOrg();
142
+ }
143
+ throw new Error(`No organizations found.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
144
+ }
145
+ if (orgs.length === 1) {
146
+ return { orgId: orgs[0].id, orgName: orgs[0].name ?? null };
147
+ }
148
+ if (allowInteractive) {
149
+ const { isCancel, select } = await loadPrompts();
150
+ const selection = await select({
151
+ message: "Choose an organization for this project",
152
+ options: [
153
+ { value: "__create__", label: "+ Create a new organization" },
154
+ ...orgs.map((org) => ({
155
+ value: org.id,
156
+ label: org.name,
157
+ hint: `${org.slug ? `${org.slug} · ` : ""}${org.id}`,
158
+ })),
159
+ ],
160
+ initialValue: orgs[0].id,
161
+ });
162
+ if (isCancel(selection)) {
163
+ throw new Error("Cancelled.");
164
+ }
165
+ if (selection === "__create__") {
166
+ return await promptAndCreateOrg();
167
+ }
168
+ const pickedOrg = orgs.find((org) => org.id === selection);
169
+ if (!pickedOrg) {
170
+ throw new Error("Selected organization not found.");
171
+ }
172
+ return { orgId: pickedOrg.id, orgName: pickedOrg.name ?? null };
173
+ }
174
+ throw new Error("Multiple organizations found.\n\nNext:\n- instafy org list\n- instafy project init --org-id <uuid>");
175
+ }
176
+ if (orgSlug && !orgName) {
177
+ if (allowInteractive) {
178
+ const { isCancel, text } = await loadPrompts();
179
+ console.log(kleur.yellow("Organization slug did not match an existing org."));
180
+ console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
181
+ console.log("");
182
+ const enteredName = await text({
183
+ message: "Organization name",
184
+ defaultValue: "Personal",
185
+ });
186
+ if (isCancel(enteredName)) {
187
+ throw new Error("Cancelled.");
188
+ }
189
+ const chosenName = String(enteredName).trim() || "Personal";
190
+ const payload = { orgName: chosenName, orgSlug };
191
+ if (options.ownerUserId) {
192
+ payload.ownerUserId = options.ownerUserId;
193
+ }
194
+ const created = await createOrganization(controllerUrl, token, payload, retryCommand);
195
+ return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
196
+ }
197
+ throw new Error(`Organization slug "${orgSlug}" did not match an existing org, and org name is required to create one.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\" --org-slug "${orgSlug}"`);
198
+ }
199
+ const payload = {};
200
+ if (orgName) {
201
+ payload.orgName = orgName;
202
+ }
203
+ if (orgSlug) {
204
+ payload.orgSlug = orgSlug;
205
+ }
206
+ if (options.ownerUserId) {
207
+ payload.ownerUserId = options.ownerUserId;
208
+ }
209
+ if (!payload.orgName) {
210
+ throw new Error(`Organization name is required.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
211
+ }
212
+ const created = await createOrganization(controllerUrl, token, payload, retryCommand);
213
+ return { orgId: created.orgId, orgName: created.orgName ?? orgName ?? null };
66
214
  }
67
215
  export async function listProjects(options) {
68
216
  const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
69
217
  const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
70
218
  if (!token) {
71
- throw formatAuthRequiredError();
219
+ throw formatAuthRequiredError({ retryCommand: "instafy project list" });
72
220
  }
73
- const orgs = await fetchOrganizations(controllerUrl, token);
221
+ const retryCommand = "instafy project list";
222
+ const orgs = await fetchOrganizations(controllerUrl, token, retryCommand);
74
223
  let targetOrgs = orgs;
75
224
  if (options.orgId) {
76
225
  targetOrgs = orgs.filter((org) => org.id === options.orgId);
@@ -86,7 +235,7 @@ export async function listProjects(options) {
86
235
  }
87
236
  const summaries = [];
88
237
  for (const org of targetOrgs) {
89
- const projects = await fetchOrgProjects(controllerUrl, token, org.id);
238
+ const projects = await fetchOrgProjects(controllerUrl, token, org.id, retryCommand);
90
239
  summaries.push({ org, projects });
91
240
  }
92
241
  if (options.json) {
@@ -113,12 +262,24 @@ export async function listProjects(options) {
113
262
  }
114
263
  export async function projectInit(options) {
115
264
  const rootDir = path.resolve(options.path ?? process.cwd());
116
- const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
117
- const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
265
+ const controllerUrl = resolveControllerUrl({
266
+ controllerUrl: options.controllerUrl ?? null,
267
+ profile: options.profile ?? null,
268
+ cwd: rootDir,
269
+ });
270
+ const token = resolveUserAccessToken({
271
+ accessToken: options.accessToken ?? null,
272
+ profile: options.profile ?? null,
273
+ cwd: rootDir,
274
+ });
118
275
  if (!token) {
119
- throw formatAuthRequiredError();
276
+ throw formatAuthRequiredError({
277
+ retryCommand: "instafy project init",
278
+ advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
279
+ });
120
280
  }
121
- const org = await resolveOrg(controllerUrl, token, options);
281
+ const retryCommand = "instafy project init";
282
+ const org = await resolveOrg(controllerUrl, token, options, retryCommand);
122
283
  const body = {
123
284
  projectType: options.projectType,
124
285
  ownerUserId: options.ownerUserId,
@@ -133,9 +294,28 @@ export async function projectInit(options) {
133
294
  });
134
295
  if (!response.ok) {
135
296
  const text = await response.text().catch(() => "");
297
+ if (response.status === 401 || response.status === 403) {
298
+ throw formatAuthRejectedError({
299
+ status: response.status,
300
+ responseBody: text,
301
+ retryCommand,
302
+ });
303
+ }
136
304
  throw new Error(`Project creation failed (${response.status} ${response.statusText}): ${text}`);
137
305
  }
138
306
  const json = (await response.json());
307
+ if (!json.project_id && json.projectId) {
308
+ json.project_id = json.projectId;
309
+ }
310
+ if (!json.org_id && json.orgId) {
311
+ json.org_id = json.orgId;
312
+ }
313
+ if (!json.org_name && json.orgName) {
314
+ json.org_name = json.orgName;
315
+ }
316
+ if (!json.project_id) {
317
+ throw new Error(`Project creation response missing project id (expected project_id/projectId). Raw: ${JSON.stringify(json)}`);
318
+ }
139
319
  const manifestDir = path.join(rootDir, ".instafy");
140
320
  const manifestPath = path.join(manifestDir, "project.json");
141
321
  try {
@@ -145,6 +325,7 @@ export async function projectInit(options) {
145
325
  orgId: json.org_id ?? org.orgId ?? null,
146
326
  orgName: json.org_name ?? org.orgName ?? null,
147
327
  controllerUrl,
328
+ profile: options.profile ?? null,
148
329
  createdAt: new Date().toISOString(),
149
330
  }, null, 2), "utf8");
150
331
  }
@@ -170,3 +351,47 @@ export async function projectInit(options) {
170
351
  }
171
352
  return json;
172
353
  }
354
+ export function projectProfile(options) {
355
+ const rootDir = path.resolve(options.path ?? process.cwd());
356
+ const manifestInfo = findProjectManifest(rootDir);
357
+ if (!manifestInfo.path || !manifestInfo.manifest) {
358
+ throw new Error("No project configured. Run `instafy project init` in this folder first.");
359
+ }
360
+ const shouldUpdate = options.unset === true || typeof options.profile === "string";
361
+ const currentProfile = typeof manifestInfo.manifest.profile === "string" && manifestInfo.manifest.profile.trim()
362
+ ? manifestInfo.manifest.profile.trim()
363
+ : null;
364
+ if (!shouldUpdate) {
365
+ if (options.json) {
366
+ console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: currentProfile }, null, 2));
367
+ }
368
+ else {
369
+ console.log(kleur.green("Project profile"));
370
+ console.log(`Manifest: ${manifestInfo.path}`);
371
+ console.log(`Profile: ${currentProfile ?? kleur.yellow("(none)")}`);
372
+ }
373
+ return { path: manifestInfo.path, profile: currentProfile };
374
+ }
375
+ const nextProfileRaw = options.unset === true ? null : typeof options.profile === "string" ? options.profile.trim() : null;
376
+ const nextProfile = nextProfileRaw && nextProfileRaw.length > 0 ? nextProfileRaw : null;
377
+ if (nextProfile) {
378
+ getInstafyProfileConfigPath(nextProfile);
379
+ }
380
+ const updated = { ...manifestInfo.manifest };
381
+ if (nextProfile) {
382
+ updated.profile = nextProfile;
383
+ }
384
+ else {
385
+ delete updated.profile;
386
+ }
387
+ fs.writeFileSync(manifestInfo.path, JSON.stringify(updated, null, 2), "utf8");
388
+ if (options.json) {
389
+ console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: nextProfile }, null, 2));
390
+ }
391
+ else {
392
+ console.log(kleur.green("Updated project profile."));
393
+ console.log(`Manifest: ${manifestInfo.path}`);
394
+ console.log(`Profile: ${nextProfile ?? kleur.yellow("(none)")}`);
395
+ }
396
+ return { path: manifestInfo.path, profile: nextProfile };
397
+ }
package/dist/rathole.js CHANGED
@@ -52,18 +52,22 @@ export async function ensureRatholeBinary(options = {}) {
52
52
  function resolveInstallPlan() {
53
53
  const arch = process.arch;
54
54
  const platform = process.platform;
55
- if (platform === "darwin" && arch === "arm64") {
56
- return { method: "cargo", binaryName: "rathole" };
57
- }
58
55
  if (platform === "darwin") {
59
- if (arch !== "x64") {
60
- throw new Error(`Unsupported platform ${platform}/${arch} for prebuilt rathole; set RATHOLE_BIN or install via cargo.`);
56
+ if (arch === "arm64") {
57
+ return {
58
+ method: "download",
59
+ assetName: "rathole-x86_64-apple-darwin.zip",
60
+ binaryName: "rathole",
61
+ };
61
62
  }
62
- return {
63
- method: "download",
64
- assetName: "rathole-x86_64-apple-darwin.zip",
65
- binaryName: "rathole",
66
- };
63
+ if (arch === "x64") {
64
+ return {
65
+ method: "download",
66
+ assetName: "rathole-x86_64-apple-darwin.zip",
67
+ binaryName: "rathole",
68
+ };
69
+ }
70
+ throw new Error(`Unsupported platform ${platform}/${arch} for prebuilt rathole; set RATHOLE_BIN or install via cargo.`);
67
71
  }
68
72
  if (platform === "linux") {
69
73
  if (arch === "arm64") {
package/dist/runtime.js CHANGED
@@ -7,6 +7,8 @@ import { randomUUID } from "node:crypto";
7
7
  import os from "node:os";
8
8
  import { ensureRatholeBinary } from "./rathole.js";
9
9
  import { resolveConfiguredAccessToken } from "./config.js";
10
+ import { formatAuthRejectedError } from "./errors.js";
11
+ import { findProjectManifest } from "./project-manifest.js";
10
12
  const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
11
13
  const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
12
14
  const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
@@ -28,7 +30,7 @@ function resolveRuntimeBinary() {
28
30
  return candidate;
29
31
  }
30
32
  }
31
- throw new Error("runtime-agent binary not found. If you're in the instafy repo, run `cargo build -p runtime-agent`. Otherwise install Docker and rerun `instafy runtime:start`.");
33
+ throw new Error("runtime-agent binary not found. If you're in the instafy repo, run `cargo build -p runtime-agent`. Otherwise install Docker and rerun `instafy runtime start`.");
32
34
  }
33
35
  function tryResolveRuntimeBinary() {
34
36
  try {
@@ -142,28 +144,7 @@ function normalizeToken(value) {
142
144
  const trimmed = value.trim();
143
145
  return trimmed.length > 0 ? trimmed : null;
144
146
  }
145
- export function findProjectManifest(startDir) {
146
- let current = path.resolve(startDir);
147
- const root = path.parse(current).root;
148
- while (true) {
149
- const candidate = path.join(current, ".instafy", "project.json");
150
- if (existsSync(candidate)) {
151
- try {
152
- const parsed = JSON.parse(readFileSync(candidate, "utf8"));
153
- if (parsed?.projectId) {
154
- return { manifest: parsed, path: candidate };
155
- }
156
- }
157
- catch {
158
- // ignore malformed manifest
159
- }
160
- }
161
- if (current === root)
162
- break;
163
- current = path.dirname(current);
164
- }
165
- return { manifest: null, path: null };
166
- }
147
+ export { findProjectManifest };
167
148
  function readTokenFromFile(filePath) {
168
149
  const normalized = normalizeToken(filePath);
169
150
  if (!normalized) {
@@ -277,6 +258,12 @@ export async function mintRuntimeAccessToken(params) {
277
258
  });
278
259
  if (!response.ok) {
279
260
  const text = await response.text().catch(() => "");
261
+ if (response.status === 401 || response.status === 403) {
262
+ throw formatAuthRejectedError({
263
+ status: response.status,
264
+ responseBody: text,
265
+ });
266
+ }
280
267
  throw new Error(`Instafy server rejected runtime token request (${response.status} ${response.statusText}): ${text}`);
281
268
  }
282
269
  const payload = (await response.json());
@@ -309,7 +296,7 @@ export async function runtimeStart(options) {
309
296
  const manifestInfo = findProjectManifest(process.cwd());
310
297
  const projectId = options.project ?? env["PROJECT_ID"] ?? manifestInfo.manifest?.projectId ?? null;
311
298
  if (!projectId) {
312
- throw new Error("No project configured. Run `instafy project:init` in this folder (recommended) or pass --project.");
299
+ throw new Error("No project configured. Run `instafy project init` in this folder (recommended) or pass --project.");
313
300
  }
314
301
  env["PROJECT_ID"] = projectId;
315
302
  const supabaseAccessToken = resolveSupabaseAccessToken(options, env);