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