qualia-framework 4.5.0 → 5.3.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/AGENTS.md +24 -0
- package/CLAUDE.md +12 -75
- package/README.md +23 -16
- package/agents/builder.md +9 -21
- package/agents/planner.md +8 -0
- package/agents/verifier.md +8 -0
- package/agents/visual-evaluator.md +132 -0
- package/bin/cli.js +54 -18
- package/bin/install.js +369 -29
- package/bin/qualia-ui.js +208 -1
- package/bin/slop-detect.mjs +5 -0
- package/bin/state.js +34 -1
- package/docs/install-redesign-builder-prompt.md +290 -0
- package/docs/install-redesign-pilot.md +234 -0
- package/docs/playwright-loop-builder-prompt.md +185 -0
- package/docs/playwright-loop-design-notes.md +108 -0
- package/docs/playwright-loop-pilot-results.md +170 -0
- package/docs/playwright-loop-tester-prompt.md +213 -0
- package/docs/polish-loop-supervised-run.md +111 -0
- package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
- package/guide.md +9 -5
- package/hooks/env-empty-guard.js +74 -0
- package/hooks/pre-compact.js +19 -9
- package/hooks/pre-deploy-gate.js +8 -2
- package/hooks/pre-push.js +26 -12
- package/hooks/supabase-destructive-guard.js +62 -0
- package/hooks/vercel-account-guard.js +91 -0
- package/package.json +2 -1
- package/rules/design-brand.md +4 -0
- package/rules/design-laws.md +4 -0
- package/rules/design-product.md +4 -0
- package/rules/design-rubric.md +4 -0
- package/rules/grounding.md +4 -0
- package/skills/qualia-build/SKILL.md +40 -46
- package/skills/qualia-discuss/SKILL.md +51 -68
- package/skills/qualia-handoff/SKILL.md +1 -0
- package/skills/qualia-hook-gen/SKILL.md +206 -0
- package/skills/qualia-issues/SKILL.md +151 -0
- package/skills/qualia-map/SKILL.md +78 -35
- package/skills/qualia-new/REFERENCE.md +139 -0
- package/skills/qualia-new/SKILL.md +45 -121
- package/skills/qualia-optimize/REFERENCE.md +265 -0
- package/skills/qualia-optimize/SKILL.md +92 -232
- package/skills/qualia-plan/SKILL.md +58 -65
- package/skills/qualia-polish-loop/REFERENCE.md +265 -0
- package/skills/qualia-polish-loop/SKILL.md +201 -0
- package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
- package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
- package/skills/qualia-polish-loop/scripts/loop.mjs +323 -0
- package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +206 -0
- package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
- package/skills/qualia-prd/SKILL.md +199 -0
- package/skills/qualia-report/SKILL.md +141 -200
- package/skills/qualia-research/SKILL.md +28 -33
- package/skills/qualia-road/SKILL.md +103 -0
- package/skills/qualia-ship/SKILL.md +1 -0
- package/skills/qualia-task/SKILL.md +1 -1
- package/skills/qualia-test/SKILL.md +50 -2
- package/skills/qualia-triage/SKILL.md +152 -0
- package/skills/qualia-verify/SKILL.md +63 -104
- package/skills/qualia-zoom/SKILL.md +51 -0
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/templates/CONTEXT.md +36 -0
- package/templates/decisions/ADR-template.md +30 -0
- package/tests/bin.test.sh +598 -7
- package/tests/state.test.sh +58 -0
package/bin/install.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
const { createInterface } = require("readline");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const ui = require("./qualia-ui.js");
|
|
6
7
|
|
|
7
|
-
// ─── Colors
|
|
8
|
+
// ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
|
|
8
9
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
9
10
|
const DIM = "\x1b[38;2;80;90;100m";
|
|
11
|
+
const DIM2 = "\x1b[38;2;70;80;90m";
|
|
10
12
|
const GREEN = "\x1b[38;2;52;211;153m";
|
|
11
13
|
const WHITE = "\x1b[38;2;220;225;230m";
|
|
12
14
|
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
@@ -14,8 +16,17 @@ const RED = "\x1b[38;2;239;68;68m";
|
|
|
14
16
|
const RESET = "\x1b[0m";
|
|
15
17
|
|
|
16
18
|
const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
|
|
19
|
+
const CODEX_DIR = path.join(require("os").homedir(), ".codex");
|
|
17
20
|
const FRAMEWORK_DIR = path.resolve(__dirname, "..");
|
|
18
21
|
|
|
22
|
+
// Target IDs match the menu numbers shown to the user.
|
|
23
|
+
const TARGET_CLAUDE_ONLY = "1";
|
|
24
|
+
const TARGET_CODEX_ONLY = "2";
|
|
25
|
+
const TARGET_BOTH = "3";
|
|
26
|
+
|
|
27
|
+
// Total install timer — set in main(), read by the final summary card.
|
|
28
|
+
const installStart = Date.now();
|
|
29
|
+
|
|
19
30
|
// ─── Team codes ──────────────────────────────────────────
|
|
20
31
|
const DEFAULT_TEAM = {
|
|
21
32
|
"QS-FAWZI-01": {
|
|
@@ -64,16 +75,30 @@ const TEAM = loadTeam();
|
|
|
64
75
|
let installed = 0;
|
|
65
76
|
let errors = 0;
|
|
66
77
|
|
|
78
|
+
// Per-section timer state. Started by printSection(), read by closeSection().
|
|
79
|
+
let sectionStart = 0;
|
|
80
|
+
let sectionLabel = "";
|
|
81
|
+
let sectionCount = 0;
|
|
82
|
+
|
|
67
83
|
function log(msg) {
|
|
68
84
|
console.log(` ${msg}`);
|
|
69
85
|
}
|
|
70
86
|
function ok(label) {
|
|
71
87
|
installed++;
|
|
88
|
+
sectionCount++;
|
|
72
89
|
log(`${GREEN}✓${RESET} ${label}`);
|
|
73
90
|
}
|
|
74
91
|
function warn(label) {
|
|
75
92
|
errors++;
|
|
76
|
-
log(`${YELLOW}
|
|
93
|
+
log(`${YELLOW}!${RESET} ${label}`);
|
|
94
|
+
}
|
|
95
|
+
// step(text) → handle returned by qualia-ui. Use for slow ops where the
|
|
96
|
+
// "doing → done" lifecycle helps the user trust the install isn't hung.
|
|
97
|
+
function step(text) {
|
|
98
|
+
return ui.step(text);
|
|
99
|
+
}
|
|
100
|
+
function spin(text) {
|
|
101
|
+
return ui.spinner(text);
|
|
77
102
|
}
|
|
78
103
|
function copy(src, dest) {
|
|
79
104
|
const destDir = path.dirname(dest);
|
|
@@ -161,23 +186,135 @@ function printHeader() {
|
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
function printSection(title) {
|
|
189
|
+
// Close prior section if one was open (shows count + elapsed).
|
|
190
|
+
closeSection();
|
|
191
|
+
sectionStart = Date.now();
|
|
192
|
+
sectionLabel = title;
|
|
193
|
+
sectionCount = 0;
|
|
164
194
|
console.log("");
|
|
165
195
|
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}${title}${RESET}`);
|
|
166
|
-
console.log(` ${
|
|
196
|
+
console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function closeSection() {
|
|
200
|
+
if (!sectionLabel) return;
|
|
201
|
+
const elapsedMs = Date.now() - sectionStart;
|
|
202
|
+
const elapsed = elapsedMs >= 1000
|
|
203
|
+
? `${(elapsedMs / 1000).toFixed(1)}s`
|
|
204
|
+
: `${elapsedMs}ms`;
|
|
205
|
+
if (sectionCount > 0) {
|
|
206
|
+
console.log(` ${DIM2}└─${RESET} ${DIM}${sectionCount} ${sectionLabel.toLowerCase()} · ${elapsed}${RESET}`);
|
|
207
|
+
}
|
|
208
|
+
sectionLabel = "";
|
|
209
|
+
sectionCount = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Prompt helpers ──────────────────────────────────────
|
|
213
|
+
// Two prompts (team code, install target) need to coexist with two stdin
|
|
214
|
+
// modes: interactive TTY and piped. The earlier two-readline approach
|
|
215
|
+
// raced 'close' against the second question on piped installs (`echo CODE
|
|
216
|
+
// | install.js` would EOF before the second question could attach a line
|
|
217
|
+
// listener) — the target prompt always saw "" and defaulted to "1" even
|
|
218
|
+
// when the user piped "CODE\n2\n".
|
|
219
|
+
//
|
|
220
|
+
// Fix: in piped mode, pre-buffer all stdin lines synchronously up front and
|
|
221
|
+
// hand them out one by one. In TTY mode, use a shared readline.
|
|
222
|
+
let SHARED_RL = null;
|
|
223
|
+
let PIPED_LINES = null;
|
|
224
|
+
let PIPED_INDEX = 0;
|
|
225
|
+
const IS_INTERACTIVE = !!(process.stdin && process.stdin.isTTY);
|
|
226
|
+
|
|
227
|
+
function getRl() {
|
|
228
|
+
if (!SHARED_RL) {
|
|
229
|
+
SHARED_RL = createInterface({ input: process.stdin, output: process.stdout });
|
|
230
|
+
}
|
|
231
|
+
return SHARED_RL;
|
|
232
|
+
}
|
|
233
|
+
function closeRl() {
|
|
234
|
+
if (SHARED_RL) { try { SHARED_RL.close(); } catch {} SHARED_RL = null; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Read every available stdin line into an array. Resolves immediately on
|
|
238
|
+
// 'end'. Used only when stdin is piped (legacy `echo ... | install`).
|
|
239
|
+
function bufferStdin() {
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
let buf = "";
|
|
242
|
+
process.stdin.setEncoding("utf8");
|
|
243
|
+
process.stdin.on("data", (chunk) => { buf += chunk; });
|
|
244
|
+
process.stdin.on("end", () => {
|
|
245
|
+
const lines = buf.split(/\r?\n/);
|
|
246
|
+
// Trim a trailing empty entry from the final newline, but preserve
|
|
247
|
+
// intentional empties (so an empty target line still defaults to "1"
|
|
248
|
+
// rather than swallowing the team-code line).
|
|
249
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
250
|
+
resolve(lines);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function nextPipedLine() {
|
|
256
|
+
if (!PIPED_LINES) return "";
|
|
257
|
+
if (PIPED_INDEX >= PIPED_LINES.length) return "";
|
|
258
|
+
return PIPED_LINES[PIPED_INDEX++];
|
|
167
259
|
}
|
|
168
260
|
|
|
169
|
-
// ─── Prompt for code ─────────────────────────────────────
|
|
170
261
|
function askCode() {
|
|
171
262
|
return new Promise((resolve) => {
|
|
172
|
-
|
|
263
|
+
if (!IS_INTERACTIVE) {
|
|
264
|
+
printHeader();
|
|
265
|
+
const line = nextPipedLine();
|
|
266
|
+
// Echo the prompt + answer for log readability.
|
|
267
|
+
process.stdout.write(` ${WHITE}Enter install code:${RESET} ${line}\n`);
|
|
268
|
+
resolve(String(line || "").trim());
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const rl = getRl();
|
|
173
272
|
printHeader();
|
|
174
273
|
rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
|
|
175
|
-
|
|
176
|
-
resolve(answer.trim());
|
|
274
|
+
resolve(String(answer || "").trim());
|
|
177
275
|
});
|
|
178
276
|
});
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
// ─── Prompt for install target (Claude / Codex / Both) ──
|
|
280
|
+
// Backward-compat: a piped install with only the team code (single stdin
|
|
281
|
+
// line) closes stdin before this prompt; we silently default to "1"
|
|
282
|
+
// (Claude only) so existing scripts keep working untouched.
|
|
283
|
+
function askTarget() {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log(` ${WHITE}Where would you like to install Qualia?${RESET}`);
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(` ${TEAL}[1]${RESET} ${WHITE}Claude Code only${RESET} ${DIM}— recommended, full feature set${RESET}`);
|
|
289
|
+
console.log(` ${TEAL}[2]${RESET} ${WHITE}OpenAI Codex only${RESET} ${DIM}— AGENTS.md (Codex's open standard)${RESET}`);
|
|
290
|
+
console.log(` ${TEAL}[3]${RESET} ${WHITE}Both${RESET} ${DIM}— max compatibility${RESET}`);
|
|
291
|
+
console.log("");
|
|
292
|
+
|
|
293
|
+
const normalize = (val) => {
|
|
294
|
+
const trimmed = String(val || "").trim();
|
|
295
|
+
// Empty or unrecognized → default to Claude only (preserves legacy
|
|
296
|
+
// single-line piped install: `echo CODE | npx qualia-framework install`).
|
|
297
|
+
if (trimmed === TARGET_CODEX_ONLY || trimmed === TARGET_BOTH) return trimmed;
|
|
298
|
+
return TARGET_CLAUDE_ONLY;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (!IS_INTERACTIVE) {
|
|
302
|
+
const line = nextPipedLine();
|
|
303
|
+
process.stdout.write(` ${WHITE}Choice [1]:${RESET} ${line}\n`);
|
|
304
|
+
resolve(normalize(line));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const rl = getRl();
|
|
308
|
+
rl.question(` ${WHITE}Choice [1]:${RESET} `, (answer) => resolve(normalize(answer)));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function targetLabel(t) {
|
|
313
|
+
if (t === TARGET_CODEX_ONLY) return "Codex";
|
|
314
|
+
if (t === TARGET_BOTH) return "Claude Code · Codex";
|
|
315
|
+
return "Claude Code";
|
|
316
|
+
}
|
|
317
|
+
|
|
181
318
|
// ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
|
|
182
319
|
// Accepts "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
|
|
183
320
|
// numeric suffix), and returns the canonical key if found, else null.
|
|
@@ -196,6 +333,12 @@ function resolveTeamCode(input) {
|
|
|
196
333
|
|
|
197
334
|
// ─── Main ────────────────────────────────────────────────
|
|
198
335
|
async function main() {
|
|
336
|
+
// Piped install: drain stdin once up front. Avoids EOF/'close' racing
|
|
337
|
+
// ahead of the second prompt's 'line' listener (the bug v5.0 didn't have
|
|
338
|
+
// because v5.0 only had one prompt).
|
|
339
|
+
if (!IS_INTERACTIVE) {
|
|
340
|
+
PIPED_LINES = await bufferStdin();
|
|
341
|
+
}
|
|
199
342
|
const rawCode = await askCode();
|
|
200
343
|
const code = resolveTeamCode(rawCode);
|
|
201
344
|
const member = code ? TEAM[code] : null;
|
|
@@ -211,7 +354,23 @@ async function main() {
|
|
|
211
354
|
console.log("");
|
|
212
355
|
const roleColor = member.role === "OWNER" ? TEAL : GREEN;
|
|
213
356
|
console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
|
|
214
|
-
console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}
|
|
357
|
+
console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}`);
|
|
358
|
+
|
|
359
|
+
// ─── Ask install target (Claude / Codex / Both) ────────
|
|
360
|
+
const target = await askTarget();
|
|
361
|
+
closeRl();
|
|
362
|
+
const installClaude = target === TARGET_CLAUDE_ONLY || target === TARGET_BOTH;
|
|
363
|
+
const installCodexTarget = target === TARGET_CODEX_ONLY || target === TARGET_BOTH;
|
|
364
|
+
|
|
365
|
+
console.log("");
|
|
366
|
+
console.log(` ${DIM} Target:${RESET} ${WHITE}${targetLabel(target)}${RESET}`);
|
|
367
|
+
|
|
368
|
+
if (!installClaude) {
|
|
369
|
+
// Codex-only path: skip the entire Claude install block. Jump straight
|
|
370
|
+
// to the Codex installer + final summary.
|
|
371
|
+
await installCodex(member, target);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
215
374
|
|
|
216
375
|
// ─── Skills ──────────────────────────────────────────
|
|
217
376
|
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
@@ -226,6 +385,21 @@ async function main() {
|
|
|
226
385
|
path.join(skillsDir, skill, "SKILL.md"),
|
|
227
386
|
path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
|
|
228
387
|
);
|
|
388
|
+
// Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
|
|
389
|
+
const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
|
|
390
|
+
if (fs.existsSync(refSrc)) {
|
|
391
|
+
copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
|
|
392
|
+
}
|
|
393
|
+
// v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish-loop ships
|
|
394
|
+
// playwright-capture.mjs, loop.mjs, score.mjs that the skill invokes at
|
|
395
|
+
// runtime). Recursive — preserves nested files. fixtures/ also copied
|
|
396
|
+
// for self-test scenarios.
|
|
397
|
+
for (const sub of ["scripts", "fixtures"]) {
|
|
398
|
+
const subSrc = path.join(skillsDir, skill, sub);
|
|
399
|
+
if (fs.existsSync(subSrc) && fs.statSync(subSrc).isDirectory()) {
|
|
400
|
+
copyTree(subSrc, path.join(CLAUDE_DIR, "skills", skill, sub));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
229
403
|
ok(skill);
|
|
230
404
|
} catch (e) {
|
|
231
405
|
warn(`${skill} — ${e.message}`);
|
|
@@ -383,6 +557,17 @@ async function main() {
|
|
|
383
557
|
claudeMd = claudeMd.replace("{{ROLE}}", member.role);
|
|
384
558
|
claudeMd = claudeMd.replace("{{ROLE_DESCRIPTION}}", member.description);
|
|
385
559
|
const claudeDest = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
560
|
+
// v5.0: backup existing CLAUDE.md before overwrite. Users may have added
|
|
561
|
+
// personal instructions; without a backup, re-install silently destroys
|
|
562
|
+
// them. .bak files are harmless and easy to clean up.
|
|
563
|
+
if (fs.existsSync(claudeDest)) {
|
|
564
|
+
const existing = fs.readFileSync(claudeDest, "utf8");
|
|
565
|
+
if (existing !== claudeMd) {
|
|
566
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
567
|
+
const bak = `${claudeDest}.bak.${ts}`;
|
|
568
|
+
try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
386
571
|
fs.writeFileSync(claudeDest, claudeMd, "utf8");
|
|
387
572
|
ok(`Configured as ${member.role}`);
|
|
388
573
|
} catch (e) {
|
|
@@ -687,6 +872,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
687
872
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
688
873
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
689
874
|
"git-guardrails.js", "stop-session-log.js",
|
|
875
|
+
// v5.0 — insights-driven destructive-op + wrong-account guards
|
|
876
|
+
"vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
|
|
690
877
|
]);
|
|
691
878
|
const isQualiaHookCmd = (cmd) => {
|
|
692
879
|
if (typeof cmd !== "string") return false;
|
|
@@ -713,6 +900,10 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
713
900
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
714
901
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
715
902
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
903
|
+
// v5.0 hooks — insights-driven friction prevention
|
|
904
|
+
{ type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "⬢ Verifying Vercel account..." },
|
|
905
|
+
{ type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "⬢ Checking env value..." },
|
|
906
|
+
{ type: "command", if: "Bash(supabase*)|Bash(npx supabase*)", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5, statusMessage: "⬢ Checking Supabase safety..." },
|
|
716
907
|
],
|
|
717
908
|
},
|
|
718
909
|
{
|
|
@@ -773,56 +964,205 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
773
964
|
ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
|
|
774
965
|
}
|
|
775
966
|
|
|
776
|
-
|
|
967
|
+
// v5.0: backup existing settings.json before overwrite. The merge logic above
|
|
968
|
+
// preserves user fields, but a partial-write or merger bug could destroy MCP
|
|
969
|
+
// configs / custom permissions. Atomic write (tmp + rename) avoids partial
|
|
970
|
+
// writes; the .bak file is the recovery point if the merger ever misbehaves.
|
|
971
|
+
if (fs.existsSync(settingsPath)) {
|
|
972
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
973
|
+
const bak = `${settingsPath}.bak.${ts}`;
|
|
974
|
+
try { fs.copyFileSync(settingsPath, bak); } catch {}
|
|
975
|
+
}
|
|
976
|
+
const settingsTmp = `${settingsPath}.tmp.${process.pid}`;
|
|
977
|
+
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
978
|
+
fs.renameSync(settingsTmp, settingsPath);
|
|
777
979
|
|
|
778
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
|
|
980
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
779
981
|
ok("Status line + spinner configured");
|
|
780
982
|
ok("Environment variables + permissions");
|
|
781
983
|
|
|
984
|
+
// ─── Codex (optional second target) ──────────────────────
|
|
985
|
+
if (installCodexTarget) {
|
|
986
|
+
await installCodex(member, target);
|
|
987
|
+
}
|
|
988
|
+
|
|
782
989
|
// ─── Summary ───────────────────────────────────────────
|
|
990
|
+
closeSection();
|
|
991
|
+
printSummary({ member, target, claudeInstalled: true });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ─── Final summary card (shared by Claude / Codex / Both paths) ──
|
|
995
|
+
function printSummary({ member, target, claudeInstalled }) {
|
|
996
|
+
const roleColor = member.role === "OWNER" ? TEAL : GREEN;
|
|
997
|
+
const totalMs = Date.now() - installStart;
|
|
998
|
+
const totalSec = totalMs >= 1000
|
|
999
|
+
? `${(totalMs / 1000).toFixed(1)}s`
|
|
1000
|
+
: `${totalMs}ms`;
|
|
1001
|
+
|
|
783
1002
|
console.log("");
|
|
784
|
-
console.log(` ${
|
|
1003
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
785
1004
|
console.log(` ${TEAL}${BOLD}⬢ INSTALLED${RESET}`);
|
|
786
|
-
console.log(` ${
|
|
1005
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
787
1006
|
console.log("");
|
|
788
1007
|
console.log(` ${WHITE}${BOLD}${member.name}${RESET} ${DIM}·${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}v${PKG_VERSION}${RESET}`);
|
|
789
1008
|
console.log("");
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1009
|
+
console.log(` ${DIM}Targets${RESET} ${TEAL}${targetLabel(target)}${RESET}`);
|
|
1010
|
+
console.log(` ${DIM}Time${RESET} ${TEAL}${totalSec}${RESET}`);
|
|
1011
|
+
|
|
1012
|
+
if (claudeInstalled) {
|
|
1013
|
+
const agentsDir = path.join(FRAMEWORK_DIR, "agents");
|
|
1014
|
+
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
1015
|
+
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
1016
|
+
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
1017
|
+
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
1018
|
+
const skillCount = fs
|
|
1019
|
+
.readdirSync(skillsDir)
|
|
1020
|
+
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
|
|
1021
|
+
const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
|
|
1022
|
+
const hookCount = fs.readdirSync(hooksSource).length;
|
|
1023
|
+
const ruleCount = fs.readdirSync(rulesDir).length;
|
|
1024
|
+
const tmplCount = fs.readdirSync(tmplDir).length;
|
|
1025
|
+
const installedBinDir = path.join(CLAUDE_DIR, "bin");
|
|
1026
|
+
const scriptCount = fs.existsSync(installedBinDir)
|
|
1027
|
+
? fs.readdirSync(installedBinDir).filter((f) => f.endsWith(".js")).length
|
|
1028
|
+
: 0;
|
|
1029
|
+
console.log("");
|
|
1030
|
+
console.log(` ${DIM}Skills${RESET} ${TEAL}${skillCount}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
|
|
1031
|
+
console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
|
|
1032
|
+
}
|
|
800
1033
|
|
|
801
1034
|
if (errors > 0) {
|
|
802
1035
|
console.log("");
|
|
803
1036
|
console.log(` ${YELLOW}${errors} warning(s)${RESET} — check output above`);
|
|
804
1037
|
}
|
|
805
1038
|
|
|
1039
|
+
// Contextual first-command suggestion: if we're in a Qualia project,
|
|
1040
|
+
// recommend /qualia (router); otherwise /qualia-new (kickoff).
|
|
1041
|
+
const inProject = (() => {
|
|
1042
|
+
try { return fs.existsSync(path.join(process.cwd(), ".planning")); }
|
|
1043
|
+
catch { return false; }
|
|
1044
|
+
})();
|
|
1045
|
+
const firstCmd = inProject ? "/qualia" : "/qualia-new";
|
|
1046
|
+
const firstCmdHint = inProject ? "router — tells you the next command" : "kickoff a new project";
|
|
1047
|
+
|
|
806
1048
|
console.log("");
|
|
807
|
-
console.log(` ${
|
|
1049
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
808
1050
|
console.log(` ${WHITE}${BOLD}Quick Start${RESET}`);
|
|
809
|
-
console.log(` ${
|
|
1051
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
810
1052
|
console.log("");
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1053
|
+
if (claudeInstalled) {
|
|
1054
|
+
console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
|
|
1055
|
+
console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
|
|
1056
|
+
console.log(` ${TEAL}3.${RESET} ${WHITE}Try${RESET} ${TEAL}${BOLD}${firstCmd}${RESET} ${DIM}— ${firstCmdHint}${RESET}`);
|
|
1057
|
+
} else {
|
|
1058
|
+
// Codex-only path
|
|
1059
|
+
console.log(` ${TEAL}1.${RESET} ${WHITE}Open Codex in any project${RESET}`);
|
|
1060
|
+
console.log(` ${TEAL}2.${RESET} ${WHITE}Codex picks up${RESET} ${TEAL}~/.codex/AGENTS.md${RESET} ${DIM}automatically${RESET}`);
|
|
1061
|
+
console.log(` ${TEAL}3.${RESET} ${WHITE}Ask Codex${RESET} ${DIM}about Qualia rules — they're in AGENTS.md${RESET}`);
|
|
1062
|
+
}
|
|
814
1063
|
console.log("");
|
|
815
1064
|
console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
|
|
816
1065
|
console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
|
|
817
1066
|
console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(mandatory)${RESET}`);
|
|
818
1067
|
console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
|
|
819
1068
|
console.log("");
|
|
820
|
-
console.log(` ${
|
|
1069
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
821
1070
|
console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
|
|
822
|
-
console.log(` ${
|
|
1071
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
823
1072
|
console.log("");
|
|
824
1073
|
}
|
|
825
1074
|
|
|
1075
|
+
// ─── Codex install (writes AGENTS.md to ~/.codex/) ───────
|
|
1076
|
+
// Scope is intentionally minimal: AGENTS.md is the open standard adopted
|
|
1077
|
+
// by Codex / Cursor / Continue / Aider / Devin. Codex's runtime does not
|
|
1078
|
+
// today consume Claude-style skills/agents/hooks on disk in a way the
|
|
1079
|
+
// framework can map 1:1, so we write the convention file and document the
|
|
1080
|
+
// scope honestly. If Codex grows skill/hook support, we extend this here.
|
|
1081
|
+
async function installCodex(member, target) {
|
|
1082
|
+
console.log("");
|
|
1083
|
+
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
|
|
1084
|
+
console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
|
|
1085
|
+
|
|
1086
|
+
// Detect Codex CLI; soft-warn if missing (file write still proceeds).
|
|
1087
|
+
const { spawnSync } = require("child_process");
|
|
1088
|
+
let codexDetected = false;
|
|
1089
|
+
try {
|
|
1090
|
+
const r = spawnSync("codex", ["--version"], {
|
|
1091
|
+
encoding: "utf8",
|
|
1092
|
+
timeout: 3000,
|
|
1093
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1094
|
+
});
|
|
1095
|
+
codexDetected = r.status === 0;
|
|
1096
|
+
} catch { codexDetected = false; }
|
|
1097
|
+
|
|
1098
|
+
if (!codexDetected) {
|
|
1099
|
+
console.log(` ${YELLOW}!${RESET} ${WHITE}Codex CLI not detected on this system${RESET}`);
|
|
1100
|
+
console.log(` ${DIM} Installing AGENTS.md to ~/.codex/AGENTS.md anyway — Codex will pick it up${RESET}`);
|
|
1101
|
+
console.log(` ${DIM} when you install via:${RESET} ${TEAL}npm install -g @openai/codex${RESET}`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Make sure the dir exists.
|
|
1105
|
+
if (!fs.existsSync(CODEX_DIR)) {
|
|
1106
|
+
try {
|
|
1107
|
+
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
warn(`Codex — could not create ${CODEX_DIR}: ${e.message}`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Render AGENTS.md with role substitution. Source is the same AGENTS.md
|
|
1115
|
+
// already shipped in the framework root (it's the cross-vendor mirror of
|
|
1116
|
+
// CLAUDE.md, kept under 25 lines per Pocock's instruction-budget rule).
|
|
1117
|
+
let agentsContent;
|
|
1118
|
+
try {
|
|
1119
|
+
agentsContent = fs.readFileSync(path.join(FRAMEWORK_DIR, "AGENTS.md"), "utf8");
|
|
1120
|
+
agentsContent = agentsContent
|
|
1121
|
+
.replace("{{ROLE}}", member.role)
|
|
1122
|
+
.replace("{{ROLE_DESCRIPTION}}", member.description);
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
warn(`Codex — could not read framework AGENTS.md: ${e.message}`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const dest = path.join(CODEX_DIR, "AGENTS.md");
|
|
1129
|
+
|
|
1130
|
+
// Backup if existing differs (matches v5.0 CLAUDE.md / settings.json
|
|
1131
|
+
// discipline — never silently destroy a hand-edited file).
|
|
1132
|
+
if (fs.existsSync(dest)) {
|
|
1133
|
+
try {
|
|
1134
|
+
const existing = fs.readFileSync(dest, "utf8");
|
|
1135
|
+
if (existing !== agentsContent) {
|
|
1136
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1137
|
+
const bak = `${dest}.bak.${ts}`;
|
|
1138
|
+
try { fs.copyFileSync(dest, bak); ok(`Backed up existing AGENTS.md → ${path.basename(bak)}`); } catch {}
|
|
1139
|
+
}
|
|
1140
|
+
} catch {}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Atomic write: tmp + rename. Same pattern as settings.json above.
|
|
1144
|
+
try {
|
|
1145
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
1146
|
+
fs.writeFileSync(tmp, agentsContent, "utf8");
|
|
1147
|
+
fs.renameSync(tmp, dest);
|
|
1148
|
+
sectionCount++;
|
|
1149
|
+
ok(`AGENTS.md (configured as ${member.role})`);
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
warn(`Codex AGENTS.md — ${e.message}`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Honest scope note.
|
|
1156
|
+
console.log(` ${DIM}└─${RESET} ${DIM}Codex install scope: AGENTS.md only — Codex's runtime does not currently${RESET}`);
|
|
1157
|
+
console.log(` ${DIM}consume the framework's skills/hooks/agents on disk. AGENTS.md carries${RESET}`);
|
|
1158
|
+
console.log(` ${DIM}the rules; commands route through Claude Code.${RESET}`);
|
|
1159
|
+
|
|
1160
|
+
// Codex-only path: still need to write the role config and print summary.
|
|
1161
|
+
if (target === TARGET_CODEX_ONLY) {
|
|
1162
|
+
printSummary({ member, target, claudeInstalled: false });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
826
1166
|
main().catch((e) => {
|
|
827
1167
|
console.error(`${RED} ✗ Installation failed: ${e.message}${RESET}`);
|
|
828
1168
|
process.exit(1);
|