@celilo/cli 0.1.5 → 0.1.6
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/drizzle/0004_caddy_hostname_list.sql +25 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +9 -2
- package/src/ansible/inventory.test.ts +3 -2
- package/src/ansible/inventory.ts +5 -1
- package/src/capabilities/public-web-helpers.test.ts +2 -2
- package/src/capabilities/public-web-publish.test.ts +34 -1
- package/src/cli/cli.test.ts +2 -2
- package/src/cli/command-registry.ts +146 -3
- package/src/cli/command-tree-parser.test.ts +1 -1
- package/src/cli/command-tree-parser.ts +9 -8
- package/src/cli/commands/hook-run.ts +15 -66
- package/src/cli/commands/module-audit.ts +14 -44
- package/src/cli/commands/module-deploy.ts +4 -1
- package/src/cli/commands/module-import-registry.test.ts +115 -0
- package/src/cli/commands/module-import.ts +106 -22
- package/src/cli/commands/module-publish.test.ts +235 -0
- package/src/cli/commands/module-publish.ts +234 -0
- package/src/cli/commands/module-remove.ts +82 -2
- package/src/cli/commands/module-search.ts +57 -0
- package/src/cli/commands/module-secret-get.ts +59 -0
- package/src/cli/commands/module-terraform-unlock.ts +57 -0
- package/src/cli/commands/module-verify.test.ts +59 -0
- package/src/cli/commands/module-verify.ts +53 -0
- package/src/cli/commands/status.ts +30 -20
- package/src/cli/commands/system-audit.test.ts +138 -0
- package/src/cli/commands/system-audit.ts +571 -0
- package/src/cli/commands/system-update.ts +391 -0
- package/src/cli/completion.ts +15 -1
- package/src/cli/fuel-gauge.ts +68 -3
- package/src/cli/generate-zsh-completion.ts +13 -3
- package/src/cli/index.ts +112 -5
- package/src/cli/parser.ts +11 -0
- package/src/cli/prompts.ts +36 -5
- package/src/cli/tui/audit-state.test.ts +246 -0
- package/src/cli/tui/audit-state.ts +525 -0
- package/src/cli/tui/audit-tui.test.tsx +135 -0
- package/src/cli/tui/audit-tui.tsx +624 -0
- package/src/cli/tui/celebration.tsx +29 -0
- package/src/cli/tui/clipboard.test.ts +94 -0
- package/src/cli/tui/clipboard.ts +101 -0
- package/src/cli/tui/icons.ts +22 -0
- package/src/cli/tui/keybar.tsx +65 -0
- package/src/cli/tui/keymap.test.ts +105 -0
- package/src/cli/tui/keymap.ts +70 -0
- package/src/cli/tui/modals/analyzing.tsx +75 -0
- package/src/cli/tui/modals/celebration.tsx +44 -0
- package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
- package/src/cli/tui/modals/remediate.tsx +44 -0
- package/src/cli/tui/modals.test.ts +137 -0
- package/src/cli/tui/mouse.test.ts +78 -0
- package/src/cli/tui/mouse.ts +114 -0
- package/src/cli/tui/panes/categories.tsx +62 -0
- package/src/cli/tui/panes/command-log.tsx +87 -0
- package/src/cli/tui/panes/detail.tsx +175 -0
- package/src/cli/tui/panes/findings.tsx +97 -0
- package/src/cli/tui/panes/summary.tsx +64 -0
- package/src/cli/tui/spawn.ts +130 -0
- package/src/cli/tui/theme.ts +42 -0
- package/src/cli/tui/wrap.test.ts +43 -0
- package/src/cli/tui/wrap.ts +45 -0
- package/src/cli/types.ts +5 -0
- package/src/db/client.ts +55 -2
- package/src/db/schema.ts +26 -17
- package/src/hooks/capability-loader.ts +133 -73
- package/src/hooks/define-hook.test.ts +9 -1
- package/src/hooks/executor.ts +22 -1
- package/src/hooks/load-hook-config.test.ts +165 -0
- package/src/hooks/load-hook-config.ts +60 -0
- package/src/hooks/logger.ts +42 -12
- package/src/hooks/run-named-hook.ts +128 -0
- package/src/hooks/types.ts +19 -0
- package/src/manifest/ensure-schema.test.ts +115 -0
- package/src/manifest/schema.ts +76 -0
- package/src/module/import.ts +20 -12
- package/src/module/packaging/build.ts +85 -16
- package/src/module/packaging/release-metadata.test.ts +103 -0
- package/src/module/packaging/release-metadata.ts +145 -0
- package/src/registry/client.test.ts +228 -0
- package/src/registry/client.ts +157 -0
- package/src/services/audit/backups.test.ts +233 -0
- package/src/services/audit/backups.ts +128 -0
- package/src/services/audit/capability-abi.test.ts +153 -0
- package/src/services/audit/capability-abi.ts +204 -0
- package/src/services/audit/cli-version.test.ts +60 -0
- package/src/services/audit/cli-version.ts +87 -0
- package/src/services/audit/health.test.ts +84 -0
- package/src/services/audit/health.ts +43 -0
- package/src/services/audit/index.test.ts +99 -0
- package/src/services/audit/index.ts +118 -0
- package/src/services/audit/machines-reachable.test.ts +87 -0
- package/src/services/audit/machines-reachable.ts +87 -0
- package/src/services/audit/module-configs.test.ts +131 -0
- package/src/services/audit/module-configs.ts +80 -0
- package/src/services/audit/module-versions.test.ts +99 -0
- package/src/services/audit/module-versions.ts +154 -0
- package/src/services/audit/schema.test.ts +68 -0
- package/src/services/audit/schema.ts +115 -0
- package/src/services/audit/secrets-decryptable.test.ts +82 -0
- package/src/services/audit/secrets-decryptable.ts +97 -0
- package/src/services/audit/services-credentials.test.ts +54 -0
- package/src/services/audit/services-credentials.ts +64 -0
- package/src/services/audit/services-reachable.test.ts +60 -0
- package/src/services/audit/services-reachable.ts +64 -0
- package/src/services/audit/terraform-plan.test.ts +127 -0
- package/src/services/audit/terraform-plan.ts +153 -0
- package/src/services/audit/types.test.ts +36 -0
- package/src/services/audit/types.ts +90 -0
- package/src/services/audit/unconfigured-modules.test.ts +48 -0
- package/src/services/audit/unconfigured-modules.ts +71 -0
- package/src/services/audit/undeployed-modules.test.ts +66 -0
- package/src/services/audit/undeployed-modules.ts +72 -0
- package/src/services/build-stream.ts +122 -122
- package/src/services/config-interview.ts +407 -2
- package/src/services/deploy-ansible.ts +73 -7
- package/src/services/deploy-preflight.ts +45 -4
- package/src/services/deploy-terraform.ts +31 -24
- package/src/services/deploy-validation.ts +167 -23
- package/src/services/ensure-interview.test.ts +245 -0
- package/src/services/health-runner.ts +110 -38
- package/src/services/module-build.ts +11 -13
- package/src/services/module-deploy.ts +370 -59
- package/src/services/ssh-key-manager.test.ts +1 -1
- package/src/services/ssh-key-manager.ts +3 -2
- package/src/services/terraform-env.ts +62 -0
- package/src/services/update/dep-graph.test.ts +214 -0
- package/src/services/update/dep-graph.ts +215 -0
- package/src/services/update/orchestrator.test.ts +463 -0
- package/src/services/update/orchestrator.ts +359 -0
- package/src/services/update/progress.ts +49 -0
- package/src/services/update/self-update.test.ts +68 -0
- package/src/services/update/self-update.ts +57 -0
- package/src/services/update/types.ts +94 -0
- package/src/templates/generator.test.ts +1 -1
- package/src/templates/generator.ts +42 -1
- package/src/test-utils/completion-harness.test.ts +1 -1
- package/src/test-utils/completion-harness.ts +4 -4
- package/src/variables/capability-self-ref.test.ts +203 -0
- package/src/variables/context.ts +49 -1
- package/src/variables/declarative-derivation.test.ts +306 -0
- package/src/variables/declarative-derivation.ts +4 -2
- package/src/variables/parser.test.ts +56 -1
- package/src/variables/parser.ts +47 -6
- package/src/variables/resolver.ts +27 -9
- package/tsconfig.json +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, statSync } from 'node:fs';
|
|
3
|
-
import { chmod, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { chmod, cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join, relative } from 'node:path';
|
|
5
5
|
import { and, eq } from 'drizzle-orm';
|
|
6
6
|
import { generateInventory } from '../ansible/inventory';
|
|
@@ -209,6 +209,31 @@ export async function readTemplateFiles(
|
|
|
209
209
|
* @param outputPath - Base output path
|
|
210
210
|
* @param files - Generated files to write
|
|
211
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* Copy each `ansible/roles/<role>/files/` directory from the module source
|
|
214
|
+
* to the generated output, verbatim (preserving binary content + mode bits).
|
|
215
|
+
*
|
|
216
|
+
* These hold Ansible role-local static assets — they may be binaries and
|
|
217
|
+
* don't need template variable resolution, so the standard template
|
|
218
|
+
* pipeline (which is utf-8) can't handle them.
|
|
219
|
+
*/
|
|
220
|
+
export async function copyAnsibleRoleFilesDirs(
|
|
221
|
+
modulePath: string,
|
|
222
|
+
outputPath: string,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const rolesDir = join(modulePath, 'ansible', 'roles');
|
|
225
|
+
if (!existsSync(rolesDir)) return;
|
|
226
|
+
const roles = await readdir(rolesDir, { withFileTypes: true });
|
|
227
|
+
for (const role of roles) {
|
|
228
|
+
if (!role.isDirectory()) continue;
|
|
229
|
+
const srcFilesDir = join(rolesDir, role.name, 'files');
|
|
230
|
+
if (!existsSync(srcFilesDir)) continue;
|
|
231
|
+
const destFilesDir = join(outputPath, 'ansible', 'roles', role.name, 'files');
|
|
232
|
+
await mkdir(dirname(destFilesDir), { recursive: true });
|
|
233
|
+
await cp(srcFilesDir, destFilesDir, { recursive: true, preserveTimestamps: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
212
237
|
export async function writeGeneratedFiles(
|
|
213
238
|
outputPath: string,
|
|
214
239
|
files: GeneratedFile[],
|
|
@@ -913,6 +938,22 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
913
938
|
};
|
|
914
939
|
}
|
|
915
940
|
|
|
941
|
+
// Execution: Copy Ansible role-local `files/` directories verbatim.
|
|
942
|
+
// These hold static assets (binaries, certs, scripts, blobs) used by
|
|
943
|
+
// Ansible's `copy` module. They're outside the template pipeline because
|
|
944
|
+
// (a) they don't need variable resolution and (b) they may be binary —
|
|
945
|
+
// utf-8 round-tripping would corrupt them. The template pipeline only
|
|
946
|
+
// handles .tpl/.j2/.yml/.yaml/.tf files.
|
|
947
|
+
try {
|
|
948
|
+
await copyAnsibleRoleFilesDirs(modulePath, outputPath);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
return {
|
|
951
|
+
success: false,
|
|
952
|
+
error: 'Failed to copy Ansible role files/ directories',
|
|
953
|
+
details: error,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
916
957
|
// Execution: Copy capability-provided Ansible tasks into generated output
|
|
917
958
|
// When a module requires capabilities that provide Ansible tasks (e.g., dns_registrar),
|
|
918
959
|
// copy those tasks so the consumer's playbook can include them.
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { expect } from 'bun:test';
|
|
9
|
-
import {
|
|
9
|
+
import type { CLIContext } from './cli-context';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Test harness for CLI completion
|
|
13
13
|
*/
|
|
14
14
|
export class CompletionHarness {
|
|
15
|
-
constructor(private cli:
|
|
15
|
+
constructor(private cli: CLIContext) {}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Get completions for a given set of words
|
|
@@ -26,10 +26,10 @@ export class CompletionHarness {
|
|
|
26
26
|
// If partial=false, we're completing the next word after all current words
|
|
27
27
|
const currentIndex = partial && words.length > 0 ? words.length - 1 : words.length;
|
|
28
28
|
try {
|
|
29
|
-
const result =
|
|
29
|
+
const result = await this.cli.run(`--get-completions ${words.join(' ')} ${currentIndex}`);
|
|
30
30
|
|
|
31
31
|
// Parse completion output (one per line)
|
|
32
|
-
return result.split('\n').filter(Boolean);
|
|
32
|
+
return result.stdout.split('\n').filter(Boolean);
|
|
33
33
|
} catch (_error) {
|
|
34
34
|
// Completion errors return empty array
|
|
35
35
|
return [];
|
|
@@ -9,6 +9,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { createDbClient } from '@/db/client';
|
|
12
|
+
import { buildResolutionContext } from './context';
|
|
12
13
|
import { resolveVariable } from './resolver';
|
|
13
14
|
import type { ResolutionContext } from './types';
|
|
14
15
|
|
|
@@ -217,4 +218,206 @@ describe('Capability data $self: variable resolution', () => {
|
|
|
217
218
|
expect(result.error).toContain("has not configured 'ip.primary'");
|
|
218
219
|
}
|
|
219
220
|
});
|
|
221
|
+
|
|
222
|
+
test('buildResolutionContext resolves capability-derived variable even when DB has raw unresolved $self: value', async () => {
|
|
223
|
+
// Regression: hook config was assembled from raw DB reads without applying
|
|
224
|
+
// capability derivation. When lunacycle stores primary_domain as "$self:primary_domain"
|
|
225
|
+
// (because the DB guard skipped persisting the resolved value), the hook
|
|
226
|
+
// received the literal template string instead of "iamtheinternet.org".
|
|
227
|
+
// After the fix, buildResolutionContext always returns the resolved value in selfConfig.
|
|
228
|
+
const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-raw-'));
|
|
229
|
+
testDirs.push(testDir);
|
|
230
|
+
const db = createDbClient({ path: join(testDir, 'test.db') });
|
|
231
|
+
|
|
232
|
+
db.$client
|
|
233
|
+
.prepare(
|
|
234
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
235
|
+
)
|
|
236
|
+
.run(
|
|
237
|
+
'namecheap',
|
|
238
|
+
'Namecheap',
|
|
239
|
+
'1.0.0',
|
|
240
|
+
'/tmp/modules/namecheap',
|
|
241
|
+
'VERIFIED',
|
|
242
|
+
JSON.stringify({ id: 'namecheap', name: 'Namecheap', version: '1.0.0' }),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
db.$client
|
|
246
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
247
|
+
.run('namecheap', 'primary_domain', 'iamtheinternet.org');
|
|
248
|
+
|
|
249
|
+
db.$client
|
|
250
|
+
.prepare(
|
|
251
|
+
'INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?)',
|
|
252
|
+
)
|
|
253
|
+
.run(
|
|
254
|
+
'namecheap',
|
|
255
|
+
'dns_registrar',
|
|
256
|
+
'2.0.0',
|
|
257
|
+
JSON.stringify({ primary_domain: '$self:primary_domain' }),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const consumerManifest = {
|
|
261
|
+
id: 'lunacycle',
|
|
262
|
+
name: 'LunaCycle',
|
|
263
|
+
version: '1.0.0',
|
|
264
|
+
celilo_contract: '1.0',
|
|
265
|
+
variables: {
|
|
266
|
+
owns: [
|
|
267
|
+
{
|
|
268
|
+
name: 'primary_domain',
|
|
269
|
+
type: 'string',
|
|
270
|
+
required: false,
|
|
271
|
+
source: 'capability',
|
|
272
|
+
derive_from: '$capability:dns_registrar.primary_domain',
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
db.$client
|
|
279
|
+
.prepare(
|
|
280
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
281
|
+
)
|
|
282
|
+
.run(
|
|
283
|
+
'lunacycle',
|
|
284
|
+
'LunaCycle',
|
|
285
|
+
'1.0.0',
|
|
286
|
+
'/tmp/modules/lunacycle',
|
|
287
|
+
'INSTALLED',
|
|
288
|
+
JSON.stringify(consumerManifest),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Simulate raw DB state: primary_domain stored as unresolved template string
|
|
292
|
+
db.$client
|
|
293
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
294
|
+
.run('lunacycle', 'primary_domain', '$self:primary_domain');
|
|
295
|
+
|
|
296
|
+
const context = await buildResolutionContext('lunacycle', db);
|
|
297
|
+
|
|
298
|
+
// selfConfig should have the resolved value, not the raw DB "$self:primary_domain"
|
|
299
|
+
expect(context.selfConfig.primary_domain).toBe('iamtheinternet.org');
|
|
300
|
+
|
|
301
|
+
// Simulate the hook installConfigMap merge logic from module-deploy.ts:
|
|
302
|
+
// Raw DB read produces the unresolved string; the fix overwrites it from context.selfConfig
|
|
303
|
+
const installConfigMap: Record<string, unknown> = { primary_domain: '$self:primary_domain' };
|
|
304
|
+
for (const [key, value] of Object.entries(context.selfConfig)) {
|
|
305
|
+
if (
|
|
306
|
+
!(key in installConfigMap) ||
|
|
307
|
+
(typeof installConfigMap[key] === 'string' &&
|
|
308
|
+
(installConfigMap[key] as string).startsWith('$'))
|
|
309
|
+
) {
|
|
310
|
+
installConfigMap[key] = value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
expect(installConfigMap.primary_domain).toBe('iamtheinternet.org');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('hook config merge does not overwrite user-set values with capability-derived ones', async () => {
|
|
317
|
+
// If a user explicitly sets a value that happens to match a capability-derived variable,
|
|
318
|
+
// their value (which does NOT start with $) should be preserved.
|
|
319
|
+
const installConfigMap: Record<string, unknown> = {
|
|
320
|
+
primary_domain: 'my-custom-domain.com', // user-set, no $ prefix
|
|
321
|
+
other_key: '$self:unresolved', // raw template — should be overwritten
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const resolvedSelfConfig: Record<string, string> = {
|
|
325
|
+
primary_domain: 'iamtheinternet.org', // capability-derived value
|
|
326
|
+
other_key: 'resolved-value',
|
|
327
|
+
new_key: 'auto-derived',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (const [key, value] of Object.entries(resolvedSelfConfig)) {
|
|
331
|
+
if (
|
|
332
|
+
!(key in installConfigMap) ||
|
|
333
|
+
(typeof installConfigMap[key] === 'string' &&
|
|
334
|
+
(installConfigMap[key] as string).startsWith('$'))
|
|
335
|
+
) {
|
|
336
|
+
installConfigMap[key] = value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// User-set value preserved (no $ prefix → not overwritten)
|
|
341
|
+
expect(installConfigMap.primary_domain).toBe('my-custom-domain.com');
|
|
342
|
+
// Unresolved template overwritten
|
|
343
|
+
expect(installConfigMap.other_key).toBe('resolved-value');
|
|
344
|
+
// New keys from context added
|
|
345
|
+
expect(installConfigMap.new_key).toBe('auto-derived');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('buildResolutionContext resolves $self: refs in capability data for consumer module', async () => {
|
|
349
|
+
// Regression test: lunacycle's primary_domain (source: capability, derive_from:
|
|
350
|
+
// "$capability:dns_registrar.primary_domain") should resolve to the provider's
|
|
351
|
+
// actual configured value, not the raw "$self:primary_domain" template string.
|
|
352
|
+
const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-ctx-'));
|
|
353
|
+
testDirs.push(testDir);
|
|
354
|
+
const db = createDbClient({ path: join(testDir, 'test.db') });
|
|
355
|
+
|
|
356
|
+
// Provider module (namecheap) with primary_domain configured
|
|
357
|
+
db.$client
|
|
358
|
+
.prepare(
|
|
359
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
360
|
+
)
|
|
361
|
+
.run(
|
|
362
|
+
'namecheap',
|
|
363
|
+
'Namecheap',
|
|
364
|
+
'1.0.0',
|
|
365
|
+
'/tmp/modules/namecheap',
|
|
366
|
+
'VERIFIED',
|
|
367
|
+
JSON.stringify({ id: 'namecheap', name: 'Namecheap', version: '1.0.0' }),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
db.$client
|
|
371
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
372
|
+
.run('namecheap', 'primary_domain', 'iamtheinternet.org');
|
|
373
|
+
|
|
374
|
+
// Capability data stored with unresolved $self: reference (as registered at import time)
|
|
375
|
+
db.$client
|
|
376
|
+
.prepare(
|
|
377
|
+
'INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?)',
|
|
378
|
+
)
|
|
379
|
+
.run(
|
|
380
|
+
'namecheap',
|
|
381
|
+
'dns_registrar',
|
|
382
|
+
'2.0.0',
|
|
383
|
+
JSON.stringify({ provider: 'namecheap', primary_domain: '$self:primary_domain' }),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Consumer module (lunacycle) with capability-derived primary_domain
|
|
387
|
+
const consumerManifest = {
|
|
388
|
+
id: 'lunacycle',
|
|
389
|
+
name: 'LunaCycle',
|
|
390
|
+
version: '1.0.0',
|
|
391
|
+
celilo_contract: '1.0',
|
|
392
|
+
variables: {
|
|
393
|
+
owns: [
|
|
394
|
+
{
|
|
395
|
+
name: 'primary_domain',
|
|
396
|
+
type: 'string',
|
|
397
|
+
required: false,
|
|
398
|
+
source: 'capability',
|
|
399
|
+
derive_from: '$capability:dns_registrar.primary_domain',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
db.$client
|
|
406
|
+
.prepare(
|
|
407
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
408
|
+
)
|
|
409
|
+
.run(
|
|
410
|
+
'lunacycle',
|
|
411
|
+
'LunaCycle',
|
|
412
|
+
'1.0.0',
|
|
413
|
+
'/tmp/modules/lunacycle',
|
|
414
|
+
'INSTALLED',
|
|
415
|
+
JSON.stringify(consumerManifest),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const context = await buildResolutionContext('lunacycle', db);
|
|
419
|
+
|
|
420
|
+
// primary_domain should be resolved to the provider's actual value
|
|
421
|
+
expect(context.selfConfig.primary_domain).toBe('iamtheinternet.org');
|
|
422
|
+
});
|
|
220
423
|
});
|
package/src/variables/context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { ModuleManifest } from '../manifest/schema';
|
|
|
15
15
|
import { decryptSecret } from '../secrets/encryption';
|
|
16
16
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
17
17
|
import { applyDeclarativeDerivations } from './declarative-derivation';
|
|
18
|
+
import { applyIndex, parsePath } from './parser';
|
|
18
19
|
import type { ResolutionContext } from './types';
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -202,6 +203,34 @@ function autoDeriveInventoryVariables(
|
|
|
202
203
|
return derived;
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Recursively resolve "$self:key" strings in a capability data object
|
|
208
|
+
* using the provider module's actual config values. Supports the
|
|
209
|
+
* optional `[N]` array-index suffix (e.g. `$self:domains[0]`) so a
|
|
210
|
+
* provider can declare computed aliases — `primary_domain:
|
|
211
|
+
* $self:domains[0]` — without the framework storing the alias as a
|
|
212
|
+
* separate value. Non-string values and strings that don't start
|
|
213
|
+
* with "$self:" are returned unchanged.
|
|
214
|
+
*/
|
|
215
|
+
function resolveSelfRefsInObject(
|
|
216
|
+
obj: Record<string, unknown>,
|
|
217
|
+
providerConfig: Record<string, unknown>,
|
|
218
|
+
): Record<string, unknown> {
|
|
219
|
+
const result: Record<string, unknown> = {};
|
|
220
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
221
|
+
if (typeof value === 'string' && value.startsWith('$self:')) {
|
|
222
|
+
const { name, index } = parsePath(value.slice(6));
|
|
223
|
+
const resolved = applyIndex(providerConfig[name], index);
|
|
224
|
+
result[key] = resolved !== undefined ? resolved : value;
|
|
225
|
+
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
226
|
+
result[key] = resolveSelfRefsInObject(value as Record<string, unknown>, providerConfig);
|
|
227
|
+
} else {
|
|
228
|
+
result[key] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
205
234
|
/**
|
|
206
235
|
* Build resolution context for a module
|
|
207
236
|
*
|
|
@@ -418,7 +447,26 @@ export async function buildResolutionContext(
|
|
|
418
447
|
} else {
|
|
419
448
|
data = row.data;
|
|
420
449
|
}
|
|
421
|
-
|
|
450
|
+
|
|
451
|
+
// Lazily resolve $self: references in capability data using the provider
|
|
452
|
+
// module's actual config values. Capability data is stored with unresolved
|
|
453
|
+
// $self: references (e.g. namecheap stores primary_domain: "$self:primary_domain")
|
|
454
|
+
// because the real value isn't known at import time. We resolve them here so
|
|
455
|
+
// consuming modules see the actual values (e.g. "iamtheinternet.org") rather
|
|
456
|
+
// than the raw template strings.
|
|
457
|
+
const providerConfigs = db
|
|
458
|
+
.select()
|
|
459
|
+
.from(moduleConfigs)
|
|
460
|
+
.where(eq(moduleConfigs.moduleId, row.moduleId))
|
|
461
|
+
.all();
|
|
462
|
+
const providerConfigMap: Record<string, unknown> = {};
|
|
463
|
+
for (const c of providerConfigs) {
|
|
464
|
+
providerConfigMap[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
|
|
465
|
+
}
|
|
466
|
+
capabilitiesMap[row.capabilityName] = resolveSelfRefsInObject(
|
|
467
|
+
data as Record<string, unknown>,
|
|
468
|
+
providerConfigMap,
|
|
469
|
+
);
|
|
422
470
|
}
|
|
423
471
|
|
|
424
472
|
// Fetch system configuration (for $system: variables)
|