hypomnema 1.0.1 → 1.2.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +12 -5
- package/README.md +12 -5
- package/commands/audit.md +46 -0
- package/commands/crystallize.md +113 -23
- package/commands/feedback.md +40 -26
- package/commands/ingest.md +31 -9
- package/commands/upgrade.md +2 -2
- package/docs/ARCHITECTURE.md +83 -9
- package/docs/CONTRIBUTING.md +2 -2
- package/hooks/hooks.json +39 -1
- package/hooks/hypo-auto-commit.mjs +23 -4
- package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
- package/hooks/hypo-auto-stage.mjs +9 -5
- package/hooks/hypo-compact-guard.mjs +33 -24
- package/hooks/hypo-cwd-change.mjs +107 -24
- package/hooks/hypo-file-watch.mjs +23 -10
- package/hooks/hypo-first-prompt.mjs +37 -23
- package/hooks/hypo-hot-rebuild.mjs +31 -8
- package/hooks/hypo-lookup.mjs +171 -65
- package/hooks/hypo-personal-check.mjs +207 -112
- package/hooks/hypo-pre-commit.mjs +46 -0
- package/hooks/hypo-session-end.mjs +58 -0
- package/hooks/hypo-session-record.mjs +60 -0
- package/hooks/hypo-session-start.mjs +312 -44
- package/hooks/hypo-shared.mjs +880 -28
- package/hooks/hypo-web-fetch-ingest.mjs +121 -0
- package/hooks/version-check-fetch.mjs +74 -0
- package/hooks/version-check.mjs +184 -0
- package/package.json +17 -3
- package/scripts/crystallize.mjs +623 -18
- package/scripts/doctor.mjs +739 -46
- package/scripts/feedback-sync.mjs +974 -0
- package/scripts/feedback.mjs +253 -44
- package/scripts/graph.mjs +35 -22
- package/scripts/ingest.mjs +89 -16
- package/scripts/init.mjs +442 -114
- package/scripts/lib/design-history-stale.mjs +83 -0
- package/scripts/lib/extensions.mjs +749 -0
- package/scripts/lib/frontmatter.mjs +5 -1
- package/scripts/lib/hypo-ignore.mjs +12 -10
- package/scripts/lib/pkg-json.mjs +23 -5
- package/scripts/lib/project-create.mjs +225 -0
- package/scripts/lib/schema-vocab.mjs +96 -0
- package/scripts/lint.mjs +238 -31
- package/scripts/query.mjs +26 -10
- package/scripts/resume.mjs +11 -5
- package/scripts/session-audit.mjs +277 -0
- package/scripts/smoke-pack.mjs +224 -0
- package/scripts/stats.mjs +24 -10
- package/scripts/uninstall.mjs +369 -48
- package/scripts/upgrade.mjs +766 -195
- package/scripts/verify.mjs +24 -14
- package/scripts/weekly-report.mjs +211 -0
- package/skills/crystallize/SKILL.md +24 -7
- package/skills/graph/SKILL.md +4 -0
- package/skills/ingest/SKILL.md +29 -5
- package/skills/lint/SKILL.md +4 -0
- package/skills/query/SKILL.md +4 -0
- package/skills/verify/SKILL.md +4 -0
- package/templates/.hypoignore +19 -2
- package/templates/Home.md +2 -0
- package/templates/SCHEMA.md +61 -6
- package/templates/extensions/agents/.gitkeep +0 -0
- package/templates/extensions/commands/.gitkeep +0 -0
- package/templates/extensions/hooks/.gitkeep +0 -0
- package/templates/extensions/skills/.gitkeep +0 -0
- package/templates/gitignore +5 -0
- package/templates/hot.md +2 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +63 -1
- package/templates/hypo-help.md +1 -1
- package/templates/pages/observability/_index.md +77 -0
- package/templates/projects/_template/index.md +2 -2
- package/templates/projects/_template/prd.md +1 -1
package/scripts/upgrade.mjs
CHANGED
|
@@ -12,37 +12,78 @@
|
|
|
12
12
|
* Options:
|
|
13
13
|
* --hypo-dir=<path> Hypomnema root directory (default: resolved via HYPO_DIR / hypo-config.md scan / ~/hypomnema)
|
|
14
14
|
* --apply Apply hook file updates and settings.json merges
|
|
15
|
+
* --force-commands Overwrite user-modified slash command files (creates .bak)
|
|
16
|
+
* --force-extensions Overwrite user-modified / conflicting extension copies (creates .bak)
|
|
17
|
+
* --codex Mirror to ~/.codex/{hooks,commands,settings.json} — core
|
|
18
|
+
* hook drift/apply, settings.json registration, the
|
|
19
|
+
* wiki-*.mjs → hypo-*.mjs rename migration (fix #48), and
|
|
20
|
+
* the user-extensions companion sync (E4 / fix #32).
|
|
15
21
|
* --json Output results as JSON
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
copyFileSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
renameSync,
|
|
32
|
+
unlinkSync,
|
|
33
|
+
} from 'fs';
|
|
19
34
|
import { join } from 'path';
|
|
20
35
|
import { homedir } from 'os';
|
|
21
36
|
import { fileURLToPath } from 'url';
|
|
22
37
|
import { createHash } from 'crypto';
|
|
23
38
|
import { resolveHypoRoot, expandHome } from './lib/hypo-root.mjs';
|
|
24
39
|
import { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
import {
|
|
41
|
+
readPkgJson as readPkgJsonSafe,
|
|
42
|
+
writePkgJsonAtomic,
|
|
43
|
+
sha256 as sha256Buf,
|
|
44
|
+
isRegularFile,
|
|
45
|
+
readFileIfRegular,
|
|
46
|
+
} from './lib/pkg-json.mjs';
|
|
47
|
+
import { syncExtensions } from './lib/extensions.mjs';
|
|
48
|
+
|
|
49
|
+
const HOME = homedir();
|
|
50
|
+
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
51
|
+
const PKG_ROOT = join(SCRIPT_DIR, '..');
|
|
52
|
+
|
|
53
|
+
// Shown after every fatal package-integrity error. These conditions mean the
|
|
54
|
+
// shipped hooks/hooks.json is missing or malformed — never a user mistake —
|
|
55
|
+
// so the only useful next step is a re-install of the package.
|
|
56
|
+
const PKG_INTEGRITY_HINT =
|
|
57
|
+
'→ This indicates a corrupt or incomplete install. Re-install with `npm install -g hypomnema` (or re-install the Claude Code plugin).';
|
|
58
|
+
const HOOKS_SRC = join(PKG_ROOT, 'hooks');
|
|
31
59
|
const COMMANDS_SRC = join(PKG_ROOT, 'commands');
|
|
32
|
-
const TEMPLATES
|
|
60
|
+
const TEMPLATES = join(PKG_ROOT, 'templates');
|
|
33
61
|
|
|
34
|
-
function sha256(buf) {
|
|
35
|
-
|
|
62
|
+
function sha256(buf) {
|
|
63
|
+
return sha256Buf(buf);
|
|
64
|
+
}
|
|
65
|
+
function pkgJsonPath() {
|
|
66
|
+
return join(HOME, '.claude', 'hypo-pkg.json');
|
|
67
|
+
}
|
|
36
68
|
|
|
37
69
|
// ── arg parsing ──────────────────────────────────────────────────────────────
|
|
38
70
|
|
|
39
71
|
function parseArgs(argv) {
|
|
40
|
-
const args = {
|
|
72
|
+
const args = {
|
|
73
|
+
hypoDir: null,
|
|
74
|
+
apply: false,
|
|
75
|
+
json: false,
|
|
76
|
+
forceCommands: false,
|
|
77
|
+
forceExtensions: false,
|
|
78
|
+
codex: false,
|
|
79
|
+
};
|
|
41
80
|
for (const arg of argv.slice(2)) {
|
|
42
81
|
if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
|
|
43
|
-
else if (arg === '--apply')
|
|
44
|
-
else if (arg === '--force-commands')
|
|
45
|
-
else if (arg === '--
|
|
82
|
+
else if (arg === '--apply') args.apply = true;
|
|
83
|
+
else if (arg === '--force-commands') args.forceCommands = true;
|
|
84
|
+
else if (arg === '--force-extensions') args.forceExtensions = true;
|
|
85
|
+
else if (arg === '--codex') args.codex = true;
|
|
86
|
+
else if (arg === '--json') args.json = true;
|
|
46
87
|
}
|
|
47
88
|
if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
|
|
48
89
|
return args;
|
|
@@ -75,14 +116,21 @@ try {
|
|
|
75
116
|
_hookConfig = JSON.parse(readFileSync(join(PKG_ROOT, 'hooks', 'hooks.json'), 'utf-8'));
|
|
76
117
|
} catch {
|
|
77
118
|
console.error(`Error: cannot read hooks/hooks.json from package root: ${PKG_ROOT}`);
|
|
119
|
+
console.error(PKG_INTEGRITY_HINT);
|
|
78
120
|
process.exit(1);
|
|
79
121
|
}
|
|
80
122
|
if (!_hookConfig || typeof _hookConfig !== 'object' || Array.isArray(_hookConfig)) {
|
|
81
123
|
console.error('Error: hooks/hooks.json must be a JSON object');
|
|
124
|
+
console.error(PKG_INTEGRITY_HINT);
|
|
82
125
|
process.exit(1);
|
|
83
126
|
}
|
|
84
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
!_hookConfig.hooks ||
|
|
129
|
+
typeof _hookConfig.hooks !== 'object' ||
|
|
130
|
+
Array.isArray(_hookConfig.hooks)
|
|
131
|
+
) {
|
|
85
132
|
console.error('Error: hooks/hooks.json must contain a "hooks" object');
|
|
133
|
+
console.error(PKG_INTEGRITY_HINT);
|
|
86
134
|
process.exit(1);
|
|
87
135
|
}
|
|
88
136
|
function _extractCommandFileName(command) {
|
|
@@ -98,53 +146,66 @@ function _isHookFileName(file) {
|
|
|
98
146
|
}
|
|
99
147
|
|
|
100
148
|
function _isHookGroup(group) {
|
|
101
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
group &&
|
|
102
151
|
typeof group === 'object' &&
|
|
103
152
|
!Array.isArray(group) &&
|
|
104
153
|
Array.isArray(group.hooks) &&
|
|
105
154
|
group.hooks.length > 0 &&
|
|
106
|
-
group.hooks.every(
|
|
107
|
-
hook
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
group.hooks.every(
|
|
156
|
+
(hook) =>
|
|
157
|
+
hook &&
|
|
158
|
+
typeof hook === 'object' &&
|
|
159
|
+
!Array.isArray(hook) &&
|
|
160
|
+
hook.type === 'command' &&
|
|
161
|
+
_extractCommandFileName(hook.command),
|
|
162
|
+
)
|
|
163
|
+
);
|
|
113
164
|
}
|
|
114
165
|
|
|
115
166
|
// Extract .mjs file names from both old format (string[]) and new format (hook-group object[])
|
|
116
167
|
function _extractFileNames(groups) {
|
|
117
|
-
return groups.flatMap(group => {
|
|
168
|
+
return groups.flatMap((group) => {
|
|
118
169
|
if (typeof group === 'string') return [group.trim()];
|
|
119
|
-
return group.hooks.map(hook => _extractCommandFileName(hook.command));
|
|
170
|
+
return group.hooks.map((hook) => _extractCommandFileName(hook.command));
|
|
120
171
|
});
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
for (const [event, groups] of Object.entries(_hookConfig.hooks)) {
|
|
124
|
-
const valid =
|
|
175
|
+
const valid =
|
|
176
|
+
Array.isArray(groups) &&
|
|
125
177
|
groups.length > 0 &&
|
|
126
|
-
groups.every(group => _isHookFileName(group) || _isHookGroup(group)) &&
|
|
178
|
+
groups.every((group) => _isHookFileName(group) || _isHookGroup(group)) &&
|
|
127
179
|
_extractFileNames(groups).length > 0;
|
|
128
180
|
if (!valid) {
|
|
129
|
-
console.error(
|
|
181
|
+
console.error(
|
|
182
|
+
`Error: hooks/hooks.json "hooks.${event}" must be a non-empty array of .mjs file names or Claude hook groups`,
|
|
183
|
+
);
|
|
184
|
+
console.error(PKG_INTEGRITY_HINT);
|
|
130
185
|
process.exit(1);
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
_hookConfig.shared !== undefined &&
|
|
190
|
+
(!Array.isArray(_hookConfig.shared) || !_hookConfig.shared.every((f) => _isHookFileName(f)))
|
|
191
|
+
) {
|
|
134
192
|
console.error('Error: hooks/hooks.json "shared" must be an array of .mjs file names');
|
|
193
|
+
console.error(PKG_INTEGRITY_HINT);
|
|
135
194
|
process.exit(1);
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
const HOOK_MAP
|
|
197
|
+
const HOOK_MAP = Object.fromEntries(
|
|
198
|
+
Object.entries(_hookConfig.hooks).map(([e, gs]) => [e, _extractFileNames(gs)]),
|
|
199
|
+
);
|
|
139
200
|
const SHARED_FILES = _hookConfig.shared ?? [];
|
|
140
201
|
|
|
141
202
|
// ── checks ───────────────────────────────────────────────────────────────────
|
|
142
203
|
|
|
143
204
|
function checkSchemaVersion(hypoDir) {
|
|
144
|
-
const pkgPath
|
|
205
|
+
const pkgPath = join(TEMPLATES, 'SCHEMA.md');
|
|
145
206
|
const hypoPath = join(hypoDir, 'SCHEMA.md');
|
|
146
207
|
|
|
147
|
-
const pkgVersion
|
|
208
|
+
const pkgVersion = existsSync(pkgPath)
|
|
148
209
|
? parseVersion((parseFrontmatter(readFileSync(pkgPath, 'utf-8')) ?? {}).version)
|
|
149
210
|
: null;
|
|
150
211
|
const hypoVersion = existsSync(hypoPath)
|
|
@@ -153,21 +214,23 @@ function checkSchemaVersion(hypoDir) {
|
|
|
153
214
|
|
|
154
215
|
return {
|
|
155
216
|
installed: hypoVersion?.raw ?? null,
|
|
156
|
-
current:
|
|
157
|
-
bump:
|
|
217
|
+
current: pkgVersion?.raw ?? null,
|
|
218
|
+
bump: bumpType(hypoVersion, pkgVersion),
|
|
158
219
|
hypoPath,
|
|
159
220
|
pkgPath,
|
|
160
221
|
};
|
|
161
222
|
}
|
|
162
223
|
|
|
163
|
-
|
|
164
|
-
|
|
224
|
+
// Target-aware: the same core-hook integrity check runs against ~/.claude/hooks/
|
|
225
|
+
// for the default claude target, and against ~/.codex/hooks/ when --codex is set
|
|
226
|
+
// (ADR 0024). The function reads only — apply happens in applyHookFiles.
|
|
227
|
+
function checkHookFiles(hooksDir) {
|
|
165
228
|
const results = [];
|
|
166
229
|
|
|
167
230
|
const allFiles = [...Object.values(HOOK_MAP).flat(), ...SHARED_FILES];
|
|
168
231
|
for (const file of allFiles) {
|
|
169
|
-
const installedPath = join(
|
|
170
|
-
const srcPath
|
|
232
|
+
const installedPath = join(hooksDir, file);
|
|
233
|
+
const srcPath = join(HOOKS_SRC, file);
|
|
171
234
|
|
|
172
235
|
if (!existsSync(installedPath)) {
|
|
173
236
|
results.push({ file, status: 'missing', installedPath, srcPath });
|
|
@@ -175,7 +238,7 @@ function checkHookFiles() {
|
|
|
175
238
|
results.push({ file, status: 'src-missing', installedPath, srcPath });
|
|
176
239
|
} else {
|
|
177
240
|
const installedContent = readFileSync(installedPath, 'utf-8');
|
|
178
|
-
const srcContent
|
|
241
|
+
const srcContent = readFileSync(srcPath, 'utf-8');
|
|
179
242
|
results.push({
|
|
180
243
|
file,
|
|
181
244
|
status: installedContent === srcContent ? 'up-to-date' : 'stale',
|
|
@@ -188,24 +251,27 @@ function checkHookFiles() {
|
|
|
188
251
|
return results;
|
|
189
252
|
}
|
|
190
253
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
// Same target-aware shape as checkHookFiles — the registered command string is
|
|
255
|
+
// reconstructed from `hooksDir`, so a codex check verifies that ~/.codex/settings.json
|
|
256
|
+
// references ~/.codex/hooks/<file>.mjs (not the claude path).
|
|
257
|
+
function checkSettingsJson(settingsPath, hooksDir) {
|
|
258
|
+
const results = [];
|
|
195
259
|
|
|
196
260
|
let settings = {};
|
|
197
261
|
if (existsSync(settingsPath)) {
|
|
198
|
-
try {
|
|
262
|
+
try {
|
|
263
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
264
|
+
} catch {
|
|
199
265
|
return [{ event: '*', file: '*', status: 'invalid-json', cmd: '' }];
|
|
200
266
|
}
|
|
201
267
|
}
|
|
202
268
|
|
|
203
269
|
for (const [event, files] of Object.entries(HOOK_MAP)) {
|
|
204
270
|
for (const file of files) {
|
|
205
|
-
const cmd
|
|
271
|
+
const cmd = `node ${hooksDir.replace(HOME, '$HOME')}/${file}`;
|
|
206
272
|
const found = (Array.isArray(settings.hooks?.[event]) ? settings.hooks[event] : [])
|
|
207
|
-
.flatMap(g => g.hooks || [])
|
|
208
|
-
.some(h => h.command === cmd);
|
|
273
|
+
.flatMap((g) => g.hooks || [])
|
|
274
|
+
.some((h) => h.command === cmd);
|
|
209
275
|
results.push({ event, file, status: found ? 'registered' : 'missing', cmd });
|
|
210
276
|
}
|
|
211
277
|
}
|
|
@@ -215,9 +281,11 @@ function checkSettingsJson() {
|
|
|
215
281
|
|
|
216
282
|
// ── apply actions ────────────────────────────────────────────────────────────
|
|
217
283
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
284
|
+
// `hooksDir` is the target the stale/missing paths in `hookResults` already point
|
|
285
|
+
// at (the check above embeds the absolute installedPath). The directory is created
|
|
286
|
+
// up front so first-time codex installs (~/.codex/hooks/ absent) succeed.
|
|
287
|
+
function applyHookFiles(hookResults, hooksDir) {
|
|
288
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
221
289
|
|
|
222
290
|
const applied = [];
|
|
223
291
|
for (const h of hookResults) {
|
|
@@ -229,11 +297,14 @@ function applyHookFiles(hookResults) {
|
|
|
229
297
|
return applied;
|
|
230
298
|
}
|
|
231
299
|
|
|
232
|
-
function applySettingsJson(settingsResults) {
|
|
233
|
-
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
300
|
+
function applySettingsJson(settingsResults, settingsPath) {
|
|
234
301
|
let settings = {};
|
|
235
302
|
if (existsSync(settingsPath)) {
|
|
236
|
-
try {
|
|
303
|
+
try {
|
|
304
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
237
308
|
}
|
|
238
309
|
if (!settings.hooks) settings.hooks = {};
|
|
239
310
|
|
|
@@ -241,6 +312,17 @@ function applySettingsJson(settingsResults) {
|
|
|
241
312
|
for (const s of settingsResults) {
|
|
242
313
|
if (s.status !== 'missing') continue;
|
|
243
314
|
if (!Array.isArray(settings.hooks[s.event])) settings.hooks[s.event] = [];
|
|
315
|
+
// fix #48 BLOCKER: re-check the current parsed settings before appending.
|
|
316
|
+
// applyHookNameMigration may have rewritten a legacy wiki-*.mjs command to
|
|
317
|
+
// exactly `s.cmd` between checkSettingsJson and now — appending without
|
|
318
|
+
// this guard creates a duplicate registration (codex 2-worker review
|
|
319
|
+
// reproduced 11 duplicate hypo-*.mjs entries on a wiki-only legacy settings
|
|
320
|
+
// file). The precheck list is allowed to be stale; the apply path must
|
|
321
|
+
// self-heal against the on-disk truth.
|
|
322
|
+
const alreadyPresent = settings.hooks[s.event]
|
|
323
|
+
.flatMap((g) => g.hooks || [])
|
|
324
|
+
.some((h) => h.command === s.cmd);
|
|
325
|
+
if (alreadyPresent) continue;
|
|
244
326
|
settings.hooks[s.event].push({ hooks: [{ type: 'command', command: s.cmd }] });
|
|
245
327
|
applied.push(`${s.event}: ${s.file}`);
|
|
246
328
|
}
|
|
@@ -254,29 +336,34 @@ function applySettingsJson(settingsResults) {
|
|
|
254
336
|
|
|
255
337
|
// Rename map: old wiki-*.mjs → new hypo-*.mjs
|
|
256
338
|
const HOOK_RENAMES = {
|
|
257
|
-
'wiki-session-start.mjs':
|
|
258
|
-
'wiki-first-prompt.mjs':
|
|
259
|
-
'wiki-lookup.mjs':
|
|
260
|
-
'wiki-compact-guard.mjs':
|
|
261
|
-
'wiki-auto-stage.mjs':
|
|
262
|
-
'wiki-hot-rebuild.mjs':
|
|
263
|
-
'wiki-auto-commit.mjs':
|
|
264
|
-
'wiki-cwd-change.mjs':
|
|
265
|
-
'wiki-file-watch.mjs':
|
|
266
|
-
'wiki-shared.mjs':
|
|
339
|
+
'wiki-session-start.mjs': 'hypo-session-start.mjs',
|
|
340
|
+
'wiki-first-prompt.mjs': 'hypo-first-prompt.mjs',
|
|
341
|
+
'wiki-lookup.mjs': 'hypo-lookup.mjs',
|
|
342
|
+
'wiki-compact-guard.mjs': 'hypo-compact-guard.mjs',
|
|
343
|
+
'wiki-auto-stage.mjs': 'hypo-auto-stage.mjs',
|
|
344
|
+
'wiki-hot-rebuild.mjs': 'hypo-hot-rebuild.mjs',
|
|
345
|
+
'wiki-auto-commit.mjs': 'hypo-auto-commit.mjs',
|
|
346
|
+
'wiki-cwd-change.mjs': 'hypo-cwd-change.mjs',
|
|
347
|
+
'wiki-file-watch.mjs': 'hypo-file-watch.mjs',
|
|
348
|
+
'wiki-shared.mjs': 'hypo-shared.mjs',
|
|
267
349
|
'personal-wiki-check.mjs': 'hypo-personal-check.mjs',
|
|
268
350
|
};
|
|
269
351
|
|
|
270
|
-
|
|
271
|
-
|
|
352
|
+
// Same target-aware shape: scans either ~/.claude/settings.json or ~/.codex/settings.json
|
|
353
|
+
// for v1.0/v1.1 `wiki-*.mjs` references that need renaming to `hypo-*.mjs`.
|
|
354
|
+
function checkOldHookNames(settingsPath) {
|
|
272
355
|
if (!existsSync(settingsPath)) return [];
|
|
273
356
|
let settings;
|
|
274
|
-
try {
|
|
357
|
+
try {
|
|
358
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
359
|
+
} catch {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
275
362
|
|
|
276
363
|
const found = [];
|
|
277
364
|
for (const [event, groups] of Object.entries(settings.hooks || {})) {
|
|
278
|
-
for (const group of
|
|
279
|
-
for (const hook of
|
|
365
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
366
|
+
for (const hook of group.hooks || []) {
|
|
280
367
|
const cmd = hook.command || '';
|
|
281
368
|
for (const oldName of Object.keys(HOOK_RENAMES)) {
|
|
282
369
|
if (cmd.includes(oldName)) found.push({ event, oldName, cmd });
|
|
@@ -287,18 +374,20 @@ function checkOldHookNames() {
|
|
|
287
374
|
return found;
|
|
288
375
|
}
|
|
289
376
|
|
|
290
|
-
function applyHookNameMigration(oldRefs) {
|
|
291
|
-
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
292
|
-
const hooksDir = join(HOME, '.claude', 'hooks');
|
|
377
|
+
function applyHookNameMigration(oldRefs, settingsPath, hooksDir) {
|
|
293
378
|
if (!existsSync(settingsPath)) return [];
|
|
294
379
|
|
|
295
380
|
let settings;
|
|
296
|
-
try {
|
|
381
|
+
try {
|
|
382
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
383
|
+
} catch {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
297
386
|
|
|
298
387
|
const applied = [];
|
|
299
388
|
for (const [event, groups] of Object.entries(settings.hooks || {})) {
|
|
300
|
-
for (const group of
|
|
301
|
-
for (const hook of
|
|
389
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
390
|
+
for (const hook of group.hooks || []) {
|
|
302
391
|
for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
|
|
303
392
|
if ((hook.command || '').includes(oldName)) {
|
|
304
393
|
hook.command = hook.command.replace(oldName, newName);
|
|
@@ -311,7 +400,8 @@ function applyHookNameMigration(oldRefs) {
|
|
|
311
400
|
|
|
312
401
|
if (applied.length > 0) {
|
|
313
402
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
314
|
-
// Copy renamed hook files to ~/.claude/hooks
|
|
403
|
+
// Copy renamed hook files to the target hooks dir (~/.claude/hooks or
|
|
404
|
+
// ~/.codex/hooks per the caller — fix #48 mirror).
|
|
315
405
|
for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
|
|
316
406
|
const oldPath = join(hooksDir, oldName);
|
|
317
407
|
const newPath = join(hooksDir, newName);
|
|
@@ -324,37 +414,152 @@ function applyHookNameMigration(oldRefs) {
|
|
|
324
414
|
return applied;
|
|
325
415
|
}
|
|
326
416
|
|
|
417
|
+
// ── .hypoignore migration — ensure required runtime patterns are present ─────
|
|
418
|
+
//
|
|
419
|
+
// Idempotent: only appends entries that are absent. Re-running --apply on an
|
|
420
|
+
// already-migrated .hypoignore is a no-op.
|
|
421
|
+
|
|
422
|
+
const HYPOIGNORE_REQUIRED_ENTRIES = [
|
|
423
|
+
{
|
|
424
|
+
pattern: '.cache/',
|
|
425
|
+
comment: '# Hypomnema runtime cache (session growth metrics, future index.jsonl, etc.)',
|
|
426
|
+
},
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
function checkHypoignore(hypoDir) {
|
|
430
|
+
const path = join(hypoDir, '.hypoignore');
|
|
431
|
+
if (!existsSync(path)) return { status: 'no-file', missing: [], path };
|
|
432
|
+
const content = readFileSync(path, 'utf-8');
|
|
433
|
+
const entries = new Set(
|
|
434
|
+
content
|
|
435
|
+
.split('\n')
|
|
436
|
+
.map((l) => l.trim())
|
|
437
|
+
.filter((l) => l && !l.startsWith('#')),
|
|
438
|
+
);
|
|
439
|
+
const missing = HYPOIGNORE_REQUIRED_ENTRIES.filter((e) => !entries.has(e.pattern));
|
|
440
|
+
return { status: missing.length === 0 ? 'up-to-date' : 'needs-migration', missing, path };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function applyHypoignoreMigration(result) {
|
|
444
|
+
if (result.status !== 'needs-migration') return [];
|
|
445
|
+
let content = readFileSync(result.path, 'utf-8');
|
|
446
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
447
|
+
const appended = [];
|
|
448
|
+
for (const entry of result.missing) {
|
|
449
|
+
content += `\n${entry.comment}\n${entry.pattern}\n`;
|
|
450
|
+
appended.push(entry.pattern);
|
|
451
|
+
}
|
|
452
|
+
writeFileSync(result.path, content);
|
|
453
|
+
return appended;
|
|
454
|
+
}
|
|
455
|
+
|
|
327
456
|
function writeMigrationReport(hypoDir, fromVersion, toVersion) {
|
|
328
|
-
const today
|
|
457
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
329
458
|
const filename = `MIGRATION-v${toVersion}.md`;
|
|
330
|
-
const dest
|
|
459
|
+
const dest = join(hypoDir, filename);
|
|
331
460
|
|
|
332
461
|
// Don't overwrite an existing report
|
|
333
462
|
if (existsSync(dest)) return dest;
|
|
334
463
|
|
|
464
|
+
// v1 → v2 specific guidance for the ADR 0031 feedback classification bump.
|
|
465
|
+
// Other major jumps fall back to the generic body. ADR 0034 reserves the
|
|
466
|
+
// right to keep SCHEMA.md as user-owned (Option C); auto-stub of the 9 new
|
|
467
|
+
// fields is rejected because scope/tier/targets/sensitivity/reason/source
|
|
468
|
+
// are semantic decisions whose wrong defaults would project wrong behavior.
|
|
469
|
+
const isV1ToV2 = fromVersion === '1.0' && toVersion === '2.0';
|
|
470
|
+
const specificBody = isV1ToV2
|
|
471
|
+
? `## What changed in SCHEMA 2.0
|
|
472
|
+
|
|
473
|
+
ADR 0031 (\`projects/hypomnema/decisions/0031-feedback-as-sot-external-memory-projection.md\`)
|
|
474
|
+
made \`feedback\` pages the single source of truth for behavior corrections and added
|
|
475
|
+
**9 hard-required frontmatter fields** for every \`feedback\` page:
|
|
476
|
+
|
|
477
|
+
- \`status\` (active | superseded | archived)
|
|
478
|
+
- \`scope\` (global | project:<project-id>)
|
|
479
|
+
- \`tier\` (L1 | L2)
|
|
480
|
+
- \`targets\` (project-memory and/or claude-learned)
|
|
481
|
+
- \`sensitivity\` (public | sanitized) — \`private\` is forbidden
|
|
482
|
+
- \`priority\` (1–5)
|
|
483
|
+
- \`memory_summary\` (one line for MEMORY.md index)
|
|
484
|
+
- \`reason\` (why the rule exists)
|
|
485
|
+
- \`source\` (session:YYYY-MM-DD | commit:<hash> | pr:<n> | URL)
|
|
486
|
+
|
|
487
|
+
**Conditional (when \`targets\` includes \`claude-learned\`):** \`global_summary\`
|
|
488
|
+
and \`promote_to_global\` become required (set \`promote_to_global: true\`), and
|
|
489
|
+
the page MUST also be \`scope: global\` + \`tier: L1\`. A claude-learned page
|
|
490
|
+
that does not satisfy all four is rejected by both \`hypomnema lint\` and
|
|
491
|
+
\`hypomnema feedback\` (see \`scripts/lint.mjs\` and \`scripts/feedback.mjs\`
|
|
492
|
+
enforcement).
|
|
493
|
+
|
|
494
|
+
ADR 0034 records this schema bump and the reasoning (semver-major because
|
|
495
|
+
existing feedback pages without these fields now fail \`hypomnema lint\`).
|
|
496
|
+
|
|
497
|
+
## Action items — existing wiki
|
|
498
|
+
|
|
499
|
+
Run \`hypomnema lint\` first to see exactly which feedback pages are missing
|
|
500
|
+
fields. Then **manually** backfill each \`pages/feedback/*.md\` — the upgrade
|
|
501
|
+
deliberately does NOT auto-stub the 9 fields because wrong defaults for
|
|
502
|
+
\`scope\` / \`tier\` / \`targets\` / \`sensitivity\` / \`reason\` / \`source\` would
|
|
503
|
+
silently project wrong behavior into MEMORY.md or CLAUDE.md.
|
|
504
|
+
|
|
505
|
+
\`SCHEMA.md\` is intentionally **not** overwritten by upgrade (Option C); the
|
|
506
|
+
checklist below is manual:
|
|
507
|
+
|
|
508
|
+
- [ ] Run \`hypomnema lint\` and read the per-file errors for type \`feedback\`
|
|
509
|
+
- [ ] Backfill the 9 fields in each \`pages/feedback/*.md\` (use an existing
|
|
510
|
+
valid page as a template; see \`templates/pages/_index.md\` if present)
|
|
511
|
+
- [ ] **Fix incomplete feedback pages BEFORE running \`/hypo:feedback\` append**
|
|
512
|
+
— append mode preserves existing frontmatter, so a partial page stays
|
|
513
|
+
partial
|
|
514
|
+
- [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template
|
|
515
|
+
(v${toVersion}) and merge changes manually — only the \`version:\` line
|
|
516
|
+
and the \`feedback\` type section under §3 are load-bearing for the
|
|
517
|
+
hard-required fields above
|
|
518
|
+
- [ ] Run \`hypomnema feedback-sync --check\` to verify the MEMORY/CLAUDE
|
|
519
|
+
projection still resolves cleanly
|
|
520
|
+
- [ ] Run \`hypomnema doctor\` to verify installation health
|
|
521
|
+
- [ ] **Re-run \`hypomnema lint\` after backfilling — confirm 0 feedback errors
|
|
522
|
+
remain (including the conditional \`claude-learned\` fields above)**
|
|
523
|
+
|
|
524
|
+
## Caveat — \`scope: project:<project-id>\` and slug regex
|
|
525
|
+
|
|
526
|
+
The lint regex \`^project:[a-z0-9][a-z0-9-]*$\` accepts only short slugs, but
|
|
527
|
+
\`feedback-sync\`'s default resolved project-id is cwd-derived (e.g.
|
|
528
|
+
\`-Users-you-Workspace-Project\`), which the regex rejects. To use a
|
|
529
|
+
\`scope: project:*\` page in v1.2.0 you must override with
|
|
530
|
+
\`--project-id=<slug>\` so the resolved id is slug-safe. The full resolved-id
|
|
531
|
+
↔ wiki-slug reconciliation is deferred to v1.3.0; \`commands/feedback.md\`
|
|
532
|
+
documents the interim pattern.
|
|
533
|
+
`
|
|
534
|
+
: `## What changed
|
|
535
|
+
|
|
536
|
+
This is a **major version bump** (v${fromVersion} → v${toVersion}).
|
|
537
|
+
Review the SCHEMA diff and update your wiki pages accordingly.
|
|
538
|
+
|
|
539
|
+
## Action items
|
|
540
|
+
|
|
541
|
+
This report was generated during \`/hypo:upgrade --apply\`. Hook files and settings.json
|
|
542
|
+
entries were applied by that run (or skipped with a warning if the target was malformed —
|
|
543
|
+
see the upgrade output). \`SCHEMA.md\` is intentionally **not** overwritten by upgrade — the
|
|
544
|
+
remaining steps are manual:
|
|
545
|
+
|
|
546
|
+
- [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template (v${toVersion}) and merge changes manually
|
|
547
|
+
- [ ] Re-check all \`adr\` and \`learning\` pages for new required frontmatter fields once SCHEMA is updated
|
|
548
|
+
- [ ] Run \`/hypo:doctor\` to verify installation health
|
|
549
|
+
`;
|
|
550
|
+
|
|
335
551
|
const content = `---
|
|
336
552
|
title: Migration Report — v${fromVersion} → v${toVersion}
|
|
337
553
|
type: reference
|
|
338
554
|
updated: ${today}
|
|
339
|
-
tags: [
|
|
555
|
+
tags: [schema]
|
|
340
556
|
---
|
|
341
557
|
|
|
342
558
|
# Migration Report: v${fromVersion} → v${toVersion}
|
|
343
559
|
|
|
344
560
|
Generated by \`/hypo:upgrade\` on ${today}.
|
|
345
561
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
This is a **major version bump** (v${fromVersion} → v${toVersion}).
|
|
349
|
-
Review the SCHEMA diff and update your wiki pages accordingly.
|
|
350
|
-
|
|
351
|
-
## Action items
|
|
352
|
-
|
|
353
|
-
- [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template (v${toVersion}) and update manually
|
|
354
|
-
- [ ] Run \`/hypo:upgrade --apply\` to install updated hook files and settings.json entries
|
|
355
|
-
- [ ] Check all \`adr\` and \`learning\` pages for new required frontmatter fields
|
|
356
|
-
- [ ] Run \`/hypo:doctor\` after applying updates to verify installation health
|
|
357
|
-
|
|
562
|
+
${specificBody}
|
|
358
563
|
## Notes
|
|
359
564
|
|
|
360
565
|
Add migration-specific notes here after reviewing the SCHEMA diff.
|
|
@@ -381,7 +586,7 @@ function checkPkgJson() {
|
|
|
381
586
|
|
|
382
587
|
function readPkgRecordedCommands() {
|
|
383
588
|
const pkg = readPkgJsonSafe(pkgJsonPath());
|
|
384
|
-
return
|
|
589
|
+
return pkg && pkg.commands && typeof pkg.commands === 'object' ? pkg.commands : {};
|
|
385
590
|
}
|
|
386
591
|
|
|
387
592
|
function checkCommands() {
|
|
@@ -396,8 +601,8 @@ function checkCommands() {
|
|
|
396
601
|
if (!file.endsWith('.md')) continue;
|
|
397
602
|
packagedFiles.add(file);
|
|
398
603
|
const srcPath = join(COMMANDS_SRC, file);
|
|
399
|
-
const dest
|
|
400
|
-
const srcSHA
|
|
604
|
+
const dest = join(targetDir, file);
|
|
605
|
+
const srcSHA = sha256(readFileSync(srcPath));
|
|
401
606
|
|
|
402
607
|
if (!existsSync(dest)) {
|
|
403
608
|
results.push({ file, status: 'missing', srcPath, dest, srcSHA });
|
|
@@ -437,8 +642,14 @@ function checkCommands() {
|
|
|
437
642
|
function writeFreshAtomic(dest, content) {
|
|
438
643
|
const tmp = `${dest}.tmp.${process.pid}.${Date.now()}`;
|
|
439
644
|
writeFileSync(tmp, content);
|
|
440
|
-
try {
|
|
441
|
-
|
|
645
|
+
try {
|
|
646
|
+
renameSync(tmp, dest);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
try {
|
|
649
|
+
unlinkSync(tmp);
|
|
650
|
+
} catch {}
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
442
653
|
}
|
|
443
654
|
|
|
444
655
|
function applyCommands(commandResults, force) {
|
|
@@ -519,13 +730,15 @@ function applyCommands(commandResults, force) {
|
|
|
519
730
|
const path = pkgJsonPath();
|
|
520
731
|
const existing = readPkgJsonSafe(path);
|
|
521
732
|
let pkgVersion = null;
|
|
522
|
-
try {
|
|
733
|
+
try {
|
|
734
|
+
pkgVersion = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')).version;
|
|
735
|
+
} catch {}
|
|
523
736
|
writePkgJsonAtomic(path, {
|
|
524
737
|
...existing,
|
|
525
|
-
pkgRoot:
|
|
738
|
+
pkgRoot: PKG_ROOT,
|
|
526
739
|
pkgVersion,
|
|
527
|
-
schemaVersion: '
|
|
528
|
-
commands:
|
|
740
|
+
schemaVersion: '2.0',
|
|
741
|
+
commands: newSHAs,
|
|
529
742
|
});
|
|
530
743
|
return applied;
|
|
531
744
|
}
|
|
@@ -534,60 +747,224 @@ function applyCommands(commandResults, force) {
|
|
|
534
747
|
|
|
535
748
|
const args = parseArgs(process.argv);
|
|
536
749
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
750
|
+
// Target paths. Claude is always checked; codex is checked only when --codex is set
|
|
751
|
+
// (ADR 0024) so users without codex installed see no false drift.
|
|
752
|
+
const claudeHooksDir = join(HOME, '.claude', 'hooks');
|
|
753
|
+
const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
|
|
754
|
+
const codexHooksDir = join(HOME, '.codex', 'hooks');
|
|
755
|
+
const codexSettingsPath = join(HOME, '.codex', 'settings.json');
|
|
756
|
+
|
|
757
|
+
const schema = checkSchemaVersion(args.hypoDir);
|
|
758
|
+
const hooks = checkHookFiles(claudeHooksDir);
|
|
759
|
+
const settings = checkSettingsJson(claudeSettingsPath, claudeHooksDir);
|
|
760
|
+
const pkgJson = checkPkgJson();
|
|
541
761
|
const commands = checkCommands();
|
|
542
|
-
const oldHookRefs = checkOldHookNames();
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
762
|
+
const oldHookRefs = checkOldHookNames(claudeSettingsPath);
|
|
763
|
+
const hypoignore = checkHypoignore(args.hypoDir);
|
|
764
|
+
|
|
765
|
+
// fix #48: when --codex is set, mirror the same core-hook checks against ~/.codex/
|
|
766
|
+
// so `hypomnema upgrade --codex` reports drift symmetrically and `--apply --codex`
|
|
767
|
+
// updates both targets in one pass (matching init.mjs behaviour).
|
|
768
|
+
const hooksCodex = args.codex ? checkHookFiles(codexHooksDir) : null;
|
|
769
|
+
const settingsCodex = args.codex ? checkSettingsJson(codexSettingsPath, codexHooksDir) : null;
|
|
770
|
+
const oldHookRefsCodex = args.codex ? checkOldHookNames(codexSettingsPath) : null;
|
|
771
|
+
|
|
772
|
+
// Extensions companion (ADR 0024). Read-only check; the apply
|
|
773
|
+
// happens below, AFTER applyCommands, so the per-target SHA map merges into the
|
|
774
|
+
// hypo-pkg.json that applyCommands writes (rather than being clobbered by it).
|
|
775
|
+
const extSettingsPath = claudeSettingsPath;
|
|
776
|
+
const extDir = join(args.hypoDir, 'extensions');
|
|
777
|
+
const extCheck = syncExtensions({
|
|
778
|
+
extDir,
|
|
779
|
+
hypoDir: args.hypoDir,
|
|
780
|
+
target: 'claude',
|
|
781
|
+
settingsPath: extSettingsPath,
|
|
782
|
+
pkgPath: pkgJsonPath(),
|
|
783
|
+
apply: false,
|
|
784
|
+
force: args.forceExtensions,
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// E4 (#32): --codex mirrors the extensions sync into ~/.codex (hooks + commands
|
|
788
|
+
// only; skills/agents skipped with a notice). The per-target SHA map lives in the
|
|
789
|
+
// same ~/.claude/hypo-pkg.json under extensions.codex, so pkgPath is unchanged.
|
|
790
|
+
const extCodexSettingsPath = codexSettingsPath;
|
|
791
|
+
const extCheckCodex = args.codex
|
|
792
|
+
? syncExtensions({
|
|
793
|
+
extDir,
|
|
794
|
+
hypoDir: args.hypoDir,
|
|
795
|
+
target: 'codex',
|
|
796
|
+
settingsPath: extCodexSettingsPath,
|
|
797
|
+
pkgPath: pkgJsonPath(),
|
|
798
|
+
apply: false,
|
|
799
|
+
force: args.forceExtensions,
|
|
800
|
+
})
|
|
801
|
+
: null;
|
|
802
|
+
|
|
803
|
+
const staleHooks = hooks.filter(
|
|
804
|
+
(h) => h.status === 'stale' || h.status === 'missing' || h.status === 'src-missing',
|
|
805
|
+
);
|
|
806
|
+
const missingSettings = settings.filter((s) => s.status === 'missing');
|
|
807
|
+
const invalidSettings = settings.some((s) => s.status === 'invalid-json');
|
|
808
|
+
const staleHooksCodex = hooksCodex
|
|
809
|
+
? hooksCodex.filter(
|
|
810
|
+
(h) => h.status === 'stale' || h.status === 'missing' || h.status === 'src-missing',
|
|
811
|
+
)
|
|
812
|
+
: [];
|
|
813
|
+
const missingSettingsCodex = settingsCodex
|
|
814
|
+
? settingsCodex.filter((s) => s.status === 'missing')
|
|
815
|
+
: [];
|
|
816
|
+
const invalidSettingsCodex = settingsCodex
|
|
817
|
+
? settingsCodex.some((s) => s.status === 'invalid-json')
|
|
818
|
+
: false;
|
|
819
|
+
const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
|
|
820
|
+
const pkgJsonDrift = pkgJson.status !== 'up-to-date';
|
|
821
|
+
const staleCommands = commands.filter((c) => c.status === 'stale' || c.status === 'missing');
|
|
822
|
+
const userModifiedCommands = commands.filter((c) => c.status === 'user-modified');
|
|
823
|
+
const orphanedCommands = commands.filter((c) => c.status === 'orphaned');
|
|
824
|
+
const nonRegularCommands = commands.filter(
|
|
825
|
+
(c) => c.status === 'non-regular' || c.status === 'unreadable',
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
let migrationPath = null;
|
|
829
|
+
let appliedHooks = [];
|
|
830
|
+
let appliedSettings = [];
|
|
831
|
+
let appliedPkgJson = false;
|
|
558
832
|
let appliedHookNameRenames = [];
|
|
559
|
-
let
|
|
833
|
+
let appliedHooksCodex = [];
|
|
834
|
+
let appliedSettingsCodex = [];
|
|
835
|
+
let appliedHookNameRenamesCodex = [];
|
|
836
|
+
let appliedCommands = [];
|
|
837
|
+
let appliedHypoignore = [];
|
|
838
|
+
let appliedExtensions = null;
|
|
839
|
+
let appliedExtensionsCodex = null;
|
|
560
840
|
|
|
561
841
|
if (args.apply) {
|
|
562
842
|
if (oldHookRefs.length > 0) {
|
|
563
|
-
appliedHookNameRenames = applyHookNameMigration(
|
|
843
|
+
appliedHookNameRenames = applyHookNameMigration(
|
|
844
|
+
oldHookRefs,
|
|
845
|
+
claudeSettingsPath,
|
|
846
|
+
claudeHooksDir,
|
|
847
|
+
);
|
|
564
848
|
}
|
|
565
849
|
if (schema.bump === 'major' && schema.installed && schema.current && existsSync(args.hypoDir)) {
|
|
566
850
|
migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current);
|
|
567
851
|
}
|
|
568
|
-
appliedHooks
|
|
569
|
-
appliedSettings = applySettingsJson(settings);
|
|
852
|
+
appliedHooks = applyHookFiles(hooks, claudeHooksDir);
|
|
853
|
+
appliedSettings = applySettingsJson(settings, claudeSettingsPath);
|
|
570
854
|
// applyCommands handles the single atomic hypo-pkg.json write (pkgRoot, version, schema, commands map)
|
|
571
855
|
appliedCommands = applyCommands(commands, args.forceCommands);
|
|
572
856
|
appliedPkgJson = true;
|
|
857
|
+
appliedHypoignore = applyHypoignoreMigration(hypoignore);
|
|
858
|
+
// fix #48: codex core hooks + settings + wiki-*→hypo-* rename mirror. Same order
|
|
859
|
+
// as the claude side (rename first so subsequent hook copy can find renamed targets).
|
|
860
|
+
if (args.codex) {
|
|
861
|
+
if (oldHookRefsCodex.length > 0) {
|
|
862
|
+
appliedHookNameRenamesCodex = applyHookNameMigration(
|
|
863
|
+
oldHookRefsCodex,
|
|
864
|
+
codexSettingsPath,
|
|
865
|
+
codexHooksDir,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
appliedHooksCodex = applyHookFiles(hooksCodex, codexHooksDir);
|
|
869
|
+
appliedSettingsCodex = applySettingsJson(settingsCodex, codexSettingsPath);
|
|
870
|
+
}
|
|
871
|
+
// After applyCommands wrote hypo-pkg.json — merges extensions.<target> alongside.
|
|
872
|
+
appliedExtensions = syncExtensions({
|
|
873
|
+
extDir,
|
|
874
|
+
hypoDir: args.hypoDir,
|
|
875
|
+
target: 'claude',
|
|
876
|
+
settingsPath: extSettingsPath,
|
|
877
|
+
pkgPath: pkgJsonPath(),
|
|
878
|
+
apply: true,
|
|
879
|
+
force: args.forceExtensions,
|
|
880
|
+
});
|
|
881
|
+
// E4 (#32): codex apply runs AFTER the claude apply so it reads the freshly
|
|
882
|
+
// written hypo-pkg.json and merges extensions.codex alongside extensions.claude
|
|
883
|
+
// (the per-target spread in syncExtensions preserves the other target's map).
|
|
884
|
+
if (args.codex) {
|
|
885
|
+
appliedExtensionsCodex = syncExtensions({
|
|
886
|
+
extDir,
|
|
887
|
+
hypoDir: args.hypoDir,
|
|
888
|
+
target: 'codex',
|
|
889
|
+
settingsPath: extCodexSettingsPath,
|
|
890
|
+
pkgPath: pkgJsonPath(),
|
|
891
|
+
apply: true,
|
|
892
|
+
force: args.forceExtensions,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
573
895
|
}
|
|
574
896
|
|
|
575
897
|
// ── output ───────────────────────────────────────────────────────────────────
|
|
576
898
|
|
|
577
|
-
const
|
|
899
|
+
const extDrift = extCheck.needsWork || (extCheckCodex?.needsWork ?? false);
|
|
900
|
+
|
|
901
|
+
// fix #48: codex drift only counts when --codex is set — without the flag the codex
|
|
902
|
+
// target is intentionally unobserved (parity with the existing extensions pattern).
|
|
903
|
+
const codexCoreDrift =
|
|
904
|
+
args.codex &&
|
|
905
|
+
(staleHooksCodex.length > 0 ||
|
|
906
|
+
missingSettingsCodex.length > 0 ||
|
|
907
|
+
invalidSettingsCodex ||
|
|
908
|
+
(oldHookRefsCodex?.length ?? 0) > 0);
|
|
909
|
+
|
|
910
|
+
const hasDrift =
|
|
911
|
+
staleHooks.length > 0 ||
|
|
912
|
+
missingSettings.length > 0 ||
|
|
913
|
+
schemaDrift ||
|
|
914
|
+
invalidSettings ||
|
|
915
|
+
pkgJsonDrift ||
|
|
916
|
+
oldHookRefs.length > 0 ||
|
|
917
|
+
staleCommands.length > 0 ||
|
|
918
|
+
userModifiedCommands.length > 0 ||
|
|
919
|
+
orphanedCommands.length > 0 ||
|
|
920
|
+
nonRegularCommands.length > 0 ||
|
|
921
|
+
hypoignore.status === 'needs-migration' ||
|
|
922
|
+
extDrift ||
|
|
923
|
+
codexCoreDrift;
|
|
578
924
|
|
|
579
925
|
if (args.json) {
|
|
580
|
-
console.log(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
926
|
+
console.log(
|
|
927
|
+
JSON.stringify(
|
|
928
|
+
{
|
|
929
|
+
schema,
|
|
930
|
+
hooks,
|
|
931
|
+
settings,
|
|
932
|
+
pkgJson,
|
|
933
|
+
commands,
|
|
934
|
+
oldHookRefs,
|
|
935
|
+
hypoignore,
|
|
936
|
+
extensions: extCheck,
|
|
937
|
+
extensionsCodex: extCheckCodex,
|
|
938
|
+
// fix #48: codex core mirror (null when --codex absent).
|
|
939
|
+
hooksCodex,
|
|
940
|
+
settingsCodex,
|
|
941
|
+
oldHookRefsCodex,
|
|
942
|
+
applied: {
|
|
943
|
+
hooks: appliedHooks,
|
|
944
|
+
settings: appliedSettings,
|
|
945
|
+
pkgJson: appliedPkgJson,
|
|
946
|
+
hookNameRenames: appliedHookNameRenames,
|
|
947
|
+
commands: appliedCommands,
|
|
948
|
+
hypoignore: appliedHypoignore,
|
|
949
|
+
extensions: appliedExtensions,
|
|
950
|
+
extensionsCodex: appliedExtensionsCodex,
|
|
951
|
+
hooksCodex: appliedHooksCodex,
|
|
952
|
+
settingsCodex: appliedSettingsCodex,
|
|
953
|
+
hookNameRenamesCodex: appliedHookNameRenamesCodex,
|
|
954
|
+
},
|
|
955
|
+
migrationReport: migrationPath,
|
|
956
|
+
},
|
|
957
|
+
null,
|
|
958
|
+
2,
|
|
959
|
+
),
|
|
960
|
+
);
|
|
961
|
+
process.exit(
|
|
962
|
+
(hasDrift && !args.apply) ||
|
|
963
|
+
extCheck.conflicts.length > 0 ||
|
|
964
|
+
(extCheckCodex?.conflicts.length ?? 0) > 0
|
|
965
|
+
? 1
|
|
966
|
+
: 0,
|
|
967
|
+
);
|
|
591
968
|
}
|
|
592
969
|
|
|
593
970
|
// Human-readable report
|
|
@@ -597,94 +974,189 @@ const lines = [];
|
|
|
597
974
|
if (schema.bump === 'none') {
|
|
598
975
|
lines.push(`✓ SCHEMA version ${schema.installed} (up to date)`);
|
|
599
976
|
} else if (schema.bump === 'unknown') {
|
|
600
|
-
lines.push(
|
|
977
|
+
lines.push(
|
|
978
|
+
`⚠ SCHEMA version installed=${schema.installed ?? 'not found'}, package=${schema.current ?? 'not found'} (cannot compare)`,
|
|
979
|
+
);
|
|
601
980
|
} else if (schema.bump === 'ahead') {
|
|
602
|
-
lines.push(
|
|
981
|
+
lines.push(
|
|
982
|
+
`⚠ SCHEMA version ${schema.installed} (installed is ahead of package ${schema.current})`,
|
|
983
|
+
);
|
|
603
984
|
} else if (schema.bump === 'major') {
|
|
604
|
-
lines.push(
|
|
985
|
+
lines.push(
|
|
986
|
+
`✗ SCHEMA version ${schema.installed} → ${schema.current} [MAJOR — --apply writes MIGRATION report; SCHEMA must be merged manually]`,
|
|
987
|
+
);
|
|
605
988
|
} else {
|
|
606
|
-
lines.push(
|
|
989
|
+
lines.push(
|
|
990
|
+
`⚠ SCHEMA version ${schema.installed} → ${schema.current} [minor update — review and update SCHEMA.md manually]`,
|
|
991
|
+
);
|
|
607
992
|
}
|
|
608
993
|
|
|
609
|
-
// Hook files
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
994
|
+
// Hook files (target-aware so --codex can mirror the same block; fix #48).
|
|
995
|
+
function pushHookSummary(hookList, label, targetPath) {
|
|
996
|
+
const colHook = `Hook files${label}`.padEnd(20);
|
|
997
|
+
const up = hookList.filter((h) => h.status === 'up-to-date').length;
|
|
998
|
+
const st = hookList.filter((h) => h.status === 'stale').length;
|
|
999
|
+
const mi = hookList.filter((h) => h.status === 'missing').length;
|
|
1000
|
+
const sm = hookList.filter((h) => h.status === 'src-missing').length;
|
|
1001
|
+
if (st === 0 && mi === 0 && sm === 0) {
|
|
1002
|
+
lines.push(`✓ ${colHook}${up}/${hookList.length} up to date`);
|
|
1003
|
+
} else {
|
|
1004
|
+
lines.push(`⚠ ${colHook}${up} up to date, ${st} stale, ${mi} missing, ${sm} src-missing:`);
|
|
1005
|
+
for (const h of hookList) {
|
|
1006
|
+
if (h.status === 'up-to-date') {
|
|
1007
|
+
lines.push(` ✓ ${h.file}`);
|
|
1008
|
+
} else if (h.status === 'stale') {
|
|
1009
|
+
lines.push(` ⚠ ${h.file} [stale — package has newer version]`);
|
|
1010
|
+
} else if (h.status === 'missing') {
|
|
1011
|
+
lines.push(` ✗ ${h.file} [not found in ${targetPath}]`);
|
|
1012
|
+
} else if (h.status === 'src-missing') {
|
|
1013
|
+
lines.push(` ⚠ ${h.file} [installed but missing from package — may be orphaned]`);
|
|
1014
|
+
}
|
|
628
1015
|
}
|
|
629
1016
|
}
|
|
630
1017
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1018
|
+
pushHookSummary(hooks, '', '~/.claude/hooks/');
|
|
1019
|
+
if (hooksCodex) pushHookSummary(hooksCodex, ' (codex)', '~/.codex/hooks/');
|
|
1020
|
+
|
|
1021
|
+
// settings.json registrations (target-aware mirror; fix #48).
|
|
1022
|
+
function pushSettingsSummary(sList, label, invalidFlag) {
|
|
1023
|
+
const colS = `settings.json${label}`.padEnd(20);
|
|
1024
|
+
const reg = sList.filter((s) => s.status === 'registered').length;
|
|
1025
|
+
const mr = sList.filter((s) => s.status === 'missing').length;
|
|
1026
|
+
if (invalidFlag) {
|
|
1027
|
+
lines.push(`✗ ${colS}invalid JSON — fix or back it up before re-running`);
|
|
1028
|
+
} else if (mr === 0) {
|
|
1029
|
+
lines.push(`✓ ${colS}${reg}/${sList.length} hook registrations present`);
|
|
1030
|
+
} else {
|
|
1031
|
+
lines.push(`⚠ ${colS}${reg}/${sList.length} registrations present — ${mr} missing:`);
|
|
1032
|
+
for (const s of sList) {
|
|
1033
|
+
if (s.status === 'missing') lines.push(` + ${s.event}: ${s.file}`);
|
|
1034
|
+
}
|
|
644
1035
|
}
|
|
645
1036
|
}
|
|
1037
|
+
pushSettingsSummary(settings, '', invalidSettings);
|
|
1038
|
+
if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSettingsCodex);
|
|
646
1039
|
|
|
647
1040
|
// Package metadata
|
|
648
1041
|
if (pkgJson.status === 'up-to-date') {
|
|
649
1042
|
lines.push(`✓ Package metadata hypo-pkg.json up to date`);
|
|
650
1043
|
} else if (pkgJson.status === 'stale') {
|
|
651
|
-
lines.push(
|
|
1044
|
+
lines.push(
|
|
1045
|
+
`⚠ Package metadata hypo-pkg.json stale (${pkgJson.installed} → ${pkgJson.current}) — run --apply to update`,
|
|
1046
|
+
);
|
|
652
1047
|
} else {
|
|
653
1048
|
lines.push(`✗ Package metadata hypo-pkg.json missing — run --apply to install`);
|
|
654
1049
|
}
|
|
655
1050
|
|
|
656
1051
|
// Slash commands
|
|
657
|
-
const cmdUpToDate
|
|
658
|
-
const cmdStaleCount
|
|
659
|
-
const cmdMissCount
|
|
660
|
-
const cmdUserCount
|
|
1052
|
+
const cmdUpToDate = commands.filter((c) => c.status === 'up-to-date').length;
|
|
1053
|
+
const cmdStaleCount = commands.filter((c) => c.status === 'stale').length;
|
|
1054
|
+
const cmdMissCount = commands.filter((c) => c.status === 'missing').length;
|
|
1055
|
+
const cmdUserCount = userModifiedCommands.length;
|
|
661
1056
|
const cmdOrphanCount = orphanedCommands.length;
|
|
662
1057
|
const cmdNonRegCount = nonRegularCommands.length;
|
|
663
1058
|
if (commands.length === 0) {
|
|
664
1059
|
lines.push(`⚠ Slash commands package commands/ is empty`);
|
|
665
|
-
} else if (
|
|
666
|
-
|
|
1060
|
+
} else if (
|
|
1061
|
+
cmdStaleCount === 0 &&
|
|
1062
|
+
cmdMissCount === 0 &&
|
|
1063
|
+
cmdUserCount === 0 &&
|
|
1064
|
+
cmdOrphanCount === 0 &&
|
|
1065
|
+
cmdNonRegCount === 0
|
|
1066
|
+
) {
|
|
1067
|
+
lines.push(
|
|
1068
|
+
`✓ Slash commands ${cmdUpToDate}/${commands.length} up to date in ~/.claude/commands/hypo/`,
|
|
1069
|
+
);
|
|
667
1070
|
} else {
|
|
668
|
-
lines.push(
|
|
1071
|
+
lines.push(
|
|
1072
|
+
`⚠ Slash commands ${cmdUpToDate} up to date, ${cmdStaleCount} stale, ${cmdMissCount} missing, ${cmdUserCount} user-modified, ${cmdOrphanCount} orphaned, ${cmdNonRegCount} non-regular:`,
|
|
1073
|
+
);
|
|
669
1074
|
for (const c of commands) {
|
|
670
|
-
if (c.status === 'up-to-date')
|
|
671
|
-
else if (c.status === 'stale')
|
|
672
|
-
|
|
673
|
-
else if (c.status === '
|
|
674
|
-
else if (c.status === '
|
|
675
|
-
|
|
676
|
-
else if (c.status === '
|
|
1075
|
+
if (c.status === 'up-to-date') lines.push(` ✓ ${c.file}`);
|
|
1076
|
+
else if (c.status === 'stale')
|
|
1077
|
+
lines.push(` ⚠ ${c.file} [stale — package has newer version]`);
|
|
1078
|
+
else if (c.status === 'missing') lines.push(` ✗ ${c.file} [not installed]`);
|
|
1079
|
+
else if (c.status === 'user-modified')
|
|
1080
|
+
lines.push(` ⚠ ${c.file} [user-modified — use --apply --force-commands to overwrite]`);
|
|
1081
|
+
else if (c.status === 'orphaned')
|
|
1082
|
+
lines.push(
|
|
1083
|
+
` ⊘ ${c.file} [orphaned — no longer shipped by package; --apply will reconcile]`,
|
|
1084
|
+
);
|
|
1085
|
+
else if (c.status === 'non-regular')
|
|
1086
|
+
lines.push(` ✗ ${c.file} [not a regular file (symlink?) — refusing to touch]`);
|
|
1087
|
+
else if (c.status === 'unreadable')
|
|
1088
|
+
lines.push(` ✗ ${c.file} [unreadable — refusing to touch]`);
|
|
677
1089
|
}
|
|
678
1090
|
}
|
|
679
1091
|
|
|
680
|
-
// Old hook names (wiki-*.mjs → hypo-*.mjs rename migration)
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1092
|
+
// Old hook names (wiki-*.mjs → hypo-*.mjs rename migration). Target-aware so
|
|
1093
|
+
// fix #48 surfaces codex settings.json that still references the v1.0/v1.1 names.
|
|
1094
|
+
function pushHookNameSummary(refs, label) {
|
|
1095
|
+
if (refs.length > 0) {
|
|
1096
|
+
lines.push(
|
|
1097
|
+
`⚠ Hook name migration${label} ${refs.length} old wiki-*.mjs reference(s) — run --apply to rename:`,
|
|
1098
|
+
);
|
|
1099
|
+
for (const r of refs) lines.push(` ${r.event}: ${r.oldName} → ${HOOK_RENAMES[r.oldName]}`);
|
|
1100
|
+
} else {
|
|
1101
|
+
const colN = `Hook names${label}`.padEnd(20);
|
|
1102
|
+
lines.push(`✓ ${colN}All hook references use current hypo-*.mjs names`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
pushHookNameSummary(oldHookRefs, '');
|
|
1106
|
+
if (oldHookRefsCodex) pushHookNameSummary(oldHookRefsCodex, ' (codex)');
|
|
1107
|
+
|
|
1108
|
+
// .hypoignore migration (ensure required runtime patterns are present)
|
|
1109
|
+
if (hypoignore.status === 'no-file') {
|
|
1110
|
+
lines.push(`⚠ .hypoignore not found at ${hypoignore.path} (init.mjs scaffolds this)`);
|
|
1111
|
+
} else if (hypoignore.status === 'up-to-date') {
|
|
1112
|
+
lines.push(`✓ .hypoignore required entries present`);
|
|
684
1113
|
} else {
|
|
685
|
-
lines.push(
|
|
1114
|
+
lines.push(
|
|
1115
|
+
`⚠ .hypoignore ${hypoignore.missing.length} missing entry(s) — run --apply to append:`,
|
|
1116
|
+
);
|
|
1117
|
+
for (const e of hypoignore.missing) lines.push(` + ${e.pattern}`);
|
|
686
1118
|
}
|
|
687
1119
|
|
|
1120
|
+
// Extensions companion (ADR 0024; conflict/drift gating E3, #31). Shared by the
|
|
1121
|
+
// claude target and, under --codex, the codex target (E4, #32) — the label keeps
|
|
1122
|
+
// the two blocks distinguishable in the report.
|
|
1123
|
+
function pushExtSummary(check, label) {
|
|
1124
|
+
// Pad to fit the longest label ("Extensions (codex)" = 18) plus a separating
|
|
1125
|
+
// space so the count never glues onto the label.
|
|
1126
|
+
const col = `Extensions${label}`.padEnd(20);
|
|
1127
|
+
const pending = check.actions.filter(
|
|
1128
|
+
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1129
|
+
);
|
|
1130
|
+
const nConflicts = check.conflicts.length;
|
|
1131
|
+
const nDrifts = check.drifts.length;
|
|
1132
|
+
if (check.actions.length === 0 && check.warnings.length === 0) {
|
|
1133
|
+
lines.push(`✓ ${col}none found in ${extDir.replace(HOME, '~')}`);
|
|
1134
|
+
} else if (pending.length === 0 && nConflicts === 0 && nDrifts === 0) {
|
|
1135
|
+
const reg = check.registered.length;
|
|
1136
|
+
lines.push(`✓ ${col}${check.actions.length} synced${reg ? `, ${reg} hook(s) registered` : ''}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
lines.push(
|
|
1139
|
+
`⚠ ${col}${pending.length} to sync, ${nConflicts} conflict(s), ${nDrifts} drift(s):`,
|
|
1140
|
+
);
|
|
1141
|
+
for (const a of pending) lines.push(` + ${a.file} [${a.action}]`);
|
|
1142
|
+
for (const c of check.conflicts) lines.push(` ✗ ${c.file} [${c.action} — left untouched]`);
|
|
1143
|
+
for (const d of check.drifts) lines.push(` ⚠ ${d.file} [drift — left untouched]`);
|
|
1144
|
+
}
|
|
1145
|
+
// E3 (#31): a hard conflict blocks install (exit 1, even under --apply); drift is
|
|
1146
|
+
// resolvable advisory. Emit the spec'd WIKI messages so the user knows the recovery.
|
|
1147
|
+
if (nConflicts > 0) {
|
|
1148
|
+
lines.push(' [WIKI: existing file conflicts. Backup and retry, or use --force-extensions]');
|
|
1149
|
+
}
|
|
1150
|
+
for (const d of check.drifts) {
|
|
1151
|
+
lines.push(
|
|
1152
|
+
` [WIKI: extension ${d.name} drift detected. Use --force-extensions to overwrite.]`,
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
for (const w of check.warnings) lines.push(` ⚠ ${w}`);
|
|
1156
|
+
}
|
|
1157
|
+
pushExtSummary(extCheck, '');
|
|
1158
|
+
if (extCheckCodex) pushExtSummary(extCheckCodex, ' (codex)');
|
|
1159
|
+
|
|
688
1160
|
// Migration report notice
|
|
689
1161
|
if (migrationPath) {
|
|
690
1162
|
lines.push('');
|
|
@@ -693,7 +1165,17 @@ if (migrationPath) {
|
|
|
693
1165
|
}
|
|
694
1166
|
|
|
695
1167
|
// Applied actions
|
|
696
|
-
if (
|
|
1168
|
+
if (
|
|
1169
|
+
appliedHooks.length > 0 ||
|
|
1170
|
+
appliedSettings.length > 0 ||
|
|
1171
|
+
appliedPkgJson ||
|
|
1172
|
+
appliedHookNameRenames.length > 0 ||
|
|
1173
|
+
appliedCommands.length > 0 ||
|
|
1174
|
+
appliedHypoignore.length > 0 ||
|
|
1175
|
+
appliedHooksCodex.length > 0 ||
|
|
1176
|
+
appliedSettingsCodex.length > 0 ||
|
|
1177
|
+
appliedHookNameRenamesCodex.length > 0
|
|
1178
|
+
) {
|
|
697
1179
|
lines.push('');
|
|
698
1180
|
if (appliedHookNameRenames.length > 0) {
|
|
699
1181
|
lines.push(`✓ Renamed legacy hook references (${appliedHookNameRenames.length}):`);
|
|
@@ -714,15 +1196,100 @@ if (appliedHooks.length > 0 || appliedSettings.length > 0 || appliedPkgJson || a
|
|
|
714
1196
|
if (appliedPkgJson) {
|
|
715
1197
|
lines.push(`✓ Written package metadata: ~/.claude/hypo-pkg.json`);
|
|
716
1198
|
}
|
|
1199
|
+
if (appliedHypoignore.length > 0) {
|
|
1200
|
+
lines.push(`✓ Appended .hypoignore entries (${appliedHypoignore.length}):`);
|
|
1201
|
+
for (const e of appliedHypoignore) lines.push(` → ${e}`);
|
|
1202
|
+
}
|
|
1203
|
+
// fix #48: codex-target applied actions (mirrors claude blocks above).
|
|
1204
|
+
if (appliedHookNameRenamesCodex.length > 0) {
|
|
1205
|
+
lines.push(`✓ Renamed legacy hook references (codex) (${appliedHookNameRenamesCodex.length}):`);
|
|
1206
|
+
for (const r of appliedHookNameRenamesCodex) lines.push(` → ${r}`);
|
|
1207
|
+
}
|
|
1208
|
+
if (appliedHooksCodex.length > 0) {
|
|
1209
|
+
lines.push(`✓ Updated hook files (codex) (${appliedHooksCodex.length}):`);
|
|
1210
|
+
for (const f of appliedHooksCodex) lines.push(` → ${f}`);
|
|
1211
|
+
}
|
|
1212
|
+
if (appliedSettingsCodex.length > 0) {
|
|
1213
|
+
lines.push(`✓ Merged settings.json entries (codex) (${appliedSettingsCodex.length}):`);
|
|
1214
|
+
for (const e of appliedSettingsCodex) lines.push(` → ${e}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function pushAppliedExt(applied, label) {
|
|
1219
|
+
if (!applied) return;
|
|
1220
|
+
const synced = applied.actions.filter(
|
|
1221
|
+
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1222
|
+
);
|
|
1223
|
+
if (synced.length > 0 || applied.settingsChanged) {
|
|
1224
|
+
lines.push('');
|
|
1225
|
+
if (synced.length > 0) {
|
|
1226
|
+
lines.push(`✓ Synced extensions${label} (${synced.length}):`);
|
|
1227
|
+
for (const a of synced) lines.push(` → ${a.file} (${a.action})`);
|
|
1228
|
+
}
|
|
1229
|
+
if (applied.settingsChanged) {
|
|
1230
|
+
lines.push(`✓ Registered extension hooks${label} (${applied.registered.length}):`);
|
|
1231
|
+
for (const r of applied.registered) lines.push(` → ${r}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
717
1234
|
}
|
|
1235
|
+
pushAppliedExt(appliedExtensions, '');
|
|
1236
|
+
pushAppliedExt(appliedExtensionsCodex, ' (codex)');
|
|
718
1237
|
|
|
719
1238
|
// Summary
|
|
720
1239
|
lines.push('');
|
|
721
|
-
const totalDrift =
|
|
1240
|
+
const totalDrift =
|
|
1241
|
+
staleHooks.length +
|
|
1242
|
+
missingSettings.length +
|
|
1243
|
+
(schemaDrift ? 1 : 0) +
|
|
1244
|
+
(invalidSettings ? 1 : 0) +
|
|
1245
|
+
(pkgJsonDrift ? 1 : 0) +
|
|
1246
|
+
oldHookRefs.length +
|
|
1247
|
+
staleCommands.length +
|
|
1248
|
+
userModifiedCommands.length +
|
|
1249
|
+
orphanedCommands.length +
|
|
1250
|
+
nonRegularCommands.length +
|
|
1251
|
+
(hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
|
|
1252
|
+
extCheck.actions.filter(
|
|
1253
|
+
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1254
|
+
).length +
|
|
1255
|
+
// E3 (#31): unresolved drift/conflict is pending work too — without these the
|
|
1256
|
+
// summary printed "up to date" while the exit code was 1 (codex review).
|
|
1257
|
+
extCheck.conflicts.length +
|
|
1258
|
+
extCheck.drifts.length +
|
|
1259
|
+
// E4 (#32): codex-target pending work counts identically (same message/exit
|
|
1260
|
+
// consistency the E3 review caught — a codex conflict must not read "up to date").
|
|
1261
|
+
(extCheckCodex
|
|
1262
|
+
? extCheckCodex.actions.filter(
|
|
1263
|
+
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1264
|
+
).length +
|
|
1265
|
+
extCheckCodex.conflicts.length +
|
|
1266
|
+
extCheckCodex.drifts.length
|
|
1267
|
+
: 0) +
|
|
1268
|
+
// fix #48: codex core mirror counts the same way as the claude side.
|
|
1269
|
+
staleHooksCodex.length +
|
|
1270
|
+
missingSettingsCodex.length +
|
|
1271
|
+
(invalidSettingsCodex ? 1 : 0) +
|
|
1272
|
+
(oldHookRefsCodex?.length ?? 0);
|
|
722
1273
|
if (totalDrift === 0) {
|
|
723
1274
|
lines.push('Result: Hypomnema is up to date');
|
|
724
1275
|
} else if (args.apply) {
|
|
725
|
-
const
|
|
1276
|
+
const countApplied = (r) =>
|
|
1277
|
+
r
|
|
1278
|
+
? r.actions.filter(
|
|
1279
|
+
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1280
|
+
).length + (r.settingsChanged ? 1 : 0)
|
|
1281
|
+
: 0;
|
|
1282
|
+
const appliedExtCount = countApplied(appliedExtensions) + countApplied(appliedExtensionsCodex);
|
|
1283
|
+
const total =
|
|
1284
|
+
appliedHooks.length +
|
|
1285
|
+
appliedSettings.length +
|
|
1286
|
+
(appliedPkgJson ? 1 : 0) +
|
|
1287
|
+
appliedHookNameRenames.length +
|
|
1288
|
+
appliedHypoignore.length +
|
|
1289
|
+
appliedExtCount +
|
|
1290
|
+
appliedHooksCodex.length +
|
|
1291
|
+
appliedSettingsCodex.length +
|
|
1292
|
+
appliedHookNameRenamesCodex.length;
|
|
726
1293
|
lines.push(`Result: ${total} update(s) applied. Run /hypo:doctor to verify.`);
|
|
727
1294
|
} else {
|
|
728
1295
|
lines.push(`Result: ${totalDrift} item(s) need updating — run with --apply to install`);
|
|
@@ -730,4 +1297,8 @@ if (totalDrift === 0) {
|
|
|
730
1297
|
|
|
731
1298
|
console.log(lines.join('\n'));
|
|
732
1299
|
|
|
733
|
-
|
|
1300
|
+
// E3 (#31): a hard extension conflict blocks even under --apply (unlike ordinary
|
|
1301
|
+
// drift, which only fails check mode). --force-extensions clears the resolvable
|
|
1302
|
+
// cases; an unfollowable symlink/non-regular dest still counts and stays exit 1.
|
|
1303
|
+
const extBlocked = extCheck.conflicts.length > 0 || (extCheckCodex?.conflicts.length ?? 0) > 0;
|
|
1304
|
+
process.exit((hasDrift && !args.apply) || extBlocked ? 1 : 0);
|