@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.
- package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
- package/.claude/commands/TechieFlow/agents/architect.md +84 -0
- package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
- package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
- package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
- package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
- package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
- package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
- package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
- package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
- package/.claude/commands/generate-html.md +124 -0
- package/.codex/config.toml +46 -0
- package/.codex/hooks.json +63 -0
- package/.codex/rules/techieflow.rules +8 -0
- package/.opencode/command/generate-html.md +129 -0
- package/.opencode/plugin/techieflow.js +350 -0
- package/.tfcore/TOKEN-GUIDE.md +59 -0
- package/.tfcore/agents/analyst.md +97 -0
- package/.tfcore/agents/architect.md +84 -0
- package/.tfcore/agents/flow-master.md +217 -0
- package/.tfcore/agents/verifier.md +72 -0
- package/.tfcore/checklists/architect-checklist.md +438 -0
- package/.tfcore/core-config.yaml +47 -0
- package/.tfcore/data/brainstorming-techniques.md +36 -0
- package/.tfcore/data/elicitation-methods.md +154 -0
- package/.tfcore/data/techieflow-kb.md +807 -0
- package/.tfcore/data/technical-preferences.md +3 -0
- package/.tfcore/enhanced-ide-development-workflow.md +248 -0
- package/.tfcore/hooks/block-git.sh +301 -0
- package/.tfcore/hooks/codex-adapter.py +248 -0
- package/.tfcore/hooks/guard-artifacts.sh +101 -0
- package/.tfcore/hooks/guard-status-html.sh +102 -0
- package/.tfcore/hooks/guard-status.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +108 -0
- package/.tfcore/hooks/metrics-session.sh +129 -0
- package/.tfcore/hooks/session-pointer.sh +52 -0
- package/.tfcore/hooks/sweep-artifacts.sh +198 -0
- package/.tfcore/install-manifest.yaml +231 -0
- package/.tfcore/routing.yaml +105 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
- package/.tfcore/tasks/_smoke-test-policy.md +98 -0
- package/.tfcore/tasks/_status-update-gate.md +78 -0
- package/.tfcore/tasks/_yolo-mode.md +77 -0
- package/.tfcore/tasks/advanced-elicitation.md +117 -0
- package/.tfcore/tasks/amend-docs.md +149 -0
- package/.tfcore/tasks/author-brd.md +79 -0
- package/.tfcore/tasks/build-phase.md +231 -0
- package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
- package/.tfcore/tasks/create-doc.md +101 -0
- package/.tfcore/tasks/day1-brownfield.md +507 -0
- package/.tfcore/tasks/day1-greenfield.md +224 -0
- package/.tfcore/tasks/devguide.md +224 -0
- package/.tfcore/tasks/document-project.md +343 -0
- package/.tfcore/tasks/execute-checklist.md +86 -0
- package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
- package/.tfcore/tasks/fix-issues.md +134 -0
- package/.tfcore/tasks/generate-html.md +115 -0
- package/.tfcore/tasks/handoff-phase.md +126 -0
- package/.tfcore/tasks/index-docs.md +173 -0
- package/.tfcore/tasks/kb-mode-interaction.md +75 -0
- package/.tfcore/tasks/log-miss.md +160 -0
- package/.tfcore/tasks/metrics-report.md +143 -0
- package/.tfcore/tasks/mockups.md +122 -0
- package/.tfcore/tasks/productguide.md +105 -0
- package/.tfcore/tasks/refresh-status.md +135 -0
- package/.tfcore/tasks/render-workflow-docs.md +117 -0
- package/.tfcore/tasks/shard-doc.md +185 -0
- package/.tfcore/tasks/split-brd.md +131 -0
- package/.tfcore/tasks/triage-issues.md +157 -0
- package/.tfcore/tasks/verify-phase.md +579 -0
- package/.tfcore/telemetry/SCHEMA.md +678 -0
- package/.tfcore/telemetry/install-metrics.sh +390 -0
- package/.tfcore/telemetry/pre-commit +94 -0
- package/.tfcore/telemetry/tf-metrics.sh +1415 -0
- package/.tfcore/templates/architecture-tmpl.yaml +650 -0
- package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/.tfcore/templates/market-research-tmpl.yaml +252 -0
- package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
- package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
- package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
- package/.tfcore/user-guide.md +633 -0
- package/.tfcore/utils/techieflow-doc-template.md +325 -0
- package/.tfcore/utils/tf-assets.sh +310 -0
- package/.tfcore/utils/tf-codex-bind.py +196 -0
- package/.tfcore/utils/tf-codex-telemetry.py +60 -0
- package/.tfcore/utils/tf-emit.sh +1075 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
- package/.tfcore/utils/tf-goal.sh +421 -0
- package/.tfcore/utils/tf-harness.sh +160 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
- package/.tfcore/utils/tf-mockup-parity.sh +65 -0
- package/.tfcore/utils/tf-opencode-audit.py +126 -0
- package/.tfcore/utils/tf-perf.sh +241 -0
- package/.tfcore/utils/tf-render-html.py +715 -0
- package/.tfcore/utils/tf-render-html.sh +28 -0
- package/.tfcore/utils/tf-routing-bind.sh +216 -0
- package/.tfcore/utils/tf-routing.sh +247 -0
- package/.tfcore/utils/tf-yolo.sh +119 -0
- package/.tfcore/working-in-the-brownfield.md +606 -0
- package/LICENSE +201 -0
- package/README.md +1212 -0
- package/WORKFLOW.html +1986 -0
- package/docs/TechieFlow-Installation.md +299 -0
- package/opencode.jsonc +929 -0
- package/package.json +60 -0
- package/scripts/install.mjs +992 -0
- package/scripts/test-install.mjs +505 -0
- package/scripts/validate.mjs +155 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/install.mjs — the TechieFlow installer that ships in the npm package.
|
|
3
|
+
//
|
|
4
|
+
// npx @techierathore/techieflow@latest install add the framework to an existing project
|
|
5
|
+
// npx @techierathore/techieflow@latest install --greenfield the same, for a new project (adds src/ and tests/)
|
|
6
|
+
// npx @techierathore/techieflow@latest update refresh the framework in a project that has it
|
|
7
|
+
// npx @techierathore/techieflow@latest uninstall --force remove the framework files again
|
|
8
|
+
//
|
|
9
|
+
// Flags:
|
|
10
|
+
// --target=<folder> the project folder (default: the current folder)
|
|
11
|
+
// --dry-run print what would happen, write nothing
|
|
12
|
+
// --force install: skip the "no .csproj found" question; uninstall: really delete
|
|
13
|
+
// --no-gitignore do not edit .gitignore or .gitattributes
|
|
14
|
+
// --greenfield install only: also create src/, tests/playwright/, tests/unit/
|
|
15
|
+
// --keep-permissions update only: leave .claude/settings.json as it is
|
|
16
|
+
// --help
|
|
17
|
+
//
|
|
18
|
+
// It does what scaffold-brownfield.sh, scaffold-greenfield.sh and update-framework.sh do,
|
|
19
|
+
// step for step, so a project set up by either route ends up with the same files.
|
|
20
|
+
// scripts/test-install.mjs proves that by running both routes and comparing the results.
|
|
21
|
+
//
|
|
22
|
+
// Needs: node, bash and python3. The framework's own helper scripts (telemetry setup,
|
|
23
|
+
// build-output ignore audit, Codex bindings) are run exactly as the shell scripts run them.
|
|
24
|
+
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import {
|
|
27
|
+
appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync,
|
|
28
|
+
readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
32
|
+
import { createInterface } from "node:readline/promises";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------- arguments
|
|
36
|
+
|
|
37
|
+
const sourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const command = args.find((a) => !a.startsWith("-")) ?? "install";
|
|
40
|
+
const flags = args.filter((a) => a.startsWith("-"));
|
|
41
|
+
const knownFlags = ["--dry-run", "--force", "--no-gitignore", "--greenfield", "--keep-permissions", "--help", "-h"];
|
|
42
|
+
const targetArg = flags.find((f) => f.startsWith("--target="));
|
|
43
|
+
const target = resolve(targetArg ? targetArg.slice("--target=".length) : process.cwd());
|
|
44
|
+
const dryRun = flags.includes("--dry-run");
|
|
45
|
+
const force = flags.includes("--force");
|
|
46
|
+
const greenfield = flags.includes("--greenfield");
|
|
47
|
+
const keepPermissions = flags.includes("--keep-permissions");
|
|
48
|
+
const manageGitignore = !flags.includes("--no-gitignore");
|
|
49
|
+
const wantHelp = flags.includes("--help") || flags.includes("-h");
|
|
50
|
+
|
|
51
|
+
function usage() {
|
|
52
|
+
console.log(`TechieFlow installer
|
|
53
|
+
|
|
54
|
+
npx @techierathore/techieflow@latest install add the framework to an existing project
|
|
55
|
+
npx @techierathore/techieflow@latest install --greenfield the same, for a new project (adds src/ and tests/)
|
|
56
|
+
npx @techierathore/techieflow@latest update refresh the framework in a project that has it
|
|
57
|
+
npx @techierathore/techieflow@latest uninstall --force remove the framework files again
|
|
58
|
+
|
|
59
|
+
Flags:
|
|
60
|
+
--target=<folder> the project folder (default: the current folder)
|
|
61
|
+
--dry-run print what would happen, write nothing
|
|
62
|
+
--force install: skip the "no .csproj found" question; uninstall: really delete
|
|
63
|
+
--no-gitignore do not edit .gitignore or .gitattributes
|
|
64
|
+
--greenfield install only: also create src/, tests/playwright/, tests/unit/
|
|
65
|
+
--keep-permissions update only: leave .claude/settings.json as it is
|
|
66
|
+
|
|
67
|
+
install never overwrites a file that already exists. update overwrites framework files and
|
|
68
|
+
leaves your own files alone: docs/, src/, tests/, PROJECT-STATUS.md, CLAUDE.md, .editorconfig,
|
|
69
|
+
.tfcore/core-config.yaml, .tfcore/routing.yaml, .claude/settings.local.json and the NuGet-deployed
|
|
70
|
+
library personas. Needs node, bash and python3.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------- small helpers
|
|
74
|
+
|
|
75
|
+
const say = (line) => console.log(line);
|
|
76
|
+
const warn = (line) => console.error(line);
|
|
77
|
+
const would = dryRun ? "WOULD " : "";
|
|
78
|
+
const rel = (path) => relative(target, path).split(sep).join("/");
|
|
79
|
+
const read = (path) => readFileSync(path, "utf8");
|
|
80
|
+
const libraryPersonas = new Set(["trblazeui.md", "techierag.md"]);
|
|
81
|
+
|
|
82
|
+
// Files a working clone may hold that a clean checkout does not. The shell scripts run
|
|
83
|
+
// from a clone and copy them by accident; the installer never copies them.
|
|
84
|
+
function isLocalOnly(sourcePath) {
|
|
85
|
+
const name = basename(sourcePath);
|
|
86
|
+
const r = relative(sourceRoot, sourcePath).split(sep).join("/");
|
|
87
|
+
return name === ".DS_Store" || name === "Thumbs.db" || name === "desktop.ini" || name.endsWith(".bak")
|
|
88
|
+
|| name.startsWith("tmpclaude-") || r === ".tfcore/.session" || r.startsWith(".tfcore/.session/");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sameFile(a, b) {
|
|
92
|
+
try {
|
|
93
|
+
const sa = statSync(a);
|
|
94
|
+
const sb = statSync(b);
|
|
95
|
+
if (sa.size !== sb.size || (sa.mode & 0o777) !== (sb.mode & 0o777)) return false;
|
|
96
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Copy one file. Prints one line per change. Returns true when the file was (or would be) written.
|
|
103
|
+
function copyFile(source, destination, { onlyIfMissing = false, quiet = false } = {}) {
|
|
104
|
+
const exists = existsSync(destination);
|
|
105
|
+
if (onlyIfMissing && exists) return false;
|
|
106
|
+
if (exists && sameFile(source, destination)) return false;
|
|
107
|
+
if (!quiet) say(` ${would}${exists ? "update" : "create"} ${rel(destination)}`);
|
|
108
|
+
if (!dryRun) {
|
|
109
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
110
|
+
copyFileSync(source, destination);
|
|
111
|
+
chmodSync(destination, statSync(source).mode & 0o777);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeText(destination, text, { onlyIfMissing = false } = {}) {
|
|
117
|
+
const exists = existsSync(destination);
|
|
118
|
+
if (onlyIfMissing && exists) return false;
|
|
119
|
+
if (exists && read(destination) === text) return false;
|
|
120
|
+
say(` ${would}${exists ? "update" : "create"} ${rel(destination)}`);
|
|
121
|
+
if (!dryRun) {
|
|
122
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
123
|
+
writeFileSync(destination, text);
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Copy a whole folder, like rsync -a. onlyIfMissing = rsync --ignore-existing.
|
|
129
|
+
// deleteExtra = rsync --delete (the destination ends up with exactly the source's files).
|
|
130
|
+
function copyTree(source, destination, { onlyIfMissing = false, deleteExtra = false, excludeNames = new Set() } = {}) {
|
|
131
|
+
const excluded = (path) => excludeNames.has(basename(path)) || isLocalOnly(path);
|
|
132
|
+
const walk = (from, to) => {
|
|
133
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
134
|
+
const s = join(from, entry.name);
|
|
135
|
+
const d = join(to, entry.name);
|
|
136
|
+
if (excluded(s)) continue;
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
if (!dryRun) mkdirSync(d, { recursive: true });
|
|
139
|
+
walk(s, d);
|
|
140
|
+
} else if (entry.isFile()) copyFile(s, d, { onlyIfMissing });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
if (!dryRun) mkdirSync(destination, { recursive: true });
|
|
144
|
+
if (existsSync(source)) walk(source, destination);
|
|
145
|
+
if (deleteExtra && existsSync(destination)) {
|
|
146
|
+
const prune = (from, to) => {
|
|
147
|
+
for (const entry of readdirSync(to, { withFileTypes: true })) {
|
|
148
|
+
const s = join(from, entry.name);
|
|
149
|
+
const d = join(to, entry.name);
|
|
150
|
+
if (!existsSync(s) || excluded(s)) {
|
|
151
|
+
say(` ${would}delete ${rel(d)}${entry.isDirectory() ? "/" : ""}`);
|
|
152
|
+
if (!dryRun) rmSync(d, { recursive: true, force: true });
|
|
153
|
+
} else if (entry.isDirectory()) prune(s, d);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
prune(source, destination);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function removeEmptyFolders(paths) {
|
|
161
|
+
for (const p of paths) {
|
|
162
|
+
const full = join(target, p);
|
|
163
|
+
try {
|
|
164
|
+
if (existsSync(full) && statSync(full).isDirectory() && readdirSync(full).length === 0) rmdirSync(full);
|
|
165
|
+
} catch { /* leave it */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Run one of the framework's own helper scripts, printing its output like the shell scripts do.
|
|
170
|
+
function runBash(script, scriptArgs, { allowFailure = true } = {}) {
|
|
171
|
+
const result = spawnSync("bash", [script, ...scriptArgs], { cwd: target, stdio: "inherit", env: process.env });
|
|
172
|
+
if (result.error) throw new Error(`bash could not be started: ${result.error.message}`);
|
|
173
|
+
if (result.status !== 0 && !allowFailure) throw new Error(`${basename(script)} exited with ${result.status}`);
|
|
174
|
+
return result.status;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let pythonCommand = null;
|
|
178
|
+
function findPython() {
|
|
179
|
+
for (const candidate of ["python3", "python"]) {
|
|
180
|
+
const result = spawnSync(candidate, ["--version"], { encoding: "utf8" });
|
|
181
|
+
if (!result.error && result.status === 0 && /Python 3/.test(`${result.stdout}${result.stderr}`)) return candidate;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function runPython(scriptArgs, { capture = false } = {}) {
|
|
187
|
+
return spawnSync(pythonCommand, scriptArgs, {
|
|
188
|
+
cwd: target,
|
|
189
|
+
encoding: "utf8",
|
|
190
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
191
|
+
env: process.env,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function checkTools() {
|
|
196
|
+
const bash = spawnSync("bash", ["-c", "true"]);
|
|
197
|
+
if (bash.error || bash.status !== 0) {
|
|
198
|
+
throw new Error("bash was not found. The framework's hooks and helper scripts run under bash.\n"
|
|
199
|
+
+ " macOS and Linux have it. On Windows, run this command inside WSL or Git Bash.");
|
|
200
|
+
}
|
|
201
|
+
pythonCommand = findPython();
|
|
202
|
+
if (!pythonCommand) {
|
|
203
|
+
throw new Error("python3 was not found. It powers the HTML renderer, the telemetry writer, the Codex bindings and every guard hook.\n"
|
|
204
|
+
+ " Install it and run this command again:\n"
|
|
205
|
+
+ " macOS: brew install python3\n"
|
|
206
|
+
+ " Ubuntu / WSL: sudo apt-get install -y python3\n"
|
|
207
|
+
+ " Windows: https://www.python.org/downloads/ (tick \"Add to PATH\")\n"
|
|
208
|
+
+ " Without it the project would look installed while its guard hooks stay silent.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureSafeTarget() {
|
|
213
|
+
if (target === sourceRoot || target.startsWith(`${sourceRoot}${sep}`)) throw new Error("Refusing to install into the framework itself. Pass --target=<your project folder>.");
|
|
214
|
+
if (target === resolve("/") || target === resolve(homedir())) throw new Error("Refusing to install into the filesystem root or your home folder.");
|
|
215
|
+
if (existsSync(target) && lstatSync(target).isSymbolicLink()) throw new Error(`Refusing a target that is a symbolic link: ${target}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------- managed blocks in .gitignore and .gitattributes
|
|
219
|
+
|
|
220
|
+
// Same lines, same patterns and same header text as the shell scripts. Append-only: an
|
|
221
|
+
// entry already present in any anchored or slash variant is respected, nothing is rewritten.
|
|
222
|
+
const frameworkIgnore = {
|
|
223
|
+
lines: [".tfcore/", ".claude/", ".opencode/", ".codex/", ".agents/skills/", "/CLAUDE.md", "/WORKFLOW.html", "/opencode.jsonc", "/.tf-scaffold-note.txt"],
|
|
224
|
+
patterns: [/^\/?\.tfcore\/?$/, /^\/?\.claude\/?$/, /^\/?\.opencode\/?$/, /^\/?\.codex\/?$/, /^\/?\.agents\/skills\/?$/, /^\/?CLAUDE\.md$/, /^\/?WORKFLOW\.html$/, /^\/?opencode\.jsonc$/, /^\/?\.tf-scaffold-note\.txt$/],
|
|
225
|
+
header: ["# TechieFlow framework — deployed copies, never commit (managed by scaffold/update-framework.sh)"],
|
|
226
|
+
label: "framework entries",
|
|
227
|
+
};
|
|
228
|
+
const artifactIgnore = {
|
|
229
|
+
lines: ["node_modules/", "/package.json", "/package-lock.json", "tests/.artifacts/", "test-results/", "test-results-*/", "/scripts-*/", "playwright-report/", ".verify/", "logs/", "/docs/.last-verify.json", ".DS_Store"],
|
|
230
|
+
patterns: [/^\/?node_modules\/?$/, /^\/?package\.json$/, /^\/?package-lock\.json$/, /^\/?tests\/\.artifacts\/?$/, /^\/?test-results\/?$/, /^\/?test-results-\*\/?$/, /^\/scripts-\*\/?$/, /^\/?playwright-report\/?$/, /^\/?\.verify\/?$/, /^\/?logs\/?$/, /^\/?docs\/\.last-verify\.json$/, /^\.DS_Store$/],
|
|
231
|
+
header: ["# TechieFlow agent artifacts — machine-generated test harness & logs, never commit (managed by scaffold/update-framework.sh)"],
|
|
232
|
+
label: "agent-artifact entries",
|
|
233
|
+
};
|
|
234
|
+
const attributes = {
|
|
235
|
+
lines: ["* text=auto eol=lf", "*.bat text eol=crlf", "*.cmd text eol=crlf", "docs/metrics/*.jsonl text eol=lf merge=union"],
|
|
236
|
+
patterns: [/^\*[ \t]+text=auto[ \t]+eol=lf$/, /^\*\.bat[ \t]+text[ \t]+eol=crlf$/, /^\*\.cmd[ \t]+text[ \t]+eol=crlf$/, /^docs\/metrics\/\*\.jsonl[ \t]+text[ \t]+eol=lf[ \t]+merge=union$/],
|
|
237
|
+
header: [
|
|
238
|
+
"# TechieFlow — LF working tree on every platform (CRLF only where Windows demands it),",
|
|
239
|
+
"# and append-only telemetry logs that keep BOTH sides on merge.",
|
|
240
|
+
"# Managed by scaffold/update-framework.sh.",
|
|
241
|
+
],
|
|
242
|
+
label: "line-ending + merge rules",
|
|
243
|
+
};
|
|
244
|
+
const legacyAttributeLine = /^docs\/metrics\/\*\.jsonl[ \t]+merge=union[ \t]*$/;
|
|
245
|
+
|
|
246
|
+
function fileLines(path) {
|
|
247
|
+
return existsSync(path) ? read(path).replace(/\r/g, "").split("\n") : [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ensureBlock(fileName, block) {
|
|
251
|
+
const path = join(target, fileName);
|
|
252
|
+
const present = fileLines(path);
|
|
253
|
+
const missing = block.lines.filter((_, i) => !present.some((line) => block.patterns[i].test(line)));
|
|
254
|
+
if (!missing.length) { say(` ${fileName} — ${block.label} already present`); return false; }
|
|
255
|
+
if (dryRun) { say(` ${fileName} — WOULD add ${block.label}: ${missing.join(" ")}`); return true; }
|
|
256
|
+
appendFileSync(path, `\n${block.header.join("\n")}\n${missing.join("\n")}\n`);
|
|
257
|
+
say(` ${fileName} — added ${block.label}: ${missing.join(" ")}`);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function ensureGitattributes() {
|
|
262
|
+
const path = join(target, ".gitattributes");
|
|
263
|
+
if (existsSync(path) && fileLines(path).some((line) => legacyAttributeLine.test(line))) {
|
|
264
|
+
if (dryRun) say(" .gitattributes — WOULD drop the legacy un-pinned telemetry line (superseded)");
|
|
265
|
+
else {
|
|
266
|
+
const kept = read(path).replace(/\n$/, "").split("\n")
|
|
267
|
+
.filter((line) => !legacyAttributeLine.test(line) && !/^# TechieFlow telemetry .* append-only logs; keep BOTH sides on merge/.test(line));
|
|
268
|
+
writeFileSync(path, `${kept.join("\n")}\n`);
|
|
269
|
+
say(" .gitattributes — dropped the legacy un-pinned telemetry line (superseded)");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (ensureBlock(".gitattributes", attributes) && !dryRun) {
|
|
273
|
+
say(" RENORMALIZE ONCE, yourself, in this repo so the committed blobs match:");
|
|
274
|
+
say(' git add --renormalize . && git commit -m "Normalize line endings"');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------- text the shell scripts write
|
|
279
|
+
|
|
280
|
+
const canonicalSettings = String.raw`{
|
|
281
|
+
"permissions": {
|
|
282
|
+
"defaultMode": "acceptEdits",
|
|
283
|
+
"allow": [
|
|
284
|
+
"Bash",
|
|
285
|
+
"Edit",
|
|
286
|
+
"Write",
|
|
287
|
+
"MultiEdit",
|
|
288
|
+
"NotebookEdit",
|
|
289
|
+
"Read",
|
|
290
|
+
"Glob",
|
|
291
|
+
"Grep",
|
|
292
|
+
"TodoWrite",
|
|
293
|
+
"WebFetch",
|
|
294
|
+
"WebSearch",
|
|
295
|
+
"Task"
|
|
296
|
+
],
|
|
297
|
+
"ask": [],
|
|
298
|
+
"deny": [
|
|
299
|
+
"Bash(rm -rf /)", "Bash(rm -rf /*)", "Bash(rm -rf ~)", "Bash(rm -rf ~/*)",
|
|
300
|
+
"Bash(git commit*)", "Bash(git push*)", "Bash(git add*)", "Bash(git rm*)",
|
|
301
|
+
"Bash(git mv*)", "Bash(git reset*)", "Bash(git checkout*)", "Bash(git switch*)",
|
|
302
|
+
"Bash(git restore*)", "Bash(git merge*)", "Bash(git rebase*)", "Bash(git cherry-pick*)",
|
|
303
|
+
"Bash(git revert*)", "Bash(git clean*)", "Bash(git am*)", "Bash(git apply*)",
|
|
304
|
+
"Bash(git init*)", "Bash(git clone*)", "Bash(git pull*)", "Bash(git fetch*)",
|
|
305
|
+
"Bash(git filter-branch*)", "Bash(git filter-repo*)", "Bash(git gc*)", "Bash(git prune*)",
|
|
306
|
+
"Bash(git update-ref*)", "Bash(git symbolic-ref*)", "Bash(git replace*)", "Bash(git update-index*)",
|
|
307
|
+
"Bash(git commit-tree*)", "Bash(git write-tree*)", "Bash(git read-tree*)", "Bash(git fast-import*)",
|
|
308
|
+
"Bash(git send-email*)", "Bash(git request-pull*)", "Bash(git svn*)",
|
|
309
|
+
"Bash(gh pr create*)", "Bash(gh pr merge*)", "Bash(gh pr close*)", "Bash(gh pr edit*)",
|
|
310
|
+
"Bash(gh pr comment*)", "Bash(gh pr review*)", "Bash(gh pr ready*)", "Bash(gh pr reopen*)",
|
|
311
|
+
"Bash(gh pr lock*)", "Bash(gh pr unlock*)", "Bash(gh pr update-branch*)", "Bash(gh pr checkout*)",
|
|
312
|
+
"Bash(gh issue create*)", "Bash(gh issue close*)", "Bash(gh issue edit*)", "Bash(gh issue comment*)",
|
|
313
|
+
"Bash(gh issue delete*)", "Bash(gh issue reopen*)", "Bash(gh issue pin*)", "Bash(gh issue unpin*)",
|
|
314
|
+
"Bash(gh issue lock*)", "Bash(gh issue unlock*)", "Bash(gh issue transfer*)", "Bash(gh issue develop*)",
|
|
315
|
+
"Bash(gh repo create*)", "Bash(gh repo delete*)", "Bash(gh repo fork*)", "Bash(gh repo clone*)",
|
|
316
|
+
"Bash(gh repo edit*)", "Bash(gh repo sync*)", "Bash(gh repo archive*)", "Bash(gh repo unarchive*)",
|
|
317
|
+
"Bash(gh repo rename*)", "Bash(gh repo set-default*)", "Bash(gh repo deploy-key add*)", "Bash(gh repo deploy-key delete*)",
|
|
318
|
+
"Bash(gh release create*)", "Bash(gh release delete*)", "Bash(gh release upload*)", "Bash(gh release edit*)",
|
|
319
|
+
"Bash(gh workflow run*)", "Bash(gh workflow enable*)", "Bash(gh workflow disable*)", "Bash(gh run cancel*)",
|
|
320
|
+
"Bash(gh run rerun*)", "Bash(gh run delete*)", "Bash(gh secret set*)", "Bash(gh secret delete*)",
|
|
321
|
+
"Bash(gh variable set*)", "Bash(gh variable delete*)", "Bash(gh label create*)", "Bash(gh label delete*)",
|
|
322
|
+
"Bash(gh label edit*)", "Bash(gh auth login*)", "Bash(gh auth logout*)", "Bash(gh auth refresh*)",
|
|
323
|
+
"Bash(gh auth setup-git*)", "Bash(gh gist create*)", "Bash(gh gist delete*)", "Bash(gh gist edit*)",
|
|
324
|
+
"Bash(gh ssh-key add*)", "Bash(gh gpg-key add*)", "Bash(gh cache delete*)"
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
"hooks": {
|
|
328
|
+
"PreToolUse": [
|
|
329
|
+
{
|
|
330
|
+
"matcher": "Bash",
|
|
331
|
+
"hooks": [
|
|
332
|
+
{
|
|
333
|
+
"type": "command",
|
|
334
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/block-git.sh\""
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"type": "command",
|
|
338
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/guard-artifacts.sh\""
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
344
|
+
"hooks": [
|
|
345
|
+
{
|
|
346
|
+
"type": "command",
|
|
347
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/guard-status.sh\""
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"type": "command",
|
|
351
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/guard-verify.sh\""
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"SessionStart": [
|
|
357
|
+
{
|
|
358
|
+
"hooks": [
|
|
359
|
+
{
|
|
360
|
+
"type": "command",
|
|
361
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/session-pointer.sh\""
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"type": "command",
|
|
365
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/sweep-artifacts.sh\""
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
"UserPromptSubmit": [
|
|
371
|
+
{
|
|
372
|
+
"hooks": [
|
|
373
|
+
{
|
|
374
|
+
"type": "command",
|
|
375
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/session-pointer.sh\""
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
],
|
|
380
|
+
"Stop": [
|
|
381
|
+
{
|
|
382
|
+
"hooks": [
|
|
383
|
+
{
|
|
384
|
+
"type": "command",
|
|
385
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/guard-status-html.sh\""
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
"SessionEnd": [
|
|
391
|
+
{
|
|
392
|
+
"hooks": [
|
|
393
|
+
{
|
|
394
|
+
"type": "command",
|
|
395
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.tfcore/hooks/metrics-session.sh\""
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
}`;
|
|
402
|
+
|
|
403
|
+
const brownfieldNote = `Scaffolded by the TechieFlow v4 customized brownfield scaffold script.
|
|
404
|
+
|
|
405
|
+
This is a BROWNFIELD project — your existing src/, tests/, and docs/ contents
|
|
406
|
+
were NOT touched. Only the TechieFlow framework files were added.
|
|
407
|
+
|
|
408
|
+
Added (only missing files filled — re-runs are safe):
|
|
409
|
+
.tfcore/ ← TechieFlow v4 customized (agents, tasks, templates)
|
|
410
|
+
.claude/commands/ ← Claude Code slash commands (TechieFlow agents)
|
|
411
|
+
.claude/settings.json ← yolo-except-git-writes permissions
|
|
412
|
+
WORKFLOW.html ← the human workflow guide (open in a browser; §17 = macOS / Windows / Linux)
|
|
413
|
+
opencode.jsonc ← OpenCode config (loads agents/tasks from .tfcore/ via {file:...} refs)
|
|
414
|
+
.gitignore ← framework entries appended (deployed copies stay uncommitted)
|
|
415
|
+
|
|
416
|
+
All TechieFlow templates the analyst will need live LOCALLY in this project under:
|
|
417
|
+
.tfcore/templates/v4custom/
|
|
418
|
+
|
|
419
|
+
So the project is portable — copy this whole tree to another machine and the
|
|
420
|
+
analyst can author every doc without reaching back to the original template.
|
|
421
|
+
|
|
422
|
+
Library agents (TrBlazeUI, TechieRag) auto-deploy on \`dotnet build\` IF the
|
|
423
|
+
project already references the NuGet packages. To add them:
|
|
424
|
+
dotnet add <YourProject>.csproj package TrBlazeUI
|
|
425
|
+
dotnet add <YourProject>.csproj package TechieRag
|
|
426
|
+
dotnet build
|
|
427
|
+
|
|
428
|
+
Next step (brownfield day-1):
|
|
429
|
+
Start Claude Code in this folder and run:
|
|
430
|
+
/analyst *day1-brownfield <AppName>
|
|
431
|
+
(Replace <AppName> with PascalCase name, e.g. AppManager, TrTools.)
|
|
432
|
+
|
|
433
|
+
That single command will produce all six day-1 deliverables in one pass:
|
|
434
|
+
docs/<AppName>-Architecture.md
|
|
435
|
+
docs/<AppName>-BRD.md
|
|
436
|
+
docs/<AppName>-Coding-Standards.md
|
|
437
|
+
.editorconfig
|
|
438
|
+
PROJECT-STATUS.md
|
|
439
|
+
CLAUDE.md
|
|
440
|
+
|
|
441
|
+
Then review the docs and edit directly if needed.
|
|
442
|
+
|
|
443
|
+
You can delete this note once you've read it.
|
|
444
|
+
`;
|
|
445
|
+
|
|
446
|
+
const greenfieldNote = `Scaffolded by the TechieFlow v4 customized greenfield scaffold script.
|
|
447
|
+
|
|
448
|
+
This is a NEW (greenfield) project — empty src/ and tests/ folders are ready.
|
|
449
|
+
|
|
450
|
+
Folders/files created (only missing files filled — re-runs are safe):
|
|
451
|
+
.tfcore/ ← TechieFlow v4 customized (agents, tasks, templates)
|
|
452
|
+
.claude/commands/ ← Claude Code slash commands (TechieFlow agents)
|
|
453
|
+
.claude/settings.json ← yolo-except-git-writes permissions
|
|
454
|
+
WORKFLOW.html ← the human workflow guide (open in a browser; §17 = macOS / Windows / Linux)
|
|
455
|
+
opencode.jsonc ← OpenCode config (loads agents/tasks from .tfcore/ via {file:...} refs)
|
|
456
|
+
.gitignore ← framework entries appended (deployed copies stay uncommitted)
|
|
457
|
+
tests/playwright/ tests/unit/ src/
|
|
458
|
+
|
|
459
|
+
All TechieFlow templates the analyst will need live LOCALLY in this project under:
|
|
460
|
+
.tfcore/templates/v4custom/
|
|
461
|
+
|
|
462
|
+
So the project is portable — copy this whole tree to another machine and the
|
|
463
|
+
analyst can author every doc without reaching back to the original template.
|
|
464
|
+
|
|
465
|
+
Next: create the .NET solution, add your library NuGet packages, build once:
|
|
466
|
+
dotnet new sln -n MyApp
|
|
467
|
+
dotnet new blazor -n MyApp.Web -o src/MyApp.Web && dotnet sln add src/MyApp.Web
|
|
468
|
+
dotnet add src/MyApp.Web package TrBlazeUI # if UI involved
|
|
469
|
+
dotnet add src/MyApp.Web package TechieRag # if AI/RAG involved
|
|
470
|
+
dotnet build ← deploys .claude/<lib>.md, .opencode/command/<lib>.md,
|
|
471
|
+
.<lib>/<Lib>-AI-Reference.md
|
|
472
|
+
|
|
473
|
+
Then start your harness in this folder and run day-1:
|
|
474
|
+
Claude Code: /analyst *day1-greenfield <AppName>
|
|
475
|
+
(full form: /TechieFlow:agents:analyst *day1-greenfield <AppName>)
|
|
476
|
+
OpenCode: /flow-analyst *day1-greenfield <AppName>
|
|
477
|
+
(Replace <AppName> with PascalCase name, e.g. AppManager, AstroLyfe.)
|
|
478
|
+
|
|
479
|
+
That single command will produce all six day-1 deliverables in one pass.
|
|
480
|
+
|
|
481
|
+
You can delete this note once you've read it.
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------- steps shared by install and update
|
|
485
|
+
|
|
486
|
+
// The OpenCode config copy inside .opencode/ has its {file:} paths pointed one folder up.
|
|
487
|
+
const openCodeConfigCopy = () => read(join(sourceRoot, "opencode.jsonc")).replaceAll("{file:./.tfcore/", "{file:../.tfcore/");
|
|
488
|
+
|
|
489
|
+
function shippedFiles(folder, extension) {
|
|
490
|
+
const full = join(sourceRoot, folder);
|
|
491
|
+
if (!existsSync(full)) return [];
|
|
492
|
+
return readdirSync(full).filter((f) => f.endsWith(extension) && statSync(join(full, f)).isFile()).sort();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function deployOpenCodeBridge() {
|
|
496
|
+
if (!dryRun) mkdirSync(join(target, ".opencode", "plugin"), { recursive: true });
|
|
497
|
+
for (const plugin of shippedFiles(".opencode/plugin", ".js")) copyFile(join(sourceRoot, ".opencode", "plugin", plugin), join(target, ".opencode", "plugin", plugin));
|
|
498
|
+
writeText(join(target, ".opencode", "opencode.jsonc"), openCodeConfigCopy());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function deployCodexAdapter() {
|
|
502
|
+
say(" .codex/ + .agents/skills/ — Codex adapter");
|
|
503
|
+
if (dryRun) { say(" WOULD preserve/create .codex/config.toml; refresh hooks/rules; regenerate agents/skills"); return; }
|
|
504
|
+
for (const folder of [".codex/agents", ".codex/rules", ".agents/skills"]) mkdirSync(join(target, folder), { recursive: true });
|
|
505
|
+
copyFile(join(sourceRoot, ".codex", "config.toml"), join(target, ".codex", "config.toml"), { onlyIfMissing: true });
|
|
506
|
+
copyFile(join(sourceRoot, ".codex", "hooks.json"), join(target, ".codex", "hooks.json"));
|
|
507
|
+
copyFile(join(sourceRoot, ".codex", "rules", "techieflow.rules"), join(target, ".codex", "rules", "techieflow.rules"));
|
|
508
|
+
const result = runPython([join(target, ".tfcore", "utils", "tf-codex-bind.py"), target]);
|
|
509
|
+
if (result.status !== 0) say(" ⚠ Codex bindings could not be generated (python3 required)");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function deployHousekeeping() {
|
|
513
|
+
if (manageGitignore) {
|
|
514
|
+
ensureBlock(".gitignore", frameworkIgnore);
|
|
515
|
+
ensureBlock(".gitignore", artifactIgnore);
|
|
516
|
+
} else say(" .gitignore — not edited (--no-gitignore)");
|
|
517
|
+
if (dryRun && !existsSync(target)) {
|
|
518
|
+
say(" gitignore audit — WOULD run once the folder exists (build-output rules for the detected stack)");
|
|
519
|
+
say(" telemetry — WOULD create docs/metrics/ and seed runs/gates/sessions/commits/misses.jsonl");
|
|
520
|
+
} else {
|
|
521
|
+
// Build-output rules for the stack the project is written in, and whether that output is
|
|
522
|
+
// already tracked. Reads .git/index directly; never runs git.
|
|
523
|
+
runBash(join(sourceRoot, ".tfcore", "utils", "tf-gitignore-audit.sh"), [".", manageGitignore && !dryRun ? "--fix" : "--dry-run"]);
|
|
524
|
+
// Telemetry: docs/metrics/, the project classification and the pre-commit hook. Never runs git.
|
|
525
|
+
runBash(join(sourceRoot, ".tfcore", "telemetry", "install-metrics.sh"), dryRun ? [".", "--dry-run"] : ["."]);
|
|
526
|
+
}
|
|
527
|
+
if (manageGitignore) ensureGitattributes();
|
|
528
|
+
else say(" .gitattributes — not edited (--no-gitignore)");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function shimLegacyPersonas({ gapOnly }) {
|
|
532
|
+
for (const lib of ["trblazeui", "techierag"]) {
|
|
533
|
+
const legacy = join(target, ".claude", `${lib}.md`);
|
|
534
|
+
const current = join(target, ".claude", "commands", `${lib}.md`);
|
|
535
|
+
if (!existsSync(legacy)) continue;
|
|
536
|
+
if (gapOnly && existsSync(current)) {
|
|
537
|
+
if (!readFileSync(legacy).equals(readFileSync(current))) {
|
|
538
|
+
say(` ⚠ .claude/${lib}.md (legacy path) differs from .claude/commands/${lib}.md (current`);
|
|
539
|
+
say(" NuGet target). Keeping the commands/ copy — it is the one dotnet build writes.");
|
|
540
|
+
say(" The legacy file is dead weight; delete it when convenient.");
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (dryRun) { say(` WOULD shim .claude/${lib}.md → .claude/commands/${lib}.md`); continue; }
|
|
545
|
+
mkdirSync(dirname(current), { recursive: true });
|
|
546
|
+
copyFileSync(legacy, current);
|
|
547
|
+
chmodSync(current, statSync(legacy).mode & 0o777);
|
|
548
|
+
say(` shimmed ${gapOnly ? "legacy " : ""}.claude/${lib}.md → .claude/commands/${lib}.md${gapOnly ? " (no current copy present)" : ""}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------- install (scaffold-brownfield.sh / scaffold-greenfield.sh)
|
|
553
|
+
|
|
554
|
+
function hasDotnetProject(directory, depth = 4) {
|
|
555
|
+
let entries;
|
|
556
|
+
try { entries = readdirSync(directory, { withFileTypes: true }); } catch { return false; }
|
|
557
|
+
for (const entry of entries) {
|
|
558
|
+
if (entry.isFile() && (entry.name.endsWith(".csproj") || entry.name.endsWith(".sln"))) return true;
|
|
559
|
+
if (entry.isDirectory() && depth > 1 && hasDotnetProject(join(directory, entry.name), depth - 1)) return true;
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function install() {
|
|
565
|
+
ensureSafeTarget();
|
|
566
|
+
if (greenfield) {
|
|
567
|
+
if (!existsSync(target)) { say(` ${would}create ${target}/`); if (!dryRun) mkdirSync(target, { recursive: true }); }
|
|
568
|
+
} else if (!existsSync(target)) {
|
|
569
|
+
throw new Error(`Target directory does not exist: ${target}\nBrownfield means an EXISTING project. For a new one add --greenfield.`);
|
|
570
|
+
}
|
|
571
|
+
if (existsSync(target) && !statSync(target).isDirectory()) throw new Error(`Target is not a directory: ${target}`);
|
|
572
|
+
|
|
573
|
+
if (!greenfield && !hasDotnetProject(target)) {
|
|
574
|
+
warn(`⚠ Warning: no .csproj or .sln found within 4 levels of ${target}`);
|
|
575
|
+
warn(" This command is intended for existing .NET projects.");
|
|
576
|
+
if (process.stdin.isTTY && !force) {
|
|
577
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
578
|
+
const answer = (await rl.question("No .csproj/.sln found — continue anyway? [y/N] ")).trim();
|
|
579
|
+
rl.close();
|
|
580
|
+
if (!/^[yY]$/.test(answer)) throw new Error("Aborted.");
|
|
581
|
+
} else warn(" (continuing despite the warning.)");
|
|
582
|
+
say("");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (existsSync(join(target, ".tfcore"))) {
|
|
586
|
+
say("ℹ This project already has .tfcore/ — it appears to be already installed.");
|
|
587
|
+
say(" install never updates existing framework files (tasks, templates, agents).");
|
|
588
|
+
say(" To refresh them while keeping your docs/, src/, tests/, PROJECT-STATUS.md, CLAUDE.md, run:");
|
|
589
|
+
say(" npx @techierathore/techieflow@latest update");
|
|
590
|
+
say("");
|
|
591
|
+
say(" Continuing with install (will only fill in any missing files)...");
|
|
592
|
+
say("");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
say(`→ ${dryRun ? "DRY RUN — " : ""}${greenfield ? "Scaffolding TechieFlow into" : "Adding TechieFlow framework to existing project"}: ${target}`);
|
|
596
|
+
|
|
597
|
+
// 1. framework core, never overwriting
|
|
598
|
+
copyTree(join(sourceRoot, ".tfcore"), join(target, ".tfcore"), { onlyIfMissing: true });
|
|
599
|
+
// 2. Claude Code slash commands (library personas excluded: NuGet deploys those)
|
|
600
|
+
copyTree(join(sourceRoot, ".claude", "commands"), join(target, ".claude", "commands"), { onlyIfMissing: true, excludeNames: libraryPersonas });
|
|
601
|
+
// 3b. force-sync the personas into the Claude mirror from the project's own .tfcore/agents/
|
|
602
|
+
say(" syncing agent files from .tfcore/agents/ → .claude/commands/TechieFlow/agents/");
|
|
603
|
+
copyTree(dryRun ? join(sourceRoot, ".tfcore", "agents") : join(target, ".tfcore", "agents"), join(target, ".claude", "commands", "TechieFlow", "agents"));
|
|
604
|
+
// 4. reference files at the root, only if missing
|
|
605
|
+
copyFile(join(sourceRoot, "WORKFLOW.html"), join(target, "WORKFLOW.html"), { onlyIfMissing: true });
|
|
606
|
+
copyFile(join(sourceRoot, "opencode.jsonc"), join(target, "opencode.jsonc"), { onlyIfMissing: true });
|
|
607
|
+
// 4b. OpenCode bridge, always refreshed
|
|
608
|
+
deployOpenCodeBridge();
|
|
609
|
+
// 4c. Codex adapter
|
|
610
|
+
deployCodexAdapter();
|
|
611
|
+
if (!dryRun) say(" Codex adapter installed — trust this repository and review /hooks before relying on guards");
|
|
612
|
+
// 5. Claude Code permissions, only if missing
|
|
613
|
+
if (writeText(join(target, ".claude", "settings.json"), `${canonicalSettings}\n`, { onlyIfMissing: true })) { if (!dryRun) say(" created .claude/settings.json"); }
|
|
614
|
+
else say(" .claude/settings.json already exists — preserved");
|
|
615
|
+
// 6. greenfield skeleton
|
|
616
|
+
if (greenfield) {
|
|
617
|
+
for (const folder of ["tests/playwright", "tests/unit", "src"]) {
|
|
618
|
+
if (!existsSync(join(target, folder))) { say(` ${would}create ${folder}/`); if (!dryRun) mkdirSync(join(target, folder), { recursive: true }); }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// 7. scaffold note, only if missing
|
|
622
|
+
writeText(join(target, ".tf-scaffold-note.txt"), greenfield ? greenfieldNote : brownfieldNote, { onlyIfMissing: true });
|
|
623
|
+
// 8. NuGet persona shims: the deployed file is authoritative, always refresh the shim
|
|
624
|
+
shimLegacyPersonas({ gapOnly: false });
|
|
625
|
+
// 9. .gitignore, build-output audit, telemetry, .gitattributes
|
|
626
|
+
deployHousekeeping();
|
|
627
|
+
|
|
628
|
+
say("");
|
|
629
|
+
if (dryRun) { say("✔ Dry run complete — no files changed."); return; }
|
|
630
|
+
say(greenfield ? "✔ Done." : "✔ Done. Existing source tree was NOT touched.");
|
|
631
|
+
say("");
|
|
632
|
+
say(`Next: cd "${target}"`);
|
|
633
|
+
if (greenfield) say(" dotnet new sln + blazor + add TrBlazeUI/TechieRag NuGets + dotnet build");
|
|
634
|
+
say(" open WORKFLOW.html in a browser");
|
|
635
|
+
say(` start Claude Code or OpenCode and run the ${greenfield ? "greenfield" : "brownfield"} day-1 command from .tf-scaffold-note.txt`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------- update (update-framework.sh)
|
|
639
|
+
|
|
640
|
+
const frameworkSubdirs = ["agents", "tasks", "telemetry", "templates", "checklists", "data", "utils", "hooks", "workflows", "agent-teams"];
|
|
641
|
+
const frameworkTopFiles = ["enhanced-ide-development-workflow.md", "user-guide.md", "working-in-the-brownfield.md", "install-manifest.yaml", "TOKEN-GUIDE.md"];
|
|
642
|
+
const legacyConfigPattern = /\.bmad-core|bmad-(master|orchestrator|analyst|architect|verifier)|BMad:/;
|
|
643
|
+
|
|
644
|
+
function migrateLegacyLayout() {
|
|
645
|
+
if (existsSync(join(target, ".tfcore")) || !existsSync(join(target, ".bmad-core"))) return true;
|
|
646
|
+
if (dryRun) {
|
|
647
|
+
say(`→ DRY RUN — ${target} uses the legacy .bmad-core/ layout.`);
|
|
648
|
+
say(" A real run would MIGRATE it first:");
|
|
649
|
+
say(" • mv .bmad-core/ → .tfcore/ (keeps your core-config.yaml + docs)");
|
|
650
|
+
say(" • rm -rf .claude/commands/BMad/ .opencode/command/BMad/ (old mirrors)");
|
|
651
|
+
say(" • patch .tfcore/core-config.yaml (slashPrefix: TechieFlow, .tfcore paths)");
|
|
652
|
+
say(" • back up & replace a legacy opencode.jsonc");
|
|
653
|
+
say(" • delete the disposable legacy scaffold note");
|
|
654
|
+
say(" • remove trimmed stock dirs (workflows/, agent-teams/) if present");
|
|
655
|
+
say(" • flag (not edit) any stale framework refs in YOUR CLAUDE.md / PROJECT-STATUS.md / docs");
|
|
656
|
+
say(" …then apply the usual framework refresh. Re-run without --dry-run to do it.");
|
|
657
|
+
say("");
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
say(`→ Migrating legacy .bmad-core/ layout to .tfcore/ in: ${target}`);
|
|
661
|
+
renameSync(join(target, ".bmad-core"), join(target, ".tfcore"));
|
|
662
|
+
say(" .bmad-core/ → .tfcore/ (per-project core-config.yaml + any docs preserved)");
|
|
663
|
+
const config = join(target, ".tfcore", "core-config.yaml");
|
|
664
|
+
if (existsSync(config)) {
|
|
665
|
+
writeFileSync(config, read(config).replace(/\.bmad-core/g, ".tfcore").replace(/^slashPrefix:[ \t]*BMad/m, "slashPrefix: TechieFlow"));
|
|
666
|
+
say(" patched .tfcore/core-config.yaml (slashPrefix: TechieFlow, .tfcore paths)");
|
|
667
|
+
}
|
|
668
|
+
rmSync(join(target, ".claude", "commands", "BMad"), { recursive: true, force: true });
|
|
669
|
+
rmSync(join(target, ".opencode", "command", "BMad"), { recursive: true, force: true });
|
|
670
|
+
say(" removed legacy .claude/commands/BMad/ and .opencode/command/BMad/");
|
|
671
|
+
rmSync(join(target, ".tfcore", "data", "bmad-kb.md"), { force: true });
|
|
672
|
+
rmSync(join(target, ".tfcore", "utils", "bmad-doc-template.md"), { force: true });
|
|
673
|
+
if (existsSync(join(target, ".bmad-scaffold-note.txt"))) {
|
|
674
|
+
rmSync(join(target, ".bmad-scaffold-note.txt"));
|
|
675
|
+
say(" removed disposable legacy .bmad-scaffold-note.txt");
|
|
676
|
+
}
|
|
677
|
+
const rootConfig = join(target, "opencode.jsonc");
|
|
678
|
+
if (existsSync(rootConfig) && legacyConfigPattern.test(read(rootConfig))) {
|
|
679
|
+
copyFileSync(rootConfig, `${rootConfig}.bak`);
|
|
680
|
+
copyFileSync(join(sourceRoot, "opencode.jsonc"), rootConfig);
|
|
681
|
+
say(" opencode.jsonc — legacy version backed up to opencode.jsonc.bak, replaced with current template");
|
|
682
|
+
}
|
|
683
|
+
say("");
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function refreshRootOpenCodeConfig() {
|
|
688
|
+
const rootConfig = join(target, "opencode.jsonc");
|
|
689
|
+
if (!existsSync(rootConfig)) return;
|
|
690
|
+
const audit = runPython([join(sourceRoot, ".tfcore", "utils", "tf-opencode-audit.py"), join(sourceRoot, "opencode.jsonc"), "opencode.jsonc"], { capture: true });
|
|
691
|
+
const output = audit.status === 0 ? audit.stdout : "";
|
|
692
|
+
const [verdict = "", ...rest] = output.split("\n");
|
|
693
|
+
let replaced = false;
|
|
694
|
+
if (verdict === "current") say(" opencode.jsonc — already current (matches template)");
|
|
695
|
+
else if (verdict === "refresh") {
|
|
696
|
+
if (dryRun) say(" opencode.jsonc — WOULD refresh from template (no project-only content; old file → .bak)");
|
|
697
|
+
else {
|
|
698
|
+
copyFileSync(rootConfig, `${rootConfig}.bak`);
|
|
699
|
+
copyFileSync(join(sourceRoot, "opencode.jsonc"), rootConfig);
|
|
700
|
+
say(" opencode.jsonc — refreshed from template (no project-only content; old file → opencode.jsonc.bak)");
|
|
701
|
+
replaced = true;
|
|
702
|
+
}
|
|
703
|
+
} else if (verdict.startsWith("project|")) {
|
|
704
|
+
say(` opencode.jsonc — preserved (project-only keys: ${verdict.slice("project|".length)})`);
|
|
705
|
+
say(" Framework keys still arrive via .opencode/opencode.jsonc, which wins on conflicts.");
|
|
706
|
+
say(" Fold those keys into a fresh copy of the package's opencode.jsonc when convenient.");
|
|
707
|
+
} else say(` opencode.jsonc — preserved (could not audit: ${verdict.replace(/^unknown\|/, "")})`);
|
|
708
|
+
|
|
709
|
+
const deadRefs = rest.filter((l) => l.startsWith("DIAG|dead-ref|")).map((l) => l.split("|")[2]);
|
|
710
|
+
const bareBash = rest.filter((l) => l.startsWith("DIAG|bare-bash-allow|")).map((l) => l.split("|")[2]);
|
|
711
|
+
if (deadRefs.length && replaced) {
|
|
712
|
+
say(` ✔ opencode.jsonc — ${deadRefs.length} dead {file:} ref(s) RESOLVED by the refresh`);
|
|
713
|
+
say(" (they were in the old file, kept as opencode.jsonc.bak; OpenCode's whole");
|
|
714
|
+
say(" config load was failing on them until now)");
|
|
715
|
+
} else {
|
|
716
|
+
for (const ref of deadRefs.slice(0, 5)) {
|
|
717
|
+
say(` ⚠ opencode.jsonc references a file that does not exist: ${ref}`);
|
|
718
|
+
say(" A dead {file:} ref hard-fails OpenCode's ENTIRE config load.");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (!replaced) {
|
|
722
|
+
for (const where of bareBash) {
|
|
723
|
+
say(` ⚠ opencode.jsonc has a bare "bash": "allow" at ${where} — that shape voids the`);
|
|
724
|
+
say(" git/gh denies (agent rules are appended AFTER the root ruleset).");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function reportStaleReferences() {
|
|
730
|
+
const rootConfig = join(target, "opencode.jsonc");
|
|
731
|
+
if (existsSync(rootConfig) && /agents\/(dev|pm|po|qa|sm|ux-expert)\.md/.test(read(rootConfig))) {
|
|
732
|
+
say("");
|
|
733
|
+
say(" ⚠ opencode.jsonc references trimmed stock agents (dev/pm/po/qa/sm/ux-expert),");
|
|
734
|
+
say(" whose files were just removed from .tfcore/agents/. Copy the package's");
|
|
735
|
+
say(" opencode.jsonc or delete those agent entries.");
|
|
736
|
+
}
|
|
737
|
+
const frameworkConfig = dryRun ? join(sourceRoot, "opencode.jsonc") : join(target, ".opencode", "opencode.jsonc");
|
|
738
|
+
if (existsSync(frameworkConfig)) {
|
|
739
|
+
const text = read(frameworkConfig);
|
|
740
|
+
for (const [file, description] of [
|
|
741
|
+
["build-invocation-ladder.md", "the mandatory build-invocation ladder instruction (OpenCode Docker may choose direct dotnet/cmd.exe and report false blockers)"],
|
|
742
|
+
["opencode-operating-contract.md", "the OpenCode operating contract instruction (OpenCode may stop after a green build instead of running smoke + verify + status gates)"],
|
|
743
|
+
]) {
|
|
744
|
+
if (!text.includes(`.tfcore/templates/v4custom/${file}`)) {
|
|
745
|
+
say("");
|
|
746
|
+
say(` ⚠ ${rel(frameworkConfig)} is missing ${description}.`);
|
|
747
|
+
say(" This is a FRAMEWORK defect, not a per-project one. Report it; do NOT hand-edit");
|
|
748
|
+
say(" the app's root opencode.jsonc to compensate.");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const stale = /\.bmad-core|\/BMad:|BMad:agents:|bmad-master|bmad-orchestrator|bmad-analyst|bmad-architect|bmad-verifier/;
|
|
753
|
+
const candidates = new Set(["CLAUDE.md", "PROJECT-STATUS.md", ".gitignore"]);
|
|
754
|
+
const docs = join(target, "docs");
|
|
755
|
+
if (existsSync(docs)) for (const f of readdirSync(docs)) if (/\.(md|html)$/.test(f) && statSync(join(docs, f)).isFile()) candidates.add(`docs/${f}`);
|
|
756
|
+
for (const f of readdirSync(target)) if (f.endsWith(".html") && statSync(join(target, f)).isFile()) candidates.add(f);
|
|
757
|
+
const hits = [...candidates].sort().filter((f) => existsSync(join(target, f)) && statSync(join(target, f)).isFile() && stale.test(read(join(target, f))));
|
|
758
|
+
if (hits.length) {
|
|
759
|
+
say("");
|
|
760
|
+
say(" ⚠ These files YOU own still reference the old framework layout (NOT auto-edited):");
|
|
761
|
+
for (const hit of hits) say(` ${hit}`);
|
|
762
|
+
say(" Update the pointers (.bmad-core→.tfcore, /BMad:→/TechieFlow:,");
|
|
763
|
+
say(" bmad-master/bmad-orchestrator→flow-master) or regenerate them");
|
|
764
|
+
say(" (re-run day-1 / *refresh-status / *render-workflow-docs).");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function update() {
|
|
769
|
+
ensureSafeTarget();
|
|
770
|
+
if (!existsSync(target)) throw new Error(`Target directory does not exist: ${target}`);
|
|
771
|
+
if (!migrateLegacyLayout()) return;
|
|
772
|
+
if (!existsSync(join(target, ".tfcore"))) {
|
|
773
|
+
throw new Error(`${target} does not look installed (no .tfcore/ or legacy .bmad-core/ found).\nRun \`npx @techierathore/techieflow@latest install\` first.`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (dryRun) say(`→ DRY RUN — showing what would change in: ${target}\n`);
|
|
777
|
+
else {
|
|
778
|
+
say(`→ Updating TechieFlow framework in: ${target}`);
|
|
779
|
+
say(" (your docs/, src/, tests/, PROJECT-STATUS.md, CLAUDE.md are NOT touched)\n");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 1. .tfcore/ framework folders: overwrite and delete extras; core-config.yaml and routing.yaml are per-project
|
|
783
|
+
for (const dir of frameworkSubdirs) {
|
|
784
|
+
const source = join(sourceRoot, ".tfcore", dir);
|
|
785
|
+
const destination = join(target, ".tfcore", dir);
|
|
786
|
+
if (existsSync(source)) {
|
|
787
|
+
say(` .tfcore/${dir}/`);
|
|
788
|
+
copyTree(source, destination, { deleteExtra: true });
|
|
789
|
+
} else if (existsSync(destination)) {
|
|
790
|
+
if (dir === "workflows" || dir === "agent-teams") {
|
|
791
|
+
say(` ${would}remove${dryRun ? "" : "d"} stale stock dir .tfcore/${dir}/ (no longer ships)`);
|
|
792
|
+
if (!dryRun) rmSync(destination, { recursive: true, force: true });
|
|
793
|
+
} else say(` NOTE: .tfcore/${dir}/ no longer ships with the framework — safe to delete manually.`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const file of frameworkTopFiles) {
|
|
797
|
+
if (existsSync(join(sourceRoot, ".tfcore", file))) copyFile(join(sourceRoot, ".tfcore", file), join(target, ".tfcore", file));
|
|
798
|
+
}
|
|
799
|
+
if (existsSync(join(target, ".tfcore", "core-config.yaml"))) say(" .tfcore/core-config.yaml — preserved (per-project)");
|
|
800
|
+
if (existsSync(join(target, ".tfcore", "routing.yaml"))) say(" .tfcore/routing.yaml — preserved (per-project routing declaration)");
|
|
801
|
+
else if (existsSync(join(sourceRoot, ".tfcore", "routing.yaml"))) {
|
|
802
|
+
copyFile(join(sourceRoot, ".tfcore", "routing.yaml"), join(target, ".tfcore", "routing.yaml"));
|
|
803
|
+
say(" .tfcore/routing.yaml — deployed (enabled: false; routing is opt-in per app)");
|
|
804
|
+
}
|
|
805
|
+
if (dryRun) say(" (routing bindings: tf-routing-bind.sh runs on a real update, per this app's routing.yaml)");
|
|
806
|
+
else runBash(join(target, ".tfcore", "utils", "tf-routing-bind.sh"), ["."]);
|
|
807
|
+
|
|
808
|
+
// 2. Claude Code mirror: overwrite and delete extras; personas re-synced from the package
|
|
809
|
+
if (!dryRun) mkdirSync(join(target, ".claude", "commands", "TechieFlow"), { recursive: true });
|
|
810
|
+
say(" .claude/commands/TechieFlow/");
|
|
811
|
+
copyTree(join(sourceRoot, ".claude", "commands", "TechieFlow"), join(target, ".claude", "commands", "TechieFlow"), { deleteExtra: true });
|
|
812
|
+
copyTree(join(sourceRoot, ".tfcore", "agents"), join(target, ".claude", "commands", "TechieFlow", "agents"));
|
|
813
|
+
for (const file of shippedFiles(".claude/commands", ".md")) {
|
|
814
|
+
if (libraryPersonas.has(file)) continue;
|
|
815
|
+
say(` .claude/commands/${file}`);
|
|
816
|
+
copyFile(join(sourceRoot, ".claude", "commands", file), join(target, ".claude", "commands", file));
|
|
817
|
+
}
|
|
818
|
+
// .claude/settings.json: refreshed to the canonical block unless --keep-permissions
|
|
819
|
+
const settingsPath = join(target, ".claude", "settings.json");
|
|
820
|
+
if (keepPermissions) {
|
|
821
|
+
if (existsSync(settingsPath)) say(" .claude/settings.json — preserved (--keep-permissions)");
|
|
822
|
+
} else if (existsSync(settingsPath) && read(settingsPath).replace(/\n+$/, "") === canonicalSettings) {
|
|
823
|
+
say(" .claude/settings.json — already current (yolo-except-git-writes)");
|
|
824
|
+
} else if (dryRun) {
|
|
825
|
+
say(" .claude/settings.json — WOULD refresh to yolo-except-git-writes (use --keep-permissions to opt out)");
|
|
826
|
+
} else {
|
|
827
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
828
|
+
if (existsSync(settingsPath)) {
|
|
829
|
+
copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
830
|
+
say(" .claude/settings.json — old version backed up to settings.json.bak");
|
|
831
|
+
}
|
|
832
|
+
writeFileSync(settingsPath, `${canonicalSettings}\n`);
|
|
833
|
+
say(" .claude/settings.json — refreshed to yolo-except-git-writes");
|
|
834
|
+
}
|
|
835
|
+
for (const file of libraryPersonas) if (existsSync(join(target, ".claude", file))) say(` .claude/${file} — preserved (NuGet-deployed)`);
|
|
836
|
+
|
|
837
|
+
// 3. OpenCode short-form commands, bridge plugin, framework config copy, root config audit
|
|
838
|
+
if (!dryRun) mkdirSync(join(target, ".opencode", "command"), { recursive: true });
|
|
839
|
+
for (const file of shippedFiles(".opencode/command", ".md")) {
|
|
840
|
+
if (libraryPersonas.has(file)) continue;
|
|
841
|
+
say(` .opencode/command/${file}`);
|
|
842
|
+
if (dryRun && !existsSync(join(target, ".opencode", "command"))) continue;
|
|
843
|
+
copyFile(join(sourceRoot, ".opencode", "command", file), join(target, ".opencode", "command", file));
|
|
844
|
+
}
|
|
845
|
+
for (const plugin of shippedFiles(".opencode/plugin", ".js")) say(` .opencode/plugin/${plugin} — framework-owned, refreshed`);
|
|
846
|
+
say(" .opencode/opencode.jsonc — framework-owned, refreshed (wins over root opencode.jsonc on conflicting keys)");
|
|
847
|
+
deployOpenCodeBridge();
|
|
848
|
+
refreshRootOpenCodeConfig();
|
|
849
|
+
|
|
850
|
+
// 4b. Codex adapter
|
|
851
|
+
deployCodexAdapter();
|
|
852
|
+
if (!dryRun) say(" Codex hooks changed or installed — trust this repository and review /hooks");
|
|
853
|
+
for (const file of libraryPersonas) if (existsSync(join(target, ".opencode", "command", file))) say(` .opencode/command/${file} — preserved (NuGet-deployed)`);
|
|
854
|
+
|
|
855
|
+
// 4. WORKFLOW.html, always refreshed
|
|
856
|
+
if (existsSync(join(sourceRoot, "WORKFLOW.html"))) {
|
|
857
|
+
copyFile(join(sourceRoot, "WORKFLOW.html"), join(target, "WORKFLOW.html"));
|
|
858
|
+
say(" WORKFLOW.html");
|
|
859
|
+
}
|
|
860
|
+
// 5. NuGet persona shims: gap-fill only, never an overwrite
|
|
861
|
+
shimLegacyPersonas({ gapOnly: true });
|
|
862
|
+
// 6, 7. messages about stale references in files the owner owns
|
|
863
|
+
reportStaleReferences();
|
|
864
|
+
// 8. .gitignore, build-output audit, telemetry, .gitattributes
|
|
865
|
+
deployHousekeeping();
|
|
866
|
+
|
|
867
|
+
say("");
|
|
868
|
+
if (dryRun) {
|
|
869
|
+
say("✔ Dry run complete — no files changed.");
|
|
870
|
+
say(" Re-run without --dry-run to apply.");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
say("✔ Framework updated. Your docs/, src/, tests/, and per-project state files were NOT touched.");
|
|
874
|
+
say("");
|
|
875
|
+
say("Reminder:");
|
|
876
|
+
say(" - In Claude Code, RESTART the session so the new task/agent definitions are loaded.");
|
|
877
|
+
say(" - If a task file references a new template under .tfcore/templates/v4custom/,");
|
|
878
|
+
say(" that template is now in place locally.");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ---------------------------------------------------------------- uninstall
|
|
882
|
+
|
|
883
|
+
function uninstall() {
|
|
884
|
+
ensureSafeTarget();
|
|
885
|
+
if (!existsSync(join(target, ".tfcore"))) throw new Error(`${target} has no .tfcore/ folder; nothing to uninstall.`);
|
|
886
|
+
const removals = [];
|
|
887
|
+
const kept = [];
|
|
888
|
+
const plan = (path, why = "") => { if (existsSync(join(target, path)) || lstatSync(join(target, path), { throwIfNoEntry: false })) removals.push([path, why]); };
|
|
889
|
+
const keep = (path, why) => { if (existsSync(join(target, path))) kept.push([path, why]); };
|
|
890
|
+
const sameAsShipped = (path, shipped) => existsSync(join(target, path)) && readFileSync(join(target, path)).equals(readFileSync(join(sourceRoot, shipped)));
|
|
891
|
+
|
|
892
|
+
plan(".tfcore", "(holds .tfcore/core-config.yaml and .tfcore/routing.yaml, your per-project settings — copy them out first if you want them)");
|
|
893
|
+
plan(".claude/commands/TechieFlow");
|
|
894
|
+
for (const file of shippedFiles(".claude/commands", ".md")) if (!libraryPersonas.has(file)) plan(`.claude/commands/${file}`);
|
|
895
|
+
if (existsSync(join(target, ".claude", "settings.json"))) {
|
|
896
|
+
if (read(join(target, ".claude", "settings.json")).replace(/\n+$/, "") === canonicalSettings) plan(".claude/settings.json");
|
|
897
|
+
else keep(".claude/settings.json", "you changed it");
|
|
898
|
+
}
|
|
899
|
+
if (existsSync(join(target, ".claude", "commands", "tf"))) plan(".claude/commands/tf", "(generated by routing)");
|
|
900
|
+
const agentsDir = join(target, ".claude", "agents");
|
|
901
|
+
if (existsSync(agentsDir)) for (const f of readdirSync(agentsDir)) if (f.endsWith(".md") && read(join(agentsDir, f)).includes("generated by tf-routing-bind.sh")) plan(`.claude/agents/${f}`, "(generated by routing)");
|
|
902
|
+
for (const plugin of shippedFiles(".opencode/plugin", ".js")) plan(`.opencode/plugin/${plugin}`);
|
|
903
|
+
plan(".opencode/opencode.jsonc");
|
|
904
|
+
plan(".opencode/opencode.json", "(generated by routing)");
|
|
905
|
+
for (const file of shippedFiles(".opencode/command", ".md")) if (!libraryPersonas.has(file)) plan(`.opencode/command/${file}`);
|
|
906
|
+
plan(".codex/hooks.json");
|
|
907
|
+
plan(".codex/rules/techieflow.rules");
|
|
908
|
+
if (existsSync(join(target, ".codex", "config.toml"))) {
|
|
909
|
+
if (sameAsShipped(".codex/config.toml", ".codex/config.toml")) plan(".codex/config.toml");
|
|
910
|
+
else keep(".codex/config.toml", "you changed it");
|
|
911
|
+
}
|
|
912
|
+
const codexAgents = join(target, ".codex", "agents");
|
|
913
|
+
if (existsSync(codexAgents)) for (const f of readdirSync(codexAgents)) {
|
|
914
|
+
if (f.endsWith(".toml") && /description = "TechieFlow [a-z-]+ (specialist|role)\."/.test(read(join(codexAgents, f)))) plan(`.codex/agents/${f}`);
|
|
915
|
+
else keep(`.codex/agents/${f}`, "not written by the framework");
|
|
916
|
+
}
|
|
917
|
+
const skills = join(target, ".agents", "skills");
|
|
918
|
+
if (existsSync(skills)) for (const f of readdirSync(skills)) if (f.startsWith("techieflow-")) plan(`.agents/skills/${f}`);
|
|
919
|
+
for (const lib of [".trblazeui", ".techierag"]) {
|
|
920
|
+
if (!existsSync(join(target, lib))) continue;
|
|
921
|
+
const rest = readdirSync(join(target, lib)).filter((f) => f !== ".codex-agent-package-owned" && f !== ".gitignore");
|
|
922
|
+
plan(`${lib}/.codex-agent-package-owned`);
|
|
923
|
+
if (rest.length === 0) plan(`${lib}/.gitignore`);
|
|
924
|
+
}
|
|
925
|
+
for (const [path, shipped] of [["WORKFLOW.html", "WORKFLOW.html"], ["opencode.jsonc", "opencode.jsonc"]]) {
|
|
926
|
+
if (!existsSync(join(target, path))) continue;
|
|
927
|
+
if (sameAsShipped(path, shipped)) plan(path);
|
|
928
|
+
else keep(path, "you changed it");
|
|
929
|
+
}
|
|
930
|
+
plan(".tf-scaffold-note.txt");
|
|
931
|
+
const hook = join(target, ".git", "hooks", "pre-commit");
|
|
932
|
+
if (existsSync(hook) && read(hook).includes("TechieFlow telemetry")) plan(".git/hooks/pre-commit", "(telemetry hook)");
|
|
933
|
+
keep("docs/metrics", "your project's telemetry history");
|
|
934
|
+
for (const lib of libraryPersonas) {
|
|
935
|
+
keep(`.claude/commands/${lib}`, "NuGet-deployed");
|
|
936
|
+
keep(`.claude/${lib}`, "NuGet-deployed");
|
|
937
|
+
keep(`.opencode/command/${lib}`, "NuGet-deployed");
|
|
938
|
+
}
|
|
939
|
+
keep(".claude/settings.local.json", "per-machine");
|
|
940
|
+
keep(".gitattributes", "line-ending policy, kept");
|
|
941
|
+
|
|
942
|
+
say(`→ ${force ? (dryRun ? "DRY RUN — " : "") : "PREVIEW — "}Uninstalling TechieFlow from: ${target}`);
|
|
943
|
+
for (const [path, why] of removals) say(` ${force && !dryRun ? "remove" : "would remove"} ${path}${why ? ` ${why}` : ""}`);
|
|
944
|
+
for (const [path, why] of kept) say(` keep ${path} — ${why}`);
|
|
945
|
+
if (manageGitignore) say(` ${force && !dryRun ? "remove" : "would remove"} the framework block from .gitignore (the agent-artifact and build-output blocks stay: they describe your own build output)`);
|
|
946
|
+
if (!force || dryRun) {
|
|
947
|
+
say("");
|
|
948
|
+
say("No files were removed. Run again with --force to remove them.");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
for (const [path] of removals) rmSync(join(target, path), { recursive: true, force: true });
|
|
952
|
+
if (manageGitignore) removeFrameworkIgnoreBlock();
|
|
953
|
+
removeEmptyFolders([".claude/commands", ".claude/agents", ".claude", ".opencode/plugin", ".opencode/command", ".opencode", ".codex/agents", ".codex/rules", ".codex", ".agents/skills", ".agents", ".trblazeui", ".techierag"]);
|
|
954
|
+
say("");
|
|
955
|
+
say("✔ TechieFlow removed. Your docs/, src/, tests/ and docs/metrics/ were not touched.");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function removeFrameworkIgnoreBlock() {
|
|
959
|
+
const path = join(target, ".gitignore");
|
|
960
|
+
if (!existsSync(path)) return;
|
|
961
|
+
const original = read(path);
|
|
962
|
+
const lines = original.split("\n");
|
|
963
|
+
const headerIndex = lines.findIndex((l) => l.replace(/\r$/, "") === frameworkIgnore.header[0]);
|
|
964
|
+
if (headerIndex === -1) return;
|
|
965
|
+
const managed = new Set(frameworkIgnore.lines);
|
|
966
|
+
let end = headerIndex + 1;
|
|
967
|
+
while (end < lines.length && managed.has(lines[end].replace(/\r$/, ""))) end++;
|
|
968
|
+
let start = headerIndex;
|
|
969
|
+
if (start > 0 && lines[start - 1].replace(/\r$/, "") === "") start--;
|
|
970
|
+
lines.splice(start, end - start);
|
|
971
|
+
writeFileSync(path, lines.join("\n"));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ---------------------------------------------------------------- main
|
|
975
|
+
|
|
976
|
+
async function main() {
|
|
977
|
+
const unknown = flags.filter((f) => !knownFlags.includes(f) && !f.startsWith("--target="));
|
|
978
|
+
if (unknown.length) throw new Error(`Unknown flag: ${unknown.join(" ")}. Run with --help to see the flags.`);
|
|
979
|
+
if (wantHelp) { usage(); return; }
|
|
980
|
+
if (!["install", "update", "uninstall"].includes(command)) throw new Error(`Unknown command: ${command}. Use install, update or uninstall.`);
|
|
981
|
+
if (greenfield && command !== "install") throw new Error("--greenfield only applies to install.");
|
|
982
|
+
if (keepPermissions && command !== "update") throw new Error("--keep-permissions only applies to update.");
|
|
983
|
+
checkTools();
|
|
984
|
+
if (command === "install") await install();
|
|
985
|
+
else if (command === "update") update();
|
|
986
|
+
else uninstall();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
main().catch((error) => {
|
|
990
|
+
console.error(`${command} failed: ${error.message}`);
|
|
991
|
+
process.exit(1);
|
|
992
|
+
});
|