hypomnema 1.10.0 → 1.10.2
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/CHANGELOG.md +31 -0
- package/README.ko.md +5 -5
- package/README.md +5 -5
- package/commands/crystallize.md +21 -15
- package/commands/upgrade.md +13 -2
- package/docs/ARCHITECTURE.md +127 -63
- package/docs/CONTRIBUTING.md +7 -11
- package/hooks/base-store.mjs +31 -17
- package/hooks/hypo-auto-minimal-crystallize.mjs +39 -6
- package/hooks/hypo-auto-stage.mjs +123 -63
- package/hooks/hypo-close-guard.mjs +3 -37
- package/hooks/hypo-compact-guard.mjs +9 -6
- package/hooks/hypo-file-watch.mjs +18 -10
- package/hooks/hypo-personal-check.mjs +137 -55
- package/hooks/hypo-session-start.mjs +17 -6
- package/hooks/hypo-shared.mjs +1507 -569
- package/hooks/hypo-stop.mjs +8 -0
- package/hooks/session-views.mjs +9 -3
- package/package.json +2 -1
- package/scripts/crystallize.mjs +1 -1
- package/scripts/doctor.mjs +160 -9
- package/scripts/init.mjs +60 -22
- package/scripts/lib/crystallize-close-apply.mjs +156 -22
- package/scripts/lib/crystallize-close-check.mjs +1 -1
- package/scripts/lib/extensions.mjs +2 -2
- package/scripts/lib/git-hooks-dir.mjs +228 -31
- package/scripts/lib/plugin-detect.mjs +57 -4
- package/scripts/lib/settings-write.mjs +254 -0
- package/scripts/lib/wikilink.mjs +10 -1
- package/scripts/uninstall.mjs +3 -2
- package/scripts/upgrade.mjs +94 -23
- package/templates/Home.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +60 -22
- package/templates/session-state.md +0 -12
|
@@ -68,9 +68,11 @@ import {
|
|
|
68
68
|
import {
|
|
69
69
|
isValidProjectName,
|
|
70
70
|
TRACK_ID_RE,
|
|
71
|
+
buildBaselineEntry,
|
|
71
72
|
closeIdFor,
|
|
72
73
|
entryFileName,
|
|
73
74
|
formatSessionEntry,
|
|
75
|
+
isBaselineId,
|
|
74
76
|
parseSessionEntry,
|
|
75
77
|
resolveSupersedesPrefix,
|
|
76
78
|
splitLegacyFrontmatter,
|
|
@@ -1074,7 +1076,7 @@ function priorReceiptCommitRewritten(hypoDir, sessionId) {
|
|
|
1074
1076
|
// between this CLI and the auto-write at the tail of applySessionClose.
|
|
1075
1077
|
//
|
|
1076
1078
|
// Contract: the marker is written only when precompactGateStatus, run in
|
|
1077
|
-
// checkpointMode (
|
|
1079
|
+
// checkpointMode (which only makes an invalid session id a blocker), is green. A failed gate exits 1 with no
|
|
1078
1080
|
// marker — the next Stop hook re-blocks.
|
|
1079
1081
|
|
|
1080
1082
|
// `testHooks` is test-only, see settleCloseResolution.
|
|
@@ -1098,10 +1100,10 @@ export function runMarkSessionClosed(args, testHooks = null) {
|
|
|
1098
1100
|
// The per-session marker is the THIRD session-close completion
|
|
1099
1101
|
// signal (after the PreCompact gate and `--check-session-close`). It uses
|
|
1100
1102
|
// precompactGateStatus, the gate the PreCompact notice reads, with one deliberate
|
|
1101
|
-
// difference: `checkpointMode` (set below)
|
|
1102
|
-
//
|
|
1103
|
-
// on an uncommitted write THIS session still owns
|
|
1104
|
-
// dirty file
|
|
1103
|
+
// difference: `checkpointMode` (set below) only makes an invalid session id a
|
|
1104
|
+
// blocker of its own. The git axis is the same for every caller: it blocks
|
|
1105
|
+
// only on an uncommitted write THIS session still owns (or failed to record),
|
|
1106
|
+
// so another session's dirty file stays a notice for this checkpoint too.
|
|
1105
1107
|
// Neither one stops /compact: PreCompact only shows a notice.
|
|
1106
1108
|
// Every other axis is the same, so the marker still enforces feedback
|
|
1107
1109
|
// projection (over-cap/conflict), W8 design-history staleness, and root
|
|
@@ -1121,11 +1123,10 @@ export function runMarkSessionClosed(args, testHooks = null) {
|
|
|
1121
1123
|
// resolveGateProjectOverride (session-close-scope-boundary spec §2/§3): closeScope
|
|
1122
1124
|
// above only widens THIS session's accountability set (evidence union for the
|
|
1123
1125
|
// marker's attribution) -- it does not narrow closeAccountableScope's base, so a
|
|
1124
|
-
// DIFFERENT project's dangling close files still land in the
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
// block, and it also decides which projects the marker this call may go on to
|
|
1126
|
+
// DIFFERENT project's dangling close files still land in the lint scope. Passed
|
|
1127
|
+
// below as `attributionScope`: this is the value that feeds the mine/foreign
|
|
1128
|
+
// partition in precompactGateStatus and demotes the foreign project's own
|
|
1129
|
+
// close-file debt to a notice instead of block, and it also decides which projects the marker this call may go on to
|
|
1129
1130
|
// write ends up attesting -- so passing `projectOverride` instead would narrow
|
|
1130
1131
|
// sessionCloseGlobalStatus to args.project alone and turn the partition off,
|
|
1131
1132
|
// which is the bug this key split fixes (a marker then claimed the demoted
|
|
@@ -1559,7 +1560,7 @@ export function runMarkSessionClosed(args, testHooks = null) {
|
|
|
1559
1560
|
// computed once per run, so neither repeats on a later gate read.
|
|
1560
1561
|
if (hostTagWarning) console.log(`\n⚠ ${hostTagWarning}`);
|
|
1561
1562
|
if (receiptResult.leftUnverifiable) console.log(` · ${LEFT_UNVERIFIABLE_NOTE}`);
|
|
1562
|
-
|
|
1563
|
+
printGateNotices(result.notices);
|
|
1563
1564
|
}
|
|
1564
1565
|
process.exit(0);
|
|
1565
1566
|
}
|
|
@@ -1909,12 +1910,38 @@ export function ensureProjectIndex(hypoDir, project, relPath, today, sessionId)
|
|
|
1909
1910
|
const dest = join(hypoDir, relPath);
|
|
1910
1911
|
const content = projectIndexSeed(project, today);
|
|
1911
1912
|
if (content === null) return null; // template missing: nothing to scaffold from
|
|
1913
|
+
// The close gate has to know the file is this session's before it exists: a death between the
|
|
1914
|
+
// create and the write leaves a zero-byte index.md that nothing else would claim. So the record
|
|
1915
|
+
// comes first, with the seed's hash rather than the empty file's, so the Stop auto-commit does
|
|
1916
|
+
// not retire it (nor does it retire a claim whose file does not exist yet). A record that fails
|
|
1917
|
+
// stops here, before the create: its failure flag names the path, and the gate blocks it.
|
|
1918
|
+
//
|
|
1919
|
+
// Only when the file is absent now: recordTouchedPaths overwrites the hash of a claim that is
|
|
1920
|
+
// already there, and a file that exists may carry this session's own edit. Replacing that claim
|
|
1921
|
+
// with the seed hash lets the Stop commit retire it when HEAD holds the seed (another session
|
|
1922
|
+
// committed it), leaving the edit dirty and unclaimed. `onlyIfUnclaimed` closes the same hole for
|
|
1923
|
+
// an edit recorded between this check and the record below: the session lock decides, so an
|
|
1924
|
+
// existing claim keeps its hash. A file created between this check and the `wx` open below
|
|
1925
|
+
// leaves the seed claim behind, which the coarse rule accepts: this session had no claim on it.
|
|
1926
|
+
if (existsSync(dest)) return null;
|
|
1927
|
+
const seedHash = bytesSha256(content);
|
|
1928
|
+
if (
|
|
1929
|
+
sessionId &&
|
|
1930
|
+
!recordTouchedPaths(hypoDir, sessionId, [relPath], {
|
|
1931
|
+
expectedHashes: { [relPath]: seedHash },
|
|
1932
|
+
onlyIfUnclaimed: true,
|
|
1933
|
+
})
|
|
1934
|
+
)
|
|
1935
|
+
return null;
|
|
1912
1936
|
mkdirSync(dirname(dest), { recursive: true });
|
|
1913
1937
|
let fd;
|
|
1914
1938
|
try {
|
|
1915
1939
|
fd = openSync(dest, 'wx');
|
|
1916
1940
|
} catch (e) {
|
|
1917
|
-
|
|
1941
|
+
// Another writer already created it. The claim just recorded stays: taking it back cannot tell
|
|
1942
|
+
// this call's claim from a same-session one that overlaps it, so the coarse rule keeps it. The
|
|
1943
|
+
// gate then blocks the file as this session's until it is committed, which retires the claim.
|
|
1944
|
+
if (e && e.code === 'EEXIST') return null;
|
|
1918
1945
|
throw e;
|
|
1919
1946
|
}
|
|
1920
1947
|
try {
|
|
@@ -2371,6 +2398,81 @@ const ENTRY_CONFLICT_HELP = (relPath, reason) =>
|
|
|
2371
2398
|
`Nothing was changed. Look at the file and the vault history, then move or remove the ` +
|
|
2372
2399
|
`foreign bytes yourself and retry.`;
|
|
2373
2400
|
|
|
2401
|
+
/**
|
|
2402
|
+
* The id a baseline built from `text` (the flat session-state.md) gets, with `entry`'s stored done
|
|
2403
|
+
* heads and scope; `null` when the entry cannot be rebuilt.
|
|
2404
|
+
*/
|
|
2405
|
+
function rebuiltBaselineId(entry, text, project) {
|
|
2406
|
+
try {
|
|
2407
|
+
return buildBaselineEntry({
|
|
2408
|
+
kind: 'state',
|
|
2409
|
+
text,
|
|
2410
|
+
date: entry.date,
|
|
2411
|
+
visibilityScope: entry.visibilityScope,
|
|
2412
|
+
project: entry.project ?? project,
|
|
2413
|
+
legacyDone: entry.tracks.filter((t) => t.done === true).map((t) => t.id),
|
|
2414
|
+
}).closeId;
|
|
2415
|
+
} catch {
|
|
2416
|
+
return null; // an input that cannot be rebuilt is not proof, so the baseline is not counted
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* The baseline heads this session was shown when it read the old flat `session-state.md`:
|
|
2422
|
+
* `{trackId: [closeId]}`. A session that began before the vault moved to entries read that file,
|
|
2423
|
+
* and its base record holds the file's hash. The move wrote the same bytes into a baseline, so a
|
|
2424
|
+
* baseline whose source (the flat file in the parent of the commit that added it) hashes to what the
|
|
2425
|
+
* session read counts as heard, and a close can replace its heads. The source must also build the
|
|
2426
|
+
* baseline's own id (see rebuiltBaselineId), else the baseline holds other bytes than the session
|
|
2427
|
+
* read and is left alone. The LF source and its CRLF form
|
|
2428
|
+
* are both compared, since a checkout with autocrlf shows the session the CRLF bytes. `{}` when the
|
|
2429
|
+
* vault is not moved, the session recorded no hash of the file, or no baseline matches (a
|
|
2430
|
+
* baseline made from local bytes that differ from HEAD has no source to find, so it is not
|
|
2431
|
+
* counted, as before).
|
|
2432
|
+
*/
|
|
2433
|
+
function flatReadBaselineHeads(hypoDir, sessionId, project, model) {
|
|
2434
|
+
if (!model.migrated) return {};
|
|
2435
|
+
const rel = `projects/${project}/session-state.md`;
|
|
2436
|
+
const base = readBaseEntry(hypoDir, sessionId, rel);
|
|
2437
|
+
const wanted = new Set(
|
|
2438
|
+
[base.state === 'hash' ? base.hash : null, readObservedHash(hypoDir, sessionId, rel)].filter(
|
|
2439
|
+
Boolean,
|
|
2440
|
+
),
|
|
2441
|
+
);
|
|
2442
|
+
if (wanted.size === 0) return {};
|
|
2443
|
+
const git = (...a) =>
|
|
2444
|
+
spawnSync('git', ['-C', hypoDir, ...a], { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
2445
|
+
const heads = {};
|
|
2446
|
+
for (const e of model.entries) {
|
|
2447
|
+
if (!isBaselineId(e.closeId) || e.tracks.length === 0) continue;
|
|
2448
|
+
const added = git(
|
|
2449
|
+
'log',
|
|
2450
|
+
'--diff-filter=A',
|
|
2451
|
+
'--format=%H',
|
|
2452
|
+
'-n',
|
|
2453
|
+
'1',
|
|
2454
|
+
'--',
|
|
2455
|
+
`:(glob)projects/${project}/sessions/*-${e.closeId}.md`,
|
|
2456
|
+
);
|
|
2457
|
+
const sha = added.status === 0 ? added.stdout.trim() : '';
|
|
2458
|
+
if (!sha) continue;
|
|
2459
|
+
const source = git('show', revPathArg(`${sha}^`, rel));
|
|
2460
|
+
if (source.status !== 0) continue;
|
|
2461
|
+
const text = source.stdout;
|
|
2462
|
+
const crlf = text.replace(/\n/g, '\r\n');
|
|
2463
|
+
const match = wanted.has(hashContent(text)) || wanted.has(hashContent(crlf));
|
|
2464
|
+
if (!match) continue;
|
|
2465
|
+
// The hash proves the parent held the bytes the session read, not that those bytes made this
|
|
2466
|
+
// baseline: an archive resumed while the flat file was still tracked can publish a baseline of
|
|
2467
|
+
// other saved bytes in a commit whose parent holds the old file. Rebuild the id from the parent
|
|
2468
|
+
// text (done heads and scope as the baseline stores them) and count the baseline only when it
|
|
2469
|
+
// is the same one.
|
|
2470
|
+
if (rebuiltBaselineId(e, text, project) !== e.closeId) continue;
|
|
2471
|
+
for (const t of e.tracks) heads[t.id] = [...new Set([...(heads[t.id] ?? []), e.closeId])];
|
|
2472
|
+
}
|
|
2473
|
+
return heads;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2374
2476
|
/**
|
|
2375
2477
|
* Everything about this close's entry that can be decided before a byte is
|
|
2376
2478
|
* written: track checks, default and explicit `supersedes`, the entry bytes, and
|
|
@@ -2405,6 +2507,11 @@ function planSessionEntry(args, payload, project, entryPath, closeId, legacyForm
|
|
|
2405
2507
|
}
|
|
2406
2508
|
}
|
|
2407
2509
|
const observed = readObservedHeads(args.hypoDir, args.sessionId, project);
|
|
2510
|
+
// Both the default `supersedes` and the `legacy-*` retirement below read `observed.full`.
|
|
2511
|
+
const flatRead = flatReadBaselineHeads(args.hypoDir, args.sessionId, project, model);
|
|
2512
|
+
for (const [id, ids] of Object.entries(flatRead)) {
|
|
2513
|
+
observed.full[id] = [...new Set([...(observed.full[id] ?? []), ...ids])];
|
|
2514
|
+
}
|
|
2408
2515
|
const entryTracks = [];
|
|
2409
2516
|
const legacySplitRetired = [];
|
|
2410
2517
|
const bodies = {};
|
|
@@ -4455,10 +4562,9 @@ function runMarkerPhase(args, project, appliedPaths, ok, hostTagWarning, receipt
|
|
|
4455
4562
|
...(closeTranscript ? { transcriptPath: closeTranscript } : {}),
|
|
4456
4563
|
...(autoMarkerOverride ? { attributionScope: autoMarkerOverride } : {}),
|
|
4457
4564
|
// This is one of the two marker-writing paths that must share ONE
|
|
4458
|
-
// `ok` invariant with `--mark-session-closed`.
|
|
4459
|
-
// there an uncommitted write THIS session still owns"
|
|
4460
|
-
//
|
|
4461
|
-
// (close files, cwd, hot, lint, W8, feedback) is unchanged.
|
|
4565
|
+
// `ok` invariant with `--mark-session-closed`. The git axis asks "is
|
|
4566
|
+
// there an uncommitted write THIS session still owns" for every caller;
|
|
4567
|
+
// checkpointMode only makes an invalid session id a blocker of its own.
|
|
4462
4568
|
checkpointMode: true,
|
|
4463
4569
|
sessionId: args.sessionId,
|
|
4464
4570
|
closeOpen,
|
|
@@ -4911,7 +5017,8 @@ function buildCloseResult({
|
|
|
4911
5017
|
gateNotices: gateNotices || [],
|
|
4912
5018
|
// The checkpoint gate's own blockers when it refused the marker
|
|
4913
5019
|
// (markerSkipReason compact-gate-not-ok); empty otherwise. --check-session-close
|
|
4914
|
-
//
|
|
5020
|
+
// runs the same git check but may lack this session's id, in which case it reads
|
|
5021
|
+
// no record and blocks the whole project folder, so it is not a substitute.
|
|
4915
5022
|
gateBlockers: gateBlockers || [],
|
|
4916
5023
|
// Always present (possibly empty), same visibility contract as `notices`/
|
|
4917
5024
|
// `otherDebtCount` above — a caller should not have to guess whether the
|
|
@@ -4944,6 +5051,24 @@ function buildCloseResult({
|
|
|
4944
5051
|
const DOCTOR_HINT =
|
|
4945
5052
|
'doctor (`/hypo:doctor` in Claude Code, or `hypomnema doctor` with the npm CLI)';
|
|
4946
5053
|
|
|
5054
|
+
// The gate's demotions (what it declined to block on, such as a lint the deadline cut short) in
|
|
5055
|
+
// the text report. JSON carries them as `notices` / `gateNotices`; without this a terminal user
|
|
5056
|
+
// reads a bare success for a close the gate could not fully check.
|
|
5057
|
+
function printGateNotices(notices) {
|
|
5058
|
+
if (!notices || notices.length === 0) return;
|
|
5059
|
+
console.log(' Gate notices (not blockers):');
|
|
5060
|
+
for (const n of notices) console.log(` · ${n.reason}`);
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
// The gate's blockers in the text report, one ✗ line each with its type, its file when it names
|
|
5064
|
+
// one, and its reason. JSON carries them as `gateBlockers`; without these lines the text report
|
|
5065
|
+
// named no blocker at all and sent the reader to --json to find out what to fix.
|
|
5066
|
+
function printGateBlockers(blockers) {
|
|
5067
|
+
if (!blockers || blockers.length === 0) return;
|
|
5068
|
+
console.log(' Gate blockers:');
|
|
5069
|
+
for (const b of blockers) console.log(` ✗ ${b.type}${b.file ? ` ${b.file}` : ''}: ${b.reason}`);
|
|
5070
|
+
}
|
|
5071
|
+
|
|
4947
5072
|
// The human-readable (non --json) rendering of the same settled state.
|
|
4948
5073
|
function printCloseReport({
|
|
4949
5074
|
project,
|
|
@@ -4969,6 +5094,8 @@ function printCloseReport({
|
|
|
4969
5094
|
restructureWaivers,
|
|
4970
5095
|
obsoleteNotices,
|
|
4971
5096
|
hostTagWarning,
|
|
5097
|
+
gateNotices,
|
|
5098
|
+
gateBlockers,
|
|
4972
5099
|
}) {
|
|
4973
5100
|
console.log(`Session-close apply (project: ${project}, date: ${date}):`);
|
|
4974
5101
|
for (const a of applied) console.log(` ✓ wrote ${a}`);
|
|
@@ -4979,6 +5106,8 @@ function printCloseReport({
|
|
|
4979
5106
|
for (const n of obsoleteNotices || []) console.log(` · ${n}`);
|
|
4980
5107
|
// What the verification saw but did not gate on (this close has no log lines under its date).
|
|
4981
5108
|
for (const n of verification?.notices || []) console.log(` · ${n.reason}`);
|
|
5109
|
+
printGateNotices(gateNotices);
|
|
5110
|
+
printGateBlockers(gateBlockers);
|
|
4982
5111
|
// Surfaced unconditionally, success or failure. `restructure: true` no
|
|
4983
5112
|
// longer waives the guard: the target below is still WITHHELD
|
|
4984
5113
|
// and shows up in the conflicts loop right after this one. This line only
|
|
@@ -5129,11 +5258,14 @@ function printCloseReport({
|
|
|
5129
5258
|
` refused the per-session Stop-chain marker because a gate blocker\n` +
|
|
5130
5259
|
` remains. The session is NOT fully closed: the Stop hook will re-prompt\n` +
|
|
5131
5260
|
` until the marker is present. This is not a --session-id problem.\n` +
|
|
5132
|
-
` To fix: resolve
|
|
5133
|
-
`
|
|
5134
|
-
`
|
|
5135
|
-
`
|
|
5136
|
-
`
|
|
5261
|
+
` To fix: resolve each gate blocker (the ✗ lines above name each blocker,\n` +
|
|
5262
|
+
` as gateBlockers[] does in --json): the uncommitted file this session\n` +
|
|
5263
|
+
` recorded writing, or a flagged one: commit or revert it; a project folder\n` +
|
|
5264
|
+
` blocked as a whole (type git: no record stamp, an incomplete record, or\n` +
|
|
5265
|
+
` no session id), which its reason says how to clear, since a file in it\n` +
|
|
5266
|
+
` may be another session's; or the cause of a skipped check. Then re-run\n` +
|
|
5267
|
+
` the same close. No fresh close phrase is needed: a close signal is spent\n` +
|
|
5268
|
+
` only once the marker lands.\n`
|
|
5137
5269
|
: markerSkipReason === 'session-close-lock-timeout'
|
|
5138
5270
|
? ` The close files were applied and committed, but another close of this\n` +
|
|
5139
5271
|
` session held its close lock past the timeout, so no receipt or marker\n` +
|
|
@@ -5794,6 +5926,8 @@ export function applySessionClose(args, testHooks = null) {
|
|
|
5794
5926
|
restructureWaivers,
|
|
5795
5927
|
obsoleteNotices: [...obsoleteNotices, ...notices],
|
|
5796
5928
|
hostTagWarning: hostTagNotice,
|
|
5929
|
+
gateNotices,
|
|
5930
|
+
gateBlockers,
|
|
5797
5931
|
});
|
|
5798
5932
|
}
|
|
5799
5933
|
process.exit(ok ? 0 : 1);
|
|
@@ -41,7 +41,7 @@ export function runSessionCloseCheck(args) {
|
|
|
41
41
|
// --project=<slug> narrows BOTH the close status and the lint scope to that one
|
|
42
42
|
// project: a project-scoped DIAGNOSTIC, NOT the global close verdict.
|
|
43
43
|
// It is check-only: the marker writers stay global (the marker gate is the
|
|
44
|
-
// global gate
|
|
44
|
+
// same global gate, and its git axis is the same one this check uses). When narrowed, the
|
|
45
45
|
// transcript widening is suppressed: a transcript touch in some OTHER project
|
|
46
46
|
// would re-add that project's files to the lint scope and re-block the scoped
|
|
47
47
|
// check, defeating the point. The global (no --project) check keeps widening.
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { join, dirname, relative, resolve, posix, sep } from 'path';
|
|
42
42
|
import { homedir } from 'os';
|
|
43
43
|
import { sha256, isRegularFile, readFileIfRegular, writePkgJsonAtomic } from './pkg-json.mjs';
|
|
44
|
+
import { writeSettingsJson } from './settings-write.mjs';
|
|
44
45
|
import { loadHypoIgnore, isIgnored } from './hypo-ignore.mjs';
|
|
45
46
|
|
|
46
47
|
const HOME = homedir();
|
|
@@ -1914,8 +1915,7 @@ function registerSettings(settingsPath, expectedEntries, apply) {
|
|
|
1914
1915
|
const next = JSON.stringify(settings, null, 2) + '\n';
|
|
1915
1916
|
const changed = next !== original;
|
|
1916
1917
|
if (changed && apply) {
|
|
1917
|
-
|
|
1918
|
-
writeFileSync(settingsPath, next);
|
|
1918
|
+
writeSettingsJson(settingsPath, next);
|
|
1919
1919
|
}
|
|
1920
1920
|
return { registered, changed, invalidJson: false };
|
|
1921
1921
|
}
|