@vellumai/assistant 0.11.6-dev.202608271738.c4a1b33 → 0.11.6-dev.202608271936.fef1c19
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/package.json +1 -1
- package/src/cli/lib/install-from-github.ts +18 -19
- package/src/cli/lib/install-plugin-dependencies.ts +2 -5
- package/src/config/feature-flag-registry.json +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/util/bun-runtime.ts +43 -13
- package/src/util/platform.ts +65 -1
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/workspace/git-service.ts +2 -12
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
writeFileSync,
|
|
37
37
|
} from "node:fs";
|
|
38
38
|
import { tmpdir } from "node:os";
|
|
39
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
39
|
+
import { delimiter, dirname, join, resolve, sep } from "node:path";
|
|
40
40
|
import { promisify } from "node:util";
|
|
41
41
|
|
|
42
42
|
import {
|
|
@@ -46,7 +46,12 @@ import {
|
|
|
46
46
|
stripPreservedUserState,
|
|
47
47
|
} from "../../plugins/plugin-tree-walk.js";
|
|
48
48
|
import { ensureBun } from "../../util/bun-runtime.js";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
addToPathEnv,
|
|
51
|
+
getExtraToolPathDirs,
|
|
52
|
+
getWorkspacePluginsDir,
|
|
53
|
+
isWindows,
|
|
54
|
+
} from "../../util/platform.js";
|
|
50
55
|
import type { FetchLike } from "./fetch-like.js";
|
|
51
56
|
import {
|
|
52
57
|
type DependencyInstaller,
|
|
@@ -1258,19 +1263,18 @@ export const defaultPostinstallRunner: PostinstallRunner = async ({
|
|
|
1258
1263
|
};
|
|
1259
1264
|
|
|
1260
1265
|
function pluginPostinstallEnv(bun: string): NodeJS.ProcessEnv {
|
|
1266
|
+
const systemDirs = isWindows()
|
|
1267
|
+
? [process.env.SystemRoot ?? "C:\\Windows"]
|
|
1268
|
+
: [...getExtraToolPathDirs(), "/usr/bin", "/bin"];
|
|
1261
1269
|
const env: NodeJS.ProcessEnv = {
|
|
1262
|
-
PATH: [
|
|
1263
|
-
dirname(bun),
|
|
1264
|
-
"/opt/homebrew/bin",
|
|
1265
|
-
"/usr/local/bin",
|
|
1266
|
-
"/usr/bin",
|
|
1267
|
-
"/bin",
|
|
1268
|
-
]
|
|
1269
|
-
.filter(Boolean)
|
|
1270
|
-
.join(":"),
|
|
1270
|
+
PATH: [dirname(bun), ...systemDirs].join(delimiter),
|
|
1271
1271
|
};
|
|
1272
|
-
|
|
1273
|
-
|
|
1272
|
+
for (const key of isWindows()
|
|
1273
|
+
? ["USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "SystemRoot"]
|
|
1274
|
+
: ["HOME"]) {
|
|
1275
|
+
if (process.env[key]) {
|
|
1276
|
+
env[key] = process.env[key];
|
|
1277
|
+
}
|
|
1274
1278
|
}
|
|
1275
1279
|
return env;
|
|
1276
1280
|
}
|
|
@@ -1385,12 +1389,7 @@ function pluginGitEnv(): NodeJS.ProcessEnv {
|
|
|
1385
1389
|
}
|
|
1386
1390
|
}
|
|
1387
1391
|
env.GIT_TERMINAL_PROMPT = "0";
|
|
1388
|
-
|
|
1389
|
-
const current = (env.PATH ?? "").split(":").filter(Boolean);
|
|
1390
|
-
const missing = extraPaths.filter((p) => !current.includes(p));
|
|
1391
|
-
if (missing.length > 0) {
|
|
1392
|
-
env.PATH = [...current, ...missing].join(":");
|
|
1393
|
-
}
|
|
1392
|
+
addToPathEnv(env, getExtraToolPathDirs(), "back");
|
|
1394
1393
|
return env;
|
|
1395
1394
|
}
|
|
1396
1395
|
|
|
@@ -59,6 +59,7 @@ import { promisify } from "node:util";
|
|
|
59
59
|
|
|
60
60
|
import { ensureBun } from "../../util/bun-runtime.js";
|
|
61
61
|
import { getLogger } from "../../util/logger.js";
|
|
62
|
+
import { addToPathEnv } from "../../util/platform.js";
|
|
62
63
|
|
|
63
64
|
const execFileAsync = promisify(execFile);
|
|
64
65
|
|
|
@@ -331,10 +332,6 @@ export const defaultDependencyInstaller: DependencyInstaller = async ({
|
|
|
331
332
|
*/
|
|
332
333
|
function dependencyInstallEnv(bun: string): NodeJS.ProcessEnv {
|
|
333
334
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
334
|
-
|
|
335
|
-
const current = (env.PATH ?? "").split(":").filter(Boolean);
|
|
336
|
-
if (!current.includes(bunDir)) {
|
|
337
|
-
env.PATH = [bunDir, ...current].join(":");
|
|
338
|
-
}
|
|
335
|
+
addToPathEnv(env, [dirname(bun)]);
|
|
339
336
|
return env;
|
|
340
337
|
}
|
|
@@ -412,6 +412,14 @@
|
|
|
412
412
|
"label": "monday.com Integration",
|
|
413
413
|
"description": "Gates the seeded monday.com OAuth provider. When off, the provider is hidden from GET /v1/oauth/providers, its get-by-id route, and the connect/update routes, so it cannot be listed or connected from the CLI, gateway, or web integrations list. The row is still seeded into oauth_providers on startup; only its visibility changes.",
|
|
414
414
|
"defaultEnabled": false
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"id": "chatgpt-device-code-login",
|
|
418
|
+
"scope": "client",
|
|
419
|
+
"key": "chatgpt-device-code-login",
|
|
420
|
+
"label": "ChatGPT Device Code Login",
|
|
421
|
+
"description": "Gates the device-code sign-in as the primary ChatGPT subscription connect flow in the web client. Off: the redirect-and-paste flow is the only flow the connect section shows, with no device code UI and no other-sign-in-options disclosure. On: device code leads, and the redirect-and-paste flow stays reachable behind that disclosure and takes the section over when the assistant has no device-auth route.",
|
|
422
|
+
"defaultEnabled": false
|
|
415
423
|
}
|
|
416
424
|
]
|
|
417
425
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
beforeEach,
|
|
4
4
|
describe,
|
|
5
5
|
expect,
|
|
6
|
+
jest,
|
|
6
7
|
mock,
|
|
7
8
|
setSystemTime,
|
|
8
9
|
test,
|
|
@@ -66,14 +67,30 @@ function makeQueue(
|
|
|
66
67
|
return queue;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
const FROZEN_NOW = new Date("2026-01-01T00:00:00.000Z").getTime();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Flush already-queued microtasks. Fake timers stop the clock but not the
|
|
74
|
+
* promise chain `enqueue` appends onto, so the PATCH work a timer kicks off
|
|
75
|
+
* lands here.
|
|
76
|
+
*/
|
|
69
77
|
async function settle(): Promise<void> {
|
|
70
|
-
for (let i = 0; i <
|
|
71
|
-
await
|
|
78
|
+
for (let i = 0; i < 20; i += 1) {
|
|
79
|
+
await Promise.resolve();
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/** Advance `Date.now()` and the queue's `setTimeout` handles together. */
|
|
84
|
+
async function elapse(ms: number): Promise<void> {
|
|
85
|
+
setSystemTime(new Date(Date.now() + ms));
|
|
86
|
+
jest.advanceTimersByTime(ms);
|
|
87
|
+
await settle();
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
describe("createPlatformPatchQueue", () => {
|
|
76
91
|
beforeEach(() => {
|
|
92
|
+
jest.useFakeTimers();
|
|
93
|
+
setSystemTime(new Date(FROZEN_NOW));
|
|
77
94
|
patches = [];
|
|
78
95
|
builds = 0;
|
|
79
96
|
respond = () => new Response("{}", { status: 200 });
|
|
@@ -85,6 +102,8 @@ describe("createPlatformPatchQueue", () => {
|
|
|
85
102
|
for (const queue of queues) {
|
|
86
103
|
queue.dispose();
|
|
87
104
|
}
|
|
105
|
+
jest.useRealTimers();
|
|
106
|
+
setSystemTime();
|
|
88
107
|
});
|
|
89
108
|
|
|
90
109
|
test("PATCHes once and dedups an unchanged key", async () => {
|
|
@@ -203,21 +222,16 @@ describe("createPlatformPatchQueue", () => {
|
|
|
203
222
|
saveSyncedKey: (synced) => saved.push(synced),
|
|
204
223
|
});
|
|
205
224
|
const start = Date.now();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
queue.enqueue("a");
|
|
217
|
-
await settle();
|
|
218
|
-
} finally {
|
|
219
|
-
setSystemTime();
|
|
220
|
-
}
|
|
225
|
+
queue.enqueue("a");
|
|
226
|
+
await settle();
|
|
227
|
+
setSystemTime(new Date(start + 999));
|
|
228
|
+
queue.enqueue("a");
|
|
229
|
+
await settle();
|
|
230
|
+
setSystemTime(new Date(start + 1000));
|
|
231
|
+
queue.enqueue("a");
|
|
232
|
+
await settle();
|
|
233
|
+
queue.enqueue("a");
|
|
234
|
+
await settle();
|
|
221
235
|
|
|
222
236
|
expect(patches).toHaveLength(2);
|
|
223
237
|
expect(saved.map((s) => s.syncedAt)).toEqual([start, start + 1000]);
|
|
@@ -243,14 +257,12 @@ describe("createPlatformPatchQueue", () => {
|
|
|
243
257
|
await settle();
|
|
244
258
|
expect(patches).toHaveLength(1);
|
|
245
259
|
|
|
246
|
-
await
|
|
247
|
-
await settle();
|
|
260
|
+
await elapse(300);
|
|
248
261
|
expect(patches).toHaveLength(2);
|
|
249
262
|
expect(builds).toBe(2);
|
|
250
263
|
|
|
251
264
|
queue.dispose();
|
|
252
|
-
await
|
|
253
|
-
await settle();
|
|
265
|
+
await elapse(300);
|
|
254
266
|
expect(patches).toHaveLength(2);
|
|
255
267
|
});
|
|
256
268
|
|
|
@@ -266,8 +278,7 @@ describe("createPlatformPatchQueue", () => {
|
|
|
266
278
|
await settle();
|
|
267
279
|
expect(patches).toHaveLength(0);
|
|
268
280
|
|
|
269
|
-
await
|
|
270
|
-
await settle();
|
|
281
|
+
await elapse(300);
|
|
271
282
|
expect(patches).toHaveLength(1);
|
|
272
283
|
});
|
|
273
284
|
|
|
@@ -279,12 +290,10 @@ describe("createPlatformPatchQueue", () => {
|
|
|
279
290
|
expect(patches).toHaveLength(1);
|
|
280
291
|
|
|
281
292
|
respond = () => new Response("{}", { status: 200 });
|
|
282
|
-
await
|
|
283
|
-
await settle();
|
|
293
|
+
await elapse(100);
|
|
284
294
|
expect(patches).toHaveLength(2);
|
|
285
295
|
|
|
286
|
-
await
|
|
287
|
-
await settle();
|
|
296
|
+
await elapse(100);
|
|
288
297
|
queue.enqueue("a");
|
|
289
298
|
await settle();
|
|
290
299
|
expect(patches).toHaveLength(2);
|
|
@@ -301,8 +310,7 @@ describe("createPlatformPatchQueue", () => {
|
|
|
301
310
|
queue.enqueue("a");
|
|
302
311
|
await settle();
|
|
303
312
|
mockClient = makeClient();
|
|
304
|
-
await
|
|
305
|
-
await settle();
|
|
313
|
+
await elapse(100);
|
|
306
314
|
|
|
307
315
|
expect(patches).toHaveLength(1);
|
|
308
316
|
});
|
|
@@ -312,14 +320,11 @@ describe("createPlatformPatchQueue", () => {
|
|
|
312
320
|
respond = () => new Response("nope", { status: 500 });
|
|
313
321
|
queue.enqueue("a");
|
|
314
322
|
await settle();
|
|
315
|
-
await
|
|
316
|
-
await
|
|
317
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
318
|
-
await settle();
|
|
323
|
+
await elapse(50);
|
|
324
|
+
await elapse(50);
|
|
319
325
|
expect(patches).toHaveLength(3);
|
|
320
326
|
|
|
321
|
-
await
|
|
322
|
-
await settle();
|
|
327
|
+
await elapse(100);
|
|
323
328
|
expect(patches).toHaveLength(3);
|
|
324
329
|
});
|
|
325
330
|
|
|
@@ -332,12 +337,10 @@ describe("createPlatformPatchQueue", () => {
|
|
|
332
337
|
await settle();
|
|
333
338
|
expect(patches.map((p) => p.body.value)).toEqual(["a", "b"]);
|
|
334
339
|
|
|
335
|
-
await
|
|
336
|
-
await settle();
|
|
340
|
+
await elapse(100);
|
|
337
341
|
expect(patches.map((p) => p.body.value)).toEqual(["a", "b", "b"]);
|
|
338
342
|
|
|
339
|
-
await
|
|
340
|
-
await settle();
|
|
343
|
+
await elapse(100);
|
|
341
344
|
expect(patches).toHaveLength(3);
|
|
342
345
|
});
|
|
343
346
|
|
|
@@ -347,8 +350,7 @@ describe("createPlatformPatchQueue", () => {
|
|
|
347
350
|
queue.enqueue("a");
|
|
348
351
|
await settle();
|
|
349
352
|
queue.dispose();
|
|
350
|
-
await
|
|
351
|
-
await settle();
|
|
353
|
+
await elapse(300);
|
|
352
354
|
|
|
353
355
|
expect(patches).toHaveLength(1);
|
|
354
356
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
_listSharedAppsForTests,
|
|
8
|
+
_resolveSharedAppDirForTests,
|
|
9
|
+
} from "../app-management-routes.js";
|
|
10
|
+
|
|
11
|
+
let rootDir: string;
|
|
12
|
+
let canonicalDir: string;
|
|
13
|
+
let legacyDir: string;
|
|
14
|
+
|
|
15
|
+
function writeSharedApp(dir: string, uuid: string, name: string): void {
|
|
16
|
+
mkdirSync(join(dir, uuid), { recursive: true });
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(dir, `${uuid}-meta.json`),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
uuid,
|
|
21
|
+
name,
|
|
22
|
+
entry: "index.html",
|
|
23
|
+
trustTier: "verified",
|
|
24
|
+
installedAt: "2026-01-01T00:00:00.000Z",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(dir, uuid, "manifest.json"),
|
|
29
|
+
JSON.stringify({ version: "1.0.0", content_id: `content-${uuid}` }),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
rootDir = mkdtempSync(join(tmpdir(), "shared-app-roots-"));
|
|
35
|
+
canonicalDir = join(rootDir, "canonical");
|
|
36
|
+
legacyDir = join(rootDir, "legacy");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("shared app roots", () => {
|
|
44
|
+
test("lists canonical and legacy apps after the canonical directory exists", () => {
|
|
45
|
+
writeSharedApp(canonicalDir, "app-new", "New App");
|
|
46
|
+
writeSharedApp(legacyDir, "app-legacy", "Legacy App");
|
|
47
|
+
|
|
48
|
+
const apps = _listSharedAppsForTests([canonicalDir, legacyDir]);
|
|
49
|
+
|
|
50
|
+
expect(apps.map((app) => app.uuid)).toEqual(["app-new", "app-legacy"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("prefers the canonical copy when both roots contain the same app", () => {
|
|
54
|
+
writeSharedApp(canonicalDir, "app-shared", "Canonical App");
|
|
55
|
+
writeSharedApp(legacyDir, "app-shared", "Legacy App");
|
|
56
|
+
|
|
57
|
+
const apps = _listSharedAppsForTests([canonicalDir, legacyDir]);
|
|
58
|
+
|
|
59
|
+
expect(apps).toHaveLength(1);
|
|
60
|
+
expect(apps[0]?.name).toBe("Canonical App");
|
|
61
|
+
expect(
|
|
62
|
+
_resolveSharedAppDirForTests("app-shared", [canonicalDir, legacyDir]),
|
|
63
|
+
).toBe(canonicalDir);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("resolves a legacy app when it is absent from the canonical root", () => {
|
|
67
|
+
mkdirSync(canonicalDir, { recursive: true });
|
|
68
|
+
writeSharedApp(legacyDir, "app-legacy", "Legacy App");
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
_resolveSharedAppDirForTests("app-legacy", [canonicalDir, legacyDir]),
|
|
72
|
+
).toBe(legacyDir);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -54,6 +54,7 @@ import { verifyBundleSignature } from "../../bundler/signature-verifier.js";
|
|
|
54
54
|
import { compareSemver } from "../../daemon/handlers/shared.js";
|
|
55
55
|
import { computeContentId } from "../../util/content-id.js";
|
|
56
56
|
import { getLogger } from "../../util/logger.js";
|
|
57
|
+
import { getUserAppDataDir } from "../../util/platform.js";
|
|
57
58
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
58
59
|
import {
|
|
59
60
|
getOriginClientId,
|
|
@@ -73,14 +74,26 @@ const log = getLogger("app-management-routes");
|
|
|
73
74
|
// Helpers
|
|
74
75
|
// ---------------------------------------------------------------------------
|
|
75
76
|
|
|
76
|
-
function
|
|
77
|
-
|
|
77
|
+
function getSharedAppsDirs(): string[] {
|
|
78
|
+
const canonical = join(
|
|
79
|
+
getUserAppDataDir(),
|
|
80
|
+
"vellum-assistant",
|
|
81
|
+
"shared-apps",
|
|
82
|
+
);
|
|
83
|
+
const legacy = join(
|
|
78
84
|
homedir(),
|
|
79
85
|
"Library",
|
|
80
86
|
"Application Support",
|
|
81
87
|
"vellum-assistant",
|
|
82
88
|
"shared-apps",
|
|
83
89
|
);
|
|
90
|
+
return legacy === canonical ? [canonical] : [canonical, legacy];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveSharedAppDir(appUuid: string, dirs: string[]): string | null {
|
|
94
|
+
return (
|
|
95
|
+
dirs.find((dir) => existsSync(join(dir, `${appUuid}-meta.json`))) ?? null
|
|
96
|
+
);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
// ---------------------------------------------------------------------------
|
|
@@ -218,13 +231,9 @@ function getAppDataResult(
|
|
|
218
231
|
}
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
function listSharedApps(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return [];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const files = readdirSync(dir).filter((f) => f.endsWith("-meta.json"));
|
|
234
|
+
function listSharedApps(
|
|
235
|
+
dirs = getSharedAppsDirs(),
|
|
236
|
+
): Array<Record<string, unknown>> {
|
|
228
237
|
const apps: Array<{
|
|
229
238
|
uuid: string;
|
|
230
239
|
name: string;
|
|
@@ -240,43 +249,54 @@ function listSharedApps(): Array<Record<string, unknown>> {
|
|
|
240
249
|
contentId?: string;
|
|
241
250
|
forked?: boolean;
|
|
242
251
|
}> = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
243
253
|
|
|
244
|
-
for (const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const manifest = JSON.parse(manifestRaw);
|
|
256
|
-
version = manifest.version;
|
|
257
|
-
contentId = manifest.content_id;
|
|
258
|
-
} catch {
|
|
259
|
-
// ignore malformed manifest
|
|
254
|
+
for (const dir of dirs) {
|
|
255
|
+
if (!existsSync(dir)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const files = readdirSync(dir).filter((f) => f.endsWith("-meta.json"));
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
try {
|
|
261
|
+
const raw = readFileSync(join(dir, file), "utf-8");
|
|
262
|
+
const meta = JSON.parse(raw);
|
|
263
|
+
if (seen.has(meta.uuid)) {
|
|
264
|
+
continue;
|
|
260
265
|
}
|
|
261
|
-
}
|
|
262
266
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
267
|
+
let version: string | undefined;
|
|
268
|
+
let contentId: string | undefined;
|
|
269
|
+
const manifestPath = join(dir, meta.uuid, "manifest.json");
|
|
270
|
+
if (existsSync(manifestPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const manifestRaw = readFileSync(manifestPath, "utf-8");
|
|
273
|
+
const manifest = JSON.parse(manifestRaw);
|
|
274
|
+
version = manifest.version;
|
|
275
|
+
contentId = manifest.content_id;
|
|
276
|
+
} catch {
|
|
277
|
+
// ignore malformed manifest
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
apps.push({
|
|
282
|
+
uuid: meta.uuid,
|
|
283
|
+
name: meta.name,
|
|
284
|
+
description: meta.description,
|
|
285
|
+
icon: meta.icon,
|
|
286
|
+
preview: meta.preview,
|
|
287
|
+
entry: meta.entry,
|
|
288
|
+
trustTier: meta.trustTier,
|
|
289
|
+
signerDisplayName: meta.signerDisplayName,
|
|
290
|
+
bundleSizeBytes: meta.bundleSizeBytes ?? 0,
|
|
291
|
+
installedAt: meta.installedAt,
|
|
292
|
+
version,
|
|
293
|
+
contentId,
|
|
294
|
+
forked: meta.forked,
|
|
295
|
+
});
|
|
296
|
+
seen.add(meta.uuid);
|
|
297
|
+
} catch {
|
|
298
|
+
log.warn({ file }, "Failed to read shared app metadata file");
|
|
299
|
+
}
|
|
280
300
|
}
|
|
281
301
|
}
|
|
282
302
|
|
|
@@ -314,6 +334,7 @@ function listSharedApps(): Array<Record<string, unknown>> {
|
|
|
314
334
|
|
|
315
335
|
function forkSharedApp(
|
|
316
336
|
appUuid: string,
|
|
337
|
+
dirs = getSharedAppsDirs(),
|
|
317
338
|
):
|
|
318
339
|
| { success: true; appId: string; name: string }
|
|
319
340
|
| { success: false; error: string } {
|
|
@@ -326,12 +347,11 @@ function forkSharedApp(
|
|
|
326
347
|
return { success: false, error: "Invalid UUID" };
|
|
327
348
|
}
|
|
328
349
|
|
|
329
|
-
const dir =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!existsSync(metaFile)) {
|
|
350
|
+
const dir = resolveSharedAppDir(appUuid, dirs);
|
|
351
|
+
if (!dir) {
|
|
333
352
|
return { success: false, error: "Shared app not found" };
|
|
334
353
|
}
|
|
354
|
+
const metaFile = join(dir, `${appUuid}-meta.json`);
|
|
335
355
|
|
|
336
356
|
const metaRaw = readFileSync(metaFile, "utf-8");
|
|
337
357
|
const meta = JSON.parse(metaRaw);
|
|
@@ -392,6 +412,19 @@ function forkSharedApp(
|
|
|
392
412
|
return { success: true, appId: newApp.id, name: newApp.name };
|
|
393
413
|
}
|
|
394
414
|
|
|
415
|
+
export function _listSharedAppsForTests(
|
|
416
|
+
dirs: string[],
|
|
417
|
+
): Array<Record<string, unknown>> {
|
|
418
|
+
return listSharedApps(dirs);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function _resolveSharedAppDirForTests(
|
|
422
|
+
appUuid: string,
|
|
423
|
+
dirs: string[],
|
|
424
|
+
): string | null {
|
|
425
|
+
return resolveSharedAppDir(appUuid, dirs);
|
|
426
|
+
}
|
|
427
|
+
|
|
395
428
|
async function openBundle(filePath: string): Promise<Record<string, unknown>> {
|
|
396
429
|
const fileStat = await stat(filePath);
|
|
397
430
|
const bundleSizeBytes = fileStat.size;
|
package/src/util/bun-runtime.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { arch, homedir, platform } from "node:os";
|
|
|
22
22
|
import { basename, join } from "node:path";
|
|
23
23
|
|
|
24
24
|
import { getLogger } from "./logger.js";
|
|
25
|
-
import { getBinDir } from "./platform.js";
|
|
25
|
+
import { getBinDir, isWindows } from "./platform.js";
|
|
26
26
|
|
|
27
27
|
const log = getLogger("bun-runtime");
|
|
28
28
|
|
|
@@ -81,19 +81,31 @@ export function findBun(): string | undefined {
|
|
|
81
81
|
return process.execPath;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const exe = isWindows() ? "bun.exe" : "bun";
|
|
85
|
+
|
|
84
86
|
// 2. Previously downloaded copy
|
|
85
|
-
const downloaded = join(getBinDir(),
|
|
87
|
+
const downloaded = join(getBinDir(), exe);
|
|
86
88
|
if (existsSync(downloaded)) {
|
|
87
89
|
return downloaded;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// 3. Common install locations
|
|
91
93
|
const home = homedir();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const candidates = isWindows()
|
|
95
|
+
? [
|
|
96
|
+
join(home, ".bun", "bin", exe),
|
|
97
|
+
join(
|
|
98
|
+
process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"),
|
|
99
|
+
"bun",
|
|
100
|
+
exe,
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
: [
|
|
104
|
+
join(home, ".bun", "bin", exe),
|
|
105
|
+
"/opt/homebrew/bin/bun",
|
|
106
|
+
"/usr/local/bin/bun",
|
|
107
|
+
];
|
|
108
|
+
for (const p of candidates) {
|
|
97
109
|
if (existsSync(p)) {
|
|
98
110
|
return p;
|
|
99
111
|
}
|
|
@@ -108,13 +120,20 @@ export function findBun(): string | undefined {
|
|
|
108
120
|
return undefined;
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
/** Escape a string for a PowerShell single-quoted literal. */
|
|
124
|
+
function psQuote(value: string): string {
|
|
125
|
+
return value.replace(/'/g, "''");
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
/**
|
|
112
129
|
* Download a pinned bun release into the given directory.
|
|
113
130
|
* Returns the absolute path to the downloaded binary.
|
|
114
131
|
*/
|
|
115
132
|
async function downloadBun(installDir: string): Promise<string> {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
133
|
+
const win = isWindows();
|
|
134
|
+
const os = win ? "windows" : platform();
|
|
135
|
+
// Bun ships no Windows ARM64 build; the x64 one runs under emulation.
|
|
136
|
+
const cpu = win ? "x64" : arch() === "arm64" ? "aarch64" : arch();
|
|
118
137
|
const target = `${os}-${cpu}`;
|
|
119
138
|
const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-${target}.zip`;
|
|
120
139
|
|
|
@@ -135,7 +154,15 @@ async function downloadBun(installDir: string): Promise<string> {
|
|
|
135
154
|
|
|
136
155
|
try {
|
|
137
156
|
const proc = Bun.spawn({
|
|
138
|
-
cmd:
|
|
157
|
+
cmd: win
|
|
158
|
+
? [
|
|
159
|
+
"powershell.exe",
|
|
160
|
+
"-NoProfile",
|
|
161
|
+
"-NonInteractive",
|
|
162
|
+
"-Command",
|
|
163
|
+
`Expand-Archive -LiteralPath '${psQuote(tmpZip)}' -DestinationPath '${psQuote(installDir)}' -Force`,
|
|
164
|
+
]
|
|
165
|
+
: ["unzip", "-o", tmpZip, "-d", installDir],
|
|
139
166
|
stdout: "ignore",
|
|
140
167
|
stderr: "pipe",
|
|
141
168
|
});
|
|
@@ -146,8 +173,9 @@ async function downloadBun(installDir: string): Promise<string> {
|
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
// Move binary from bun-{target}/bun to installDir/bun
|
|
149
|
-
const
|
|
150
|
-
const
|
|
176
|
+
const exe = win ? "bun.exe" : "bun";
|
|
177
|
+
const extractedBun = join(installDir, `bun-${target}`, exe);
|
|
178
|
+
const finalPath = join(installDir, exe);
|
|
151
179
|
if (existsSync(extractedBun)) {
|
|
152
180
|
// Remove old binary first to avoid "Text file busy" on some systems
|
|
153
181
|
if (existsSync(finalPath)) {
|
|
@@ -164,7 +192,9 @@ async function downloadBun(installDir: string): Promise<string> {
|
|
|
164
192
|
);
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
|
|
195
|
+
if (!win) {
|
|
196
|
+
chmodSync(finalPath, 0o755);
|
|
197
|
+
}
|
|
168
198
|
|
|
169
199
|
log.info({ path: finalPath }, "Bun binary downloaded successfully");
|
|
170
200
|
return finalPath;
|
package/src/util/platform.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
basename,
|
|
5
|
+
delimiter,
|
|
6
|
+
dirname,
|
|
7
|
+
isAbsolute,
|
|
8
|
+
join,
|
|
9
|
+
relative,
|
|
10
|
+
sep,
|
|
11
|
+
} from "node:path";
|
|
4
12
|
|
|
5
13
|
import {
|
|
6
14
|
AVATAR_IMAGE_FILENAME,
|
|
@@ -54,6 +62,62 @@ export function isWindows(): boolean {
|
|
|
54
62
|
return process.platform === "win32";
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Per-user application data root: `~/Library/Application Support` on macOS,
|
|
67
|
+
* `%APPDATA%` on Windows, `$XDG_DATA_HOME` (default `~/.local/share`) elsewhere.
|
|
68
|
+
*/
|
|
69
|
+
export function getUserAppDataDir(): string {
|
|
70
|
+
if (isMacOS()) {
|
|
71
|
+
return join(homedir(), "Library", "Application Support");
|
|
72
|
+
}
|
|
73
|
+
if (isWindows()) {
|
|
74
|
+
return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
75
|
+
}
|
|
76
|
+
return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extra directories to prepend to PATH so daemon-spawned tools are found
|
|
81
|
+
* even when launched from a minimal environment (macOS .app bundle). Empty
|
|
82
|
+
* on Windows, where PATH already carries the installer-managed entries.
|
|
83
|
+
*/
|
|
84
|
+
export function getExtraToolPathDirs(): string[] {
|
|
85
|
+
if (isWindows()) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
"/opt/homebrew/bin",
|
|
90
|
+
"/usr/local/bin",
|
|
91
|
+
join(homedir(), ".local", "bin"),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add missing `dirs` to the PATH entry of `env` in place, using the platform
|
|
97
|
+
* delimiter. Windows keys the variable as `Path`, so the existing key is
|
|
98
|
+
* matched case-insensitively rather than assuming `PATH`.
|
|
99
|
+
*/
|
|
100
|
+
export function addToPathEnv(
|
|
101
|
+
env: Record<string, string | undefined>,
|
|
102
|
+
dirs: string[],
|
|
103
|
+
position: "front" | "back" = "front",
|
|
104
|
+
): void {
|
|
105
|
+
if (dirs.length === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const key =
|
|
109
|
+
Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
110
|
+
const entries = (env[key] ?? "").split(delimiter).filter(Boolean);
|
|
111
|
+
const missing = dirs.filter((d) => !entries.includes(d));
|
|
112
|
+
if (missing.length === 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
env[key] =
|
|
116
|
+
position === "front"
|
|
117
|
+
? [...missing, ...entries].join(delimiter)
|
|
118
|
+
: [...entries, ...missing].join(delimiter);
|
|
119
|
+
}
|
|
120
|
+
|
|
57
121
|
/**
|
|
58
122
|
* Returns the raw platform string from Node.js (e.g. 'darwin', 'linux', 'win32').
|
|
59
123
|
* Prefer this over accessing process.platform directly so all platform
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
import { isWindows } from "./platform.js";
|
|
24
28
|
|
|
25
29
|
let cachedSqlite3Path: string | undefined | null;
|
|
26
30
|
|
|
@@ -37,11 +41,27 @@ export function findSqlite3(): string | undefined {
|
|
|
37
41
|
// 1. Common install locations. We check these before `Bun.which` so we
|
|
38
42
|
// pick a deterministic, predictable binary even when PATH is
|
|
39
43
|
// unusual (e.g. a daemon launched with a stripped env).
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const candidates = isWindows()
|
|
45
|
+
? [
|
|
46
|
+
join(
|
|
47
|
+
process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"),
|
|
48
|
+
"Programs",
|
|
49
|
+
"sqlite",
|
|
50
|
+
"sqlite3.exe",
|
|
51
|
+
),
|
|
52
|
+
join(
|
|
53
|
+
process.env.ProgramFiles ?? "C:\\Program Files",
|
|
54
|
+
"SQLite",
|
|
55
|
+
"sqlite3.exe",
|
|
56
|
+
),
|
|
57
|
+
"C:\\sqlite\\sqlite3.exe",
|
|
58
|
+
]
|
|
59
|
+
: [
|
|
60
|
+
"/usr/bin/sqlite3",
|
|
61
|
+
"/usr/local/bin/sqlite3",
|
|
62
|
+
"/opt/homebrew/bin/sqlite3",
|
|
63
|
+
];
|
|
64
|
+
for (const p of candidates) {
|
|
45
65
|
if (existsSync(p)) {
|
|
46
66
|
cachedSqlite3Path = p;
|
|
47
67
|
return p;
|
|
@@ -15,6 +15,7 @@ import { getConfig } from "../config/loader.js";
|
|
|
15
15
|
import { isFileUnheld } from "../util/file-use.js";
|
|
16
16
|
import { getLogger } from "../util/logger.js";
|
|
17
17
|
import { Mutex } from "../util/mutex.js";
|
|
18
|
+
import { addToPathEnv, getExtraToolPathDirs } from "../util/platform.js";
|
|
18
19
|
import { PromiseGuard } from "../util/promise-guard.js";
|
|
19
20
|
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
@@ -41,18 +42,7 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
|
|
|
41
42
|
}
|
|
42
43
|
env.GIT_CEILING_DIRECTORIES = workspaceDir;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
const extraDirs = [
|
|
46
|
-
"/opt/homebrew/bin",
|
|
47
|
-
"/usr/local/bin",
|
|
48
|
-
`${home}/.local/bin`,
|
|
49
|
-
];
|
|
50
|
-
const currentPath = env.PATH ?? "";
|
|
51
|
-
const pathDirs = currentPath.split(":");
|
|
52
|
-
const missing = extraDirs.filter((d) => !pathDirs.includes(d));
|
|
53
|
-
if (missing.length > 0) {
|
|
54
|
-
env.PATH = [...missing, currentPath].filter(Boolean).join(":");
|
|
55
|
-
}
|
|
45
|
+
addToPathEnv(env, getExtraToolPathDirs());
|
|
56
46
|
|
|
57
47
|
return env;
|
|
58
48
|
}
|