@openpalm/lib 0.9.9 → 0.10.1

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.
Files changed (56) hide show
  1. package/README.md +31 -71
  2. package/package.json +1 -1
  3. package/src/control-plane/audit.ts +4 -4
  4. package/src/control-plane/backup.ts +31 -0
  5. package/src/control-plane/channels.ts +88 -156
  6. package/src/control-plane/cleanup-guardrails.test.ts +289 -0
  7. package/src/control-plane/compose-args.test.ts +170 -0
  8. package/src/control-plane/compose-args.ts +57 -0
  9. package/src/control-plane/config-persistence.ts +270 -0
  10. package/src/control-plane/core-assets.ts +58 -234
  11. package/src/control-plane/crypto.ts +14 -0
  12. package/src/control-plane/docker.ts +94 -204
  13. package/src/control-plane/env-schema-validation.test.ts +118 -0
  14. package/src/control-plane/extends-support.test.ts +105 -0
  15. package/src/control-plane/home.ts +133 -0
  16. package/src/control-plane/install-edge-cases.test.ts +314 -717
  17. package/src/control-plane/lifecycle.ts +215 -233
  18. package/src/control-plane/lock.test.ts +194 -0
  19. package/src/control-plane/lock.ts +176 -0
  20. package/src/control-plane/memory-config.ts +34 -160
  21. package/src/control-plane/opencode-client.test.ts +154 -0
  22. package/src/control-plane/opencode-client.ts +113 -0
  23. package/src/control-plane/provider-config.ts +34 -0
  24. package/src/control-plane/redact-schema.ts +50 -0
  25. package/src/control-plane/registry-components.test.ts +313 -0
  26. package/src/control-plane/registry.test.ts +414 -0
  27. package/src/control-plane/registry.ts +418 -0
  28. package/src/control-plane/rollback.ts +128 -0
  29. package/src/control-plane/scheduler.ts +18 -190
  30. package/src/control-plane/secret-backend.test.ts +359 -0
  31. package/src/control-plane/secret-backend.ts +322 -0
  32. package/src/control-plane/secret-mappings.ts +185 -0
  33. package/src/control-plane/secrets.ts +186 -112
  34. package/src/control-plane/setup-config.schema.json +306 -0
  35. package/src/control-plane/setup-status.ts +15 -8
  36. package/src/control-plane/setup-validation.ts +90 -0
  37. package/src/control-plane/setup.test.ts +336 -929
  38. package/src/control-plane/setup.ts +158 -886
  39. package/src/control-plane/spec-to-env.test.ts +100 -0
  40. package/src/control-plane/spec-to-env.ts +195 -0
  41. package/src/control-plane/spec-validator.ts +159 -0
  42. package/src/control-plane/stack-spec.test.ts +150 -0
  43. package/src/control-plane/stack-spec.ts +101 -22
  44. package/src/control-plane/types.ts +6 -99
  45. package/src/control-plane/validate.ts +107 -0
  46. package/src/index.ts +101 -159
  47. package/src/provider-constants.ts +2 -31
  48. package/src/control-plane/connection-mapping.ts +0 -191
  49. package/src/control-plane/connection-migration-flags.ts +0 -40
  50. package/src/control-plane/connection-profiles.ts +0 -317
  51. package/src/control-plane/core-asset-provider.ts +0 -21
  52. package/src/control-plane/fs-asset-provider.ts +0 -65
  53. package/src/control-plane/fs-registry-provider.ts +0 -46
  54. package/src/control-plane/paths.ts +0 -77
  55. package/src/control-plane/registry-provider.ts +0 -19
  56. package/src/control-plane/staging.ts +0 -399
@@ -1,317 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { PROVIDER_KEY_MAP } from '../provider-constants.js';
3
- import type {
4
- CapabilityAssignments,
5
- CanonicalConnectionProfile,
6
- CanonicalConnectionsDocument,
7
- ConnectionKind,
8
- } from './types.js';
9
-
10
- const CONNECTIONS_DIRNAME = 'connections';
11
- const CONNECTION_PROFILES_FILENAME = 'profiles.json';
12
-
13
- const LOCAL_PROVIDERS = new Set(['ollama', 'lmstudio', 'model-runner']);
14
- const INSTACK_PROVIDERS = new Set(['ollama-instack']);
15
-
16
- function normalizeConnectionKind(provider: string): ConnectionKind {
17
- if (INSTACK_PROVIDERS.has(provider)) return 'ollama_local';
18
- if (LOCAL_PROVIDERS.has(provider)) return 'openai_compatible_local';
19
- return 'openai_compatible_remote';
20
- }
21
-
22
- export function getConnectionProfilesDir(configDir: string): string {
23
- return `${configDir}/${CONNECTIONS_DIRNAME}`;
24
- }
25
-
26
- export function getConnectionProfilesPath(configDir: string): string {
27
- return `${getConnectionProfilesDir(configDir)}/${CONNECTION_PROFILES_FILENAME}`;
28
- }
29
-
30
- // ── Validation helpers ──────────────────────────────────────────────────
31
-
32
- function isRecord(value: unknown): value is Record<string, unknown> {
33
- return typeof value === 'object' && value !== null && !Array.isArray(value);
34
- }
35
-
36
- function isNonEmptyString(value: unknown): value is string {
37
- return typeof value === 'string' && value.trim().length > 0;
38
- }
39
-
40
- function isValidProfile(value: unknown): value is CanonicalConnectionProfile {
41
- if (!isRecord(value)) return false;
42
- if (!isNonEmptyString(value.id)) return false;
43
- if (!isNonEmptyString(value.name)) return false;
44
- if (value.kind !== 'openai_compatible_remote' && value.kind !== 'openai_compatible_local' && value.kind !== 'ollama_local') return false;
45
- if (!isNonEmptyString(value.provider)) return false;
46
- if (typeof value.baseUrl !== 'string') return false;
47
- if (!isRecord(value.auth)) return false;
48
- if (value.auth.mode !== 'api_key' && value.auth.mode !== 'none') return false;
49
- if (value.auth.mode === 'api_key' && !isNonEmptyString(value.auth.apiKeySecretRef)) return false;
50
- return true;
51
- }
52
-
53
- function isValidConnectionDocument(value: unknown): value is CanonicalConnectionsDocument {
54
- if (!isRecord(value)) return false;
55
- if (value.version !== 1) return false;
56
- if (!Array.isArray(value.profiles) || value.profiles.length === 0) return false;
57
- if (!isRecord(value.assignments)) return false;
58
-
59
- if (!value.profiles.every(isValidProfile)) return false;
60
-
61
- const llm = value.assignments.llm;
62
- const embeddings = value.assignments.embeddings;
63
- if (!isRecord(llm) || !isRecord(embeddings)) return false;
64
- if (!isNonEmptyString(llm.connectionId) || !isNonEmptyString(llm.model)) return false;
65
- if (!isNonEmptyString(embeddings.connectionId) || !isNonEmptyString(embeddings.model)) return false;
66
- const embeddingDims = embeddings.embeddingDims;
67
- if (
68
- embeddingDims !== undefined
69
- && (typeof embeddingDims !== 'number' || !Number.isInteger(embeddingDims) || embeddingDims <= 0)
70
- ) {
71
- return false;
72
- }
73
-
74
- return true;
75
- }
76
-
77
- // ── Read / Write ────────────────────────────────────────────────────────
78
-
79
- export function writeConnectionProfilesDocument(
80
- configDir: string,
81
- document: CanonicalConnectionsDocument
82
- ): void {
83
- const dir = getConnectionProfilesDir(configDir);
84
- mkdirSync(dir, { recursive: true });
85
- writeFileSync(
86
- getConnectionProfilesPath(configDir),
87
- JSON.stringify(document, null, 2) + '\n'
88
- );
89
- }
90
-
91
- export function readConnectionProfilesDocument(configDir: string): CanonicalConnectionsDocument {
92
- const path = getConnectionProfilesPath(configDir);
93
- if (!existsSync(path)) {
94
- return {
95
- version: 1,
96
- profiles: [],
97
- assignments: {
98
- llm: { connectionId: '', model: '' },
99
- embeddings: { connectionId: '', model: '' },
100
- },
101
- };
102
- }
103
-
104
- let parsed: unknown;
105
- try {
106
- parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
107
- } catch {
108
- throw new Error('connections/profiles.json is invalid JSON');
109
- }
110
-
111
- if (!isValidConnectionDocument(parsed)) {
112
- throw new Error('connections/profiles.json is invalid: expected CanonicalConnectionsDocument v1');
113
- }
114
-
115
- return parsed;
116
- }
117
-
118
- export function ensureConnectionProfilesStore(configDir: string): void {
119
- mkdirSync(getConnectionProfilesDir(configDir), { recursive: true });
120
- }
121
-
122
- // ── Multi-connection write ──────────────────────────────────────────────
123
-
124
- export type WriteConnectionsInput = {
125
- profiles: Array<{
126
- id: string;
127
- name: string;
128
- provider: string;
129
- baseUrl: string;
130
- hasApiKey: boolean;
131
- apiKeyEnvVar: string;
132
- }>;
133
- assignments: CapabilityAssignments;
134
- };
135
-
136
- export function writeConnectionsDocument(
137
- configDir: string,
138
- input: WriteConnectionsInput
139
- ): CanonicalConnectionsDocument {
140
- if (input.profiles.length === 0) {
141
- throw new Error('writeConnectionsDocument: profiles must not be empty');
142
- }
143
-
144
- const profileIds = new Set(input.profiles.map((p) => p.id));
145
- if (!profileIds.has(input.assignments.llm.connectionId)) {
146
- throw new Error(`writeConnectionsDocument: llm.connectionId "${input.assignments.llm.connectionId}" not found in profiles`);
147
- }
148
- if (!profileIds.has(input.assignments.embeddings.connectionId)) {
149
- throw new Error(`writeConnectionsDocument: embeddings.connectionId "${input.assignments.embeddings.connectionId}" not found in profiles`);
150
- }
151
-
152
- const document: CanonicalConnectionsDocument = {
153
- version: 1,
154
- profiles: input.profiles.map((p) => ({
155
- id: p.id,
156
- name: p.name,
157
- kind: normalizeConnectionKind(p.provider),
158
- provider: p.provider,
159
- baseUrl: p.baseUrl,
160
- auth: {
161
- mode: p.hasApiKey ? 'api_key' as const : 'none' as const,
162
- ...(p.hasApiKey ? { apiKeySecretRef: `env:${p.apiKeyEnvVar}` } : {}),
163
- },
164
- })),
165
- assignments: input.assignments,
166
- };
167
-
168
- writeConnectionProfilesDocument(configDir, document);
169
- return document;
170
- }
171
-
172
- // ── CRUD operations ─────────────────────────────────────────────────────
173
-
174
- type MutationResult<T> =
175
- | { ok: true; value: T }
176
- | { ok: false; status: 400 | 404 | 409; message: string };
177
-
178
- function validateProfile(profile: CanonicalConnectionProfile): MutationResult<CanonicalConnectionProfile> {
179
- if (!profile.id.trim()) {
180
- return { ok: false, status: 400, message: 'profile.id is required' };
181
- }
182
- if (!profile.name.trim()) {
183
- return { ok: false, status: 400, message: 'profile.name is required' };
184
- }
185
- if (!profile.provider.trim()) {
186
- return { ok: false, status: 400, message: 'profile.provider is required' };
187
- }
188
- if (profile.auth.mode !== 'api_key' && profile.auth.mode !== 'none') {
189
- return { ok: false, status: 400, message: 'profile.auth.mode must be api_key or none' };
190
- }
191
- if (profile.auth.mode === 'api_key' && !profile.auth.apiKeySecretRef?.trim()) {
192
- return { ok: false, status: 400, message: 'profile.auth.apiKeySecretRef is required when auth.mode is api_key' };
193
- }
194
- return { ok: true, value: profile };
195
- }
196
-
197
- function validateAssignments(
198
- assignments: CapabilityAssignments,
199
- profileIds: Set<string>
200
- ): MutationResult<CapabilityAssignments> {
201
- if (!isRecord(assignments.llm)) {
202
- return { ok: false, status: 400, message: 'assignments.llm must be an object' };
203
- }
204
- if (!isRecord(assignments.embeddings)) {
205
- return { ok: false, status: 400, message: 'assignments.embeddings must be an object' };
206
- }
207
- if (!isNonEmptyString(assignments.llm.connectionId) || !isNonEmptyString(assignments.llm.model)) {
208
- return { ok: false, status: 400, message: 'assignments.llm requires connectionId and model' };
209
- }
210
- if (!isNonEmptyString(assignments.embeddings.connectionId) || !isNonEmptyString(assignments.embeddings.model)) {
211
- return { ok: false, status: 400, message: 'assignments.embeddings requires connectionId and model' };
212
- }
213
- if (!profileIds.has(assignments.llm.connectionId)) {
214
- return { ok: false, status: 409, message: `assignments.llm.connectionId not found: ${assignments.llm.connectionId}` };
215
- }
216
- if (!profileIds.has(assignments.embeddings.connectionId)) {
217
- return { ok: false, status: 409, message: `assignments.embeddings.connectionId not found: ${assignments.embeddings.connectionId}` };
218
- }
219
- if (
220
- assignments.embeddings.embeddingDims !== undefined
221
- && (!Number.isInteger(assignments.embeddings.embeddingDims) || assignments.embeddings.embeddingDims <= 0)
222
- ) {
223
- return { ok: false, status: 400, message: 'assignments.embeddings.embeddingDims must be a positive integer' };
224
- }
225
- return { ok: true, value: assignments };
226
- }
227
-
228
- export function listConnectionProfiles(configDir: string): CanonicalConnectionProfile[] {
229
- return readConnectionProfilesDocument(configDir).profiles;
230
- }
231
-
232
- export function getCapabilityAssignments(configDir: string): CapabilityAssignments {
233
- return readConnectionProfilesDocument(configDir).assignments;
234
- }
235
-
236
- export function createConnectionProfile(
237
- configDir: string,
238
- profile: CanonicalConnectionProfile
239
- ): MutationResult<CanonicalConnectionProfile> {
240
- const validated = validateProfile(profile);
241
- if (!validated.ok) return validated;
242
-
243
- const document = readConnectionProfilesDocument(configDir);
244
- if (document.profiles.some((existing) => existing.id === profile.id)) {
245
- return { ok: false, status: 409, message: `profile already exists: ${profile.id}` };
246
- }
247
-
248
- const updated: CanonicalConnectionsDocument = {
249
- ...document,
250
- profiles: [...document.profiles, profile],
251
- };
252
- writeConnectionProfilesDocument(configDir, updated);
253
- return { ok: true, value: profile };
254
- }
255
-
256
- export function updateConnectionProfile(
257
- configDir: string,
258
- profile: CanonicalConnectionProfile
259
- ): MutationResult<CanonicalConnectionProfile> {
260
- const validated = validateProfile(profile);
261
- if (!validated.ok) return validated;
262
-
263
- const document = readConnectionProfilesDocument(configDir);
264
- const index = document.profiles.findIndex((existing) => existing.id === profile.id);
265
- if (index < 0) {
266
- return { ok: false, status: 404, message: `profile not found: ${profile.id}` };
267
- }
268
-
269
- const profiles = [...document.profiles];
270
- profiles[index] = profile;
271
- writeConnectionProfilesDocument(configDir, { ...document, profiles });
272
- return { ok: true, value: profile };
273
- }
274
-
275
- export function deleteConnectionProfile(
276
- configDir: string,
277
- id: string
278
- ): MutationResult<{ id: string }> {
279
- if (!id.trim()) {
280
- return { ok: false, status: 400, message: 'profile id is required' };
281
- }
282
-
283
- const document = readConnectionProfilesDocument(configDir);
284
- const existing = document.profiles.find((profile) => profile.id === id);
285
- if (!existing) {
286
- return { ok: false, status: 404, message: `profile not found: ${id}` };
287
- }
288
- const assignmentFields = ['llm', 'embeddings', 'reranking', 'tts', 'stt'] as const;
289
- for (const field of assignmentFields) {
290
- const assignment = document.assignments[field];
291
- if (assignment && 'connectionId' in assignment && assignment.connectionId === id) {
292
- return { ok: false, status: 409, message: `Cannot delete profile: it is assigned to ${field}` };
293
- }
294
- }
295
-
296
- writeConnectionProfilesDocument(configDir, {
297
- ...document,
298
- profiles: document.profiles.filter((profile) => profile.id !== id),
299
- });
300
- return { ok: true, value: { id } };
301
- }
302
-
303
- export function saveCapabilityAssignments(
304
- configDir: string,
305
- assignments: CapabilityAssignments
306
- ): MutationResult<CapabilityAssignments> {
307
- const document = readConnectionProfilesDocument(configDir);
308
- const profileIds = new Set(document.profiles.map((profile) => profile.id));
309
- const validated = validateAssignments(assignments, profileIds);
310
- if (!validated.ok) return validated;
311
-
312
- writeConnectionProfilesDocument(configDir, {
313
- ...document,
314
- assignments,
315
- });
316
- return { ok: true, value: assignments };
317
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * CoreAssetProvider interface — dependency injection for bundled assets.
3
- *
4
- * Admin implements this with Vite $assets imports (ViteAssetProvider).
5
- * CLI/lib implements this by reading from DATA_HOME (FilesystemAssetProvider).
6
- */
7
-
8
- export interface CoreAssetProvider {
9
- coreCompose(): string;
10
- caddyfile(): string;
11
- ollamaCompose(): string;
12
- adminCompose(): string;
13
- agentsMd(): string;
14
- opencodeConfig(): string;
15
- adminOpencodeConfig(): string;
16
- secretsSchema(): string;
17
- stackSchema(): string;
18
- cleanupLogs(): string;
19
- cleanupData(): string;
20
- validateConfig(): string;
21
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * FilesystemAssetProvider — reads core assets from DATA_HOME on disk.
3
- *
4
- * Used by the CLI and any non-Vite consumer. Assets are downloaded from
5
- * GitHub during `openpalm install` and stored in DATA_HOME.
6
- */
7
- import { readFileSync } from "node:fs";
8
- import { join } from "node:path";
9
- import type { CoreAssetProvider } from "./core-asset-provider.js";
10
-
11
- export class FilesystemAssetProvider implements CoreAssetProvider {
12
- constructor(private readonly assetsDir: string) {}
13
-
14
- private read(relPath: string): string {
15
- return readFileSync(join(this.assetsDir, relPath), "utf-8");
16
- }
17
-
18
- coreCompose(): string {
19
- return this.read("docker-compose.yml");
20
- }
21
-
22
- caddyfile(): string {
23
- return this.read("caddy/Caddyfile");
24
- }
25
-
26
- ollamaCompose(): string {
27
- return this.read("ollama.yml");
28
- }
29
-
30
- adminCompose(): string {
31
- return this.read("admin.yml");
32
- }
33
-
34
- agentsMd(): string {
35
- return this.read("assistant/AGENTS.md");
36
- }
37
-
38
- opencodeConfig(): string {
39
- return this.read("assistant/opencode.jsonc");
40
- }
41
-
42
- adminOpencodeConfig(): string {
43
- return this.read("admin/opencode.jsonc");
44
- }
45
-
46
- secretsSchema(): string {
47
- return this.read("secrets.env.schema");
48
- }
49
-
50
- stackSchema(): string {
51
- return this.read("stack.env.schema");
52
- }
53
-
54
- cleanupLogs(): string {
55
- return this.read("automations/cleanup-logs.yml");
56
- }
57
-
58
- cleanupData(): string {
59
- return this.read("automations/cleanup-data.yml");
60
- }
61
-
62
- validateConfig(): string {
63
- return this.read("automations/validate-config.yml");
64
- }
65
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * FilesystemRegistryProvider — reads registry catalog from a directory on disk.
3
- *
4
- * Used by the CLI. Reads .yml and .caddy files from the registry/ directory,
5
- * which is downloaded from GitHub during install or available in the repo.
6
- */
7
- import { existsSync, readdirSync, readFileSync } from "node:fs";
8
- import { join } from "node:path";
9
- import type { RegistryProvider } from "./registry-provider.js";
10
-
11
- export class FilesystemRegistryProvider implements RegistryProvider {
12
- constructor(private readonly registryDir: string) {}
13
-
14
- channelYml(): Record<string, string> {
15
- return this.loadDir("channels", ".yml");
16
- }
17
-
18
- channelCaddy(): Record<string, string> {
19
- return this.loadDir("channels", ".caddy");
20
- }
21
-
22
- channelNames(): string[] {
23
- return Object.keys(this.channelYml());
24
- }
25
-
26
- automationYml(): Record<string, string> {
27
- return this.loadDir("automations", ".yml");
28
- }
29
-
30
- automationNames(): string[] {
31
- return Object.keys(this.automationYml());
32
- }
33
-
34
- private loadDir(subdir: string, ext: string): Record<string, string> {
35
- const dir = join(this.registryDir, subdir);
36
- if (!existsSync(dir)) return {};
37
-
38
- const result: Record<string, string> = {};
39
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
40
- if (!entry.isFile() || !entry.name.endsWith(ext)) continue;
41
- const name = entry.name.replace(new RegExp(`\\${ext}$`), "");
42
- result[name] = readFileSync(join(dir, entry.name), "utf-8");
43
- }
44
- return result;
45
- }
46
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * XDG path resolution and directory setup for the OpenPalm control plane.
3
- *
4
- * Directory model (XDG-compliant):
5
- * CONFIG_HOME (~/.config/openpalm) — user-editable: secrets.env, channels/, assistant/
6
- * DATA_HOME (~/.local/share/openpalm) — opaque service data (memory, etc.)
7
- * STATE_HOME (~/.local/state/openpalm) — assembled runtime, audit logs
8
- */
9
- import { mkdirSync } from "node:fs";
10
- import { resolve as resolvePath } from "node:path";
11
-
12
- export function resolveHome(): string {
13
- return process.env.HOME ?? "/tmp";
14
- }
15
-
16
- export function resolveConfigHome(): string {
17
- const raw = process.env.OPENPALM_CONFIG_HOME;
18
- if (!raw) return `${resolveHome()}/.config/openpalm`;
19
- return resolvePath(raw);
20
- }
21
-
22
- export function resolveStateHome(): string {
23
- const raw = process.env.OPENPALM_STATE_HOME;
24
- if (!raw) return `${resolveHome()}/.local/state/openpalm`;
25
- return resolvePath(raw);
26
- }
27
-
28
- export function resolveDataHome(): string {
29
- const raw = process.env.OPENPALM_DATA_HOME;
30
- if (!raw) return `${resolveHome()}/.local/share/openpalm`;
31
- return resolvePath(raw);
32
- }
33
-
34
- /**
35
- * Create the full XDG directory tree.
36
- *
37
- * CONFIG_HOME (~/.config/openpalm) — user-editable configuration
38
- * DATA_HOME (~/.local/share/openpalm) — opaque persistent service data
39
- * STATE_HOME (~/.local/state/openpalm) — generated artifacts, audit logs
40
- */
41
- export function ensureXdgDirs(): void {
42
- const dataHome = resolveDataHome();
43
- const configHome = resolveConfigHome();
44
- const stateHome = resolveStateHome();
45
-
46
- for (const dir of [
47
- // CONFIG_HOME — user-editable
48
- configHome,
49
- `${configHome}/channels`,
50
- `${configHome}/connections`,
51
- `${configHome}/assistant`,
52
- `${configHome}/automations`,
53
- `${configHome}/stash`,
54
-
55
- // DATA_HOME — persistent service data (pre-created to avoid root-owned dirs)
56
- dataHome,
57
- `${dataHome}/admin`,
58
- `${dataHome}/memory`,
59
- `${dataHome}/assistant`,
60
- `${dataHome}/guardian`,
61
- `${dataHome}/caddy`,
62
- `${dataHome}/caddy/data`,
63
- `${dataHome}/caddy/config`,
64
- `${dataHome}/automations`,
65
- `${dataHome}/opencode`,
66
-
67
- // STATE_HOME — assembled runtime
68
- stateHome,
69
- `${stateHome}/artifacts`,
70
- `${stateHome}/audit`,
71
- `${stateHome}/artifacts/channels`,
72
- `${stateHome}/automations`,
73
- `${stateHome}/opencode`
74
- ]) {
75
- mkdirSync(dir, { recursive: true });
76
- }
77
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * RegistryProvider interface — dependency injection for registry catalog.
3
- *
4
- * Admin implements this with Vite import.meta.glob (ViteRegistryProvider).
5
- * CLI/lib implements this by reading from registry/ directory (FilesystemRegistryProvider).
6
- */
7
-
8
- export interface RegistryProvider {
9
- /** Channel compose overlay YMLs, keyed by channel name. */
10
- channelYml(): Record<string, string>;
11
- /** Channel Caddy routes (optional), keyed by channel name. */
12
- channelCaddy(): Record<string, string>;
13
- /** Names of available registry channels. */
14
- channelNames(): string[];
15
- /** Automation configs, keyed by automation name. */
16
- automationYml(): Record<string, string>;
17
- /** Names of available registry automations. */
18
- automationNames(): string[];
19
- }