openpalm 0.9.8 → 0.9.10
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 -2
- package/playwright.config.ts +16 -0
- package/src/commands/install-file.test.ts +306 -0
- package/src/commands/install-services.test.ts +12 -7
- package/src/commands/install-services.ts +1 -1
- package/src/commands/install.ts +142 -34
- package/src/commands/restart.ts +2 -35
- package/src/commands/service.ts +0 -17
- package/src/commands/start.ts +5 -43
- package/src/commands/status.ts +0 -9
- package/src/commands/stop.ts +4 -36
- package/src/commands/uninstall.ts +23 -14
- package/src/commands/update.ts +0 -9
- package/src/lib/docker.ts +25 -7
- package/src/lib/env.ts +13 -60
- package/src/lib/paths.ts +11 -1
- package/src/lib/staging.ts +3 -3
- package/src/main.test.ts +118 -80
- package/src/setup-wizard/index.html +114 -180
- package/src/setup-wizard/server-errors.test.ts +429 -0
- package/src/setup-wizard/server-integration.test.ts +511 -0
- package/src/setup-wizard/server.test.ts +6 -6
- package/src/setup-wizard/server.ts +17 -5
- package/src/setup-wizard/standalone.ts +166 -0
- package/src/setup-wizard/wizard.css +892 -299
- package/src/setup-wizard/wizard.js +1172 -559
- package/src/lib/admin.ts +0 -107
package/src/main.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { detectHostInfo, main, reconcileStackEnvImageTag, resolveRequestedImageTag, upsertEnvValue } from './main.ts';
|
|
6
7
|
|
|
7
8
|
// Helpers to mock Bun.spawn and Bun.which for tests that would otherwise
|
|
@@ -57,92 +58,57 @@ describe('cli main', () => {
|
|
|
57
58
|
process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
it('
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
68
|
-
|
|
69
|
-
await main(['update']);
|
|
70
|
-
|
|
71
|
-
// tryAdminRequest attempts directly — no separate health check
|
|
72
|
-
expect(calls).toEqual([
|
|
73
|
-
'http://localhost:8100/admin/containers/pull',
|
|
74
|
-
]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
|
|
78
|
-
const adminTokens: string[] = [];
|
|
79
|
-
process.env.ADMIN_TOKEN = 'env-admin-token';
|
|
80
|
-
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
81
|
-
|
|
82
|
-
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
83
|
-
const headers = new Headers(init?.headers);
|
|
84
|
-
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
85
|
-
return new Response('{"ok":true}', { status: 200 });
|
|
86
|
-
}) as typeof fetch;
|
|
87
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
88
|
-
|
|
89
|
-
await main(['update']);
|
|
90
|
-
|
|
91
|
-
// Direct request — single call with token header
|
|
92
|
-
expect(adminTokens).toEqual(['env-admin-token']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('falls back to the legacy parent secrets.env for admin requests', async () => {
|
|
96
|
-
const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
|
|
97
|
-
const configHome = join(base, 'openpalm');
|
|
98
|
-
const adminTokens: string[] = [];
|
|
61
|
+
it('runs bootstrap install directly without admin delegation', async () => {
|
|
62
|
+
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
63
|
+
const configHome = join(base, 'config');
|
|
64
|
+
const dataHome = join(base, 'data');
|
|
65
|
+
const stateHome = join(base, 'state');
|
|
66
|
+
const workDir = join(base, 'work');
|
|
67
|
+
const binDir = join(stateHome, 'bin');
|
|
99
68
|
|
|
100
|
-
mkdirSync(
|
|
101
|
-
writeFileSync(join(
|
|
102
|
-
|
|
69
|
+
mkdirSync(binDir, { recursive: true });
|
|
70
|
+
writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
|
|
71
|
+
chmodSync(join(binDir, 'varlock'), 0o755);
|
|
103
72
|
|
|
104
73
|
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
74
|
+
process.env.OPENPALM_DATA_HOME = dataHome;
|
|
75
|
+
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
76
|
+
process.env.OPENPALM_WORK_DIR = workDir;
|
|
105
77
|
delete process.env.ADMIN_TOKEN;
|
|
106
78
|
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
107
79
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
111
|
-
return new Response('{"ok":true}', { status: 200 });
|
|
112
|
-
}) as typeof fetch;
|
|
113
|
-
console.log = mock(() => {}) as typeof console.log;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await main(['update']);
|
|
117
|
-
// Direct request — single call with legacy token
|
|
118
|
-
expect(adminTokens).toEqual(['legacy-admin-token']);
|
|
119
|
-
} finally {
|
|
120
|
-
rmSync(base, { recursive: true, force: true });
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('calls admin install when stack is already running and token exists', async () => {
|
|
125
|
-
const calls: string[] = [];
|
|
126
|
-
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
80
|
+
mockDockerCli();
|
|
81
|
+
const fetchedUrls: string[] = [];
|
|
127
82
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
128
83
|
const url = String(input);
|
|
129
|
-
|
|
84
|
+
fetchedUrls.push(url);
|
|
130
85
|
if (url.endsWith('/health')) {
|
|
131
86
|
return new Response('ok', { status: 200 });
|
|
132
87
|
}
|
|
133
|
-
|
|
88
|
+
if (url.includes('/docker-compose.yml')) {
|
|
89
|
+
return new Response('services: {}\n', { status: 200 });
|
|
90
|
+
}
|
|
91
|
+
if (url.includes('/Caddyfile')) {
|
|
92
|
+
return new Response(':80 {\n}\n', { status: 200 });
|
|
93
|
+
}
|
|
94
|
+
if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
|
|
95
|
+
return new Response('KEY=string\n', { status: 200 });
|
|
96
|
+
}
|
|
97
|
+
return new Response('', { status: 503 });
|
|
134
98
|
}) as typeof fetch;
|
|
135
99
|
console.log = mock(() => {}) as typeof console.log;
|
|
100
|
+
console.warn = mock(() => {}) as typeof console.warn;
|
|
136
101
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
102
|
+
try {
|
|
103
|
+
await main(['install', '--no-start', '--force', '--no-open']);
|
|
104
|
+
// Bootstrap runs directly, creating directories
|
|
105
|
+
expect(existsSync(join(dataHome, 'admin'))).toBe(true);
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(base, { recursive: true, force: true });
|
|
108
|
+
}
|
|
143
109
|
});
|
|
144
110
|
|
|
145
|
-
it('
|
|
111
|
+
it('creates the admin data directory during bootstrap install', async () => {
|
|
146
112
|
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
147
113
|
const configHome = join(base, 'config');
|
|
148
114
|
const dataHome = join(base, 'data');
|
|
@@ -158,16 +124,12 @@ describe('cli main', () => {
|
|
|
158
124
|
process.env.OPENPALM_DATA_HOME = dataHome;
|
|
159
125
|
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
160
126
|
process.env.OPENPALM_WORK_DIR = workDir;
|
|
161
|
-
delete process.env.ADMIN_TOKEN;
|
|
162
|
-
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
163
127
|
|
|
164
128
|
mockDockerCli();
|
|
165
|
-
const fetchedUrls: string[] = [];
|
|
166
129
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
167
130
|
const url = String(input);
|
|
168
|
-
fetchedUrls.push(url);
|
|
169
131
|
if (url.endsWith('/health')) {
|
|
170
|
-
|
|
132
|
+
throw new TypeError('fetch failed');
|
|
171
133
|
}
|
|
172
134
|
if (url.includes('/docker-compose.yml')) {
|
|
173
135
|
return new Response('services: {}\n', { status: 200 });
|
|
@@ -181,24 +143,23 @@ describe('cli main', () => {
|
|
|
181
143
|
return new Response('', { status: 503 });
|
|
182
144
|
}) as typeof fetch;
|
|
183
145
|
console.log = mock(() => {}) as typeof console.log;
|
|
184
|
-
console.warn = mock(() => {}) as typeof console.warn;
|
|
185
146
|
|
|
186
147
|
try {
|
|
187
148
|
await main(['install', '--no-start', '--force', '--no-open']);
|
|
188
|
-
// Should have fallen through to bootstrap, creating directories
|
|
189
149
|
expect(existsSync(join(dataHome, 'admin'))).toBe(true);
|
|
190
150
|
} finally {
|
|
191
151
|
rmSync(base, { recursive: true, force: true });
|
|
192
152
|
}
|
|
193
153
|
});
|
|
194
154
|
|
|
195
|
-
it('
|
|
155
|
+
it('resolves version-pinned install ref (falls back to CLI package version)', async () => {
|
|
196
156
|
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
197
157
|
const configHome = join(base, 'config');
|
|
198
158
|
const dataHome = join(base, 'data');
|
|
199
159
|
const stateHome = join(base, 'state');
|
|
200
160
|
const workDir = join(base, 'work');
|
|
201
161
|
const binDir = join(stateHome, 'bin');
|
|
162
|
+
const fetchedUrls: string[] = [];
|
|
202
163
|
|
|
203
164
|
mkdirSync(binDir, { recursive: true });
|
|
204
165
|
writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
|
|
@@ -209,12 +170,20 @@ describe('cli main', () => {
|
|
|
209
170
|
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
210
171
|
process.env.OPENPALM_WORK_DIR = workDir;
|
|
211
172
|
|
|
173
|
+
// Read the CLI package version to verify pinning behaviour
|
|
174
|
+
const cliPkg = JSON.parse(
|
|
175
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
176
|
+
) as { version: string };
|
|
177
|
+
const expectedRef = `v${cliPkg.version}`;
|
|
178
|
+
|
|
212
179
|
mockDockerCli();
|
|
213
180
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
214
181
|
const url = String(input);
|
|
182
|
+
fetchedUrls.push(url);
|
|
215
183
|
if (url.endsWith('/health')) {
|
|
216
184
|
throw new TypeError('fetch failed');
|
|
217
185
|
}
|
|
186
|
+
// Respond to version-pinned asset URLs
|
|
218
187
|
if (url.includes('/docker-compose.yml')) {
|
|
219
188
|
return new Response('services: {}\n', { status: 200 });
|
|
220
189
|
}
|
|
@@ -227,10 +196,20 @@ describe('cli main', () => {
|
|
|
227
196
|
return new Response('', { status: 503 });
|
|
228
197
|
}) as typeof fetch;
|
|
229
198
|
console.log = mock(() => {}) as typeof console.log;
|
|
199
|
+
console.warn = mock(() => {}) as typeof console.warn;
|
|
230
200
|
|
|
231
201
|
try {
|
|
232
202
|
await main(['install', '--no-start', '--force', '--no-open']);
|
|
233
|
-
|
|
203
|
+
|
|
204
|
+
// Verify that assets were fetched using the version-pinned ref, not 'main'
|
|
205
|
+
const composeUrl = fetchedUrls.find((u) => u.includes('/docker-compose.yml'));
|
|
206
|
+
expect(composeUrl).toBeDefined();
|
|
207
|
+
expect(composeUrl).toContain(expectedRef);
|
|
208
|
+
expect(composeUrl).not.toContain('/main/');
|
|
209
|
+
|
|
210
|
+
const caddyUrl = fetchedUrls.find((u) => u.includes('/Caddyfile'));
|
|
211
|
+
expect(caddyUrl).toBeDefined();
|
|
212
|
+
expect(caddyUrl).toContain(expectedRef);
|
|
234
213
|
} finally {
|
|
235
214
|
rmSync(base, { recursive: true, force: true });
|
|
236
215
|
}
|
|
@@ -251,6 +230,65 @@ describe('npm bin launcher', () => {
|
|
|
251
230
|
|
|
252
231
|
expect(launcher.startsWith('#!/usr/bin/env bun\n')).toBe(true);
|
|
253
232
|
});
|
|
233
|
+
|
|
234
|
+
it('packs a real semver range for @openpalm/lib so published installs can resolve the latest compatible lib', {
|
|
235
|
+
timeout: 15000,
|
|
236
|
+
}, () => {
|
|
237
|
+
const cliPkg = JSON.parse(
|
|
238
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
239
|
+
) as {
|
|
240
|
+
dependencies?: Record<string, string>;
|
|
241
|
+
};
|
|
242
|
+
const libPkg = JSON.parse(
|
|
243
|
+
readFileSync(new URL('../../lib/package.json', import.meta.url), 'utf8'),
|
|
244
|
+
) as {
|
|
245
|
+
version: string;
|
|
246
|
+
};
|
|
247
|
+
const versionMatch = libPkg.version.match(/^(\d+)\.\d+\.\d+(?:-.+)?$/);
|
|
248
|
+
if (!versionMatch) throw new Error(`Unexpected lib version format: ${libPkg.version}`);
|
|
249
|
+
const libMajor = Number.parseInt(versionMatch[1], 10);
|
|
250
|
+
|
|
251
|
+
const expectedRange = `>=${libPkg.version} <${libMajor + 1}.0.0`;
|
|
252
|
+
|
|
253
|
+
expect(cliPkg.dependencies?.['@openpalm/lib']).toBe(expectedRange);
|
|
254
|
+
|
|
255
|
+
const packageDir = fileURLToPath(new URL('../', import.meta.url));
|
|
256
|
+
const packDir = mkdtempSync(join(tmpdir(), 'openpalm-cli-pack-'));
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const pack = Bun.spawnSync(
|
|
260
|
+
[process.execPath, 'pm', 'pack', '--destination', packDir, '--quiet'],
|
|
261
|
+
{
|
|
262
|
+
cwd: packageDir,
|
|
263
|
+
stdout: 'pipe',
|
|
264
|
+
stderr: 'pipe',
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(pack.exitCode).toBe(0);
|
|
269
|
+
|
|
270
|
+
const tarball = readdirSync(packDir).find((name) => name.endsWith('.tgz'));
|
|
271
|
+
if (!tarball) throw new Error('Expected bun pm pack to produce a tarball');
|
|
272
|
+
|
|
273
|
+
const extract = Bun.spawnSync(
|
|
274
|
+
['tar', '-xOf', join(packDir, tarball), 'package/package.json'],
|
|
275
|
+
{
|
|
276
|
+
stdout: 'pipe',
|
|
277
|
+
stderr: 'pipe',
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(extract.exitCode).toBe(0);
|
|
282
|
+
|
|
283
|
+
const packedPkg = JSON.parse(new TextDecoder().decode(extract.stdout)) as {
|
|
284
|
+
dependencies?: Record<string, string>;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
expect(packedPkg.dependencies?.['@openpalm/lib']).toBe(expectedRange);
|
|
288
|
+
} finally {
|
|
289
|
+
rmSync(packDir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
254
292
|
});
|
|
255
293
|
|
|
256
294
|
describe('validate command', () => {
|