qualia-framework 2.6.0 → 3.1.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.md +63 -0
- package/README.md +108 -30
- package/agents/builder.md +110 -0
- package/agents/planner.md +186 -0
- package/agents/qa-browser.md +186 -0
- package/agents/verifier.md +369 -0
- package/bin/cli.js +691 -492
- package/bin/install.js +622 -0
- package/bin/qualia-ui.js +284 -0
- package/bin/state.js +824 -0
- package/bin/statusline.js +252 -0
- package/docs/erp-contract.md +161 -0
- package/guide.md +63 -0
- package/hooks/auto-update.js +117 -0
- package/hooks/block-env-edit.js +52 -0
- package/hooks/branch-guard.js +68 -0
- package/hooks/migration-guard.js +83 -0
- package/hooks/pre-compact.js +52 -0
- package/hooks/pre-deploy-gate.js +149 -0
- package/hooks/pre-push.js +53 -0
- package/hooks/session-start.js +126 -0
- package/package.json +30 -20
- package/rules/design-reference.md +179 -0
- package/rules/frontend.md +126 -0
- package/rules/infrastructure.md +87 -0
- package/skills/qualia/SKILL.md +88 -0
- package/skills/qualia-build/SKILL.md +115 -0
- package/skills/qualia-debug/SKILL.md +87 -0
- package/skills/qualia-design/SKILL.md +99 -0
- package/skills/qualia-handoff/SKILL.md +66 -0
- package/skills/qualia-help/SKILL.md +60 -0
- package/skills/qualia-idk/SKILL.md +8 -0
- package/skills/qualia-learn/SKILL.md +111 -0
- package/skills/qualia-new/SKILL.md +323 -0
- package/skills/qualia-pause/SKILL.md +63 -0
- package/skills/qualia-plan/SKILL.md +101 -0
- package/skills/qualia-polish/SKILL.md +207 -0
- package/skills/qualia-quick/SKILL.md +37 -0
- package/skills/qualia-report/SKILL.md +114 -0
- package/skills/qualia-resume/SKILL.md +49 -0
- package/skills/qualia-review/SKILL.md +161 -0
- package/skills/qualia-ship/SKILL.md +90 -0
- package/skills/qualia-skill-new/SKILL.md +167 -0
- package/skills/qualia-task/SKILL.md +91 -0
- package/skills/qualia-test/SKILL.md +134 -0
- package/skills/qualia-verify/SKILL.md +113 -0
- package/templates/DESIGN.md +475 -0
- package/templates/help.html +476 -0
- package/templates/plan.md +42 -0
- package/templates/project.md +22 -0
- package/templates/state.md +27 -0
- package/templates/tracking.json +20 -0
- package/tests/bin.test.sh +687 -0
- package/tests/hooks.test.sh +384 -0
- package/tests/runner.js +1956 -0
- package/tests/state.test.sh +713 -0
- package/tests/statusline.test.sh +243 -0
- package/bin/collect-metrics.sh +0 -62
- package/framework/.claudeignore +0 -51
- package/framework/CLAUDE.md +0 -51
- package/framework/MCP_SETUP.md +0 -229
- package/framework/agents/architecture-strategist.md +0 -53
- package/framework/agents/backend-agent.md +0 -150
- package/framework/agents/code-simplicity-reviewer.md +0 -86
- package/framework/agents/frontend-agent.md +0 -111
- package/framework/agents/kieran-typescript-reviewer.md +0 -96
- package/framework/agents/performance-oracle.md +0 -111
- package/framework/agents/qualia-codebase-mapper.md +0 -761
- package/framework/agents/qualia-debugger.md +0 -1204
- package/framework/agents/qualia-executor.md +0 -882
- package/framework/agents/qualia-integration-checker.md +0 -424
- package/framework/agents/qualia-phase-researcher.md +0 -457
- package/framework/agents/qualia-plan-checker.md +0 -700
- package/framework/agents/qualia-planner.md +0 -1245
- package/framework/agents/qualia-project-researcher.md +0 -603
- package/framework/agents/qualia-research-synthesizer.md +0 -200
- package/framework/agents/qualia-roadmapper.md +0 -606
- package/framework/agents/qualia-verifier.md +0 -686
- package/framework/agents/red-team-qa.md +0 -130
- package/framework/agents/security-auditor.md +0 -72
- package/framework/agents/team-orchestrator.md +0 -229
- package/framework/agents/teams/framework-audit-team.md +0 -66
- package/framework/agents/teams/full-stack-team.md +0 -48
- package/framework/agents/teams/optimize-team.md +0 -53
- package/framework/agents/teams/review-team.md +0 -70
- package/framework/agents/teams/ship-team.md +0 -86
- package/framework/agents/test-agent.md +0 -182
- package/framework/hooks/auto-format.sh +0 -54
- package/framework/hooks/block-env-edit.sh +0 -42
- package/framework/hooks/branch-guard.sh +0 -43
- package/framework/hooks/confirm-delete.sh +0 -59
- package/framework/hooks/migration-validate.sh +0 -77
- package/framework/hooks/notification-speak.sh +0 -16
- package/framework/hooks/pre-commit.sh +0 -100
- package/framework/hooks/pre-compact.sh +0 -56
- package/framework/hooks/pre-deploy-gate.sh +0 -160
- package/framework/hooks/qualia-colors.sh +0 -32
- package/framework/hooks/retention-cleanup.sh +0 -62
- package/framework/hooks/save-session-state.sh +0 -185
- package/framework/hooks/session-context-loader.sh +0 -96
- package/framework/hooks/session-learn.sh +0 -32
- package/framework/hooks/skill-announce.sh +0 -123
- package/framework/hooks/tool-error-announce.sh +0 -27
- package/framework/install.ps1 +0 -323
- package/framework/install.sh +0 -313
- package/framework/qualia-framework/VERSION +0 -1
- package/framework/qualia-framework/assets/qualia-logo.png +0 -0
- package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
- package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
- package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
- package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
- package/framework/qualia-framework/references/checkpoints.md +0 -775
- package/framework/qualia-framework/references/completion-checklists.md +0 -359
- package/framework/qualia-framework/references/continuation-format.md +0 -249
- package/framework/qualia-framework/references/continuation-prompt.md +0 -97
- package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
- package/framework/qualia-framework/references/design-quality.md +0 -56
- package/framework/qualia-framework/references/employee-guide.md +0 -167
- package/framework/qualia-framework/references/git-integration.md +0 -254
- package/framework/qualia-framework/references/git-planning-commit.md +0 -50
- package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
- package/framework/qualia-framework/references/model-profiles.md +0 -73
- package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
- package/framework/qualia-framework/references/planning-config.md +0 -195
- package/framework/qualia-framework/references/questioning.md +0 -141
- package/framework/qualia-framework/references/tdd.md +0 -263
- package/framework/qualia-framework/references/ui-brand.md +0 -160
- package/framework/qualia-framework/references/verification-patterns.md +0 -612
- package/framework/qualia-framework/templates/DEBUG.md +0 -159
- package/framework/qualia-framework/templates/DESIGN.md +0 -81
- package/framework/qualia-framework/templates/UAT.md +0 -247
- package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
- package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
- package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
- package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
- package/framework/qualia-framework/templates/codebase/stack.md +0 -186
- package/framework/qualia-framework/templates/codebase/structure.md +0 -285
- package/framework/qualia-framework/templates/codebase/testing.md +0 -480
- package/framework/qualia-framework/templates/config.json +0 -35
- package/framework/qualia-framework/templates/context.md +0 -283
- package/framework/qualia-framework/templates/continue-here.md +0 -78
- package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
- package/framework/qualia-framework/templates/discovery.md +0 -146
- package/framework/qualia-framework/templates/lab-notes.md +0 -16
- package/framework/qualia-framework/templates/milestone-archive.md +0 -123
- package/framework/qualia-framework/templates/milestone.md +0 -115
- package/framework/qualia-framework/templates/phase-prompt.md +0 -567
- package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
- package/framework/qualia-framework/templates/project.md +0 -184
- package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
- package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
- package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
- package/framework/qualia-framework/templates/projects/website.md +0 -137
- package/framework/qualia-framework/templates/requirements.md +0 -231
- package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
- package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
- package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
- package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
- package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
- package/framework/qualia-framework/templates/research.md +0 -552
- package/framework/qualia-framework/templates/roadmap.md +0 -206
- package/framework/qualia-framework/templates/state.md +0 -179
- package/framework/qualia-framework/templates/summary-complex.md +0 -59
- package/framework/qualia-framework/templates/summary-minimal.md +0 -41
- package/framework/qualia-framework/templates/summary-standard.md +0 -48
- package/framework/qualia-framework/templates/summary.md +0 -246
- package/framework/qualia-framework/templates/user-setup.md +0 -311
- package/framework/qualia-framework/templates/verification-report.md +0 -322
- package/framework/qualia-framework/workflows/add-phase.md +0 -179
- package/framework/qualia-framework/workflows/add-todo.md +0 -157
- package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
- package/framework/qualia-framework/workflows/check-todos.md +0 -176
- package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
- package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
- package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
- package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
- package/framework/qualia-framework/workflows/execute-phase.md +0 -559
- package/framework/qualia-framework/workflows/execute-plan.md +0 -438
- package/framework/qualia-framework/workflows/help.md +0 -470
- package/framework/qualia-framework/workflows/insert-phase.md +0 -220
- package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
- package/framework/qualia-framework/workflows/map-codebase.md +0 -327
- package/framework/qualia-framework/workflows/new-milestone.md +0 -363
- package/framework/qualia-framework/workflows/new-project.md +0 -982
- package/framework/qualia-framework/workflows/pause-work.md +0 -122
- package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
- package/framework/qualia-framework/workflows/plan-phase.md +0 -422
- package/framework/qualia-framework/workflows/progress.md +0 -389
- package/framework/qualia-framework/workflows/quick.md +0 -252
- package/framework/qualia-framework/workflows/remove-phase.md +0 -326
- package/framework/qualia-framework/workflows/research-phase.md +0 -74
- package/framework/qualia-framework/workflows/resume-project.md +0 -306
- package/framework/qualia-framework/workflows/set-profile.md +0 -80
- package/framework/qualia-framework/workflows/settings.md +0 -145
- package/framework/qualia-framework/workflows/transition.md +0 -556
- package/framework/qualia-framework/workflows/update.md +0 -197
- package/framework/qualia-framework/workflows/verify-phase.md +0 -195
- package/framework/qualia-framework/workflows/verify-work.md +0 -625
- package/framework/rules/context7.md +0 -14
- package/framework/rules/frontend.md +0 -33
- package/framework/rules/speed.md +0 -23
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +0 -120
- package/framework/scripts/bootstrap-pop-os.sh +0 -354
- package/framework/scripts/claude-voice +0 -13
- package/framework/scripts/cleanup.sh +0 -131
- package/framework/scripts/cowork-mode.sh +0 -141
- package/framework/scripts/generate-project-claude-md.sh +0 -153
- package/framework/scripts/load-test-webhook.js +0 -172
- package/framework/scripts/say.py +0 -236
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
- package/framework/scripts/speak.py +0 -55
- package/framework/scripts/speak.sh +0 -18
- package/framework/scripts/status.sh +0 -138
- package/framework/scripts/sync-to-framework.sh +0 -65
- package/framework/scripts/voice-hotkey.py +0 -227
- package/framework/scripts/voice-input.sh +0 -51
- package/framework/skills/animate/SKILL.md +0 -202
- package/framework/skills/bolder/SKILL.md +0 -144
- package/framework/skills/browser-qa/SKILL.md +0 -536
- package/framework/skills/clarify/SKILL.md +0 -179
- package/framework/skills/client-handoff/SKILL.md +0 -135
- package/framework/skills/collab-onboard/SKILL.md +0 -111
- package/framework/skills/colorize/SKILL.md +0 -170
- package/framework/skills/critique/SKILL.md +0 -126
- package/framework/skills/deep-research/SKILL.md +0 -240
- package/framework/skills/delight/SKILL.md +0 -329
- package/framework/skills/deploy/SKILL.md +0 -261
- package/framework/skills/deploy-verify/SKILL.md +0 -377
- package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
- package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
- package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
- package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
- package/framework/skills/design-quieter/SKILL.md +0 -130
- package/framework/skills/distill/SKILL.md +0 -149
- package/framework/skills/docs-lookup/SKILL.md +0 -79
- package/framework/skills/fcm-notifications/SKILL.md +0 -125
- package/framework/skills/financial-ledger/SKILL.md +0 -1039
- package/framework/skills/frontend-master/NOTICE.md +0 -4
- package/framework/skills/frontend-master/SKILL.md +0 -127
- package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
- package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
- package/framework/skills/frontend-master/reference/motion-design.md +0 -99
- package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
- package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
- package/framework/skills/frontend-master/reference/typography.md +0 -131
- package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
- package/framework/skills/harden/SKILL.md +0 -357
- package/framework/skills/i18n-rtl/SKILL.md +0 -752
- package/framework/skills/learn/SKILL.md +0 -95
- package/framework/skills/memory/SKILL.md +0 -50
- package/framework/skills/mobile-expo/SKILL.md +0 -977
- package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
- package/framework/skills/nestjs-backend/README.md +0 -73
- package/framework/skills/nestjs-backend/SKILL.md +0 -446
- package/framework/skills/nestjs-backend/references/templates.md +0 -1173
- package/framework/skills/normalize/SKILL.md +0 -79
- package/framework/skills/onboard/SKILL.md +0 -242
- package/framework/skills/openrouter-agent/SKILL.md +0 -922
- package/framework/skills/polish/SKILL.md +0 -209
- package/framework/skills/pr/SKILL.md +0 -66
- package/framework/skills/qualia/SKILL.md +0 -199
- package/framework/skills/qualia-add-todo/SKILL.md +0 -68
- package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
- package/framework/skills/qualia-check-todos/SKILL.md +0 -55
- package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
- package/framework/skills/qualia-debug/SKILL.md +0 -149
- package/framework/skills/qualia-design/SKILL.md +0 -203
- package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
- package/framework/skills/qualia-evolve/SKILL.md +0 -200
- package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
- package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
- package/framework/skills/qualia-guide/SKILL.md +0 -32
- package/framework/skills/qualia-help/SKILL.md +0 -114
- package/framework/skills/qualia-idk/SKILL.md +0 -352
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
- package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
- package/framework/skills/qualia-new-project/SKILL.md +0 -232
- package/framework/skills/qualia-optimize/SKILL.md +0 -417
- package/framework/skills/qualia-pause-work/SKILL.md +0 -96
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
- package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
- package/framework/skills/qualia-production-check/SKILL.md +0 -0
- package/framework/skills/qualia-progress/SKILL.md +0 -53
- package/framework/skills/qualia-quick/SKILL.md +0 -89
- package/framework/skills/qualia-report/SKILL.md +0 -166
- package/framework/skills/qualia-research-phase/SKILL.md +0 -88
- package/framework/skills/qualia-resume-work/SKILL.md +0 -62
- package/framework/skills/qualia-review/SKILL.md +0 -263
- package/framework/skills/qualia-start/SKILL.md +0 -161
- package/framework/skills/qualia-verify-work/SKILL.md +0 -132
- package/framework/skills/rag/SKILL.md +0 -750
- package/framework/skills/responsive/SKILL.md +0 -231
- package/framework/skills/retro/SKILL.md +0 -284
- package/framework/skills/sakani-conventions/SKILL.md +0 -136
- package/framework/skills/sakani-conventions/evals/evals.json +0 -23
- package/framework/skills/sakani-conventions/references/entities.md +0 -365
- package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
- package/framework/skills/seo-master/SKILL.md +0 -490
- package/framework/skills/seo-master/references/checklist.md +0 -199
- package/framework/skills/seo-master/references/structured-data.md +0 -609
- package/framework/skills/ship/SKILL.md +0 -239
- package/framework/skills/stack-researcher/SKILL.md +0 -215
- package/framework/skills/status/SKILL.md +0 -154
- package/framework/skills/status/scripts/health-check.sh +0 -562
- package/framework/skills/subscription-payments/SKILL.md +0 -250
- package/framework/skills/supabase/SKILL.md +0 -973
- package/framework/skills/supabase/references/templates.md +0 -159
- package/framework/skills/team/SKILL.md +0 -67
- package/framework/skills/test-runner/SKILL.md +0 -202
- package/framework/skills/voice-agent/SKILL.md +0 -1312
- package/framework/skills/zoho-workflow/SKILL.md +0 -51
- package/framework/statusline-command.sh +0 -117
- package/framework/teams/default/inboxes/plan-04.json +0 -9
- package/framework/teams/review-team.md +0 -75
- package/framework/teams/ship-team.md +0 -86
- package/profiles/fawzi.json +0 -16
- package/profiles/hasan.json +0 -16
- package/profiles/moayad.json +0 -16
- package/templates/CLAUDE-owner.md +0 -52
- package/templates/CLAUDE.md.hbs +0 -58
- package/templates/env.claude.template +0 -12
- package/templates/settings.json +0 -172
- package/uninstall.sh +0 -90
- /package/{framework/rules → rules}/deployment.md +0 -0
- /package/{framework/rules → rules}/security.md +0 -0
package/bin/cli.js
CHANGED
|
@@ -1,614 +1,813 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const path = require(
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function ok(msg) { log(` ${c.green}\u2713${c.reset} ${msg}`); }
|
|
28
|
-
function warn(msg) { log(` ${c.yellow}\u26A0${c.reset} ${msg}`); }
|
|
29
|
-
function fail(msg) { log(` ${c.red}\u2717${c.reset} ${msg}`); }
|
|
30
|
-
function step(n, total, msg) { log(`\n${c.cyan}[${n}/${total}]${c.reset} ${msg}`); }
|
|
31
|
-
|
|
32
|
-
// ── Helpers ──
|
|
33
|
-
function copyDirSync(src, dest) {
|
|
34
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
35
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
36
|
-
const srcPath = path.join(src, entry.name);
|
|
37
|
-
const destPath = path.join(dest, entry.name);
|
|
38
|
-
if (entry.isDirectory()) {
|
|
39
|
-
copyDirSync(srcPath, destPath);
|
|
40
|
-
} else {
|
|
41
|
-
fs.copyFileSync(srcPath, destPath);
|
|
42
|
-
}
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const readline = require("readline");
|
|
7
|
+
|
|
8
|
+
const TEAL = "\x1b[38;2;0;206;209m";
|
|
9
|
+
const TG = "\x1b[38;2;0;170;175m";
|
|
10
|
+
const DIM = "\x1b[38;2;80;90;100m";
|
|
11
|
+
const GREEN = "\x1b[38;2;52;211;153m";
|
|
12
|
+
const WHITE = "\x1b[38;2;220;225;230m";
|
|
13
|
+
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
14
|
+
const RED = "\x1b[38;2;239;68;68m";
|
|
15
|
+
const RESET = "\x1b[0m";
|
|
16
|
+
const BOLD = "\x1b[1m";
|
|
17
|
+
|
|
18
|
+
const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
|
|
19
|
+
const PKG = require("../package.json");
|
|
20
|
+
const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
|
|
21
|
+
|
|
22
|
+
function readConfig() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
43
27
|
}
|
|
44
28
|
}
|
|
45
29
|
|
|
46
|
-
function
|
|
47
|
-
if (!fs.existsSync(
|
|
48
|
-
|
|
49
|
-
if (type === 'dirs') return entries.filter(e => e.isDirectory()).length;
|
|
50
|
-
return entries.filter(e => e.isFile()).length;
|
|
30
|
+
function writeConfig(cfg) {
|
|
31
|
+
if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
32
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
&& target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
|
|
58
|
-
result[key] = deepMerge(target[key], source[key]);
|
|
59
|
-
} else if (result[key] === undefined || result[key] === null || result[key] === '') {
|
|
60
|
-
result[key] = source[key];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
35
|
+
function banner() {
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}Qualia Framework${RESET} ${DIM}v${PKG.version}${RESET}`);
|
|
38
|
+
console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
64
39
|
}
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const profile = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
|
|
71
|
-
profiles[profile.code.toUpperCase()] = profile;
|
|
72
|
-
}
|
|
73
|
-
return profiles;
|
|
41
|
+
// ─── Commands ────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function cmdInstall() {
|
|
44
|
+
require("./install.js");
|
|
74
45
|
}
|
|
75
46
|
|
|
76
|
-
function
|
|
77
|
-
|
|
47
|
+
function cmdVersion() {
|
|
48
|
+
banner();
|
|
49
|
+
const cfg = readConfig();
|
|
78
50
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
51
|
+
console.log(` ${DIM}Installed:${RESET} ${WHITE}${PKG.version}${RESET}`);
|
|
52
|
+
if (cfg.installed_by) {
|
|
53
|
+
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role})${RESET}`);
|
|
54
|
+
}
|
|
55
|
+
if (cfg.installed_at) {
|
|
56
|
+
console.log(` ${DIM}Date:${RESET} ${WHITE}${cfg.installed_at}${RESET}`);
|
|
57
|
+
}
|
|
82
58
|
|
|
83
|
-
|
|
59
|
+
// Check for updates
|
|
60
|
+
try {
|
|
61
|
+
// spawnSync with argv — no bash-only `2>/dev/null` redirect, no shell
|
|
62
|
+
// interpolation. stdio: "ignore" on stderr silences any npm warnings
|
|
63
|
+
// (offline, proxy, etc.) without a shell redirect. shell: true on
|
|
64
|
+
// Windows because `npm` is a .cmd shim that only resolves through cmd.
|
|
65
|
+
const r = spawnSync("npm", ["view", "qualia-framework", "version"], {
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
shell: process.platform === "win32",
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
});
|
|
71
|
+
const latest = (r.stdout || "").trim();
|
|
72
|
+
const semverGt = (a, b) => {
|
|
73
|
+
const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
|
|
74
|
+
for (let i = 0; i < 3; i++) { if (pa[i] > pb[i]) return true; if (pa[i] < pb[i]) return false; }
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
if (latest && semverGt(latest, PKG.version)) {
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(` ${YELLOW}Update available:${RESET} ${WHITE}${latest}${RESET}`);
|
|
80
|
+
console.log(` ${DIM}Run:${RESET} npx qualia-framework update`);
|
|
81
|
+
} else if (latest) {
|
|
82
|
+
console.log(` ${DIM}Latest:${RESET} ${GREEN}${latest} ✓${RESET} ${DIM}(up to date)${RESET}`);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
console.log(` ${DIM}Latest:${RESET} ${DIM}(offline — couldn't check)${RESET}`);
|
|
86
|
+
}
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'{{role}}': profile.role,
|
|
89
|
-
'{{roleTitle}}': roleTitle,
|
|
90
|
-
'{{location}}': profile.location || '',
|
|
91
|
-
};
|
|
90
|
+
function cmdUpdate() {
|
|
91
|
+
banner();
|
|
92
|
+
const cfg = readConfig();
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
if (!cfg.code) {
|
|
95
|
+
console.log(` ${RED}✗${RESET} No install code saved. Run ${TEAL}install${RESET} first.`);
|
|
96
|
+
console.log("");
|
|
97
|
+
process.exit(1);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
template = processConditional(template, 'isOwner', isOwner);
|
|
101
|
-
template = processConditional(template, 'isDeveloper', isDeveloper);
|
|
102
|
-
template = processConditional(template, 'isArabic', isArabic);
|
|
100
|
+
console.log(` ${DIM}Current:${RESET} ${WHITE}${PKG.version}${RESET}`);
|
|
101
|
+
console.log(` ${DIM}Updating...${RESET}`);
|
|
102
|
+
console.log("");
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
try {
|
|
105
|
+
const r = spawnSync("npx", ["qualia-framework@latest", "install"], {
|
|
106
|
+
input: cfg.code + "\n",
|
|
107
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
108
|
+
shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
|
|
109
|
+
timeout: 120000,
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
});
|
|
112
|
+
if (r.status !== 0) {
|
|
113
|
+
console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework@latest install`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log(` ${RED}✗${RESET} Update failed: ${e.message}`);
|
|
118
|
+
console.log(` ${DIM}Run manually:${RESET} npx qualia-framework@latest install`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
// ─── Uninstall ───────────────────────────────────────────
|
|
124
|
+
// Surgical removal of the Qualia Framework from ~/.claude/.
|
|
125
|
+
// Preserves CLAUDE.md (user may have customized it) and preserves any
|
|
126
|
+
// non-Qualia entries in settings.json (other hooks, user env vars, etc.).
|
|
127
|
+
// --yes / -y skips the confirmation prompt for scripted use.
|
|
128
|
+
|
|
129
|
+
// 8 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
|
|
130
|
+
// any other hooks the user dropped in there are left alone.
|
|
131
|
+
const QUALIA_HOOK_FILES = [
|
|
132
|
+
"session-start.js",
|
|
133
|
+
"auto-update.js",
|
|
134
|
+
"branch-guard.js",
|
|
135
|
+
"pre-push.js",
|
|
136
|
+
"block-env-edit.js",
|
|
137
|
+
"migration-guard.js",
|
|
138
|
+
"pre-deploy-gate.js",
|
|
139
|
+
"pre-compact.js",
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// 4 Qualia agents — only these are removed.
|
|
143
|
+
const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
|
|
144
|
+
|
|
145
|
+
// 3 Qualia bin scripts.
|
|
146
|
+
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
|
|
147
|
+
|
|
148
|
+
// 5 Qualia rules.
|
|
149
|
+
const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
|
|
150
|
+
|
|
151
|
+
function promptYesNo(question, defaultYes) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
+
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
155
|
+
rl.question(` ${WHITE}${question}${RESET}${suffix} `, (answer) => {
|
|
156
|
+
rl.close();
|
|
157
|
+
const a = String(answer || "").trim().toLowerCase();
|
|
158
|
+
if (!a) return resolve(defaultYes);
|
|
159
|
+
resolve(a === "y" || a === "yes");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
108
162
|
}
|
|
109
163
|
|
|
110
|
-
function
|
|
111
|
-
|
|
164
|
+
function safeUnlink(p, counters) {
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(p)) {
|
|
167
|
+
fs.unlinkSync(p);
|
|
168
|
+
counters.filesRemoved++;
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
counters.errors.push(`${p}: ${e.message}`);
|
|
172
|
+
}
|
|
112
173
|
}
|
|
113
174
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
175
|
+
function safeRmDir(p, counters) {
|
|
176
|
+
try {
|
|
177
|
+
if (fs.existsSync(p)) {
|
|
178
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
179
|
+
counters.dirsRemoved++;
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
counters.errors.push(`${p}: ${e.message}`);
|
|
183
|
+
}
|
|
119
184
|
}
|
|
120
185
|
|
|
121
|
-
function
|
|
122
|
-
const
|
|
186
|
+
function cleanSettingsJson(counters) {
|
|
187
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
188
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
189
|
+
let settings;
|
|
190
|
+
try {
|
|
191
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
192
|
+
} catch (e) {
|
|
193
|
+
counters.errors.push(`settings.json: ${e.message}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
123
196
|
|
|
124
|
-
|
|
197
|
+
// Only remove entries that point at qualia paths. Leave everything else.
|
|
198
|
+
const isQualiaCommand = (cmd) =>
|
|
199
|
+
typeof cmd === "string" && (cmd.includes("qualia") || cmd.includes(".claude/hooks/") || cmd.includes(".claude/bin/"));
|
|
125
200
|
|
|
126
|
-
const
|
|
201
|
+
const filterHookArray = (arr) => {
|
|
202
|
+
if (!Array.isArray(arr)) return arr;
|
|
203
|
+
return arr
|
|
204
|
+
.map((entry) => {
|
|
205
|
+
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
206
|
+
const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
|
|
207
|
+
return { ...entry, hooks };
|
|
208
|
+
})
|
|
209
|
+
.filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
|
|
210
|
+
};
|
|
127
211
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
212
|
+
if (settings.hooks && typeof settings.hooks === "object") {
|
|
213
|
+
for (const key of ["SessionStart", "PreToolUse", "PreCompact"]) {
|
|
214
|
+
if (settings.hooks[key]) {
|
|
215
|
+
const cleaned = filterHookArray(settings.hooks[key]);
|
|
216
|
+
if (cleaned && cleaned.length > 0) {
|
|
217
|
+
settings.hooks[key] = cleaned;
|
|
218
|
+
} else {
|
|
219
|
+
delete settings.hooks[key];
|
|
220
|
+
}
|
|
135
221
|
}
|
|
136
222
|
}
|
|
223
|
+
// If hooks is now empty, remove it entirely.
|
|
224
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
137
225
|
}
|
|
138
226
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
} else {
|
|
146
|
-
merged.statusLine = template.statusLine;
|
|
227
|
+
// Status line — only drop it if it points at our renderer.
|
|
228
|
+
if (settings.statusLine && typeof settings.statusLine === "object") {
|
|
229
|
+
const cmd = settings.statusLine.command || "";
|
|
230
|
+
if (isQualiaCommand(cmd) || cmd.includes("statusline.js") || cmd.includes("qualia-ui")) {
|
|
231
|
+
delete settings.statusLine;
|
|
232
|
+
}
|
|
147
233
|
}
|
|
148
234
|
|
|
149
|
-
//
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (existing.mcpServers) {
|
|
154
|
-
merged.mcpServers = existing.mcpServers;
|
|
155
|
-
}
|
|
235
|
+
// Qualia-specific spinner overrides.
|
|
236
|
+
if (settings.spinnerVerbs) delete settings.spinnerVerbs;
|
|
237
|
+
if (settings.spinnerTipsOverride) delete settings.spinnerTipsOverride;
|
|
156
238
|
|
|
157
|
-
|
|
158
|
-
}
|
|
239
|
+
// Leave settings.env alone — the user may have other env vars in there.
|
|
159
240
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
167
|
-
});
|
|
241
|
+
try {
|
|
242
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
243
|
+
counters.settingsCleaned = true;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
counters.errors.push(`settings.json write: ${e.message}`);
|
|
246
|
+
}
|
|
168
247
|
}
|
|
169
248
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
log('');
|
|
173
|
-
log(`${c.red}\u2554${'═'.repeat(54)}\u2557${c.reset}`);
|
|
174
|
-
log(`${c.red}\u2551${c.reset}${c.bold} QUALIA FRAMEWORK \u2014 UNINSTALLER ${c.reset}${c.red}\u2551${c.reset}`);
|
|
175
|
-
log(`${c.red}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
|
|
176
|
-
log('');
|
|
249
|
+
async function cmdUninstall() {
|
|
250
|
+
banner();
|
|
177
251
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
252
|
+
const args = process.argv.slice(3);
|
|
253
|
+
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
254
|
+
|
|
255
|
+
const cfg = readConfig();
|
|
256
|
+
console.log("");
|
|
257
|
+
if (cfg.installed_by) {
|
|
258
|
+
console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET}`);
|
|
181
261
|
}
|
|
262
|
+
console.log("");
|
|
182
263
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
log(` ${c.red}\u2022${c.reset} settings.json will be reset to {}`);
|
|
190
|
-
log(` ${c.red}\u2022${c.reset} All skills, agents, hooks, rules, knowledge \u2014 gone`);
|
|
191
|
-
log(` ${c.red}\u2022${c.reset} All memory, projects, plans, cache \u2014 gone`);
|
|
192
|
-
log(` ${c.red}\u2022${c.reset} CLAUDE.md, .env.claude, all config \u2014 gone`);
|
|
193
|
-
log('');
|
|
194
|
-
|
|
195
|
-
// Confirmation
|
|
196
|
-
const force = process.argv.includes('--force');
|
|
197
|
-
if (!force) {
|
|
198
|
-
log(`${c.red} \u26A0 THIS CANNOT BE UNDONE.${c.reset}`);
|
|
199
|
-
log(`${c.red} \u26A0 ~/.claude/ will be wiped clean. Nothing survives.${c.reset}`);
|
|
200
|
-
log('');
|
|
201
|
-
const answer = await prompt(` Type 'UNINSTALL' to confirm: `);
|
|
202
|
-
if (answer !== 'UNINSTALL') {
|
|
203
|
-
log('');
|
|
204
|
-
ok('Cancelled. Nothing was deleted.');
|
|
264
|
+
if (!skipConfirm) {
|
|
265
|
+
const confirm = await promptYesNo("Are you sure you want to uninstall the Qualia Framework?", false);
|
|
266
|
+
if (!confirm) {
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log(` ${DIM}Aborted.${RESET}`);
|
|
269
|
+
console.log("");
|
|
205
270
|
return;
|
|
206
271
|
}
|
|
207
|
-
log('');
|
|
208
272
|
}
|
|
209
273
|
|
|
210
|
-
//
|
|
211
|
-
|
|
274
|
+
// Preserve knowledge base by default.
|
|
275
|
+
let preserveKnowledge = true;
|
|
276
|
+
if (!skipConfirm) {
|
|
277
|
+
preserveKnowledge = await promptYesNo(
|
|
278
|
+
"Preserve knowledge base? (your learned patterns, fixes, client prefs)",
|
|
279
|
+
true
|
|
280
|
+
);
|
|
281
|
+
}
|
|
212
282
|
|
|
213
|
-
|
|
214
|
-
|
|
283
|
+
console.log("");
|
|
284
|
+
console.log(` ${DIM}Removing framework files...${RESET}`);
|
|
285
|
+
console.log("");
|
|
215
286
|
|
|
216
|
-
|
|
217
|
-
if (entry.name === 'settings.json') continue;
|
|
287
|
+
const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
|
|
218
288
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
fs.unlinkSync(p);
|
|
228
|
-
ok(entry.name);
|
|
289
|
+
// Skills — any directory starting with "qualia" under ~/.claude/skills/.
|
|
290
|
+
const skillsDir = path.join(CLAUDE_DIR, "skills");
|
|
291
|
+
try {
|
|
292
|
+
if (fs.existsSync(skillsDir)) {
|
|
293
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
294
|
+
if (name === "qualia" || name.startsWith("qualia-")) {
|
|
295
|
+
safeRmDir(path.join(skillsDir, name), counters);
|
|
296
|
+
}
|
|
229
297
|
}
|
|
230
|
-
} catch (e) {
|
|
231
|
-
fail(`${entry.name}: ${e.message}`);
|
|
232
298
|
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
counters.errors.push(`skills scan: ${e.message}`);
|
|
233
301
|
}
|
|
234
302
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (fs.existsSync(settingsPath)) {
|
|
239
|
-
fs.writeFileSync(settingsPath, '{}\n');
|
|
240
|
-
ok('settings.json reset to {}');
|
|
241
|
-
} else {
|
|
242
|
-
ok('No settings.json found');
|
|
303
|
+
// Agents — only the 4 Qualia ones.
|
|
304
|
+
for (const f of QUALIA_AGENT_FILES) {
|
|
305
|
+
safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
|
|
243
306
|
}
|
|
244
307
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
log('');
|
|
250
|
-
log(` ${c.dim}Claude Code will use default behavior on next launch.${c.reset}`);
|
|
251
|
-
log('');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function isSymlink(p) {
|
|
255
|
-
try { fs.lstatSync(p); return fs.lstatSync(p).isSymbolicLink(); } catch { return false; }
|
|
256
|
-
}
|
|
308
|
+
// Hooks — only the 8 Qualia ones.
|
|
309
|
+
for (const f of QUALIA_HOOK_FILES) {
|
|
310
|
+
safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
|
|
311
|
+
}
|
|
257
312
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
313
|
+
// Bin scripts — only the 3 Qualia ones.
|
|
314
|
+
for (const f of QUALIA_BIN_FILES) {
|
|
315
|
+
safeUnlink(path.join(CLAUDE_DIR, "bin", f), counters);
|
|
316
|
+
}
|
|
261
317
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
runVerify();
|
|
266
|
-
} else if (command === 'uninstall') {
|
|
267
|
-
await runUninstall();
|
|
268
|
-
} else {
|
|
269
|
-
await runInstall();
|
|
318
|
+
// Rules — all 4.
|
|
319
|
+
for (const f of QUALIA_RULE_FILES) {
|
|
320
|
+
safeUnlink(path.join(CLAUDE_DIR, "rules", f), counters);
|
|
270
321
|
}
|
|
271
|
-
}
|
|
272
322
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const profiles = loadProfiles();
|
|
281
|
-
const code = await prompt(` Enter your employee code: `);
|
|
282
|
-
const profile = profiles[code.toUpperCase()];
|
|
283
|
-
|
|
284
|
-
if (!profile) {
|
|
285
|
-
log('');
|
|
286
|
-
fail(`Unknown code "${code}". Contact Fawzi for your employee code.`);
|
|
287
|
-
log(`${c.dim} Available format: QS-NAME-YEAR${c.reset}`);
|
|
288
|
-
process.exit(1);
|
|
323
|
+
// Templates directory (entire).
|
|
324
|
+
safeRmDir(path.join(CLAUDE_DIR, "qualia-templates"), counters);
|
|
325
|
+
|
|
326
|
+
// Knowledge directory (optional preservation).
|
|
327
|
+
if (!preserveKnowledge) {
|
|
328
|
+
safeRmDir(path.join(CLAUDE_DIR, "knowledge"), counters);
|
|
289
329
|
}
|
|
290
330
|
|
|
291
|
-
|
|
292
|
-
|
|
331
|
+
// Config + state files.
|
|
332
|
+
safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
|
|
333
|
+
safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
|
|
334
|
+
safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
|
|
335
|
+
safeUnlink(path.join(CLAUDE_DIR, ".qualia-team.json"), counters);
|
|
336
|
+
safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
|
|
293
337
|
|
|
294
|
-
|
|
338
|
+
// Traces directory.
|
|
339
|
+
safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
|
|
295
340
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
ok(`Config backed up to ${c.dim}~/.claude.backup-${date}/${c.reset}`);
|
|
311
|
-
} else {
|
|
312
|
-
ok('Backup already exists for today');
|
|
313
|
-
}
|
|
341
|
+
// Clean settings.json surgically.
|
|
342
|
+
cleanSettingsJson(counters);
|
|
343
|
+
|
|
344
|
+
// Summary.
|
|
345
|
+
console.log("");
|
|
346
|
+
console.log(`${TEAL} ⬢ Uninstall complete${RESET}`);
|
|
347
|
+
console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
348
|
+
console.log(` ${DIM}Files removed:${RESET} ${WHITE}${counters.filesRemoved}${RESET}`);
|
|
349
|
+
console.log(` ${DIM}Directories removed:${RESET} ${WHITE}${counters.dirsRemoved}${RESET}`);
|
|
350
|
+
console.log(
|
|
351
|
+
` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
|
|
352
|
+
);
|
|
353
|
+
if (preserveKnowledge) {
|
|
354
|
+
console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
|
|
314
355
|
} else {
|
|
315
|
-
|
|
316
|
-
ok('Created ~/.claude/');
|
|
356
|
+
console.log(` ${DIM}Knowledge base:${RESET} ${YELLOW}removed${RESET}`);
|
|
317
357
|
}
|
|
318
358
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const src = path.join(FRAMEWORK_DIR, dir);
|
|
325
|
-
const dest = path.join(CLAUDE_DIR, dir);
|
|
326
|
-
if (fs.existsSync(src)) {
|
|
327
|
-
copyDirSync(src, dest);
|
|
359
|
+
if (counters.errors.length > 0) {
|
|
360
|
+
console.log("");
|
|
361
|
+
console.log(` ${YELLOW}${counters.errors.length} warning(s):${RESET}`);
|
|
362
|
+
for (const err of counters.errors.slice(0, 5)) {
|
|
363
|
+
console.log(` ${DIM}${err}${RESET}`);
|
|
328
364
|
}
|
|
329
365
|
}
|
|
330
366
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
367
|
+
console.log("");
|
|
368
|
+
console.log(
|
|
369
|
+
` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
|
|
370
|
+
);
|
|
371
|
+
console.log("");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── Team Management ────────────────────────────────────
|
|
375
|
+
// External team file at ~/.claude/.qualia-team.json.
|
|
376
|
+
// Falls back to embedded defaults in install.js.
|
|
377
|
+
|
|
378
|
+
function getDefaultTeam() {
|
|
379
|
+
return {
|
|
380
|
+
"QS-FAWZI-01": { name: "Fawzi Goussous", role: "OWNER", description: "Company owner. Full access. Can push to main, approve deploys, edit secrets." },
|
|
381
|
+
"QS-HASAN-02": { name: "Hasan", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
|
|
382
|
+
"QS-MOAYAD-03": { name: "Moayad", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
|
|
383
|
+
"QS-RAMA-04": { name: "Rama", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
|
|
384
|
+
"QS-SALLY-05": { name: "Sally", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function readTeamFile() {
|
|
389
|
+
const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
|
|
390
|
+
try {
|
|
391
|
+
if (fs.existsSync(teamFile)) {
|
|
392
|
+
const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
|
|
393
|
+
if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
|
|
336
394
|
}
|
|
395
|
+
} catch {}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function writeTeamFile(team) {
|
|
400
|
+
if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
401
|
+
fs.writeFileSync(path.join(CLAUDE_DIR, ".qualia-team.json"), JSON.stringify(team, null, 2) + "\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseTeamArgs(argv) {
|
|
405
|
+
const args = {};
|
|
406
|
+
for (let i = 0; i < argv.length; i++) {
|
|
407
|
+
if (argv[i] === "--code" && argv[i + 1]) { args.code = argv[++i]; }
|
|
408
|
+
else if (argv[i] === "--name" && argv[i + 1]) { args.name = argv[++i]; }
|
|
409
|
+
else if (argv[i] === "--role" && argv[i + 1]) { args.role = argv[++i].toUpperCase(); }
|
|
337
410
|
}
|
|
411
|
+
return args;
|
|
412
|
+
}
|
|
338
413
|
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
414
|
+
function cmdTeam() {
|
|
415
|
+
const sub = process.argv[3];
|
|
416
|
+
|
|
417
|
+
switch (sub) {
|
|
418
|
+
case "list": {
|
|
419
|
+
banner();
|
|
420
|
+
console.log("");
|
|
421
|
+
const team = readTeamFile();
|
|
422
|
+
const source = team || getDefaultTeam();
|
|
423
|
+
const label = team ? "team file" : "embedded defaults";
|
|
424
|
+
console.log(` ${DIM}Source: ${label}${RESET}`);
|
|
425
|
+
console.log("");
|
|
426
|
+
for (const [code, member] of Object.entries(source)) {
|
|
427
|
+
const roleColor = member.role === "OWNER" ? TEAL : WHITE;
|
|
428
|
+
console.log(` ${WHITE}${code}${RESET} ${roleColor}${member.role}${RESET} ${DIM}${member.name}${RESET}`);
|
|
345
429
|
}
|
|
430
|
+
console.log("");
|
|
431
|
+
break;
|
|
346
432
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
433
|
+
|
|
434
|
+
case "add": {
|
|
435
|
+
const args = parseTeamArgs(process.argv.slice(4));
|
|
436
|
+
if (!args.code || !args.name) {
|
|
437
|
+
console.log(` ${RED}Usage:${RESET} qualia-framework team add --code QS-NAME-NN --name "Full Name" [--role EMPLOYEE|OWNER]`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const team = readTeamFile() || getDefaultTeam();
|
|
441
|
+
const code = args.code.toUpperCase();
|
|
442
|
+
const role = args.role || "EMPLOYEE";
|
|
443
|
+
team[code] = {
|
|
444
|
+
name: args.name,
|
|
445
|
+
role,
|
|
446
|
+
description: role === "OWNER"
|
|
447
|
+
? "Company owner. Full access. Can push to main, approve deploys, edit secrets."
|
|
448
|
+
: "Developer. Feature branches only. Cannot push to main or edit .env files.",
|
|
449
|
+
};
|
|
450
|
+
writeTeamFile(team);
|
|
451
|
+
console.log(` ${GREEN}+${RESET} ${WHITE}${code}${RESET} ${DIM}(${args.name}, ${role})${RESET}`);
|
|
452
|
+
break;
|
|
359
453
|
}
|
|
360
|
-
}
|
|
361
454
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// Step 4: Configure settings.json
|
|
380
|
-
step(4, TOTAL, 'Configuring settings.json ...');
|
|
381
|
-
const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
|
|
382
|
-
const templateSettingsPath = path.join(TEMPLATES_DIR, 'settings.json');
|
|
383
|
-
const mergedSettings = mergeSettings(settingsPath, templateSettingsPath);
|
|
384
|
-
|
|
385
|
-
// Fix SUDO_ASKPASS to absolute path
|
|
386
|
-
mergedSettings.env.SUDO_ASKPASS = path.join(CLAUDE_DIR, 'askpass.sh');
|
|
387
|
-
mergedSettings.env.CLAUDE_ENV_FILE = path.join(CLAUDE_DIR, '.env.claude');
|
|
388
|
-
|
|
389
|
-
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
|
|
390
|
-
ok('Hooks wired');
|
|
391
|
-
ok('Preserved your existing API keys');
|
|
392
|
-
ok('Settings configured');
|
|
393
|
-
|
|
394
|
-
// Step 5: Environment
|
|
395
|
-
step(5, TOTAL, 'Setting up environment ...');
|
|
396
|
-
const envPath = path.join(CLAUDE_DIR, '.env.claude');
|
|
397
|
-
if (fs.existsSync(envPath)) {
|
|
398
|
-
ok('.env.claude already exists (preserved)');
|
|
399
|
-
} else {
|
|
400
|
-
fs.copyFileSync(path.join(TEMPLATES_DIR, 'env.claude.template'), envPath);
|
|
401
|
-
fs.chmodSync(envPath, 0o600);
|
|
402
|
-
warn(`Fill in your API keys: ${c.dim}~/.claude/.env.claude${c.reset}`);
|
|
403
|
-
}
|
|
455
|
+
case "remove": {
|
|
456
|
+
const code = (process.argv[4] || "").toUpperCase();
|
|
457
|
+
if (!code) {
|
|
458
|
+
console.log(` ${RED}Usage:${RESET} qualia-framework team remove QS-NAME-NN`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
const team = readTeamFile();
|
|
462
|
+
if (!team || !team[code]) {
|
|
463
|
+
console.log(` ${YELLOW}!${RESET} ${code} not found in team file.`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
delete team[code];
|
|
467
|
+
writeTeamFile(team);
|
|
468
|
+
console.log(` ${RED}-${RESET} ${WHITE}${code}${RESET} removed`);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
404
471
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
472
|
+
default:
|
|
473
|
+
console.log(` ${RED}Usage:${RESET} qualia-framework team <list|add|remove>`);
|
|
474
|
+
process.exit(1);
|
|
408
475
|
}
|
|
476
|
+
}
|
|
409
477
|
|
|
410
|
-
|
|
411
|
-
step(6, TOTAL, 'Verifying installation ...');
|
|
478
|
+
// ─── Traces ─────────────────────────────────────────────
|
|
412
479
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
480
|
+
function cmdTraces() {
|
|
481
|
+
banner();
|
|
482
|
+
console.log("");
|
|
483
|
+
const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
|
|
484
|
+
if (!fs.existsSync(tracesDir)) {
|
|
485
|
+
console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
|
|
486
|
+
console.log("");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const files = fs.readdirSync(tracesDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
490
|
+
if (files.length === 0) {
|
|
491
|
+
console.log(` ${DIM}No trace files found.${RESET}`);
|
|
492
|
+
console.log("");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const latest = path.join(tracesDir, files[0]);
|
|
496
|
+
const lines = fs.readFileSync(latest, "utf8").trim().split("\n").slice(-20);
|
|
497
|
+
console.log(` ${WHITE}Recent traces${RESET} ${DIM}(${files[0]})${RESET}`);
|
|
498
|
+
console.log("");
|
|
499
|
+
for (const line of lines) {
|
|
417
500
|
try {
|
|
418
|
-
const
|
|
419
|
-
|
|
501
|
+
const e = JSON.parse(line);
|
|
502
|
+
const color = e.result === "block" ? RED : e.result === "allow" ? GREEN : DIM;
|
|
503
|
+
const time = (e.timestamp || "").split("T")[1] || "";
|
|
504
|
+
const ts = time.split(".")[0] || "";
|
|
505
|
+
console.log(` ${DIM}${ts}${RESET} ${color}${e.result}${RESET} ${WHITE}${e.hook}${RESET} ${DIM}${e.duration_ms || 0}ms${RESET}`);
|
|
420
506
|
} catch {}
|
|
421
507
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} else {
|
|
425
|
-
warn(`${hooksOk}/${hooksTotal} hooks executable`);
|
|
426
|
-
}
|
|
508
|
+
console.log("");
|
|
509
|
+
}
|
|
427
510
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
511
|
+
// ─── Migrate ────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
function cmdMigrate() {
|
|
514
|
+
banner();
|
|
515
|
+
console.log("");
|
|
516
|
+
|
|
517
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
518
|
+
if (!fs.existsSync(settingsPath)) {
|
|
519
|
+
console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
|
|
520
|
+
console.log("");
|
|
521
|
+
process.exit(1);
|
|
433
522
|
}
|
|
434
523
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
524
|
+
let settings;
|
|
525
|
+
try {
|
|
526
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
527
|
+
} catch (e) {
|
|
528
|
+
console.log(` ${RED}✗${RESET} Failed to parse settings.json: ${e.message}`);
|
|
529
|
+
process.exit(1);
|
|
439
530
|
}
|
|
440
531
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
log(`${c.green}\u2551${c.reset}${c.bold} \u2713 INSTALLED \u2014 Run /qualia-start in Claude Code ${c.reset}${c.green}\u2551${c.reset}`);
|
|
445
|
-
log(`${c.green}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
|
|
446
|
-
log('');
|
|
447
|
-
log(` ${c.dim}Update later: npx github:Qualiasolutions/qualia-framework update${c.reset}`);
|
|
448
|
-
log('');
|
|
449
|
-
}
|
|
532
|
+
const cfg = readConfig();
|
|
533
|
+
const fromVersion = cfg.version || "unknown";
|
|
534
|
+
let changes = 0;
|
|
450
535
|
|
|
451
|
-
|
|
452
|
-
log(
|
|
453
|
-
log(
|
|
454
|
-
log('');
|
|
536
|
+
console.log(` ${DIM}Current version:${RESET} ${WHITE}${fromVersion}${RESET}`);
|
|
537
|
+
console.log(` ${DIM}Target version:${RESET} ${WHITE}${PKG.version}${RESET}`);
|
|
538
|
+
console.log("");
|
|
455
539
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
540
|
+
// 1. Ensure all 8 hooks are wired (v2 missed block-env-edit and branch-guard)
|
|
541
|
+
const hd = path.join(CLAUDE_DIR, "hooks");
|
|
542
|
+
const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
|
|
460
543
|
|
|
461
|
-
|
|
462
|
-
const profiles = loadProfiles();
|
|
463
|
-
const code = await prompt(` Enter your employee code: `);
|
|
464
|
-
const profile = profiles[code.toUpperCase()];
|
|
544
|
+
if (!settings.hooks) settings.hooks = {};
|
|
465
545
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
546
|
+
// Check SessionStart hooks
|
|
547
|
+
if (!settings.hooks.SessionStart || !Array.isArray(settings.hooks.SessionStart)) {
|
|
548
|
+
settings.hooks.SessionStart = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] }];
|
|
549
|
+
changes++;
|
|
550
|
+
console.log(` ${GREEN}+${RESET} Added SessionStart hook`);
|
|
470
551
|
}
|
|
471
552
|
|
|
472
|
-
|
|
473
|
-
|
|
553
|
+
// Check PreToolUse hooks — ensure all critical hooks are present
|
|
554
|
+
const requiredBashHooks = ["auto-update.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
|
|
555
|
+
const requiredEditHooks = ["block-env-edit.js", "migration-guard.js"];
|
|
474
556
|
|
|
475
|
-
|
|
476
|
-
const frameworkDirs = ['skills', 'hooks', 'agents', 'rules', 'qualia-framework', 'scripts', 'knowledge'];
|
|
477
|
-
const counts = {};
|
|
478
|
-
let totalFiles = 0;
|
|
557
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
479
558
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
559
|
+
// Find or create Bash matcher entry
|
|
560
|
+
let bashEntry = settings.hooks.PreToolUse.find(e => e.matcher === "Bash");
|
|
561
|
+
if (!bashEntry) {
|
|
562
|
+
bashEntry = { matcher: "Bash", hooks: [] };
|
|
563
|
+
settings.hooks.PreToolUse.push(bashEntry);
|
|
564
|
+
}
|
|
565
|
+
if (!bashEntry.hooks) bashEntry.hooks = [];
|
|
566
|
+
|
|
567
|
+
for (const hookFile of requiredBashHooks) {
|
|
568
|
+
const cmd = nodeCmd(hookFile);
|
|
569
|
+
const exists = bashEntry.hooks.some(h => h.command && h.command.includes(hookFile));
|
|
570
|
+
if (!exists) {
|
|
571
|
+
const hookDef = { type: "command", command: cmd, timeout: hookFile === "pre-deploy-gate.js" ? 180 : 5 };
|
|
572
|
+
if (hookFile === "branch-guard.js") hookDef.if = "Bash(git push*)";
|
|
573
|
+
if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
|
|
574
|
+
if (hookFile === "pre-deploy-gate.js") hookDef.if = "Bash(vercel --prod*)";
|
|
575
|
+
bashEntry.hooks.push(hookDef);
|
|
576
|
+
changes++;
|
|
577
|
+
console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
|
|
486
578
|
}
|
|
487
579
|
}
|
|
488
580
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
581
|
+
// Find or create Edit|Write matcher entry
|
|
582
|
+
let editEntry = settings.hooks.PreToolUse.find(e => e.matcher === "Edit|Write");
|
|
583
|
+
if (!editEntry) {
|
|
584
|
+
editEntry = { matcher: "Edit|Write", hooks: [] };
|
|
585
|
+
settings.hooks.PreToolUse.push(editEntry);
|
|
493
586
|
}
|
|
494
|
-
|
|
495
|
-
log('');
|
|
587
|
+
if (!editEntry.hooks) editEntry.hooks = [];
|
|
496
588
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
589
|
+
for (const hookFile of requiredEditHooks) {
|
|
590
|
+
const cmd = nodeCmd(hookFile);
|
|
591
|
+
const exists = editEntry.hooks.some(h => h.command && h.command.includes(hookFile));
|
|
592
|
+
if (!exists) {
|
|
593
|
+
const hookDef = { type: "command", command: cmd, timeout: hookFile === "migration-guard.js" ? 10 : 5 };
|
|
594
|
+
if (hookFile === "migration-guard.js") hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)";
|
|
595
|
+
editEntry.hooks.push(hookDef);
|
|
596
|
+
changes++;
|
|
597
|
+
console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Edit|Write`);
|
|
506
598
|
}
|
|
507
599
|
}
|
|
508
600
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
try { fs.chmodSync(path.join(CLAUDE_DIR, f), 0o755); } catch {}
|
|
515
|
-
}
|
|
601
|
+
// Check PreCompact hook
|
|
602
|
+
if (!settings.hooks.PreCompact) {
|
|
603
|
+
settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
|
|
604
|
+
changes++;
|
|
605
|
+
console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
|
|
516
606
|
}
|
|
517
607
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
608
|
+
// 2. Ensure env vars are up to date
|
|
609
|
+
if (!settings.env) settings.env = {};
|
|
610
|
+
const requiredEnv = {
|
|
611
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
612
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
|
|
613
|
+
MAX_MCP_OUTPUT_TOKENS: "25000",
|
|
614
|
+
CLAUDE_CODE_NO_FLICKER: "1",
|
|
615
|
+
};
|
|
616
|
+
for (const [k, v] of Object.entries(requiredEnv)) {
|
|
617
|
+
if (settings.env[k] !== v) {
|
|
618
|
+
settings.env[k] = v;
|
|
619
|
+
changes++;
|
|
620
|
+
console.log(` ${GREEN}+${RESET} Set env.${k}`);
|
|
523
621
|
}
|
|
524
622
|
}
|
|
525
623
|
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
624
|
+
// 3. Update status line if missing
|
|
625
|
+
if (!settings.statusLine) {
|
|
626
|
+
settings.statusLine = { type: "command", command: `node "${path.join(CLAUDE_DIR, "bin", "statusline.js")}"` };
|
|
627
|
+
changes++;
|
|
628
|
+
console.log(` ${GREEN}+${RESET} Added status line`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 3b. Add next-devtools MCP if not present
|
|
632
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
633
|
+
if (!settings.mcpServers["next-devtools"]) {
|
|
634
|
+
settings.mcpServers["next-devtools"] = {
|
|
635
|
+
command: "npx",
|
|
636
|
+
args: ["next-devtools-mcp@0.3.10"],
|
|
637
|
+
disabled: false,
|
|
638
|
+
};
|
|
639
|
+
changes++;
|
|
640
|
+
console.log(` ${GREEN}+${RESET} Added next-devtools MCP server`);
|
|
532
641
|
}
|
|
533
642
|
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
643
|
+
// 4. Update config version
|
|
644
|
+
cfg.version = PKG.version;
|
|
645
|
+
cfg.migrated_at = new Date().toISOString().split("T")[0];
|
|
646
|
+
writeConfig(cfg);
|
|
647
|
+
|
|
648
|
+
// Write settings
|
|
649
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
538
650
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
mergedSettings.env.SUDO_ASKPASS = path.join(CLAUDE_DIR, 'askpass.sh');
|
|
545
|
-
mergedSettings.env.CLAUDE_ENV_FILE = path.join(CLAUDE_DIR, '.env.claude');
|
|
546
|
-
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
|
|
547
|
-
ok('settings.json hooks merged');
|
|
651
|
+
console.log("");
|
|
652
|
+
if (changes === 0) {
|
|
653
|
+
console.log(` ${GREEN}✓${RESET} Already up to date — no migration needed.`);
|
|
654
|
+
} else {
|
|
655
|
+
console.log(` ${GREEN}✓${RESET} Applied ${WHITE}${changes}${RESET} changes. Restart Claude Code to take effect.`);
|
|
548
656
|
}
|
|
657
|
+
console.log("");
|
|
658
|
+
}
|
|
549
659
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
660
|
+
// ─── Analytics ──────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
function cmdAnalytics() {
|
|
663
|
+
banner();
|
|
664
|
+
console.log("");
|
|
665
|
+
|
|
666
|
+
const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
|
|
667
|
+
if (!fs.existsSync(tracesDir)) {
|
|
668
|
+
console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
|
|
669
|
+
console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
|
|
670
|
+
console.log("");
|
|
671
|
+
return;
|
|
557
672
|
}
|
|
558
673
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
674
|
+
const files = fs.readdirSync(tracesDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
675
|
+
if (files.length === 0) {
|
|
676
|
+
console.log(` ${DIM}No trace data yet.${RESET}`);
|
|
677
|
+
console.log("");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
564
680
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
for (const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
count++;
|
|
681
|
+
// Parse all traces
|
|
682
|
+
const traces = [];
|
|
683
|
+
for (const file of files) {
|
|
684
|
+
const lines = fs.readFileSync(path.join(tracesDir, file), "utf8").trim().split("\n");
|
|
685
|
+
for (const line of lines) {
|
|
686
|
+
try { traces.push(JSON.parse(line)); } catch {}
|
|
572
687
|
}
|
|
573
688
|
}
|
|
574
|
-
|
|
689
|
+
|
|
690
|
+
// Aggregate stats
|
|
691
|
+
const hookStats = {};
|
|
692
|
+
let totalBlocks = 0;
|
|
693
|
+
let totalAllows = 0;
|
|
694
|
+
let totalDuration = 0;
|
|
695
|
+
|
|
696
|
+
for (const t of traces) {
|
|
697
|
+
const hook = t.hook || "unknown";
|
|
698
|
+
if (!hookStats[hook]) hookStats[hook] = { allow: 0, block: 0, total_ms: 0 };
|
|
699
|
+
if (t.result === "block") { hookStats[hook].block++; totalBlocks++; }
|
|
700
|
+
else { hookStats[hook].allow++; totalAllows++; }
|
|
701
|
+
hookStats[hook].total_ms += t.duration_ms || 0;
|
|
702
|
+
totalDuration += t.duration_ms || 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Verification outcomes (from traces that include verification data)
|
|
706
|
+
const verifications = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.verification);
|
|
707
|
+
const passes = verifications.filter(t => t.extra.verification === "pass").length;
|
|
708
|
+
const fails = verifications.filter(t => t.extra.verification === "fail").length;
|
|
709
|
+
|
|
710
|
+
// Gap cycle data
|
|
711
|
+
const gapTraces = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.gap_closure);
|
|
712
|
+
const totalGapCycles = gapTraces.length;
|
|
713
|
+
|
|
714
|
+
// Display
|
|
715
|
+
console.log(` ${WHITE}Framework Analytics${RESET}`);
|
|
716
|
+
console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
717
|
+
console.log("");
|
|
718
|
+
console.log(` ${WHITE}Overview${RESET}`);
|
|
719
|
+
console.log(` ${DIM}Trace files:${RESET} ${WHITE}${files.length}${RESET} ${DIM}(${files[0]} → ${files[files.length - 1]})${RESET}`);
|
|
720
|
+
console.log(` ${DIM}Total events:${RESET} ${WHITE}${traces.length}${RESET}`);
|
|
721
|
+
console.log(` ${DIM}Total blocks:${RESET} ${RED}${totalBlocks}${RESET}`);
|
|
722
|
+
console.log(` ${DIM}Total allows:${RESET} ${GREEN}${totalAllows}${RESET}`);
|
|
723
|
+
console.log(` ${DIM}Avg hook time:${RESET} ${WHITE}${traces.length ? Math.round(totalDuration / traces.length) : 0}ms${RESET}`);
|
|
724
|
+
console.log("");
|
|
725
|
+
|
|
726
|
+
// Verification stats
|
|
727
|
+
if (passes + fails > 0) {
|
|
728
|
+
const rate = Math.round((passes / (passes + fails)) * 100);
|
|
729
|
+
console.log(` ${WHITE}Verification Outcomes${RESET}`);
|
|
730
|
+
console.log(` ${DIM}First-pass rate:${RESET} ${rate >= 70 ? GREEN : rate >= 50 ? YELLOW : RED}${rate}%${RESET} ${DIM}(${passes} pass / ${fails} fail)${RESET}`);
|
|
731
|
+
console.log(` ${DIM}Gap cycles:${RESET} ${WHITE}${totalGapCycles}${RESET}`);
|
|
732
|
+
console.log("");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Per-hook breakdown
|
|
736
|
+
console.log(` ${WHITE}Per-Hook Breakdown${RESET}`);
|
|
737
|
+
const sorted = Object.entries(hookStats).sort((a, b) => (b[1].allow + b[1].block) - (a[1].allow + a[1].block));
|
|
738
|
+
for (const [hook, stats] of sorted) {
|
|
739
|
+
const total = stats.allow + stats.block;
|
|
740
|
+
const avg = Math.round(stats.total_ms / total);
|
|
741
|
+
const blockRate = stats.block > 0 ? ` ${RED}${stats.block} blocked${RESET}` : "";
|
|
742
|
+
console.log(` ${DIM}${hook}:${RESET} ${WHITE}${total}${RESET} calls, ${DIM}avg ${avg}ms${RESET}${blockRate}`);
|
|
743
|
+
}
|
|
744
|
+
console.log("");
|
|
575
745
|
}
|
|
576
746
|
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
log(
|
|
580
|
-
log(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
log('');
|
|
603
|
-
if (pass === total) {
|
|
604
|
-
log(`${c.green}All ${total} checks passed.${c.reset}`);
|
|
605
|
-
} else {
|
|
606
|
-
log(`${c.yellow}${pass}/${total} checks passed.${c.reset}`);
|
|
607
|
-
}
|
|
608
|
-
log('');
|
|
747
|
+
function cmdHelp() {
|
|
748
|
+
banner();
|
|
749
|
+
console.log("");
|
|
750
|
+
console.log(` ${WHITE}Commands:${RESET}`);
|
|
751
|
+
console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
|
|
752
|
+
console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
|
|
753
|
+
console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
|
|
754
|
+
console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
|
|
755
|
+
console.log(` qualia-framework ${TEAL}migrate${RESET} Migrate settings from v2 to v3`);
|
|
756
|
+
console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
|
|
757
|
+
console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
|
|
758
|
+
console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
|
|
759
|
+
console.log("");
|
|
760
|
+
console.log(` ${WHITE}After install:${RESET}`);
|
|
761
|
+
console.log(` ${TG}/qualia${RESET} What should I do next?`);
|
|
762
|
+
console.log(` ${TG}/qualia-new${RESET} Set up a new project`);
|
|
763
|
+
console.log(` ${TG}/qualia-plan${RESET} Plan a phase`);
|
|
764
|
+
console.log(` ${TG}/qualia-build${RESET} Build it (parallel tasks)`);
|
|
765
|
+
console.log(` ${TG}/qualia-verify${RESET} Verify it works`);
|
|
766
|
+
console.log(` ${TG}/qualia-design${RESET} One-shot design fix`);
|
|
767
|
+
console.log(` ${TG}/qualia-debug${RESET} Structured debugging`);
|
|
768
|
+
console.log(` ${TG}/qualia-review${RESET} Production audit`);
|
|
769
|
+
console.log(` ${TG}/qualia-ship${RESET} Deploy to production`);
|
|
770
|
+
console.log(` ${TG}/qualia-report${RESET} Log your work`);
|
|
771
|
+
console.log("");
|
|
609
772
|
}
|
|
610
773
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
774
|
+
|
|
775
|
+
// ─── Main ────────────────────────────────────────────────
|
|
776
|
+
const cmd = process.argv[2];
|
|
777
|
+
|
|
778
|
+
switch (cmd) {
|
|
779
|
+
case "install":
|
|
780
|
+
cmdInstall();
|
|
781
|
+
break;
|
|
782
|
+
case "version":
|
|
783
|
+
case "-v":
|
|
784
|
+
case "--version":
|
|
785
|
+
cmdVersion();
|
|
786
|
+
break;
|
|
787
|
+
case "update":
|
|
788
|
+
case "upgrade":
|
|
789
|
+
cmdUpdate();
|
|
790
|
+
break;
|
|
791
|
+
case "uninstall":
|
|
792
|
+
case "remove":
|
|
793
|
+
cmdUninstall().catch((e) => {
|
|
794
|
+
console.error(`${RED} ✗ Uninstall failed: ${e.message}${RESET}`);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
});
|
|
797
|
+
break;
|
|
798
|
+
case "team":
|
|
799
|
+
cmdTeam();
|
|
800
|
+
break;
|
|
801
|
+
case "traces":
|
|
802
|
+
cmdTraces();
|
|
803
|
+
break;
|
|
804
|
+
case "migrate":
|
|
805
|
+
cmdMigrate();
|
|
806
|
+
break;
|
|
807
|
+
case "analytics":
|
|
808
|
+
case "stats":
|
|
809
|
+
cmdAnalytics();
|
|
810
|
+
break;
|
|
811
|
+
default:
|
|
812
|
+
cmdHelp();
|
|
813
|
+
}
|