@instafy/cli 0.1.6 → 0.1.7
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 -1
- package/dist/api.js +130 -0
- package/dist/git.js +96 -0
- package/dist/index.js +82 -56
- package/dist/runtime.js +9 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
|
|
|
20
20
|
- `instafy runtime:status` — show health of the last started runtime.
|
|
21
21
|
- `instafy runtime:stop` — stop the last started runtime.
|
|
22
22
|
- `instafy tunnel` — request a tunnel and forward a local port.
|
|
23
|
-
- `instafy
|
|
23
|
+
- `instafy api:get` — query controller endpoints (conversations, messages, runs, etc).
|
|
24
24
|
|
|
25
25
|
Run `instafy --help` for the full command list and options.
|
|
26
26
|
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
function normalizeUrl(raw) {
|
|
3
|
+
const value = (raw ?? "").trim();
|
|
4
|
+
if (!value)
|
|
5
|
+
return "http://127.0.0.1:8788";
|
|
6
|
+
return value.replace(/\/$/, "");
|
|
7
|
+
}
|
|
8
|
+
function normalizeToken(raw) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return null;
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
return trimmed.length ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
function resolveBearerToken(options) {
|
|
15
|
+
return (normalizeToken(options.accessToken) ??
|
|
16
|
+
normalizeToken(options.serviceToken) ??
|
|
17
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
18
|
+
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
19
|
+
normalizeToken(process.env["INSTAFY_SERVICE_TOKEN"]) ??
|
|
20
|
+
normalizeToken(process.env["CONTROLLER_TOKEN"]) ??
|
|
21
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]));
|
|
22
|
+
}
|
|
23
|
+
function parseKeyValue(raw) {
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
throw new Error("Key/value pair cannot be empty");
|
|
27
|
+
}
|
|
28
|
+
const equals = trimmed.indexOf("=");
|
|
29
|
+
if (equals === -1) {
|
|
30
|
+
return { key: trimmed, value: "" };
|
|
31
|
+
}
|
|
32
|
+
const key = trimmed.slice(0, equals).trim();
|
|
33
|
+
const value = trimmed.slice(equals + 1).trim();
|
|
34
|
+
if (!key) {
|
|
35
|
+
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
36
|
+
}
|
|
37
|
+
return { key, value };
|
|
38
|
+
}
|
|
39
|
+
function parseHeader(raw) {
|
|
40
|
+
const trimmed = raw.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
throw new Error("Header cannot be empty");
|
|
43
|
+
}
|
|
44
|
+
const colon = trimmed.indexOf(":");
|
|
45
|
+
if (colon !== -1) {
|
|
46
|
+
const key = trimmed.slice(0, colon).trim();
|
|
47
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
48
|
+
if (!key) {
|
|
49
|
+
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
50
|
+
}
|
|
51
|
+
return { key, value };
|
|
52
|
+
}
|
|
53
|
+
return parseKeyValue(trimmed);
|
|
54
|
+
}
|
|
55
|
+
function parseJsonBody(options) {
|
|
56
|
+
if (options.jsonFile) {
|
|
57
|
+
const raw = fs.readFileSync(options.jsonFile, "utf8");
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
return JSON.stringify(parsed);
|
|
60
|
+
}
|
|
61
|
+
if (options.json) {
|
|
62
|
+
const parsed = JSON.parse(options.json);
|
|
63
|
+
return JSON.stringify(parsed);
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function buildRequestUrl(options) {
|
|
68
|
+
const base = normalizeUrl(options.controllerUrl ??
|
|
69
|
+
process.env["INSTAFY_SERVER_URL"] ??
|
|
70
|
+
process.env["CONTROLLER_BASE_URL"] ??
|
|
71
|
+
"http://127.0.0.1:8788");
|
|
72
|
+
const rawPath = options.path.trim();
|
|
73
|
+
if (!rawPath) {
|
|
74
|
+
throw new Error("Path is required");
|
|
75
|
+
}
|
|
76
|
+
const url = rawPath.startsWith("http://") || rawPath.startsWith("https://")
|
|
77
|
+
? new URL(rawPath)
|
|
78
|
+
: new URL(rawPath.startsWith("/") ? rawPath : `/${rawPath}`, `${base}/`);
|
|
79
|
+
for (const pair of options.query ?? []) {
|
|
80
|
+
const { key, value } = parseKeyValue(pair);
|
|
81
|
+
url.searchParams.set(key, value);
|
|
82
|
+
}
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
function maybePrettyPrintJson(text, pretty) {
|
|
86
|
+
if (!pretty)
|
|
87
|
+
return text;
|
|
88
|
+
if (!text.trim())
|
|
89
|
+
return text;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function requestControllerApi(options) {
|
|
98
|
+
const url = buildRequestUrl(options);
|
|
99
|
+
const bearer = resolveBearerToken(options);
|
|
100
|
+
const headers = new Headers();
|
|
101
|
+
headers.set("accept", "application/json");
|
|
102
|
+
if (bearer) {
|
|
103
|
+
headers.set("authorization", `Bearer ${bearer}`);
|
|
104
|
+
}
|
|
105
|
+
for (const header of options.headers ?? []) {
|
|
106
|
+
const { key, value } = parseHeader(header);
|
|
107
|
+
headers.set(key, value);
|
|
108
|
+
}
|
|
109
|
+
const body = parseJsonBody(options);
|
|
110
|
+
if (body !== undefined && !headers.has("content-type")) {
|
|
111
|
+
headers.set("content-type", "application/json");
|
|
112
|
+
}
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
method: options.method,
|
|
115
|
+
headers,
|
|
116
|
+
body,
|
|
117
|
+
});
|
|
118
|
+
const responseText = await response.text().catch(() => "");
|
|
119
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
120
|
+
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
121
|
+
const pretty = options.pretty !== false;
|
|
122
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
125
|
+
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
126
|
+
throw new Error(`${prefix}${suffix}`);
|
|
127
|
+
}
|
|
128
|
+
if (formattedBody)
|
|
129
|
+
console.log(formattedBody);
|
|
130
|
+
}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
function normalizeToken(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
10
|
+
}
|
|
11
|
+
function readTokenFromFile(filePath) {
|
|
12
|
+
const normalized = normalizeToken(filePath);
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const resolved = path.resolve(normalized);
|
|
17
|
+
const contents = fs.readFileSync(resolved, "utf8").trim();
|
|
18
|
+
if (!contents) {
|
|
19
|
+
throw new Error(`token file ${resolved} was empty`);
|
|
20
|
+
}
|
|
21
|
+
return contents;
|
|
22
|
+
}
|
|
23
|
+
function resolveControllerAccessToken(options) {
|
|
24
|
+
return (normalizeToken(options.controllerAccessToken) ??
|
|
25
|
+
normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
26
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
27
|
+
normalizeToken(options.supabaseAccessToken) ??
|
|
28
|
+
readTokenFromFile(options.supabaseAccessTokenFile) ??
|
|
29
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
|
|
30
|
+
null);
|
|
31
|
+
}
|
|
32
|
+
export async function mintGitAccessToken(params) {
|
|
33
|
+
const url = params.controllerUrl.replace(/\/$/, "");
|
|
34
|
+
const target = `${url}/projects/${encodeURIComponent(params.projectId)}/git/access_token`;
|
|
35
|
+
const scopes = params.scopes && params.scopes.length > 0
|
|
36
|
+
? params.scopes
|
|
37
|
+
: ["git.read", "git.write"];
|
|
38
|
+
const response = await fetch(target, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
authorization: `Bearer ${params.controllerAccessToken}`,
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
scopes,
|
|
46
|
+
ttlSeconds: params.ttlSeconds,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const text = await response.text().catch(() => "");
|
|
51
|
+
throw new Error(`Instafy server rejected git token request (${response.status} ${response.statusText}): ${text}`);
|
|
52
|
+
}
|
|
53
|
+
const payload = (await response.json());
|
|
54
|
+
const token = typeof payload.token === "string" ? payload.token.trim() : "";
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new Error("Instafy server response missing token field while minting git token.");
|
|
57
|
+
}
|
|
58
|
+
const expiresIn = typeof payload.expiresIn === "number" ? payload.expiresIn : 0;
|
|
59
|
+
return {
|
|
60
|
+
projectId: payload.projectId ?? params.projectId,
|
|
61
|
+
token,
|
|
62
|
+
expiresIn,
|
|
63
|
+
scopes: Array.isArray(payload.scopes) ? payload.scopes : scopes,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function gitToken(options) {
|
|
67
|
+
const controllerUrl = options.controllerUrl ??
|
|
68
|
+
process.env["INSTAFY_SERVER_URL"] ??
|
|
69
|
+
process.env["CONTROLLER_BASE_URL"] ??
|
|
70
|
+
"http://127.0.0.1:8788";
|
|
71
|
+
const token = resolveControllerAccessToken({
|
|
72
|
+
controllerAccessToken: options.controllerAccessToken,
|
|
73
|
+
supabaseAccessToken: options.supabaseAccessToken,
|
|
74
|
+
supabaseAccessTokenFile: options.supabaseAccessTokenFile,
|
|
75
|
+
});
|
|
76
|
+
if (!token) {
|
|
77
|
+
throw new Error("Access token is required (--access-token/INSTAFY_ACCESS_TOKEN or --supabase-access-token/SUPABASE_ACCESS_TOKEN).");
|
|
78
|
+
}
|
|
79
|
+
const minted = await mintGitAccessToken({
|
|
80
|
+
controllerUrl,
|
|
81
|
+
controllerAccessToken: token,
|
|
82
|
+
projectId: options.project,
|
|
83
|
+
scopes: options.scopes,
|
|
84
|
+
ttlSeconds: options.ttlSeconds,
|
|
85
|
+
});
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify(minted));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(minted.token);
|
|
91
|
+
if (minted.expiresIn > 0) {
|
|
92
|
+
console.error(kleur.gray(`expiresIn=${minted.expiresIn}s`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return minted.token;
|
|
96
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@ import { Command, Option } from "commander";
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import kleur from "kleur";
|
|
5
|
-
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken,
|
|
5
|
+
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, } from "./runtime.js";
|
|
6
|
+
import { gitToken } from "./git.js";
|
|
6
7
|
import { projectInit } from "./project.js";
|
|
7
8
|
import { runTunnelCommand } from "./tunnel.js";
|
|
9
|
+
import { requestControllerApi } from "./api.js";
|
|
8
10
|
export const program = new Command();
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
12
|
const pkg = require("../package.json");
|
|
@@ -23,6 +25,9 @@ function addServiceTokenOptions(command, description) {
|
|
|
23
25
|
.option("--service-token <token>", description)
|
|
24
26
|
.addOption(new Option("--controller-token <token>").hideHelp());
|
|
25
27
|
}
|
|
28
|
+
function collectStringOption(value, previous) {
|
|
29
|
+
return [...previous, value];
|
|
30
|
+
}
|
|
26
31
|
program
|
|
27
32
|
.name("instafy")
|
|
28
33
|
.description("Instafy CLI — run your project locally and connect to Studio")
|
|
@@ -50,8 +55,6 @@ runtimeStartCommand
|
|
|
50
55
|
.option("--provider <provider>", "Runtime provider label (default: self-hosted)")
|
|
51
56
|
.option("--bind-host <host>", "Origin bind host (default 127.0.0.1)")
|
|
52
57
|
.option("--bind-port <port>", "Origin bind port (default 54332)")
|
|
53
|
-
.option("--mount-project-files", "Mount project files from Instafy (SSHFS)")
|
|
54
|
-
.addOption(new Option("--mount-controller-fs").hideHelp())
|
|
55
58
|
.option("--detach", "Run runtime in background and exit immediately")
|
|
56
59
|
.option("--log-file <path>", "Write runtime stdout/stderr to a file (implied when detached)")
|
|
57
60
|
.action(async (opts) => {
|
|
@@ -61,8 +64,6 @@ runtimeStartCommand
|
|
|
61
64
|
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
62
65
|
controllerToken: opts.serviceToken ?? opts.controllerToken,
|
|
63
66
|
controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
|
|
64
|
-
mountProjectFiles: Boolean(opts.mountProjectFiles),
|
|
65
|
-
mountControllerFs: Boolean(opts.mountControllerFs),
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
catch (error) {
|
|
@@ -83,21 +84,6 @@ program
|
|
|
83
84
|
process.exit(1);
|
|
84
85
|
}
|
|
85
86
|
});
|
|
86
|
-
program
|
|
87
|
-
.command("runtime:sshfs:check")
|
|
88
|
-
.description("Check SSHFS availability (needed for file mounts)")
|
|
89
|
-
.option("--sshfs-bin <path>", "sshfs binary (default: sshfs or SHARED_FS_BIN)")
|
|
90
|
-
.action(async (opts) => {
|
|
91
|
-
try {
|
|
92
|
-
const { checkSshfs } = await import("./runtime.js");
|
|
93
|
-
await checkSshfs(opts.sshfsBin);
|
|
94
|
-
console.log(kleur.green(`sshfs OK (${opts.sshfsBin ?? process.env.SHARED_FS_BIN ?? "sshfs"})`));
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
console.error(kleur.red(String(error)));
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
87
|
program
|
|
102
88
|
.command("runtime:stop")
|
|
103
89
|
.description("Stop the local Instafy runtime")
|
|
@@ -137,6 +123,41 @@ runtimeTokenCommand
|
|
|
137
123
|
process.exit(1);
|
|
138
124
|
}
|
|
139
125
|
});
|
|
126
|
+
const gitTokenCommand = program
|
|
127
|
+
.command("git:token")
|
|
128
|
+
.description("Mint a git access token for the project repo")
|
|
129
|
+
.requiredOption("--project <id>", "Project UUID");
|
|
130
|
+
addServerUrlOptions(gitTokenCommand);
|
|
131
|
+
addAccessTokenOptions(gitTokenCommand, "Instafy access token (required)");
|
|
132
|
+
gitTokenCommand
|
|
133
|
+
.option("--supabase-access-token <token>", "Supabase session token (alternative to Studio token)")
|
|
134
|
+
.option("--supabase-access-token-file <path>", "File containing the Supabase session token")
|
|
135
|
+
.option("--scope <scope...>", "Requested scopes (git.read, git.write)")
|
|
136
|
+
.option("--ttl-seconds <seconds>", "Token TTL in seconds (default server policy)")
|
|
137
|
+
.option("--json", "Output token response as JSON")
|
|
138
|
+
.action(async (opts) => {
|
|
139
|
+
try {
|
|
140
|
+
const ttlSecondsRaw = typeof opts.ttlSeconds === "string" ? opts.ttlSeconds.trim() : "";
|
|
141
|
+
const ttlSeconds = ttlSecondsRaw.length > 0 ? Number.parseInt(ttlSecondsRaw, 10) : undefined;
|
|
142
|
+
if (ttlSecondsRaw.length > 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) {
|
|
143
|
+
throw new Error("--ttl-seconds must be a positive integer");
|
|
144
|
+
}
|
|
145
|
+
await gitToken({
|
|
146
|
+
project: opts.project,
|
|
147
|
+
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
148
|
+
controllerAccessToken: opts.accessToken ?? opts.controllerAccessToken,
|
|
149
|
+
supabaseAccessToken: opts.supabaseAccessToken,
|
|
150
|
+
supabaseAccessTokenFile: opts.supabaseAccessTokenFile,
|
|
151
|
+
scopes: opts.scope,
|
|
152
|
+
ttlSeconds,
|
|
153
|
+
json: opts.json,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error(kleur.red(String(error)));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
140
161
|
const projectInitCommand = program
|
|
141
162
|
.command("project:init")
|
|
142
163
|
.description("Create an Instafy project and link this folder (.instafy/project.json)")
|
|
@@ -238,39 +259,30 @@ projectListCommand
|
|
|
238
259
|
process.exit(1);
|
|
239
260
|
}
|
|
240
261
|
});
|
|
241
|
-
|
|
242
|
-
const token = opts.accessToken ??
|
|
243
|
-
opts.controllerAccessToken ??
|
|
244
|
-
process.env.INSTAFY_ACCESS_TOKEN ??
|
|
245
|
-
process.env.CONTROLLER_ACCESS_TOKEN ??
|
|
246
|
-
process.env.CONTROLLER_TOKEN;
|
|
247
|
-
if (!token) {
|
|
248
|
-
throw new Error("Access token is required for project:mount.");
|
|
249
|
-
}
|
|
250
|
-
const serverUrl = opts.serverUrl ??
|
|
251
|
-
opts.controllerUrl ??
|
|
252
|
-
process.env.INSTAFY_SERVER_URL ??
|
|
253
|
-
process.env.CONTROLLER_BASE_URL ??
|
|
254
|
-
"http://127.0.0.1:8788";
|
|
255
|
-
const mountDir = await mountWorkspace({
|
|
256
|
-
project: opts.project,
|
|
257
|
-
controllerUrl: serverUrl,
|
|
258
|
-
controllerAccessToken: token,
|
|
259
|
-
mountPath: opts.path,
|
|
260
|
-
sshfsBin: opts.sshfsBin,
|
|
261
|
-
extraOptions: opts.options,
|
|
262
|
-
});
|
|
263
|
-
console.log(kleur.green(`Mounted project files to ${mountDir}`));
|
|
264
|
-
}
|
|
265
|
-
function configureProjectMountCommand(command) {
|
|
262
|
+
function configureApiCommand(command, method) {
|
|
266
263
|
addServerUrlOptions(command);
|
|
267
264
|
addAccessTokenOptions(command, "Instafy access token");
|
|
265
|
+
addServiceTokenOptions(command, "Instafy service token (advanced)");
|
|
268
266
|
command
|
|
269
|
-
.option("--
|
|
270
|
-
.option("--
|
|
271
|
-
.
|
|
267
|
+
.option("--query <key=value>", "Query param (repeatable)", collectStringOption, [])
|
|
268
|
+
.option("--header <header>", "Extra header (repeatable, Key: Value)", collectStringOption, [])
|
|
269
|
+
.option("--json <json>", "JSON body as string")
|
|
270
|
+
.option("--json-file <path>", "JSON body read from file")
|
|
271
|
+
.option("--no-pretty", "Disable JSON pretty-printing")
|
|
272
|
+
.action(async (pathArg, opts) => {
|
|
272
273
|
try {
|
|
273
|
-
await
|
|
274
|
+
await requestControllerApi({
|
|
275
|
+
method,
|
|
276
|
+
path: pathArg,
|
|
277
|
+
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
278
|
+
accessToken: opts.accessToken ?? opts.controllerAccessToken,
|
|
279
|
+
serviceToken: opts.serviceToken ?? opts.controllerToken,
|
|
280
|
+
query: opts.query,
|
|
281
|
+
headers: opts.header,
|
|
282
|
+
json: opts.json,
|
|
283
|
+
jsonFile: opts.jsonFile,
|
|
284
|
+
pretty: opts.pretty,
|
|
285
|
+
});
|
|
274
286
|
}
|
|
275
287
|
catch (error) {
|
|
276
288
|
console.error(kleur.red(String(error)));
|
|
@@ -278,12 +290,26 @@ function configureProjectMountCommand(command) {
|
|
|
278
290
|
}
|
|
279
291
|
});
|
|
280
292
|
}
|
|
281
|
-
const
|
|
282
|
-
.command("
|
|
283
|
-
.description("
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
const apiGetCommand = program
|
|
294
|
+
.command("api:get")
|
|
295
|
+
.description("Perform an authenticated GET request to the controller API")
|
|
296
|
+
.argument("<path>", "API path (or full URL), e.g. /conversations/<id>/messages?limit=50");
|
|
297
|
+
configureApiCommand(apiGetCommand, "GET");
|
|
298
|
+
const apiPostCommand = program
|
|
299
|
+
.command("api:post")
|
|
300
|
+
.description("Perform an authenticated POST request to the controller API")
|
|
301
|
+
.argument("<path>", "API path (or full URL)");
|
|
302
|
+
configureApiCommand(apiPostCommand, "POST");
|
|
303
|
+
const apiPatchCommand = program
|
|
304
|
+
.command("api:patch")
|
|
305
|
+
.description("Perform an authenticated PATCH request to the controller API")
|
|
306
|
+
.argument("<path>", "API path (or full URL)");
|
|
307
|
+
configureApiCommand(apiPatchCommand, "PATCH");
|
|
308
|
+
const apiDeleteCommand = program
|
|
309
|
+
.command("api:delete")
|
|
310
|
+
.description("Perform an authenticated DELETE request to the controller API")
|
|
311
|
+
.argument("<path>", "API path (or full URL)");
|
|
312
|
+
configureApiCommand(apiDeleteCommand, "DELETE");
|
|
287
313
|
export async function runCli(argv = process.argv) {
|
|
288
314
|
if (argv.length <= 2) {
|
|
289
315
|
program.outputHelp();
|
|
@@ -295,6 +321,6 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
|
295
321
|
void runCli(process.argv);
|
|
296
322
|
}
|
|
297
323
|
// Re-export programmatic APIs for embedders (e.g., VS Code extension).
|
|
298
|
-
export { runtimeStart as startRuntime, runtimeStatus as getRuntimeStatus, runtimeStop as stopRuntime, runtimeToken as mintRuntimeToken, mintRuntimeAccessToken, findProjectManifest,
|
|
324
|
+
export { runtimeStart as startRuntime, runtimeStatus as getRuntimeStatus, runtimeStop as stopRuntime, runtimeToken as mintRuntimeToken, mintRuntimeAccessToken, findProjectManifest, } from "./runtime.js";
|
|
299
325
|
export { projectInit, listProjects } from "./project.js";
|
|
300
326
|
export { listOrganizations } from "./org.js";
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import kleur from "kleur";
|
|
@@ -66,137 +66,6 @@ function normalizeToken(value) {
|
|
|
66
66
|
const trimmed = value.trim();
|
|
67
67
|
return trimmed.length > 0 ? trimmed : null;
|
|
68
68
|
}
|
|
69
|
-
function setSharedFsEnv(env, opts) {
|
|
70
|
-
if (!opts)
|
|
71
|
-
return;
|
|
72
|
-
if (opts.host)
|
|
73
|
-
env["SHARED_FS_HOST"] = opts.host;
|
|
74
|
-
if (opts.port)
|
|
75
|
-
env["SHARED_FS_PORT"] = String(opts.port);
|
|
76
|
-
if (opts.user)
|
|
77
|
-
env["SHARED_FS_USER"] = opts.user;
|
|
78
|
-
if (opts.path)
|
|
79
|
-
env["SHARED_FS_PATH"] = opts.path;
|
|
80
|
-
if (opts.keyB64)
|
|
81
|
-
env["SHARED_FS_KEY_B64"] = opts.keyB64;
|
|
82
|
-
if (opts.keyPath)
|
|
83
|
-
env["SHARED_FS_KEY_PATH"] = opts.keyPath;
|
|
84
|
-
if (opts.options)
|
|
85
|
-
env["SHARED_FS_OPTIONS"] = opts.options;
|
|
86
|
-
if (opts.flat)
|
|
87
|
-
env["RUNTIME_SHARED_FS_FLAT"] = "1";
|
|
88
|
-
env["SHARED_FS_PROTOCOL"] = env["SHARED_FS_PROTOCOL"] ?? "sshfs";
|
|
89
|
-
}
|
|
90
|
-
async function fetchWorkspaceMountToken(params) {
|
|
91
|
-
const base = params.controllerUrl.replace(/\/+$/, "");
|
|
92
|
-
const url = `${base}/projects/${encodeURIComponent(params.projectId)}/workspace/mount-token`;
|
|
93
|
-
const res = await fetch(url, {
|
|
94
|
-
headers: {
|
|
95
|
-
authorization: `Bearer ${params.controllerAccessToken}`,
|
|
96
|
-
accept: "application/json",
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
if (!res.ok) {
|
|
100
|
-
const text = await res.text().catch(() => "");
|
|
101
|
-
throw new Error(`Failed to fetch workspace mount token (${res.status} ${res.statusText}): ${text}`);
|
|
102
|
-
}
|
|
103
|
-
const json = (await res.json());
|
|
104
|
-
if (!json?.protocol || !json.host || !json.path) {
|
|
105
|
-
throw new Error("Workspace mount token response missing required fields.");
|
|
106
|
-
}
|
|
107
|
-
return json;
|
|
108
|
-
}
|
|
109
|
-
function prepareTempKey(keyB64) {
|
|
110
|
-
if (!keyB64)
|
|
111
|
-
return null;
|
|
112
|
-
const buf = Buffer.from(keyB64.trim(), "base64");
|
|
113
|
-
const tmpDir = path.join(os.tmpdir(), "instafy-shared-fs-cli");
|
|
114
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
115
|
-
const file = path.join(tmpDir, `sshfs-key-${Date.now()}.pem`);
|
|
116
|
-
writeFileSync(file, buf, { mode: 0o600 });
|
|
117
|
-
return file;
|
|
118
|
-
}
|
|
119
|
-
export async function mountWorkspace(options) {
|
|
120
|
-
const mountDir = path.resolve(options.mountPath);
|
|
121
|
-
mkdirSync(mountDir, { recursive: true });
|
|
122
|
-
const token = await fetchWorkspaceMountToken({
|
|
123
|
-
controllerUrl: options.controllerUrl,
|
|
124
|
-
controllerAccessToken: options.controllerAccessToken,
|
|
125
|
-
projectId: options.project,
|
|
126
|
-
});
|
|
127
|
-
if (token.protocol !== "sshfs") {
|
|
128
|
-
throw new Error(`Unsupported mount protocol: ${token.protocol}`);
|
|
129
|
-
}
|
|
130
|
-
const sshfsBin = options.sshfsBin || process.env.SHARED_FS_BIN || "sshfs";
|
|
131
|
-
assertSshfsAvailable(sshfsBin);
|
|
132
|
-
const keyPath = prepareTempKey(token.key_b64 ?? null);
|
|
133
|
-
const args = [
|
|
134
|
-
`${token.user}@${token.host}:${token.path}`,
|
|
135
|
-
mountDir,
|
|
136
|
-
"-p",
|
|
137
|
-
String(token.port ?? 22),
|
|
138
|
-
"-o",
|
|
139
|
-
"StrictHostKeyChecking=no",
|
|
140
|
-
];
|
|
141
|
-
const mergedOpts = token.options || options.extraOptions;
|
|
142
|
-
if (keyPath) {
|
|
143
|
-
args.push("-o", `IdentityFile=${keyPath}`);
|
|
144
|
-
}
|
|
145
|
-
if (mergedOpts && mergedOpts.trim()) {
|
|
146
|
-
args.push("-o", mergedOpts.trim());
|
|
147
|
-
}
|
|
148
|
-
const result = spawnSync(sshfsBin, args, {
|
|
149
|
-
stdio: "inherit",
|
|
150
|
-
env: process.env,
|
|
151
|
-
});
|
|
152
|
-
if (keyPath) {
|
|
153
|
-
try {
|
|
154
|
-
rmSync(keyPath);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// ignore
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (result.status !== 0) {
|
|
161
|
-
throw new Error(`sshfs mount failed (code ${result.status ?? result.error?.message ?? "unknown"})\n` +
|
|
162
|
-
`Command: ${sshfsBin} ${args.join(" ")}`);
|
|
163
|
-
}
|
|
164
|
-
validateMountHealthy(mountDir);
|
|
165
|
-
return mountDir;
|
|
166
|
-
}
|
|
167
|
-
export async function checkSshfs(binOverride) {
|
|
168
|
-
const bin = binOverride || process.env.SHARED_FS_BIN || "sshfs";
|
|
169
|
-
assertSshfsAvailable(bin);
|
|
170
|
-
}
|
|
171
|
-
function assertSshfsAvailable(bin) {
|
|
172
|
-
const check = spawnSync(bin, ["-V"], { stdio: "ignore" });
|
|
173
|
-
if ((check.status ?? check["code"] ?? 1) === 0) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const platform = process.platform;
|
|
177
|
-
const hints = {
|
|
178
|
-
darwin: "brew install --cask macfuse && brew install gromgit/fuse/sshfs-mac",
|
|
179
|
-
linux: "sudo apt-get update && sudo apt-get install -y sshfs",
|
|
180
|
-
win32: "Install WinFsp and SSHFS-Win: https://github.com/billziss-gh/sshfs-win",
|
|
181
|
-
};
|
|
182
|
-
const hint = hints[platform] ??
|
|
183
|
-
"Install sshfs for your platform and ensure it is on PATH (or pass --sshfs-bin).";
|
|
184
|
-
throw new Error(`sshfs binary '${bin}' not found or not executable. ${hint}`);
|
|
185
|
-
}
|
|
186
|
-
function validateMountHealthy(mountDir) {
|
|
187
|
-
try {
|
|
188
|
-
// Best-effort sanity check to catch "Transport endpoint not connected" right after mount.
|
|
189
|
-
const entries = spawnSync("ls", ["-la", mountDir], { stdio: "ignore" });
|
|
190
|
-
if ((entries.status ?? entries["code"] ?? 1) !== 0) {
|
|
191
|
-
throw new Error("ls failed");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
-
throw new Error(`sshfs mount appears unhealthy at ${mountDir}: ${message}. ` +
|
|
197
|
-
"Ensure the mount host is reachable and sshfs/fuse is enabled.");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
69
|
export function findProjectManifest(startDir) {
|
|
201
70
|
let current = path.resolve(startDir);
|
|
202
71
|
const root = path.parse(current).root;
|
|
@@ -392,28 +261,6 @@ export async function runtimeStart(options) {
|
|
|
392
261
|
if (runtimeAccessToken) {
|
|
393
262
|
env["RUNTIME_ACCESS_TOKEN"] = runtimeAccessToken;
|
|
394
263
|
}
|
|
395
|
-
// Auto-fetch workspace mount token if requested
|
|
396
|
-
const shouldMountProjectFiles = Boolean(options.mountProjectFiles || options.mountControllerFs);
|
|
397
|
-
if (shouldMountProjectFiles && controllerAccessToken && env["CONTROLLER_BASE_URL"]) {
|
|
398
|
-
const token = await fetchWorkspaceMountToken({
|
|
399
|
-
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
400
|
-
controllerAccessToken,
|
|
401
|
-
projectId,
|
|
402
|
-
});
|
|
403
|
-
setSharedFsEnv(env, {
|
|
404
|
-
host: token.host,
|
|
405
|
-
port: token.port,
|
|
406
|
-
user: token.user,
|
|
407
|
-
path: token.path,
|
|
408
|
-
keyB64: token.key_b64 ?? undefined,
|
|
409
|
-
options: token.options ?? undefined,
|
|
410
|
-
flat: true,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
// Shared FS mount options (sshfs) from flags/env
|
|
415
|
-
setSharedFsEnv(env, options.sharedFs);
|
|
416
|
-
}
|
|
417
264
|
if (options.codexBin)
|
|
418
265
|
env["CODEX_BIN"] = options.codexBin;
|
|
419
266
|
if (options.proxyBaseUrl)
|
|
@@ -448,9 +295,14 @@ export async function runtimeStart(options) {
|
|
|
448
295
|
throw new Error("Runtime/origin token is required (--origin-token or ORIGIN_INTERNAL_TOKEN), or provide --runtime-token/--controller-access-token to mint/use one");
|
|
449
296
|
}
|
|
450
297
|
env["ORIGIN_INTERNAL_TOKEN"] = originToken;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
298
|
+
const skipAuthRaw = env["ORIGIN_SKIP_AUTH"]?.trim().toLowerCase();
|
|
299
|
+
const skipAuth = skipAuthRaw === "1" || skipAuthRaw === "true" || skipAuthRaw === "yes";
|
|
300
|
+
const usesTunnel = !env["ORIGIN_ENDPOINT"];
|
|
301
|
+
const usesExplicitEndpoint = Boolean(env["ORIGIN_ENDPOINT"]);
|
|
302
|
+
if (skipAuth && (usesTunnel || usesExplicitEndpoint)) {
|
|
303
|
+
console.warn(kleur.yellow("Warning: ORIGIN_SKIP_AUTH is enabled while the origin may be reachable remotely (tunnel or --origin-endpoint). " +
|
|
304
|
+
"This is unsafe; disable ORIGIN_SKIP_AUTH for any non-local usage."));
|
|
305
|
+
}
|
|
454
306
|
env["RUNTIME_DISPLAY_NAME"] =
|
|
455
307
|
options.displayName ?? env["RUNTIME_DISPLAY_NAME"] ?? "Instafy CLI Runtime";
|
|
456
308
|
const manifestProvider = manifestInfo.manifest?.orgName?.trim() ||
|