@remnic/plugin-pi 9.3.702 → 9.3.704

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.
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { MemoryExtensionPublisher, PublisherCapabilities, PublishContext, PublishResult } from '@remnic/core';
2
3
 
3
4
  /**
@@ -33,6 +34,36 @@ declare class HostMemoryExtensionPublisher implements MemoryExtensionPublisher {
33
34
  private readonly host;
34
35
  static readonly capabilities: PublisherCapabilities;
35
36
  protected constructor(host: HostPublisherDescriptor);
37
+ /**
38
+ * File basenames this publisher owns inside the extension root. The shared
39
+ * set is config + wrapper + readme; subclasses add host-specific files
40
+ * (e.g. omp's pre-bundle loader + package manifest). Used for snapshot,
41
+ * atomic-write rollback, and unpublish cleanup.
42
+ */
43
+ protected get ownedFileNames(): readonly string[];
44
+ /**
45
+ * Directory names this publisher owns inside the extension root (build
46
+ * outputs). Recursively removed on unpublish and on publish rollback when
47
+ * newly created.
48
+ */
49
+ protected get ownedDirNames(): readonly string[];
50
+ /**
51
+ * Whether the generated wrapper must use a bun-buildable import specifier
52
+ * (relative path) instead of a file:// URL. omp pre-bundles the wrapper with
53
+ * `bun build`, which cannot resolve file:// specifiers; pi loads the wrapper
54
+ * directly via tsx and keeps the file:// URL.
55
+ */
56
+ protected get usesBundledWrapper(): boolean;
57
+ /**
58
+ * Hook for subclasses to write host-specific files and run install-time
59
+ * build steps after the shared config/wrapper/readme are written. Runs
60
+ * inside the publish try-block: a throw triggers full rollback.
61
+ */
62
+ protected finalizePublish(_ctx: PublishContext, _extensionRoot: string, _paths: {
63
+ configPath: string;
64
+ wrapperPath: string;
65
+ pluginPiDistPath: string;
66
+ }): void;
36
67
  get hostId(): string;
37
68
  resolveExtensionRoot(env?: NodeJS.ProcessEnv): Promise<string>;
38
69
  isHostAvailable(): Promise<boolean>;
@@ -47,6 +78,61 @@ declare class PiMemoryExtensionPublisher extends HostMemoryExtensionPublisher {
47
78
  /** Publisher for Oh My Pi / omp (`~/.omp/agent/extensions/remnic`). */
48
79
  declare class OmpMemoryExtensionPublisher extends HostMemoryExtensionPublisher {
49
80
  constructor();
81
+ protected get ownedFileNames(): readonly string[];
82
+ protected get ownedDirNames(): readonly string[];
83
+ protected get usesBundledWrapper(): boolean;
84
+ protected finalizePublish(ctx: PublishContext, extensionRoot: string, paths: {
85
+ configPath: string;
86
+ wrapperPath: string;
87
+ pluginPiDistPath: string;
88
+ }): void;
89
+ /**
90
+ * Pre-bundles the omp extension with `bun build` so omp's embedded runtime
91
+ * never resolves bare npm specifiers (e.g. @sinclair/typebox) from the
92
+ * extension's node_modules at load time. The bundle is written to a temp
93
+ * directory and swapped into dist-bundle/ on success. The pre-existing
94
+ * dist-bundle is renamed aside (not removed) before the swap, so a failure
95
+ * during the final rename restores the previously working bundle rather than
96
+ * leaving the install with no bundle at all.
97
+ *
98
+ * Override in tests to skip the real bun invocation.
99
+ */
100
+ protected runBundleBuild(ctx: PublishContext, extensionRoot: string, bunBin: string): void;
50
101
  }
102
+ /**
103
+ * Resolves the import specifier the omp wrapper uses to reach the
104
+ * `@remnic/plugin-pi` dist entry from the generated `index.ts`. omp pre-bundles
105
+ * that wrapper with `bun build`, whose bundler cannot resolve `file://`
106
+ * specifiers ("Could not resolve: file://…" on Bun 1.2–1.3, verified), so the
107
+ * specifier must be a relative path. On Windows, when the extension directory
108
+ * and the plugin-pi install sit on different drives, `path.relative` cannot
109
+ * express a relative path and returns an absolute drive path (e.g. `D:\…`);
110
+ * prefixing `./` then yields an invalid module specifier that fails `bun build`
111
+ * with a cryptic error. Detect that layout and fail fast with an actionable
112
+ * message instead. (Cross-drive omp installs are unsupported because neither a
113
+ * relative specifier nor a `file://` URL is acceptable to `bun build`.) Drive
114
+ * roots are compared case-insensitively so a same-drive Windows install is not
115
+ * falsely rejected when the agent home and the plugin-pi install report the
116
+ * drive letter in different casing (`C:\\` vs `c:\\`).
117
+ *
118
+ * Exported so the cross-drive guard can be exercised on non-Windows hosts via
119
+ * `path.win32`.
120
+ */
121
+ declare function resolveOmpWrapperImportSpecifier(extensionModulePath: string, wrapperDir: string, pathApi?: typeof path): string;
122
+ /**
123
+ * Walks `PATH` the way a shell does and returns the first `bun` executable it
124
+ * finds, as a realpath-resolved absolute path (or null when nothing on PATH
125
+ * is an executable `bun`). Used so the install-time PATH probe can embed an
126
+ * absolute bun path in the generated loader/postinstall instead of the bare
127
+ * string `"bun"`, which would break self-heal rebuilds under a stripped
128
+ * runtime PATH (GUI/service launches). Mirrors `which(1)`; no dependency.
129
+ */
130
+ declare function resolveBunOnPath(): string | null;
131
+ /**
132
+ * Resolves the `bun` binary for the install-time pre-bundle. Honours
133
+ * `REMNIC_OMP_BUN_BIN` (test/override seam), then PATH, then common locations.
134
+ * Returns null when bun is unavailable so the caller can fail with guidance.
135
+ */
136
+ declare function resolveBunBinary(): string | null;
51
137
 
52
- export { HostMemoryExtensionPublisher, type HostPublisherDescriptor, OmpMemoryExtensionPublisher, PiMemoryExtensionPublisher };
138
+ export { HostMemoryExtensionPublisher, type HostPublisherDescriptor, OmpMemoryExtensionPublisher, PiMemoryExtensionPublisher, resolveBunBinary, resolveBunOnPath, resolveOmpWrapperImportSpecifier };
package/dist/publisher.js CHANGED
@@ -4,20 +4,22 @@ import {
4
4
  resolveOmpExtensionRoot,
5
5
  resolvePiAgentHome,
6
6
  resolvePiExtensionRoot
7
- } from "./chunk-A5WP5NTC.js";
7
+ } from "./chunk-ASGQGBO2.js";
8
8
 
9
9
  // src/publisher.ts
10
10
  import fs from "fs";
11
+ import { spawnSync } from "child_process";
11
12
  import path from "path";
12
13
  import { fileURLToPath, pathToFileURL } from "url";
14
+ import os from "os";
13
15
  import {
14
16
  getConnectorToken,
15
17
  loadTokenStore,
16
18
  saveTokenStore
17
19
  } from "@remnic/core";
18
20
  var DEFAULT_DAEMON_PORT = 4318;
19
- var EXTENSION_OWNED_FILES = ["remnic.config.json", "index.ts", "README.md"];
20
- var EXTENSION_OWNED_TEMP_FILE_PATTERN = /^(?:remnic\.config\.json|index\.ts|README\.md)\.tmp-\d+-\d+$/u;
21
+ var BASE_OWNED_FILES = ["remnic.config.json", "index.ts", "README.md"];
22
+ var EXTENSION_OWNED_TEMP_FILE_SUFFIX = /\.tmp-\d+-\d+$/u;
21
23
  var PI_HOST = {
22
24
  hostId: "pi",
23
25
  connectorId: "pi",
@@ -71,6 +73,39 @@ var HostMemoryExtensionPublisher = class {
71
73
  citationFormat: false,
72
74
  readPathTemplate: false
73
75
  };
76
+ /**
77
+ * File basenames this publisher owns inside the extension root. The shared
78
+ * set is config + wrapper + readme; subclasses add host-specific files
79
+ * (e.g. omp's pre-bundle loader + package manifest). Used for snapshot,
80
+ * atomic-write rollback, and unpublish cleanup.
81
+ */
82
+ get ownedFileNames() {
83
+ return BASE_OWNED_FILES;
84
+ }
85
+ /**
86
+ * Directory names this publisher owns inside the extension root (build
87
+ * outputs). Recursively removed on unpublish and on publish rollback when
88
+ * newly created.
89
+ */
90
+ get ownedDirNames() {
91
+ return [];
92
+ }
93
+ /**
94
+ * Whether the generated wrapper must use a bun-buildable import specifier
95
+ * (relative path) instead of a file:// URL. omp pre-bundles the wrapper with
96
+ * `bun build`, which cannot resolve file:// specifiers; pi loads the wrapper
97
+ * directly via tsx and keeps the file:// URL.
98
+ */
99
+ get usesBundledWrapper() {
100
+ return false;
101
+ }
102
+ /**
103
+ * Hook for subclasses to write host-specific files and run install-time
104
+ * build steps after the shared config/wrapper/readme are written. Runs
105
+ * inside the publish try-block: a throw triggers full rollback.
106
+ */
107
+ finalizePublish(_ctx, _extensionRoot, _paths) {
108
+ }
74
109
  get hostId() {
75
110
  return this.host.hostId;
76
111
  }
@@ -112,9 +147,16 @@ var HostMemoryExtensionPublisher = class {
112
147
  const filesWritten = [];
113
148
  const skipped = [];
114
149
  ctx.log.info(`Publishing ${this.host.displayName} memory extension to ${extensionRoot}`);
115
- const [configPath, wrapperPath, readmePath] = extensionOwnedPaths(extensionRoot);
150
+ const ownedFilePaths = this.ownedFileNames.map((fileName) => path.join(extensionRoot, fileName));
151
+ const configPath = ownedFilePaths[0];
152
+ const wrapperPath = ownedFilePaths[1];
153
+ const readmePath = ownedFilePaths[2];
154
+ const pluginPiDistPath = resolveExtensionModulePath();
116
155
  const rootExisted = fs.existsSync(extensionRoot);
117
- const snapshots = snapshotFiles([configPath, wrapperPath, readmePath]);
156
+ const fileSnapshots = snapshotFiles(ownedFilePaths);
157
+ const dirSnapshots = snapshotDirs(
158
+ this.ownedDirNames.map((dirName) => path.join(extensionRoot, dirName))
159
+ );
118
160
  const priorTokenEntry = ctx.rollbackTokenEntry === void 0 ? snapshotTokenEntry(this.host.connectorId) : cloneTokenEntry(ctx.rollbackTokenEntry);
119
161
  const token = getConnectorToken(this.host.connectorId);
120
162
  if (!token) {
@@ -149,25 +191,40 @@ var HostMemoryExtensionPublisher = class {
149
191
  atomicWriteFile(configPath, `${JSON.stringify(config, null, 2)}
150
192
  `, 384);
151
193
  filesWritten.push(configPath);
152
- atomicWriteFile(wrapperPath, renderWrapper(resolveExtensionModulePath(), configPath), 420);
194
+ atomicWriteFile(
195
+ wrapperPath,
196
+ renderWrapper(
197
+ pluginPiDistPath,
198
+ configPath,
199
+ this.usesBundledWrapper ? extensionRoot : void 0
200
+ ),
201
+ 420
202
+ );
153
203
  filesWritten.push(wrapperPath);
154
204
  atomicWriteFile(readmePath, `${await this.renderInstructions(ctx)}
155
205
  `, 420);
156
206
  filesWritten.push(readmePath);
207
+ this.finalizePublish(ctx, extensionRoot, { configPath, wrapperPath, pluginPiDistPath });
208
+ for (let i = BASE_OWNED_FILES.length; i < ownedFilePaths.length; i++) {
209
+ filesWritten.push(ownedFilePaths[i]);
210
+ }
157
211
  } catch (err) {
158
212
  try {
159
- restorePublishSnapshot(extensionRoot, rootExisted, snapshots);
213
+ restoreDirSnapshots(dirSnapshots);
214
+ restorePublishSnapshot(extensionRoot, rootExisted, fileSnapshots);
160
215
  } catch (restoreErr) {
161
216
  ctx.log.warn(
162
217
  `${this.host.displayName} extension rollback failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`
163
218
  );
164
219
  }
165
- try {
166
- restoreTokenEntry(priorTokenEntry, this.host.connectorId);
167
- } catch (tokenErr) {
168
- ctx.log.warn(
169
- `${this.host.displayName} connector token rollback failed: ${tokenErr instanceof Error ? tokenErr.message : String(tokenErr)}`
170
- );
220
+ if (!(err instanceof OmpPreBundleError)) {
221
+ try {
222
+ restoreTokenEntry(priorTokenEntry, this.host.connectorId);
223
+ } catch (tokenErr) {
224
+ ctx.log.warn(
225
+ `${this.host.displayName} connector token rollback failed: ${tokenErr instanceof Error ? tokenErr.message : String(tokenErr)}`
226
+ );
227
+ }
171
228
  }
172
229
  throw err;
173
230
  }
@@ -180,6 +237,8 @@ var HostMemoryExtensionPublisher = class {
180
237
  }
181
238
  async unpublish() {
182
239
  const agentHomes = this.host.listRemovalAgentHomes ? this.host.listRemovalAgentHomes(process.env) : [this.host.resolveAgentHome(process.env)];
240
+ const ownedFileNames = this.ownedFileNames;
241
+ const ownedDirNames = this.ownedDirNames;
183
242
  const seen = /* @__PURE__ */ new Set();
184
243
  for (const agentHome of agentHomes) {
185
244
  const extensionRoot = path.join(path.resolve(agentHome), "extensions", "remnic");
@@ -187,10 +246,28 @@ var HostMemoryExtensionPublisher = class {
187
246
  seen.add(extensionRoot);
188
247
  if (!fs.existsSync(extensionRoot)) continue;
189
248
  assertSafeExtensionRoot(extensionRoot, agentHome);
190
- const removableFiles = removableOwnedExtensionFiles(extensionOwnedUnpublishPaths(extensionRoot));
249
+ const removableFiles = removableOwnedExtensionFiles(
250
+ extensionOwnedUnpublishPaths(extensionRoot, ownedFileNames)
251
+ );
191
252
  for (const filePath of removableFiles) {
192
253
  fs.rmSync(filePath, { force: true });
193
254
  }
255
+ for (const dirName of ownedDirNames) {
256
+ const dirPath = path.join(extensionRoot, dirName);
257
+ let stat;
258
+ try {
259
+ stat = fs.lstatSync(dirPath);
260
+ } catch (err) {
261
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") continue;
262
+ throw err;
263
+ }
264
+ if (stat.isSymbolicLink()) {
265
+ throw new Error(`Extension path must not be a symlink: ${dirPath}`);
266
+ }
267
+ if (stat.isDirectory()) {
268
+ fs.rmSync(dirPath, { recursive: true, force: true });
269
+ }
270
+ }
194
271
  removeEmptyDirectory(extensionRoot);
195
272
  }
196
273
  }
@@ -200,18 +277,112 @@ var PiMemoryExtensionPublisher = class extends HostMemoryExtensionPublisher {
200
277
  super(PI_HOST);
201
278
  }
202
279
  };
280
+ var OmpPreBundleError = class extends Error {
281
+ };
203
282
  var OmpMemoryExtensionPublisher = class extends HostMemoryExtensionPublisher {
204
283
  constructor() {
205
284
  super(OMP_HOST);
206
285
  }
286
+ get ownedFileNames() {
287
+ return [...BASE_OWNED_FILES, "loader.js", "package.json", "postinstall-bundle.cjs"];
288
+ }
289
+ get ownedDirNames() {
290
+ return ["dist-bundle"];
291
+ }
292
+ // omp pre-bundles index.ts with `bun build`; the wrapper must use a relative
293
+ // import specifier (bun's bundler cannot resolve file:// URLs).
294
+ get usesBundledWrapper() {
295
+ return true;
296
+ }
297
+ finalizePublish(ctx, extensionRoot, paths) {
298
+ const bunBin = resolveBunBinary();
299
+ if (!bunBin) {
300
+ throw new OmpPreBundleError(
301
+ "Remnic omp extension requires `bun` to pre-bundle the extension: omp's embedded runtime cannot resolve bare npm specifiers from the extension's node_modules. Install bun from https://bun.sh, then re-run `remnic connectors install omp`."
302
+ );
303
+ }
304
+ const loaderPath = path.join(extensionRoot, "loader.js");
305
+ const packageJsonPath = path.join(extensionRoot, "package.json");
306
+ const postinstallPath = path.join(extensionRoot, "postinstall-bundle.cjs");
307
+ atomicWriteFile(loaderPath, renderOmpLoader(paths.pluginPiDistPath, bunBin), 420);
308
+ atomicWriteFile(postinstallPath, renderOmpPostinstall(bunBin), 420);
309
+ atomicWriteFile(packageJsonPath, renderOmpPackageJson(), 420);
310
+ try {
311
+ this.runBundleBuild(ctx, extensionRoot, bunBin);
312
+ } catch (err) {
313
+ const message = err instanceof Error ? err.message : String(err);
314
+ throw new OmpPreBundleError(message);
315
+ }
316
+ }
317
+ /**
318
+ * Pre-bundles the omp extension with `bun build` so omp's embedded runtime
319
+ * never resolves bare npm specifiers (e.g. @sinclair/typebox) from the
320
+ * extension's node_modules at load time. The bundle is written to a temp
321
+ * directory and swapped into dist-bundle/ on success. The pre-existing
322
+ * dist-bundle is renamed aside (not removed) before the swap, so a failure
323
+ * during the final rename restores the previously working bundle rather than
324
+ * leaving the install with no bundle at all.
325
+ *
326
+ * Override in tests to skip the real bun invocation.
327
+ */
328
+ runBundleBuild(ctx, extensionRoot, bunBin) {
329
+ const sourceEntry = path.join(extensionRoot, "index.ts");
330
+ const tmpOutDir = path.join(extensionRoot, `.dist-bundle.tmp-${process.pid}-${Date.now()}`);
331
+ const finalOutDir = path.join(extensionRoot, "dist-bundle");
332
+ const result = spawnSync(bunBin, ["build", sourceEntry, "--target=bun", `--outdir=${tmpOutDir}`], {
333
+ cwd: extensionRoot,
334
+ encoding: "utf-8"
335
+ });
336
+ if (result.error || result.status !== 0) {
337
+ try {
338
+ fs.rmSync(tmpOutDir, { recursive: true, force: true });
339
+ } catch {
340
+ }
341
+ const detail = (typeof result.stderr === "string" ? result.stderr.trim() : "") || (result.error instanceof Error ? result.error.message : "") || `bun exited with status ${result.status ?? "null"}`;
342
+ throw new Error(
343
+ `Remnic omp extension: bun build failed (${detail}). Resolve the error and re-run \`remnic connectors install omp\`, or build manually with \`bun build index.ts --target=bun --outdir=dist-bundle\` inside ${extensionRoot}.`
344
+ );
345
+ }
346
+ let backupDir = null;
347
+ try {
348
+ if (fs.existsSync(finalOutDir)) {
349
+ backupDir = path.join(extensionRoot, `.dist-bundle.bak-${process.pid}-${Date.now()}`);
350
+ fs.renameSync(finalOutDir, backupDir);
351
+ }
352
+ fs.renameSync(tmpOutDir, finalOutDir);
353
+ if (backupDir) {
354
+ try {
355
+ fs.rmSync(backupDir, { recursive: true, force: true });
356
+ } catch {
357
+ }
358
+ }
359
+ } catch (err) {
360
+ try {
361
+ if (fs.existsSync(tmpOutDir)) fs.rmSync(tmpOutDir, { recursive: true, force: true });
362
+ } catch {
363
+ }
364
+ if (backupDir && fs.existsSync(backupDir) && !fs.existsSync(finalOutDir)) {
365
+ try {
366
+ fs.renameSync(backupDir, finalOutDir);
367
+ } catch {
368
+ }
369
+ }
370
+ throw new Error(
371
+ `Remnic omp extension: failed to finalize bundle output \u2014 ${err instanceof Error ? err.message : String(err)}.`
372
+ );
373
+ }
374
+ ctx.log.info(`Pre-bundled omp extension into ${finalOutDir}`);
375
+ }
207
376
  };
208
- function extensionOwnedPaths(extensionRoot) {
209
- return EXTENSION_OWNED_FILES.map((fileName) => path.join(extensionRoot, fileName));
377
+ function extensionOwnedPaths(extensionRoot, ownedFileNames) {
378
+ return ownedFileNames.map((fileName) => path.join(extensionRoot, fileName));
210
379
  }
211
- function extensionOwnedUnpublishPaths(extensionRoot) {
212
- const ownedPaths = extensionOwnedPaths(extensionRoot);
380
+ function extensionOwnedUnpublishPaths(extensionRoot, ownedFileNames) {
381
+ const ownedBaseNames = new Set(ownedFileNames);
382
+ const ownedPaths = extensionOwnedPaths(extensionRoot, ownedFileNames);
213
383
  for (const fileName of fs.readdirSync(extensionRoot)) {
214
- if (EXTENSION_OWNED_TEMP_FILE_PATTERN.test(fileName)) {
384
+ const match = EXTENSION_OWNED_TEMP_FILE_SUFFIX.exec(fileName);
385
+ if (match && ownedBaseNames.has(fileName.slice(0, match.index))) {
215
386
  ownedPaths.push(path.join(extensionRoot, fileName));
216
387
  }
217
388
  }
@@ -236,15 +407,223 @@ function resolveExtensionModulePath() {
236
407
  if (fs.existsSync(source)) return source;
237
408
  return built;
238
409
  }
239
- function renderWrapper(extensionModulePath, configPath) {
240
- const moduleUrl = pathToFileURL(extensionModulePath).href;
410
+ function resolveOmpWrapperImportSpecifier(extensionModulePath, wrapperDir, pathApi = path) {
411
+ if (pathApi.parse(wrapperDir).root.toLowerCase() !== pathApi.parse(extensionModulePath).root.toLowerCase()) {
412
+ throw new Error(
413
+ `Remnic omp extension cannot pre-bundle: the extension directory (${wrapperDir}) and the @remnic/plugin-pi install (${extensionModulePath}) are on different drives, so no relative import specifier can be generated for \`bun build\` (and \`bun build\` cannot resolve a \`file://\` specifier). Move the omp agent home and the Remnic install onto the same drive.`
414
+ );
415
+ }
416
+ let rel = pathApi.relative(wrapperDir, extensionModulePath);
417
+ rel = rel.split(pathApi.sep).join("/");
418
+ return rel.startsWith(".") ? rel : `./${rel}`;
419
+ }
420
+ function renderWrapper(extensionModulePath, configPath, wrapperDir) {
421
+ let importSpecifier;
422
+ if (wrapperDir) {
423
+ importSpecifier = resolveOmpWrapperImportSpecifier(extensionModulePath, wrapperDir);
424
+ } else {
425
+ importSpecifier = pathToFileURL(extensionModulePath).href;
426
+ }
241
427
  return [
242
- `import { createRemnicPiExtension } from ${JSON.stringify(moduleUrl)};`,
428
+ `import { createRemnicPiExtension } from ${JSON.stringify(importSpecifier)};`,
243
429
  "",
244
430
  `export default createRemnicPiExtension({ configPath: ${JSON.stringify(configPath)} });`,
245
431
  ""
246
432
  ].join("\n");
247
433
  }
434
+ function renderOmpLoader(pluginPiDistPath, bunBin) {
435
+ return [
436
+ "// Auto-generated by Remnic's OmpMemoryExtensionPublisher.",
437
+ "// omp's embedded runtime cannot resolve bare npm specifiers from this",
438
+ "// extension's node_modules, so we pre-bundle with `bun build` and import",
439
+ "// the self-contained bundle here. Rebuilt automatically when index.ts or",
440
+ "// the underlying @remnic/plugin-pi dist changes.",
441
+ "",
442
+ 'import { existsSync, renameSync, rmSync, statSync } from "node:fs";',
443
+ 'import { spawnSync } from "node:child_process";',
444
+ 'import { dirname, join } from "node:path";',
445
+ 'import { fileURLToPath, pathToFileURL } from "node:url";',
446
+ "",
447
+ "const here = dirname(fileURLToPath(import.meta.url));",
448
+ 'const bundleDir = join(here, "dist-bundle");',
449
+ 'const bundleEntry = join(bundleDir, "index.js");',
450
+ 'const sourceEntry = join(here, "index.ts");',
451
+ `const pluginPiEntry = ${JSON.stringify(pluginPiDistPath)};`,
452
+ // Reuse the bun path resolved at install time (REMNIC_OMP_BUN_BIN, PATH,
453
+ // or a common absolute location). Fall back to "bun" on PATH if the
454
+ // resolved path no longer exists (e.g. the extension tree was moved), so
455
+ // self-healing still works when bun is reachable only via PATH.
456
+ `const resolvedBunBin = ${JSON.stringify(bunBin)};`,
457
+ 'const bunForRebuild = resolvedBunBin && existsSync(resolvedBunBin) ? resolvedBunBin : "bun";',
458
+ "",
459
+ "function bundleIsStale() {",
460
+ " if (!existsSync(bundleEntry)) return true;",
461
+ " const bundleMtime = statSync(bundleEntry).mtimeMs;",
462
+ " if (existsSync(sourceEntry) && bundleMtime < statSync(sourceEntry).mtimeMs) return true;",
463
+ " if (pluginPiEntry && existsSync(pluginPiEntry) && bundleMtime < statSync(pluginPiEntry).mtimeMs) return true;",
464
+ " return false;",
465
+ "}",
466
+ "",
467
+ "function rebuildBundle() {",
468
+ " // Build to a temp dir and swap, mirroring the install-time build, so a",
469
+ " // failed self-heal rebuild never corrupts the working bundle.",
470
+ ' var tmp = join(here, ".dist-bundle.tmp-" + process.pid + "-" + Date.now());',
471
+ " var result = spawnSync(bunForRebuild, [",
472
+ ' "build",',
473
+ " sourceEntry,",
474
+ ' "--target=bun",',
475
+ ' "--outdir=" + tmp',
476
+ " ], {",
477
+ " cwd: here,",
478
+ ' stdio: "inherit",',
479
+ " });",
480
+ " if (result.status !== 0 || result.error) {",
481
+ " try { rmSync(tmp, { recursive: true, force: true }); } catch (e) {}",
482
+ " throw new Error(",
483
+ ' "Remnic omp extension: bundle is stale or missing and could not be rebuilt. " +',
484
+ ' "Install bun (https://bun.sh), then run " +',
485
+ ' "`bun build index.ts --target=bun --outdir=dist-bundle` inside " + here',
486
+ " );",
487
+ " }",
488
+ " var backup = null;",
489
+ " try {",
490
+ " if (existsSync(bundleDir)) {",
491
+ ' backup = join(here, ".dist-bundle.bak-" + process.pid + "-" + Date.now());',
492
+ " renameSync(bundleDir, backup);",
493
+ " }",
494
+ " renameSync(tmp, bundleDir);",
495
+ " if (backup) { try { rmSync(backup, { recursive: true, force: true }); } catch (e) {} }",
496
+ " } catch (err) {",
497
+ " try { rmSync(tmp, { recursive: true, force: true }); } catch (e) {}",
498
+ " if (backup && existsSync(backup) && !existsSync(bundleDir)) { try { renameSync(backup, bundleDir); } catch (e) {} }",
499
+ " throw new Error(",
500
+ ' "Remnic omp extension: failed to finalize rebuilt bundle - " + (err && err.message ? err.message : err)',
501
+ " );",
502
+ " }",
503
+ "}",
504
+ "",
505
+ "if (bundleIsStale()) rebuildBundle();",
506
+ "",
507
+ "// Cache-bust so a freshly rebuilt bundle is loaded instead of a stale cached copy.",
508
+ 'const bundle = await import(pathToFileURL(bundleEntry).href + "?t=" + Date.now());',
509
+ "export default bundle.default;",
510
+ ""
511
+ ].join("\n");
512
+ }
513
+ function renderOmpPackageJson() {
514
+ const manifest = {
515
+ name: "remnic-omp-extension",
516
+ version: "0.0.0",
517
+ private: true,
518
+ type: "module",
519
+ omp: { extensions: ["./loader.js"] },
520
+ // Legacy key so older omp builds that only read `pi.extensions` also
521
+ // resolve loader.js instead of falling through to index.ts.
522
+ pi: { extensions: ["./loader.js"] },
523
+ scripts: { postinstall: "node postinstall-bundle.cjs" }
524
+ };
525
+ return `${JSON.stringify(manifest, null, 2)}
526
+ `;
527
+ }
528
+ function renderOmpPostinstall(bunBin) {
529
+ return `// Auto-generated by Remnic's OmpMemoryExtensionPublisher.
530
+ // Re-bundles the omp extension after npm install (e.g. a plugin-pi upgrade)
531
+ // using the bun path resolved at install time, with a PATH fallback. Node-only
532
+ // so it runs under npm's default cmd.exe shell on Windows as well as POSIX bash.
533
+ "use strict";
534
+ var fs = require("node:fs");
535
+ var cp = require("node:child_process");
536
+ var path = require("node:path");
537
+
538
+ var RESOLVED_BUN = ${JSON.stringify(bunBin)};
539
+ var dir = __dirname;
540
+ var entry = path.join(dir, "index.ts");
541
+ var out = path.join(dir, "dist-bundle");
542
+
543
+ function pickBun() {
544
+ var env = process.env.REMNIC_OMP_BUN_BIN;
545
+ if (env && fs.existsSync(env)) return env;
546
+ if (RESOLVED_BUN && fs.existsSync(RESOLVED_BUN)) return RESOLVED_BUN;
547
+ return "bun";
548
+ }
549
+
550
+ function rebuild() {
551
+ var bun = pickBun();
552
+ var tmp = path.join(dir, ".dist-bundle.tmp-" + process.pid + "-" + Date.now());
553
+ var r = cp.spawnSync(bun, ["build", entry, "--target=bun", "--outdir=" + tmp], { cwd: dir, stdio: "inherit" });
554
+ if (r.error || r.status !== 0) {
555
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (e) {}
556
+ throw new Error("Remnic omp extension: postinstall bun build failed (bun=" + bun + "). Run bun build index.ts --target=bun --outdir=dist-bundle manually inside " + dir);
557
+ }
558
+ var backup = null;
559
+ try {
560
+ if (fs.existsSync(out)) {
561
+ backup = path.join(dir, ".dist-bundle.bak-" + process.pid + "-" + Date.now());
562
+ fs.renameSync(out, backup);
563
+ }
564
+ fs.renameSync(tmp, out);
565
+ if (backup) { try { fs.rmSync(backup, { recursive: true, force: true }); } catch (e) {} }
566
+ } catch (err) {
567
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (e) {}
568
+ if (backup && fs.existsSync(backup) && !fs.existsSync(out)) { try { fs.renameSync(backup, out); } catch (e) {} }
569
+ throw err;
570
+ }
571
+ }
572
+
573
+ try {
574
+ rebuild();
575
+ } catch (err) {
576
+ console.error(err && err.message ? err.message : err);
577
+ process.exit(1);
578
+ }
579
+ `;
580
+ }
581
+ function isExecutableFile(candidate) {
582
+ try {
583
+ const stat = fs.statSync(candidate);
584
+ if (!stat.isFile()) return false;
585
+ fs.accessSync(candidate, fs.constants.X_OK);
586
+ return true;
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+ function resolveBunOnPath() {
592
+ const pathVar = process.env.PATH ?? process.env.Path ?? process.env.path ?? "";
593
+ const separator = process.platform === "win32" ? ";" : ":";
594
+ const candidateNames = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
595
+ for (const dir of pathVar.split(separator)) {
596
+ if (!dir) continue;
597
+ for (const name of candidateNames) {
598
+ const candidate = path.isAbsolute(dir) ? path.join(dir, name) : path.resolve(dir, name);
599
+ if (isExecutableFile(candidate)) {
600
+ return fs.realpathSync(candidate);
601
+ }
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ function resolveBunBinary() {
607
+ const override = process.env.REMNIC_OMP_BUN_BIN;
608
+ if (override !== void 0) {
609
+ return fs.existsSync(override) ? override : null;
610
+ }
611
+ const pathProbe = spawnSync("bun", ["--version"], { encoding: "utf-8" });
612
+ if (!pathProbe.error && pathProbe.status === 0) {
613
+ return resolveBunOnPath() ?? "bun";
614
+ }
615
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
616
+ const candidates = [
617
+ path.join(home ?? "", ".bun", "bin", "bun"),
618
+ path.join(home ?? "", ".bun", "bin", "bun.exe"),
619
+ "/usr/local/bin/bun",
620
+ "/opt/homebrew/bin/bun"
621
+ ];
622
+ for (const candidate of candidates) {
623
+ if (isExecutableFile(candidate)) return candidate;
624
+ }
625
+ return null;
626
+ }
248
627
  function atomicWriteFile(filePath, content, mode) {
249
628
  rejectSymlinkPath(filePath);
250
629
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
@@ -300,6 +679,34 @@ function restorePublishSnapshot(extensionRoot, rootExisted, snapshots) {
300
679
  removeEmptyDirectory(extensionRoot);
301
680
  }
302
681
  }
682
+ function snapshotDirs(paths) {
683
+ return paths.map((dirPath) => {
684
+ let existed = false;
685
+ try {
686
+ const stat = fs.lstatSync(dirPath);
687
+ if (stat.isSymbolicLink()) {
688
+ throw new Error(`Extension path must not be a symlink: ${dirPath}`);
689
+ }
690
+ existed = stat.isDirectory();
691
+ } catch (err) {
692
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
693
+ existed = false;
694
+ } else {
695
+ throw err;
696
+ }
697
+ }
698
+ return { path: dirPath, existed };
699
+ });
700
+ }
701
+ function restoreDirSnapshots(snapshots) {
702
+ for (const snapshot of snapshots) {
703
+ if (snapshot.existed) continue;
704
+ try {
705
+ fs.rmSync(snapshot.path, { recursive: true, force: true });
706
+ } catch {
707
+ }
708
+ }
709
+ }
303
710
  function canCleanNewExtensionRoot(extensionRoot) {
304
711
  let stat;
305
712
  try {
@@ -417,6 +824,9 @@ function restoreTokenEntry(priorEntry, connectorId) {
417
824
  export {
418
825
  HostMemoryExtensionPublisher,
419
826
  OmpMemoryExtensionPublisher,
420
- PiMemoryExtensionPublisher
827
+ PiMemoryExtensionPublisher,
828
+ resolveBunBinary,
829
+ resolveBunOnPath,
830
+ resolveOmpWrapperImportSpecifier
421
831
  };
422
832
  //# sourceMappingURL=publisher.js.map