@openpalm/lib 0.10.2 → 0.11.0-beta.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/README.md +4 -2
- package/package.json +11 -3
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +311 -0
- package/src/control-plane/channels.ts +11 -9
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -33
- package/src/control-plane/compose-args.ts +0 -4
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +148 -73
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +111 -58
- package/src/control-plane/docker.ts +70 -25
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +84 -1
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +260 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +190 -292
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +65 -75
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/operator-ids.test.ts +130 -0
- package/src/control-plane/operator-ids.ts +89 -0
- package/src/control-plane/paths.ts +80 -0
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +105 -27
- package/src/control-plane/registry.test.ts +247 -51
- package/src/control-plane/registry.ts +404 -54
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +97 -55
- package/src/control-plane/setup-config.schema.json +5 -17
- package/src/control-plane/setup-status.ts +9 -29
- package/src/control-plane/setup-validation.ts +23 -23
- package/src/control-plane/setup.test.ts +143 -244
- package/src/control-plane/setup.ts +216 -133
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +75 -60
- package/src/control-plane/spec-to-env.ts +68 -153
- package/src/control-plane/stack-spec.test.ts +22 -84
- package/src/control-plane/stack-spec.ts +9 -89
- package/src/control-plane/types.ts +9 -29
- package/src/control-plane/ui-assets.ts +385 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +102 -56
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/audit.ts +0 -40
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/lock.test.ts +0 -194
- package/src/control-plane/lock.ts +0 -176
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/provider-config.ts +0 -34
- package/src/control-plane/redact-schema.ts +0 -50
- package/src/control-plane/secret-backend.test.ts +0 -359
- package/src/control-plane/secret-backend.ts +0 -322
- package/src/control-plane/spec-validator.ts +0 -159
|
@@ -37,9 +37,16 @@ function run(
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the Docker Compose project name.
|
|
42
|
+
* Honors COMPOSE_PROJECT_NAME (Docker standard) and OP_PROJECT_NAME (legacy).
|
|
43
|
+
*/
|
|
41
44
|
export function resolveComposeProjectName(): string {
|
|
42
|
-
return
|
|
45
|
+
return (
|
|
46
|
+
process.env.OP_PROJECT_NAME?.trim() ||
|
|
47
|
+
process.env.COMPOSE_PROJECT_NAME?.trim() ||
|
|
48
|
+
"openpalm"
|
|
49
|
+
);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/** Check if Docker is available */
|
|
@@ -108,6 +115,24 @@ export async function composePreflight(
|
|
|
108
115
|
return run(args, undefined, 30_000, collectEnvOverrides(options.envFiles));
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Run compose config preflight validation before any mutation.
|
|
120
|
+
* Skipped when OP_SKIP_COMPOSE_PREFLIGHT is set (tests, CI).
|
|
121
|
+
*/
|
|
122
|
+
async function runPreflight(options: { files: string[]; envFiles?: string[] }): Promise<void> {
|
|
123
|
+
if (options.files.length === 0 || process.env.OP_SKIP_COMPOSE_PREFLIGHT) return;
|
|
124
|
+
const result = await composePreflight(options);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
const project = resolveComposeProjectName();
|
|
127
|
+
const fileArgs = options.files.map((f) => `-f ${f}`).join(" ");
|
|
128
|
+
const envArgs = (options.envFiles ?? []).map((f) => `--env-file ${f}`).join(" ");
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Compose preflight failed: ${result.stderr}\n` +
|
|
131
|
+
`Resolved command: docker compose ${fileArgs} --project-name ${project} ${envArgs} config --quiet`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
export async function composeConfigServices(
|
|
112
137
|
options: { files: string[]; envFiles?: string[] }
|
|
113
138
|
): Promise<{ ok: boolean; services: string[] }> {
|
|
@@ -133,6 +158,7 @@ export async function composeUp(
|
|
|
133
158
|
removeOrphans?: boolean;
|
|
134
159
|
}
|
|
135
160
|
): Promise<DockerResult> {
|
|
161
|
+
await runPreflight(options);
|
|
136
162
|
if (!existsSync(options.files[0])) {
|
|
137
163
|
return { ok: false, stdout: "", stderr: "Compose file not found", code: 1 };
|
|
138
164
|
}
|
|
@@ -156,6 +182,7 @@ export async function composeDown(
|
|
|
156
182
|
envFiles?: string[];
|
|
157
183
|
}
|
|
158
184
|
): Promise<DockerResult> {
|
|
185
|
+
await runPreflight(options);
|
|
159
186
|
if (!existsSync(options.files[0])) {
|
|
160
187
|
return { ok: false, stdout: "", stderr: "Compose file not found", code: 1 };
|
|
161
188
|
}
|
|
@@ -173,6 +200,7 @@ export async function composeRestart(
|
|
|
173
200
|
services: string[],
|
|
174
201
|
options: { files: string[]; envFiles?: string[] }
|
|
175
202
|
): Promise<DockerResult> {
|
|
203
|
+
await runPreflight(options);
|
|
176
204
|
const primaryFile = options.files[0];
|
|
177
205
|
if (!existsSync(primaryFile)) {
|
|
178
206
|
return {
|
|
@@ -196,6 +224,7 @@ export async function composeStop(
|
|
|
196
224
|
services: string[],
|
|
197
225
|
options: { files: string[]; envFiles?: string[] }
|
|
198
226
|
): Promise<DockerResult> {
|
|
227
|
+
await runPreflight(options);
|
|
199
228
|
const args = buildComposeArgs(options);
|
|
200
229
|
args.push("stop", ...services);
|
|
201
230
|
|
|
@@ -209,6 +238,7 @@ export async function composeStart(
|
|
|
209
238
|
services: string[],
|
|
210
239
|
options: { files: string[]; envFiles?: string[] }
|
|
211
240
|
): Promise<DockerResult> {
|
|
241
|
+
await runPreflight(options);
|
|
212
242
|
const args = buildComposeArgs(options);
|
|
213
243
|
// Use up -d for specific services to ensure they're created
|
|
214
244
|
args.push("up", "-d", ...services);
|
|
@@ -265,24 +295,37 @@ export async function composeLogs(
|
|
|
265
295
|
return run(args, undefined);
|
|
266
296
|
}
|
|
267
297
|
|
|
298
|
+
// 60-minute pull timeout. Voice addon ships a ~2.4 GB image (CPU) /
|
|
299
|
+
// ~7.6 GB (CUDA); on a 1-2 Mbps home connection these legitimately take
|
|
300
|
+
// 30+ minutes. The previous 5-min cap silently killed pulls mid-stream
|
|
301
|
+
// on first install, surfacing as an opaque "pull failed". The wizard's
|
|
302
|
+
// retry layer wraps this, so an actually-hung pull is bounded by the
|
|
303
|
+
// outer retry budget; this just gives any progressing pull room to
|
|
304
|
+
// finish on slow connections.
|
|
305
|
+
const PULL_TIMEOUT_MS = 60 * 60_000;
|
|
306
|
+
|
|
268
307
|
/**
|
|
269
308
|
* Pull image for a single service.
|
|
270
309
|
*/
|
|
271
310
|
export async function composePullService(
|
|
272
311
|
service: string,
|
|
273
|
-
options: { files: string[]; envFiles?: string[] }
|
|
312
|
+
options: { files: string[]; envFiles?: string[]; profiles?: string[] }
|
|
274
313
|
): Promise<DockerResult> {
|
|
314
|
+
await runPreflight(options);
|
|
275
315
|
const args = buildComposeArgs(options);
|
|
316
|
+
for (const p of options.profiles ?? []) args.push("--profile", p);
|
|
276
317
|
args.push("pull", service);
|
|
277
|
-
return run(args, undefined,
|
|
318
|
+
return run(args, undefined, PULL_TIMEOUT_MS, collectEnvOverrides(options.envFiles));
|
|
278
319
|
}
|
|
279
320
|
|
|
280
321
|
export async function composePull(
|
|
281
|
-
options: { files: string[]; envFiles?: string[] }
|
|
322
|
+
options: { files: string[]; envFiles?: string[]; profiles?: string[] }
|
|
282
323
|
): Promise<DockerResult> {
|
|
324
|
+
await runPreflight(options);
|
|
283
325
|
const args = buildComposeArgs(options);
|
|
326
|
+
for (const p of options.profiles ?? []) args.push("--profile", p);
|
|
284
327
|
args.push("pull");
|
|
285
|
-
return run(args, undefined,
|
|
328
|
+
return run(args, undefined, PULL_TIMEOUT_MS, collectEnvOverrides(options.envFiles));
|
|
286
329
|
}
|
|
287
330
|
|
|
288
331
|
/**
|
|
@@ -315,25 +358,27 @@ export async function getDockerEvents(
|
|
|
315
358
|
return run(args, undefined, 15_000);
|
|
316
359
|
}
|
|
317
360
|
|
|
361
|
+
|
|
318
362
|
/**
|
|
319
|
-
*
|
|
363
|
+
* Query Docker for a container's running state by name.
|
|
364
|
+
* Returns "running" or "stopped". Falls back to "unknown" on error.
|
|
320
365
|
*/
|
|
321
|
-
export function
|
|
322
|
-
|
|
323
|
-
):
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
366
|
+
export function inspectContainerStatus(
|
|
367
|
+
containerName: string
|
|
368
|
+
): Promise<"running" | "stopped" | "unknown"> {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
execFile(
|
|
371
|
+
"docker",
|
|
372
|
+
["inspect", "--format", "{{.State.Status}}", containerName],
|
|
373
|
+
{ timeout: 5000 },
|
|
374
|
+
(error, stdout) => {
|
|
375
|
+
if (error) {
|
|
376
|
+
resolve("unknown");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const status = (stdout ?? "").toString().trim();
|
|
380
|
+
resolve(status === "running" ? "running" : "stopped");
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
});
|
|
339
384
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { parseEnvContent, mergeEnvContent } from "./env.js";
|
|
2
|
+
import { parseEnvContent, mergeEnvContent, removeEnvKey } from "./env.js";
|
|
3
3
|
|
|
4
4
|
// ── Special character round-trips ────────────────────────────────────────
|
|
5
5
|
// Values written by mergeEnvContent (which uses quoteEnvValue internally)
|
|
@@ -107,3 +107,27 @@ describe("mergeEnvContent updates existing keys with special char values", () =>
|
|
|
107
107
|
expect(parsed.ADMIN_TOKEN).toBe("new#value");
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("removeEnvKey", () => {
|
|
112
|
+
it("removes a simple key", () => {
|
|
113
|
+
const out = removeEnvKey("FOO=1\nBAR=2\n", "FOO");
|
|
114
|
+
expect(parseEnvContent(out)).toEqual({ BAR: "2" });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns content unchanged when key is absent", () => {
|
|
118
|
+
const input = "FOO=1\nBAR=2\n";
|
|
119
|
+
expect(removeEnvKey(input, "MISSING")).toBe(input);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("handles the export prefix form", () => {
|
|
123
|
+
const out = removeEnvKey("export FOO=1\nBAR=2\n", "FOO");
|
|
124
|
+
expect(parseEnvContent(out)).toEqual({ BAR: "2" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("leaves comments above the deleted key intact", () => {
|
|
128
|
+
const out = removeEnvKey("# header comment\nFOO=1\nBAR=2\n", "FOO");
|
|
129
|
+
expect(out).toContain("# header comment");
|
|
130
|
+
expect(parseEnvContent(out).FOO).toBeUndefined();
|
|
131
|
+
expect(parseEnvContent(out).BAR).toBe("2");
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/control-plane/env.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse as dotenvParse } from 'dotenv';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, copyFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
export function parseEnvContent(content: string): Record<string, string> {
|
|
5
5
|
return dotenvParse(content);
|
|
@@ -10,10 +10,22 @@ export function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
10
10
|
try {
|
|
11
11
|
return dotenvParse(readFileSync(filePath, 'utf-8'));
|
|
12
12
|
} catch {
|
|
13
|
+
// File is unreadable or malformed — back it up before returning empty so
|
|
14
|
+
// the next write doesn't silently discard all existing values.
|
|
15
|
+
try { copyFileSync(filePath, `${filePath}.corrupt-${Date.now()}`); } catch { /* best-effort */ }
|
|
13
16
|
return {};
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Resolve `${VAR}` and `${VAR:-default}` patterns in a string against the
|
|
22
|
+
* provided variable map. Unknown vars without a default expand to an empty
|
|
23
|
+
* string — mirrors compose's variable substitution semantics.
|
|
24
|
+
*/
|
|
25
|
+
export function expandEnvVars(input: string, vars: Record<string, string>): string {
|
|
26
|
+
return input.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, name, def) => vars[name] ?? def ?? '');
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
function quoteEnvValue(value: string): string {
|
|
18
30
|
if (value.length === 0) return '';
|
|
19
31
|
const needsQuoting = /[#"'\\\n\r$]/.test(value) || value !== value.trim();
|
|
@@ -25,6 +37,77 @@ function quoteEnvValue(value: string): string {
|
|
|
25
37
|
return `"${escaped}"`;
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Remove a key from .env content. Comments above the line and the
|
|
42
|
+
* surrounding blank-line structure are preserved exactly as written so
|
|
43
|
+
* round-tripping the file through this helper is non-destructive.
|
|
44
|
+
* If the key is absent the input is returned unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export function removeEnvKey(content: string, key: string): string {
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
const out: string[] = [];
|
|
49
|
+
let removed = false;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
let testLine = line.trim();
|
|
52
|
+
if (testLine.startsWith('export ')) testLine = testLine.slice(7).trimStart();
|
|
53
|
+
const eq = testLine.indexOf('=');
|
|
54
|
+
if (eq > 0 && testLine.slice(0, eq).trim() === key) {
|
|
55
|
+
removed = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out.push(line);
|
|
59
|
+
}
|
|
60
|
+
// If we matched, drop a trailing blank line that the deletion left behind so
|
|
61
|
+
// the file does not accumulate empty lines on repeated edits.
|
|
62
|
+
if (removed && out.length > 1 && out[out.length - 1] === '' && out[out.length - 2] === '') {
|
|
63
|
+
out.pop();
|
|
64
|
+
}
|
|
65
|
+
return out.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Upserts a key=value pair in env file content. If the key exists, replaces the line;
|
|
70
|
+
* otherwise appends a new line.
|
|
71
|
+
*/
|
|
72
|
+
export function upsertEnvValue(content: string, key: string, value: string): string {
|
|
73
|
+
const escapedKey = key.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
|
|
74
|
+
const pattern = new RegExp(`^((?:export\\s+)?)${escapedKey}=.*$`, 'm');
|
|
75
|
+
if (pattern.test(content)) {
|
|
76
|
+
// Preserve the `export ` prefix if the original line had one
|
|
77
|
+
return content.replace(pattern, `$1${key}=${value}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const line = `${key}=${value}`;
|
|
81
|
+
const suffix = content.endsWith('\n') || content.length === 0 ? '' : '\n';
|
|
82
|
+
return `${content}${suffix}${line}\n`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const RELEASE_TAG_REGEX = /^v?\d+\.\d+\.\d+(?:[-+](?:[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalizes a repository ref to an image tag. Returns null for non-release refs.
|
|
89
|
+
* E.g. "0.9.0" → "v0.9.0", "v0.9.0" → "v0.9.0", "main" → null.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveRequestedImageTag(repoRef: string): string | null {
|
|
92
|
+
const trimmed = repoRef.trim();
|
|
93
|
+
if (!trimmed || trimmed === 'main') return null;
|
|
94
|
+
if (!RELEASE_TAG_REGEX.test(trimmed)) return null;
|
|
95
|
+
return trimmed.startsWith('v') ? trimmed : `v${trimmed}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reconciles the OP_IMAGE_TAG value in stack.env content.
|
|
100
|
+
*/
|
|
101
|
+
export function reconcileStackEnvImageTag(
|
|
102
|
+
content: string,
|
|
103
|
+
repoRef: string,
|
|
104
|
+
explicitImageTag?: string,
|
|
105
|
+
): string {
|
|
106
|
+
const desiredImageTag = explicitImageTag || resolveRequestedImageTag(repoRef);
|
|
107
|
+
if (!desiredImageTag) return content;
|
|
108
|
+
return upsertEnvValue(content, 'OP_IMAGE_TAG', desiredImageTag);
|
|
109
|
+
}
|
|
110
|
+
|
|
28
111
|
export function mergeEnvContent(
|
|
29
112
|
content: string,
|
|
30
113
|
updates: Record<string, string>,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Home directory layout for the OpenPalm control plane (v0.
|
|
2
|
+
* Home directory layout for the OpenPalm control plane (v0.11.0+).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* config/
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Single ~/.openpalm/ root:
|
|
5
|
+
* config/ — user-editable config + system config files (auth.json, akm/)
|
|
6
|
+
* config/stack/ — compose runtime + stack config (stack.env, guardian.env, stack.yml, addons/)
|
|
7
|
+
* cache/ — regenerable/semi-persistent data (akm cache, guardian cache, rollback)
|
|
8
|
+
* state/ — persistent service data (assistant, admin, guardian, logs, backups, registry)
|
|
9
|
+
* stash/ — akm knowledge (skills, vaults, agents)
|
|
10
|
+
* workspace/ — shared assistant work area
|
|
11
|
+
* config/stack/ — compose runtime assets + stack config (stack.env, guardian.env, stack.yml)
|
|
11
12
|
*/
|
|
12
13
|
import { mkdirSync } from "node:fs";
|
|
13
14
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -32,28 +33,37 @@ export function resolveConfigDir(): string {
|
|
|
32
33
|
return `${resolveOpenPalmHome()}/config`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export function
|
|
36
|
-
return `${resolveOpenPalmHome()}/
|
|
36
|
+
export function resolveStashDir(): string {
|
|
37
|
+
return `${resolveOpenPalmHome()}/stash`;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export function
|
|
40
|
-
return `${resolveOpenPalmHome()}/
|
|
40
|
+
export function resolveWorkspaceDir(): string {
|
|
41
|
+
return `${resolveOpenPalmHome()}/workspace`;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export function
|
|
44
|
-
return `${resolveOpenPalmHome()}/
|
|
44
|
+
export function resolveCacheDir(): string {
|
|
45
|
+
return `${resolveOpenPalmHome()}/cache`;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export function
|
|
48
|
-
return `${
|
|
48
|
+
export function resolveStateDir(): string {
|
|
49
|
+
return `${resolveOpenPalmHome()}/state`;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export function
|
|
52
|
-
return `${
|
|
52
|
+
export function resolveStackDir(): string {
|
|
53
|
+
return `${resolveConfigDir()}/stack`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Derived from stateDir — used by registry.ts, rollback.ts, backup.ts, core-assets.ts
|
|
57
|
+
export function resolveLogsDir(): string {
|
|
58
|
+
return `${resolveStateDir()}/logs`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveBackupsDir(): string {
|
|
62
|
+
return `${resolveStateDir()}/backups`;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
export function resolveRegistryDir(): string {
|
|
56
|
-
return `${
|
|
66
|
+
return `${resolveStateDir()}/registry`;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
export function resolveRegistryAddonsDir(): string {
|
|
@@ -64,69 +74,56 @@ export function resolveRegistryAutomationsDir(): string {
|
|
|
64
74
|
return `${resolveRegistryDir()}/automations`;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
export function
|
|
68
|
-
return `${
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function resolveBackupsDir(): string {
|
|
72
|
-
return `${resolveOpenPalmHome()}/backups`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function resolveWorkspaceDir(): string {
|
|
76
|
-
return `${resolveOpenPalmHome()}/data/workspace`;
|
|
77
|
+
export function resolveRollbackDir(): string {
|
|
78
|
+
return `${resolveCacheDir()}/rollback`;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// ── Directory Setup ──────────────────────────────────────────────────
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
|
-
* Create the full ~/.openpalm/ directory tree
|
|
84
|
+
* Create the full ~/.openpalm/ directory tree.
|
|
83
85
|
*/
|
|
84
86
|
export function ensureHomeDirs(): void {
|
|
85
87
|
const home = resolveOpenPalmHome();
|
|
86
|
-
const cache = resolveCacheHome();
|
|
87
88
|
|
|
88
89
|
for (const dir of [
|
|
89
|
-
// config/ — user-editable
|
|
90
|
+
// config/ — user-editable config + system config files
|
|
90
91
|
`${home}/config`,
|
|
91
|
-
`${home}/config/automations`,
|
|
92
92
|
`${home}/config/assistant`,
|
|
93
93
|
`${home}/config/guardian`,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`${home}/
|
|
98
|
-
`${home}/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`${home}/
|
|
103
|
-
`${home}/
|
|
104
|
-
`${home}/
|
|
105
|
-
`${home}/
|
|
106
|
-
`${home}/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`${home}/
|
|
110
|
-
`${home}/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`${home}/registry`,
|
|
114
|
-
`${home}/registry/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`${home}/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`${home}/
|
|
126
|
-
|
|
127
|
-
// cache/ — ephemeral, regenerable
|
|
128
|
-
cache,
|
|
129
|
-
`${cache}/rollback`,
|
|
94
|
+
`${home}/config/akm`, // AKM_CONFIG_DIR — akm setup config.json lives here
|
|
95
|
+
|
|
96
|
+
// cache/ — regenerable/semi-persistent data
|
|
97
|
+
`${home}/cache`,
|
|
98
|
+
`${home}/cache/akm`, // akm registry index, downloaded artifacts
|
|
99
|
+
`${home}/cache/rollback`, // rollback snapshots
|
|
100
|
+
|
|
101
|
+
// state/ — persistent service data
|
|
102
|
+
`${home}/state`,
|
|
103
|
+
`${home}/state/assistant`, // assistant HOME bind mount
|
|
104
|
+
`${home}/state/admin`, // admin home bind mount
|
|
105
|
+
`${home}/state/guardian`, // guardian runtime data
|
|
106
|
+
`${home}/state/akm`, // shared akm operational data (NOT config)
|
|
107
|
+
`${home}/state/akm/data`,
|
|
108
|
+
`${home}/state/akm/state`,
|
|
109
|
+
`${home}/state/logs`,
|
|
110
|
+
`${home}/state/logs/opencode`,
|
|
111
|
+
`${home}/state/backups`,
|
|
112
|
+
`${home}/state/registry`,
|
|
113
|
+
`${home}/state/registry/addons`,
|
|
114
|
+
`${home}/state/registry/automations`,
|
|
115
|
+
|
|
116
|
+
// stash/ — akm knowledge (skills, vaults, agents); stash/tasks/ for scheduled automations
|
|
117
|
+
`${home}/stash`,
|
|
118
|
+
`${home}/stash/vaults`,
|
|
119
|
+
`${home}/stash/tasks`,
|
|
120
|
+
|
|
121
|
+
// workspace/ — shared assistant work area
|
|
122
|
+
`${home}/workspace`,
|
|
123
|
+
|
|
124
|
+
// config/stack/ — compose runtime (addon overlays + stack config files)
|
|
125
|
+
`${home}/config/stack`,
|
|
126
|
+
`${home}/config/stack/addons`,
|
|
130
127
|
]) {
|
|
131
128
|
mkdirSync(dir, { recursive: true });
|
|
132
129
|
}
|