@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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift check: every active module's committed `celilo/types.d.ts`
|
|
3
|
+
* must match what `generateModuleTypes()` would produce from its
|
|
4
|
+
* current `manifest.yml`.
|
|
5
|
+
*
|
|
6
|
+
* This is the CI-level backstop for `celilo module types check`.
|
|
7
|
+
* If a manifest changes `variables.owns`/`variables.imports` but the
|
|
8
|
+
* committed types file is not regenerated, this test fails.
|
|
9
|
+
*
|
|
10
|
+
* Scope: `modules/<id>/manifest.yml` at the repo root. Archived
|
|
11
|
+
* modules (`modules/archive/**`) and scratch extract dirs
|
|
12
|
+
* (`modules/.tmp-*`) are excluded — they are not active modules.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from 'bun:test';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { Glob } from 'bun';
|
|
20
|
+
import { validateManifest } from '../manifest/validate';
|
|
21
|
+
import { generateModuleTypes } from './module-types-generator';
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
24
|
+
const MODULES_DIR = join(REPO_ROOT, 'modules');
|
|
25
|
+
|
|
26
|
+
function findActiveManifests(): string[] {
|
|
27
|
+
const glob = new Glob('*/manifest.yml');
|
|
28
|
+
const results: string[] = [];
|
|
29
|
+
for (const match of glob.scanSync({ cwd: MODULES_DIR, onlyFiles: true })) {
|
|
30
|
+
results.push(join(MODULES_DIR, match));
|
|
31
|
+
}
|
|
32
|
+
return results.sort();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('module types drift check', () => {
|
|
36
|
+
const manifests = findActiveManifests();
|
|
37
|
+
|
|
38
|
+
test('discovers at least one active module', () => {
|
|
39
|
+
expect(manifests.length).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
for (const manifestPath of manifests) {
|
|
43
|
+
const moduleDir = dirname(manifestPath);
|
|
44
|
+
const moduleId = moduleDir.split('/').pop() ?? moduleDir;
|
|
45
|
+
|
|
46
|
+
test(`${moduleId}: committed types.d.ts matches generated output`, () => {
|
|
47
|
+
const yaml = readFileSync(manifestPath, 'utf-8');
|
|
48
|
+
const result = validateManifest(yaml);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
const errorMessages = result.errors.map((e) => ` ${e.path}: ${e.message}`).join('\n');
|
|
51
|
+
throw new Error(`Manifest validation failed for ${moduleId}:\n${errorMessages}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const expected = generateModuleTypes(result.data);
|
|
55
|
+
const typesPath = join(moduleDir, 'celilo', 'types.d.ts');
|
|
56
|
+
|
|
57
|
+
let actual: string;
|
|
58
|
+
try {
|
|
59
|
+
actual = readFileSync(typesPath, 'utf-8');
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Missing committed types file: ${typesPath}\nRun: celilo module types generate ${moduleDir}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (actual !== expected) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Types file is stale: ${typesPath}\nIt does not match what would be generated from the current manifest.yml.\nRun: celilo module types generate ${moduleDir}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the module types generator.
|
|
3
|
+
*
|
|
4
|
+
* These exercise pure codegen — no filesystem I/O. Each test builds a
|
|
5
|
+
* ModuleManifest object in-memory and asserts on the generated string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
10
|
+
import {
|
|
11
|
+
generateModuleTypes,
|
|
12
|
+
moduleIdToPascalCase,
|
|
13
|
+
variableTypeToTs,
|
|
14
|
+
} from './module-types-generator';
|
|
15
|
+
|
|
16
|
+
function baseManifest(overrides: Partial<ModuleManifest> = {}): ModuleManifest {
|
|
17
|
+
return {
|
|
18
|
+
celilo_contract: '1.0',
|
|
19
|
+
id: 'test-module',
|
|
20
|
+
name: 'Test Module',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
requires: { capabilities: [] },
|
|
23
|
+
provides: { capabilities: [] },
|
|
24
|
+
variables: { owns: [], imports: [] },
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('moduleIdToPascalCase', () => {
|
|
30
|
+
test('converts single-word IDs', () => {
|
|
31
|
+
expect(moduleIdToPascalCase('lunacycle')).toBe('Lunacycle');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('converts kebab-case IDs', () => {
|
|
35
|
+
expect(moduleIdToPascalCase('dns-external')).toBe('DnsExternal');
|
|
36
|
+
expect(moduleIdToPascalCase('my-fancy-app')).toBe('MyFancyApp');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('handles empty segments defensively', () => {
|
|
40
|
+
expect(moduleIdToPascalCase('a--b')).toBe('AB');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('preserves numeric characters', () => {
|
|
44
|
+
expect(moduleIdToPascalCase('caddy-v2')).toBe('CaddyV2');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('variableTypeToTs', () => {
|
|
49
|
+
test('maps primitives', () => {
|
|
50
|
+
expect(variableTypeToTs('string')).toBe('string');
|
|
51
|
+
expect(variableTypeToTs('boolean')).toBe('boolean');
|
|
52
|
+
expect(variableTypeToTs('integer')).toBe('number');
|
|
53
|
+
expect(variableTypeToTs('number')).toBe('number');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('maps array to unknown[]', () => {
|
|
57
|
+
expect(variableTypeToTs('array')).toBe('unknown[]');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('maps object to Record<string, unknown>', () => {
|
|
61
|
+
expect(variableTypeToTs('object')).toBe('Record<string, unknown>');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('generateModuleTypes', () => {
|
|
66
|
+
test('emits a file header and empty interface for a manifest with no variables', () => {
|
|
67
|
+
const out = generateModuleTypes(baseManifest({ id: 'empty', name: 'Empty Module' }));
|
|
68
|
+
expect(out).toContain('// Generated from manifest.yml');
|
|
69
|
+
expect(out).toContain('Do not edit by hand');
|
|
70
|
+
expect(out).toContain('export interface EmptyConfig {');
|
|
71
|
+
expect(out).toContain('(No variables declared — module has no typed config surface)');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('produces the right interface name from a kebab-case module ID', () => {
|
|
75
|
+
const out = generateModuleTypes(baseManifest({ id: 'dns-external', name: 'DNS External' }));
|
|
76
|
+
expect(out).toContain('export interface DnsExternalConfig {');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('renders required fields as non-optional', () => {
|
|
80
|
+
const out = generateModuleTypes(
|
|
81
|
+
baseManifest({
|
|
82
|
+
id: 'test',
|
|
83
|
+
name: 'Test',
|
|
84
|
+
variables: {
|
|
85
|
+
owns: [
|
|
86
|
+
{
|
|
87
|
+
name: 'hostname',
|
|
88
|
+
type: 'string',
|
|
89
|
+
required: true,
|
|
90
|
+
source: 'user',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
imports: [],
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(out).toContain('hostname: string;');
|
|
98
|
+
expect(out).not.toContain('hostname?: string;');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('renders optional fields as optional when not required and no default', () => {
|
|
102
|
+
const out = generateModuleTypes(
|
|
103
|
+
baseManifest({
|
|
104
|
+
id: 'test',
|
|
105
|
+
name: 'Test',
|
|
106
|
+
variables: {
|
|
107
|
+
owns: [
|
|
108
|
+
{
|
|
109
|
+
name: 'hostname',
|
|
110
|
+
type: 'string',
|
|
111
|
+
required: false,
|
|
112
|
+
source: 'user',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
imports: [],
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
expect(out).toContain('hostname?: string;');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('treats not-required + default as guaranteed (non-optional)', () => {
|
|
123
|
+
const out = generateModuleTypes(
|
|
124
|
+
baseManifest({
|
|
125
|
+
id: 'test',
|
|
126
|
+
name: 'Test',
|
|
127
|
+
variables: {
|
|
128
|
+
owns: [
|
|
129
|
+
{
|
|
130
|
+
name: 'app_port',
|
|
131
|
+
type: 'integer',
|
|
132
|
+
required: false,
|
|
133
|
+
default: 3000,
|
|
134
|
+
source: 'user',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
imports: [],
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
expect(out).toContain('app_port: number;');
|
|
142
|
+
expect(out).not.toContain('app_port?: number;');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('maps every primitive type correctly', () => {
|
|
146
|
+
const out = generateModuleTypes(
|
|
147
|
+
baseManifest({
|
|
148
|
+
id: 'test',
|
|
149
|
+
name: 'Test',
|
|
150
|
+
variables: {
|
|
151
|
+
owns: [
|
|
152
|
+
{ name: 'str', type: 'string', required: true, source: 'user' },
|
|
153
|
+
{ name: 'num', type: 'number', required: true, source: 'user' },
|
|
154
|
+
{ name: 'int', type: 'integer', required: true, source: 'user' },
|
|
155
|
+
{ name: 'bool', type: 'boolean', required: true, source: 'user' },
|
|
156
|
+
{ name: 'arr', type: 'array', required: true, source: 'user' },
|
|
157
|
+
{ name: 'obj', type: 'object', required: true, source: 'user' },
|
|
158
|
+
],
|
|
159
|
+
imports: [],
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
expect(out).toContain('str: string;');
|
|
164
|
+
expect(out).toContain('num: number;');
|
|
165
|
+
expect(out).toContain('int: number;');
|
|
166
|
+
expect(out).toContain('bool: boolean;');
|
|
167
|
+
expect(out).toContain('arr: unknown[];');
|
|
168
|
+
expect(out).toContain('obj: Record<string, unknown>;');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('emits JSDoc comments from variable descriptions', () => {
|
|
172
|
+
const out = generateModuleTypes(
|
|
173
|
+
baseManifest({
|
|
174
|
+
id: 'test',
|
|
175
|
+
name: 'Test',
|
|
176
|
+
variables: {
|
|
177
|
+
owns: [
|
|
178
|
+
{
|
|
179
|
+
name: 'hostname',
|
|
180
|
+
type: 'string',
|
|
181
|
+
required: true,
|
|
182
|
+
source: 'user',
|
|
183
|
+
description: 'Container hostname for DNS resolution',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
imports: [],
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
expect(out).toContain('/** Container hostname for DNS resolution */');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('renders imported capability variables as unknown with source-annotated JSDoc', () => {
|
|
194
|
+
const out = generateModuleTypes(
|
|
195
|
+
baseManifest({
|
|
196
|
+
id: 'test',
|
|
197
|
+
name: 'Test',
|
|
198
|
+
variables: {
|
|
199
|
+
owns: [],
|
|
200
|
+
imports: [
|
|
201
|
+
{
|
|
202
|
+
name: 'dns_nameserver',
|
|
203
|
+
source: 'capability',
|
|
204
|
+
from: 'dns_external.nameserver',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
expect(out).toContain('Imported capability variables');
|
|
211
|
+
expect(out).toContain('dns_nameserver: unknown;');
|
|
212
|
+
expect(out).toContain('Imported from capability: dns_external.nameserver');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('separates owns and imports sections with a blank line', () => {
|
|
216
|
+
const out = generateModuleTypes(
|
|
217
|
+
baseManifest({
|
|
218
|
+
id: 'test',
|
|
219
|
+
name: 'Test',
|
|
220
|
+
variables: {
|
|
221
|
+
owns: [{ name: 'hostname', type: 'string', required: true, source: 'user' }],
|
|
222
|
+
imports: [{ name: 'dns_ns', source: 'capability', from: 'dns_external.nameserver' }],
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
expect(out).toContain('Module-owned variables');
|
|
227
|
+
expect(out).toContain('Imported capability variables');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('includes the module name and description in the interface JSDoc', () => {
|
|
231
|
+
const out = generateModuleTypes(
|
|
232
|
+
baseManifest({
|
|
233
|
+
id: 'lunacycle',
|
|
234
|
+
name: 'LunaCycle',
|
|
235
|
+
description: 'Family task management',
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
expect(out).toContain('Configuration shape for the LunaCycle module');
|
|
239
|
+
expect(out).toContain('Family task management');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('escapes */ sequences in descriptions to avoid breaking the JSDoc', () => {
|
|
243
|
+
const out = generateModuleTypes(
|
|
244
|
+
baseManifest({
|
|
245
|
+
id: 'test',
|
|
246
|
+
name: 'Test',
|
|
247
|
+
variables: {
|
|
248
|
+
owns: [
|
|
249
|
+
{
|
|
250
|
+
name: 'weird',
|
|
251
|
+
type: 'string',
|
|
252
|
+
required: true,
|
|
253
|
+
source: 'user',
|
|
254
|
+
description: 'A field with */ in the description',
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
imports: [],
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
expect(out).not.toContain('*/ in the description');
|
|
262
|
+
expect(out).toContain('* / in the description');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('output is deterministic across multiple runs', () => {
|
|
266
|
+
const manifest = baseManifest({
|
|
267
|
+
id: 'lunacycle',
|
|
268
|
+
name: 'LunaCycle',
|
|
269
|
+
variables: {
|
|
270
|
+
owns: [
|
|
271
|
+
{ name: 'hostname', type: 'string', required: true, source: 'user' },
|
|
272
|
+
{ name: 'app_port', type: 'integer', required: false, default: 3000, source: 'user' },
|
|
273
|
+
],
|
|
274
|
+
imports: [],
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const first = generateModuleTypes(manifest);
|
|
278
|
+
const second = generateModuleTypes(manifest);
|
|
279
|
+
const third = generateModuleTypes(manifest);
|
|
280
|
+
expect(first).toBe(second);
|
|
281
|
+
expect(second).toBe(third);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('output ends with a trailing newline', () => {
|
|
285
|
+
const out = generateModuleTypes(baseManifest());
|
|
286
|
+
expect(out.endsWith('\n')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Types Generator
|
|
3
|
+
*
|
|
4
|
+
* Pure codegen: given a validated ModuleManifest, produces the content of
|
|
5
|
+
* `<module>/celilo/types.d.ts` — a TypeScript declaration file exposing
|
|
6
|
+
* a typed `<ModuleName>Config` interface derived from the manifest's
|
|
7
|
+
* `variables.owns` and `variables.imports` arrays.
|
|
8
|
+
*
|
|
9
|
+
* This file is intentionally I/O-free. The CLI command in
|
|
10
|
+
* `cli/commands/module-types.ts` handles reading manifests and writing
|
|
11
|
+
* output files; this module is responsible for the translation only, so
|
|
12
|
+
* unit tests can exercise every branch without touching the filesystem.
|
|
13
|
+
*
|
|
14
|
+
* See `design/TECHNICAL_DESIGN_HOOK_API_V2.md` D2 for the design rationale.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ModuleManifest, VariableDeclare, VariableImport } from '../manifest/schema';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a kebab-case module ID to a PascalCase type name.
|
|
21
|
+
*
|
|
22
|
+
* "lunacycle" → "Lunacycle"
|
|
23
|
+
* "dns-external" → "DnsExternal"
|
|
24
|
+
* "my-fancy-app" → "MyFancyApp"
|
|
25
|
+
*/
|
|
26
|
+
export function moduleIdToPascalCase(id: string): string {
|
|
27
|
+
return id
|
|
28
|
+
.split('-')
|
|
29
|
+
.map((part) => (part.length === 0 ? '' : part[0].toUpperCase() + part.slice(1)))
|
|
30
|
+
.join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Translate a variable's declared `type:` field into a TypeScript type
|
|
35
|
+
* expression. Per HOOK_API_V2 D2, arrays and objects get generic
|
|
36
|
+
* unknown-element types for now; richer item-level typing can be added
|
|
37
|
+
* later without breaking any committed `types.d.ts`.
|
|
38
|
+
*/
|
|
39
|
+
export function variableTypeToTs(type: VariableDeclare['type']): string {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case 'string':
|
|
42
|
+
return 'string';
|
|
43
|
+
case 'integer':
|
|
44
|
+
case 'number':
|
|
45
|
+
return 'number';
|
|
46
|
+
case 'boolean':
|
|
47
|
+
return 'boolean';
|
|
48
|
+
case 'array':
|
|
49
|
+
return 'unknown[]';
|
|
50
|
+
case 'object':
|
|
51
|
+
return 'Record<string, unknown>';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Determine whether a declared variable is guaranteed to have a value at
|
|
57
|
+
* hook execution time. A variable is guaranteed if:
|
|
58
|
+
* - It's marked `required: true`, OR
|
|
59
|
+
* - It has a `default:` value (the default guarantees presence).
|
|
60
|
+
*
|
|
61
|
+
* `infrastructure` sources are treated as guaranteed when the variable is
|
|
62
|
+
* marked required; Celilo's infrastructure-variable-resolver populates
|
|
63
|
+
* them before hooks run and fails fast if it can't.
|
|
64
|
+
*/
|
|
65
|
+
function isVariableGuaranteed(variable: VariableDeclare): boolean {
|
|
66
|
+
if (variable.required) return true;
|
|
67
|
+
if (variable.default !== undefined) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isImportGuaranteed(_variable: VariableImport): boolean {
|
|
72
|
+
// Imports are sourced from another module's capability. The capability
|
|
73
|
+
// must be declared in requires.capabilities (enforced by
|
|
74
|
+
// validateVariableSources), and at hook invocation time Celilo's
|
|
75
|
+
// resolver populates the value. We treat imports as guaranteed —
|
|
76
|
+
// Phase 3 will add an explicit runtime pre-flight check.
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Escape a string so it can appear safely inside a JSDoc block comment.
|
|
82
|
+
* Converts sequences that would terminate the comment.
|
|
83
|
+
*/
|
|
84
|
+
function escapeJsDoc(text: string): string {
|
|
85
|
+
return text.replace(/\*\//g, '* /');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatJsDocLine(description: string | undefined): string[] {
|
|
89
|
+
if (!description) return [];
|
|
90
|
+
return [` /** ${escapeJsDoc(description)} */`];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Render a single interface field: optional JSDoc + name + optionality + type.
|
|
95
|
+
*/
|
|
96
|
+
function renderField(
|
|
97
|
+
name: string,
|
|
98
|
+
type: string,
|
|
99
|
+
optional: boolean,
|
|
100
|
+
description: string | undefined,
|
|
101
|
+
): string[] {
|
|
102
|
+
const doc = formatJsDocLine(description);
|
|
103
|
+
const optionalMark = optional ? '?' : '';
|
|
104
|
+
return [...doc, ` ${name}${optionalMark}: ${type};`];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sort variables deterministically so regenerating against the same
|
|
109
|
+
* manifest always produces byte-identical output. Manifest author order
|
|
110
|
+
* is the canonical order — stable across runs, meaningful to readers.
|
|
111
|
+
* `variables.owns` is already ordered by the YAML parser; we just
|
|
112
|
+
* preserve it.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate the full `<module>/celilo/types.d.ts` content for a manifest.
|
|
117
|
+
*
|
|
118
|
+
* Deterministic: calling this twice with the same input produces
|
|
119
|
+
* byte-identical output, so the drift check can compare against the
|
|
120
|
+
* committed file.
|
|
121
|
+
*/
|
|
122
|
+
export function generateModuleTypes(manifest: ModuleManifest): string {
|
|
123
|
+
const typeName = `${moduleIdToPascalCase(manifest.id)}Config`;
|
|
124
|
+
const lines: string[] = [];
|
|
125
|
+
|
|
126
|
+
lines.push('// Generated from manifest.yml by `celilo module types generate`.');
|
|
127
|
+
lines.push('// Do not edit by hand. Run the command again after changing `variables.owns`.');
|
|
128
|
+
lines.push('// CI enforces this file stays in sync via `celilo module types check`.');
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('/**');
|
|
131
|
+
lines.push(` * Configuration shape for the ${manifest.name} module.`);
|
|
132
|
+
if (manifest.description) {
|
|
133
|
+
lines.push(' *');
|
|
134
|
+
for (const descLine of manifest.description.split('\n')) {
|
|
135
|
+
lines.push(` * ${descLine}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
lines.push(' *');
|
|
139
|
+
lines.push(' * Fields are derived from `variables.owns` and `variables.imports` in the');
|
|
140
|
+
lines.push(' * module manifest. Optional fields (marked with `?`) correspond to variables');
|
|
141
|
+
lines.push(' * that are neither `required: true` nor have a `default:` value.');
|
|
142
|
+
lines.push(' */');
|
|
143
|
+
lines.push(`export interface ${typeName} {`);
|
|
144
|
+
|
|
145
|
+
const ownsFields: string[] = [];
|
|
146
|
+
const importsFields: string[] = [];
|
|
147
|
+
|
|
148
|
+
for (const variable of manifest.variables.owns) {
|
|
149
|
+
const tsType = variableTypeToTs(variable.type);
|
|
150
|
+
const optional = !isVariableGuaranteed(variable);
|
|
151
|
+
const rendered = renderField(variable.name, tsType, optional, variable.description);
|
|
152
|
+
ownsFields.push(...rendered);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const imp of manifest.variables.imports) {
|
|
156
|
+
// Imports don't carry type info in the manifest schema — they're
|
|
157
|
+
// raw references to capability data. Treat as `unknown` so consumers
|
|
158
|
+
// cast as needed. A future enhancement could look up the capability's
|
|
159
|
+
// `data_schema` to infer a more precise type.
|
|
160
|
+
const optional = !isImportGuaranteed(imp);
|
|
161
|
+
const rendered = renderField(
|
|
162
|
+
imp.name,
|
|
163
|
+
'unknown',
|
|
164
|
+
optional,
|
|
165
|
+
`Imported from ${imp.source}: ${imp.from}`,
|
|
166
|
+
);
|
|
167
|
+
importsFields.push(...rendered);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (ownsFields.length > 0) {
|
|
171
|
+
lines.push(' // Module-owned variables (from variables.owns)');
|
|
172
|
+
lines.push(...ownsFields);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (importsFields.length > 0) {
|
|
176
|
+
if (ownsFields.length > 0) lines.push('');
|
|
177
|
+
lines.push(' // Imported capability variables (from variables.imports)');
|
|
178
|
+
lines.push(...importsFields);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (ownsFields.length === 0 && importsFields.length === 0) {
|
|
182
|
+
lines.push(' // (No variables declared — module has no typed config surface)');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('}');
|
|
186
|
+
lines.push('');
|
|
187
|
+
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxmox State Recovery Service
|
|
3
|
+
*
|
|
4
|
+
* Handles state drift scenarios where Terraform state and module_configs are out of sync.
|
|
5
|
+
* This typically occurs when containers are deleted outside Terraform and then recreated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { and, eq } from 'drizzle-orm';
|
|
11
|
+
import { log } from '../cli/prompts';
|
|
12
|
+
import type { DbClient } from '../db/client';
|
|
13
|
+
import { moduleConfigs } from '../db/schema';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Terraform state file structure (subset we care about)
|
|
17
|
+
*/
|
|
18
|
+
interface TerraformState {
|
|
19
|
+
resources?: Array<{
|
|
20
|
+
type: string;
|
|
21
|
+
name: string;
|
|
22
|
+
instances?: Array<{
|
|
23
|
+
attributes?: {
|
|
24
|
+
vmid?: number;
|
|
25
|
+
network?: Array<{
|
|
26
|
+
ip?: string;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure module_configs has vmid and container_ip from Terraform state
|
|
35
|
+
* This recovers from state drift scenarios where container was deleted/recreated
|
|
36
|
+
*
|
|
37
|
+
* @param moduleId - Module identifier
|
|
38
|
+
* @param terraformDir - Terraform working directory
|
|
39
|
+
* @param db - Database connection
|
|
40
|
+
*/
|
|
41
|
+
export async function ensureProxmoxConfigFromState(
|
|
42
|
+
moduleId: string,
|
|
43
|
+
terraformDir: string,
|
|
44
|
+
db: DbClient,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
// Check if vmid and container_ip already exist
|
|
47
|
+
const existingVmid = await db
|
|
48
|
+
.select()
|
|
49
|
+
.from(moduleConfigs)
|
|
50
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, 'vmid')))
|
|
51
|
+
.get();
|
|
52
|
+
|
|
53
|
+
const existingContainerIp = await db
|
|
54
|
+
.select()
|
|
55
|
+
.from(moduleConfigs)
|
|
56
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, 'container_ip')))
|
|
57
|
+
.get();
|
|
58
|
+
|
|
59
|
+
// If both exist, no recovery needed
|
|
60
|
+
if (existingVmid && existingContainerIp) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read Terraform state file
|
|
65
|
+
const statePath = join(terraformDir, 'terraform.tfstate');
|
|
66
|
+
let stateContent: string;
|
|
67
|
+
try {
|
|
68
|
+
stateContent = await readFile(statePath, 'utf-8');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to read Terraform state for recovery: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let state: TerraformState;
|
|
76
|
+
try {
|
|
77
|
+
state = JSON.parse(stateContent) as TerraformState;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Failed to parse Terraform state: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find proxmox_lxc resource
|
|
85
|
+
const lxcResource = state.resources?.find((r) => r.type === 'proxmox_lxc');
|
|
86
|
+
if (!lxcResource || !lxcResource.instances || lxcResource.instances.length === 0) {
|
|
87
|
+
throw new Error('No proxmox_lxc resource found in Terraform state');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const attributes = lxcResource.instances[0].attributes;
|
|
91
|
+
if (!attributes) {
|
|
92
|
+
throw new Error('No attributes found in proxmox_lxc resource');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const vmid = attributes.vmid;
|
|
96
|
+
const containerIp = attributes.network?.[0]?.ip;
|
|
97
|
+
|
|
98
|
+
if (!vmid) {
|
|
99
|
+
throw new Error('vmid not found in Terraform state');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!containerIp) {
|
|
103
|
+
throw new Error('container IP not found in Terraform state');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Store in module_configs (recovery)
|
|
107
|
+
log.warn(' Recovering vmid and container_ip from Terraform state...');
|
|
108
|
+
|
|
109
|
+
if (!existingVmid) {
|
|
110
|
+
await db
|
|
111
|
+
.insert(moduleConfigs)
|
|
112
|
+
.values({
|
|
113
|
+
moduleId,
|
|
114
|
+
key: 'vmid',
|
|
115
|
+
value: vmid.toString(),
|
|
116
|
+
})
|
|
117
|
+
.onConflictDoUpdate({
|
|
118
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
119
|
+
set: { value: vmid.toString() },
|
|
120
|
+
})
|
|
121
|
+
.run();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!existingContainerIp) {
|
|
125
|
+
await db
|
|
126
|
+
.insert(moduleConfigs)
|
|
127
|
+
.values({
|
|
128
|
+
moduleId,
|
|
129
|
+
key: 'container_ip',
|
|
130
|
+
value: containerIp,
|
|
131
|
+
})
|
|
132
|
+
.onConflictDoUpdate({
|
|
133
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
134
|
+
set: { value: containerIp },
|
|
135
|
+
})
|
|
136
|
+
.run();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.success(` Recovered: vmid=${vmid}, container_ip=${containerIp}`);
|
|
140
|
+
}
|