sshlg-skills 1.63.2 → 1.63.3
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/CHANGELOG.md +17 -0
- package/bin/sshlg-skills.js +26 -14
- package/lib/claude-pin-transaction.js +6 -0
- package/lib/claude-pinned.js +20 -9
- package/lib/json-spans.js +1 -1
- package/lib/runtime.js +45 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## v1.63.3 — pinned sources for every Claude member; blocked members no longer stop the run
|
|
2
|
+
|
|
3
|
+
- A Claude member whose installed bytes already equal the pin is now rebound to the hub's
|
|
4
|
+
pinned catalog: its marketplace source moves to the pin, with no install or uninstall, and
|
|
5
|
+
the run verifies that its install records did not change and its bytes still equal the pin.
|
|
6
|
+
A later native update therefore follows the pin, not upstream (B-191).
|
|
7
|
+
- A member the native contract cannot change — installed only at project or local scope,
|
|
8
|
+
in user plus other scopes with different bytes, or a configured plugin — is reported as
|
|
9
|
+
blocked and keeps its installed version, while the other members and every later channel
|
|
10
|
+
(plain skills, runtime, hooks, Codex) proceed; the result is partial and exits nonzero.
|
|
11
|
+
Malformed install records still stop the whole run (B-192).
|
|
12
|
+
- The hook runtime records which bytes the hub installed (`.managed.json`). A library module a
|
|
13
|
+
later package drops is moved to `.retired/<stamp>/` only while it still has those bytes, and
|
|
14
|
+
`update`/`hooks install` print what moved; hook scripts, edited, foreign or older unrecorded
|
|
15
|
+
files stay where they are (B-193).
|
|
16
|
+
- Inserting into a non-empty object of a tab-indented JSON file keeps tabs (B-194).
|
|
17
|
+
|
|
1
18
|
## v1.63.2 — the Claude transaction keeps settings in Claude Code's format
|
|
2
19
|
|
|
3
20
|
- Replacing a marketplace source in `~/.claude/settings.json` wrote the new object on one
|
package/bin/sshlg-skills.js
CHANGED
|
@@ -407,8 +407,9 @@ function withClaudePlan(steps, f) {
|
|
|
407
407
|
steps.unshift(opres.step('claude-pins', 'home',
|
|
408
408
|
'native immutable source transaction; protected catalogs/settings, scoped repair, rollback on failure, payload readback',
|
|
409
409
|
{ fingerprint: native.fingerprint, profile: native.profile,
|
|
410
|
-
members: native.jobs.map(j => `${j.pluginInstall}: ${j.action} @ ${j.row.commit}; ${j.row.files.length} files`)
|
|
411
|
-
.concat(native.equivalent.map(e => `${e.pluginInstall}: payload-equivalent
|
|
410
|
+
members: native.jobs.filter(j => j.action !== 'rebind').map(j => `${j.pluginInstall}: ${j.action} @ ${j.row.commit}; ${j.row.files.length} files`)
|
|
411
|
+
.concat(native.equivalent.map(e => `${e.pluginInstall}: payload-equivalent (installed ${String(e.installed).slice(0, 7)}, pin ${e.pin.slice(0, 7)}; ${e.files} files equal) — source rebind, no reinstall`))
|
|
412
|
+
.concat(native.blocked.map(b => `${b.pluginInstall}: blocked, left as installed — ${b.reason}`)),
|
|
412
413
|
journal: path.join(native.transactionRoot, 'latest.json') }));
|
|
413
414
|
}
|
|
414
415
|
const result = attachWaitPlan(steps, f);
|
|
@@ -419,26 +420,31 @@ function runClaudePlan(planned, f) {
|
|
|
419
420
|
const saved = claudePlans.get(planned);
|
|
420
421
|
if (!saved) {
|
|
421
422
|
log(`\n== Updating Claude Code plugins ==\n skipped: ${claudeScope(f).skippedBy} (the Claude host is outside this run's scope)`);
|
|
422
|
-
return true;
|
|
423
|
+
return { ok: true, partial: false };
|
|
423
424
|
}
|
|
424
|
-
for (const e of saved.native.equivalent) log(` ${e.member || e.name}: payload equals pin ${e.pin.slice(0, 7)} (${e.files} files);
|
|
425
|
+
for (const e of saved.native.equivalent) log(` ${e.member || e.name}: payload equals pin ${e.pin.slice(0, 7)} (${e.files} files); no reinstall — only its marketplace source moves to the pin catalog`);
|
|
426
|
+
for (const b of saved.native.blocked) log(` ${b.name}: blocked, left as installed — ${b.reason}`);
|
|
427
|
+
const clean = !saved.native.blocked.length;
|
|
425
428
|
if (!saved.native.jobs.length) {
|
|
426
429
|
// Nothing is written; still describe the state at apply time, not at preview.
|
|
427
430
|
let again;
|
|
428
431
|
try { again = require('../lib/claude-pinned').prepare(SKILLS, saved.inventory, { home: os.homedir(), profileRoot: claudeRoot() }); }
|
|
429
|
-
catch (e) { log(` Claude native transaction refused before execution: ${e.message}`); return false; }
|
|
430
|
-
if (again.fingerprint !== saved.native.fingerprint) { log(' Claude native state changed since the preview; run update again'); return false; }
|
|
431
|
-
log(' Claude native transaction: not needed — every member already carries its pinned payload'
|
|
432
|
-
|
|
432
|
+
catch (e) { log(` Claude native transaction refused before execution: ${e.message}`); return { ok: false, partial: false }; }
|
|
433
|
+
if (again.fingerprint !== saved.native.fingerprint) { log(' Claude native state changed since the preview; run update again'); return { ok: false, partial: false }; }
|
|
434
|
+
log(clean ? ' Claude native transaction: not needed — every member already carries its pinned payload'
|
|
435
|
+
: ' Claude native transaction: nothing else to do; blocked members above keep their installed version (partial)');
|
|
436
|
+
return { ok: true, partial: !clean };
|
|
433
437
|
}
|
|
434
438
|
// Admission, version check and all backups happen before any other channel.
|
|
435
439
|
let result;
|
|
436
440
|
try { result = require('../lib/claude-pin-transaction').execute(saved.native, SKILLS, saved.inventory); }
|
|
437
|
-
catch(e) { log(` Claude native transaction refused before execution: ${e.message}`); return false; }
|
|
441
|
+
catch(e) { log(` Claude native transaction refused before execution: ${e.message}`); return { ok: false, partial: false }; }
|
|
438
442
|
log(` Claude native transaction: ${result.ok ? 'verified immutable payloads' : 'failed; inspect recovery receipt'} — ${result.receipt}`);
|
|
439
443
|
for (const r of result.results) log(` ${r.member}: ${r.status}${r.rollback ? '; rollback: ' + r.rollback : ''}`);
|
|
440
444
|
if (result.error) log(` ${result.error}`);
|
|
441
|
-
|
|
445
|
+
if (!clean) log(' Claude: partial — blocked members above keep their installed version');
|
|
446
|
+
// A blocked member makes the result partial; the other channels still run.
|
|
447
|
+
return { ok: result.ok, partial: !clean };
|
|
442
448
|
}
|
|
443
449
|
|
|
444
450
|
/** The same contract for `update` — every subprocess, prune, router and runtime action. */
|
|
@@ -516,8 +522,9 @@ function renderDryRun(planned, mode) {
|
|
|
516
522
|
function cmdInstall(f) {
|
|
517
523
|
const planned = frozenOperations.get(f) || planInstall(f);
|
|
518
524
|
if (f.dryRun) return renderDryRun(planned, 'install');
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
const claude = runClaudePlan(planned, f);
|
|
526
|
+
if (!claude.ok) return false;
|
|
527
|
+
let ok = !claude.partial;
|
|
521
528
|
if (!f.claudeOnly) {
|
|
522
529
|
const agents = skillsCliAgents(f);
|
|
523
530
|
log(`\n== Installing to agents via skills CLI: ${agents.join(', ')} ==`);
|
|
@@ -590,8 +597,9 @@ function scopedRoutingTargets(f) {
|
|
|
590
597
|
function cmdUpdate(f) {
|
|
591
598
|
const planned = frozenOperations.get(f) || planUpdate(f);
|
|
592
599
|
if (f.dryRun) return renderDryRun(planned, 'update');
|
|
593
|
-
|
|
594
|
-
|
|
600
|
+
const claude = runClaudePlan(planned, f);
|
|
601
|
+
if (!claude.ok) return false;
|
|
602
|
+
let ok = !claude.partial;
|
|
595
603
|
// Count and execute the subprocess entries from the same frozen plan.
|
|
596
604
|
progress.total =
|
|
597
605
|
(!f.claudeOnly && fs.existsSync(path.join(ROOT, '.gitmodules')) ? 1 : 0)
|
|
@@ -659,6 +667,8 @@ function cmdUpdate(f) {
|
|
|
659
667
|
+ `${before.differing.length} changed`
|
|
660
668
|
: ' already level with this package');
|
|
661
669
|
for (const m of before.missing) log(` + ${m}`);
|
|
670
|
+
if (r.retired && r.retired.length) log(` retired ${r.retired.length} file(s) this package dropped (kept under ${root}/.retired/): ${r.retired.join(', ')}`);
|
|
671
|
+
if (r.ownedError) log(` warning: ${r.ownedError}`);
|
|
662
672
|
}
|
|
663
673
|
} catch (e) {
|
|
664
674
|
// Other update steps may finish, but failed runtime refresh makes the aggregate red.
|
|
@@ -3260,6 +3270,8 @@ function cmdHooks(argv) {
|
|
|
3260
3270
|
function syncRuntime() {
|
|
3261
3271
|
const result = require('../lib/runtime.js').sync(pkgRoot, root, { create: true });
|
|
3262
3272
|
if (result.ok === false) throw new Error(result.reason);
|
|
3273
|
+
if (result.retired && result.retired.length) log(` runtime: retired ${result.retired.length} dropped file(s) to ${root}/.retired/: ${result.retired.join(', ')}`);
|
|
3274
|
+
if (result.ownedError) log(` runtime warning: ${result.ownedError}`);
|
|
3263
3275
|
return root;
|
|
3264
3276
|
}
|
|
3265
3277
|
|
|
@@ -58,6 +58,12 @@ function verifyForeign(plan) {
|
|
|
58
58
|
function verify(plan,job,row=job.row,files=row.files) {
|
|
59
59
|
const registry=P.parseMetadata(fs.readFileSync(plan.registryFile,'utf8')).data;
|
|
60
60
|
const records=registry.plugins?.[job.pluginInstall];
|
|
61
|
+
if(job.action==='rebind') {
|
|
62
|
+
// Only the marketplace source moved: install records stay exactly as planned and
|
|
63
|
+
// the installed bytes still equal the pin.
|
|
64
|
+
if(JSON.stringify(records)!==job.records) throw Error(job.name+': native install records changed during source rebind');
|
|
65
|
+
return S.verifyPayload(job.old.installPath,{files});
|
|
66
|
+
}
|
|
61
67
|
if(!Array.isArray(records) || records.length!==1 || records[0].scope!=='user' || records[0].version!==row.version || records[0].gitCommitSha!==row.commit) throw Error(job.name+': native installed source readback differs from pin');
|
|
62
68
|
const install=records[0].installPath;
|
|
63
69
|
if(typeof install!=='string' || !require('./visibility-state').profilePath(install).startsWith(path.join(plan.profile,'plugins/cache')+path.sep)) throw Error('native installed root is outside expected cache');
|
package/lib/claude-pinned.js
CHANGED
|
@@ -62,7 +62,7 @@ function prepare(members,inventory,opts) {
|
|
|
62
62
|
const transactionRoot=path.join(home,'.sshlg-skills/claude-pinned',Source.sha256(profile).slice(0,16),'transactions');
|
|
63
63
|
const active=read(path.join(transactionRoot,'latest.json'),'null').data;
|
|
64
64
|
if(fs.existsSync(path.join(transactionRoot,'lock')) || (active && (active.status==='running' || active.status==='failed' && (!active.settingsRestored || active.uncertain)))) throw Error('incomplete Claude transaction; inspect '+path.join(transactionRoot,'latest.json'));
|
|
65
|
-
const jobs=[],equivalent=[];
|
|
65
|
+
const jobs=[],equivalent=[],blocked=[];
|
|
66
66
|
for(const row of rows) {
|
|
67
67
|
const marketplace=row.pluginInstall.split('@')[1],dir=path.join(home,'.sshlg-skills/claude-pinned',Source.sha256(profile).slice(0,16),marketplace);
|
|
68
68
|
const declaration=settings.data.extraKnownMarketplaces?.[marketplace];
|
|
@@ -73,17 +73,28 @@ function prepare(members,inventory,opts) {
|
|
|
73
73
|
// (docs-only) source commit or extra project/local scopes need no native mutation,
|
|
74
74
|
// and a reinstall would rewrite a payload directory live sessions are using.
|
|
75
75
|
const user=Array.isArray(existing)?existing.filter(r=>r&&r.scope==='user'):[];
|
|
76
|
+
// B-191: such a member still gets the hub catalog and its marketplace source, so a
|
|
77
|
+
// later native update follows the pin; `rebind` never installs or uninstalls.
|
|
78
|
+
let rebind=null;
|
|
76
79
|
if(user.length===1 && user[0].version===row.version && typeof user[0].installPath==='string'
|
|
77
80
|
&& !(existing.length===1 && user[0].gitCommitSha===row.commit)
|
|
78
81
|
&& Bindings.profilePath(user[0].installPath).startsWith(path.join(profile,'plugins/cache')+path.sep)
|
|
79
82
|
&& existing.every(r=>r && r.installPath===user[0].installPath && r.version===row.version)) {
|
|
80
83
|
let equal=false;try {equal=Source.verifyPayload(user[0].installPath,row)>=1;}catch(_) {}
|
|
81
|
-
if(equal) {
|
|
84
|
+
if(equal) {
|
|
85
|
+
equivalent.push({name:row.name,pluginInstall:row.pluginInstall,version:row.version,pin:row.commit,installed:user[0].gitCommitSha||null,scopes:existing.length,files:row.files.length});
|
|
86
|
+
rebind=user[0];
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
|
-
if(existing!==undefined && (!Array.isArray(existing) || existing.length
|
|
89
|
+
if(!rebind && existing!==undefined && (!Array.isArray(existing) || !existing.length || existing.some(r=>!plain(r)) || existing.filter(r=>r.scope==='user').length>1)) throw Error(row.name+': malformed native install record');
|
|
90
|
+
// A supported operator configuration this contract cannot change blocks only its own
|
|
91
|
+
// member; the run continues for the others and reports partial.
|
|
92
|
+
const block=reason=>{blocked.push({name:row.name,pluginInstall:row.pluginInstall,reason});};
|
|
93
|
+
if(!rebind && existing!==undefined && (existing.length!==1 || existing[0].scope!=='user')) {block('native immutable repair supports one user-scope install only (installed scopes: '+existing.map(r=>r.scope).join(', ')+')');continue;}
|
|
84
94
|
for(const id of Object.keys(registry.data.plugins)) if(id.endsWith('@'+marketplace) && id!==row.pluginInstall) throw Error(row.name+': marketplace has another installed plugin');
|
|
85
95
|
let action='install',old=null,oldFiles=null,repairEligible=false;
|
|
86
|
-
if(
|
|
96
|
+
if(rebind) {action='rebind';old=rebind;}
|
|
97
|
+
else if(existing) {
|
|
87
98
|
old=existing[0];
|
|
88
99
|
if(typeof old.installPath!=='string' || !path.isAbsolute(old.installPath) || typeof old.version!=='string') throw Error(row.name+': invalid installed plugin metadata');
|
|
89
100
|
const cacheRoot=path.join(profile,'plugins/cache')+path.sep;
|
|
@@ -97,9 +108,9 @@ function prepare(members,inventory,opts) {
|
|
|
97
108
|
if(action==='reinstall' && !/^[a-f0-9]{40}$/.test(old.gitCommitSha||'')) throw Error(row.name+': repair requires prior immutable Git identity');
|
|
98
109
|
if(action==='reinstall' && (oldFiles.length!==row.files.length || oldFiles.some(f=>!row.files.some(p=>p.path===f.path)))) throw Error(row.name+': unknown same-version payload files; preserve before repair');
|
|
99
110
|
repairEligible=!(row.configSurface || configured(manifest.data) || oldFiles.some(f=>/\.(mcpb|dxt)$/i.test(f.path)) || Object.prototype.hasOwnProperty.call(settings.data.pluginConfigs||{},row.pluginInstall));
|
|
100
|
-
if(action==='reinstall' && !repairEligible)
|
|
111
|
+
if(action==='reinstall' && !repairEligible) {block('configured plugin repair is unsupported; options/data preserved');continue;}
|
|
101
112
|
}
|
|
102
|
-
if(row.configSurface && action==='install')
|
|
113
|
+
if(row.configSurface && action==='install') {block('configured native install is outside the verified contract');continue;}
|
|
103
114
|
const file=path.join(dir,'.claude-plugin/marketplace.json'),catalog=JSON.stringify(Source.catalog(row),null,2)+'\n';
|
|
104
115
|
// An existing hub-owned catalog must still belong to this exact member.
|
|
105
116
|
if(!Bindings.profilePath(file).startsWith(home+path.sep)) throw Error(row.name+': managed catalog escapes home');
|
|
@@ -108,9 +119,9 @@ function prepare(members,inventory,opts) {
|
|
|
108
119
|
const p=prior.data;
|
|
109
120
|
if(!plain(p) || p.name!==marketplace || !Array.isArray(p.plugins) || p.plugins.length!==1 || p.plugins[0].name!==row.pluginInstall.split('@')[0] || p.plugins[0].source?.source!=='git-subdir' || p.plugins[0].source.url!=='https://github.com/'+row.repo+'.git') throw Error(row.name+': existing catalog is not owned by this member');
|
|
110
121
|
}
|
|
111
|
-
jobs.push({name:row.name,pluginInstall:row.pluginInstall,marketplace,dir,file,catalog,action,row,old,oldFiles,repairEligible});
|
|
122
|
+
jobs.push({name:row.name,pluginInstall:row.pluginInstall,marketplace,dir,file,catalog,action,row,old,oldFiles,repairEligible,records:rebind?JSON.stringify(existing):null});
|
|
112
123
|
}
|
|
113
|
-
const fingerprint=Source.sha256(JSON.stringify({equivalent,settings:settings.text,registry:registry.text,known:known.text,catalogs:jobs.map(j=>({text:read(j.file,'null').text,action:j.action,oldFiles:j.oldFiles})),inventory:rows,profile,home,bindings:[settingsFile,registryFile,knownFile,...jobs.map(j=>j.file)].map(Bindings.profilePath)}));
|
|
114
|
-
return {fingerprint,requested,profile,home,settingsFile,registryFile,knownFile,settings,registry,known,jobs,equivalent,transactionRoot};
|
|
124
|
+
const fingerprint=Source.sha256(JSON.stringify({equivalent,blocked,settings:settings.text,registry:registry.text,known:known.text,catalogs:jobs.map(j=>({text:read(j.file,'null').text,action:j.action,oldFiles:j.oldFiles})),inventory:rows,profile,home,bindings:[settingsFile,registryFile,knownFile,...jobs.map(j=>j.file)].map(Bindings.profilePath)}));
|
|
125
|
+
return {fingerprint,requested,profile,home,settingsFile,registryFile,knownFile,settings,registry,known,jobs,equivalent,blocked,transactionRoot};
|
|
115
126
|
}
|
|
116
127
|
module.exports={prepare,editAt,ownedSource,configured,parseMetadata};
|
package/lib/json-spans.js
CHANGED
|
@@ -80,7 +80,7 @@ function edit(text, object, key, value) {
|
|
|
80
80
|
const multiline = /[\r\n]/.test(prefix);
|
|
81
81
|
const indent = multiline ? prefix.slice(prefix.lastIndexOf('\n') + 1) : '';
|
|
82
82
|
const colon = first ? text.slice(first.keyEnd, first.value.start) : ': ';
|
|
83
|
-
const encoded = multiline ? JSON.stringify(value, null, 2).replace(/\n/g, newline + indent) : JSON.stringify(value);
|
|
83
|
+
const encoded = multiline ? JSON.stringify(value, null, indent.includes('\t') ? '\t' : 2).replace(/\n/g, newline + indent) : JSON.stringify(value);
|
|
84
84
|
const member = JSON.stringify(key) + colon + encoded;
|
|
85
85
|
if (ps.length) return splice(ps[ps.length - 1].end, ps[ps.length - 1].end, ',' + (multiline ? newline + indent : '') + member);
|
|
86
86
|
// Empty object has no foreign property bytes to alter. In a multi-line document an
|
package/lib/runtime.js
CHANGED
|
@@ -119,6 +119,49 @@ function managedFiles(pkgRoot) {
|
|
|
119
119
|
const crypto = require('crypto');
|
|
120
120
|
const JOURNAL = '.switch-journal.json';
|
|
121
121
|
const PREVIOUS = '.prev-generation';
|
|
122
|
+
const OWNED = '.managed.json';
|
|
123
|
+
const RETIRED = '.retired';
|
|
124
|
+
|
|
125
|
+
// B-193: the runtime records which bytes the hub itself installed. A later package
|
|
126
|
+
// that drops a module retires it to `.retired/<stamp>/` only when the file still has
|
|
127
|
+
// exactly those bytes; edited, foreign or unrecorded files stay. Never a deletion.
|
|
128
|
+
function readOwned(root) {
|
|
129
|
+
try {
|
|
130
|
+
const d = JSON.parse(fs.readFileSync(path.join(root, OWNED), 'utf8'));
|
|
131
|
+
if (!d || d.version !== 1 || !d.files || typeof d.files !== 'object' || Array.isArray(d.files)) return null;
|
|
132
|
+
for (const p of Object.values(d.files)) if (!validPrint(p)) return null;
|
|
133
|
+
return d;
|
|
134
|
+
} catch (_) { return null; }
|
|
135
|
+
}
|
|
136
|
+
function settle(runtimeRoot, rels, result) {
|
|
137
|
+
try {
|
|
138
|
+
const retired = [], old = readOwned(runtimeRoot), keep = new Set(rels);
|
|
139
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
140
|
+
for (const [rel, print] of Object.entries(old ? old.files : {})) {
|
|
141
|
+
// A hook script may still be wired in a host's settings: only library modules retire.
|
|
142
|
+
if (keep.has(rel) || !rel.startsWith('lib/')) continue;
|
|
143
|
+
let file, now;
|
|
144
|
+
try { file = managedPath(runtimeRoot, rel); now = fingerprint(file); } catch (_) { continue; }
|
|
145
|
+
if (!now || !same(now, print)) continue;
|
|
146
|
+
const dst = path.join(runtimeRoot, RETIRED, stamp, rel);
|
|
147
|
+
directory(path.join(runtimeRoot, RETIRED)); directory(path.join(runtimeRoot, RETIRED, stamp));
|
|
148
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
149
|
+
for (let d = path.dirname(dst); d !== path.join(runtimeRoot, RETIRED); d = path.dirname(d)) directory(d);
|
|
150
|
+
fs.renameSync(file, dst);
|
|
151
|
+
retired.push(rel);
|
|
152
|
+
}
|
|
153
|
+
const files = {};
|
|
154
|
+
for (const rel of rels) { const p = fingerprint(managedPath(runtimeRoot, rel)); if (p) files[rel] = p; }
|
|
155
|
+
const pending = path.join(runtimeRoot, `${OWNED}.${process.pid}.tmp`);
|
|
156
|
+
// Leftovers of a crashed writer (any pid) are ours to clear: we hold the runtime lock.
|
|
157
|
+
for (const n of fs.readdirSync(runtimeRoot)) if (n.startsWith(`${OWNED}.`) && n.endsWith('.tmp')) { try { fs.unlinkSync(path.join(runtimeRoot, n)); } catch (_) { /* gone */ } }
|
|
158
|
+
durable(pending, JSON.stringify({ version: 1, files }) + '\n');
|
|
159
|
+
fs.renameSync(pending, path.join(runtimeRoot, OWNED));
|
|
160
|
+
return { ...result, retired: retired.sort() };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { ...result, retired: [], ownedError: `runtime ownership record not updated: ${e.message}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
122
165
|
|
|
123
166
|
function stat(file) {
|
|
124
167
|
try { return fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
@@ -250,7 +293,7 @@ function syncLocked(pkgRoot, runtimeRoot, exists) {
|
|
|
250
293
|
if (!rels.length) return failure('package has no managed runtime files', 'not-needed');
|
|
251
294
|
const rows = rels.map(rel => ({ rel, before: fingerprint(managedPath(runtimeRoot, rel)) }));
|
|
252
295
|
if (exists && rows.every(row => same(row.before, fingerprint(managedPath(pkgRoot, row.rel))))) {
|
|
253
|
-
return { ok: true, status: 'unchanged', created: false, copied: [], reason: null };
|
|
296
|
+
return settle(runtimeRoot, rels, { ok: true, status: 'unchanged', created: false, copied: [], reason: null });
|
|
254
297
|
}
|
|
255
298
|
const staging = fs.mkdtempSync(path.join(runtimeRoot, '.staging-'));
|
|
256
299
|
const prev = path.join(runtimeRoot, PREVIOUS);
|
|
@@ -298,7 +341,7 @@ function syncLocked(pkgRoot, runtimeRoot, exists) {
|
|
|
298
341
|
fs.unlinkSync(journal);
|
|
299
342
|
journaled = false;
|
|
300
343
|
try { fs.rmSync(staging, { recursive: true, force: true }); } catch (_) { /* committed, inert staging */ }
|
|
301
|
-
return { ok: true, status: 'synced', created: !exists, copied: rels, reason: null };
|
|
344
|
+
return settle(runtimeRoot, rels, { ok: true, status: 'synced', created: !exists, copied: rels, reason: null });
|
|
302
345
|
} catch (e) {
|
|
303
346
|
if (journaled) {
|
|
304
347
|
const rolled = recoverUnlocked(runtimeRoot);
|
package/package.json
CHANGED