@techierathore/techieflow 1.0.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 (151) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
  2. package/.claude/commands/TechieFlow/agents/architect.md +84 -0
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
  9. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
  11. package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
  12. package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
  13. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
  14. package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
  17. package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
  18. package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
  19. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
  20. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
  21. package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
  22. package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
  23. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
  24. package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
  25. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
  26. package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
  27. package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
  28. package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
  29. package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
  30. package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
  31. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
  32. package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
  33. package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
  34. package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
  35. package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
  36. package/.claude/commands/generate-html.md +124 -0
  37. package/.codex/config.toml +46 -0
  38. package/.codex/hooks.json +63 -0
  39. package/.codex/rules/techieflow.rules +8 -0
  40. package/.opencode/command/generate-html.md +129 -0
  41. package/.opencode/plugin/techieflow.js +350 -0
  42. package/.tfcore/TOKEN-GUIDE.md +59 -0
  43. package/.tfcore/agents/analyst.md +97 -0
  44. package/.tfcore/agents/architect.md +84 -0
  45. package/.tfcore/agents/flow-master.md +217 -0
  46. package/.tfcore/agents/verifier.md +72 -0
  47. package/.tfcore/checklists/architect-checklist.md +438 -0
  48. package/.tfcore/core-config.yaml +47 -0
  49. package/.tfcore/data/brainstorming-techniques.md +36 -0
  50. package/.tfcore/data/elicitation-methods.md +154 -0
  51. package/.tfcore/data/techieflow-kb.md +807 -0
  52. package/.tfcore/data/technical-preferences.md +3 -0
  53. package/.tfcore/enhanced-ide-development-workflow.md +248 -0
  54. package/.tfcore/hooks/block-git.sh +301 -0
  55. package/.tfcore/hooks/codex-adapter.py +248 -0
  56. package/.tfcore/hooks/guard-artifacts.sh +101 -0
  57. package/.tfcore/hooks/guard-status-html.sh +102 -0
  58. package/.tfcore/hooks/guard-status.sh +124 -0
  59. package/.tfcore/hooks/guard-verify.sh +108 -0
  60. package/.tfcore/hooks/metrics-session.sh +129 -0
  61. package/.tfcore/hooks/session-pointer.sh +52 -0
  62. package/.tfcore/hooks/sweep-artifacts.sh +198 -0
  63. package/.tfcore/install-manifest.yaml +231 -0
  64. package/.tfcore/routing.yaml +105 -0
  65. package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
  66. package/.tfcore/tasks/_smoke-test-policy.md +98 -0
  67. package/.tfcore/tasks/_status-update-gate.md +78 -0
  68. package/.tfcore/tasks/_yolo-mode.md +77 -0
  69. package/.tfcore/tasks/advanced-elicitation.md +117 -0
  70. package/.tfcore/tasks/amend-docs.md +149 -0
  71. package/.tfcore/tasks/author-brd.md +79 -0
  72. package/.tfcore/tasks/build-phase.md +231 -0
  73. package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
  74. package/.tfcore/tasks/create-doc.md +101 -0
  75. package/.tfcore/tasks/day1-brownfield.md +507 -0
  76. package/.tfcore/tasks/day1-greenfield.md +224 -0
  77. package/.tfcore/tasks/devguide.md +224 -0
  78. package/.tfcore/tasks/document-project.md +343 -0
  79. package/.tfcore/tasks/execute-checklist.md +86 -0
  80. package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
  81. package/.tfcore/tasks/fix-issues.md +134 -0
  82. package/.tfcore/tasks/generate-html.md +115 -0
  83. package/.tfcore/tasks/handoff-phase.md +126 -0
  84. package/.tfcore/tasks/index-docs.md +173 -0
  85. package/.tfcore/tasks/kb-mode-interaction.md +75 -0
  86. package/.tfcore/tasks/log-miss.md +160 -0
  87. package/.tfcore/tasks/metrics-report.md +143 -0
  88. package/.tfcore/tasks/mockups.md +122 -0
  89. package/.tfcore/tasks/productguide.md +105 -0
  90. package/.tfcore/tasks/refresh-status.md +135 -0
  91. package/.tfcore/tasks/render-workflow-docs.md +117 -0
  92. package/.tfcore/tasks/shard-doc.md +185 -0
  93. package/.tfcore/tasks/split-brd.md +131 -0
  94. package/.tfcore/tasks/triage-issues.md +157 -0
  95. package/.tfcore/tasks/verify-phase.md +579 -0
  96. package/.tfcore/telemetry/SCHEMA.md +678 -0
  97. package/.tfcore/telemetry/install-metrics.sh +390 -0
  98. package/.tfcore/telemetry/pre-commit +94 -0
  99. package/.tfcore/telemetry/tf-metrics.sh +1415 -0
  100. package/.tfcore/templates/architecture-tmpl.yaml +650 -0
  101. package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
  102. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
  103. package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
  104. package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
  105. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
  106. package/.tfcore/templates/market-research-tmpl.yaml +252 -0
  107. package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
  108. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
  109. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
  110. package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
  111. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
  112. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
  113. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
  114. package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
  115. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
  116. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
  117. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
  118. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
  119. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
  120. package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
  121. package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
  122. package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
  123. package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
  124. package/.tfcore/user-guide.md +633 -0
  125. package/.tfcore/utils/techieflow-doc-template.md +325 -0
  126. package/.tfcore/utils/tf-assets.sh +310 -0
  127. package/.tfcore/utils/tf-codex-bind.py +196 -0
  128. package/.tfcore/utils/tf-codex-telemetry.py +60 -0
  129. package/.tfcore/utils/tf-emit.sh +1075 -0
  130. package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
  131. package/.tfcore/utils/tf-goal.sh +421 -0
  132. package/.tfcore/utils/tf-harness.sh +160 -0
  133. package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
  134. package/.tfcore/utils/tf-mockup-parity.sh +65 -0
  135. package/.tfcore/utils/tf-opencode-audit.py +126 -0
  136. package/.tfcore/utils/tf-perf.sh +241 -0
  137. package/.tfcore/utils/tf-render-html.py +715 -0
  138. package/.tfcore/utils/tf-render-html.sh +28 -0
  139. package/.tfcore/utils/tf-routing-bind.sh +216 -0
  140. package/.tfcore/utils/tf-routing.sh +247 -0
  141. package/.tfcore/utils/tf-yolo.sh +119 -0
  142. package/.tfcore/working-in-the-brownfield.md +606 -0
  143. package/LICENSE +201 -0
  144. package/README.md +1212 -0
  145. package/WORKFLOW.html +1986 -0
  146. package/docs/TechieFlow-Installation.md +299 -0
  147. package/opencode.jsonc +929 -0
  148. package/package.json +60 -0
  149. package/scripts/install.mjs +992 -0
  150. package/scripts/test-install.mjs +505 -0
  151. package/scripts/validate.mjs +155 -0
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+ // scripts/test-install.mjs — proves that the npm installer produces exactly the same
3
+ // files as the three shell scripts.
4
+ //
5
+ // How it works, in plain words:
6
+ // 1. It packs this repository with `npm pack` and unpacks the result into a temporary
7
+ // folder. That unpacked copy is a clean copy of the framework: it holds only the
8
+ // files the package ships, none of the local-only files a working clone collects
9
+ // (session files, node_modules, personal settings). The three shell scripts are
10
+ // copied next to it so they can run from that clean copy.
11
+ // 2. For brownfield, greenfield, update and the old .bmad-core layout, it runs the shell
12
+ // script on one folder and the installer on a second, identical folder, then
13
+ // compares every file in both (path, content, executable bit). Any difference fails.
14
+ // 3. It checks the installed Claude mirror against .tfcore/ and every {file:} reference
15
+ // in both installed opencode.jsonc files.
16
+ // 4. It checks uninstall, --dry-run, --no-gitignore, --keep-permissions, a second run,
17
+ // and the one-shot `npm exec` form that `npx` uses.
18
+ //
19
+ // Every check runs even if an earlier one fails; the summary at the end lists them all.
20
+ // Exit code 0 means every check passed.
21
+ //
22
+ // Needs: node, npm, bash, python3 and rsync (rsync only because the shell scripts use it).
23
+ // Pass --keep to leave the temporary folder on disk for inspection.
24
+
25
+ import { spawnSync } from "node:child_process";
26
+ import { createHash } from "node:crypto";
27
+ import {
28
+ appendFileSync, chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync,
29
+ readFileSync, readdirSync, readlinkSync, rmSync, statSync, writeFileSync,
30
+ } from "node:fs";
31
+ import { tmpdir } from "node:os";
32
+ import { basename, dirname, join, relative, resolve } from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+
35
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
36
+ const installer = join(root, "scripts", "install.mjs");
37
+ const keepSandbox = process.argv.includes("--keep");
38
+ const sandbox = mkdtempSync(join(tmpdir(), "techieflow-install-"));
39
+ const failures = [];
40
+ const notes = [];
41
+ let passed = 0;
42
+
43
+ // The Codex binder (.tfcore/utils/tf-codex-bind.py) needs Python 3.10 or newer. On an older
44
+ // Python both routes print a warning and skip the Codex agents and skills, so those files are
45
+ // only expected when the binder can run.
46
+ const pythonVersion = (() => {
47
+ const result = spawnSync("python3", ["--version"], { encoding: "utf8" });
48
+ const match = /Python (\d+)\.(\d+)/.exec(`${result.stdout ?? ""}${result.stderr ?? ""}`);
49
+ return match ? `${match[1]}.${match[2]}` : "unknown";
50
+ })();
51
+ const codexBinderRuns = (() => {
52
+ const [major, minor] = pythonVersion.split(".").map(Number);
53
+ return major > 3 || (major === 3 && minor >= 10);
54
+ })();
55
+ if (!codexBinderRuns) notes.push(`python3 is ${pythonVersion}; the Codex binder needs 3.10 or newer, so neither route generates .codex/agents/ or .agents/skills/ on this machine (CI does).`);
56
+
57
+ // ---------------------------------------------------------------- helpers
58
+
59
+ function check(name, fn) {
60
+ try {
61
+ fn();
62
+ passed++;
63
+ console.log(`ok ${name}`);
64
+ } catch (error) {
65
+ failures.push(`${name}\n ${error.message.split("\n").join("\n ")}`);
66
+ console.log(`FAIL ${name}\n ${error.message.split("\n").join("\n ")}`);
67
+ }
68
+ }
69
+
70
+ function assert(condition, message) {
71
+ if (!condition) throw new Error(message);
72
+ }
73
+
74
+ function run(command, args, options = {}) {
75
+ const result = spawnSync(command, args, {
76
+ cwd: options.cwd ?? root,
77
+ encoding: "utf8",
78
+ input: "", // stdin is not a terminal, so the shell scripts never wait for an answer
79
+ env: { ...process.env, TF_NO_INSTALL: "1", ...options.env },
80
+ shell: options.shell ?? false,
81
+ });
82
+ if (result.error) throw result.error;
83
+ if (result.status !== 0 && !options.allowFailure) {
84
+ throw new Error(`${command} ${args.join(" ")} exited with ${result.status}\n${result.stdout}${result.stderr}`);
85
+ }
86
+ return result;
87
+ }
88
+
89
+ const node = (args, options) => run(process.execPath, args, options);
90
+
91
+ function npm(args, options = {}) {
92
+ if (process.env.npm_execpath) return node([process.env.npm_execpath, ...args], options);
93
+ const onWindows = process.platform === "win32";
94
+ return run(onWindows ? "npm.cmd" : "npm", args, { ...options, shell: onWindows });
95
+ }
96
+
97
+ const shell = (script, args, options) => run("bash", [script, ...args], options);
98
+
99
+ // A snapshot maps every relative path in a folder to what it is: a directory, a link, or a
100
+ // file described by its executable bit and the SHA-256 of its content.
101
+ function snapshot(directory) {
102
+ const entries = new Map();
103
+ const walk = (current) => {
104
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
105
+ const path = join(current, entry.name);
106
+ const rel = relative(directory, path).split("\\").join("/");
107
+ if (entry.isSymbolicLink()) entries.set(rel, `link:${readlinkSync(path)}`);
108
+ else if (entry.isDirectory()) { entries.set(`${rel}/`, "dir"); walk(path); }
109
+ else {
110
+ const executable = (statSync(path).mode & 0o111) ? "x" : "-";
111
+ entries.set(rel, `${executable}:${createHash("sha256").update(readFileSync(path)).digest("hex")}`);
112
+ }
113
+ }
114
+ };
115
+ walk(directory);
116
+ return entries;
117
+ }
118
+
119
+ function diffSnapshots(left, right, leftLabel, rightLabel) {
120
+ const lines = [];
121
+ for (const [path, value] of left) {
122
+ if (!right.has(path)) lines.push(`only in ${leftLabel}: ${path}`);
123
+ else if (right.get(path) !== value) lines.push(`differs: ${path}`);
124
+ }
125
+ for (const path of right.keys()) if (!left.has(path)) lines.push(`only in ${rightLabel}: ${path}`);
126
+ return lines;
127
+ }
128
+
129
+ function assertSameTree(shellDir, installerDir) {
130
+ const lines = diffSnapshots(snapshot(shellDir), snapshot(installerDir), "shell result", "installer result");
131
+ assert(lines.length === 0, `${lines.length} difference(s) between the shell script and the installer:\n${lines.join("\n")}`);
132
+ }
133
+
134
+ function assertUnchanged(before, directory, what) {
135
+ const lines = diffSnapshots(before, snapshot(directory), "before", "after");
136
+ assert(lines.length === 0, `${what} changed the folder:\n${lines.join("\n")}`);
137
+ }
138
+
139
+ const read = (path) => readFileSync(path, "utf8");
140
+ const lines = (path) => read(path).replace(/\r/g, "").split("\n");
141
+
142
+ // Every file under .claude/commands/TechieFlow/<sub>/ must equal .tfcore/<sub>/ byte for byte.
143
+ function mirrorDifferences(project) {
144
+ const core = join(project, ".tfcore");
145
+ const mirror = join(project, ".claude", "commands", "TechieFlow");
146
+ const out = [];
147
+ for (const sub of readdirSync(mirror)) {
148
+ out.push(...diffSnapshots(snapshot(join(core, sub)), snapshot(join(mirror, sub)), `.tfcore/${sub}`, `mirror ${sub}`));
149
+ }
150
+ return out;
151
+ }
152
+
153
+ // Every {file:...} reference resolves relative to the folder holding the config file.
154
+ function unresolvedReferences(configPath) {
155
+ const text = read(configPath);
156
+ const refs = [...new Set([...text.matchAll(/\{file:([^}]*)\}/g)].map((m) => m[1]))];
157
+ return refs.filter((ref) => !existsSync(join(dirname(configPath), ref)));
158
+ }
159
+
160
+ // ---------------------------------------------------------------- fixtures
161
+
162
+ // A small existing .NET project: enough for the brownfield checks, the build-output
163
+ // ignore audit and the pre-commit hook (a .git/hooks folder is enough; no git is run).
164
+ function seedProject(dir, { dotnet = true, gitDir = true, legacyPersona = true } = {}) {
165
+ mkdirSync(dir, { recursive: true });
166
+ if (dotnet) {
167
+ mkdirSync(join(dir, "src", "App"), { recursive: true });
168
+ writeFileSync(join(dir, "src", "App", "App.csproj"), '<Project Sdk="Microsoft.NET.Sdk.Web">\n</Project>\n');
169
+ writeFileSync(join(dir, "src", "App", "Program.cs"), "// application code\n");
170
+ }
171
+ writeFileSync(join(dir, ".gitignore"), "# Project rules\n/dist/\n");
172
+ mkdirSync(join(dir, "docs"), { recursive: true });
173
+ writeFileSync(join(dir, "docs", "notes.md"), "project document\n");
174
+ if (gitDir) mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
175
+ if (legacyPersona) {
176
+ mkdirSync(join(dir, ".claude"), { recursive: true });
177
+ writeFileSync(join(dir, ".claude", "trblazeui.md"), "# legacy NuGet persona\n");
178
+ }
179
+ }
180
+
181
+ // Drift that update must repair, and owner content that update must leave alone.
182
+ function perturbInstalledProject(dir) {
183
+ appendFileSync(join(dir, ".tfcore", "tasks", "build-phase.md"), "\nlocal edit that update must undo\n");
184
+ writeFileSync(join(dir, ".tfcore", "tasks", "stale-old-task.md"), "no longer ships\n");
185
+ mkdirSync(join(dir, ".tfcore", "workflows"), { recursive: true });
186
+ writeFileSync(join(dir, ".tfcore", "workflows", "old.yaml"), "stock cruft\n");
187
+ mkdirSync(join(dir, ".tfcore", "mine"), { recursive: true });
188
+ writeFileSync(join(dir, ".tfcore", "mine", "keep.md"), "owner content\n");
189
+ appendFileSync(join(dir, ".tfcore", "core-config.yaml"), "customTechnicalDocuments: docs/MyApp-Architecture.md\n");
190
+ appendFileSync(join(dir, ".tfcore", "routing.yaml"), "# owner tuned\n");
191
+ const settings = join(dir, ".claude", "settings.json");
192
+ writeFileSync(settings, read(settings).replace("{\n", '{\n "ownerKey": true,\n'));
193
+ writeFileSync(join(dir, ".claude", "settings.local.json"), "{ \"local\": true }\n");
194
+ writeFileSync(join(dir, ".claude", "commands", "trblazeui.md"), "# NuGet persona v2\n");
195
+ rmSync(join(dir, ".claude", "commands", "TechieFlow", "tasks", "verify-phase.md"));
196
+ writeFileSync(join(dir, ".claude", "commands", "TechieFlow", "tasks", "old.md"), "stale mirror file\n");
197
+ mkdirSync(join(dir, ".opencode", "command"), { recursive: true });
198
+ writeFileSync(join(dir, ".opencode", "command", "techierag.md"), "# NuGet persona\n");
199
+ appendFileSync(join(dir, ".opencode", "plugin", "techieflow.js"), "\n// local edit\n");
200
+ appendFileSync(join(dir, "WORKFLOW.html"), "<!-- local edit -->\n");
201
+ appendFileSync(join(dir, "opencode.jsonc"), "// a comment only, no project keys\n");
202
+ appendFileSync(join(dir, ".codex", "config.toml"), "\n# owner tuned\n");
203
+ appendFileSync(join(dir, ".codex", "hooks.json"), "\n");
204
+ writeFileSync(join(dir, "PROJECT-STATUS.md"), "# status\n");
205
+ writeFileSync(join(dir, "CLAUDE.md"), "# claude\n");
206
+ writeFileSync(join(dir, "docs", "MyApp-BRD.md"), "# BRD\n");
207
+ }
208
+
209
+ // The pre-2026-06 layout that update-framework.sh migrates in place.
210
+ function seedLegacyProject(dir) {
211
+ seedProject(dir, { legacyPersona: false });
212
+ mkdirSync(join(dir, ".bmad-core", "tasks"), { recursive: true });
213
+ mkdirSync(join(dir, ".bmad-core", "data"), { recursive: true });
214
+ writeFileSync(join(dir, ".bmad-core", "core-config.yaml"), "slashPrefix: BMad\ndevDebugLog: .bmad-core/debug.md\ncustomTechnicalDocuments: docs/Mine.md\n");
215
+ writeFileSync(join(dir, ".bmad-core", "tasks", "old.md"), "old task\n");
216
+ writeFileSync(join(dir, ".bmad-core", "data", "bmad-kb.md"), "old kb\n");
217
+ mkdirSync(join(dir, ".claude", "commands", "BMad"), { recursive: true });
218
+ writeFileSync(join(dir, ".claude", "commands", "BMad", "x.md"), "old mirror\n");
219
+ writeFileSync(join(dir, ".bmad-scaffold-note.txt"), "old note\n");
220
+ writeFileSync(join(dir, "opencode.jsonc"), '{ "agent": { "bmad-master": { "prompt": "{file:./.bmad-core/agents/bmad-master.md}" } } }\n');
221
+ writeFileSync(join(dir, "CLAUDE.md"), "See .bmad-core/ for the framework.\n");
222
+ }
223
+
224
+ // The clean copy of the framework: what `npm pack` ships, plus the three shell scripts.
225
+ function stageCleanTemplate() {
226
+ const packed = npm(["pack", "--json", `--pack-destination=${sandbox}`]);
227
+ const tarball = join(sandbox, basename(JSON.parse(packed.stdout)[0].filename));
228
+ const template = join(sandbox, "template");
229
+ mkdirSync(template);
230
+ run("tar", ["-xzf", tarball, "-C", template, "--strip-components=1"]);
231
+ for (const script of ["scaffold-brownfield.sh", "scaffold-greenfield.sh", "update-framework.sh"]) {
232
+ copyFileSync(join(root, script), join(template, script));
233
+ chmodSync(join(template, script), 0o755);
234
+ }
235
+ return { template, tarball };
236
+ }
237
+
238
+ // ---------------------------------------------------------------- the checks
239
+
240
+ try {
241
+ const { template, tarball } = stageCleanTemplate();
242
+ console.log(`clean template staged from ${basename(tarball)}`);
243
+
244
+ // ---- brownfield: scaffold-brownfield.sh versus `install`
245
+ const brownShell = join(sandbox, "brownfield-shell");
246
+ const brownNpm = join(sandbox, "brownfield-installer");
247
+ seedProject(brownShell);
248
+ seedProject(brownNpm);
249
+ shell(join(template, "scaffold-brownfield.sh"), [brownShell]);
250
+ node([installer, "install", `--target=${brownNpm}`]);
251
+
252
+ check("brownfield: installer result equals scaffold-brownfield.sh result", () => assertSameTree(brownShell, brownNpm));
253
+ check("brownfield: no node_modules, package.json or lock file left in the target", () => {
254
+ for (const path of ["node_modules", "package.json", "package-lock.json"]) assert(!existsSync(join(brownNpm, path)), `${path} was left behind`);
255
+ });
256
+ check("brownfield: the expected files are present", () => {
257
+ for (const path of [
258
+ ".tfcore/tasks/build-phase.md", ".tfcore/core-config.yaml", ".tfcore/routing.yaml",
259
+ ".claude/commands/TechieFlow/agents/analyst.md", ".claude/commands/TechieFlow/tasks/build-phase.md",
260
+ ".claude/commands/generate-html.md", ".claude/settings.json", ".claude/commands/trblazeui.md",
261
+ ".opencode/plugin/techieflow.js", ".opencode/opencode.jsonc", "opencode.jsonc", "WORKFLOW.html",
262
+ ".codex/config.toml", ".codex/hooks.json", ".codex/rules/techieflow.rules",
263
+ ...(codexBinderRuns ? [".codex/agents/analyst.toml", ".agents/skills/techieflow-build/SKILL.md"] : []),
264
+ ".tf-scaffold-note.txt",
265
+ "docs/metrics/runs.jsonl", "docs/metrics/gates.jsonl", "docs/metrics/sessions.jsonl",
266
+ "docs/metrics/commits.jsonl", "docs/metrics/misses.jsonl", "docs/metrics/README.md",
267
+ ".git/hooks/pre-commit", ".gitattributes",
268
+ ]) assert(existsSync(join(brownNpm, path)), `missing ${path}`);
269
+ assert(!existsSync(join(brownNpm, ".claude/commands/techierag.md")), "a library persona was installed from the framework");
270
+ assert(!existsSync(join(brownNpm, ".tfcore/.session")), "a local-only session folder was installed");
271
+ assert(!existsSync(join(brownNpm, ".opencode/command/generate-html.md")), "install deployed a file only update deploys");
272
+ });
273
+ check("brownfield: project files are untouched", () => {
274
+ assert(read(join(brownNpm, ".gitignore")).startsWith("# Project rules\n/dist/\n"), "project .gitignore rules were rewritten");
275
+ assert(read(join(brownNpm, "docs/notes.md")) === "project document\n", "docs/ content changed");
276
+ assert(read(join(brownNpm, "src/App/Program.cs")) === "// application code\n", "src/ content changed");
277
+ assert(read(join(brownNpm, ".claude/trblazeui.md")) === "# legacy NuGet persona\n", "a NuGet-deployed persona changed");
278
+ });
279
+ check("brownfield: managed .gitignore entries present, docs/metrics not ignored", () => {
280
+ const all = new Set(lines(join(brownNpm, ".gitignore")));
281
+ for (const entry of [".tfcore/", ".claude/", ".opencode/", ".codex/", ".agents/skills/", "/CLAUDE.md", "/WORKFLOW.html", "/opencode.jsonc", "/.tf-scaffold-note.txt", "node_modules/", "/package.json", "/package-lock.json", "bin/", "obj/"]) {
282
+ assert(all.has(entry), `.gitignore is missing ${entry}`);
283
+ }
284
+ assert(!all.has("docs/") && !all.has("docs/metrics/"), ".gitignore hides docs/metrics");
285
+ });
286
+ check("brownfield: Claude mirror is byte-identical to .tfcore/", () => {
287
+ const diff = mirrorDifferences(brownNpm);
288
+ assert(diff.length === 0, `${diff.length} mirror difference(s):\n${diff.join("\n")}`);
289
+ });
290
+ check("brownfield: every {file:} reference in both opencode.jsonc files resolves", () => {
291
+ for (const file of ["opencode.jsonc", ".opencode/opencode.jsonc"]) {
292
+ const bad = unresolvedReferences(join(brownNpm, file));
293
+ assert(bad.length === 0, `${file} has unresolved references: ${bad.join(", ")}`);
294
+ }
295
+ assert(read(join(brownNpm, ".opencode/opencode.jsonc")).includes("{file:../.tfcore/"), ".opencode/opencode.jsonc does not point one folder up");
296
+ });
297
+ check("brownfield: a second install run changes nothing", () => {
298
+ const before = snapshot(brownNpm);
299
+ node([installer, "install", `--target=${brownNpm}`]);
300
+ assertUnchanged(before, brownNpm, "the second install run");
301
+ });
302
+
303
+ // ---- greenfield: scaffold-greenfield.sh versus `install --greenfield`
304
+ const greenShell = join(sandbox, "greenfield-shell");
305
+ const greenNpm = join(sandbox, "greenfield-installer");
306
+ shell(join(template, "scaffold-greenfield.sh"), [greenShell]);
307
+ node([installer, "install", "--greenfield", `--target=${greenNpm}`]);
308
+ check("greenfield: installer result equals scaffold-greenfield.sh result", () => assertSameTree(greenShell, greenNpm));
309
+ check("greenfield: solution skeleton folders and greenfield note", () => {
310
+ for (const path of ["src", "tests/playwright", "tests/unit"]) assert(existsSync(join(greenNpm, path)), `missing ${path}/`);
311
+ assert(read(join(greenNpm, ".tf-scaffold-note.txt")).includes("greenfield"), "note is not the greenfield note");
312
+ });
313
+ check("greenfield: Claude mirror and OpenCode references", () => {
314
+ const diff = mirrorDifferences(greenNpm);
315
+ assert(diff.length === 0, `${diff.length} mirror difference(s):\n${diff.join("\n")}`);
316
+ const bad = unresolvedReferences(join(greenNpm, ".opencode/opencode.jsonc"));
317
+ assert(bad.length === 0, `unresolved references: ${bad.join(", ")}`);
318
+ });
319
+
320
+ // ---- update: update-framework.sh versus `update`, starting from one installed project
321
+ const updateShell = join(sandbox, "update-shell");
322
+ const updateNpm = join(sandbox, "update-installer");
323
+ cpSync(brownNpm, updateShell, { recursive: true });
324
+ cpSync(brownNpm, updateNpm, { recursive: true });
325
+ perturbInstalledProject(updateShell);
326
+ perturbInstalledProject(updateNpm);
327
+ shell(join(template, "update-framework.sh"), [updateShell]);
328
+ node([installer, "update", `--target=${updateNpm}`]);
329
+ check("update: installer result equals update-framework.sh result", () => assertSameTree(updateShell, updateNpm));
330
+ check("update: framework files refreshed", () => {
331
+ assert(read(join(updateNpm, ".tfcore/tasks/build-phase.md")) === read(join(template, ".tfcore/tasks/build-phase.md")), "edited task not restored");
332
+ assert(!existsSync(join(updateNpm, ".tfcore/tasks/stale-old-task.md")), "stale task not deleted");
333
+ assert(!existsSync(join(updateNpm, ".tfcore/workflows")), "stale stock folder not removed");
334
+ assert(existsSync(join(updateNpm, ".claude/commands/TechieFlow/tasks/verify-phase.md")), "deleted mirror file not restored");
335
+ assert(!existsSync(join(updateNpm, ".claude/commands/TechieFlow/tasks/old.md")), "stale mirror file not deleted");
336
+ assert(read(join(updateNpm, "WORKFLOW.html")) === read(join(template, "WORKFLOW.html")), "WORKFLOW.html not refreshed");
337
+ assert(read(join(updateNpm, "opencode.jsonc")) === read(join(template, "opencode.jsonc")), "root opencode.jsonc with no project keys not refreshed");
338
+ assert(existsSync(join(updateNpm, "opencode.jsonc.bak")), "old opencode.jsonc not backed up");
339
+ assert(read(join(updateNpm, ".opencode/plugin/techieflow.js")) === read(join(template, ".opencode/plugin/techieflow.js")), "plugin not refreshed");
340
+ assert(existsSync(join(updateNpm, ".opencode/command/generate-html.md")), "short-form OpenCode command not deployed");
341
+ assert(read(join(updateNpm, ".codex/hooks.json")) === read(join(template, ".codex/hooks.json")), "Codex hooks not refreshed");
342
+ assert(existsSync(join(updateNpm, ".claude/settings.json.bak")), "old settings.json not backed up");
343
+ assert(!read(join(updateNpm, ".claude/settings.json")).includes("ownerKey"), "settings.json not refreshed");
344
+ });
345
+ check("update: owner content preserved", () => {
346
+ assert(read(join(updateNpm, ".tfcore/core-config.yaml")).includes("customTechnicalDocuments: docs/MyApp-Architecture.md"), "core-config.yaml lost the owner's line");
347
+ assert(read(join(updateNpm, ".tfcore/routing.yaml")).includes("# owner tuned"), "routing.yaml was replaced");
348
+ assert(existsSync(join(updateNpm, ".tfcore/mine/keep.md")), "an unknown .tfcore folder was deleted");
349
+ assert(read(join(updateNpm, ".claude/settings.local.json")) === "{ \"local\": true }\n", "settings.local.json touched");
350
+ assert(read(join(updateNpm, ".claude/commands/trblazeui.md")) === "# NuGet persona v2\n", "NuGet persona under .claude/commands overwritten");
351
+ assert(read(join(updateNpm, ".opencode/command/techierag.md")) === "# NuGet persona\n", "NuGet persona under .opencode/command overwritten");
352
+ assert(read(join(updateNpm, ".codex/config.toml")).includes("# owner tuned"), ".codex/config.toml replaced");
353
+ for (const path of ["PROJECT-STATUS.md", "CLAUDE.md", "docs/MyApp-BRD.md", "docs/notes.md", "src/App/Program.cs"]) {
354
+ assert(existsSync(join(updateNpm, path)), `${path} missing after update`);
355
+ }
356
+ });
357
+ check("update: Claude mirror and OpenCode references after update", () => {
358
+ const diff = mirrorDifferences(updateNpm);
359
+ assert(diff.length === 0, `${diff.length} mirror difference(s):\n${diff.join("\n")}`);
360
+ for (const file of ["opencode.jsonc", ".opencode/opencode.jsonc"]) {
361
+ const bad = unresolvedReferences(join(updateNpm, file));
362
+ assert(bad.length === 0, `${file} has unresolved references: ${bad.join(", ")}`);
363
+ }
364
+ });
365
+
366
+ // ---- update on a project that is already current: nothing to back up
367
+ const currentShell = join(sandbox, "update-current-shell");
368
+ const currentNpm = join(sandbox, "update-current-installer");
369
+ cpSync(brownNpm, currentShell, { recursive: true });
370
+ cpSync(brownNpm, currentNpm, { recursive: true });
371
+ shell(join(template, "update-framework.sh"), [currentShell]);
372
+ node([installer, "update", `--target=${currentNpm}`]);
373
+ check("update on a current project: same result as the shell script, no backups made", () => {
374
+ assertSameTree(currentShell, currentNpm);
375
+ assert(!existsSync(join(currentNpm, ".claude/settings.json.bak")), "settings.json was backed up although it was current");
376
+ assert(!existsSync(join(currentNpm, "opencode.jsonc.bak")), "opencode.jsonc was backed up although it was current");
377
+ });
378
+
379
+ // ---- update --dry-run writes nothing (both routes)
380
+ const dryShell = join(sandbox, "update-dry-shell");
381
+ const dryNpm = join(sandbox, "update-dry-installer");
382
+ cpSync(brownNpm, dryShell, { recursive: true });
383
+ cpSync(brownNpm, dryNpm, { recursive: true });
384
+ perturbInstalledProject(dryShell);
385
+ perturbInstalledProject(dryNpm);
386
+ check("update --dry-run: neither route changes a file", () => {
387
+ const beforeShell = snapshot(dryShell);
388
+ const beforeNpm = snapshot(dryNpm);
389
+ shell(join(template, "update-framework.sh"), [dryShell, "--dry-run"]);
390
+ node([installer, "update", "--dry-run", `--target=${dryNpm}`]);
391
+ assertUnchanged(beforeShell, dryShell, "update-framework.sh --dry-run");
392
+ assertUnchanged(beforeNpm, dryNpm, "update --dry-run");
393
+ });
394
+
395
+ // ---- update --keep-permissions leaves an edited settings.json alone
396
+ const keepShell = join(sandbox, "update-keep-shell");
397
+ const keepNpm = join(sandbox, "update-keep-installer");
398
+ cpSync(brownNpm, keepShell, { recursive: true });
399
+ cpSync(brownNpm, keepNpm, { recursive: true });
400
+ perturbInstalledProject(keepShell);
401
+ perturbInstalledProject(keepNpm);
402
+ shell(join(template, "update-framework.sh"), [keepShell, "--keep-permissions"]);
403
+ node([installer, "update", "--keep-permissions", `--target=${keepNpm}`]);
404
+ check("update --keep-permissions: same result as the shell script, settings.json kept", () => {
405
+ assertSameTree(keepShell, keepNpm);
406
+ assert(read(join(keepNpm, ".claude/settings.json")).includes("ownerKey"), "settings.json was refreshed despite --keep-permissions");
407
+ assert(!existsSync(join(keepNpm, ".claude/settings.json.bak")), "a backup was made despite --keep-permissions");
408
+ });
409
+
410
+ // ---- update on the old .bmad-core layout
411
+ const legacyShell = join(sandbox, "legacy-shell");
412
+ const legacyNpm = join(sandbox, "legacy-installer");
413
+ seedLegacyProject(legacyShell);
414
+ seedLegacyProject(legacyNpm);
415
+ shell(join(template, "update-framework.sh"), [legacyShell]);
416
+ node([installer, "update", `--target=${legacyNpm}`]);
417
+ check("update of the old .bmad-core layout: same result as the shell script", () => {
418
+ assertSameTree(legacyShell, legacyNpm);
419
+ assert(!existsSync(join(legacyNpm, ".bmad-core")), ".bmad-core was not renamed");
420
+ assert(read(join(legacyNpm, ".tfcore/core-config.yaml")).includes("slashPrefix: TechieFlow"), "core-config.yaml not patched");
421
+ assert(read(join(legacyNpm, ".tfcore/core-config.yaml")).includes("customTechnicalDocuments: docs/Mine.md"), "core-config.yaml owner line lost");
422
+ assert(!existsSync(join(legacyNpm, ".claude/commands/BMad")), "old mirror not removed");
423
+ assert(existsSync(join(legacyNpm, "opencode.jsonc.bak")), "legacy opencode.jsonc not backed up");
424
+ });
425
+
426
+ // ---- uninstall: preview first, then --force
427
+ const uninstallDir = join(sandbox, "uninstall");
428
+ cpSync(brownNpm, uninstallDir, { recursive: true });
429
+ check("uninstall without --force removes nothing", () => {
430
+ const before = snapshot(uninstallDir);
431
+ node([installer, "uninstall", `--target=${uninstallDir}`]);
432
+ assertUnchanged(before, uninstallDir, "uninstall without --force");
433
+ });
434
+ check("uninstall --force removes the framework and keeps the project", () => {
435
+ node([installer, "uninstall", "--force", `--target=${uninstallDir}`]);
436
+ for (const path of [".tfcore", ".claude/commands/TechieFlow", ".claude/commands/generate-html.md", ".claude/settings.json", ".opencode", ".codex", ".agents", "WORKFLOW.html", "opencode.jsonc", ".tf-scaffold-note.txt", ".git/hooks/pre-commit"]) {
437
+ assert(!existsSync(join(uninstallDir, path)), `${path} still present after uninstall`);
438
+ }
439
+ for (const path of ["docs/notes.md", "docs/metrics/runs.jsonl", "docs/metrics/README.md", "src/App/App.csproj", ".claude/trblazeui.md", ".claude/commands/trblazeui.md", ".gitattributes"]) {
440
+ assert(existsSync(join(uninstallDir, path)), `${path} was removed by uninstall`);
441
+ }
442
+ const ignore = read(join(uninstallDir, ".gitignore"));
443
+ assert(ignore.startsWith("# Project rules\n/dist/\n"), "project .gitignore rules changed");
444
+ const all = new Set(ignore.replace(/\r/g, "").split("\n"));
445
+ for (const entry of [".tfcore/", ".claude/", "/WORKFLOW.html", "/opencode.jsonc"]) assert(!all.has(entry), `.gitignore still lists ${entry}`);
446
+ assert(!ignore.includes("deployed copies, never commit"), ".gitignore still carries the framework block header");
447
+ assert(all.has("node_modules/") && all.has("bin/"), "uninstall removed ignore rules that describe the project's own build output");
448
+ });
449
+
450
+ // ---- install --dry-run and --no-gitignore
451
+ check("install --greenfield --dry-run creates nothing", () => {
452
+ const dryTarget = join(sandbox, "dry-greenfield");
453
+ node([installer, "install", "--greenfield", "--dry-run", `--target=${dryTarget}`]);
454
+ assert(!existsSync(dryTarget), "dry run created the target folder");
455
+ });
456
+ check("install --dry-run on an existing project changes nothing", () => {
457
+ const dryTarget = join(sandbox, "dry-brownfield");
458
+ seedProject(dryTarget);
459
+ const before = snapshot(dryTarget);
460
+ node([installer, "install", "--dry-run", `--target=${dryTarget}`]);
461
+ assertUnchanged(before, dryTarget, "install --dry-run");
462
+ });
463
+ check("install --no-gitignore leaves .gitignore and .gitattributes alone", () => {
464
+ const target = join(sandbox, "no-gitignore");
465
+ seedProject(target);
466
+ node([installer, "install", "--no-gitignore", `--target=${target}`]);
467
+ assert(read(join(target, ".gitignore")) === "# Project rules\n/dist/\n", ".gitignore was edited");
468
+ assert(!existsSync(join(target, ".gitattributes")), ".gitattributes was created");
469
+ assert(existsSync(join(target, ".tfcore/tasks/build-phase.md")), "framework was not installed");
470
+ });
471
+ check("install refuses the framework's own folder", () => {
472
+ const result = node([installer, "install", `--target=${root}`], { allowFailure: true });
473
+ assert(result.status !== 0, "installing into the framework repository was not refused");
474
+ });
475
+ check("update refuses a folder that has no framework", () => {
476
+ const target = join(sandbox, "not-scaffolded");
477
+ seedProject(target);
478
+ const result = node([installer, "update", `--target=${target}`], { allowFailure: true });
479
+ assert(result.status !== 0, "update did not refuse a folder without .tfcore/");
480
+ });
481
+
482
+ // ---- the one-shot form: what `npx @techierathore/techieflow@latest install` runs
483
+ const npxTarget = join(sandbox, "npx-install");
484
+ seedProject(npxTarget);
485
+ check("one-shot `npm exec` install from the packed tarball matches the installer", () => {
486
+ npm(["exec", "--yes", `--package=${tarball}`, "--", "techieflow", "install"], { cwd: npxTarget });
487
+ assertSameTree(brownNpm, npxTarget);
488
+ for (const path of ["node_modules", "package.json", "package-lock.json"]) assert(!existsSync(join(npxTarget, path)), `${path} left behind by npm exec`);
489
+ });
490
+ } catch (error) {
491
+ failures.push(`setup: ${error.message}`);
492
+ console.log(`FAIL setup\n ${error.message.split("\n").join("\n ")}`);
493
+ } finally {
494
+ if (keepSandbox) console.log(`temporary folder kept: ${sandbox}`);
495
+ else rmSync(sandbox, { recursive: true, force: true });
496
+ }
497
+
498
+ console.log("");
499
+ for (const note of notes) console.log(`note: ${note}`);
500
+ if (failures.length) {
501
+ console.log(`${passed} check(s) passed, ${failures.length} failed:`);
502
+ for (const failure of failures) console.log(` - ${failure}`);
503
+ process.exit(1);
504
+ }
505
+ console.log(`installer tests passed (${passed} checks)`);