@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/git.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const run = (cwd: string, args: string[]): { stdout: string; stderr: string; exitCode: number } => {
|
|
4
|
+
try {
|
|
5
|
+
const stdout = execFileSync("git", args, { cwd, encoding: "utf8" });
|
|
6
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
7
|
+
} catch (error) {
|
|
8
|
+
const e = error as { stdout?: string; stderr?: string; status?: number };
|
|
9
|
+
return {
|
|
10
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
11
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
12
|
+
exitCode: e.status ?? 1,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
|
|
18
|
+
const cwd = workspaceRoot;
|
|
19
|
+
const branch = run(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout || "unknown";
|
|
20
|
+
const status_short = run(cwd, ["status", "--porcelain"]).stdout;
|
|
21
|
+
|
|
22
|
+
const staged: string[] = [];
|
|
23
|
+
const unstaged: string[] = [];
|
|
24
|
+
const untracked: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const line of status_short.split("\n").filter(Boolean)) {
|
|
27
|
+
const code = line.slice(0, 2);
|
|
28
|
+
const file = line.slice(3);
|
|
29
|
+
if (code === "??") {
|
|
30
|
+
untracked.push(file);
|
|
31
|
+
} else {
|
|
32
|
+
if (code[0] !== " " && code[0] !== "?") staged.push(file);
|
|
33
|
+
if (code[1] !== " " && code[1] !== "?") unstaged.push(file);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
38
|
+
const diff_stat = run(cwd, ["diff", "--stat", ...pathArgs]).stdout;
|
|
39
|
+
const cached_stat = run(cwd, ["diff", "--cached", "--stat", ...pathArgs]).stdout;
|
|
40
|
+
|
|
41
|
+
const stagedSet = new Set(staged);
|
|
42
|
+
const unstagedSet = new Set(unstaged);
|
|
43
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
workspace_root: cwd,
|
|
47
|
+
branch,
|
|
48
|
+
status_short,
|
|
49
|
+
staged,
|
|
50
|
+
unstaged,
|
|
51
|
+
untracked,
|
|
52
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join("\n"),
|
|
53
|
+
partial_staged: partial_staged.length > 0,
|
|
54
|
+
partial_staged_files: partial_staged,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const GITIGNORE_ENTRIES = [
|
|
5
|
+
"# workit: SDD working state (never commit)",
|
|
6
|
+
"docs/*/sdd/",
|
|
7
|
+
"",
|
|
8
|
+
"# OS / editor cruft",
|
|
9
|
+
".DS_Store",
|
|
10
|
+
"Thumbs.db",
|
|
11
|
+
"*.swp",
|
|
12
|
+
".idea/",
|
|
13
|
+
".vscode/",
|
|
14
|
+
".env",
|
|
15
|
+
"node_modules/",
|
|
16
|
+
"dist/",
|
|
17
|
+
"*.log",
|
|
18
|
+
".cache/",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const ensureProjectGitignore = (
|
|
22
|
+
workspaceRoot: string,
|
|
23
|
+
confirmed: boolean,
|
|
24
|
+
): { ok: true; path: string; added: string[] } | { ok: false; error: string } => {
|
|
25
|
+
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
26
|
+
const file = path.join(workspaceRoot, ".gitignore");
|
|
27
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
28
|
+
const existingLines = new Set(existing.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
29
|
+
const added: string[] = [];
|
|
30
|
+
const append: string[] = [];
|
|
31
|
+
for (const entry of GITIGNORE_ENTRIES) {
|
|
32
|
+
if (entry.trim() === "" || existingLines.has(entry.trim())) continue;
|
|
33
|
+
append.push(entry);
|
|
34
|
+
added.push(entry);
|
|
35
|
+
}
|
|
36
|
+
if (append.length) {
|
|
37
|
+
const separator = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
38
|
+
writeFileSync(file, existing + separator + (existing ? "\n" : "") + append.join("\n") + "\n", "utf8");
|
|
39
|
+
} else if (!existsSync(file)) {
|
|
40
|
+
writeFileSync(file, "", "utf8");
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, path: file, added };
|
|
43
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { docsValidate } from "./docs-validate";
|
|
4
|
+
import { resolveBranch } from "./branch";
|
|
5
|
+
import { parseTasksFromPlan } from "./docs-validate";
|
|
6
|
+
|
|
7
|
+
const DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
|
|
8
|
+
|
|
9
|
+
const listMd = (dir: string): string[] => {
|
|
10
|
+
try {
|
|
11
|
+
return readdirSync(dir)
|
|
12
|
+
.filter((f) => f.endsWith(".md"))
|
|
13
|
+
.sort();
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Resolved = { spec: string; plan: string; source: string } | { error: string };
|
|
20
|
+
|
|
21
|
+
const extractMessagePaths = (message: string): string[] =>
|
|
22
|
+
[...new Set(message.match(DOC_RE) ?? [])].sort();
|
|
23
|
+
|
|
24
|
+
const resolveFromMessagePaths = (root: string, message: string): Resolved => {
|
|
25
|
+
const paths = extractMessagePaths(message);
|
|
26
|
+
if (paths.length === 0) return { error: "no paths" };
|
|
27
|
+
const slugs = [...new Set(paths.map((p) => p.split("/")[1]))];
|
|
28
|
+
if (slugs.length !== 1) return { error: "multiple features in message — use exactly one docs/<slug>/ pair" };
|
|
29
|
+
const slug = slugs[0];
|
|
30
|
+
const plan = `docs/${slug}/plan.md`;
|
|
31
|
+
const spec = `docs/${slug}/spec.md`;
|
|
32
|
+
if (!existsSync(path.join(root, plan)) || !existsSync(path.join(root, spec))) {
|
|
33
|
+
return { error: `docs/${slug}/ must contain both plan.md and spec.md` };
|
|
34
|
+
}
|
|
35
|
+
return { spec, plan, source: "message_paths" };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveActivePair = (root: string): Resolved => {
|
|
39
|
+
const docsDir = path.join(root, "docs");
|
|
40
|
+
let best: { score: number; spec: string; plan: string; source: string } | null = null;
|
|
41
|
+
let entries: string[] = [];
|
|
42
|
+
try {
|
|
43
|
+
entries = readdirSync(docsDir);
|
|
44
|
+
} catch {
|
|
45
|
+
return { error: "no pair" };
|
|
46
|
+
}
|
|
47
|
+
for (const slug of entries) {
|
|
48
|
+
if (slug.startsWith(".")) continue;
|
|
49
|
+
const plan = path.join("docs", slug, "plan.md");
|
|
50
|
+
const spec = path.join("docs", slug, "spec.md");
|
|
51
|
+
if (!existsSync(path.join(root, plan)) || !existsSync(path.join(root, spec))) continue;
|
|
52
|
+
const score = Math.max(statSync(path.join(root, spec)).mtimeMs, statSync(path.join(root, plan)).mtimeMs);
|
|
53
|
+
if (best === null || score > best.score || (score === best.score && slug < best.spec.split("/")[1])) {
|
|
54
|
+
best = { score, spec, plan, source: "active_pair" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (best === null) return { error: "no pair" };
|
|
58
|
+
return { spec: best.spec, plan: best.plan, source: best.source };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const resolveWorkflowPaths = (root: string, message: string): Resolved => {
|
|
62
|
+
const fromMessage = resolveFromMessagePaths(root, message);
|
|
63
|
+
if (!("error" in fromMessage)) return fromMessage;
|
|
64
|
+
if (fromMessage.error !== "no paths") return fromMessage;
|
|
65
|
+
const active = resolveActivePair(root);
|
|
66
|
+
if (!("error" in active)) return active;
|
|
67
|
+
|
|
68
|
+
const docsDir = path.join(root, "docs");
|
|
69
|
+
if (!existsSync(docsDir) || listMd(docsDir).length === 0) {
|
|
70
|
+
return { error: "no docs/<slug>/ features found under docs/" };
|
|
71
|
+
}
|
|
72
|
+
return { error: "could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}" };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const buildHandoffContract = ({
|
|
76
|
+
root,
|
|
77
|
+
spec,
|
|
78
|
+
plan,
|
|
79
|
+
templatePath,
|
|
80
|
+
}: {
|
|
81
|
+
root: string;
|
|
82
|
+
spec: string;
|
|
83
|
+
plan: string;
|
|
84
|
+
templatePath: string;
|
|
85
|
+
}): { prompt: string } | { error: string } => {
|
|
86
|
+
const validated = docsValidate({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
87
|
+
if (validated.ok === false) {
|
|
88
|
+
return { error: `docs validation failed\n${JSON.stringify({ ok: false, errors: validated.errors })}` };
|
|
89
|
+
}
|
|
90
|
+
const branchResolved = resolveBranch({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
91
|
+
if ("error" in branchResolved) return { error: branchResolved.error as string };
|
|
92
|
+
const branch = branchResolved.branch;
|
|
93
|
+
const slug = path.basename(path.dirname(plan));
|
|
94
|
+
const sddDir = `docs/${slug}/sdd`;
|
|
95
|
+
|
|
96
|
+
const planText = readFileSync(path.join(root, plan), "utf8");
|
|
97
|
+
const tasks = parseTasksFromPlan(planText);
|
|
98
|
+
const taskList = tasks.map((t) => `- Task ${t.id}: ${t.title}`).join("\n");
|
|
99
|
+
|
|
100
|
+
let contract: string;
|
|
101
|
+
try {
|
|
102
|
+
contract = readFileSync(templatePath, "utf8");
|
|
103
|
+
} catch {
|
|
104
|
+
return { error: "missing template templates/execution-contract.md" };
|
|
105
|
+
}
|
|
106
|
+
contract = contract
|
|
107
|
+
.replace(/<SPEC_PATH>/g, spec)
|
|
108
|
+
.replace(/<PLAN_PATH>/g, plan)
|
|
109
|
+
.replace(/<BRANCH>/g, branch)
|
|
110
|
+
.replace(/<SLUG>/g, slug)
|
|
111
|
+
.replace(/<SDD_DIR>/g, sddDir)
|
|
112
|
+
.replace(/<TASK_LIST>/g, taskList);
|
|
113
|
+
|
|
114
|
+
return { prompt: contract };
|
|
115
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { changelogUnreleasedStats } from "./changelog";
|
|
5
|
+
|
|
6
|
+
export type HygieneFile = "CHANGELOG.md" | "README.md" | ".editorconfig" | ".gitattributes" | "LICENSE" | "CONTRIBUTING.md";
|
|
7
|
+
type State = "missing" | "invalid" | "ok" | "skip";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10
|
+
const templatesDir = () => path.join(repoRoot, "templates", "hygiene");
|
|
11
|
+
|
|
12
|
+
const packageJson = (root: string): Record<string, unknown> | null => {
|
|
13
|
+
if (!existsSync(path.join(root, "package.json"))) return null;
|
|
14
|
+
try { return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); } catch { return null; }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isOpenSource = (root: string): boolean => {
|
|
18
|
+
if (existsSync(path.join(root, "LICENSE"))) return true;
|
|
19
|
+
const pkg = packageJson(root);
|
|
20
|
+
if (pkg && !pkg.private) return true; // I2: missing `private` = public (npm default)
|
|
21
|
+
return path.basename(root) === "workflow-toolkit";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const licenseHolder = (root: string): string => {
|
|
25
|
+
const pkg = packageJson(root);
|
|
26
|
+
if (pkg) {
|
|
27
|
+
const author = pkg.author;
|
|
28
|
+
if (typeof author === "string" && author.trim()) return author.trim();
|
|
29
|
+
if (author && typeof author === "object") {
|
|
30
|
+
const name = (author as Record<string, unknown>).name;
|
|
31
|
+
if (typeof name === "string" && name.trim()) return name.trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return "";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const hygieneFiles = (root: string): { state: Record<HygieneFile, State>; openSource: boolean } => {
|
|
38
|
+
const openSource = isOpenSource(root);
|
|
39
|
+
const state = {} as Record<HygieneFile, State>;
|
|
40
|
+
for (const file of ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes", "LICENSE", "CONTRIBUTING.md"] as HygieneFile[]) {
|
|
41
|
+
if (file === "LICENSE" || file === "CONTRIBUTING.md") {
|
|
42
|
+
state[file] = openSource ? (existsSync(path.join(root, file)) ? "ok" : "missing") : "skip";
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(path.join(root, file))) { state[file] = "missing"; continue; }
|
|
46
|
+
if (file === "CHANGELOG.md") {
|
|
47
|
+
const stats = changelogUnreleasedStats(root);
|
|
48
|
+
state[file] = stats.exists && stats.has_unreleased ? "ok" : "invalid";
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
state[file] = "ok";
|
|
52
|
+
}
|
|
53
|
+
return { state, openSource };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const ensureHygieneFiles = (
|
|
57
|
+
root: string,
|
|
58
|
+
opts: { confirmed: boolean; includeOpenSource?: boolean },
|
|
59
|
+
): { ok: true; created: string[] } | { ok: false; error: string } => {
|
|
60
|
+
if (!opts.confirmed) return { ok: false, error: "confirmed: true required" };
|
|
61
|
+
const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"] as HygieneFile[];
|
|
62
|
+
if (opts.includeOpenSource) files.push("LICENSE", "CONTRIBUTING.md");
|
|
63
|
+
const created: string[] = [];
|
|
64
|
+
const tplDir = templatesDir();
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (existsSync(path.join(root, file))) continue;
|
|
67
|
+
const tpl = path.join(tplDir, file);
|
|
68
|
+
if (!existsSync(tpl)) continue; // skip missing template, never fail
|
|
69
|
+
const content = readFileSync(tpl, "utf8")
|
|
70
|
+
.replace(/<PROJECT>/g, path.basename(root))
|
|
71
|
+
.replace(/<YEAR>/g, String(new Date().getFullYear()))
|
|
72
|
+
.replace(/<HOLDER>\s*/g, licenseHolder(root));
|
|
73
|
+
writeFileSync(path.join(root, file), content, "utf8");
|
|
74
|
+
created.push(file);
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, created };
|
|
77
|
+
};
|