@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.
Files changed (43) hide show
  1. package/README.md +4 -17
  2. package/package.json +9 -9
  3. package/scripts/_shared/common.sh +18 -3
  4. package/scripts/install-opencode-plugin.sh +14 -9
  5. package/scripts/lib/config-dir.sh +26 -0
  6. package/scripts/sync-runtime.sh +4 -1
  7. package/src/core/branch.ts +143 -48
  8. package/src/core/changelog.ts +17 -14
  9. package/src/core/config-guard.ts +9 -2
  10. package/src/core/config.ts +95 -15
  11. package/src/core/detector.ts +22 -11
  12. package/src/core/docs-repo.ts +50 -15
  13. package/src/core/docs-validate.ts +163 -37
  14. package/src/core/flow-state.ts +6 -2
  15. package/src/core/gitignore.ts +11 -2
  16. package/src/core/handoff-context.ts +18 -5
  17. package/src/core/hygiene.ts +27 -5
  18. package/src/core/init.ts +86 -21
  19. package/src/core/parse-sections.ts +2 -2
  20. package/src/core/plan-tasks.ts +13 -3
  21. package/src/core/ports/youtrack-api.ts +3 -1
  22. package/src/core/ports/youtrack-config.ts +1 -3
  23. package/src/core/pr-create.ts +47 -15
  24. package/src/core/present.ts +11 -2
  25. package/src/core/reminder.ts +1 -2
  26. package/src/core/repo-tool.ts +4 -1
  27. package/src/core/rules.ts +10 -7
  28. package/src/core/scripts.ts +7 -2
  29. package/src/core/sdd.ts +11 -3
  30. package/src/core/templates.ts +14 -4
  31. package/src/core/vcs-config.ts +94 -36
  32. package/src/core/verify-parse.ts +4 -2
  33. package/src/core/workspaces.ts +2 -2
  34. package/src/core/youtrack.ts +233 -58
  35. package/src/core.ts +18 -3
  36. package/src/tools/docs-repo.ts +12 -3
  37. package/src/tools/flow.ts +24 -13
  38. package/src/tools/handoff.ts +28 -23
  39. package/src/tools/present.ts +14 -10
  40. package/src/tools/repo.ts +220 -87
  41. package/src/tools/sdd.ts +93 -66
  42. package/src/tools/youtrack.ts +119 -52
  43. package/templates/superpowers-doc-contract.md +1 -1
@@ -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 => process.env.WORKFLOW_WORKSPACE_ROOT ?? cwd ?? process.cwd();
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 { /* missing or invalid */ }
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 (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
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(os.homedir(), ".config", "workflow-toolkit", `${provider}.token`));
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 = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_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 = { host: prov.host ?? "gitlab.com", apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4" };
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
- const result = spawnSync("curl", ["-fsS", "-H", `PRIVATE-TOKEN: ${token}`, `${api}/user`], { encoding: "utf8" });
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 { ok: false, provider, error: "GitLab API rejected token", detail: (result.stderr ?? result.stdout ?? "").slice(0, 200) };
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"], { encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
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 { ok: false, provider, error: "GitHub API rejected token", detail: (result.stderr ?? result.stdout ?? "").slice(0, 200) };
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({ name, description: desc, scopes: gitlabScopes.join(",") });
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 ?? { pull_requests: "write", contents: "write", metadata: "read" };
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) ? defaults.githubClassicScopes : ["repo"];
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
- gitlab: {
173
- tokenFile: gitlab.tokenFile ?? path.join(os.homedir(), ".config", "workflow-toolkit", "gitlab.token"),
174
- createUrl: gitlabUrl,
175
- scopes: gitlabScopes,
176
- name,
177
- },
178
- github: {
179
- tokenFile: (cfg.github as Record<string, any>)?.tokenFile ?? path.join(os.homedir(), ".config", "workflow-toolkit", "github.token"),
180
- createUrl: githubFineUrl,
181
- createUrlClassic: githubClassicUrl,
182
- permissions: githubPerms,
183
- name,
184
- },
185
- }[provider] ?? {};
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: { createUrl: githubFineUrl, createUrlClassic: githubClassicUrl, permissions: githubPerms, tokenFile: (cfg.github as Record<string, any>)?.tokenFile },
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.split("\n").filter((l) => l.startsWith("## ")).map((l) => l.trim()),
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([`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
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({ title: mr.title, url: mr.web_url, squash: mr.squash, ...descInfo(desc, true) });
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("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"],
231
- { encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
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();
@@ -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('\n');
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(/# Summary[\s\S]*?passed: (\d+)[\s\S]*?failed: (\d+)[\s\S]*?skipped: (\d+)/);
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;
@@ -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.split(path.sep).join("/");
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
  };