@remnic/plugin-pi 9.3.703 → 9.3.705
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/dist/{chunk-A5WP5NTC.js → chunk-ASGQGBO2.js} +2 -2
- package/dist/chunk-ASGQGBO2.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/publisher.d.ts +87 -1
- package/dist/publisher.js +433 -23
- package/dist/publisher.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-A5WP5NTC.js.map +0 -1
package/dist/publisher.d.ts
CHANGED
|
@@ -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-
|
|
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
|
|
20
|
-
var
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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(
|
|
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
|
|
377
|
+
function extensionOwnedPaths(extensionRoot, ownedFileNames) {
|
|
378
|
+
return ownedFileNames.map((fileName) => path.join(extensionRoot, fileName));
|
|
210
379
|
}
|
|
211
|
-
function extensionOwnedUnpublishPaths(extensionRoot) {
|
|
212
|
-
const
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
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(
|
|
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
|