@goondocks/myco 1.1.1 → 1.2.0-beta.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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Myco</title>
8
- <script type="module" crossorigin src="/assets/index-C5w3HRp-.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DhdTCITd.css">
8
+ <script type="module" crossorigin src="/assets/index-BnYhMW3F.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-B7gU3-ZJ.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goondocks/myco",
3
- "version": "1.1.1",
3
+ "version": "1.2.0-beta.1",
4
4
  "description": "Collective agent intelligence — Claude Code plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,11 +73,11 @@
73
73
  "zod": "^4.4.3"
74
74
  },
75
75
  "optionalDependencies": {
76
- "@goondocks/myco-darwin-arm64": "1.1.1",
77
- "@goondocks/myco-darwin-x64": "1.1.1",
78
- "@goondocks/myco-linux-arm64": "1.1.1",
79
- "@goondocks/myco-linux-x64": "1.1.1",
80
- "@goondocks/myco-windows-x64": "1.1.1"
76
+ "@goondocks/myco-darwin-arm64": "1.2.0-beta.1",
77
+ "@goondocks/myco-darwin-x64": "1.2.0-beta.1",
78
+ "@goondocks/myco-linux-arm64": "1.2.0-beta.1",
79
+ "@goondocks/myco-linux-x64": "1.2.0-beta.1",
80
+ "@goondocks/myco-windows-x64": "1.2.0-beta.1"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@goondocks/myco-shared": "*",
@@ -1,23 +1,27 @@
1
- // Postinstall: locate the platform-specific binary that npm should have
2
- // installed as an optionalDependency, mark it executable, and write
3
- // `vendor/resolved.json` so `bin/myco` can dispatch without re-resolving on
4
- // every invocation.
1
+ // Postinstall: bootstrap npm into the single self-updating managed binary.
5
2
  //
6
3
  // Each supported platform has its own published package
7
4
  // (`@goondocks/myco-<target>`) whose `package.json` carries the matching
8
5
  // `os` and `cpu` filters. npm installs only the matching one; the rest are
9
6
  // skipped. This script uses `require.resolve` to find the binary inside the
10
- // installed platform package, and exits non-zero with a clear message if
11
- // nothing resolves do NOT silently succeed, because `bin/myco` would then
12
- // fail at every call.
7
+ // installed platform package, writes `vendor/resolved.json` (so a fallback
8
+ // `bin/myco` dispatch can find it), and then CONVERGES: it copies the
9
+ // selected binary into the canonical managed location (`~/.myco/bin/myco`),
10
+ // reconciles the `runtime.command` pin so every CLI invocation re-execs the
11
+ // managed binary, writes the install marker, and re-points the OS service at
12
+ // the managed binary. npm thus becomes a one-time bootstrap that hands off to
13
+ // the same self-updating binary the curl installer produces.
14
+ //
15
+ // This module exports `convergeNpmInstall` so the convergence mechanics are
16
+ // unit-testable in isolation. The postinstall side effects run only when the
17
+ // file is executed as the main module (the is-main guard at the bottom), so
18
+ // importing it for a test is side-effect free.
13
19
 
14
20
  import fs from 'node:fs';
15
21
  import path from 'node:path';
22
+ import os from 'node:os';
16
23
  import { createRequire } from 'node:module';
17
- import { fileURLToPath } from 'node:url';
18
-
19
- const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
- const require = createRequire(import.meta.url);
24
+ import { fileURLToPath, pathToFileURL } from 'node:url';
21
25
 
22
26
  function detectTarget() {
23
27
  const { platform, arch } = process;
@@ -27,79 +31,291 @@ function detectTarget() {
27
31
  return null;
28
32
  }
29
33
 
30
- const target = detectTarget();
31
- if (!target) {
32
- process.stderr.write(
33
- `[myco] Unsupported platform: ${process.platform}-${process.arch}. ` +
34
- `Supported: darwin-{arm64,x64}, linux-{x64,arm64}, windows-x64.\n`,
35
- );
36
- process.exit(1);
34
+ /**
35
+ * Converge the npm install onto the single managed binary. Pure-ish: all fs
36
+ * I/O is confined to `home` / `dest`. Every step is wrapped so a failure logs
37
+ * to stderr and is NON-FATAL — npm postinstall must never fail because the
38
+ * daemon's lazy-spawn path and `myco doctor` still recover the gap.
39
+ *
40
+ * `dest` and `versionedDest` are INJECTED (not computed here) so the paths
41
+ * live in exactly one place — `@myco/install/managed-binary` — which prod
42
+ * computes from the compiled `dist` module and tests compute from the `.ts`.
43
+ * Same source, zero `.ts`/`.mjs` drift.
44
+ *
45
+ * Layout produced (mirrors install.sh / the daemon helpers):
46
+ * `versionedDest` → <bindir>/versions/<bare-semver>/myco[.exe]
47
+ * `dest` → <bindir>/myco[.exe] (stable, current slot)
48
+ *
49
+ * Sequence: place at versioned slot via atomic temp+rename, then copy from
50
+ * the versioned slot to the stable path via a second atomic temp+rename. A
51
+ * partial copy can never leave a broken stable binary.
52
+ *
53
+ * Returns `{ dest, copied, pinAction }` for callers/tests to assert.
54
+ *
55
+ * @param {{ home: string, platform: string, resolvedBinary: string, dest: string, channel: string, version?: string, versionedDest?: string, writeMarker?: Function }} args
56
+ */
57
+ export function convergeNpmInstall({ home, platform, resolvedBinary, dest, channel, version, versionedDest, writeMarker }) {
58
+ const log = (msg) => process.stderr.write(`[myco] ${msg}\n`);
59
+ const mycoHome = path.join(home, '.myco');
60
+ let copied = false;
61
+ let pinAction = 'skipped';
62
+
63
+ // --- Step 1: Atomic placement into the versioned slot -------------------
64
+ // When `versionedDest` is provided, place the binary at the versioned path
65
+ // first. This is the canonical layout: <bindir>/versions/<semver>/myco[.exe].
66
+ // Uses the same temp+rename pattern as the stable copy below.
67
+ let sourceForStable = resolvedBinary;
68
+ if (versionedDest) {
69
+ try {
70
+ fs.mkdirSync(path.dirname(versionedDest), { recursive: true });
71
+ const tmpV = `${versionedDest}.tmp-${process.pid}`;
72
+ try {
73
+ fs.copyFileSync(resolvedBinary, tmpV);
74
+ if (platform !== 'win32') {
75
+ try { fs.chmodSync(tmpV, 0o755); } catch { /* best effort */ }
76
+ }
77
+ fs.renameSync(tmpV, versionedDest);
78
+ // Stable copy reads from the versioned slot — ensures both paths
79
+ // hold identical bytes from the same verified source.
80
+ sourceForStable = versionedDest;
81
+ } catch (err) {
82
+ try { fs.rmSync(tmpV, { force: true }); } catch { /* best effort */ }
83
+ throw err;
84
+ }
85
+ } catch (err) {
86
+ log(`versioned binary placement skipped: ${err?.message ?? err}`);
87
+ // Fall back to copying directly from the resolved source binary.
88
+ }
89
+ }
90
+
91
+ // --- Step 2: Atomic copy to the stable dest ----------------------------
92
+ // Write to a pid-suffixed temp file in the same directory, then rename onto
93
+ // `dest`. A reader either sees the old binary or the new one, never a
94
+ // half-written file.
95
+ try {
96
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
97
+ const tmp = `${dest}.tmp-${process.pid}`;
98
+ try {
99
+ fs.copyFileSync(sourceForStable, tmp);
100
+ if (platform !== 'win32') {
101
+ try { fs.chmodSync(tmp, 0o755); } catch { /* best effort */ }
102
+ }
103
+ fs.renameSync(tmp, dest);
104
+ copied = true;
105
+ } catch (err) {
106
+ // Clean up the temp file on any failure.
107
+ try { fs.rmSync(tmp, { force: true }); } catch { /* best effort */ }
108
+ // On win32 the existing managed binary may be running (the daemon /
109
+ // the launcher), so the rename fails EBUSY/EPERM. Task 9 handles the
110
+ // win32 in-place swap; here we skip and continue, non-fatal.
111
+ if (platform === 'win32' && (err?.code === 'EBUSY' || err?.code === 'EPERM')) {
112
+ log('managed binary in use; skipped (win32 swap deferred to update path)');
113
+ } else {
114
+ throw err;
115
+ }
116
+ }
117
+ } catch (err) {
118
+ log(`managed binary copy skipped: ${err?.message ?? err}`);
119
+ }
120
+
121
+ // --- Pin reconciliation -------------------------------------------------
122
+ // `runtime.command` is the machine pin every CLI shim re-execs. We may
123
+ // safely point it at the managed binary, but must NOT clobber an active
124
+ // beta managed-runtime pin or a deliberate external/dev pin.
125
+ try {
126
+ const pinPath = path.join(mycoHome, 'runtime.command');
127
+ let pin = '';
128
+ try { pin = fs.readFileSync(pinPath, 'utf8').trim(); } catch { /* absent */ }
129
+
130
+ // Preserve a legacy managed-runtime pin (a path under ~/.myco/runtime/node_modules/)
131
+ // so a pre-native-installer setup isn't stranded.
132
+ const normalize = (p) => p.split(path.sep).join('/');
133
+ const managedPrefix = `${normalize(path.join(mycoHome, 'runtime'))}/node_modules/`;
134
+ const isManagedRuntimePin = pin !== '' && normalize(pin).startsWith(managedPrefix);
135
+
136
+ const shouldWrite =
137
+ pin === '' ||
138
+ pin === dest ||
139
+ (pin.includes('/node_modules/') && !isManagedRuntimePin);
140
+
141
+ if (shouldWrite) {
142
+ fs.mkdirSync(mycoHome, { recursive: true });
143
+ fs.writeFileSync(pinPath, `${dest}\n`, { mode: 0o644 });
144
+ // `mode` in writeFileSync is masked by umask; chmod to be certain the
145
+ // pin is never group/other-writable (runtime-redirect.cjs refuses it).
146
+ try { fs.chmodSync(pinPath, 0o644); } catch { /* best effort */ }
147
+ pinAction = 'wrote';
148
+ } else if (isManagedRuntimePin) {
149
+ log('legacy managed-runtime pin detected; not re-pointing runtime.command');
150
+ pinAction = 'preserved-managed';
151
+ } else {
152
+ log('external runtime.command pin preserved; not re-pointing');
153
+ pinAction = 'preserved-external';
154
+ }
155
+ } catch (err) {
156
+ log(`runtime.command reconcile skipped: ${err?.message ?? err}`);
157
+ }
158
+
159
+ // --- Install marker -----------------------------------------------------
160
+ // When `writeMarker` is injected (production: `managed.writeInstallMarker`
161
+ // from dist/src/install/managed-binary.js), delegate to the canonical helper
162
+ // so the format is defined in exactly one place. Tests that call
163
+ // convergeNpmInstall directly do not inject `writeMarker`, so the inline
164
+ // fallback keeps convergence unit-testable without a compiled dist/.
165
+ try {
166
+ if (writeMarker) {
167
+ writeMarker(mycoHome, { channel, source: 'npm', bin: dest });
168
+ } else {
169
+ fs.mkdirSync(mycoHome, { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(mycoHome, 'install.json'),
172
+ JSON.stringify({ channel, source: 'npm', bin: dest }, null, 2),
173
+ );
174
+ }
175
+ } catch (err) {
176
+ log(`install marker skipped: ${err?.message ?? err}`);
177
+ }
178
+
179
+ return { dest, copied, pinAction };
37
180
  }
38
181
 
39
- const platformPkg = `@goondocks/myco-${target}`;
40
- const binaryName = process.platform === 'win32' ? 'myco.exe' : 'myco';
41
-
42
- // Source-checkout escape hatch: during local dev (`npm ci` in the monorepo)
43
- // the platform package's `bin/` directory is empty until `make dev-link` (or
44
- // an explicit `npm run build:binary`) compiles the host-target binary into
45
- // it. Detect that state via the presence of `src/`, and exit 0 with a hint
46
- // so monorepo installs don't trip postinstall.
47
- const isSourceCheckout = fs.existsSync(path.join(pkgRoot, 'src'));
48
-
49
- let binaryPath;
50
- try {
51
- binaryPath = require.resolve(`${platformPkg}/bin/${binaryName}`);
52
- } catch (err) {
53
- if (isSourceCheckout) {
182
+ /**
183
+ * Derive the release channel from this package's own version: a semver
184
+ * prerelease component (`-beta`, `-alpha`, `-rc`, …) => 'beta', else 'stable'.
185
+ * Any error defaults to 'stable'.
186
+ */
187
+ function deriveChannel(pkgRoot) {
188
+ try {
189
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
190
+ return /-(?:beta|alpha|rc|next|canary|dev)\b/.test(String(pkg.version)) ? 'beta' : 'stable';
191
+ } catch {
192
+ return 'stable';
193
+ }
194
+ }
195
+
196
+ async function main() {
197
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
198
+ const require = createRequire(import.meta.url);
199
+
200
+ const target = detectTarget();
201
+ if (!target) {
202
+ process.stderr.write(
203
+ `[myco] Unsupported platform: ${process.platform}-${process.arch}. ` +
204
+ `Supported: darwin-{arm64,x64}, linux-{x64,arm64}, windows-x64.\n`,
205
+ );
206
+ process.exit(1);
207
+ }
208
+
209
+ const platformPkg = `@goondocks/myco-${target}`;
210
+ const binaryName = process.platform === 'win32' ? 'myco.exe' : 'myco';
211
+
212
+ // Source-checkout escape hatch: during local dev (`npm ci` in the monorepo)
213
+ // the platform package's `bin/` directory is empty until `make dev-link` (or
214
+ // an explicit `npm run build:binary`) compiles the host-target binary into
215
+ // it. Detect that state via the presence of `src/`, and exit 0 with a hint
216
+ // so monorepo installs don't trip postinstall.
217
+ const isSourceCheckout = fs.existsSync(path.join(pkgRoot, 'src'));
218
+
219
+ let binaryPath;
220
+ try {
221
+ binaryPath = require.resolve(`${platformPkg}/bin/${binaryName}`);
222
+ } catch (err) {
223
+ if (isSourceCheckout) {
224
+ process.stderr.write(
225
+ `[myco] No platform binary found in ${platformPkg}/bin/${binaryName}. ` +
226
+ `Skipping postinstall in source checkout (expected before \`make dev-link\`).\n`,
227
+ );
228
+ process.exit(0);
229
+ }
54
230
  process.stderr.write(
55
- `[myco] No platform binary found in ${platformPkg}/bin/${binaryName}. ` +
56
- `Skipping postinstall in source checkout (expected before \`make dev-link\`).\n`,
231
+ `[myco] Platform binary package ${platformPkg} is not installed. ` +
232
+ `npm should have installed it as an optionalDependency of @goondocks/myco. ` +
233
+ `Try: npm install --include=optional -g @goondocks/myco\n` +
234
+ `(reason: ${err.message})\n`,
57
235
  );
58
- process.exit(0);
236
+ process.exit(1);
59
237
  }
60
- process.stderr.write(
61
- `[myco] Platform binary package ${platformPkg} is not installed. ` +
62
- `npm should have installed it as an optionalDependency of @goondocks/myco. ` +
63
- `Try: npm install --include=optional -g @goondocks/myco\n` +
64
- `(reason: ${err.message})\n`,
238
+
239
+ if (process.platform !== 'win32') {
240
+ try { fs.chmodSync(binaryPath, 0o755); } catch { /* best effort */ }
241
+ }
242
+
243
+ const vendorDir = path.join(pkgRoot, 'vendor');
244
+ fs.mkdirSync(vendorDir, { recursive: true });
245
+ const resolvedPath = path.join(vendorDir, 'resolved.json');
246
+ fs.writeFileSync(
247
+ resolvedPath,
248
+ JSON.stringify({ target, binaryPath }, null, 2) + '\n',
249
+ 'utf-8',
65
250
  );
66
- process.exit(1);
67
- }
68
251
 
69
- if (process.platform !== 'win32') {
70
- try { fs.chmodSync(binaryPath, 0o755); } catch { /* best effort */ }
71
- }
252
+ process.stdout.write(`[myco] Selected platform binary: ${binaryPath}\n`);
72
253
 
73
- const vendorDir = path.join(pkgRoot, 'vendor');
74
- fs.mkdirSync(vendorDir, { recursive: true });
75
- const resolvedPath = path.join(vendorDir, 'resolved.json');
76
- fs.writeFileSync(
77
- resolvedPath,
78
- JSON.stringify({ target, binaryPath }, null, 2) + '\n',
79
- 'utf-8',
80
- );
81
-
82
- process.stdout.write(`[myco] Selected platform binary: ${binaryPath}\n`);
83
-
84
- // Self-install as a managed OS service so launchd / systemd starts the
85
- // daemon at every login from the moment Myco is installed. Skipped in
86
- // source checkouts (no published dist/), skipped silently on failure
87
- // (the daemon's lazy-spawn path still works; doctor will surface the
88
- // gap). Plan reference: Decision 13 / Step 12.
89
- if (!isSourceCheckout) {
90
- const distSelfInstall = path.join(pkgRoot, 'dist/src/service/self-install.js');
91
- if (fs.existsSync(distSelfInstall)) {
92
- try {
93
- const mod = await import(distSelfInstall);
94
- const stderrLogger = {
95
- info: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
96
- debug: () => undefined,
97
- warn: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
98
- error: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
99
- };
100
- await mod.ensureSelfInstalledAsService(stderrLogger);
101
- } catch (err) {
102
- process.stderr.write(`[myco] Service install skipped: ${err?.message ?? err}\n`);
254
+ // Converge npm into the single managed binary, then self-install/repoint the
255
+ // OS service at it. Skipped in source checkouts (no published dist/). Both
256
+ // steps are wrapped so neither can fail the postinstall — the daemon's
257
+ // lazy-spawn path still works and `myco doctor` surfaces any gap.
258
+ // Plan reference: Decision 13 / Step 12.
259
+ if (!isSourceCheckout) {
260
+ const distManagedBinary = path.join(pkgRoot, 'dist/src/install/managed-binary.js');
261
+ let dest = null;
262
+ if (fs.existsSync(distManagedBinary)) {
263
+ try {
264
+ const managed = await import(distManagedBinary);
265
+ const home = os.homedir();
266
+ const platform = process.platform;
267
+ // `pkg.version` is the bare semver (e.g. "1.2.3") npm packages never
268
+ // carry the "myco/v" tag prefix that curl installers use. This is the
269
+ // exact version string the daemon's versionBinaryPath() expects.
270
+ let pkg = { version: null };
271
+ try {
272
+ pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
273
+ } catch { /* version stays null; versionedDest skipped */ }
274
+ const version = pkg.version ?? null;
275
+ dest = managed.managedBinaryPath(home, platform);
276
+ const versionedDest = version
277
+ ? managed.versionBinaryPath(home, platform, version)
278
+ : null;
279
+ const channel = deriveChannel(pkgRoot);
280
+ convergeNpmInstall({
281
+ home,
282
+ platform,
283
+ resolvedBinary: binaryPath,
284
+ dest,
285
+ channel,
286
+ version,
287
+ versionedDest,
288
+ writeMarker: managed.writeInstallMarker,
289
+ });
290
+ } catch (err) {
291
+ process.stderr.write(`[myco] Convergence skipped: ${err?.message ?? err}\n`);
292
+ }
293
+ } else {
294
+ process.stderr.write('[myco] Convergence skipped: managed-binary module not found in dist/\n');
295
+ }
296
+
297
+ const distSelfInstall = path.join(pkgRoot, 'dist/src/service/self-install.js');
298
+ if (fs.existsSync(distSelfInstall)) {
299
+ try {
300
+ const mod = await import(distSelfInstall);
301
+ const stderrLogger = {
302
+ info: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
303
+ debug: () => undefined,
304
+ warn: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
305
+ error: (kind, message) => process.stderr.write(`[myco] ${kind}: ${message}\n`),
306
+ };
307
+ // Re-point the service unit at the managed binary so a self-update's
308
+ // in-place swap takes effect on the next supervisor restart. Falls
309
+ // back to the default executable inside self-install if `dest` is null.
310
+ await mod.ensureSelfInstalledAsService(stderrLogger, dest ? { executable: dest } : {});
311
+ } catch (err) {
312
+ process.stderr.write(`[myco] Service install skipped: ${err?.message ?? err}\n`);
313
+ }
103
314
  }
104
315
  }
105
316
  }
317
+
318
+ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
319
+ if (isMain) {
320
+ await main();
321
+ }