@brainervirus/workit-core 0.5.6 → 0.6.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/README.md +4 -17
- package/package.json +9 -9
- package/scripts/_shared/common.sh +18 -3
- package/scripts/install-opencode-plugin.sh +14 -9
- package/scripts/lib/config-dir.sh +26 -0
- package/scripts/sync-runtime.sh +4 -1
- package/src/core/branch.ts +143 -48
- package/src/core/changelog.ts +17 -14
- package/src/core/config-guard.ts +9 -2
- package/src/core/config.ts +95 -15
- package/src/core/detector.ts +22 -11
- package/src/core/docs-repo.ts +50 -15
- package/src/core/docs-validate.ts +163 -37
- package/src/core/flow-state.ts +6 -2
- package/src/core/gitignore.ts +11 -2
- package/src/core/handoff-context.ts +18 -5
- package/src/core/hygiene.ts +27 -5
- package/src/core/init.ts +86 -21
- package/src/core/parse-sections.ts +2 -2
- package/src/core/plan-tasks.ts +13 -3
- package/src/core/ports/youtrack-api.ts +3 -1
- package/src/core/ports/youtrack-config.ts +1 -3
- package/src/core/pr-create.ts +47 -15
- package/src/core/present.ts +11 -2
- package/src/core/reminder.ts +1 -2
- package/src/core/repo-tool.ts +4 -1
- package/src/core/rules.ts +10 -7
- package/src/core/scripts.ts +7 -2
- package/src/core/sdd.ts +11 -3
- package/src/core/templates.ts +14 -4
- package/src/core/vcs-config.ts +94 -36
- package/src/core/verify-parse.ts +4 -2
- package/src/core/workspaces.ts +2 -2
- package/src/core/youtrack.ts +233 -58
- package/src/core.ts +18 -3
- package/src/tools/docs-repo.ts +12 -3
- package/src/tools/flow.ts +24 -13
- package/src/tools/handoff.ts +28 -23
- package/src/tools/present.ts +14 -10
- package/src/tools/repo.ts +220 -87
- package/src/tools/sdd.ts +93 -66
- package/src/tools/youtrack.ts +119 -52
- package/templates/superpowers-doc-contract.md +1 -1
package/src/core/vcs-config.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { configDir } from "./config";
|
|
@@ -15,7 +14,8 @@ export const vcsConfigPath = (): string =>
|
|
|
15
14
|
|
|
16
15
|
const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
17
16
|
|
|
18
|
-
const vcsCwd = (cwd?: string): string =>
|
|
17
|
+
const vcsCwd = (cwd?: string): string =>
|
|
18
|
+
process.env.WORKFLOW_WORKSPACE_ROOT ?? cwd ?? process.cwd();
|
|
19
19
|
|
|
20
20
|
function readVcsJson(): { config: Record<string, any>; path: string; ok: boolean } {
|
|
21
21
|
const cfgPath = vcsConfigPath();
|
|
@@ -28,7 +28,9 @@ function readVcsJson(): { config: Record<string, any>; path: string; ok: boolean
|
|
|
28
28
|
config = parsed as Record<string, any>;
|
|
29
29
|
ok = true;
|
|
30
30
|
}
|
|
31
|
-
} catch {
|
|
31
|
+
} catch {
|
|
32
|
+
/* missing or invalid */
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
return { config, path: cfgPath, ok };
|
|
34
36
|
}
|
|
@@ -48,7 +50,11 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
48
50
|
// github issues path only when BOTH providers are github (mirrors WorkspaceConfig.issues).
|
|
49
51
|
let issuesProvider: string | null = null;
|
|
50
52
|
let linkOnPr: boolean | null = null;
|
|
51
|
-
if (
|
|
53
|
+
if (
|
|
54
|
+
provider === "github" &&
|
|
55
|
+
typeof wsIssues.provider === "string" &&
|
|
56
|
+
wsIssues.provider.toLowerCase() === "github"
|
|
57
|
+
) {
|
|
52
58
|
issuesProvider = "github";
|
|
53
59
|
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
54
60
|
}
|
|
@@ -69,12 +75,13 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
69
75
|
if (!cfgOk) return { ok: false, error: "missing or invalid vcs.json" };
|
|
70
76
|
|
|
71
77
|
const prov = (cfg[provider] ?? {}) as Record<string, any>;
|
|
72
|
-
const tokenFile = String(prov.tokenFile ?? path.join(
|
|
78
|
+
const tokenFile = String(prov.tokenFile ?? path.join(configDir(), `${provider}.token`));
|
|
73
79
|
const tokenPath = path.resolve(tokenFile);
|
|
74
80
|
let tokenOk = false;
|
|
75
81
|
if (fs.existsSync(tokenPath)) {
|
|
76
82
|
const token = fs.readFileSync(tokenPath, "utf8").trim();
|
|
77
|
-
const placeholder =
|
|
83
|
+
const placeholder =
|
|
84
|
+
!token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
78
85
|
tokenOk = !placeholder;
|
|
79
86
|
}
|
|
80
87
|
|
|
@@ -94,7 +101,10 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
94
101
|
link_on_pr: linkOnPr,
|
|
95
102
|
};
|
|
96
103
|
if (provider === "gitlab") {
|
|
97
|
-
out.gitlab = {
|
|
104
|
+
out.gitlab = {
|
|
105
|
+
host: prov.host ?? "gitlab.com",
|
|
106
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4",
|
|
107
|
+
};
|
|
98
108
|
} else if (provider === "github") {
|
|
99
109
|
out.github = { host: prov.host ?? "github.com" };
|
|
100
110
|
}
|
|
@@ -119,10 +129,20 @@ export function vcsVerifyToken(): Record<string, any> {
|
|
|
119
129
|
|
|
120
130
|
if (provider === "gitlab") {
|
|
121
131
|
const host = (cfg.gitlab as Record<string, any>)?.host ?? "gitlab.com";
|
|
122
|
-
const api = ((cfg.gitlab as Record<string, any>)?.apiUrl ?? `https://${host}/api/v4`).replace(
|
|
123
|
-
|
|
132
|
+
const api = ((cfg.gitlab as Record<string, any>)?.apiUrl ?? `https://${host}/api/v4`).replace(
|
|
133
|
+
/\/+$/,
|
|
134
|
+
"",
|
|
135
|
+
);
|
|
136
|
+
const result = spawnSync("curl", ["-fsS", "-H", `PRIVATE-TOKEN: ${token}`, `${api}/user`], {
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
});
|
|
124
139
|
if (result.status !== 0) {
|
|
125
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
provider,
|
|
143
|
+
error: "GitLab API rejected token",
|
|
144
|
+
detail: (result.stderr ?? result.stdout ?? "").slice(0, 200),
|
|
145
|
+
};
|
|
126
146
|
}
|
|
127
147
|
try {
|
|
128
148
|
const user = JSON.parse(result.stdout ?? "") as Record<string, any>;
|
|
@@ -132,9 +152,17 @@ export function vcsVerifyToken(): Record<string, any> {
|
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
154
|
if (provider === "github") {
|
|
135
|
-
const result = spawnSync("gh", ["api", "user"], {
|
|
155
|
+
const result = spawnSync("gh", ["api", "user"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
env: { ...process.env, GH_TOKEN: token },
|
|
158
|
+
});
|
|
136
159
|
if (result.status !== 0) {
|
|
137
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
provider,
|
|
163
|
+
error: "GitHub API rejected token",
|
|
164
|
+
detail: (result.stderr ?? result.stdout ?? "").slice(0, 200),
|
|
165
|
+
};
|
|
138
166
|
}
|
|
139
167
|
try {
|
|
140
168
|
const user = JSON.parse(result.stdout ?? "") as Record<string, any>;
|
|
@@ -157,32 +185,44 @@ export function vcsTokenCreateUrls(): Record<string, any> {
|
|
|
157
185
|
const gitlab = (cfg.gitlab ?? {}) as Record<string, any>;
|
|
158
186
|
const host = String(gitlab.host ?? "gitlab.com");
|
|
159
187
|
const gitlabScopes = Array.isArray(defaults.gitlabScopes) ? defaults.gitlabScopes : ["api"];
|
|
160
|
-
const gitlabParams = new URLSearchParams({
|
|
188
|
+
const gitlabParams = new URLSearchParams({
|
|
189
|
+
name,
|
|
190
|
+
description: desc,
|
|
191
|
+
scopes: gitlabScopes.join(","),
|
|
192
|
+
});
|
|
161
193
|
const gitlabUrl = `https://${host}/-/user_settings/personal_access_tokens?${gitlabParams}`;
|
|
162
194
|
|
|
163
|
-
const githubPerms = defaults.githubPermissions ?? {
|
|
195
|
+
const githubPerms = defaults.githubPermissions ?? {
|
|
196
|
+
pull_requests: "write",
|
|
197
|
+
contents: "write",
|
|
198
|
+
metadata: "read",
|
|
199
|
+
};
|
|
164
200
|
const ghParams = new URLSearchParams({ name, description: desc, ...githubPerms });
|
|
165
201
|
const githubFineUrl = `https://github.com/settings/personal-access-tokens/new?${ghParams}`;
|
|
166
202
|
|
|
167
|
-
const classicScopes = Array.isArray(defaults.githubClassicScopes)
|
|
203
|
+
const classicScopes = Array.isArray(defaults.githubClassicScopes)
|
|
204
|
+
? defaults.githubClassicScopes
|
|
205
|
+
: ["repo"];
|
|
168
206
|
const githubClassicUrl = `https://github.com/settings/tokens/new?${new URLSearchParams({ description: name, scopes: classicScopes.join(",") })}`;
|
|
169
207
|
|
|
170
208
|
const provider = String(cfg.provider ?? "gitlab").toLowerCase();
|
|
171
|
-
const active =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
209
|
+
const active =
|
|
210
|
+
{
|
|
211
|
+
gitlab: {
|
|
212
|
+
tokenFile: gitlab.tokenFile ?? path.join(configDir(), "gitlab.token"),
|
|
213
|
+
createUrl: gitlabUrl,
|
|
214
|
+
scopes: gitlabScopes,
|
|
215
|
+
name,
|
|
216
|
+
},
|
|
217
|
+
github: {
|
|
218
|
+
tokenFile:
|
|
219
|
+
(cfg.github as Record<string, any>)?.tokenFile ?? path.join(configDir(), "github.token"),
|
|
220
|
+
createUrl: githubFineUrl,
|
|
221
|
+
createUrlClassic: githubClassicUrl,
|
|
222
|
+
permissions: githubPerms,
|
|
223
|
+
name,
|
|
224
|
+
},
|
|
225
|
+
}[provider] ?? {};
|
|
186
226
|
|
|
187
227
|
return {
|
|
188
228
|
tokenName: name,
|
|
@@ -190,7 +230,12 @@ export function vcsTokenCreateUrls(): Record<string, any> {
|
|
|
190
230
|
activeProvider: provider,
|
|
191
231
|
active,
|
|
192
232
|
gitlab: { host, createUrl: gitlabUrl, scopes: gitlabScopes, tokenFile: gitlab.tokenFile },
|
|
193
|
-
github: {
|
|
233
|
+
github: {
|
|
234
|
+
createUrl: githubFineUrl,
|
|
235
|
+
createUrlClassic: githubClassicUrl,
|
|
236
|
+
permissions: githubPerms,
|
|
237
|
+
tokenFile: (cfg.github as Record<string, any>)?.tokenFile,
|
|
238
|
+
},
|
|
194
239
|
};
|
|
195
240
|
}
|
|
196
241
|
|
|
@@ -204,7 +249,10 @@ export function mergedPrStyle(limit = 6): Record<string, any> {
|
|
|
204
249
|
|
|
205
250
|
const descInfo = (desc: string, caseInsensitiveNotes = false): Record<string, any> => ({
|
|
206
251
|
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
207
|
-
sections: desc
|
|
252
|
+
sections: desc
|
|
253
|
+
.split("\n")
|
|
254
|
+
.filter((l) => l.startsWith("## "))
|
|
255
|
+
.map((l) => l.trim()),
|
|
208
256
|
descriptionPreview: desc.slice(0, 600),
|
|
209
257
|
});
|
|
210
258
|
|
|
@@ -217,18 +265,28 @@ export function mergedPrStyle(limit = 6): Record<string, any> {
|
|
|
217
265
|
const project = m[1];
|
|
218
266
|
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
219
267
|
const run = (args: string[]) => spawnSync("glab", ["api", ...args], { encoding: "utf8", env });
|
|
220
|
-
let r = run([
|
|
268
|
+
let r = run([
|
|
269
|
+
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`,
|
|
270
|
+
]);
|
|
221
271
|
if (r.status !== 0) {
|
|
222
272
|
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
223
273
|
}
|
|
224
274
|
if (r.status !== 0) return { ok: false, error: "could not list merge requests" };
|
|
225
275
|
for (const mr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|
|
226
276
|
const desc = String(mr.description ?? "").trim();
|
|
227
|
-
examples.push({
|
|
277
|
+
examples.push({
|
|
278
|
+
title: mr.title,
|
|
279
|
+
url: mr.web_url,
|
|
280
|
+
squash: mr.squash,
|
|
281
|
+
...descInfo(desc, true),
|
|
282
|
+
});
|
|
228
283
|
}
|
|
229
284
|
} else if (provider === "github") {
|
|
230
|
-
const r = spawnSync(
|
|
231
|
-
|
|
285
|
+
const r = spawnSync(
|
|
286
|
+
"gh",
|
|
287
|
+
["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"],
|
|
288
|
+
{ encoding: "utf8", env: { ...process.env, GH_TOKEN: token } },
|
|
289
|
+
);
|
|
232
290
|
if (r.status !== 0) return { ok: false, error: "could not list pull requests" };
|
|
233
291
|
for (const pr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|
|
234
292
|
const desc = String(pr.body ?? "").trim();
|
package/src/core/verify-parse.ts
CHANGED
|
@@ -3,7 +3,7 @@ export function parseVerifyOutput(stdout: string): Record<string, any> {
|
|
|
3
3
|
const commands = [];
|
|
4
4
|
const parts = stdout.split(/\n## /);
|
|
5
5
|
for (const part of parts.slice(1)) {
|
|
6
|
-
const nl = part.indexOf(
|
|
6
|
+
const nl = part.indexOf("\n");
|
|
7
7
|
const label = part.slice(0, nl).trim();
|
|
8
8
|
const body = part.slice(nl + 1);
|
|
9
9
|
const cmdMatch = body.match(/^command: (.+)$/m);
|
|
@@ -18,7 +18,9 @@ export function parseVerifyOutput(stdout: string): Record<string, any> {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const summaryMatch = stdout.match(
|
|
21
|
+
const summaryMatch = stdout.match(
|
|
22
|
+
/# Summary[\s\S]*?passed: (\d+)[\s\S]*?failed: (\d+)[\s\S]*?skipped: (\d+)/,
|
|
23
|
+
);
|
|
22
24
|
const passed = summaryMatch ? Number(summaryMatch[1]) : 0;
|
|
23
25
|
const failed = summaryMatch ? Number(summaryMatch[2]) : 0;
|
|
24
26
|
const skipped = summaryMatch ? Number(summaryMatch[3]) : 0;
|
package/src/core/workspaces.ts
CHANGED
|
@@ -70,12 +70,12 @@ export const resolveWorkspace = (cwd: string): WorkspaceConfig | null => {
|
|
|
70
70
|
if (!parsed || typeof parsed !== "object") return null;
|
|
71
71
|
const list = (parsed as { workspaces?: unknown }).workspaces;
|
|
72
72
|
if (!Array.isArray(list)) return null;
|
|
73
|
-
const cwdPosix = cwd.
|
|
73
|
+
const cwdPosix = cwd.replaceAll("\\", "/");
|
|
74
74
|
for (const entry of list) {
|
|
75
75
|
if (!entry || typeof entry !== "object") continue;
|
|
76
76
|
const ws = entry as WorkspaceConfig;
|
|
77
77
|
if (typeof ws.glob !== "string" || !ws.glob) continue;
|
|
78
|
-
if (globToRegExp(ws.glob).test(cwdPosix)) return ws;
|
|
78
|
+
if (globToRegExp(ws.glob.replaceAll("\\", "/")).test(cwdPosix)) return ws;
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
81
81
|
};
|