@brainervirus/workit-core 0.6.0 → 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/package.json +9 -9
- package/scripts/_shared/common.sh +18 -3
- package/scripts/install-opencode-plugin.sh +14 -9
- 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 +48 -15
- package/src/core/detector.ts +22 -11
- package/src/core/docs-repo.ts +49 -14
- 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 +93 -34
- package/src/core/verify-parse.ts +4 -2
- package/src/core/workspaces.ts +2 -2
- package/src/core/youtrack.ts +231 -56
- 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 +115 -52
- package/templates/superpowers-doc-contract.md +1 -1
package/src/core/hygiene.ts
CHANGED
|
@@ -3,7 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { changelogUnreleasedStats } from "./changelog";
|
|
5
5
|
|
|
6
|
-
export type HygieneFile =
|
|
6
|
+
export type HygieneFile =
|
|
7
|
+
| "CHANGELOG.md"
|
|
8
|
+
| "README.md"
|
|
9
|
+
| ".editorconfig"
|
|
10
|
+
| ".gitattributes"
|
|
11
|
+
| "LICENSE"
|
|
12
|
+
| "CONTRIBUTING.md";
|
|
7
13
|
type State = "missing" | "invalid" | "ok" | "skip";
|
|
8
14
|
|
|
9
15
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
@@ -11,7 +17,11 @@ const templatesDir = () => path.join(repoRoot, "templates", "hygiene");
|
|
|
11
17
|
|
|
12
18
|
const packageJson = (root: string): Record<string, unknown> | null => {
|
|
13
19
|
if (!existsSync(path.join(root, "package.json"))) return null;
|
|
14
|
-
try {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
const isOpenSource = (root: string): boolean => {
|
|
@@ -34,15 +44,27 @@ const licenseHolder = (root: string): string => {
|
|
|
34
44
|
return "";
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
export const hygieneFiles = (
|
|
47
|
+
export const hygieneFiles = (
|
|
48
|
+
root: string,
|
|
49
|
+
): { state: Record<HygieneFile, State>; openSource: boolean } => {
|
|
38
50
|
const openSource = isOpenSource(root);
|
|
39
51
|
const state = {} as Record<HygieneFile, State>;
|
|
40
|
-
for (const file of [
|
|
52
|
+
for (const file of [
|
|
53
|
+
"CHANGELOG.md",
|
|
54
|
+
"README.md",
|
|
55
|
+
".editorconfig",
|
|
56
|
+
".gitattributes",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"CONTRIBUTING.md",
|
|
59
|
+
] as HygieneFile[]) {
|
|
41
60
|
if (file === "LICENSE" || file === "CONTRIBUTING.md") {
|
|
42
61
|
state[file] = openSource ? (existsSync(path.join(root, file)) ? "ok" : "missing") : "skip";
|
|
43
62
|
continue;
|
|
44
63
|
}
|
|
45
|
-
if (!existsSync(path.join(root, file))) {
|
|
64
|
+
if (!existsSync(path.join(root, file))) {
|
|
65
|
+
state[file] = "missing";
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
46
68
|
if (file === "CHANGELOG.md") {
|
|
47
69
|
const stats = changelogUnreleasedStats(root);
|
|
48
70
|
state[file] = stats.exists && stats.has_unreleased ? "ok" : "invalid";
|
package/src/core/init.ts
CHANGED
|
@@ -9,7 +9,11 @@ import { youTrackTokenCreateUrl, youTrackVerifyToken } from "./youtrack";
|
|
|
9
9
|
const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
10
10
|
|
|
11
11
|
const readJson = (p: string): Record<string, any> | null => {
|
|
12
|
-
try {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, any>;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
const isPlaceholder = (text: string): boolean =>
|
|
@@ -19,7 +23,11 @@ const modeOk = (p: string): boolean =>
|
|
|
19
23
|
process.platform === "win32" || (fs.statSync(p).mode & 0o777) === 0o600;
|
|
20
24
|
|
|
21
25
|
const resolvePath = (p: string): string => {
|
|
22
|
-
try {
|
|
26
|
+
try {
|
|
27
|
+
return fs.realpathSync(p);
|
|
28
|
+
} catch {
|
|
29
|
+
return path.resolve(p);
|
|
30
|
+
}
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
/** Port of scripts/init/status.sh — filesystem init state. */
|
|
@@ -50,11 +58,14 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
|
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
const expanded = tokenFile ? path.resolve(tokenFile) : null;
|
|
53
|
-
const resolvedTokenFile =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const resolvedTokenFile =
|
|
62
|
+
expanded && fs.existsSync(expanded)
|
|
63
|
+
? resolvePath(expanded)
|
|
64
|
+
: expanded
|
|
65
|
+
? path.isAbsolute(expanded)
|
|
66
|
+
? expanded
|
|
67
|
+
: path.resolve(configDirPath, expanded)
|
|
68
|
+
: null;
|
|
58
69
|
youtrackConfig = {
|
|
59
70
|
config_edit_path: resolvePath(ytJson),
|
|
60
71
|
baseUrl: base,
|
|
@@ -67,8 +78,18 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
|
|
|
67
78
|
tokenFile: resolvedTokenFile,
|
|
68
79
|
tokenDefaults: ytParsed.tokenDefaults,
|
|
69
80
|
timeLogging: {
|
|
70
|
-
meetings: {
|
|
71
|
-
|
|
81
|
+
meetings: {
|
|
82
|
+
options: meetingIssues,
|
|
83
|
+
skill: "/wk-meetings",
|
|
84
|
+
logsTime: true,
|
|
85
|
+
postsComment: false,
|
|
86
|
+
},
|
|
87
|
+
taskWork: {
|
|
88
|
+
issueSource: "active spec/plan **YouTrack:** field or --issue",
|
|
89
|
+
skill: "/wk-issue-update",
|
|
90
|
+
logsTime: true,
|
|
91
|
+
postsComment: true,
|
|
92
|
+
},
|
|
72
93
|
},
|
|
73
94
|
};
|
|
74
95
|
youtrackTokenCreate = youTrackTokenCreateUrl().data;
|
|
@@ -91,20 +112,28 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
|
|
|
91
112
|
const ytTokenPath = youtrackConfig?.tokenFile ?? path.join(configDirPath, "youtrack.token");
|
|
92
113
|
const tokenText = fs.existsSync(ytTokenPath) ? fs.readFileSync(ytTokenPath, "utf8").trim() : "";
|
|
93
114
|
const placeholder = fs.existsSync(ytTokenPath) && isPlaceholder(tokenText);
|
|
94
|
-
const tokenOk =
|
|
115
|
+
const tokenOk =
|
|
116
|
+
fs.existsSync(ytTokenPath) &&
|
|
117
|
+
modeOk(ytTokenPath) &&
|
|
118
|
+
Boolean(tokenText) &&
|
|
119
|
+
!isPlaceholder(tokenText);
|
|
95
120
|
|
|
96
121
|
const youtrackTokenItem: Record<string, any> = {
|
|
97
122
|
id: "youtrack_token",
|
|
98
123
|
label: "YouTrack API token (mode 600, not placeholder)",
|
|
99
124
|
ok: tokenOk,
|
|
100
125
|
path: fs.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path.resolve(ytTokenPath),
|
|
101
|
-
token_edit_path: fs.existsSync(ytTokenPath)
|
|
126
|
+
token_edit_path: fs.existsSync(ytTokenPath)
|
|
127
|
+
? resolvePath(ytTokenPath)
|
|
128
|
+
: path.resolve(ytTokenPath),
|
|
102
129
|
placeholder,
|
|
103
130
|
fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER} with your permanent token, save, then /wk-status`,
|
|
104
131
|
};
|
|
105
132
|
if (youtrackTokenCreate) {
|
|
106
|
-
if (youtrackTokenCreate.createUrl)
|
|
107
|
-
|
|
133
|
+
if (youtrackTokenCreate.createUrl)
|
|
134
|
+
youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
|
|
135
|
+
if (youtrackTokenCreate.docsUrl)
|
|
136
|
+
youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
|
|
108
137
|
if (youtrackTokenCreate.scopes) youtrackTokenItem.token_scopes = youtrackTokenCreate.scopes;
|
|
109
138
|
if (youtrackTokenCreate.tokenName) youtrackTokenItem.token_name = youtrackTokenCreate.tokenName;
|
|
110
139
|
if (youtrackTokenCreate.steps) youtrackTokenItem.token_create_steps = youtrackTokenCreate.steps;
|
|
@@ -181,8 +210,22 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
|
|
|
181
210
|
for (const k of ["gitlab", "github"]) {
|
|
182
211
|
vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path.join(configDirPath, `${k}.token`));
|
|
183
212
|
}
|
|
184
|
-
items.push(
|
|
185
|
-
|
|
213
|
+
items.push(
|
|
214
|
+
tokenItem(
|
|
215
|
+
"gitlab_token",
|
|
216
|
+
"GitLab token (mode 600, not placeholder)",
|
|
217
|
+
vcsTokenFiles.gitlab,
|
|
218
|
+
"gitlab",
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
items.push(
|
|
222
|
+
tokenItem(
|
|
223
|
+
"github_token",
|
|
224
|
+
"GitHub token (mode 600, not placeholder)",
|
|
225
|
+
vcsTokenFiles.github,
|
|
226
|
+
"github",
|
|
227
|
+
),
|
|
228
|
+
);
|
|
186
229
|
|
|
187
230
|
return {
|
|
188
231
|
config_dir: configDirPath,
|
|
@@ -218,7 +261,8 @@ export function toolkitStatusData(configDirPath = configDir()): Record<string, a
|
|
|
218
261
|
status.youtrack_verify = verify;
|
|
219
262
|
status.youtrack_ok = placeholder ? false : Boolean((verify as Record<string, any>).ok);
|
|
220
263
|
status.vcs_verify = vcsVerify;
|
|
221
|
-
status.vcs_ok =
|
|
264
|
+
status.vcs_ok =
|
|
265
|
+
vcsJsonOk && !vcsPlaceholder ? Boolean((vcsVerify as Record<string, any>).ok) : false;
|
|
222
266
|
|
|
223
267
|
const fsReady = status.items.every((i: Record<string, any>) => i.required === false || i.ok);
|
|
224
268
|
status.ready = fsReady && Boolean(status.youtrack_ok) && (!vcsJsonOk || Boolean(status.vcs_ok));
|
|
@@ -302,7 +346,10 @@ const vcsJsonContent = (dir: string): Record<string, any> => ({
|
|
|
302
346
|
});
|
|
303
347
|
|
|
304
348
|
/** Port of scripts/init/apply.sh — confirmed scaffold actions. */
|
|
305
|
-
export function initApplyData(
|
|
349
|
+
export function initApplyData(
|
|
350
|
+
action: string,
|
|
351
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
352
|
+
): Record<string, any> {
|
|
306
353
|
const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
|
|
307
354
|
fs.mkdirSync(dir, { recursive: true });
|
|
308
355
|
|
|
@@ -360,7 +407,12 @@ export function initApplyData(action: string, env: NodeJS.ProcessEnv = process.e
|
|
|
360
407
|
locale: cfg.locale,
|
|
361
408
|
tokenCreate,
|
|
362
409
|
timeLogging: {
|
|
363
|
-
meetings: {
|
|
410
|
+
meetings: {
|
|
411
|
+
issue: meeting,
|
|
412
|
+
skill: "/wk-meetings",
|
|
413
|
+
logsTime: true,
|
|
414
|
+
postsComment: false,
|
|
415
|
+
},
|
|
364
416
|
taskWork: {
|
|
365
417
|
issueSource: "active spec/plan **YouTrack:** field or --issue",
|
|
366
418
|
skill: "/wk-issue-update",
|
|
@@ -421,21 +473,34 @@ export function initApplyData(action: string, env: NodeJS.ProcessEnv = process.e
|
|
|
421
473
|
}
|
|
422
474
|
}
|
|
423
475
|
default: {
|
|
424
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)`,
|
|
478
|
+
};
|
|
425
479
|
}
|
|
426
480
|
}
|
|
427
481
|
}
|
|
428
482
|
|
|
429
483
|
export function initStatus(): Record<string, any> {
|
|
430
484
|
const data = initStatusData();
|
|
431
|
-
return {
|
|
485
|
+
return {
|
|
486
|
+
...data,
|
|
487
|
+
workspaces: { resolved: resolveWorkspace(process.cwd()), path: workspacesPath() },
|
|
488
|
+
};
|
|
432
489
|
}
|
|
433
490
|
|
|
434
491
|
export function toolkitStatus(): Record<string, any> {
|
|
435
492
|
return toolkitStatusData();
|
|
436
493
|
}
|
|
437
494
|
|
|
438
|
-
export function initApply({
|
|
495
|
+
export function initApply({
|
|
496
|
+
action,
|
|
497
|
+
confirmed,
|
|
498
|
+
env,
|
|
499
|
+
}: {
|
|
500
|
+
action: string;
|
|
501
|
+
confirmed: boolean;
|
|
502
|
+
env?: Record<string, string>;
|
|
503
|
+
}): Record<string, any> {
|
|
439
504
|
if (!confirmed) return { error: "confirmed: true required" };
|
|
440
505
|
return { data: initApplyData(action, env ? { ...process.env, ...env } : process.env) };
|
|
441
506
|
}
|
|
@@ -3,7 +3,7 @@ export function parseSections(stdout: string): Record<string, string> {
|
|
|
3
3
|
const sections: Record<string, string> = {};
|
|
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 title = part.slice(0, nl).trim();
|
|
8
8
|
sections[title] = part.slice(nl + 1).trim();
|
|
9
9
|
}
|
|
@@ -12,7 +12,7 @@ export function parseSections(stdout: string): Record<string, string> {
|
|
|
12
12
|
|
|
13
13
|
export function parseKeyValueLines(text: string, keys: string[]): Record<string, string> {
|
|
14
14
|
const out: Record<string, string> = {};
|
|
15
|
-
for (const line of text.split(
|
|
15
|
+
for (const line of text.split("\n")) {
|
|
16
16
|
for (const key of keys) {
|
|
17
17
|
const prefix = `${key}: `;
|
|
18
18
|
if (line.startsWith(prefix)) {
|
package/src/core/plan-tasks.ts
CHANGED
|
@@ -4,13 +4,19 @@ import { parseTasksFromPlan } from "./docs-validate";
|
|
|
4
4
|
import { resolveBranch } from "./branch";
|
|
5
5
|
|
|
6
6
|
const readSafe = (p: string): string | null => {
|
|
7
|
-
try {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(p, "utf8");
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
export function parsePlanTasks(
|
|
11
15
|
planPath: string,
|
|
12
16
|
workspaceRoot: string,
|
|
13
|
-
):
|
|
17
|
+
):
|
|
18
|
+
| { task_count: number; tasks: { id: number; title: string; section_text: string }[] }
|
|
19
|
+
| { error: string } {
|
|
14
20
|
const cwd = path.resolve(workspaceRoot);
|
|
15
21
|
const resolved = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
|
|
16
22
|
const text = readSafe(resolved);
|
|
@@ -27,7 +33,11 @@ export function resolveHandoffBranch(
|
|
|
27
33
|
planPath: string,
|
|
28
34
|
workspaceRoot: string,
|
|
29
35
|
): { branch: string } | { error: string } {
|
|
30
|
-
const resolved = resolveBranch({
|
|
36
|
+
const resolved = resolveBranch({
|
|
37
|
+
spec_path: specPath,
|
|
38
|
+
plan_path: planPath,
|
|
39
|
+
workspace_root: workspaceRoot,
|
|
40
|
+
});
|
|
31
41
|
if ("error" in resolved) return { error: resolved.error as string };
|
|
32
42
|
return { branch: resolved.branch };
|
|
33
43
|
}
|
|
@@ -5,7 +5,9 @@ const args = process.argv.slice(2);
|
|
|
5
5
|
const cmd = args[0];
|
|
6
6
|
if (cmd === "log-time" || cmd === "post-comment") {
|
|
7
7
|
if ((process.env.WORKFLOW_YT_WRITE ?? "") !== "1") {
|
|
8
|
-
console.error(
|
|
8
|
+
console.error(
|
|
9
|
+
"ERROR: YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)",
|
|
10
|
+
);
|
|
9
11
|
process.exit(1);
|
|
10
12
|
}
|
|
11
13
|
}
|
package/src/core/pr-create.ts
CHANGED
|
@@ -61,7 +61,8 @@ function buildBody(
|
|
|
61
61
|
return body ? `${body}\n\n${line}` : line;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const truthy = (v: string | undefined): boolean =>
|
|
64
|
+
const truthy = (v: string | undefined): boolean =>
|
|
65
|
+
["1", "true", "yes"].includes(String(v ?? "").toLowerCase());
|
|
65
66
|
|
|
66
67
|
// Port of python's shutil.which — scan PATH in-process (no `which` binary needed).
|
|
67
68
|
function whichOnPath(tool: string): string | null {
|
|
@@ -71,7 +72,9 @@ function whichOnPath(tool: string): string | null {
|
|
|
71
72
|
try {
|
|
72
73
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
73
74
|
return candidate;
|
|
74
|
-
} catch {
|
|
75
|
+
} catch {
|
|
76
|
+
/* keep scanning */
|
|
77
|
+
}
|
|
75
78
|
}
|
|
76
79
|
return null;
|
|
77
80
|
}
|
|
@@ -107,12 +110,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
107
110
|
const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? repoRoot(cwd);
|
|
108
111
|
const cfg = vcsConfig("load", root);
|
|
109
112
|
if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
|
|
110
|
-
if (!cfg.tokenReady)
|
|
113
|
+
if (!cfg.tokenReady)
|
|
114
|
+
return { error: "VCS token not ready — run /wk-init and edit token file locally" };
|
|
111
115
|
|
|
112
116
|
const provider = cfg.provider as string;
|
|
113
|
-
if (provider !== "gitlab" && provider !== "github")
|
|
117
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
118
|
+
return { error: `unsupported provider: ${provider}` };
|
|
114
119
|
const cli = provider === "gitlab" ? "glab" : "gh";
|
|
115
|
-
const installUrl =
|
|
120
|
+
const installUrl =
|
|
121
|
+
provider === "gitlab" ? "https://gitlab.com/gitlab-org/cli" : "https://cli.github.com";
|
|
116
122
|
if (whichOnPath(cli) === null) {
|
|
117
123
|
return {
|
|
118
124
|
ok: false,
|
|
@@ -129,16 +135,23 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
129
135
|
const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
|
|
130
136
|
const target = env.WF_PR_TARGET || String(cfg.defaultTargetBranch ?? "develop");
|
|
131
137
|
|
|
132
|
-
const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
138
|
+
const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
139
|
+
cwd: root,
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
});
|
|
133
142
|
const branch = br.status === 0 ? (br.stdout ?? "").trim() : "";
|
|
134
143
|
|
|
135
144
|
let baseUrl = cfg.youtrack_base_url as string | undefined;
|
|
136
145
|
if (!baseUrl) {
|
|
137
|
-
const ytCfg =
|
|
146
|
+
const ytCfg =
|
|
147
|
+
process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
148
|
+
path.join(path.dirname(String(cfg.configPath)), "youtrack.json");
|
|
138
149
|
try {
|
|
139
150
|
const yt = JSON.parse(fs.readFileSync(ytCfg, "utf8")) as Record<string, any>;
|
|
140
151
|
if (yt && typeof yt === "object") baseUrl = yt.baseUrl;
|
|
141
|
-
} catch {
|
|
152
|
+
} catch {
|
|
153
|
+
/* optional */
|
|
154
|
+
}
|
|
142
155
|
}
|
|
143
156
|
const ghLinkOnPr = cfg.issues_provider === "github" && cfg.link_on_pr === true;
|
|
144
157
|
let ghRepo: string | null = null;
|
|
@@ -147,8 +160,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
147
160
|
if (rr.status === 0) ghRepo = parseGhRepo(rr.stdout ?? "");
|
|
148
161
|
}
|
|
149
162
|
const finalBody = buildBody(
|
|
150
|
-
body,
|
|
151
|
-
|
|
163
|
+
body,
|
|
164
|
+
branch,
|
|
165
|
+
cfg.link_issues === true,
|
|
166
|
+
baseUrl ?? "",
|
|
167
|
+
env.WORKFLOW_YT_ISSUE ?? "",
|
|
168
|
+
ghLinkOnPr,
|
|
169
|
+
env.WORKFLOW_GH_ISSUE ?? "",
|
|
170
|
+
env.WORKFLOW_GH_ISSUE_RELATION ?? "closes",
|
|
171
|
+
ghRepo,
|
|
152
172
|
);
|
|
153
173
|
|
|
154
174
|
const squash = pr.squashOnMerge !== false;
|
|
@@ -178,9 +198,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
178
198
|
if (result.status !== 0) {
|
|
179
199
|
const err = (result.stderr ?? result.stdout ?? "").trim();
|
|
180
200
|
let hint: Record<string, any> | null = null;
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
if (
|
|
202
|
+
provider === "gitlab" &&
|
|
203
|
+
(err.includes("409") || err.toLowerCase().includes("already exists"))
|
|
204
|
+
) {
|
|
205
|
+
const list = spawnSync("glab", ["mr", "list", `--source-branch=${branch}`, "--output=json"], {
|
|
206
|
+
cwd: root,
|
|
207
|
+
encoding: "utf8",
|
|
208
|
+
env: cmdEnv,
|
|
209
|
+
});
|
|
184
210
|
if (list.status === 0 && (list.stdout ?? "").trim()) {
|
|
185
211
|
try {
|
|
186
212
|
const mrs = JSON.parse(list.stdout ?? "") as Array<Record<string, any>>;
|
|
@@ -191,10 +217,16 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
191
217
|
next_step: "Use glab mr update or close the open MR before creating again",
|
|
192
218
|
};
|
|
193
219
|
}
|
|
194
|
-
} catch {
|
|
220
|
+
} catch {
|
|
221
|
+
/* no hint */
|
|
222
|
+
}
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
|
-
const payload: Record<string, any> = {
|
|
225
|
+
const payload: Record<string, any> = {
|
|
226
|
+
error: "create failed",
|
|
227
|
+
provider,
|
|
228
|
+
stderr: err.slice(0, 800),
|
|
229
|
+
};
|
|
198
230
|
if (hint) payload.hint = hint;
|
|
199
231
|
return payload;
|
|
200
232
|
}
|
package/src/core/present.ts
CHANGED
|
@@ -30,7 +30,12 @@ export function renderAsciiWireframe(spec: unknown): string {
|
|
|
30
30
|
}
|
|
31
31
|
if (kind === "button") {
|
|
32
32
|
const label = "[ " + String(row?.label ?? "Button") + " ]";
|
|
33
|
-
lines.push(
|
|
33
|
+
lines.push(
|
|
34
|
+
boxLine(
|
|
35
|
+
label.padStart(Math.max(0, (width - 4 + label.length) / 2)).padEnd(width - 4),
|
|
36
|
+
width,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
36
41
|
if (kind === "field") {
|
|
@@ -42,7 +47,11 @@ export function renderAsciiWireframe(spec: unknown): string {
|
|
|
42
47
|
if (kind === "columns") {
|
|
43
48
|
const cols = Array.isArray(row?.columns) ? row.columns : [];
|
|
44
49
|
const colW = Math.floor((width - 4 - cols.length + 1) / Math.max(cols.length, 1));
|
|
45
|
-
const parts = cols.map((c: any) =>
|
|
50
|
+
const parts = cols.map((c: any) =>
|
|
51
|
+
String(c?.label ?? "")
|
|
52
|
+
.slice(0, Math.max(0, colW - 1))
|
|
53
|
+
.padEnd(Math.max(0, colW)),
|
|
54
|
+
);
|
|
46
55
|
lines.push(boxLine(parts.join(" | ").trim(), width));
|
|
47
56
|
continue;
|
|
48
57
|
}
|
package/src/core/reminder.ts
CHANGED
|
@@ -57,8 +57,7 @@ Skill: receiving-code-review. Verify before implementing — evaluate review fee
|
|
|
57
57
|
export const shouldInjectVerification = (currentText: string): boolean =>
|
|
58
58
|
!currentText.includes(VERIFICATION_TEXT);
|
|
59
59
|
|
|
60
|
-
export const shouldInjectTdd = (currentText: string): boolean =>
|
|
61
|
-
!currentText.includes(TDD_TEXT);
|
|
60
|
+
export const shouldInjectTdd = (currentText: string): boolean => !currentText.includes(TDD_TEXT);
|
|
62
61
|
|
|
63
62
|
export const shouldInjectBrainstorm = (currentText: string): boolean =>
|
|
64
63
|
!currentText.includes(BRAINSTORM_TEXT);
|
package/src/core/repo-tool.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { resolveWorkspaceRoot } from "./scripts";
|
|
2
2
|
|
|
3
3
|
/** Attach the resolved repository root to each repository tool response. */
|
|
4
|
-
export function withWorkspace(
|
|
4
|
+
export function withWorkspace(
|
|
5
|
+
workspaceRoot: string,
|
|
6
|
+
data: Record<string, any> = {},
|
|
7
|
+
): Record<string, any> {
|
|
5
8
|
return {
|
|
6
9
|
workspace_root: resolveWorkspaceRoot(workspaceRoot),
|
|
7
10
|
...data,
|
package/src/core/rules.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { configDir } from "./config";
|
|
5
4
|
|
|
6
5
|
export type RulePlatform = "cursor" | "opencode";
|
|
@@ -11,13 +10,12 @@ export type CanonicalRule = {
|
|
|
11
10
|
body: string;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
15
|
-
|
|
16
13
|
export const rulesDir = () => path.join(configDir(), "rules");
|
|
17
14
|
|
|
18
15
|
export const parseRule = (markdown: string): CanonicalRule | { error: string } => {
|
|
19
16
|
const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
20
|
-
if (!fm)
|
|
17
|
+
if (!fm)
|
|
18
|
+
return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
|
|
21
19
|
const meta: Record<string, string> = {};
|
|
22
20
|
const unquote = (v: string) => v.replace(/^["']|["']$/g, "").trim();
|
|
23
21
|
for (const line of fm[1].split("\n")) {
|
|
@@ -27,9 +25,13 @@ export const parseRule = (markdown: string): CanonicalRule | { error: string } =
|
|
|
27
25
|
const name = meta.name ?? "";
|
|
28
26
|
const description = meta.description ?? "";
|
|
29
27
|
const rawPlatforms = (meta.platforms ?? "")
|
|
30
|
-
.replace(/^\[|\]$/g, "")
|
|
28
|
+
.replace(/^\[|\]$/g, "")
|
|
29
|
+
.split(",")
|
|
30
|
+
.map((p) => p.trim().replace(/['"]/g, ""))
|
|
31
31
|
.filter(Boolean);
|
|
32
|
-
const platforms = rawPlatforms.filter(
|
|
32
|
+
const platforms = rawPlatforms.filter(
|
|
33
|
+
(p): p is RulePlatform => p === "cursor" || p === "opencode",
|
|
34
|
+
);
|
|
33
35
|
if (!name || !description || platforms.length === 0) {
|
|
34
36
|
return { error: "rule frontmatter requires name, description, and platforms" };
|
|
35
37
|
}
|
|
@@ -73,7 +75,8 @@ export const writeRule = (
|
|
|
73
75
|
confirmed: boolean,
|
|
74
76
|
): { ok: true; path: string } | { ok: false; error: string } => {
|
|
75
77
|
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
76
|
-
if (!RULE_NAME_RE.test(rule.name))
|
|
78
|
+
if (!RULE_NAME_RE.test(rule.name))
|
|
79
|
+
return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
|
|
77
80
|
const dir = path.join(rulesDir(), rule.name);
|
|
78
81
|
mkdirSync(dir, { recursive: true });
|
|
79
82
|
const file = path.join(dir, "rule.md");
|
package/src/core/scripts.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function runScript(
|
|
|
18
18
|
const result = spawnSync("bash", [scriptPath, ...args], {
|
|
19
19
|
cwd,
|
|
20
20
|
encoding: "utf8",
|
|
21
|
-
env: { ...process.env, ...
|
|
21
|
+
env: { ...process.env, ...extraEnv },
|
|
22
22
|
});
|
|
23
23
|
return {
|
|
24
24
|
stdout: result.stdout ?? "",
|
|
@@ -29,7 +29,12 @@ export function runScript(
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function runScriptJson(
|
|
32
|
+
export function runScriptJson(
|
|
33
|
+
scriptName: string,
|
|
34
|
+
args: string[],
|
|
35
|
+
workspaceRoot: string,
|
|
36
|
+
extraEnv?: Record<string, string>,
|
|
37
|
+
) {
|
|
33
38
|
const { stdout, stderr, exitCode } = runScript(scriptName, args, workspaceRoot, extraEnv);
|
|
34
39
|
if (exitCode !== 0) {
|
|
35
40
|
return { error: (stderr || stdout || "script failed").trim(), exitCode };
|
package/src/core/sdd.ts
CHANGED
|
@@ -57,7 +57,10 @@ export function sddContext({
|
|
|
57
57
|
let completed_task_ids: number[] = [];
|
|
58
58
|
const absProgress = path.join(cwd, progress_path);
|
|
59
59
|
if (existsSync(absProgress)) {
|
|
60
|
-
progress_lines = readFileSync(absProgress, "utf8")
|
|
60
|
+
progress_lines = readFileSync(absProgress, "utf8")
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((ln) => ln.trim())
|
|
63
|
+
.filter(Boolean);
|
|
61
64
|
const pat = /^Task\s+(\d+):\s+complete\b/i;
|
|
62
65
|
completed_task_ids = progress_lines
|
|
63
66
|
.map((ln) => pat.exec(ln)?.[1])
|
|
@@ -68,7 +71,11 @@ export function sddContext({
|
|
|
68
71
|
let manifest: Record<string, unknown> = {};
|
|
69
72
|
const absManifest = path.join(cwd, manifest_path);
|
|
70
73
|
if (existsSync(absManifest)) {
|
|
71
|
-
try {
|
|
74
|
+
try {
|
|
75
|
+
manifest = JSON.parse(readFileSync(absManifest, "utf8"));
|
|
76
|
+
} catch {
|
|
77
|
+
manifest = {};
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
const legacy_path = path.join(cwd, ".superpowers/sdd");
|
|
@@ -83,7 +90,8 @@ export function sddContext({
|
|
|
83
90
|
const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
|
|
84
91
|
if (spec_path) {
|
|
85
92
|
const validated = docsValidate({ spec_path, plan_path, workspace_root: cwd });
|
|
86
|
-
if (validated.ok === false)
|
|
93
|
+
if (validated.ok === false)
|
|
94
|
+
return { ok: false, errors: validated.errors, error: validated.error };
|
|
87
95
|
}
|
|
88
96
|
const tasks = parseTasksFromPlan(planText);
|
|
89
97
|
if (tasks.length > 0) {
|
package/src/core/templates.ts
CHANGED
|
@@ -7,12 +7,18 @@ export type TemplateName = "issue-update" | "greeting" | "headers";
|
|
|
7
7
|
|
|
8
8
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
9
|
|
|
10
|
-
export const templatePath = (name: TemplateName): string =>
|
|
10
|
+
export const templatePath = (name: TemplateName): string =>
|
|
11
|
+
path.join(configDir(), "templates", `${name}.md`);
|
|
11
12
|
|
|
12
|
-
export const readTemplate = (
|
|
13
|
+
export const readTemplate = (
|
|
14
|
+
name: TemplateName,
|
|
15
|
+
): { source: "config" | "repo"; content: string } => {
|
|
13
16
|
const cfg = templatePath(name);
|
|
14
17
|
if (existsSync(cfg)) return { source: "config", content: readFileSync(cfg, "utf8") };
|
|
15
|
-
return {
|
|
18
|
+
return {
|
|
19
|
+
source: "repo",
|
|
20
|
+
content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8"),
|
|
21
|
+
};
|
|
16
22
|
};
|
|
17
23
|
|
|
18
24
|
export const writeTemplate = (
|
|
@@ -27,7 +33,11 @@ export const writeTemplate = (
|
|
|
27
33
|
return { ok: true, path: file };
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
export const listTemplates = (): {
|
|
36
|
+
export const listTemplates = (): {
|
|
37
|
+
name: TemplateName;
|
|
38
|
+
source: "config" | "repo" | "missing";
|
|
39
|
+
path: string;
|
|
40
|
+
}[] =>
|
|
31
41
|
(["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
|
|
32
42
|
const cfg = templatePath(name);
|
|
33
43
|
const repoFile = path.join(repoRoot, "templates", `${name}.md`);
|