@vellumai/assistant 0.4.37 → 0.4.41

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.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/README.md +13 -13
  3. package/bun.lock +80 -24
  4. package/docs/architecture/integrations.md +126 -128
  5. package/docs/runbook-trusted-contacts.md +1 -1
  6. package/docs/trusted-contact-access.md +12 -12
  7. package/package.json +3 -1
  8. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
  9. package/src/__tests__/app-bundler.test.ts +209 -0
  10. package/src/__tests__/app-compiler.test.ts +279 -0
  11. package/src/__tests__/app-executors.test.ts +293 -483
  12. package/src/__tests__/app-migration.test.ts +148 -0
  13. package/src/__tests__/app-routes-csp.test.ts +202 -0
  14. package/src/__tests__/avatar-e2e.test.ts +452 -0
  15. package/src/__tests__/avatar-generator.test.ts +193 -0
  16. package/src/__tests__/avatar-router.test.ts +186 -0
  17. package/src/__tests__/browser-download-timeout.test.ts +28 -0
  18. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
  19. package/src/__tests__/call-domain.test.ts +3 -7
  20. package/src/__tests__/credential-security-e2e.test.ts +19 -12
  21. package/src/__tests__/credentials-cli.test.ts +30 -4
  22. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
  23. package/src/__tests__/handlers-slack-config.test.ts +0 -72
  24. package/src/__tests__/handlers-telegram-config.test.ts +19 -12
  25. package/src/__tests__/handlers-twitter-config.test.ts +105 -48
  26. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  27. package/src/__tests__/integration-status.test.ts +15 -5
  28. package/src/__tests__/integrations-cli.test.ts +1 -1
  29. package/src/__tests__/invite-redemption-service.test.ts +62 -7
  30. package/src/__tests__/ipc-snapshot.test.ts +0 -8
  31. package/src/__tests__/managed-avatar-client.test.ts +280 -0
  32. package/src/__tests__/mcp-cli.test.ts +3 -3
  33. package/src/__tests__/oauth-cli.test.ts +203 -0
  34. package/src/__tests__/relay-server.test.ts +3 -3
  35. package/src/__tests__/secret-onetime-send.test.ts +19 -12
  36. package/src/__tests__/secure-keys.test.ts +78 -0
  37. package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
  38. package/src/__tests__/slack-channel-config.test.ts +23 -16
  39. package/src/__tests__/slack-share-routes.test.ts +263 -0
  40. package/src/__tests__/sms-messaging-provider.test.ts +3 -1
  41. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
  42. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  43. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  44. package/src/__tests__/twilio-config.test.ts +15 -36
  45. package/src/__tests__/twilio-provider.test.ts +4 -0
  46. package/src/__tests__/twitter-auth-handler.test.ts +27 -14
  47. package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
  48. package/src/__tests__/twitter-cli-routing.test.ts +38 -53
  49. package/src/__tests__/twitter-oauth-client.test.ts +18 -47
  50. package/src/__tests__/voice-invite-redemption.test.ts +27 -3
  51. package/src/amazon/cart.ts +1 -1
  52. package/src/amazon/client.ts +89 -7
  53. package/src/approvals/guardian-request-resolvers.ts +2 -2
  54. package/src/bundler/app-bundler.ts +77 -32
  55. package/src/bundler/app-compiler.ts +195 -0
  56. package/src/bundler/manifest.ts +1 -1
  57. package/src/bundler/package-resolver.ts +185 -0
  58. package/src/calls/call-domain.ts +4 -14
  59. package/src/calls/relay-server.ts +2 -2
  60. package/src/calls/twilio-config.ts +5 -24
  61. package/src/calls/twilio-rest.ts +19 -5
  62. package/src/cli/amazon.ts +74 -249
  63. package/src/cli/audit.ts +2 -2
  64. package/src/cli/autonomy.ts +9 -9
  65. package/src/cli/channels.ts +5 -5
  66. package/src/cli/completions.ts +27 -27
  67. package/src/cli/config.ts +14 -14
  68. package/src/cli/contacts.ts +27 -27
  69. package/src/cli/credentials.ts +28 -28
  70. package/src/cli/dev.ts +2 -2
  71. package/src/cli/doctor.ts +2 -2
  72. package/src/cli/email.ts +82 -82
  73. package/src/cli/influencer.ts +13 -13
  74. package/src/cli/integrations.ts +19 -144
  75. package/src/cli/keys.ts +10 -10
  76. package/src/cli/map.ts +4 -4
  77. package/src/cli/mcp.ts +17 -17
  78. package/src/cli/memory.ts +18 -18
  79. package/src/cli/notifications.ts +13 -13
  80. package/src/cli/oauth.ts +77 -0
  81. package/src/cli/program.ts +2 -0
  82. package/src/cli/sequence.ts +27 -27
  83. package/src/cli/sessions.ts +12 -12
  84. package/src/cli/trust.ts +8 -8
  85. package/src/cli/twitter.ts +124 -70
  86. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  87. package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
  88. package/src/config/bundled-skills/amazon/SKILL.md +54 -54
  89. package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
  90. package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
  91. package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
  92. package/src/config/bundled-skills/contacts/SKILL.md +12 -12
  93. package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
  94. package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
  95. package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
  96. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
  97. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
  98. package/src/config/bundled-skills/influencer/SKILL.md +13 -13
  99. package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
  101. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  102. package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
  103. package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
  104. package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
  105. package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
  106. package/src/config/bundled-skills/twitter/SKILL.md +68 -44
  107. package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
  108. package/src/config/core-schema.ts +26 -0
  109. package/src/config/env.ts +4 -0
  110. package/src/config/feature-flag-registry.json +9 -1
  111. package/src/config/schema.ts +8 -0
  112. package/src/config/system-prompt.ts +6 -3
  113. package/src/config/templates/BOOTSTRAP.md +7 -5
  114. package/src/contacts/contacts-write.ts +5 -1
  115. package/src/daemon/handlers/apps.ts +31 -4
  116. package/src/daemon/handlers/config-ingress.ts +3 -3
  117. package/src/daemon/handlers/config-integrations.ts +120 -49
  118. package/src/daemon/handlers/config-slack-channel.ts +26 -7
  119. package/src/daemon/handlers/config-slack.ts +1 -54
  120. package/src/daemon/handlers/config-telegram.ts +28 -10
  121. package/src/daemon/handlers/config.ts +1 -4
  122. package/src/daemon/handlers/twitter-auth.ts +11 -4
  123. package/src/daemon/ipc-contract/apps.ts +0 -13
  124. package/src/daemon/ipc-contract-inventory.json +0 -2
  125. package/src/daemon/lifecycle.ts +8 -1
  126. package/src/daemon/session-messaging.ts +2 -2
  127. package/src/daemon/tool-side-effects.ts +30 -0
  128. package/src/email/providers/agentmail.ts +1 -1
  129. package/src/email/providers/index.ts +1 -1
  130. package/src/email/service.ts +1 -1
  131. package/src/gallery/default-gallery.ts +538 -0
  132. package/src/gallery/gallery-manifest.ts +5 -1
  133. package/src/influencer/client.ts +8 -6
  134. package/src/mcp/client.ts +1 -1
  135. package/src/media/avatar-router.ts +99 -0
  136. package/src/media/avatar-types.ts +60 -0
  137. package/src/media/managed-avatar-client.ts +189 -0
  138. package/src/memory/app-migration.ts +114 -0
  139. package/src/memory/app-store.ts +11 -0
  140. package/src/memory/qdrant-client.ts +1 -1
  141. package/src/messaging/providers/slack/client.ts +12 -2
  142. package/src/messaging/providers/sms/adapter.ts +6 -10
  143. package/src/migrations/data-layout.ts +8 -1
  144. package/src/oauth/token-persistence.ts +9 -6
  145. package/src/runtime/assistant-scope.ts +5 -0
  146. package/src/runtime/auth/route-policy.ts +4 -0
  147. package/src/runtime/channel-readiness-service.ts +9 -4
  148. package/src/runtime/gateway-internal-client.ts +11 -3
  149. package/src/runtime/http-server.ts +2 -0
  150. package/src/runtime/invite-redemption-service.ts +23 -13
  151. package/src/runtime/middleware/twilio-validation.ts +2 -2
  152. package/src/runtime/routes/app-routes.ts +131 -3
  153. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
  154. package/src/runtime/routes/integration-routes.ts +2 -2
  155. package/src/runtime/routes/slack-share-routes.ts +235 -0
  156. package/src/runtime/routes/twilio-routes.ts +47 -34
  157. package/src/schedule/integration-status.ts +2 -3
  158. package/src/security/token-manager.ts +11 -3
  159. package/src/tools/apps/executors.ts +116 -8
  160. package/src/tools/browser/browser-manager.ts +30 -2
  161. package/src/tools/browser/chrome-cdp.ts +31 -3
  162. package/src/tools/credentials/vault.ts +9 -7
  163. package/src/tools/executor.ts +4 -0
  164. package/src/tools/system/avatar-generator.ts +55 -34
  165. package/src/twitter/client.ts +1 -1
  166. package/src/twitter/oauth-client.ts +31 -43
  167. package/src/twitter/router.ts +25 -23
  168. package/src/util/platform.ts +5 -0
  169. package/src/slack/slack-webhook.ts +0 -66
@@ -1,5 +1,11 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
1
5
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
6
 
7
+ import JSZip from "jszip";
8
+
3
9
  // Mock the logger before importing the module under test
4
10
  mock.module("../util/logger.js", () => ({
5
11
  getLogger: () =>
@@ -8,10 +14,33 @@ mock.module("../util/logger.js", () => ({
8
14
  }),
9
15
  }));
10
16
 
17
+ // Temp directory for fake app data used in packageApp tests
18
+ const testAppsDir = join(tmpdir(), `app-bundler-test-${Date.now()}`);
19
+
20
+ // Mock app-store so packageApp can find our test apps
21
+ const mockApps = new Map<string, Record<string, unknown>>();
22
+ mock.module("../memory/app-store.js", () => ({
23
+ getApp: (id: string) => mockApps.get(id) ?? null,
24
+ getAppsDir: () => testAppsDir,
25
+ isMultifileApp: (app: Record<string, unknown>) => app.formatVersion === 2,
26
+ }));
27
+
28
+ // Mock content-id to avoid pulling in crypto internals
29
+ mock.module("../util/content-id.js", () => ({
30
+ computeContentId: () => "abcd1234abcd1234",
31
+ }));
32
+
33
+ // Mock bundle-signer (not exercised in these tests)
34
+ mock.module("./bundle-signer.js", () => ({
35
+ signBundle: async () => ({}),
36
+ }));
37
+
11
38
  import {
12
39
  extractRemoteUrls,
13
40
  materializeAssets,
41
+ packageApp,
14
42
  } from "../bundler/app-bundler.js";
43
+ import type { AppManifest } from "../bundler/manifest.js";
15
44
 
16
45
  // ---------------------------------------------------------------------------
17
46
  // extractRemoteUrls
@@ -323,3 +352,183 @@ describe("materializeAssets", () => {
323
352
  expect(result.rewrittenHtml).not.toContain(cssUrl);
324
353
  });
325
354
  });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // packageApp — multifile apps
358
+ // ---------------------------------------------------------------------------
359
+
360
+ describe("packageApp", () => {
361
+ afterEach(() => {
362
+ mockApps.clear();
363
+ try {
364
+ rmSync(testAppsDir, { recursive: true, force: true });
365
+ } catch {
366
+ // ignore cleanup errors
367
+ }
368
+ });
369
+
370
+ /**
371
+ * Helper: set up a fake multifile app on disk with src/ and dist/ dirs
372
+ * so that compileApp (which we mock below) can pretend to succeed.
373
+ */
374
+ function setupMultifileApp(appId: string, opts?: { withCss?: boolean }) {
375
+ const appDir = join(testAppsDir, appId);
376
+ const srcDir = join(appDir, "src");
377
+ mkdirSync(srcDir, { recursive: true });
378
+
379
+ // Write minimal src/ so the app directory looks valid
380
+ if (opts?.withCss) {
381
+ writeFileSync(join(srcDir, "styles.css"), "body { margin: 0; }");
382
+ writeFileSync(
383
+ join(srcDir, "main.tsx"),
384
+ 'import "./styles.css";\nexport default () => "hi";',
385
+ );
386
+ } else {
387
+ writeFileSync(join(srcDir, "main.tsx"), 'export default () => "hi";');
388
+ }
389
+ writeFileSync(
390
+ join(srcDir, "index.html"),
391
+ "<!DOCTYPE html><html><head></head><body></body></html>",
392
+ );
393
+
394
+ // Write the app JSON (getApp reads from {appsDir}/{id}.json)
395
+ const appDef = {
396
+ id: appId,
397
+ name: "Test Multifile App",
398
+ description: "A test app",
399
+ schemaJson: "{}",
400
+ htmlDefinition: "<unused>",
401
+ createdAt: Date.now(),
402
+ updatedAt: Date.now(),
403
+ formatVersion: 2,
404
+ };
405
+ writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
406
+ mockApps.set(appId, appDef);
407
+
408
+ return appDef;
409
+ }
410
+
411
+ function setupLegacyApp(appId: string) {
412
+ const appDir = join(testAppsDir, appId);
413
+ mkdirSync(appDir, { recursive: true });
414
+
415
+ const appDef = {
416
+ id: appId,
417
+ name: "Test Legacy App",
418
+ schemaJson: "{}",
419
+ htmlDefinition: "<html><body>Hello legacy</body></html>",
420
+ createdAt: Date.now(),
421
+ updatedAt: Date.now(),
422
+ };
423
+ writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
424
+ mockApps.set(appId, appDef);
425
+ return appDef;
426
+ }
427
+
428
+ test("packages a multifile app with compiled dist/ files in the zip", async () => {
429
+ const appId = "multi-test-1";
430
+ setupMultifileApp(appId, { withCss: true });
431
+
432
+ const result = await packageApp(appId);
433
+ const zipData = await readFile(result.bundlePath);
434
+ const zip = await JSZip.loadAsync(zipData);
435
+
436
+ // Verify compiled files are present
437
+ expect(zip.file("index.html")).not.toBeNull();
438
+ expect(zip.file("main.js")).not.toBeNull();
439
+ expect(zip.file("main.css")).not.toBeNull();
440
+ expect(zip.file("manifest.json")).not.toBeNull();
441
+
442
+ // Verify content matches what we wrote to dist/
443
+ const indexContent = await zip.file("index.html")!.async("string");
444
+ expect(indexContent).toContain('src="main.js"');
445
+
446
+ // main.js should contain compiled output (esbuild minifies the source)
447
+ const jsContent = await zip.file("main.js")!.async("string");
448
+ expect(jsContent.length).toBeGreaterThan(0);
449
+
450
+ // CSS was imported, so main.css should be present
451
+ const cssContent = await zip.file("main.css")!.async("string");
452
+ expect(cssContent).toContain("margin");
453
+ });
454
+
455
+ test("sets format_version to 2 in manifest for multifile apps", async () => {
456
+ const appId = "multi-test-2";
457
+ setupMultifileApp(appId);
458
+
459
+ const result = await packageApp(appId);
460
+ const zipData = await readFile(result.bundlePath);
461
+ const zip = await JSZip.loadAsync(zipData);
462
+
463
+ const manifestJson = await zip.file("manifest.json")!.async("string");
464
+ const manifest: AppManifest = JSON.parse(manifestJson);
465
+
466
+ expect(manifest.format_version).toBe(2);
467
+ expect(manifest.entry).toBe("index.html");
468
+ expect(manifest.name).toBe("Test Multifile App");
469
+ });
470
+
471
+ test("compile failure produces a clear error", async () => {
472
+ const appId = "multi-fail-1";
473
+ const appDir = join(testAppsDir, appId);
474
+ const srcDir = join(appDir, "src");
475
+ mkdirSync(srcDir, { recursive: true });
476
+
477
+ // Write intentionally broken source so esbuild fails
478
+ writeFileSync(join(srcDir, "main.tsx"), "const x: number = {{{BROKEN");
479
+ writeFileSync(
480
+ join(srcDir, "index.html"),
481
+ "<!DOCTYPE html><html><head></head><body></body></html>",
482
+ );
483
+
484
+ const appDef = {
485
+ id: appId,
486
+ name: "Broken App",
487
+ schemaJson: "{}",
488
+ htmlDefinition: "<unused>",
489
+ createdAt: Date.now(),
490
+ updatedAt: Date.now(),
491
+ formatVersion: 2,
492
+ };
493
+ writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
494
+ mockApps.set(appId, appDef);
495
+
496
+ await expect(packageApp(appId)).rejects.toThrow(
497
+ /Compilation failed for app "Broken App"/,
498
+ );
499
+ });
500
+
501
+ test("legacy app packaging remains unchanged (format_version 1)", async () => {
502
+ const appId = "legacy-test-1";
503
+ setupLegacyApp(appId);
504
+
505
+ const result = await packageApp(appId);
506
+ const zipData = await readFile(result.bundlePath);
507
+ const zip = await JSZip.loadAsync(zipData);
508
+
509
+ const manifestJson = await zip.file("manifest.json")!.async("string");
510
+ const manifest: AppManifest = JSON.parse(manifestJson);
511
+
512
+ expect(manifest.format_version).toBe(1);
513
+
514
+ // Legacy app should have index.html with the original content
515
+ const indexContent = await zip.file("index.html")!.async("string");
516
+ expect(indexContent).toContain("Hello legacy");
517
+
518
+ // Should NOT have main.js (not a compiled app)
519
+ expect(zip.file("main.js")).toBeNull();
520
+ });
521
+
522
+ test("multifile app without CSS omits main.css from zip", async () => {
523
+ const appId = "multi-no-css";
524
+ setupMultifileApp(appId, { withCss: false });
525
+
526
+ const result = await packageApp(appId);
527
+ const zipData = await readFile(result.bundlePath);
528
+ const zip = await JSZip.loadAsync(zipData);
529
+
530
+ expect(zip.file("index.html")).not.toBeNull();
531
+ expect(zip.file("main.js")).not.toBeNull();
532
+ expect(zip.file("main.css")).toBeNull();
533
+ });
534
+ });
@@ -0,0 +1,279 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
+
7
+ import { compileApp } from "../bundler/app-compiler.js";
8
+ import {
9
+ ALLOWED_PACKAGES,
10
+ getCacheDir,
11
+ isBareImport,
12
+ packageName,
13
+ resolvePackage,
14
+ } from "../bundler/package-resolver.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Shared temp directory
18
+ // ---------------------------------------------------------------------------
19
+
20
+ let tempDir: string;
21
+
22
+ beforeAll(async () => {
23
+ tempDir = await mkdtemp(join(tmpdir(), "app-compiler-test-"));
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await rm(tempDir, { recursive: true, force: true });
28
+ });
29
+
30
+ /** Scaffold a minimal app directory with src/main.tsx and src/index.html. */
31
+ async function scaffold(
32
+ name: string,
33
+ files: Record<string, string>,
34
+ ): Promise<string> {
35
+ const appDir = join(tempDir, name);
36
+ const srcDir = join(appDir, "src");
37
+ await mkdir(srcDir, { recursive: true });
38
+ for (const [filename, content] of Object.entries(files)) {
39
+ const filePath = join(srcDir, filename);
40
+ await writeFile(filePath, content);
41
+ }
42
+ return appDir;
43
+ }
44
+
45
+ const MINIMAL_HTML = `<!DOCTYPE html>
46
+ <html>
47
+ <head><title>Test</title></head>
48
+ <body>
49
+ </body>
50
+ </html>`;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Tests
54
+ // ---------------------------------------------------------------------------
55
+
56
+ describe("compileApp", () => {
57
+ test("compiles minimal TSX app and produces dist/main.js + dist/index.html", async () => {
58
+ const appDir = await scaffold("basic", {
59
+ "main.tsx": `const App = () => { const el = document.createElement("div"); el.textContent = "hello"; return el; }; App();`,
60
+ "index.html": MINIMAL_HTML,
61
+ });
62
+
63
+ const result = await compileApp(appDir);
64
+
65
+ expect(result.ok).toBe(true);
66
+ expect(result.errors).toHaveLength(0);
67
+ expect(result.durationMs).toBeLessThan(5000);
68
+
69
+ // dist/main.js should exist and contain bundled code
70
+ const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
71
+ expect(js.length).toBeGreaterThan(0);
72
+
73
+ // dist/index.html should exist and have the script tag injected
74
+ const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
75
+ expect(html).toContain('src="main.js"');
76
+ expect(html).toContain('type="module"');
77
+ });
78
+
79
+ test("compiles preact JSX correctly", async () => {
80
+ const appDir = await scaffold("preact-jsx", {
81
+ "main.tsx": `import { render } from "preact";
82
+ const App = () => <div>Hello</div>;
83
+ render(<App />, document.body);`,
84
+ "index.html": MINIMAL_HTML,
85
+ });
86
+
87
+ const result = await compileApp(appDir);
88
+
89
+ expect(result.ok).toBe(true);
90
+ expect(result.errors).toHaveLength(0);
91
+
92
+ const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
93
+ // The output should contain preact's runtime code (bundled)
94
+ expect(js.length).toBeGreaterThan(100);
95
+ });
96
+
97
+ test("strips TypeScript types", async () => {
98
+ const appDir = await scaffold("ts-types", {
99
+ "main.tsx": `interface Greeting { name: string; }
100
+ const greet = (g: Greeting): string => g.name;
101
+ console.log(greet({ name: "world" }));`,
102
+ "index.html": MINIMAL_HTML,
103
+ });
104
+
105
+ const result = await compileApp(appDir);
106
+
107
+ expect(result.ok).toBe(true);
108
+
109
+ const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
110
+ // Interface declarations should be stripped
111
+ expect(js).not.toContain("interface");
112
+ expect(js).toContain("world");
113
+ });
114
+
115
+ test("CSS imports produce dist/main.css and inject link tag", async () => {
116
+ const appDir = await scaffold("css-import", {
117
+ "main.tsx": `import "./style.css";
118
+ console.log("styled");`,
119
+ "style.css": `body { background: red; }`,
120
+ "index.html": MINIMAL_HTML,
121
+ });
122
+
123
+ const result = await compileApp(appDir);
124
+
125
+ expect(result.ok).toBe(true);
126
+
127
+ const css = await readFile(join(appDir, "dist", "main.css"), "utf-8");
128
+ expect(css).toContain("background");
129
+
130
+ const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
131
+ expect(html).toContain('href="main.css"');
132
+ expect(html).toContain("stylesheet");
133
+ });
134
+
135
+ test("returns ok: false with diagnostics on syntax error", async () => {
136
+ const appDir = await scaffold("syntax-error", {
137
+ "main.tsx": `const x: number = <<<INVALID>>>;`,
138
+ "index.html": MINIMAL_HTML,
139
+ });
140
+
141
+ const result = await compileApp(appDir);
142
+
143
+ expect(result.ok).toBe(false);
144
+ expect(result.errors.length).toBeGreaterThan(0);
145
+ expect(result.errors[0].text).toBeTruthy();
146
+ });
147
+
148
+ test("dist/index.html has script tag injected", async () => {
149
+ const appDir = await scaffold("script-injection", {
150
+ "main.tsx": `console.log("hi");`,
151
+ "index.html": `<!DOCTYPE html>
152
+ <html>
153
+ <head><title>Inject Test</title></head>
154
+ <body>
155
+ <div id="app"></div>
156
+ </body>
157
+ </html>`,
158
+ });
159
+
160
+ const result = await compileApp(appDir);
161
+ expect(result.ok).toBe(true);
162
+
163
+ const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
164
+ expect(html).toContain('<script type="module" src="main.js"></script>');
165
+ // Original content should be preserved
166
+ expect(html).toContain('<div id="app"></div>');
167
+ });
168
+
169
+ test("does not duplicate script tag if already present", async () => {
170
+ const appDir = await scaffold("no-dup-script", {
171
+ "main.tsx": `console.log("hi");`,
172
+ "index.html": `<!DOCTYPE html>
173
+ <html>
174
+ <head><title>Test</title></head>
175
+ <body>
176
+ <script type="module" src="main.js"></script>
177
+ </body>
178
+ </html>`,
179
+ });
180
+
181
+ const result = await compileApp(appDir);
182
+ expect(result.ok).toBe(true);
183
+
184
+ const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
185
+ const matches = html.match(/src="main\.js"/g);
186
+ expect(matches).toHaveLength(1);
187
+ });
188
+
189
+ test("disallowed package import produces a clear error", async () => {
190
+ const appDir = await scaffold("disallowed-pkg", {
191
+ "main.tsx": `import leftpad from "left-pad";\nconsole.log(leftpad("hi", 5));`,
192
+ "index.html": MINIMAL_HTML,
193
+ });
194
+
195
+ const result = await compileApp(appDir);
196
+
197
+ expect(result.ok).toBe(false);
198
+ expect(result.errors.length).toBeGreaterThan(0);
199
+ expect(result.errors[0].text).toContain("not in the allowed list");
200
+ expect(result.errors[0].text).toContain("left-pad");
201
+ });
202
+
203
+ test("allowed package (zod) compiles successfully", async () => {
204
+ const appDir = await scaffold("allowed-pkg-zod", {
205
+ "main.tsx": `import { z } from "zod";\nconst schema = z.string();\nconsole.log(schema.parse("hello"));`,
206
+ "index.html": MINIMAL_HTML,
207
+ });
208
+
209
+ const result = await compileApp(appDir);
210
+
211
+ expect(result.ok).toBe(true);
212
+ expect(result.errors).toHaveLength(0);
213
+
214
+ const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
215
+ expect(js.length).toBeGreaterThan(100);
216
+ }, 30_000);
217
+
218
+ test("allowed package uses shared cache on second build", async () => {
219
+ // First build installs the package
220
+ const appDir1 = await scaffold("cache-test-1", {
221
+ "main.tsx": `import { z } from "zod";\nconsole.log(z.string());`,
222
+ "index.html": MINIMAL_HTML,
223
+ });
224
+ const r1 = await compileApp(appDir1);
225
+ expect(r1.ok).toBe(true);
226
+
227
+ // The cache dir should now have zod
228
+ const cacheDir = getCacheDir();
229
+ expect(existsSync(join(cacheDir, "node_modules", "zod"))).toBe(true);
230
+
231
+ // Second build should reuse the cache (no install needed)
232
+ const appDir2 = await scaffold("cache-test-2", {
233
+ "main.tsx": `import { z } from "zod";\nconst s = z.number();\nconsole.log(s.parse(42));`,
234
+ "index.html": MINIMAL_HTML,
235
+ });
236
+ const r2 = await compileApp(appDir2);
237
+ expect(r2.ok).toBe(true);
238
+ }, 30_000);
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Package resolver unit tests
243
+ // ---------------------------------------------------------------------------
244
+
245
+ describe("package-resolver", () => {
246
+ test("isBareImport identifies bare specifiers", () => {
247
+ expect(isBareImport("date-fns")).toBe(true);
248
+ expect(isBareImport("zod")).toBe(true);
249
+ expect(isBareImport("@scope/pkg")).toBe(true);
250
+ expect(isBareImport("./local")).toBe(false);
251
+ expect(isBareImport("../parent")).toBe(false);
252
+ expect(isBareImport("/absolute")).toBe(false);
253
+ expect(isBareImport("preact")).toBe(false);
254
+ expect(isBareImport("preact/hooks")).toBe(false);
255
+ expect(isBareImport("react")).toBe(false);
256
+ expect(isBareImport("react-dom")).toBe(false);
257
+ });
258
+
259
+ test("packageName extracts top-level name", () => {
260
+ expect(packageName("date-fns")).toBe("date-fns");
261
+ expect(packageName("date-fns/format")).toBe("date-fns");
262
+ expect(packageName("@scope/pkg")).toBe("@scope/pkg");
263
+ expect(packageName("@scope/pkg/sub")).toBe("@scope/pkg");
264
+ });
265
+
266
+ test("resolvePackage returns null for disallowed packages", async () => {
267
+ const result = await resolvePackage("left-pad");
268
+ expect(result).toBeNull();
269
+ });
270
+
271
+ test("ALLOWED_PACKAGES contains expected entries", () => {
272
+ expect(ALLOWED_PACKAGES).toContain("date-fns");
273
+ expect(ALLOWED_PACKAGES).toContain("chart.js");
274
+ expect(ALLOWED_PACKAGES).toContain("lodash-es");
275
+ expect(ALLOWED_PACKAGES).toContain("zod");
276
+ expect(ALLOWED_PACKAGES).toContain("clsx");
277
+ expect(ALLOWED_PACKAGES).toContain("lucide");
278
+ });
279
+ });