kataster 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/acceptance-tester.md +53 -0
- package/.claude/agents/code-reviewer.md +56 -0
- package/.claude/agents/domain-architect.md +40 -0
- package/.claude/agents/lead-dev.md +50 -0
- package/.claude/agents/product-owner.md +39 -0
- package/.claude/agents/repo-analyst.md +78 -0
- package/.claude/agents/requirements-engineer.md +44 -0
- package/.claude/agents/ux-designer.md +64 -0
- package/.claude/hooks/bash-guard.ts +92 -0
- package/.claude/hooks/code-check.ts +37 -0
- package/.claude/hooks/path-guard.ts +34 -0
- package/.claude/hooks/path-rules.ts +66 -0
- package/.claude/hooks/project-check.ts +108 -0
- package/.claude/hooks/protect-files.ts +55 -0
- package/.claude/hooks/session-start.ts +213 -0
- package/.claude/hooks/stop-gate.ts +56 -0
- package/.claude/hooks/validate-docs.ts +58 -0
- package/.claude/lib/architecture.ts +736 -0
- package/.claude/lib/backlog-github.ts +454 -0
- package/.claude/lib/backlog-jira.ts +347 -0
- package/.claude/lib/backlog-sync.ts +42 -0
- package/.claude/lib/backlog.ts +396 -0
- package/.claude/lib/bazel.ts +212 -0
- package/.claude/lib/config.ts +122 -0
- package/.claude/lib/discovery.ts +821 -0
- package/.claude/lib/engineering.ts +406 -0
- package/.claude/lib/nx.ts +129 -0
- package/.claude/lib/repo.ts +1194 -0
- package/.claude/lib/toolgraph.ts +74 -0
- package/.claude/lib/turbo.ts +166 -0
- package/.claude/package-lock.json +427 -0
- package/.claude/package.json +21 -0
- package/.claude/settings.json +82 -0
- package/.claude/skills/adopt/SKILL.md +283 -0
- package/.claude/skills/adopt/scripts/boundary-check.ts +156 -0
- package/.claude/skills/adopt/scripts/package-doc.ts +71 -0
- package/.claude/skills/adopt/scripts/repo-map.ts +123 -0
- package/.claude/skills/adopt/scripts/rule-doc.ts +80 -0
- package/.claude/skills/architect/ADR-FORMAT.md +97 -0
- package/.claude/skills/architect/SKILL.md +64 -0
- package/.claude/skills/architect/TECH-STACK.md +21 -0
- package/.claude/skills/backlog/SKILL.md +198 -0
- package/.claude/skills/backlog/scripts/backlog-pull.ts +45 -0
- package/.claude/skills/backlog/scripts/backlog-push.ts +38 -0
- package/.claude/skills/backlog/scripts/backlog-status.ts +32 -0
- package/.claude/skills/backlog/scripts/jira-import.ts +40 -0
- package/.claude/skills/backlog/scripts/jira-plan.ts +26 -0
- package/.claude/skills/backlog/scripts/jira-record.ts +41 -0
- package/.claude/skills/bootstrap/SKILL.md +119 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/domain-model/CONTEXT-FORMAT.md +47 -0
- package/.claude/skills/domain-model/SKILL.md +64 -0
- package/.claude/skills/domain-model/templates/data-model.md +29 -0
- package/.claude/skills/engineering-conventions/SKILL.md +31 -0
- package/.claude/skills/event-storming/SKILL.md +44 -0
- package/.claude/skills/event-storming/schema/events.schema.json +124 -0
- package/.claude/skills/event-storming/scripts/render-event-storming.ts +11 -0
- package/.claude/skills/event-storming/scripts/validate-events.ts +11 -0
- package/.claude/skills/event-storming/templates/events.example.yaml +91 -0
- package/.claude/skills/feature/SKILL.md +86 -0
- package/.claude/skills/feature/scripts/intake.ts +356 -0
- package/.claude/skills/grill-me/SKILL.md +7 -0
- package/.claude/skills/grill-with-docs/SKILL.md +7 -0
- package/.claude/skills/grilling/SKILL.md +30 -0
- package/.claude/skills/groom-backlog/SKILL.md +93 -0
- package/.claude/skills/groom-backlog/scripts/backlog-health.ts +238 -0
- package/.claude/skills/implement/SKILL.md +129 -0
- package/.claude/skills/implement/scripts/affected.ts +245 -0
- package/.claude/skills/implement/scripts/check-commands.ts +37 -0
- package/.claude/skills/implement/scripts/check-language.ts +15 -0
- package/.claude/skills/implement/scripts/check-scenarios.ts +59 -0
- package/.claude/skills/implement/scripts/next-story.ts +23 -0
- package/.claude/skills/implement/scripts/story-context.ts +135 -0
- package/.claude/skills/implement/scripts/story-status.ts +47 -0
- package/.claude/skills/implement/scripts/verify.ts +92 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/.claude/skills/retro/SKILL.md +53 -0
- package/.claude/skills/review-stories/SKILL.md +31 -0
- package/.claude/skills/shape/SKILL.md +193 -0
- package/.claude/skills/tdd/SKILL.md +38 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/tests.md +77 -0
- package/.claude/skills/user-stories/SKILL.md +138 -0
- package/.claude/skills/user-stories/schema/story.schema.json +39 -0
- package/.claude/skills/user-stories/scripts/render-backlog.ts +4 -0
- package/.claude/skills/user-stories/scripts/validate-stories.ts +13 -0
- package/.claude/skills/user-stories/templates/story-template.md +38 -0
- package/.claude/skills/ux-design/SKILL.md +107 -0
- package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/.claude/skills/writing-for-agents/SKILL.md +81 -0
- package/.claude/tsconfig.json +15 -0
- package/.opencode/agents/acceptance-tester.md +64 -0
- package/.opencode/agents/code-reviewer.md +69 -0
- package/.opencode/agents/domain-architect.md +59 -0
- package/.opencode/agents/lead-dev.md +67 -0
- package/.opencode/agents/product-owner.md +59 -0
- package/.opencode/agents/repo-analyst.md +86 -0
- package/.opencode/agents/requirements-engineer.md +58 -0
- package/.opencode/agents/ux-designer.md +83 -0
- package/.opencode/commands/adopt.md +7 -0
- package/.opencode/commands/backlog-sync.md +7 -0
- package/.opencode/commands/bootstrap.md +7 -0
- package/.opencode/commands/event-storming.md +7 -0
- package/.opencode/commands/feature.md +7 -0
- package/.opencode/commands/grill-me.md +7 -0
- package/.opencode/commands/grill-with-docs.md +7 -0
- package/.opencode/commands/groom-backlog.md +7 -0
- package/.opencode/commands/implement.md +7 -0
- package/.opencode/commands/improve-codebase-architecture.md +7 -0
- package/.opencode/commands/retro.md +7 -0
- package/.opencode/commands/review-stories.md +7 -0
- package/.opencode/commands/shape.md +7 -0
- package/.opencode/opencode.jsonc +15 -0
- package/.opencode/plugins/kataster/index.ts +283 -0
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +324 -2
- package/bin/kataster.mjs +7 -0
- package/extras/skills/shadcn/SKILL.md +277 -0
- package/extras/skills/shadcn/agents/openai.yml +5 -0
- package/extras/skills/shadcn/assets/shadcn-small.png +0 -0
- package/extras/skills/shadcn/assets/shadcn.png +0 -0
- package/extras/skills/shadcn/cli.md +290 -0
- package/extras/skills/shadcn/customization.md +209 -0
- package/extras/skills/shadcn/evals/evals.json +77 -0
- package/extras/skills/shadcn/mcp.md +105 -0
- package/extras/skills/shadcn/registry.md +277 -0
- package/extras/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/extras/skills/shadcn/rules/chat.md +250 -0
- package/extras/skills/shadcn/rules/composition.md +213 -0
- package/extras/skills/shadcn/rules/forms.md +192 -0
- package/extras/skills/shadcn/rules/icons.md +101 -0
- package/extras/skills/shadcn/rules/styling.md +185 -0
- package/guide/README.md +9 -0
- package/guide/brownfield.md +242 -0
- package/guide/concepts.md +109 -0
- package/guide/greenfield.md +84 -0
- package/guide/install.md +144 -0
- package/guide/release.md +62 -0
- package/package.json +56 -4
- package/src/cli.mjs +302 -0
- package/src/detect.mjs +237 -0
- package/src/install.mjs +427 -0
- package/src/repo.mjs +129 -0
- package/src/util.mjs +110 -0
- package/templates/CONTEXT.md +21 -0
- package/templates/conventions.md +63 -0
- package/templates/docs/adr/README.md +7 -0
- package/templates/docs/architecture/data-model.md +31 -0
- package/templates/docs/domain/context-map.md +21 -0
- package/templates/docs/domain/events.yaml +23 -0
- package/templates/docs/product/initiatives/README.md +41 -0
- package/templates/docs/product/ux-guidelines.md +27 -0
- package/templates/docs/product/vision.md +36 -0
- package/templates/docs/reviews/.gitkeep +0 -0
- package/templates/docs/stories/OPEN_QUESTIONS.md +8 -0
- package/templates/docs/stories/README.md +26 -0
- package/templates/docs/workflow.md +135 -0
package/src/install.mjs
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
exists, mergeUnique, parseJsonc, read, readJson, relPosix, sha, timestamp, toPosix, upsertBlock, write, writeJson,
|
|
7
|
+
} from "./util.mjs";
|
|
8
|
+
|
|
9
|
+
export const VERSION = readJson(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"))?.version ?? "0.0.0";
|
|
10
|
+
const PKG = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
/** Everything the installer manages inside a target repository. */
|
|
13
|
+
const WORKFLOW_DIRS = [".claude", ".opencode"];
|
|
14
|
+
const SKIP = new Set(["node_modules", "state", "install.json", "project.json", "settings.local.json", ".npmignore", ".DS_Store"]);
|
|
15
|
+
|
|
16
|
+
const AGENTS_BEGIN = "<!-- kataster:begin – managed by kataster; edits inside are overwritten on upgrade -->";
|
|
17
|
+
const AGENTS_END = "<!-- kataster:end -->";
|
|
18
|
+
const CLAUDE_BEGIN = "<!-- kataster:claude:begin – imports the shared workflow; managed by kataster -->";
|
|
19
|
+
const CLAUDE_END = "<!-- kataster:claude:end -->";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The tool was called La Guardia before 0.2. Repositories installed with that name carry its markers, its state
|
|
23
|
+
* directory and its plugin path. They are rewritten on the way in, so an upgrade does not leave a second managed
|
|
24
|
+
* block, a dead plugin or an orphaned backup directory behind.
|
|
25
|
+
*/
|
|
26
|
+
const LEGACY_MARKERS = [
|
|
27
|
+
[/<!--\s*laguardia:begin[^>]*-->/, AGENTS_BEGIN],
|
|
28
|
+
[/<!--\s*laguardia:end\s*-->/, AGENTS_END],
|
|
29
|
+
[/<!--\s*laguardia:claude:begin[^>]*-->/, CLAUDE_BEGIN],
|
|
30
|
+
[/<!--\s*laguardia:claude:end\s*-->/, CLAUDE_END],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const HOOK_EVENTS = {
|
|
34
|
+
SessionStart: [{ hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.ts"' }] }],
|
|
35
|
+
PreToolUse: [
|
|
36
|
+
{
|
|
37
|
+
matcher: "Write|Edit|MultiEdit",
|
|
38
|
+
hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.ts"' }],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
matcher: "Bash",
|
|
42
|
+
hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/bash-guard.ts"' }],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
PostToolUse: [
|
|
46
|
+
{
|
|
47
|
+
matcher: "Write|Edit|MultiEdit",
|
|
48
|
+
hooks: [
|
|
49
|
+
{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-docs.ts"' },
|
|
50
|
+
{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/code-check.ts"', timeout: 90 },
|
|
51
|
+
{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/project-check.ts"', timeout: 60 },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
Stop: [{ hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-gate.ts"', timeout: 600 }] }],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ALLOW = [
|
|
59
|
+
"Bash(node .claude/skills/event-storming/scripts/validate-events.ts)",
|
|
60
|
+
"Bash(node .claude/skills/event-storming/scripts/render-event-storming.ts)",
|
|
61
|
+
"Bash(node .claude/skills/user-stories/scripts/validate-stories.ts)",
|
|
62
|
+
"Bash(node .claude/skills/user-stories/scripts/render-backlog.ts)",
|
|
63
|
+
"Bash(node .claude/skills/adopt/scripts/*)",
|
|
64
|
+
"Bash(node .claude/skills/backlog/scripts/*)",
|
|
65
|
+
"Bash(node .claude/skills/implement/scripts/*)",
|
|
66
|
+
"Bash(gh issue *)",
|
|
67
|
+
"Bash(gh label *)",
|
|
68
|
+
"Bash(git status *)",
|
|
69
|
+
"Bash(git diff *)",
|
|
70
|
+
"Bash(git log *)",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export class Installer {
|
|
74
|
+
constructor(root, { dryRun = false, force = false, log = () => {} } = {}) {
|
|
75
|
+
this.root = resolve(root);
|
|
76
|
+
this.dryRun = dryRun;
|
|
77
|
+
this.force = force;
|
|
78
|
+
this.log = log;
|
|
79
|
+
this.actions = [];
|
|
80
|
+
this.manifestPath = join(this.root, ".claude", "install.json");
|
|
81
|
+
this.manifest = readJson(this.manifestPath);
|
|
82
|
+
this.files = { ...(this.manifest?.files ?? {}) };
|
|
83
|
+
this.conflicts = [];
|
|
84
|
+
this.created = [];
|
|
85
|
+
this.updated = [];
|
|
86
|
+
this.skipped = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
record(kind, path, detail = "") {
|
|
90
|
+
this.actions.push({ kind, path: relPosix(this.root, path), detail });
|
|
91
|
+
this.log(`${kind.padEnd(9)} ${relPosix(this.root, path)}${detail ? ` ${detail}` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
backup(path) {
|
|
95
|
+
if (this.dryRun || !existsSync(path)) return;
|
|
96
|
+
const dir = join(this.root, ".kataster", "backup", timestamp());
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
const target = join(dir, relPosix(this.root, path).replaceAll("/", "__"));
|
|
99
|
+
renameSync(path, target);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Migrates an installation made under the old name: state directory, OpenCode plugin path and the markers in
|
|
104
|
+
* AGENTS.md/CLAUDE.md. Idempotent and skipped in a dry run.
|
|
105
|
+
*/
|
|
106
|
+
migrateLegacy() {
|
|
107
|
+
const moves = [
|
|
108
|
+
[join(this.root, ".laguardia"), join(this.root, ".kataster")],
|
|
109
|
+
[join(this.root, ".opencode", "plugins", "laguardia"), join(this.root, ".opencode", "plugins", "kataster")],
|
|
110
|
+
];
|
|
111
|
+
for (const [from, to] of moves) {
|
|
112
|
+
if (!existsSync(from) || existsSync(to)) continue;
|
|
113
|
+
if (!this.dryRun) renameSync(from, to);
|
|
114
|
+
this.record("migrate", to, `from ${relPosix(this.root, from)}`);
|
|
115
|
+
}
|
|
116
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
117
|
+
const path = join(this.root, name);
|
|
118
|
+
if (!existsSync(path)) continue;
|
|
119
|
+
let text = read(path);
|
|
120
|
+
for (const [pattern, replacement] of LEGACY_MARKERS) text = text.replace(pattern, replacement);
|
|
121
|
+
if (text === read(path)) continue;
|
|
122
|
+
if (!this.dryRun) write(path, text);
|
|
123
|
+
this.record("migrate", path, "markers from the old name");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Writes a managed workflow file, respecting local modifications on upgrade. */
|
|
128
|
+
put(path, content, { merge = false, track = true } = {}) {
|
|
129
|
+
const existing = existsSync(path) ? read(path) : undefined;
|
|
130
|
+
const hash = sha(content);
|
|
131
|
+
const key = relPosix(this.root, path);
|
|
132
|
+
|
|
133
|
+
if (existing === content) {
|
|
134
|
+
if (track) this.files[key] = hash;
|
|
135
|
+
return "unchanged";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (existing !== undefined && !merge) {
|
|
139
|
+
const known = this.manifest?.files?.[key];
|
|
140
|
+
const local = sha(existing);
|
|
141
|
+
const modifiedLocally = known !== undefined && local !== known;
|
|
142
|
+
if (modifiedLocally) {
|
|
143
|
+
if (!this.force) {
|
|
144
|
+
this.conflicts.push(key);
|
|
145
|
+
this.record("conflict", path, "locally modified – kept (use --force to overwrite)");
|
|
146
|
+
return "conflict";
|
|
147
|
+
}
|
|
148
|
+
this.backup(path);
|
|
149
|
+
} else if (known === undefined && !this.force) {
|
|
150
|
+
// A file from another tool with the same name: back it up, never silently lose it.
|
|
151
|
+
this.backup(path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!this.dryRun) write(path, content);
|
|
156
|
+
if (track) this.files[key] = hash;
|
|
157
|
+
if (existing === undefined) {
|
|
158
|
+
this.created.push(path);
|
|
159
|
+
this.record("create", path);
|
|
160
|
+
return "created";
|
|
161
|
+
}
|
|
162
|
+
this.updated.push(path);
|
|
163
|
+
this.record("update", path);
|
|
164
|
+
return "updated";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
copyWorkflowDirs() {
|
|
168
|
+
for (const dir of WORKFLOW_DIRS) {
|
|
169
|
+
const source = join(PKG, dir);
|
|
170
|
+
if (!existsSync(source)) continue;
|
|
171
|
+
for (const file of walk(source)) {
|
|
172
|
+
const name = toPosix(relative(source, file));
|
|
173
|
+
if (name.split("/").some((part) => SKIP.has(part))) continue;
|
|
174
|
+
const content = read(file);
|
|
175
|
+
// settings.json and opencode.jsonc are merged, not replaced
|
|
176
|
+
if (file === join(source, "settings.json") || file === join(source, "opencode.jsonc")) continue;
|
|
177
|
+
this.put(join(this.root, dir, name), content);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
mergeSettings() {
|
|
183
|
+
const path = join(this.root, ".claude", "settings.json");
|
|
184
|
+
const existing = readJson(path);
|
|
185
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
186
|
+
const permissions = base.permissions ?? {};
|
|
187
|
+
const hooks = { ...(base.hooks ?? {}) };
|
|
188
|
+
|
|
189
|
+
for (const [event, entries] of Object.entries(HOOK_EVENTS)) {
|
|
190
|
+
const ours = JSON.stringify(entries);
|
|
191
|
+
// Remove only the hooks this installer manages, by exact command. A project may add its own hook under
|
|
192
|
+
// .claude/hooks/ as well – that one must survive an upgrade.
|
|
193
|
+
const managed = new Set(
|
|
194
|
+
entries.flatMap((entry) => (entry.hooks ?? []).map((hook) => hook.command)).filter(Boolean),
|
|
195
|
+
);
|
|
196
|
+
const foreign = (hooks[event] ?? [])
|
|
197
|
+
.map((entry) => ({
|
|
198
|
+
...entry,
|
|
199
|
+
hooks: (entry.hooks ?? []).filter((hook) => !managed.has(hook.command ?? "")),
|
|
200
|
+
}))
|
|
201
|
+
.filter((entry) => (entry.hooks ?? []).length > 0);
|
|
202
|
+
hooks[event] = [...foreign, ...JSON.parse(ours)];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const merged = {
|
|
206
|
+
...base,
|
|
207
|
+
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
208
|
+
permissions: {
|
|
209
|
+
...permissions,
|
|
210
|
+
allow: mergeUnique(permissions.allow ?? [], ALLOW),
|
|
211
|
+
deny: permissions.deny ?? [],
|
|
212
|
+
ask: permissions.ask ?? [],
|
|
213
|
+
},
|
|
214
|
+
hooks,
|
|
215
|
+
};
|
|
216
|
+
this.put(path, `${JSON.stringify(merged, null, 2)}\n`, { merge: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
mergeOpencodeConfig() {
|
|
220
|
+
const path = join(this.root, ".opencode", "opencode.jsonc");
|
|
221
|
+
const template = read(join(PKG, ".opencode", "opencode.jsonc"));
|
|
222
|
+
if (!existsSync(path)) {
|
|
223
|
+
this.put(path, template);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const existing = parseJsonc(read(path));
|
|
227
|
+
const ours = parseJsonc(template) ?? {};
|
|
228
|
+
if (!existing) {
|
|
229
|
+
this.record("skip", path, "existing config is not plain JSON/JSONC – left untouched");
|
|
230
|
+
this.skipped.push(path);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const merged = {
|
|
234
|
+
...ours,
|
|
235
|
+
...existing,
|
|
236
|
+
watcher: {
|
|
237
|
+
...(ours.watcher ?? {}),
|
|
238
|
+
...(existing.watcher ?? {}),
|
|
239
|
+
ignore: mergeUnique(ours.watcher?.ignore ?? [], existing.watcher?.ignore ?? []),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
this.put(path, `${JSON.stringify(merged, null, 2)}\n`, { merge: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
mergeAgentsFile(profile) {
|
|
246
|
+
const path = join(this.root, "AGENTS.md");
|
|
247
|
+
const block = agentsBlock(profile);
|
|
248
|
+
const existing = existsSync(path) ? read(path) : "";
|
|
249
|
+
const next = upsertBlock(existing, block, { begin: AGENTS_BEGIN, end: AGENTS_END });
|
|
250
|
+
this.put(path, next, { merge: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
copyDocsSkeleton(profile) { const source = join(PKG, "templates", "docs");
|
|
254
|
+
if (!existsSync(source)) return;
|
|
255
|
+
for (const file of walk(source)) {
|
|
256
|
+
const name = relative(source, file);
|
|
257
|
+
const target = join(this.root, "docs", name);
|
|
258
|
+
if (existsSync(target)) {
|
|
259
|
+
this.record("keep", target);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
this.put(target, render(read(file), profile), { track: false });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
putContext(profile) {
|
|
267
|
+
const path = join(this.root, "CONTEXT.md");
|
|
268
|
+
if (existsSync(path)) {
|
|
269
|
+
this.record("keep", path);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.put(path, render(read(join(PKG, "templates", "CONTEXT.md")), profile), { track: false });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Claude Code's entry file: an import of AGENTS.md, so the rules live in one place.
|
|
277
|
+
*
|
|
278
|
+
* A repository often already has a CLAUDE.md with house rules. Claude Code then reads that file and does not
|
|
279
|
+
* read AGENTS.md, so the import has to be added – without touching the existing content.
|
|
280
|
+
*/
|
|
281
|
+
putClaudeMd(profile) {
|
|
282
|
+
const path = join(this.root, "CLAUDE.md");
|
|
283
|
+
const existing = existsSync(path) ? read(path) : undefined;
|
|
284
|
+
if (existing === undefined) {
|
|
285
|
+
this.put(path, `# ${profile.name ?? "Project"}\n\n@AGENTS.md\n`, { track: false });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (existing.includes("@AGENTS.md")) {
|
|
289
|
+
this.record("keep", path, "already imports AGENTS.md");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const block = `${CLAUDE_BEGIN}\n@AGENTS.md\n${CLAUDE_END}`;
|
|
293
|
+
const start = existing.indexOf(CLAUDE_BEGIN);
|
|
294
|
+
const stop = existing.indexOf(CLAUDE_END);
|
|
295
|
+
const next = start !== -1 && stop > start
|
|
296
|
+
? `${existing.slice(0, start)}${block}${existing.slice(stop + CLAUDE_END.length)}`
|
|
297
|
+
: `${block}\n\n${existing}`;
|
|
298
|
+
this.put(path, next, { merge: true, track: false });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
putProfile(profile) {
|
|
302
|
+
const path = join(this.root, ".claude", "project.json");
|
|
303
|
+
if (existsSync(path)) {
|
|
304
|
+
this.record("keep", path, "existing project profile");
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
this.put(path, `${JSON.stringify(profile, null, 2)}\n`, { track: false });
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
putConventions(profile) {
|
|
312
|
+
const path = join(this.root, "docs", "engineering", "conventions.md");
|
|
313
|
+
if (existsSync(path)) {
|
|
314
|
+
this.record("keep", path);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.put(path, render(read(join(PKG, "templates", "conventions.md")), profile), { track: false });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
updateGitignore() {
|
|
321
|
+
const path = join(this.root, ".gitignore");
|
|
322
|
+
const entries = [".claude/node_modules/", ".claude/state/", ".kataster/"];
|
|
323
|
+
const original = existsSync(path) ? read(path) : "";
|
|
324
|
+
// An installation from before the rename still lists the old state directory.
|
|
325
|
+
const existing = original.replace(/^\.laguardia\/$/m, ".kataster/");
|
|
326
|
+
const missing = entries.filter((e) => !existing.split("\n").map((l) => l.trim()).includes(e));
|
|
327
|
+
if (!missing.length && existing === original) return;
|
|
328
|
+
const next = missing.length
|
|
329
|
+
? `${existing.trimEnd()}${existing.trim() ? "\n" : ""}\n# Kataster\n${missing.join("\n")}\n`
|
|
330
|
+
: existing;
|
|
331
|
+
this.put(path, next, { merge: true, track: false });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
writeManifest() {
|
|
335
|
+
if (this.dryRun) return;
|
|
336
|
+
writeJson(this.manifestPath, {
|
|
337
|
+
tool: "kataster",
|
|
338
|
+
version: VERSION,
|
|
339
|
+
installedAt: new Date().toISOString(),
|
|
340
|
+
files: Object.fromEntries(Object.entries(this.files).sort(([a], [b]) => a.localeCompare(b))),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
installExtras(names = []) {
|
|
345
|
+
for (const name of names) {
|
|
346
|
+
const source = join(PKG, "extras", "skills", name);
|
|
347
|
+
if (!existsSync(source)) {
|
|
348
|
+
this.record("skip", source, `unknown extra '${name}'`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
for (const file of walk(source)) {
|
|
352
|
+
this.put(join(this.root, ".claude", "skills", name, relative(source, file)), read(file));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
installToolingDependencies() {
|
|
358
|
+
if (this.dryRun) return { ok: true, skipped: true };
|
|
359
|
+
if (!existsSync(join(this.root, ".claude", "package.json"))) return { ok: true, skipped: true };
|
|
360
|
+
const r = spawnSync("npm", ["install", "--prefix", ".claude", "--no-audit", "--no-fund"], {
|
|
361
|
+
cwd: this.root,
|
|
362
|
+
stdio: "ignore",
|
|
363
|
+
});
|
|
364
|
+
return { ok: r.status === 0, skipped: false };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
summary() {
|
|
368
|
+
return {
|
|
369
|
+
root: this.root,
|
|
370
|
+
created: this.created.length,
|
|
371
|
+
updated: this.updated.length,
|
|
372
|
+
conflicts: this.conflicts,
|
|
373
|
+
actions: this.actions,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function walk(dir, out = []) {
|
|
379
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
380
|
+
const path = join(dir, entry.name);
|
|
381
|
+
if (entry.isDirectory()) walk(path, out);
|
|
382
|
+
else if (entry.isFile()) out.push(path);
|
|
383
|
+
}
|
|
384
|
+
return out.sort();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function agentsBlock(profile) {
|
|
388
|
+
const name = profile.name;
|
|
389
|
+
return `${AGENTS_BEGIN}
|
|
390
|
+
## ${name} – agent workflow (Kataster)
|
|
391
|
+
|
|
392
|
+
This repository runs the Kataster workflow. Discovery lives in \`.claude/skills/\`, the OpenCode wiring in
|
|
393
|
+
\`.opencode/\`; Claude Code uses the same skills and hooks.
|
|
394
|
+
|
|
395
|
+
- **Profile:** \`.claude/project.json\` – name, stack, languages, commands, backlog backend. Read it before assuming anything.
|
|
396
|
+
- **Backlog:** \`backlog.provider\` is \`local\` (default), \`github\` or \`jira\`. With a tracker, \`docs/stories/\` is the
|
|
397
|
+
local mirror and the tracker is the source of truth – sync with \`/backlog-sync\`; \`BACKLOG.md\` stays generated.
|
|
398
|
+
- **Map:** \`docs/workflow.md\` – sources of truth, commands, phases, rules. Read it when unsure where something
|
|
399
|
+
belongs.
|
|
400
|
+
- **Repo map (brownfield):** \`docs/architecture/repo-map.md\` – packages, dependency edges, consumers, adopted
|
|
401
|
+
slices. A package's \`AGENTS.md\` is loaded only when you work there; the root file stays free of domain.
|
|
402
|
+
- **Start:** greenfield → \`/bootstrap\`; existing code → \`/adopt\` (in slices, not the whole monorepo).
|
|
403
|
+
- **Never guess:** unclear → a hotspot (\`HS-n\`) or \`[OPEN]\` plus a row in \`OPEN_QUESTIONS.md\`.
|
|
404
|
+
- **Project checks:** \`docs/engineering/checks.yaml\` – what runs at edit time and Stop/CI; architecture boundaries
|
|
405
|
+
in \`docs/architecture/boundaries.yaml\` with existing debt in the baseline (only new violations fail).
|
|
406
|
+
- **Language:** domain terms exactly as in \`CONTEXT.md\`; UI words from its \`_UI (<lang>)_\` lines.
|
|
407
|
+
- **Traceability:** every scenario has a test titled \`ST-NNN: <scenario title>\`; domain IDs come from
|
|
408
|
+
\`docs/domain/events.yaml\`.
|
|
409
|
+
- **ADRs:** new ones are \`proposed\`; only the user sets \`accepted\`. A \`done\` story is never reopened.
|
|
410
|
+
- **Gate:** \`node .claude/skills/implement/scripts/verify.ts\`.
|
|
411
|
+
|
|
412
|
+
Keep this block short: it is loaded into every session. Put rules into the skill that owns them, not here.
|
|
413
|
+
${AGENTS_END}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function render(text, profile = {}) {
|
|
417
|
+
return text
|
|
418
|
+
.replaceAll("{{project}}", profile.name ?? "this project")
|
|
419
|
+
.replaceAll("{{uiLanguage}}", profile.languages?.ui ?? "en")
|
|
420
|
+
.replaceAll("{{artifacts}}", profile.languages?.artifacts ?? "en");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function createInstaller(root, options) {
|
|
424
|
+
return new Installer(root, options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export { PKG, AGENTS_BEGIN, AGENTS_END };
|
package/src/repo.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monorepo detection for the installer.
|
|
3
|
+
*
|
|
4
|
+
* The profile needs to know which build tool drives the repository and where its workspaces are, because the
|
|
5
|
+
* workflow must use that tool (`nx affected`, `turbo --filter`, `pnpm --filter`, `bazel query`) instead of
|
|
6
|
+
* assuming one package at the root. This file is deliberately dependency-free – it runs before
|
|
7
|
+
* `.claude/node_modules` exists. The full package graph is built later by `.claude/lib/repo.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const TOOL_MARKERS = [
|
|
13
|
+
{ kind: "bazel", config: ["MODULE.bazel", "WORKSPACE.bazel", "WORKSPACE"] },
|
|
14
|
+
{ kind: "nx", config: ["nx.json"] },
|
|
15
|
+
{ kind: "turbo", config: ["turbo.json"] },
|
|
16
|
+
{ kind: "rush", config: ["rush.json"] },
|
|
17
|
+
{ kind: "lerna", config: ["lerna.json"] },
|
|
18
|
+
{ kind: "pnpm", config: ["pnpm-workspace.yaml"] },
|
|
19
|
+
{ kind: "go-work", config: ["go.work"] },
|
|
20
|
+
{ kind: "cargo-workspace", config: ["Cargo.toml"], require: (text) => /^\s*\[workspace\]/m.test(text) },
|
|
21
|
+
{ kind: "gradle", config: ["settings.gradle.kts", "settings.gradle"], require: (text) => /\binclude\s*[(:]/.test(text) },
|
|
22
|
+
{ kind: "maven", config: ["pom.xml"], require: (text) => /<modules>/.test(text) },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function readText(path) {
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(path, "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(path) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The package manager that owns the lockfile, or undefined. */
|
|
42
|
+
export function detectPackageManager(root) {
|
|
43
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
44
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
45
|
+
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun";
|
|
46
|
+
if (existsSync(join(root, "package-lock.json"))) return "npm";
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findTool(root) {
|
|
51
|
+
for (const tool of TOOL_MARKERS) {
|
|
52
|
+
for (const name of tool.config) {
|
|
53
|
+
const path = join(root, name);
|
|
54
|
+
if (!existsSync(path)) continue;
|
|
55
|
+
if (tool.require) {
|
|
56
|
+
const text = readText(path);
|
|
57
|
+
if (text === undefined || !tool.require(text)) continue;
|
|
58
|
+
}
|
|
59
|
+
return { kind: tool.kind, config: name };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { kind: "none", config: "" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Workspace globs from the manifest that owns them. Never guesses from directory names. */
|
|
66
|
+
export function workspaceGlobs(root) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const pkg = readJson(join(root, "package.json"));
|
|
69
|
+
const declared = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
70
|
+
if (Array.isArray(declared)) out.push(...declared.filter((v) => typeof v === "string"));
|
|
71
|
+
|
|
72
|
+
const pnpm = readText(join(root, "pnpm-workspace.yaml"));
|
|
73
|
+
if (pnpm !== undefined) {
|
|
74
|
+
let inPackages = false;
|
|
75
|
+
for (const raw of pnpm.split("\n")) {
|
|
76
|
+
const line = raw.replace(/#.*$/, "");
|
|
77
|
+
if (/^packages\s*:/.test(line)) {
|
|
78
|
+
inPackages = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (inPackages && /^\S/.test(line) && line.trim()) break;
|
|
82
|
+
const match = /^\s*-\s*['"]?([^'"]+?)['"]?\s*$/.exec(line);
|
|
83
|
+
if (inPackages && match) out.push(match[1]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const lerna = readJson(join(root, "lerna.json"));
|
|
88
|
+
if (Array.isArray(lerna?.packages)) out.push(...lerna.packages.filter((v) => typeof v === "string"));
|
|
89
|
+
|
|
90
|
+
const rush = readJson(join(root, "rush.json"));
|
|
91
|
+
if (Array.isArray(rush?.projects)) {
|
|
92
|
+
for (const project of rush.projects) {
|
|
93
|
+
if (typeof project?.projectFolder === "string") out.push(project.projectFolder);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [...new Set(out)];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What the repository uses to build and test across packages.
|
|
102
|
+
* `kind` is the strongest detected tool; `manager` is the package manager from the lockfile.
|
|
103
|
+
*/
|
|
104
|
+
export function detectRepoTooling(root) {
|
|
105
|
+
const tool = findTool(root);
|
|
106
|
+
const workspaces = workspaceGlobs(root);
|
|
107
|
+
const pkg = readJson(join(root, "package.json"));
|
|
108
|
+
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
|
|
109
|
+
const manager = detectPackageManager(root)
|
|
110
|
+
?? (["npm", "pnpm", "yarn", "bun"].includes(declared) ? declared : undefined)
|
|
111
|
+
?? (tool.kind === "pnpm" || existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm" : undefined);
|
|
112
|
+
const declaredWorkspaces = Array.isArray(pkg?.workspaces)
|
|
113
|
+
|| (pkg?.workspaces && Array.isArray(pkg.workspaces.packages))
|
|
114
|
+
|| workspaces.length > 0;
|
|
115
|
+
|
|
116
|
+
const kind = tool.kind !== "none"
|
|
117
|
+
? tool.kind
|
|
118
|
+
: declaredWorkspaces
|
|
119
|
+
? "workspaces"
|
|
120
|
+
: "none";
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind,
|
|
124
|
+
config: tool.config,
|
|
125
|
+
workspaces,
|
|
126
|
+
manager: manager ?? (pkg ? "npm" : ""),
|
|
127
|
+
monorepo: kind !== "none",
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/util.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
6
|
+
|
|
7
|
+
export const read = (path) => readFileSync(path, "utf8");
|
|
8
|
+
export const exists = (path) => existsSync(path);
|
|
9
|
+
|
|
10
|
+
export function write(path, content) {
|
|
11
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
12
|
+
writeFileSync(path, content, "utf8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readJson(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeJson(path, value) {
|
|
24
|
+
write(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const toPosix = (path) => path.split(sep).join("/");
|
|
28
|
+
export const relPosix = (from, to) => toPosix(relative(from, to));
|
|
29
|
+
|
|
30
|
+
export function isInside(root, path) {
|
|
31
|
+
const r = resolve(root);
|
|
32
|
+
const p = resolve(path);
|
|
33
|
+
return p === r || p.startsWith(`${r}${sep}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function timestamp() {
|
|
37
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Minimal JSONC → JSON: strips comments and trailing commas, respecting strings. */
|
|
41
|
+
export function stripJsonc(text) {
|
|
42
|
+
let out = "";
|
|
43
|
+
let inString = false;
|
|
44
|
+
let quote = "";
|
|
45
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
46
|
+
const c = text[i];
|
|
47
|
+
const next = text[i + 1];
|
|
48
|
+
if (inString) {
|
|
49
|
+
out += c;
|
|
50
|
+
if (c === "\\") {
|
|
51
|
+
out += next ?? "";
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (c === quote) {
|
|
54
|
+
inString = false;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === '"' || c === "'") {
|
|
59
|
+
inString = true;
|
|
60
|
+
quote = c;
|
|
61
|
+
out += c;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === "/" && next === "/") {
|
|
65
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
66
|
+
out += "\n";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === "/" && next === "*") {
|
|
70
|
+
i += 2;
|
|
71
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out += c;
|
|
76
|
+
}
|
|
77
|
+
return out.replace(/,\s*([}\]])/g, "$1");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function parseJsonc(text) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stripJsonc(text));
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Replaces (or appends) a marker-delimited block. Idempotent. */
|
|
89
|
+
export function upsertBlock(text, block, { begin = "<!-- kataster:begin -->", end = "<!-- kataster:end -->" } = {}) {
|
|
90
|
+
const start = text.indexOf(begin);
|
|
91
|
+
const stop = text.indexOf(end);
|
|
92
|
+
if (start !== -1 && stop !== -1 && stop > start) {
|
|
93
|
+
return `${text.slice(0, start)}${block}${text.slice(stop + end.length)}`.replace(/\n{3,}/g, "\n\n");
|
|
94
|
+
}
|
|
95
|
+
const trimmed = text.trimEnd();
|
|
96
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${block}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mergeUnique(base = [], add = []) {
|
|
100
|
+
return [...new Set([...base, ...add])];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const packageRoot = (importMetaUrl) => resolve(dirname(new URL(importMetaUrl).pathname), "..");
|
|
104
|
+
|
|
105
|
+
export function relativeDisplay(root, path) {
|
|
106
|
+
const r = relPosix(root, path);
|
|
107
|
+
return r.startsWith("../") ? path : r;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { join };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# {{project}}
|
|
2
|
+
|
|
3
|
+
{One or two sentences: which domain is this, for whom, and what does the software do? Keep it free of technology.}
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
<!--
|
|
8
|
+
Glossary – the single source of truth for domain language. Format:
|
|
9
|
+
.claude/skills/domain-model/CONTEXT-FORMAT.md
|
|
10
|
+
|
|
11
|
+
Rules: be opinionated (pick one word, list the others under _Avoid_), keep definitions to one or two
|
|
12
|
+
sentences, only domain-specific terms, group by bounded context once they exist. Every term gets the
|
|
13
|
+
one word the UI uses for it in {{uiLanguage}}.
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
**Term**:
|
|
18
|
+
What it is, in one or two sentences.
|
|
19
|
+
_Avoid_: Synonym, other word
|
|
20
|
+
_UI ({{uiLanguage}})_: the one UI word
|
|
21
|
+
-->
|