@brainervirus/workit-core 0.5.6 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +4 -17
  2. package/package.json +9 -9
  3. package/scripts/_shared/common.sh +18 -3
  4. package/scripts/install-opencode-plugin.sh +14 -9
  5. package/scripts/lib/config-dir.sh +26 -0
  6. package/scripts/sync-runtime.sh +4 -1
  7. package/src/core/branch.ts +143 -48
  8. package/src/core/changelog.ts +17 -14
  9. package/src/core/config-guard.ts +9 -2
  10. package/src/core/config.ts +95 -15
  11. package/src/core/detector.ts +22 -11
  12. package/src/core/docs-repo.ts +50 -15
  13. package/src/core/docs-validate.ts +163 -37
  14. package/src/core/flow-state.ts +6 -2
  15. package/src/core/gitignore.ts +11 -2
  16. package/src/core/handoff-context.ts +18 -5
  17. package/src/core/hygiene.ts +27 -5
  18. package/src/core/init.ts +86 -21
  19. package/src/core/parse-sections.ts +2 -2
  20. package/src/core/plan-tasks.ts +13 -3
  21. package/src/core/ports/youtrack-api.ts +3 -1
  22. package/src/core/ports/youtrack-config.ts +1 -3
  23. package/src/core/pr-create.ts +47 -15
  24. package/src/core/present.ts +11 -2
  25. package/src/core/reminder.ts +1 -2
  26. package/src/core/repo-tool.ts +4 -1
  27. package/src/core/rules.ts +10 -7
  28. package/src/core/scripts.ts +7 -2
  29. package/src/core/sdd.ts +11 -3
  30. package/src/core/templates.ts +14 -4
  31. package/src/core/vcs-config.ts +94 -36
  32. package/src/core/verify-parse.ts +4 -2
  33. package/src/core/workspaces.ts +2 -2
  34. package/src/core/youtrack.ts +233 -58
  35. package/src/core.ts +18 -3
  36. package/src/tools/docs-repo.ts +12 -3
  37. package/src/tools/flow.ts +24 -13
  38. package/src/tools/handoff.ts +28 -23
  39. package/src/tools/present.ts +14 -10
  40. package/src/tools/repo.ts +220 -87
  41. package/src/tools/sdd.ts +93 -66
  42. package/src/tools/youtrack.ts +119 -52
  43. package/templates/superpowers-doc-contract.md +1 -1
@@ -15,17 +15,20 @@ const err = (code: string, message: string, path?: string): DocError => {
15
15
  return item;
16
16
  };
17
17
 
18
- const failValidate = (
19
- errors: DocError[],
20
- ): { ok: false; errors: DocError[]; error: string } => ({
18
+ const failValidate = (errors: DocError[]): { ok: false; errors: DocError[]; error: string } => ({
21
19
  ok: false,
22
20
  errors,
23
- error: errors.map((e) => e.message).filter(Boolean).join("; ") || "docs validation failed",
21
+ error:
22
+ errors
23
+ .map((e) => e.message)
24
+ .filter(Boolean)
25
+ .join("; ") || "docs validation failed",
24
26
  });
25
27
 
26
28
  const readBranch = (text: string, label: string): [string | null, DocError | null] => {
27
29
  const match = text.match(BRANCH_RE);
28
- if (!match) return [null, err("missing_branch", `**Branch:** feature/* or bugfix/* required in ${label}`)];
30
+ if (!match)
31
+ return [null, err("missing_branch", `**Branch:** feature/* or bugfix/* required in ${label}`)];
29
32
  return [match[1].trim().replace(/`/g, ""), null];
30
33
  };
31
34
 
@@ -34,37 +37,55 @@ const scanTaskHeadings = (planText: string): [number[], string[], DocError | nul
34
37
  const titles: string[] = [];
35
38
  let inFence = false;
36
39
  for (const line of planText.split("\n")) {
37
- if (line.startsWith("```")) { inFence = !inFence; continue; }
40
+ if (line.startsWith("```")) {
41
+ inFence = !inFence;
42
+ continue;
43
+ }
38
44
  if (inFence) continue;
39
45
  const match = line.match(TASK_RE);
40
- if (match) { ids.push(Number(match[1])); titles.push(match[2].trim()); }
46
+ if (match) {
47
+ ids.push(Number(match[1]));
48
+ titles.push(match[2].trim());
49
+ }
41
50
  }
42
- if (ids.length === 0) return [ids, titles, err("task_order", "no ### Task N sections found outside fences")];
51
+ if (ids.length === 0)
52
+ return [ids, titles, err("task_order", "no ### Task N sections found outside fences")];
43
53
  const expected = ids.map((_, i) => i + 1);
44
54
  const sorted = [...ids].sort((a, b) => a - b);
45
55
  if (JSON.stringify(sorted) !== JSON.stringify(expected) || new Set(ids).size !== ids.length) {
46
- return [ids, titles, err("task_order", `task headings must be contiguous from 1..${ids.length}; found ${ids}`)];
56
+ return [
57
+ ids,
58
+ titles,
59
+ err("task_order", `task headings must be contiguous from 1..${ids.length}; found ${ids}`),
60
+ ];
47
61
  }
48
62
  return [ids, titles, null];
49
63
  };
50
64
 
51
65
  // Port of scripts/lib/parse-plan-tasks.sh (JSON mode)
52
- export const parseTasksFromPlan = (planText: string): { id: number; title: string; section_text: string }[] => {
66
+ export const parseTasksFromPlan = (
67
+ planText: string,
68
+ ): { id: number; title: string; section_text: string }[] => {
53
69
  const tasks: { id: number; title: string; section_text: string }[] = [];
54
70
  let current: { id: number; title: string; body: string[] } | null = null;
55
71
  let inFence = false;
56
72
  for (const line of planText.split("\n")) {
57
- if (line.startsWith("```")) { inFence = !inFence; continue; }
73
+ if (line.startsWith("```")) {
74
+ inFence = !inFence;
75
+ continue;
76
+ }
58
77
  if (inFence) continue;
59
78
  const match = line.match(TASK_RE);
60
79
  if (match) {
61
- if (current) tasks.push({ id: current.id, title: current.title, section_text: current.body.join("\n") });
80
+ if (current)
81
+ tasks.push({ id: current.id, title: current.title, section_text: current.body.join("\n") });
62
82
  current = { id: Number(match[1]), title: match[2].trim(), body: [] };
63
83
  } else if (current) {
64
84
  current.body.push(line);
65
85
  }
66
86
  }
67
- if (current) tasks.push({ id: current.id, title: current.title, section_text: current.body.join("\n") });
87
+ if (current)
88
+ tasks.push({ id: current.id, title: current.title, section_text: current.body.join("\n") });
68
89
  return tasks;
69
90
  };
70
91
 
@@ -77,7 +98,14 @@ export const docsValidate = ({
77
98
  plan_path: string;
78
99
  workspace_root: string;
79
100
  }):
80
- | { ok: true; spec: string; plan: string; branch: string; task_count: number; quality: QualityFinding[] }
101
+ | {
102
+ ok: true;
103
+ spec: string;
104
+ plan: string;
105
+ branch: string;
106
+ task_count: number;
107
+ quality: QualityFinding[];
108
+ }
81
109
  | { ok: false; errors: DocError[]; error: string } => {
82
110
  const cwd = path.resolve(workspace_root);
83
111
  const specAbs = path.isAbsolute(spec_path) ? spec_path : path.join(cwd, spec_path);
@@ -85,13 +113,19 @@ export const docsValidate = ({
85
113
  const errors: DocError[] = [];
86
114
 
87
115
  const read = (p: string): string | null => {
88
- try { return readFileSync(p, "utf8"); } catch { return null; }
116
+ try {
117
+ return readFileSync(p, "utf8");
118
+ } catch {
119
+ return null;
120
+ }
89
121
  };
90
122
 
91
123
  const specText = read(specAbs);
92
124
  const planText = read(planAbs);
93
- if (specText === null) errors.push(err("missing_file", `spec not found: ${spec_path}`, spec_path));
94
- if (planText === null) errors.push(err("missing_file", `plan not found: ${plan_path}`, plan_path));
125
+ if (specText === null)
126
+ errors.push(err("missing_file", `spec not found: ${spec_path}`, spec_path));
127
+ if (planText === null)
128
+ errors.push(err("missing_file", `plan not found: ${plan_path}`, plan_path));
95
129
  if (errors.length) return failValidate(errors);
96
130
 
97
131
  const [specBranch, specErr] = readBranch(specText!, "spec");
@@ -106,12 +140,24 @@ export const docsValidate = ({
106
140
  const linked = (linkMatch[1] ?? linkMatch[2] ?? "").trim();
107
141
  const linkedAbs = path.isAbsolute(linked) ? linked : path.join(cwd, linked);
108
142
  if (path.resolve(linkedAbs) !== path.resolve(specAbs)) {
109
- errors.push(err("spec_mismatch", `plan **Spec:** ${linked} does not match spec_path ${spec_path}`, plan_path));
143
+ errors.push(
144
+ err(
145
+ "spec_mismatch",
146
+ `plan **Spec:** ${linked} does not match spec_path ${spec_path}`,
147
+ plan_path,
148
+ ),
149
+ );
110
150
  }
111
151
  }
112
152
 
113
153
  if (specBranch && planBranch && specBranch !== planBranch) {
114
- errors.push(err("branch_mismatch", `spec branch ${JSON.stringify(specBranch)} != plan branch ${JSON.stringify(planBranch)}`, plan_path));
154
+ errors.push(
155
+ err(
156
+ "branch_mismatch",
157
+ `spec branch ${JSON.stringify(specBranch)} != plan branch ${JSON.stringify(planBranch)}`,
158
+ plan_path,
159
+ ),
160
+ );
115
161
  }
116
162
 
117
163
  const [, , taskErr] = scanTaskHeadings(planText!);
@@ -123,10 +169,19 @@ export const docsValidate = ({
123
169
  const [headingIds, headingTitles, headingErr] = scanTaskHeadings(planText!);
124
170
  if (headingErr) return failValidate([headingErr]);
125
171
  if (tasks.length !== headingIds.length) {
126
- return failValidate([err("task_order", `parse count ${tasks.length} != heading count ${headingIds.length}`, plan_path)]);
172
+ return failValidate([
173
+ err(
174
+ "task_order",
175
+ `parse count ${tasks.length} != heading count ${headingIds.length}`,
176
+ plan_path,
177
+ ),
178
+ ]);
127
179
  }
128
180
  for (let i = 0; i < tasks.length; i++) {
129
- if (String(tasks[i].id) !== String(headingIds[i]) || tasks[i].title.trim() !== headingTitles[i]) {
181
+ if (
182
+ String(tasks[i].id) !== String(headingIds[i]) ||
183
+ tasks[i].title.trim() !== headingTitles[i]
184
+ ) {
130
185
  return failValidate([err("task_order", `task mismatch at position ${i + 1}`, plan_path)]);
131
186
  }
132
187
  }
@@ -144,13 +199,45 @@ export const docsValidate = ({
144
199
  }
145
200
  const hygiene = hygieneFiles(cwd);
146
201
  const hyState = hygiene.state;
147
- if (hyState["CHANGELOG.md"] === "missing") quality.push({ code: "changelog_missing", message: "CHANGELOG.md missing — create it with Keep a Changelog format (run wk-init hygiene)", severity: "warning" });
148
- if (hyState["CHANGELOG.md"] === "invalid") quality.push({ code: "changelog_invalid_format", message: "CHANGELOG.md lacks ## [Unreleased] — Keep a Changelog format required", severity: "warning" });
149
- if (hyState["README.md"] === "missing") quality.push({ code: "readme_missing", message: "README.md missing", severity: "warning" });
150
- if (hyState[".editorconfig"] === "missing") quality.push({ code: "editorconfig_missing", message: ".editorconfig missing", severity: "warning" });
151
- if (hyState[".gitattributes"] === "missing") quality.push({ code: "gitattributes_missing", message: ".gitattributes missing", severity: "warning" });
152
- if (hygiene.openSource && hyState.LICENSE === "missing") quality.push({ code: "license_missing", message: "LICENSE missing (open-source repo)", severity: "warning" });
153
- if (hygiene.openSource && hyState["CONTRIBUTING.md"] === "missing") quality.push({ code: "contributing_missing", message: "CONTRIBUTING.md missing (open-source repo)", severity: "warning" });
202
+ if (hyState["CHANGELOG.md"] === "missing")
203
+ quality.push({
204
+ code: "changelog_missing",
205
+ message:
206
+ "CHANGELOG.md missing create it with Keep a Changelog format (run wk-init hygiene)",
207
+ severity: "warning",
208
+ });
209
+ if (hyState["CHANGELOG.md"] === "invalid")
210
+ quality.push({
211
+ code: "changelog_invalid_format",
212
+ message: "CHANGELOG.md lacks ## [Unreleased] — Keep a Changelog format required",
213
+ severity: "warning",
214
+ });
215
+ if (hyState["README.md"] === "missing")
216
+ quality.push({ code: "readme_missing", message: "README.md missing", severity: "warning" });
217
+ if (hyState[".editorconfig"] === "missing")
218
+ quality.push({
219
+ code: "editorconfig_missing",
220
+ message: ".editorconfig missing",
221
+ severity: "warning",
222
+ });
223
+ if (hyState[".gitattributes"] === "missing")
224
+ quality.push({
225
+ code: "gitattributes_missing",
226
+ message: ".gitattributes missing",
227
+ severity: "warning",
228
+ });
229
+ if (hygiene.openSource && hyState.LICENSE === "missing")
230
+ quality.push({
231
+ code: "license_missing",
232
+ message: "LICENSE missing (open-source repo)",
233
+ severity: "warning",
234
+ });
235
+ if (hygiene.openSource && hyState["CONTRIBUTING.md"] === "missing")
236
+ quality.push({
237
+ code: "contributing_missing",
238
+ message: "CONTRIBUTING.md missing (open-source repo)",
239
+ severity: "warning",
240
+ });
154
241
  return {
155
242
  ok: true,
156
243
  spec: relSpec,
@@ -175,12 +262,22 @@ const REQUIRED_SECTIONS = [
175
262
  "## Acceptance criteria",
176
263
  ];
177
264
 
178
- const UI_KEYWORDS = [/\bui\b/, /\binterface\b/, /\bscreen\b/, /\bmodal\b/, /\bform\b/, /\bcomponent\b/];
265
+ const UI_KEYWORDS = [
266
+ /\bui\b/,
267
+ /\binterface\b/,
268
+ /\bscreen\b/,
269
+ /\bmodal\b/,
270
+ /\bform\b/,
271
+ /\bcomponent\b/,
272
+ ];
179
273
  const FLOW_KEYWORDS = [/\bflow\b/, /\bpipeline\b/, /\bsequence\b/, /\bdiagram\b/];
180
274
  const GLOSSARY_KEYWORDS = [/\bglossary\b/, /\bcontracts?\b/, /\bscope\b/];
181
275
 
182
- const finding = (code: string, message: string, severity: "warning" | "hard"): QualityFinding =>
183
- ({ code, message, severity });
276
+ const finding = (code: string, message: string, severity: "warning" | "hard"): QualityFinding => ({
277
+ code,
278
+ message,
279
+ severity,
280
+ });
184
281
 
185
282
  // Replace fenced code blocks with a single marker line so their content cannot
186
283
  // satisfy the checks, but the fence itself (and its language) stays detectable.
@@ -212,27 +309,52 @@ export const qualitySpec = (text: string): QualityFinding[] => {
212
309
 
213
310
  const hasCa = /^\s*(?:- CA-\d+|CA-\d+[.:])/m.test(body); // M1: ^ with /m covers line starts
214
311
  if (!hasCa) {
215
- findings.push(finding("missing_acceptance_criteria", "no enumerable CA-XX acceptance criteria found", "hard"));
312
+ findings.push(
313
+ finding(
314
+ "missing_acceptance_criteria",
315
+ "no enumerable CA-XX acceptance criteria found",
316
+ "hard",
317
+ ),
318
+ );
216
319
  }
217
320
 
218
321
  const hasAsciiFence = /```(?:text|ascii)/.test(body);
219
322
  const mentionsUi = UI_KEYWORDS.some((k) => k.test(lower));
220
323
  if (mentionsUi && !hasAsciiFence) {
221
- findings.push(finding("missing_ascii_for_ui", "spec mentions UI but has no ASCII wireframe fence", "warning"));
324
+ findings.push(
325
+ finding(
326
+ "missing_ascii_for_ui",
327
+ "spec mentions UI but has no ASCII wireframe fence",
328
+ "warning",
329
+ ),
330
+ );
222
331
  }
223
332
 
224
333
  const hasMermaid = /```mermaid/.test(body);
225
334
  const explicitlyNoFlow = /\bno (?:flow|pipeline|sequence|diagram)\b/.test(lower);
226
335
  const mentionsFlow = FLOW_KEYWORDS.some((k) => k.test(lower));
227
336
  if (mentionsFlow && !hasMermaid && !explicitlyNoFlow) {
228
- findings.push(finding("missing_mermaid_for_flow", "spec describes a flow/pipeline/sequence but has no mermaid fence", "warning"));
337
+ findings.push(
338
+ finding(
339
+ "missing_mermaid_for_flow",
340
+ "spec describes a flow/pipeline/sequence but has no mermaid fence",
341
+ "warning",
342
+ ),
343
+ );
229
344
  }
230
345
 
231
346
  const hasTable = /^\s*\|.+\|.+\|/m.test(body);
232
347
  const mentionsGlossary = GLOSSARY_KEYWORDS.some((k) => k.test(lower));
233
- const onlyOutOfScope = /\bout of scope\b/.test(lower) && !/\bglossary\b/.test(lower) && !/\bcontracts?\b/.test(lower);
348
+ const onlyOutOfScope =
349
+ /\bout of scope\b/.test(lower) && !/\bglossary\b/.test(lower) && !/\bcontracts?\b/.test(lower);
234
350
  if (mentionsGlossary && !hasTable && !onlyOutOfScope) {
235
- findings.push(finding("missing_table", "spec has glossary/contract/scope content but no markdown table", "warning"));
351
+ findings.push(
352
+ finding(
353
+ "missing_table",
354
+ "spec has glossary/contract/scope content but no markdown table",
355
+ "warning",
356
+ ),
357
+ );
236
358
  }
237
359
 
238
360
  return findings;
@@ -247,7 +369,11 @@ const sddIgnored = (cwd: string, slug: string): boolean => {
247
369
  return true; // not a git repo — nothing to be unignored in; validation presupposes a repo
248
370
  }
249
371
  try {
250
- execFileSync("git", ["-C", cwd, "check-ignore", path.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
372
+ execFileSync(
373
+ "git",
374
+ ["-C", cwd, "check-ignore", path.posix.join("docs", slug, "sdd", "progress.md")],
375
+ { stdio: "pipe" },
376
+ );
251
377
  return true;
252
378
  } catch {
253
379
  return false;
@@ -104,7 +104,10 @@ export const transitionSpec = (
104
104
  ok: false,
105
105
  error:
106
106
  "spec self-review failed: " +
107
- hard.map((f) => `${f.code} — ${f.message}`).concat(missing).join("; ") +
107
+ hard
108
+ .map((f) => `${f.code} — ${f.message}`)
109
+ .concat(missing)
110
+ .join("; ") +
108
111
  " — see templates/spec-template.md for the required structure",
109
112
  };
110
113
  }
@@ -150,7 +153,8 @@ export const transitionPlan = (
150
153
  }
151
154
  const missing: string[] = [];
152
155
  const stripped = stripFences(text);
153
- if (parseTasksFromPlan(text).length === 0) missing.push("no ### Task N: sections outside fences");
156
+ if (parseTasksFromPlan(text).length === 0)
157
+ missing.push("no ### Task N: sections outside fences");
154
158
  if (!/^\s*\*+Spec:\*+/im.test(stripped)) missing.push("**Spec:** header missing");
155
159
  if (!/^\s*\*+Branch:\*+/im.test(stripped)) missing.push("**Branch:** header missing");
156
160
  if (missing.length > 0) {
@@ -25,7 +25,12 @@ export const ensureProjectGitignore = (
25
25
  if (!confirmed) return { ok: false, error: "confirmed: true required" };
26
26
  const file = path.join(workspaceRoot, ".gitignore");
27
27
  const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
28
- const existingLines = new Set(existing.split("\n").map((l) => l.trim()).filter(Boolean));
28
+ const existingLines = new Set(
29
+ existing
30
+ .split("\n")
31
+ .map((l) => l.trim())
32
+ .filter(Boolean),
33
+ );
29
34
  const added: string[] = [];
30
35
  const append: string[] = [];
31
36
  for (const entry of GITIGNORE_ENTRIES) {
@@ -35,7 +40,11 @@ export const ensureProjectGitignore = (
35
40
  }
36
41
  if (append.length) {
37
42
  const separator = existing && !existing.endsWith("\n") ? "\n" : "";
38
- writeFileSync(file, existing + separator + (existing ? "\n" : "") + append.join("\n") + "\n", "utf8");
43
+ writeFileSync(
44
+ file,
45
+ existing + separator + (existing ? "\n" : "") + append.join("\n") + "\n",
46
+ "utf8",
47
+ );
39
48
  } else if (!existsSync(file)) {
40
49
  writeFileSync(file, "", "utf8");
41
50
  }
@@ -25,7 +25,8 @@ const resolveFromMessagePaths = (root: string, message: string): Resolved => {
25
25
  const paths = extractMessagePaths(message);
26
26
  if (paths.length === 0) return { error: "no paths" };
27
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" };
28
+ if (slugs.length !== 1)
29
+ return { error: "multiple features in message — use exactly one docs/<slug>/ pair" };
29
30
  const slug = slugs[0];
30
31
  const plan = `docs/${slug}/plan.md`;
31
32
  const spec = `docs/${slug}/spec.md`;
@@ -49,8 +50,15 @@ const resolveActivePair = (root: string): Resolved => {
49
50
  const plan = path.join("docs", slug, "plan.md");
50
51
  const spec = path.join("docs", slug, "spec.md");
51
52
  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])) {
53
+ const score = Math.max(
54
+ statSync(path.join(root, spec)).mtimeMs,
55
+ statSync(path.join(root, plan)).mtimeMs,
56
+ );
57
+ if (
58
+ best === null ||
59
+ score > best.score ||
60
+ (score === best.score && slug < best.spec.split("/")[1])
61
+ ) {
54
62
  best = { score, spec, plan, source: "active_pair" };
55
63
  }
56
64
  }
@@ -69,7 +77,10 @@ export const resolveWorkflowPaths = (root: string, message: string): Resolved =>
69
77
  if (!existsSync(docsDir) || listMd(docsDir).length === 0) {
70
78
  return { error: "no docs/<slug>/ features found under docs/" };
71
79
  }
72
- return { error: "could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}" };
80
+ return {
81
+ error:
82
+ "could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}",
83
+ };
73
84
  };
74
85
 
75
86
  export const buildHandoffContract = ({
@@ -85,7 +96,9 @@ export const buildHandoffContract = ({
85
96
  }): { prompt: string } | { error: string } => {
86
97
  const validated = docsValidate({ spec_path: spec, plan_path: plan, workspace_root: root });
87
98
  if (validated.ok === false) {
88
- return { error: `docs validation failed\n${JSON.stringify({ ok: false, errors: validated.errors })}` };
99
+ return {
100
+ error: `docs validation failed\n${JSON.stringify({ ok: false, errors: validated.errors })}`,
101
+ };
89
102
  }
90
103
  const branchResolved = resolveBranch({ spec_path: spec, plan_path: plan, workspace_root: root });
91
104
  if ("error" in branchResolved) return { error: branchResolved.error as string };
@@ -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 = "CHANGELOG.md" | "README.md" | ".editorconfig" | ".gitattributes" | "LICENSE" | "CONTRIBUTING.md";
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 { return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); } catch { return null; }
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 = (root: string): { state: Record<HygieneFile, State>; openSource: boolean } => {
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 ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes", "LICENSE", "CONTRIBUTING.md"] as HygieneFile[]) {
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))) { state[file] = "missing"; continue; }
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";