@brainervirus/workit-core 0.4.0
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/LICENSE +21 -0
- package/README.md +180 -0
- package/commands/wk-changelog.md +2 -0
- package/commands/wk-commit.md +2 -0
- package/commands/wk-docs-refresh.md +2 -0
- package/commands/wk-handoff.md +2 -0
- package/commands/wk-implement.md +2 -0
- package/commands/wk-init.md +2 -0
- package/commands/wk-issue-update.md +2 -0
- package/commands/wk-meetings.md +2 -0
- package/commands/wk-pr.md +2 -0
- package/commands/wk-release-notes.md +2 -0
- package/commands/wk-status.md +2 -0
- package/commands/wk-verify.md +2 -0
- package/package.json +43 -0
- package/scripts/_shared/common.sh +158 -0
- package/scripts/changelog-context.sh +42 -0
- package/scripts/docs-refresh-context.sh +40 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/init/apply.sh +5 -0
- package/scripts/init/status.sh +5 -0
- package/scripts/init/toolkit-status.sh +5 -0
- package/scripts/install-cursor-plugin.sh +83 -0
- package/scripts/install-opencode-plugin.sh +76 -0
- package/scripts/pr-create.sh +5 -0
- package/scripts/pr-ready-context.sh +88 -0
- package/scripts/present/ascii-wireframe.sh +5 -0
- package/scripts/present/flow-diagram.sh +5 -0
- package/scripts/release-notes-context.sh +40 -0
- package/scripts/rewrite-workspace-deps.ts +17 -0
- package/scripts/run-cursor-mcp.sh +10 -0
- package/scripts/sync-runtime.sh +96 -0
- package/scripts/update-superpowers.sh +56 -0
- package/scripts/vcs/config.sh +5 -0
- package/scripts/vcs/merged-style.sh +5 -0
- package/scripts/vcs/token-create-urls.sh +5 -0
- package/scripts/vcs/verify-token.sh +5 -0
- package/scripts/verify-project.sh +140 -0
- package/scripts/youtrack/api.sh +5 -0
- package/scripts/youtrack/config.sh +5 -0
- package/scripts/youtrack/greeting.sh +5 -0
- package/scripts/youtrack/parse-duration.sh +5 -0
- package/scripts/youtrack/token-create-url.sh +5 -0
- package/scripts/youtrack/verify-token.sh +5 -0
- package/scripts/youtrack/work-date-ms.sh +5 -0
- package/skills/wk-changelog/SKILL.md +15 -0
- package/skills/wk-commit/SKILL.md +16 -0
- package/skills/wk-docs-refresh/SKILL.md +15 -0
- package/skills/wk-handoff/SKILL.md +17 -0
- package/skills/wk-implement/SKILL.md +41 -0
- package/skills/wk-init/SKILL.md +31 -0
- package/skills/wk-issue-update/SKILL.md +27 -0
- package/skills/wk-issue-update/references/youtrack-update-style.md +81 -0
- package/skills/wk-meetings/SKILL.md +17 -0
- package/skills/wk-pr/SKILL.md +27 -0
- package/skills/wk-release-notes/SKILL.md +15 -0
- package/skills/wk-status/SKILL.md +16 -0
- package/skills/wk-verify/SKILL.md +16 -0
- package/src/core/branch.ts +246 -0
- package/src/core/changelog.ts +312 -0
- package/src/core/config-guard.ts +26 -0
- package/src/core/config.ts +73 -0
- package/src/core/detector.ts +207 -0
- package/src/core/doc-render.ts +14 -0
- package/src/core/docs-repo.ts +196 -0
- package/src/core/docs-validate.ts +255 -0
- package/src/core/flow-state.ts +225 -0
- package/src/core/git.ts +56 -0
- package/src/core/gitignore.ts +43 -0
- package/src/core/handoff-context.ts +115 -0
- package/src/core/hygiene.ts +77 -0
- package/src/core/init.ts +443 -0
- package/src/core/parse-sections.ts +24 -0
- package/src/core/plan-tasks.ts +33 -0
- package/src/core/ports/init-apply.ts +15 -0
- package/src/core/ports/init-status.ts +4 -0
- package/src/core/ports/init-toolkit-status.ts +4 -0
- package/src/core/ports/pr-create.ts +22 -0
- package/src/core/ports/present-ascii.ts +10 -0
- package/src/core/ports/present-flow.ts +10 -0
- package/src/core/ports/vcs-config.ts +14 -0
- package/src/core/ports/vcs-merged-style.ts +5 -0
- package/src/core/ports/vcs-token-create-urls.ts +4 -0
- package/src/core/ports/vcs-verify-token.ts +4 -0
- package/src/core/ports/youtrack-api.ts +17 -0
- package/src/core/ports/youtrack-config.ts +23 -0
- package/src/core/ports/youtrack-greeting.ts +10 -0
- package/src/core/ports/youtrack-parse-duration.ts +14 -0
- package/src/core/ports/youtrack-token-create-url.ts +4 -0
- package/src/core/ports/youtrack-verify-token.ts +12 -0
- package/src/core/ports/youtrack-work-date-ms.ts +10 -0
- package/src/core/pr-create.ts +212 -0
- package/src/core/present.ts +100 -0
- package/src/core/reminder.ts +82 -0
- package/src/core/repo-tool.ts +9 -0
- package/src/core/rules.ts +122 -0
- package/src/core/scripts.ts +42 -0
- package/src/core/sdd.ts +188 -0
- package/src/core/templates.ts +37 -0
- package/src/core/vcs-config.ts +252 -0
- package/src/core/verify-parse.ts +27 -0
- package/src/core/workspaces.ts +81 -0
- package/src/core/youtrack.ts +488 -0
- package/src/core.ts +62 -0
- package/src/state.ts +22 -0
- package/src/tools/docs-repo.ts +42 -0
- package/src/tools/flow.ts +88 -0
- package/src/tools/handoff.ts +160 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/present.ts +45 -0
- package/src/tools/repo.ts +357 -0
- package/src/tools/rules.ts +30 -0
- package/src/tools/sdd.ts +189 -0
- package/src/tools/templates.ts +27 -0
- package/src/tools/youtrack.ts +356 -0
- package/templates/execution-contract.md +56 -0
- package/templates/greeting.md +1 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +6 -0
- package/templates/plan-template.md +25 -0
- package/templates/spec-template.md +51 -0
- package/templates/superpowers-doc-contract.md +69 -0
- package/vendor/superpowers/skills/brainstorming/SKILL.md +159 -0
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +167 -0
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +291 -0
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +185 -0
- package/vendor/superpowers/skills/executing-plans/SKILL.md +70 -0
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +241 -0
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +213 -0
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +103 -0
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +172 -0
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +418 -0
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +139 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +44 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +22 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +40 -0
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +296 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +63 -0
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +14 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +371 -0
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +202 -0
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +62 -0
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +39 -0
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +139 -0
- package/vendor/superpowers/skills/writing-plans/SKILL.md +174 -0
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/writing-skills/SKILL.md +689 -0
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +168 -0
- package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
package/src/core/sdd.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseTasksFromPlan } from "./docs-validate";
|
|
5
|
+
import { docsValidate } from "./docs-validate";
|
|
6
|
+
import { readFlowState } from "./flow-state";
|
|
7
|
+
|
|
8
|
+
const posix = (p: string) => p.split(path.sep).join("/");
|
|
9
|
+
|
|
10
|
+
export const slugFromPlan = (planPath: string) => {
|
|
11
|
+
const dirName = path.basename(path.dirname(planPath));
|
|
12
|
+
return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** OpenCode todowrite payload. SDD ledger is persistence, not a UI substitute. */
|
|
16
|
+
export function todosFromTasks(
|
|
17
|
+
tasks: { id: number | string; title: string }[],
|
|
18
|
+
completedTaskIds: (number | string)[] = [],
|
|
19
|
+
) {
|
|
20
|
+
const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
|
|
21
|
+
const todos = (tasks ?? []).map((t) => {
|
|
22
|
+
const id = Number(t.id);
|
|
23
|
+
return {
|
|
24
|
+
id: `task-${id}`,
|
|
25
|
+
content: `Task ${id}: ${t.title ?? ""}`.trim(),
|
|
26
|
+
status: done.has(id) ? "completed" : "pending",
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const firstPending = todos.find((t) => t.status === "pending");
|
|
30
|
+
if (firstPending) firstPending.status = "in_progress";
|
|
31
|
+
return todos;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sddContext({
|
|
35
|
+
slug,
|
|
36
|
+
plan_path,
|
|
37
|
+
workspace_root,
|
|
38
|
+
}: {
|
|
39
|
+
slug?: string;
|
|
40
|
+
plan_path?: string;
|
|
41
|
+
workspace_root: string;
|
|
42
|
+
}) {
|
|
43
|
+
const cwd = path.resolve(workspace_root);
|
|
44
|
+
let resolvedSlug = slug;
|
|
45
|
+
let resolvedPlanPath = plan_path;
|
|
46
|
+
if (!resolvedSlug && plan_path) {
|
|
47
|
+
resolvedPlanPath = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
|
|
48
|
+
resolvedSlug = slugFromPlan(resolvedPlanPath);
|
|
49
|
+
}
|
|
50
|
+
if (!resolvedSlug) return { error: "slug or plan_path required" };
|
|
51
|
+
|
|
52
|
+
const sdd_dir = path.posix.join("docs", resolvedSlug, "sdd");
|
|
53
|
+
const progress_path = path.posix.join(sdd_dir, "progress.md");
|
|
54
|
+
const manifest_path = path.posix.join(sdd_dir, "manifest.json");
|
|
55
|
+
|
|
56
|
+
let progress_lines: string[] = [];
|
|
57
|
+
let completed_task_ids: number[] = [];
|
|
58
|
+
const absProgress = path.join(cwd, progress_path);
|
|
59
|
+
if (existsSync(absProgress)) {
|
|
60
|
+
progress_lines = readFileSync(absProgress, "utf8").split("\n").map((ln) => ln.trim()).filter(Boolean);
|
|
61
|
+
const pat = /^Task\s+(\d+):\s+complete\b/i;
|
|
62
|
+
completed_task_ids = progress_lines
|
|
63
|
+
.map((ln) => pat.exec(ln)?.[1])
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map(Number);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let manifest: Record<string, unknown> = {};
|
|
69
|
+
const absManifest = path.join(cwd, manifest_path);
|
|
70
|
+
if (existsSync(absManifest)) {
|
|
71
|
+
try { manifest = JSON.parse(readFileSync(absManifest, "utf8")); } catch { manifest = {}; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const legacy_path = path.join(cwd, ".superpowers/sdd");
|
|
75
|
+
const legacy_exists = existsSync(legacy_path);
|
|
76
|
+
|
|
77
|
+
let todos: { id: string; content: string; status: string }[] = [];
|
|
78
|
+
let task_count = 0;
|
|
79
|
+
if (plan_path) {
|
|
80
|
+
const absPlan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
|
|
81
|
+
const planText = readFileSync(absPlan, "utf8");
|
|
82
|
+
const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
|
|
83
|
+
const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
|
|
84
|
+
if (spec_path) {
|
|
85
|
+
const validated = docsValidate({ spec_path, plan_path, workspace_root: cwd });
|
|
86
|
+
if (validated.ok === false) return { ok: false, errors: validated.errors, error: validated.error };
|
|
87
|
+
}
|
|
88
|
+
const tasks = parseTasksFromPlan(planText);
|
|
89
|
+
if (tasks.length > 0) {
|
|
90
|
+
task_count = tasks.length;
|
|
91
|
+
todos = todosFromTasks(tasks, completed_task_ids);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const flow = readFlowState(cwd, resolvedSlug);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
slug: resolvedSlug,
|
|
99
|
+
sdd_dir,
|
|
100
|
+
progress_path,
|
|
101
|
+
manifest_path,
|
|
102
|
+
progress_lines,
|
|
103
|
+
completed_task_ids,
|
|
104
|
+
manifest,
|
|
105
|
+
created: existsSync(path.join(cwd, sdd_dir)),
|
|
106
|
+
forbidden_legacy_path: ".superpowers/sdd",
|
|
107
|
+
legacy_sdd_exists: legacy_exists,
|
|
108
|
+
warning: legacy_exists
|
|
109
|
+
? "Ignore .superpowers/sdd — use sdd_dir from this tool only"
|
|
110
|
+
: undefined,
|
|
111
|
+
todos,
|
|
112
|
+
task_count,
|
|
113
|
+
flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
|
|
114
|
+
todowrite_required: true,
|
|
115
|
+
todowrite_hint:
|
|
116
|
+
"REQUIRED: Call OpenCode todowrite with todos from this result so the native task list shows progress. Before each task set status in_progress; after workflow_sdd_append_progress set it completed.",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function sddTaskBrief({
|
|
121
|
+
sdd_dir,
|
|
122
|
+
task_id,
|
|
123
|
+
section_text,
|
|
124
|
+
workspace_root,
|
|
125
|
+
}: {
|
|
126
|
+
sdd_dir: string;
|
|
127
|
+
task_id: number;
|
|
128
|
+
section_text: string;
|
|
129
|
+
workspace_root: string;
|
|
130
|
+
}) {
|
|
131
|
+
const cwd = path.resolve(workspace_root);
|
|
132
|
+
const dir = path.isAbsolute(sdd_dir) ? sdd_dir : path.join(cwd, sdd_dir);
|
|
133
|
+
mkdirSync(dir, { recursive: true });
|
|
134
|
+
const out = path.join(dir, `task-${task_id}-brief.md`);
|
|
135
|
+
writeFileSync(out, `# Task ${task_id} brief\n\n${section_text}\n`, "utf8");
|
|
136
|
+
const rel = posix(path.relative(cwd, out));
|
|
137
|
+
return { brief_path: rel, task_id };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function sddReviewPackage({
|
|
141
|
+
sdd_dir,
|
|
142
|
+
base_sha,
|
|
143
|
+
head_sha,
|
|
144
|
+
workspace_root,
|
|
145
|
+
}: {
|
|
146
|
+
sdd_dir: string;
|
|
147
|
+
base_sha: string;
|
|
148
|
+
head_sha: string;
|
|
149
|
+
workspace_root: string;
|
|
150
|
+
}) {
|
|
151
|
+
const cwd = path.resolve(workspace_root);
|
|
152
|
+
const dir = path.isAbsolute(sdd_dir) ? sdd_dir : path.join(cwd, sdd_dir);
|
|
153
|
+
mkdirSync(dir, { recursive: true });
|
|
154
|
+
const base7 = base_sha.slice(0, 7);
|
|
155
|
+
const head7 = head_sha.slice(0, 7);
|
|
156
|
+
const diffPath = path.join(dir, `review-${base7}..${head7}.diff`);
|
|
157
|
+
try {
|
|
158
|
+
const diff = execFileSync("git", ["diff", base_sha, head_sha], { cwd, encoding: "utf8" });
|
|
159
|
+
writeFileSync(diffPath, diff, "utf8");
|
|
160
|
+
const rel = posix(path.relative(cwd, diffPath));
|
|
161
|
+
return { diff_path: rel, base_sha, head_sha, base7, head7 };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { error: error instanceof Error ? error.message : "git diff failed" };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
|
|
168
|
+
|
|
169
|
+
export function sddAppendProgress({
|
|
170
|
+
progress_path,
|
|
171
|
+
line,
|
|
172
|
+
workspace_root,
|
|
173
|
+
}: {
|
|
174
|
+
progress_path: string;
|
|
175
|
+
line: string;
|
|
176
|
+
workspace_root: string;
|
|
177
|
+
}) {
|
|
178
|
+
const cwd = path.resolve(workspace_root);
|
|
179
|
+
const path_ = path.isAbsolute(progress_path) ? progress_path : path.join(cwd, progress_path);
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (!PROGRESS_RE.test(trimmed)) {
|
|
182
|
+
return { error: "invalid progress line format" };
|
|
183
|
+
}
|
|
184
|
+
mkdirSync(path.dirname(path_), { recursive: true });
|
|
185
|
+
appendFileSync(path_, trimmed + "\n", "utf8");
|
|
186
|
+
const rel = posix(path.relative(cwd, path_));
|
|
187
|
+
return { ok: true, line: trimmed, progress_path: rel };
|
|
188
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configDir } from "./config";
|
|
5
|
+
|
|
6
|
+
export type TemplateName = "issue-update" | "greeting" | "headers";
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
|
+
|
|
10
|
+
export const templatePath = (name: TemplateName): string => path.join(configDir(), "templates", `${name}.md`);
|
|
11
|
+
|
|
12
|
+
export const readTemplate = (name: TemplateName): { source: "config" | "repo"; content: string } => {
|
|
13
|
+
const cfg = templatePath(name);
|
|
14
|
+
if (existsSync(cfg)) return { source: "config", content: readFileSync(cfg, "utf8") };
|
|
15
|
+
return { source: "repo", content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8") };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const writeTemplate = (
|
|
19
|
+
name: TemplateName,
|
|
20
|
+
content: string,
|
|
21
|
+
confirmed: boolean,
|
|
22
|
+
): { ok: true; path: string } | { ok: false; error: string } => {
|
|
23
|
+
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
24
|
+
const file = templatePath(name);
|
|
25
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
writeFileSync(file, content, "utf8");
|
|
27
|
+
return { ok: true, path: file };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const listTemplates = (): { name: TemplateName; source: "config" | "repo" | "missing"; path: string }[] =>
|
|
31
|
+
(["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
|
|
32
|
+
const cfg = templatePath(name);
|
|
33
|
+
const repoFile = path.join(repoRoot, "templates", `${name}.md`);
|
|
34
|
+
if (existsSync(cfg)) return { name, source: "config", path: cfg };
|
|
35
|
+
if (existsSync(repoFile)) return { name, source: "repo", path: repoFile };
|
|
36
|
+
return { name, source: "missing", path: cfg };
|
|
37
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { configDir } from "./config";
|
|
6
|
+
import { resolveWorkspace } from "./workspaces";
|
|
7
|
+
|
|
8
|
+
// Ports of scripts/vcs/config.sh + verify-token.sh + token-create-urls.sh + merged-style.sh.
|
|
9
|
+
// Token never printed.
|
|
10
|
+
|
|
11
|
+
const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
12
|
+
|
|
13
|
+
export const vcsConfigPath = (): string =>
|
|
14
|
+
process.env.WORKFLOW_VCS_CONFIG ?? path.join(configDir(), "vcs.json");
|
|
15
|
+
|
|
16
|
+
const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
17
|
+
|
|
18
|
+
const vcsCwd = (cwd?: string): string => process.env.WORKFLOW_WORKSPACE_ROOT ?? cwd ?? process.cwd();
|
|
19
|
+
|
|
20
|
+
function readVcsJson(): { config: Record<string, any>; path: string; ok: boolean } {
|
|
21
|
+
const cfgPath = vcsConfigPath();
|
|
22
|
+
let config: Record<string, any> = {};
|
|
23
|
+
let ok = false;
|
|
24
|
+
if (fs.existsSync(cfgPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as unknown;
|
|
27
|
+
if (parsed && typeof parsed === "object") {
|
|
28
|
+
config = parsed as Record<string, any>;
|
|
29
|
+
ok = true;
|
|
30
|
+
}
|
|
31
|
+
} catch { /* missing or invalid */ }
|
|
32
|
+
}
|
|
33
|
+
return { config, path: cfgPath, ok };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Port of scripts/vcs/config.sh — mode: load | summary | resolve. */
|
|
37
|
+
export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): Record<string, any> {
|
|
38
|
+
const ws = resolveWorkspace(vcsCwd(cwd));
|
|
39
|
+
const wsVcs = (ws?.vcs ?? {}) as Record<string, any>;
|
|
40
|
+
const wsYt = (ws?.youtrack ?? {}) as Record<string, any>;
|
|
41
|
+
const wsIssues = (ws?.issues ?? {}) as Record<string, any>;
|
|
42
|
+
const { config: cfg, path: cfgPath, ok: cfgOk } = readVcsJson();
|
|
43
|
+
|
|
44
|
+
const provider = String(wsVcs.provider ?? cfg.provider ?? "gitlab").toLowerCase();
|
|
45
|
+
const defaultTarget = String(wsVcs.defaultTargetBranch ?? cfg.defaultTargetBranch ?? "develop");
|
|
46
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
47
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
48
|
+
// github issues path only when BOTH providers are github (mirrors WorkspaceConfig.issues).
|
|
49
|
+
let issuesProvider: string | null = null;
|
|
50
|
+
let linkOnPr: boolean | null = null;
|
|
51
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
52
|
+
issuesProvider = "github";
|
|
53
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (mode === "resolve") {
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
workspace_name: ws?.name ?? null,
|
|
60
|
+
provider,
|
|
61
|
+
defaultTargetBranch: defaultTarget,
|
|
62
|
+
link_issues: linkIssues,
|
|
63
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
64
|
+
issues_provider: issuesProvider,
|
|
65
|
+
link_on_pr: linkOnPr,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!cfgOk) return { ok: false, error: "missing or invalid vcs.json" };
|
|
70
|
+
|
|
71
|
+
const prov = (cfg[provider] ?? {}) as Record<string, any>;
|
|
72
|
+
const tokenFile = String(prov.tokenFile ?? path.join(os.homedir(), ".config", "workflow-toolkit", `${provider}.token`));
|
|
73
|
+
const tokenPath = path.resolve(tokenFile);
|
|
74
|
+
let tokenOk = false;
|
|
75
|
+
if (fs.existsSync(tokenPath)) {
|
|
76
|
+
const token = fs.readFileSync(tokenPath, "utf8").trim();
|
|
77
|
+
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
78
|
+
tokenOk = !placeholder;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const out: Record<string, any> = {
|
|
82
|
+
ok: true,
|
|
83
|
+
configPath: path.resolve(cfgPath),
|
|
84
|
+
provider,
|
|
85
|
+
defaultTargetBranch: defaultTarget,
|
|
86
|
+
pr: cfg.pr ?? {},
|
|
87
|
+
tokenPath,
|
|
88
|
+
tokenPresent: fs.existsSync(tokenPath),
|
|
89
|
+
tokenReady: tokenOk,
|
|
90
|
+
workspace_name: ws?.name ?? null,
|
|
91
|
+
link_issues: linkIssues,
|
|
92
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
93
|
+
issues_provider: issuesProvider,
|
|
94
|
+
link_on_pr: linkOnPr,
|
|
95
|
+
};
|
|
96
|
+
if (provider === "gitlab") {
|
|
97
|
+
out.gitlab = { host: prov.host ?? "gitlab.com", apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4" };
|
|
98
|
+
} else if (provider === "github") {
|
|
99
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
100
|
+
}
|
|
101
|
+
if (mode === "summary") delete out.tokenPath;
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Port of scripts/vcs/verify-token.sh — soft-fail JSON out, always exit 0. */
|
|
106
|
+
export function vcsVerifyToken(): Record<string, any> {
|
|
107
|
+
const cfg = vcsConfig("load");
|
|
108
|
+
if (!cfg.ok) return { ok: false, error: cfg.error ?? "vcs config not ready" };
|
|
109
|
+
const provider = cfg.provider as string;
|
|
110
|
+
if (!cfg.tokenReady) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
provider,
|
|
114
|
+
error: "token file still placeholder YOUR_TOKEN_HERE — edit locally, then /wk-status",
|
|
115
|
+
path: cfg.tokenPath,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
|
|
119
|
+
|
|
120
|
+
if (provider === "gitlab") {
|
|
121
|
+
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" });
|
|
124
|
+
if (result.status !== 0) {
|
|
125
|
+
return { ok: false, provider, error: "GitLab API rejected token", detail: (result.stderr ?? result.stdout ?? "").slice(0, 200) };
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const user = JSON.parse(result.stdout ?? "") as Record<string, any>;
|
|
129
|
+
return { ok: true, provider, username: user.username ?? user.login, name: user.name };
|
|
130
|
+
} catch {
|
|
131
|
+
return { ok: false, provider, error: "invalid JSON from GitLab /user" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (provider === "github") {
|
|
135
|
+
const result = spawnSync("gh", ["api", "user"], { encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
136
|
+
if (result.status !== 0) {
|
|
137
|
+
return { ok: false, provider, error: "GitHub API rejected token", detail: (result.stderr ?? result.stdout ?? "").slice(0, 200) };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const user = JSON.parse(result.stdout ?? "") as Record<string, any>;
|
|
141
|
+
return { ok: true, provider, username: user.login, name: user.name };
|
|
142
|
+
} catch {
|
|
143
|
+
return { ok: false, provider, error: "invalid JSON from gh api user" };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { ok: false, error: `unsupported provider: ${provider}` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Port of scripts/vcs/token-create-urls.sh — provider token-creation deep links. */
|
|
150
|
+
export function vcsTokenCreateUrls(): Record<string, any> {
|
|
151
|
+
const tokenName = process.env.WORKFLOW_VCS_TOKEN_NAME ?? "workit";
|
|
152
|
+
const { config: cfg } = readVcsJson();
|
|
153
|
+
const defaults = (cfg.tokenDefaults ?? {}) as Record<string, any>;
|
|
154
|
+
const name = String(defaults.name ?? tokenName);
|
|
155
|
+
const desc = String(defaults.description ?? "OpenCode workit — /wk-pr and glab/gh");
|
|
156
|
+
|
|
157
|
+
const gitlab = (cfg.gitlab ?? {}) as Record<string, any>;
|
|
158
|
+
const host = String(gitlab.host ?? "gitlab.com");
|
|
159
|
+
const gitlabScopes = Array.isArray(defaults.gitlabScopes) ? defaults.gitlabScopes : ["api"];
|
|
160
|
+
const gitlabParams = new URLSearchParams({ name, description: desc, scopes: gitlabScopes.join(",") });
|
|
161
|
+
const gitlabUrl = `https://${host}/-/user_settings/personal_access_tokens?${gitlabParams}`;
|
|
162
|
+
|
|
163
|
+
const githubPerms = defaults.githubPermissions ?? { pull_requests: "write", contents: "write", metadata: "read" };
|
|
164
|
+
const ghParams = new URLSearchParams({ name, description: desc, ...githubPerms });
|
|
165
|
+
const githubFineUrl = `https://github.com/settings/personal-access-tokens/new?${ghParams}`;
|
|
166
|
+
|
|
167
|
+
const classicScopes = Array.isArray(defaults.githubClassicScopes) ? defaults.githubClassicScopes : ["repo"];
|
|
168
|
+
const githubClassicUrl = `https://github.com/settings/tokens/new?${new URLSearchParams({ description: name, scopes: classicScopes.join(",") })}`;
|
|
169
|
+
|
|
170
|
+
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] ?? {};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
tokenName: name,
|
|
189
|
+
tokenDescription: desc,
|
|
190
|
+
activeProvider: provider,
|
|
191
|
+
active,
|
|
192
|
+
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 },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Port of scripts/vcs/merged-style.sh — recent merged MR/PR bodies for style reference. */
|
|
198
|
+
export function mergedPrStyle(limit = 6): Record<string, any> {
|
|
199
|
+
const cfg = vcsConfig("load");
|
|
200
|
+
if (!cfg.ok || !cfg.tokenReady) return { ok: false, error: "vcs not configured" };
|
|
201
|
+
const provider = cfg.provider as string;
|
|
202
|
+
const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
|
|
203
|
+
const examples: Array<Record<string, any>> = [];
|
|
204
|
+
|
|
205
|
+
const descInfo = (desc: string, caseInsensitiveNotes = false): Record<string, any> => ({
|
|
206
|
+
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
207
|
+
sections: desc.split("\n").filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
208
|
+
descriptionPreview: desc.slice(0, 600),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (provider === "gitlab") {
|
|
212
|
+
const remote = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf8" });
|
|
213
|
+
if (remote.status !== 0) return { ok: false, error: "no origin remote" };
|
|
214
|
+
const url = (remote.stdout ?? "").trim();
|
|
215
|
+
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
216
|
+
if (!m) return { ok: false, error: "not a gitlab.com origin" };
|
|
217
|
+
const project = m[1];
|
|
218
|
+
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
219
|
+
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`]);
|
|
221
|
+
if (r.status !== 0) {
|
|
222
|
+
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
223
|
+
}
|
|
224
|
+
if (r.status !== 0) return { ok: false, error: "could not list merge requests" };
|
|
225
|
+
for (const mr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|
|
226
|
+
const desc = String(mr.description ?? "").trim();
|
|
227
|
+
examples.push({ title: mr.title, url: mr.web_url, squash: mr.squash, ...descInfo(desc, true) });
|
|
228
|
+
}
|
|
229
|
+
} 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 } });
|
|
232
|
+
if (r.status !== 0) return { ok: false, error: "could not list pull requests" };
|
|
233
|
+
for (const pr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
|
|
234
|
+
const desc = String(pr.body ?? "").trim();
|
|
235
|
+
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
provider,
|
|
242
|
+
count: examples.length,
|
|
243
|
+
styleHints: [
|
|
244
|
+
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
245
|
+
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
246
|
+
"Do not paste commit log or diff stat into the body",
|
|
247
|
+
],
|
|
248
|
+
examples,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const vcsWorkspacesPath = workspacesPath;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Parse verify-project.sh stdout (# Summary + ## check sections). */
|
|
2
|
+
export function parseVerifyOutput(stdout: string): Record<string, any> {
|
|
3
|
+
const commands = [];
|
|
4
|
+
const parts = stdout.split(/\n## /);
|
|
5
|
+
for (const part of parts.slice(1)) {
|
|
6
|
+
const nl = part.indexOf('\n');
|
|
7
|
+
const label = part.slice(0, nl).trim();
|
|
8
|
+
const body = part.slice(nl + 1);
|
|
9
|
+
const cmdMatch = body.match(/^command: (.+)$/m);
|
|
10
|
+
const statusMatch = body.match(/^status: (pass|fail|skipped)(?: \((.+)\))?/m);
|
|
11
|
+
if (cmdMatch && statusMatch) {
|
|
12
|
+
commands.push({
|
|
13
|
+
label,
|
|
14
|
+
command: cmdMatch[1].trim(),
|
|
15
|
+
status: statusMatch[1],
|
|
16
|
+
reason: statusMatch[2]?.trim(),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const summaryMatch = stdout.match(/# Summary[\s\S]*?passed: (\d+)[\s\S]*?failed: (\d+)[\s\S]*?skipped: (\d+)/);
|
|
22
|
+
const passed = summaryMatch ? Number(summaryMatch[1]) : 0;
|
|
23
|
+
const failed = summaryMatch ? Number(summaryMatch[2]) : 0;
|
|
24
|
+
const skipped = summaryMatch ? Number(summaryMatch[3]) : 0;
|
|
25
|
+
|
|
26
|
+
return { passed, failed, skipped, commands };
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { configDir } from "./config";
|
|
4
|
+
|
|
5
|
+
export type VcsProvider = "gitlab" | "github";
|
|
6
|
+
|
|
7
|
+
export type WorkspaceConfig = {
|
|
8
|
+
name: string;
|
|
9
|
+
glob: string;
|
|
10
|
+
vcs?: { provider: VcsProvider; defaultTargetBranch?: string };
|
|
11
|
+
youtrack?: { baseUrl?: string; link_issues?: boolean };
|
|
12
|
+
issues?: { provider?: "github"; link_on_pr?: boolean };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
16
|
+
|
|
17
|
+
// ponytail: only globstar (`**`) is supported; if more minimatch parity is needed
|
|
18
|
+
// (char classes, braces, `?`), swap this matcher for the minimatch dependency.
|
|
19
|
+
const globToRegExp = (glob: string): RegExp => {
|
|
20
|
+
let out = "";
|
|
21
|
+
for (let i = 0; i < glob.length; i++) {
|
|
22
|
+
const c = glob[i];
|
|
23
|
+
if (c === "*") {
|
|
24
|
+
if (glob[i + 1] === "*") {
|
|
25
|
+
if (glob[i + 2] === "/") {
|
|
26
|
+
// **/ matches zero or more segments; a leading **/ also consumes the leading slash
|
|
27
|
+
if (out === "") out += "/?";
|
|
28
|
+
out += "(?:[^/]+/)*";
|
|
29
|
+
i += 2;
|
|
30
|
+
} else {
|
|
31
|
+
// trailing ** also matches the bare parent: /x/y/** -> /x/y and /x/y/deep.
|
|
32
|
+
// (?:/.*)? only when the prefix ends with / (POSIX absolute); otherwise
|
|
33
|
+
// the trailing ** must match any remainder, e.g. a bare ** against a
|
|
34
|
+
// drive-letter cwd like D:/a/x.
|
|
35
|
+
if (i + 2 >= glob.length) {
|
|
36
|
+
if (out.endsWith("/")) {
|
|
37
|
+
out = out.slice(0, -1);
|
|
38
|
+
out += "(?:/.*)?";
|
|
39
|
+
} else {
|
|
40
|
+
out += ".*";
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
out += ".*";
|
|
44
|
+
}
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
out += "[^/]*";
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return new RegExp(`^${out}$`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const resolveWorkspace = (cwd: string): WorkspaceConfig | null => {
|
|
58
|
+
let raw: string;
|
|
59
|
+
try {
|
|
60
|
+
raw = readFileSync(workspacesPath(), "utf8");
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let parsed: unknown;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
71
|
+
const list = (parsed as { workspaces?: unknown }).workspaces;
|
|
72
|
+
if (!Array.isArray(list)) return null;
|
|
73
|
+
const cwdPosix = cwd.split(path.sep).join("/");
|
|
74
|
+
for (const entry of list) {
|
|
75
|
+
if (!entry || typeof entry !== "object") continue;
|
|
76
|
+
const ws = entry as WorkspaceConfig;
|
|
77
|
+
if (typeof ws.glob !== "string" || !ws.glob) continue;
|
|
78
|
+
if (globToRegExp(ws.glob).test(cwdPosix)) return ws;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
};
|