openpalm 0.9.1 → 0.9.3
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 +5 -2
- package/src/commands/install.ts +170 -0
- package/src/commands/logs.ts +36 -0
- package/src/commands/restart.ts +43 -0
- package/src/commands/scan.ts +48 -0
- package/src/commands/service.ts +67 -0
- package/src/commands/start.ts +48 -0
- package/src/commands/status.ts +12 -0
- package/src/commands/stop.ts +45 -0
- package/src/commands/uninstall.ts +12 -0
- package/src/commands/update.ts +12 -0
- package/src/commands/validate.ts +47 -0
- package/src/lib/admin.ts +82 -0
- package/src/lib/docker.ts +180 -0
- package/src/lib/env.ts +196 -0
- package/src/lib/host-info.ts +47 -0
- package/src/lib/paths.ts +37 -0
- package/src/lib/varlock.ts +124 -0
- package/src/main.test.ts +135 -27
- package/src/main.ts +33 -954
package/src/main.test.ts
CHANGED
|
@@ -36,23 +36,30 @@ function restoreDockerCli(): void {
|
|
|
36
36
|
describe('cli main', () => {
|
|
37
37
|
const originalFetch = globalThis.fetch;
|
|
38
38
|
const originalLog = console.log;
|
|
39
|
+
const originalWarn = console.warn;
|
|
39
40
|
const originalConfigHome = process.env.OPENPALM_CONFIG_HOME;
|
|
40
41
|
const originalDataHome = process.env.OPENPALM_DATA_HOME;
|
|
41
42
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
42
43
|
const originalWorkDir = process.env.OPENPALM_WORK_DIR;
|
|
44
|
+
const originalAdminToken = process.env.ADMIN_TOKEN;
|
|
45
|
+
const originalOpenPalmAdminToken = process.env.OPENPALM_ADMIN_TOKEN;
|
|
43
46
|
|
|
44
47
|
afterEach(() => {
|
|
45
48
|
globalThis.fetch = originalFetch;
|
|
46
49
|
console.log = originalLog;
|
|
50
|
+
console.warn = originalWarn;
|
|
47
51
|
restoreDockerCli();
|
|
48
52
|
process.env.OPENPALM_CONFIG_HOME = originalConfigHome;
|
|
49
53
|
process.env.OPENPALM_DATA_HOME = originalDataHome;
|
|
50
54
|
process.env.OPENPALM_STATE_HOME = originalStateHome;
|
|
51
55
|
process.env.OPENPALM_WORK_DIR = originalWorkDir;
|
|
56
|
+
process.env.ADMIN_TOKEN = originalAdminToken;
|
|
57
|
+
process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
|
|
52
58
|
});
|
|
53
59
|
|
|
54
60
|
it('calls containers pull for update', async () => {
|
|
55
61
|
const calls: string[] = [];
|
|
62
|
+
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
56
63
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
57
64
|
calls.push(String(input));
|
|
58
65
|
return new Response('{"ok":true}', { status: 200 });
|
|
@@ -64,8 +71,54 @@ describe('cli main', () => {
|
|
|
64
71
|
expect(calls).toEqual(['http://localhost:8100/admin/containers/pull']);
|
|
65
72
|
});
|
|
66
73
|
|
|
67
|
-
it('
|
|
74
|
+
it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
|
|
75
|
+
const adminTokens: string[] = [];
|
|
76
|
+
process.env.ADMIN_TOKEN = 'env-admin-token';
|
|
77
|
+
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
78
|
+
|
|
79
|
+
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
80
|
+
const headers = new Headers(init?.headers);
|
|
81
|
+
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
82
|
+
return new Response('{"ok":true}', { status: 200 });
|
|
83
|
+
}) as typeof fetch;
|
|
84
|
+
console.log = mock(() => {}) as typeof console.log;
|
|
85
|
+
|
|
86
|
+
await main(['update']);
|
|
87
|
+
|
|
88
|
+
expect(adminTokens).toEqual(['env-admin-token']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('falls back to the legacy parent secrets.env for admin requests', async () => {
|
|
92
|
+
const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
|
|
93
|
+
const configHome = join(base, 'openpalm');
|
|
94
|
+
const adminTokens: string[] = [];
|
|
95
|
+
|
|
96
|
+
mkdirSync(configHome, { recursive: true });
|
|
97
|
+
writeFileSync(join(configHome, 'secrets.env'), 'OPENPALM_ADMIN_TOKEN=\nADMIN_TOKEN=\n');
|
|
98
|
+
writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
|
|
99
|
+
|
|
100
|
+
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
101
|
+
delete process.env.ADMIN_TOKEN;
|
|
102
|
+
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
103
|
+
|
|
104
|
+
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
105
|
+
const headers = new Headers(init?.headers);
|
|
106
|
+
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
107
|
+
return new Response('{"ok":true}', { status: 200 });
|
|
108
|
+
}) as typeof fetch;
|
|
109
|
+
console.log = mock(() => {}) as typeof console.log;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await main(['update']);
|
|
113
|
+
expect(adminTokens).toEqual(['legacy-admin-token']);
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(base, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('calls admin install when stack is already running and token exists', async () => {
|
|
68
120
|
const calls: string[] = [];
|
|
121
|
+
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
69
122
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
70
123
|
const url = String(input);
|
|
71
124
|
calls.push(url);
|
|
@@ -79,13 +132,59 @@ describe('cli main', () => {
|
|
|
79
132
|
await main(['install']);
|
|
80
133
|
|
|
81
134
|
expect(calls).toEqual([
|
|
82
|
-
'http://
|
|
135
|
+
'http://localhost:8100/health',
|
|
83
136
|
'http://localhost:8100/admin/install',
|
|
84
137
|
]);
|
|
85
138
|
});
|
|
86
139
|
|
|
87
|
-
it('
|
|
88
|
-
|
|
140
|
+
it('falls back to bootstrap when stack is running but no token exists', async () => {
|
|
141
|
+
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
142
|
+
const configHome = join(base, 'config');
|
|
143
|
+
const dataHome = join(base, 'data');
|
|
144
|
+
const stateHome = join(base, 'state');
|
|
145
|
+
const workDir = join(base, 'work');
|
|
146
|
+
const binDir = join(stateHome, 'bin');
|
|
147
|
+
|
|
148
|
+
mkdirSync(binDir, { recursive: true });
|
|
149
|
+
writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
|
|
150
|
+
chmodSync(join(binDir, 'varlock'), 0o755);
|
|
151
|
+
|
|
152
|
+
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
153
|
+
process.env.OPENPALM_DATA_HOME = dataHome;
|
|
154
|
+
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
155
|
+
process.env.OPENPALM_WORK_DIR = workDir;
|
|
156
|
+
delete process.env.ADMIN_TOKEN;
|
|
157
|
+
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
158
|
+
|
|
159
|
+
mockDockerCli();
|
|
160
|
+
const fetchedUrls: string[] = [];
|
|
161
|
+
globalThis.fetch = mock(async (input: string | URL) => {
|
|
162
|
+
const url = String(input);
|
|
163
|
+
fetchedUrls.push(url);
|
|
164
|
+
if (url.endsWith('/health')) {
|
|
165
|
+
return new Response('ok', { status: 200 });
|
|
166
|
+
}
|
|
167
|
+
if (url.includes('/docker-compose.yml')) {
|
|
168
|
+
return new Response('services: {}\n', { status: 200 });
|
|
169
|
+
}
|
|
170
|
+
if (url.includes('/Caddyfile')) {
|
|
171
|
+
return new Response(':80 {\n}\n', { status: 200 });
|
|
172
|
+
}
|
|
173
|
+
if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
|
|
174
|
+
return new Response('KEY=string\n', { status: 200 });
|
|
175
|
+
}
|
|
176
|
+
return new Response('', { status: 503 });
|
|
177
|
+
}) as typeof fetch;
|
|
178
|
+
console.log = mock(() => {}) as typeof console.log;
|
|
179
|
+
console.warn = mock(() => {}) as typeof console.warn;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await main(['install', '--no-start', '--force', '--no-open']);
|
|
183
|
+
// Should have fallen through to bootstrap, creating directories
|
|
184
|
+
expect(existsSync(join(dataHome, 'admin'))).toBe(true);
|
|
185
|
+
} finally {
|
|
186
|
+
rmSync(base, { recursive: true, force: true });
|
|
187
|
+
}
|
|
89
188
|
});
|
|
90
189
|
|
|
91
190
|
it('creates the admin data directory during bootstrap install', async () => {
|
|
@@ -134,18 +233,13 @@ describe('cli main', () => {
|
|
|
134
233
|
});
|
|
135
234
|
|
|
136
235
|
describe('validate command', () => {
|
|
137
|
-
it('
|
|
138
|
-
// validate is now a known command, so it won't throw Unknown command.
|
|
139
|
-
// It will fail because varlock exits non-zero on a missing env/schema, but that's a different error.
|
|
140
|
-
// We set up a temp state dir with a fake varlock binary that exits immediately to avoid a network download.
|
|
141
|
-
|
|
236
|
+
it('is a recognized command (does not throw Unknown command)', async () => {
|
|
142
237
|
const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
143
238
|
const binDir = join(tempStateHome, 'bin');
|
|
144
239
|
const artifactsDir = join(tempStateHome, 'artifacts');
|
|
145
240
|
mkdirSync(binDir, { recursive: true });
|
|
146
241
|
mkdirSync(artifactsDir, { recursive: true });
|
|
147
242
|
|
|
148
|
-
// Create a fake varlock script that exits 1 immediately
|
|
149
243
|
const fakeVarlock = join(binDir, 'varlock');
|
|
150
244
|
writeFileSync(fakeVarlock, '#!/bin/sh\nexit 1\n');
|
|
151
245
|
chmodSync(fakeVarlock, 0o755);
|
|
@@ -153,32 +247,22 @@ describe('validate command', () => {
|
|
|
153
247
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
154
248
|
const originalExit = process.exit;
|
|
155
249
|
process.env.OPENPALM_STATE_HOME = tempStateHome;
|
|
156
|
-
// Prevent process.exit from terminating the test runner
|
|
157
250
|
process.exit = mock((_code?: number) => { throw new Error(`process.exit(${_code})`); }) as typeof process.exit;
|
|
158
251
|
|
|
159
252
|
try {
|
|
160
253
|
const err = await main(['validate']).catch((e: unknown) => e);
|
|
161
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
-
expect(message).not.toContain('Unknown command
|
|
255
|
+
expect(message).not.toContain('Unknown command');
|
|
163
256
|
} finally {
|
|
164
257
|
process.exit = originalExit;
|
|
165
258
|
process.env.OPENPALM_STATE_HOME = originalStateHome;
|
|
166
259
|
rmSync(tempStateHome, { recursive: true, force: true });
|
|
167
260
|
}
|
|
168
261
|
});
|
|
169
|
-
|
|
170
262
|
});
|
|
171
263
|
|
|
172
264
|
describe('scan command', () => {
|
|
173
265
|
it('is a recognized command (does not throw Unknown command)', async () => {
|
|
174
|
-
// Verifies 'scan' is in the COMMANDS list and dispatches correctly.
|
|
175
|
-
// Does NOT test full scan behavior (which requires a real varlock binary,
|
|
176
|
-
// secrets.env, and secrets.env.schema). A more complete test would:
|
|
177
|
-
// - Stage a secrets.env.schema + secrets.env in temp dirs
|
|
178
|
-
// - Provide a fake varlock binary that echoes its args
|
|
179
|
-
// - Assert varlock is invoked with 'scan --path <tmpDir>/'
|
|
180
|
-
// - Verify the temp dir is cleaned up after execution
|
|
181
|
-
|
|
182
266
|
const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
183
267
|
const tempConfigHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
184
268
|
const binDir = join(tempStateHome, 'bin');
|
|
@@ -186,12 +270,10 @@ describe('scan command', () => {
|
|
|
186
270
|
mkdirSync(binDir, { recursive: true });
|
|
187
271
|
mkdirSync(artifactsDir, { recursive: true });
|
|
188
272
|
|
|
189
|
-
// Create a fake varlock script that exits 0 immediately
|
|
190
273
|
const fakeVarlock = join(binDir, 'varlock');
|
|
191
274
|
writeFileSync(fakeVarlock, '#!/bin/sh\nexit 0\n');
|
|
192
275
|
chmodSync(fakeVarlock, 0o755);
|
|
193
276
|
|
|
194
|
-
// Create required files so the command reaches the varlock invocation
|
|
195
277
|
writeFileSync(join(artifactsDir, 'secrets.env.schema'), 'ADMIN_TOKEN\n');
|
|
196
278
|
writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
|
|
197
279
|
|
|
@@ -205,9 +287,7 @@ describe('scan command', () => {
|
|
|
205
287
|
try {
|
|
206
288
|
const err = await main(['scan']).catch((e: unknown) => e);
|
|
207
289
|
const message = err instanceof Error ? err.message : String(err);
|
|
208
|
-
|
|
209
|
-
expect(message).not.toContain('Unknown command: scan');
|
|
210
|
-
// The fake varlock exits 0, so process.exit(0) should be called
|
|
290
|
+
expect(message).not.toContain('Unknown command');
|
|
211
291
|
expect(message).toBe('process.exit(0)');
|
|
212
292
|
} finally {
|
|
213
293
|
process.exit = originalExit;
|
|
@@ -224,7 +304,6 @@ describe('scan command', () => {
|
|
|
224
304
|
const artifactsDir = join(tempStateHome, 'artifacts');
|
|
225
305
|
mkdirSync(artifactsDir, { recursive: true });
|
|
226
306
|
|
|
227
|
-
// secrets.env exists but secrets.env.schema does NOT
|
|
228
307
|
writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
|
|
229
308
|
|
|
230
309
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
@@ -349,4 +428,33 @@ describe('install image tag pinning', () => {
|
|
|
349
428
|
'KEY.WITH-HYPHEN=new\n',
|
|
350
429
|
);
|
|
351
430
|
});
|
|
431
|
+
|
|
432
|
+
it('preserves export prefix when upserting a key', () => {
|
|
433
|
+
expect(upsertEnvValue('export OPENPALM_ADMIN_TOKEN=old\n', 'OPENPALM_ADMIN_TOKEN', 'new')).toBe(
|
|
434
|
+
'export OPENPALM_ADMIN_TOKEN=new\n',
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('upserts without export prefix when original has none', () => {
|
|
439
|
+
expect(upsertEnvValue('OPENPALM_IMAGE_TAG=latest\n', 'OPENPALM_IMAGE_TAG', 'v1.0.0')).toBe(
|
|
440
|
+
'OPENPALM_IMAGE_TAG=v1.0.0\n',
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('secrets.env generation', () => {
|
|
446
|
+
it('generates secrets.env with export prefix and OPENPALM_ADMIN_TOKEN', async () => {
|
|
447
|
+
const { ensureSecrets } = await import('./lib/env.ts');
|
|
448
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'openpalm-secrets-'));
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await ensureSecrets(tempDir);
|
|
452
|
+
const content = await Bun.file(join(tempDir, 'secrets.env')).text();
|
|
453
|
+
expect(content).toContain('export OPENPALM_ADMIN_TOKEN=');
|
|
454
|
+
expect(content).toContain('export OPENAI_API_KEY=');
|
|
455
|
+
expect(content).toContain('export MEMORY_USER_ID=');
|
|
456
|
+
} finally {
|
|
457
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
352
460
|
});
|