@botconnector/bccli 0.4.0-beta.8 → 0.4.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.
Files changed (59) hide show
  1. package/README.en.md +242 -0
  2. package/README.md +95 -3
  3. package/dist/acp-ATIL4JOZ.js +182 -0
  4. package/dist/args-J5ONVGIF.js +13 -0
  5. package/dist/chunk-45SDLMTV.js +341 -0
  6. package/dist/chunk-6M5X2KZE.js +8 -0
  7. package/dist/chunk-7F3IWBPQ.js +191 -0
  8. package/dist/{chunk-CLN4Z5E3.js → chunk-BLOYFIAD.js} +6 -3
  9. package/dist/chunk-BNA74HPN.js +145 -0
  10. package/dist/{chunk-DX7CZTBB.js → chunk-BQ2IGPBR.js} +14 -9
  11. package/dist/chunk-JL5ZCSP2.js +76 -0
  12. package/dist/chunk-KEMYIGCP.js +235 -0
  13. package/dist/{chunk-BFZXRKW5.js → chunk-L7QI6JN2.js} +24 -2
  14. package/dist/chunk-LACUWBWS.js +361 -0
  15. package/dist/chunk-M7CALSP3.js +733 -0
  16. package/dist/{chunk-ELML4KVQ.js → chunk-O7M3TGIW.js} +34 -2
  17. package/dist/chunk-PSO7R5AD.js +10 -0
  18. package/dist/chunk-PUN4HMAG.js +940 -0
  19. package/dist/{chunk-SJH3YLW4.js → chunk-QDQU6VY2.js} +60 -6
  20. package/dist/chunk-QZKLZELZ.js +64 -0
  21. package/dist/{chunk-7S6JXQCH.js → chunk-SPRNLS7K.js} +2 -1
  22. package/dist/chunk-TLPFQIS2.js +231 -0
  23. package/dist/chunk-TVALKHFB.js +2143 -0
  24. package/dist/chunk-UZWJQF67.js +500 -0
  25. package/dist/chunk-XBO5GACQ.js +56 -0
  26. package/dist/cli.js +162 -1383
  27. package/dist/completion-HPPFDAPB.js +125 -0
  28. package/dist/{config-YUNKPCNB.js → config-COPNMMV6.js} +5 -2
  29. package/dist/config-RZ5JONMX.js +30 -0
  30. package/dist/ghBotCli-IBCZIWT5.js +677 -0
  31. package/dist/integrationCli-P75R2VSC.js +685 -0
  32. package/dist/login-SYIQP46J.js +16 -0
  33. package/dist/{mcpCli-DZWQPTOS.js → mcpCli-QUV5SD64.js} +42 -9
  34. package/dist/modelsCli-TRRB5LX4.js +36 -0
  35. package/dist/npmRunner-LU43GM3W.js +46 -0
  36. package/dist/oauthLogin-6FWQABJ7.js +210 -0
  37. package/dist/pluginsCli-GNW4TPVV.js +284 -0
  38. package/dist/prCli-5IMV3VNQ.js +78 -0
  39. package/dist/providerCli-FAOZHOKW.js +15 -0
  40. package/dist/sdk.js +5490 -0
  41. package/dist/serve-6QLBJUYW.js +178 -0
  42. package/dist/serve-JG7RLIUD.js +515 -0
  43. package/dist/session-KBAQ4BZ5.js +12 -0
  44. package/dist/setup-FJAWDWV4.js +24 -0
  45. package/dist/statsCli-CF3R7ATL.js +353 -0
  46. package/dist/transferCli-TK2GSVRK.js +71 -0
  47. package/dist/ui-B3CJ6TGE.js +2365 -0
  48. package/dist/update-K5IQ2MYR.js +145 -0
  49. package/package.json +25 -4
  50. package/dist/args-FLTRM57K.js +0 -12
  51. package/dist/chunk-2MGINW4O.js +0 -114
  52. package/dist/chunk-IM3KMPVZ.js +0 -52
  53. package/dist/chunk-KV27KTOX.js +0 -254
  54. package/dist/chunk-U7GK73CK.js +0 -341
  55. package/dist/chunk-Z7PM3LTC.js +0 -93
  56. package/dist/login-JXFORO3J.js +0 -9
  57. package/dist/providerCli-Q7LRM2NP.js +0 -12
  58. package/dist/session-7UB77CEG.js +0 -9
  59. package/dist/ui-6GTQUJOE.js +0 -1415
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ defaultLoginDeps,
4
+ readLine,
5
+ readSecret,
6
+ runLogin
7
+ } from "./chunk-BNA74HPN.js";
8
+ import "./chunk-7F3IWBPQ.js";
9
+ import "./chunk-LACUWBWS.js";
10
+ import "./chunk-UZWJQF67.js";
11
+ export {
12
+ defaultLoginDeps,
13
+ readLine,
14
+ readSecret,
15
+ runLogin
16
+ };
@@ -2,21 +2,32 @@
2
2
  import {
3
3
  CATALOG,
4
4
  fillTemplate
5
- } from "./chunk-DX7CZTBB.js";
5
+ } from "./chunk-BQ2IGPBR.js";
6
+ import {
7
+ authorizeMcpServer,
8
+ clearTokens
9
+ } from "./chunk-7F3IWBPQ.js";
6
10
  import {
7
11
  addGlobalServer,
8
12
  globalMcpPath,
9
13
  readMcpFile,
10
14
  removeGlobalServer
11
- } from "./chunk-BFZXRKW5.js";
15
+ } from "./chunk-L7QI6JN2.js";
12
16
  import {
13
17
  bccliHome
14
- } from "./chunk-2MGINW4O.js";
18
+ } from "./chunk-LACUWBWS.js";
19
+ import {
20
+ t
21
+ } from "./chunk-UZWJQF67.js";
15
22
 
16
23
  // src/mcpCli.ts
17
24
  async function runMcpCommand(args, deps) {
18
25
  const home = bccliHome(deps.env);
19
26
  const [action = "list", name] = args.subArgs;
27
+ if (action === "serve") {
28
+ const { runMcpServe } = await import("./serve-6QLBJUYW.js");
29
+ return runMcpServe(args, deps);
30
+ }
20
31
  const installed = readMcpFile(globalMcpPath(home));
21
32
  if (action === "list") {
22
33
  for (const entry2 of CATALOG) deps.out(`${installed[entry2.name] ? "\u2713" : "\u25CB"} ${entry2.name.padEnd(12)} ${entry2.description}`);
@@ -24,26 +35,48 @@ async function runMcpCommand(args, deps) {
24
35
  return 0;
25
36
  }
26
37
  if (!name) {
27
- deps.err(`Pemakaian: bccli mcp ${action} <nama>`);
38
+ deps.err(t("Usage: bccli mcp {action} <name>", { action }));
28
39
  return 1;
29
40
  }
30
41
  if (action === "remove") {
31
42
  removeGlobalServer(home, name);
32
- deps.out(`${name} dihapus.`);
43
+ deps.out(t("{name} removed.", { name }));
33
44
  return 0;
34
45
  }
46
+ if (action === "auth" || action === "logout") {
47
+ const config = installed[name];
48
+ if (config?.type !== "http") {
49
+ deps.err(
50
+ t("{name} is not an installed HTTP MCP server. Add it first with: bccli mcp add {name} --url <url>", { name })
51
+ );
52
+ return 1;
53
+ }
54
+ if (action === "logout") {
55
+ const removed = clearTokens(home, config.url);
56
+ deps.out(removed ? t("{name} logged out.", { name }) : t("{name} has no stored OAuth tokens.", { name }));
57
+ return 0;
58
+ }
59
+ try {
60
+ await authorizeMcpServer({ name, config }, { home });
61
+ deps.out(t("{name} authorized. Active in the next bccli session.", { name }));
62
+ return 0;
63
+ } catch (error) {
64
+ deps.err(t("OAuth login failed for {name}: {reason}", { name, reason: error.message }));
65
+ return 1;
66
+ }
67
+ }
35
68
  if (action !== "add") {
36
- deps.err(`Aksi tidak dikenal: ${action}. Pakai list, add, atau remove.`);
69
+ deps.err(t("Unknown action: {action}. Use list, add, remove, auth, logout, or serve.", { action }));
37
70
  return 1;
38
71
  }
39
72
  if (args.url) {
40
73
  addGlobalServer(home, name, { type: "http", url: args.url });
41
- deps.out(`${name} terpasang (${args.url}).`);
74
+ deps.out(t("{name} installed ({url}).", { name, url: args.url }));
42
75
  return 0;
43
76
  }
44
77
  const entry = CATALOG.find((c) => c.name === name);
45
78
  if (!entry) {
46
- deps.err(`${name} tidak ada di katalog. Untuk server remote lain pakai: bccli mcp add ${name} --url <url>`);
79
+ deps.err(t("{name} is not in the catalog. For another remote server use: bccli mcp add {name} --url <url>", { name }));
47
80
  return 1;
48
81
  }
49
82
  const values = Object.fromEntries(args.values.map((v) => [v.slice(0, v.indexOf("=")), v.slice(v.indexOf("=") + 1)]));
@@ -57,7 +90,7 @@ async function runMcpCommand(args, deps) {
57
90
  deps.err(`${error.message}${hint ? `. Contoh: bccli mcp add ${name} ${hint}` : ""}`);
58
91
  return 1;
59
92
  }
60
- deps.out(`${name} terpasang. Aktif di sesi bccli berikutnya.`);
93
+ deps.out(t("{name} installed. Active in the next bccli session.", { name }));
61
94
  return 0;
62
95
  }
63
96
  export {
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ listAllModels
4
+ } from "./chunk-XBO5GACQ.js";
5
+ import "./chunk-SPRNLS7K.js";
6
+ import "./chunk-PUN4HMAG.js";
7
+ import "./chunk-BLOYFIAD.js";
8
+ import {
9
+ loadConfig,
10
+ resolveModel
11
+ } from "./chunk-LACUWBWS.js";
12
+ import {
13
+ t
14
+ } from "./chunk-UZWJQF67.js";
15
+
16
+ // src/modelsCli.ts
17
+ async function runModelsCommand(args, deps) {
18
+ const config = loadConfig(deps.cwd, deps.env);
19
+ const arg = (args.subArgs[0] ?? "").trim();
20
+ const providerId = arg ? arg.split("/")[0] : resolveModel(config, args.model ?? config.model, deps.env).providerId;
21
+ if (!Object.hasOwn(config.providers, providerId)) {
22
+ deps.err(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
23
+ return 1;
24
+ }
25
+ const groups = await listAllModels(config, deps.env, { fetch: deps.fetch, only: providerId });
26
+ const group = groups.find((g) => g.providerId === providerId);
27
+ if (!group || group.error) {
28
+ deps.err(`${providerId}: ${group?.error ?? t("could not be reached")}`);
29
+ return 1;
30
+ }
31
+ deps.out(group.models.map((m) => `${providerId}/${m}`).join("\n"));
32
+ return 0;
33
+ }
34
+ export {
35
+ runModelsCommand
36
+ };
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/npmRunner.ts
4
+ import { spawnSync } from "child_process";
5
+ import { existsSync } from "fs";
6
+ import { delimiter, posix, win32 } from "path";
7
+ function findNpmCli(platform, env, exists, execPath) {
8
+ const path = platform === "win32" ? win32 : posix;
9
+ const sibling = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
10
+ if (exists(sibling)) return sibling;
11
+ const sep = platform === "win32" ? ";" : delimiter;
12
+ for (const dir of (env.PATH ?? "").split(sep)) {
13
+ if (!dir) continue;
14
+ const shim = path.join(dir, platform === "win32" ? "npm.cmd" : "npm");
15
+ if (!exists(shim)) continue;
16
+ const cli = path.join(dir, "node_modules", "npm", "bin", "npm-cli.js");
17
+ if (exists(cli)) return cli;
18
+ }
19
+ return null;
20
+ }
21
+ var text = (v) => typeof v === "string" ? v : v ? v.toString() : "";
22
+ function toResult(r) {
23
+ return {
24
+ code: r.status ?? 1,
25
+ stderr: text(r.stderr) || (r.error ? r.error.message : ""),
26
+ stdout: text(r.stdout)
27
+ };
28
+ }
29
+ function runNpmSync(prefix, argv, opts = {}) {
30
+ const platform = opts.platform ?? process.platform;
31
+ const spawn = opts.spawn ?? spawnSync;
32
+ const env = opts.env ?? process.env;
33
+ const exists = opts.exists ?? existsSync;
34
+ const execPath = opts.execPath ?? process.execPath;
35
+ if (platform !== "win32") {
36
+ return toResult(spawn("npm", ["--prefix", prefix, ...argv], { encoding: "utf8", env }));
37
+ }
38
+ const cli = findNpmCli(platform, env, exists, execPath);
39
+ if (!cli) {
40
+ return { code: 1, stderr: "npm CLI not found (looked beside the node executable and on PATH)" };
41
+ }
42
+ return toResult(spawn(execPath, [cli, "--prefix", prefix, ...argv], { encoding: "utf8", env }));
43
+ }
44
+ export {
45
+ runNpmSync
46
+ };
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ t
4
+ } from "./chunk-UZWJQF67.js";
5
+
6
+ // src/oauthLogin.ts
7
+ import { createHash, randomBytes } from "crypto";
8
+ var OPENROUTER_AUTH_URL = "https://openrouter.ai/auth";
9
+ var OPENROUTER_EXCHANGE_URL = "https://openrouter.ai/api/v1/auth/keys";
10
+ var OPENROUTER_DASHBOARD_URL = "https://openrouter.ai/settings/keys";
11
+ var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
12
+ var KEY_LABEL = "bccli";
13
+ var EXCHANGE_TIMEOUT_MS = 15e3;
14
+ var DETAIL_MAX = 200;
15
+ function pkcePair() {
16
+ const verifier = randomBytes(32).toString("base64url");
17
+ const challenge = createHash("sha256").update(verifier, "ascii").digest("base64url");
18
+ return { verifier, challenge };
19
+ }
20
+ function buildAuthUrl(challenge) {
21
+ const url = new URL(OPENROUTER_AUTH_URL);
22
+ url.searchParams.set("code_challenge", challenge);
23
+ url.searchParams.set("code_challenge_method", "S256");
24
+ url.searchParams.set("key_label", KEY_LABEL);
25
+ return url.href;
26
+ }
27
+ function redact(text, secrets) {
28
+ let out = text;
29
+ for (const s of secrets) {
30
+ if (!s) continue;
31
+ out = out.split(s).join("***");
32
+ }
33
+ return out;
34
+ }
35
+ function classifyExchange403(detail) {
36
+ return detail.toLowerCase().includes("expired") ? "expired" : "rejected";
37
+ }
38
+ function extractWhitelistDetail(body) {
39
+ if (typeof body !== "object" || body === null) return "";
40
+ const rec = body;
41
+ return typeof rec.error === "string" ? rec.error : typeof rec.message === "string" ? rec.message : "";
42
+ }
43
+ function safeDetail(raw, secrets) {
44
+ return redact(raw, secrets).slice(0, DETAIL_MAX);
45
+ }
46
+ async function exchangeAuthCode(code, verifier, deps) {
47
+ const secrets = [code, verifier];
48
+ const timeoutSignal = AbortSignal.timeout(deps.timeoutMs ?? EXCHANGE_TIMEOUT_MS);
49
+ const signal = deps.signal ? AbortSignal.any([timeoutSignal, deps.signal]) : timeoutSignal;
50
+ let parsed;
51
+ let status = 0;
52
+ try {
53
+ const res = await deps.fetch(OPENROUTER_EXCHANGE_URL, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: "S256" }),
57
+ signal
58
+ });
59
+ status = res.status;
60
+ if (signal.aborted) {
61
+ if (deps.signal?.aborted) return { ok: false, kind: "cancelled", detail: "" };
62
+ return { ok: false, kind: "timeout", detail: "" };
63
+ }
64
+ try {
65
+ parsed = await res.json();
66
+ } catch (error) {
67
+ if (deps.signal?.aborted || timeoutSignal.aborted) throw error;
68
+ parsed = void 0;
69
+ }
70
+ } catch (error) {
71
+ if (deps.signal?.aborted) return { ok: false, kind: "cancelled", detail: "" };
72
+ if (timeoutSignal.aborted) return { ok: false, kind: "timeout", detail: "" };
73
+ return { ok: false, kind: "network", detail: safeDetail(String(error?.message ?? ""), secrets) };
74
+ }
75
+ const detail = safeDetail(extractWhitelistDetail(parsed), secrets);
76
+ if (status !== 200) {
77
+ if (status === 403) {
78
+ return classifyExchange403(detail) === "expired" ? { ok: false, kind: "http403-expired", status: 403, detail } : { ok: false, kind: "http403-rejected", status: 403, detail };
79
+ }
80
+ return { ok: false, kind: "http-other", status, detail };
81
+ }
82
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
83
+ return { ok: false, kind: "invalid-body", detail: "" };
84
+ }
85
+ const key = parsed.key;
86
+ if (typeof key !== "string" || key.trim() === "") return { ok: false, kind: "invalid-body", detail: "" };
87
+ return { ok: true, key };
88
+ }
89
+ var ABORT = /* @__PURE__ */ Symbol("aborted");
90
+ function isReadCancel(error) {
91
+ const e = error;
92
+ return e?.name === "AbortError" || e?.code === "SIGINT";
93
+ }
94
+ async function runOauthLogin(deps) {
95
+ if (!deps.isTTY) {
96
+ deps.err(t("bccli login openrouter needs a terminal. Use --key to paste an API key instead."));
97
+ return 1;
98
+ }
99
+ const { verifier, challenge } = pkcePair();
100
+ const url = buildAuthUrl(challenge);
101
+ try {
102
+ deps.open(url);
103
+ } catch {
104
+ deps.out(t("Could not open the browser automatically \u2014 open this URL manually: {url}", { url }));
105
+ }
106
+ let code;
107
+ try {
108
+ code = await deps.readLine(t("Paste the authorization code from the browser: "));
109
+ } catch (error) {
110
+ if (isReadCancel(error)) return 130;
111
+ deps.err(
112
+ t("Failed to read the authorization code: {reason}. Your stored credential was not changed.", {
113
+ reason: String(error?.message ?? error).slice(0, 200)
114
+ })
115
+ );
116
+ return 1;
117
+ }
118
+ if (code === ABORT) return 130;
119
+ if (typeof code !== "string" || code.trim() === "") {
120
+ deps.err(t("Authorization code is empty. Run bccli login openrouter again."));
121
+ return 1;
122
+ }
123
+ const trimmed = code.trim();
124
+ const secrets = [trimmed, verifier];
125
+ const result = await (async () => {
126
+ const controller = new AbortController();
127
+ const onSigint = () => controller.abort();
128
+ process.once("SIGINT", onSigint);
129
+ try {
130
+ return await exchangeAuthCode(trimmed, verifier, {
131
+ fetch: deps.fetch,
132
+ timeoutMs: deps.timeoutMs,
133
+ signal: controller.signal
134
+ });
135
+ } finally {
136
+ process.removeListener("SIGINT", onSigint);
137
+ }
138
+ })();
139
+ if (!result.ok) {
140
+ const detail = redact(result.detail, secrets);
141
+ switch (result.kind) {
142
+ case "cancelled":
143
+ deps.err(t("Login cancelled."));
144
+ return 130;
145
+ case "timeout":
146
+ deps.err(
147
+ t("The exchange with OpenRouter timed out after {seconds}s. Your stored credential was not changed.", {
148
+ seconds: String(Math.round((deps.timeoutMs ?? EXCHANGE_TIMEOUT_MS) / 1e3))
149
+ })
150
+ );
151
+ return 1;
152
+ case "network":
153
+ deps.err(t("Could not reach OpenRouter: {reason}. Your stored credential was not changed.", { reason: detail }));
154
+ return 1;
155
+ case "http403-expired":
156
+ deps.err(t("Authorization code expired \u2014 codes are valid for 10 minutes. Run bccli login openrouter again."));
157
+ return 1;
158
+ case "http403-rejected":
159
+ deps.err(t("OpenRouter rejected the authorization code (403): {detail}. Run bccli login openrouter again.", { detail: detail || "no details" }));
160
+ return 1;
161
+ case "invalid-body":
162
+ deps.err(t("OpenRouter did not return an API key in the expected format. Your stored credential was not changed."));
163
+ return 1;
164
+ case "http-other":
165
+ deps.err(t("OpenRouter exchange failed ({status}): {detail}. Your stored credential was not changed.", { status: String(result.status ?? 0), detail }));
166
+ return 1;
167
+ }
168
+ }
169
+ try {
170
+ deps.saveCredential("openrouter", result.key);
171
+ } catch (error) {
172
+ deps.err(
173
+ t("The key was created on your OpenRouter account but could not be saved locally ({reason}). Manage it (copy or revoke) at {dashboard}, then run bccli login openrouter again.", {
174
+ reason: redact(String(error?.message ?? ""), [result.key, trimmed, verifier]).slice(0, 200),
175
+ dashboard: OPENROUTER_DASHBOARD_URL
176
+ })
177
+ );
178
+ return 1;
179
+ }
180
+ deps.out(t("Logged in: openrouter credential saved."));
181
+ if (deps.env[OPENROUTER_ENV_KEY]) {
182
+ deps.out(t("Note: {env} is also set in the environment; the stored credential takes priority and {env} is only the fallback.", { env: OPENROUTER_ENV_KEY }));
183
+ }
184
+ deps.out(t("Verify with: bccli models openrouter (your key is never printed)."));
185
+ return 0;
186
+ }
187
+ async function runOauthLogout(deps) {
188
+ const removed = deps.removeCredential("openrouter");
189
+ if (removed) deps.out(t("openrouter credential removed."));
190
+ else deps.out(t("openrouter credential was not stored."));
191
+ if (deps.env[OPENROUTER_ENV_KEY]) {
192
+ deps.out(t("Note: {env} is still set in the environment and will keep being used.", { env: OPENROUTER_ENV_KEY }));
193
+ }
194
+ deps.out(t("To revoke keys on your account, open {dashboard}.", { dashboard: OPENROUTER_DASHBOARD_URL }));
195
+ return 0;
196
+ }
197
+ export {
198
+ ABORT,
199
+ OPENROUTER_AUTH_URL,
200
+ OPENROUTER_DASHBOARD_URL,
201
+ OPENROUTER_ENV_KEY,
202
+ OPENROUTER_EXCHANGE_URL,
203
+ buildAuthUrl,
204
+ classifyExchange403,
205
+ exchangeAuthCode,
206
+ pkcePair,
207
+ redact,
208
+ runOauthLogin,
209
+ runOauthLogout
210
+ };
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ bccliHome,
4
+ globalConfigPath
5
+ } from "./chunk-LACUWBWS.js";
6
+ import {
7
+ t
8
+ } from "./chunk-UZWJQF67.js";
9
+
10
+ // src/pluginsCli.ts
11
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
12
+ import { createRequire } from "module";
13
+ import { dirname, isAbsolute, join } from "path";
14
+
15
+ // src/pluginSpec.ts
16
+ var NPM_SPEC_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-z0-9][a-z0-9._+-]*)?$/;
17
+ var BARE_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
18
+ var MANAGED_PREFIX = "./plugins/node_modules/";
19
+ function parseNpmSpec(raw) {
20
+ const spec = raw.trim();
21
+ if (!NPM_SPEC_RE.test(spec)) return null;
22
+ const at = spec.lastIndexOf("@");
23
+ const name = at > 0 ? spec.slice(0, at) : spec;
24
+ if (!NPM_SPEC_RE.test(name)) return null;
25
+ return { name, spec };
26
+ }
27
+ function managedEntry(name) {
28
+ return `${MANAGED_PREFIX}${name}`;
29
+ }
30
+ function isManagedEntry(entry) {
31
+ return entry.startsWith(MANAGED_PREFIX);
32
+ }
33
+ function managedEntryName(entry) {
34
+ if (!isManagedEntry(entry)) return null;
35
+ const rest = entry.slice(MANAGED_PREFIX.length);
36
+ return BARE_NAME_RE.test(rest) ? rest : null;
37
+ }
38
+
39
+ // src/pluginsCli.ts
40
+ function ensurePrefixDir(prefix) {
41
+ mkdirSync(prefix, { recursive: true });
42
+ const pkg = join(prefix, "package.json");
43
+ if (!existsSync(pkg)) {
44
+ writeFileSync(pkg, `${JSON.stringify({ name: "bccli-plugins", private: true, version: "0.0.0" }, null, 2)}
45
+ `);
46
+ }
47
+ }
48
+ async function runPluginsCommand(args, deps) {
49
+ const [action, target] = args.subArgs;
50
+ const prefix = join(bccliHome(deps.env), "plugins");
51
+ switch (action ?? "list") {
52
+ case "list":
53
+ return listAction(deps);
54
+ case "install":
55
+ return installAction(target, deps, prefix);
56
+ case "remove":
57
+ return removeAction(target, deps, prefix);
58
+ case "update":
59
+ return updateAction(target, deps, prefix);
60
+ default:
61
+ deps.err(t("Unknown action: {action}. Use list, install <spec>, remove <name>, or update [name].", { action: action ?? "" }));
62
+ return 1;
63
+ }
64
+ }
65
+ var require_ = createRequire(import.meta.url);
66
+ function resolveEntryPath(entry, home) {
67
+ if (isAbsolute(entry)) return entry;
68
+ if (entry.startsWith(".")) return join(home, entry);
69
+ try {
70
+ return require_.resolve(entry);
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function readFailure(action, error, deps) {
76
+ deps.err(
77
+ t("Cannot read the plugin config while running {action}: {reason} at {path} \u2014 no changes were made; fix the config, then retry.", {
78
+ action,
79
+ reason: error.message,
80
+ path: globalConfigPath(deps.env)
81
+ })
82
+ );
83
+ return 1;
84
+ }
85
+ function listAction(deps) {
86
+ let entries;
87
+ try {
88
+ entries = deps.readPlugins();
89
+ } catch (error) {
90
+ return readFailure("list", error, deps);
91
+ }
92
+ const home = bccliHome(deps.env);
93
+ for (const entry of entries) {
94
+ const managed = isManagedEntry(entry);
95
+ const bare = managed ? managedEntryName(entry) : entry;
96
+ const suffix = !managed ? " (manual)" : bare === null ? " (malformed)" : "";
97
+ const resolved = resolveEntryPath(entry, home);
98
+ if (!resolved || !existsSync(resolved)) {
99
+ deps.out(`\u2717 ${entry}${suffix}`);
100
+ continue;
101
+ }
102
+ let version = "";
103
+ try {
104
+ const pkgPath = statSync(resolved).isDirectory() ? join(resolved, "package.json") : join(dirname(resolved), "package.json");
105
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
106
+ if (pkg.version) version = ` (${pkg.version})`;
107
+ } catch {
108
+ }
109
+ deps.out(`\u2713 ${entry}${suffix}${version}`);
110
+ }
111
+ return 0;
112
+ }
113
+ function npmFailure(res, deps) {
114
+ deps.err(t("npm install failed ({code}): {stderr}", { code: String(res.code), stderr: res.stderr.slice(-400) }));
115
+ return 1;
116
+ }
117
+ async function installAction(target, deps, prefix) {
118
+ if (!target) {
119
+ deps.err(t("install requires a package spec: bccli plugins install <spec>"));
120
+ return 1;
121
+ }
122
+ const parsed = parseNpmSpec(target);
123
+ if (!parsed) {
124
+ deps.err(
125
+ t("Invalid npm package spec: {spec}. Use a name, @scope/name, name@version, or name@tag \u2014 URLs, git, paths, and flags are not accepted.", { spec: target })
126
+ );
127
+ return 1;
128
+ }
129
+ ensurePrefixDir(prefix);
130
+ const res = await deps.runNpm(prefix, ["install", parsed.spec]);
131
+ if (res.code !== 0) return npmFailure(res, deps);
132
+ const entry = managedEntry(parsed.name);
133
+ let entries;
134
+ try {
135
+ entries = deps.readPlugins();
136
+ } catch (error) {
137
+ deps.err(t("npm finished but the config could not be read: {reason} \u2014 the entry list is unchanged; fix {path}, then rerun the same command (safe to repeat).", { reason: error.message, path: globalConfigPath(deps.env) }));
138
+ return 1;
139
+ }
140
+ const wasRegistered = entries.includes(entry);
141
+ try {
142
+ deps.savePlugins(wasRegistered ? entries : [...entries, entry]);
143
+ } catch (error) {
144
+ const reason = error.message;
145
+ deps.err(
146
+ wasRegistered ? t("npm finished but the config update failed: {reason}. {name} stays enabled through its existing entry {entry}; the package files may already be new. Fix the config error at {path} \u2014 no entry change was required.", { reason, name: parsed.name, entry, path: globalConfigPath(deps.env) }) : t('npm finished but the config update failed: {reason}. {name} is installed but NOT enabled \u2014 fix the config error, then run "bccli plugins install {name}" again (safe to repeat), or add {entry} to {path} manually.', { reason, name: parsed.name, entry, path: globalConfigPath(deps.env) })
147
+ );
148
+ return 1;
149
+ }
150
+ deps.out(wasRegistered ? t("{name} is already enabled.", { name: parsed.name }) : t("{name} installed and enabled.", { name: parsed.name }));
151
+ return 0;
152
+ }
153
+ function resolveTarget(target, deps, action) {
154
+ if (!target) return "missing-arg";
155
+ let entries;
156
+ try {
157
+ entries = deps.readPlugins();
158
+ } catch (error) {
159
+ return {
160
+ error: t("Cannot read the plugin config while running {action}: {reason} at {path} \u2014 no changes were made; fix the config, then retry.", {
161
+ action,
162
+ reason: error.message,
163
+ path: globalConfigPath(deps.env)
164
+ })
165
+ };
166
+ }
167
+ if (entries.includes(target)) {
168
+ if (isManagedEntry(target)) {
169
+ const name = managedEntryName(target);
170
+ if (!name) {
171
+ return {
172
+ error: t("Malformed managed entry: {entry} \u2014 fix it manually in {path}; bccli will not pass it to npm.", {
173
+ entry: target,
174
+ path: globalConfigPath(deps.env)
175
+ })
176
+ };
177
+ }
178
+ return { entry: target, name };
179
+ }
180
+ return { error: t("{entry} is a manual plugin entry \u2014 edit the global config.json directly to remove it.", { entry: target }) };
181
+ }
182
+ const parsed = parseNpmSpec(target);
183
+ if (parsed) {
184
+ const entry = managedEntry(parsed.name);
185
+ if (entries.includes(entry)) return { entry, name: parsed.name };
186
+ return { error: t("{name} is not an installed managed plugin.", { name: parsed.name }) };
187
+ }
188
+ return { error: t("Unknown plugin: {arg}.", { arg: target }) };
189
+ }
190
+ async function removeAction(target, deps, prefix) {
191
+ const r = resolveTarget(target, deps, "remove");
192
+ if (r === "missing-arg") {
193
+ deps.err(t("remove requires a plugin name: bccli plugins remove <name>"));
194
+ return 1;
195
+ }
196
+ if ("error" in r) {
197
+ deps.err(r.error);
198
+ return 1;
199
+ }
200
+ const res = await deps.runNpm(prefix, ["uninstall", r.name]);
201
+ if (res.code !== 0) return npmFailure(res, deps);
202
+ let next;
203
+ try {
204
+ next = deps.readPlugins().filter((e) => e !== r.entry);
205
+ } catch (error) {
206
+ deps.err(
207
+ t('npm removed the files of {name} but the plugin config could not be read: {reason} \u2014 the entry list is unchanged; fix {path}, then run "bccli plugins remove {name}" again (safe to repeat).', {
208
+ name: r.name,
209
+ reason: error.message,
210
+ path: globalConfigPath(deps.env)
211
+ })
212
+ );
213
+ return 1;
214
+ }
215
+ try {
216
+ deps.savePlugins(next);
217
+ } catch (error) {
218
+ deps.err(
219
+ t('npm removed the files of {name} but the config entry {entry} remains: {reason}. bccli will refuse to start while that entry is listed \u2014 fix the config error at {path}, then run "bccli plugins remove {name}" again (safe to repeat), or delete the entry manually.', {
220
+ name: r.name,
221
+ entry: r.entry,
222
+ reason: error.message,
223
+ path: globalConfigPath(deps.env)
224
+ })
225
+ );
226
+ return 1;
227
+ }
228
+ deps.out(t("{name} removed.", { name: r.name }));
229
+ return 0;
230
+ }
231
+ async function updateAction(target, deps, prefix) {
232
+ let names;
233
+ let hadMalformed = false;
234
+ if (target === void 0) {
235
+ let entries;
236
+ try {
237
+ entries = deps.readPlugins();
238
+ } catch (error) {
239
+ return readFailure("update", error, deps);
240
+ }
241
+ names = [];
242
+ for (const entry of entries) {
243
+ if (!isManagedEntry(entry)) continue;
244
+ const name = managedEntryName(entry);
245
+ if (name) names.push(name);
246
+ else {
247
+ hadMalformed = true;
248
+ deps.err(t("Skipping malformed entry: {entry} \u2014 fix it manually in {path}.", { entry, path: globalConfigPath(deps.env) }));
249
+ }
250
+ }
251
+ if (!names.length) {
252
+ if (!hadMalformed) deps.out(t("No managed plugins to update."));
253
+ return hadMalformed ? 1 : 0;
254
+ }
255
+ } else {
256
+ const r = resolveTarget(target, deps, "update");
257
+ if (r === "missing-arg") {
258
+ deps.err(t("update requires a plugin name or no argument: bccli plugins update [name]"));
259
+ return 1;
260
+ }
261
+ if ("error" in r) {
262
+ deps.err(r.error);
263
+ return 1;
264
+ }
265
+ names = [r.name];
266
+ }
267
+ ensurePrefixDir(prefix);
268
+ let failed = false;
269
+ for (const name of names) {
270
+ const argv = ["install", `${name}@latest`];
271
+ const res = await deps.runNpm(prefix, argv);
272
+ if (res.code !== 0) {
273
+ failed = true;
274
+ deps.err(t("npm install failed ({code}): {stderr}", { code: String(res.code), stderr: res.stderr.slice(-400) }));
275
+ } else {
276
+ deps.out(t("{name} updated to latest.", { name }));
277
+ }
278
+ }
279
+ return failed || hadMalformed ? 1 : 0;
280
+ }
281
+ export {
282
+ ensurePrefixDir,
283
+ runPluginsCommand
284
+ };