kataster 0.0.0-stage → 0.2.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 (162) hide show
  1. package/.claude/agents/acceptance-tester.md +53 -0
  2. package/.claude/agents/code-reviewer.md +56 -0
  3. package/.claude/agents/domain-architect.md +40 -0
  4. package/.claude/agents/lead-dev.md +50 -0
  5. package/.claude/agents/product-owner.md +39 -0
  6. package/.claude/agents/repo-analyst.md +78 -0
  7. package/.claude/agents/requirements-engineer.md +44 -0
  8. package/.claude/agents/ux-designer.md +64 -0
  9. package/.claude/hooks/bash-guard.ts +92 -0
  10. package/.claude/hooks/code-check.ts +37 -0
  11. package/.claude/hooks/path-guard.ts +34 -0
  12. package/.claude/hooks/path-rules.ts +66 -0
  13. package/.claude/hooks/project-check.ts +108 -0
  14. package/.claude/hooks/protect-files.ts +55 -0
  15. package/.claude/hooks/session-start.ts +213 -0
  16. package/.claude/hooks/stop-gate.ts +56 -0
  17. package/.claude/hooks/validate-docs.ts +58 -0
  18. package/.claude/lib/architecture.ts +736 -0
  19. package/.claude/lib/backlog-github.ts +454 -0
  20. package/.claude/lib/backlog-jira.ts +347 -0
  21. package/.claude/lib/backlog-sync.ts +42 -0
  22. package/.claude/lib/backlog.ts +396 -0
  23. package/.claude/lib/bazel.ts +212 -0
  24. package/.claude/lib/config.ts +122 -0
  25. package/.claude/lib/discovery.ts +821 -0
  26. package/.claude/lib/engineering.ts +406 -0
  27. package/.claude/lib/nx.ts +129 -0
  28. package/.claude/lib/repo.ts +1194 -0
  29. package/.claude/lib/toolgraph.ts +74 -0
  30. package/.claude/lib/turbo.ts +166 -0
  31. package/.claude/package-lock.json +427 -0
  32. package/.claude/package.json +21 -0
  33. package/.claude/settings.json +82 -0
  34. package/.claude/skills/adopt/SKILL.md +283 -0
  35. package/.claude/skills/adopt/scripts/boundary-check.ts +156 -0
  36. package/.claude/skills/adopt/scripts/package-doc.ts +71 -0
  37. package/.claude/skills/adopt/scripts/repo-map.ts +123 -0
  38. package/.claude/skills/adopt/scripts/rule-doc.ts +80 -0
  39. package/.claude/skills/architect/ADR-FORMAT.md +97 -0
  40. package/.claude/skills/architect/SKILL.md +64 -0
  41. package/.claude/skills/architect/TECH-STACK.md +21 -0
  42. package/.claude/skills/backlog/SKILL.md +198 -0
  43. package/.claude/skills/backlog/scripts/backlog-pull.ts +45 -0
  44. package/.claude/skills/backlog/scripts/backlog-push.ts +38 -0
  45. package/.claude/skills/backlog/scripts/backlog-status.ts +32 -0
  46. package/.claude/skills/backlog/scripts/jira-import.ts +40 -0
  47. package/.claude/skills/backlog/scripts/jira-plan.ts +26 -0
  48. package/.claude/skills/backlog/scripts/jira-record.ts +41 -0
  49. package/.claude/skills/bootstrap/SKILL.md +119 -0
  50. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  51. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/.claude/skills/codebase-design/SKILL.md +114 -0
  53. package/.claude/skills/domain-model/CONTEXT-FORMAT.md +47 -0
  54. package/.claude/skills/domain-model/SKILL.md +64 -0
  55. package/.claude/skills/domain-model/templates/data-model.md +29 -0
  56. package/.claude/skills/engineering-conventions/SKILL.md +31 -0
  57. package/.claude/skills/event-storming/SKILL.md +44 -0
  58. package/.claude/skills/event-storming/schema/events.schema.json +124 -0
  59. package/.claude/skills/event-storming/scripts/render-event-storming.ts +11 -0
  60. package/.claude/skills/event-storming/scripts/validate-events.ts +11 -0
  61. package/.claude/skills/event-storming/templates/events.example.yaml +91 -0
  62. package/.claude/skills/feature/SKILL.md +86 -0
  63. package/.claude/skills/feature/scripts/intake.ts +356 -0
  64. package/.claude/skills/grill-me/SKILL.md +7 -0
  65. package/.claude/skills/grill-with-docs/SKILL.md +7 -0
  66. package/.claude/skills/grilling/SKILL.md +30 -0
  67. package/.claude/skills/groom-backlog/SKILL.md +93 -0
  68. package/.claude/skills/groom-backlog/scripts/backlog-health.ts +238 -0
  69. package/.claude/skills/implement/SKILL.md +129 -0
  70. package/.claude/skills/implement/scripts/affected.ts +245 -0
  71. package/.claude/skills/implement/scripts/check-commands.ts +37 -0
  72. package/.claude/skills/implement/scripts/check-language.ts +15 -0
  73. package/.claude/skills/implement/scripts/check-scenarios.ts +59 -0
  74. package/.claude/skills/implement/scripts/next-story.ts +23 -0
  75. package/.claude/skills/implement/scripts/story-context.ts +135 -0
  76. package/.claude/skills/implement/scripts/story-status.ts +47 -0
  77. package/.claude/skills/implement/scripts/verify.ts +92 -0
  78. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  79. package/.claude/skills/improve-codebase-architecture/SKILL.md +71 -0
  80. package/.claude/skills/retro/SKILL.md +53 -0
  81. package/.claude/skills/review-stories/SKILL.md +31 -0
  82. package/.claude/skills/shape/SKILL.md +193 -0
  83. package/.claude/skills/tdd/SKILL.md +38 -0
  84. package/.claude/skills/tdd/mocking.md +59 -0
  85. package/.claude/skills/tdd/tests.md +77 -0
  86. package/.claude/skills/user-stories/SKILL.md +138 -0
  87. package/.claude/skills/user-stories/schema/story.schema.json +39 -0
  88. package/.claude/skills/user-stories/scripts/render-backlog.ts +4 -0
  89. package/.claude/skills/user-stories/scripts/validate-stories.ts +13 -0
  90. package/.claude/skills/user-stories/templates/story-template.md +38 -0
  91. package/.claude/skills/ux-design/SKILL.md +107 -0
  92. package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  93. package/.claude/skills/writing-for-agents/SKILL.md +81 -0
  94. package/.claude/tsconfig.json +15 -0
  95. package/.opencode/agents/acceptance-tester.md +64 -0
  96. package/.opencode/agents/code-reviewer.md +69 -0
  97. package/.opencode/agents/domain-architect.md +59 -0
  98. package/.opencode/agents/lead-dev.md +67 -0
  99. package/.opencode/agents/product-owner.md +59 -0
  100. package/.opencode/agents/repo-analyst.md +86 -0
  101. package/.opencode/agents/requirements-engineer.md +58 -0
  102. package/.opencode/agents/ux-designer.md +83 -0
  103. package/.opencode/commands/adopt.md +7 -0
  104. package/.opencode/commands/backlog-sync.md +7 -0
  105. package/.opencode/commands/bootstrap.md +7 -0
  106. package/.opencode/commands/event-storming.md +7 -0
  107. package/.opencode/commands/feature.md +7 -0
  108. package/.opencode/commands/grill-me.md +7 -0
  109. package/.opencode/commands/grill-with-docs.md +7 -0
  110. package/.opencode/commands/groom-backlog.md +7 -0
  111. package/.opencode/commands/implement.md +7 -0
  112. package/.opencode/commands/improve-codebase-architecture.md +7 -0
  113. package/.opencode/commands/retro.md +7 -0
  114. package/.opencode/commands/review-stories.md +7 -0
  115. package/.opencode/commands/shape.md +7 -0
  116. package/.opencode/opencode.jsonc +15 -0
  117. package/.opencode/plugins/kataster/index.ts +283 -0
  118. package/AGENTS.md +21 -0
  119. package/CHANGELOG.md +91 -0
  120. package/LICENSE +21 -0
  121. package/README.md +331 -2
  122. package/bin/kataster.mjs +7 -0
  123. package/extras/skills/shadcn/SKILL.md +277 -0
  124. package/extras/skills/shadcn/agents/openai.yml +5 -0
  125. package/extras/skills/shadcn/assets/shadcn-small.png +0 -0
  126. package/extras/skills/shadcn/assets/shadcn.png +0 -0
  127. package/extras/skills/shadcn/cli.md +290 -0
  128. package/extras/skills/shadcn/customization.md +209 -0
  129. package/extras/skills/shadcn/evals/evals.json +77 -0
  130. package/extras/skills/shadcn/mcp.md +105 -0
  131. package/extras/skills/shadcn/registry.md +277 -0
  132. package/extras/skills/shadcn/rules/base-vs-radix.md +306 -0
  133. package/extras/skills/shadcn/rules/chat.md +250 -0
  134. package/extras/skills/shadcn/rules/composition.md +213 -0
  135. package/extras/skills/shadcn/rules/forms.md +192 -0
  136. package/extras/skills/shadcn/rules/icons.md +101 -0
  137. package/extras/skills/shadcn/rules/styling.md +185 -0
  138. package/guide/README.md +9 -0
  139. package/guide/brownfield.md +242 -0
  140. package/guide/concepts.md +109 -0
  141. package/guide/greenfield.md +84 -0
  142. package/guide/install.md +144 -0
  143. package/guide/release.md +92 -0
  144. package/package.json +56 -4
  145. package/src/cli.mjs +302 -0
  146. package/src/detect.mjs +237 -0
  147. package/src/install.mjs +427 -0
  148. package/src/repo.mjs +129 -0
  149. package/src/util.mjs +110 -0
  150. package/templates/CONTEXT.md +21 -0
  151. package/templates/conventions.md +63 -0
  152. package/templates/docs/adr/README.md +7 -0
  153. package/templates/docs/architecture/data-model.md +31 -0
  154. package/templates/docs/domain/context-map.md +21 -0
  155. package/templates/docs/domain/events.yaml +23 -0
  156. package/templates/docs/product/initiatives/README.md +41 -0
  157. package/templates/docs/product/ux-guidelines.md +27 -0
  158. package/templates/docs/product/vision.md +36 -0
  159. package/templates/docs/reviews/.gitkeep +0 -0
  160. package/templates/docs/stories/OPEN_QUESTIONS.md +8 -0
  161. package/templates/docs/stories/README.md +26 -0
  162. package/templates/docs/workflow.md +135 -0
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "kataster",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Agent workflow for domain-driven discovery and delivery – drop it into a greenfield or brownfield repository and build the domain by grilling.",
5
+ "keywords": [
6
+ "agents",
7
+ "opencode",
8
+ "claude-code",
9
+ "domain-driven-design",
10
+ "ddd",
11
+ "domain-model",
12
+ "event-storming",
13
+ "workflow",
14
+ "scaffold"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Michael Schempp",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/hemi1986/kataster.git"
21
+ },
22
+ "homepage": "https://github.com/hemi1986/kataster#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/hemi1986/kataster/issues"
25
+ },
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=22.18"
29
+ },
30
+ "bin": {
31
+ "kataster": "./bin/kataster.mjs"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "exports": {
37
+ ".": "./src/install.mjs"
38
+ },
39
+ "files": [
40
+ "bin",
41
+ "src",
42
+ "templates",
43
+ "extras",
44
+ "guide",
45
+ ".claude",
46
+ ".opencode",
47
+ "AGENTS.md",
48
+ "README.md",
49
+ "CHANGELOG.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "test": "node --test test/*.test.mjs",
54
+ "test:install": "node --test test/install.test.mjs",
55
+ "doctor": "node bin/kataster.mjs doctor .",
56
+ "prepublishOnly": "npm test"
57
+ }
58
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,302 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin, stdout } from "node:process";
6
+ import { Installer, VERSION } from "./install.mjs";
7
+ import { buildProfile, detectProject } from "./detect.mjs";
8
+ import { readJson, write } from "./util.mjs";
9
+
10
+ const HELP = `Kataster ${VERSION} – agent workflow for domain-driven discovery and delivery
11
+
12
+ Usage:
13
+ kataster init [dir] Install the workflow into a repository (default: .)
14
+ kataster upgrade [dir] Update the workflow, keep project content and local changes
15
+ kataster doctor [dir] Check the installation
16
+ kataster --version
17
+ kataster --help
18
+
19
+ Options:
20
+ --brownfield Existing codebase: start with /adopt (default when code is found)
21
+ --greenfield New project: start with /bootstrap
22
+ --name <name> Project name (default: directory name)
23
+ --ui-language <lang> Language of the user interface (default: en)
24
+ --artifacts <lang> Language of docs, stories and code comments (default: en)
25
+ --backlog <provider> Where stories live: local (default), github, jira
26
+ --jira-project <key> Jira project key (with --backlog jira)
27
+ --github-repo <repo> GitHub repository owner/name (with --backlog github; default: this repo)
28
+ --with <list> Optional extras, comma-separated (available: shadcn)
29
+ --force Overwrite locally modified workflow files (backup is kept)
30
+ --no-install Do not run "npm install --prefix .claude"
31
+ --yes, -y Non-interactive: accept the detected defaults
32
+ --dry-run Show what would happen, write nothing
33
+ `;
34
+
35
+ function parse(argv) {
36
+ const options = { _: [] };
37
+ const takesValue = new Set(["name", "ui-language", "artifacts", "with", "backlog", "jira-project", "github-repo"]);
38
+ for (let i = 0; i < argv.length; i += 1) {
39
+ const arg = argv[i];
40
+ if (!arg.startsWith("-")) {
41
+ options._.push(arg);
42
+ continue;
43
+ }
44
+ if (arg === "--help" || arg === "-h") options.help = true;
45
+ else if (arg === "--version" || arg === "-v") options.version = true;
46
+ else if (arg === "--brownfield") options.brownfield = true;
47
+ else if (arg === "--greenfield") options.greenfield = true;
48
+ else if (arg === "--force") options.force = true;
49
+ else if (arg === "--no-install") options.noInstall = true;
50
+ else if (arg === "--yes" || arg === "-y") options.yes = true;
51
+ else if (arg === "--dry-run") options.dryRun = true;
52
+ else if (arg.startsWith("--") && arg.includes("=")) {
53
+ const [key, ...rest] = arg.slice(2).split("=");
54
+ options[camel(key)] = rest.join("=");
55
+ } else if (arg.startsWith("--") && takesValue.has(arg.slice(2))) {
56
+ options[camel(arg.slice(2))] = argv[++i];
57
+ } else if (arg.startsWith("--")) options[camel(arg.slice(2))] = true;
58
+ else throw new Error(`Unknown option: ${arg}`);
59
+ }
60
+ return options;
61
+ }
62
+
63
+ const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
64
+
65
+ async function ask(question, fallback, { yes } = {}) {
66
+ if (yes) return fallback;
67
+ if (!stdin.isTTY) return fallback;
68
+ const rl = createInterface({ input: stdin, output: stdout });
69
+ try {
70
+ const answer = (await rl.question(`${question}${fallback ? ` [${fallback}]` : ""}: `)).trim();
71
+ return answer || fallback;
72
+ } finally {
73
+ rl.close();
74
+ }
75
+ }
76
+
77
+ function banner() {
78
+ console.log(`\n Kataster ${VERSION}\n Discovery and delivery workflow for agent harnesses\n`);
79
+ }
80
+
81
+ function nextSteps(root, profile) {
82
+ const rel = root === resolve(".") ? "." : root;
83
+ const command = profile.kind === "brownfield" ? "/adopt" : "/bootstrap";
84
+ console.log(`
85
+ Next steps in ${rel}:
86
+ 1. Review .claude/project.json – name, stack, languages, commands.
87
+ 2. Open the repository in OpenCode (or Claude Code) and run ${command}.
88
+ ${profile.kind === "brownfield"
89
+ ? "It analyses code, docs, tests and git history, drafts the domain model, then grills you to correct it."
90
+ : "It grills the vision, runs the event storming and builds the domain model with you."}
91
+ 3. Read CONTEXT.md and docs/domain/events.yaml as they fill up – they are the source of truth.
92
+ ${profile.backlog?.provider && profile.backlog.provider !== "local"
93
+ ? ` 4. Backlog backend: ${profile.backlog.provider}. Fill in .claude/project.json and sync with /backlog-sync.`
94
+ : ""}
95
+
96
+ Useful commands afterwards:
97
+ /grill-me /grill-with-docs sharpen a plan and write the docs as you go
98
+ /event-storming explore a process or the whole domain
99
+ /feature <idea> add a feature through grilling, storming, stories
100
+ /review-stories one lead-dev pass before a story is ready
101
+ /implement [ST-NNN] test-first implementation of one story
102
+ /backlog-sync pull/push the backlog mirror (Jira or GitHub Issues)
103
+ /retro retrospective on a session
104
+ `);
105
+ }
106
+
107
+ function printSummary(installer, extras) {
108
+ const { created, updated, conflicts } = installer.summary();
109
+ console.log(`\nDone: ${created} created, ${updated} updated${extras.length ? `, extras: ${extras.join(", ")}` : ""}.`);
110
+ if (conflicts.length) {
111
+ console.log(`\n${conflicts.length} file(s) kept because they were modified locally:`);
112
+ for (const c of conflicts) console.log(` - ${c}`);
113
+ console.log(" Run with --force to overwrite them (a backup is written to .kataster/backup/).");
114
+ }
115
+ }
116
+
117
+ async function init(options) {
118
+ const root = resolve(options._[1] ?? ".");
119
+ const detection = detectProject(root);
120
+ const extras = String(options.with ?? "").split(",").map((s) => s.trim()).filter(Boolean);
121
+
122
+ banner();
123
+ if (existsSync(join(root, ".claude", "install.json"))) {
124
+ console.log(`${root} already has a Kataster installation – running upgrade.\n`);
125
+ return upgrade({ ...options, _: ["upgrade", root] });
126
+ }
127
+ console.log(`Installing into ${root}`);
128
+ console.log(`Detected: ${detection.kind}, ${detection.languages.join(", ") || "no code yet"}${
129
+ detection.frameworks.length ? `, ${detection.frameworks.join(", ")}` : ""
130
+ }${detection.databases.length ? `, ${detection.databases.join(", ")}` : ""}${
131
+ detection.repoTooling?.monorepo ? `, ${detection.repoTooling.kind} monorepo` : ""
132
+ }\n`);
133
+
134
+ const kind = options.brownfield ? "brownfield" : options.greenfield ? "greenfield" : detection.kind;
135
+ const name = await ask("Project name", options.name || detection.packageName || basename(root), options);
136
+ const uiLanguage = await ask("UI language (en, de, …)", options.uiLanguage || "en", options);
137
+ const artifactLanguage = await ask("Language of docs and stories", options.artifacts || "en", options);
138
+ const backlog = await ask("Backlog backend (local, github, jira)", options.backlog || "local", options);
139
+ if (!["local", "github", "jira"].includes(backlog)) {
140
+ throw new Error(`Unknown backlog backend '${backlog}' – use local, github or jira.`);
141
+ }
142
+ const jiraProject = backlog === "jira"
143
+ ? await ask("Jira project key (e.g. PROJ)", options.jiraProject || "", options)
144
+ : "";
145
+ const githubRepo = backlog === "github"
146
+ ? await ask("GitHub repository (owner/name, empty = this repo)", options.githubRepo || "", options)
147
+ : "";
148
+
149
+ const profile = buildProfile(root, {
150
+ name, kind, uiLanguage, artifactLanguage, detection, extras,
151
+ backlog, jiraProject, githubRepo,
152
+ });
153
+ profile.workflow.version = VERSION;
154
+
155
+ const installer = new Installer(root, { dryRun: options.dryRun, force: options.force, log: (line) => console.log(` ${line}`) });
156
+ installer.migrateLegacy();
157
+ installer.copyWorkflowDirs();
158
+ installer.mergeSettings();
159
+ installer.mergeOpencodeConfig();
160
+ installer.mergeAgentsFile(profile);
161
+ installer.copyDocsSkeleton(profile);
162
+ installer.putContext(profile);
163
+ installer.putClaudeMd(profile);
164
+ installer.putProfile(profile);
165
+ installer.putConventions(profile);
166
+ installer.installExtras(extras);
167
+ installer.updateGitignore();
168
+ installer.writeManifest();
169
+
170
+ if (options.dryRun) {
171
+ console.log("\nDry run – nothing was written.");
172
+ return;
173
+ }
174
+
175
+ if (!options.noInstall) {
176
+ const result = installer.installToolingDependencies();
177
+ if (!result.ok) console.log("\nTooling dependencies could not be installed – run: npm install --prefix .claude");
178
+ }
179
+
180
+ printSummary(installer, extras);
181
+ nextSteps(root, profile);
182
+ }
183
+
184
+ async function upgrade(options) {
185
+ const root = resolve(options._[1] ?? ".");
186
+ banner();
187
+ const installer = new Installer(root, { dryRun: options.dryRun, force: options.force, log: (line) => console.log(` ${line}`) });
188
+ if (!installer.manifest) {
189
+ console.log(`No installation found in ${root} – running init instead.\n`);
190
+ return init({ ...options, _: ["init", root] });
191
+ }
192
+ const profile = readJson(join(root, ".claude", "project.json")) ?? {};
193
+ const extras = options.with ? String(options.with).split(",").map((s) => s.trim()).filter(Boolean) : profile.workflow?.extras ?? [];
194
+
195
+ console.log(`Upgrading ${root} from ${installer.manifest.version ?? "?"} to ${VERSION}\n`);
196
+ installer.migrateLegacy();
197
+ installer.copyWorkflowDirs();
198
+ installer.mergeSettings();
199
+ installer.mergeOpencodeConfig();
200
+ if (profile.name) installer.mergeAgentsFile(profile);
201
+ installer.copyDocsSkeleton(profile);
202
+ installer.putContext(profile);
203
+ installer.putClaudeMd(profile);
204
+ installer.putConventions(profile);
205
+ installer.installExtras(extras);
206
+ installer.updateGitignore();
207
+ installer.writeManifest();
208
+
209
+ if (profile.name && !options.dryRun) {
210
+ profile.workflow = { ...(profile.workflow ?? {}), version: VERSION, extras };
211
+ write(join(root, ".claude", "project.json"), `${JSON.stringify(profile, null, 2)}\n`);
212
+ }
213
+
214
+ if (options.dryRun) {
215
+ console.log("\nDry run – nothing was written.");
216
+ return;
217
+ }
218
+ if (!options.noInstall) installer.installToolingDependencies();
219
+ printSummary(installer, extras);
220
+ console.log("\nUpgrade complete. Project content (CONTEXT.md, docs/domain, docs/stories, ADRs) was not touched.");
221
+ }
222
+
223
+ function doctor(options) {
224
+ const root = resolve(options._[1] ?? ".");
225
+ const checks = [];
226
+ const add = (name, ok, detail = "") => checks.push({ name, ok, detail });
227
+
228
+ add("Node >= 22.18", Number(process.versions.node.split(".")[0]) > 22 || (Number(process.versions.node.split(".")[0]) === 22 && Number(process.versions.node.split(".")[1]) >= 18), process.versions.node);
229
+ add("Workflow installed", existsSync(join(root, ".claude", "skills")) && existsSync(join(root, ".opencode")), root);
230
+ const manifest = readJson(join(root, ".claude", "install.json"));
231
+ add("Install manifest", Boolean(manifest), manifest ? `version ${manifest.version}` : "missing – run init");
232
+ const leftovers = existsSync(join(root, ".laguardia")) || existsSync(join(root, ".opencode", "plugins", "laguardia"));
233
+ add("No leftovers from the old name", !leftovers, leftovers ? "run upgrade to migrate them" : "");
234
+ const profile = readJson(join(root, ".claude", "project.json"));
235
+ add("Project profile", Boolean(profile?.name), profile?.name ?? "missing");
236
+
237
+ const backlog = profile?.backlog ?? {};
238
+ const provider = backlog.provider ?? "local";
239
+ add("Backlog provider", ["local", "github", "jira"].includes(provider), provider);
240
+ if (provider === "jira") {
241
+ add("Backlog: Jira project", Boolean(backlog.jira?.project), backlog.jira?.project || "backlog.jira.project is missing");
242
+ const statuses = ["draft", "review", "ready", "in-progress", "done", "superseded"];
243
+ const missingStatuses = statuses.filter((status) => !backlog.jira?.statusMap?.[status]);
244
+ add("Backlog: Jira status map", missingStatuses.length === 0, missingStatuses.join(", ") || "all statuses mapped");
245
+ const priorities = ["must", "should", "could", "wont"];
246
+ const missingPriorities = priorities.filter((priority) => !backlog.jira?.priorityMap?.[priority]);
247
+ add("Backlog: Jira priority map", missingPriorities.length === 0, missingPriorities.join(", ") || "all priorities mapped");
248
+ const server = backlog.jira?.mcpServer || "mcp-atlassian";
249
+ const configured = [".mcp.json", ".claude/settings.json", ".claude/settings.local.json", ".opencode/opencode.jsonc"].some((file) => {
250
+ try {
251
+ return existsSync(join(root, file)) && readFileSync(join(root, file), "utf8").includes(server);
252
+ } catch {
253
+ return false;
254
+ }
255
+ });
256
+ add(`Backlog: MCP server '${server}'`, configured, configured ? "" : "configure mcp-atlassian in your harness");
257
+ }
258
+ if (provider === "github") {
259
+ const gh = spawnSync("gh", ["--version"], { encoding: "utf8" });
260
+ add("Backlog: GitHub CLI", gh.status === 0, gh.status === 0 ? "" : "https://cli.github.com");
261
+ if (gh.status === 0) {
262
+ const auth = spawnSync("gh", ["auth", "status"], { encoding: "utf8" });
263
+ add("Backlog: gh authenticated", auth.status === 0, auth.status === 0 ? "" : "gh auth login");
264
+ }
265
+ add("Backlog: GitHub repo", true, backlog.github?.repo || "current repository");
266
+ }
267
+ add("Tooling dependencies", existsSync(join(root, ".claude", "node_modules", "yaml")), "npm install --prefix .claude");
268
+ for (const file of ["CONTEXT.md", "docs/domain/events.yaml", "docs/product/vision.md", "docs/stories"]) {
269
+ add(file, existsSync(join(root, file)), "");
270
+ }
271
+ const events = join(root, ".claude/skills/event-storming/scripts/validate-events.ts");
272
+ if (existsSync(events) && existsSync(join(root, ".claude", "node_modules", "yaml"))) {
273
+ const r = spawnSync(process.execPath, [events], { cwd: root, encoding: "utf8" });
274
+ add("events.yaml valid", r.status === 0, (r.stdout + r.stderr).trim().split("\n").slice(-1)[0] ?? "");
275
+ } else if (existsSync(events)) {
276
+ add("events.yaml valid", true, "skipped – tooling dependencies missing");
277
+ }
278
+
279
+ banner();
280
+ console.log(`Doctor – ${root}\n`);
281
+ for (const check of checks) console.log(` ${check.ok ? "✓" : "✗"} ${check.name}${check.detail ? ` (${check.detail})` : ""}`);
282
+ const failed = checks.filter((c) => !c.ok);
283
+ console.log(failed.length ? `\n${failed.length} check(s) failed.` : "\nEverything looks good.");
284
+ process.exitCode = failed.length ? 1 : 0;
285
+ }
286
+
287
+ export async function main(argv) {
288
+ const options = parse(argv);
289
+ if (options.help || argv.length === 0) {
290
+ console.log(HELP);
291
+ return;
292
+ }
293
+ if (options.version) {
294
+ console.log(VERSION);
295
+ return;
296
+ }
297
+ const command = options._[0];
298
+ if (command === "init") return init(options);
299
+ if (command === "upgrade") return upgrade(options);
300
+ if (command === "doctor") return doctor(options);
301
+ throw new Error(`Unknown command '${command}'. Try --help.`);
302
+ }
package/src/detect.mjs ADDED
@@ -0,0 +1,237 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { detectRepoTooling } from "./repo.mjs";
4
+
5
+ const IGNORED = new Set([".git", ".claude", ".opencode", "node_modules", "dist", "build", "out", "coverage", ".next", "vendor", "target"]);
6
+ const CODE_EXT = new Set([
7
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte", ".astro",
8
+ ".py", ".go", ".rs", ".java", ".kt", ".kts", ".rb", ".php", ".cs", ".swift", ".scala", ".ex", ".exs", ".dart",
9
+ ]);
10
+
11
+ function anyFile(dir, names) {
12
+ return names.some((name) => existsSync(join(dir, name)));
13
+ }
14
+
15
+ function readJson(path) {
16
+ try {
17
+ return JSON.parse(readFileSync(path, "utf8"));
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ function countCode(root, limit = 4000) {
24
+ const byExt = new Map();
25
+ let total = 0;
26
+ const walk = (dir) => {
27
+ if (total >= limit) return;
28
+ let entries;
29
+ try {
30
+ entries = readdirSync(dir, { withFileTypes: true });
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const entry of entries) {
35
+ if (total >= limit) return;
36
+ if (entry.isDirectory()) {
37
+ if (!IGNORED.has(entry.name) && !entry.name.startsWith(".")) walk(join(dir, entry.name));
38
+ } else if (entry.isFile()) {
39
+ const dot = entry.name.lastIndexOf(".");
40
+ if (dot < 0) continue;
41
+ const ext = entry.name.slice(dot).toLowerCase();
42
+ if (!CODE_EXT.has(ext)) continue;
43
+ if (/\.(test|spec)\./.test(entry.name)) continue;
44
+ byExt.set(ext, (byExt.get(ext) ?? 0) + 1);
45
+ total += 1;
46
+ }
47
+ }
48
+ };
49
+ walk(root);
50
+ return byExt;
51
+ }
52
+
53
+ // Lockfiles before the manifest: a pnpm/yarn/bun repository also has a package.json, and the lockfile is the
54
+ // package manager that actually installs and runs the scripts.
55
+ const ECOSYSTEMS = [
56
+ { marker: ["pnpm-lock.yaml"], language: "TypeScript/JavaScript", manager: "pnpm", install: "pnpm install" },
57
+ { marker: ["yarn.lock"], language: "TypeScript/JavaScript", manager: "yarn", install: "yarn install" },
58
+ { marker: ["bun.lockb", "bun.lock"], language: "TypeScript/JavaScript", manager: "bun", install: "bun install" },
59
+ { marker: ["package.json"], language: "TypeScript/JavaScript", manager: "npm", install: "npm install" },
60
+ { marker: ["go.mod"], language: "Go", manager: "go", install: "go mod download" },
61
+ { marker: ["Cargo.toml"], language: "Rust", manager: "cargo", install: "cargo build" },
62
+ { marker: ["pyproject.toml", "requirements.txt", "setup.py"], language: "Python", manager: "pip", install: "pip install -r requirements.txt" },
63
+ { marker: ["pom.xml"], language: "Java", manager: "maven", install: "mvn install" },
64
+ { marker: ["build.gradle", "build.gradle.kts"], language: "Kotlin/Java", manager: "gradle", install: "./gradlew build" },
65
+ { marker: ["Gemfile"], language: "Ruby", manager: "bundler", install: "bundle install" },
66
+ { marker: ["composer.json"], language: "PHP", manager: "composer", install: "composer install" },
67
+ { marker: ["*.csproj", "*.sln"], language: "C#", manager: "dotnet", install: "dotnet restore" },
68
+ ];
69
+
70
+ const FRAMEWORKS = {
71
+ next: "Next.js", react: "React", vue: "Vue", nuxt: "Nuxt", svelte: "Svelte", "@sveltejs/kit": "SvelteKit",
72
+ astro: "Astro", angular: "Angular", "@angular/core": "Angular", express: "Express", fastify: "Fastify",
73
+ "@nestjs/core": "NestJS", koa: "Koa", remix: "Remix", "@remix-run/react": "Remix", electron: "Electron",
74
+ "solid-js": "Solid", hono: "Hono", trpc: "tRPC", "@trpc/server": "tRPC",
75
+ };
76
+ const DATABASES = {
77
+ prisma: "Prisma", "@prisma/client": "PostgreSQL/Prisma", drizzle: "Drizzle", "drizzle-orm": "Drizzle",
78
+ pg: "PostgreSQL", postgres: "PostgreSQL", mysql2: "MySQL", mongoose: "MongoDB", mongodb: "MongoDB",
79
+ sequelize: "SQL", knex: "SQL", typeorm: "SQL", "@supabase/supabase-js": "Supabase", redis: "Redis",
80
+ };
81
+ const TESTERS = { vitest: "Vitest", jest: "Jest", mocha: "Mocha", playwright: "Playwright", "@playwright/test": "Playwright", cypress: "Cypress", ava: "AVA" };
82
+
83
+ const CODE_ROOTS = ["src", "app", "lib", "packages", "apps", "services", "server", "client", "web", "api", "cmd", "internal", "pkg"];
84
+
85
+ export function detectProject(root) {
86
+ const codeCounts = countCode(root);
87
+ const codeFiles = [...codeCounts.values()].reduce((a, b) => a + b, 0);
88
+ const ecosystem = ECOSYSTEMS.find((e) => anyFile(root, e.marker)) ?? undefined;
89
+ const repoTooling = detectRepoTooling(root);
90
+ const pkg = readJson(join(root, "package.json"));
91
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
92
+ const names = Object.keys(deps);
93
+ const hasDependency = (name) => names.some((n) => n === name || n.startsWith(`${name}/`));
94
+
95
+ const frameworks = Object.entries(FRAMEWORKS).filter(([dep]) => hasDependency(dep)).map(([, label]) => label);
96
+ const databases = Object.entries(DATABASES).filter(([dep]) => hasDependency(dep)).map(([, label]) => label);
97
+ const testers = Object.entries(TESTERS).filter(([dep]) => hasDependency(dep)).map(([, label]) => label);
98
+
99
+ const languages = [];
100
+ if (existsSync(join(root, "tsconfig.json")) || codeCounts.has(".ts") || codeCounts.has(".tsx")) languages.push("TypeScript");
101
+ if ([".js", ".jsx", ".mjs", ".cjs"].some((e) => codeCounts.has(e))) languages.push("JavaScript");
102
+ for (const [ext, label] of [[".py", "Python"], [".go", "Go"], [".rs", "Rust"], [".java", "Java"], [".kt", "Kotlin"], [".rb", "Ruby"], [".php", "PHP"], [".cs", "C#"], [".vue", "Vue"], [".svelte", "Svelte"]]) {
103
+ if (codeCounts.has(ext) && !languages.includes(label)) languages.push(label);
104
+ }
105
+
106
+ const codeRoots = CODE_ROOTS.filter((dir) => {
107
+ try {
108
+ return statSync(join(root, dir)).isDirectory();
109
+ } catch {
110
+ return false;
111
+ }
112
+ });
113
+ const scripts = pkg?.scripts ?? {};
114
+ const manager = ecosystem?.manager ?? "npm";
115
+ const run = (script) => (manager === "npm" || manager === "bun" ? `${manager} run ${script}` : `${manager} ${script}`);
116
+
117
+ // An empty `package.json` is not a codebase; a manifest with dependencies, scripts or another ecosystem is.
118
+ const packageContent =
119
+ Boolean(pkg) && (Object.keys(deps).length > 0 || Object.keys(scripts).length > 0);
120
+ const otherEcosystem = Boolean(ecosystem) && !["npm", "bun", "yarn", "pnpm"].includes(ecosystem.manager);
121
+
122
+ const commands = {
123
+ install: ecosystem?.install ?? "",
124
+ lint: scripts.lint ? run("lint") : "",
125
+ typecheck: scripts.typecheck ? run("typecheck") : scripts["type-check"] ? run("type-check") : "",
126
+ test: scripts.test ? run("test") : "",
127
+ e2e: scripts["test:e2e"] ? run("test:e2e") : "",
128
+ verify: "node .claude/skills/implement/scripts/verify.ts",
129
+ };
130
+
131
+ // Sensible defaults per ecosystem, only where the repository does not name a command itself.
132
+ const defaults = {
133
+ go: { lint: "go vet ./...", typecheck: "go build ./...", test: "go test ./..." },
134
+ cargo: { lint: "cargo clippy -- -D warnings", typecheck: "cargo check", test: "cargo test" },
135
+ maven: { test: "mvn -q test" },
136
+ gradle: { test: "./gradlew test" },
137
+ bundler: { test: "bundle exec rspec" },
138
+ composer: { test: "composer test" },
139
+ pip: { test: "pytest" },
140
+ };
141
+ for (const [key, value] of Object.entries(defaults[manager] ?? {})) if (!commands[key]) commands[key] = value;
142
+
143
+ return {
144
+ kind: codeFiles > 0 || packageContent || otherEcosystem ? "brownfield" : "greenfield",
145
+ packageName: typeof pkg?.name === "string" ? pkg.name : undefined,
146
+ ecosystem: ecosystem?.language ?? languages[0] ?? "unknown",
147
+ languages,
148
+ frameworks: [...new Set(frameworks)],
149
+ databases: [...new Set(databases)],
150
+ testers: [...new Set(testers)],
151
+ packageManager: manager,
152
+ codeRoots: codeRoots.length ? codeRoots : ["."],
153
+ codeFiles,
154
+ scripts,
155
+ commands,
156
+ repoTooling,
157
+ };
158
+ }
159
+
160
+ export function buildProfile(root, { name, kind, uiLanguage, artifactLanguage, detection, extras = [], backlog = "local", jiraProject = "", githubRepo = "" }) {
161
+ const stackSummary = [
162
+ ...detection.languages,
163
+ ...detection.frameworks,
164
+ ...detection.databases,
165
+ ].filter((v, i, a) => a.indexOf(v) === i).join(", ") || "unknown";
166
+ const provider = ["local", "github", "jira"].includes(backlog) ? backlog : "local";
167
+
168
+ return {
169
+ name: name || basename(root),
170
+ kind: kind || detection.kind,
171
+ domain: name || basename(root),
172
+ languages: {
173
+ artifacts: artifactLanguage || "en",
174
+ ui: uiLanguage || "en",
175
+ uiAdditional: [],
176
+ },
177
+ stack: {
178
+ summary: stackSummary,
179
+ languages: detection.languages,
180
+ frameworks: detection.frameworks,
181
+ databases: detection.databases,
182
+ testers: detection.testers,
183
+ packageManager: detection.packageManager,
184
+ },
185
+ repo: {
186
+ defaultBranch: "main",
187
+ codeRoots: detection.codeRoots,
188
+ // Monorepo build tooling. The workflow uses this instead of assuming one package at the root:
189
+ // commands per package, dependency edges, consumers of a shared library and affected tests.
190
+ tooling: detection.repoTooling ?? { kind: "none", config: "", workspaces: [], manager: "", monorepo: false },
191
+ },
192
+ commands: detection.commands,
193
+ review: {
194
+ preview: "none",
195
+ pullRequest: existsSync(join(root, ".git")) ? "gh" : "none",
196
+ },
197
+ // Where stories live. `local` keeps docs/stories/ as the backlog; `github`/`jira` make the tracker the source of
198
+ // truth and docs/stories/ its local mirror. The defaults are deliberately generic – edit them after init.
199
+ // Keep the maps in sync with .claude/lib/config.ts (DEFAULT_STATUS_MAP / DEFAULT_PRIORITY_MAP).
200
+ backlog: {
201
+ provider,
202
+ github: {
203
+ repo: githubRepo,
204
+ labelPrefix: "kataster",
205
+ },
206
+ jira: {
207
+ mcpServer: "mcp-atlassian",
208
+ project: jiraProject,
209
+ issueType: "Story",
210
+ labelPrefix: "kataster",
211
+ statusMap: {
212
+ draft: "To Do",
213
+ review: "In Review",
214
+ ready: "Ready",
215
+ "in-progress": "In Progress",
216
+ done: "Done",
217
+ superseded: "Won't Do",
218
+ },
219
+ priorityMap: {
220
+ must: "Highest",
221
+ should: "High",
222
+ could: "Medium",
223
+ wont: "Lowest",
224
+ },
225
+ },
226
+ },
227
+ setup: {
228
+ envFile: "",
229
+ envHint: "",
230
+ },
231
+ workflow: {
232
+ version: "0.1.0",
233
+ extras,
234
+ installedAt: new Date().toISOString(),
235
+ },
236
+ };
237
+ }