hypomnema 1.0.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/plugin.json +11 -0
- package/LICENSE +21 -0
- package/README.ko.md +160 -0
- package/README.md +160 -0
- package/commands/.gitkeep +0 -0
- package/commands/crystallize.md +116 -0
- package/commands/doctor.md +66 -0
- package/commands/feedback.md +67 -0
- package/commands/graph.md +54 -0
- package/commands/ingest.md +85 -0
- package/commands/init.md +101 -0
- package/commands/lint.md +55 -0
- package/commands/query.md +55 -0
- package/commands/resume.md +48 -0
- package/commands/stats.md +39 -0
- package/commands/uninstall.md +52 -0
- package/commands/upgrade.md +63 -0
- package/commands/verify.md +60 -0
- package/docs/.gitkeep +0 -0
- package/docs/ARCHITECTURE.md +183 -0
- package/docs/CONTRIBUTING.md +115 -0
- package/docs/TEST-CASES.md +580 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/hooks.json +109 -0
- package/hooks/hypo-auto-commit.mjs +36 -0
- package/hooks/hypo-auto-stage.mjs +30 -0
- package/hooks/hypo-compact-guard.mjs +71 -0
- package/hooks/hypo-cwd-change.mjs +91 -0
- package/hooks/hypo-file-watch.mjs +47 -0
- package/hooks/hypo-first-prompt.mjs +59 -0
- package/hooks/hypo-hot-rebuild.mjs +95 -0
- package/hooks/hypo-lookup.mjs +178 -0
- package/hooks/hypo-personal-check.mjs +195 -0
- package/hooks/hypo-session-start.mjs +141 -0
- package/hooks/hypo-shared.mjs +213 -0
- package/package.json +37 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/bump-version.mjs +53 -0
- package/scripts/crystallize.mjs +153 -0
- package/scripts/doctor.mjs +361 -0
- package/scripts/feedback.mjs +130 -0
- package/scripts/graph.mjs +183 -0
- package/scripts/ingest.mjs +130 -0
- package/scripts/init.mjs +515 -0
- package/scripts/lib/frontmatter.mjs +11 -0
- package/scripts/lib/hypo-ignore.mjs +54 -0
- package/scripts/lib/hypo-root.mjs +53 -0
- package/scripts/lint.mjs +210 -0
- package/scripts/query.mjs +124 -0
- package/scripts/resume.mjs +115 -0
- package/scripts/stats.mjs +132 -0
- package/scripts/uninstall.mjs +188 -0
- package/scripts/upgrade.mjs +538 -0
- package/scripts/verify.mjs +172 -0
- package/skills/.gitkeep +0 -0
- package/skills/crystallize/SKILL.md +85 -0
- package/skills/graph/SKILL.md +54 -0
- package/skills/ingest/SKILL.md +83 -0
- package/skills/lint/SKILL.md +55 -0
- package/skills/query/SKILL.md +58 -0
- package/skills/verify/SKILL.md +92 -0
- package/templates/.gitkeep +0 -0
- package/templates/.hypoignore +18 -0
- package/templates/Home.md +34 -0
- package/templates/Overview.md +50 -0
- package/templates/SCHEMA.md +106 -0
- package/templates/hot.md +22 -0
- package/templates/hypo-automation.md +69 -0
- package/templates/hypo-config.md +41 -0
- package/templates/hypo-guide.md +146 -0
- package/templates/hypo-help.md +53 -0
- package/templates/index.md +44 -0
- package/templates/log.md +25 -0
- package/templates/pages/_index.md +61 -0
- package/templates/projects/_template/hot.md +28 -0
- package/templates/projects/_template/index.md +39 -0
- package/templates/projects/_template/prd.md +29 -0
- package/templates/projects/_template/session-state.md +9 -0
- package/templates/session-state.md +12 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hypomnema upgrade script
|
|
4
|
+
*
|
|
5
|
+
* Compares the installed wiki against the current package version.
|
|
6
|
+
* Reports schema version drift, stale hook files, and missing settings.json
|
|
7
|
+
* registrations — without overwriting anything unless --apply is passed.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/upgrade.mjs [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --hypo-dir=<path> Hypomnema root directory (default: resolved via HYPO_DIR / hypo-config.md scan / ~/hypomnema)
|
|
14
|
+
* --apply Apply hook file updates and settings.json merges
|
|
15
|
+
* --json Output results as JSON
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import { resolveHypoRoot, expandHome } from './lib/hypo-root.mjs';
|
|
23
|
+
import { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
24
|
+
|
|
25
|
+
const HOME = homedir();
|
|
26
|
+
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
27
|
+
const PKG_ROOT = join(SCRIPT_DIR, '..');
|
|
28
|
+
const HOOKS_SRC = join(PKG_ROOT, 'hooks');
|
|
29
|
+
const TEMPLATES = join(PKG_ROOT, 'templates');
|
|
30
|
+
|
|
31
|
+
// ── arg parsing ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const args = { hypoDir: null, apply: false, json: false };
|
|
35
|
+
for (const arg of argv.slice(2)) {
|
|
36
|
+
if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
|
|
37
|
+
else if (arg === '--apply') args.apply = true;
|
|
38
|
+
else if (arg === '--json') args.json = true;
|
|
39
|
+
}
|
|
40
|
+
if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── version helpers ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function parseVersion(str) {
|
|
47
|
+
if (!str) return null;
|
|
48
|
+
const m = String(str).match(/^(\d+)(?:\.(\d+))?/);
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
return { major: parseInt(m[1], 10), minor: parseInt(m[2] || '0', 10), raw: String(str) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bumpType(installed, current) {
|
|
54
|
+
if (!installed || !current) return 'unknown';
|
|
55
|
+
if (installed.major !== current.major) {
|
|
56
|
+
return installed.major > current.major ? 'ahead' : 'major';
|
|
57
|
+
}
|
|
58
|
+
if (installed.minor !== current.minor) {
|
|
59
|
+
return installed.minor > current.minor ? 'ahead' : 'minor';
|
|
60
|
+
}
|
|
61
|
+
return 'none';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── hook map (loaded from hooks/hooks.json — single source of truth) ─────────
|
|
65
|
+
|
|
66
|
+
let _hookConfig;
|
|
67
|
+
try {
|
|
68
|
+
_hookConfig = JSON.parse(readFileSync(join(PKG_ROOT, 'hooks', 'hooks.json'), 'utf-8'));
|
|
69
|
+
} catch {
|
|
70
|
+
console.error(`Error: cannot read hooks/hooks.json from package root: ${PKG_ROOT}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
if (!_hookConfig || typeof _hookConfig !== 'object' || Array.isArray(_hookConfig)) {
|
|
74
|
+
console.error('Error: hooks/hooks.json must be a JSON object');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (!_hookConfig.hooks || typeof _hookConfig.hooks !== 'object' || Array.isArray(_hookConfig.hooks)) {
|
|
78
|
+
console.error('Error: hooks/hooks.json must contain a "hooks" object');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
function _extractCommandFileName(command) {
|
|
82
|
+
if (typeof command !== 'string') return null;
|
|
83
|
+
const matches = [...command.matchAll(/(?:^|[\/\\])([^\/\\\s"'`]+\.mjs)(?=$|[\s"'`])/g)];
|
|
84
|
+
if (matches.length > 0) return matches[matches.length - 1][1];
|
|
85
|
+
const bare = command.match(/(?:^|\s)([^\/\\\s"'`]+\.mjs)(?=$|[\s"'`])/);
|
|
86
|
+
return bare ? bare[1] : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _isHookFileName(file) {
|
|
90
|
+
return typeof file === 'string' && /^[^/\\\s]+\.mjs$/.test(file.trim());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _isHookGroup(group) {
|
|
94
|
+
return group &&
|
|
95
|
+
typeof group === 'object' &&
|
|
96
|
+
!Array.isArray(group) &&
|
|
97
|
+
Array.isArray(group.hooks) &&
|
|
98
|
+
group.hooks.length > 0 &&
|
|
99
|
+
group.hooks.every(hook =>
|
|
100
|
+
hook &&
|
|
101
|
+
typeof hook === 'object' &&
|
|
102
|
+
!Array.isArray(hook) &&
|
|
103
|
+
hook.type === 'command' &&
|
|
104
|
+
_extractCommandFileName(hook.command)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract .mjs file names from both old format (string[]) and new format (hook-group object[])
|
|
109
|
+
function _extractFileNames(groups) {
|
|
110
|
+
return groups.flatMap(group => {
|
|
111
|
+
if (typeof group === 'string') return [group.trim()];
|
|
112
|
+
return group.hooks.map(hook => _extractCommandFileName(hook.command));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [event, groups] of Object.entries(_hookConfig.hooks)) {
|
|
117
|
+
const valid = Array.isArray(groups) &&
|
|
118
|
+
groups.length > 0 &&
|
|
119
|
+
groups.every(group => _isHookFileName(group) || _isHookGroup(group)) &&
|
|
120
|
+
_extractFileNames(groups).length > 0;
|
|
121
|
+
if (!valid) {
|
|
122
|
+
console.error(`Error: hooks/hooks.json "hooks.${event}" must be a non-empty array of .mjs file names or Claude hook groups`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (_hookConfig.shared !== undefined && (!Array.isArray(_hookConfig.shared) || !_hookConfig.shared.every(f => _isHookFileName(f)))) {
|
|
127
|
+
console.error('Error: hooks/hooks.json "shared" must be an array of .mjs file names');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const HOOK_MAP = Object.fromEntries(Object.entries(_hookConfig.hooks).map(([e, gs]) => [e, _extractFileNames(gs)]));
|
|
132
|
+
const SHARED_FILES = _hookConfig.shared ?? [];
|
|
133
|
+
|
|
134
|
+
// ── checks ───────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function checkSchemaVersion(hypoDir) {
|
|
137
|
+
const pkgPath = join(TEMPLATES, 'SCHEMA.md');
|
|
138
|
+
const hypoPath = join(hypoDir, 'SCHEMA.md');
|
|
139
|
+
|
|
140
|
+
const pkgVersion = existsSync(pkgPath)
|
|
141
|
+
? parseVersion((parseFrontmatter(readFileSync(pkgPath, 'utf-8')) ?? {}).version)
|
|
142
|
+
: null;
|
|
143
|
+
const hypoVersion = existsSync(hypoPath)
|
|
144
|
+
? parseVersion((parseFrontmatter(readFileSync(hypoPath, 'utf-8')) ?? {}).version)
|
|
145
|
+
: null;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
installed: hypoVersion?.raw ?? null,
|
|
149
|
+
current: pkgVersion?.raw ?? null,
|
|
150
|
+
bump: bumpType(hypoVersion, pkgVersion),
|
|
151
|
+
hypoPath,
|
|
152
|
+
pkgPath,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkHookFiles() {
|
|
157
|
+
const claudeHooks = join(HOME, '.claude', 'hooks');
|
|
158
|
+
const results = [];
|
|
159
|
+
|
|
160
|
+
const allFiles = [...Object.values(HOOK_MAP).flat(), ...SHARED_FILES];
|
|
161
|
+
for (const file of allFiles) {
|
|
162
|
+
const installedPath = join(claudeHooks, file);
|
|
163
|
+
const srcPath = join(HOOKS_SRC, file);
|
|
164
|
+
|
|
165
|
+
if (!existsSync(installedPath)) {
|
|
166
|
+
results.push({ file, status: 'missing', installedPath, srcPath });
|
|
167
|
+
} else if (!existsSync(srcPath)) {
|
|
168
|
+
results.push({ file, status: 'src-missing', installedPath, srcPath });
|
|
169
|
+
} else {
|
|
170
|
+
const installedContent = readFileSync(installedPath, 'utf-8');
|
|
171
|
+
const srcContent = readFileSync(srcPath, 'utf-8');
|
|
172
|
+
results.push({
|
|
173
|
+
file,
|
|
174
|
+
status: installedContent === srcContent ? 'up-to-date' : 'stale',
|
|
175
|
+
installedPath,
|
|
176
|
+
srcPath,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function checkSettingsJson() {
|
|
185
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
186
|
+
const hooksDir = join(HOME, '.claude', 'hooks');
|
|
187
|
+
const results = [];
|
|
188
|
+
|
|
189
|
+
let settings = {};
|
|
190
|
+
if (existsSync(settingsPath)) {
|
|
191
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {
|
|
192
|
+
return [{ event: '*', file: '*', status: 'invalid-json', cmd: '' }];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [event, files] of Object.entries(HOOK_MAP)) {
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
const cmd = `node ${hooksDir.replace(HOME, '$HOME')}/${file}`;
|
|
199
|
+
const found = (Array.isArray(settings.hooks?.[event]) ? settings.hooks[event] : [])
|
|
200
|
+
.flatMap(g => g.hooks || [])
|
|
201
|
+
.some(h => h.command === cmd);
|
|
202
|
+
results.push({ event, file, status: found ? 'registered' : 'missing', cmd });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── apply actions ────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
function applyHookFiles(hookResults) {
|
|
212
|
+
const claudeHooks = join(HOME, '.claude', 'hooks');
|
|
213
|
+
mkdirSync(claudeHooks, { recursive: true });
|
|
214
|
+
|
|
215
|
+
const applied = [];
|
|
216
|
+
for (const h of hookResults) {
|
|
217
|
+
if ((h.status === 'stale' || h.status === 'missing') && existsSync(h.srcPath)) {
|
|
218
|
+
copyFileSync(h.srcPath, h.installedPath);
|
|
219
|
+
applied.push(h.file);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return applied;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applySettingsJson(settingsResults) {
|
|
226
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
227
|
+
let settings = {};
|
|
228
|
+
if (existsSync(settingsPath)) {
|
|
229
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return []; }
|
|
230
|
+
}
|
|
231
|
+
if (!settings.hooks) settings.hooks = {};
|
|
232
|
+
|
|
233
|
+
const applied = [];
|
|
234
|
+
for (const s of settingsResults) {
|
|
235
|
+
if (s.status !== 'missing') continue;
|
|
236
|
+
if (!Array.isArray(settings.hooks[s.event])) settings.hooks[s.event] = [];
|
|
237
|
+
settings.hooks[s.event].push({ hooks: [{ type: 'command', command: s.cmd }] });
|
|
238
|
+
applied.push(`${s.event}: ${s.file}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (applied.length > 0) {
|
|
242
|
+
mkdirSync(join(settingsPath, '..'), { recursive: true });
|
|
243
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
244
|
+
}
|
|
245
|
+
return applied;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Rename map: old wiki-*.mjs → new hypo-*.mjs
|
|
249
|
+
const HOOK_RENAMES = {
|
|
250
|
+
'wiki-session-start.mjs': 'hypo-session-start.mjs',
|
|
251
|
+
'wiki-first-prompt.mjs': 'hypo-first-prompt.mjs',
|
|
252
|
+
'wiki-lookup.mjs': 'hypo-lookup.mjs',
|
|
253
|
+
'wiki-compact-guard.mjs': 'hypo-compact-guard.mjs',
|
|
254
|
+
'wiki-auto-stage.mjs': 'hypo-auto-stage.mjs',
|
|
255
|
+
'wiki-hot-rebuild.mjs': 'hypo-hot-rebuild.mjs',
|
|
256
|
+
'wiki-auto-commit.mjs': 'hypo-auto-commit.mjs',
|
|
257
|
+
'wiki-cwd-change.mjs': 'hypo-cwd-change.mjs',
|
|
258
|
+
'wiki-file-watch.mjs': 'hypo-file-watch.mjs',
|
|
259
|
+
'wiki-shared.mjs': 'hypo-shared.mjs',
|
|
260
|
+
'personal-wiki-check.mjs': 'hypo-personal-check.mjs',
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function checkOldHookNames() {
|
|
264
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
265
|
+
if (!existsSync(settingsPath)) return [];
|
|
266
|
+
let settings;
|
|
267
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return []; }
|
|
268
|
+
|
|
269
|
+
const found = [];
|
|
270
|
+
for (const [event, groups] of Object.entries(settings.hooks || {})) {
|
|
271
|
+
for (const group of (Array.isArray(groups) ? groups : [])) {
|
|
272
|
+
for (const hook of (group.hooks || [])) {
|
|
273
|
+
const cmd = hook.command || '';
|
|
274
|
+
for (const oldName of Object.keys(HOOK_RENAMES)) {
|
|
275
|
+
if (cmd.includes(oldName)) found.push({ event, oldName, cmd });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return found;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function applyHookNameMigration(oldRefs) {
|
|
284
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
285
|
+
const hooksDir = join(HOME, '.claude', 'hooks');
|
|
286
|
+
if (!existsSync(settingsPath)) return [];
|
|
287
|
+
|
|
288
|
+
let settings;
|
|
289
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return []; }
|
|
290
|
+
|
|
291
|
+
const applied = [];
|
|
292
|
+
for (const [event, groups] of Object.entries(settings.hooks || {})) {
|
|
293
|
+
for (const group of (Array.isArray(groups) ? groups : [])) {
|
|
294
|
+
for (const hook of (group.hooks || [])) {
|
|
295
|
+
for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
|
|
296
|
+
if ((hook.command || '').includes(oldName)) {
|
|
297
|
+
hook.command = hook.command.replace(oldName, newName);
|
|
298
|
+
applied.push(`${event}: ${oldName} → ${newName}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (applied.length > 0) {
|
|
306
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
307
|
+
// Copy renamed hook files to ~/.claude/hooks/
|
|
308
|
+
for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
|
|
309
|
+
const oldPath = join(hooksDir, oldName);
|
|
310
|
+
const newPath = join(hooksDir, newName);
|
|
311
|
+
const srcPath = join(HOOKS_SRC, newName);
|
|
312
|
+
if (existsSync(oldPath) && !existsSync(newPath) && existsSync(srcPath)) {
|
|
313
|
+
copyFileSync(srcPath, newPath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return applied;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeMigrationReport(hypoDir, fromVersion, toVersion) {
|
|
321
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
322
|
+
const filename = `MIGRATION-v${toVersion}.md`;
|
|
323
|
+
const dest = join(hypoDir, filename);
|
|
324
|
+
|
|
325
|
+
// Don't overwrite an existing report
|
|
326
|
+
if (existsSync(dest)) return dest;
|
|
327
|
+
|
|
328
|
+
const content = `---
|
|
329
|
+
title: Migration Report — v${fromVersion} → v${toVersion}
|
|
330
|
+
type: reference
|
|
331
|
+
updated: ${today}
|
|
332
|
+
tags: [hypomnema, migration, schema]
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
# Migration Report: v${fromVersion} → v${toVersion}
|
|
336
|
+
|
|
337
|
+
Generated by \`/hypo:upgrade\` on ${today}.
|
|
338
|
+
|
|
339
|
+
## What changed
|
|
340
|
+
|
|
341
|
+
This is a **major version bump** (v${fromVersion} → v${toVersion}).
|
|
342
|
+
Review the SCHEMA diff and update your wiki pages accordingly.
|
|
343
|
+
|
|
344
|
+
## Action items
|
|
345
|
+
|
|
346
|
+
- [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template (v${toVersion}) and update manually
|
|
347
|
+
- [ ] Run \`/hypo:upgrade --apply\` to install updated hook files and settings.json entries
|
|
348
|
+
- [ ] Check all \`adr\` and \`learning\` pages for new required frontmatter fields
|
|
349
|
+
- [ ] Run \`/hypo:doctor\` after applying updates to verify installation health
|
|
350
|
+
|
|
351
|
+
## Notes
|
|
352
|
+
|
|
353
|
+
Add migration-specific notes here after reviewing the SCHEMA diff.
|
|
354
|
+
`;
|
|
355
|
+
|
|
356
|
+
writeFileSync(dest, content);
|
|
357
|
+
return dest;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function checkPkgJson() {
|
|
361
|
+
const path = join(HOME, '.claude', 'hypo-pkg.json');
|
|
362
|
+
if (!existsSync(path)) return { status: 'missing', path };
|
|
363
|
+
try {
|
|
364
|
+
const v = JSON.parse(readFileSync(path, 'utf-8')).pkgRoot;
|
|
365
|
+
if (typeof v !== 'string' || !v) return { status: 'missing', path };
|
|
366
|
+
if (v !== PKG_ROOT) return { status: 'stale', path, installed: v, current: PKG_ROOT };
|
|
367
|
+
return { status: 'up-to-date', path };
|
|
368
|
+
} catch {
|
|
369
|
+
return { status: 'missing', path };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
const args = parseArgs(process.argv);
|
|
376
|
+
|
|
377
|
+
const schema = checkSchemaVersion(args.hypoDir);
|
|
378
|
+
const hooks = checkHookFiles();
|
|
379
|
+
const settings = checkSettingsJson();
|
|
380
|
+
const pkgJson = checkPkgJson();
|
|
381
|
+
const oldHookRefs = checkOldHookNames();
|
|
382
|
+
|
|
383
|
+
const staleHooks = hooks.filter(h => h.status === 'stale' || h.status === 'missing' || h.status === 'src-missing');
|
|
384
|
+
const missingSettings = settings.filter(s => s.status === 'missing');
|
|
385
|
+
const invalidSettings = settings.some(s => s.status === 'invalid-json');
|
|
386
|
+
const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
|
|
387
|
+
const pkgJsonDrift = pkgJson.status !== 'up-to-date';
|
|
388
|
+
|
|
389
|
+
let migrationPath = null;
|
|
390
|
+
let appliedHooks = [];
|
|
391
|
+
let appliedSettings = [];
|
|
392
|
+
let appliedPkgJson = false;
|
|
393
|
+
let appliedHookNameRenames = [];
|
|
394
|
+
|
|
395
|
+
if (args.apply) {
|
|
396
|
+
if (oldHookRefs.length > 0) {
|
|
397
|
+
appliedHookNameRenames = applyHookNameMigration(oldHookRefs);
|
|
398
|
+
}
|
|
399
|
+
if (schema.bump === 'major' && schema.installed && schema.current && existsSync(args.hypoDir)) {
|
|
400
|
+
migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current);
|
|
401
|
+
}
|
|
402
|
+
appliedHooks = applyHookFiles(hooks);
|
|
403
|
+
appliedSettings = applySettingsJson(settings);
|
|
404
|
+
const pkgJsonPath = join(HOME, '.claude', 'hypo-pkg.json');
|
|
405
|
+
writeFileSync(pkgJsonPath, JSON.stringify({ pkgRoot: PKG_ROOT }, null, 2) + '\n');
|
|
406
|
+
appliedPkgJson = true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── output ───────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
const hasDrift = staleHooks.length > 0 || missingSettings.length > 0 || schemaDrift || invalidSettings || pkgJsonDrift || oldHookRefs.length > 0;
|
|
412
|
+
|
|
413
|
+
if (args.json) {
|
|
414
|
+
console.log(JSON.stringify({
|
|
415
|
+
schema,
|
|
416
|
+
hooks,
|
|
417
|
+
settings,
|
|
418
|
+
pkgJson,
|
|
419
|
+
oldHookRefs,
|
|
420
|
+
applied: { hooks: appliedHooks, settings: appliedSettings, pkgJson: appliedPkgJson, hookNameRenames: appliedHookNameRenames },
|
|
421
|
+
migrationReport: migrationPath,
|
|
422
|
+
}, null, 2));
|
|
423
|
+
process.exit(hasDrift && !args.apply ? 1 : 0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Human-readable report
|
|
427
|
+
const lines = [];
|
|
428
|
+
|
|
429
|
+
// Schema version
|
|
430
|
+
if (schema.bump === 'none') {
|
|
431
|
+
lines.push(`✓ SCHEMA version ${schema.installed} (up to date)`);
|
|
432
|
+
} else if (schema.bump === 'unknown') {
|
|
433
|
+
lines.push(`⚠ SCHEMA version installed=${schema.installed ?? 'not found'}, package=${schema.current ?? 'not found'} (cannot compare)`);
|
|
434
|
+
} else if (schema.bump === 'ahead') {
|
|
435
|
+
lines.push(`⚠ SCHEMA version ${schema.installed} (installed is ahead of package ${schema.current})`);
|
|
436
|
+
} else if (schema.bump === 'major') {
|
|
437
|
+
lines.push(`✗ SCHEMA version ${schema.installed} → ${schema.current} [MAJOR — review MIGRATION report, update manually]`);
|
|
438
|
+
} else {
|
|
439
|
+
lines.push(`⚠ SCHEMA version ${schema.installed} → ${schema.current} [minor update — review and update SCHEMA.md manually]`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Hook files
|
|
443
|
+
const upToDate = hooks.filter(h => h.status === 'up-to-date').length;
|
|
444
|
+
const staleCount = hooks.filter(h => h.status === 'stale').length;
|
|
445
|
+
const missCount = hooks.filter(h => h.status === 'missing').length;
|
|
446
|
+
const srcMiss = hooks.filter(h => h.status === 'src-missing').length;
|
|
447
|
+
|
|
448
|
+
if (staleCount === 0 && missCount === 0 && srcMiss === 0) {
|
|
449
|
+
lines.push(`✓ Hook files ${upToDate}/${hooks.length} up to date`);
|
|
450
|
+
} else {
|
|
451
|
+
lines.push(`⚠ Hook files ${upToDate} up to date, ${staleCount} stale, ${missCount} missing, ${srcMiss} src-missing:`);
|
|
452
|
+
for (const h of hooks) {
|
|
453
|
+
if (h.status === 'up-to-date') {
|
|
454
|
+
lines.push(` ✓ ${h.file}`);
|
|
455
|
+
} else if (h.status === 'stale') {
|
|
456
|
+
lines.push(` ⚠ ${h.file} [stale — package has newer version]`);
|
|
457
|
+
} else if (h.status === 'missing') {
|
|
458
|
+
lines.push(` ✗ ${h.file} [not found in ~/.claude/hooks/]`);
|
|
459
|
+
} else if (h.status === 'src-missing') {
|
|
460
|
+
lines.push(` ⚠ ${h.file} [installed but missing from package — may be orphaned]`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// settings.json
|
|
466
|
+
const regCount = settings.filter(s => s.status === 'registered').length;
|
|
467
|
+
const missReg = settings.filter(s => s.status === 'missing').length;
|
|
468
|
+
|
|
469
|
+
if (invalidSettings) {
|
|
470
|
+
lines.push(`✗ settings.json invalid JSON — fix or back it up before re-running`);
|
|
471
|
+
} else if (missReg === 0) {
|
|
472
|
+
lines.push(`✓ settings.json ${regCount}/${settings.length} hook registrations present`);
|
|
473
|
+
} else {
|
|
474
|
+
lines.push(`⚠ settings.json ${regCount}/${settings.length} registrations present — ${missReg} missing:`);
|
|
475
|
+
for (const s of settings) {
|
|
476
|
+
if (s.status === 'missing') lines.push(` + ${s.event}: ${s.file}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Package metadata
|
|
481
|
+
if (pkgJson.status === 'up-to-date') {
|
|
482
|
+
lines.push(`✓ Package metadata hypo-pkg.json up to date`);
|
|
483
|
+
} else if (pkgJson.status === 'stale') {
|
|
484
|
+
lines.push(`⚠ Package metadata hypo-pkg.json stale (${pkgJson.installed} → ${pkgJson.current}) — run --apply to update`);
|
|
485
|
+
} else {
|
|
486
|
+
lines.push(`✗ Package metadata hypo-pkg.json missing — run --apply to install`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Old hook names (wiki-*.mjs → hypo-*.mjs rename migration)
|
|
490
|
+
if (oldHookRefs.length > 0) {
|
|
491
|
+
lines.push(`⚠ Hook name migration ${oldHookRefs.length} old wiki-*.mjs reference(s) in settings.json — run --apply to rename:`);
|
|
492
|
+
for (const r of oldHookRefs) lines.push(` ${r.event}: ${r.oldName} → ${HOOK_RENAMES[r.oldName]}`);
|
|
493
|
+
} else {
|
|
494
|
+
lines.push(`✓ Hook names All hook references use current hypo-*.mjs names`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Migration report notice
|
|
498
|
+
if (migrationPath) {
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(`📋 Migration report: ${migrationPath}`);
|
|
501
|
+
lines.push(` Review and update SCHEMA.md manually — auto-overwrite is intentionally disabled.`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Applied actions
|
|
505
|
+
if (appliedHooks.length > 0 || appliedSettings.length > 0 || appliedPkgJson || appliedHookNameRenames.length > 0) {
|
|
506
|
+
lines.push('');
|
|
507
|
+
if (appliedHookNameRenames.length > 0) {
|
|
508
|
+
lines.push(`✓ Renamed legacy hook references (${appliedHookNameRenames.length}):`);
|
|
509
|
+
for (const r of appliedHookNameRenames) lines.push(` → ${r}`);
|
|
510
|
+
}
|
|
511
|
+
if (appliedHooks.length > 0) {
|
|
512
|
+
lines.push(`✓ Updated hook files (${appliedHooks.length}):`);
|
|
513
|
+
for (const f of appliedHooks) lines.push(` → ${f}`);
|
|
514
|
+
}
|
|
515
|
+
if (appliedSettings.length > 0) {
|
|
516
|
+
lines.push(`✓ Merged settings.json entries (${appliedSettings.length}):`);
|
|
517
|
+
for (const e of appliedSettings) lines.push(` → ${e}`);
|
|
518
|
+
}
|
|
519
|
+
if (appliedPkgJson) {
|
|
520
|
+
lines.push(`✓ Written package metadata: ~/.claude/hypo-pkg.json`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Summary
|
|
525
|
+
lines.push('');
|
|
526
|
+
const totalDrift = staleHooks.length + missingSettings.length + (schemaDrift ? 1 : 0) + (invalidSettings ? 1 : 0) + (pkgJsonDrift ? 1 : 0) + oldHookRefs.length;
|
|
527
|
+
if (totalDrift === 0) {
|
|
528
|
+
lines.push('Result: Hypomnema is up to date');
|
|
529
|
+
} else if (args.apply) {
|
|
530
|
+
const total = appliedHooks.length + appliedSettings.length + (appliedPkgJson ? 1 : 0) + appliedHookNameRenames.length;
|
|
531
|
+
lines.push(`Result: ${total} update(s) applied. Run /hypo:doctor to verify.`);
|
|
532
|
+
} else {
|
|
533
|
+
lines.push(`Result: ${totalDrift} item(s) need updating — run with --apply to install`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
console.log(lines.join('\n'));
|
|
537
|
+
|
|
538
|
+
process.exit(hasDrift && !args.apply ? 1 : 0);
|