@brainervirus/workit-core 0.6.0 → 0.7.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 (95) hide show
  1. package/package.json +10 -14
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +25 -22
  5. package/scripts/rewrite-workspace-deps.ts +15 -9
  6. package/scripts/sync-runtime.sh +71 -19
  7. package/scripts/vendor-assets.ts +37 -0
  8. package/skills/wk-implement/SKILL.md +2 -2
  9. package/skills/wk-pr/SKILL.md +1 -1
  10. package/src/core/boundary.ts +27 -0
  11. package/src/core/branch-policy.ts +63 -0
  12. package/src/core/branch.ts +166 -57
  13. package/src/core/changelog.ts +17 -14
  14. package/src/core/config-guard.ts +9 -2
  15. package/src/core/config.ts +227 -32
  16. package/src/core/detector.ts +22 -11
  17. package/src/core/docs-layout.ts +251 -0
  18. package/src/core/docs-migration.ts +639 -0
  19. package/src/core/docs-repo.ts +58 -21
  20. package/src/core/docs-validate.ts +181 -43
  21. package/src/core/doctor.ts +801 -0
  22. package/src/core/flow-state.ts +1579 -137
  23. package/src/core/git.ts +22 -5
  24. package/src/core/gitignore.ts +11 -2
  25. package/src/core/handoff-context.ts +18 -5
  26. package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
  27. package/src/core/hygiene.ts +53 -17
  28. package/src/core/init.ts +128 -31
  29. package/src/core/logger.ts +321 -0
  30. package/src/core/package-root.ts +28 -0
  31. package/src/core/parse-sections.ts +2 -2
  32. package/src/core/plan-tasks.ts +13 -3
  33. package/src/core/ports/init-toolkit-status.ts +1 -1
  34. package/src/core/ports/vcs-verify-token.ts +1 -1
  35. package/src/core/ports/youtrack-api.ts +4 -2
  36. package/src/core/ports/youtrack-config.ts +1 -3
  37. package/src/core/ports/youtrack-verify-token.ts +1 -1
  38. package/src/core/pr-create.ts +156 -29
  39. package/src/core/present.ts +11 -2
  40. package/src/core/registration.ts +215 -0
  41. package/src/core/reminder.ts +1 -2
  42. package/src/core/repo-context.ts +447 -0
  43. package/src/core/repo-tool.ts +4 -1
  44. package/src/core/repo-tools.ts +23 -0
  45. package/src/core/rules.ts +10 -7
  46. package/src/core/safe-write.ts +22 -0
  47. package/src/core/scripts.ts +3 -39
  48. package/src/core/sdd.ts +56 -31
  49. package/src/core/setup-state.ts +54 -0
  50. package/src/core/setup.ts +1216 -0
  51. package/src/core/skill-manifests.ts +95 -0
  52. package/src/core/support-matrix.ts +12 -0
  53. package/src/core/sync-runtime.ts +348 -0
  54. package/src/core/templates.ts +16 -6
  55. package/src/core/vcs-config.ts +191 -62
  56. package/src/core/verify-parse.ts +4 -2
  57. package/src/core/verify-project.ts +181 -0
  58. package/src/core/workspaces.ts +136 -17
  59. package/src/core/youtrack-tools.ts +228 -0
  60. package/src/core/youtrack.ts +320 -87
  61. package/src/core.ts +18 -3
  62. package/templates/execution-contract.md +9 -7
  63. package/templates/superpowers-doc-contract.md +5 -4
  64. package/scripts/_shared/common.sh +0 -158
  65. package/scripts/changelog-context.sh +0 -42
  66. package/scripts/docs-refresh-context.sh +0 -40
  67. package/scripts/init/apply.sh +0 -5
  68. package/scripts/init/status.sh +0 -5
  69. package/scripts/init/toolkit-status.sh +0 -5
  70. package/scripts/pr-create.sh +0 -5
  71. package/scripts/pr-ready-context.sh +0 -88
  72. package/scripts/present/ascii-wireframe.sh +0 -5
  73. package/scripts/present/flow-diagram.sh +0 -5
  74. package/scripts/release-notes-context.sh +0 -40
  75. package/scripts/vcs/config.sh +0 -5
  76. package/scripts/vcs/merged-style.sh +0 -5
  77. package/scripts/vcs/token-create-urls.sh +0 -5
  78. package/scripts/vcs/verify-token.sh +0 -5
  79. package/scripts/verify-project.sh +0 -140
  80. package/scripts/youtrack/api.sh +0 -5
  81. package/scripts/youtrack/config.sh +0 -5
  82. package/scripts/youtrack/greeting.sh +0 -5
  83. package/scripts/youtrack/parse-duration.sh +0 -5
  84. package/scripts/youtrack/token-create-url.sh +0 -5
  85. package/scripts/youtrack/verify-token.sh +0 -5
  86. package/scripts/youtrack/work-date-ms.sh +0 -5
  87. package/src/tools/docs-repo.ts +0 -42
  88. package/src/tools/flow.ts +0 -88
  89. package/src/tools/index.ts +0 -22
  90. package/src/tools/present.ts +0 -45
  91. package/src/tools/repo.ts +0 -357
  92. package/src/tools/rules.ts +0 -30
  93. package/src/tools/sdd.ts +0 -189
  94. package/src/tools/templates.ts +0 -27
  95. package/src/tools/youtrack.ts +0 -360
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const CANONICAL_SKILLS = {
5
+ superpowers: [
6
+ "brainstorming",
7
+ "dispatching-parallel-agents",
8
+ "executing-plans",
9
+ "finishing-a-development-branch",
10
+ "receiving-code-review",
11
+ "requesting-code-review",
12
+ "subagent-driven-development",
13
+ "systematic-debugging",
14
+ "test-driven-development",
15
+ "using-git-worktrees",
16
+ "using-superpowers",
17
+ "verification-before-completion",
18
+ "writing-plans",
19
+ "writing-skills",
20
+ ],
21
+ workit: [
22
+ "wk-changelog",
23
+ "wk-commit",
24
+ "wk-docs-refresh",
25
+ "wk-handoff",
26
+ "wk-implement",
27
+ "wk-init",
28
+ "wk-issue-update",
29
+ "wk-meetings",
30
+ "wk-pr",
31
+ "wk-release-notes",
32
+ "wk-status",
33
+ "wk-verify",
34
+ ],
35
+ } as const;
36
+
37
+ export const skillManifestNames = (root: string): string[] =>
38
+ existsSync(root)
39
+ ? readdirSync(root)
40
+ .filter((name) => existsSync(path.join(root, name, "SKILL.md")))
41
+ .sort()
42
+ : [];
43
+
44
+ export const validateSkillManifests = (
45
+ root: string,
46
+ expected: readonly string[],
47
+ label: string,
48
+ ): string | null => {
49
+ const actual = skillManifestNames(root);
50
+ const missing = expected.filter((name) => !actual.includes(name));
51
+ const extra = actual.filter((name) => !expected.includes(name));
52
+ return missing.length === 0 && extra.length === 0
53
+ ? null
54
+ : `${label} mismatch at ${root} (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`;
55
+ };
56
+
57
+ export const validateCursorSkills = (pluginDir: string): string | null => {
58
+ const workit = validateSkillManifests(
59
+ path.join(pluginDir, "skills"),
60
+ CANONICAL_SKILLS.workit,
61
+ "Cursor Workit skills",
62
+ );
63
+ if (workit) return workit;
64
+ const vendor = path.join(pluginDir, "vendor/superpowers/skills");
65
+ const superpowers = validateSkillManifests(
66
+ vendor,
67
+ CANONICAL_SKILLS.superpowers,
68
+ "Cursor Superpowers skills",
69
+ );
70
+ if (superpowers) return superpowers;
71
+
72
+ const pending = [vendor];
73
+ while (pending.length > 0) {
74
+ const dir = pending.pop()!;
75
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
76
+ const file = path.join(dir, entry.name);
77
+ if (entry.isDirectory()) pending.push(file);
78
+ else if (
79
+ (statSync(file).mode & 0o111) !== 0 ||
80
+ readFileSync(file).subarray(0, 2).toString("latin1") === "#!"
81
+ ) {
82
+ return `Cursor vendor contains active file: ${file}`;
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+
89
+ if (import.meta.main) {
90
+ const error = validateCursorSkills(process.argv[2] ?? "");
91
+ if (error) {
92
+ process.stderr.write(`${error}\n`);
93
+ process.exit(1);
94
+ }
95
+ }
@@ -0,0 +1,12 @@
1
+ // Declared support matrix for published Workit artifacts (RR-10/PT-11/PT-12).
2
+ // Single source of truth for the pinned toolchain and host versions; the CI
3
+ // workflow, package engines, and lockfiles must stay in sync (enforced by
4
+ // test/artifacts/manifests.test.ts and test/artifacts/packed-runtime.test.ts).
5
+ // Deno is intentionally not part of the matrix (PT-12): nothing advertises a
6
+ // host that has no executable artifact/test evidence.
7
+ export const SUPPORT_MATRIX = {
8
+ bun: "1.3.14",
9
+ node: { minimum: "20", current: "22" },
10
+ opencode: { minimum: "1.15.0", current: "1.17.7" },
11
+ os: ["ubuntu-latest", "macos-latest", "windows-latest"],
12
+ } as const;
@@ -0,0 +1,348 @@
1
+ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
2
+ import {
3
+ accessSync,
4
+ constants,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { validateCursorSkills } from "./skill-manifests";
16
+
17
+ // Port of scripts/sync-runtime.sh — explicit runtime sync used by the installers.
18
+ // NOT part of runtime startup: the session-start hook performs no sync (RL-09).
19
+ // The bash installer scripts still invoke sync-runtime.sh directly; this typed
20
+ // operation mirrors the same RR-05 failure semantics (missing flock, held lock,
21
+ // and failed fetch all fail loudly).
22
+
23
+ export type SyncRuntimeResult = { ok: true } | { ok: false; error: string };
24
+
25
+ export type SyncRuntimeOptions = {
26
+ home?: string;
27
+ dev?: string;
28
+ repoSlug?: string;
29
+ lockDir?: string;
30
+ env?: NodeJS.ProcessEnv;
31
+ };
32
+
33
+ const run = (cmd: string, args: string[], opts: { cwd?: string; env: NodeJS.ProcessEnv }) => {
34
+ const r = spawnSync(cmd, args, {
35
+ cwd: opts.cwd,
36
+ env: opts.env,
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ });
40
+ return {
41
+ exitCode: r.status ?? 1,
42
+ stdout: (r.stdout ?? "").trim(),
43
+ stderr: (r.stderr ?? "").trim(),
44
+ };
45
+ };
46
+
47
+ /**
48
+ * Acquire a non-blocking flock for the duration of the sync (mirrors
49
+ * `exec 9>"$LOCK"; flock -n 9`). The `sh` child opens fd 9, takes the flock,
50
+ * then blocks reading its stdin. When this parent exits — normally, or
51
+ * hard-killed (SIGKILL/SIGINT/crash) — the kernel closes the stdin pipe write
52
+ * end, the child's read hits EOF, and the child exits, releasing the flock.
53
+ * The lock thus dies with its owner (no orphan holder), matching
54
+ * sync-runtime.sh where the flock dies with the shell process itself. No
55
+ * probe-then-hold TOCTOU window and no duration cap: the child holds the lock
56
+ * for exactly the parent's lifetime. The child signals acquisition by writing
57
+ * "ok" to stdout; any other exit means the flock was already held.
58
+ */
59
+ async function acquireLock(
60
+ lock: string,
61
+ env: NodeJS.ProcessEnv,
62
+ ): Promise<{ proc: ChildProcess } | { error: string }> {
63
+ const proc = spawn(
64
+ "sh",
65
+ [
66
+ "-c",
67
+ `exec 9>"$1"; flock -n 9 || exit 7; printf 'ok\\n'; while IFS= read -r _; do :; done`,
68
+ "workit-lock",
69
+ lock,
70
+ ],
71
+ { stdio: ["pipe", "pipe", "ignore"], env },
72
+ );
73
+ const acquired = await new Promise<boolean>((resolve) => {
74
+ let settled = false;
75
+ const finish = (ok: boolean) => {
76
+ if (!settled) {
77
+ settled = true;
78
+ resolve(ok);
79
+ }
80
+ };
81
+ proc.stdout?.once("data", () => finish(true));
82
+ proc.once("exit", () => finish(false));
83
+ proc.once("error", () => finish(false));
84
+ });
85
+ if (!acquired) {
86
+ killLockProcess(proc);
87
+ return { error: `sync-runtime: another process holds ${lock} — state unverified, failing` };
88
+ }
89
+ return { proc };
90
+ }
91
+
92
+ /** Kill the lock-holder child so the flock is released immediately. */
93
+ function killLockProcess(proc: ChildProcess): void {
94
+ try {
95
+ proc.kill();
96
+ } catch {
97
+ /* already exited */
98
+ }
99
+ }
100
+
101
+ export async function syncRuntime(options: SyncRuntimeOptions = {}): Promise<SyncRuntimeResult> {
102
+ const env = options.env ?? process.env;
103
+ const home = options.home ?? env.HOME ?? os.homedir();
104
+ const dev =
105
+ options.dev ??
106
+ env.WORKFLOW_TOOLKIT_DEV ??
107
+ path.join(home, "Documents/projects/personal/workflow-toolkit");
108
+ const repoSlug = options.repoSlug ?? env.WORKFLOW_TOOLKIT_REPO ?? "BrainerVirus/workit";
109
+ const share = path.join(home, ".local/share/workflow-toolkit");
110
+ const pluginDir = path.join(home, ".cursor/plugins/local/workflow-toolkit");
111
+ const opencodePlugins = path.join(home, ".config/opencode/plugins");
112
+ const lock = path.join(
113
+ options.lockDir ?? env.XDG_RUNTIME_DIR ?? "/tmp",
114
+ "workflow-toolkit-sync.lock",
115
+ );
116
+
117
+ // RR-05: a missing required tool must never look like a successful sync.
118
+ if (run("flock", ["--version"], { env }).exitCode !== 0) {
119
+ return {
120
+ ok: false,
121
+ error: "FATAL: sync-runtime requires flock (util-linux) — not found in PATH",
122
+ };
123
+ }
124
+
125
+ const acquired = await acquireLock(lock, env);
126
+ if ("error" in acquired) return { ok: false, error: acquired.error };
127
+ const lockProc = acquired.proc;
128
+
129
+ try {
130
+ let src = "";
131
+ if (
132
+ existsSync(path.join(dev, "packages/workit-opencode/src/plugin.ts")) &&
133
+ existsSync(path.join(dev, "packages/workit-cursor/.cursor-plugin"))
134
+ ) {
135
+ src = dev;
136
+ } else if (existsSync(path.join(share, ".git"))) {
137
+ // RR-05: propagate fetch/pull failures to a FATAL nonzero exit.
138
+ if (run("git", ["-C", share, "fetch", "--quiet", "origin"], { env }).exitCode !== 0) {
139
+ return { ok: false, error: `FATAL: could not fetch updates for ${share}` };
140
+ }
141
+ const pull = run("git", ["-C", share, "pull", "--ff-only", "--quiet", "origin", "main"], {
142
+ env,
143
+ });
144
+ if (pull.exitCode !== 0) {
145
+ return { ok: false, error: `FATAL: could not update ${share} from origin/main` };
146
+ }
147
+ src = share;
148
+ } else if (!existsSync(path.join(share, "packages/workit-core/src"))) {
149
+ mkdirSync(path.dirname(share), { recursive: true });
150
+ const clone = run(
151
+ "git",
152
+ ["clone", "--depth", "1", `https://github.com/${repoSlug}.git`, share],
153
+ { env },
154
+ );
155
+ if (clone.exitCode !== 0) {
156
+ return {
157
+ ok: false,
158
+ error: `FATAL: could not clone https://github.com/${repoSlug}.git into ${share}`,
159
+ };
160
+ }
161
+ src = share;
162
+ } else {
163
+ src = share;
164
+ }
165
+
166
+ const cursorSrc = path.join(src, "packages/workit-cursor");
167
+ const cursorEntries = ["mcp-server.js", "cursor-session-start.js"];
168
+ const bun = resolveBun(env, home);
169
+ if (!bun.ok) return bun;
170
+ const dependencies = ["@brainervirus/workit-core", "@modelcontextprotocol/sdk", "zod"];
171
+ if (
172
+ dependencies.some((dependency) => !existsSync(path.join(src, "node_modules", dependency)))
173
+ ) {
174
+ if (!existsSync(path.join(src, "bun.lock"))) {
175
+ return { ok: false, error: `FATAL: dependency install requires ${src}/bun.lock` };
176
+ }
177
+ const install = run(bun.path, ["install", "--frozen-lockfile"], { cwd: src, env });
178
+ if (install.exitCode !== 0) {
179
+ return {
180
+ ok: false,
181
+ error: `FATAL: root dependency install failed in ${src}: ${install.stderr}`,
182
+ };
183
+ }
184
+ }
185
+ const build = run(bun.path, [path.join(cursorSrc, "scripts/build.ts")], {
186
+ cwd: src,
187
+ env: { ...env, PATH: `${path.dirname(bun.path)}${path.delimiter}${env.PATH ?? ""}` },
188
+ });
189
+ if (build.exitCode !== 0) {
190
+ return {
191
+ ok: false,
192
+ error: `FATAL: Cursor adapter build failed in ${cursorSrc}: ${build.stderr}`,
193
+ };
194
+ }
195
+ for (const entry of cursorEntries) {
196
+ const distEntry = path.join(cursorSrc, "dist", entry);
197
+ try {
198
+ const stat = statSync(distEntry);
199
+ if (
200
+ !stat.isFile() ||
201
+ stat.size === 0 ||
202
+ !readFileSync(distEntry, "utf8").startsWith("#!/usr/bin/env node\n")
203
+ ) {
204
+ throw new Error("invalid");
205
+ }
206
+ } catch {
207
+ return {
208
+ ok: false,
209
+ error: `FATAL: Cursor adapter invalid dist entry: ${distEntry}`,
210
+ };
211
+ }
212
+ }
213
+
214
+ if (src !== share) {
215
+ mkdirSync(share, { recursive: true });
216
+ // Keep .git if share is a clone; never wipe it when syncing from the monorepo.
217
+ const r = run(
218
+ "rsync",
219
+ [
220
+ "-a",
221
+ "--delete",
222
+ "--exclude",
223
+ ".git",
224
+ "--exclude",
225
+ "node_modules",
226
+ "--exclude",
227
+ "cursor/mcp/node_modules",
228
+ "--exclude",
229
+ ".cache",
230
+ `${src}/`,
231
+ `${share}/`,
232
+ ],
233
+ { env },
234
+ );
235
+ if (r.exitCode !== 0) return { ok: false, error: `FATAL: rsync share failed: ${r.stderr}` };
236
+ }
237
+
238
+ mkdirSync(path.join(home, ".cursor/plugins/local"), { recursive: true });
239
+ mkdirSync(pluginDir, { recursive: true });
240
+ const cursorCopy = run(
241
+ "rsync",
242
+ [
243
+ "-a",
244
+ "--delete",
245
+ "--exclude",
246
+ "mcp/node_modules",
247
+ `${share}/packages/workit-cursor/`,
248
+ `${pluginDir}/`,
249
+ ],
250
+ { env },
251
+ );
252
+ if (cursorCopy.exitCode !== 0) {
253
+ return { ok: false, error: `FATAL: rsync cursor plugin failed: ${cursorCopy.stderr}` };
254
+ }
255
+ const vendorError = validateCursorSkills(pluginDir);
256
+ if (vendorError) return { ok: false, error: `FATAL: ${vendorError}` };
257
+
258
+ // Canonical user rules -> Cursor .mdc (compiled by the shared core).
259
+ const configRules = path.join(env.WORKFLOW_TOOLKIT_CONFIG ?? "", "rules");
260
+ if (env.WORKFLOW_TOOLKIT_CONFIG && existsSync(configRules)) {
261
+ const rulesBun = resolveBun(env, home);
262
+ if (rulesBun.ok) {
263
+ run(
264
+ rulesBun.path,
265
+ [
266
+ "-e",
267
+ `import('${share}/packages/workit-core/src/core/rules.ts').then(({ writeCompiledCursorRules }) => writeCompiledCursorRules('${pluginDir}/rules'));`,
268
+ ],
269
+ { env },
270
+ );
271
+ }
272
+ }
273
+ writeFileSync(
274
+ path.join(pluginDir, ".workflow-toolkit-root"),
275
+ `${share}/packages/workit-core\n`,
276
+ );
277
+
278
+ // Remove broken TLA live-loader if present (OpenCode ignored it; /wk-* vanished).
279
+ rmSync(path.join(opencodePlugins, "workflow-toolkit.ts"), { force: true });
280
+
281
+ // Ensure OpenCode has plugin peer dep.
282
+ const pkg = path.join(home, ".config/opencode/package.json");
283
+ if (existsSync(pkg)) {
284
+ try {
285
+ const data = JSON.parse(readFileSync(pkg, "utf8")) as {
286
+ dependencies?: Record<string, string>;
287
+ };
288
+ data.dependencies = data.dependencies ?? {};
289
+ data.dependencies["@opencode-ai/plugin"] ??= "1.17.7";
290
+ writeFileSync(pkg, JSON.stringify(data, null, 2) + "\n");
291
+ } catch {
292
+ /* best-effort */
293
+ }
294
+ }
295
+
296
+ // Drop bun package cache so old github/file installs cannot shadow file:// plugin.ts.
297
+ const cacheDir = path.join(home, ".cache/opencode/packages");
298
+ if (existsSync(cacheDir)) {
299
+ try {
300
+ for (const entry of readdirSync(cacheDir)) {
301
+ if (entry.startsWith("workflow-toolkit-opencode@")) {
302
+ rmSync(path.join(cacheDir, entry), { recursive: true, force: true });
303
+ }
304
+ }
305
+ } catch {
306
+ /* best-effort */
307
+ }
308
+ }
309
+
310
+ return { ok: true };
311
+ } finally {
312
+ killLockProcess(lockProc);
313
+ }
314
+ }
315
+
316
+ const resolveBun = (
317
+ env: NodeJS.ProcessEnv,
318
+ home: string,
319
+ ): { ok: true; path: string } | { ok: false; error: string } => {
320
+ if (env.BUN) {
321
+ return executable(env.BUN)
322
+ ? { ok: true, path: env.BUN }
323
+ : { ok: false, error: `FATAL: BUN is set but unusable: ${env.BUN}` };
324
+ }
325
+ const homeBun = path.join(home, ".bun/bin/bun");
326
+ if (executable(homeBun)) return { ok: true, path: homeBun };
327
+ const pathBun = executableOnPath("bun", env);
328
+ return pathBun
329
+ ? { ok: true, path: pathBun }
330
+ : { ok: false, error: "FATAL: Bun is required to build the Cursor adapter" };
331
+ };
332
+
333
+ const executable = (file: string): boolean => {
334
+ try {
335
+ accessSync(file, constants.X_OK);
336
+ return true;
337
+ } catch {
338
+ return false;
339
+ }
340
+ };
341
+
342
+ const executableOnPath = (name: string, env: NodeJS.ProcessEnv): string | null => {
343
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
344
+ const candidate = path.join(dir, name);
345
+ if (executable(candidate)) return candidate;
346
+ }
347
+ return null;
348
+ };
@@ -1,18 +1,24 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { configDir } from "./config";
4
+ import { assetRoot } from "./package-root";
5
5
 
6
6
  export type TemplateName = "issue-update" | "greeting" | "headers";
7
7
 
8
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
+ const repoRoot = assetRoot();
9
9
 
10
- export const templatePath = (name: TemplateName): string => path.join(configDir(), "templates", `${name}.md`);
10
+ export const templatePath = (name: TemplateName): string =>
11
+ path.join(configDir(), "templates", `${name}.md`);
11
12
 
12
- export const readTemplate = (name: TemplateName): { source: "config" | "repo"; content: string } => {
13
+ export const readTemplate = (
14
+ name: TemplateName,
15
+ ): { source: "config" | "repo"; content: string } => {
13
16
  const cfg = templatePath(name);
14
17
  if (existsSync(cfg)) return { source: "config", content: readFileSync(cfg, "utf8") };
15
- return { source: "repo", content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8") };
18
+ return {
19
+ source: "repo",
20
+ content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8"),
21
+ };
16
22
  };
17
23
 
18
24
  export const writeTemplate = (
@@ -27,7 +33,11 @@ export const writeTemplate = (
27
33
  return { ok: true, path: file };
28
34
  };
29
35
 
30
- export const listTemplates = (): { name: TemplateName; source: "config" | "repo" | "missing"; path: string }[] =>
36
+ export const listTemplates = (): {
37
+ name: TemplateName;
38
+ source: "config" | "repo" | "missing";
39
+ path: string;
40
+ }[] =>
31
41
  (["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
32
42
  const cfg = templatePath(name);
33
43
  const repoFile = path.join(repoRoot, "templates", `${name}.md`);