openpalm 0.9.9 → 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 +2 -2
- package/src/commands/install.ts +31 -6
- package/src/lib/env.ts +7 -1
- package/src/main.test.ts +81 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpalm",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build:windows-arm64": "bun build src/main.ts --compile --target=bun-windows-arm64 --outfile dist/openpalm-cli-windows-arm64.exe"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@openpalm/lib": "0.9.
|
|
29
|
+
"@openpalm/lib": ">=0.9.8 <1.0.0",
|
|
30
30
|
"citty": "^0.2.1"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/commands/install.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
4
|
import cliPkg from '../../package.json' with { type: 'json' };
|
|
5
5
|
import { defaultConfigHome, defaultDataHome, defaultStateHome, defaultWorkDir } from '../lib/paths.ts';
|
|
6
|
-
import { ensureSecrets, ensureStackEnv } from '../lib/env.ts';
|
|
6
|
+
import { ensureSecrets, ensureStackEnv, resolveRequestedImageTag } from '../lib/env.ts';
|
|
7
7
|
import { ensureDirectoryTree, fetchAsset, runDockerCompose, openBrowser } from '../lib/docker.ts';
|
|
8
8
|
import {
|
|
9
9
|
ensureOpenCodeConfig, ensureOpenCodeSystemConfig, ensureAdminOpenCodeConfig, FilesystemAssetProvider,
|
|
@@ -16,9 +16,31 @@ import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '..
|
|
|
16
16
|
import { createSetupServer } from '../setup-wizard/server.ts';
|
|
17
17
|
import { buildInstallServiceNames, buildDeployStatusEntries } from './install-services.ts';
|
|
18
18
|
|
|
19
|
-
const DEFAULT_INSTALL_REF = 'main';
|
|
20
19
|
const SETUP_WIZARD_PORT = Number(process.env.OPENPALM_SETUP_PORT) || 8100;
|
|
21
20
|
|
|
21
|
+
const REPO_OWNER = 'itlackey';
|
|
22
|
+
const REPO_NAME = 'openpalm';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the latest release tag from GitHub. Falls back to the CLI package
|
|
26
|
+
* version (prefixed with 'v') so the install never silently defaults to 'main'
|
|
27
|
+
* which produces an un-pinned 'latest' image tag.
|
|
28
|
+
*/
|
|
29
|
+
async function resolveDefaultInstallRef(): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(
|
|
32
|
+
`https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
|
|
33
|
+
{ redirect: 'manual', signal: AbortSignal.timeout(10000) },
|
|
34
|
+
);
|
|
35
|
+
const location = res.headers.get('location') ?? '';
|
|
36
|
+
const match = location.match(/\/tag\/(v[0-9]+\.[0-9]+\.[0-9]+[^\s]*)$/);
|
|
37
|
+
if (match?.[1]) return match[1];
|
|
38
|
+
} catch {
|
|
39
|
+
// Network error — fall through to package version
|
|
40
|
+
}
|
|
41
|
+
return cliPkg.version ? `v${cliPkg.version}` : 'main';
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
export default defineCommand({
|
|
23
45
|
meta: {
|
|
24
46
|
name: 'install',
|
|
@@ -32,8 +54,7 @@ export default defineCommand({
|
|
|
32
54
|
},
|
|
33
55
|
version: {
|
|
34
56
|
type: 'string',
|
|
35
|
-
description: 'Install specific repository ref (default:
|
|
36
|
-
default: DEFAULT_INSTALL_REF,
|
|
57
|
+
description: 'Install specific repository ref (default: latest release)',
|
|
37
58
|
},
|
|
38
59
|
start: {
|
|
39
60
|
type: 'boolean',
|
|
@@ -52,9 +73,10 @@ export default defineCommand({
|
|
|
52
73
|
},
|
|
53
74
|
},
|
|
54
75
|
async run({ args }) {
|
|
76
|
+
const version = args.version || await resolveDefaultInstallRef();
|
|
55
77
|
await bootstrapInstall({
|
|
56
78
|
force: args.force,
|
|
57
|
-
version
|
|
79
|
+
version,
|
|
58
80
|
noStart: !args.start,
|
|
59
81
|
noOpen: !args.open,
|
|
60
82
|
file: args.file,
|
|
@@ -147,7 +169,10 @@ export async function bootstrapInstall(options: InstallOptions): Promise<void> {
|
|
|
147
169
|
);
|
|
148
170
|
|
|
149
171
|
await ensureSecrets(configHome);
|
|
150
|
-
|
|
172
|
+
// Derive the image tag from the resolved version so that stale or
|
|
173
|
+
// architecture-suffixed OPENPALM_IMAGE_TAG env vars don't leak in.
|
|
174
|
+
const imageTag = resolveRequestedImageTag(options.version) ?? undefined;
|
|
175
|
+
await ensureStackEnv(configHome, dataHome, stateHome, workDir, options.version, imageTag);
|
|
151
176
|
// Seed OpenCode config — non-fatal since performSetup() also seeds these
|
|
152
177
|
try {
|
|
153
178
|
const fsAssets = new FilesystemAssetProvider(dataHome);
|
package/src/lib/env.ts
CHANGED
|
@@ -96,6 +96,11 @@ export MEMORY_AUTH_TOKEN=${randomBytes(32).toString('hex')}
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Creates or updates the stack.env bootstrap file.
|
|
99
|
+
*
|
|
100
|
+
* When `imageTagOverride` is provided (e.g. derived from --version during
|
|
101
|
+
* install), it takes precedence over both the OPENPALM_IMAGE_TAG env var
|
|
102
|
+
* and the repo-ref heuristic. This prevents stale or architecture-suffixed
|
|
103
|
+
* env vars (e.g. "latest-arm64") from leaking into the stack.
|
|
99
104
|
*/
|
|
100
105
|
export async function ensureStackEnv(
|
|
101
106
|
configHome: string,
|
|
@@ -103,10 +108,11 @@ export async function ensureStackEnv(
|
|
|
103
108
|
stateHome: string,
|
|
104
109
|
workDir: string,
|
|
105
110
|
repoRef: string,
|
|
111
|
+
imageTagOverride?: string,
|
|
106
112
|
): Promise<void> {
|
|
107
113
|
const dataStackEnv = join(dataHome, 'stack.env');
|
|
108
114
|
const stagedStackEnv = join(stateHome, 'artifacts', 'stack.env');
|
|
109
|
-
const explicitImageTag = process.env.OPENPALM_IMAGE_TAG;
|
|
115
|
+
const explicitImageTag = imageTagOverride ?? process.env.OPENPALM_IMAGE_TAG;
|
|
110
116
|
const hasExplicitImageTag = explicitImageTag !== undefined && explicitImageTag !== '';
|
|
111
117
|
if (!(await Bun.file(dataStackEnv).exists())) {
|
|
112
118
|
const defaultImageTag = hasExplicitImageTag
|
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
|
|
@@ -151,7 +152,7 @@ describe('cli main', () => {
|
|
|
151
152
|
}
|
|
152
153
|
});
|
|
153
154
|
|
|
154
|
-
it('
|
|
155
|
+
it('resolves version-pinned install ref (falls back to CLI package version)', async () => {
|
|
155
156
|
const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
|
|
156
157
|
const configHome = join(base, 'config');
|
|
157
158
|
const dataHome = join(base, 'data');
|
|
@@ -169,6 +170,12 @@ describe('cli main', () => {
|
|
|
169
170
|
process.env.OPENPALM_STATE_HOME = stateHome;
|
|
170
171
|
process.env.OPENPALM_WORK_DIR = workDir;
|
|
171
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
|
+
|
|
172
179
|
mockDockerCli();
|
|
173
180
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
174
181
|
const url = String(input);
|
|
@@ -176,13 +183,14 @@ describe('cli main', () => {
|
|
|
176
183
|
if (url.endsWith('/health')) {
|
|
177
184
|
throw new TypeError('fetch failed');
|
|
178
185
|
}
|
|
179
|
-
|
|
186
|
+
// Respond to version-pinned asset URLs
|
|
187
|
+
if (url.includes('/docker-compose.yml')) {
|
|
180
188
|
return new Response('services: {}\n', { status: 200 });
|
|
181
189
|
}
|
|
182
|
-
if (url.includes('/
|
|
190
|
+
if (url.includes('/Caddyfile')) {
|
|
183
191
|
return new Response(':80 {\n}\n', { status: 200 });
|
|
184
192
|
}
|
|
185
|
-
if (url.includes('/
|
|
193
|
+
if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
|
|
186
194
|
return new Response('KEY=string\n', { status: 200 });
|
|
187
195
|
}
|
|
188
196
|
return new Response('', { status: 503 });
|
|
@@ -193,12 +201,15 @@ describe('cli main', () => {
|
|
|
193
201
|
try {
|
|
194
202
|
await main(['install', '--no-start', '--force', '--no-open']);
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
199
|
-
expect(
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
|
202
213
|
} finally {
|
|
203
214
|
rmSync(base, { recursive: true, force: true });
|
|
204
215
|
}
|
|
@@ -219,6 +230,65 @@ describe('npm bin launcher', () => {
|
|
|
219
230
|
|
|
220
231
|
expect(launcher.startsWith('#!/usr/bin/env bun\n')).toBe(true);
|
|
221
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
|
+
});
|
|
222
292
|
});
|
|
223
293
|
|
|
224
294
|
describe('validate command', () => {
|