@instafy/cli 0.1.11 → 0.2.0

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.
@@ -1,24 +0,0 @@
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
- }
package/dist/project.js DELETED
@@ -1,412 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import kleur from "kleur";
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;
13
- }
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,
22
- });
23
- auth.accessToken = accessToken;
24
- return response;
25
- }
26
- async function fetchOrganizations(controllerUrl, auth, retryCommand) {
27
- const response = await controllerFetch(controllerUrl, auth, "/orgs");
28
- if (!response.ok) {
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
- }
37
- throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
38
- }
39
- const body = (await response.json());
40
- return Array.isArray(body.orgs) ? body.orgs : [];
41
- }
42
- async function fetchOrgProjects(controllerUrl, auth, orgId, retryCommand) {
43
- const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(orgId)}/projects`);
44
- if (!response.ok) {
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
- }
53
- throw new Error(`Project list failed (${response.status} ${response.statusText}): ${text}`);
54
- }
55
- const body = (await response.json());
56
- return Array.isArray(body.projects) ? body.projects : [];
57
- }
58
- async function createOrganization(controllerUrl, auth, payload, retryCommand) {
59
- const response = await controllerFetch(controllerUrl, auth, "/orgs", {
60
- method: "POST",
61
- headers: {
62
- "content-type": "application/json",
63
- },
64
- body: JSON.stringify(payload),
65
- });
66
- if (!response.ok) {
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
- }
75
- throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
76
- }
77
- const json = (await response.json());
78
- const orgId = json.org_id ?? json.orgId ?? json.id;
79
- if (!orgId) {
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\"`);
215
- }
216
- const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
217
- return { orgId: created.orgId, orgName: created.orgName ?? orgName ?? null };
218
- }
219
- export async function listProjects(options) {
220
- const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
221
- const resolved = resolveUserAccessTokenWithSource({ accessToken: options.accessToken ?? null });
222
- if (!resolved.token) {
223
- throw formatAuthRequiredError({ retryCommand: "instafy project list" });
224
- }
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);
233
- let targetOrgs = orgs;
234
- if (options.orgId) {
235
- targetOrgs = orgs.filter((org) => org.id === options.orgId);
236
- if (targetOrgs.length === 0) {
237
- throw new Error(`No organization found for id ${options.orgId}`);
238
- }
239
- }
240
- else if (options.orgSlug) {
241
- targetOrgs = orgs.filter((org) => org.slug === options.orgSlug);
242
- if (targetOrgs.length === 0) {
243
- throw new Error(`No organization found for slug ${options.orgSlug}`);
244
- }
245
- }
246
- const summaries = [];
247
- for (const org of targetOrgs) {
248
- const projects = await fetchOrgProjects(controllerUrl, auth, org.id, retryCommand);
249
- summaries.push({ org, projects });
250
- }
251
- if (options.json) {
252
- console.log(JSON.stringify(summaries, null, 2));
253
- }
254
- else if (summaries.length === 0) {
255
- console.log(kleur.yellow("No organizations found for this account."));
256
- }
257
- else {
258
- for (const summary of summaries) {
259
- console.log(`${kleur.green(summary.org.name)} (${summary.org.id})`);
260
- if (summary.projects.length === 0) {
261
- console.log(kleur.yellow(" No projects found."));
262
- continue;
263
- }
264
- for (const project of summary.projects) {
265
- const type = project.project_type ? ` · ${project.project_type}` : "";
266
- const status = project.status ? ` · ${project.status}` : "";
267
- console.log(` ${project.project_id}${type}${status}`);
268
- }
269
- }
270
- }
271
- return summaries.flatMap((summary) => summary.projects);
272
- }
273
- export async function projectInit(options) {
274
- const rootDir = path.resolve(options.path ?? process.cwd());
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
- });
290
- }
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);
299
- const body = {
300
- projectType: options.projectType,
301
- ownerUserId: options.ownerUserId,
302
- };
303
- const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(org.orgId)}/projects`, {
304
- method: "POST",
305
- headers: {
306
- "content-type": "application/json",
307
- },
308
- body: JSON.stringify(body),
309
- });
310
- if (!response.ok) {
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
- }
319
- throw new Error(`Project creation failed (${response.status} ${response.statusText}): ${text}`);
320
- }
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
- }
334
- const manifestDir = path.join(rootDir, ".instafy");
335
- const manifestPath = path.join(manifestDir, "project.json");
336
- try {
337
- fs.mkdirSync(manifestDir, { recursive: true });
338
- fs.writeFileSync(manifestPath, JSON.stringify({
339
- projectId: json.project_id,
340
- orgId: json.org_id ?? org.orgId ?? null,
341
- orgName: json.org_name ?? org.orgName ?? null,
342
- controllerUrl,
343
- profile: options.profile ?? null,
344
- createdAt: new Date().toISOString(),
345
- }, null, 2), "utf8");
346
- }
347
- catch (error) {
348
- throw new Error(`Project created but failed to write manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
349
- }
350
- if (options.json) {
351
- console.log(JSON.stringify({
352
- projectId: json.project_id,
353
- orgId: json.org_id ?? org.orgId ?? null,
354
- orgName: json.org_name ?? org.orgName ?? null,
355
- manifest: manifestPath,
356
- }));
357
- }
358
- else {
359
- console.log(kleur.green(`Project created: ${json.project_id}`));
360
- const resolvedOrgName = json.org_name ?? org.orgName;
361
- const resolvedOrgId = json.org_id ?? org.orgId;
362
- if (resolvedOrgName) {
363
- console.log(kleur.cyan(`Org: ${resolvedOrgName}${resolvedOrgId ? ` (${resolvedOrgId})` : ""}`));
364
- }
365
- console.log(kleur.cyan(`Manifest written to ${manifestPath}`));
366
- }
367
- return json;
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 DELETED
@@ -1,216 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { Readable } from "node:stream";
6
- import { pipeline } from "node:stream/promises";
7
- const DEFAULT_VERSION = process.env.RATHOLE_VERSION ?? "latest";
8
- const DEFAULT_CACHE_DIR = process.env.RATHOLE_CACHE_DIR ?? path.join(os.homedir(), ".instafy", "rathole");
9
- export async function ensureRatholeBinary(options = {}) {
10
- const logger = options.logger ?? (() => { });
11
- const versionInput = (options.version ?? DEFAULT_VERSION).trim() || "latest";
12
- const cacheRoot = path.resolve(options.cacheDir ?? DEFAULT_CACHE_DIR);
13
- const plan = resolveInstallPlan();
14
- if (plan.method === "cargo") {
15
- return ensureRatholeViaCargo({
16
- version: versionInput,
17
- cacheRoot,
18
- binaryName: plan.binaryName,
19
- logger,
20
- });
21
- }
22
- const tag = normalizeGithubTag(versionInput);
23
- const targetDir = path.join(cacheRoot, tag, plan.assetName.replace(/\.zip$/i, ""));
24
- const binaryPath = path.join(targetDir, plan.binaryName);
25
- try {
26
- await fs.promises.access(binaryPath, fs.constants.X_OK);
27
- return binaryPath;
28
- }
29
- catch {
30
- // continue and attempt download
31
- }
32
- await fs.promises.mkdir(targetDir, { recursive: true });
33
- const downloadUrl = buildDownloadUrl(plan.assetName, tag);
34
- logger(`Downloading rathole (${plan.assetName}) from ${downloadUrl}`);
35
- const tempFile = path.join(targetDir, `${plan.assetName}.${Date.now()}.download`);
36
- try {
37
- await downloadFile(downloadUrl, tempFile, options.fetchImpl);
38
- await extractZip(tempFile, targetDir);
39
- }
40
- finally {
41
- await safeUnlink(tempFile);
42
- }
43
- if (!(await fileExists(binaryPath))) {
44
- throw new Error(`rathole binary missing after extraction (${binaryPath})`);
45
- }
46
- if (process.platform !== "win32") {
47
- await fs.promises.chmod(binaryPath, 0o755);
48
- }
49
- logger(`rathole ready at ${binaryPath}`);
50
- return binaryPath;
51
- }
52
- function resolveInstallPlan() {
53
- const arch = process.arch;
54
- const platform = process.platform;
55
- if (platform === "darwin") {
56
- if (arch === "arm64") {
57
- return {
58
- method: "download",
59
- assetName: "rathole-x86_64-apple-darwin.zip",
60
- binaryName: "rathole",
61
- };
62
- }
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.`);
71
- }
72
- if (platform === "linux") {
73
- if (arch === "arm64") {
74
- return {
75
- method: "download",
76
- assetName: "rathole-aarch64-unknown-linux-musl.zip",
77
- binaryName: "rathole",
78
- };
79
- }
80
- if (arch === "x64") {
81
- return {
82
- method: "download",
83
- assetName: "rathole-x86_64-unknown-linux-gnu.zip",
84
- binaryName: "rathole",
85
- };
86
- }
87
- throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
88
- }
89
- if (platform === "win32") {
90
- if (arch !== "x64") {
91
- throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
92
- }
93
- return {
94
- method: "download",
95
- assetName: "rathole-x86_64-pc-windows-msvc.zip",
96
- binaryName: "rathole.exe",
97
- };
98
- }
99
- throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
100
- }
101
- function normalizeGithubTag(raw) {
102
- const trimmed = raw.trim();
103
- if (!trimmed || trimmed.toLowerCase() === "latest") {
104
- return "latest";
105
- }
106
- return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
107
- }
108
- function buildDownloadUrl(assetName, tag) {
109
- const base = tag === "latest"
110
- ? "https://github.com/rapiz1/rathole/releases/latest/download"
111
- : `https://github.com/rapiz1/rathole/releases/download/${tag}`;
112
- return `${base}/${assetName}`;
113
- }
114
- async function downloadFile(url, destination, fetchImpl) {
115
- const fetcher = fetchImpl ?? fetch;
116
- const response = await fetcher(url);
117
- if (!response.ok || !response.body) {
118
- const text = await response.text().catch(() => "");
119
- throw new Error(`failed to download rathole (${response.status}): ${text}`);
120
- }
121
- const nodeStream = typeof Readable.fromWeb === "function"
122
- ? Readable.fromWeb(response.body)
123
- : response.body;
124
- await pipeline(nodeStream, fs.createWriteStream(destination));
125
- }
126
- async function extractZip(archivePath, targetDir) {
127
- const cmd = process.platform === "win32" ? "powershell.exe" : "unzip";
128
- const args = process.platform === "win32"
129
- ? [
130
- "-NoProfile",
131
- "-NonInteractive",
132
- "-Command",
133
- `Expand-Archive -LiteralPath '${escapePowershellString(archivePath)}' -DestinationPath '${escapePowershellString(targetDir)}' -Force`,
134
- ]
135
- : ["-o", archivePath, "-d", targetDir];
136
- await new Promise((resolve, reject) => {
137
- const child = spawn(cmd, args);
138
- child.on("error", reject);
139
- child.on("close", (code) => {
140
- if (code === 0) {
141
- resolve();
142
- }
143
- else {
144
- reject(new Error(`${cmd} exited with code ${code ?? -1}`));
145
- }
146
- });
147
- });
148
- }
149
- function escapePowershellString(value) {
150
- return value.replace(/'/g, "''");
151
- }
152
- async function ensureRatholeViaCargo(params) {
153
- const tag = normalizeGithubTag(params.version);
154
- const installRoot = path.join(params.cacheRoot, tag, "cargo");
155
- const binPath = path.join(installRoot, "bin", params.binaryName);
156
- try {
157
- await fs.promises.access(binPath, fs.constants.X_OK);
158
- return binPath;
159
- }
160
- catch {
161
- // continue
162
- }
163
- await fs.promises.mkdir(installRoot, { recursive: true });
164
- const crateVersion = tag === "latest" ? null : tag.replace(/^v/, "").trim() || null;
165
- params.logger(crateVersion
166
- ? `Installing rathole ${crateVersion} via cargo (arm64 macOS fallback)...`
167
- : "Installing rathole via cargo (arm64 macOS fallback)...");
168
- const argsLocked = ["install", "rathole", "--locked", "--root", installRoot];
169
- const argsUnlocked = ["install", "rathole", "--root", installRoot];
170
- if (crateVersion) {
171
- argsLocked.push("--version", crateVersion);
172
- argsUnlocked.push("--version", crateVersion);
173
- }
174
- const runCargoInstall = async (args) => await new Promise((resolve, reject) => {
175
- const child = spawn("cargo", args, { stdio: "inherit" });
176
- child.on("error", (error) => {
177
- reject(new Error(`failed to run cargo install for rathole (${String(error)}). Install Rust/cargo or set RATHOLE_BIN.`));
178
- });
179
- child.on("close", (code) => {
180
- if (code === 0) {
181
- resolve();
182
- }
183
- else {
184
- reject(new Error(`cargo install exited with code ${code ?? -1}`));
185
- }
186
- });
187
- });
188
- try {
189
- await runCargoInstall(argsLocked);
190
- }
191
- catch (error) {
192
- params.logger(`cargo install --locked failed (${error instanceof Error ? error.message : String(error)}); retrying without --locked...`);
193
- await runCargoInstall(argsUnlocked);
194
- }
195
- if (!(await fileExists(binPath))) {
196
- throw new Error(`rathole binary missing after cargo install (${binPath})`);
197
- }
198
- return binPath;
199
- }
200
- async function fileExists(candidate) {
201
- try {
202
- await fs.promises.access(candidate, fs.constants.X_OK);
203
- return true;
204
- }
205
- catch {
206
- return false;
207
- }
208
- }
209
- async function safeUnlink(filePath) {
210
- try {
211
- await fs.promises.unlink(filePath);
212
- }
213
- catch {
214
- // ignore
215
- }
216
- }