@instafy/cli 0.1.8 → 0.1.10
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/README.md +16 -9
- package/dist/api.js +50 -16
- package/dist/auth.js +510 -26
- package/dist/config-command.js +2 -0
- package/dist/config.js +187 -6
- package/dist/controller-fetch.js +33 -0
- package/dist/errors.js +63 -0
- package/dist/git-credential.js +205 -0
- package/dist/git-setup.js +56 -0
- package/dist/git-wrapper.js +502 -0
- package/dist/git.js +11 -5
- package/dist/index.js +293 -108
- package/dist/org.js +19 -9
- package/dist/project-manifest.js +24 -0
- package/dist/project.js +285 -45
- package/dist/rathole.js +14 -10
- package/dist/runtime.js +86 -45
- package/dist/supabase-session.js +89 -0
- package/dist/tunnel.js +293 -21
- package/package.json +3 -1
package/dist/project.js
CHANGED
|
@@ -1,76 +1,235 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { stdin as input } from "node:process";
|
|
5
|
+
import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessTokenWithSource, } from "./config.js";
|
|
6
|
+
import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
|
|
7
|
+
import { findProjectManifest } from "./project-manifest.js";
|
|
8
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
9
|
+
let promptsModule = null;
|
|
10
|
+
async function loadPrompts() {
|
|
11
|
+
promptsModule ?? (promptsModule = import("@clack/prompts"));
|
|
12
|
+
return promptsModule;
|
|
7
13
|
}
|
|
8
|
-
async function
|
|
9
|
-
const response = await
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
async function controllerFetch(controllerUrl, auth, pathname, init) {
|
|
15
|
+
const { response, accessToken } = await fetchWithControllerAuth({
|
|
16
|
+
url: `${controllerUrl}${pathname}`,
|
|
17
|
+
init,
|
|
18
|
+
accessToken: auth.accessToken,
|
|
19
|
+
tokenSource: auth.tokenSource,
|
|
20
|
+
profile: auth.profile,
|
|
21
|
+
cwd: auth.cwd,
|
|
13
22
|
});
|
|
23
|
+
auth.accessToken = accessToken;
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
async function fetchOrganizations(controllerUrl, auth, retryCommand) {
|
|
27
|
+
const response = await controllerFetch(controllerUrl, auth, "/orgs");
|
|
14
28
|
if (!response.ok) {
|
|
15
29
|
const text = await response.text().catch(() => "");
|
|
30
|
+
if (response.status === 401 || response.status === 403) {
|
|
31
|
+
throw formatAuthRejectedError({
|
|
32
|
+
status: response.status,
|
|
33
|
+
responseBody: text,
|
|
34
|
+
retryCommand,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
16
37
|
throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
|
|
17
38
|
}
|
|
18
39
|
const body = (await response.json());
|
|
19
40
|
return Array.isArray(body.orgs) ? body.orgs : [];
|
|
20
41
|
}
|
|
21
|
-
async function fetchOrgProjects(controllerUrl,
|
|
22
|
-
const response = await
|
|
23
|
-
headers: {
|
|
24
|
-
authorization: `Bearer ${token}`,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
42
|
+
async function fetchOrgProjects(controllerUrl, auth, orgId, retryCommand) {
|
|
43
|
+
const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(orgId)}/projects`);
|
|
27
44
|
if (!response.ok) {
|
|
28
45
|
const text = await response.text().catch(() => "");
|
|
46
|
+
if (response.status === 401 || response.status === 403) {
|
|
47
|
+
throw formatAuthRejectedError({
|
|
48
|
+
status: response.status,
|
|
49
|
+
responseBody: text,
|
|
50
|
+
retryCommand,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
29
53
|
throw new Error(`Project list failed (${response.status} ${response.statusText}): ${text}`);
|
|
30
54
|
}
|
|
31
55
|
const body = (await response.json());
|
|
32
56
|
return Array.isArray(body.projects) ? body.projects : [];
|
|
33
57
|
}
|
|
34
|
-
async function
|
|
35
|
-
|
|
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
|
-
}
|
|
48
|
-
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
58
|
+
async function createOrganization(controllerUrl, auth, payload, retryCommand) {
|
|
59
|
+
const response = await controllerFetch(controllerUrl, auth, "/orgs", {
|
|
49
60
|
method: "POST",
|
|
50
61
|
headers: {
|
|
51
|
-
authorization: `Bearer ${token}`,
|
|
52
62
|
"content-type": "application/json",
|
|
53
63
|
},
|
|
54
64
|
body: JSON.stringify(payload),
|
|
55
65
|
});
|
|
56
66
|
if (!response.ok) {
|
|
57
67
|
const text = await response.text().catch(() => "");
|
|
68
|
+
if (response.status === 401 || response.status === 403) {
|
|
69
|
+
throw formatAuthRejectedError({
|
|
70
|
+
status: response.status,
|
|
71
|
+
responseBody: text,
|
|
72
|
+
retryCommand,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
58
75
|
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
59
76
|
}
|
|
60
77
|
const json = (await response.json());
|
|
61
|
-
const orgId = json.org_id;
|
|
78
|
+
const orgId = json.org_id ?? json.orgId ?? json.id;
|
|
62
79
|
if (!orgId) {
|
|
63
|
-
throw new Error(
|
|
80
|
+
throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
|
|
81
|
+
}
|
|
82
|
+
return { orgId, orgName: json.org_name ?? json.orgName ?? null };
|
|
83
|
+
}
|
|
84
|
+
async function resolveOrg(controllerUrl, auth, options, retryCommand) {
|
|
85
|
+
if (options.orgId) {
|
|
86
|
+
return { orgId: options.orgId, orgName: options.orgName ?? null };
|
|
87
|
+
}
|
|
88
|
+
const orgSlug = options.orgSlug?.trim() || null;
|
|
89
|
+
const orgName = options.orgName?.trim() || null;
|
|
90
|
+
const studioUrl = resolveConfiguredStudioUrl({ profile: options.profile ?? null }) ?? "https://staging.instafy.dev";
|
|
91
|
+
const studioOrgUrl = `${studioUrl.replace(/\/$/, "")}/studio?panel=settings`;
|
|
92
|
+
const allowInteractive = Boolean(input.isTTY && process.stdout.isTTY && options.json !== true && process.env.CI !== "true");
|
|
93
|
+
async function promptAndCreateOrg() {
|
|
94
|
+
const { isCancel, text } = await loadPrompts();
|
|
95
|
+
const enteredName = await text({
|
|
96
|
+
message: "Organization name",
|
|
97
|
+
defaultValue: "Personal",
|
|
98
|
+
});
|
|
99
|
+
if (isCancel(enteredName)) {
|
|
100
|
+
throw new Error("Cancelled.");
|
|
101
|
+
}
|
|
102
|
+
const chosenName = String(enteredName).trim() || "Personal";
|
|
103
|
+
const enteredSlug = await text({
|
|
104
|
+
message: "Organization slug (optional)",
|
|
105
|
+
});
|
|
106
|
+
if (isCancel(enteredSlug)) {
|
|
107
|
+
throw new Error("Cancelled.");
|
|
108
|
+
}
|
|
109
|
+
const chosenSlug = String(enteredSlug).trim();
|
|
110
|
+
const payload = {
|
|
111
|
+
orgName: chosenName,
|
|
112
|
+
};
|
|
113
|
+
if (options.ownerUserId) {
|
|
114
|
+
payload.ownerUserId = options.ownerUserId;
|
|
115
|
+
}
|
|
116
|
+
if (chosenSlug) {
|
|
117
|
+
payload.orgSlug = chosenSlug;
|
|
118
|
+
}
|
|
119
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
120
|
+
return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
|
|
121
|
+
}
|
|
122
|
+
if (orgSlug) {
|
|
123
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
124
|
+
const matches = orgs.filter((org) => org.slug === orgSlug);
|
|
125
|
+
if (matches.length === 1) {
|
|
126
|
+
return { orgId: matches[0].id, orgName: matches[0].name ?? null };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!orgSlug && !orgName) {
|
|
130
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
131
|
+
if (orgs.length === 0) {
|
|
132
|
+
if (allowInteractive) {
|
|
133
|
+
const { confirm, isCancel } = await loadPrompts();
|
|
134
|
+
console.log(kleur.yellow("No organizations found for this account."));
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
|
|
137
|
+
console.log("");
|
|
138
|
+
const shouldCreate = await confirm({
|
|
139
|
+
message: "Create a new organization now?",
|
|
140
|
+
initialValue: true,
|
|
141
|
+
});
|
|
142
|
+
if (isCancel(shouldCreate) || !shouldCreate) {
|
|
143
|
+
throw new Error("No organization selected.");
|
|
144
|
+
}
|
|
145
|
+
return await promptAndCreateOrg();
|
|
146
|
+
}
|
|
147
|
+
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\"`);
|
|
148
|
+
}
|
|
149
|
+
if (orgs.length === 1) {
|
|
150
|
+
return { orgId: orgs[0].id, orgName: orgs[0].name ?? null };
|
|
151
|
+
}
|
|
152
|
+
if (allowInteractive) {
|
|
153
|
+
const { isCancel, select } = await loadPrompts();
|
|
154
|
+
const selection = await select({
|
|
155
|
+
message: "Choose an organization for this project",
|
|
156
|
+
options: [
|
|
157
|
+
{ value: "__create__", label: "+ Create a new organization" },
|
|
158
|
+
...orgs.map((org) => ({
|
|
159
|
+
value: org.id,
|
|
160
|
+
label: org.name,
|
|
161
|
+
hint: `${org.slug ? `${org.slug} · ` : ""}${org.id}`,
|
|
162
|
+
})),
|
|
163
|
+
],
|
|
164
|
+
initialValue: orgs[0].id,
|
|
165
|
+
});
|
|
166
|
+
if (isCancel(selection)) {
|
|
167
|
+
throw new Error("Cancelled.");
|
|
168
|
+
}
|
|
169
|
+
if (selection === "__create__") {
|
|
170
|
+
return await promptAndCreateOrg();
|
|
171
|
+
}
|
|
172
|
+
const pickedOrg = orgs.find((org) => org.id === selection);
|
|
173
|
+
if (!pickedOrg) {
|
|
174
|
+
throw new Error("Selected organization not found.");
|
|
175
|
+
}
|
|
176
|
+
return { orgId: pickedOrg.id, orgName: pickedOrg.name ?? null };
|
|
177
|
+
}
|
|
178
|
+
throw new Error("Multiple organizations found.\n\nNext:\n- instafy org list\n- instafy project init --org-id <uuid>");
|
|
179
|
+
}
|
|
180
|
+
if (orgSlug && !orgName) {
|
|
181
|
+
if (allowInteractive) {
|
|
182
|
+
const { isCancel, text } = await loadPrompts();
|
|
183
|
+
console.log(kleur.yellow("Organization slug did not match an existing org."));
|
|
184
|
+
console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
|
|
185
|
+
console.log("");
|
|
186
|
+
const enteredName = await text({
|
|
187
|
+
message: "Organization name",
|
|
188
|
+
defaultValue: "Personal",
|
|
189
|
+
});
|
|
190
|
+
if (isCancel(enteredName)) {
|
|
191
|
+
throw new Error("Cancelled.");
|
|
192
|
+
}
|
|
193
|
+
const chosenName = String(enteredName).trim() || "Personal";
|
|
194
|
+
const payload = { orgName: chosenName, orgSlug };
|
|
195
|
+
if (options.ownerUserId) {
|
|
196
|
+
payload.ownerUserId = options.ownerUserId;
|
|
197
|
+
}
|
|
198
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
199
|
+
return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
|
|
200
|
+
}
|
|
201
|
+
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}"`);
|
|
202
|
+
}
|
|
203
|
+
const payload = {};
|
|
204
|
+
if (orgName) {
|
|
205
|
+
payload.orgName = orgName;
|
|
206
|
+
}
|
|
207
|
+
if (orgSlug) {
|
|
208
|
+
payload.orgSlug = orgSlug;
|
|
209
|
+
}
|
|
210
|
+
if (options.ownerUserId) {
|
|
211
|
+
payload.ownerUserId = options.ownerUserId;
|
|
212
|
+
}
|
|
213
|
+
if (!payload.orgName) {
|
|
214
|
+
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\"`);
|
|
64
215
|
}
|
|
65
|
-
|
|
216
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
217
|
+
return { orgId: created.orgId, orgName: created.orgName ?? orgName ?? null };
|
|
66
218
|
}
|
|
67
219
|
export async function listProjects(options) {
|
|
68
220
|
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
69
|
-
const
|
|
70
|
-
if (!token) {
|
|
71
|
-
throw formatAuthRequiredError();
|
|
221
|
+
const resolved = resolveUserAccessTokenWithSource({ accessToken: options.accessToken ?? null });
|
|
222
|
+
if (!resolved.token) {
|
|
223
|
+
throw formatAuthRequiredError({ retryCommand: "instafy project list" });
|
|
72
224
|
}
|
|
73
|
-
const
|
|
225
|
+
const retryCommand = "instafy project list";
|
|
226
|
+
const auth = {
|
|
227
|
+
accessToken: resolved.token,
|
|
228
|
+
tokenSource: resolved.source,
|
|
229
|
+
profile: resolved.profile,
|
|
230
|
+
cwd: process.cwd(),
|
|
231
|
+
};
|
|
232
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
74
233
|
let targetOrgs = orgs;
|
|
75
234
|
if (options.orgId) {
|
|
76
235
|
targetOrgs = orgs.filter((org) => org.id === options.orgId);
|
|
@@ -86,7 +245,7 @@ export async function listProjects(options) {
|
|
|
86
245
|
}
|
|
87
246
|
const summaries = [];
|
|
88
247
|
for (const org of targetOrgs) {
|
|
89
|
-
const projects = await fetchOrgProjects(controllerUrl,
|
|
248
|
+
const projects = await fetchOrgProjects(controllerUrl, auth, org.id, retryCommand);
|
|
90
249
|
summaries.push({ org, projects });
|
|
91
250
|
}
|
|
92
251
|
if (options.json) {
|
|
@@ -113,29 +272,65 @@ export async function listProjects(options) {
|
|
|
113
272
|
}
|
|
114
273
|
export async function projectInit(options) {
|
|
115
274
|
const rootDir = path.resolve(options.path ?? process.cwd());
|
|
116
|
-
const controllerUrl = resolveControllerUrl({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
275
|
+
const controllerUrl = resolveControllerUrl({
|
|
276
|
+
controllerUrl: options.controllerUrl ?? null,
|
|
277
|
+
profile: options.profile ?? null,
|
|
278
|
+
cwd: rootDir,
|
|
279
|
+
});
|
|
280
|
+
const resolved = resolveUserAccessTokenWithSource({
|
|
281
|
+
accessToken: options.accessToken ?? null,
|
|
282
|
+
profile: options.profile ?? null,
|
|
283
|
+
cwd: rootDir,
|
|
284
|
+
});
|
|
285
|
+
if (!resolved.token) {
|
|
286
|
+
throw formatAuthRequiredError({
|
|
287
|
+
retryCommand: "instafy project init",
|
|
288
|
+
advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
|
|
289
|
+
});
|
|
120
290
|
}
|
|
121
|
-
const
|
|
291
|
+
const retryCommand = "instafy project init";
|
|
292
|
+
const auth = {
|
|
293
|
+
accessToken: resolved.token,
|
|
294
|
+
tokenSource: resolved.source,
|
|
295
|
+
profile: resolved.profile,
|
|
296
|
+
cwd: rootDir,
|
|
297
|
+
};
|
|
298
|
+
const org = await resolveOrg(controllerUrl, auth, options, retryCommand);
|
|
122
299
|
const body = {
|
|
123
300
|
projectType: options.projectType,
|
|
124
301
|
ownerUserId: options.ownerUserId,
|
|
125
302
|
};
|
|
126
|
-
const response = await
|
|
303
|
+
const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(org.orgId)}/projects`, {
|
|
127
304
|
method: "POST",
|
|
128
305
|
headers: {
|
|
129
|
-
authorization: `Bearer ${token}`,
|
|
130
306
|
"content-type": "application/json",
|
|
131
307
|
},
|
|
132
308
|
body: JSON.stringify(body),
|
|
133
309
|
});
|
|
134
310
|
if (!response.ok) {
|
|
135
311
|
const text = await response.text().catch(() => "");
|
|
312
|
+
if (response.status === 401 || response.status === 403) {
|
|
313
|
+
throw formatAuthRejectedError({
|
|
314
|
+
status: response.status,
|
|
315
|
+
responseBody: text,
|
|
316
|
+
retryCommand,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
136
319
|
throw new Error(`Project creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
137
320
|
}
|
|
138
321
|
const json = (await response.json());
|
|
322
|
+
if (!json.project_id && json.projectId) {
|
|
323
|
+
json.project_id = json.projectId;
|
|
324
|
+
}
|
|
325
|
+
if (!json.org_id && json.orgId) {
|
|
326
|
+
json.org_id = json.orgId;
|
|
327
|
+
}
|
|
328
|
+
if (!json.org_name && json.orgName) {
|
|
329
|
+
json.org_name = json.orgName;
|
|
330
|
+
}
|
|
331
|
+
if (!json.project_id) {
|
|
332
|
+
throw new Error(`Project creation response missing project id (expected project_id/projectId). Raw: ${JSON.stringify(json)}`);
|
|
333
|
+
}
|
|
139
334
|
const manifestDir = path.join(rootDir, ".instafy");
|
|
140
335
|
const manifestPath = path.join(manifestDir, "project.json");
|
|
141
336
|
try {
|
|
@@ -145,6 +340,7 @@ export async function projectInit(options) {
|
|
|
145
340
|
orgId: json.org_id ?? org.orgId ?? null,
|
|
146
341
|
orgName: json.org_name ?? org.orgName ?? null,
|
|
147
342
|
controllerUrl,
|
|
343
|
+
profile: options.profile ?? null,
|
|
148
344
|
createdAt: new Date().toISOString(),
|
|
149
345
|
}, null, 2), "utf8");
|
|
150
346
|
}
|
|
@@ -170,3 +366,47 @@ export async function projectInit(options) {
|
|
|
170
366
|
}
|
|
171
367
|
return json;
|
|
172
368
|
}
|
|
369
|
+
export function projectProfile(options) {
|
|
370
|
+
const rootDir = path.resolve(options.path ?? process.cwd());
|
|
371
|
+
const manifestInfo = findProjectManifest(rootDir);
|
|
372
|
+
if (!manifestInfo.path || !manifestInfo.manifest) {
|
|
373
|
+
throw new Error("No project configured. Run `instafy project init` in this folder first.");
|
|
374
|
+
}
|
|
375
|
+
const shouldUpdate = options.unset === true || typeof options.profile === "string";
|
|
376
|
+
const currentProfile = typeof manifestInfo.manifest.profile === "string" && manifestInfo.manifest.profile.trim()
|
|
377
|
+
? manifestInfo.manifest.profile.trim()
|
|
378
|
+
: null;
|
|
379
|
+
if (!shouldUpdate) {
|
|
380
|
+
if (options.json) {
|
|
381
|
+
console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: currentProfile }, null, 2));
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
console.log(kleur.green("Project profile"));
|
|
385
|
+
console.log(`Manifest: ${manifestInfo.path}`);
|
|
386
|
+
console.log(`Profile: ${currentProfile ?? kleur.yellow("(none)")}`);
|
|
387
|
+
}
|
|
388
|
+
return { path: manifestInfo.path, profile: currentProfile };
|
|
389
|
+
}
|
|
390
|
+
const nextProfileRaw = options.unset === true ? null : typeof options.profile === "string" ? options.profile.trim() : null;
|
|
391
|
+
const nextProfile = nextProfileRaw && nextProfileRaw.length > 0 ? nextProfileRaw : null;
|
|
392
|
+
if (nextProfile) {
|
|
393
|
+
getInstafyProfileConfigPath(nextProfile);
|
|
394
|
+
}
|
|
395
|
+
const updated = { ...manifestInfo.manifest };
|
|
396
|
+
if (nextProfile) {
|
|
397
|
+
updated.profile = nextProfile;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
delete updated.profile;
|
|
401
|
+
}
|
|
402
|
+
fs.writeFileSync(manifestInfo.path, JSON.stringify(updated, null, 2), "utf8");
|
|
403
|
+
if (options.json) {
|
|
404
|
+
console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: nextProfile }, null, 2));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log(kleur.green("Updated project profile."));
|
|
408
|
+
console.log(`Manifest: ${manifestInfo.path}`);
|
|
409
|
+
console.log(`Profile: ${nextProfile ?? kleur.yellow("(none)")}`);
|
|
410
|
+
}
|
|
411
|
+
return { path: manifestInfo.path, profile: nextProfile };
|
|
412
|
+
}
|
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
|
|
60
|
-
|
|
56
|
+
if (arch === "arm64") {
|
|
57
|
+
return {
|
|
58
|
+
method: "download",
|
|
59
|
+
assetName: "rathole-x86_64-apple-darwin.zip",
|
|
60
|
+
binaryName: "rathole",
|
|
61
|
+
};
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
@@ -6,7 +6,10 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { ensureRatholeBinary } from "./rathole.js";
|
|
9
|
-
import { resolveConfiguredAccessToken } from "./config.js";
|
|
9
|
+
import { resolveActiveProfileName, resolveConfiguredAccessToken } from "./config.js";
|
|
10
|
+
import { formatAuthRejectedError } from "./errors.js";
|
|
11
|
+
import { findProjectManifest } from "./project-manifest.js";
|
|
12
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
10
13
|
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
11
14
|
const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
|
|
12
15
|
const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
|
|
@@ -28,7 +31,7 @@ function resolveRuntimeBinary() {
|
|
|
28
31
|
return candidate;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
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
|
|
34
|
+
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
35
|
}
|
|
33
36
|
function tryResolveRuntimeBinary() {
|
|
34
37
|
try {
|
|
@@ -142,28 +145,7 @@ function normalizeToken(value) {
|
|
|
142
145
|
const trimmed = value.trim();
|
|
143
146
|
return trimmed.length > 0 ? trimmed : null;
|
|
144
147
|
}
|
|
145
|
-
export
|
|
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
|
-
}
|
|
148
|
+
export { findProjectManifest };
|
|
167
149
|
function readTokenFromFile(filePath) {
|
|
168
150
|
const normalized = normalizeToken(filePath);
|
|
169
151
|
if (!normalized) {
|
|
@@ -207,13 +189,28 @@ function findRatholeOnPath() {
|
|
|
207
189
|
}
|
|
208
190
|
return null;
|
|
209
191
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
192
|
+
function resolveControllerAccessTokenForCliWithSource(options, env, supabaseAccessToken, profile, cwd) {
|
|
193
|
+
const explicit = normalizeToken(options.controllerAccessToken);
|
|
194
|
+
if (explicit) {
|
|
195
|
+
return { token: explicit, source: "explicit" };
|
|
196
|
+
}
|
|
197
|
+
const envToken = normalizeToken(env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
198
|
+
normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]);
|
|
199
|
+
if (envToken) {
|
|
200
|
+
return { token: envToken, source: "env" };
|
|
201
|
+
}
|
|
202
|
+
if (supabaseAccessToken) {
|
|
203
|
+
return { token: supabaseAccessToken, source: "explicit" };
|
|
204
|
+
}
|
|
205
|
+
const supabaseEnvToken = normalizeToken(env["SUPABASE_ACCESS_TOKEN"]);
|
|
206
|
+
if (supabaseEnvToken) {
|
|
207
|
+
return { token: supabaseEnvToken, source: "env" };
|
|
208
|
+
}
|
|
209
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
210
|
+
if (stored) {
|
|
211
|
+
return { token: stored, source: "config" };
|
|
212
|
+
}
|
|
213
|
+
return { token: null, source: "none" };
|
|
217
214
|
}
|
|
218
215
|
export async function resolveRatholeBinaryForCli(options) {
|
|
219
216
|
const warn = options.warn ??
|
|
@@ -264,19 +261,31 @@ export async function resolveRatholeBinaryForCli(options) {
|
|
|
264
261
|
export async function mintRuntimeAccessToken(params) {
|
|
265
262
|
const url = params.controllerUrl.replace(/\/$/, "");
|
|
266
263
|
const target = `${url}/projects/${encodeURIComponent(params.projectId)}/runtime/token`;
|
|
267
|
-
const response = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
const { response } = await fetchWithControllerAuth({
|
|
265
|
+
url: target,
|
|
266
|
+
init: {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
},
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
runtimeId: params.runtimeId,
|
|
273
|
+
scopes: params.scopes,
|
|
274
|
+
}),
|
|
272
275
|
},
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
accessToken: params.controllerAccessToken,
|
|
277
|
+
tokenSource: params.tokenSource ?? "env",
|
|
278
|
+
profile: params.profile ?? null,
|
|
279
|
+
cwd: params.cwd ?? null,
|
|
277
280
|
});
|
|
278
281
|
if (!response.ok) {
|
|
279
282
|
const text = await response.text().catch(() => "");
|
|
283
|
+
if (response.status === 401 || response.status === 403) {
|
|
284
|
+
throw formatAuthRejectedError({
|
|
285
|
+
status: response.status,
|
|
286
|
+
responseBody: text,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
280
289
|
throw new Error(`Instafy server rejected runtime token request (${response.status} ${response.statusText}): ${text}`);
|
|
281
290
|
}
|
|
282
291
|
const payload = (await response.json());
|
|
@@ -297,6 +306,7 @@ function isProcessAlive(pid) {
|
|
|
297
306
|
}
|
|
298
307
|
export async function runtimeStart(options) {
|
|
299
308
|
const env = { ...process.env };
|
|
309
|
+
const cwd = process.cwd();
|
|
300
310
|
const existing = readState();
|
|
301
311
|
if (existing) {
|
|
302
312
|
if (existing.runner === "docker" && existing.containerId && dockerContainerRunning(existing.containerId)) {
|
|
@@ -306,17 +316,20 @@ export async function runtimeStart(options) {
|
|
|
306
316
|
throw new Error(`Runtime already running (pid ${existing.pid}) for project ${existing.projectId}. Stop it first.`);
|
|
307
317
|
}
|
|
308
318
|
}
|
|
309
|
-
const manifestInfo = findProjectManifest(
|
|
319
|
+
const manifestInfo = findProjectManifest(cwd);
|
|
310
320
|
const projectId = options.project ?? env["PROJECT_ID"] ?? manifestInfo.manifest?.projectId ?? null;
|
|
311
321
|
if (!projectId) {
|
|
312
|
-
throw new Error("No project configured. Run `instafy project
|
|
322
|
+
throw new Error("No project configured. Run `instafy project init` in this folder (recommended) or pass --project.");
|
|
313
323
|
}
|
|
314
324
|
env["PROJECT_ID"] = projectId;
|
|
315
325
|
const supabaseAccessToken = resolveSupabaseAccessToken(options, env);
|
|
316
326
|
if (supabaseAccessToken) {
|
|
317
327
|
env["SUPABASE_ACCESS_TOKEN"] = supabaseAccessToken;
|
|
318
328
|
}
|
|
319
|
-
|
|
329
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
330
|
+
const controllerAccessTokenResult = resolveControllerAccessTokenForCliWithSource(options, env, supabaseAccessToken, profile, cwd);
|
|
331
|
+
let controllerAccessToken = controllerAccessTokenResult.token;
|
|
332
|
+
const controllerAccessTokenSource = controllerAccessTokenResult.source;
|
|
320
333
|
let runtimeAccessToken = normalizeToken(options.runtimeToken) ?? normalizeToken(env["RUNTIME_ACCESS_TOKEN"]);
|
|
321
334
|
const agentKey = options.controllerToken ??
|
|
322
335
|
env["INSTAFY_SERVICE_TOKEN"] ??
|
|
@@ -342,6 +355,9 @@ export async function runtimeStart(options) {
|
|
|
342
355
|
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
343
356
|
controllerAccessToken,
|
|
344
357
|
projectId,
|
|
358
|
+
tokenSource: controllerAccessTokenSource,
|
|
359
|
+
profile,
|
|
360
|
+
cwd,
|
|
345
361
|
});
|
|
346
362
|
}
|
|
347
363
|
if (runtimeAccessToken) {
|
|
@@ -375,6 +391,9 @@ export async function runtimeStart(options) {
|
|
|
375
391
|
controllerAccessToken,
|
|
376
392
|
projectId,
|
|
377
393
|
runtimeId: env["RUNTIME_ID"],
|
|
394
|
+
tokenSource: controllerAccessTokenSource,
|
|
395
|
+
profile,
|
|
396
|
+
cwd,
|
|
378
397
|
});
|
|
379
398
|
}
|
|
380
399
|
if (!originToken) {
|
|
@@ -513,7 +532,14 @@ export async function runtimeStart(options) {
|
|
|
513
532
|
const started = spawnSync("docker", runArgs, { encoding: "utf8" });
|
|
514
533
|
if (started.status !== 0) {
|
|
515
534
|
const stderr = String(started.stderr ?? "").trim();
|
|
516
|
-
|
|
535
|
+
const normalized = (stderr || "").toLowerCase();
|
|
536
|
+
const hints = [];
|
|
537
|
+
if (image.startsWith("ghcr.io/") && (normalized.includes("not found") || normalized.includes("denied") || normalized.includes("unauthorized") || normalized.includes("manifest unknown"))) {
|
|
538
|
+
hints.push("If this is a GHCR image, ensure it's published and you are authenticated (`docker login ghcr.io`).");
|
|
539
|
+
}
|
|
540
|
+
hints.push("Override the image with `INSTAFY_RUNTIME_AGENT_IMAGE=... instafy runtime start`.");
|
|
541
|
+
const suffix = hints.length > 0 ? `\n\nNext:\n- ${hints.join("\n- ")}` : "";
|
|
542
|
+
throw new Error(`docker run failed: ${stderr || "unknown error"}${suffix}`);
|
|
517
543
|
}
|
|
518
544
|
const containerId = String(started.stdout ?? "").trim();
|
|
519
545
|
if (!containerId) {
|
|
@@ -753,15 +779,27 @@ export async function runtimeStop(options) {
|
|
|
753
779
|
}
|
|
754
780
|
}
|
|
755
781
|
export async function runtimeToken(options) {
|
|
782
|
+
const cwd = process.cwd();
|
|
756
783
|
const controllerUrl = options.controllerUrl ??
|
|
757
784
|
process.env["INSTAFY_SERVER_URL"] ??
|
|
758
785
|
process.env["CONTROLLER_BASE_URL"] ??
|
|
759
786
|
"http://127.0.0.1:8788";
|
|
787
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
788
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
789
|
+
const tokenSource = options.controllerAccessToken
|
|
790
|
+
? "explicit"
|
|
791
|
+
: process.env["INSTAFY_ACCESS_TOKEN"] ||
|
|
792
|
+
process.env["CONTROLLER_ACCESS_TOKEN"] ||
|
|
793
|
+
process.env["SUPABASE_ACCESS_TOKEN"]
|
|
794
|
+
? "env"
|
|
795
|
+
: stored
|
|
796
|
+
? "config"
|
|
797
|
+
: "none";
|
|
760
798
|
const token = options.controllerAccessToken ??
|
|
761
799
|
process.env["INSTAFY_ACCESS_TOKEN"] ??
|
|
762
800
|
process.env["CONTROLLER_ACCESS_TOKEN"] ??
|
|
763
801
|
process.env["SUPABASE_ACCESS_TOKEN"] ??
|
|
764
|
-
|
|
802
|
+
stored;
|
|
765
803
|
if (!token) {
|
|
766
804
|
throw new Error("Login required. Run `instafy login` or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
767
805
|
}
|
|
@@ -771,6 +809,9 @@ export async function runtimeToken(options) {
|
|
|
771
809
|
projectId: options.project,
|
|
772
810
|
runtimeId: options.runtimeId,
|
|
773
811
|
scopes: options.scopes,
|
|
812
|
+
tokenSource: tokenSource === "none" ? "env" : tokenSource,
|
|
813
|
+
profile,
|
|
814
|
+
cwd,
|
|
774
815
|
});
|
|
775
816
|
if (options.json) {
|
|
776
817
|
console.log(JSON.stringify({ token: minted }));
|