hypomnema 1.2.1 → 1.3.1
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 +4 -2
- package/README.md +4 -2
- package/commands/crystallize.md +23 -6
- package/commands/feedback.md +1 -1
- package/commands/upgrade.md +2 -0
- package/docs/CONTRIBUTING.md +96 -11
- package/hooks/hypo-auto-commit.mjs +3 -3
- package/hooks/hypo-auto-minimal-crystallize.mjs +8 -3
- package/hooks/hypo-cwd-change.mjs +2 -2
- package/hooks/hypo-first-prompt.mjs +1 -1
- package/hooks/hypo-personal-check.mjs +57 -7
- package/hooks/hypo-session-start.mjs +73 -19
- package/hooks/hypo-shared.mjs +206 -16
- package/hooks/version-check.mjs +204 -6
- package/package.json +5 -2
- package/scripts/bump-version.mjs +9 -3
- package/scripts/check-bilingual.mjs +115 -0
- package/scripts/crystallize.mjs +130 -16
- package/scripts/doctor.mjs +45 -9
- package/scripts/feedback-sync.mjs +44 -15
- package/scripts/feedback.mjs +5 -5
- package/scripts/fix-status-verify.mjs +256 -0
- package/scripts/init.mjs +45 -4
- package/scripts/install-git-hooks.mjs +258 -0
- package/scripts/lib/adr-corpus.mjs +79 -0
- package/scripts/lib/check-bilingual.mjs +141 -0
- package/scripts/lib/extensions.mjs +3 -3
- package/scripts/lib/feedback-scope.mjs +21 -0
- package/scripts/lib/fix-manifest.mjs +109 -0
- package/scripts/lib/fix-status-verify.mjs +438 -0
- package/scripts/lib/plugin-detect.mjs +51 -0
- package/scripts/lib/pre-commit-format.mjs +251 -0
- package/scripts/lib/project-create.mjs +2 -2
- package/scripts/lint.mjs +48 -8
- package/scripts/pre-commit-format.mjs +198 -0
- package/scripts/resume.mjs +61 -3
- package/scripts/smoke-pack.mjs +39 -2
- package/scripts/upgrade.mjs +308 -58
- package/skills/crystallize/SKILL.md +13 -2
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +4 -0
package/scripts/smoke-pack.mjs
CHANGED
|
@@ -32,6 +32,24 @@ import { fileURLToPath } from 'node:url';
|
|
|
32
32
|
const REPO = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
33
33
|
const KEEP = process.argv.includes('--keep');
|
|
34
34
|
|
|
35
|
+
// When this script runs inside `npm publish --dry-run` (the release workflow's
|
|
36
|
+
// publish-credential pre-check), npm exports `npm_config_dry_run=true` into the
|
|
37
|
+
// lifecycle environment. spawnSync inherits process.env, so the nested
|
|
38
|
+
// `npm pack` below would ALSO run in dry-run mode — reporting a tarball
|
|
39
|
+
// filename/size it never actually writes to disk — and the subsequent
|
|
40
|
+
// `npm install <tarball>` then fails with ENOENT (npm maps errno -2 → exit 254).
|
|
41
|
+
// Strip the flag so the nested npm commands always perform real writes,
|
|
42
|
+
// matching the real tag-push publish job (which has no outer `--dry-run`). The
|
|
43
|
+
// outer workflow stays dry-run and still skips the registry PUT.
|
|
44
|
+
// (This was the precheck-254 root cause; the earlier "missing NODE_AUTH_TOKEN"
|
|
45
|
+
// theory was wrong — it is file absence, not auth.)
|
|
46
|
+
const npmEnv = { ...process.env };
|
|
47
|
+
for (const key of Object.keys(npmEnv)) {
|
|
48
|
+
if (key.toLowerCase().replaceAll('-', '_') === 'npm_config_dry_run') {
|
|
49
|
+
delete npmEnv[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
const PKG = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf-8'));
|
|
36
54
|
const PKG_NAME = PKG.name;
|
|
37
55
|
|
|
@@ -56,8 +74,15 @@ const wikiDir = join(work, 'wiki');
|
|
|
56
74
|
let cleanupOk = false;
|
|
57
75
|
|
|
58
76
|
try {
|
|
77
|
+
// Capture pre-commit hook contents (if any) BEFORE pack so we can prove
|
|
78
|
+
// `npm pack` didn't mutate it. The `prepare` lifecycle script runs during
|
|
79
|
+
// `npm pack` and could theoretically touch .git/hooks/pre-commit; the
|
|
80
|
+
// installer's CI/lifecycle guards must prevent that.
|
|
81
|
+
const preCommitPath = join(REPO, '.git', 'hooks', 'pre-commit');
|
|
82
|
+
const preCommitBefore = existsSync(preCommitPath) ? readFileSync(preCommitPath, 'utf-8') : null;
|
|
83
|
+
|
|
59
84
|
step('npm pack');
|
|
60
|
-
const pack = run('npm', ['pack', '--json'], { cwd: REPO });
|
|
85
|
+
const pack = run('npm', ['pack', '--json'], { cwd: REPO, env: npmEnv });
|
|
61
86
|
const meta = JSON.parse(pack.stdout)[0];
|
|
62
87
|
const tarball = join(REPO, meta.filename);
|
|
63
88
|
console.log(` → ${meta.filename} (${meta.size} bytes, ${meta.entryCount} entries)`);
|
|
@@ -72,7 +97,10 @@ try {
|
|
|
72
97
|
private: true,
|
|
73
98
|
}) + '\n',
|
|
74
99
|
);
|
|
75
|
-
run(
|
|
100
|
+
// No `--silent`: run() only prints npm's stdout/stderr on a non-zero exit, so
|
|
101
|
+
// a real nested-install failure must not be muted (the precheck-254 error was
|
|
102
|
+
// masked by `--silent` for two release cycles).
|
|
103
|
+
run('npm', ['install', '--no-audit', '--no-fund', tarball], { cwd: installRoot, env: npmEnv });
|
|
76
104
|
|
|
77
105
|
// Move tarball into the work dir so it's not left in the repo.
|
|
78
106
|
renameSync(tarball, join(work, meta.filename));
|
|
@@ -203,6 +231,15 @@ try {
|
|
|
203
231
|
);
|
|
204
232
|
}
|
|
205
233
|
|
|
234
|
+
step('verify pre-commit hook was not mutated by npm pack');
|
|
235
|
+
const preCommitAfter = existsSync(preCommitPath) ? readFileSync(preCommitPath, 'utf-8') : null;
|
|
236
|
+
if (preCommitBefore !== preCommitAfter) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'npm pack mutated .git/hooks/pre-commit — the prepare lifecycle ' +
|
|
239
|
+
'guard (npm_command=pack) is not firing.',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
206
243
|
console.log('\n✓ smoke-pack passed');
|
|
207
244
|
cleanupOk = true;
|
|
208
245
|
} catch (err) {
|
package/scripts/upgrade.mjs
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* wiki-*.mjs → hypo-*.mjs rename migration (fix #48), and
|
|
20
20
|
* the user-extensions companion sync (E4 / fix #32).
|
|
21
21
|
* --json Output results as JSON
|
|
22
|
+
* --allow-downgrade Override the guard that refuses to overwrite a NEWER
|
|
23
|
+
* active install with an older package (ADR 0038)
|
|
24
|
+
* --allow-dual-install Override the ISSUE-8 guard: register the Claude core
|
|
25
|
+
* surface even though the Hypomnema plugin is also enabled
|
|
26
|
+
* (knowingly accept the double-registration risk)
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
29
|
import {
|
|
@@ -45,6 +50,8 @@ import {
|
|
|
45
50
|
readFileIfRegular,
|
|
46
51
|
} from './lib/pkg-json.mjs';
|
|
47
52
|
import { syncExtensions } from './lib/extensions.mjs';
|
|
53
|
+
import { isHypomnemaPluginEnabled } from './lib/plugin-detect.mjs';
|
|
54
|
+
import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
|
|
48
55
|
|
|
49
56
|
const HOME = homedir();
|
|
50
57
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -76,6 +83,8 @@ function parseArgs(argv) {
|
|
|
76
83
|
forceCommands: false,
|
|
77
84
|
forceExtensions: false,
|
|
78
85
|
codex: false,
|
|
86
|
+
allowDowngrade: false,
|
|
87
|
+
allowDualInstall: false,
|
|
79
88
|
};
|
|
80
89
|
for (const arg of argv.slice(2)) {
|
|
81
90
|
if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
|
|
@@ -84,6 +93,8 @@ function parseArgs(argv) {
|
|
|
84
93
|
else if (arg === '--force-extensions') args.forceExtensions = true;
|
|
85
94
|
else if (arg === '--codex') args.codex = true;
|
|
86
95
|
else if (arg === '--json') args.json = true;
|
|
96
|
+
else if (arg === '--allow-downgrade') args.allowDowngrade = true;
|
|
97
|
+
else if (arg === '--allow-dual-install') args.allowDualInstall = true;
|
|
87
98
|
}
|
|
88
99
|
if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
|
|
89
100
|
return args;
|
|
@@ -312,7 +323,7 @@ function applySettingsJson(settingsResults, settingsPath) {
|
|
|
312
323
|
for (const s of settingsResults) {
|
|
313
324
|
if (s.status !== 'missing') continue;
|
|
314
325
|
if (!Array.isArray(settings.hooks[s.event])) settings.hooks[s.event] = [];
|
|
315
|
-
//
|
|
326
|
+
// re-check the current parsed settings before appending.
|
|
316
327
|
// applyHookNameMigration may have rewritten a legacy wiki-*.mjs command to
|
|
317
328
|
// exactly `s.cmd` between checkSettingsJson and now — appending without
|
|
318
329
|
// this guard creates a duplicate registration (codex 2-worker review
|
|
@@ -453,7 +464,7 @@ function applyHypoignoreMigration(result) {
|
|
|
453
464
|
return appended;
|
|
454
465
|
}
|
|
455
466
|
|
|
456
|
-
function writeMigrationReport(hypoDir, fromVersion, toVersion) {
|
|
467
|
+
function writeMigrationReport(hypoDir, fromVersion, toVersion, { pluginMode = false } = {}) {
|
|
457
468
|
const today = new Date().toISOString().slice(0, 10);
|
|
458
469
|
const filename = `MIGRATION-v${toVersion}.md`;
|
|
459
470
|
const dest = join(hypoDir, filename);
|
|
@@ -521,15 +532,16 @@ checklist below is manual:
|
|
|
521
532
|
- [ ] **Re-run \`hypomnema lint\` after backfilling — confirm 0 feedback errors
|
|
522
533
|
remain (including the conditional \`claude-learned\` fields above)**
|
|
523
534
|
|
|
524
|
-
##
|
|
535
|
+
## Note — \`scope: project:<project-id>\` and the scope regex
|
|
525
536
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
\`-Users-you-Workspace-Project\`),
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
537
|
+
As of v1.3.0 the feedback scope regex \`^(global|project:[A-Za-z0-9_-]+)\$\`
|
|
538
|
+
accepts cwd-derived project-ids directly (e.g.
|
|
539
|
+
\`-Users-you-Workspace-Project\`), so a \`scope: project:*\` page no longer needs
|
|
540
|
+
a \`--project-id=<slug>\` override just to pass \`lint\`. The resolved id must
|
|
541
|
+
still exact-match \`feedback-sync\`'s project-id for projection (default: cwd
|
|
542
|
+
with \`/\` and \`.\` replaced by \`-\`). Known limit: a cwd containing spaces or
|
|
543
|
+
other characters outside \`[A-Za-z0-9_-]\` still derives an id the regex
|
|
544
|
+
rejects — pass \`--project-id=<id>\` for those.
|
|
533
545
|
`
|
|
534
546
|
: `## What changed
|
|
535
547
|
|
|
@@ -538,9 +550,15 @@ Review the SCHEMA diff and update your wiki pages accordingly.
|
|
|
538
550
|
|
|
539
551
|
## Action items
|
|
540
552
|
|
|
541
|
-
This report was generated during \`/hypo:upgrade --apply\`.
|
|
542
|
-
|
|
543
|
-
|
|
553
|
+
This report was generated during \`/hypo:upgrade --apply\`. ${
|
|
554
|
+
pluginMode
|
|
555
|
+
? 'You are on a **plugin install**, so the core hook files and settings.json hook ' +
|
|
556
|
+
'registrations were NOT touched — the Claude Code plugin loader owns them (upgrade the ' +
|
|
557
|
+
'plugin via `/plugin marketplace update hypomnema` then `/reload-plugins`). Vault ' +
|
|
558
|
+
'extensions, if any, were still synced.'
|
|
559
|
+
: 'Hook files and settings.json entries were applied by that run (or skipped with a ' +
|
|
560
|
+
'warning if the target was malformed — see the upgrade output).'
|
|
561
|
+
} \`SCHEMA.md\` is intentionally **not** overwritten by upgrade — the
|
|
544
562
|
remaining steps are manual:
|
|
545
563
|
|
|
546
564
|
- [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template (v${toVersion}) and merge changes manually
|
|
@@ -743,6 +761,31 @@ function applyCommands(commandResults, force) {
|
|
|
743
761
|
return applied;
|
|
744
762
|
}
|
|
745
763
|
|
|
764
|
+
// ISSUE-6: in plugin mode `applyCommands` is skipped (no command copy), but the
|
|
765
|
+
// runtime still needs hypo-pkg.json to resolve PKG_ROOT for lint/feedback scripts
|
|
766
|
+
// (hooks/hypo-shared.mjs → hypo-personal-check). Write minimal metadata pointing
|
|
767
|
+
// at the plugin's package root, preserving any existing fields (e.g. `extensions`)
|
|
768
|
+
// but DROPPING any prior `commands` map (no commands were copied, so a stale map
|
|
769
|
+
// would falsely assert ownership of ~/.claude/commands/hypo).
|
|
770
|
+
function writePluginModeMetadata() {
|
|
771
|
+
const path = pkgJsonPath();
|
|
772
|
+
// Drop any prior top-level `commands` SHA map: no commands were copied in plugin
|
|
773
|
+
// mode, so keeping a manual install's map would falsely assert ownership of
|
|
774
|
+
// ~/.claude/commands/hypo. Preserve every other field (e.g. `extensions`).
|
|
775
|
+
const { commands: _droppedCommands, ...existing } = readPkgJsonSafe(path) || {};
|
|
776
|
+
let pkgVersion = null;
|
|
777
|
+
try {
|
|
778
|
+
pkgVersion = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')).version;
|
|
779
|
+
} catch {}
|
|
780
|
+
writePkgJsonAtomic(path, {
|
|
781
|
+
...existing,
|
|
782
|
+
pkgRoot: PKG_ROOT,
|
|
783
|
+
pkgVersion,
|
|
784
|
+
schemaVersion: '2.0',
|
|
785
|
+
});
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
|
|
746
789
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
747
790
|
|
|
748
791
|
const args = parseArgs(process.argv);
|
|
@@ -754,6 +797,43 @@ const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
|
|
|
754
797
|
const codexHooksDir = join(HOME, '.codex', 'hooks');
|
|
755
798
|
const codexSettingsPath = join(HOME, '.codex', 'settings.json');
|
|
756
799
|
|
|
800
|
+
// ISSUE-6: when `/hypo:upgrade` runs as the Claude Code PLUGIN, the 15 core hooks
|
|
801
|
+
// and 14 slash commands are provided by the plugin's hooks.json + commands/
|
|
802
|
+
// (auto-wired by Claude Code), NOT copied into ~/.claude/. The manual/npm health
|
|
803
|
+
// check below would then report all of them "missing" and recommend `--apply`,
|
|
804
|
+
// which copies the hooks into ~/.claude/hooks/ and registers 14 settings.json
|
|
805
|
+
// events → Claude Code runs BOTH the plugin hooks.json AND user settings.json, so
|
|
806
|
+
// every hook fires TWICE. The decisive signal: the plugin command runs the
|
|
807
|
+
// PLUGIN's upgrade.mjs, so PKG_ROOT lives under ~/.claude/plugins/. (A manual/npm
|
|
808
|
+
// upgrade.mjs run while the plugin is ALSO enabled is a different failure mode —
|
|
809
|
+
// dual install — tracked as ISSUE-8.)
|
|
810
|
+
// Match the Claude plugin cache shape specifically (`~/.claude/plugins/…`), NOT a
|
|
811
|
+
// generic `/plugins/` substring — this flag now GATES install behavior, so a
|
|
812
|
+
// legitimate npm/dev checkout under some unrelated `…/plugins/…` path must not be
|
|
813
|
+
// misclassified and silently stop managing its hooks. (detectChannel's broad
|
|
814
|
+
// `/plugins/` test is fine for the notifier's display-only use, but too loose here.)
|
|
815
|
+
const pluginMode = PKG_ROOT.replace(/\\/g, '/').includes('/.claude/plugins/');
|
|
816
|
+
// ISSUE-8 (dual install): the OTHER way the same double-registration can happen.
|
|
817
|
+
// Here the MANUAL/npm upgrade.mjs is running (pluginMode=false), but the Hypomnema
|
|
818
|
+
// plugin is ALSO enabled in ~/.claude/settings.json — so the plugin loader already
|
|
819
|
+
// provides the core hooks/commands/settings. A manual/npm `--apply` would copy and
|
|
820
|
+
// register them on top, and every core hook fires twice. The detector is fail-open
|
|
821
|
+
// (see lib/plugin-detect.mjs): a false positive would wrongly alter a legitimate
|
|
822
|
+
// npm-only user's upgrade, so it only fires on an exact `hypomnema@<mp>: true`.
|
|
823
|
+
const hypomnemaPluginEnabled = !pluginMode && isHypomnemaPluginEnabled(claudeSettingsPath);
|
|
824
|
+
const dualInstallCoreConflict = hypomnemaPluginEnabled;
|
|
825
|
+
// Surface policy: the Claude core surface (hooks/settings/commands/hook-name
|
|
826
|
+
// migration) is skipped when EITHER the plugin runs this script (pluginMode) OR a
|
|
827
|
+
// manual/npm run detects the plugin is enabled (dualInstallCoreConflict) — unless
|
|
828
|
+
// the user knowingly overrides with --allow-dual-install. The codex core surface
|
|
829
|
+
// (--codex) and vault-defined extensions are NOT plugin-provided, so they stay
|
|
830
|
+
// managed in every case.
|
|
831
|
+
const managesClaudeCore = !pluginMode && (!dualInstallCoreConflict || args.allowDualInstall);
|
|
832
|
+
// dualSkip = core was skipped specifically because of the dual install (not a true
|
|
833
|
+
// plugin-mode run, and not overridden). Drives the warning banner and the metadata
|
|
834
|
+
// preservation below.
|
|
835
|
+
const dualSkip = dualInstallCoreConflict && !args.allowDualInstall;
|
|
836
|
+
|
|
757
837
|
const schema = checkSchemaVersion(args.hypoDir);
|
|
758
838
|
const hooks = checkHookFiles(claudeHooksDir);
|
|
759
839
|
const settings = checkSettingsJson(claudeSettingsPath, claudeHooksDir);
|
|
@@ -762,7 +842,7 @@ const commands = checkCommands();
|
|
|
762
842
|
const oldHookRefs = checkOldHookNames(claudeSettingsPath);
|
|
763
843
|
const hypoignore = checkHypoignore(args.hypoDir);
|
|
764
844
|
|
|
765
|
-
//
|
|
845
|
+
// when --codex is set, mirror the same core-hook checks against ~/.codex/
|
|
766
846
|
// so `hypomnema upgrade --codex` reports drift symmetrically and `--apply --codex`
|
|
767
847
|
// updates both targets in one pass (matching init.mjs behaviour).
|
|
768
848
|
const hooksCodex = args.codex ? checkHookFiles(codexHooksDir) : null;
|
|
@@ -784,7 +864,7 @@ const extCheck = syncExtensions({
|
|
|
784
864
|
force: args.forceExtensions,
|
|
785
865
|
});
|
|
786
866
|
|
|
787
|
-
// E4 (#32): --codex mirrors the extensions sync into ~/.codex (hooks + commands
|
|
867
|
+
// E4 (fix #32): --codex mirrors the extensions sync into ~/.codex (hooks + commands
|
|
788
868
|
// only; skills/agents skipped with a notice). The per-target SHA map lives in the
|
|
789
869
|
// same ~/.claude/hypo-pkg.json under extensions.codex, so pkgPath is unchanged.
|
|
790
870
|
const extCodexSettingsPath = codexSettingsPath;
|
|
@@ -817,7 +897,14 @@ const invalidSettingsCodex = settingsCodex
|
|
|
817
897
|
? settingsCodex.some((s) => s.status === 'invalid-json')
|
|
818
898
|
: false;
|
|
819
899
|
const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
|
|
820
|
-
|
|
900
|
+
// ISSUE-8 dual-install: when core is skipped, hypo-pkg.json is deliberately left
|
|
901
|
+
// pointing at the PLUGIN's package root (preserved identity), so checkPkgJson()
|
|
902
|
+
// reports it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is
|
|
903
|
+
// INTENTIONAL — `--apply` will not (and must not) rewrite it — so it must not
|
|
904
|
+
// count as actionable drift, or the user would be nagged to run --apply forever.
|
|
905
|
+
// A genuinely missing/corrupt file (status 'missing') is still surfaced (warning
|
|
906
|
+
// below), because the runtime then cannot resolve its package root at all.
|
|
907
|
+
const pkgJsonDrift = pkgJson.status !== 'up-to-date' && !(dualSkip && pkgJson.status === 'stale');
|
|
821
908
|
const staleCommands = commands.filter((c) => c.status === 'stale' || c.status === 'missing');
|
|
822
909
|
const userModifiedCommands = commands.filter((c) => c.status === 'user-modified');
|
|
823
910
|
const orphanedCommands = commands.filter((c) => c.status === 'orphaned');
|
|
@@ -839,23 +926,87 @@ let appliedExtensions = null;
|
|
|
839
926
|
let appliedExtensionsCodex = null;
|
|
840
927
|
|
|
841
928
|
if (args.apply) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
929
|
+
// Downgrade guard (ADR 0038, P): an `--apply` from an OLDER package than the
|
|
930
|
+
// active install would overwrite newer hooks (upgrade.mjs:287 copyFileSync) and
|
|
931
|
+
// rewrite hypo-pkg.json to the older version. Refuse before the first mutation.
|
|
932
|
+
// A dev workspace re-running its own --apply (incl. the post-commit sync hook)
|
|
933
|
+
// is exempt via realpath'd pkgRoot equality. Exit 2 = refused downgrade.
|
|
934
|
+
if (!args.allowDowngrade) {
|
|
935
|
+
const _active = readPkgJsonSafe(pkgJsonPath());
|
|
936
|
+
let _incomingVersion = null;
|
|
937
|
+
try {
|
|
938
|
+
_incomingVersion = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')).version;
|
|
939
|
+
} catch {
|
|
940
|
+
/* unreadable own package.json — cannot prove a downgrade, allow */
|
|
941
|
+
}
|
|
942
|
+
if (
|
|
943
|
+
_active &&
|
|
944
|
+
_active.pkgVersion &&
|
|
945
|
+
_incomingVersion &&
|
|
946
|
+
classifyInstall(
|
|
947
|
+
{ pkgRoot: PKG_ROOT, version: _incomingVersion },
|
|
948
|
+
{ pkgRoot: _active.pkgRoot, version: _active.pkgVersion },
|
|
949
|
+
) === 'downgrade'
|
|
950
|
+
) {
|
|
951
|
+
console.error(downgradeGuardMessage(_incomingVersion, _active.pkgVersion, 'upgrade --apply'));
|
|
952
|
+
process.exit(2);
|
|
953
|
+
}
|
|
848
954
|
}
|
|
955
|
+
// Migration report is vault-side (writes into the Hypomnema root) and applies
|
|
956
|
+
// in both install models.
|
|
849
957
|
if (schema.bump === 'major' && schema.installed && schema.current && existsSync(args.hypoDir)) {
|
|
850
|
-
migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current
|
|
958
|
+
migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current, {
|
|
959
|
+
// Use the core-skipped predicate, not raw pluginMode: in a dual-install skip
|
|
960
|
+
// the core surface is plugin-owned too, so the report must not claim the
|
|
961
|
+
// core hooks/settings were applied (ISSUE-8, W2 review note).
|
|
962
|
+
pluginMode: !managesClaudeCore,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (managesClaudeCore) {
|
|
966
|
+
if (oldHookRefs.length > 0) {
|
|
967
|
+
appliedHookNameRenames = applyHookNameMigration(
|
|
968
|
+
oldHookRefs,
|
|
969
|
+
claudeSettingsPath,
|
|
970
|
+
claudeHooksDir,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
appliedHooks = applyHookFiles(hooks, claudeHooksDir);
|
|
974
|
+
appliedSettings = applySettingsJson(settings, claudeSettingsPath);
|
|
975
|
+
// applyCommands handles the single atomic hypo-pkg.json write (pkgRoot, version, schema, commands map)
|
|
976
|
+
appliedCommands = applyCommands(commands, args.forceCommands);
|
|
977
|
+
appliedPkgJson = true;
|
|
978
|
+
} else if (pluginMode) {
|
|
979
|
+
// ISSUE-6 plugin mode: the plugin loader owns the core hooks/commands and
|
|
980
|
+
// settings.json wiring — copying them here would double-register. Skip those,
|
|
981
|
+
// but STILL write minimal package metadata so the runtime can resolve PKG_ROOT
|
|
982
|
+
// for lint/feedback scripts (hooks/hypo-shared.mjs → hypo-personal-check). The
|
|
983
|
+
// commands SHA map is intentionally omitted (no command copy happened). PKG_ROOT
|
|
984
|
+
// is the plugin's own path here, so this metadata is authoritative.
|
|
985
|
+
appliedPkgJson = writePluginModeMetadata();
|
|
986
|
+
} else {
|
|
987
|
+
// ISSUE-8 dual-install skip: a manual/npm run while the plugin is enabled. We
|
|
988
|
+
// skip the core surface (the plugin owns it), but — unlike true plugin mode —
|
|
989
|
+
// PKG_ROOT here is the npm/manual path while the ACTIVE runtime hooks are the
|
|
990
|
+
// PLUGIN's. Rewriting a VALID hypo-pkg.json.pkgRoot to this npm path would
|
|
991
|
+
// mis-point the plugin runtime's lint/feedback resolution, so we PRESERVE an
|
|
992
|
+
// existing plugin-written identity (pkgJson.status 'stale'/'up-to-date' both
|
|
993
|
+
// mean a usable pkgRoot is already on disk) and do not touch it.
|
|
994
|
+
//
|
|
995
|
+
// If the metadata is MISSING or corrupt (status 'missing'; corrupt files are
|
|
996
|
+
// renamed to *.corrupt-*.json by readPkgJson and then read as absent), there is
|
|
997
|
+
// no plugin identity to preserve. Write minimal fallback metadata pointing at
|
|
998
|
+
// this (same-version) npm copy so the plugin runtime can resolve a package root
|
|
999
|
+
// at all — strictly better than the pkgRoot-less file extension sync would
|
|
1000
|
+
// otherwise create, or no file at all. The dual-install banner still tells the
|
|
1001
|
+
// user to resolve the dual install.
|
|
1002
|
+
if (pkgJson.status === 'missing') {
|
|
1003
|
+
appliedPkgJson = writePluginModeMetadata();
|
|
1004
|
+
} else {
|
|
1005
|
+
appliedPkgJson = false;
|
|
1006
|
+
}
|
|
851
1007
|
}
|
|
852
|
-
appliedHooks = applyHookFiles(hooks, claudeHooksDir);
|
|
853
|
-
appliedSettings = applySettingsJson(settings, claudeSettingsPath);
|
|
854
|
-
// applyCommands handles the single atomic hypo-pkg.json write (pkgRoot, version, schema, commands map)
|
|
855
|
-
appliedCommands = applyCommands(commands, args.forceCommands);
|
|
856
|
-
appliedPkgJson = true;
|
|
857
1008
|
appliedHypoignore = applyHypoignoreMigration(hypoignore);
|
|
858
|
-
//
|
|
1009
|
+
// codex core hooks + settings + wiki-*→hypo-* rename mirror. Same order
|
|
859
1010
|
// as the claude side (rename first so subsequent hook copy can find renamed targets).
|
|
860
1011
|
if (args.codex) {
|
|
861
1012
|
if (oldHookRefsCodex.length > 0) {
|
|
@@ -878,7 +1029,7 @@ if (args.apply) {
|
|
|
878
1029
|
apply: true,
|
|
879
1030
|
force: args.forceExtensions,
|
|
880
1031
|
});
|
|
881
|
-
// E4 (#32): codex apply runs AFTER the claude apply so it reads the freshly
|
|
1032
|
+
// E4 (fix #32): codex apply runs AFTER the claude apply so it reads the freshly
|
|
882
1033
|
// written hypo-pkg.json and merges extensions.codex alongside extensions.claude
|
|
883
1034
|
// (the per-target spread in syncExtensions preserves the other target's map).
|
|
884
1035
|
if (args.codex) {
|
|
@@ -898,7 +1049,7 @@ if (args.apply) {
|
|
|
898
1049
|
|
|
899
1050
|
const extDrift = extCheck.needsWork || (extCheckCodex?.needsWork ?? false);
|
|
900
1051
|
|
|
901
|
-
//
|
|
1052
|
+
// codex drift only counts when --codex is set — without the flag the codex
|
|
902
1053
|
// target is intentionally unobserved (parity with the existing extensions pattern).
|
|
903
1054
|
const codexCoreDrift =
|
|
904
1055
|
args.codex &&
|
|
@@ -907,17 +1058,28 @@ const codexCoreDrift =
|
|
|
907
1058
|
invalidSettingsCodex ||
|
|
908
1059
|
(oldHookRefsCodex?.length ?? 0) > 0);
|
|
909
1060
|
|
|
910
|
-
|
|
1061
|
+
// Claude core-surface drift (hooks/settings/commands/rename/metadata). In plugin
|
|
1062
|
+
// mode these are plugin-managed, so they must NOT count as drift — otherwise the
|
|
1063
|
+
// report nags "N items need updating" and recommends a double-registering --apply.
|
|
1064
|
+
// Plugin-provided surface (hooks/settings/commands/rename) — excluded from drift
|
|
1065
|
+
// in plugin mode. pkgJsonDrift is intentionally NOT here: hypo-pkg.json is written
|
|
1066
|
+
// in BOTH install models (plugin mode writes minimal metadata so the runtime can
|
|
1067
|
+
// resolve PKG_ROOT for lint/feedback), so a missing/stale metadata file should
|
|
1068
|
+
// still prompt a (safe, metadata-only) --apply.
|
|
1069
|
+
const claudeCoreDrift =
|
|
911
1070
|
staleHooks.length > 0 ||
|
|
912
1071
|
missingSettings.length > 0 ||
|
|
913
|
-
schemaDrift ||
|
|
914
1072
|
invalidSettings ||
|
|
915
|
-
pkgJsonDrift ||
|
|
916
1073
|
oldHookRefs.length > 0 ||
|
|
917
1074
|
staleCommands.length > 0 ||
|
|
918
1075
|
userModifiedCommands.length > 0 ||
|
|
919
1076
|
orphanedCommands.length > 0 ||
|
|
920
|
-
nonRegularCommands.length > 0
|
|
1077
|
+
nonRegularCommands.length > 0;
|
|
1078
|
+
|
|
1079
|
+
const hasDrift =
|
|
1080
|
+
(managesClaudeCore && claudeCoreDrift) ||
|
|
1081
|
+
pkgJsonDrift ||
|
|
1082
|
+
schemaDrift ||
|
|
921
1083
|
hypoignore.status === 'needs-migration' ||
|
|
922
1084
|
extDrift ||
|
|
923
1085
|
codexCoreDrift;
|
|
@@ -926,6 +1088,12 @@ if (args.json) {
|
|
|
926
1088
|
console.log(
|
|
927
1089
|
JSON.stringify(
|
|
928
1090
|
{
|
|
1091
|
+
pluginMode,
|
|
1092
|
+
// ISSUE-8 dual-install signals.
|
|
1093
|
+
hypomnemaPluginEnabled,
|
|
1094
|
+
dualInstallCoreConflict,
|
|
1095
|
+
coreManagedBy: managesClaudeCore ? 'self' : pluginMode ? 'plugin' : 'plugin-enabled',
|
|
1096
|
+
dualInstallOverride: args.allowDualInstall,
|
|
929
1097
|
schema,
|
|
930
1098
|
hooks,
|
|
931
1099
|
settings,
|
|
@@ -935,7 +1103,7 @@ if (args.json) {
|
|
|
935
1103
|
hypoignore,
|
|
936
1104
|
extensions: extCheck,
|
|
937
1105
|
extensionsCodex: extCheckCodex,
|
|
938
|
-
//
|
|
1106
|
+
// codex core mirror (null when --codex absent).
|
|
939
1107
|
hooksCodex,
|
|
940
1108
|
settingsCodex,
|
|
941
1109
|
oldHookRefsCodex,
|
|
@@ -970,6 +1138,49 @@ if (args.json) {
|
|
|
970
1138
|
// Human-readable report
|
|
971
1139
|
const lines = [];
|
|
972
1140
|
|
|
1141
|
+
// ISSUE-6: lead with the plugin-mode banner so the user understands why the core
|
|
1142
|
+
// hook/command/settings sections read "managed by plugin" and that `--apply` will
|
|
1143
|
+
// NOT touch them (only vault-side migrations + package metadata).
|
|
1144
|
+
if (pluginMode) {
|
|
1145
|
+
lines.push(
|
|
1146
|
+
'ℹ Plugin install detected — Hypomnema is loaded via the Claude Code plugin.',
|
|
1147
|
+
' Core hooks, slash commands, and settings.json wiring are provided by the',
|
|
1148
|
+
' plugin loader, so `/hypo:upgrade` does NOT manage them (and `--apply` will',
|
|
1149
|
+
' not copy/register them — that would double-register every hook).',
|
|
1150
|
+
' → To upgrade the plugin: `/plugin marketplace update hypomnema` then `/reload-plugins`.',
|
|
1151
|
+
' → `/hypo:upgrade --apply` here applies vault-side migrations (SCHEMA,',
|
|
1152
|
+
' .hypoignore), refreshes package metadata, and still syncs any vault',
|
|
1153
|
+
' extensions — but does NOT install the core hooks/commands/settings.',
|
|
1154
|
+
'',
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ISSUE-8: a manual/npm upgrade.mjs is running while the Hypomnema plugin is ALSO
|
|
1159
|
+
// enabled — a dual install. Lead with a loud banner: the core surface is owned by
|
|
1160
|
+
// the plugin and is intentionally skipped, so `--apply` will not double-register.
|
|
1161
|
+
if (dualSkip) {
|
|
1162
|
+
lines.push(
|
|
1163
|
+
'⚠ Dual install detected — you are running the MANUAL/npm `upgrade.mjs`, but the',
|
|
1164
|
+
' Hypomnema plugin is ALSO enabled in ~/.claude/settings.json. The plugin loader',
|
|
1165
|
+
' already provides the core hooks, slash commands, and settings.json wiring, so',
|
|
1166
|
+
' this run does NOT copy/register them (doing so would double-register every hook).',
|
|
1167
|
+
' → Recommended: pick ONE install. To keep the plugin, remove the npm/manual copy',
|
|
1168
|
+
' (`npm uninstall -g hypomnema`) and upgrade via `/plugin marketplace update',
|
|
1169
|
+
' hypomnema` + `/reload-plugins`. Vault extensions + codex (if any) are still synced.',
|
|
1170
|
+
' → To register the core surface here anyway (knowingly accept the double-register',
|
|
1171
|
+
' risk), re-run with `--allow-dual-install`.',
|
|
1172
|
+
'',
|
|
1173
|
+
);
|
|
1174
|
+
} else if (dualInstallCoreConflict && args.allowDualInstall) {
|
|
1175
|
+
// Override path: the user forced core registration despite the enabled plugin.
|
|
1176
|
+
lines.push(
|
|
1177
|
+
'⚠ Dual install — `--allow-dual-install` set: registering the Claude core surface',
|
|
1178
|
+
' even though the Hypomnema plugin is enabled. Every core hook may now fire TWICE',
|
|
1179
|
+
' (plugin loader + ~/.claude registration) until one install is removed.',
|
|
1180
|
+
'',
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
973
1184
|
// Schema version
|
|
974
1185
|
if (schema.bump === 'none') {
|
|
975
1186
|
lines.push(`✓ SCHEMA version ${schema.installed} (up to date)`);
|
|
@@ -1015,7 +1226,13 @@ function pushHookSummary(hookList, label, targetPath) {
|
|
|
1015
1226
|
}
|
|
1016
1227
|
}
|
|
1017
1228
|
}
|
|
1018
|
-
|
|
1229
|
+
if (managesClaudeCore) {
|
|
1230
|
+
pushHookSummary(hooks, '', '~/.claude/hooks/');
|
|
1231
|
+
} else {
|
|
1232
|
+
lines.push(
|
|
1233
|
+
'✓ Hook files provided by the plugin loader (not managed in ~/.claude/hooks/)',
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1019
1236
|
if (hooksCodex) pushHookSummary(hooksCodex, ' (codex)', '~/.codex/hooks/');
|
|
1020
1237
|
|
|
1021
1238
|
// settings.json registrations (target-aware mirror; fix #48).
|
|
@@ -1034,11 +1251,33 @@ function pushSettingsSummary(sList, label, invalidFlag) {
|
|
|
1034
1251
|
}
|
|
1035
1252
|
}
|
|
1036
1253
|
}
|
|
1037
|
-
|
|
1254
|
+
if (managesClaudeCore) {
|
|
1255
|
+
pushSettingsSummary(settings, '', invalidSettings);
|
|
1256
|
+
} else {
|
|
1257
|
+
lines.push(
|
|
1258
|
+
'✓ settings.json hook wiring provided by the plugin (no ~/.claude registration)',
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1038
1261
|
if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSettingsCodex);
|
|
1039
1262
|
|
|
1040
1263
|
// Package metadata
|
|
1041
|
-
if (pkgJson.status === '
|
|
1264
|
+
if (dualSkip && pkgJson.status === 'stale') {
|
|
1265
|
+
// ISSUE-8: the 'stale' here is the preserved plugin identity (pkgRoot points at
|
|
1266
|
+
// the plugin, not this npm/manual copy). That is intentional — not actionable.
|
|
1267
|
+
lines.push(
|
|
1268
|
+
`✓ Package metadata hypo-pkg.json plugin-owned (preserved — not rewritten in a dual install)`,
|
|
1269
|
+
);
|
|
1270
|
+
} else if (dualSkip && pkgJson.status === 'missing') {
|
|
1271
|
+
// Missing/corrupt in a dual install: there is no plugin identity to preserve, so
|
|
1272
|
+
// `--apply` writes minimal fallback metadata (pointing at this same-version npm
|
|
1273
|
+
// copy) — enough for the plugin runtime to resolve its scripts. The real fix is
|
|
1274
|
+
// still to resolve the dual install.
|
|
1275
|
+
lines.push(
|
|
1276
|
+
`⚠ Package metadata hypo-pkg.json missing/unreadable — \`--apply\` writes fallback metadata`,
|
|
1277
|
+
` for this npm copy so the plugin runtime can resolve its scripts.`,
|
|
1278
|
+
` Better: resolve the dual install (remove the npm/manual copy).`,
|
|
1279
|
+
);
|
|
1280
|
+
} else if (pkgJson.status === 'up-to-date') {
|
|
1042
1281
|
lines.push(`✓ Package metadata hypo-pkg.json up to date`);
|
|
1043
1282
|
} else if (pkgJson.status === 'stale') {
|
|
1044
1283
|
lines.push(
|
|
@@ -1055,7 +1294,9 @@ const cmdMissCount = commands.filter((c) => c.status === 'missing').length;
|
|
|
1055
1294
|
const cmdUserCount = userModifiedCommands.length;
|
|
1056
1295
|
const cmdOrphanCount = orphanedCommands.length;
|
|
1057
1296
|
const cmdNonRegCount = nonRegularCommands.length;
|
|
1058
|
-
if (
|
|
1297
|
+
if (!managesClaudeCore) {
|
|
1298
|
+
lines.push('✓ Slash commands provided by the plugin loader (not ~/.claude/commands/hypo/)');
|
|
1299
|
+
} else if (commands.length === 0) {
|
|
1059
1300
|
lines.push(`⚠ Slash commands package commands/ is empty`);
|
|
1060
1301
|
} else if (
|
|
1061
1302
|
cmdStaleCount === 0 &&
|
|
@@ -1090,7 +1331,7 @@ if (commands.length === 0) {
|
|
|
1090
1331
|
}
|
|
1091
1332
|
|
|
1092
1333
|
// Old hook names (wiki-*.mjs → hypo-*.mjs rename migration). Target-aware so
|
|
1093
|
-
//
|
|
1334
|
+
// codex settings.json entries that still reference the v1.0/v1.1 names are surfaced.
|
|
1094
1335
|
function pushHookNameSummary(refs, label) {
|
|
1095
1336
|
if (refs.length > 0) {
|
|
1096
1337
|
lines.push(
|
|
@@ -1102,7 +1343,10 @@ function pushHookNameSummary(refs, label) {
|
|
|
1102
1343
|
lines.push(`✓ ${colN}All hook references use current hypo-*.mjs names`);
|
|
1103
1344
|
}
|
|
1104
1345
|
}
|
|
1105
|
-
|
|
1346
|
+
// In plugin mode the Claude settings.json is plugin-owned and --apply skips the
|
|
1347
|
+
// rename migration, so do not print a "run --apply to rename" instruction it will
|
|
1348
|
+
// not honor. (codex hook-name migration is unaffected by pluginMode.)
|
|
1349
|
+
if (managesClaudeCore) pushHookNameSummary(oldHookRefs, '');
|
|
1106
1350
|
if (oldHookRefsCodex) pushHookNameSummary(oldHookRefsCodex, ' (codex)');
|
|
1107
1351
|
|
|
1108
1352
|
// .hypoignore migration (ensure required runtime patterns are present)
|
|
@@ -1142,7 +1386,7 @@ function pushExtSummary(check, label) {
|
|
|
1142
1386
|
for (const c of check.conflicts) lines.push(` ✗ ${c.file} [${c.action} — left untouched]`);
|
|
1143
1387
|
for (const d of check.drifts) lines.push(` ⚠ ${d.file} [drift — left untouched]`);
|
|
1144
1388
|
}
|
|
1145
|
-
// E3 (#31): a hard conflict blocks install (exit 1, even under --apply); drift is
|
|
1389
|
+
// E3 (fix #31): a hard conflict blocks install (exit 1, even under --apply); drift is
|
|
1146
1390
|
// resolvable advisory. Emit the spec'd WIKI messages so the user knows the recovery.
|
|
1147
1391
|
if (nConflicts > 0) {
|
|
1148
1392
|
lines.push(' [WIKI: existing file conflicts. Backup and retry, or use --force-extensions]');
|
|
@@ -1200,7 +1444,7 @@ if (
|
|
|
1200
1444
|
lines.push(`✓ Appended .hypoignore entries (${appliedHypoignore.length}):`);
|
|
1201
1445
|
for (const e of appliedHypoignore) lines.push(` → ${e}`);
|
|
1202
1446
|
}
|
|
1203
|
-
//
|
|
1447
|
+
// codex-target applied actions (mirrors claude blocks above).
|
|
1204
1448
|
if (appliedHookNameRenamesCodex.length > 0) {
|
|
1205
1449
|
lines.push(`✓ Renamed legacy hook references (codex) (${appliedHookNameRenamesCodex.length}):`);
|
|
1206
1450
|
for (const r of appliedHookNameRenamesCodex) lines.push(` → ${r}`);
|
|
@@ -1237,26 +1481,32 @@ pushAppliedExt(appliedExtensionsCodex, ' (codex)');
|
|
|
1237
1481
|
|
|
1238
1482
|
// Summary
|
|
1239
1483
|
lines.push('');
|
|
1484
|
+
// Claude core-surface item count — zeroed in plugin mode (plugin-managed), so the
|
|
1485
|
+
// summary never reads "N items need updating" for hooks/settings/commands.
|
|
1486
|
+
const claudeCoreCount = managesClaudeCore
|
|
1487
|
+
? staleHooks.length +
|
|
1488
|
+
missingSettings.length +
|
|
1489
|
+
(invalidSettings ? 1 : 0) +
|
|
1490
|
+
oldHookRefs.length +
|
|
1491
|
+
staleCommands.length +
|
|
1492
|
+
userModifiedCommands.length +
|
|
1493
|
+
orphanedCommands.length +
|
|
1494
|
+
nonRegularCommands.length
|
|
1495
|
+
: 0;
|
|
1496
|
+
|
|
1240
1497
|
const totalDrift =
|
|
1241
|
-
|
|
1242
|
-
missingSettings.length +
|
|
1243
|
-
(schemaDrift ? 1 : 0) +
|
|
1244
|
-
(invalidSettings ? 1 : 0) +
|
|
1498
|
+
claudeCoreCount +
|
|
1245
1499
|
(pkgJsonDrift ? 1 : 0) +
|
|
1246
|
-
|
|
1247
|
-
staleCommands.length +
|
|
1248
|
-
userModifiedCommands.length +
|
|
1249
|
-
orphanedCommands.length +
|
|
1250
|
-
nonRegularCommands.length +
|
|
1500
|
+
(schemaDrift ? 1 : 0) +
|
|
1251
1501
|
(hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
|
|
1252
1502
|
extCheck.actions.filter(
|
|
1253
1503
|
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1254
1504
|
).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
|
|
1505
|
+
// E3 (fix #31): unresolved drift/conflict is pending work too — without these the
|
|
1506
|
+
// summary printed "up to date" while the exit code was 1.
|
|
1257
1507
|
extCheck.conflicts.length +
|
|
1258
1508
|
extCheck.drifts.length +
|
|
1259
|
-
// E4 (#32): codex-target pending work counts identically (same message/exit
|
|
1509
|
+
// E4 (fix #32): codex-target pending work counts identically (same message/exit
|
|
1260
1510
|
// consistency the E3 review caught — a codex conflict must not read "up to date").
|
|
1261
1511
|
(extCheckCodex
|
|
1262
1512
|
? extCheckCodex.actions.filter(
|
|
@@ -1265,7 +1515,7 @@ const totalDrift =
|
|
|
1265
1515
|
extCheckCodex.conflicts.length +
|
|
1266
1516
|
extCheckCodex.drifts.length
|
|
1267
1517
|
: 0) +
|
|
1268
|
-
//
|
|
1518
|
+
// codex core mirror counts the same way as the claude side.
|
|
1269
1519
|
staleHooksCodex.length +
|
|
1270
1520
|
missingSettingsCodex.length +
|
|
1271
1521
|
(invalidSettingsCodex ? 1 : 0) +
|
|
@@ -1297,7 +1547,7 @@ if (totalDrift === 0) {
|
|
|
1297
1547
|
|
|
1298
1548
|
console.log(lines.join('\n'));
|
|
1299
1549
|
|
|
1300
|
-
// E3 (#31): a hard extension conflict blocks even under --apply (unlike ordinary
|
|
1550
|
+
// E3 (fix #31): a hard extension conflict blocks even under --apply (unlike ordinary
|
|
1301
1551
|
// drift, which only fails check mode). --force-extensions clears the resolvable
|
|
1302
1552
|
// cases; an unfollowable symlink/non-regular dest still counts and stays exit 1.
|
|
1303
1553
|
const extBlocked = extCheck.conflicts.length > 0 || (extCheckCodex?.conflicts.length ?? 0) > 0;
|