@instafy/cli 0.1.8-staging.348 → 0.1.8
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 +1 -5
- package/dist/auth.js +0 -18
- package/dist/git.js +0 -1
- package/dist/index.js +7 -127
- package/dist/org.js +4 -2
- package/dist/project.js +5 -6
- package/dist/tunnel.js +2 -260
- package/package.json +1 -2
- package/dist/errors.js +0 -10
- package/dist/git-credential.js +0 -201
- package/dist/git-setup.js +0 -56
package/README.md
CHANGED
|
@@ -7,7 +7,6 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
9
9
|
0. Log in once: `instafy login`
|
|
10
|
-
- Also enables Git auth (credential helper) for Instafy Git Service. Disable with `instafy login --no-git-setup`.
|
|
11
10
|
- Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
|
|
12
11
|
1. Link a folder to a project:
|
|
13
12
|
- VS Code: install the Instafy extension and run `Instafy: Link Workspace to Project`, or
|
|
@@ -22,10 +21,7 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
|
|
|
22
21
|
- `instafy runtime:start` — start a local runtime (agent + origin).
|
|
23
22
|
- `instafy runtime:status` — show health of the last started runtime.
|
|
24
23
|
- `instafy runtime:stop` — stop the last started runtime.
|
|
25
|
-
- `instafy tunnel` —
|
|
26
|
-
- `instafy tunnel:list` — list local tunnels started by the CLI.
|
|
27
|
-
- `instafy tunnel:logs <tunnelId> --follow` — tail tunnel logs.
|
|
28
|
-
- `instafy tunnel:stop <tunnelId>` — stop + revoke a tunnel.
|
|
24
|
+
- `instafy tunnel` — request a tunnel and forward a local port.
|
|
29
25
|
- `instafy api:get` — query controller endpoints (conversations, messages, runs, etc).
|
|
30
26
|
|
|
31
27
|
Run `instafy --help` for the full command list and options.
|
package/dist/auth.js
CHANGED
|
@@ -2,7 +2,6 @@ import kleur from "kleur";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
|
|
5
|
-
import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
|
|
6
5
|
function normalizeUrl(raw) {
|
|
7
6
|
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
8
7
|
if (!trimmed) {
|
|
@@ -75,17 +74,6 @@ export async function login(options) {
|
|
|
75
74
|
writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
|
|
76
75
|
console.log("");
|
|
77
76
|
console.log(kleur.green(`Saved token to ${getInstafyConfigPath()}`));
|
|
78
|
-
if (options.gitSetup !== false) {
|
|
79
|
-
try {
|
|
80
|
-
const result = installGitCredentialHelper();
|
|
81
|
-
if (result.changed) {
|
|
82
|
-
console.log(kleur.green("Enabled git auth (credential helper installed)."));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
console.log(kleur.yellow(`Warning: failed to configure git credential helper: ${error instanceof Error ? error.message : String(error)}`));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
77
|
}
|
|
90
78
|
else if (existing) {
|
|
91
79
|
console.log("");
|
|
@@ -98,12 +86,6 @@ export async function login(options) {
|
|
|
98
86
|
}
|
|
99
87
|
export async function logout(options) {
|
|
100
88
|
clearInstafyCliConfig(["accessToken"]);
|
|
101
|
-
try {
|
|
102
|
-
uninstallGitCredentialHelper();
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// ignore git helper cleanup failures
|
|
106
|
-
}
|
|
107
89
|
if (options?.json) {
|
|
108
90
|
console.log(JSON.stringify({ ok: true }));
|
|
109
91
|
return;
|
package/dist/git.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -5,9 +5,8 @@ import kleur from "kleur";
|
|
|
5
5
|
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, findProjectManifest, } from "./runtime.js";
|
|
6
6
|
import { login, logout } from "./auth.js";
|
|
7
7
|
import { gitToken } from "./git.js";
|
|
8
|
-
import { runGitCredentialHelper } from "./git-credential.js";
|
|
9
8
|
import { projectInit } from "./project.js";
|
|
10
|
-
import {
|
|
9
|
+
import { runTunnelCommand } from "./tunnel.js";
|
|
11
10
|
import { requestControllerApi } from "./api.js";
|
|
12
11
|
import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
|
|
13
12
|
export const program = new Command();
|
|
@@ -41,7 +40,6 @@ program
|
|
|
41
40
|
.option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
|
|
42
41
|
.option("--server-url <url>", "Instafy server/controller URL")
|
|
43
42
|
.option("--token <token>", "Provide token directly (skips prompt)")
|
|
44
|
-
.option("--no-git-setup", "Do not configure git credential helper")
|
|
45
43
|
.option("--no-store", "Do not save token to ~/.instafy/config.json")
|
|
46
44
|
.option("--json", "Output JSON")
|
|
47
45
|
.action(async (opts) => {
|
|
@@ -50,7 +48,6 @@ program
|
|
|
50
48
|
controllerUrl: opts.serverUrl,
|
|
51
49
|
studioUrl: opts.studioUrl,
|
|
52
50
|
token: opts.token,
|
|
53
|
-
gitSetup: opts.gitSetup,
|
|
54
51
|
noStore: opts.store === false,
|
|
55
52
|
json: opts.json,
|
|
56
53
|
});
|
|
@@ -282,19 +279,6 @@ gitTokenCommand
|
|
|
282
279
|
process.exit(1);
|
|
283
280
|
}
|
|
284
281
|
});
|
|
285
|
-
program
|
|
286
|
-
.command("git:credential", { hidden: true })
|
|
287
|
-
.description("Internal: git credential helper (used by Git when configured)")
|
|
288
|
-
.argument("<operation>", "Operation (get|store|erase)")
|
|
289
|
-
.action(async (operation) => {
|
|
290
|
-
try {
|
|
291
|
-
await runGitCredentialHelper(operation);
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
console.error(String(error));
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
282
|
const projectInitCommand = program
|
|
299
283
|
.command("project:init")
|
|
300
284
|
.description("Create an Instafy project and link this folder (.instafy/project.json)")
|
|
@@ -329,130 +313,26 @@ projectInitCommand
|
|
|
329
313
|
});
|
|
330
314
|
const tunnelCommand = program
|
|
331
315
|
.command("tunnel")
|
|
332
|
-
.description("
|
|
316
|
+
.description("Create a shareable tunnel URL for a local port")
|
|
333
317
|
.option("--port <port>", "Local port to expose (default 3000)")
|
|
334
318
|
.option("--project <id>", "Project UUID (defaults to .instafy/project.json or PROJECT_ID)");
|
|
335
319
|
addServerUrlOptions(tunnelCommand);
|
|
336
320
|
addAccessTokenOptions(tunnelCommand, "Instafy access token (defaults to saved `instafy login` token)");
|
|
337
321
|
addServiceTokenOptions(tunnelCommand, "Instafy service token (advanced)");
|
|
338
322
|
tunnelCommand
|
|
339
|
-
.option("--no-detach", "Run in foreground until interrupted")
|
|
340
323
|
.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")
|
|
343
324
|
.action(async (opts) => {
|
|
344
325
|
try {
|
|
345
326
|
const port = opts.port ? Number(opts.port) : undefined;
|
|
346
|
-
|
|
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({
|
|
327
|
+
await runTunnelCommand({
|
|
361
328
|
project: opts.project,
|
|
362
329
|
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
363
|
-
controllerToken
|
|
330
|
+
controllerToken: opts.serviceToken ??
|
|
331
|
+
opts.controllerToken ??
|
|
332
|
+
opts.accessToken ??
|
|
333
|
+
opts.controllerAccessToken,
|
|
364
334
|
port,
|
|
365
335
|
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),
|
|
456
336
|
});
|
|
457
337
|
}
|
|
458
338
|
catch (error) {
|
package/dist/org.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
2
|
import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
3
|
-
|
|
3
|
+
function formatAuthRequiredError() {
|
|
4
|
+
return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
5
|
+
}
|
|
4
6
|
export async function listOrganizations(params) {
|
|
5
7
|
const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
|
|
6
8
|
const token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
|
|
7
9
|
if (!token) {
|
|
8
|
-
throw formatAuthRequiredError(
|
|
10
|
+
throw formatAuthRequiredError();
|
|
9
11
|
}
|
|
10
12
|
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
11
13
|
headers: { authorization: `Bearer ${token}` },
|
package/dist/project.js
CHANGED
|
@@ -2,7 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
4
|
import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
5
|
-
|
|
5
|
+
function formatAuthRequiredError() {
|
|
6
|
+
return new Error("Login required. Run `instafy login` (recommended) or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
7
|
+
}
|
|
6
8
|
async function fetchOrganizations(controllerUrl, token) {
|
|
7
9
|
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
8
10
|
headers: {
|
|
@@ -66,7 +68,7 @@ export async function listProjects(options) {
|
|
|
66
68
|
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
67
69
|
const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
|
|
68
70
|
if (!token) {
|
|
69
|
-
throw formatAuthRequiredError(
|
|
71
|
+
throw formatAuthRequiredError();
|
|
70
72
|
}
|
|
71
73
|
const orgs = await fetchOrganizations(controllerUrl, token);
|
|
72
74
|
let targetOrgs = orgs;
|
|
@@ -114,10 +116,7 @@ export async function projectInit(options) {
|
|
|
114
116
|
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
115
117
|
const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
|
|
116
118
|
if (!token) {
|
|
117
|
-
throw formatAuthRequiredError(
|
|
118
|
-
retryCommand: "instafy project:init",
|
|
119
|
-
advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
|
|
120
|
-
});
|
|
119
|
+
throw formatAuthRequiredError();
|
|
121
120
|
}
|
|
122
121
|
const org = await resolveOrg(controllerUrl, token, options);
|
|
123
122
|
const body = {
|
package/dist/tunnel.js
CHANGED
|
@@ -2,15 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
5
|
import kleur from "kleur";
|
|
7
6
|
import { findProjectManifest, resolveRatholeBinaryForCli } from "./runtime.js";
|
|
8
7
|
import { resolveConfiguredControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
9
|
-
import { formatAuthRequiredError } from "./errors.js";
|
|
10
|
-
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
11
|
-
const TUNNEL_STATE_FILE = path.join(INSTAFY_DIR, "cli-tunnel-state.json");
|
|
12
|
-
const TUNNEL_LOG_DIR = path.join(INSTAFY_DIR, "cli-tunnel-logs");
|
|
13
|
-
const TUNNEL_WORKDIR_DIR = path.join(INSTAFY_DIR, "cli-tunnel-workdirs");
|
|
14
8
|
function cleanUrl(raw) {
|
|
15
9
|
return raw.replace(/\/+$/, "");
|
|
16
10
|
}
|
|
@@ -32,7 +26,7 @@ function resolveProject(opts) {
|
|
|
32
26
|
if (manifest?.projectId) {
|
|
33
27
|
return manifest.projectId;
|
|
34
28
|
}
|
|
35
|
-
throw new Error("No project configured
|
|
29
|
+
throw new Error("No project configured. Run `instafy project:init` or pass --project.");
|
|
36
30
|
}
|
|
37
31
|
function resolveControllerUrl(opts) {
|
|
38
32
|
const explicit = opts.controllerUrl?.trim();
|
|
@@ -73,10 +67,7 @@ function resolveControllerToken(opts) {
|
|
|
73
67
|
if (serviceToken) {
|
|
74
68
|
return serviceToken;
|
|
75
69
|
}
|
|
76
|
-
throw
|
|
77
|
-
retryCommand: "instafy tunnel",
|
|
78
|
-
advancedHint: "pass --access-token / --service-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN / INSTAFY_SERVICE_TOKEN",
|
|
79
|
-
});
|
|
70
|
+
throw new Error("Login required. Run `instafy login`, pass --access-token, or provide --service-token / INSTAFY_SERVICE_TOKEN.");
|
|
80
71
|
}
|
|
81
72
|
function resolvePort(opts) {
|
|
82
73
|
const fromEnv = readEnv("WEBHOOK_LOCAL_PORT") ||
|
|
@@ -85,77 +76,6 @@ function resolvePort(opts) {
|
|
|
85
76
|
const parsedEnv = fromEnv ? Number(fromEnv) : NaN;
|
|
86
77
|
return opts.port ?? (Number.isFinite(parsedEnv) ? parsedEnv : 3000);
|
|
87
78
|
}
|
|
88
|
-
function ensureDir(dir) {
|
|
89
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
-
}
|
|
91
|
-
function safePathSegment(value) {
|
|
92
|
-
const trimmed = value.trim();
|
|
93
|
-
if (!trimmed)
|
|
94
|
-
return randomUUID();
|
|
95
|
-
return trimmed.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
96
|
-
}
|
|
97
|
-
function writeStateFile(state) {
|
|
98
|
-
ensureDir(INSTAFY_DIR);
|
|
99
|
-
fs.writeFileSync(TUNNEL_STATE_FILE, JSON.stringify(state, null, 2), "utf8");
|
|
100
|
-
try {
|
|
101
|
-
fs.chmodSync(TUNNEL_STATE_FILE, 0o600);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// ignore chmod failures (windows / unusual fs)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function readStateFile() {
|
|
108
|
-
try {
|
|
109
|
-
const raw = fs.readFileSync(TUNNEL_STATE_FILE, "utf8");
|
|
110
|
-
const parsed = JSON.parse(raw);
|
|
111
|
-
if (!parsed || typeof parsed !== "object")
|
|
112
|
-
return { version: 1, tunnels: [] };
|
|
113
|
-
if (!Array.isArray(parsed.tunnels))
|
|
114
|
-
return { version: 1, tunnels: [] };
|
|
115
|
-
return {
|
|
116
|
-
version: 1,
|
|
117
|
-
tunnels: parsed.tunnels.filter(Boolean),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return { version: 1, tunnels: [] };
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function isProcessAlive(pid) {
|
|
125
|
-
if (!Number.isFinite(pid) || pid <= 0)
|
|
126
|
-
return false;
|
|
127
|
-
try {
|
|
128
|
-
process.kill(pid, 0);
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
const code = error instanceof Error ? error.code : null;
|
|
133
|
-
// EPERM means the process exists but we don't have permission to signal it.
|
|
134
|
-
return code === "EPERM";
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function waitForProcessExit(pid, timeoutMs) {
|
|
138
|
-
const deadline = Date.now() + timeoutMs;
|
|
139
|
-
while (Date.now() < deadline) {
|
|
140
|
-
if (!isProcessAlive(pid))
|
|
141
|
-
return true;
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
143
|
-
}
|
|
144
|
-
return !isProcessAlive(pid);
|
|
145
|
-
}
|
|
146
|
-
function upsertTunnelState(entry) {
|
|
147
|
-
const state = readStateFile();
|
|
148
|
-
const next = state.tunnels.filter((tunnel) => tunnel.tunnelId !== entry.tunnelId);
|
|
149
|
-
next.unshift(entry);
|
|
150
|
-
writeStateFile({ version: 1, tunnels: next });
|
|
151
|
-
}
|
|
152
|
-
function removeTunnelState(tunnelId) {
|
|
153
|
-
const state = readStateFile();
|
|
154
|
-
const existing = state.tunnels.find((tunnel) => tunnel.tunnelId === tunnelId) ?? null;
|
|
155
|
-
const next = state.tunnels.filter((tunnel) => tunnel.tunnelId !== tunnelId);
|
|
156
|
-
writeStateFile({ version: 1, tunnels: next });
|
|
157
|
-
return existing;
|
|
158
|
-
}
|
|
159
79
|
async function requestTunnel(controllerUrl, token, projectId, metadata) {
|
|
160
80
|
const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/request`;
|
|
161
81
|
const response = await fetch(target, {
|
|
@@ -290,181 +210,3 @@ export async function runTunnelCommand(opts, options) {
|
|
|
290
210
|
process.once("SIGTERM", handle);
|
|
291
211
|
});
|
|
292
212
|
}
|
|
293
|
-
export async function startTunnelDetached(opts) {
|
|
294
|
-
const projectId = resolveProject(opts);
|
|
295
|
-
const controllerUrl = resolveControllerUrl(opts);
|
|
296
|
-
const controllerToken = resolveControllerToken(opts);
|
|
297
|
-
const port = resolvePort(opts);
|
|
298
|
-
if (opts.ratholeBin) {
|
|
299
|
-
process.env.RATHOLE_BIN = opts.ratholeBin;
|
|
300
|
-
}
|
|
301
|
-
const rathole = await resolveRatholeBinaryForCli({
|
|
302
|
-
env: process.env,
|
|
303
|
-
version: process.env.RATHOLE_VERSION ?? null,
|
|
304
|
-
cacheDir: process.env.RATHOLE_CACHE_DIR ?? null,
|
|
305
|
-
logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
|
|
306
|
-
warn: (message) => console.warn(kleur.yellow(message)),
|
|
307
|
-
});
|
|
308
|
-
if (!rathole) {
|
|
309
|
-
throw new Error("rathole is required to start a tunnel. Set RATHOLE_BIN or ensure it is on PATH.");
|
|
310
|
-
}
|
|
311
|
-
const metadata = { localPort: port, source: "instafy-cli" };
|
|
312
|
-
const grant = await requestTunnel(controllerUrl, controllerToken, projectId, metadata);
|
|
313
|
-
ensureDir(TUNNEL_WORKDIR_DIR);
|
|
314
|
-
ensureDir(TUNNEL_LOG_DIR);
|
|
315
|
-
const tunnelIdSafe = safePathSegment(grant.tunnelId);
|
|
316
|
-
const workdir = path.join(TUNNEL_WORKDIR_DIR, tunnelIdSafe);
|
|
317
|
-
ensureDir(workdir);
|
|
318
|
-
const creds = grant.credentials && typeof grant.credentials === "object"
|
|
319
|
-
? grant.credentials
|
|
320
|
-
: {};
|
|
321
|
-
const configBody = buildRatholeConfig(creds, port);
|
|
322
|
-
const configPath = path.join(workdir, "rathole.toml");
|
|
323
|
-
fs.writeFileSync(configPath, configBody, { encoding: "utf8", mode: 0o600 });
|
|
324
|
-
const logFile = opts.logFile?.trim()
|
|
325
|
-
? path.resolve(opts.logFile.trim())
|
|
326
|
-
: path.join(TUNNEL_LOG_DIR, `tunnel-${tunnelIdSafe}.log`);
|
|
327
|
-
const logFd = fs.openSync(logFile, "a");
|
|
328
|
-
const child = spawn(rathole, ["-c", configPath], {
|
|
329
|
-
stdio: ["ignore", logFd, logFd],
|
|
330
|
-
cwd: workdir,
|
|
331
|
-
detached: true,
|
|
332
|
-
});
|
|
333
|
-
let exitedEarly = false;
|
|
334
|
-
let exitMessage = "";
|
|
335
|
-
child.on("exit", (code, signal) => {
|
|
336
|
-
exitedEarly = true;
|
|
337
|
-
exitMessage = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
338
|
-
});
|
|
339
|
-
// Give rathole a moment to fail fast, so `instafy tunnel` can report errors.
|
|
340
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
341
|
-
if (exitedEarly || !isProcessAlive(child.pid ?? -1)) {
|
|
342
|
-
const logTail = (() => {
|
|
343
|
-
try {
|
|
344
|
-
const text = fs.readFileSync(logFile, "utf8");
|
|
345
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
346
|
-
return lines.slice(-20).join("\n");
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
return "";
|
|
350
|
-
}
|
|
351
|
-
})();
|
|
352
|
-
const suffix = logTail ? `\n\nLast logs:\n${logTail}` : "";
|
|
353
|
-
throw new Error(`Tunnel process exited (${exitMessage || "unknown"}).${suffix}`);
|
|
354
|
-
}
|
|
355
|
-
child.unref();
|
|
356
|
-
const entry = {
|
|
357
|
-
tunnelId: grant.tunnelId,
|
|
358
|
-
projectId,
|
|
359
|
-
hostname: grant.hostname,
|
|
360
|
-
url: grant.url ?? `https://${grant.hostname}`,
|
|
361
|
-
localPort: port,
|
|
362
|
-
controllerUrl: cleanUrl(controllerUrl),
|
|
363
|
-
pid: child.pid ?? -1,
|
|
364
|
-
logFile,
|
|
365
|
-
workdir,
|
|
366
|
-
startedAt: new Date().toISOString(),
|
|
367
|
-
};
|
|
368
|
-
upsertTunnelState(entry);
|
|
369
|
-
return entry;
|
|
370
|
-
}
|
|
371
|
-
export function listTunnelSessions(options) {
|
|
372
|
-
const state = readStateFile();
|
|
373
|
-
if (options?.all) {
|
|
374
|
-
return state.tunnels;
|
|
375
|
-
}
|
|
376
|
-
return state.tunnels.filter((entry) => isProcessAlive(entry.pid));
|
|
377
|
-
}
|
|
378
|
-
export async function stopTunnelSession(opts) {
|
|
379
|
-
const tunnelId = opts.tunnelId?.trim() ?? "";
|
|
380
|
-
if (!tunnelId) {
|
|
381
|
-
const active = listTunnelSessions({ all: false });
|
|
382
|
-
if (active.length === 1) {
|
|
383
|
-
return stopTunnelSession({ ...opts, tunnelId: active[0]?.tunnelId });
|
|
384
|
-
}
|
|
385
|
-
throw new Error("Tunnel id is required. Use `instafy tunnel:list` to find it.");
|
|
386
|
-
}
|
|
387
|
-
const entry = removeTunnelState(tunnelId);
|
|
388
|
-
if (!entry) {
|
|
389
|
-
throw new Error(`Tunnel not found in local state: ${tunnelId}`);
|
|
390
|
-
}
|
|
391
|
-
if (isProcessAlive(entry.pid)) {
|
|
392
|
-
try {
|
|
393
|
-
process.kill(entry.pid, "SIGTERM");
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
// ignore
|
|
397
|
-
}
|
|
398
|
-
const stopped = await waitForProcessExit(entry.pid, 4000);
|
|
399
|
-
if (!stopped) {
|
|
400
|
-
try {
|
|
401
|
-
process.kill(entry.pid, "SIGKILL");
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
// ignore
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
const controllerToken = resolveControllerToken(opts);
|
|
409
|
-
await revokeTunnel(entry.controllerUrl, controllerToken, entry.projectId, entry.tunnelId);
|
|
410
|
-
try {
|
|
411
|
-
fs.rmSync(entry.workdir, { recursive: true, force: true });
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
// ignore
|
|
415
|
-
}
|
|
416
|
-
return { ok: true, tunnelId: entry.tunnelId };
|
|
417
|
-
}
|
|
418
|
-
export function resolveTunnelLogFile(tunnelId) {
|
|
419
|
-
const chosen = tunnelId?.trim() ?? "";
|
|
420
|
-
const all = readStateFile().tunnels;
|
|
421
|
-
if (chosen) {
|
|
422
|
-
const entry = all.find((tunnel) => tunnel.tunnelId === chosen);
|
|
423
|
-
if (!entry) {
|
|
424
|
-
throw new Error(`Tunnel not found in local state: ${chosen}`);
|
|
425
|
-
}
|
|
426
|
-
return entry;
|
|
427
|
-
}
|
|
428
|
-
const active = all.filter((entry) => isProcessAlive(entry.pid));
|
|
429
|
-
if (active.length === 1) {
|
|
430
|
-
return active[0];
|
|
431
|
-
}
|
|
432
|
-
throw new Error("Tunnel id is required. Use `instafy tunnel:list` to find it.");
|
|
433
|
-
}
|
|
434
|
-
export async function tailTunnelLogs(options) {
|
|
435
|
-
const entry = resolveTunnelLogFile(options.tunnelId);
|
|
436
|
-
const logFile = entry.logFile;
|
|
437
|
-
const lines = Number.isFinite(options.lines) ? options.lines : NaN;
|
|
438
|
-
const lineCount = Number.isFinite(lines) && lines > 0 ? Math.floor(lines) : 200;
|
|
439
|
-
const follow = Boolean(options.follow);
|
|
440
|
-
if (options.json) {
|
|
441
|
-
console.log(JSON.stringify({ tunnelId: entry.tunnelId, logFile, follow, lines: lineCount }, null, 2));
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (!follow) {
|
|
445
|
-
const raw = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
|
|
446
|
-
const rows = raw.split(/\r?\n/);
|
|
447
|
-
const tail = rows.slice(Math.max(0, rows.length - lineCount));
|
|
448
|
-
console.log(tail.join("\n"));
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
// Follow mode: prefer system tail tools.
|
|
452
|
-
const child = process.platform === "win32"
|
|
453
|
-
? spawn("powershell.exe", [
|
|
454
|
-
"-NoProfile",
|
|
455
|
-
"-Command",
|
|
456
|
-
`Get-Content -LiteralPath '${logFile.replace(/'/g, "''")}' -Tail ${lineCount} -Wait`,
|
|
457
|
-
], { stdio: "inherit" })
|
|
458
|
-
: spawn("tail", ["-n", String(lineCount), "-f", logFile], { stdio: "inherit" });
|
|
459
|
-
const handleExit = () => {
|
|
460
|
-
try {
|
|
461
|
-
child.kill("SIGTERM");
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
// ignore
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
process.once("SIGINT", handleExit);
|
|
468
|
-
process.once("SIGTERM", handleExit);
|
|
469
|
-
await new Promise((resolve) => child.on("exit", () => resolve()));
|
|
470
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instafy/cli",
|
|
3
|
-
"version": "0.1.8
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"type": "module",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"prepack": "tsc -p tsconfig.json",
|
|
21
20
|
"dev": "ts-node src/index.ts",
|
|
22
21
|
"test": "pnpm test:unit",
|
|
23
22
|
"test:unit": "pnpm build && vitest run",
|
package/dist/errors.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function formatAuthRequiredError(params) {
|
|
2
|
-
const lines = ["Not authenticated. Run `instafy login` first."];
|
|
3
|
-
if (params?.retryCommand) {
|
|
4
|
-
lines.push("", `Then retry: ${params.retryCommand}`);
|
|
5
|
-
}
|
|
6
|
-
if (params?.advancedHint) {
|
|
7
|
-
lines.push("", `Advanced: ${params.advancedHint}`);
|
|
8
|
-
}
|
|
9
|
-
return new Error(lines.join("\n"));
|
|
10
|
-
}
|
package/dist/git-credential.js
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
|
|
3
|
-
import { mintGitAccessToken } from "./git.js";
|
|
4
|
-
function parseCredentialRequest(raw) {
|
|
5
|
-
const request = {};
|
|
6
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
7
|
-
if (!line.trim())
|
|
8
|
-
continue;
|
|
9
|
-
const idx = line.indexOf("=");
|
|
10
|
-
if (idx <= 0)
|
|
11
|
-
continue;
|
|
12
|
-
const key = line.slice(0, idx).trim();
|
|
13
|
-
const value = line.slice(idx + 1).trim();
|
|
14
|
-
if (!value)
|
|
15
|
-
continue;
|
|
16
|
-
if (key === "protocol")
|
|
17
|
-
request.protocol = value;
|
|
18
|
-
if (key === "host")
|
|
19
|
-
request.host = value;
|
|
20
|
-
if (key === "path")
|
|
21
|
-
request.path = value;
|
|
22
|
-
if (key === "url")
|
|
23
|
-
request.url = value;
|
|
24
|
-
if (key === "username")
|
|
25
|
-
request.username = value;
|
|
26
|
-
}
|
|
27
|
-
return request;
|
|
28
|
-
}
|
|
29
|
-
function normalizeHost(rawHost) {
|
|
30
|
-
const host = rawHost.trim();
|
|
31
|
-
if (!host)
|
|
32
|
-
return null;
|
|
33
|
-
const lowered = host.toLowerCase();
|
|
34
|
-
const bracketed = lowered.match(/^\[(.+)\](?::\d+)?$/);
|
|
35
|
-
if (bracketed) {
|
|
36
|
-
return { host: lowered, hostname: bracketed[1] ?? lowered };
|
|
37
|
-
}
|
|
38
|
-
const lastColon = lowered.lastIndexOf(":");
|
|
39
|
-
if (lastColon > 0) {
|
|
40
|
-
const possiblePort = lowered.slice(lastColon + 1);
|
|
41
|
-
if (/^\d+$/.test(possiblePort)) {
|
|
42
|
-
return { host: lowered, hostname: lowered.slice(0, lastColon) };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return { host: lowered, hostname: lowered };
|
|
46
|
-
}
|
|
47
|
-
function resolveRequestHost(request) {
|
|
48
|
-
if (request.host?.trim())
|
|
49
|
-
return request.host.trim();
|
|
50
|
-
if (request.url?.trim()) {
|
|
51
|
-
try {
|
|
52
|
-
const parsed = new URL(request.url.trim());
|
|
53
|
-
return parsed.host;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
function splitCsv(value) {
|
|
62
|
-
return (value ?? "")
|
|
63
|
-
.split(",")
|
|
64
|
-
.map((entry) => entry.trim().toLowerCase())
|
|
65
|
-
.filter(Boolean);
|
|
66
|
-
}
|
|
67
|
-
function isAllowedGitHost(rawHost) {
|
|
68
|
-
if (!rawHost)
|
|
69
|
-
return false;
|
|
70
|
-
const normalized = normalizeHost(rawHost);
|
|
71
|
-
if (!normalized)
|
|
72
|
-
return false;
|
|
73
|
-
const allowHosts = splitCsv(process.env["INSTAFY_GIT_HOSTS"]);
|
|
74
|
-
if (allowHosts.includes(normalized.host) || allowHosts.includes(normalized.hostname)) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
// Safe-by-default allow-list: Instafy domains and local dev.
|
|
78
|
-
if (normalized.hostname === "localhost" ||
|
|
79
|
-
normalized.hostname === "127.0.0.1" ||
|
|
80
|
-
normalized.hostname === "::1" ||
|
|
81
|
-
normalized.hostname === "host.docker.internal") {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
return normalized.hostname.endsWith(".instafy.dev");
|
|
85
|
-
}
|
|
86
|
-
function normalizeRepoName(raw) {
|
|
87
|
-
const trimmed = raw.trim().replace(/^\/+/, "");
|
|
88
|
-
if (!trimmed)
|
|
89
|
-
return null;
|
|
90
|
-
const first = trimmed.split("/")[0] ?? "";
|
|
91
|
-
if (!first.endsWith(".git"))
|
|
92
|
-
return null;
|
|
93
|
-
const withoutSuffix = first.slice(0, -".git".length);
|
|
94
|
-
return withoutSuffix || null;
|
|
95
|
-
}
|
|
96
|
-
function isUuid(value) {
|
|
97
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
98
|
-
}
|
|
99
|
-
function parseProjectIdFromUrl(raw) {
|
|
100
|
-
const trimmed = raw.trim();
|
|
101
|
-
if (!trimmed)
|
|
102
|
-
return null;
|
|
103
|
-
try {
|
|
104
|
-
const parsed = new URL(trimmed);
|
|
105
|
-
const repo = normalizeRepoName(parsed.pathname);
|
|
106
|
-
return repo && isUuid(repo) ? repo : null;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Not a WHATWG URL (e.g. scp-like).
|
|
110
|
-
}
|
|
111
|
-
const scpLike = trimmed.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
|
|
112
|
-
if (scpLike) {
|
|
113
|
-
const repo = normalizeRepoName(scpLike[2] ?? "");
|
|
114
|
-
return repo && isUuid(repo) ? repo : null;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
function resolveProjectIdFromRequest(request) {
|
|
119
|
-
const fromPath = request.path ? normalizeRepoName(request.path) : null;
|
|
120
|
-
if (fromPath && isUuid(fromPath))
|
|
121
|
-
return fromPath;
|
|
122
|
-
const fromUrl = request.url ? parseProjectIdFromUrl(request.url) : null;
|
|
123
|
-
if (fromUrl)
|
|
124
|
-
return fromUrl;
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
function resolveProjectIdFromGitRemotes(host) {
|
|
128
|
-
const result = spawnSync("git", ["config", "--get-regexp", "^remote\\..*\\.url$"], {
|
|
129
|
-
encoding: "utf8",
|
|
130
|
-
});
|
|
131
|
-
if (result.status !== 0) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
const lines = (result.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
135
|
-
for (const line of lines) {
|
|
136
|
-
const parts = line.split(/\s+/, 2);
|
|
137
|
-
const url = parts[1] ?? "";
|
|
138
|
-
if (!url)
|
|
139
|
-
continue;
|
|
140
|
-
if (host) {
|
|
141
|
-
try {
|
|
142
|
-
const parsed = new URL(url);
|
|
143
|
-
if (parsed.host !== host)
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// ignore non-url remotes when host is known
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const projectId = parseProjectIdFromUrl(url);
|
|
152
|
-
if (projectId)
|
|
153
|
-
return projectId;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
export async function runGitCredentialHelper(operation) {
|
|
158
|
-
const normalized = (operation ?? "").trim().toLowerCase();
|
|
159
|
-
if (!normalized) {
|
|
160
|
-
throw new Error("git credential helper requires an operation (get|store|erase)");
|
|
161
|
-
}
|
|
162
|
-
// We mint tokens on demand; no persistence needed.
|
|
163
|
-
if (normalized === "store" || normalized === "erase") {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (normalized !== "get") {
|
|
167
|
-
throw new Error(`unsupported git credential operation: ${operation}`);
|
|
168
|
-
}
|
|
169
|
-
const stdin = await new Promise((resolve) => {
|
|
170
|
-
let buffer = "";
|
|
171
|
-
process.stdin.setEncoding("utf8");
|
|
172
|
-
process.stdin.on("data", (chunk) => (buffer += chunk));
|
|
173
|
-
process.stdin.on("end", () => resolve(buffer));
|
|
174
|
-
process.stdin.resume();
|
|
175
|
-
});
|
|
176
|
-
const request = parseCredentialRequest(stdin);
|
|
177
|
-
const requestHost = resolveRequestHost(request);
|
|
178
|
-
if (!isAllowedGitHost(requestHost)) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const projectId = resolveProjectIdFromRequest(request) ??
|
|
182
|
-
resolveProjectIdFromGitRemotes(requestHost);
|
|
183
|
-
if (!projectId) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const controllerUrl = resolveControllerUrl({ controllerUrl: null });
|
|
187
|
-
const userAccessToken = resolveUserAccessToken({ accessToken: null });
|
|
188
|
-
if (!userAccessToken) {
|
|
189
|
-
throw new Error("Not authenticated. Run `instafy login` first.");
|
|
190
|
-
}
|
|
191
|
-
const abort = new AbortController();
|
|
192
|
-
const timeout = setTimeout(() => abort.abort(), 5000);
|
|
193
|
-
const minted = await mintGitAccessToken({
|
|
194
|
-
controllerUrl,
|
|
195
|
-
controllerAccessToken: userAccessToken,
|
|
196
|
-
projectId,
|
|
197
|
-
scopes: ["git.read", "git.write"],
|
|
198
|
-
signal: abort.signal,
|
|
199
|
-
}).finally(() => clearTimeout(timeout));
|
|
200
|
-
process.stdout.write(`username=instafy\npassword=${minted.token}\n`);
|
|
201
|
-
}
|
package/dist/git-setup.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
const INSTAFY_GIT_HELPER_VALUE = "!instafy git:credential";
|
|
3
|
-
function isLikelySameHelper(value) {
|
|
4
|
-
return value.includes("instafy git:credential");
|
|
5
|
-
}
|
|
6
|
-
function runGit(args) {
|
|
7
|
-
const result = spawnSync("git", args, { encoding: "utf8" });
|
|
8
|
-
if (result.error) {
|
|
9
|
-
throw result.error;
|
|
10
|
-
}
|
|
11
|
-
return result;
|
|
12
|
-
}
|
|
13
|
-
export function isGitAvailable() {
|
|
14
|
-
try {
|
|
15
|
-
const result = runGit(["--version"]);
|
|
16
|
-
return result.status === 0;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export function installGitCredentialHelper() {
|
|
23
|
-
if (!isGitAvailable()) {
|
|
24
|
-
return { changed: false };
|
|
25
|
-
}
|
|
26
|
-
const existing = runGit(["config", "--global", "--get-all", "credential.helper"]);
|
|
27
|
-
const helpers = (existing.stdout ?? "")
|
|
28
|
-
.split(/\r?\n/)
|
|
29
|
-
.map((line) => line.trim())
|
|
30
|
-
.filter(Boolean);
|
|
31
|
-
if (helpers.some(isLikelySameHelper)) {
|
|
32
|
-
return { changed: false };
|
|
33
|
-
}
|
|
34
|
-
runGit(["config", "--global", "--add", "credential.helper", INSTAFY_GIT_HELPER_VALUE]);
|
|
35
|
-
return { changed: true };
|
|
36
|
-
}
|
|
37
|
-
export function uninstallGitCredentialHelper() {
|
|
38
|
-
if (!isGitAvailable()) {
|
|
39
|
-
return { changed: false };
|
|
40
|
-
}
|
|
41
|
-
const existing = runGit(["config", "--global", "--get-all", "credential.helper"]);
|
|
42
|
-
const helpers = (existing.stdout ?? "")
|
|
43
|
-
.split(/\r?\n/)
|
|
44
|
-
.map((line) => line.trim())
|
|
45
|
-
.filter(Boolean);
|
|
46
|
-
if (!helpers.some(isLikelySameHelper)) {
|
|
47
|
-
return { changed: false };
|
|
48
|
-
}
|
|
49
|
-
const remaining = helpers.filter((helper) => !isLikelySameHelper(helper));
|
|
50
|
-
// Remove all helpers, then re-add the ones we didn't own.
|
|
51
|
-
runGit(["config", "--global", "--unset-all", "credential.helper"]);
|
|
52
|
-
for (const helper of remaining) {
|
|
53
|
-
runGit(["config", "--global", "--add", "credential.helper", helper]);
|
|
54
|
-
}
|
|
55
|
-
return { changed: true };
|
|
56
|
-
}
|