@instafy/cli 0.1.7 → 0.1.8-staging.350

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,9 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
6
6
 
7
7
  ## Quickstart
8
8
 
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
+ - Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
9
12
  1. Link a folder to a project:
10
13
  - VS Code: install the Instafy extension and run `Instafy: Link Workspace to Project`, or
11
14
  - Terminal: `instafy project:init`
@@ -19,7 +22,10 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
19
22
  - `instafy runtime:start` — start a local runtime (agent + origin).
20
23
  - `instafy runtime:status` — show health of the last started runtime.
21
24
  - `instafy runtime:stop` — stop the last started runtime.
22
- - `instafy tunnel` — request a tunnel and forward a local port.
25
+ - `instafy tunnel` — start a detached tunnel for a local port.
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.
23
29
  - `instafy api:get` — query controller endpoints (conversations, messages, runs, etc).
24
30
 
25
31
  Run `instafy --help` for the full command list and options.
package/dist/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { resolveConfiguredAccessToken } from "./config.js";
2
3
  function normalizeUrl(raw) {
3
4
  const value = (raw ?? "").trim();
4
5
  if (!value)
@@ -18,7 +19,8 @@ function resolveBearerToken(options) {
18
19
  normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
19
20
  normalizeToken(process.env["INSTAFY_SERVICE_TOKEN"]) ??
20
21
  normalizeToken(process.env["CONTROLLER_TOKEN"]) ??
21
- normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]));
22
+ normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
23
+ resolveConfiguredAccessToken());
22
24
  }
23
25
  function parseKeyValue(raw) {
24
26
  const trimmed = raw.trim();
package/dist/auth.js ADDED
@@ -0,0 +1,112 @@
1
+ import kleur from "kleur";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
5
+ import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
6
+ function normalizeUrl(raw) {
7
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
8
+ if (!trimmed) {
9
+ return null;
10
+ }
11
+ return trimmed.replace(/\/$/, "");
12
+ }
13
+ function normalizeToken(raw) {
14
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
15
+ if (!trimmed) {
16
+ return null;
17
+ }
18
+ const lowered = trimmed.toLowerCase();
19
+ if (lowered === "null" || lowered === "undefined") {
20
+ return null;
21
+ }
22
+ return trimmed;
23
+ }
24
+ function looksLikeLocalControllerUrl(controllerUrl) {
25
+ try {
26
+ const parsed = new URL(controllerUrl);
27
+ const host = parsed.hostname.toLowerCase();
28
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
29
+ }
30
+ catch {
31
+ return controllerUrl.includes("127.0.0.1") || controllerUrl.includes("localhost");
32
+ }
33
+ }
34
+ function deriveDefaultStudioUrl(controllerUrl) {
35
+ if (looksLikeLocalControllerUrl(controllerUrl)) {
36
+ return "http://localhost:5173";
37
+ }
38
+ return "https://staging.instafy.dev";
39
+ }
40
+ export async function login(options) {
41
+ const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
42
+ const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
43
+ normalizeUrl(process.env["INSTAFY_STUDIO_URL"] ?? null) ??
44
+ resolveConfiguredStudioUrl() ??
45
+ deriveDefaultStudioUrl(controllerUrl);
46
+ const url = new URL("/cli/login", studioUrl);
47
+ url.searchParams.set("serverUrl", controllerUrl);
48
+ if (options.json) {
49
+ console.log(JSON.stringify({ url: url.toString(), configPath: getInstafyConfigPath() }));
50
+ return;
51
+ }
52
+ console.log(kleur.green("Instafy CLI login"));
53
+ console.log("");
54
+ console.log("1) Open this URL in your browser:");
55
+ console.log(kleur.cyan(url.toString()));
56
+ console.log("");
57
+ console.log("2) After you sign in, copy the token shown on that page.");
58
+ console.log("");
59
+ const provided = normalizeToken(options.token ?? null);
60
+ const existing = resolveUserAccessToken();
61
+ let token = provided;
62
+ if (!token) {
63
+ const rl = createInterface({ input, output });
64
+ try {
65
+ token = normalizeToken(await rl.question("Paste token: "));
66
+ }
67
+ finally {
68
+ rl.close();
69
+ }
70
+ }
71
+ if (!token) {
72
+ throw new Error("No token provided.");
73
+ }
74
+ if (!options.noStore) {
75
+ writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
76
+ console.log("");
77
+ 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
+ }
90
+ else if (existing) {
91
+ console.log("");
92
+ console.log(kleur.yellow("Token not stored (existing token kept)."));
93
+ }
94
+ console.log("");
95
+ console.log("Next:");
96
+ console.log(`- ${kleur.cyan("instafy project:init")}`);
97
+ console.log(`- ${kleur.cyan("instafy runtime:start")}`);
98
+ }
99
+ export async function logout(options) {
100
+ clearInstafyCliConfig(["accessToken"]);
101
+ try {
102
+ uninstallGitCredentialHelper();
103
+ }
104
+ catch {
105
+ // ignore git helper cleanup failures
106
+ }
107
+ if (options?.json) {
108
+ console.log(JSON.stringify({ ok: true }));
109
+ return;
110
+ }
111
+ console.log(kleur.green("Logged out (cleared saved access token)."));
112
+ }
@@ -0,0 +1,104 @@
1
+ import kleur from "kleur";
2
+ import { clearInstafyCliConfig, getInstafyConfigPath, readInstafyCliConfig, writeInstafyCliConfig, } from "./config.js";
3
+ function normalizeKey(raw) {
4
+ const key = raw.trim().toLowerCase().replace(/_/g, "-");
5
+ if (key === "controller-url" || key === "controllerurl" || key === "server-url") {
6
+ return "controller-url";
7
+ }
8
+ if (key === "studio-url" || key === "studiourl") {
9
+ return "studio-url";
10
+ }
11
+ throw new Error(`Unknown config key: ${raw} (supported: controller-url, studio-url)`);
12
+ }
13
+ function getValue(config, key) {
14
+ if (key === "controller-url")
15
+ return config.controllerUrl ?? null;
16
+ if (key === "studio-url")
17
+ return config.studioUrl ?? null;
18
+ return null;
19
+ }
20
+ function updateConfig(key, value) {
21
+ if (key === "controller-url") {
22
+ return writeInstafyCliConfig({ controllerUrl: value });
23
+ }
24
+ if (key === "studio-url") {
25
+ return writeInstafyCliConfig({ studioUrl: value });
26
+ }
27
+ return writeInstafyCliConfig({});
28
+ }
29
+ function clearConfig(key) {
30
+ if (key === "controller-url") {
31
+ clearInstafyCliConfig(["controllerUrl"]);
32
+ return;
33
+ }
34
+ if (key === "studio-url") {
35
+ clearInstafyCliConfig(["studioUrl"]);
36
+ return;
37
+ }
38
+ }
39
+ export function configPath(options) {
40
+ const path = getInstafyConfigPath();
41
+ if (options?.json) {
42
+ console.log(JSON.stringify({ path }, null, 2));
43
+ return;
44
+ }
45
+ console.log(path);
46
+ }
47
+ export function configList(options) {
48
+ const config = readInstafyCliConfig();
49
+ const payload = {
50
+ path: getInstafyConfigPath(),
51
+ controllerUrl: config.controllerUrl ?? null,
52
+ studioUrl: config.studioUrl ?? null,
53
+ accessTokenSet: Boolean(config.accessToken),
54
+ updatedAt: config.updatedAt ?? null,
55
+ };
56
+ if (options?.json) {
57
+ console.log(JSON.stringify(payload, null, 2));
58
+ return;
59
+ }
60
+ console.log(kleur.green("Instafy CLI config"));
61
+ console.log(`Path: ${payload.path}`);
62
+ console.log(`controller-url: ${payload.controllerUrl ?? kleur.yellow("(not set)")}`);
63
+ console.log(`studio-url: ${payload.studioUrl ?? kleur.yellow("(not set)")}`);
64
+ console.log(`access-token: ${payload.accessTokenSet ? kleur.green("(set)") : kleur.yellow("(not set)")}`);
65
+ if (payload.updatedAt) {
66
+ console.log(`updated-at: ${payload.updatedAt}`);
67
+ }
68
+ }
69
+ export function configGet(params) {
70
+ const key = normalizeKey(params.key);
71
+ const config = readInstafyCliConfig();
72
+ const value = getValue(config, key);
73
+ if (!value) {
74
+ throw new Error(`Config key ${key} is not set. Run \`instafy config set ${key} <value>\`.`);
75
+ }
76
+ if (params.json) {
77
+ console.log(JSON.stringify({ key, value }, null, 2));
78
+ return;
79
+ }
80
+ console.log(value);
81
+ }
82
+ export function configSet(params) {
83
+ const key = normalizeKey(params.key);
84
+ const updated = updateConfig(key, params.value);
85
+ const value = getValue(updated, key);
86
+ if (!value) {
87
+ throw new Error(`Failed to set ${key}.`);
88
+ }
89
+ if (params.json) {
90
+ console.log(JSON.stringify({ key, value, path: getInstafyConfigPath() }, null, 2));
91
+ return;
92
+ }
93
+ console.log(kleur.green(`Set ${key}`));
94
+ console.log(value);
95
+ }
96
+ export function configUnset(params) {
97
+ const key = normalizeKey(params.key);
98
+ clearConfig(key);
99
+ if (params.json) {
100
+ console.log(JSON.stringify({ ok: true, key }, null, 2));
101
+ return;
102
+ }
103
+ console.log(kleur.green(`Unset ${key}`));
104
+ }
package/dist/config.js ADDED
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
5
+ const CONFIG_PATH = path.join(INSTAFY_DIR, "config.json");
6
+ function normalizeToken(value) {
7
+ if (typeof value !== "string") {
8
+ return null;
9
+ }
10
+ const trimmed = value.trim();
11
+ if (!trimmed) {
12
+ return null;
13
+ }
14
+ const lowered = trimmed.toLowerCase();
15
+ if (lowered === "null" || lowered === "undefined") {
16
+ return null;
17
+ }
18
+ return trimmed;
19
+ }
20
+ function normalizeUrl(value) {
21
+ const trimmed = typeof value === "string" ? value.trim() : "";
22
+ if (!trimmed) {
23
+ return null;
24
+ }
25
+ return trimmed.replace(/\/$/, "");
26
+ }
27
+ export function getInstafyConfigPath() {
28
+ return CONFIG_PATH;
29
+ }
30
+ export function readInstafyCliConfig() {
31
+ try {
32
+ const raw = fs.readFileSync(CONFIG_PATH, "utf8");
33
+ const parsed = JSON.parse(raw);
34
+ if (!parsed || typeof parsed !== "object") {
35
+ return {};
36
+ }
37
+ const record = parsed;
38
+ return {
39
+ controllerUrl: normalizeUrl(typeof record.controllerUrl === "string" ? record.controllerUrl : null),
40
+ studioUrl: normalizeUrl(typeof record.studioUrl === "string" ? record.studioUrl : null),
41
+ accessToken: normalizeToken(typeof record.accessToken === "string" ? record.accessToken : null),
42
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
43
+ };
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ export function writeInstafyCliConfig(update) {
50
+ const existing = readInstafyCliConfig();
51
+ const next = {
52
+ controllerUrl: normalizeUrl(update.controllerUrl ?? existing.controllerUrl ?? null),
53
+ studioUrl: normalizeUrl(update.studioUrl ?? existing.studioUrl ?? null),
54
+ accessToken: normalizeToken(update.accessToken ?? existing.accessToken ?? null),
55
+ updatedAt: new Date().toISOString(),
56
+ };
57
+ fs.mkdirSync(INSTAFY_DIR, { recursive: true });
58
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2), { encoding: "utf8" });
59
+ try {
60
+ fs.chmodSync(CONFIG_PATH, 0o600);
61
+ }
62
+ catch {
63
+ // ignore chmod failures (windows / unusual fs)
64
+ }
65
+ return next;
66
+ }
67
+ export function clearInstafyCliConfig(keys) {
68
+ if (!keys || keys.length === 0) {
69
+ try {
70
+ fs.rmSync(CONFIG_PATH, { force: true });
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ return;
76
+ }
77
+ const existing = readInstafyCliConfig();
78
+ const next = { ...existing };
79
+ for (const key of keys) {
80
+ next[key] = null;
81
+ }
82
+ writeInstafyCliConfig(next);
83
+ }
84
+ export function resolveConfiguredControllerUrl() {
85
+ const config = readInstafyCliConfig();
86
+ return normalizeUrl(config.controllerUrl ?? null);
87
+ }
88
+ export function resolveConfiguredStudioUrl() {
89
+ const config = readInstafyCliConfig();
90
+ return normalizeUrl(config.studioUrl ?? null);
91
+ }
92
+ export function resolveConfiguredAccessToken() {
93
+ const config = readInstafyCliConfig();
94
+ return normalizeToken(config.accessToken ?? null);
95
+ }
96
+ export function resolveControllerUrl(params) {
97
+ const config = readInstafyCliConfig();
98
+ return (normalizeUrl(params?.controllerUrl ?? null) ??
99
+ normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
100
+ normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
101
+ normalizeUrl(config.controllerUrl ?? null) ??
102
+ "http://127.0.0.1:8788");
103
+ }
104
+ export function resolveUserAccessToken(params) {
105
+ const config = readInstafyCliConfig();
106
+ return (normalizeToken(params?.accessToken ?? null) ??
107
+ normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"] ?? null) ??
108
+ normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"] ?? null) ??
109
+ normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"] ?? null) ??
110
+ normalizeToken(config.accessToken ?? null));
111
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,201 @@
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
+ }
@@ -0,0 +1,56 @@
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
+ }
package/dist/git.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
+ import { resolveConfiguredAccessToken } from "./config.js";
4
5
  function normalizeToken(value) {
5
6
  if (typeof value !== "string") {
6
7
  return null;
@@ -27,6 +28,7 @@ function resolveControllerAccessToken(options) {
27
28
  normalizeToken(options.supabaseAccessToken) ??
28
29
  readTokenFromFile(options.supabaseAccessTokenFile) ??
29
30
  normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
31
+ resolveConfiguredAccessToken() ??
30
32
  null);
31
33
  }
32
34
  export async function mintGitAccessToken(params) {
@@ -41,6 +43,7 @@ export async function mintGitAccessToken(params) {
41
43
  authorization: `Bearer ${params.controllerAccessToken}`,
42
44
  "content-type": "application/json",
43
45
  },
46
+ signal: params.signal,
44
47
  body: JSON.stringify({
45
48
  scopes,
46
49
  ttlSeconds: params.ttlSeconds,
@@ -74,7 +77,7 @@ export async function gitToken(options) {
74
77
  supabaseAccessTokenFile: options.supabaseAccessTokenFile,
75
78
  });
76
79
  if (!token) {
77
- throw new Error("Access token is required (--access-token/INSTAFY_ACCESS_TOKEN or --supabase-access-token/SUPABASE_ACCESS_TOKEN).");
80
+ throw new Error("Login required. Run `instafy login` or pass --access-token / --supabase-access-token.");
78
81
  }
79
82
  const minted = await mintGitAccessToken({
80
83
  controllerUrl,