@d5render/cli 0.1.60 → 0.1.61

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/bin/d5cli CHANGED
@@ -1,51 +1,51 @@
1
1
  #!/usr/bin/env node
2
+ import { argv, env, platform } from "node:process";
2
3
  import { execSync, spawn } from "node:child_process";
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
5
  import { dirname, join } from "node:path";
5
- import { argv, env, platform } from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- //#region copilot/server/config.ts
9
- const name$1 = "d5_mcp_review_builtin";
8
+ //#region package.json
9
+ var name$1 = "@d5render/cli";
10
+
11
+ //#endregion
12
+ //#region packages/env.ts
13
+ const name = "d5_mcp_review_builtin";
10
14
  const report = "report";
11
15
  const getHash = "hash";
12
- const file = "bin/copilot.js";
16
+ const file = "bin/mcpServer.js";
13
17
  const RUNTIME_CWD = join(dirname(fileURLToPath(import.meta.url)), "../");
14
18
  const serveFile = join(RUNTIME_CWD, file);
15
19
  const envJson = buildEnv();
16
- const envUsed = {
17
- CI_SERVER_URL: toEnv("CI_SERVER_URL"),
18
- CI_PROJECT_PATH: toEnv("CI_PROJECT_PATH"),
19
- CI_PROJECT_ID: toEnv("CI_PROJECT_ID"),
20
- CI_PROJECT_NAME: toEnv("CI_PROJECT_NAME"),
21
- CI_COMMIT_SHA: toEnv("CI_COMMIT_SHA"),
22
- CI_COMMIT_BEFORE_SHA: toEnv("CI_COMMIT_BEFORE_SHA"),
23
- CI_MERGE_REQUEST_IID: toEnv("CI_MERGE_REQUEST_IID"),
24
- CI_MERGE_REQUEST_TITLE: toEnv("CI_MERGE_REQUEST_TITLE"),
25
- CI_MERGE_REQUEST_DESCRIPTION: toEnv("CI_MERGE_REQUEST_DESCRIPTION"),
26
- JIRA_BASE_URL: toEnv("JIRA_BASE_URL", "http://jira.d5techs.com.cn"),
27
- DINGTALK_WEBHOOK: toEnv("DINGTALK_WEBHOOK"),
28
- GITLAB_TOKEN: toEnv("GITLAB_TOKEN"),
29
- JIRA_PAT: toEnv("JIRA_PAT"),
30
- JIRA_COOKIE: toEnv("JIRA_COOKIE"),
31
- JIRA_USERNAME: toEnv("JIRA_USERNAME"),
32
- JIRA_PASSWORD: toEnv("JIRA_PASSWORD")
33
- };
34
- const tools = [
35
- "--additional-mcp-config",
36
- JSON.stringify({ mcpServers: { [name$1]: {
37
- type: "local",
38
- command: "node",
39
- args: [serveFile, `--customizenv=${JSON.stringify(envUsed)}`],
40
- tools: ["*"]
41
- } } }),
42
- "--allow-all-paths",
43
- "--allow-all-tools",
44
- "--deny-tool",
45
- "write",
46
- "--deny-tool",
47
- "github-mcp-server"
20
+ const envConfigKeys = [
21
+ "CI_SERVER_URL",
22
+ "CI_PROJECT_PATH",
23
+ "CI_PROJECT_ID",
24
+ "CI_PROJECT_NAME",
25
+ "CI_COMMIT_SHA",
26
+ "CI_COMMIT_BEFORE_SHA",
27
+ "CI_MERGE_REQUEST_IID",
28
+ "CI_MERGE_REQUEST_TITLE",
29
+ "CI_MERGE_REQUEST_DESCRIPTION",
30
+ "JIRA_BASE_URL",
31
+ "DINGTALK_WEBHOOK",
32
+ "GITLAB_TOKEN",
33
+ "JIRA_PAT",
34
+ "JIRA_COOKIE",
35
+ "JIRA_USERNAME",
36
+ "JIRA_PASSWORD",
37
+ "TOKEN_USAGE"
48
38
  ];
39
+ const envUsed = Object.defineProperty({}, "JIRA_BASE_URL", {
40
+ get: () => toEnv("CI_SERVER_URL", "http://jira.d5techs.com.cn"),
41
+ enumerable: true,
42
+ configurable: true
43
+ });
44
+ envConfigKeys.forEach((key) => Object.defineProperty(envUsed, key, {
45
+ get: () => toEnv(key),
46
+ enumerable: true,
47
+ configurable: true
48
+ }));
49
49
  function toEnv(key, defaultValue) {
50
50
  return envJson[key] || process.env[key] || defaultValue;
51
51
  }
@@ -56,59 +56,6 @@ function buildEnv() {
56
56
  return envJson;
57
57
  }
58
58
 
59
- //#endregion
60
- //#region copilot/bin/install.ts
61
- function install() {
62
- if (!env.CI) return;
63
- console.log("install copilot...");
64
- let success = false;
65
- let local = "";
66
- try {
67
- local = execSync("npm list @github/copilot -g --depth=0 --json").toString();
68
- } catch {
69
- local = "{}";
70
- }
71
- try {
72
- const localInfo = JSON.parse(local);
73
- const localVersion = localInfo.dependencies ? localInfo.dependencies?.["@github/copilot"]?.version : void 0;
74
- if (!localVersion) {
75
- installCopilot();
76
- success = true;
77
- console.log("install copilot success");
78
- return;
79
- }
80
- if (localVersion !== execSync("npm view @github/copilot version --registry=https://registry.npmmirror.com").toString().trim()) {
81
- execSync("npm uninstall -g @github/copilot --force");
82
- installCopilot();
83
- success = true;
84
- console.log("update copilot success");
85
- } else {
86
- success = true;
87
- console.log("copilot exists and is up-to-date");
88
- }
89
- } catch (error) {
90
- console.error(error);
91
- }
92
- if (!success) try {
93
- console.warn("try to reinstall copilot...");
94
- installCopilot();
95
- success = true;
96
- } catch (error) {
97
- console.error(error);
98
- }
99
- if (success) return;
100
- console.warn("try to reinstall copilot...");
101
- installCopilot();
102
- }
103
- function installCopilot() {
104
- execSync("npm install -g @github/copilot --registry=https://registry.npmmirror.com", { stdio: "inherit" });
105
- }
106
-
107
- //#endregion
108
- //#region package.json
109
- var name = "@d5render/cli";
110
- var version = "0.1.60";
111
-
112
59
  //#endregion
113
60
  //#region packages/gitlab/url.ts
114
61
  function buildHeaders() {
@@ -153,8 +100,8 @@ const commits = () => {
153
100
  //#endregion
154
101
  //#region packages/message/sendding.ts
155
102
  async function sendding(title, text) {
156
- if (!envUsed.DINGTALK_WEBHOOK) throw new Error("non DINGTALK_WEBHOOK");
157
103
  let res = new Response();
104
+ if (!envUsed.DINGTALK_WEBHOOK) throw res;
158
105
  for (const url of envUsed.DINGTALK_WEBHOOK.split(",")) res = await fetch(url, {
159
106
  method: "POST",
160
107
  headers: { "Content-Type": "application/json" },
@@ -174,53 +121,39 @@ async function sendding(title, text) {
174
121
  }
175
122
 
176
123
  //#endregion
177
- //#region copilot/bin/utils.ts
178
- const NAME = name.replaceAll("/", "_");
179
- const VERSION = version;
180
- const TEMP = env.CI_PROJECT_DIR + "." + NAME;
181
- const dingding = async (...args) => {
182
- try {
183
- const msg = await (await sendding(...args)).text();
184
- console.log(msg);
185
- } catch (error) {
186
- console.error(error);
187
- }
188
- };
189
- async function deploy() {
190
- if (!env.CI) return;
124
+ //#region review/helper.ts
125
+ const NAME = name$1.replaceAll("/", "_");
126
+ const TEMP = join(env.CI_PROJECT_DIR || "", `../../.${NAME}`);
127
+ const common_review_prompt = `Load code-review skills, then call the mcp tool '${name}-${getHash}' to load code-review commits.
128
+ Then use chinese(中文) to call the mcp tool '${name}-${report}', only main agent can call the tool '${name}-${report}', **prevent** subagent from calling tool '${name}-${report}`;
129
+ async function changelog() {
130
+ const changelog = readFileSync(join(RUNTIME_CWD, "README.md"), "utf8");
131
+ const cachepath = join(TEMP, "CHANGELOG_" + (env.CI_RUNNER_ID ?? env.CI_PROJECT_ID ?? "0"));
132
+ if (changelog === (existsSync(cachepath) ? readFileSync(cachepath, "utf8") : "")) return;
133
+ if (!existsSync(TEMP)) mkdirSync(TEMP, { recursive: true });
134
+ writeFileSync(cachepath, changelog, "utf8");
135
+ console.log("updated CHANGELOG cache.");
136
+ let matched = changelog.match(/CHANGELOG[\s\S]*?(#+[^\n]*\n+([\s\S]*?))(?=#+|$)/)?.[1] ?? "";
137
+ const matcheds = matched.split("\n");
138
+ matcheds.shift();
139
+ matched = matcheds.join("\n").trim();
140
+ if (matched) await dingding("NOTICE", `skills 更新\n\n\n${matched}\n\n\n历史请参考 [线上文档内容.skills文件夹](https://www.npmjs.com/package/@d5render/cli)`);
141
+ }
142
+ function deploySkills(SKILLS_DIR = ".copilot/skills") {
191
143
  const HOME = env.USERPROFILE ?? env.HOME ?? env.HOMEPATH;
192
144
  if (!HOME) throw new Error("cannot find `USERPROFILE` directory");
193
- const changelog = readFileSync(join(RUNTIME_CWD, "CHANGELOG.md"), "utf8");
194
- const cachepath = join(TEMP, "CHANGELOG-" + env.CI_RUNNER_ID || "0");
195
- let cache = "";
196
- if (existsSync(cachepath)) cache = readFileSync(cachepath, "utf8");
197
- else if (!existsSync(TEMP)) mkdirSync(TEMP, { recursive: true });
198
- if (changelog !== cache) {
199
- writeFileSync(cachepath, changelog, "utf8");
200
- console.log("updated CHANGELOG cache.");
201
- await dingding("NOTICE", `code-review/SKILL.md 更新\n\n细节请参考 [线上文档内容.skills](https://www.npmjs.com/package/@d5render/cli?activeTab=code)`);
202
- }
203
- const config = join(HOME, ".copilot/config.json"), dir = join(HOME, ".copilot/skills/code-review");
204
- console.log("deploy...");
205
- if (existsSync(config)) {
206
- rmSync(config);
207
- console.log("removed config cache.");
208
- }
209
- if (existsSync(dir)) rmSync(dir, {
210
- recursive: true,
211
- force: true
212
- });
145
+ const dir = join(HOME, `${SKILLS_DIR}/code-review`);
213
146
  mkdirSync(dir, { recursive: true });
147
+ console.log("deploy code-review skills...");
214
148
  const skillRoot = join(RUNTIME_CWD, ".skills/code-review");
215
- readdirSync(skillRoot).forEach((skill) => copyFileSync(join(skillRoot, skill), join(dir, skill)));
149
+ readdirSync(skillRoot).forEach((file) => copyFileSync(join(skillRoot, file), join(dir, file)));
216
150
  const instructionsRoot = join(RUNTIME_CWD, ".github/instructions");
217
151
  readdirSync(instructionsRoot).forEach((instruction) => copyFileSync(join(instructionsRoot, instruction), join(dir, instruction)));
218
- console.log("to new skill.");
219
152
  }
220
153
  async function need() {
221
154
  if (!env.CI) return true;
222
155
  const { CI_MERGE_REQUEST_IID, CI_COMMIT_SHA } = env;
223
- const file = join(TEMP, "CODEREVIEW");
156
+ const file = join(TEMP, "CODEREVIEW_" + (env.CI_PROJECT_ID ?? "0"));
224
157
  if (CI_MERGE_REQUEST_IID) {
225
158
  let appended = `${existsSync(file) ? readFileSync(file, "utf8") : ""}\n${CI_MERGE_REQUEST_IID}`.split("\n");
226
159
  const max = 1e4;
@@ -237,7 +170,7 @@ async function need() {
237
170
  encoding: "utf8"
238
171
  }).toString().match(/See merge request[\s\S]+!(\d+)/);
239
172
  if (!match) return true;
240
- const iid = match[1];
173
+ const [, iid] = match;
241
174
  const yes = (existsSync(file) ? readFileSync(file, "utf8") : "").split("\n").includes(iid);
242
175
  if (yes) {
243
176
  console.warn(`Merge Request !${iid} has been AI reviewed before.`);
@@ -254,31 +187,135 @@ async function need() {
254
187
  }
255
188
  return !yes;
256
189
  }
190
+ const dingding = async (...args) => {
191
+ try {
192
+ const msg = await (await sendding(...args)).text();
193
+ console.log(msg);
194
+ } catch (error) {
195
+ console.warn(error);
196
+ }
197
+ };
257
198
 
258
199
  //#endregion
259
- //#region copilot/bin/index.ts
260
- codereview().catch(async (error) => {
261
- await dingding("CRITICAL", "CI ERROR: 未知错误,请自行检查日志");
262
- throw error;
263
- });
264
- async function codereview() {
265
- await deploy();
266
- if (!await need()) {
267
- console.log("重复提交,进程跳过");
200
+ //#region review/token.copilot.ts
201
+ const GITHUB_API = "https://api.github.com";
202
+ const COPILOT_HEADERS = {
203
+ "User-Agent": "GitHubCopilotChat/0.35.0",
204
+ "Editor-Version": "vscode/1.107.0",
205
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
206
+ "Copilot-Integration-Id": "vscode-chat"
207
+ };
208
+ function getOAuthToken() {
209
+ const token = env.GITHUB_COPILOT_TOKEN || env.COPILOT_TOKEN || env.GITHUB_TOKEN || env.GH_TOKEN;
210
+ if (!token) throw new Error("未找到 GitHub Token,请设置 GITHUB_COPILOT_TOKEN / GITHUB_TOKEN / GH_TOKEN 环境变量");
211
+ return token;
212
+ }
213
+ /**
214
+ * 将 OAuth token 换成 Copilot session token
215
+ * 新版 OpenCode 官方 OAuth 接入需要此步骤才能访问 /copilot_internal/* API
216
+ */
217
+ async function exchangeForCopilotToken(oauthToken) {
218
+ try {
219
+ const res = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, { headers: {
220
+ Accept: "application/json",
221
+ Authorization: `Bearer ${oauthToken}`,
222
+ ...COPILOT_HEADERS
223
+ } });
224
+ if (!res.ok) return void 0;
225
+ return (await res.json()).token || void 0;
226
+ } catch {
268
227
  return;
269
228
  }
270
- install();
271
- const prompt = `Load skills, then call the mcp tool '${name$1}-${getHash}' to load code-review commits, if the task encounters an error, throw that.
272
- Otherwise, use chinese as default language to call the mcp tool '${name$1}-${report}'`;
273
- const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || "";
274
- const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || "";
275
- const copilot = spawn("node", [
276
- findCopilopt(),
229
+ }
230
+ async function fetchInternalUsage() {
231
+ const oauthToken = getOAuthToken();
232
+ const directRes = await fetch(`${GITHUB_API}/copilot_internal/user`, { headers: {
233
+ Accept: "application/json",
234
+ Authorization: `token ${oauthToken}`,
235
+ ...COPILOT_HEADERS
236
+ } });
237
+ if (directRes.ok) return directRes.json();
238
+ const copilotToken = await exchangeForCopilotToken(oauthToken);
239
+ if (!copilotToken) throw new Error([
240
+ "Copilot OAuth token 无法访问配额 API。",
241
+ "请创建 fine-grained PAT(https://github.com/settings/tokens?type=beta)",
242
+ "并在 Account permissions 中设置 Plan = Read-only,",
243
+ "然后通过 GITHUB_PAT 环境变量传入。"
244
+ ].join("\n"));
245
+ const res = await fetch(`${GITHUB_API}/copilot_internal/user`, { headers: {
246
+ Accept: "application/json",
247
+ Authorization: `Bearer ${copilotToken}`,
248
+ ...COPILOT_HEADERS
249
+ } });
250
+ if (!res.ok) {
251
+ const text = await res.text();
252
+ throw new Error(`查询 Copilot 内部 API 失败: ${res.status} ${text}`);
253
+ }
254
+ return res.json();
255
+ }
256
+ function formatInternalResult(data) {
257
+ const premium = data.quota_snapshots.premium_interactions;
258
+ return premium.entitlement - premium.remaining;
259
+ }
260
+ /**
261
+ * 查询 Copilot Premium Request 使用量
262
+ *
263
+ * 优先级:
264
+ * 1. 若设置了 GITHUB_PAT(fine-grained PAT)→ 使用 Public Billing API,支持模型明细
265
+ * 2. 否则使用 Internal API(GITHUB_COPILOT_TOKEN OAuth token),返回配额百分比和重置时间
266
+ */
267
+ async function getCopilotUsage(options = {}) {
268
+ try {
269
+ return formatInternalResult(await fetchInternalUsage());
270
+ } catch {
271
+ return "使用量未知";
272
+ }
273
+ }
274
+
275
+ //#endregion
276
+ //#region review/copilot/deploy.ts
277
+ const config = join(dirname(fileURLToPath(import.meta.url)), "copilot-mcp.json");
278
+ const tools = [
279
+ "--additional-mcp-config",
280
+ `"@${config}"`,
281
+ "--model",
282
+ "claude-opus-4.6",
283
+ "--allow-all-paths",
284
+ "--allow-all-tools",
285
+ "--enable-all-github-mcp-tools",
286
+ "--deny-tool",
287
+ "write",
288
+ "--stream",
289
+ "off"
290
+ ];
291
+ async function deploy() {
292
+ if (!env.CI) return;
293
+ await changelog();
294
+ execSync("npm i -g @github/copilot@latest --registry=https://registry.npmmirror.com", { stdio: "inherit" });
295
+ deploySkills();
296
+ const token = await getCopilotUsage();
297
+ env["TOKEN_USAGE"] = String(token);
298
+ writeFileSync(config, JSON.stringify({ mcpServers: { [name]: {
299
+ type: "local",
300
+ command: "node",
301
+ args: [serveFile, `--customizenv=${JSON.stringify(envUsed)}`],
302
+ tools: ["*"]
303
+ } } }, void 0, 2), "utf8");
304
+ }
305
+
306
+ //#endregion
307
+ //#region review/copilot/index.ts
308
+ const bind = "copilot";
309
+ async function cli() {
310
+ await deploy();
311
+ const httpProxy = env.HTTP_PROXY || env.http_proxy || "";
312
+ const httpsProxy = env.HTTPS_PROXY || env.https_proxy || "";
313
+ if (httpProxy) env.HTTP_PROXY = httpProxy;
314
+ if (httpsProxy) env.HTTPS_PROXY = httpsProxy;
315
+ const child = spawn(bind, [
277
316
  ...tools,
278
- "--stream",
279
- "off",
280
317
  "-p",
281
- prompt
318
+ `"${common_review_prompt.replace(/"/g, "\\\"")}"`
282
319
  ], {
283
320
  cwd: env.CI_PROJECT_DIR,
284
321
  stdio: [
@@ -286,67 +323,30 @@ Otherwise, use chinese as default language to call the mcp tool '${name$1}-${rep
286
323
  "pipe",
287
324
  "pipe"
288
325
  ],
289
- ...platform === "win32" && { windowsHide: true },
290
- env: {
291
- ...process.env,
292
- HTTP_PROXY: httpProxy,
293
- HTTPS_PROXY: httpsProxy
294
- }
295
- });
296
- copilot.stdout.on("data", (chunk) => console.log(String(chunk)));
297
- copilot.stderr.on("data", (chunk) => console.error(String(chunk)));
298
- return new Promise((res, rej) => {
299
- copilot.on("close", (code) => res());
326
+ ...platform === "win32" && { shell: true }
300
327
  });
328
+ child.stdout.on("data", (chunk) => console.log(String(chunk)));
329
+ child.stderr.on("data", (chunk) => console.error(String(chunk)));
330
+ return new Promise((res, rej) => child.on("close", (code) => getCopilotUsage().then((res) => console.log("本次Token积累使用量:", res)).catch(() => {}).finally(() => {
331
+ if (code === 0) res();
332
+ else rej(/* @__PURE__ */ new Error(`${bind} exited with code ${code}`));
333
+ })));
301
334
  }
302
- function findCopilopt() {
303
- let copilot = "";
335
+
336
+ //#endregion
337
+ //#region review/index.ts
338
+ async function codereview() {
304
339
  try {
305
- copilot = execSync("npm list @github/copilot -g -p").toString().trim();
306
- } catch {}
307
- if (!copilot) {
308
- const first = platform === "win32" ? win : linux;
309
- const second = platform === "win32" ? linux : win;
310
- copilot = first();
311
- if (!copilot) copilot = second();
312
- if (!copilot) throw new Error("没找到安装的包");
313
- }
314
- const pkg = join(copilot, "package.json");
315
- if (!existsSync(pkg)) throw new Error("安装的包找不到正确版本 " + pkg);
316
- const copilotPackage = JSON.parse(readFileSync(pkg, "utf8"));
317
- const binPath = typeof copilotPackage.bin === "string" ? copilotPackage.bin : copilotPackage.bin?.copilot || copilotPackage.bin?.["@github/copilot"];
318
- if (!binPath) throw new Error("non copilot executable found");
319
- const copilotVersion = copilotPackage.version || "unknown";
320
- const copilotPath = join(copilot, binPath);
321
- console.log(`${NAME} server:
322
- version: ${VERSION}
323
- path: ${serveFile}
324
- copilot:
325
- version: ${copilotVersion}
326
- path: ${copilotPath}`);
327
- return copilotPath;
328
- }
329
- function win() {
330
- const pathEnv = env.PATH || env.Path || "";
331
- const pathSeparator = platform === "win32" ? ";" : ":";
332
- const npm = pathEnv.split(pathSeparator).find((p) => p.includes("npm"));
333
- if (!npm) return "";
334
- const fallbackPath = join(npm, "node_modules", "@github/copilot");
335
- if (existsSync(join(fallbackPath, "package.json"))) return fallbackPath;
336
- return "";
337
- }
338
- function linux() {
339
- let cached = env.NVM_BIN;
340
- if (!cached) {
341
- const pathEnv = env.PATH || env.Path || "";
342
- const pathSeparator = platform === "win32" ? ";" : ":";
343
- const npm = pathEnv.split(pathSeparator).find((p) => p.includes(".nvm"));
344
- if (npm) cached = npm;
340
+ if (!await need()) {
341
+ console.log("重复提交,进程跳过");
342
+ return;
343
+ }
344
+ await cli();
345
+ } catch (error) {
346
+ await dingding("CRITICAL", "CI ERROR: 未知错误,请自行检查日志");
347
+ throw error;
345
348
  }
346
- if (!cached) return "";
347
- const fallbackPath = join(cached, "..", "lib", "node_modules", "@github/copilot");
348
- if (existsSync(join(fallbackPath, "package.json"))) return fallbackPath;
349
- return "";
350
349
  }
350
+ if (argv.includes("codereview")) codereview();
351
351
 
352
352
  //#endregion