hypomnema 1.6.2 → 1.7.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 +39 -14
- package/README.md +39 -14
- package/commands/capture.md +8 -6
- package/commands/crystallize.md +39 -20
- package/docs/ARCHITECTURE.md +49 -14
- package/docs/CONTRIBUTING.md +31 -29
- package/hooks/base-store.mjs +265 -0
- package/hooks/hooks.json +7 -1
- package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
- package/hooks/hypo-auto-stage.mjs +30 -0
- package/hooks/hypo-cwd-change.mjs +31 -2
- package/hooks/hypo-file-watch.mjs +21 -2
- package/hooks/hypo-first-prompt.mjs +19 -3
- package/hooks/hypo-lookup.mjs +86 -29
- package/hooks/hypo-personal-check.mjs +24 -3
- package/hooks/hypo-session-record.mjs +2 -3
- package/hooks/hypo-session-start.mjs +89 -7
- package/hooks/hypo-shared.mjs +904 -128
- package/hooks/proposal-store.mjs +513 -0
- package/package.json +40 -14
- package/scripts/capture.mjs +556 -37
- package/scripts/crystallize.mjs +639 -108
- package/scripts/doctor.mjs +304 -9
- package/scripts/feedback-sync.mjs +515 -44
- package/scripts/graph.mjs +9 -2
- package/scripts/init.mjs +230 -34
- package/scripts/lib/extensions.mjs +656 -1
- package/scripts/lib/hypo-ignore.mjs +54 -6
- package/scripts/lib/hypo-root.mjs +56 -6
- package/scripts/lib/page-usage.mjs +15 -2
- package/scripts/lib/pkg-json.mjs +40 -0
- package/scripts/lib/plugin-detect.mjs +96 -6
- package/scripts/lib/wd-match.mjs +23 -5
- package/scripts/lib/wikilink.mjs +32 -6
- package/scripts/lint.mjs +20 -4
- package/scripts/proposal.mjs +1032 -0
- package/scripts/query.mjs +25 -4
- package/scripts/resume.mjs +34 -12
- package/scripts/stats.mjs +28 -8
- package/scripts/uninstall.mjs +141 -6
- package/scripts/upgrade.mjs +197 -15
- package/skills/crystallize/SKILL.md +44 -7
- package/skills/debate/SKILL.md +88 -0
- package/skills/debate/references/orchestration-patterns.md +83 -0
- package/templates/.hyposcanignore +10 -0
- package/templates/SCHEMA.md +12 -0
- package/templates/gitignore +5 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +6 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/check-bilingual.mjs +0 -153
- package/scripts/check-readme-version.mjs +0 -126
- package/scripts/check-tracker-ids.mjs +0 -426
- package/scripts/check-versions.mjs +0 -171
- package/scripts/install-git-hooks.mjs +0 -293
- package/scripts/lib/changelog-classify.mjs +0 -216
- package/scripts/lib/check-bilingual.mjs +0 -244
- package/scripts/lib/check-tracker-ids.mjs +0 -217
- package/scripts/lib/pre-commit-format.mjs +0 -251
- package/scripts/pre-commit-format.mjs +0 -198
package/scripts/graph.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { readFileSync } from 'fs';
|
|
18
18
|
import { join } from 'path';
|
|
19
|
-
import {
|
|
19
|
+
import { resolveHypoRootInfo, checkVaultOrExit, expandHome } from './lib/hypo-root.mjs';
|
|
20
20
|
import { loadHypoIgnore } from './lib/hypo-ignore.mjs';
|
|
21
21
|
import { collectPagesGraph, extractWikilinks } from './lib/wikilink.mjs';
|
|
22
22
|
|
|
@@ -29,7 +29,11 @@ function parseArgs(argv) {
|
|
|
29
29
|
else if (arg.startsWith('--format=')) args.format = arg.slice(9);
|
|
30
30
|
else if (arg.startsWith('--min-edges=')) args.minEdges = parseInt(arg.slice(12), 10) || 0;
|
|
31
31
|
}
|
|
32
|
-
if (!args.hypoDir)
|
|
32
|
+
if (!args.hypoDir) {
|
|
33
|
+
const info = resolveHypoRootInfo();
|
|
34
|
+
args.hypoDir = info.root;
|
|
35
|
+
args.hypoDirSource = info.source;
|
|
36
|
+
}
|
|
33
37
|
return args;
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -150,6 +154,9 @@ function formatDot(pages, graph, minEdges) {
|
|
|
150
154
|
// ── main ──────────────────────────────────────────────────────────────────────
|
|
151
155
|
|
|
152
156
|
const args = parseArgs(process.argv);
|
|
157
|
+
// Only validate the auto-resolved path (env/marker/default). An explicit
|
|
158
|
+
// --hypo-dir=<path> (tests, other tooling) is trusted as-is, valid or not.
|
|
159
|
+
if (args.hypoDirSource) checkVaultOrExit(args.hypoDir, args.hypoDirSource);
|
|
153
160
|
|
|
154
161
|
const ignorePatterns = loadHypoIgnore(args.hypoDir);
|
|
155
162
|
const scanDirs = ['pages', 'projects'].map((d) => join(args.hypoDir, d));
|
package/scripts/init.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
renameSync,
|
|
33
33
|
unlinkSync,
|
|
34
34
|
} from 'fs';
|
|
35
|
-
import { join, basename, dirname } from 'path';
|
|
35
|
+
import { join, basename, dirname, resolve } from 'path';
|
|
36
36
|
import { homedir } from 'os';
|
|
37
37
|
import { execSync, spawnSync } from 'child_process';
|
|
38
38
|
import { fileURLToPath } from 'url';
|
|
@@ -49,6 +49,11 @@ import {
|
|
|
49
49
|
import { syncExtensions } from './lib/extensions.mjs';
|
|
50
50
|
import { templateSchemaVersion } from './lib/template-schema-version.mjs';
|
|
51
51
|
import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
|
|
52
|
+
import {
|
|
53
|
+
isHypomnemaPluginEnabled,
|
|
54
|
+
resolveEnabledPluginRoot as resolveEnabledPluginRootShared,
|
|
55
|
+
usablePkgRoot,
|
|
56
|
+
} from './lib/plugin-detect.mjs';
|
|
52
57
|
|
|
53
58
|
const HOME = homedir();
|
|
54
59
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -93,6 +98,7 @@ const KNOWN_SUBCOMMANDS = new Set([
|
|
|
93
98
|
'uninstall',
|
|
94
99
|
'feedback-sync',
|
|
95
100
|
'capture',
|
|
101
|
+
'proposal',
|
|
96
102
|
]);
|
|
97
103
|
const _verb = process.argv[2];
|
|
98
104
|
if (_verb && KNOWN_SUBCOMMANDS.has(_verb) && _verb !== 'init') {
|
|
@@ -119,6 +125,7 @@ function parseArgs(argv) {
|
|
|
119
125
|
shellSetup: true,
|
|
120
126
|
shellConfig: null,
|
|
121
127
|
allowDowngrade: false,
|
|
128
|
+
lintStrict: false,
|
|
122
129
|
};
|
|
123
130
|
for (const arg of argv.slice(2)) {
|
|
124
131
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -132,6 +139,9 @@ Commands:
|
|
|
132
139
|
uninstall Remove hooks and registrations (dry-run by default; pass --apply)
|
|
133
140
|
feedback-sync Project feedback (SoT) → MEMORY.md / CLAUDE.md learned-behaviors
|
|
134
141
|
projection (--check default, --write to apply)
|
|
142
|
+
capture Reverse-capture ~/.claude/{commands,agents} extensions and
|
|
143
|
+
settings.json hooks into the wiki for cross-machine sync
|
|
144
|
+
(pass names or --all; --dry-run to preview)
|
|
135
145
|
|
|
136
146
|
Running \`hypomnema\` with no command is equivalent to \`hypomnema init\`.
|
|
137
147
|
|
|
@@ -149,11 +159,15 @@ Init options:
|
|
|
149
159
|
--shell-config=<path> Shell config file path (default: auto-detect)
|
|
150
160
|
--allow-downgrade Override the guard that refuses to overwrite NEWER active
|
|
151
161
|
hooks with an older package (stale-PATH-CLI footgun)
|
|
162
|
+
--lint-strict Opt-in: also gate the wiki pre-commit hook on
|
|
163
|
+
\`lint --strict\` (promotes STRICT_PROMOTE_IDS warnings
|
|
164
|
+
to a blocking error), sequenced after the .hypoignore
|
|
165
|
+
guard. Off by default; re-run init to add or drop it.
|
|
152
166
|
--dry-run Show what would be done without making changes
|
|
153
167
|
--help, -h Show this help message
|
|
154
168
|
|
|
155
|
-
Subcommand-specific flags (upgrade/doctor/uninstall) live in the
|
|
156
|
-
docstring at the top of scripts/<command>.mjs
|
|
169
|
+
Subcommand-specific flags (upgrade/doctor/uninstall/capture) live in the
|
|
170
|
+
docstring at the top of scripts/<command>.mjs; capture also accepts \`--help\`.`);
|
|
157
171
|
process.exit(0);
|
|
158
172
|
} else if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
|
|
159
173
|
else if (arg === '--no-hooks') args.hooks = false;
|
|
@@ -174,6 +188,7 @@ docstring at the top of scripts/<command>.mjs.`);
|
|
|
174
188
|
else if (arg === '--no-shell') args.shellSetup = false;
|
|
175
189
|
else if (arg.startsWith('--shell-config=')) args.shellConfig = expandHome(arg.slice(15));
|
|
176
190
|
else if (arg === '--allow-downgrade') args.allowDowngrade = true;
|
|
191
|
+
else if (arg === '--lint-strict') args.lintStrict = true;
|
|
177
192
|
}
|
|
178
193
|
return args;
|
|
179
194
|
}
|
|
@@ -286,6 +301,23 @@ function writeWikiignore(hypoDir, dryRun) {
|
|
|
286
301
|
log('created', dest);
|
|
287
302
|
}
|
|
288
303
|
|
|
304
|
+
// ── .hyposcanignore ──────────────────────────────────────────────────────────
|
|
305
|
+
// Scan-only exclusion sibling to .hypoignore. NOT a privacy boundary — see
|
|
306
|
+
// templates/.hyposcanignore's header. Absence is fine: every scan-ignore
|
|
307
|
+
// reader treats a missing file as an empty pattern list.
|
|
308
|
+
|
|
309
|
+
function writeScanIgnore(hypoDir, dryRun) {
|
|
310
|
+
const dest = join(hypoDir, '.hyposcanignore');
|
|
311
|
+
if (existsSync(dest)) {
|
|
312
|
+
log('skipped', dest);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const src = join(TEMPLATES, '.hyposcanignore');
|
|
316
|
+
const content = existsSync(src) ? readFileSync(src, 'utf-8') : '';
|
|
317
|
+
if (!dryRun) writeFileSync(dest, content);
|
|
318
|
+
log('created', dest);
|
|
319
|
+
}
|
|
320
|
+
|
|
289
321
|
// ── .gitignore ───────────────────────────────────────────────────────────────
|
|
290
322
|
|
|
291
323
|
function writeGitignore(hypoDir, dryRun) {
|
|
@@ -468,34 +500,77 @@ function pkgJsonPath() {
|
|
|
468
500
|
* install (no metadata), unparseable versions, the same package re-running itself,
|
|
469
501
|
* --allow-downgrade, or --dry-run.
|
|
470
502
|
*/
|
|
471
|
-
function maybeRefuseDowngrade(op, allowDowngrade, dryRun) {
|
|
472
|
-
if (allowDowngrade || dryRun) return;
|
|
503
|
+
function maybeRefuseDowngrade(op, allowDowngrade, dryRun, durableRoot) {
|
|
504
|
+
if (allowDowngrade || dryRun || !PKG_VERSION) return;
|
|
473
505
|
const active = readPkgJsonSafe(pkgJsonPath());
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
506
|
+
const candidates = [];
|
|
507
|
+
// The recorded metadata: catches a stale bin trying to downgrade a newer
|
|
508
|
+
// recorded install (the original guard).
|
|
509
|
+
if (active && active.pkgVersion) {
|
|
510
|
+
candidates.push({ pkgRoot: active.pkgRoot, version: active.pkgVersion });
|
|
511
|
+
}
|
|
512
|
+
// The positively-resolved durable (plugin) install, when it is a DIFFERENT root
|
|
513
|
+
// than this package. In an npm-first dual install the recorded pkgVersion is the
|
|
514
|
+
// stale npm one, so comparing only against it lets an OLDER npm init run against a
|
|
515
|
+
// NEWER plugin; also compare against the plugin's own version. readPkgVersionAt
|
|
516
|
+
// yields a version only for a usable root.
|
|
517
|
+
if (durableRoot && durableRoot !== PKG_ROOT) {
|
|
518
|
+
const durableVersion = readPkgVersionAt(durableRoot);
|
|
519
|
+
if (durableVersion) candidates.push({ pkgRoot: durableRoot, version: durableVersion });
|
|
520
|
+
}
|
|
521
|
+
for (const active of candidates) {
|
|
522
|
+
const verdict = classifyInstall(
|
|
523
|
+
{ pkgRoot: PKG_ROOT, version: PKG_VERSION },
|
|
524
|
+
{ pkgRoot: active.pkgRoot, version: active.version },
|
|
525
|
+
);
|
|
526
|
+
if (verdict === 'downgrade') {
|
|
527
|
+
console.error(downgradeGuardMessage(PKG_VERSION, active.version, op));
|
|
528
|
+
process.exit(2);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function readPkgVersionAt(root) {
|
|
534
|
+
try {
|
|
535
|
+
const v = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version;
|
|
536
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
482
539
|
}
|
|
483
540
|
}
|
|
484
541
|
|
|
485
|
-
|
|
542
|
+
// usablePkgRoot now lives in ./lib/plugin-detect.mjs (imported above), shared with
|
|
543
|
+
// upgrade.mjs's dualSkip provenance correction so both agree on what is real.
|
|
544
|
+
|
|
545
|
+
function writePkgJson(dryRun, extraFields = {}, root = PKG_ROOT) {
|
|
486
546
|
const dest = pkgJsonPath();
|
|
487
547
|
const existing = readPkgJsonSafe(dest);
|
|
548
|
+
// `root` is the DURABLE install root the runtime resolves scripts through
|
|
549
|
+
// (hooks/hypo-shared.mjs reads pkgRoot): PKG_ROOT for a normal install, the
|
|
550
|
+
// positively-resolved plugin cache root for a dual install (see resolveDurableRoot).
|
|
551
|
+
// pkgVersion is read from `root` too — stamping a dual-install pointer at the
|
|
552
|
+
// plugin with THIS npm package's version would make doctor's own version-match
|
|
553
|
+
// check contradict itself. schemaVersion describes the templates init writes,
|
|
554
|
+
// which always come from PKG_ROOT regardless of where scripts resolve.
|
|
488
555
|
const merged = {
|
|
489
556
|
...existing,
|
|
490
|
-
pkgRoot:
|
|
491
|
-
pkgVersion: PKG_VERSION,
|
|
557
|
+
pkgRoot: root,
|
|
558
|
+
pkgVersion: readPkgVersionAt(root) ?? PKG_VERSION,
|
|
492
559
|
schemaVersion: templateSchemaVersion(PKG_ROOT) ?? '2.1',
|
|
493
560
|
...extraFields,
|
|
494
561
|
};
|
|
495
|
-
|
|
562
|
+
// Skip the write outright when the durable identity already matches what is on
|
|
563
|
+
// disk, rather than re-serialize an unchanged file. That keeps the dry-run and
|
|
564
|
+
// real write-sets identical: a real run must never silently rewrite a file a dry
|
|
565
|
+
// run reported as skipped. A genuine change (fresh identity, a version bump, or a
|
|
566
|
+
// dual-install correction from a stale npm pointer to the plugin root) is logged
|
|
567
|
+
// as "created" so it shows up in both write-sets. Log is emitted unconditionally
|
|
568
|
+
// (outside the dryRun guard) so a dry run previews exactly what a real run does.
|
|
569
|
+
const noWrite = JSON.stringify(merged) === JSON.stringify(existing);
|
|
570
|
+
if (!dryRun && !noWrite) {
|
|
496
571
|
writePkgJsonAtomic(dest, merged);
|
|
497
|
-
log('created', dest);
|
|
498
572
|
}
|
|
573
|
+
log(noWrite ? 'skipped' : 'created', noWrite ? `${dest} (durable pkgRoot unchanged)` : dest);
|
|
499
574
|
return merged;
|
|
500
575
|
}
|
|
501
576
|
|
|
@@ -638,19 +713,56 @@ function installPkgGitHook(dryRun) {
|
|
|
638
713
|
const WIKI_PRE_COMMIT_MARKER_START = '# hypo-managed:pre-commit:start';
|
|
639
714
|
const WIKI_PRE_COMMIT_MARKER_END = '# hypo-managed:pre-commit:end';
|
|
640
715
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
716
|
+
// Single-quote escaping prevents shell expansion of special chars (e.g. $HOME, backticks) in path
|
|
717
|
+
function shellSingleQuote(p) {
|
|
718
|
+
return `'${p.replace(/'/g, "'\\''")}'`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// `root` is the DURABLE install root the hook should resolve hypo-pre-commit.mjs
|
|
722
|
+
// (and, when opted in, lint.mjs) through — not necessarily PKG_ROOT. In a dual
|
|
723
|
+
// install (a manual/npm init while the plugin is enabled) PKG_ROOT is the
|
|
724
|
+
// manual/npm checkout the dual-install notice tells the user to uninstall;
|
|
725
|
+
// embedding it here would leave the vault's git hook dangling the moment they
|
|
726
|
+
// do, breaking every wiki commit. The caller passes the preserved plugin-owned
|
|
727
|
+
// root instead (see `durableRoot`), so the hook points at the install that
|
|
728
|
+
// actually persists.
|
|
729
|
+
//
|
|
730
|
+
// The block runs its steps sequentially rather than tail-calling `exit $?` on
|
|
731
|
+
// the first one, so a second step (the opt-in --lint-strict gate below) can
|
|
732
|
+
// run after the .hypoignore guard instead of being unreachable dead code.
|
|
733
|
+
// `lintStrict` is baked into the generated shim at install time — the same
|
|
734
|
+
// pattern already used for HYPOMNEMA_ROOT/HYPOMNEMA_GIT_DIR in
|
|
735
|
+
// install-git-hooks.mjs — so toggling it means re-running init with/without
|
|
736
|
+
// `--lint-strict`, not editing the hook by hand.
|
|
737
|
+
//
|
|
738
|
+
// `hypoDir` MUST be absolutized before it is baked in. Git runs a pre-commit
|
|
739
|
+
// hook with cwd set to the wiki's own working-tree root, so a relative
|
|
740
|
+
// `--hypo-dir` (e.g. from `hypo init --hypo-dir=wiki` run from its parent) is
|
|
741
|
+
// re-resolved AT COMMIT TIME against that root instead of the directory the
|
|
742
|
+
// caller meant — `wiki` becomes `<wiki-root>/wiki`, a path that doesn't exist,
|
|
743
|
+
// and lint.mjs falls through to its own default resolution (HYPO_DIR/
|
|
744
|
+
// hypo-config.md scan) and may silently lint an unrelated vault. `worker`
|
|
745
|
+
// needs no such treatment: `root` is already realpath'd upstream (see
|
|
746
|
+
// `durableRoot` / PKG_ROOT resolution), so it's absolute at every call site.
|
|
747
|
+
function wikiPreCommitContent(root, hypoDir, lintStrict) {
|
|
748
|
+
const absHypoDir = resolve(hypoDir);
|
|
749
|
+
const worker = join(root, 'hooks', 'hypo-pre-commit.mjs');
|
|
750
|
+
const steps = [`node ${shellSingleQuote(worker)} || exit 1`];
|
|
751
|
+
if (lintStrict) {
|
|
752
|
+
const lintScript = join(root, 'scripts', 'lint.mjs');
|
|
753
|
+
steps.push(
|
|
754
|
+
`node ${shellSingleQuote(lintScript)} --hypo-dir=${shellSingleQuote(absHypoDir)} --strict || exit 1`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return `#!/bin/sh\n${WIKI_PRE_COMMIT_MARKER_START}\n${steps.join('\n')}\nexit 0\n${WIKI_PRE_COMMIT_MARKER_END}\n`;
|
|
646
758
|
}
|
|
647
759
|
|
|
648
|
-
function installWikiPreCommitHook(hypoDir, dryRun, force) {
|
|
760
|
+
function installWikiPreCommitHook(hypoDir, dryRun, force, root, lintStrict) {
|
|
649
761
|
const gitDir = join(hypoDir, '.git');
|
|
650
762
|
if (!existsSync(gitDir)) return; // no git repo — silently skip
|
|
651
763
|
const hooksDir = join(gitDir, 'hooks');
|
|
652
764
|
const hookPath = join(hooksDir, 'pre-commit');
|
|
653
|
-
const newContent = wikiPreCommitContent();
|
|
765
|
+
const newContent = wikiPreCommitContent(root, hypoDir, lintStrict);
|
|
654
766
|
|
|
655
767
|
if (existsSync(hookPath)) {
|
|
656
768
|
const existing = readFileSync(hookPath, 'utf-8');
|
|
@@ -837,6 +949,66 @@ function firstCommit(hypoDir, remote, dryRun) {
|
|
|
837
949
|
|
|
838
950
|
const args = parseArgs(process.argv);
|
|
839
951
|
|
|
952
|
+
// ── install channel ───────────────────────────────────────────────────────────
|
|
953
|
+
//
|
|
954
|
+
// A plugin-channel install already provides the 14 core hooks + slash commands
|
|
955
|
+
// through the package's own hooks/hooks.json + commands/ (Claude Code auto-wires
|
|
956
|
+
// them). Copying the same hooks into ~/.claude/hooks and registering the same
|
|
957
|
+
// events in settings.json on top makes every core hook fire TWICE, the same
|
|
958
|
+
// failure mode already fixed for `upgrade --apply`. `pluginMode` mirrors
|
|
959
|
+
// upgrade.mjs's own detector (this init.mjs itself running from a
|
|
960
|
+
// `.claude/plugins/…` root); `hypomnemaPluginEnabled` catches the dual-install
|
|
961
|
+
// variant (a manual/npm init.mjs run while the plugin is ALSO enabled — see
|
|
962
|
+
// lib/plugin-detect.mjs, fail-open on any uncertainty). Either signal means
|
|
963
|
+
// Claude's core hook/command surface is plugin-managed already.
|
|
964
|
+
const pluginMode = PKG_ROOT.replace(/\\/g, '/').includes('/.claude/plugins/');
|
|
965
|
+
const hypomnemaPluginEnabled =
|
|
966
|
+
!pluginMode && isHypomnemaPluginEnabled(join(HOME, '.claude', 'settings.json'));
|
|
967
|
+
const coreManagedByPlugin = pluginMode || hypomnemaPluginEnabled;
|
|
968
|
+
|
|
969
|
+
// resolveEnabledPluginRoot lives in ./lib/plugin-detect.mjs (shared with
|
|
970
|
+
// upgrade.mjs's dualSkip provenance correction, imported above as
|
|
971
|
+
// resolveEnabledPluginRootShared). This wrapper binds it to init's own HOME-derived
|
|
972
|
+
// settings/registry paths so callers below read the same as before the move.
|
|
973
|
+
function resolveEnabledPluginRoot(settingsPath) {
|
|
974
|
+
return resolveEnabledPluginRootShared(
|
|
975
|
+
settingsPath,
|
|
976
|
+
join(HOME, '.claude', 'plugins', 'installed_plugins.json'),
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Non-mutating read of the recorded pkgRoot. resolveDurableRoot runs before init's
|
|
981
|
+
// package validation and its "no writes before validation" guarantee, so it must
|
|
982
|
+
// NOT use readPkgJsonSafe (which renames a corrupt hypo-pkg.json aside — a write).
|
|
983
|
+
function recordedPkgRoot() {
|
|
984
|
+
try {
|
|
985
|
+
const v = JSON.parse(readFileSync(pkgJsonPath(), 'utf-8')).pkgRoot;
|
|
986
|
+
return typeof v === 'string' ? v : null;
|
|
987
|
+
} catch {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// The durable install root that persistent artifacts (the vault's git pre-commit
|
|
993
|
+
// hook, hypo-pkg.json) should point at. In a dual install PKG_ROOT is the
|
|
994
|
+
// manual/npm checkout the dual-install notice tells the user to uninstall, so
|
|
995
|
+
// point at the plugin's REAL cache root, positively resolved from the registry —
|
|
996
|
+
// NOT at whatever pkgRoot is already recorded, which after an npm-first sequence
|
|
997
|
+
// (npm init, enable plugin, npm init again) may itself be that soon-to-be-removed
|
|
998
|
+
// npm root. Only if the registry cannot positively identify the plugin do we fall
|
|
999
|
+
// back to a usable recorded pointer, then PKG_ROOT (never worse than pre-fix). In
|
|
1000
|
+
// true plugin mode PKG_ROOT already IS the plugin path; for npm-only it is the
|
|
1001
|
+
// only install.
|
|
1002
|
+
function resolveDurableRoot() {
|
|
1003
|
+
if (!hypomnemaPluginEnabled) return PKG_ROOT;
|
|
1004
|
+
const pluginRoot = resolveEnabledPluginRoot(join(HOME, '.claude', 'settings.json'));
|
|
1005
|
+
if (pluginRoot) return pluginRoot;
|
|
1006
|
+
const recorded = recordedPkgRoot();
|
|
1007
|
+
if (usablePkgRoot(recorded)) return recorded;
|
|
1008
|
+
return PKG_ROOT;
|
|
1009
|
+
}
|
|
1010
|
+
const durableRoot = resolveDurableRoot();
|
|
1011
|
+
|
|
840
1012
|
// Validate hooks.json before any file writes so a bad package leaves no partial state
|
|
841
1013
|
const HOOK_MAP = args.hooks || args.codex ? loadHookMap() : null;
|
|
842
1014
|
|
|
@@ -849,7 +1021,7 @@ const HOOK_MAP = args.hooks || args.codex ? loadHookMap() : null;
|
|
|
849
1021
|
// and, with `--codex`, writes ~/.codex hooks/settings. The guard no-ops on a fresh
|
|
850
1022
|
// install, same realpath'd pkgRoot (dev re-run / npm-link / post-commit sync),
|
|
851
1023
|
// --allow-downgrade, or --dry-run, so legitimate flows are unaffected.
|
|
852
|
-
maybeRefuseDowngrade('init', args.allowDowngrade, args.dryRun);
|
|
1024
|
+
maybeRefuseDowngrade('init', args.allowDowngrade, args.dryRun, durableRoot);
|
|
853
1025
|
|
|
854
1026
|
if (args.fromRemote) {
|
|
855
1027
|
// ── from-remote path: clone → read config → install hooks ──────────────────
|
|
@@ -917,24 +1089,38 @@ if (args.fromRemote) {
|
|
|
917
1089
|
args.dryRun,
|
|
918
1090
|
);
|
|
919
1091
|
|
|
920
|
-
// 3. hypo-config.md + .hypoignore + .gitignore
|
|
1092
|
+
// 3. hypo-config.md + .hypoignore + .hyposcanignore + .gitignore
|
|
921
1093
|
writeHypoConfig(args.hypoDir, args.dryRun);
|
|
922
1094
|
writeWikiignore(args.hypoDir, args.dryRun);
|
|
1095
|
+
writeScanIgnore(args.hypoDir, args.dryRun);
|
|
923
1096
|
writeGitignore(args.hypoDir, args.dryRun);
|
|
924
1097
|
}
|
|
925
1098
|
|
|
926
1099
|
// 4. hooks
|
|
927
1100
|
|
|
1101
|
+
if (coreManagedByPlugin) {
|
|
1102
|
+
log(
|
|
1103
|
+
'skipped',
|
|
1104
|
+
`Claude core hooks/settings/commands — provided by the plugin loader (${
|
|
1105
|
+
pluginMode
|
|
1106
|
+
? 'this script is running from a Claude Code plugin install'
|
|
1107
|
+
: 'the Hypomnema plugin is enabled in settings.json'
|
|
1108
|
+
}); installing them here would register every hook twice. Use \`claude plugin update\` to upgrade instead.`,
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
928
1112
|
let commandSHAs = null;
|
|
929
|
-
if (args.commands) {
|
|
1113
|
+
if (args.commands && !coreManagedByPlugin) {
|
|
930
1114
|
const claudeCommands = join(HOME, '.claude', 'commands', 'hypo');
|
|
931
1115
|
commandSHAs = installCommands(claudeCommands, args.dryRun, args.forceCommands);
|
|
932
1116
|
}
|
|
933
1117
|
|
|
934
1118
|
if (args.hooks) {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1119
|
+
if (!coreManagedByPlugin) {
|
|
1120
|
+
const claudeHooks = join(HOME, '.claude', 'hooks');
|
|
1121
|
+
installHooks(claudeHooks, args.dryRun);
|
|
1122
|
+
mergeSettingsJson(join(HOME, '.claude', 'settings.json'), claudeHooks, args.dryRun, HOOK_MAP);
|
|
1123
|
+
}
|
|
938
1124
|
}
|
|
939
1125
|
|
|
940
1126
|
// Always record the active install identity (pkgRoot + pkgVersion), even for a
|
|
@@ -943,7 +1129,9 @@ if (args.hooks) {
|
|
|
943
1129
|
// the downgrade guard has nothing to compare against, so a later stale
|
|
944
1130
|
// sibling could repoint those surfaces unguarded. writePkgJson merges (...existing),
|
|
945
1131
|
// so this never clobbers a commands/extensions map written elsewhere.
|
|
946
|
-
|
|
1132
|
+
// Record hypo-pkg.json against the durable root (the plugin's real cache root in a
|
|
1133
|
+
// dual install, PKG_ROOT otherwise), the same root the wiki pre-commit hook uses.
|
|
1134
|
+
writePkgJson(args.dryRun, commandSHAs ? { commands: commandSHAs } : {}, durableRoot);
|
|
947
1135
|
|
|
948
1136
|
// 4b. user extensions companion sync. Runs after
|
|
949
1137
|
// writePkgJson so the per-target SHA map is merged into the same hypo-pkg.json
|
|
@@ -1033,8 +1221,16 @@ if (args.hooks) {
|
|
|
1033
1221
|
installPkgGitHook(args.dryRun);
|
|
1034
1222
|
}
|
|
1035
1223
|
|
|
1036
|
-
// 8b. wiki pre-commit hook (.hypoignore last-line-of-defence guard — §6.8)
|
|
1037
|
-
|
|
1224
|
+
// 8b. wiki pre-commit hook (.hypoignore last-line-of-defence guard — §6.8).
|
|
1225
|
+
// Point it at the durable install root, not PKG_ROOT, so a dual install does not
|
|
1226
|
+
// embed a manual/npm path that dangles once the user uninstalls it.
|
|
1227
|
+
installWikiPreCommitHook(
|
|
1228
|
+
args.hypoDir,
|
|
1229
|
+
args.dryRun,
|
|
1230
|
+
args.forceCommands,
|
|
1231
|
+
durableRoot,
|
|
1232
|
+
args.lintStrict,
|
|
1233
|
+
);
|
|
1038
1234
|
|
|
1039
1235
|
// 9. first commit + push (skip when cloned from remote — already has commits)
|
|
1040
1236
|
if (args.gitInit && !args.fromRemote) {
|