@celilo/cli 0.1.0
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 +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module import command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { getModuleStoragePath, shortenPath } from '../../config/paths';
|
|
7
|
+
import { getDb } from '../../db/client';
|
|
8
|
+
import { importModule } from '../../module/import';
|
|
9
|
+
import { getArg, getFlag, validateRequiredArgs } from '../parser';
|
|
10
|
+
import type { CommandResult } from '../types';
|
|
11
|
+
import { generateTypesForImportedModule } from './module-types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle module import command
|
|
15
|
+
*
|
|
16
|
+
* Usage: celilo module import <path> [--target <path>]
|
|
17
|
+
*
|
|
18
|
+
* @param args - Command arguments
|
|
19
|
+
* @param flags - Command flags
|
|
20
|
+
* @returns Command result
|
|
21
|
+
*/
|
|
22
|
+
export async function handleModuleImport(
|
|
23
|
+
args: string[],
|
|
24
|
+
flags: Record<string, string | boolean>,
|
|
25
|
+
): Promise<CommandResult> {
|
|
26
|
+
// Validate arguments
|
|
27
|
+
const error = validateRequiredArgs(args, 1);
|
|
28
|
+
if (error) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `${error}\n\nUsage: celilo module import <path> [--target <path>]`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sourcePath = getArg(args, 0);
|
|
36
|
+
if (!sourcePath) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Source path is required',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve paths
|
|
44
|
+
const resolvedSourcePath = resolve(sourcePath);
|
|
45
|
+
const targetBasePath = getFlag(flags, 'target', getModuleStoragePath());
|
|
46
|
+
const resolvedTargetBasePath = resolve(targetBasePath);
|
|
47
|
+
|
|
48
|
+
// Import module
|
|
49
|
+
const db = getDb();
|
|
50
|
+
const result = await importModule({
|
|
51
|
+
sourcePath: resolvedSourcePath,
|
|
52
|
+
targetBasePath: resolvedTargetBasePath,
|
|
53
|
+
db,
|
|
54
|
+
flags,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: result.error,
|
|
61
|
+
details: result.details,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Belt-and-suspenders: regenerate the module's celilo/types.d.ts so
|
|
66
|
+
// the imported module can never have stale types. Non-fatal on error —
|
|
67
|
+
// a codegen failure should not abort a successful import.
|
|
68
|
+
const typesPath = await generateTypesForImportedModule(result.targetPath);
|
|
69
|
+
const typesMessage = typesPath ? `\nGenerated types: ${shortenPath(typesPath)}` : '';
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Successfully imported module: ${result.moduleId}\nFiles copied to: ${shortenPath(result.targetPath)}${typesMessage}`,
|
|
74
|
+
data: {
|
|
75
|
+
moduleId: result.moduleId,
|
|
76
|
+
targetPath: result.targetPath,
|
|
77
|
+
typesPath: typesPath ?? undefined,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getDb } from '../../db/client';
|
|
2
|
+
import { modules } from '../../db/schema';
|
|
3
|
+
import type { CommandResult } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handle module list command
|
|
7
|
+
*
|
|
8
|
+
* Usage: celilo module list
|
|
9
|
+
*
|
|
10
|
+
* @returns Command result
|
|
11
|
+
*/
|
|
12
|
+
export async function handleModuleList(): Promise<CommandResult> {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
|
|
15
|
+
// Query all modules
|
|
16
|
+
const moduleRows = db.select().from(modules).all();
|
|
17
|
+
|
|
18
|
+
if (moduleRows.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
success: true,
|
|
21
|
+
message: 'No modules installed',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Format module list
|
|
26
|
+
const lines = ['Installed modules:', ''];
|
|
27
|
+
for (const module of moduleRows) {
|
|
28
|
+
lines.push(`${module.id} (v${module.version}) - ${module.state}`);
|
|
29
|
+
if (module.description) {
|
|
30
|
+
lines.push(` ${module.description}`);
|
|
31
|
+
}
|
|
32
|
+
if (module.errorMessage) {
|
|
33
|
+
lines.push(` Error: ${module.errorMessage}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: lines.join('\n'),
|
|
41
|
+
data: moduleRows,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module logs command - show deploy logs for a module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { eq } from 'drizzle-orm';
|
|
8
|
+
import { getModuleStoragePath } from '../../config/paths';
|
|
9
|
+
import { getDb } from '../../db/client';
|
|
10
|
+
import { modules } from '../../db/schema';
|
|
11
|
+
import { getArg, hasFlag, validateRequiredArgs } from '../parser';
|
|
12
|
+
import type { CommandResult } from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle module logs command
|
|
16
|
+
*
|
|
17
|
+
* Usage: celilo module logs <id> [--tail <n>]
|
|
18
|
+
*/
|
|
19
|
+
export async function handleModuleLogs(
|
|
20
|
+
args: string[],
|
|
21
|
+
flags: Record<string, string | boolean>,
|
|
22
|
+
): Promise<CommandResult> {
|
|
23
|
+
const error = validateRequiredArgs(args, 1);
|
|
24
|
+
if (error) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: `${error}\n\nUsage: celilo module logs <id> [--tail <n>]`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const moduleId = getArg(args, 0);
|
|
32
|
+
if (!moduleId) {
|
|
33
|
+
return { success: false, error: 'Module ID is required' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const db = getDb();
|
|
37
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
38
|
+
if (!module) {
|
|
39
|
+
return { success: false, error: `Module not found: ${moduleId}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const logPath = join(getModuleStoragePath(), moduleId, 'generated', 'deploy.log');
|
|
43
|
+
|
|
44
|
+
if (!existsSync(logPath)) {
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
message: `No deploy logs found for ${moduleId}.\nLogs are created on the next deployment.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
52
|
+
|
|
53
|
+
// --tail N: show only the last N lines
|
|
54
|
+
const tailValue = flags.tail;
|
|
55
|
+
if (typeof tailValue === 'string') {
|
|
56
|
+
const n = Number.parseInt(tailValue, 10);
|
|
57
|
+
if (Number.isNaN(n) || n < 1) {
|
|
58
|
+
return { success: false, error: '--tail requires a positive number' };
|
|
59
|
+
}
|
|
60
|
+
const lines = content.trimEnd().split('\n');
|
|
61
|
+
const tail = lines.slice(-n);
|
|
62
|
+
return { success: true, message: tail.join('\n') };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --last: show only the most recent deploy run
|
|
66
|
+
if (hasFlag(flags, 'last')) {
|
|
67
|
+
const runs = content.split(/(?=\n--- Ansible deploy )/);
|
|
68
|
+
const lastRun = runs[runs.length - 1];
|
|
69
|
+
return { success: true, message: (lastRun || content).trim() };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { success: true, message: content.trim() };
|
|
73
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module remove command
|
|
3
|
+
*
|
|
4
|
+
* Removes a module, destroying infrastructure if present.
|
|
5
|
+
* Requires confirmation for infrastructure modules unless --force is passed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { eq } from 'drizzle-orm';
|
|
11
|
+
import { getDb } from '../../db/client';
|
|
12
|
+
import { moduleInfrastructure, modules } from '../../db/schema';
|
|
13
|
+
import { deallocateForModule } from '../../ipam/auto-allocator';
|
|
14
|
+
import { executeBuildWithProgress } from '../../services/build-stream';
|
|
15
|
+
import { getContainerService, getServiceCredentials } from '../../services/container-service';
|
|
16
|
+
import { getArg, hasFlag, validateRequiredArgs } from '../parser';
|
|
17
|
+
import { log, promptConfirm } from '../prompts';
|
|
18
|
+
import type { CommandResult } from '../types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle module remove command
|
|
22
|
+
*
|
|
23
|
+
* Usage: celilo module remove <id> [--force]
|
|
24
|
+
*
|
|
25
|
+
* @param args - Command arguments
|
|
26
|
+
* @param flags - Command flags
|
|
27
|
+
* @returns Command result
|
|
28
|
+
*/
|
|
29
|
+
export async function handleModuleRemove(
|
|
30
|
+
args: string[],
|
|
31
|
+
flags: Record<string, string | boolean> = {},
|
|
32
|
+
): Promise<CommandResult> {
|
|
33
|
+
// Validate arguments
|
|
34
|
+
const error = validateRequiredArgs(args, 1);
|
|
35
|
+
if (error) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: `${error}\n\nUsage: celilo module remove <id> [--force]`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const moduleId = getArg(args, 0);
|
|
43
|
+
|
|
44
|
+
if (!moduleId) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: 'Module ID is required',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const force = hasFlag(flags, 'force');
|
|
52
|
+
const db = getDb();
|
|
53
|
+
|
|
54
|
+
// Check if module exists
|
|
55
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
56
|
+
|
|
57
|
+
if (!module) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: `Module not found: ${moduleId}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if module has infrastructure that needs to be destroyed
|
|
65
|
+
const infra = db
|
|
66
|
+
.select()
|
|
67
|
+
.from(moduleInfrastructure)
|
|
68
|
+
.where(eq(moduleInfrastructure.moduleId, moduleId))
|
|
69
|
+
.get();
|
|
70
|
+
|
|
71
|
+
const terraformDir = join(module.sourcePath, 'generated', 'terraform');
|
|
72
|
+
const hasTerraformState = existsSync(join(terraformDir, 'terraform.tfstate'));
|
|
73
|
+
|
|
74
|
+
if (infra && hasTerraformState) {
|
|
75
|
+
// Module has infrastructure — need to destroy it
|
|
76
|
+
if (!force) {
|
|
77
|
+
const confirmed = await promptConfirm({
|
|
78
|
+
message: `Module '${moduleId}' has deployed infrastructure. This will run terraform destroy to remove it. Continue?`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!confirmed) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: 'Removal cancelled',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build Terraform env vars (need service credentials for destroy)
|
|
90
|
+
const terraformEnvVars: Record<string, string> = {
|
|
91
|
+
TF_IN_AUTOMATION: '1',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (infra.infrastructureType === 'container_service' && infra.serviceId) {
|
|
95
|
+
const service = await getContainerService(infra.serviceId);
|
|
96
|
+
if (service) {
|
|
97
|
+
const credentials = await getServiceCredentials(infra.serviceId);
|
|
98
|
+
if (service.providerName === 'digitalocean' && 'api_token' in credentials) {
|
|
99
|
+
terraformEnvVars.TF_VAR_digitalocean_token = credentials.api_token;
|
|
100
|
+
} else if (service.providerName === 'proxmox' && 'api_url' in credentials) {
|
|
101
|
+
terraformEnvVars.TF_VAR_proxmox_api_url = credentials.api_url;
|
|
102
|
+
terraformEnvVars.TF_VAR_proxmox_token_id = credentials.api_token_id;
|
|
103
|
+
terraformEnvVars.TF_VAR_proxmox_token_secret = credentials.api_token_secret;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Run terraform destroy
|
|
109
|
+
log.info(`Destroying infrastructure for ${moduleId}...`);
|
|
110
|
+
|
|
111
|
+
const initResult = await executeBuildWithProgress({
|
|
112
|
+
command: 'terraform',
|
|
113
|
+
args: ['init', '-upgrade'],
|
|
114
|
+
cwd: terraformDir,
|
|
115
|
+
title: 'Initializing Terraform',
|
|
116
|
+
env: terraformEnvVars,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!initResult.success) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: `Terraform init failed: ${initResult.error}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const destroyResult = await executeBuildWithProgress({
|
|
127
|
+
command: 'terraform',
|
|
128
|
+
args: ['destroy', '-auto-approve', '-no-color'],
|
|
129
|
+
cwd: terraformDir,
|
|
130
|
+
title: 'Destroying infrastructure',
|
|
131
|
+
env: terraformEnvVars,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!destroyResult.success) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: `Terraform destroy failed: ${destroyResult.error}\n\nInfrastructure may still exist. Check your provider console.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log.success('Infrastructure destroyed');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Remove DNS record (if dns_internal is available)
|
|
145
|
+
try {
|
|
146
|
+
const { autoDeregisterDns } = await import('../../services/dns-auto-register');
|
|
147
|
+
await autoDeregisterDns(moduleId, db, log);
|
|
148
|
+
} catch {
|
|
149
|
+
// Non-fatal -- continue with removal
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Deallocate IPAM resources (if any)
|
|
153
|
+
await deallocateForModule(moduleId, db);
|
|
154
|
+
|
|
155
|
+
// Delete module (cascade will remove configs, secrets, capabilities, infrastructure records)
|
|
156
|
+
db.delete(modules).where(eq(modules.id, moduleId)).run();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
message: `Successfully removed module: ${moduleId}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module debugging commands
|
|
3
|
+
* Show detailed module information including auto-derived values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { eq } from 'drizzle-orm';
|
|
7
|
+
import { getDb } from '../../db/client';
|
|
8
|
+
import { modules } from '../../db/schema';
|
|
9
|
+
import type { ModuleManifest } from '../../manifest/schema';
|
|
10
|
+
import { buildResolutionContext } from '../../variables/context';
|
|
11
|
+
import { getArg, validateRequiredArgs } from '../parser';
|
|
12
|
+
import type { CommandResult } from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle module show-config command
|
|
16
|
+
* Shows ALL configuration including auto-derived values
|
|
17
|
+
*
|
|
18
|
+
* Usage: celilo module show-config <module-id>
|
|
19
|
+
*
|
|
20
|
+
* @param args - Command arguments
|
|
21
|
+
* @returns Command result
|
|
22
|
+
*/
|
|
23
|
+
export async function handleModuleShowConfig(args: string[]): Promise<CommandResult> {
|
|
24
|
+
const error = validateRequiredArgs(args, 1);
|
|
25
|
+
if (error) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: `${error}\n\nUsage: celilo module show-config <module-id>`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const moduleId = getArg(args, 0);
|
|
33
|
+
if (!moduleId) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: 'Module ID is required',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const db = getDb();
|
|
41
|
+
|
|
42
|
+
// Check if module exists
|
|
43
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
44
|
+
if (!module) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: `Module not found: ${moduleId}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Build resolution context to get ALL config (including auto-derived)
|
|
53
|
+
const context = await buildResolutionContext(moduleId, db);
|
|
54
|
+
|
|
55
|
+
const lines = [`Complete configuration for ${moduleId}:`, ''];
|
|
56
|
+
|
|
57
|
+
// Group config by source
|
|
58
|
+
const userConfig: string[] = [];
|
|
59
|
+
const autoConfig: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const [key, value] of Object.entries(context.selfConfig)) {
|
|
62
|
+
// Skip internal/derived keys that aren't useful for display
|
|
63
|
+
if (key.startsWith('requires.machine.')) continue;
|
|
64
|
+
|
|
65
|
+
const line = ` ${key} = ${value}`;
|
|
66
|
+
|
|
67
|
+
// Categorize: inventory.* and some others are auto-derived
|
|
68
|
+
if (
|
|
69
|
+
key.startsWith('inventory.') ||
|
|
70
|
+
key === 'vmid' ||
|
|
71
|
+
key === 'container_ip' ||
|
|
72
|
+
key === 'gateway' ||
|
|
73
|
+
key === 'vlan' ||
|
|
74
|
+
key === 'subnet' ||
|
|
75
|
+
key === 'bridge'
|
|
76
|
+
) {
|
|
77
|
+
autoConfig.push(line);
|
|
78
|
+
} else {
|
|
79
|
+
userConfig.push(line);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (userConfig.length > 0) {
|
|
84
|
+
lines.push('User Configuration:');
|
|
85
|
+
lines.push(...userConfig);
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (autoConfig.length > 0) {
|
|
90
|
+
lines.push('Auto-Derived Configuration:');
|
|
91
|
+
lines.push(...autoConfig);
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show zone if set
|
|
96
|
+
if (context.selfConfig.zone) {
|
|
97
|
+
lines.push(`Network Zone: ${context.selfConfig.zone}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
message: lines.join('\n'),
|
|
103
|
+
data: context.selfConfig,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: `Failed to get module configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handle module show-zone command
|
|
115
|
+
* Shows which zone the module is in
|
|
116
|
+
*
|
|
117
|
+
* Usage: celilo module show-zone <module-id>
|
|
118
|
+
*
|
|
119
|
+
* @param args - Command arguments
|
|
120
|
+
* @returns Command result
|
|
121
|
+
*/
|
|
122
|
+
export async function handleModuleShowZone(args: string[]): Promise<CommandResult> {
|
|
123
|
+
const error = validateRequiredArgs(args, 1);
|
|
124
|
+
if (error) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: `${error}\n\nUsage: celilo module show-zone <module-id>`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const moduleId = getArg(args, 0);
|
|
132
|
+
if (!moduleId) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: 'Module ID is required',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const db = getDb();
|
|
140
|
+
|
|
141
|
+
// Check if module exists
|
|
142
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
143
|
+
if (!module) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: `Module not found: ${moduleId}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Build resolution context to get zone
|
|
152
|
+
const context = await buildResolutionContext(moduleId, db);
|
|
153
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
154
|
+
|
|
155
|
+
const zone = context.selfConfig.zone || manifest.requires?.machine?.zone;
|
|
156
|
+
|
|
157
|
+
if (!zone) {
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
message: `Module ${moduleId} has no zone configured`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const zoneDescriptions: Record<string, string> = {
|
|
165
|
+
dmz: 'DMZ (Public-facing services)',
|
|
166
|
+
app: 'Application (Internal services)',
|
|
167
|
+
secure: 'Secure (Authentication/Database)',
|
|
168
|
+
external: 'External (VPS/Cloud)',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const description = zoneDescriptions[zone] || 'Unknown zone';
|
|
172
|
+
|
|
173
|
+
const lines = [`Module: ${moduleId}`, `Zone: ${zone} - ${description}`, ''];
|
|
174
|
+
|
|
175
|
+
// Show network config if available
|
|
176
|
+
if (zone !== 'external') {
|
|
177
|
+
lines.push('Network Configuration:');
|
|
178
|
+
if (context.selfConfig.gateway) lines.push(` Gateway: ${context.selfConfig.gateway}`);
|
|
179
|
+
if (context.selfConfig.vlan) lines.push(` VLAN: ${context.selfConfig.vlan}`);
|
|
180
|
+
if (context.selfConfig.subnet) lines.push(` Subnet: ${context.selfConfig.subnet}`);
|
|
181
|
+
if (context.selfConfig.bridge) lines.push(` Bridge: ${context.selfConfig.bridge}`);
|
|
182
|
+
} else {
|
|
183
|
+
lines.push('External zone (VPS/Cloud) - no local network config');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
message: lines.join('\n'),
|
|
189
|
+
data: {
|
|
190
|
+
zone,
|
|
191
|
+
network:
|
|
192
|
+
zone !== 'external'
|
|
193
|
+
? {
|
|
194
|
+
gateway: context.selfConfig.gateway,
|
|
195
|
+
vlan: context.selfConfig.vlan,
|
|
196
|
+
subnet: context.selfConfig.subnet,
|
|
197
|
+
bridge: context.selfConfig.bridge,
|
|
198
|
+
}
|
|
199
|
+
: null,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: `Failed to get module zone: ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module status command - show detailed information about a module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { eq } from 'drizzle-orm';
|
|
6
|
+
import { getDb } from '../../db/client';
|
|
7
|
+
import { capabilities, moduleConfigs, modules, secrets } from '../../db/schema';
|
|
8
|
+
import { getArg, validateRequiredArgs } from '../parser';
|
|
9
|
+
import type { CommandResult } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle module status command
|
|
13
|
+
*
|
|
14
|
+
* Usage: celilo module status <id>
|
|
15
|
+
*
|
|
16
|
+
* @param args - Command arguments
|
|
17
|
+
* @returns Command result
|
|
18
|
+
*/
|
|
19
|
+
export async function handleModuleStatus(args: string[]): Promise<CommandResult> {
|
|
20
|
+
// Validate arguments
|
|
21
|
+
const error = validateRequiredArgs(args, 1);
|
|
22
|
+
if (error) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
error: `${error}\n\nUsage: celilo module status <id>`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const moduleId = getArg(args, 0);
|
|
30
|
+
|
|
31
|
+
if (!moduleId) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: 'Module ID is required',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const db = getDb();
|
|
39
|
+
|
|
40
|
+
// Check if module exists
|
|
41
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
42
|
+
|
|
43
|
+
if (!module) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Module not found: ${moduleId}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get configuration values
|
|
51
|
+
const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
|
|
52
|
+
|
|
53
|
+
// Get secrets (names only, not values)
|
|
54
|
+
const moduleSecrets = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
|
|
55
|
+
|
|
56
|
+
// Get capabilities
|
|
57
|
+
const moduleCapabilities = db
|
|
58
|
+
.select()
|
|
59
|
+
.from(capabilities)
|
|
60
|
+
.where(eq(capabilities.moduleId, moduleId))
|
|
61
|
+
.all();
|
|
62
|
+
|
|
63
|
+
// Build sections as multi-line blocks joined by \n\n.
|
|
64
|
+
// The display code in index.ts splits on \n\n and passes each section
|
|
65
|
+
// as a single multi-line string to p.log.message(), avoiding clack's
|
|
66
|
+
// per-line extra spacing.
|
|
67
|
+
const sections: string[] = [];
|
|
68
|
+
|
|
69
|
+
// Section 1: Module metadata
|
|
70
|
+
const metadataLines = [
|
|
71
|
+
`Module: ${module.id}`,
|
|
72
|
+
`Name: ${module.name}`,
|
|
73
|
+
`Version: ${module.version}`,
|
|
74
|
+
`State: ${module.state}`,
|
|
75
|
+
];
|
|
76
|
+
if (module.description) {
|
|
77
|
+
metadataLines.push(`Description: ${module.description}`);
|
|
78
|
+
}
|
|
79
|
+
metadataLines.push(`Source: ${module.sourcePath}`);
|
|
80
|
+
metadataLines.push(`Imported: ${module.importedAt.toISOString()}`);
|
|
81
|
+
metadataLines.push(`Updated: ${module.updatedAt.toISOString()}`);
|
|
82
|
+
if (module.errorMessage) {
|
|
83
|
+
metadataLines.push(`Error: ${module.errorMessage}`);
|
|
84
|
+
}
|
|
85
|
+
sections.push(metadataLines.join('\n'));
|
|
86
|
+
|
|
87
|
+
// Section 2: Configuration
|
|
88
|
+
if (configs.length > 0) {
|
|
89
|
+
const configLines = ['Configuration:'];
|
|
90
|
+
for (const config of configs) {
|
|
91
|
+
const value = config.valueJson ? JSON.stringify(JSON.parse(config.valueJson)) : config.value;
|
|
92
|
+
configLines.push(` ${config.key}: ${value}`);
|
|
93
|
+
}
|
|
94
|
+
sections.push(configLines.join('\n'));
|
|
95
|
+
} else {
|
|
96
|
+
sections.push('Configuration: (none)');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Section 3: Secrets
|
|
100
|
+
if (moduleSecrets.length > 0) {
|
|
101
|
+
const secretLines = ['Secrets:'];
|
|
102
|
+
for (const secret of moduleSecrets) {
|
|
103
|
+
secretLines.push(` ${secret.name}: ********`);
|
|
104
|
+
}
|
|
105
|
+
sections.push(secretLines.join('\n'));
|
|
106
|
+
} else {
|
|
107
|
+
sections.push('Secrets: (none)');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Section 4: Capabilities
|
|
111
|
+
if (moduleCapabilities.length > 0) {
|
|
112
|
+
const capabilityLines = ['Provides Capabilities:'];
|
|
113
|
+
for (const capability of moduleCapabilities) {
|
|
114
|
+
capabilityLines.push(` ${capability.capabilityName} (v${capability.version})`);
|
|
115
|
+
}
|
|
116
|
+
sections.push(capabilityLines.join('\n'));
|
|
117
|
+
} else {
|
|
118
|
+
sections.push('Provides Capabilities: (none)');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
message: sections.join('\n\n'),
|
|
124
|
+
data: {
|
|
125
|
+
module,
|
|
126
|
+
configs,
|
|
127
|
+
secrets: moduleSecrets.map((s) => ({ name: s.name })),
|
|
128
|
+
capabilities: moduleCapabilities,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|