@tekyzinc/gsd-t 5.8.10 → 5.10.10

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.
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-install-check.cjs
4
+ *
5
+ * M108 — Install self-check and repair.
6
+ *
7
+ * [RULE] install-check-repairs-then-continues-never-routes-around
8
+ * [RULE] install-check-records-every-repair
9
+ * [RULE] install-check-halts-when-it-cannot-repair
10
+ *
11
+ * A project's `bin/` is supposed to hold every tool the installer ships. When
12
+ * one is missing, the tools that need it break — and until now nothing noticed.
13
+ * Binvoice ran for weeks with 21 of 38 tools.
14
+ *
15
+ * This is NOT a fallback. A fallback continues PAST a failure with a worse
16
+ * answer. This FIXES the failure — it copies the missing file from the
17
+ * installed package — and then the work continues with everything present. If
18
+ * it cannot fix it, it HALTS and says so; it never lets the work proceed on a
19
+ * broken install.
20
+ *
21
+ * Every repair is written to a shared log so a pattern across projects becomes
22
+ * visible: if the same tool keeps going missing everywhere, the installer
23
+ * itself is what needs fixing.
24
+ *
25
+ * ─── Usage ──────────────────────────────────────────────────────────────────
26
+ * node gsd-t-install-check.cjs --project <dir> # check + repair
27
+ * node gsd-t-install-check.cjs --project <dir> --check # report only
28
+ * node gsd-t-install-check.cjs --report # read the shared log
29
+ *
30
+ * ─── Exit codes ─────────────────────────────────────────────────────────────
31
+ * 0 install is complete (possibly after a repair)
32
+ * 4 something is missing and could not be repaired — HALT
33
+ * 64 bad input
34
+ *
35
+ * Zero dependencies.
36
+ */
37
+
38
+ "use strict";
39
+
40
+ const fs = require("fs");
41
+ const os = require("os");
42
+ const path = require("path");
43
+ const { execFileSync } = require("child_process");
44
+
45
+ const EXIT_OK = 0;
46
+ const EXIT_UNREPAIRABLE = 4;
47
+ const EXIT_BAD_INPUT = 64;
48
+
49
+ const LOG_PATH = path.join(os.homedir(), ".claude", "gsd-t-install-repairs.jsonl");
50
+
51
+ /**
52
+ * Where the installed package lives — the source every repair copies from.
53
+ *
54
+ * Two places, asked in order, because they answer DIFFERENT questions: "am I
55
+ * running from inside the package?" and "where does npm keep global installs?".
56
+ * That is not hunting for a file; it is two distinct facts about this machine.
57
+ *
58
+ * Throws with what went wrong. It never returns nothing as if that were an
59
+ * ordinary answer — without the package there is nothing to repair from, and
60
+ * the caller must stop.
61
+ */
62
+ function findPackageRoot() {
63
+ const problems = [];
64
+
65
+ // 1. Running from inside the package itself.
66
+ const here = path.resolve(__dirname, "..");
67
+ const herePkg = path.join(here, "package.json");
68
+ if (fs.existsSync(herePkg)) {
69
+ let pkg;
70
+ try {
71
+ pkg = JSON.parse(fs.readFileSync(herePkg, "utf8"));
72
+ } catch (e) {
73
+ problems.push(`${herePkg} could not be read: ${e.message}`);
74
+ }
75
+ if (pkg && pkg.name === "@tekyzinc/gsd-t") return here;
76
+ }
77
+
78
+ // 2. Ask npm where global installs live.
79
+ let root;
80
+ try {
81
+ root = execFileSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 10000 }).trim();
82
+ } catch (e) {
83
+ problems.push(`npm could not be asked where global packages live: ${e.message}`);
84
+ }
85
+ if (root) {
86
+ const p = path.join(root, "@tekyzinc", "gsd-t");
87
+ if (fs.existsSync(path.join(p, "package.json"))) return p;
88
+ problems.push(`GSD-T is not installed at ${p}`);
89
+ }
90
+
91
+ throw new Error(
92
+ "The installed GSD-T package could not be located, so nothing can be checked " +
93
+ "or repaired.\n " + problems.join("\n ")
94
+ );
95
+ }
96
+
97
+ /** The list of tools every project is supposed to have. Read from the installer. */
98
+ function expectedTools(pkgRoot) {
99
+ const installer = path.join(pkgRoot, "bin", "gsd-t.js");
100
+ const src = fs.readFileSync(installer, "utf8");
101
+ const m = src.match(/const PROJECT_BIN_TOOLS = \[([^]*?)\n\];/);
102
+ if (!m) throw new Error(`could not read the tool list from ${installer}`);
103
+ return [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]);
104
+ }
105
+
106
+ function packageVersion(pkgRoot) {
107
+ try {
108
+ return JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")).version || "unknown";
109
+ } catch (_) {
110
+ return "unknown";
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Compare a project's bin/ against the expected list.
116
+ * Returns { missing, stale, present } — stale means the file exists but its
117
+ * content differs from the package's copy.
118
+ */
119
+ function inspect(projectDir, pkgRoot, tools) {
120
+ const binDir = path.join(projectDir, "bin");
121
+ const missing = [];
122
+ const stale = [];
123
+ let present = 0;
124
+
125
+ for (const tool of tools) {
126
+ const src = path.join(pkgRoot, "bin", tool);
127
+ const dest = path.join(binDir, tool);
128
+ if (!fs.existsSync(src)) {
129
+ // The package itself is missing a tool it promises. That is an installer
130
+ // defect, not a project one — recorded separately, never skipped quietly.
131
+ missing.push({ tool, reason: "absent from the installed package" });
132
+ continue;
133
+ }
134
+ if (!fs.existsSync(dest)) {
135
+ missing.push({ tool, reason: "absent from the project" });
136
+ continue;
137
+ }
138
+ try {
139
+ if (fs.readFileSync(src, "utf8") !== fs.readFileSync(dest, "utf8")) {
140
+ stale.push({ tool, reason: "differs from the installed package" });
141
+ } else {
142
+ present++;
143
+ }
144
+ } catch (e) {
145
+ missing.push({ tool, reason: `could not be compared: ${e.message}` });
146
+ }
147
+ }
148
+ return { missing, stale, present };
149
+ }
150
+
151
+ /** Copy one tool from the package into the project. Throws if it cannot. */
152
+ function repairOne(projectDir, pkgRoot, tool) {
153
+ const src = path.join(pkgRoot, "bin", tool);
154
+ const dest = path.join(projectDir, "bin", tool);
155
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
156
+ fs.copyFileSync(src, dest);
157
+ try { fs.chmodSync(dest, 0o755); } catch (_) { /* permissions are advisory here */ }
158
+ if (!fs.existsSync(dest)) throw new Error(`copy reported success but ${dest} is not there`);
159
+ }
160
+
161
+ /** Append a record of what was repaired, so a cross-project pattern is visible. */
162
+ function recordRepair(entry) {
163
+ try {
164
+ fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
165
+ fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
166
+ return true;
167
+ } catch (e) {
168
+ // The log could not be written. Say so out loud — a repair nobody can see
169
+ // is how this problem stayed invisible for weeks in the first place.
170
+ process.stderr.write(`[gsd-t] could not record the repair in ${LOG_PATH}: ${e.message}\n`);
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /** Read the shared log and summarise which tools keep going missing. */
176
+ function readLog() {
177
+ if (!fs.existsSync(LOG_PATH)) return { entries: [], byTool: {}, byProject: {} };
178
+ const entries = [];
179
+ for (const line of fs.readFileSync(LOG_PATH, "utf8").split("\n")) {
180
+ if (!line.trim()) continue;
181
+ try { entries.push(JSON.parse(line)); } catch (_) { /* one bad line does not hide the rest */ }
182
+ }
183
+ const byTool = {};
184
+ const byProject = {};
185
+ for (const e of entries) {
186
+ for (const t of e.repaired || []) byTool[t] = (byTool[t] || 0) + 1;
187
+ byProject[e.project] = (byProject[e.project] || 0) + 1;
188
+ }
189
+ return { entries, byTool, byProject };
190
+ }
191
+
192
+ /**
193
+ * Which tools have gone missing in MORE THAN ONE project? That points at the
194
+ * installer, not at any single project.
195
+ */
196
+ function installerSuspects(byTool, entries) {
197
+ const projectsPerTool = {};
198
+ for (const e of entries) {
199
+ for (const t of e.repaired || []) {
200
+ (projectsPerTool[t] = projectsPerTool[t] || new Set()).add(e.project);
201
+ }
202
+ }
203
+ return Object.entries(projectsPerTool)
204
+ .filter(([, set]) => set.size > 1)
205
+ .map(([tool, set]) => ({ tool, projects: [...set] }))
206
+ .sort((a, b) => b.projects.length - a.projects.length);
207
+ }
208
+
209
+ function parseArgs(argv) {
210
+ const args = {};
211
+ for (let i = 2; i < argv.length; i++) {
212
+ const a = argv[i];
213
+ if (a === "--check") args.checkOnly = true;
214
+ else if (a === "--report") args.report = true;
215
+ else if (a === "--json") args.json = true;
216
+ else if (a === "--project") args.project = argv[++i];
217
+ }
218
+ return args;
219
+ }
220
+
221
+ function main() {
222
+ const args = parseArgs(process.argv);
223
+
224
+ if (args.report) {
225
+ const { entries, byTool, byProject } = readLog();
226
+ const suspects = installerSuspects(byTool, entries);
227
+ const out = { ok: true, exitCode: EXIT_OK, repairs: entries.length, byTool, byProject, installerSuspects: suspects };
228
+ if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
229
+ else if (!entries.length) process.stdout.write("No installs have needed repair.\n");
230
+ else {
231
+ process.stdout.write(`${entries.length} repair(s) recorded.\n\n`);
232
+ for (const [p, n] of Object.entries(byProject)) process.stdout.write(` ${p}: ${n}\n`);
233
+ if (suspects.length) {
234
+ process.stdout.write("\nThese tools went missing in more than one project — the installer is the likely cause:\n");
235
+ for (const s of suspects) process.stdout.write(` ${s.tool} (${s.projects.length} projects)\n`);
236
+ }
237
+ }
238
+ process.exit(EXIT_OK);
239
+ }
240
+
241
+ const projectDir = path.resolve(args.project || process.cwd());
242
+ if (!fs.existsSync(path.join(projectDir, ".gsd-t"))) {
243
+ // Not a GSD-T project. Nothing to check — this is "not applicable", which
244
+ // is a different thing from "checked and fine".
245
+ process.exit(EXIT_OK);
246
+ }
247
+
248
+ let pkgRoot;
249
+ try {
250
+ pkgRoot = findPackageRoot();
251
+ } catch (e) {
252
+ if (args.json) process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREPAIRABLE, halt: e.message }, null, 2) + "\n");
253
+ else process.stderr.write(`[gsd-t] ${e.message}\n`);
254
+ process.exit(EXIT_UNREPAIRABLE);
255
+ }
256
+
257
+ let tools;
258
+ try {
259
+ tools = expectedTools(pkgRoot);
260
+ } catch (e) {
261
+ const msg = `Cannot read the list of tools a project should have: ${e.message}`;
262
+ if (args.json) process.stdout.write(JSON.stringify({ ok: false, exitCode: EXIT_UNREPAIRABLE, halt: msg }, null, 2) + "\n");
263
+ else process.stderr.write(`[gsd-t] ${msg}\n`);
264
+ process.exit(EXIT_UNREPAIRABLE);
265
+ }
266
+
267
+ const found = inspect(projectDir, pkgRoot, tools);
268
+ const needsWork = [...found.missing, ...found.stale];
269
+
270
+ if (!needsWork.length) {
271
+ const out = { ok: true, exitCode: EXIT_OK, expected: tools.length, present: found.present, repaired: [] };
272
+ if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
273
+ process.exit(EXIT_OK);
274
+ }
275
+
276
+ if (args.checkOnly) {
277
+ const out = {
278
+ ok: false, exitCode: EXIT_UNREPAIRABLE, expected: tools.length,
279
+ present: found.present, missing: found.missing, stale: found.stale,
280
+ };
281
+ if (args.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
282
+ else process.stdout.write(`${needsWork.length} of ${tools.length} tools need attention in ${projectDir}\n`);
283
+ process.exit(EXIT_UNREPAIRABLE);
284
+ }
285
+
286
+ // Repair.
287
+ const repaired = [];
288
+ const unrepairable = [];
289
+ for (const item of needsWork) {
290
+ if (item.reason === "absent from the installed package") {
291
+ unrepairable.push(item);
292
+ continue;
293
+ }
294
+ try {
295
+ repairOne(projectDir, pkgRoot, item.tool);
296
+ repaired.push(item.tool);
297
+ } catch (e) {
298
+ unrepairable.push({ tool: item.tool, reason: e.message });
299
+ }
300
+ }
301
+
302
+ const projectName = path.basename(projectDir);
303
+ if (repaired.length) {
304
+ recordRepair({
305
+ at: new Date().toISOString(),
306
+ project: projectName,
307
+ projectDir,
308
+ packageVersion: packageVersion(pkgRoot),
309
+ expected: tools.length,
310
+ wasPresent: found.present,
311
+ repaired,
312
+ unrepairable: unrepairable.map((u) => u.tool),
313
+ });
314
+ }
315
+
316
+ const out = {
317
+ ok: unrepairable.length === 0,
318
+ exitCode: unrepairable.length === 0 ? EXIT_OK : EXIT_UNREPAIRABLE,
319
+ project: projectName,
320
+ expected: tools.length,
321
+ repaired,
322
+ unrepairable,
323
+ };
324
+
325
+ if (args.json) {
326
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
327
+ } else {
328
+ if (repaired.length) {
329
+ process.stdout.write(`[gsd-t] Repaired this project's install: restored ${repaired.length} missing tool(s).\n`);
330
+ }
331
+ if (unrepairable.length) {
332
+ process.stdout.write(`[gsd-t] ${unrepairable.length} tool(s) could NOT be restored:\n`);
333
+ for (const u of unrepairable) process.stdout.write(` ${u.tool} — ${u.reason}\n`);
334
+ process.stdout.write(`Work should not continue on a broken install. Reinstall GSD-T.\n`);
335
+ }
336
+ }
337
+ process.exit(out.exitCode);
338
+ }
339
+
340
+ if (require.main === module) main();
341
+
342
+ module.exports = { inspect, expectedTools, findPackageRoot, readLog, installerSuspects, LOG_PATH };