openpalm 0.10.2 → 0.11.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +11 -19
  2. package/package.json +4 -2
  3. package/src/commands/addon.ts +5 -4
  4. package/src/commands/automations.ts +63 -0
  5. package/src/commands/install.ts +98 -280
  6. package/src/commands/logs.ts +1 -1
  7. package/src/commands/restart.ts +5 -4
  8. package/src/commands/rollback.ts +4 -3
  9. package/src/commands/scan.ts +66 -32
  10. package/src/commands/service.ts +5 -4
  11. package/src/commands/start.ts +5 -4
  12. package/src/commands/status.ts +1 -1
  13. package/src/commands/stop.ts +2 -4
  14. package/src/commands/uninstall.ts +3 -5
  15. package/src/commands/update.ts +19 -2
  16. package/src/commands/validate.ts +16 -34
  17. package/src/install-flow.test.ts +153 -154
  18. package/src/lib/admin-skills/index.test.ts +70 -0
  19. package/src/lib/admin-skills/index.ts +113 -0
  20. package/src/lib/browser.ts +20 -0
  21. package/src/lib/cli-compose.ts +2 -20
  22. package/src/lib/cli-state.ts +1 -1
  23. package/src/lib/docker.ts +8 -214
  24. package/src/lib/env.ts +12 -83
  25. package/src/lib/io.ts +130 -0
  26. package/src/lib/opencode-subprocess.ts +14 -6
  27. package/src/lib/paths.ts +2 -2
  28. package/src/lib/ui-server.ts +150 -0
  29. package/src/main.test.ts +76 -173
  30. package/src/main.ts +131 -7
  31. package/e2e/start-wizard-server.ts +0 -59
  32. package/src/commands/admin.ts +0 -43
  33. package/src/commands/install-services.test.ts +0 -13
  34. package/src/commands/install-services.ts +0 -9
  35. package/src/commands/upgrade.ts +0 -12
  36. package/src/lib/embedded-assets.ts +0 -115
  37. package/src/lib/varlock.ts +0 -126
  38. package/src/setup-wizard/index.html +0 -321
  39. package/src/setup-wizard/server-errors.test.ts +0 -418
  40. package/src/setup-wizard/server-integration.test.ts +0 -511
  41. package/src/setup-wizard/server.test.ts +0 -508
  42. package/src/setup-wizard/server.ts +0 -342
  43. package/src/setup-wizard/wizard-renderers.js +0 -1294
  44. package/src/setup-wizard/wizard-state.js +0 -346
  45. package/src/setup-wizard/wizard-validators.js +0 -81
  46. package/src/setup-wizard/wizard.css +0 -1611
  47. package/src/setup-wizard/wizard.js +0 -613
@@ -1,418 +0,0 @@
1
- import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { createSetupServer } from "./server.ts";
6
-
7
- // ── Helpers ──────────────────────────────────────────────────────────────
8
-
9
- let tempBase: string;
10
- let homeDir: string;
11
- let configDir: string;
12
- let vaultDir: string;
13
- let dataDir: string;
14
- let logsDir: string;
15
-
16
- const savedEnv: Record<string, string | undefined> = {};
17
-
18
- /** Seed minimal asset files so performSetup() can read them at OP_HOME. */
19
- function seedRequiredAssets(homeDir: string): void {
20
- mkdirSync(join(homeDir, "stack"), { recursive: true });
21
- writeFileSync(join(homeDir, "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
22
- mkdirSync(join(homeDir, "data", "assistant"), { recursive: true });
23
- writeFileSync(join(homeDir, "data", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
24
- writeFileSync(join(homeDir, "data", "assistant", "AGENTS.md"), "# Agents\n");
25
- writeFileSync(join(homeDir, "vault", "user", "user.env.schema"), "OP_ADMIN_TOKEN=string\n");
26
- writeFileSync(join(homeDir, "vault", "stack", "stack.env.schema"), "OP_IMAGE_TAG=string\n");
27
- mkdirSync(join(homeDir, "config", "automations"), { recursive: true });
28
- writeFileSync(join(homeDir, "config", "automations", "cleanup-logs.yml"), "name: cleanup-logs\nschedule: daily\n");
29
- writeFileSync(join(homeDir, "config", "automations", "cleanup-data.yml"), "name: cleanup-data\nschedule: weekly\n");
30
- writeFileSync(join(homeDir, "config", "automations", "validate-config.yml"), "name: validate-config\nschedule: hourly\n");
31
- }
32
-
33
- function makeSetupDirs(): void {
34
- tempBase = mkdtempSync(join(tmpdir(), "openpalm-server-err-test-"));
35
- homeDir = tempBase;
36
- configDir = join(homeDir, "config");
37
- vaultDir = join(homeDir, "vault");
38
- dataDir = join(homeDir, "data");
39
- logsDir = join(homeDir, "logs");
40
-
41
- for (const dir of [
42
- configDir,
43
- join(configDir, "components"),
44
- join(configDir, "capabilities"),
45
- join(configDir, "assistant"),
46
- join(configDir, "automations"),
47
- vaultDir,
48
- dataDir,
49
- join(dataDir, "admin"),
50
- join(dataDir, "memory"),
51
- join(dataDir, "assistant"),
52
- join(dataDir, "guardian"),
53
- join(dataDir, "stash"),
54
- join(dataDir, "workspace"),
55
- logsDir,
56
- join(logsDir, "opencode"),
57
- ]) {
58
- mkdirSync(dir, { recursive: true });
59
- }
60
-
61
- mkdirSync(join(vaultDir, "stack"), { recursive: true });
62
- mkdirSync(join(vaultDir, "user"), { recursive: true });
63
- writeFileSync(
64
- join(vaultDir, "stack", "stack.env"),
65
- [
66
- "OP_SETUP_COMPLETE=false",
67
- "OP_ADMIN_TOKEN=",
68
- "OPENAI_API_KEY=",
69
- "OPENAI_BASE_URL=",
70
- "ANTHROPIC_API_KEY=",
71
- "GROQ_API_KEY=",
72
- "MISTRAL_API_KEY=",
73
- "GOOGLE_API_KEY=",
74
- "OWNER_NAME=",
75
- "OWNER_EMAIL=",
76
- "",
77
- ].join("\n")
78
- );
79
- writeFileSync(
80
- join(vaultDir, "user", "user.env"),
81
- [
82
- "# OpenPalm — User Extensions",
83
- "# Add any custom environment variables here.",
84
- "# These are loaded by compose alongside stack.env.",
85
- "",
86
- ].join("\n")
87
- );
88
-
89
- // Seed asset files for performSetup() reads
90
- seedRequiredAssets(homeDir);
91
- }
92
-
93
- // Incrementing port counter to avoid conflicts
94
- let nextPort = 19200;
95
-
96
- describe("setup wizard server error scenarios", () => {
97
- let serverPort: number;
98
-
99
- beforeEach(() => {
100
- makeSetupDirs();
101
-
102
- savedEnv.OP_HOME = process.env.OP_HOME;
103
- process.env.OP_HOME = homeDir;
104
-
105
- serverPort = nextPort++;
106
- });
107
-
108
- afterEach(() => {
109
- process.env.OP_HOME = savedEnv.OP_HOME;
110
- if (tempBase) rmSync(tempBase, { recursive: true, force: true });
111
- });
112
-
113
- // ── POST /api/setup/complete validation errors ────────────────────────
114
-
115
- it("returns 400 when adminToken is missing", async () => {
116
- const { stop } = createSetupServer(serverPort, {
117
- configDir,
118
- });
119
-
120
- try {
121
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
122
- method: "POST",
123
- headers: { "Content-Type": "application/json" },
124
- body: JSON.stringify({
125
- // no security.adminToken
126
- version: 2,
127
- capabilities: {
128
- llm: "openai/gpt-4o",
129
- embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
130
- memory: { userId: "user1", customInstructions: "" },
131
- },
132
- connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
133
- }),
134
- });
135
- expect(res.status).toBe(400);
136
- const data = (await res.json()) as { ok: boolean; error: string };
137
- expect(data.ok).toBe(false);
138
- expect(data.error).toContain("security");
139
- } finally {
140
- stop();
141
- }
142
- });
143
-
144
- it("returns 400 when connections array is not an array", async () => {
145
- const { stop } = createSetupServer(serverPort, {
146
- configDir,
147
- });
148
-
149
- try {
150
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
151
- method: "POST",
152
- headers: { "Content-Type": "application/json" },
153
- body: JSON.stringify({
154
- version: 2,
155
- capabilities: {
156
- llm: "openai/gpt-4o",
157
- embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
158
- memory: { userId: "user1", customInstructions: "" },
159
- },
160
- security: { adminToken: "valid-token-12345" },
161
- owner: { name: "Test User", email: "test@example.com" },
162
- connections: "not-an-array",
163
- }),
164
- });
165
- expect(res.status).toBe(400);
166
- const data = (await res.json()) as { ok: boolean; error: string };
167
- expect(data.ok).toBe(false);
168
- expect(data.error).toContain("connections");
169
- } finally {
170
- stop();
171
- }
172
- });
173
-
174
- it("returns 400 when capabilities config is missing", async () => {
175
- const { stop } = createSetupServer(serverPort, {
176
- configDir,
177
- });
178
-
179
- try {
180
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
181
- method: "POST",
182
- headers: { "Content-Type": "application/json" },
183
- body: JSON.stringify({
184
- version: 2,
185
- security: { adminToken: "valid-token-12345" },
186
- owner: { name: "Test User", email: "test@example.com" },
187
- connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
188
- // no capabilities
189
- }),
190
- });
191
- expect(res.status).toBe(400);
192
- const data = (await res.json()) as { ok: boolean; error: string };
193
- expect(data.ok).toBe(false);
194
- expect(data.error).toContain("capabilities");
195
- } finally {
196
- stop();
197
- }
198
- });
199
-
200
- it("succeeds even when capability provider does not match embeddings provider", async () => {
201
- const { stop } = createSetupServer(serverPort, {
202
- configDir,
203
- });
204
-
205
- try {
206
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
207
- method: "POST",
208
- headers: { "Content-Type": "application/json" },
209
- body: JSON.stringify({
210
- version: 2,
211
- capabilities: {
212
- llm: "fakeprovider/gpt-4o",
213
- embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
214
- memory: { userId: "user1", customInstructions: "" },
215
- },
216
- security: { adminToken: "valid-token-12345" },
217
- owner: { name: "Test User", email: "test@example.com" },
218
- connections: [{ id: "c1", name: "C1", provider: "fakeprovider", baseUrl: "", apiKey: "sk-test" }],
219
- }),
220
- });
221
- // Connection-provider matching is no longer validated; setup succeeds
222
- expect(res.status).toBe(200);
223
- const data = (await res.json()) as { ok: boolean };
224
- expect(data.ok).toBe(true);
225
- } finally {
226
- stop();
227
- }
228
- });
229
-
230
- it("succeeds even when no capability matches LLM provider", async () => {
231
- const { stop } = createSetupServer(serverPort, {
232
- configDir,
233
- });
234
-
235
- try {
236
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
237
- method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify({
240
- version: 2,
241
- capabilities: {
242
- llm: "anthropic/claude-3-opus", // No anthropic connection provided
243
- embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
244
- memory: { userId: "user1", customInstructions: "" },
245
- },
246
- security: { adminToken: "valid-token-12345" },
247
- owner: { name: "Test User", email: "test@example.com" },
248
- connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
249
- }),
250
- });
251
- // Provider matching is no longer validated; setup succeeds
252
- expect(res.status).toBe(200);
253
- const data = (await res.json()) as { ok: boolean };
254
- expect(data.ok).toBe(true);
255
- } finally {
256
- stop();
257
- }
258
- });
259
-
260
- // ── POST /api/setup/models/:provider errors ───────────────────────────
261
-
262
- it("returns 400 for invalid JSON on model fetch", async () => {
263
- const { stop } = createSetupServer(serverPort, {
264
- configDir,
265
- });
266
-
267
- try {
268
- const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`, {
269
- method: "POST",
270
- headers: { "Content-Type": "application/json" },
271
- body: "not-json",
272
- });
273
- expect(res.status).toBe(400);
274
- const data = (await res.json()) as { ok: boolean; error: string };
275
- expect(data.ok).toBe(false);
276
- expect(data.error).toBe("invalid_json");
277
- } finally {
278
- stop();
279
- }
280
- });
281
-
282
- // lmstudio fetch to 127.0.0.1:1234 can take >5s to fail when nothing listens
283
- it("returns empty model list when provider has no base URL", async () => {
284
- const { stop } = createSetupServer(serverPort, {
285
- configDir,
286
- });
287
-
288
- try {
289
- // lmstudio without a base URL should return 502 with recoverable_error
290
- const res = await fetch(`http://localhost:${serverPort}/api/setup/models/lmstudio`, {
291
- method: "POST",
292
- headers: { "Content-Type": "application/json" },
293
- body: JSON.stringify({ apiKey: "", baseUrl: "" }),
294
- });
295
- expect(res.status).toBe(502);
296
- const data = (await res.json()) as { ok: boolean; models: string[]; status: string; reason: string };
297
- expect(data.ok).toBe(false);
298
- expect(Array.isArray(data.models)).toBe(true);
299
- expect(data.status).toBe("recoverable_error");
300
- } finally {
301
- stop();
302
- }
303
- }, 15000);
304
-
305
- it("returns recoverable error when model fetch hits unreachable server", async () => {
306
- const { stop } = createSetupServer(serverPort, {
307
- configDir,
308
- });
309
-
310
- try {
311
- // Use a baseUrl that definitely will not connect — should return 502
312
- const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`, {
313
- method: "POST",
314
- headers: { "Content-Type": "application/json" },
315
- body: JSON.stringify({ apiKey: "sk-fake", baseUrl: "http://127.0.0.1:1" }),
316
- });
317
- expect(res.status).toBe(502);
318
- const data = (await res.json()) as {
319
- ok: boolean;
320
- models: string[];
321
- status: string;
322
- reason: string;
323
- error?: string;
324
- };
325
- expect(data.ok).toBe(false);
326
- expect(data.models).toEqual([]);
327
- expect(data.status).toBe("recoverable_error");
328
- expect(data.reason).toBe("network");
329
- expect(data.error).toBeDefined();
330
- } finally {
331
- stop();
332
- }
333
- }, 10000);
334
-
335
- it("returns static model list for anthropic (no network call needed)", async () => {
336
- const { stop } = createSetupServer(serverPort, {
337
- configDir,
338
- });
339
-
340
- try {
341
- const res = await fetch(`http://localhost:${serverPort}/api/setup/models/anthropic`, {
342
- method: "POST",
343
- headers: { "Content-Type": "application/json" },
344
- body: JSON.stringify({ apiKey: "sk-ant-test", baseUrl: "" }),
345
- });
346
- expect(res.status).toBe(200);
347
- const data = (await res.json()) as { ok: boolean; models: string[]; status: string; reason: string };
348
- expect(data.ok).toBe(true);
349
- expect(data.models.length).toBeGreaterThan(0);
350
- expect(data.status).toBe("ok");
351
- expect(data.reason).toBe("provider_static");
352
- } finally {
353
- stop();
354
- }
355
- });
356
-
357
- // ── Deploy status with error state ────────────────────────────────────
358
-
359
- it("reports deploy error via deploy-status endpoint", async () => {
360
- const { stop, updateDeployStatus, setDeployError } = createSetupServer(serverPort, {
361
- configDir,
362
- });
363
-
364
- try {
365
- updateDeployStatus([
366
- { service: "assistant", status: "running", label: "Assistant" },
367
- { service: "memory", status: "error", label: "Failed to pull" },
368
- ]);
369
- setDeployError("memory container failed to start");
370
-
371
- const res = await fetch(`http://localhost:${serverPort}/api/setup/deploy-status`);
372
- expect(res.status).toBe(200);
373
- const data = (await res.json()) as {
374
- ok: boolean;
375
- deployStatus: Array<{ service: string; status: string; label: string }>;
376
- deployError: string | null;
377
- };
378
- expect(data.ok).toBe(true);
379
- expect(data.deployError).toBe("memory container failed to start");
380
- const memEntry = data.deployStatus.find((e) => e.service === "memory");
381
- expect(memEntry?.status).toBe("error");
382
- } finally {
383
- stop();
384
- }
385
- });
386
-
387
- // ── HTTP method mismatches ────────────────────────────────────────────
388
-
389
- it("returns 404 for GET on model endpoint (requires POST)", async () => {
390
- const { stop } = createSetupServer(serverPort, {
391
- configDir,
392
- });
393
-
394
- try {
395
- const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`);
396
- expect(res.status).toBe(404);
397
- const data = (await res.json()) as { ok: boolean; error: string };
398
- expect(data.ok).toBe(false);
399
- } finally {
400
- stop();
401
- }
402
- });
403
-
404
- it("returns 404 for GET on /api/setup/complete (requires POST)", async () => {
405
- const { stop } = createSetupServer(serverPort, {
406
- configDir,
407
- });
408
-
409
- try {
410
- const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`);
411
- expect(res.status).toBe(404);
412
- const data = (await res.json()) as { ok: boolean; error: string };
413
- expect(data.ok).toBe(false);
414
- } finally {
415
- stop();
416
- }
417
- });
418
- });