total-recall-brain 3.43.0 → 3.43.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.43.0",
3
+ "version": "3.43.1",
4
4
  "description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tr-plugin-ssss",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-ssss",
9
- "version": "0.1.1",
9
+ "version": "0.1.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.14.4"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-ssss",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Canonical SSSS distribution, scaffolding and validation for Total Recall installations.",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "ssss",
4
4
  "name": "SSSS",
5
- "version": "0.1.1",
5
+ "version": "0.1.2",
6
6
  "description": "Canonical SSSS distribution, scaffolding and validation for Total Recall installations.",
7
7
  "license": "MIT",
8
8
  "use_cases": ["software-development", "state-conformance"],
@@ -99,16 +99,25 @@ function inspectScaffold(dir, source) {
99
99
  return { ok: true, path: dir, files: Object.keys(expected.files).length };
100
100
  }
101
101
 
102
- function skillRootFor(target, sharedBrainDir) {
103
- if (!stat(target)?.isSymbolicLink()) return target;
104
- const sharedSkill = sharedBrainDir && path.join(path.dirname(sharedBrainDir), 'ssss');
105
- const resolved = fs.realpathSync(target);
106
- if (!sharedSkill || resolved !== sharedSkill) throw new Error('SSSS skill points outside the shared skill installation');
107
- return resolved;
102
+ const realOrNull = file => { try { return fs.realpathSync(file); } catch { return null; } };
103
+ /** A path with its folders resolved and its last part kept, so a link is compared as itself. */
104
+ const entryOf = file => path.join(realOrNull(path.dirname(file)) ?? path.dirname(file), path.basename(file));
105
+
106
+ /**
107
+ * True for a project's link to the machine's shared skill. A link at the shared location itself
108
+ * is not one: that location is what has to hold the skill.
109
+ */
110
+ function linksToSharedSkill(target, sharedBrainDir) {
111
+ if (!sharedBrainDir) return false;
112
+ const shared = path.join(path.dirname(sharedBrainDir), 'ssss');
113
+ if (entryOf(shared) === entryOf(target)) return false;
114
+ const resolved = realOrNull(target);
115
+ return resolved !== null && resolved === realOrNull(shared);
108
116
  }
109
117
 
110
- function inspectSkill(target, source, sharedBrainDir) {
111
- const dir = skillRootFor(target, sharedBrainDir);
118
+ /** A link that reaches a complete, current skill is left alone, like a linked plugin. */
119
+ function inspectSkill(target, source) {
120
+ const dir = stat(target)?.isSymbolicLink() ? fs.realpathSync(target) : target;
112
121
  requireDirectory(dir);
113
122
  const wrapper = fs.readFileSync(regularPath(dir, 'SKILL.md'), 'utf8');
114
123
  if (!/^name:\s*['"]?ssss['"]?\s*$/m.test(wrapper) || !wrapper.includes('core/SKILL.md')) {
@@ -164,31 +173,63 @@ function writeAsset(root, relative, content) {
164
173
  fs.writeFileSync(file, content, { flag: 'wx' });
165
174
  }
166
175
 
176
+ const NAMES_THIS_SKILL = /^name:\s*['"]?ssss['"]?\s*$/m;
177
+
178
+ /** Fill a staged skill: any files already there stay, and the portable core is always replaced. */
179
+ function completeSkill(stage, source) {
180
+ fs.rmSync(path.join(stage, 'core'), { recursive: true, force: true });
181
+ for (const relative of Object.keys(source.skill)) {
182
+ if (!stat(regularPath(stage, relative))) {
183
+ writeAsset(stage, relative, fs.readFileSync(regularPath(source.skillRoot, relative)));
184
+ }
185
+ }
186
+ const wrapper = path.join(stage, 'SKILL.md');
187
+ if (!fs.readFileSync(wrapper, 'utf8').includes('core/SKILL.md')) fs.appendFileSync(wrapper, ROUTE);
188
+ inspectSkill(stage, source);
189
+ }
190
+
191
+ /**
192
+ * An older layout linked the skill in, and the link is now dangling or stale. Only the link is
193
+ * replaced: nothing is written through it, so whatever it pointed at stays exactly as it was.
194
+ * A wrapper with this skill's name carries over, because it can hold repository guidance.
195
+ */
196
+ function replaceLink(target, source) {
197
+ const pointsAt = fs.readlinkSync(target);
198
+ let wrapper = null;
199
+ try {
200
+ const text = fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8');
201
+ if (NAMES_THIS_SKILL.test(text)) wrapper = text;
202
+ } catch { /* a dangling link or a folder with no entry has no wrapper to keep */ }
203
+ publishDirectory(target, stage => {
204
+ if (wrapper) writeAsset(stage, 'SKILL.md', wrapper);
205
+ completeSkill(stage, source);
206
+ }, true);
207
+ return { path: target, target: pointsAt };
208
+ }
209
+
210
+ /** @returns {{ path: string, target: string } | undefined} the link it replaced, if it replaced one */
167
211
  function installSkill(target, source, sharedBrainDir) {
168
- const dir = skillRootFor(target, sharedBrainDir);
169
- if (dir !== target) return inspectSkill(target, source, sharedBrainDir);
170
- checkRoot(dir);
171
- const exists = Boolean(stat(dir));
212
+ if (stat(target)?.isSymbolicLink()) {
213
+ if (!linksToSharedSkill(target, sharedBrainDir)) return replaceLink(target, source);
214
+ // The shared skill is repaired where it lives. A project does not take a private copy.
215
+ inspectSkill(target, source);
216
+ return undefined;
217
+ }
218
+ checkRoot(target);
219
+ const exists = Boolean(stat(target));
172
220
  // Stage the complete skill so a failed source read cannot leave a partial skill.
173
221
  if (exists) {
174
- const wrapper = regularPath(dir, 'SKILL.md');
175
- if (stat(wrapper) && !/^name:\s*['"]?ssss['"]?\s*$/m.test(fs.readFileSync(wrapper, 'utf8'))) {
176
- throw new Error('Existing skill directory belongs to another skill: ' + dir);
222
+ const wrapper = regularPath(target, 'SKILL.md');
223
+ if (stat(wrapper) && !NAMES_THIS_SKILL.test(fs.readFileSync(wrapper, 'utf8'))) {
224
+ throw new Error('Existing skill directory belongs to another skill: ' + target);
177
225
  }
178
- listFiles(dir);
226
+ listFiles(target);
179
227
  }
180
- publishDirectory(dir, stage => {
181
- if (exists) fs.cpSync(dir, stage, { recursive: true });
182
- fs.rmSync(path.join(stage, 'core'), { recursive: true, force: true });
183
- for (const relative of Object.keys(source.skill)) {
184
- if (!stat(regularPath(stage, relative))) {
185
- writeAsset(stage, relative, fs.readFileSync(regularPath(source.skillRoot, relative)));
186
- }
187
- }
188
- const wrapper = path.join(stage, 'SKILL.md');
189
- if (!fs.readFileSync(wrapper, 'utf8').includes('core/SKILL.md')) fs.appendFileSync(wrapper, ROUTE);
190
- inspectSkill(stage, source);
228
+ publishDirectory(target, stage => {
229
+ if (exists) fs.cpSync(target, stage, { recursive: true });
230
+ completeSkill(stage, source);
191
231
  }, exists);
232
+ return undefined;
192
233
  }
193
234
 
194
235
  function hasUpdaterDrift(dir, source, receipt) {
@@ -247,7 +288,7 @@ function inspectInstalled(options, source) {
247
288
  catch (error) { return { ok: false, path: target, error: error.message }; }
248
289
  };
249
290
  const observed = {
250
- skill: check(skill, () => inspectSkill(skill, source, options.sharedBrainDir)),
291
+ skill: check(skill, () => inspectSkill(skill, source)),
251
292
  scaffold: check(scaffold, () => {
252
293
  checkRoot(options.brainDir);
253
294
  checkRoot(path.dirname(scaffold));
@@ -269,10 +310,10 @@ function ensureInstallation(options) {
269
310
  checkRoot(options.brainDir);
270
311
  const current = inspectInstalled(options, source);
271
312
  const recoveredFrom = current.scaffold.ok ? undefined : installScaffold(scaffold, source, options.repair === true);
272
- if (!current.skill.ok) installSkill(skill, source, options.sharedBrainDir);
313
+ const replacedLink = current.skill.ok ? undefined : installSkill(skill, source, options.sharedBrainDir);
273
314
  const installed = inspectInstalled(options, source);
274
315
  if (!installed.ok) throw new Error([installed.skill.error, installed.scaffold.error].filter(Boolean).join('; '));
275
- return recoveredFrom ? { ...installed, recoveredFrom } : installed;
316
+ return { ...installed, ...(recoveredFrom ? { recoveredFrom } : {}), ...(replacedLink ? { replacedLink } : {}) };
276
317
  }
277
318
 
278
319
  async function maintain({ brainDir, entries, apply, run }) {
@@ -243,7 +243,11 @@ export function consumerState(root, pkg, version = null) {
243
243
  /* keep looking */
244
244
  }
245
245
  }
246
- const satisfied = range ? Boolean(installed) && installed === version : Boolean(want && installed === want);
246
+ // What a consumer runs is what counts, and the version named is the question. A pin can lag on
247
+ // purpose, because `update` installs plugin copies with --no-save. Comparing against the pin
248
+ // would fail those and pass a consumer whose pin and install both lag.
249
+ const expected = version || want;
250
+ const satisfied = Boolean(expected && installed === expected);
247
251
  return {
248
252
  root,
249
253
  declared,
@@ -251,6 +255,7 @@ export function consumerState(root, pkg, version = null) {
251
255
  range,
252
256
  installed,
253
257
  satisfied,
258
+ pinBehind: Boolean(satisfied && want && want !== installed),
254
259
  reason: !installed ? 'not-installed' : satisfied ? 'ok' : 'installed-mismatch',
255
260
  };
256
261
  }
@@ -279,6 +284,7 @@ export function verifyConsumers(pkg, version, opts = {}) {
279
284
  consumers: relevant.length,
280
285
  failing: failing.length,
281
286
  failures: failing.map((s) => ({ root: s.root, declared: s.declared, installed: s.installed, reason: s.reason })),
287
+ pin_behind: relevant.filter((s) => s.pinBehind).map((s) => ({ root: s.root, declared: s.declared, installed: s.installed })),
282
288
  };
283
289
  }
284
290
 
@@ -362,6 +368,7 @@ export async function run(argv = process.argv, deps = {}) {
362
368
  return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers not satisfied. First: ${consumers.failures[0].root} (${consumers.failures[0].installed ?? 'not installed'}).`);
363
369
  }
364
370
  log(` ✓ ${pkg}@${version} verified: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
371
+ if (consumers.pin_behind.length) log(` ${consumers.pin_behind.length} run it with an older pin in package.json, such as ${consumers.pin_behind[0].root}`);
365
372
  continue;
366
373
  }
367
374