create-claude-cabinet 0.50.0 → 0.51.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/lib/CLAUDE.md CHANGED
@@ -53,7 +53,15 @@ No build step; CommonJS.
53
53
  `patterns-scope-check.mjs`).
54
54
  - `lib/installer-gate.js` — shared version/hash gating for the setup
55
55
  engines (Wave 1, act:a6377c82). Exports `sha256`, `compareVersions`,
56
- `hashSourceDir`, `tarballIsStale`. Before it, `sha256()` was
56
+ `hashSourceDir`, `tarballIsStale`, and since act:9a0af01a —
57
+ `readGlobalManifest`/`writeGlobalManifest` (+ `ccHome`/
58
+ `globalManifestPath`), hoisted here from mux-setup, watchtower-setup, and
59
+ engagement-server-setup, which each carried a BYTE-IDENTICAL copy of the
60
+ pair: the same duplication condition that created this module in the first
61
+ place. All four user-level engines (the fourth being output-register) now
62
+ delegate; the shape-drift guard (a non-object top level or non-object
63
+ `files` reads as empty) lives in the one copy, since every caller indexes
64
+ `manifest.files[dest]` unconditionally. Before it, `sha256()` was
57
65
  copy-pasted three times and `compareVersions()` twice, and the
58
66
  equal-version "don't hard-skip, fall through to a per-file hash-compare"
59
67
  rule was re-implemented per installer. Per-file-manifest installers
@@ -195,6 +203,27 @@ No build step; CommonJS.
195
203
  follows mux-setup.js pattern with global manifest SHA256 tracking,
196
204
  incl. the equal-version hash-compare fall-through of act:df1a1dc3 —
197
205
  equal versions copy only changed files, `'unchanged'` when none do).
206
+ - `lib/output-register.js` — Cabinet Register output-style installer
207
+ (act:9a0af01a). Seeds `templates/output-styles/*.md` (GLOBBED, never
208
+ hand-enumerated — the orphan test only scans `templates/skills/`) into the
209
+ user-level `~/.claude/output-styles/`, then seeds `outputStyle` into the
210
+ PROJECT's `.claude/settings.json`. Unlike the other four engines this one
211
+ is SEED-ONLY: `isProjectOwnedSeed()` semantics (never overwrite — the
212
+ operator hand-tunes their register) PLUS loud validation of the INSTALLED
213
+ copy, because neither sibling engine's ownership model is right here (mux
214
+ never re-examines a hand edit → the footgun persists; watchtower reverts
215
+ hand edits every reinstall). Its load-bearing export is `versionGate()`:
216
+ `keep-coding-instructions` only exists as of Claude Code **v2.0.37**, and
217
+ below that the field is silently ignored, its `false` default applies, and
218
+ CC's software-engineering instructions are STRIPPED with no error — a
219
+ hazard NO file-content test can see, so the guard shells `claude --version`
220
+ (refuse below the floor; fail OPEN when absent/unparseable, since absence is
221
+ not an old version). `resolveRegisterLevel()` is a three-valued
222
+ `{level, ours, source}` ENUMERATOR (blind to force-for-plugin / CLI /
223
+ managed layers, and says so); `REGISTER_STYLE_NAME` must equal the style's
224
+ frontmatter `name` — CC keys custom styles by that field verbatim. Full
225
+ rationale in `templates/CLAUDE.md` § output-register; 40 tests in
226
+ `test/output-register/`.
198
227
  - `lib/mux-setup.js` — mux runtime installer (copies bin/mux, config
199
228
  scripts, and popup to user-level paths; creates data dirs;
200
229
  `setupDarwinIntegration()` appends source-file to `~/.tmux.conf`,
package/lib/cli.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const crypto = require('crypto');
6
6
  const { copyTemplates, recordSkip, isProjectOwnedSeed, classifyFileOwnership } = require('./copy');
7
- const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeCcDevHooks, mergeSkillOverrides } = require('./settings-merge');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeRegisterHooks, mergeCcDevHooks, mergeSkillOverrides } = require('./settings-merge');
8
8
  const { create: createMetadata, read: readMetadata, readFrozenModules, setFrozenModules, readMcpOptOut } = require('./metadata');
9
9
  const { setupDb, shouldSkipInit, readBundledSchemaVersion } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
@@ -13,6 +13,7 @@ const { setupEngagement } = require('./engagement-setup');
13
13
  const { setupMux } = require('./mux-setup');
14
14
  const { setupEngagementServer } = require('./engagement-server-setup');
15
15
  const { refreshWatchtowerRuntime } = require('./watchtower-setup');
16
+ const { setupOutputRegister, renderRegisterReport } = require('./output-register');
16
17
  const { reset } = require('./reset');
17
18
 
18
19
  const VERSION = require('../package.json').version;
@@ -635,6 +636,7 @@ function parseArgs(argv) {
635
636
  else if (arg === '--migrate-memory') flags.migrateMemory = true;
636
637
  else if (arg === '--unmigrate-memory') flags.unmigrateMemory = true;
637
638
  else if (arg === '--list-frozen') flags.listFrozen = true;
639
+ else if (arg === '--register') flags.register = true;
638
640
  else if (arg === '--modules' && i + 1 < args.length) {
639
641
  flags.modules = args[++i].split(',').map(s => s.trim()).filter(Boolean);
640
642
  }
@@ -692,6 +694,11 @@ function printHelp() {
692
694
  --unfreeze [keys] Clear the freeze for the given module(s), or all if no
693
695
  keys. Re-run the installer to refresh the unfrozen module(s).
694
696
  --list-frozen Show which modules are frozen (+ their closure) and exit.
697
+ --register Show where the Cabinet Register output style stands: every
698
+ settings file that names an outputStyle and what each says.
699
+ Enumerates only — it cannot see plugin force-for-plugin, CLI
700
+ --settings, or managed policy, and says so. Use /status and
701
+ /context for ground truth. Exits without installing.
695
702
  --reset Remove Claude Cabinet files (uses manifest for safety)
696
703
  --force With --reset: remove even customized files;
697
704
  with --migrate-memory: override already-migrated guard
@@ -843,6 +850,18 @@ async function run() {
843
850
  }
844
851
  }
845
852
 
853
+ // Standalone register enumeration (act:9a0af01a). Prints every settings file
854
+ // that mentions outputStyle and what each says, then names the layers it
855
+ // could NOT consult and points at /status + /context for ground truth. An
856
+ // ENUMERATOR, never a verdict — see renderRegisterReport.
857
+ if (flags.register) {
858
+ const projectDir = path.resolve(flags.targetDir);
859
+ console.log('');
860
+ for (const line of renderRegisterReport({ projectDir })) console.log(line);
861
+ console.log('');
862
+ return;
863
+ }
864
+
846
865
  if (flags.migrateMemory) {
847
866
  const { migrateMemoryCmd } = require('./migrate-memory-cmd');
848
867
  const projectDir = path.resolve(flags.targetDir);
@@ -1422,6 +1441,11 @@ async function run() {
1422
1441
  console.log(' ⚙️ Registered bash-output compression PostToolUse hook');
1423
1442
  }
1424
1443
 
1444
+ if (selectedModules.includes('output-register')) {
1445
+ mergeRegisterHooks(settingsPath);
1446
+ console.log(' ⚙️ Registered Cabinet Register reinforcement UserPromptSubmit hook');
1447
+ }
1448
+
1425
1449
  // Repo-local maintainer hooks — no-op unless this IS the CC source repo
1426
1450
  // (dogfood). Registers the node --check PostToolUse gate durably so a
1427
1451
  // reinstall can't silently regenerate it away (architecture-0004).
@@ -1537,6 +1561,7 @@ async function run() {
1537
1561
  'engagement-setup': setupEngagement,
1538
1562
  'mux-setup': setupMux,
1539
1563
  'engagement-server-setup': setupEngagementServer,
1564
+ 'output-register-setup': setupOutputRegister,
1540
1565
  };
1541
1566
  for (const moduleKey of selectedModules) {
1542
1567
  const mod = MODULES[moduleKey];
@@ -20,10 +20,9 @@
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
22
  const os = require('os');
23
- const { sha256, compareVersions } = require('./installer-gate');
23
+ const { sha256, compareVersions, readGlobalManifest, writeGlobalManifest } = require('./installer-gate');
24
24
 
25
25
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
26
- const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
27
26
  const INSTALL_DIR = path.join(CC_HOME, 'engagement-server');
28
27
  const VERSION_FILE = path.join(INSTALL_DIR, '.cc-version');
29
28
  const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'engagement-server');
@@ -42,25 +41,6 @@ const DATA_DIRS = [
42
41
  path.join(INSTALL_DIR, 'migrations'),
43
42
  ];
44
43
 
45
- function readGlobalManifest() {
46
- if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
47
- try {
48
- const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
49
- // Guard shape drift: manifest.files is indexed unconditionally below.
50
- if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
51
- if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
52
- return m;
53
- } catch {
54
- return { files: {} };
55
- }
56
- }
57
-
58
- function writeGlobalManifest(manifest) {
59
- fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
60
- const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
61
- fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
62
- fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
63
- }
64
44
 
65
45
  function readInstalledVersion() {
66
46
  if (!fs.existsSync(VERSION_FILE)) return null;
@@ -26,8 +26,26 @@
26
26
 
27
27
  const crypto = require('crypto');
28
28
  const fs = require('fs');
29
+ const os = require('os');
29
30
  const path = require('path');
30
31
 
32
+ // The user-level install root and the SHA256 manifest every user-level engine
33
+ // records its managed files in. Resolved per-call (not frozen at module load)
34
+ // because the tests point HOME at a sandbox in a child process.
35
+ //
36
+ // `home` is injectable for the engines that take an in-process `home` option
37
+ // instead of a child-process $HOME (output-register does). Without it, such an
38
+ // engine threads its sandbox home to its file destinations but still resolves
39
+ // the manifest against the REAL os.homedir() — a half-injection that writes
40
+ // dead temp paths into the operator's live manifest on every test run
41
+ // (act:9a0af01a; caught by the QA drain, 8 stale entries deep).
42
+ function ccHome(home = os.homedir()) {
43
+ return path.join(home, '.claude-cabinet');
44
+ }
45
+ function globalManifestPath(home = os.homedir()) {
46
+ return path.join(ccHome(home), 'global-manifest.json');
47
+ }
48
+
31
49
  /** SHA-256 hex digest of a string or Buffer. */
32
50
  function sha256(content) {
33
51
  return crypto.createHash('sha256').update(content).digest('hex');
@@ -124,6 +142,40 @@ function tarballIsStale({ tarballPath, installDir, runtimeSourceDir, minSize = 1
124
142
  return { stale: false, sourceHash, reason: 'unchanged' };
125
143
  }
126
144
 
145
+ /**
146
+ * Read the user-level global manifest (`~/.claude-cabinet/global-manifest.json`).
147
+ *
148
+ * Hoisted here from the three user-level engines (mux-setup, watchtower-setup,
149
+ * engagement-server-setup), which each carried a byte-identical copy of this
150
+ * function and its writer — the exact duplication condition that created this
151
+ * module ("one concept, one place"). A fourth copy was about to land with
152
+ * output-register; instead all four delegate.
153
+ *
154
+ * Shape-drift defensive: a non-object top level, or a non-object `files`, reads
155
+ * as an empty manifest, because every caller indexes `manifest.files[dest]`
156
+ * unconditionally. Never throws — an unreadable/corrupt manifest degrades to
157
+ * "nothing recorded", which makes callers re-copy rather than silently skip.
158
+ */
159
+ function readGlobalManifest(manifestPath = globalManifestPath()) {
160
+ if (!fs.existsSync(manifestPath)) return { files: {} };
161
+ try {
162
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
163
+ if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
164
+ if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
165
+ return m;
166
+ } catch {
167
+ return { files: {} };
168
+ }
169
+ }
170
+
171
+ /** Persist the global manifest atomically (tmp + rename). */
172
+ function writeGlobalManifest(manifest, manifestPath = globalManifestPath()) {
173
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
174
+ const tmp = manifestPath + '.tmp';
175
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
176
+ fs.renameSync(tmp, manifestPath);
177
+ }
178
+
127
179
  module.exports = {
128
180
  sha256,
129
181
  compareVersions,
@@ -131,5 +183,9 @@ module.exports = {
131
183
  readSourceHash,
132
184
  writeSourceHash,
133
185
  tarballIsStale,
186
+ readGlobalManifest,
187
+ writeGlobalManifest,
188
+ ccHome,
189
+ globalManifestPath,
134
190
  SOURCE_HASH_FILE,
135
191
  };
package/lib/modules.js CHANGED
@@ -278,6 +278,21 @@ const MODULES = {
278
278
  postInstall: 'engagement-server-setup',
279
279
  templates: [],
280
280
  },
281
+ 'output-register': {
282
+ name: 'Cabinet Register (plain-English output style)',
283
+ description: 'Makes plain-English, decision-level communication the DEFAULT rather than guidance that decays. Ships a Claude Code output style (the contract lands in the system prompt, re-sent every request) to ~/.claude/output-styles/, plus a one-line-per-turn UserPromptSubmit reminder — custom styles get no per-turn reminder from the platform, only built-ins do. Code, paths, errors, and every filed artifact stay technical. On by default; the style is seeded, never clobbered, so a project that already sets an output style is left alone. Requires Claude Code v2.0.37+ (below that the engine refuses, loudly, and installs nothing).',
284
+ mandatory: false,
285
+ default: true,
286
+ lean: false,
287
+ requires: ['hooks'],
288
+ postInstall: 'output-register-setup',
289
+ // Project-level payload only: the per-turn hook. The STYLE itself is
290
+ // user-level (~/.claude/output-styles/ is where Claude Code reads custom
291
+ // styles from) and is seeded by the postInstall engine, which is why this
292
+ // array doesn't list it. Same shape as engagement-server (templates: [] +
293
+ // postInstall + user-level payload) — an existing, green precedent.
294
+ templates: ['hooks/register-reinforce.sh'],
295
+ },
281
296
  'bash-compress': {
282
297
  name: 'Bash Output Compression Hook',
283
298
  description: 'PostToolUse hook that compresses noisy Bash stdout (git status walls, npm/yarn install output, find/ls dumps) to reclaim context in long sessions. Off by default. stderr and error/warning lines pass through verbatim; every rewrite carries a visible [compressed] marker; fail-open on any error. Requires the hooks module (it wires into .claude/settings.json).',
package/lib/mux-setup.js CHANGED
@@ -19,10 +19,8 @@
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
  const os = require('os');
22
- const { sha256, compareVersions } = require('./installer-gate');
22
+ const { sha256, compareVersions, readGlobalManifest, writeGlobalManifest } = require('./installer-gate');
23
23
 
24
- const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
25
- const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
26
24
  const MUX_VERSION_FILE = path.join(os.homedir(), '.config', 'mux', '.cc-version');
27
25
  const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'mux');
28
26
 
@@ -65,25 +63,6 @@ const DATA_DIRS = [
65
63
  path.join(os.homedir(), '.local', 'share', 'mux', 'qa-handoff'),
66
64
  ];
67
65
 
68
- function readGlobalManifest() {
69
- if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
70
- try {
71
- const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
72
- // Guard shape drift: manifest.files is indexed unconditionally below.
73
- if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
74
- if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
75
- return m;
76
- } catch {
77
- return { files: {} };
78
- }
79
- }
80
-
81
- function writeGlobalManifest(manifest) {
82
- fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
83
- const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
84
- fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
85
- fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
86
- }
87
66
 
88
67
  function readInstalledVersion() {
89
68
  if (!fs.existsSync(MUX_VERSION_FILE)) return null;