create-claude-cabinet 0.46.0 → 0.48.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/README.md +3 -6
- package/lib/CLAUDE.md +218 -0
- package/lib/cli.js +397 -382
- package/lib/copy.js +182 -6
- package/lib/db-setup.js +122 -17
- package/lib/engagement-server-setup.js +1 -17
- package/lib/engagement-setup.js +1 -1
- package/lib/installer-gate.js +135 -0
- package/lib/metadata.js +51 -2
- package/lib/modules.js +292 -0
- package/lib/mux-setup.js +1 -17
- package/lib/project-context.js +16 -96
- package/lib/settings-merge.js +194 -13
- package/lib/site-audit-setup.js +23 -7
- package/lib/verify-setup.js +20 -9
- package/lib/watchtower-setup.js +23 -7
- package/package.json +1 -1
- package/templates/CLAUDE.md +959 -0
- package/templates/briefing/_briefing-template.md +6 -11
- package/templates/cabinet/advisories-state-schema.md +1 -1
- package/templates/cabinet/checklist-stats-schema.md +15 -1
- package/templates/cabinet/eval-protocol.md +27 -10
- package/templates/cabinet/memory-lifecycle-contract.md +135 -0
- package/templates/cabinet/pib-db-access.md +13 -0
- package/templates/cabinet/watchtower-contracts.md +453 -0
- package/templates/cabinet/worktree-invocation-contract.md +111 -0
- package/templates/engagement/OVERVIEW.md +12 -0
- package/templates/engagement/__tests__/checklist-visibility.test.mjs +76 -0
- package/templates/engagement/__tests__/engagement.test.mjs +177 -2
- package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
- package/templates/engagement/engagement-checklist.mjs +26 -2
- package/templates/engagement/engagement-preview.mjs +100 -0
- package/templates/engagement/engagement-schema.md +248 -6
- package/templates/engagement/engagement.mjs +391 -11
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +49 -9
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
- package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
- package/templates/engagement/pib-db-patches/pib-db.mjs +9 -3
- package/templates/engagement/sql-constants.mjs +9 -1
- package/templates/engagement-server/__tests__/cross-tenant-auth.test.mjs +87 -0
- package/templates/engagement-server/server.mjs +65 -40
- package/templates/hooks/action-completion-gate.sh +19 -3
- package/templates/hooks/action-quality-gate.sh +10 -0
- package/templates/hooks/memory-index-guard.sh +17 -11
- package/templates/hooks/skill-telemetry.sh +10 -5
- package/templates/hooks/skill-tool-telemetry.sh +11 -5
- package/templates/hooks/watchtower-session-start.sh +24 -2
- package/templates/mcp/pib-db.json +1 -4
- package/templates/mux/bin/mux +8 -1
- package/templates/mux/config/help.txt +1 -0
- package/templates/rules/acknowledge-when-corrected.md +33 -0
- package/templates/rules/enforcement-pipeline.md +0 -12
- package/templates/rules/maintainability.md +11 -0
- package/templates/rules/memory-capture.md +26 -4
- package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
- package/templates/rules/verify-before-asserting.md +79 -0
- package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
- package/templates/scripts/__tests__/api-usage-idle-gate.test.mjs +81 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +98 -0
- package/templates/scripts/__tests__/bsql-loader-verify.test.mjs +47 -0
- package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
- package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
- package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
- package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
- package/templates/scripts/__tests__/inbox-assessment.test.mjs +341 -0
- package/templates/scripts/__tests__/memory-reachability.test.mjs +150 -0
- package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
- package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
- package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
- package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
- package/templates/scripts/__tests__/ring-state-ownership.test.mjs +120 -0
- package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
- package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
- package/templates/scripts/__tests__/ring1-pib-error-surfacing.test.mjs +69 -0
- package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
- package/templates/scripts/__tests__/ring1-stale-open-counts.test.mjs +143 -0
- package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
- package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
- package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
- package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
- package/templates/scripts/__tests__/ring3-dedup.test.mjs +92 -12
- package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
- package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
- package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
- package/templates/scripts/__tests__/ring3-thread-capture.test.mjs +213 -0
- package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
- package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
- package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
- package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
- package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
- package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
- package/templates/scripts/__tests__/watchtower-status-ring4.test.mjs +89 -0
- package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
- package/templates/scripts/audit-coherence-check.mjs +123 -0
- package/templates/scripts/audit-synth.mjs +117 -0
- package/templates/scripts/load-triage-history.js +5 -2
- package/templates/scripts/merge-findings.js +4 -1
- package/templates/scripts/patterns-scope-check.mjs +122 -0
- package/templates/scripts/pib-db-lib.mjs +61 -11
- package/templates/scripts/pib-db-mcp-server.mjs +7 -45
- package/templates/scripts/pib-db-path.mjs +61 -0
- package/templates/scripts/pib-db.mjs +7 -3
- package/templates/scripts/review-server.mjs +7 -12
- package/templates/scripts/triage-server.mjs +7 -8
- package/templates/scripts/validate-memory.mjs +214 -16
- package/templates/scripts/watchtower-advisories.mjs +7 -3
- package/templates/scripts/watchtower-build-context.mjs +192 -8
- package/templates/scripts/watchtower-cross-ring-reader.mjs +714 -0
- package/templates/scripts/watchtower-hook-runner.mjs +422 -0
- package/templates/scripts/watchtower-inbox-assessment.mjs +706 -0
- package/templates/scripts/watchtower-lib.mjs +555 -8
- package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
- package/templates/scripts/watchtower-phase-shim.mjs +171 -0
- package/templates/scripts/watchtower-queue.mjs +341 -3
- package/templates/scripts/watchtower-ring1.mjs +481 -81
- package/templates/scripts/watchtower-ring2.mjs +748 -80
- package/templates/scripts/watchtower-ring3-close.mjs +1147 -167
- package/templates/scripts/watchtower-ring4-runner.sh +85 -0
- package/templates/scripts/watchtower-ring4.mjs +753 -0
- package/templates/scripts/watchtower-routines.mjs +1 -1
- package/templates/scripts/watchtower-snapshot.mjs +452 -0
- package/templates/scripts/watchtower-status.sh +17 -0
- package/templates/scripts/watchtower-sync.mjs +393 -0
- package/templates/scripts/work-tracker-server.mjs +26 -22
- package/templates/skills/audit/SKILL.md +18 -5
- package/templates/skills/audit/phases/structural-checks.md +22 -0
- package/templates/skills/briefing/SKILL.md +289 -41
- package/templates/skills/cabinet-accessibility/SKILL.md +1 -1
- package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
- package/templates/skills/cabinet-security/SKILL.md +11 -0
- package/templates/skills/catch-up/SKILL.md +113 -0
- package/templates/skills/cc-publish/SKILL.md +89 -18
- package/templates/skills/cc-remember/SKILL.md +45 -0
- package/templates/skills/close/SKILL.md +107 -0
- package/templates/skills/collab-client/SKILL.md +22 -5
- package/templates/skills/collab-consultant/SKILL.md +110 -2
- package/templates/skills/debrief/SKILL.md +21 -4
- package/templates/skills/debrief/phases/audit-pattern-capture.md +13 -0
- package/templates/skills/debrief-classic/SKILL.md +696 -0
- package/templates/skills/debrief-classic/calibration.md +44 -0
- package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
- package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
- package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
- package/templates/skills/debrief-classic/phases/close-work.md +163 -0
- package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
- package/templates/skills/debrief-classic/phases/inventory.md +40 -0
- package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
- package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
- package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
- package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
- package/templates/skills/debrief-classic/phases/report.md +59 -0
- package/templates/skills/debrief-classic/phases/update-state.md +48 -0
- package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
- package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
- package/templates/skills/execute/SKILL.md +71 -6
- package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
- package/templates/skills/execute-group/SKILL.md +23 -26
- package/templates/skills/generate-plan-groups/SKILL.md +20 -0
- package/templates/skills/inbox/SKILL.md +177 -31
- package/templates/skills/memory/SKILL.md +22 -6
- package/templates/skills/orient/SKILL.md +75 -5
- package/templates/skills/orient-classic/SKILL.md +770 -0
- package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
- package/templates/skills/orient-classic/phases/briefing.md +53 -0
- package/templates/skills/orient-classic/phases/cabinet.md +46 -0
- package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
- package/templates/skills/orient-classic/phases/context.md +88 -0
- package/templates/skills/orient-classic/phases/data-sync.md +35 -0
- package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
- package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
- package/templates/skills/orient-classic/phases/health-checks.md +50 -0
- package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
- package/templates/skills/orient-classic/phases/work-scan.md +69 -0
- package/templates/skills/pulse/SKILL.md +10 -8
- package/templates/skills/qa-drain/SKILL.md +38 -0
- package/templates/skills/qa-handoff/SKILL.md +287 -46
- package/templates/skills/setup-accounts/SKILL.md +38 -16
- package/templates/skills/spring-clean/SKILL.md +1 -1
- package/templates/skills/spring-clean/phases/execute-decisions.md +1 -1
- package/templates/skills/threads/SKILL.md +4 -0
- package/templates/skills/validate/phases/validators.md +127 -0
- package/templates/skills/watchtower/SKILL.md +174 -30
- package/templates/verify-runtime/CONVENTIONS.md +9 -0
- package/templates/verify-runtime/README.md +37 -0
- package/templates/watchtower/config.json.template +8 -2
- package/templates/workflows/deliberative-audit.js +258 -87
- package/templates/workflows/execute-group-complete.js +26 -3
- package/templates/workflows/execute-group-implement.js +16 -2
package/lib/copy.js
CHANGED
|
@@ -50,17 +50,145 @@ function recordSkip(manifest, key, { identical = false, incomingHash = null } =
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Files shipped as STARTER content but owned by the project the moment they
|
|
55
|
+
* land. The installer seeds them if absent, NEVER overwrites them, and NEVER
|
|
56
|
+
* records them in the manifest. Manifest presence means "CC-owned", which has
|
|
57
|
+
* two consequences a project-owned file must avoid: cc-upstream-guard blocks
|
|
58
|
+
* edits to it, and a `--yes` reinstall silently overwrites it. The recordSkip
|
|
59
|
+
* "identical → keep tracked" rule is content-based and so still recorded an
|
|
60
|
+
* UNEDITED seed in the manifest — which false-blocked the very first edit (a
|
|
61
|
+
* project-owned file is unedited exactly until someone edits it). These files
|
|
62
|
+
* are project-owned by DECLARATION, not by content divergence.
|
|
63
|
+
*
|
|
64
|
+
* Keyed by basename: there is one such seed today and the src→dest relPath
|
|
65
|
+
* mapping varies by call site, so a basename match is the robust check.
|
|
66
|
+
* `directives-project.yaml`'s own header says it is "NOT overwritten by the
|
|
67
|
+
* installer ... extend upstream members with project-specific mandates" —
|
|
68
|
+
* recording it as CC-owned false-blocked the exact edits it exists to receive
|
|
69
|
+
* (field incident 2026-06-17, act:2001bf54).
|
|
70
|
+
*/
|
|
71
|
+
const PROJECT_OWNED_SEED_BASENAMES = new Set(['directives-project.yaml']);
|
|
72
|
+
function isProjectOwnedSeed(relPath) {
|
|
73
|
+
return PROJECT_OWNED_SEED_BASENAMES.has(path.basename(relPath));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The canonical FREEZE rule for an EXISTING file in a frozen module that
|
|
78
|
+
* differs from the incoming template (act:c1013967). Shared by both copy
|
|
79
|
+
* paths — copy.js's directory walk and cli.js's single-file branch — so the
|
|
80
|
+
* "third ownership state" is defined in exactly one place (the recordSkip
|
|
81
|
+
* precedent).
|
|
82
|
+
*
|
|
83
|
+
* Freeze is "CC-owned but temporarily pinned": the file is NOT overwritten,
|
|
84
|
+
* and — crucially — only a file that is ALREADY CC-owned (present in the prior
|
|
85
|
+
* manifest) is RETAINED in the new manifest, at its ON-DISK hash. Two reasons
|
|
86
|
+
* the on-disk hash is the only correct choice:
|
|
87
|
+
* - It keeps the file PRESENT in the manifest, so the upstream-cleanup loop
|
|
88
|
+
* never lists it for deletion (a frozen file dropped from allManifest would
|
|
89
|
+
* be deleted — the exact opposite of freeze).
|
|
90
|
+
* - On-disk == recorded keeps the value-comparison consumers green:
|
|
91
|
+
* cc-drift-check sees no drift, reset.js classifies it unmodified.
|
|
92
|
+
*
|
|
93
|
+
* A frozen-module file that differs but is NOT already CC-owned is
|
|
94
|
+
* project-owned (project-created or a kept/edited file deliberately omitted
|
|
95
|
+
* from the manifest). Freeze adds nothing for it — it's already never
|
|
96
|
+
* overwritten — so it must FALL THROUGH to the normal skip-and-omit path.
|
|
97
|
+
* RETAINING it would adopt a project-owned file into the manifest and clobber
|
|
98
|
+
* it on the unfreeze reinstall (violates the recordSkip invariant above).
|
|
99
|
+
*
|
|
100
|
+
* @returns {{manifestHash: string}} retain at on-disk hash (CC-owned)
|
|
101
|
+
* | {{omit: true}} fall through to skip-and-omit (project-owned)
|
|
102
|
+
*/
|
|
103
|
+
function freezeRetain({ existing, isCcOwned }) {
|
|
104
|
+
return isCcOwned ? { manifestHash: hashContent(existing) } : { omit: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pure ownership classifier for a SINGLE incoming template file — the
|
|
109
|
+
* non-interactive install cascade in one place (elegance-0002, act:a6377c82).
|
|
110
|
+
*
|
|
111
|
+
* The rule cascade below is the same one the walkAndCopy directory walk runs
|
|
112
|
+
* inline; cli.js's single-file install branch had hand-inlined a copy of it
|
|
113
|
+
* and the copy had already drifted — it was MISSING the project-owned-seed
|
|
114
|
+
* guard (step 1), so the single-file path could adopt/clobber a consumer
|
|
115
|
+
* overlay. Making cli.js delegate to this function removes that "kept in step
|
|
116
|
+
* by comments" fragility for the single-file path; the shared RULES
|
|
117
|
+
* (isProjectOwnedSeed, freezeRetain, recordSkip's omission semantics) already
|
|
118
|
+
* live in one place and are reused here.
|
|
119
|
+
*
|
|
120
|
+
* Order matches walkAndCopy exactly: seed → customized-phase → identical →
|
|
121
|
+
* frozen-hold → skipConflicts(manifest-driven) → interactive-prompt. Purely a
|
|
122
|
+
* decision — the caller performs all I/O, counter, and logging side effects.
|
|
123
|
+
*
|
|
124
|
+
* @returns {{ action: string, manifest: 'omit'|'identical'|'frozen'|'hash'|null, manifestHash?: string }}
|
|
125
|
+
* action ∈ 'seed' | 'preserve-phase' | 'skip-identical' | 'freeze-hold' |
|
|
126
|
+
* 'freeze-hold-new' | 'overwrite' | 'skip-omit' | 'copy-new' | 'prompt'
|
|
127
|
+
*/
|
|
128
|
+
function classifyFileOwnership({
|
|
129
|
+
relPath,
|
|
130
|
+
destExists,
|
|
131
|
+
existingContent,
|
|
132
|
+
incoming,
|
|
133
|
+
isPhaseFile = false,
|
|
134
|
+
isInstructionPhase = false,
|
|
135
|
+
frozen = false,
|
|
136
|
+
isCcOwned = false,
|
|
137
|
+
skipConflicts = false,
|
|
138
|
+
}) {
|
|
139
|
+
// 1. Project-owned seed — before any existsSync branch (act:2001bf54).
|
|
140
|
+
if (isProjectOwnedSeed(relPath)) {
|
|
141
|
+
return { action: 'seed', manifest: 'omit' };
|
|
142
|
+
}
|
|
143
|
+
if (destExists) {
|
|
144
|
+
// 2. Customized phase file → preserve (independent of skipPhases). Ahead
|
|
145
|
+
// of the identical check to match the walk; behavior-identical since
|
|
146
|
+
// identical content never trips this guard.
|
|
147
|
+
if (
|
|
148
|
+
isPhaseFile &&
|
|
149
|
+
!isInstructionPhase &&
|
|
150
|
+
existingContent.trim() !== '' &&
|
|
151
|
+
existingContent.trim() !== incoming.trim()
|
|
152
|
+
) {
|
|
153
|
+
return { action: 'preserve-phase', manifest: 'omit' };
|
|
154
|
+
}
|
|
155
|
+
// 3. Byte-identical → skip, recorded as CC-owned-unchanged.
|
|
156
|
+
if (existingContent === incoming) {
|
|
157
|
+
return { action: 'skip-identical', manifest: 'identical' };
|
|
158
|
+
}
|
|
159
|
+
// 4. Frozen module (act:c1013967): hold a CC-owned file at its on-disk
|
|
160
|
+
// hash; a project-owned file falls through.
|
|
161
|
+
if (frozen) {
|
|
162
|
+
const decision = freezeRetain({ existing: existingContent, isCcOwned });
|
|
163
|
+
if (decision.manifestHash) {
|
|
164
|
+
return { action: 'freeze-hold', manifest: 'frozen', manifestHash: decision.manifestHash };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 5. Non-interactive install (--yes / existing-install): manifest-driven.
|
|
168
|
+
if (skipConflicts) {
|
|
169
|
+
return isCcOwned
|
|
170
|
+
? { action: 'overwrite', manifest: 'hash' }
|
|
171
|
+
: { action: 'skip-omit', manifest: 'omit' };
|
|
172
|
+
}
|
|
173
|
+
// 6. Interactive → the caller prompts.
|
|
174
|
+
return { action: 'prompt', manifest: null };
|
|
175
|
+
}
|
|
176
|
+
// New file (dest absent): a frozen module holds it back; else copy.
|
|
177
|
+
if (frozen) return { action: 'freeze-hold-new', manifest: null };
|
|
178
|
+
return { action: 'copy-new', manifest: 'hash' };
|
|
179
|
+
}
|
|
180
|
+
|
|
53
181
|
/**
|
|
54
182
|
* Recursively copy files from src to dest, surfacing conflicts.
|
|
55
183
|
* Returns { copied: string[], skipped: string[], overwritten: string[] }
|
|
56
184
|
*/
|
|
57
|
-
async function copyTemplates(src, dest, { dryRun = false, skipConflicts = false, skipPhases = false, projectRoot = null, existingManifest = {}, instructionPhases = new Set() } = {}) {
|
|
58
|
-
const results = { copied: [], skipped: [], overwritten: [], manifest: {} };
|
|
59
|
-
await walkAndCopy(src, dest, src, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases);
|
|
185
|
+
async function copyTemplates(src, dest, { dryRun = false, skipConflicts = false, skipPhases = false, projectRoot = null, existingManifest = {}, instructionPhases = new Set(), frozen = false } = {}) {
|
|
186
|
+
const results = { copied: [], skipped: [], overwritten: [], manifest: {}, frozen: [] };
|
|
187
|
+
await walkAndCopy(src, dest, src, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases, frozen);
|
|
60
188
|
return results;
|
|
61
189
|
}
|
|
62
190
|
|
|
63
|
-
async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases) {
|
|
191
|
+
async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases, frozen) {
|
|
64
192
|
const entries = fs.readdirSync(currentSrc, { withFileTypes: true });
|
|
65
193
|
|
|
66
194
|
for (const entry of entries) {
|
|
@@ -80,11 +208,32 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
80
208
|
if (!dryRun && !fs.existsSync(destPath)) {
|
|
81
209
|
fs.mkdirSync(destPath, { recursive: true });
|
|
82
210
|
}
|
|
83
|
-
await walkAndCopy(srcRoot, destRoot, srcPath, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases);
|
|
211
|
+
await walkAndCopy(srcRoot, destRoot, srcPath, results, dryRun, skipConflicts, skipPhases, projectRoot, existingManifest, instructionPhases, frozen);
|
|
84
212
|
} else {
|
|
85
213
|
const incoming = fs.readFileSync(srcPath, 'utf8');
|
|
86
214
|
const incomingHash = hashContent(incoming);
|
|
87
215
|
|
|
216
|
+
// Project-owned seed (act:2001bf54): seed if absent, never overwrite,
|
|
217
|
+
// and ALWAYS omit from the manifest — recording it would let
|
|
218
|
+
// cc-upstream-guard block edits and a --yes reinstall clobber the
|
|
219
|
+
// consumer's content. Runs before any existsSync branch so neither the
|
|
220
|
+
// byte-identical path (which would re-record an unedited seed) nor the
|
|
221
|
+
// manifest-driven overwrite path can reach it.
|
|
222
|
+
if (isProjectOwnedSeed(relPath)) {
|
|
223
|
+
if (!fs.existsSync(destPath)) {
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
const dir = path.dirname(destPath);
|
|
226
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
227
|
+
fs.copyFileSync(srcPath, destPath);
|
|
228
|
+
}
|
|
229
|
+
results.copied.push(relPath);
|
|
230
|
+
} else {
|
|
231
|
+
results.skipped.push(relPath);
|
|
232
|
+
}
|
|
233
|
+
recordSkip(results.manifest, relPath); // omit — project-owned by declaration
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
88
237
|
if (fs.existsSync(destPath)) {
|
|
89
238
|
const existing = fs.readFileSync(destPath, 'utf8');
|
|
90
239
|
|
|
@@ -112,6 +261,24 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
112
261
|
continue;
|
|
113
262
|
}
|
|
114
263
|
|
|
264
|
+
// Frozen module (act:c1013967): the file differs and is past the
|
|
265
|
+
// identical + seed + customized-phase guards, so it is a CC-managed
|
|
266
|
+
// file that WOULD be overwritten. Hold it instead. Only a file already
|
|
267
|
+
// CC-owned (in the prior manifest) is retained at its on-disk hash; a
|
|
268
|
+
// project-owned file falls through to the normal skip-and-omit path.
|
|
269
|
+
if (frozen) {
|
|
270
|
+
const manifestKey = projectRoot ? path.relative(projectRoot, destPath) : relPath;
|
|
271
|
+
const decision = freezeRetain({ existing, isCcOwned: Boolean(existingManifest[manifestKey]) });
|
|
272
|
+
if (decision.manifestHash) {
|
|
273
|
+
results.skipped.push(relPath);
|
|
274
|
+
results.frozen.push(relPath);
|
|
275
|
+
results.manifest[relPath] = decision.manifestHash;
|
|
276
|
+
console.log(` ❄ Frozen (held): ${displayPath}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// decision.omit → project-owned; fall through.
|
|
280
|
+
}
|
|
281
|
+
|
|
115
282
|
if (skipConflicts) {
|
|
116
283
|
// Check if file is upstream-managed (in the old manifest).
|
|
117
284
|
// If so, overwrite — it can't be customized (hook enforces this).
|
|
@@ -177,6 +344,15 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
177
344
|
recordSkip(results.manifest, relPath);
|
|
178
345
|
}
|
|
179
346
|
} else {
|
|
347
|
+
// New file in a frozen module: hold it back so the on-disk module
|
|
348
|
+
// stays internally consistent at its pinned version. Report only — no
|
|
349
|
+
// manifest entry (there's no on-disk content to hash; a phantom entry
|
|
350
|
+
// would break cc-drift-check/cleanup). On unfreeze this branch creates it.
|
|
351
|
+
if (frozen) {
|
|
352
|
+
results.frozen.push(relPath);
|
|
353
|
+
console.log(` ❄ Frozen (held new): ${displayPath}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
180
356
|
if (!dryRun) {
|
|
181
357
|
const dir = path.dirname(destPath);
|
|
182
358
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -215,4 +391,4 @@ function showDiff(existing, incoming, relPath) {
|
|
|
215
391
|
console.log('');
|
|
216
392
|
}
|
|
217
393
|
|
|
218
|
-
module.exports = { copyTemplates, recordSkip };
|
|
394
|
+
module.exports = { copyTemplates, recordSkip, isProjectOwnedSeed, freezeRetain, classifyFileOwnership };
|
package/lib/db-setup.js
CHANGED
|
@@ -1,46 +1,151 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const metadata = require('./metadata');
|
|
5
|
+
|
|
6
|
+
// Substrings that identify a better-sqlite3 native-module ABI mismatch —
|
|
7
|
+
// the compiled .node binary was built under a different Node than the one
|
|
8
|
+
// running `pib-db.mjs init`. Node surfaces this as an ERR_DLOPEN_FAILED
|
|
9
|
+
// with a NODE_MODULE_VERSION line; field reports during consumer walks
|
|
10
|
+
// also showed the shorter "native module version mismatch" phrasing.
|
|
11
|
+
const ABI_MISMATCH_SIGNATURES = [
|
|
12
|
+
'NODE_MODULE_VERSION',
|
|
13
|
+
'different Node.js version',
|
|
14
|
+
'was compiled against',
|
|
15
|
+
'native module version mismatch',
|
|
16
|
+
'ERR_DLOPEN_FAILED',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function isAbiMismatch(text) {
|
|
20
|
+
const s = String(text || '');
|
|
21
|
+
return ABI_MISMATCH_SIGNATURES.some((sig) => s.includes(sig));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Read the bundled pib-db schema version from the (already-copied)
|
|
25
|
+
// pib-db-lib.mjs as TEXT — never by importing it, so learning the version
|
|
26
|
+
// does not itself load better-sqlite3. Returns a number, or null when the
|
|
27
|
+
// file is missing or the constant can't be parsed.
|
|
28
|
+
function readBundledSchemaVersion(scriptsDir) {
|
|
29
|
+
try {
|
|
30
|
+
const src = fs.readFileSync(path.join(scriptsDir, 'pib-db-lib.mjs'), 'utf8');
|
|
31
|
+
const m = src.match(/SCHEMA_VERSION\s*=\s*(\d+)/);
|
|
32
|
+
return m ? Number(m[1]) : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Lazy-init gate: a reinstall whose recorded schema version already matches
|
|
39
|
+
// the bundled one (and whose pib.db exists) needs no migration, so the init
|
|
40
|
+
// step can be skipped entirely — and a skipped init never loads the
|
|
41
|
+
// (possibly stale) native module. Conservative by construction: any
|
|
42
|
+
// missing/unequal input returns false, so we only skip when certain there
|
|
43
|
+
// is nothing to migrate.
|
|
44
|
+
function shouldSkipInit({ dbExists, recordedVersion, bundledVersion }) {
|
|
45
|
+
return Boolean(
|
|
46
|
+
dbExists &&
|
|
47
|
+
bundledVersion != null &&
|
|
48
|
+
recordedVersion != null &&
|
|
49
|
+
recordedVersion === bundledVersion
|
|
50
|
+
);
|
|
51
|
+
}
|
|
4
52
|
|
|
5
53
|
/**
|
|
6
54
|
* Set up the PIB database in the target project.
|
|
7
|
-
* - Copies
|
|
55
|
+
* - Copies are already in place (scripts/ copied by the main install loop)
|
|
8
56
|
* - Runs npm init if no package.json
|
|
9
|
-
* - Installs better-sqlite3
|
|
10
|
-
* -
|
|
57
|
+
* - Installs better-sqlite3 if absent
|
|
58
|
+
* - Initializes/migrates the database, UNLESS the schema is already current
|
|
59
|
+
* (lazy-init), and self-heals an ABI-mismatched native binary.
|
|
60
|
+
*
|
|
61
|
+
* Returns { results: string[], schemaVersion: number|null }. schemaVersion
|
|
62
|
+
* is the applied (or confirmed-current) schema version when init succeeded
|
|
63
|
+
* or was correctly skipped, and null when init could not complete (ABI
|
|
64
|
+
* mismatch with no working rebuild) — so the caller never records a false
|
|
65
|
+
* "schema current" claim in .ccrc.json.
|
|
66
|
+
*
|
|
67
|
+
* `opts.exec` and `opts.log` are injectable for testing; they default to
|
|
68
|
+
* execSync and console.log.
|
|
11
69
|
*/
|
|
12
|
-
function setupDb(projectDir) {
|
|
70
|
+
function setupDb(projectDir, opts = {}) {
|
|
71
|
+
const exec = opts.exec || ((cmd, o) => execSync(cmd, o));
|
|
72
|
+
const log = opts.log || console.log;
|
|
13
73
|
const scriptsDir = path.join(projectDir, 'scripts');
|
|
14
74
|
const results = [];
|
|
15
75
|
|
|
16
76
|
// Check if package.json exists; if not, init
|
|
17
77
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
18
78
|
if (!fs.existsSync(pkgPath)) {
|
|
19
|
-
|
|
20
|
-
|
|
79
|
+
log(' Initializing package.json...');
|
|
80
|
+
exec('npm init -y', { cwd: projectDir, stdio: 'pipe' });
|
|
21
81
|
results.push('Created package.json');
|
|
22
82
|
}
|
|
23
83
|
|
|
24
84
|
// pib-db.mjs uses ESM imports — .mjs extension lets Node treat it as ESM
|
|
25
85
|
// regardless of the project's package.json type field
|
|
86
|
+
const dbScript = path.join(scriptsDir, 'pib-db.mjs');
|
|
87
|
+
const dbPath = path.join(projectDir, 'pib.db');
|
|
88
|
+
const bundledVersion = readBundledSchemaVersion(scriptsDir);
|
|
89
|
+
|
|
90
|
+
// Lazy-init: skip when the schema is already current. This is the common
|
|
91
|
+
// reinstall case; skipping means a no-migration release never loads
|
|
92
|
+
// better-sqlite3, so a stale native binary can't wall the install with an
|
|
93
|
+
// ABI-mismatch error it never needed to risk.
|
|
94
|
+
const existingMeta = metadata.read(projectDir) || {};
|
|
95
|
+
if (
|
|
96
|
+
shouldSkipInit({
|
|
97
|
+
dbExists: fs.existsSync(dbPath),
|
|
98
|
+
recordedVersion: existingMeta.dbSchemaVersion,
|
|
99
|
+
bundledVersion,
|
|
100
|
+
})
|
|
101
|
+
) {
|
|
102
|
+
results.push(`Database schema already current (v${bundledVersion}) — skipped init`);
|
|
103
|
+
return { results, schemaVersion: bundledVersion };
|
|
104
|
+
}
|
|
26
105
|
|
|
27
|
-
// Install better-sqlite3 if not already installed
|
|
106
|
+
// Install better-sqlite3 if not already installed. A pre-existing but
|
|
107
|
+
// stale binary is left for the rebuild path below to heal — a plain
|
|
108
|
+
// reinstall wouldn't change an already-satisfied semver.
|
|
28
109
|
const nodeModules = path.join(projectDir, 'node_modules', 'better-sqlite3');
|
|
29
110
|
if (!fs.existsSync(nodeModules)) {
|
|
30
|
-
|
|
31
|
-
|
|
111
|
+
log(' Installing better-sqlite3...');
|
|
112
|
+
exec('npm install better-sqlite3', { cwd: projectDir, stdio: 'pipe' });
|
|
32
113
|
results.push('Installed better-sqlite3');
|
|
33
114
|
}
|
|
34
115
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (fs.existsSync(dbScript)) {
|
|
38
|
-
console.log(' Initializing database...');
|
|
39
|
-
execSync(`node ${dbScript} init`, { cwd: projectDir, stdio: 'pipe' });
|
|
40
|
-
results.push('Initialized pib.db');
|
|
116
|
+
if (!fs.existsSync(dbScript)) {
|
|
117
|
+
return { results, schemaVersion: null };
|
|
41
118
|
}
|
|
42
119
|
|
|
43
|
-
|
|
120
|
+
log(' Initializing database...');
|
|
121
|
+
try {
|
|
122
|
+
exec(`node ${dbScript} init`, { cwd: projectDir, stdio: 'pipe' });
|
|
123
|
+
results.push('Initialized pib.db');
|
|
124
|
+
return { results, schemaVersion: bundledVersion };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const detail = `${String(err.stderr || '')}\n${String(err.message || '')}`;
|
|
127
|
+
// Non-ABI failures keep their old handling — propagate to the caller's
|
|
128
|
+
// existing "Database setup failed" catch.
|
|
129
|
+
if (!isAbiMismatch(detail)) throw err;
|
|
130
|
+
|
|
131
|
+
// ABI mismatch: rebuild the native binary once and retry. If that
|
|
132
|
+
// works, the install self-heals. If it doesn't, fall back to a single
|
|
133
|
+
// actionable line — never a stack trace, never a silent skip.
|
|
134
|
+
log(' better-sqlite3 built for a different Node — rebuilding once and retrying...');
|
|
135
|
+
try {
|
|
136
|
+
exec('npm rebuild better-sqlite3', { cwd: projectDir, stdio: 'pipe' });
|
|
137
|
+
exec(`node ${dbScript} init`, { cwd: projectDir, stdio: 'pipe' });
|
|
138
|
+
results.push('Rebuilt better-sqlite3 (ABI mismatch) and initialized pib.db');
|
|
139
|
+
return { results, schemaVersion: bundledVersion };
|
|
140
|
+
} catch {
|
|
141
|
+
results.push(
|
|
142
|
+
'DB step skipped: better-sqlite3 was built for a different Node. Run ' +
|
|
143
|
+
'`npm rebuild better-sqlite3` then `node scripts/pib-db.mjs init` in ' +
|
|
144
|
+
'this project — your existing pib.db is untouched.'
|
|
145
|
+
);
|
|
146
|
+
return { results, schemaVersion: null };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
44
149
|
}
|
|
45
150
|
|
|
46
|
-
module.exports = { setupDb };
|
|
151
|
+
module.exports = { setupDb, isAbiMismatch, readBundledSchemaVersion, shouldSkipInit };
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const os = require('os');
|
|
23
|
-
const
|
|
23
|
+
const { sha256, compareVersions } = require('./installer-gate');
|
|
24
24
|
|
|
25
25
|
const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
|
|
26
26
|
const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
|
|
@@ -42,22 +42,6 @@ const DATA_DIRS = [
|
|
|
42
42
|
path.join(INSTALL_DIR, 'migrations'),
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
-
function sha256(content) {
|
|
46
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function compareVersions(a, b) {
|
|
50
|
-
const pa = a.split('.').map(Number);
|
|
51
|
-
const pb = b.split('.').map(Number);
|
|
52
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
53
|
-
const va = pa[i] || 0;
|
|
54
|
-
const vb = pb[i] || 0;
|
|
55
|
-
if (va > vb) return 1;
|
|
56
|
-
if (va < vb) return -1;
|
|
57
|
-
}
|
|
58
|
-
return 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
45
|
function readGlobalManifest() {
|
|
62
46
|
if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
|
|
63
47
|
try {
|
package/lib/engagement-setup.js
CHANGED
|
@@ -177,7 +177,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
|
|
|
177
177
|
target_fid TEXT,
|
|
178
178
|
packet_id TEXT,
|
|
179
179
|
kind TEXT NOT NULL
|
|
180
|
-
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent'
|
|
180
|
+
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent')),
|
|
181
181
|
author TEXT NOT NULL,
|
|
182
182
|
verdict TEXT CHECK(verdict IS NULL OR verdict IN ('approve','object','comment','none')),
|
|
183
183
|
body TEXT CHECK(body IS NULL OR length(body) <= 10000),
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* installer-gate.js — the SINGLE source of truth for the version/hash gate
|
|
3
|
+
* logic shared by CC's five runtime installers (mux, watchtower,
|
|
4
|
+
* engagement-server, verify, site-audit).
|
|
5
|
+
*
|
|
6
|
+
* Before this helper, `sha256()` was copy-pasted three times,
|
|
7
|
+
* `compareVersions()` twice, and the equal-version "don't hard-skip, fall
|
|
8
|
+
* through and copy only what changed" fix (act:df1a1dc3) was applied twice
|
|
9
|
+
* to the per-file-manifest installers while the two tarball installers
|
|
10
|
+
* (verify, site-audit) still hard-skipped on an equal version — so a
|
|
11
|
+
* template edit without a package.json bump never propagated. This module
|
|
12
|
+
* unifies all of it so the rule lives in one place ("one concept, one
|
|
13
|
+
* place"; technical-debt-0003, act:a6377c82).
|
|
14
|
+
*
|
|
15
|
+
* Two installer shapes delegate here:
|
|
16
|
+
* - Per-file manifest (mux, watchtower, engagement-server): use `sha256`
|
|
17
|
+
* + `compareVersions`; equal version falls through to a per-file
|
|
18
|
+
* hash-compare in the caller.
|
|
19
|
+
* - Tarball (verify, site-audit): npm-pack a runtime source dir into a
|
|
20
|
+
* versioned tarball. `tarballIsStale` gives them the equal-version
|
|
21
|
+
* fall-through — re-pack when the runtime SOURCE content changed even
|
|
22
|
+
* though the version is identical, instead of hard-skipping.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
/** SHA-256 hex digest of a string or Buffer. */
|
|
32
|
+
function sha256(content) {
|
|
33
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare two dotted numeric version strings.
|
|
38
|
+
* @returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
39
|
+
*/
|
|
40
|
+
function compareVersions(a, b) {
|
|
41
|
+
const pa = String(a).split('.').map(Number);
|
|
42
|
+
const pb = String(b).split('.').map(Number);
|
|
43
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
44
|
+
const va = pa[i] || 0;
|
|
45
|
+
const vb = pb[i] || 0;
|
|
46
|
+
if (va > vb) return 1;
|
|
47
|
+
if (va < vb) return -1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic content hash of a source directory tree: every file's
|
|
54
|
+
* relative path + bytes, in a stable sorted order. Two trees with identical
|
|
55
|
+
* file contents hash identically regardless of readdir order. node_modules
|
|
56
|
+
* and .git are ignored (build/VCS artifacts, not source).
|
|
57
|
+
*/
|
|
58
|
+
function hashSourceDir(dir, { ignore = ['node_modules', '.git'] } = {}) {
|
|
59
|
+
const entries = [];
|
|
60
|
+
const walk = (d, rel) => {
|
|
61
|
+
const names = fs.readdirSync(d, { withFileTypes: true });
|
|
62
|
+
for (const ent of names) {
|
|
63
|
+
if (ignore.includes(ent.name)) continue;
|
|
64
|
+
const full = path.join(d, ent.name);
|
|
65
|
+
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
66
|
+
if (ent.isDirectory()) walk(full, r);
|
|
67
|
+
else if (ent.isFile()) entries.push([r, fs.readFileSync(full)]);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(dir, '');
|
|
71
|
+
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
72
|
+
const hash = crypto.createHash('sha256');
|
|
73
|
+
for (const [r, buf] of entries) {
|
|
74
|
+
hash.update(r);
|
|
75
|
+
hash.update('\0');
|
|
76
|
+
hash.update(buf);
|
|
77
|
+
hash.update('\0');
|
|
78
|
+
}
|
|
79
|
+
return hash.digest('hex');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const SOURCE_HASH_FILE = '.source-hash';
|
|
83
|
+
|
|
84
|
+
/** Read the stored runtime-source hash sidecar from an install dir (or null). */
|
|
85
|
+
function readSourceHash(installDir) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.readFileSync(path.join(installDir, SOURCE_HASH_FILE), 'utf8').trim() || null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Persist the runtime-source hash sidecar next to the packed tarball. */
|
|
94
|
+
function writeSourceHash(installDir, hash) {
|
|
95
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
96
|
+
fs.writeFileSync(path.join(installDir, SOURCE_HASH_FILE), hash, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Equal-version fall-through decision for the TARBALL installers.
|
|
101
|
+
*
|
|
102
|
+
* A tarball is stale (must re-pack) when it is missing/truncated OR when the
|
|
103
|
+
* runtime source content changed since it was last packed (the sidecar hash
|
|
104
|
+
* differs / is absent). Otherwise it is fresh and the caller may skip. This
|
|
105
|
+
* is what stops verify/site-audit from hard-skipping a same-version reinstall
|
|
106
|
+
* that carries a real template change.
|
|
107
|
+
*
|
|
108
|
+
* @returns {{ stale: boolean, sourceHash: string, reason: string }}
|
|
109
|
+
*/
|
|
110
|
+
function tarballIsStale({ tarballPath, installDir, runtimeSourceDir, minSize = 1024 }) {
|
|
111
|
+
const sourceHash = hashSourceDir(runtimeSourceDir);
|
|
112
|
+
let size = 0;
|
|
113
|
+
try {
|
|
114
|
+
size = fs.statSync(tarballPath).size;
|
|
115
|
+
} catch {
|
|
116
|
+
size = 0;
|
|
117
|
+
}
|
|
118
|
+
if (size <= minSize) {
|
|
119
|
+
return { stale: true, sourceHash, reason: size === 0 ? 'missing' : 'truncated' };
|
|
120
|
+
}
|
|
121
|
+
const stored = readSourceHash(installDir);
|
|
122
|
+
if (!stored) return { stale: true, sourceHash, reason: 'no-source-hash' };
|
|
123
|
+
if (stored !== sourceHash) return { stale: true, sourceHash, reason: 'source-changed' };
|
|
124
|
+
return { stale: false, sourceHash, reason: 'unchanged' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
sha256,
|
|
129
|
+
compareVersions,
|
|
130
|
+
hashSourceDir,
|
|
131
|
+
readSourceHash,
|
|
132
|
+
writeSourceHash,
|
|
133
|
+
tarballIsStale,
|
|
134
|
+
SOURCE_HASH_FILE,
|
|
135
|
+
};
|
package/lib/metadata.js
CHANGED
|
@@ -44,7 +44,7 @@ function merge(projectDir, partial) {
|
|
|
44
44
|
return data;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function create(projectDir, { modules, skipped, version, manifest = {} }) {
|
|
47
|
+
function create(projectDir, { modules, skipped, version, manifest = {}, dbSchemaVersion } = {}) {
|
|
48
48
|
// Read existing first so unknown top-level keys (e.g.
|
|
49
49
|
// `migrated_from_omega` set by --migrate-memory) survive the
|
|
50
50
|
// install/upgrade rewrite. Only the install-specific fields below
|
|
@@ -60,6 +60,13 @@ function create(projectDir, { modules, skipped, version, manifest = {} }) {
|
|
|
60
60
|
manifest,
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
// Record the applied pib-db schema version when this install confirmed
|
|
64
|
+
// one (init ran or was skipped as already-current). When it didn't
|
|
65
|
+
// (--no-db, dry-run, or an unhealed ABI mismatch), `dbSchemaVersion` is
|
|
66
|
+
// undefined and any prior value rides through via `...existing` — we
|
|
67
|
+
// never claim "schema current" off the back of a skipped/failed init.
|
|
68
|
+
if (dbSchemaVersion != null) data.dbSchemaVersion = dbSchemaVersion;
|
|
69
|
+
|
|
63
70
|
for (const mod of modules) {
|
|
64
71
|
data.modules[mod] = true;
|
|
65
72
|
}
|
|
@@ -71,4 +78,46 @@ function create(projectDir, { modules, skipped, version, manifest = {} }) {
|
|
|
71
78
|
return data;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Read the project's frozen-module list from `.ccrc.json`. Returns a
|
|
83
|
+
* normalized, deduped `string[]` of module keys the operator has pinned
|
|
84
|
+
* (the temporary, manifest-RETAINING third ownership state — act:c1013967).
|
|
85
|
+
*
|
|
86
|
+
* Defensive by construction: a non-array `frozenModules` (shape drift) reads
|
|
87
|
+
* as "nothing frozen" rather than throwing, and a genuinely-absent `.ccrc.json`
|
|
88
|
+
* returns `[]`. It deliberately does NOT swallow a JSON parse error — `read()`
|
|
89
|
+
* throws on a malformed `.ccrc.json`, which HALTS the reinstall. That fail-
|
|
90
|
+
* closed behavior is the point: when the freeze state can't be read, the
|
|
91
|
+
* installer must stop, never proceed as if nothing were frozen (which would
|
|
92
|
+
* silently rewrite the very files a freeze exists to protect).
|
|
93
|
+
*/
|
|
94
|
+
function readFrozenModules(projectDir) {
|
|
95
|
+
const m = read(projectDir);
|
|
96
|
+
const raw = m && m.frozenModules;
|
|
97
|
+
if (!Array.isArray(raw)) return [];
|
|
98
|
+
return [...new Set(raw.filter((x) => typeof x === 'string'))];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Persist the frozen-module list, shallow-merged into `.ccrc.json` so every
|
|
103
|
+
* other field (manifest, modules, dbSchemaVersion, …) rides through. Input is
|
|
104
|
+
* normalized to a deduped string[]. Returns the written array.
|
|
105
|
+
*/
|
|
106
|
+
function setFrozenModules(projectDir, mods) {
|
|
107
|
+
const normalized = Array.isArray(mods)
|
|
108
|
+
? [...new Set(mods.filter((x) => typeof x === 'string'))]
|
|
109
|
+
: [];
|
|
110
|
+
merge(projectDir, { frozenModules: normalized });
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
read,
|
|
116
|
+
write,
|
|
117
|
+
merge,
|
|
118
|
+
create,
|
|
119
|
+
metadataPath,
|
|
120
|
+
METADATA_FILE,
|
|
121
|
+
readFrozenModules,
|
|
122
|
+
setFrozenModules,
|
|
123
|
+
};
|