@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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/commands/wk-changelog.md +2 -0
  4. package/commands/wk-commit.md +2 -0
  5. package/commands/wk-docs-refresh.md +2 -0
  6. package/commands/wk-handoff.md +2 -0
  7. package/commands/wk-implement.md +2 -0
  8. package/commands/wk-init.md +2 -0
  9. package/commands/wk-issue-update.md +2 -0
  10. package/commands/wk-meetings.md +2 -0
  11. package/commands/wk-pr.md +2 -0
  12. package/commands/wk-release-notes.md +2 -0
  13. package/commands/wk-status.md +2 -0
  14. package/commands/wk-verify.md +2 -0
  15. package/package.json +43 -0
  16. package/scripts/_shared/common.sh +158 -0
  17. package/scripts/changelog-context.sh +42 -0
  18. package/scripts/docs-refresh-context.sh +40 -0
  19. package/scripts/fixtures/sample-plan.md +15 -0
  20. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  21. package/scripts/init/apply.sh +5 -0
  22. package/scripts/init/status.sh +5 -0
  23. package/scripts/init/toolkit-status.sh +5 -0
  24. package/scripts/install-cursor-plugin.sh +83 -0
  25. package/scripts/install-opencode-plugin.sh +76 -0
  26. package/scripts/pr-create.sh +5 -0
  27. package/scripts/pr-ready-context.sh +88 -0
  28. package/scripts/present/ascii-wireframe.sh +5 -0
  29. package/scripts/present/flow-diagram.sh +5 -0
  30. package/scripts/release-notes-context.sh +40 -0
  31. package/scripts/rewrite-workspace-deps.ts +17 -0
  32. package/scripts/run-cursor-mcp.sh +10 -0
  33. package/scripts/sync-runtime.sh +96 -0
  34. package/scripts/update-superpowers.sh +56 -0
  35. package/scripts/vcs/config.sh +5 -0
  36. package/scripts/vcs/merged-style.sh +5 -0
  37. package/scripts/vcs/token-create-urls.sh +5 -0
  38. package/scripts/vcs/verify-token.sh +5 -0
  39. package/scripts/verify-project.sh +140 -0
  40. package/scripts/youtrack/api.sh +5 -0
  41. package/scripts/youtrack/config.sh +5 -0
  42. package/scripts/youtrack/greeting.sh +5 -0
  43. package/scripts/youtrack/parse-duration.sh +5 -0
  44. package/scripts/youtrack/token-create-url.sh +5 -0
  45. package/scripts/youtrack/verify-token.sh +5 -0
  46. package/scripts/youtrack/work-date-ms.sh +5 -0
  47. package/skills/wk-changelog/SKILL.md +15 -0
  48. package/skills/wk-commit/SKILL.md +16 -0
  49. package/skills/wk-docs-refresh/SKILL.md +15 -0
  50. package/skills/wk-handoff/SKILL.md +17 -0
  51. package/skills/wk-implement/SKILL.md +41 -0
  52. package/skills/wk-init/SKILL.md +31 -0
  53. package/skills/wk-issue-update/SKILL.md +27 -0
  54. package/skills/wk-issue-update/references/youtrack-update-style.md +81 -0
  55. package/skills/wk-meetings/SKILL.md +17 -0
  56. package/skills/wk-pr/SKILL.md +27 -0
  57. package/skills/wk-release-notes/SKILL.md +15 -0
  58. package/skills/wk-status/SKILL.md +16 -0
  59. package/skills/wk-verify/SKILL.md +16 -0
  60. package/src/core/branch.ts +246 -0
  61. package/src/core/changelog.ts +312 -0
  62. package/src/core/config-guard.ts +26 -0
  63. package/src/core/config.ts +73 -0
  64. package/src/core/detector.ts +207 -0
  65. package/src/core/doc-render.ts +14 -0
  66. package/src/core/docs-repo.ts +196 -0
  67. package/src/core/docs-validate.ts +255 -0
  68. package/src/core/flow-state.ts +225 -0
  69. package/src/core/git.ts +56 -0
  70. package/src/core/gitignore.ts +43 -0
  71. package/src/core/handoff-context.ts +115 -0
  72. package/src/core/hygiene.ts +77 -0
  73. package/src/core/init.ts +443 -0
  74. package/src/core/parse-sections.ts +24 -0
  75. package/src/core/plan-tasks.ts +33 -0
  76. package/src/core/ports/init-apply.ts +15 -0
  77. package/src/core/ports/init-status.ts +4 -0
  78. package/src/core/ports/init-toolkit-status.ts +4 -0
  79. package/src/core/ports/pr-create.ts +22 -0
  80. package/src/core/ports/present-ascii.ts +10 -0
  81. package/src/core/ports/present-flow.ts +10 -0
  82. package/src/core/ports/vcs-config.ts +14 -0
  83. package/src/core/ports/vcs-merged-style.ts +5 -0
  84. package/src/core/ports/vcs-token-create-urls.ts +4 -0
  85. package/src/core/ports/vcs-verify-token.ts +4 -0
  86. package/src/core/ports/youtrack-api.ts +17 -0
  87. package/src/core/ports/youtrack-config.ts +23 -0
  88. package/src/core/ports/youtrack-greeting.ts +10 -0
  89. package/src/core/ports/youtrack-parse-duration.ts +14 -0
  90. package/src/core/ports/youtrack-token-create-url.ts +4 -0
  91. package/src/core/ports/youtrack-verify-token.ts +12 -0
  92. package/src/core/ports/youtrack-work-date-ms.ts +10 -0
  93. package/src/core/pr-create.ts +212 -0
  94. package/src/core/present.ts +100 -0
  95. package/src/core/reminder.ts +82 -0
  96. package/src/core/repo-tool.ts +9 -0
  97. package/src/core/rules.ts +122 -0
  98. package/src/core/scripts.ts +42 -0
  99. package/src/core/sdd.ts +188 -0
  100. package/src/core/templates.ts +37 -0
  101. package/src/core/vcs-config.ts +252 -0
  102. package/src/core/verify-parse.ts +27 -0
  103. package/src/core/workspaces.ts +81 -0
  104. package/src/core/youtrack.ts +488 -0
  105. package/src/core.ts +62 -0
  106. package/src/state.ts +22 -0
  107. package/src/tools/docs-repo.ts +42 -0
  108. package/src/tools/flow.ts +88 -0
  109. package/src/tools/handoff.ts +160 -0
  110. package/src/tools/index.ts +22 -0
  111. package/src/tools/present.ts +45 -0
  112. package/src/tools/repo.ts +357 -0
  113. package/src/tools/rules.ts +30 -0
  114. package/src/tools/sdd.ts +189 -0
  115. package/src/tools/templates.ts +27 -0
  116. package/src/tools/youtrack.ts +356 -0
  117. package/templates/execution-contract.md +56 -0
  118. package/templates/greeting.md +1 -0
  119. package/templates/headers.md +3 -0
  120. package/templates/hygiene/.editorconfig +8 -0
  121. package/templates/hygiene/.gitattributes +3 -0
  122. package/templates/hygiene/CHANGELOG.md +14 -0
  123. package/templates/hygiene/CONTRIBUTING.md +3 -0
  124. package/templates/hygiene/LICENSE +21 -0
  125. package/templates/hygiene/README.md +3 -0
  126. package/templates/issue-update.md +6 -0
  127. package/templates/plan-template.md +25 -0
  128. package/templates/spec-template.md +51 -0
  129. package/templates/superpowers-doc-contract.md +69 -0
  130. package/vendor/superpowers/skills/brainstorming/SKILL.md +159 -0
  131. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
  132. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +167 -0
  133. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
  134. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
  135. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
  136. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  137. package/vendor/superpowers/skills/brainstorming/visual-companion.md +291 -0
  138. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +185 -0
  139. package/vendor/superpowers/skills/executing-plans/SKILL.md +70 -0
  140. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +241 -0
  141. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +213 -0
  142. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +103 -0
  143. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +172 -0
  144. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +418 -0
  145. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +139 -0
  146. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +44 -0
  147. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +22 -0
  148. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +40 -0
  149. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
  150. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  151. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +296 -0
  152. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  153. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
  154. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
  155. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +63 -0
  156. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
  157. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  158. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  159. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  160. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  161. package/vendor/superpowers/skills/test-driven-development/SKILL.md +371 -0
  162. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +299 -0
  163. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +202 -0
  164. package/vendor/superpowers/skills/using-superpowers/SKILL.md +62 -0
  165. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
  166. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +39 -0
  167. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
  168. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +139 -0
  169. package/vendor/superpowers/skills/writing-plans/SKILL.md +174 -0
  170. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  171. package/vendor/superpowers/skills/writing-skills/SKILL.md +689 -0
  172. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  173. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  174. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  175. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  176. package/vendor/superpowers/skills/writing-skills/render-graphs.js +168 -0
  177. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
@@ -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
+ };