@bigbrainforge/setup 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/setup.cjs +1197 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @bigbrainforge/setup
|
|
2
|
+
|
|
3
|
+
Forge workstation setup — paste one license, get a working Forge install.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @bigbrainforge/setup <your-forge-license>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Or run it with no argument and paste the license when prompted (the value is
|
|
10
|
+
never echoed to the terminal):
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npx @bigbrainforge/setup
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
1. Checks the machine has `tar` (needed to unpack the plugin bundle).
|
|
19
|
+
2. Stores the license at `~/.claude/forge/license` (0600).
|
|
20
|
+
3. Discovers and pins your Forge instance (`~/.claude/forge/config.json`).
|
|
21
|
+
4. Seeds `~/.npmrc` with the `@bigbrainforge` registry + auth line.
|
|
22
|
+
5. Downloads and sha256-verifies the Forge plugin bundle, then stages it as a
|
|
23
|
+
local Claude Code marketplace under `~/.claude/forge/plugin-bundle/`.
|
|
24
|
+
6. Registers the marketplace and installs the plugin via the `claude` CLI —
|
|
25
|
+
if `claude` isn't on this machine, prints the two commands to run by hand.
|
|
26
|
+
7. Hands off to the plugin's own bootstrap (server check + issue tracker
|
|
27
|
+
setup).
|
|
28
|
+
|
|
29
|
+
Every step names its own failure tier (machine / credential / instance
|
|
30
|
+
discovery / package plane / distribution plane / Claude Code CLI / plugin
|
|
31
|
+
bootstrap) and the one fix to try next — nothing is a silent skip.
|
|
32
|
+
|
|
33
|
+
### If a step fails after setup has already run once
|
|
34
|
+
|
|
35
|
+
Step 4 seeds `~/.npmrc` so `@bigbrainforge/forge` resolves through your Forge
|
|
36
|
+
instance. Once that has happened, a bare `npx @bigbrainforge/setup` no longer
|
|
37
|
+
resolves this package itself (it lives on the public npm registry, not your
|
|
38
|
+
Forge instance) — if a later step fails and you need to run setup again, use:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
npx --@bigbrainforge:registry=https://registry.npmjs.org @bigbrainforge/setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This one-shot override is what any printed "rerun" fix hint means when it
|
|
45
|
+
names this exact command.
|
|
46
|
+
|
|
47
|
+
## Runtime dependencies
|
|
48
|
+
|
|
49
|
+
None. This package is published PUBLIC to npmjs and runs via `npx` on a
|
|
50
|
+
machine with zero prior Forge configuration; everything it needs beyond
|
|
51
|
+
Node's own builtins is bundled at build time (`scripts/build.mjs`, esbuild).
|
|
52
|
+
It carries no secrets and no sealed Forge IP — anything of value it downloads
|
|
53
|
+
is license-gated server-side.
|
package/dist/setup.cjs
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
4
|
+
try {
|
|
5
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw mod = 0, e;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ../forge-plugin/lib/rename-with-retry.js
|
|
12
|
+
var require_rename_with_retry = __commonJS({
|
|
13
|
+
"../forge-plugin/lib/rename-with-retry.js"(exports2, module2) {
|
|
14
|
+
var fs = require("node:fs");
|
|
15
|
+
var RENAME_RETRY_ERR_CODES_LIST = Object.freeze(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
|
|
16
|
+
var RENAME_RETRY_ERR_CODES_SET = new Set(RENAME_RETRY_ERR_CODES_LIST);
|
|
17
|
+
function isWindows() {
|
|
18
|
+
return process.platform === "win32";
|
|
19
|
+
}
|
|
20
|
+
var RENAME_RETRY_MAX_ATTEMPTS = isWindows() ? 10 : 1;
|
|
21
|
+
var WAIT_SAB = new SharedArrayBuffer(4);
|
|
22
|
+
var WAIT_VIEW = new Int32Array(WAIT_SAB);
|
|
23
|
+
function osSleepSync(delayMs) {
|
|
24
|
+
if (delayMs <= 0) return;
|
|
25
|
+
Atomics.wait(WAIT_VIEW, 0, 0, delayMs);
|
|
26
|
+
}
|
|
27
|
+
function logRetry(attempt, err, src, dst) {
|
|
28
|
+
if (!process.env.FORGE_RENAME_DEBUG) return;
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`[rename-with-retry] attempt ${attempt + 1}/${RENAME_RETRY_MAX_ATTEMPTS} failed (${err.code}); retrying src=${src} dst=${dst}
|
|
31
|
+
`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function renameWithRetry(src, dst, doRename = fs.renameSync) {
|
|
35
|
+
let lastErr;
|
|
36
|
+
for (let attempt = 0; attempt < RENAME_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
doRename(src, dst);
|
|
39
|
+
return;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
lastErr = err;
|
|
42
|
+
if (!isWindows() || !RENAME_RETRY_ERR_CODES_SET.has(err.code)) throw err;
|
|
43
|
+
logRetry(attempt, err, src, dst);
|
|
44
|
+
const delayMs = Math.min(1 << attempt, 100);
|
|
45
|
+
osSleepSync(delayMs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastErr;
|
|
49
|
+
}
|
|
50
|
+
function unlinkWithRetry(target, doUnlink = fs.unlinkSync) {
|
|
51
|
+
let lastErr;
|
|
52
|
+
for (let attempt = 0; attempt < RENAME_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
doUnlink(target);
|
|
55
|
+
return;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
lastErr = err;
|
|
58
|
+
if (!isWindows() || !RENAME_RETRY_ERR_CODES_SET.has(err.code)) throw err;
|
|
59
|
+
if (process.env.FORGE_RENAME_DEBUG) {
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
`[unlink-with-retry] attempt ${attempt + 1}/${RENAME_RETRY_MAX_ATTEMPTS} failed (${err.code}); retrying target=${target}
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const delayMs = Math.min(1 << attempt, 100);
|
|
66
|
+
osSleepSync(delayMs);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastErr;
|
|
70
|
+
}
|
|
71
|
+
module2.exports = {
|
|
72
|
+
renameWithRetry,
|
|
73
|
+
unlinkWithRetry,
|
|
74
|
+
RENAME_RETRY_MAX_ATTEMPTS,
|
|
75
|
+
RENAME_RETRY_ERR_CODES_LIST,
|
|
76
|
+
get RENAME_RETRY_ERR_CODES() {
|
|
77
|
+
return new Set(RENAME_RETRY_ERR_CODES_LIST);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ../forge-plugin/lib/license.js
|
|
84
|
+
var require_license = __commonJS({
|
|
85
|
+
"../forge-plugin/lib/license.js"(exports2, module2) {
|
|
86
|
+
var fs = require("node:fs");
|
|
87
|
+
var os2 = require("node:os");
|
|
88
|
+
var path2 = require("node:path");
|
|
89
|
+
var { renameWithRetry } = require_rename_with_retry();
|
|
90
|
+
var LICENSE_RE2 = /^forge_lic_[0-9A-Za-z]{43}$/;
|
|
91
|
+
function licensePath(io = {}) {
|
|
92
|
+
const env = io.env ?? process.env;
|
|
93
|
+
const homedir = io.homedir ?? os2.homedir();
|
|
94
|
+
return env.FORGE_LICENSE_FILE || path2.join(homedir, ".claude", "forge", "license");
|
|
95
|
+
}
|
|
96
|
+
function readLicenseFileValue(io = {}) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = fs.readFileSync(licensePath(io), "utf8").trim();
|
|
99
|
+
return raw.length > 0 ? raw : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function licenseFilePresent(io = {}) {
|
|
105
|
+
const raw = readLicenseFileValue(io);
|
|
106
|
+
return raw !== null && LICENSE_RE2.test(raw);
|
|
107
|
+
}
|
|
108
|
+
function missingHint() {
|
|
109
|
+
return "no Forge license on this machine \u2014 run /forge:setup license, or paste it in your own terminal with: node <plugin-root>/scripts/secrets.js set license";
|
|
110
|
+
}
|
|
111
|
+
function resolveLicense(io = {}) {
|
|
112
|
+
const env = io.env ?? process.env;
|
|
113
|
+
const warn = io.warn ?? ((line) => process.stderr.write(`${line}
|
|
114
|
+
`));
|
|
115
|
+
const fromEnv = env.FORGE_LICENSE;
|
|
116
|
+
const envHit = typeof fromEnv === "string" && fromEnv.length > 0;
|
|
117
|
+
const fromFile = readLicenseFileValue(io);
|
|
118
|
+
if (envHit) {
|
|
119
|
+
if (!LICENSE_RE2.test(fromEnv)) {
|
|
120
|
+
return {
|
|
121
|
+
value: null,
|
|
122
|
+
reason: "malformed",
|
|
123
|
+
hint: "FORGE_LICENSE is set but is not a forge_lic_ key \u2014 check the value your CI/container injects."
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let drift = false;
|
|
127
|
+
if (fromFile !== null && fromFile !== fromEnv) {
|
|
128
|
+
drift = true;
|
|
129
|
+
warn("FORGE_LICENSE differs between env and the license file; using env FORGE_LICENSE.");
|
|
130
|
+
}
|
|
131
|
+
return { value: fromEnv, source: "env FORGE_LICENSE", drift };
|
|
132
|
+
}
|
|
133
|
+
if (fromFile === null) {
|
|
134
|
+
return { value: null, reason: "missing", hint: missingHint() };
|
|
135
|
+
}
|
|
136
|
+
if (!LICENSE_RE2.test(fromFile)) {
|
|
137
|
+
return {
|
|
138
|
+
value: null,
|
|
139
|
+
reason: "malformed",
|
|
140
|
+
hint: `the license file exists but does not hold a forge_lic_ key \u2014 re-run /forge:setup license (${licensePath(io)})`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { value: fromFile, source: "license file", drift: false };
|
|
144
|
+
}
|
|
145
|
+
function writeLicense2(value, io = {}) {
|
|
146
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
147
|
+
if (!LICENSE_RE2.test(trimmed)) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: "not a valid license \u2014 expected forge_lic_ followed by 43 base62 characters"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const target = licensePath(io);
|
|
154
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
155
|
+
try {
|
|
156
|
+
fs.mkdirSync(path2.dirname(target), { recursive: true });
|
|
157
|
+
fs.writeFileSync(tmp, `${trimmed}
|
|
158
|
+
`, { encoding: "utf8", mode: 384 });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
try {
|
|
161
|
+
fs.unlinkSync(tmp);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
return { ok: false, error: `could not write the license file: ${err.code || "write failed"}` };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
renameWithRetry(tmp, target);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
try {
|
|
170
|
+
fs.unlinkSync(tmp);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return { ok: false, error: `could not write the license file: ${err.code || "rename failed"}` };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
fs.chmodSync(target, 384);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, path: target };
|
|
180
|
+
}
|
|
181
|
+
module2.exports = {
|
|
182
|
+
resolveLicense,
|
|
183
|
+
writeLicense: writeLicense2,
|
|
184
|
+
licensePath,
|
|
185
|
+
licenseFilePresent,
|
|
186
|
+
readLicenseFileValue,
|
|
187
|
+
LICENSE_RE: LICENSE_RE2
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ../forge-plugin/lib/discover-instance.js
|
|
193
|
+
var require_discover_instance = __commonJS({
|
|
194
|
+
"../forge-plugin/lib/discover-instance.js"(exports2, module2) {
|
|
195
|
+
var fs = require("node:fs");
|
|
196
|
+
var os2 = require("node:os");
|
|
197
|
+
var path2 = require("node:path");
|
|
198
|
+
var { resolveLicense } = require_license();
|
|
199
|
+
var DEFAULT_LICENSING_URL2 = "https://licensing.bigbrainforge.com";
|
|
200
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
201
|
+
function isSecureUrl2(value) {
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = new URL(value);
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (parsed.protocol === "https:") return true;
|
|
209
|
+
return parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
210
|
+
}
|
|
211
|
+
function checkLicensingUrlTransportSecurity(base) {
|
|
212
|
+
if (isSecureUrl2(base)) return null;
|
|
213
|
+
let scheme;
|
|
214
|
+
try {
|
|
215
|
+
const parsed = new URL(base);
|
|
216
|
+
scheme = `${parsed.protocol}//${parsed.hostname}`;
|
|
217
|
+
} catch {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
reason: "insecure_licensing_url",
|
|
221
|
+
hint: `FORGE_LICENSING_URL is not a valid URL: ${base}`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: "insecure_licensing_url",
|
|
227
|
+
hint: `FORGE_LICENSING_URL must use https:// (got ${scheme}). Plain http:// is allowed only for loopback (127.0.0.1, localhost, ::1) \u2014 refusing to send the license credential in cleartext.`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function pluginConfigPath(io = {}) {
|
|
231
|
+
const env = io.env ?? process.env;
|
|
232
|
+
const homedir = io.homedir ?? os2.homedir();
|
|
233
|
+
return env.FORGE_PLUGIN_CONFIG || path2.join(homedir, ".claude", "forge", "config.json");
|
|
234
|
+
}
|
|
235
|
+
function readPinnedUrl(io) {
|
|
236
|
+
try {
|
|
237
|
+
const config = JSON.parse(fs.readFileSync(pluginConfigPath(io), "utf8"));
|
|
238
|
+
return typeof config.mcp_url === "string" && config.mcp_url.length > 0 ? config.mcp_url : null;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function pinInstanceUrl(io, instanceUrl) {
|
|
244
|
+
const target = pluginConfigPath(io);
|
|
245
|
+
let config = {};
|
|
246
|
+
if (fs.existsSync(target)) {
|
|
247
|
+
try {
|
|
248
|
+
config = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
249
|
+
} catch {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: `existing ${target} is not valid JSON \u2014 fix or remove it, then retry`
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
config.mcp_url = instanceUrl.replace(/\/$/, "");
|
|
257
|
+
fs.mkdirSync(path2.dirname(target), { recursive: true });
|
|
258
|
+
fs.writeFileSync(target, `${JSON.stringify(config, null, 2)}
|
|
259
|
+
`, "utf8");
|
|
260
|
+
return { ok: true, mcp_url: config.mcp_url };
|
|
261
|
+
}
|
|
262
|
+
function sameHost(a, b) {
|
|
263
|
+
try {
|
|
264
|
+
return new URL(a).host === new URL(b).host;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function fetchDiscovery(doFetch, base, licenseValue) {
|
|
270
|
+
let response;
|
|
271
|
+
try {
|
|
272
|
+
response = await doFetch(`${base}/license/discover`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ license: licenseValue }),
|
|
276
|
+
signal: AbortSignal.timeout(15e3)
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
reason: "discovery_unreachable",
|
|
282
|
+
hint: `licensing service unreachable \u2014 check network egress to ${base}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
let body = {};
|
|
286
|
+
try {
|
|
287
|
+
body = await response.json();
|
|
288
|
+
} catch {
|
|
289
|
+
body = {};
|
|
290
|
+
}
|
|
291
|
+
if (response.status === 401) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: "license_invalid",
|
|
295
|
+
hint: body.error || "unknown or revoked license \u2014 contact your org admin"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (!response.ok || typeof body.instance_url !== "string" || body.instance_url.length === 0) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
reason: "discovery_unreachable",
|
|
302
|
+
hint: `discovery failed (HTTP ${response.status}) at ${base}/license/discover`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
instance_url: body.instance_url.replace(/\/$/, ""),
|
|
308
|
+
deployment_model: body.deployment_model ?? null
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function checkPinConflict(pinned, discovered, acceptNewInstance) {
|
|
312
|
+
if (!pinned || sameHost(pinned, discovered) || acceptNewInstance === true) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
reason: "pin_conflict",
|
|
318
|
+
pinned_url: pinned,
|
|
319
|
+
discovered_url: discovered,
|
|
320
|
+
hint: `discovery returned ${discovered} but this machine is pinned to ${pinned}. Refusing to release the license to a new host. If the change is expected (your org migrated instances), re-run with --accept-new-instance.`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async function discoverInstance2(io = {}) {
|
|
324
|
+
const env = io.env ?? process.env;
|
|
325
|
+
const doFetch = io.fetchImpl ?? fetch;
|
|
326
|
+
const license = resolveLicense(io);
|
|
327
|
+
if (license.value === null) {
|
|
328
|
+
return { ok: false, reason: "no_license", hint: license.hint };
|
|
329
|
+
}
|
|
330
|
+
const pinned = readPinnedUrl(io);
|
|
331
|
+
if (pinned && io.forceDiscovery !== true) {
|
|
332
|
+
return { ok: true, instance_url: pinned, deployment_model: null, source: "pin" };
|
|
333
|
+
}
|
|
334
|
+
const base = (env.FORGE_LICENSING_URL || DEFAULT_LICENSING_URL2).replace(/\/$/, "");
|
|
335
|
+
const transportFailure = checkLicensingUrlTransportSecurity(base);
|
|
336
|
+
if (transportFailure) {
|
|
337
|
+
return transportFailure;
|
|
338
|
+
}
|
|
339
|
+
const discovery = await fetchDiscovery(doFetch, base, license.value);
|
|
340
|
+
if (!discovery.ok) {
|
|
341
|
+
return discovery;
|
|
342
|
+
}
|
|
343
|
+
const { instance_url: discovered, deployment_model: deploymentModel } = discovery;
|
|
344
|
+
if (!isSecureUrl2(discovered)) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
reason: "insecure_instance_url",
|
|
348
|
+
hint: `discovery returned an insecure instance_url (${discovered}) \u2014 refusing to pin it. The instance must be served over https:// (plain http:// is allowed only for loopback), so the license is never exchanged over a cleartext connection.`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const conflict = checkPinConflict(pinned, discovered, io.acceptNewInstance);
|
|
352
|
+
if (conflict) {
|
|
353
|
+
return conflict;
|
|
354
|
+
}
|
|
355
|
+
if (!pinned || !sameHost(pinned, discovered)) {
|
|
356
|
+
const pinResult = pinInstanceUrl(io, discovered);
|
|
357
|
+
if (!pinResult.ok) {
|
|
358
|
+
return { ok: false, reason: "discovery_unreachable", hint: pinResult.error };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
instance_url: discovered,
|
|
364
|
+
deployment_model: deploymentModel,
|
|
365
|
+
source: "discovery"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
module2.exports = { discoverInstance: discoverInstance2, DEFAULT_LICENSING_URL: DEFAULT_LICENSING_URL2, isSecureUrl: isSecureUrl2 };
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ../forge-plugin/lib/npmrc-license.js
|
|
373
|
+
var require_npmrc_license = __commonJS({
|
|
374
|
+
"../forge-plugin/lib/npmrc-license.js"(exports2, module2) {
|
|
375
|
+
var fs = require("node:fs");
|
|
376
|
+
var path2 = require("node:path");
|
|
377
|
+
var DIST_HOST = "registry.bigbrainforge.com";
|
|
378
|
+
var LEGACY_HOST = "npm.pkg.github.com";
|
|
379
|
+
var SCOPE_LINE = `@bigbrainforge:registry=https://${DIST_HOST}/npm/`;
|
|
380
|
+
function authLine(license) {
|
|
381
|
+
return `//${DIST_HOST}/npm/:_authToken=${license}`;
|
|
382
|
+
}
|
|
383
|
+
var DIST_AUTH_KEY = `//${DIST_HOST}/npm/`;
|
|
384
|
+
var LEGACY_AUTH_KEY = `//${LEGACY_HOST}/`;
|
|
385
|
+
var OWNED_SCOPE_TARGETS = /* @__PURE__ */ new Set([
|
|
386
|
+
`https://${DIST_HOST}/npm/`,
|
|
387
|
+
`https://${DIST_HOST}/npm`,
|
|
388
|
+
`https://${LEGACY_HOST}`,
|
|
389
|
+
`https://${LEGACY_HOST}/`
|
|
390
|
+
]);
|
|
391
|
+
function isCommentLine(trimmed) {
|
|
392
|
+
return trimmed.startsWith("#") || trimmed.startsWith(";");
|
|
393
|
+
}
|
|
394
|
+
function ownsLine(trimmed) {
|
|
395
|
+
if (isCommentLine(trimmed)) return false;
|
|
396
|
+
const scopeMatch = trimmed.match(/^@bigbrainforge:registry=(.*)$/);
|
|
397
|
+
if (scopeMatch) {
|
|
398
|
+
return OWNED_SCOPE_TARGETS.has(scopeMatch[1].trim());
|
|
399
|
+
}
|
|
400
|
+
const m = trimmed.match(/^(.*):_authToken=/);
|
|
401
|
+
if (!m) return false;
|
|
402
|
+
const registry = m[1] ?? "";
|
|
403
|
+
return registry === DIST_AUTH_KEY || registry === LEGACY_AUTH_KEY;
|
|
404
|
+
}
|
|
405
|
+
function foreignScopeTargetOf(trimmed) {
|
|
406
|
+
if (trimmed.length === 0 || isCommentLine(trimmed)) return null;
|
|
407
|
+
const m = trimmed.match(/^@bigbrainforge:registry=(.*)$/);
|
|
408
|
+
return m ? m[1].trim() : null;
|
|
409
|
+
}
|
|
410
|
+
function partitionLines(src) {
|
|
411
|
+
const kept = [];
|
|
412
|
+
let replacedLegacy = false;
|
|
413
|
+
let foreignScopeTarget = null;
|
|
414
|
+
for (const raw of src.split("\n")) {
|
|
415
|
+
const trimmed = raw.trim();
|
|
416
|
+
if (trimmed.length > 0 && ownsLine(trimmed)) {
|
|
417
|
+
if (trimmed.includes(LEGACY_HOST) || trimmed.includes("FORGE_PACKAGE_TOKEN")) {
|
|
418
|
+
replacedLegacy = true;
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const foreignTarget = foreignScopeTargetOf(trimmed);
|
|
423
|
+
if (foreignTarget !== null) foreignScopeTarget = foreignTarget;
|
|
424
|
+
kept.push(raw);
|
|
425
|
+
}
|
|
426
|
+
while (kept.length > 0 && kept[kept.length - 1] === "") kept.pop();
|
|
427
|
+
return { kept, replacedLegacy, foreignScopeTarget };
|
|
428
|
+
}
|
|
429
|
+
function applyNpmrcLicense(content, license) {
|
|
430
|
+
const src = String(content ?? "");
|
|
431
|
+
const { kept, replacedLegacy, foreignScopeTarget } = partitionLines(src);
|
|
432
|
+
const foreignScope = foreignScopeTarget !== null;
|
|
433
|
+
const linesToAdd = foreignScope ? [] : [SCOPE_LINE, authLine(license)];
|
|
434
|
+
const body = [...kept, ...linesToAdd, ""].join("\n");
|
|
435
|
+
const changed = body !== src;
|
|
436
|
+
const status = foreignScope ? "declined" : changed ? "seeded" : "already";
|
|
437
|
+
return { content: body, changed, replacedLegacy, foreignScope, foreignScopeTarget, status };
|
|
438
|
+
}
|
|
439
|
+
function writeNpmrcLicense2(license, io) {
|
|
440
|
+
const target = path2.join(io.homedir, ".npmrc");
|
|
441
|
+
let existing = "";
|
|
442
|
+
try {
|
|
443
|
+
existing = fs.readFileSync(target, "utf8");
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
const result = applyNpmrcLicense(existing, license);
|
|
447
|
+
if (result.changed) {
|
|
448
|
+
fs.writeFileSync(target, result.content, { encoding: "utf8", mode: 384 });
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
fs.chmodSync(target, 384);
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
return { ...result, path: target };
|
|
455
|
+
}
|
|
456
|
+
module2.exports = { applyNpmrcLicense, writeNpmrcLicense: writeNpmrcLicense2, SCOPE_LINE, authLine, DIST_HOST };
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ../forge-plugin/lib/plugin-bundle.js
|
|
461
|
+
var require_plugin_bundle = __commonJS({
|
|
462
|
+
"../forge-plugin/lib/plugin-bundle.js"(exports2, module2) {
|
|
463
|
+
var { createHash } = require("node:crypto");
|
|
464
|
+
var { execFileSync } = require("node:child_process");
|
|
465
|
+
var fs = require("node:fs");
|
|
466
|
+
var os2 = require("node:os");
|
|
467
|
+
var path2 = require("node:path");
|
|
468
|
+
var { renameWithRetry } = require_rename_with_retry();
|
|
469
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
470
|
+
function assertSafeVersion2(version) {
|
|
471
|
+
if (typeof version !== "string" || !VERSION_RE.test(version)) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`refusing to use manifest version ${JSON.stringify(version)} \u2014 it must be a plain semver-shaped string (digits.digits.digits) since it is used as a filesystem path segment. This looks like a corrupted or tampered distribution-plane response.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function bundleRoot2(homeDir = os2.homedir()) {
|
|
478
|
+
return path2.join(homeDir, ".claude", "forge", "plugin-bundle");
|
|
479
|
+
}
|
|
480
|
+
function stagePath() {
|
|
481
|
+
return path2.resolve(__dirname, "..", "scripts", "stage.js");
|
|
482
|
+
}
|
|
483
|
+
function envFor(io) {
|
|
484
|
+
return io.homedir ? { ...process.env, HOME: io.homedir, USERPROFILE: io.homedir } : process.env;
|
|
485
|
+
}
|
|
486
|
+
function fetchTgzViaStage(manifest, io = {}) {
|
|
487
|
+
const tmp = fs.mkdtempSync(path2.join(os2.tmpdir(), "forge-bundle-"));
|
|
488
|
+
const out = path2.join(tmp, `${manifest.version}.tgz`);
|
|
489
|
+
try {
|
|
490
|
+
execFileSync(
|
|
491
|
+
process.execPath,
|
|
492
|
+
[
|
|
493
|
+
stagePath(),
|
|
494
|
+
"dist-fetch",
|
|
495
|
+
"--dist-path",
|
|
496
|
+
`/dist/plugin/${manifest.version}.tgz`,
|
|
497
|
+
"--output",
|
|
498
|
+
out
|
|
499
|
+
],
|
|
500
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, env: envFor(io) }
|
|
501
|
+
);
|
|
502
|
+
return fs.readFileSync(out);
|
|
503
|
+
} finally {
|
|
504
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function checkManifest(io = {}) {
|
|
508
|
+
const exec = io.execFileSync ?? execFileSync;
|
|
509
|
+
const tmp = fs.mkdtempSync(path2.join(os2.tmpdir(), "forge-manifest-"));
|
|
510
|
+
const out = path2.join(tmp, "manifest.json");
|
|
511
|
+
try {
|
|
512
|
+
exec(
|
|
513
|
+
process.execPath,
|
|
514
|
+
[stagePath(), "dist-fetch", "--dist-path", "/dist/plugin/manifest", "--output", out],
|
|
515
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 15e3, env: envFor(io) }
|
|
516
|
+
);
|
|
517
|
+
const manifest = JSON.parse(fs.readFileSync(out, "utf8"));
|
|
518
|
+
if (!manifest?.version || !VERSION_RE.test(manifest.version) || !/^[0-9a-f]{64}$/.test(manifest?.sha256 ?? "")) {
|
|
519
|
+
return { ok: false, revoked: false, reason: "manifest missing version/sha256" };
|
|
520
|
+
}
|
|
521
|
+
return { ok: true, manifest };
|
|
522
|
+
} catch (err) {
|
|
523
|
+
const msg = String(err?.stderr ?? err?.message ?? err);
|
|
524
|
+
const revoked = /revoked or unknown|rejected this license/i.test(msg);
|
|
525
|
+
return { ok: false, revoked, reason: revoked ? "LICENSE_INVALID" : msg.split("\n")[0] };
|
|
526
|
+
} finally {
|
|
527
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function writeMarketplaceJson(root, version) {
|
|
531
|
+
const dir = path2.join(root, ".claude-plugin");
|
|
532
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
533
|
+
const doc = {
|
|
534
|
+
name: "forge",
|
|
535
|
+
owner: { name: "Big Brain Forge" },
|
|
536
|
+
plugins: [
|
|
537
|
+
{
|
|
538
|
+
name: "forge",
|
|
539
|
+
source: `./${version}`,
|
|
540
|
+
description: "Forge for Claude Code \u2014 agentic orchestration for high-compliance environments."
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
fs.writeFileSync(path2.join(dir, "marketplace.json"), `${JSON.stringify(doc, null, 2)}
|
|
545
|
+
`);
|
|
546
|
+
}
|
|
547
|
+
function readInstalledVersion(root) {
|
|
548
|
+
let source;
|
|
549
|
+
try {
|
|
550
|
+
const mp = JSON.parse(
|
|
551
|
+
fs.readFileSync(path2.join(root, ".claude-plugin", "marketplace.json"), "utf8")
|
|
552
|
+
);
|
|
553
|
+
source = mp?.plugins?.[0]?.source;
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (typeof source !== "string" || source.length === 0) return null;
|
|
558
|
+
const version = source.replace(/^\.\//, "");
|
|
559
|
+
try {
|
|
560
|
+
const pluginJson = JSON.parse(
|
|
561
|
+
fs.readFileSync(path2.join(root, version, ".claude-plugin", "plugin.json"), "utf8")
|
|
562
|
+
);
|
|
563
|
+
if (typeof pluginJson?.version === "string" && pluginJson.version.length > 0) {
|
|
564
|
+
return pluginJson.version;
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
return version;
|
|
569
|
+
}
|
|
570
|
+
function readLiveVersionStamp(root) {
|
|
571
|
+
try {
|
|
572
|
+
return fs.readFileSync(path2.join(path2.dirname(root), "VERSION"), "utf8").trim() || null;
|
|
573
|
+
} catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function pruneOldVersions(root, keep = 2) {
|
|
578
|
+
const semver = /^\d+\.\d+\.\d+$/;
|
|
579
|
+
let entries;
|
|
580
|
+
try {
|
|
581
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
582
|
+
} catch {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const dirs = entries.filter((d) => d.isDirectory() && semver.test(d.name)).map((d) => d.name).sort((a, b) => {
|
|
586
|
+
const pa = a.split(".").map(Number);
|
|
587
|
+
const pb = b.split(".").map(Number);
|
|
588
|
+
return pa[0] - pb[0] || pa[1] - pb[1] || pa[2] - pb[2];
|
|
589
|
+
});
|
|
590
|
+
const liveVersion = readLiveVersionStamp(root);
|
|
591
|
+
const stale = dirs.slice(0, Math.max(0, dirs.length - keep)).filter((name) => name !== liveVersion);
|
|
592
|
+
for (const name of stale) {
|
|
593
|
+
fs.rmSync(path2.join(root, name), { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function stageBundle2(manifest, io = {}) {
|
|
597
|
+
assertSafeVersion2(manifest?.version);
|
|
598
|
+
const homedir = io.homedir ?? os2.homedir();
|
|
599
|
+
const root = bundleRoot2(homedir);
|
|
600
|
+
const target = path2.join(root, manifest.version);
|
|
601
|
+
if (fs.existsSync(path2.join(target, ".claude-plugin", "plugin.json"))) {
|
|
602
|
+
writeMarketplaceJson(root, manifest.version);
|
|
603
|
+
return { staged: false, already: true, root, target };
|
|
604
|
+
}
|
|
605
|
+
const bytes = io.fetchTgz ? await io.fetchTgz(manifest) : fetchTgzViaStage(manifest, io);
|
|
606
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
607
|
+
if (digest !== manifest.sha256) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`bundle sha256 mismatch for ${manifest.version} \u2014 refusing to unpack (expected the manifest digest; the download may be corrupt or tampered).`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const tmp = fs.mkdtempSync(path2.join(os2.tmpdir(), `forge-bundle-stage-${manifest.version}-`));
|
|
613
|
+
try {
|
|
614
|
+
const tgz = path2.join(tmp, "bundle.tgz");
|
|
615
|
+
fs.writeFileSync(tgz, bytes);
|
|
616
|
+
const dest = path2.join(tmp, "unpacked");
|
|
617
|
+
fs.mkdirSync(dest);
|
|
618
|
+
try {
|
|
619
|
+
execFileSync("tar", ["-xzf", tgz, "-C", dest, "--strip-components=1"], {
|
|
620
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
621
|
+
});
|
|
622
|
+
} catch (err) {
|
|
623
|
+
if (err?.code === "ENOENT") {
|
|
624
|
+
throw new Error("system `tar` was not found on this machine \u2014 cannot unpack the bundle");
|
|
625
|
+
}
|
|
626
|
+
throw new Error(`tar failed to unpack the bundle: ${err?.stderr ?? err?.message ?? err}`);
|
|
627
|
+
}
|
|
628
|
+
if (!fs.existsSync(path2.join(dest, ".claude-plugin", "plugin.json"))) {
|
|
629
|
+
throw new Error("unpacked bundle has no .claude-plugin/plugin.json \u2014 not a plugin bundle");
|
|
630
|
+
}
|
|
631
|
+
fs.mkdirSync(root, { recursive: true });
|
|
632
|
+
try {
|
|
633
|
+
renameWithRetry(dest, target, io.doRename);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (err?.code === "EXDEV") {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`could not publish the staged bundle: the OS temp directory and ${root} are on different filesystems (EXDEV) \u2014 set TMPDIR (or TMP/TEMP on Windows) to a path on the same volume as ~/.claude/forge and retry. A copy-based fallback was deliberately not used here because it cannot preserve this operation's atomicity guarantee.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
642
|
+
} finally {
|
|
643
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
644
|
+
}
|
|
645
|
+
writeMarketplaceJson(root, manifest.version);
|
|
646
|
+
pruneOldVersions(root);
|
|
647
|
+
return { staged: true, already: false, root, target };
|
|
648
|
+
}
|
|
649
|
+
module2.exports = {
|
|
650
|
+
bundleRoot: bundleRoot2,
|
|
651
|
+
checkManifest,
|
|
652
|
+
readInstalledVersion,
|
|
653
|
+
stageBundle: stageBundle2,
|
|
654
|
+
writeMarketplaceJson,
|
|
655
|
+
pruneOldVersions,
|
|
656
|
+
assertSafeVersion: assertSafeVersion2,
|
|
657
|
+
VERSION_RE
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ../forge-plugin/lib/windows-shell-quote.js
|
|
663
|
+
var require_windows_shell_quote = __commonJS({
|
|
664
|
+
"../forge-plugin/lib/windows-shell-quote.js"(exports2, module2) {
|
|
665
|
+
function quoteForWindowsShell(arg) {
|
|
666
|
+
if (typeof arg !== "string") {
|
|
667
|
+
throw new TypeError("quoteForWindowsShell expects a string");
|
|
668
|
+
}
|
|
669
|
+
if (arg.length === 0) {
|
|
670
|
+
return '""';
|
|
671
|
+
}
|
|
672
|
+
if (!/[\s"&|<>^%]/.test(arg)) {
|
|
673
|
+
return arg;
|
|
674
|
+
}
|
|
675
|
+
const escaped = arg.replace(/(\\*)"/g, '$1$1""').replace(/(\\+)$/, "$1$1");
|
|
676
|
+
return `"${escaped}"`;
|
|
677
|
+
}
|
|
678
|
+
function buildWindowsCmdArgs(claudeCmd, argv) {
|
|
679
|
+
const commandLine = [claudeCmd, ...argv].map(quoteForWindowsShell).join(" ");
|
|
680
|
+
return ["/d", "/s", "/c", `"${commandLine}"`];
|
|
681
|
+
}
|
|
682
|
+
module2.exports = { quoteForWindowsShell, buildWindowsCmdArgs };
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ../forge-plugin/lib/exec-cli.js
|
|
687
|
+
var require_exec_cli = __commonJS({
|
|
688
|
+
"../forge-plugin/lib/exec-cli.js"(exports2, module2) {
|
|
689
|
+
var { execFileSync, spawnSync: spawnSync2 } = require("node:child_process");
|
|
690
|
+
var { existsSync } = require("node:fs");
|
|
691
|
+
var { join } = require("node:path");
|
|
692
|
+
var { buildWindowsCmdArgs } = require_windows_shell_quote();
|
|
693
|
+
var DIRECT_EXTS = [".exe", ".com"];
|
|
694
|
+
var SHIM_EXTS = [".cmd", ".bat"];
|
|
695
|
+
function classifyWindowsCommand(cmd, env = process.env) {
|
|
696
|
+
const lower = cmd.toLowerCase();
|
|
697
|
+
const extMatch = /\.[^.\\/]+$/.exec(lower);
|
|
698
|
+
if (extMatch) {
|
|
699
|
+
if (DIRECT_EXTS.includes(extMatch[0])) return { kind: "direct", path: cmd };
|
|
700
|
+
if (SHIM_EXTS.includes(extMatch[0])) return { kind: "shim" };
|
|
701
|
+
return { kind: "shim" };
|
|
702
|
+
}
|
|
703
|
+
if (/[\\/]/.test(cmd)) {
|
|
704
|
+
for (const ext of pathExts(env)) {
|
|
705
|
+
if (existsSync(cmd + ext)) {
|
|
706
|
+
return DIRECT_EXTS.includes(ext) ? { kind: "direct", path: cmd + ext } : { kind: "shim" };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return { kind: "unknown" };
|
|
710
|
+
}
|
|
711
|
+
const dirs = (env.PATH || env.Path || "").split(";").filter(Boolean);
|
|
712
|
+
for (const dir of dirs) {
|
|
713
|
+
for (const ext of pathExts(env)) {
|
|
714
|
+
const candidate = join(dir, cmd + ext);
|
|
715
|
+
if (existsSync(candidate)) {
|
|
716
|
+
return DIRECT_EXTS.includes(ext) ? { kind: "direct", path: candidate } : { kind: "shim" };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { kind: "unknown" };
|
|
721
|
+
}
|
|
722
|
+
function pathExts(env) {
|
|
723
|
+
return (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((e) => e.toLowerCase());
|
|
724
|
+
}
|
|
725
|
+
function resolveSpawn2(platform, cmd, argv = [], opts = {}, env = process.env) {
|
|
726
|
+
if (platform !== "win32") {
|
|
727
|
+
return { cmd, argv, opts };
|
|
728
|
+
}
|
|
729
|
+
const target = classifyWindowsCommand(cmd, env);
|
|
730
|
+
if (target.kind === "direct") {
|
|
731
|
+
return {
|
|
732
|
+
cmd: target.path,
|
|
733
|
+
argv,
|
|
734
|
+
opts: { ...opts, shell: false }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
cmd: "cmd.exe",
|
|
739
|
+
argv: buildWindowsCmdArgs(cmd, argv),
|
|
740
|
+
opts: {
|
|
741
|
+
...opts,
|
|
742
|
+
shell: false,
|
|
743
|
+
windowsVerbatimArguments: true
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function execCli(cmd, argv = [], opts = {}) {
|
|
748
|
+
const r = resolveSpawn2(process.platform, cmd, argv, opts);
|
|
749
|
+
return execFileSync(r.cmd, r.argv, r.opts);
|
|
750
|
+
}
|
|
751
|
+
function spawnCli(cmd, argv = [], opts = {}) {
|
|
752
|
+
const r = resolveSpawn2(process.platform, cmd, argv, opts);
|
|
753
|
+
return spawnSync2(r.cmd, r.argv, r.opts);
|
|
754
|
+
}
|
|
755
|
+
var winPath = require("node:path").win32;
|
|
756
|
+
var WINDOWS_PATH_DELIMITER = ";";
|
|
757
|
+
var WINDOWS_BASH_SHIM_DIRS = /* @__PURE__ */ new Set(["system32", "windowsapps"]);
|
|
758
|
+
function isWindowsBashShim(candidate) {
|
|
759
|
+
if (!candidate) return false;
|
|
760
|
+
const leaf = winPath.basename(winPath.dirname(String(candidate)));
|
|
761
|
+
return WINDOWS_BASH_SHIM_DIRS.has(leaf.toLowerCase());
|
|
762
|
+
}
|
|
763
|
+
function bashFromPath(env, exists) {
|
|
764
|
+
for (const raw of (env.PATH || env.Path || "").split(WINDOWS_PATH_DELIMITER)) {
|
|
765
|
+
const dir = raw.trim().replace(/^"|"$/g, "");
|
|
766
|
+
if (!dir) continue;
|
|
767
|
+
const candidate = winPath.join(dir, "bash.exe");
|
|
768
|
+
if (isWindowsBashShim(candidate)) continue;
|
|
769
|
+
if (exists(candidate)) return candidate;
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
var MSYS_BIN_RELATIVE = [["bin"], ["usr", "bin"]];
|
|
774
|
+
function bashFromKnownInstall(env, exists) {
|
|
775
|
+
const roots = [
|
|
776
|
+
env.GIT_INSTALL_ROOT,
|
|
777
|
+
env.ProgramFiles && winPath.join(env.ProgramFiles, "Git"),
|
|
778
|
+
env.ProgramW6432 && winPath.join(env.ProgramW6432, "Git"),
|
|
779
|
+
env["ProgramFiles(x86)"] && winPath.join(env["ProgramFiles(x86)"], "Git"),
|
|
780
|
+
env.LOCALAPPDATA && winPath.join(env.LOCALAPPDATA, "Programs", "Git"),
|
|
781
|
+
"C:\\msys64"
|
|
782
|
+
].filter(Boolean);
|
|
783
|
+
for (const root of roots) {
|
|
784
|
+
for (const rel of MSYS_BIN_RELATIVE) {
|
|
785
|
+
const candidate = winPath.join(root, ...rel, "bash.exe");
|
|
786
|
+
if (exists(candidate)) return candidate;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
function bashFromGitExecPath(exists, runGitExecPath) {
|
|
792
|
+
const out = runGitExecPath();
|
|
793
|
+
if (!out) return null;
|
|
794
|
+
let dir = winPath.normalize(String(out).trim().split("/").join("\\"));
|
|
795
|
+
for (let i = 0; i < 8; i++) {
|
|
796
|
+
for (const rel of MSYS_BIN_RELATIVE) {
|
|
797
|
+
const candidate = winPath.join(dir, ...rel, "bash.exe");
|
|
798
|
+
if (exists(candidate)) return candidate;
|
|
799
|
+
}
|
|
800
|
+
const parent = winPath.dirname(dir);
|
|
801
|
+
if (parent === dir) break;
|
|
802
|
+
dir = parent;
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
function defaultRunGitExecPath() {
|
|
807
|
+
try {
|
|
808
|
+
const r = spawnCli("git", ["--exec-path"], { encoding: "utf8" });
|
|
809
|
+
return r.status === 0 ? r.stdout : null;
|
|
810
|
+
} catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function resolveBash({
|
|
815
|
+
platform = process.platform,
|
|
816
|
+
env = process.env,
|
|
817
|
+
exists = existsSync,
|
|
818
|
+
runGitExecPath = defaultRunGitExecPath
|
|
819
|
+
} = {}) {
|
|
820
|
+
if (platform !== "win32") return { command: "bash", source: "posix", shimOnly: false };
|
|
821
|
+
const fromPath = bashFromPath(env, exists);
|
|
822
|
+
if (fromPath) return { command: fromPath, source: "path", shimOnly: false };
|
|
823
|
+
const fromInstall = bashFromKnownInstall(env, exists);
|
|
824
|
+
if (fromInstall) return { command: fromInstall, source: "git-install", shimOnly: false };
|
|
825
|
+
const fromGit = bashFromGitExecPath(exists, runGitExecPath);
|
|
826
|
+
if (fromGit) return { command: fromGit, source: "git-exec-path", shimOnly: false };
|
|
827
|
+
const shimOnly = (env.PATH || env.Path || "").split(WINDOWS_PATH_DELIMITER).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean).some((dir) => {
|
|
828
|
+
const candidate = winPath.join(dir, "bash.exe");
|
|
829
|
+
return isWindowsBashShim(candidate) && exists(candidate);
|
|
830
|
+
});
|
|
831
|
+
return { command: null, source: "none", shimOnly };
|
|
832
|
+
}
|
|
833
|
+
function msysBinDirsFrom(bashPath) {
|
|
834
|
+
const bashDir = winPath.dirname(bashPath);
|
|
835
|
+
const dirs = [bashDir];
|
|
836
|
+
let root = bashDir;
|
|
837
|
+
for (let level = 0; level < MSYS_BIN_RELATIVE.length; level++) {
|
|
838
|
+
const parent = winPath.dirname(root);
|
|
839
|
+
if (parent === root) break;
|
|
840
|
+
root = parent;
|
|
841
|
+
for (const rel of MSYS_BIN_RELATIVE) {
|
|
842
|
+
const dir = winPath.join(root, ...rel);
|
|
843
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return dirs;
|
|
847
|
+
}
|
|
848
|
+
function resolveMsysToolDirs({
|
|
849
|
+
tools = ["bash"],
|
|
850
|
+
platform = process.platform,
|
|
851
|
+
env = process.env,
|
|
852
|
+
exists = existsSync,
|
|
853
|
+
runGitExecPath = defaultRunGitExecPath
|
|
854
|
+
} = {}) {
|
|
855
|
+
if (platform !== "win32") {
|
|
856
|
+
return {
|
|
857
|
+
platform,
|
|
858
|
+
bash: "bash",
|
|
859
|
+
source: "posix",
|
|
860
|
+
shimOnly: false,
|
|
861
|
+
dirs: [],
|
|
862
|
+
resolved: {},
|
|
863
|
+
missing: []
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const bash = resolveBash({ platform, env, exists, runGitExecPath });
|
|
867
|
+
if (!bash.command) {
|
|
868
|
+
return {
|
|
869
|
+
platform,
|
|
870
|
+
bash: null,
|
|
871
|
+
source: bash.source,
|
|
872
|
+
shimOnly: bash.shimOnly,
|
|
873
|
+
dirs: [],
|
|
874
|
+
resolved: {},
|
|
875
|
+
missing: [...tools]
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const searchDirs = msysBinDirsFrom(bash.command);
|
|
879
|
+
const dirs = [];
|
|
880
|
+
const resolved = {};
|
|
881
|
+
const missing = [];
|
|
882
|
+
for (const tool of tools) {
|
|
883
|
+
const exe = /\.exe$/i.test(tool) ? tool : `${tool}.exe`;
|
|
884
|
+
const dir = searchDirs.find((d) => exists(winPath.join(d, exe)));
|
|
885
|
+
if (!dir) {
|
|
886
|
+
missing.push(tool);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
resolved[tool] = winPath.join(dir, exe);
|
|
890
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
platform,
|
|
894
|
+
bash: bash.command,
|
|
895
|
+
source: bash.source,
|
|
896
|
+
shimOnly: false,
|
|
897
|
+
dirs,
|
|
898
|
+
resolved,
|
|
899
|
+
missing
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
module2.exports = {
|
|
903
|
+
execCli,
|
|
904
|
+
spawnCli,
|
|
905
|
+
resolveSpawn: resolveSpawn2,
|
|
906
|
+
classifyWindowsCommand,
|
|
907
|
+
resolveBash,
|
|
908
|
+
isWindowsBashShim,
|
|
909
|
+
resolveMsysToolDirs
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// src/setup.js
|
|
915
|
+
var os = require("node:os");
|
|
916
|
+
var path = require("node:path");
|
|
917
|
+
var { spawnSync } = require("node:child_process");
|
|
918
|
+
var { writeLicense } = require_license();
|
|
919
|
+
var { discoverInstance, isSecureUrl } = require_discover_instance();
|
|
920
|
+
var { writeNpmrcLicense } = require_npmrc_license();
|
|
921
|
+
var {
|
|
922
|
+
stageBundle,
|
|
923
|
+
bundleRoot,
|
|
924
|
+
assertSafeVersion
|
|
925
|
+
} = require_plugin_bundle();
|
|
926
|
+
var { resolveSpawn } = require_exec_cli();
|
|
927
|
+
var LICENSE_RE = /^forge_lic_[0-9A-Za-z]{43}$/;
|
|
928
|
+
var DEFAULT_LICENSING_URL = "https://licensing.bigbrainforge.com";
|
|
929
|
+
var DEFAULT_DIST_URL = "https://registry.bigbrainforge.com";
|
|
930
|
+
var RERUN_SETUP_CMD = "npx --@bigbrainforge:registry=https://registry.npmjs.org @bigbrainforge/setup";
|
|
931
|
+
function runShim(io, cmd, args) {
|
|
932
|
+
const platform = io.platform ?? process.platform;
|
|
933
|
+
const env = io.env ?? process.env;
|
|
934
|
+
const resolved = resolveSpawn(platform, cmd, args, {}, env);
|
|
935
|
+
return io.run(resolved.cmd, resolved.argv, resolved.opts);
|
|
936
|
+
}
|
|
937
|
+
function errMessage(err) {
|
|
938
|
+
return err && err.message ? err.message : String(err);
|
|
939
|
+
}
|
|
940
|
+
function defaultIo() {
|
|
941
|
+
return {
|
|
942
|
+
homedir: os.homedir(),
|
|
943
|
+
env: process.env,
|
|
944
|
+
print: (l) => process.stdout.write(`${l}
|
|
945
|
+
`),
|
|
946
|
+
promptHidden: async (q) => {
|
|
947
|
+
const readline = require("node:readline");
|
|
948
|
+
return new Promise((res) => {
|
|
949
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
950
|
+
const orig = rl._writeToOutput.bind(rl);
|
|
951
|
+
rl._writeToOutput = (s) => s.includes("\n") || s === q ? orig(s) : orig("");
|
|
952
|
+
process.stdout.write(q);
|
|
953
|
+
rl.question("", (a) => {
|
|
954
|
+
rl.close();
|
|
955
|
+
process.stdout.write("\n");
|
|
956
|
+
res(a.trim());
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
},
|
|
960
|
+
fetch: (url, opts) => globalThis.fetch(url, opts),
|
|
961
|
+
run: (cmd, args = [], opts = {}) => {
|
|
962
|
+
const { stdio, ...rest } = opts;
|
|
963
|
+
const r = spawnSync(cmd, args, {
|
|
964
|
+
encoding: "utf8",
|
|
965
|
+
stdio: stdio ?? ["ignore", "pipe", "pipe"],
|
|
966
|
+
...rest
|
|
967
|
+
});
|
|
968
|
+
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", error: r.error };
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function fail(io, tier, message, fix) {
|
|
973
|
+
io.print("");
|
|
974
|
+
io.print(`\u2717 [${tier}] ${message}`);
|
|
975
|
+
if (fix) io.print(` fix: ${fix}`);
|
|
976
|
+
return 1;
|
|
977
|
+
}
|
|
978
|
+
async function runSetup(argv, io = defaultIo()) {
|
|
979
|
+
const args = argv.filter((a) => !a.startsWith("--"));
|
|
980
|
+
const acceptNewInstance = argv.includes("--accept-new-instance");
|
|
981
|
+
const tarProbe = io.run("tar", ["--version"]);
|
|
982
|
+
if (tarProbe.error || tarProbe.status !== 0) {
|
|
983
|
+
return fail(
|
|
984
|
+
io,
|
|
985
|
+
"machine",
|
|
986
|
+
"the tar tool is missing (needed to unpack the plugin bundle)",
|
|
987
|
+
"macOS/Linux: install tar via your package manager; Windows 10+ ships it as tar.exe"
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
let license = args[0] ?? "";
|
|
991
|
+
if (!license) license = await io.promptHidden("Paste your Forge license: ");
|
|
992
|
+
if (!LICENSE_RE.test(license)) {
|
|
993
|
+
return fail(
|
|
994
|
+
io,
|
|
995
|
+
"credential",
|
|
996
|
+
"that license is not the right shape (expected forge_lic_ + 43 characters) \u2014 it may be malformed or truncated",
|
|
997
|
+
"re-paste it exactly as delivered, or contact your Forge administrator"
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const wrote = writeLicense(license, { homedir: io.homedir, env: io.env });
|
|
1001
|
+
if (!wrote.ok) {
|
|
1002
|
+
return fail(
|
|
1003
|
+
io,
|
|
1004
|
+
"credential",
|
|
1005
|
+
`could not store the license: ${wrote.error}`,
|
|
1006
|
+
`fix the named problem (usually permissions on ~/.claude/forge), then rerun: ${RERUN_SETUP_CMD}`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
io.print(`\u2713 license stored (${wrote.path})`);
|
|
1010
|
+
const licensingUrl = io.env.FORGE_LICENSING_URL || DEFAULT_LICENSING_URL;
|
|
1011
|
+
const discoveryFetch = async (url, opts) => {
|
|
1012
|
+
const res = await io.fetch(url, opts);
|
|
1013
|
+
return { ...res, ok: res.ok ?? (res.status >= 200 && res.status < 300) };
|
|
1014
|
+
};
|
|
1015
|
+
const discovered = await discoverInstance({
|
|
1016
|
+
homedir: io.homedir,
|
|
1017
|
+
env: io.env,
|
|
1018
|
+
fetchImpl: discoveryFetch,
|
|
1019
|
+
forceDiscovery: true,
|
|
1020
|
+
acceptNewInstance
|
|
1021
|
+
});
|
|
1022
|
+
if (!discovered.ok) {
|
|
1023
|
+
return fail(
|
|
1024
|
+
io,
|
|
1025
|
+
"instance discovery",
|
|
1026
|
+
discovered.hint,
|
|
1027
|
+
discovered.reason === "pin_conflict" ? (
|
|
1028
|
+
// Non-blocking review residual (round 2): a pin conflict only ever
|
|
1029
|
+
// fires on a machine that was ALREADY pinned by a prior successful
|
|
1030
|
+
// run — i.e. exactly the state most likely to have already seeded
|
|
1031
|
+
// ~/.npmrc, so this rerun needs the same registry-override form
|
|
1032
|
+
// as the other three "rerun setup" hints (C5).
|
|
1033
|
+
`rerun with --accept-new-instance only if your org really moved instances: ${RERUN_SETUP_CMD} --accept-new-instance`
|
|
1034
|
+
) : `check network egress to ${licensingUrl} (it must be on your allowlist)`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
io.print(`\u2713 instance pinned: ${discovered.instance_url} (${discovered.deployment_model})`);
|
|
1038
|
+
let npmrc;
|
|
1039
|
+
try {
|
|
1040
|
+
npmrc = writeNpmrcLicense(license, { homedir: io.homedir, env: io.env });
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
return fail(
|
|
1043
|
+
io,
|
|
1044
|
+
"package plane",
|
|
1045
|
+
`could not seed npm auth: ${errMessage(err)}`,
|
|
1046
|
+
`check that ~/.npmrc is a writable file (not a directory) and this account has permission, then rerun: ${RERUN_SETUP_CMD}`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
if (npmrc.status === "declined") {
|
|
1050
|
+
const legacyNote = npmrc.replacedLegacy ? " (a legacy npm.pkg.github.com credential line found alongside it was removed)" : "";
|
|
1051
|
+
io.print(
|
|
1052
|
+
npmrc.foreignScopeTarget === "" ? `! npm NOT seeded \u2014 ~/.npmrc has a malformed @bigbrainforge:registry= line (no value). Fix or remove that line, then rerun setup; otherwise no action is needed.${legacyNote}` : `! npm NOT seeded \u2014 @bigbrainforge:registry already points at ${npmrc.foreignScopeTarget} (an enterprise proxy or other custom config). If that isn't your organization's proxy, remove that line from ~/.npmrc and rerun setup; otherwise no action is needed.${legacyNote}`
|
|
1053
|
+
);
|
|
1054
|
+
} else {
|
|
1055
|
+
io.print(
|
|
1056
|
+
npmrc.status === "seeded" ? `\u2713 npm seeded (${npmrc.path})${npmrc.replacedLegacy ? " \u2014 legacy GitHub Packages lines replaced" : ""}` : `\u2713 npm already seeded (${npmrc.path}) \u2014 no change needed`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
const distUrl = io.env.FORGE_DIST_URL || DEFAULT_DIST_URL;
|
|
1060
|
+
if (!isSecureUrl(distUrl)) {
|
|
1061
|
+
return fail(
|
|
1062
|
+
io,
|
|
1063
|
+
"distribution plane",
|
|
1064
|
+
`FORGE_DIST_URL must use https:// (got ${distUrl}) \u2014 refusing to send the license to an insecure host`,
|
|
1065
|
+
"unset FORGE_DIST_URL to use the default, or point it at an https:// endpoint (plain http:// is allowed only for loopback: 127.0.0.1, localhost, ::1)"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const auth = { headers: { authorization: `Bearer ${license}` } };
|
|
1069
|
+
let mRes;
|
|
1070
|
+
try {
|
|
1071
|
+
mRes = await io.fetch(`${distUrl}/dist/plugin/manifest`, auth);
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
return fail(
|
|
1074
|
+
io,
|
|
1075
|
+
"distribution plane",
|
|
1076
|
+
`could not reach ${distUrl}: ${errMessage(err)}`,
|
|
1077
|
+
`check network egress to ${distUrl} (it must be on your allowlist)`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (mRes.status === 401) {
|
|
1081
|
+
return fail(
|
|
1082
|
+
io,
|
|
1083
|
+
"distribution plane",
|
|
1084
|
+
"this license was reported revoked or unknown",
|
|
1085
|
+
"contact your Forge administrator for a current license"
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
if (mRes.status !== 200) {
|
|
1089
|
+
return fail(
|
|
1090
|
+
io,
|
|
1091
|
+
"distribution plane",
|
|
1092
|
+
`manifest fetch failed (HTTP ${mRes.status})`,
|
|
1093
|
+
`check network egress to ${distUrl}`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
let manifest;
|
|
1097
|
+
try {
|
|
1098
|
+
manifest = await mRes.json();
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
return fail(
|
|
1101
|
+
io,
|
|
1102
|
+
"distribution plane",
|
|
1103
|
+
`manifest response was not valid JSON: ${errMessage(err)}`,
|
|
1104
|
+
`check network egress to ${distUrl} \u2014 a captive portal or proxy may have intercepted the response`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
assertSafeVersion(manifest?.version);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
return fail(
|
|
1111
|
+
io,
|
|
1112
|
+
"distribution plane",
|
|
1113
|
+
`manifest response was malformed: ${errMessage(err)}`,
|
|
1114
|
+
`the download may be corrupted or tampered \u2014 rerun to retry: ${RERUN_SETUP_CMD} (if it persists, contact your Forge administrator)`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
let tRes;
|
|
1118
|
+
try {
|
|
1119
|
+
tRes = await io.fetch(`${distUrl}/dist/plugin/${manifest.version}.tgz`, auth);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
return fail(
|
|
1122
|
+
io,
|
|
1123
|
+
"distribution plane",
|
|
1124
|
+
`could not reach ${distUrl}: ${errMessage(err)}`,
|
|
1125
|
+
`check network egress to ${distUrl} (it must be on your allowlist)`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
if (tRes.status !== 200) {
|
|
1129
|
+
return fail(
|
|
1130
|
+
io,
|
|
1131
|
+
"distribution plane",
|
|
1132
|
+
`bundle download failed (HTTP ${tRes.status})`,
|
|
1133
|
+
`check network egress to ${distUrl}`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
let staged;
|
|
1137
|
+
try {
|
|
1138
|
+
const bytes = Buffer.from(await tRes.arrayBuffer());
|
|
1139
|
+
staged = await stageBundle(
|
|
1140
|
+
{ version: manifest.version, sha256: manifest.sha256, url: manifest.url },
|
|
1141
|
+
{ homedir: io.homedir, fetchTgz: async () => bytes }
|
|
1142
|
+
);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
return fail(
|
|
1145
|
+
io,
|
|
1146
|
+
"distribution plane",
|
|
1147
|
+
`could not stage the plugin bundle: ${errMessage(err)}`,
|
|
1148
|
+
`the download may be corrupted or tampered \u2014 rerun to retry: ${RERUN_SETUP_CMD} (if it persists, contact your Forge administrator)`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
io.print(`\u2713 plugin bundle ${manifest.version} verified (sha256) and staged at ${staged.target}`);
|
|
1152
|
+
const root = bundleRoot(io.homedir);
|
|
1153
|
+
const addRes = runShim(io, "claude", ["plugin", "marketplace", "add", root]);
|
|
1154
|
+
const installRes = addRes.status === 0 ? runShim(io, "claude", ["plugin", "install", "forge@forge"]) : addRes;
|
|
1155
|
+
const alreadyRegistered = addRes.status === 0 && installRes.status !== 0 && /already installed|already exists/i.test(
|
|
1156
|
+
`${installRes.stdout || ""}
|
|
1157
|
+
${installRes.stderr || ""}`
|
|
1158
|
+
);
|
|
1159
|
+
if (addRes.status === 0 && installRes.status === 0) {
|
|
1160
|
+
io.print("\u2713 plugin installed from the local Forge marketplace");
|
|
1161
|
+
} else if (alreadyRegistered) {
|
|
1162
|
+
io.print("\u2713 plugin already installed from the local Forge marketplace \u2014 nothing to do");
|
|
1163
|
+
} else {
|
|
1164
|
+
io.print("");
|
|
1165
|
+
io.print("! Claude Code CLI not reachable from this shell \u2014 run these two commands yourself:");
|
|
1166
|
+
io.print("");
|
|
1167
|
+
io.print(` claude plugin marketplace add "${root}"`);
|
|
1168
|
+
io.print(" claude plugin install forge@forge");
|
|
1169
|
+
io.print("");
|
|
1170
|
+
}
|
|
1171
|
+
const bundledBootstrap = path.join(staged.target, "scripts", "bootstrap.js");
|
|
1172
|
+
io.print("Handing off to the Forge plugin bootstrap (server check + issue tracker)\u2026");
|
|
1173
|
+
const hand = io.run(process.execPath, [bundledBootstrap], { stdio: "inherit" });
|
|
1174
|
+
if (hand.status !== 0) {
|
|
1175
|
+
return fail(
|
|
1176
|
+
io,
|
|
1177
|
+
"plugin bootstrap",
|
|
1178
|
+
"the plugin bootstrap did not finish",
|
|
1179
|
+
`rerun it yourself: node "${bundledBootstrap}"`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
io.print("");
|
|
1183
|
+
io.print("Done. Open Claude Code \u2014 Forge is ready.");
|
|
1184
|
+
return 0;
|
|
1185
|
+
}
|
|
1186
|
+
module.exports = { runSetup, LICENSE_RE, defaultIo, RERUN_SETUP_CMD };
|
|
1187
|
+
if (require.main === module) {
|
|
1188
|
+
runSetup(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
|
|
1189
|
+
process.stderr.write(`
|
|
1190
|
+
\u2717 [unexpected] ${errMessage(err)}
|
|
1191
|
+
`);
|
|
1192
|
+
process.stderr.write(
|
|
1193
|
+
" fix: this is unexpected \u2014 please report it (including this message) to your Forge administrator\n"
|
|
1194
|
+
);
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigbrainforge/setup",
|
|
3
|
+
"version": "3.18.0",
|
|
4
|
+
"description": "Forge workstation setup — paste one license, get a working Forge install.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://github.com/bigbrainforge/forge",
|
|
7
|
+
"bin": { "forge-setup": "dist/setup.cjs" },
|
|
8
|
+
"files": ["dist/", "README.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/build.mjs",
|
|
11
|
+
"test": "node --test \"test/*.test.js\""
|
|
12
|
+
},
|
|
13
|
+
"engines": { "node": ">=24.18.1", "pnpm": ">=11.0.0" },
|
|
14
|
+
"publishConfig": { "registry": "https://registry.npmjs.org", "access": "public" },
|
|
15
|
+
"devDependencies": { "esbuild": "0.28.1" }
|
|
16
|
+
}
|