@mindrian_os/install 1.13.0-beta.14 → 1.13.0-beta.16
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 +1 -1
- package/CHANGELOG.md +5 -0
- package/lib/core/cache-prune.cjs +114 -8
- package/lib/core/install-state.cjs +242 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mos",
|
|
3
3
|
"description": "MindrianOS -- Your AI innovation co-founder. Larry thinks with you through PWS methodology, builds your Data Room as you explore, and chains frameworks intelligently. Install and go.",
|
|
4
|
-
"version": "1.13.0-beta.
|
|
4
|
+
"version": "1.13.0-beta.16",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Sagir",
|
|
7
7
|
"url": "https://mindrian.ai"
|
package/CHANGELOG.md
CHANGED
package/lib/core/cache-prune.cjs
CHANGED
|
@@ -31,10 +31,33 @@
|
|
|
31
31
|
*
|
|
32
32
|
* If installed_plugins.json is unreadable (ENOENT, parse error, or no
|
|
33
33
|
* mos@mindrian-marketplace entry), the function returns
|
|
34
|
-
* { kept: [], removed: [], skipped: true, reason: '...' }
|
|
34
|
+
* { kept: [], removed: [], removedBackups: [], skipped: true, reason: '...' }
|
|
35
35
|
* with NO mutation. Never guesses; never deletes anything in the absence
|
|
36
36
|
* of an authoritative active-version answer.
|
|
37
37
|
*
|
|
38
|
+
* Phase 126 Plan-06 extension -- STALE BACKUP DIR PRUNING.
|
|
39
|
+
* In addition to the marketplace-cache version dirs, this function ALSO
|
|
40
|
+
* prunes Phase 95.2's atomic-swap backup directories at
|
|
41
|
+
* ~/.claude/plugins/mindrian-os.stale-<tag>-<timestamp>/
|
|
42
|
+
* that are older than MOS_CACHE_PRUNE_AGE_DAYS (default 30 days).
|
|
43
|
+
*
|
|
44
|
+
* Background: Phase 95.2 wraps `claude plugin update` in an atomic-swap
|
|
45
|
+
* recovery flow. Each recovery leaves a backup of the prior install at
|
|
46
|
+
* the sibling path above. By Phase 95.2 contract, backups are retained
|
|
47
|
+
* "indefinitely" (after 24h the user can delete manually). Long-running
|
|
48
|
+
* tester installs (Lawrence, Gary, etc) never see manual cleanup, so
|
|
49
|
+
* backups accumulate. 30 days is the next operational threshold beyond
|
|
50
|
+
* the 24h user-driven cleanup window.
|
|
51
|
+
*
|
|
52
|
+
* Pattern match is LITERAL prefix `mindrian-os.stale-` (regex
|
|
53
|
+
* /^mindrian-os\.stale-/). Does NOT match `mindrian-os/` (the live
|
|
54
|
+
* install) or unrelated siblings -- the period after `mindrian-os` is
|
|
55
|
+
* what gates the match.
|
|
56
|
+
*
|
|
57
|
+
* Window override: set process.env.MOS_CACHE_PRUNE_AGE_DAYS to any
|
|
58
|
+
* positive integer. Invalid values (non-numeric, negative) fall back
|
|
59
|
+
* to the 30-day default. v2 may move this to .mos/config.json.
|
|
60
|
+
*
|
|
38
61
|
* Signature:
|
|
39
62
|
* pruneMarketplaceCache({
|
|
40
63
|
* home = os.homedir(),
|
|
@@ -42,14 +65,22 @@
|
|
|
42
65
|
* retainCount = 2,
|
|
43
66
|
* dryRun = false,
|
|
44
67
|
* } = {}) => {
|
|
45
|
-
* kept: string[],
|
|
46
|
-
* removed: string[],
|
|
47
|
-
*
|
|
48
|
-
*
|
|
68
|
+
* kept: string[], // version basenames kept on disk (incl. active)
|
|
69
|
+
* removed: string[], // version basenames removed (or would be, in dryRun)
|
|
70
|
+
* removedBackups: string[], // absolute paths of stale backup dirs removed (Phase 126)
|
|
71
|
+
* skipped: boolean, // true if we declined the marketplace-cache prune
|
|
72
|
+
* reason: string|null, // human-readable explanation, or null on success
|
|
73
|
+
* ageDays: number, // the active stale-backup age window in days (Phase 126)
|
|
49
74
|
* }
|
|
50
75
|
*
|
|
76
|
+
* Backward compat: the original Phase 123 consumer at scripts/doctor.cjs
|
|
77
|
+
* line ~2100 reads r.removed.length + r.kept only; both unchanged here.
|
|
78
|
+
* `removedBackups` and `ageDays` are ADDITIVE fields.
|
|
79
|
+
*
|
|
51
80
|
* Canon Part 8 sanity check (cp.6): the forbidden-network-token grep
|
|
52
|
-
* exits 1 (no match) on this source file. Test cp.6 enforces it.
|
|
81
|
+
* exits 1 (no match) on this source file. Test cp.6 enforces it. The
|
|
82
|
+
* Phase 126 extension reads only local fs (stat + readdir + rmSync on
|
|
83
|
+
* scratch / on-disk backup dirs); zero network surface.
|
|
53
84
|
*/
|
|
54
85
|
|
|
55
86
|
const fs = require('node:fs');
|
|
@@ -137,10 +168,20 @@ function pruneMarketplaceCache(opts) {
|
|
|
137
168
|
const retainCount = (typeof o.retainCount === 'number' && o.retainCount >= 0) ? o.retainCount : 2;
|
|
138
169
|
const dryRun = !!o.dryRun;
|
|
139
170
|
|
|
171
|
+
// Phase 126 Plan-06: stale-backup age window. Read once at the top so the
|
|
172
|
+
// returned `ageDays` field reflects the value that gated the prune.
|
|
173
|
+
// Env override is integer-only; invalid values fall back to the default.
|
|
174
|
+
const ageDaysRaw = process.env.MOS_CACHE_PRUNE_AGE_DAYS;
|
|
175
|
+
const ageDays = (typeof ageDaysRaw === 'string' && /^\d+$/.test(ageDaysRaw))
|
|
176
|
+
? parseInt(ageDaysRaw, 10)
|
|
177
|
+
: 30;
|
|
178
|
+
|
|
140
179
|
// Step 1: read active version. Skip entirely if unavailable -- never guess.
|
|
180
|
+
// (When skipped, we also decline the Phase-126 stale-backup pass, mirroring
|
|
181
|
+
// "no mutation in the absence of authoritative state".)
|
|
141
182
|
const active = readActiveVersion(home);
|
|
142
183
|
if (active.skipped) {
|
|
143
|
-
return { kept: [], removed: [], skipped: true, reason: active.reason };
|
|
184
|
+
return { kept: [], removed: [], removedBackups: [], skipped: true, reason: active.reason, ageDays };
|
|
144
185
|
}
|
|
145
186
|
const activeVersion = active.activeVersion;
|
|
146
187
|
|
|
@@ -149,7 +190,9 @@ function pruneMarketplaceCache(opts) {
|
|
|
149
190
|
if (names.length === 0) {
|
|
150
191
|
// No cache dir to prune. The active version is still "kept" conceptually
|
|
151
192
|
// (it lives in the cache when it exists; absent cache = nothing to do).
|
|
152
|
-
|
|
193
|
+
// Phase 126: still run the stale-backup pass below (returns at end).
|
|
194
|
+
const removedBackups = pruneStaleBackups(home, ageDays, dryRun);
|
|
195
|
+
return { kept: [activeVersion], removed: [], removedBackups, skipped: false, reason: 'no cache dir to prune', ageDays };
|
|
153
196
|
}
|
|
154
197
|
|
|
155
198
|
// Step 3: build the keep-set.
|
|
@@ -191,18 +234,81 @@ function pruneMarketplaceCache(opts) {
|
|
|
191
234
|
}
|
|
192
235
|
}
|
|
193
236
|
|
|
237
|
+
// Phase 126 Plan-06: stale-backup pass. ADDITIVE to the cache prune above;
|
|
238
|
+
// walks PLUGIN_HOME (sibling of cache/) for mindrian-os.stale-* dirs older
|
|
239
|
+
// than `ageDays`. Failures are swallowed per-entry (best-effort, mirrors the
|
|
240
|
+
// existing per-version rmSync error path).
|
|
241
|
+
const removedBackups = pruneStaleBackups(home, ageDays, dryRun);
|
|
242
|
+
|
|
194
243
|
return {
|
|
195
244
|
kept: Array.from(keep),
|
|
196
245
|
removed,
|
|
246
|
+
removedBackups,
|
|
197
247
|
skipped: false,
|
|
198
248
|
reason: null,
|
|
249
|
+
ageDays,
|
|
199
250
|
};
|
|
200
251
|
}
|
|
201
252
|
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Phase 126 Plan-06 helper -- prune stale backup directories.
|
|
255
|
+
//
|
|
256
|
+
// Walks <home>/.claude/plugins/ for entries whose basename matches the
|
|
257
|
+
// LITERAL prefix /^mindrian-os\.stale-/ (the period after `mindrian-os`
|
|
258
|
+
// is what gates the match -- does NOT match `mindrian-os/` live install
|
|
259
|
+
// or unrelated siblings such as `mindrian-marketplace/`).
|
|
260
|
+
//
|
|
261
|
+
// Returns an array of absolute paths that were removed (or that would
|
|
262
|
+
// have been removed in dryRun mode). On error (unreadable dir, stat
|
|
263
|
+
// failure, rm failure), the entry is silently skipped -- best-effort
|
|
264
|
+
// semantics mirror the in-loop pattern of the cache-version prune above.
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
function pruneStaleBackups(home, ageDays, dryRun) {
|
|
267
|
+
const removedBackups = [];
|
|
268
|
+
const pluginsDir = path.join(home, '.claude', 'plugins');
|
|
269
|
+
const cutoffMs = Date.now() - (ageDays * 86400000);
|
|
270
|
+
|
|
271
|
+
let entries = [];
|
|
272
|
+
try {
|
|
273
|
+
entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
274
|
+
} catch (_) {
|
|
275
|
+
// pluginsDir absent: nothing to prune. Return empty list.
|
|
276
|
+
return removedBackups;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const e of entries) {
|
|
280
|
+
if (!e.isDirectory()) continue;
|
|
281
|
+
if (!/^mindrian-os\.stale-/.test(e.name)) continue;
|
|
282
|
+
const fullPath = path.join(pluginsDir, e.name);
|
|
283
|
+
let mtimeMs;
|
|
284
|
+
try {
|
|
285
|
+
mtimeMs = fs.statSync(fullPath).mtimeMs;
|
|
286
|
+
} catch (_) {
|
|
287
|
+
continue; // stat failure: leave for inspection
|
|
288
|
+
}
|
|
289
|
+
if (mtimeMs >= cutoffMs) continue; // recent enough; retain
|
|
290
|
+
|
|
291
|
+
if (dryRun) {
|
|
292
|
+
removedBackups.push(fullPath);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
297
|
+
removedBackups.push(fullPath);
|
|
298
|
+
} catch (_) {
|
|
299
|
+
// Best-effort: a failed removal does not abort the whole prune.
|
|
300
|
+
// (Mirrors the existing pattern in the cache-version prune above.)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return removedBackups;
|
|
305
|
+
}
|
|
306
|
+
|
|
202
307
|
module.exports = {
|
|
203
308
|
pruneMarketplaceCache,
|
|
204
309
|
// Internal helpers exposed for testability + future reuse.
|
|
205
310
|
readActiveVersion,
|
|
206
311
|
listCacheVersions,
|
|
207
312
|
sortByMtimeDesc,
|
|
313
|
+
pruneStaleBackups,
|
|
208
314
|
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* lib/core/install-state.cjs -- install-state.json read / write / migrate
|
|
5
|
+
* module.
|
|
6
|
+
*
|
|
7
|
+
* Extracts the inline session-start read+write+derive logic into a focused
|
|
8
|
+
* single-purpose module; adds the v1 -> v2 schema migration with additive-only
|
|
9
|
+
* semantics + future-version detection + atomic-write crash safety.
|
|
10
|
+
*
|
|
11
|
+
* Phase 126 Plan 07. Canon Part 6 (dog-fooding: schema evolution surfaces only
|
|
12
|
+
* via shipped harness -- v1 was shipped in v1.13.0-beta.13). Canon Part 7
|
|
13
|
+
* (reuse: extracts inline session-start write into a module; does NOT
|
|
14
|
+
* re-architect the write path).
|
|
15
|
+
*
|
|
16
|
+
* The wire shape evolves through a single integer sentinel (`schema_version`).
|
|
17
|
+
* Per CONTEXT.md D3 (LOCKED): integer not semver string (simpler comparison
|
|
18
|
+
* for additive-only migrations). Per Open Question 5 settlement: `===` integer
|
|
19
|
+
* equality is the comparison.
|
|
20
|
+
*
|
|
21
|
+
* The v1 -> v2 additive-only mapping (CONTEXT.md D3 + Plan 07 must_haves):
|
|
22
|
+
*
|
|
23
|
+
* v1 file (Phase 123, NO schema_version):
|
|
24
|
+
* {
|
|
25
|
+
* active_root, active_version, topology, installed_at,
|
|
26
|
+
* snapshot, surfaces?, resolved_at?, ...
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* v2 file (Phase 126):
|
|
30
|
+
* v1-fields-preserved-byte-identical +
|
|
31
|
+
* {
|
|
32
|
+
* schema_version: 2,
|
|
33
|
+
* topology_class: derived from topology (see deriveTopologyClass below),
|
|
34
|
+
* last_acceptance_run: null, // Plan 03 + Plan 05 fill this
|
|
35
|
+
* renderer_contract_version: 'unknown' // Plan 01 sets this
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* Future-version detection: schema_version > 2 -> warn on stderr + DEFER to
|
|
39
|
+
* /mos:doctor --fix. Do NOT downgrade. Do NOT touch the file. Return a typed
|
|
40
|
+
* sentinel so callers can decide.
|
|
41
|
+
*
|
|
42
|
+
* Atomic write: write to `<path>.tmp`, fsync, rename. A crash between write
|
|
43
|
+
* and rename leaves the original file untouched. The MOS_TEST_FORCE_FAIL=rename
|
|
44
|
+
* env-var hook mirrors scripts/doctor.cjs lines ~304-307 -- it injects a throw
|
|
45
|
+
* at the rename moment so the crash-recovery contract is testable.
|
|
46
|
+
*
|
|
47
|
+
* Canon Part 8: this module reads + writes LOCAL files only ($HOME/.mindrian/).
|
|
48
|
+
* Zero network calls. Zero Brain queries. Zero side-channel to remote services.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const fs = require('node:fs');
|
|
52
|
+
const path = require('node:path');
|
|
53
|
+
|
|
54
|
+
const SCHEMA_VERSION = 2;
|
|
55
|
+
const STATE_FILE_PATH_REL = path.join('.mindrian', 'install-state.json');
|
|
56
|
+
|
|
57
|
+
function statePath(home) {
|
|
58
|
+
return path.join(home, STATE_FILE_PATH_REL);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* readInstallState({ home }) -> object | null
|
|
63
|
+
*
|
|
64
|
+
* Returns the parsed JSON object, or null when the file is absent OR
|
|
65
|
+
* unparseable. Never throws -- the goal is a robust read for downstream
|
|
66
|
+
* consumers (doctor, session-start, the migrator itself). A corrupt file is
|
|
67
|
+
* surfaced as null so the caller can decide whether to derive-from-scratch
|
|
68
|
+
* (session-start's existing behavior) or surface a finding (doctor class I).
|
|
69
|
+
*/
|
|
70
|
+
function readInstallState(opts) {
|
|
71
|
+
const home = (opts && opts.home) || '';
|
|
72
|
+
if (!home) return null;
|
|
73
|
+
const p = statePath(home);
|
|
74
|
+
if (!fs.existsSync(p)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// Corrupt file -> null (mirrors doctor.cjs's class-I "absent" finding).
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* writeInstallState({ home, state }) -> void
|
|
85
|
+
*
|
|
86
|
+
* Atomic write: $HOME/.mindrian/install-state.json.tmp -> fsync -> rename.
|
|
87
|
+
*
|
|
88
|
+
* The MOS_TEST_FORCE_FAIL=rename env injection mirrors the doctor.cjs
|
|
89
|
+
* pattern -- if set, throws AFTER the .tmp write but BEFORE the rename so
|
|
90
|
+
* tests can verify the original file is left untouched.
|
|
91
|
+
*
|
|
92
|
+
* Test-only side note: when MOS_TEST_FORCE_FAIL=rename, the .tmp file is left
|
|
93
|
+
* on disk; the rename never happens; the original target is untouched. A
|
|
94
|
+
* subsequent successful call will overwrite the .tmp via openSync('w') so
|
|
95
|
+
* stale .tmp files are not a long-term hazard.
|
|
96
|
+
*/
|
|
97
|
+
function writeInstallState(opts) {
|
|
98
|
+
const home = opts.home;
|
|
99
|
+
const state = opts.state;
|
|
100
|
+
const p = statePath(home);
|
|
101
|
+
const tmp = p + '.tmp';
|
|
102
|
+
// Ensure parent dir exists. Idempotent.
|
|
103
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
104
|
+
// Atomic write: write to tmp, fsync (best-effort), rename.
|
|
105
|
+
const fd = fs.openSync(tmp, 'w');
|
|
106
|
+
try {
|
|
107
|
+
fs.writeSync(fd, JSON.stringify(state, null, 2) + '\n');
|
|
108
|
+
try { fs.fsyncSync(fd); } catch (_) { /* fsync best-effort -- some FS lack support */ }
|
|
109
|
+
} finally {
|
|
110
|
+
fs.closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
// Test injection point (mirrors scripts/doctor.cjs MOS_TEST_FORCE_FAIL).
|
|
113
|
+
// Throw AFTER the .tmp write so the original target is byte-identical.
|
|
114
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'rename') {
|
|
115
|
+
throw new Error('MOS_TEST_FORCE_FAIL=rename injection (test-only)');
|
|
116
|
+
}
|
|
117
|
+
fs.renameSync(tmp, p);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* deriveTopologyClass(topology) -> 'healthy' | 'missing' | 'drifted'
|
|
122
|
+
*
|
|
123
|
+
* Canonical 4-state taxonomy from CONTEXT.md D3 + Plan 03 needs.
|
|
124
|
+
*
|
|
125
|
+
* Mapping (see PLAN.md Task 2 behavior block):
|
|
126
|
+
* marketplace-cache -> healthy (Claude Code's standard install path)
|
|
127
|
+
* direct -> healthy (npx round-trip install -- @mindrian_os/install)
|
|
128
|
+
* dev-clone -> healthy (developer machine -- a clone with a remote)
|
|
129
|
+
* not-found -> missing (resolver returned null root)
|
|
130
|
+
* legacy -> drifted (the legacy hand-clone path -- pre-Phase 123)
|
|
131
|
+
* <unknown> -> drifted (conservative default for any new topology
|
|
132
|
+
* introduced after this module's release)
|
|
133
|
+
*/
|
|
134
|
+
function deriveTopologyClass(topology) {
|
|
135
|
+
switch (topology) {
|
|
136
|
+
case 'marketplace-cache': return 'healthy';
|
|
137
|
+
case 'direct': return 'healthy';
|
|
138
|
+
case 'dev-clone': return 'healthy';
|
|
139
|
+
case 'not-found': return 'missing';
|
|
140
|
+
case 'legacy': return 'drifted';
|
|
141
|
+
default: return 'drifted';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* migrateIfNeeded({ home }) -> {
|
|
147
|
+
* migrated: boolean,
|
|
148
|
+
* // present in v1 -> v2 path:
|
|
149
|
+
* fromVersion?: 1, toVersion?: 2,
|
|
150
|
+
* // present in v2 path:
|
|
151
|
+
* currentVersion?: 2,
|
|
152
|
+
* // present in future-version path:
|
|
153
|
+
* futureVersion?: true, currentVersion?: number, advice?: string,
|
|
154
|
+
* // present in no-file path:
|
|
155
|
+
* fileAbsent?: true,
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* Behavior matrix:
|
|
159
|
+
*
|
|
160
|
+
* File absent
|
|
161
|
+
* -> { migrated:false, fileAbsent:true }
|
|
162
|
+
* -> Do NOT create the file (creation is session-start's job).
|
|
163
|
+
*
|
|
164
|
+
* File present + no schema_version (or schema_version absent/null)
|
|
165
|
+
* -> Treat as v1. Run additive migration. Write back. Return
|
|
166
|
+
* { migrated:true, fromVersion:1, toVersion:2 }.
|
|
167
|
+
*
|
|
168
|
+
* File present + schema_version === 2
|
|
169
|
+
* -> No-op. Return { migrated:false, currentVersion:2 }.
|
|
170
|
+
*
|
|
171
|
+
* File present + schema_version > 2 (a future-version file)
|
|
172
|
+
* -> Emit stderr warn. Do NOT touch the file. Return
|
|
173
|
+
* { migrated:false, futureVersion:true, currentVersion:<n>,
|
|
174
|
+
* advice:'run /mos:doctor --fix' }.
|
|
175
|
+
*
|
|
176
|
+
* File present + schema_version < 2 (unexpected lower-than-v1, e.g. 0)
|
|
177
|
+
* -> Treat as v1 (coerce). Same outcome as the v1 path.
|
|
178
|
+
*
|
|
179
|
+
* File present + corrupt JSON
|
|
180
|
+
* -> readInstallState returned null -> indistinguishable from absent at
|
|
181
|
+
* this layer -> { migrated:false, fileAbsent:true }. doctor class I
|
|
182
|
+
* is the surface that catches corrupt-but-present and offers --fix.
|
|
183
|
+
*/
|
|
184
|
+
function migrateIfNeeded(opts) {
|
|
185
|
+
const home = opts.home;
|
|
186
|
+
const state = readInstallState({ home });
|
|
187
|
+
if (!state) return { migrated: false, fileAbsent: true };
|
|
188
|
+
|
|
189
|
+
const sv = state.schema_version;
|
|
190
|
+
if (typeof sv === 'undefined' || sv === null) {
|
|
191
|
+
// v1 (or unmarked) -> v2 additive migration.
|
|
192
|
+
return _runV1ToV2({ home, state });
|
|
193
|
+
}
|
|
194
|
+
if (sv === SCHEMA_VERSION) {
|
|
195
|
+
return { migrated: false, currentVersion: SCHEMA_VERSION };
|
|
196
|
+
}
|
|
197
|
+
if (typeof sv === 'number' && sv > SCHEMA_VERSION) {
|
|
198
|
+
// Future-version: warn + defer. Do NOT downgrade. Do NOT touch the file.
|
|
199
|
+
process.stderr.write(
|
|
200
|
+
'[install-state] schema_version ' + sv + ' is newer than the plugin understands (' +
|
|
201
|
+
SCHEMA_VERSION + '). Skipping migration; run /mos:doctor --fix.\n'
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
migrated: false,
|
|
205
|
+
futureVersion: true,
|
|
206
|
+
currentVersion: sv,
|
|
207
|
+
advice: 'run /mos:doctor --fix',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Unexpected lower-than-v1 (e.g. schema_version: 0 or a non-number) -- coerce
|
|
211
|
+
// to v1 and run the additive migration. This is a safety net; not expected
|
|
212
|
+
// to fire in practice.
|
|
213
|
+
return _runV1ToV2({ home, state });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Internal: perform the v1 -> v2 additive migration + write.
|
|
217
|
+
function _runV1ToV2(opts) {
|
|
218
|
+
const home = opts.home;
|
|
219
|
+
const state = opts.state;
|
|
220
|
+
// Object.assign preserves v1 fields byte-identically; new fields are
|
|
221
|
+
// appended last (since v2's schema_version is the sentinel, the order
|
|
222
|
+
// could be reshuffled but additive-on-top is the simplest invariant).
|
|
223
|
+
const v2 = Object.assign({}, state, {
|
|
224
|
+
schema_version: SCHEMA_VERSION,
|
|
225
|
+
topology_class: deriveTopologyClass(state.topology),
|
|
226
|
+
last_acceptance_run: null,
|
|
227
|
+
renderer_contract_version: 'unknown',
|
|
228
|
+
});
|
|
229
|
+
writeInstallState({ home, state: v2 });
|
|
230
|
+
return { migrated: true, fromVersion: 1, toVersion: SCHEMA_VERSION };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
SCHEMA_VERSION,
|
|
235
|
+
readInstallState,
|
|
236
|
+
writeInstallState,
|
|
237
|
+
migrateIfNeeded,
|
|
238
|
+
deriveTopologyClass,
|
|
239
|
+
// Test-only helpers (intentionally exported with leading underscore so
|
|
240
|
+
// consumers do not depend on them and grep can spot test surface usage).
|
|
241
|
+
_statePath: statePath,
|
|
242
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/install",
|
|
3
|
-
"version": "1.13.0-beta.
|
|
3
|
+
"version": "1.13.0-beta.16",
|
|
4
4
|
"description": "Install MindrianOS into Claude Code with one command -- `npx @mindrian_os/install`. Ships the MindrianOS plugin (Larry + PWS methodology + Data Room) plus a setup/diagnostics CLI (install/doctor/update).",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"mcp": "node bin/mindrian-mcp-server.cjs",
|