openpalm 0.11.0-beta.7 → 0.11.0-beta.9
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 +7 -2
- package/playwright.config.ts +0 -16
- package/src/commands/addon.ts +0 -131
- package/src/commands/automations.ts +0 -63
- package/src/commands/install.ts +0 -305
- package/src/commands/logs.ts +0 -25
- package/src/commands/restart.ts +0 -37
- package/src/commands/rollback.ts +0 -44
- package/src/commands/scan.ts +0 -81
- package/src/commands/self-update.ts +0 -148
- package/src/commands/start.ts +0 -40
- package/src/commands/status.ts +0 -14
- package/src/commands/stop.ts +0 -34
- package/src/commands/uninstall.ts +0 -43
- package/src/commands/update.ts +0 -42
- package/src/commands/validate.ts +0 -28
- package/src/install-flow.test.ts +0 -386
- package/src/lib/admin-skills/index.test.ts +0 -70
- package/src/lib/admin-skills/index.ts +0 -113
- package/src/lib/browser.ts +0 -20
- package/src/lib/cli-compose.ts +0 -60
- package/src/lib/cli-state.ts +0 -26
- package/src/lib/docker.ts +0 -43
- package/src/lib/env.ts +0 -111
- package/src/lib/host-info.ts +0 -47
- package/src/lib/io.ts +0 -130
- package/src/lib/opencode-subprocess.ts +0 -121
- package/src/lib/paths.ts +0 -31
- package/src/lib/ui-server.ts +0 -150
- package/src/main.test.ts +0 -618
- package/src/main.ts +0 -171
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpalm",
|
|
3
|
-
"version": "0.11.0-beta.
|
|
3
|
+
"version": "0.11.0-beta.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"bin": {
|
|
13
13
|
"openpalm": "./bin/openpalm.js"
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
15
20
|
"scripts": {
|
|
16
21
|
"start": "bun run src/main.ts",
|
|
17
22
|
"test": "bun test",
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
"bun": ">=1.0.0"
|
|
32
37
|
},
|
|
33
38
|
"dependencies": {
|
|
34
|
-
"@openpalm/lib": ">=0.11.0-beta.
|
|
39
|
+
"@openpalm/lib": ">=0.11.0-beta.8 <1.0.0",
|
|
35
40
|
"citty": "^0.2.1",
|
|
36
41
|
"yaml": "^2.8.0"
|
|
37
42
|
}
|
package/playwright.config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
const WIZARD_PORT = 18200;
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
testDir: 'e2e',
|
|
7
|
-
webServer: {
|
|
8
|
-
command: `bun run e2e/start-wizard-server.ts ${WIZARD_PORT}`,
|
|
9
|
-
port: WIZARD_PORT,
|
|
10
|
-
stdout: 'pipe',
|
|
11
|
-
reuseExistingServer: !process.env.CI,
|
|
12
|
-
},
|
|
13
|
-
use: {
|
|
14
|
-
baseURL: `http://localhost:${WIZARD_PORT}`,
|
|
15
|
-
},
|
|
16
|
-
});
|
package/src/commands/addon.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
3
|
-
buildComposeCliArgs,
|
|
4
|
-
getAddonServiceNames,
|
|
5
|
-
listAvailableAddonIds,
|
|
6
|
-
listEnabledAddonIds,
|
|
7
|
-
setAddonEnabled,
|
|
8
|
-
} from '@openpalm/lib';
|
|
9
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
10
|
-
import { runComposeWithPreflight } from '../lib/cli-compose.ts';
|
|
11
|
-
import { runDockerCompose } from '../lib/docker.ts';
|
|
12
|
-
|
|
13
|
-
function requireKnownAddon(name: string): void {
|
|
14
|
-
const available = listAvailableAddonIds();
|
|
15
|
-
const hint = available.length > 0 ? ` Run \`openpalm addon list\` to see the available addons.` : '';
|
|
16
|
-
if (!available.includes(name)) {
|
|
17
|
-
throw new Error(`Addon "${name}" is not available. Known addons: ${available.join(', ') || '(none)'}.${hint}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function runAddonListAction(): Promise<void> {
|
|
22
|
-
const state = ensureValidState();
|
|
23
|
-
const enabled = new Set(listEnabledAddonIds(state.homeDir));
|
|
24
|
-
const available = listAvailableAddonIds();
|
|
25
|
-
|
|
26
|
-
if (available.length === 0) {
|
|
27
|
-
console.log('No registry addons are available.');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (const name of available) {
|
|
32
|
-
console.log(`${enabled.has(name) ? '[enabled]' : '[disabled]'} ${name}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function runAddonEnableAction(name: string): Promise<void> {
|
|
37
|
-
requireKnownAddon(name);
|
|
38
|
-
const state = ensureValidState();
|
|
39
|
-
const mutation = setAddonEnabled(state.homeDir, state.stackDir, name, true);
|
|
40
|
-
if (!mutation.ok) throw new Error(mutation.error);
|
|
41
|
-
|
|
42
|
-
if (!mutation.changed) {
|
|
43
|
-
console.log(`Addon "${name}" is already enabled.`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
console.log(`Enabled addon "${name}".`);
|
|
48
|
-
|
|
49
|
-
if (mutation.services.length === 0) return;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const nextState = ensureValidState();
|
|
53
|
-
await runComposeWithPreflight(nextState, ['up', '-d', ...mutation.services]);
|
|
54
|
-
console.log(`Started services: ${mutation.services.join(', ')}`);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.warn(
|
|
57
|
-
`Warning: addon "${name}" was enabled but its services were not started automatically: ${err instanceof Error ? err.message : String(err)}`,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function runAddonDisableAction(name: string): Promise<void> {
|
|
63
|
-
requireKnownAddon(name);
|
|
64
|
-
const state = ensureValidState();
|
|
65
|
-
const services = getAddonServiceNames(state.homeDir, name);
|
|
66
|
-
const wasEnabled = listEnabledAddonIds(state.homeDir).includes(name);
|
|
67
|
-
|
|
68
|
-
if (wasEnabled && services.length > 0) {
|
|
69
|
-
try {
|
|
70
|
-
await runDockerCompose([...buildComposeCliArgs(state), 'stop', ...services]);
|
|
71
|
-
console.log(`Stopped services: ${services.join(', ')}`);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.warn(
|
|
74
|
-
`Warning: failed to stop services for addon "${name}" before disabling it: ${err instanceof Error ? err.message : String(err)}`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const mutation = setAddonEnabled(state.homeDir, state.stackDir, name, false);
|
|
80
|
-
if (!mutation.ok) throw new Error(mutation.error);
|
|
81
|
-
|
|
82
|
-
if (!mutation.changed) {
|
|
83
|
-
console.log(`Addon "${name}" is already disabled.`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.log(`Disabled addon "${name}".`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const enableCmd = defineCommand({
|
|
91
|
-
meta: { name: 'enable', description: 'Enable a registry addon' },
|
|
92
|
-
args: {
|
|
93
|
-
name: { type: 'positional', description: 'Addon name', required: true },
|
|
94
|
-
},
|
|
95
|
-
async run({ args }) {
|
|
96
|
-
const name = String(args._?.[0] ?? '').trim();
|
|
97
|
-
if (!name) throw new Error('Addon name is required. Run `openpalm addon list` to see the available addons.');
|
|
98
|
-
await runAddonEnableAction(name);
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const disableCmd = defineCommand({
|
|
103
|
-
meta: { name: 'disable', description: 'Disable a registry addon' },
|
|
104
|
-
args: {
|
|
105
|
-
name: { type: 'positional', description: 'Addon name', required: true },
|
|
106
|
-
},
|
|
107
|
-
async run({ args }) {
|
|
108
|
-
const name = String(args._?.[0] ?? '').trim();
|
|
109
|
-
if (!name) throw new Error('Addon name is required. Run `openpalm addon list` to see the available addons.');
|
|
110
|
-
await runAddonDisableAction(name);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const listCmd = defineCommand({
|
|
115
|
-
meta: { name: 'list', description: 'List registry addons and whether they are enabled' },
|
|
116
|
-
async run() {
|
|
117
|
-
await runAddonListAction();
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
export default defineCommand({
|
|
122
|
-
meta: {
|
|
123
|
-
name: 'addon',
|
|
124
|
-
description: 'Enable, disable, or list registry addons',
|
|
125
|
-
},
|
|
126
|
-
subCommands: {
|
|
127
|
-
list: listCmd,
|
|
128
|
-
enable: enableCmd,
|
|
129
|
-
disable: disableCmd,
|
|
130
|
-
},
|
|
131
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { resolveOpenPalmHome } from '@openpalm/lib';
|
|
6
|
-
|
|
7
|
-
async function automationsCheck(): Promise<void> {
|
|
8
|
-
const home = resolveOpenPalmHome();
|
|
9
|
-
const tasksDir = join(home, 'stash', 'tasks');
|
|
10
|
-
|
|
11
|
-
if (!existsSync(tasksDir)) {
|
|
12
|
-
console.log('No tasks directory found at', tasksDir);
|
|
13
|
-
process.exit(0);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith('.md'));
|
|
17
|
-
if (taskFiles.length === 0) {
|
|
18
|
-
console.log('No automation tasks installed.');
|
|
19
|
-
process.exit(0);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console.log(`Found ${taskFiles.length} automation task(s):`);
|
|
23
|
-
for (const file of taskFiles) {
|
|
24
|
-
console.log(` - ${file.replace('.md', '')}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check crontab for registered tasks
|
|
28
|
-
await new Promise<void>((resolve) => {
|
|
29
|
-
execFile('crontab', ['-l'], (error, stdout) => {
|
|
30
|
-
if (error) {
|
|
31
|
-
console.log('No crontab found — tasks not yet registered (assistant not started?)');
|
|
32
|
-
resolve();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const registered = taskFiles.filter((f) => stdout.includes(f.replace('.md', '')));
|
|
36
|
-
console.log(`Registered in crontab: ${registered.length}/${taskFiles.length}`);
|
|
37
|
-
if (registered.length < taskFiles.length) {
|
|
38
|
-
console.log(
|
|
39
|
-
"Run 'akm tasks sync' inside the assistant container to register remaining tasks."
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
resolve();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default defineCommand({
|
|
48
|
-
meta: {
|
|
49
|
-
name: 'automations',
|
|
50
|
-
description: 'Manage automation tasks',
|
|
51
|
-
},
|
|
52
|
-
subCommands: {
|
|
53
|
-
check: defineCommand({
|
|
54
|
-
meta: {
|
|
55
|
-
name: 'check',
|
|
56
|
-
description: 'Report automation task registration status',
|
|
57
|
-
},
|
|
58
|
-
async run() {
|
|
59
|
-
await automationsCheck();
|
|
60
|
-
},
|
|
61
|
-
}),
|
|
62
|
-
},
|
|
63
|
-
});
|
package/src/commands/install.ts
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { createInterface } from 'node:readline';
|
|
4
|
-
import cliPkg from '../../package.json' with { type: 'json' };
|
|
5
|
-
import { defaultWorkDir } from '../lib/paths.ts';
|
|
6
|
-
import { resolveOpenPalmHome, resolveConfigDir } from '@openpalm/lib';
|
|
7
|
-
import { ensureSecrets, ensureStackEnv } from '../lib/env.ts';
|
|
8
|
-
import { ensureDirectoryTree, seedOpenPalmDir, seedUiBuild } from '../lib/io.ts';
|
|
9
|
-
import { openBrowser } from '../lib/browser.ts';
|
|
10
|
-
import { runDockerCompose } from '../lib/docker.ts';
|
|
11
|
-
import {
|
|
12
|
-
backupOpenPalmHome,
|
|
13
|
-
buildComposeCliArgs,
|
|
14
|
-
ensureOpenCodeConfig, ensureOpenCodeSystemConfig,
|
|
15
|
-
performSetup,
|
|
16
|
-
applyInstall,
|
|
17
|
-
buildManagedServices,
|
|
18
|
-
createLogger,
|
|
19
|
-
resolveRequestedImageTag,
|
|
20
|
-
type SetupSpec,
|
|
21
|
-
} from '@openpalm/lib';
|
|
22
|
-
import { detectHostInfo } from '../lib/host-info.ts';
|
|
23
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
24
|
-
|
|
25
|
-
const logger = createLogger('cli:install');
|
|
26
|
-
|
|
27
|
-
async function resolveDefaultInstallRef(): Promise<string> {
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch('https://github.com/itlackey/openpalm/releases/latest', { redirect: 'manual', signal: AbortSignal.timeout(10000) });
|
|
30
|
-
const match = (res.headers.get('location') ?? '').match(/\/tag\/(v[0-9]+\.[0-9]+\.[0-9]+[^\s]*)$/);
|
|
31
|
-
if (match?.[1]) return match[1];
|
|
32
|
-
} catch { /* fall through */ }
|
|
33
|
-
return cliPkg.version ? `v${cliPkg.version}` : 'main';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default defineCommand({
|
|
37
|
-
meta: {
|
|
38
|
-
name: 'install',
|
|
39
|
-
description: 'Bootstrap home dirs, download assets, run setup wizard, start core services',
|
|
40
|
-
},
|
|
41
|
-
args: {
|
|
42
|
-
force: {
|
|
43
|
-
type: 'boolean',
|
|
44
|
-
description: 'Skip "already installed" check',
|
|
45
|
-
default: false,
|
|
46
|
-
},
|
|
47
|
-
version: {
|
|
48
|
-
type: 'string',
|
|
49
|
-
description: 'Install specific repository ref (default: latest release)',
|
|
50
|
-
},
|
|
51
|
-
start: {
|
|
52
|
-
type: 'boolean',
|
|
53
|
-
description: 'Start services after install (use --no-start to skip)',
|
|
54
|
-
default: true,
|
|
55
|
-
},
|
|
56
|
-
open: {
|
|
57
|
-
type: 'boolean',
|
|
58
|
-
description: 'Open browser after install (use --no-open to skip)',
|
|
59
|
-
default: true,
|
|
60
|
-
},
|
|
61
|
-
file: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
alias: 'f',
|
|
64
|
-
description: 'Path to setup config file (JSON or YAML) — skips wizard',
|
|
65
|
-
},
|
|
66
|
-
yes: {
|
|
67
|
-
type: 'boolean',
|
|
68
|
-
alias: 'y',
|
|
69
|
-
description: 'Auto-confirm destructive prompts (e.g. --force backup of existing OP_HOME)',
|
|
70
|
-
default: false,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
async run({ args }) {
|
|
74
|
-
try {
|
|
75
|
-
const version = args.version || await resolveDefaultInstallRef();
|
|
76
|
-
await bootstrapInstall({
|
|
77
|
-
force: args.force,
|
|
78
|
-
version,
|
|
79
|
-
noStart: !args.start,
|
|
80
|
-
noOpen: !args.open,
|
|
81
|
-
file: args.file,
|
|
82
|
-
assumeYes: args.yes,
|
|
83
|
-
});
|
|
84
|
-
} catch (err) {
|
|
85
|
-
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
type InstallOptions = {
|
|
92
|
-
force: boolean;
|
|
93
|
-
version: string;
|
|
94
|
-
noStart: boolean;
|
|
95
|
-
noOpen: boolean;
|
|
96
|
-
file?: string;
|
|
97
|
-
assumeYes: boolean;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Prompt the user for a y/N confirmation on stdin/stdout. Returns false in
|
|
102
|
-
* any non-interactive context (no TTY) so CI runs do not hang waiting on
|
|
103
|
-
* input — callers must pair this with an explicit `--yes` flag for
|
|
104
|
-
* unattended invocations.
|
|
105
|
-
*/
|
|
106
|
-
async function promptYesNo(question: string): Promise<boolean> {
|
|
107
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
108
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
109
|
-
try {
|
|
110
|
-
const answer = await new Promise<string>((resolve) => rl.question(`${question} `, resolve));
|
|
111
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
112
|
-
} finally {
|
|
113
|
-
rl.close();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function requireCmd(cmd: string[], msg: string): Promise<void> {
|
|
118
|
-
if ((await Bun.spawn(cmd, { stdout: 'ignore', stderr: 'ignore' }).exited) !== 0) throw new Error(msg);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function requireDocker(): Promise<void> {
|
|
122
|
-
if (!Bun.which('docker')) throw new Error('Docker is not installed. Install Docker first: https://docs.docker.com/get-docker/');
|
|
123
|
-
await requireCmd(['docker', 'info'], 'Docker is not running (or current user lacks permission). Start Docker and retry.');
|
|
124
|
-
await requireCmd(['docker', 'compose', 'version'], 'Docker Compose v2 is required. Install it: https://docs.docker.com/compose/install/');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function deployServices(mode: string, pull = true): Promise<string[]> {
|
|
128
|
-
const state = ensureValidState();
|
|
129
|
-
await applyInstall(state);
|
|
130
|
-
const managedServices = await buildManagedServices(state);
|
|
131
|
-
const composeArgs = buildComposeCliArgs(state);
|
|
132
|
-
if (pull) await runDockerCompose([...composeArgs, 'pull', ...managedServices]).catch(() => console.warn('Warning: image pull failed.'));
|
|
133
|
-
await runDockerCompose([...composeArgs, 'up', '-d', ...managedServices]);
|
|
134
|
-
console.log(JSON.stringify({ ok: true, mode, services: managedServices }, null, 2));
|
|
135
|
-
return managedServices;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function parseConfigFile(filePath: string, raw: string): Promise<Record<string, unknown>> {
|
|
139
|
-
const ext = filePath.toLowerCase();
|
|
140
|
-
const isYaml = ext.endsWith('.yaml') || ext.endsWith('.yml');
|
|
141
|
-
if (!isYaml && !ext.endsWith('.json')) throw new Error(`Unsupported config file format: ${filePath}. Use .json or .yaml.`);
|
|
142
|
-
try {
|
|
143
|
-
return isYaml ? (await import('yaml')).parse(raw) : JSON.parse(raw);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
throw new Error(`Failed to parse setup config '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export async function bootstrapInstall(options: InstallOptions): Promise<void> {
|
|
150
|
-
const homeDir = resolveOpenPalmHome();
|
|
151
|
-
const configDir = resolveConfigDir();
|
|
152
|
-
const stateDir = `${homeDir}/state`;
|
|
153
|
-
const workDir = defaultWorkDir();
|
|
154
|
-
|
|
155
|
-
// Use config/stack/stack.env (always present after a successful install) as the
|
|
156
|
-
// canonical "already installed" indicator.
|
|
157
|
-
const alreadyInstalled = await Bun.file(join(configDir, 'stack', 'stack.env')).exists();
|
|
158
|
-
if (alreadyInstalled && !options.force) {
|
|
159
|
-
throw new Error('OpenPalm appears to already be installed. Re-run install with --force to continue.');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (alreadyInstalled && options.force) {
|
|
163
|
-
// Use the helper's own backup-path convention so the prompt is honest about
|
|
164
|
-
// where the existing install will land. Match backupOpenPalmHome():
|
|
165
|
-
// `${homeDir}.backup.${YYYYMMDD-HHMMSS}`.
|
|
166
|
-
const now = new Date();
|
|
167
|
-
const stamp =
|
|
168
|
-
`${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}` +
|
|
169
|
-
`-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
|
170
|
-
const plannedBackup = `${homeDir}.backup.${stamp}`;
|
|
171
|
-
|
|
172
|
-
// Skip the prompt when --yes was passed OR when there's no TTY (CI/scripts).
|
|
173
|
-
// Without the TTY exemption we would silently hang a non-interactive
|
|
174
|
-
// pipeline waiting for stdin, which is worse than auto-confirming.
|
|
175
|
-
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
176
|
-
if (!options.assumeYes && interactive) {
|
|
177
|
-
const proceed = await promptYesNo(
|
|
178
|
-
`--force will move the existing OpenPalm install at ${homeDir} to ${plannedBackup}. Continue? [y/N]`,
|
|
179
|
-
);
|
|
180
|
-
if (!proceed) {
|
|
181
|
-
console.log('Install aborted. Re-run with --yes (or -y) to skip this confirmation in non-interactive use.');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const backupDir = backupOpenPalmHome(homeDir);
|
|
186
|
-
if (backupDir) {
|
|
187
|
-
console.log(`Backed up existing OP_HOME to ${backupDir}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ── Bootstrap files ────────────────────────────────────────────────────
|
|
192
|
-
await prepareInstallFiles(homeDir, configDir, stateDir, workDir, options.version);
|
|
193
|
-
|
|
194
|
-
// ── Configure ──────────────────────────────────────────────────────────
|
|
195
|
-
// File-based install: read config, run performSetup, optionally deploy
|
|
196
|
-
if (options.file) {
|
|
197
|
-
await runFileInstall(options.file, options.noStart);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Interactive wizard: start the admin UI which serves the setup wizard
|
|
202
|
-
const needsWizard = !alreadyInstalled || options.force;
|
|
203
|
-
if (needsWizard) {
|
|
204
|
-
await runWizardInstall(options.noOpen);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Update mode (already installed, no --force): just redeploy
|
|
209
|
-
if (options.noStart) {
|
|
210
|
-
console.log('Config updated. Run `openpalm start` to start services.');
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
await requireDocker();
|
|
214
|
-
await deployServices('update', false);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function prepareInstallFiles(
|
|
218
|
-
homeDir: string, configDir: string, stateDir: string, workDir: string, version: string,
|
|
219
|
-
): Promise<void> {
|
|
220
|
-
console.log('Preparing directories...');
|
|
221
|
-
await ensureDirectoryTree(homeDir, configDir, '', '', workDir);
|
|
222
|
-
|
|
223
|
-
try { await Bun.write(join(stateDir, 'host.json'), JSON.stringify(await detectHostInfo(), null, 2) + '\n'); }
|
|
224
|
-
catch (err) { logger.debug('failed to write host.json', { error: String(err) }); }
|
|
225
|
-
|
|
226
|
-
// Seed OP_HOME from .openpalm/ (local source if available, else GitHub tarball)
|
|
227
|
-
await seedOpenPalmDir(version, homeDir, configDir, stateDir);
|
|
228
|
-
// Install UI build to state/ui/ (local build if available, else GitHub release asset)
|
|
229
|
-
await seedUiBuild(version, stateDir);
|
|
230
|
-
|
|
231
|
-
console.log('Configuring secrets...');
|
|
232
|
-
await ensureSecrets(stateDir);
|
|
233
|
-
await ensureStackEnv(homeDir, configDir, workDir, version, resolveRequestedImageTag(version) ?? undefined);
|
|
234
|
-
|
|
235
|
-
for (const [path, content] of [
|
|
236
|
-
[join(configDir, 'stack', 'guardian.env'), '# Guardian channel HMAC secrets — managed by openpalm\n'],
|
|
237
|
-
[join(configDir, 'stack', 'auth.json'), '{}\n'],
|
|
238
|
-
[join(homeDir, 'stash', 'vaults', 'user.env'), '# OpenPalm user vault — add LLM API keys and other secrets here\n'],
|
|
239
|
-
] as const) {
|
|
240
|
-
if (!(await Bun.file(path).exists())) await Bun.write(path, content);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
try { ensureOpenCodeConfig(); ensureOpenCodeSystemConfig(); } catch (err) { logger.debug('failed to ensure OpenCode config', { error: String(err) }); }
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Launch the UI host server to handle first-time setup.
|
|
248
|
-
*
|
|
249
|
-
* The SvelteKit UI detects that setup is not complete (via hooks.server.ts)
|
|
250
|
-
* and redirects to /setup where the wizard runs. Deploy is triggered from
|
|
251
|
-
* within the UI process after the user completes the wizard.
|
|
252
|
-
*
|
|
253
|
-
* Pre-flight: `requireDocker()` runs FIRST so users hit our friendly Docker
|
|
254
|
-
* error before the browser opens to a wizard that will fail at the end of
|
|
255
|
-
* a 10-step flow.
|
|
256
|
-
*/
|
|
257
|
-
async function runWizardInstall(noOpen: boolean): Promise<void> {
|
|
258
|
-
await requireDocker();
|
|
259
|
-
const port = Number(process.env.OP_HOST_UI_PORT) || 3880;
|
|
260
|
-
console.log(`Setup wizard: http://localhost:${port}/setup`);
|
|
261
|
-
const { startUIServer } = await import('../lib/ui-server.ts');
|
|
262
|
-
await startUIServer({ open: !noOpen, port });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function runFileInstall(filePath: string, noStart: boolean): Promise<void> {
|
|
266
|
-
console.log(`Reading setup config from ${filePath}...`);
|
|
267
|
-
if (!(await Bun.file(filePath).exists())) {
|
|
268
|
-
throw new Error(`Setup config file not found: ${filePath}. Check the --file path and try again.`);
|
|
269
|
-
}
|
|
270
|
-
const config = await parseConfigFile(filePath, await Bun.file(filePath).text());
|
|
271
|
-
|
|
272
|
-
// Normalize old wrapped format: { spec: { version, capabilities }, capabilities: [...] }
|
|
273
|
-
// into flat format: { version, capabilities: {...}, connections: [...] }
|
|
274
|
-
if (config.spec && typeof config.spec === 'object') {
|
|
275
|
-
const spec = config.spec as Record<string, unknown>;
|
|
276
|
-
// Old format had connections array as top-level "capabilities"
|
|
277
|
-
if (Array.isArray(config.capabilities)) config.connections = config.capabilities;
|
|
278
|
-
config.version = spec.version;
|
|
279
|
-
config.capabilities = spec.capabilities;
|
|
280
|
-
delete config.spec;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (config.version !== 2) throw new Error('Setup config must be version 2. See example.spec.yaml for the format.');
|
|
284
|
-
if (!config.capabilities || typeof config.capabilities !== 'object' || Array.isArray(config.capabilities)) {
|
|
285
|
-
throw new Error('Setup config must contain a "capabilities" object (llm, embeddings).');
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Resolve security.uiLoginPassword from environment when not in spec.
|
|
289
|
-
// Phase 4 (auth/proxy refactor) renamed the env var to OP_UI_LOGIN_PASSWORD
|
|
290
|
-
// and the spec field to security.uiLoginPassword.
|
|
291
|
-
const security = (config.security ?? {}) as Record<string, unknown>;
|
|
292
|
-
if (!security.uiLoginPassword && process.env.OP_UI_LOGIN_PASSWORD) {
|
|
293
|
-
security.uiLoginPassword = process.env.OP_UI_LOGIN_PASSWORD;
|
|
294
|
-
config.security = security;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const result = await performSetup(config as unknown as SetupSpec);
|
|
298
|
-
if (!result.ok) throw new Error(`Setup failed: ${result.error}`);
|
|
299
|
-
console.log('Setup complete.');
|
|
300
|
-
if (noStart) { console.log('Config written. Run `openpalm start` to start services.'); return; }
|
|
301
|
-
await requireDocker();
|
|
302
|
-
await deployServices('install');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
package/src/commands/logs.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
3
|
-
import { runComposeReadOnly } from '../lib/cli-compose.ts';
|
|
4
|
-
|
|
5
|
-
export async function runLogsAction(services: string[]): Promise<void> {
|
|
6
|
-
const state = ensureValidState();
|
|
7
|
-
await runComposeReadOnly(state, ['logs', '--tail', '100', ...services]);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export default defineCommand({
|
|
11
|
-
meta: {
|
|
12
|
-
name: 'logs',
|
|
13
|
-
description: 'Tail last 100 log lines for services',
|
|
14
|
-
},
|
|
15
|
-
args: {
|
|
16
|
-
services: {
|
|
17
|
-
type: 'positional',
|
|
18
|
-
description: 'Service names (omit for all)',
|
|
19
|
-
required: false,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
async run({ args }) {
|
|
23
|
-
await runLogsAction(args._ ?? []);
|
|
24
|
-
},
|
|
25
|
-
});
|
package/src/commands/restart.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { buildManagedServices } from '@openpalm/lib';
|
|
3
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
4
|
-
import { runComposeWithPreflight } from '../lib/cli-compose.ts';
|
|
5
|
-
|
|
6
|
-
export default defineCommand({
|
|
7
|
-
meta: {
|
|
8
|
-
name: 'restart',
|
|
9
|
-
description: 'Restart services (all or named)',
|
|
10
|
-
},
|
|
11
|
-
args: {
|
|
12
|
-
services: {
|
|
13
|
-
type: 'positional',
|
|
14
|
-
description: 'Service names to restart (omit for all)',
|
|
15
|
-
required: false,
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
async run({ args }) {
|
|
19
|
-
const services = args._ ?? [];
|
|
20
|
-
await runRestartAction(services);
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export async function runRestartAction(services: string[]): Promise<void> {
|
|
25
|
-
if (services.length === 0) {
|
|
26
|
-
// Restart all managed services (admin included if enabled)
|
|
27
|
-
const state = ensureValidState();
|
|
28
|
-
const managedServices = await buildManagedServices(state);
|
|
29
|
-
await runComposeWithPreflight(state, ['restart', ...managedServices]);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const state = ensureValidState();
|
|
34
|
-
for (const service of services) {
|
|
35
|
-
await runComposeWithPreflight(state, ['restart', service]);
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/commands/rollback.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
3
|
-
import { runComposeWithPreflight } from '../lib/cli-compose.ts';
|
|
4
|
-
import {
|
|
5
|
-
buildManagedServices,
|
|
6
|
-
createState,
|
|
7
|
-
restoreSnapshot,
|
|
8
|
-
hasSnapshot,
|
|
9
|
-
snapshotTimestamp,
|
|
10
|
-
} from '@openpalm/lib';
|
|
11
|
-
|
|
12
|
-
export default defineCommand({
|
|
13
|
-
meta: {
|
|
14
|
-
name: 'rollback',
|
|
15
|
-
description: 'Restore the most recent configuration snapshot and restart services',
|
|
16
|
-
},
|
|
17
|
-
async run() {
|
|
18
|
-
if (!hasSnapshot()) {
|
|
19
|
-
console.error('No rollback snapshot available.');
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const ts = snapshotTimestamp();
|
|
24
|
-
console.log(`Restoring snapshot from ${ts ?? 'unknown'}...`);
|
|
25
|
-
|
|
26
|
-
// Create state without persisting so we don't overwrite live config
|
|
27
|
-
// before the snapshot is restored.
|
|
28
|
-
const rollbackState = createState();
|
|
29
|
-
restoreSnapshot(rollbackState);
|
|
30
|
-
|
|
31
|
-
console.log('Snapshot restored. Rebuilding configuration...');
|
|
32
|
-
|
|
33
|
-
// Now validate and persist with the restored files in place
|
|
34
|
-
const state = ensureValidState();
|
|
35
|
-
|
|
36
|
-
const managedServices = await buildManagedServices(state);
|
|
37
|
-
|
|
38
|
-
await runComposeWithPreflight(state, [
|
|
39
|
-
'up', '-d', '--remove-orphans', ...managedServices,
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
console.log('Rollback complete.');
|
|
43
|
-
},
|
|
44
|
-
});
|