@brainervirus/workit-core 1.2.1 → 1.3.1
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/package.json +1 -1
- package/skills/workit-babysit/SKILL.md +20 -26
- package/skills/workit-behavioral-tdd/SKILL.md +0 -5
- package/skills/workit-blast-radius/SKILL.md +0 -5
- package/skills/workit-challenge/SKILL.md +30 -47
- package/skills/workit-debug/SKILL.md +5 -27
- package/skills/workit-deslop/SKILL.md +0 -5
- package/skills/workit-diagram/SKILL.md +0 -5
- package/skills/workit-green-run/SKILL.md +0 -5
- package/skills/workit-handoff/SKILL.md +2 -8
- package/skills/workit-implement/SKILL.md +14 -20
- package/skills/workit-mockup/SKILL.md +0 -5
- package/skills/workit-plan/SKILL.md +30 -63
- package/skills/workit-review/SKILL.md +0 -5
- package/skills/workit-steer/SKILL.md +27 -27
- package/src/core/authority.ts +6 -1
- package/src/core/auto-approval.ts +34 -14
- package/src/core/branch.ts +293 -42
- package/src/core/config-conversion.ts +4 -4
- package/src/core/config-guard.ts +1 -7
- package/src/core/config.ts +1 -1
- package/src/core/cutover.ts +1207 -140
- package/src/core/doctor.ts +155 -82
- package/src/core/external-action-effects.ts +886 -172
- package/src/core/external-action.ts +222 -64
- package/src/core/init.ts +25 -115
- package/src/core/legacy-ownership.ts +47 -0
- package/src/core/methods.ts +65 -52
- package/src/core/policy-resolver.ts +13 -5
- package/src/core/ports/pr-create.ts +23 -15
- package/src/core/pr-create.ts +374 -84
- package/src/core/registration.ts +0 -15
- package/src/core/repo-tools.ts +0 -1
- package/src/core/route-intent.ts +34 -82
- package/src/core/safe-write.ts +66 -1
- package/src/core/setup-state.ts +7 -0
- package/src/core/setup.ts +175 -82
- package/src/core/task-contract.ts +66 -6
- package/src/core/task-engine.ts +47 -2
- package/src/core/task-store.ts +159 -1
- package/src/core/tracker-issues.ts +55 -118
- package/src/core/vcs-config.ts +178 -187
- package/src/core/workspaces.ts +554 -46
- package/src/core.ts +3 -2
- package/src/core/ports/vcs-token-create-urls.ts +0 -4
package/src/core/vcs-config.ts
CHANGED
|
@@ -2,12 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { configDir, PRESETS } from "./config";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveRuntimeWorkspaceVcs } from "./workspaces";
|
|
6
6
|
import { resolveBranchPolicyFor } from "./branch";
|
|
7
|
-
//
|
|
8
|
-
// Token never printed.
|
|
9
|
-
|
|
10
|
-
const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
7
|
+
// VCS resolution and CLI-backed identity/style reads.
|
|
11
8
|
|
|
12
9
|
export const vcsConfigPath = (): string =>
|
|
13
10
|
process.env.WORKFLOW_VCS_CONFIG ?? path.join(configDir(), "vcs.json");
|
|
@@ -30,6 +27,95 @@ const remoteProvider = (cwd: string): string | null => {
|
|
|
30
27
|
return null;
|
|
31
28
|
};
|
|
32
29
|
|
|
30
|
+
const remoteEndpoint = (
|
|
31
|
+
remote: string,
|
|
32
|
+
): { host: string; path: string; protocol: string; port: string } | null => {
|
|
33
|
+
const raw = remote.trim();
|
|
34
|
+
if (!raw) return null;
|
|
35
|
+
const scp = /^(?:[^@\s]+@)?([^:]+):(.+)$/u.exec(raw);
|
|
36
|
+
if (scp && !raw.includes("://"))
|
|
37
|
+
return {
|
|
38
|
+
host: scp[1].toLowerCase(),
|
|
39
|
+
path: scp[2].replace(/^\//u, "").replace(/\.git$/u, ""),
|
|
40
|
+
protocol: "ssh:",
|
|
41
|
+
port: "",
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(raw);
|
|
45
|
+
return {
|
|
46
|
+
host: url.hostname.toLowerCase(),
|
|
47
|
+
path: url.pathname
|
|
48
|
+
.replace(/^\//u, "")
|
|
49
|
+
.replace(/\.git$/u, "")
|
|
50
|
+
.replace(/\/$/u, ""),
|
|
51
|
+
protocol: url.protocol,
|
|
52
|
+
port: url.port,
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const configuredSshEndpoint = (host: string): { host: string; port: string } | null => {
|
|
60
|
+
if (host.startsWith("-")) return null;
|
|
61
|
+
const resolved = spawnSync("ssh", ["-G", host], {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
maxBuffer: 1024 * 1024,
|
|
65
|
+
env: { ...process.env, PATH: process.env.PATH ?? "" },
|
|
66
|
+
});
|
|
67
|
+
if (resolved.status !== 0) return null;
|
|
68
|
+
const output = resolved.stdout ?? "";
|
|
69
|
+
const hostname = /^hostname\s+(\S+)/imu.exec(output)?.[1]?.toLowerCase();
|
|
70
|
+
if (!hostname) return null;
|
|
71
|
+
return { host: hostname, port: /^port\s+(\d+)/imu.exec(output)?.[1] ?? "22" };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const sshApiHostAliases: Record<string, Array<{ host: string; port: string }>> = {
|
|
75
|
+
"github.com": [{ host: "ssh.github.com", port: "443" }],
|
|
76
|
+
"gitlab.com": [{ host: "altssh.gitlab.com", port: "443" }],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Confirm that Git transport and authenticated provider API calls target the same host. */
|
|
80
|
+
export const hostingApiHostMatches = (
|
|
81
|
+
remote: string,
|
|
82
|
+
apiHost: string,
|
|
83
|
+
resolveSshEndpoint: (
|
|
84
|
+
host: string,
|
|
85
|
+
) => { host: string; port: string } | null = configuredSshEndpoint,
|
|
86
|
+
): boolean => {
|
|
87
|
+
const endpoint = remoteEndpoint(remote);
|
|
88
|
+
if (!endpoint) return false;
|
|
89
|
+
let expected: { host: string; port: string };
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(`https://${apiHost}`);
|
|
92
|
+
if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
|
|
93
|
+
return false;
|
|
94
|
+
expected = {
|
|
95
|
+
host: url.hostname.toLowerCase().replace(/\.$/u, ""),
|
|
96
|
+
port: url.port,
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (endpoint.protocol !== "ssh:") {
|
|
102
|
+
return endpoint.host.replace(/\.$/u, "") === expected.host && endpoint.port === expected.port;
|
|
103
|
+
}
|
|
104
|
+
const resolved = resolveSshEndpoint(endpoint.host);
|
|
105
|
+
if (!resolved) return false;
|
|
106
|
+
const hostname = resolved.host.toLowerCase().replace(/\.$/u, "");
|
|
107
|
+
const port = endpoint.port || resolved.port;
|
|
108
|
+
if (hostname === expected.host) return port === (expected.port || "22");
|
|
109
|
+
return Boolean(
|
|
110
|
+
sshApiHostAliases[expected.host]?.some(
|
|
111
|
+
(alias) =>
|
|
112
|
+
alias.host === hostname &&
|
|
113
|
+
alias.port === port &&
|
|
114
|
+
(!expected.port || expected.port === port),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
33
119
|
// RL-01: typed vcs.json reader. Missing is a legitimate unconfigured state;
|
|
34
120
|
// malformed (parse failure or a non-object) is reported with the exact path so
|
|
35
121
|
// risky consumers stop instead of silently reading defaults.
|
|
@@ -60,14 +146,9 @@ export const readVcsConfig = (): VcsConfigResult => {
|
|
|
60
146
|
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
61
147
|
};
|
|
62
148
|
|
|
63
|
-
function readVcsJson(): { config: Record<string, any>; path: string; ok: boolean } {
|
|
64
|
-
const { status, path: cfgPath, config } = readVcsConfig();
|
|
65
|
-
return { config, path: cfgPath, ok: status === "valid" };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
149
|
/** Port of scripts/vcs/config.sh — mode: load | summary | resolve. */
|
|
69
150
|
export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): Record<string, any> {
|
|
70
|
-
const ws =
|
|
151
|
+
const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
|
|
71
152
|
const wsVcs = (ws?.vcs ?? {}) as Record<string, any>;
|
|
72
153
|
const wsYt = (ws?.youtrack ?? {}) as Record<string, any>;
|
|
73
154
|
const wsIssues = (ws?.issues ?? {}) as Record<string, any>;
|
|
@@ -110,8 +191,10 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
110
191
|
(hasWorkspacePolicy ? policyDefault : (cfg.defaultTargetBranch ?? policyDefault)) ??
|
|
111
192
|
"develop",
|
|
112
193
|
);
|
|
113
|
-
const linkIssues =
|
|
114
|
-
|
|
194
|
+
const linkIssues =
|
|
195
|
+
provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
196
|
+
const youtrackBaseUrl =
|
|
197
|
+
provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
115
198
|
// github issues path only when BOTH providers are github (mirrors WorkspaceConfig.issues).
|
|
116
199
|
let issuesProvider: string | null = null;
|
|
117
200
|
let linkOnPr: boolean | null = null;
|
|
@@ -144,11 +227,9 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
144
227
|
if (cfgStatus === "malformed") {
|
|
145
228
|
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
146
229
|
}
|
|
147
|
-
if (cfgStatus === "missing")
|
|
230
|
+
if (cfgStatus === "missing" && provider === null)
|
|
148
231
|
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
149
|
-
|
|
150
|
-
// Credentials cannot resolve without a provider: fail closed instead of
|
|
151
|
-
// assuming a host. Branch policy (resolve mode above) never needed one.
|
|
232
|
+
// Hosting cannot resolve without a provider; branch policy still can.
|
|
152
233
|
if (provider === null) {
|
|
153
234
|
return {
|
|
154
235
|
ok: false,
|
|
@@ -158,33 +239,12 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
158
239
|
}
|
|
159
240
|
|
|
160
241
|
const prov = (cfg[provider] ?? {}) as Record<string, any>;
|
|
161
|
-
// Workspace-tight tokens: an explicit workspace vcs.tokenFile wins (per-area
|
|
162
|
-
// accounts without collisions), else the global provider tokenFile, else the
|
|
163
|
-
// default <provider>.token path. Same order as the per-workspace
|
|
164
|
-
// commitPolicy/branchPolicy overrides.
|
|
165
|
-
const wsTokenFile =
|
|
166
|
-
typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
167
|
-
const tokenFile = String(
|
|
168
|
-
wsTokenFile ?? prov.tokenFile ?? path.join(configDir(), `${provider}.token`),
|
|
169
|
-
);
|
|
170
|
-
const tokenPath = path.resolve(tokenFile);
|
|
171
|
-
let tokenOk = false;
|
|
172
|
-
if (fs.existsSync(tokenPath)) {
|
|
173
|
-
const token = fs.readFileSync(tokenPath, "utf8").trim();
|
|
174
|
-
const placeholder =
|
|
175
|
-
!token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
176
|
-
tokenOk = !placeholder;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
242
|
const out: Record<string, any> = {
|
|
180
243
|
ok: true,
|
|
181
244
|
configPath: path.resolve(cfgPath),
|
|
182
245
|
provider,
|
|
183
246
|
defaultTargetBranch: defaultTarget,
|
|
184
247
|
pr: cfg.pr ?? {},
|
|
185
|
-
tokenPath,
|
|
186
|
-
tokenPresent: fs.existsSync(tokenPath),
|
|
187
|
-
tokenReady: tokenOk,
|
|
188
248
|
workspace_name: ws?.name ?? null,
|
|
189
249
|
link_issues: linkIssues,
|
|
190
250
|
youtrack_base_url: youtrackBaseUrl,
|
|
@@ -199,159 +259,69 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
199
259
|
} else if (provider === "github") {
|
|
200
260
|
out.github = { host: prov.host ?? "github.com" };
|
|
201
261
|
}
|
|
202
|
-
if (mode === "summary") delete out.tokenPath;
|
|
203
262
|
return out;
|
|
204
263
|
}
|
|
205
264
|
|
|
206
|
-
/**
|
|
207
|
-
export
|
|
208
|
-
const
|
|
265
|
+
/** Probe the CLI credential actually used for this checkout, never a Workit token file. */
|
|
266
|
+
export function vcsCliIdentity(cwd?: string): Record<string, any> {
|
|
267
|
+
const root = vcsCwd(cwd);
|
|
268
|
+
const cfg = vcsConfig("load", root);
|
|
209
269
|
if (!cfg.ok) return { ok: false, error: cfg.error ?? "vcs config not ready" };
|
|
210
270
|
const provider = cfg.provider as string;
|
|
211
|
-
if (
|
|
212
|
-
return {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
error: "GitLab API rejected token",
|
|
237
|
-
detail: (await res.text()).slice(0, 200),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
user = JSON.parse(await res.text()) as Record<string, any>;
|
|
241
|
-
} catch (err) {
|
|
242
|
-
if (err instanceof SyntaxError) {
|
|
243
|
-
return { ok: false, provider, error: "invalid JSON from GitLab /user" };
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
ok: false,
|
|
247
|
-
provider,
|
|
248
|
-
error: "GitLab API rejected token",
|
|
249
|
-
detail: (err instanceof Error ? err.message : "network error").slice(0, 200),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
return { ok: true, provider, username: user.username ?? user.login, name: user.name };
|
|
253
|
-
}
|
|
254
|
-
if (provider === "github") {
|
|
255
|
-
const result = spawnSync("gh", ["api", "user"], {
|
|
256
|
-
encoding: "utf8",
|
|
257
|
-
env: { ...process.env, GH_TOKEN: token },
|
|
258
|
-
});
|
|
259
|
-
if (result.status !== 0) {
|
|
271
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
272
|
+
return { ok: false, error: `unsupported provider: ${provider}` };
|
|
273
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
274
|
+
const host = String(
|
|
275
|
+
provider === "github" ? (cfg.github?.host ?? "github.com") : (cfg.gitlab?.host ?? "gitlab.com"),
|
|
276
|
+
).toLowerCase();
|
|
277
|
+
const env = {
|
|
278
|
+
...process.env,
|
|
279
|
+
PATH: process.env.PATH ?? "",
|
|
280
|
+
...(provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
|
|
281
|
+
};
|
|
282
|
+
const result = spawnSync(bin, ["api", "user"], {
|
|
283
|
+
cwd: root,
|
|
284
|
+
encoding: "utf8",
|
|
285
|
+
env,
|
|
286
|
+
});
|
|
287
|
+
if (result.status !== 0)
|
|
288
|
+
return { ok: false, provider, error: `${bin} is missing or not authenticated` };
|
|
289
|
+
try {
|
|
290
|
+
const user = JSON.parse(result.stdout ?? "") as Record<string, unknown>;
|
|
291
|
+
const username = provider === "gitlab" ? user.username : user.login;
|
|
292
|
+
if (typeof username !== "string" || !username)
|
|
293
|
+
return { ok: false, provider, error: `invalid ${bin} identity response` };
|
|
294
|
+
const expected = resolveRuntimeWorkspaceVcs(root)?.vcs?.account;
|
|
295
|
+
if (expected && username.toLowerCase() !== expected.toLowerCase())
|
|
260
296
|
return {
|
|
261
297
|
ok: false,
|
|
262
298
|
provider,
|
|
263
|
-
error:
|
|
264
|
-
detail: (result.stderr ?? result.stdout ?? "").slice(0, 200),
|
|
299
|
+
error: `${bin} account ${username} does not match ${expected}`,
|
|
265
300
|
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return { ok: true, provider, username: user.login, name: user.name };
|
|
270
|
-
} catch {
|
|
271
|
-
return { ok: false, provider, error: "invalid JSON from gh api user" };
|
|
272
|
-
}
|
|
301
|
+
return { ok: true, provider, host, username, name: user.name };
|
|
302
|
+
} catch {
|
|
303
|
+
return { ok: false, provider, error: `invalid JSON from ${bin} api user` };
|
|
273
304
|
}
|
|
274
|
-
return { ok: false, error: `unsupported provider: ${provider}` };
|
|
275
305
|
}
|
|
276
306
|
|
|
277
|
-
/**
|
|
278
|
-
export function
|
|
279
|
-
|
|
280
|
-
const { config: cfg } = readVcsJson();
|
|
281
|
-
const defaults = (cfg.tokenDefaults ?? {}) as Record<string, any>;
|
|
282
|
-
const name = String(defaults.name ?? tokenName);
|
|
283
|
-
const desc = String(defaults.description ?? "OpenCode workit — /wk-pr and glab/gh");
|
|
284
|
-
|
|
285
|
-
const gitlab = (cfg.gitlab ?? {}) as Record<string, any>;
|
|
286
|
-
const host = String(gitlab.host ?? "gitlab.com");
|
|
287
|
-
const gitlabScopes = Array.isArray(defaults.gitlabScopes) ? defaults.gitlabScopes : ["api"];
|
|
288
|
-
const gitlabParams = new URLSearchParams({
|
|
289
|
-
name,
|
|
290
|
-
description: desc,
|
|
291
|
-
scopes: gitlabScopes.join(","),
|
|
292
|
-
});
|
|
293
|
-
const gitlabUrl = `https://${host}/-/user_settings/personal_access_tokens?${gitlabParams}`;
|
|
294
|
-
|
|
295
|
-
const githubPerms = defaults.githubPermissions ?? {
|
|
296
|
-
pull_requests: "write",
|
|
297
|
-
contents: "write",
|
|
298
|
-
metadata: "read",
|
|
299
|
-
};
|
|
300
|
-
const ghParams = new URLSearchParams({ name, description: desc, ...githubPerms });
|
|
301
|
-
const githubFineUrl = `https://github.com/settings/personal-access-tokens/new?${ghParams}`;
|
|
302
|
-
|
|
303
|
-
const classicScopes = Array.isArray(defaults.githubClassicScopes)
|
|
304
|
-
? defaults.githubClassicScopes
|
|
305
|
-
: ["repo"];
|
|
306
|
-
const githubClassicUrl = `https://github.com/settings/tokens/new?${new URLSearchParams({ description: name, scopes: classicScopes.join(",") })}`;
|
|
307
|
-
|
|
308
|
-
const rawProvider = cfg.provider;
|
|
309
|
-
// Display-only URL helper: both providers' URLs are always returned, but
|
|
310
|
-
// nothing is preselected without an explicit provider — no silent default.
|
|
311
|
-
const provider =
|
|
312
|
-
typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.toLowerCase() : null;
|
|
313
|
-
const active =
|
|
314
|
-
provider === null
|
|
315
|
-
? undefined
|
|
316
|
-
: ({
|
|
317
|
-
gitlab: {
|
|
318
|
-
tokenFile: gitlab.tokenFile ?? path.join(configDir(), "gitlab.token"),
|
|
319
|
-
createUrl: gitlabUrl,
|
|
320
|
-
scopes: gitlabScopes,
|
|
321
|
-
name,
|
|
322
|
-
},
|
|
323
|
-
github: {
|
|
324
|
-
tokenFile:
|
|
325
|
-
(cfg.github as Record<string, any>)?.tokenFile ??
|
|
326
|
-
path.join(configDir(), "github.token"),
|
|
327
|
-
createUrl: githubFineUrl,
|
|
328
|
-
createUrlClassic: githubClassicUrl,
|
|
329
|
-
permissions: githubPerms,
|
|
330
|
-
name,
|
|
331
|
-
},
|
|
332
|
-
}[provider] ?? {});
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
tokenName: name,
|
|
336
|
-
tokenDescription: desc,
|
|
337
|
-
activeProvider: provider,
|
|
338
|
-
active,
|
|
339
|
-
gitlab: { host, createUrl: gitlabUrl, scopes: gitlabScopes, tokenFile: gitlab.tokenFile },
|
|
340
|
-
github: {
|
|
341
|
-
createUrl: githubFineUrl,
|
|
342
|
-
createUrlClassic: githubClassicUrl,
|
|
343
|
-
permissions: githubPerms,
|
|
344
|
-
tokenFile: (cfg.github as Record<string, any>)?.tokenFile,
|
|
345
|
-
},
|
|
346
|
-
};
|
|
307
|
+
/** Legacy command name, now verifies native CLI auth rather than a separate token. */
|
|
308
|
+
export async function vcsVerifyToken(): Promise<Record<string, any>> {
|
|
309
|
+
return vcsCliIdentity();
|
|
347
310
|
}
|
|
348
311
|
|
|
349
312
|
/** Port of scripts/vcs/merged-style.sh — recent merged MR/PR bodies for style reference. */
|
|
350
313
|
export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
|
|
351
314
|
const cfg = vcsConfig("load", cwd);
|
|
352
|
-
if (!cfg.ok
|
|
315
|
+
if (!cfg.ok) return { ok: false, error: "vcs not configured" };
|
|
353
316
|
const provider = cfg.provider as string;
|
|
354
|
-
const
|
|
317
|
+
const host = String(
|
|
318
|
+
provider === "github" ? (cfg.github?.host ?? "github.com") : (cfg.gitlab?.host ?? "gitlab.com"),
|
|
319
|
+
).toLowerCase();
|
|
320
|
+
const env = {
|
|
321
|
+
...process.env,
|
|
322
|
+
PATH: process.env.PATH ?? "",
|
|
323
|
+
...(provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
|
|
324
|
+
};
|
|
355
325
|
const examples: Array<Record<string, any>> = [];
|
|
356
326
|
|
|
357
327
|
const descInfo = (desc: string, caseInsensitiveNotes = false): Record<string, any> => ({
|
|
@@ -363,22 +333,32 @@ export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
|
|
|
363
333
|
descriptionPreview: desc.slice(0, 600),
|
|
364
334
|
});
|
|
365
335
|
|
|
336
|
+
const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
337
|
+
cwd,
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
});
|
|
340
|
+
const remotes = (remoteResult.stdout ?? "")
|
|
341
|
+
.split(/\r?\n/u)
|
|
342
|
+
.map((value) => value.trim())
|
|
343
|
+
.filter(Boolean);
|
|
344
|
+
if (remotes.length === 0) return { ok: false, error: "no origin remote" };
|
|
345
|
+
if (remotes.length !== 1) return { ok: false, error: "origin has ambiguous push destinations" };
|
|
346
|
+
const remote = remotes[0];
|
|
347
|
+
const endpoint = remoteEndpoint(remote);
|
|
348
|
+
if (!endpoint?.path) return { ok: false, error: "hosting repository could not be resolved" };
|
|
349
|
+
if (!hostingApiHostMatches(remote, host))
|
|
350
|
+
return { ok: false, error: "PR style target does not match the provider host" };
|
|
351
|
+
|
|
366
352
|
if (provider === "gitlab") {
|
|
367
|
-
const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
368
|
-
if (remote.status !== 0) return { ok: false, error: "no origin remote" };
|
|
369
|
-
const url = (remote.stdout ?? "").trim();
|
|
370
|
-
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
371
|
-
if (!m) return { ok: false, error: "not a gitlab.com origin" };
|
|
372
|
-
const project = m[1];
|
|
373
|
-
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
374
353
|
const run = (args: string[]) =>
|
|
375
|
-
spawnSync("glab", ["api", ...args], {
|
|
354
|
+
spawnSync("glab", ["api", ...args], {
|
|
355
|
+
cwd,
|
|
356
|
+
encoding: "utf8",
|
|
357
|
+
env,
|
|
358
|
+
});
|
|
376
359
|
let r = run([
|
|
377
|
-
`projects/${
|
|
360
|
+
`projects/${endpoint.path.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`,
|
|
378
361
|
]);
|
|
379
|
-
if (r.status !== 0) {
|
|
380
|
-
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
381
|
-
}
|
|
382
362
|
if (r.status !== 0) return { ok: false, error: "could not list merge requests" };
|
|
383
363
|
for (const mr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|
|
384
364
|
const desc = String(mr.description ?? "").trim();
|
|
@@ -392,8 +372,19 @@ export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
|
|
|
392
372
|
} else if (provider === "github") {
|
|
393
373
|
const r = spawnSync(
|
|
394
374
|
"gh",
|
|
395
|
-
[
|
|
396
|
-
|
|
375
|
+
[
|
|
376
|
+
"pr",
|
|
377
|
+
"list",
|
|
378
|
+
"--repo",
|
|
379
|
+
`${host}/${endpoint.path}`,
|
|
380
|
+
"--state",
|
|
381
|
+
"merged",
|
|
382
|
+
"--limit",
|
|
383
|
+
String(limit),
|
|
384
|
+
"--json",
|
|
385
|
+
"title,url,body",
|
|
386
|
+
],
|
|
387
|
+
{ cwd, encoding: "utf8", env },
|
|
397
388
|
);
|
|
398
389
|
if (r.status !== 0) return { ok: false, error: "could not list pull requests" };
|
|
399
390
|
for (const pr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|