total-recall-brain 3.46.0 → 3.46.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.
@@ -275,6 +275,9 @@ async function main() {
275
275
  console.error(`Expected handler at: ${handlerPath}`);
276
276
  process.exit(1);
277
277
  }
278
+ // A new plugin contract refuses the previous release's defaults, `update` included; refresh them and rerun once.
279
+ const rerun = await import('../src/core/default-plugin-refresh.mjs').then(m => m.rerunAfterDefaultRefresh(err), () => null);
280
+ if (rerun !== null) process.exit(rerun);
278
281
  console.error(`Error executing '${command}':`, err.message);
279
282
  process.exit(1);
280
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.46.0",
3
+ "version": "3.46.2",
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": {
@@ -14,8 +14,10 @@
14
14
  * background with the frontend's own build script, and keeps serving the
15
15
  * current bundle until the new one is complete. It never installs
16
16
  * dependencies: missing build tools are reported, not repaired behind the
17
- * operator's back. An npm tarball gives every file the same mtime, so an
18
- * installed package is never stale and never builds.
17
+ * operator's back. An installed package (a root inside node_modules) serves
18
+ * the bundle its release built, which the publish gate checked, and is never
19
+ * stale and never builds: npm does not keep the tarball's file times on every
20
+ * install, so comparing times there reports a stale bundle that is not.
19
21
  */
20
22
  import { spawn } from 'node:child_process';
21
23
  import fs from 'node:fs';
@@ -60,9 +62,10 @@ export async function newestUnder(target) {
60
62
  * Returns { ok, reason, bundle, source }; never throws, never prints, so the
61
63
  * decision can be tested apart from how it is reported.
62
64
  */
63
- export async function checkDistFreshness({ distDir, sourcePaths }) {
65
+ export async function checkDistFreshness({ distDir, sourcePaths, packaged = false }) {
64
66
  const bundle = await newestUnder(distDir);
65
67
  if (!bundle) return { ok: false, reason: 'missing-bundle', bundle: null, source: null };
68
+ if (packaged) return { ok: true, reason: 'packaged', bundle, source: null };
66
69
 
67
70
  let source = null;
68
71
  for (const candidate of sourcePaths) {
@@ -76,6 +79,11 @@ export async function checkDistFreshness({ distDir, sourcePaths }) {
76
79
  return { ok: true, reason: 'current', bundle, source };
77
80
  }
78
81
 
82
+ /** An npm-installed package: its root sits inside a node_modules directory. */
83
+ export function isInstalledPackage(root) {
84
+ return path.resolve(root).split(path.sep).includes('node_modules');
85
+ }
86
+
79
87
  /** Every input whose change changes the built dashboard. */
80
88
  export function dashboardTargets(root) {
81
89
  const frontend = path.join(root, 'frontend');
@@ -85,6 +93,7 @@ export function dashboardTargets(root) {
85
93
  } catch { /* no frontend directory: nothing to compare */ }
86
94
  return {
87
95
  distDir: path.join(frontend, 'dist'),
96
+ packaged: isInstalledPackage(root),
88
97
  sourcePaths: [
89
98
  path.join(frontend, 'src'),
90
99
  path.join(frontend, 'public'),
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A release that changes the plugin contract refuses the shared default plugins the previous release
3
+ * installed, and with them every command that loads one, `update` included: the very command that would
4
+ * replace them. So a machine moving to such a release could not update itself. When a command fails that
5
+ * way, refresh the defaults from this package's bundled copies (a link is its owner's checkout and is left
6
+ * alone) and run the command again, once.
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import { ensureRequiredPlugins } from './required-plugins.mjs';
11
+ import { globalPluginsDir } from './plugin-loader.mjs';
12
+
13
+ const REFUSAL = /\bRequired (?:plugin [\w-]+|[\w-]+ plugin) is missing or invalid\b/;
14
+ export const REFRESHED_ENV = 'TR_DEFAULT_PLUGINS_REFRESHED';
15
+
16
+ export function isDefaultPluginRefusal(error) {
17
+ return REFUSAL.test(String(error?.message || ''));
18
+ }
19
+
20
+ /** Returns the rerun's exit status, or null when this error is not one a refresh can fix. */
21
+ export function rerunAfterDefaultRefresh(error, {
22
+ env = process.env, argv = process.argv, execPath = process.execPath, ensure = ensureRequiredPlugins,
23
+ sharedBrainDir = path.dirname(globalPluginsDir()), run = spawnSync, log = (line) => console.error(line),
24
+ } = {}) {
25
+ if (!isDefaultPluginRefusal(error) || env[REFRESHED_ENV] === '1') return null;
26
+ try {
27
+ ensure(sharedBrainDir, { sharedBrainDir });
28
+ } catch (refreshError) {
29
+ log(`The default plugins could not be refreshed from this release: ${refreshError.message}`);
30
+ return null;
31
+ }
32
+ log('Refreshed the default plugins from this release; running the command again.');
33
+ const result = run(execPath, argv.slice(1), { stdio: 'inherit', env: { ...env, [REFRESHED_ENV]: '1' } });
34
+ if (result.error) {
35
+ log(`The command could not be run again: ${result.error.message}`);
36
+ return 1;
37
+ }
38
+ return result.status ?? 1;
39
+ }