openpalm 0.9.2 → 0.9.4
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 +4 -1
- 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 +132 -0
- package/src/main.test.ts +87 -28
- package/src/main.ts +33 -984
package/src/main.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ 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;
|
|
@@ -46,6 +47,7 @@ describe('cli main', () => {
|
|
|
46
47
|
afterEach(() => {
|
|
47
48
|
globalThis.fetch = originalFetch;
|
|
48
49
|
console.log = originalLog;
|
|
50
|
+
console.warn = originalWarn;
|
|
49
51
|
restoreDockerCli();
|
|
50
52
|
process.env.OPENPALM_CONFIG_HOME = originalConfigHome;
|
|
51
53
|
process.env.OPENPALM_DATA_HOME = originalDataHome;
|
|
@@ -57,6 +59,7 @@ describe('cli main', () => {
|
|
|
57
59
|
|
|
58
60
|
it('calls containers pull for update', async () => {
|
|
59
61
|
const calls: string[] = [];
|
|
62
|
+
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
60
63
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
61
64
|
calls.push(String(input));
|
|
62
65
|
return new Response('{"ok":true}', { status: 200 });
|
|
@@ -91,7 +94,7 @@ describe('cli main', () => {
|
|
|
91
94
|
const adminTokens: string[] = [];
|
|
92
95
|
|
|
93
96
|
mkdirSync(configHome, { recursive: true });
|
|
94
|
-
writeFileSync(join(configHome, 'secrets.env'), '
|
|
97
|
+
writeFileSync(join(configHome, 'secrets.env'), 'OPENPALM_ADMIN_TOKEN=\nADMIN_TOKEN=\n');
|
|
95
98
|
writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
|
|
96
99
|
|
|
97
100
|
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
@@ -113,8 +116,9 @@ describe('cli main', () => {
|
|
|
113
116
|
}
|
|
114
117
|
});
|
|
115
118
|
|
|
116
|
-
it('calls admin install when stack is already running', async () => {
|
|
119
|
+
it('calls admin install when stack is already running and token exists', async () => {
|
|
117
120
|
const calls: string[] = [];
|
|
121
|
+
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
118
122
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
119
123
|
const url = String(input);
|
|
120
124
|
calls.push(url);
|
|
@@ -128,13 +132,59 @@ describe('cli main', () => {
|
|
|
128
132
|
await main(['install']);
|
|
129
133
|
|
|
130
134
|
expect(calls).toEqual([
|
|
131
|
-
'http://
|
|
135
|
+
'http://localhost:8100/health',
|
|
132
136
|
'http://localhost:8100/admin/install',
|
|
133
137
|
]);
|
|
134
138
|
});
|
|
135
139
|
|
|
136
|
-
it('
|
|
137
|
-
|
|
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
|
+
}
|
|
138
188
|
});
|
|
139
189
|
|
|
140
190
|
it('creates the admin data directory during bootstrap install', async () => {
|
|
@@ -183,18 +233,13 @@ describe('cli main', () => {
|
|
|
183
233
|
});
|
|
184
234
|
|
|
185
235
|
describe('validate command', () => {
|
|
186
|
-
it('
|
|
187
|
-
// validate is now a known command, so it won't throw Unknown command.
|
|
188
|
-
// It will fail because varlock exits non-zero on a missing env/schema, but that's a different error.
|
|
189
|
-
// We set up a temp state dir with a fake varlock binary that exits immediately to avoid a network download.
|
|
190
|
-
|
|
236
|
+
it('is a recognized command (does not throw Unknown command)', async () => {
|
|
191
237
|
const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
192
238
|
const binDir = join(tempStateHome, 'bin');
|
|
193
239
|
const artifactsDir = join(tempStateHome, 'artifacts');
|
|
194
240
|
mkdirSync(binDir, { recursive: true });
|
|
195
241
|
mkdirSync(artifactsDir, { recursive: true });
|
|
196
242
|
|
|
197
|
-
// Create a fake varlock script that exits 1 immediately
|
|
198
243
|
const fakeVarlock = join(binDir, 'varlock');
|
|
199
244
|
writeFileSync(fakeVarlock, '#!/bin/sh\nexit 1\n');
|
|
200
245
|
chmodSync(fakeVarlock, 0o755);
|
|
@@ -202,32 +247,22 @@ describe('validate command', () => {
|
|
|
202
247
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
203
248
|
const originalExit = process.exit;
|
|
204
249
|
process.env.OPENPALM_STATE_HOME = tempStateHome;
|
|
205
|
-
// Prevent process.exit from terminating the test runner
|
|
206
250
|
process.exit = mock((_code?: number) => { throw new Error(`process.exit(${_code})`); }) as typeof process.exit;
|
|
207
251
|
|
|
208
252
|
try {
|
|
209
253
|
const err = await main(['validate']).catch((e: unknown) => e);
|
|
210
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
-
expect(message).not.toContain('Unknown command
|
|
255
|
+
expect(message).not.toContain('Unknown command');
|
|
212
256
|
} finally {
|
|
213
257
|
process.exit = originalExit;
|
|
214
258
|
process.env.OPENPALM_STATE_HOME = originalStateHome;
|
|
215
259
|
rmSync(tempStateHome, { recursive: true, force: true });
|
|
216
260
|
}
|
|
217
261
|
});
|
|
218
|
-
|
|
219
262
|
});
|
|
220
263
|
|
|
221
264
|
describe('scan command', () => {
|
|
222
265
|
it('is a recognized command (does not throw Unknown command)', async () => {
|
|
223
|
-
// Verifies 'scan' is in the COMMANDS list and dispatches correctly.
|
|
224
|
-
// Does NOT test full scan behavior (which requires a real varlock binary,
|
|
225
|
-
// secrets.env, and secrets.env.schema). A more complete test would:
|
|
226
|
-
// - Stage a secrets.env.schema + secrets.env in temp dirs
|
|
227
|
-
// - Provide a fake varlock binary that echoes its args
|
|
228
|
-
// - Assert varlock is invoked with 'scan --path <tmpDir>/'
|
|
229
|
-
// - Verify the temp dir is cleaned up after execution
|
|
230
|
-
|
|
231
266
|
const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
232
267
|
const tempConfigHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
|
|
233
268
|
const binDir = join(tempStateHome, 'bin');
|
|
@@ -235,12 +270,10 @@ describe('scan command', () => {
|
|
|
235
270
|
mkdirSync(binDir, { recursive: true });
|
|
236
271
|
mkdirSync(artifactsDir, { recursive: true });
|
|
237
272
|
|
|
238
|
-
// Create a fake varlock script that exits 0 immediately
|
|
239
273
|
const fakeVarlock = join(binDir, 'varlock');
|
|
240
274
|
writeFileSync(fakeVarlock, '#!/bin/sh\nexit 0\n');
|
|
241
275
|
chmodSync(fakeVarlock, 0o755);
|
|
242
276
|
|
|
243
|
-
// Create required files so the command reaches the varlock invocation
|
|
244
277
|
writeFileSync(join(artifactsDir, 'secrets.env.schema'), 'ADMIN_TOKEN\n');
|
|
245
278
|
writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
|
|
246
279
|
|
|
@@ -254,9 +287,7 @@ describe('scan command', () => {
|
|
|
254
287
|
try {
|
|
255
288
|
const err = await main(['scan']).catch((e: unknown) => e);
|
|
256
289
|
const message = err instanceof Error ? err.message : String(err);
|
|
257
|
-
|
|
258
|
-
expect(message).not.toContain('Unknown command: scan');
|
|
259
|
-
// The fake varlock exits 0, so process.exit(0) should be called
|
|
290
|
+
expect(message).not.toContain('Unknown command');
|
|
260
291
|
expect(message).toBe('process.exit(0)');
|
|
261
292
|
} finally {
|
|
262
293
|
process.exit = originalExit;
|
|
@@ -273,7 +304,6 @@ describe('scan command', () => {
|
|
|
273
304
|
const artifactsDir = join(tempStateHome, 'artifacts');
|
|
274
305
|
mkdirSync(artifactsDir, { recursive: true });
|
|
275
306
|
|
|
276
|
-
// secrets.env exists but secrets.env.schema does NOT
|
|
277
307
|
writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
|
|
278
308
|
|
|
279
309
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
@@ -398,4 +428,33 @@ describe('install image tag pinning', () => {
|
|
|
398
428
|
'KEY.WITH-HYPHEN=new\n',
|
|
399
429
|
);
|
|
400
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
|
+
});
|
|
401
460
|
});
|