@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
package/README.md
ADDED
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
# Celilo Backend
|
|
2
|
+
|
|
3
|
+
Phase 0 implementation of Celilo's backend services.
|
|
4
|
+
|
|
5
|
+
## Phase 0 Scope
|
|
6
|
+
|
|
7
|
+
CLI-only interface for:
|
|
8
|
+
- Module import and validation
|
|
9
|
+
- Manifest validation
|
|
10
|
+
- Variable resolution
|
|
11
|
+
- Template generation
|
|
12
|
+
- Secret management
|
|
13
|
+
- **Zero-configuration modules** (Phase 0 Part 4):
|
|
14
|
+
- Automatic hostname/zone assignment from well-known capabilities
|
|
15
|
+
- Automatic VMID/IP allocation (IPAM)
|
|
16
|
+
- Automatic network config derivation from zones
|
|
17
|
+
- Automatic VM resource defaults from manifests
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
celilo/backend/
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── db/
|
|
25
|
+
│ │ ├── schema.ts # Drizzle ORM schema
|
|
26
|
+
│ │ ├── schema.test.ts # Schema tests
|
|
27
|
+
│ │ ├── client.ts # Database connection
|
|
28
|
+
│ │ └── migrate.ts # Migration runner
|
|
29
|
+
│ ├── manifest/
|
|
30
|
+
│ │ ├── schema.ts # Zod schemas for manifest validation
|
|
31
|
+
│ │ ├── validate.ts # Manifest validation logic
|
|
32
|
+
│ │ └── validate.test.ts # Validation tests
|
|
33
|
+
│ ├── module/
|
|
34
|
+
│ │ ├── import.ts # Module import logic
|
|
35
|
+
│ │ └── import.test.ts # Import tests
|
|
36
|
+
│ ├── variables/
|
|
37
|
+
│ │ ├── types.ts # Variable resolution types
|
|
38
|
+
│ │ ├── parser.ts # Variable parsing logic
|
|
39
|
+
│ │ ├── parser.test.ts # Parser tests
|
|
40
|
+
│ │ ├── resolver.ts # Variable resolution logic
|
|
41
|
+
│ │ ├── resolver.test.ts # Resolver tests
|
|
42
|
+
│ │ ├── context.ts # Resolution context builder
|
|
43
|
+
│ │ └── context.test.ts # Context tests
|
|
44
|
+
│ ├── secrets/
|
|
45
|
+
│ │ ├── master-key.ts # Master key generation and management
|
|
46
|
+
│ │ ├── master-key.test.ts # Master key tests
|
|
47
|
+
│ │ ├── encryption.ts # AES-256-GCM encryption/decryption
|
|
48
|
+
│ │ └── encryption.test.ts # Encryption tests
|
|
49
|
+
│ ├── templates/
|
|
50
|
+
│ │ ├── types.ts # Template generation types
|
|
51
|
+
│ │ ├── generator.ts # Template generation logic
|
|
52
|
+
│ │ └── generator.test.ts # Generation tests
|
|
53
|
+
│ └── index.ts # Entry point
|
|
54
|
+
├── drizzle/ # Generated migrations
|
|
55
|
+
├── package.json
|
|
56
|
+
├── tsconfig.json
|
|
57
|
+
├── biome.json # Linting/formatting config
|
|
58
|
+
└── drizzle.config.ts # Drizzle Kit config
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Database Schema
|
|
62
|
+
|
|
63
|
+
### Tables
|
|
64
|
+
|
|
65
|
+
**`modules`** - Module metadata and manifest data
|
|
66
|
+
- `id` (TEXT, PK) - Module identifier
|
|
67
|
+
- `name` (TEXT) - Display name
|
|
68
|
+
- `version` (TEXT) - Semantic version
|
|
69
|
+
- `description` (TEXT) - Optional description
|
|
70
|
+
- `state` (TEXT) - Lifecycle state (IMPORTED, VALIDATED, CONFIGURED, etc.)
|
|
71
|
+
- `manifest_data` (JSON) - Full manifest content
|
|
72
|
+
- `source_path` (TEXT) - Original import path
|
|
73
|
+
- `imported_at` (TIMESTAMP) - Import timestamp
|
|
74
|
+
- `updated_at` (TIMESTAMP) - Last update timestamp
|
|
75
|
+
- `error_message` (TEXT) - Error details if state is ERROR
|
|
76
|
+
|
|
77
|
+
**`module_configs`** - User configuration key-value pairs
|
|
78
|
+
- `id` (INTEGER, PK, AUTO)
|
|
79
|
+
- `module_id` (TEXT, FK → modules.id) - Module reference
|
|
80
|
+
- `key` (TEXT) - Configuration key
|
|
81
|
+
- `value` (TEXT) - Configuration value
|
|
82
|
+
- `created_at` (TIMESTAMP)
|
|
83
|
+
- `updated_at` (TIMESTAMP)
|
|
84
|
+
|
|
85
|
+
**`capabilities`** - Registered capabilities from modules
|
|
86
|
+
- `id` (INTEGER, PK, AUTO)
|
|
87
|
+
- `module_id` (TEXT, FK → modules.id) - Provider module
|
|
88
|
+
- `capability_name` (TEXT) - Capability identifier (e.g., dns_external)
|
|
89
|
+
- `version` (TEXT) - Capability version
|
|
90
|
+
- `data` (JSON) - Capability data (nameserver, zone, etc.)
|
|
91
|
+
- `registered_at` (TIMESTAMP)
|
|
92
|
+
|
|
93
|
+
**`secrets`** - Encrypted secrets per module
|
|
94
|
+
- `id` (INTEGER, PK, AUTO)
|
|
95
|
+
- `module_id` (TEXT, FK → modules.id) - Module reference
|
|
96
|
+
- `name` (TEXT) - Secret name
|
|
97
|
+
- `encrypted_value` (TEXT) - AES-256-GCM encrypted value
|
|
98
|
+
- `iv` (TEXT) - Initialization vector
|
|
99
|
+
- `auth_tag` (TEXT) - Authentication tag
|
|
100
|
+
- `created_at` (TIMESTAMP)
|
|
101
|
+
- `updated_at` (TIMESTAMP)
|
|
102
|
+
|
|
103
|
+
All tables use CASCADE DELETE when module is removed.
|
|
104
|
+
|
|
105
|
+
## Dependencies
|
|
106
|
+
|
|
107
|
+
### Required
|
|
108
|
+
|
|
109
|
+
- **Bun** (v1.0+) - JavaScript runtime
|
|
110
|
+
- **Ansible** (v2.9+) - Required for Ansible Vault secret encryption
|
|
111
|
+
- **Terraform** (v1.0+) - Required for validating generated Terraform code
|
|
112
|
+
|
|
113
|
+
See [../SETUP.md](../SETUP.md) for installation instructions.
|
|
114
|
+
|
|
115
|
+
## Setup
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Install dependencies
|
|
119
|
+
bun install
|
|
120
|
+
|
|
121
|
+
# Generate migrations (if schema changed)
|
|
122
|
+
bunx drizzle-kit generate
|
|
123
|
+
|
|
124
|
+
# Run migrations
|
|
125
|
+
bun run db:migrate
|
|
126
|
+
|
|
127
|
+
# Run tests
|
|
128
|
+
bun test # Unit tests (216 tests)
|
|
129
|
+
bun run test:integration # Integration tests (56 tests)
|
|
130
|
+
|
|
131
|
+
# Lint and format
|
|
132
|
+
bun run lint:fix
|
|
133
|
+
bun run format
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
### Common Development Workflows
|
|
139
|
+
|
|
140
|
+
**Full development cycle**:
|
|
141
|
+
```bash
|
|
142
|
+
# 1. Make code changes
|
|
143
|
+
vim src/module/import.ts
|
|
144
|
+
|
|
145
|
+
# 2. Run unit tests (< 1s)
|
|
146
|
+
bun test src/module/import.test.ts
|
|
147
|
+
|
|
148
|
+
# 3. Run all unit tests (includes zero-config integration tests)
|
|
149
|
+
bun test
|
|
150
|
+
|
|
151
|
+
# 4. Run CLI-based integration tests (10-20s)
|
|
152
|
+
bun run test:integration
|
|
153
|
+
|
|
154
|
+
# 5. Before commit: Run all tests
|
|
155
|
+
bun test && bun run test:integration
|
|
156
|
+
|
|
157
|
+
# 6. Before major release: Run slow tests (3-5min)
|
|
158
|
+
bun run test:integration-slow
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Working with the CLI**:
|
|
162
|
+
```bash
|
|
163
|
+
# Use the wrapper script (works from any directory)
|
|
164
|
+
../../celilo module list
|
|
165
|
+
|
|
166
|
+
# Or run directly from backend
|
|
167
|
+
bun run src/cli/index.ts module list
|
|
168
|
+
|
|
169
|
+
# Or use bun link for system-wide command
|
|
170
|
+
bun link
|
|
171
|
+
celilo module list # From anywhere
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Testing module changes**:
|
|
175
|
+
```bash
|
|
176
|
+
# 1. Import test module
|
|
177
|
+
bun run src/cli/index.ts module import ../../modules/test-module
|
|
178
|
+
|
|
179
|
+
# 2. Configure
|
|
180
|
+
bun run src/cli/index.ts module config set test-module hostname test-app
|
|
181
|
+
bun run src/cli/index.ts module config set test-module container_ip 10.0.20.100
|
|
182
|
+
|
|
183
|
+
# 3. Generate
|
|
184
|
+
bun run src/cli/index.ts module generate test-module
|
|
185
|
+
|
|
186
|
+
# 4. Inspect generated files
|
|
187
|
+
ls -la /tmp/celilo/modules/test-module/generated/
|
|
188
|
+
|
|
189
|
+
# 5. Clean up
|
|
190
|
+
bun run src/cli/index.ts module remove test-module
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Working with secrets**:
|
|
194
|
+
```bash
|
|
195
|
+
# Set module secret
|
|
196
|
+
bun run src/cli/index.ts secret set homebridge api_key test-key-123
|
|
197
|
+
|
|
198
|
+
# Get Ansible Vault password (for inspecting secrets.yml)
|
|
199
|
+
bun run src/cli/index.ts system vault-password
|
|
200
|
+
|
|
201
|
+
# View encrypted Ansible secrets
|
|
202
|
+
ansible-vault view /tmp/celilo/modules/homebridge/generated/ansible/inventory/secrets.yml \
|
|
203
|
+
--vault-password-file=<(bun run src/cli/index.ts system vault-password)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Zero-Configuration Workflow** (Phase 0 Part 4):
|
|
207
|
+
```bash
|
|
208
|
+
# One-time system setup: Configure network zones
|
|
209
|
+
celilo system config set network.dmz.subnet "10.0.10.0/24"
|
|
210
|
+
celilo system config set network.dmz.gateway "10.0.10.1"
|
|
211
|
+
celilo system config set network.dmz.vlan "10"
|
|
212
|
+
celilo system config set network.dmz.bridge "vmbr0"
|
|
213
|
+
|
|
214
|
+
# Import module with well-known capability (e.g., Caddy with public_web)
|
|
215
|
+
celilo module import ./modules/caddy
|
|
216
|
+
|
|
217
|
+
# Configure ONLY app-specific settings (infrastructure auto-configured!)
|
|
218
|
+
celilo module config set caddy domain "example.com"
|
|
219
|
+
|
|
220
|
+
# Generate - everything else is automatic!
|
|
221
|
+
celilo module generate caddy
|
|
222
|
+
|
|
223
|
+
# Debug: Show all configuration (user + auto-derived)
|
|
224
|
+
celilo module show-config caddy
|
|
225
|
+
|
|
226
|
+
# Debug: Show zone and network settings
|
|
227
|
+
celilo module show-zone caddy
|
|
228
|
+
|
|
229
|
+
# IPAM Management: Reserve VMIDs/IPs for existing infrastructure
|
|
230
|
+
celilo ipam vmid reserve 2100 --reason "Existing Proxmox VM"
|
|
231
|
+
celilo ipam ip reserve 10.0.10.1-10.0.10.9 --zone dmz --reason "Infrastructure"
|
|
232
|
+
|
|
233
|
+
# IPAM Management: List allocations and reservations
|
|
234
|
+
celilo ipam list-allocations
|
|
235
|
+
celilo ipam vmid list-reservations
|
|
236
|
+
celilo ipam ip list-reservations
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**What Gets Auto-Configured**:
|
|
240
|
+
- ✅ **Hostname** - From well-known capabilities (e.g., `public_web` → `www`)
|
|
241
|
+
- ✅ **Zone** - From well-known capabilities (e.g., `public_web` → `dmz`)
|
|
242
|
+
- ✅ **VMID** - Auto-allocated from 2100+ (IPAM)
|
|
243
|
+
- ✅ **Container IP** - Auto-allocated from zone subnet (IPAM)
|
|
244
|
+
- ✅ **Gateway** - From `network.{zone}.gateway` system config
|
|
245
|
+
- ✅ **VLAN** - From `network.{zone}.vlan` system config
|
|
246
|
+
- ✅ **Subnet** - From `network.{zone}.subnet` system config
|
|
247
|
+
- ✅ **Bridge** - From `network.{zone}.bridge` system config
|
|
248
|
+
- ✅ **VM Resources** - cores, memory, disk, storage from manifest defaults
|
|
249
|
+
|
|
250
|
+
See [../../design/MODULE_DEVELOPMENT_GUIDE.md](../../design/MODULE_DEVELOPMENT_GUIDE.md) for complete zero-config documentation.
|
|
251
|
+
|
|
252
|
+
### Database Location
|
|
253
|
+
|
|
254
|
+
**Default locations**:
|
|
255
|
+
- **macOS**: `~/Library/Application Support/celilo/celilo.db`
|
|
256
|
+
- **Linux**: `/var/lib/celilo/celilo.db`
|
|
257
|
+
- **Development** (`ENVIRONMENT=dev`): `./celilo-data/celilo.db`
|
|
258
|
+
|
|
259
|
+
**Override**: Set `CELILO_DB_PATH` environment variable to use a custom location.
|
|
260
|
+
|
|
261
|
+
**Override base directory**: Set `CELILO_DATA_DIR` to change where all Celilo data (including database) is stored.
|
|
262
|
+
|
|
263
|
+
### Testing Strategy
|
|
264
|
+
|
|
265
|
+
**Three test tiers** (see [TESTING_STRATEGY.md](../../design/TESTING_STRATEGY.md) for details):
|
|
266
|
+
|
|
267
|
+
**Tier 1: Unit Tests** (< 1 second)
|
|
268
|
+
```bash
|
|
269
|
+
bun test # Run all unit tests (494 tests)
|
|
270
|
+
bun test schema.test.ts # Run specific test file
|
|
271
|
+
bun test --watch # Watch mode for active development
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Tier 2: Fast Integration Tests** (10-20 seconds)
|
|
275
|
+
```bash
|
|
276
|
+
bun run test:integration # All integration tests (run via bun test)
|
|
277
|
+
|
|
278
|
+
# Coverage:
|
|
279
|
+
# - Module import/validation via CLI
|
|
280
|
+
# - Configuration management
|
|
281
|
+
# - Template generation
|
|
282
|
+
# - Ansible/Terraform generation
|
|
283
|
+
# - Well-known capability auto-assignment
|
|
284
|
+
# - IPAM auto-allocation
|
|
285
|
+
# - Zone-based networking
|
|
286
|
+
# - VM resource defaults
|
|
287
|
+
|
|
288
|
+
# What gets skipped:
|
|
289
|
+
# - Nix builds
|
|
290
|
+
# - Docker builds
|
|
291
|
+
# - Slow module builds (Caddy)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Tier 3: Slow Integration Tests** (3-5 minutes)
|
|
295
|
+
```bash
|
|
296
|
+
bun run test:integration-slow # Full builds + end-to-end
|
|
297
|
+
|
|
298
|
+
# Includes everything from fast tests PLUS:
|
|
299
|
+
# - Full Nix builds (Caddy with modules)
|
|
300
|
+
# - Docker builds
|
|
301
|
+
# - Complete module packaging
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Before committing** (MANDATORY per Rule 7.4):
|
|
305
|
+
```bash
|
|
306
|
+
bun test && bun run test:integration
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Before releasing**:
|
|
310
|
+
```bash
|
|
311
|
+
bun test && bun run test:integration && bun run test:integration-slow
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Database Studio
|
|
315
|
+
|
|
316
|
+
View database contents with Drizzle Studio:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
bun run db:studio
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Opens web interface at http://localhost:4983
|
|
323
|
+
|
|
324
|
+
### Debugging
|
|
325
|
+
|
|
326
|
+
**Enable verbose output**:
|
|
327
|
+
```bash
|
|
328
|
+
# Set log level
|
|
329
|
+
export CONDUCTOR_LOG_LEVEL=debug
|
|
330
|
+
bun run src/cli/index.ts module import ./path
|
|
331
|
+
|
|
332
|
+
# Or use Bun's debugger
|
|
333
|
+
bun --inspect run src/cli/index.ts module import ./path
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Inspect database**:
|
|
337
|
+
```bash
|
|
338
|
+
# Use Drizzle Studio
|
|
339
|
+
bun run db:studio
|
|
340
|
+
|
|
341
|
+
# Or sqlite3 directly
|
|
342
|
+
sqlite3 celilo.db
|
|
343
|
+
sqlite> .tables
|
|
344
|
+
sqlite> SELECT * FROM modules;
|
|
345
|
+
sqlite> SELECT * FROM module_configs WHERE module_id = 'homebridge';
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Check generated files**:
|
|
349
|
+
```bash
|
|
350
|
+
# Generated files location
|
|
351
|
+
ls -la /tmp/celilo/modules/<module-id>/generated/
|
|
352
|
+
|
|
353
|
+
# View Terraform
|
|
354
|
+
cat /tmp/celilo/modules/<module-id>/generated/terraform/main.tf
|
|
355
|
+
|
|
356
|
+
# View Ansible
|
|
357
|
+
cat /tmp/celilo/modules/<module-id>/generated/ansible/playbook.yml
|
|
358
|
+
|
|
359
|
+
# Check encrypted secrets
|
|
360
|
+
ansible-vault view /tmp/celilo/modules/<module-id>/generated/ansible/inventory/secrets.yml \
|
|
361
|
+
--vault-password-file=<(bun run src/cli/index.ts system vault-password)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Architecture Decisions
|
|
365
|
+
|
|
366
|
+
### Why Drizzle ORM?
|
|
367
|
+
|
|
368
|
+
- Type-safe schema-first approach
|
|
369
|
+
- Excellent TypeScript inference
|
|
370
|
+
- Lightweight (no heavy runtime)
|
|
371
|
+
- SQL-like API (easier for developers familiar with SQL)
|
|
372
|
+
|
|
373
|
+
### Why SQLite?
|
|
374
|
+
|
|
375
|
+
- Single-file database (easy backup/restore)
|
|
376
|
+
- No separate server process needed
|
|
377
|
+
- Sufficient for home lab scale
|
|
378
|
+
- ACID compliant with WAL mode
|
|
379
|
+
|
|
380
|
+
### Foreign Key Cascades
|
|
381
|
+
|
|
382
|
+
All child tables (configs, capabilities, secrets) cascade delete when parent module is removed. This ensures no orphaned data.
|
|
383
|
+
|
|
384
|
+
### JSON Columns
|
|
385
|
+
|
|
386
|
+
`manifest_data` and capability `data` use JSON columns for flexibility. These are validated with Zod schemas in application code.
|
|
387
|
+
|
|
388
|
+
## Manifest Validation
|
|
389
|
+
|
|
390
|
+
### Schema (`src/manifest/schema.ts`)
|
|
391
|
+
|
|
392
|
+
Zod schemas for validating module manifest structure:
|
|
393
|
+
|
|
394
|
+
- **ModuleManifestSchema** - Complete manifest validation
|
|
395
|
+
- **VariableDeclareSchema** - Variable declarations
|
|
396
|
+
- **VariableUseSchema** - Variable usage from capabilities
|
|
397
|
+
- **CapabilityRequirementSchema** - Required capabilities
|
|
398
|
+
- **CapabilityProviderSchema** - Provided capabilities
|
|
399
|
+
- **LifecycleHookSchema** - Lifecycle hooks (on_install, health_check, etc.)
|
|
400
|
+
|
|
401
|
+
### Validation Functions (`src/manifest/validate.ts`)
|
|
402
|
+
|
|
403
|
+
**`validateManifest(yamlContent: string): ValidationResult`**
|
|
404
|
+
- Parses YAML and validates against schema
|
|
405
|
+
- Returns success with typed manifest or errors with path/message
|
|
406
|
+
- Policy function - no side effects
|
|
407
|
+
|
|
408
|
+
**`validateCapabilityRequirements(manifest, availableCapabilities): ValidationError | null`**
|
|
409
|
+
- Checks that all required capabilities exist in system
|
|
410
|
+
- Caller provides list of available capabilities (from database)
|
|
411
|
+
|
|
412
|
+
**`validateVariableSources(manifest): ValidationError | null`**
|
|
413
|
+
- Validates that capability references in variables match required capabilities
|
|
414
|
+
- Ensures consistency between `requires.capabilities` and `variables.uses`
|
|
415
|
+
|
|
416
|
+
### Example Usage
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { validateManifest } from './manifest/validate';
|
|
420
|
+
|
|
421
|
+
const yamlContent = await readFile('./modules/homebridge/manifest.yml', 'utf-8');
|
|
422
|
+
const result = validateManifest(yamlContent);
|
|
423
|
+
|
|
424
|
+
if (!result.success) {
|
|
425
|
+
console.error('Validation errors:', result.errors);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const manifest = result.data;
|
|
430
|
+
console.log(`Module: ${manifest.name} v${manifest.version}`);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Module Import
|
|
434
|
+
|
|
435
|
+
### Import Functions (`src/module/import.ts`)
|
|
436
|
+
|
|
437
|
+
Following Rule 10.1, functions are separated by responsibility:
|
|
438
|
+
|
|
439
|
+
**Policy Functions** (validation, no side effects):
|
|
440
|
+
- **`validateModuleDirectory(sourcePath)`** - Checks directory exists and has manifest.yml
|
|
441
|
+
- **`readModuleManifest(sourcePath)`** - Reads and validates manifest from YAML
|
|
442
|
+
|
|
443
|
+
**Execution Functions** (perform I/O):
|
|
444
|
+
- **`copyModuleFiles(sourcePath, targetPath)`** - Recursively copies all module files
|
|
445
|
+
- **`insertModuleToDb(manifest, targetPath, db?)`** - Inserts module record to database
|
|
446
|
+
- **`moduleExists(moduleId, db?)`** - Checks if module already exists
|
|
447
|
+
|
|
448
|
+
**Orchestration Function**:
|
|
449
|
+
- **`importModule(options)`** - Main entry point, coordinates all steps
|
|
450
|
+
|
|
451
|
+
### Import Process
|
|
452
|
+
|
|
453
|
+
1. **Validate** directory structure and manifest
|
|
454
|
+
2. **Check** module doesn't already exist
|
|
455
|
+
3. **Copy** files to target location (`/tmp/celilo/modules/{module-id}/`)
|
|
456
|
+
4. **Insert** module record to database with state `IMPORTED`
|
|
457
|
+
|
|
458
|
+
### Example Usage
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { importModule } from './module/import';
|
|
462
|
+
|
|
463
|
+
const result = await importModule({
|
|
464
|
+
sourcePath: './modules/homebridge',
|
|
465
|
+
targetBasePath: '/data/celilo/modules',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if (!result.success) {
|
|
469
|
+
console.error('Import failed:', result.error);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log(`Imported module: ${result.moduleId}`);
|
|
474
|
+
console.log(`Files copied to: ${result.targetPath}`);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Dependency Injection
|
|
478
|
+
|
|
479
|
+
Functions accept optional `db` parameter for testing (Rule 2.3):
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// Production - uses global singleton
|
|
483
|
+
const exists = moduleExists('homebridge');
|
|
484
|
+
|
|
485
|
+
// Testing - inject test database
|
|
486
|
+
const testDb = createDbClient({ path: './test.db' });
|
|
487
|
+
const exists = moduleExists('homebridge', testDb);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Variable Resolution
|
|
491
|
+
|
|
492
|
+
### System Overview (`src/variables/`)
|
|
493
|
+
|
|
494
|
+
The variable resolution system follows Rule 10.1 with clear separation of concerns:
|
|
495
|
+
|
|
496
|
+
**Parser** (`parser.ts`) - Policy function:
|
|
497
|
+
- Extracts variable references from template strings
|
|
498
|
+
- Pattern: `$type:path.to.value`
|
|
499
|
+
- Validates variable format
|
|
500
|
+
|
|
501
|
+
**Resolver** (`resolver.ts`) - Policy + Orchestration:
|
|
502
|
+
- `resolveVariable()` - Resolves single variable from context
|
|
503
|
+
- `resolveTemplate()` - Resolves all variables in template
|
|
504
|
+
|
|
505
|
+
**Context** (`context.ts`) - Execution function:
|
|
506
|
+
- `buildResolutionContext()` - Fetches data from database
|
|
507
|
+
- `buildContextFromData()` - Creates context from explicit data (testing)
|
|
508
|
+
|
|
509
|
+
### Variable Types
|
|
510
|
+
|
|
511
|
+
**`$self:path`** - Module's own configuration
|
|
512
|
+
- Example: `$self:container_ip` → `192.168.0.50`
|
|
513
|
+
- Source: `module_configs` table
|
|
514
|
+
|
|
515
|
+
**`$system:path`** - System-wide configuration
|
|
516
|
+
- Example: `$system:management.ip` → `192.168.0.10`
|
|
517
|
+
- Source: Hardcoded defaults (Phase 0), database (Phase 1+)
|
|
518
|
+
|
|
519
|
+
**`$secret:name`** - Encrypted secrets
|
|
520
|
+
- Example: `$secret:api_key` → `decrypted_value`
|
|
521
|
+
- Source: `secrets` table (Phase 0: plaintext, Phase 0.5: encrypted)
|
|
522
|
+
|
|
523
|
+
**`$capability:name.path`** - Capability provider data
|
|
524
|
+
- Example: `$capability:dns_external.nameserver` → `ns1.example.com`
|
|
525
|
+
- Source: `capabilities` table
|
|
526
|
+
- Format: `capability_name.data.path`
|
|
527
|
+
|
|
528
|
+
### Example Usage
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { buildResolutionContext, resolveTemplate } from './variables';
|
|
532
|
+
|
|
533
|
+
// Build context from database
|
|
534
|
+
const context = await buildResolutionContext('homebridge');
|
|
535
|
+
|
|
536
|
+
// Resolve template
|
|
537
|
+
const template = `
|
|
538
|
+
hostname: $self:hostname
|
|
539
|
+
ip: $self:container_ip
|
|
540
|
+
gateway: $system:management.ip
|
|
541
|
+
dns: $capability:dns_external.nameserver
|
|
542
|
+
api_key: $secret:api_key
|
|
543
|
+
`;
|
|
544
|
+
|
|
545
|
+
const result = resolveTemplate(template, context);
|
|
546
|
+
|
|
547
|
+
if (result.success) {
|
|
548
|
+
console.log(result.content);
|
|
549
|
+
// hostname: homebridge
|
|
550
|
+
// ip: 192.168.0.50
|
|
551
|
+
// gateway: 192.168.0.10
|
|
552
|
+
// dns: ns1.example.com
|
|
553
|
+
// api_key: secret123
|
|
554
|
+
} else {
|
|
555
|
+
console.error('Resolution errors:', result.errors);
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Resolution Process
|
|
560
|
+
|
|
561
|
+
1. **Parse** - Extract all variable references from template
|
|
562
|
+
2. **Resolve** - For each variable:
|
|
563
|
+
- Determine type (`self`, `system`, `secret`, `capability`)
|
|
564
|
+
- Lookup value from appropriate data source in context
|
|
565
|
+
- Handle nested paths for capabilities
|
|
566
|
+
3. **Replace** - Substitute all variables with resolved values
|
|
567
|
+
4. **Return** - Success with content or errors with details
|
|
568
|
+
|
|
569
|
+
### Testing
|
|
570
|
+
|
|
571
|
+
All functions are testable without database:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { buildContextFromData, resolveTemplate } from './variables';
|
|
575
|
+
|
|
576
|
+
const context = buildContextFromData('test-module', {
|
|
577
|
+
selfConfig: { ip: '192.168.0.50' },
|
|
578
|
+
secrets: { key: 'secret' },
|
|
579
|
+
capabilities: { dns: { server: 'ns1' } },
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const result = resolveTemplate('ip: $self:ip', context);
|
|
583
|
+
// result.success === true
|
|
584
|
+
// result.content === 'ip: 192.168.0.50'
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Secret Encryption
|
|
588
|
+
|
|
589
|
+
### System Overview (`src/secrets/`)
|
|
590
|
+
|
|
591
|
+
AES-256-GCM encryption for module secrets following Rule 10.1 separation:
|
|
592
|
+
|
|
593
|
+
**Master Key** (`master-key.ts`):
|
|
594
|
+
- `generateMasterKey()` - Policy: Generates 32-byte key
|
|
595
|
+
- `isValidMasterKey()` - Policy: Validates key format
|
|
596
|
+
- `writeMasterKey()` - Execution: Writes to file with 0600 permissions
|
|
597
|
+
- `readMasterKey()` - Execution: Reads from file
|
|
598
|
+
- `getOrCreateMasterKey()` - Orchestration: Ensures key exists
|
|
599
|
+
- `masterKeyExists()` - Check if key file exists
|
|
600
|
+
|
|
601
|
+
**Encryption** (`encryption.ts`):
|
|
602
|
+
- `encryptSecret()` - Policy: AES-256-GCM encryption with random IV
|
|
603
|
+
- `decryptSecret()` - Policy: Decrypts with key verification
|
|
604
|
+
- `isValidEncryptedSecret()` - Policy: Validates encrypted data format
|
|
605
|
+
|
|
606
|
+
### Encryption Details
|
|
607
|
+
|
|
608
|
+
**Algorithm**: AES-256-GCM (Authenticated Encryption with Associated Data)
|
|
609
|
+
- **Key**: 256 bits (32 bytes)
|
|
610
|
+
- **IV**: 128 bits (16 bytes) - randomly generated per encryption
|
|
611
|
+
- **Auth Tag**: 128 bits (16 bytes) - ensures integrity
|
|
612
|
+
|
|
613
|
+
**Storage Format**:
|
|
614
|
+
```typescript
|
|
615
|
+
{
|
|
616
|
+
encryptedValue: string, // Hex-encoded ciphertext
|
|
617
|
+
iv: string, // Hex-encoded initialization vector
|
|
618
|
+
authTag: string // Hex-encoded authentication tag
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Master Key Management
|
|
623
|
+
|
|
624
|
+
**Location**:
|
|
625
|
+
- Development: `/tmp/celilo/master.key` (configurable via `CELILO_MASTER_KEY_PATH`)
|
|
626
|
+
- Production: `/etc/celilo/master.key`
|
|
627
|
+
|
|
628
|
+
**Generation**:
|
|
629
|
+
```typescript
|
|
630
|
+
import { getOrCreateMasterKey } from './secrets/master-key';
|
|
631
|
+
|
|
632
|
+
// Automatically generates if missing
|
|
633
|
+
const masterKey = await getOrCreateMasterKey();
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**File Permissions**: 0600 (owner read/write only)
|
|
637
|
+
|
|
638
|
+
### Example Usage
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
import { getOrCreateMasterKey } from './secrets/master-key';
|
|
642
|
+
import { encryptSecret, decryptSecret } from './secrets/encryption';
|
|
643
|
+
|
|
644
|
+
// Get master key (generates if missing)
|
|
645
|
+
const masterKey = await getOrCreateMasterKey();
|
|
646
|
+
|
|
647
|
+
// Encrypt a secret
|
|
648
|
+
const encrypted = encryptSecret('my-api-key-12345', masterKey);
|
|
649
|
+
console.log(encrypted);
|
|
650
|
+
// {
|
|
651
|
+
// encryptedValue: '4a3b2c1d...',
|
|
652
|
+
// iv: 'f1e2d3c4...',
|
|
653
|
+
// authTag: 'a9b8c7d6...'
|
|
654
|
+
// }
|
|
655
|
+
|
|
656
|
+
// Decrypt the secret
|
|
657
|
+
const plaintext = decryptSecret(encrypted, masterKey);
|
|
658
|
+
console.log(plaintext); // 'my-api-key-12345'
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Security Features
|
|
662
|
+
|
|
663
|
+
1. **Authenticated Encryption**: GCM mode provides both confidentiality and integrity
|
|
664
|
+
2. **Random IVs**: Each encryption uses unique IV, same plaintext → different ciphertext
|
|
665
|
+
3. **Key Validation**: Master key length enforced (32 bytes)
|
|
666
|
+
4. **Tamper Detection**: Auth tag verification prevents corrupted/modified ciphertext
|
|
667
|
+
5. **Fail-Fast**: Invalid keys or corrupted data throw clear errors
|
|
668
|
+
|
|
669
|
+
### Error Handling
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
try {
|
|
673
|
+
const decrypted = decryptSecret(encrypted, masterKey);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
// Possible errors:
|
|
676
|
+
// - "Master key must be 32 bytes"
|
|
677
|
+
// - "Invalid encrypted secret: missing required fields"
|
|
678
|
+
// - "Invalid IV length: expected 16, got N"
|
|
679
|
+
// - "Failed to decrypt secret: [reason]" (wrong key, corrupted data)
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Testing
|
|
684
|
+
|
|
685
|
+
All functions are pure (no global state):
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
import { generateMasterKey, encryptSecret, decryptSecret } from './secrets';
|
|
689
|
+
|
|
690
|
+
const masterKey = generateMasterKey();
|
|
691
|
+
const encrypted = encryptSecret('test-secret', masterKey);
|
|
692
|
+
const decrypted = decryptSecret(encrypted, masterKey);
|
|
693
|
+
// decrypted === 'test-secret'
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Ansible Vault Integration
|
|
697
|
+
|
|
698
|
+
### System Overview (`src/ansible/`, `src/secrets/vault.ts`)
|
|
699
|
+
|
|
700
|
+
Celilo uses Ansible Vault to encrypt secrets in generated Ansible configurations, ensuring no plaintext secrets are written to disk.
|
|
701
|
+
|
|
702
|
+
**Key Components**:
|
|
703
|
+
- **`vault.ts`** - Derives vault password from master key (deterministic)
|
|
704
|
+
- **`ansible/secrets.ts`** - Generates and encrypts secrets.yml file
|
|
705
|
+
- **`ansible-resolver.ts`** - Converts `$secret:` variables to Jinja2 `{{ }}` syntax
|
|
706
|
+
|
|
707
|
+
### Encryption Flow
|
|
708
|
+
|
|
709
|
+
1. **Decrypt secrets** from database (using master key)
|
|
710
|
+
2. **Format as YAML** with all module secrets
|
|
711
|
+
3. **Derive vault password** from master key (SHA-256)
|
|
712
|
+
4. **Encrypt with ansible-vault** (AES-256)
|
|
713
|
+
5. **Write encrypted file** to `ansible/inventory/secrets.yml`
|
|
714
|
+
|
|
715
|
+
### Vault Password Derivation
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
import { deriveVaultPassword, getVaultPassword } from './secrets/vault';
|
|
719
|
+
|
|
720
|
+
// Deterministic derivation from master key
|
|
721
|
+
const masterKey = await getOrCreateMasterKey();
|
|
722
|
+
const vaultPassword = deriveVaultPassword(masterKey);
|
|
723
|
+
// Same master key always produces same vault password
|
|
724
|
+
|
|
725
|
+
// Or use orchestration function
|
|
726
|
+
const vaultPassword = await getVaultPassword();
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Algorithm**: `SHA-256(concat("ansible-vault:", masterKey))`
|
|
730
|
+
|
|
731
|
+
**Properties**:
|
|
732
|
+
- Deterministic (same master key → same vault password)
|
|
733
|
+
- Domain-separated (prevents key reuse attacks)
|
|
734
|
+
- Standard hash output (64 hex characters)
|
|
735
|
+
|
|
736
|
+
### Generated Ansible Structure
|
|
737
|
+
|
|
738
|
+
**Playbook** (`playbook.yml`):
|
|
739
|
+
```yaml
|
|
740
|
+
---
|
|
741
|
+
- name: Deploy Module
|
|
742
|
+
hosts: module-host
|
|
743
|
+
become: true
|
|
744
|
+
|
|
745
|
+
vars_files:
|
|
746
|
+
- inventory/secrets.yml # Encrypted with ansible-vault
|
|
747
|
+
|
|
748
|
+
roles:
|
|
749
|
+
- module-role
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Secrets File** (`inventory/secrets.yml`):
|
|
753
|
+
```
|
|
754
|
+
$ANSIBLE_VAULT;1.1;AES256
|
|
755
|
+
35646166616130633832363334383234306139626264373935623630393937313639623334356138
|
|
756
|
+
6561383035303565323364373934306632336461626562630a386237663365633039396631623639
|
|
757
|
+
...
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
**Templates** (e.g., `templates/config.json`):
|
|
761
|
+
```json
|
|
762
|
+
{
|
|
763
|
+
"api_key": "{{ api_key }}",
|
|
764
|
+
"password": "{{ db_password }}"
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Variable Resolution
|
|
769
|
+
|
|
770
|
+
Ansible templates use different resolution from Terraform:
|
|
771
|
+
|
|
772
|
+
**Terraform** (resolved at generation time):
|
|
773
|
+
- `$self:hostname` → `"myhost"`
|
|
774
|
+
- `$system:dns.primary` → `"192.168.0.1"`
|
|
775
|
+
- `$secret:api_key` → `"actual_secret_value"` (**plaintext in file!**)
|
|
776
|
+
|
|
777
|
+
**Ansible** (resolved at runtime):
|
|
778
|
+
- `$self:hostname` → `"myhost"` (resolved at generation)
|
|
779
|
+
- `$system:dns.primary` → `"192.168.0.1"` (resolved at generation)
|
|
780
|
+
- `$secret:api_key` → `{{ api_key }}` (Jinja2 variable, resolved from encrypted secrets.yml)
|
|
781
|
+
|
|
782
|
+
### CLI Usage
|
|
783
|
+
|
|
784
|
+
**Get vault password**:
|
|
785
|
+
```bash
|
|
786
|
+
# Display vault password
|
|
787
|
+
celilo system vault-password
|
|
788
|
+
|
|
789
|
+
# Use with ansible-vault
|
|
790
|
+
ansible-vault view inventory/secrets.yml \
|
|
791
|
+
--vault-password-file=<(celilo system vault-password)
|
|
792
|
+
|
|
793
|
+
# Edit encrypted secrets
|
|
794
|
+
ansible-vault edit inventory/secrets.yml \
|
|
795
|
+
--vault-password-file=<(celilo system vault-password)
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
**Run Ansible playbook**:
|
|
799
|
+
```bash
|
|
800
|
+
cd /tmp/celilo/modules/homebridge/generated/ansible
|
|
801
|
+
|
|
802
|
+
# Option 1: Process substitution (recommended)
|
|
803
|
+
ansible-playbook playbook.yml \
|
|
804
|
+
--vault-password-file=<(celilo system vault-password)
|
|
805
|
+
|
|
806
|
+
# Option 2: Environment variable
|
|
807
|
+
export ANSIBLE_VAULT_PASSWORD=$(celilo system vault-password)
|
|
808
|
+
echo "$ANSIBLE_VAULT_PASSWORD" | ansible-playbook playbook.yml --vault-password-file=/dev/stdin
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Security Considerations
|
|
812
|
+
|
|
813
|
+
**Benefits**:
|
|
814
|
+
- ✅ No plaintext secrets on disk
|
|
815
|
+
- ✅ Industry-standard encryption (Ansible Vault AES-256)
|
|
816
|
+
- ✅ Deterministic (same master key → same vault password)
|
|
817
|
+
- ✅ Works with standard Ansible tooling
|
|
818
|
+
- ✅ Secrets encrypted independently from celilo database
|
|
819
|
+
|
|
820
|
+
**Requirements**:
|
|
821
|
+
- ⚠️ **Ansible must be installed** - Generation fails without ansible-vault
|
|
822
|
+
- ⚠️ Master key must be protected (file permissions 0600)
|
|
823
|
+
- ⚠️ Vault password derivation is deterministic (anyone with master key can decrypt)
|
|
824
|
+
|
|
825
|
+
**Threat Model**:
|
|
826
|
+
- **Protects against**: Accidental secret exposure (logs, commits, backups)
|
|
827
|
+
- **Does NOT protect against**: Attacker with master key access
|
|
828
|
+
- **Assumption**: Master key is stored securely with restricted file permissions
|
|
829
|
+
|
|
830
|
+
### Implementation Details
|
|
831
|
+
|
|
832
|
+
**Files**:
|
|
833
|
+
- `src/secrets/vault.ts` - Vault password derivation
|
|
834
|
+
- `src/ansible/secrets.ts` - Secrets file generation and encryption
|
|
835
|
+
- `src/variables/ansible-resolver.ts` - Jinja2 variable conversion
|
|
836
|
+
- `src/templates/generator.ts` - Integration with template generation
|
|
837
|
+
|
|
838
|
+
**Functions**:
|
|
839
|
+
- `deriveVaultPassword(masterKey)` - Deterministic password derivation
|
|
840
|
+
- `generateAnsibleSecrets(moduleId, outputPath, db)` - Full generation pipeline
|
|
841
|
+
- `encryptWithAnsibleVault(yamlContent, password)` - Ansible Vault wrapper
|
|
842
|
+
- `convertSecretsToJinja(content, context)` - Variable conversion for Ansible
|
|
843
|
+
|
|
844
|
+
### Testing
|
|
845
|
+
|
|
846
|
+
**Unit Tests** (13 tests):
|
|
847
|
+
```bash
|
|
848
|
+
bun test src/secrets/vault.test.ts
|
|
849
|
+
bun test src/variables/ansible-resolver.test.ts
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Integration Tests**:
|
|
853
|
+
```bash
|
|
854
|
+
bun run test:integration
|
|
855
|
+
# Validates:
|
|
856
|
+
# - Secrets encrypted with ansible-vault
|
|
857
|
+
# - Ansible templates use Jinja2 variables
|
|
858
|
+
# - Vault password command works
|
|
859
|
+
# - Generated secrets can be decrypted
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**Manual Verification**:
|
|
863
|
+
```bash
|
|
864
|
+
# 1. Generate module
|
|
865
|
+
celilo module generate homebridge
|
|
866
|
+
|
|
867
|
+
# 2. Check secrets file is encrypted
|
|
868
|
+
head /tmp/celilo/modules/homebridge/generated/ansible/inventory/secrets.yml
|
|
869
|
+
# Should start with: $ANSIBLE_VAULT;1.1;AES256
|
|
870
|
+
|
|
871
|
+
# 3. Decrypt and verify
|
|
872
|
+
ansible-vault view /tmp/celilo/modules/homebridge/generated/ansible/inventory/secrets.yml \
|
|
873
|
+
--vault-password-file=<(celilo system vault-password)
|
|
874
|
+
# Should show YAML with secret values
|
|
875
|
+
|
|
876
|
+
# 4. Check templates use Jinja2
|
|
877
|
+
cat /tmp/celilo/modules/homebridge/generated/ansible/roles/homebridge/templates/config.json
|
|
878
|
+
# Should contain {{ variable_name }}, not plaintext secrets
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
## Template Generation
|
|
882
|
+
|
|
883
|
+
### System Overview (`src/templates/`)
|
|
884
|
+
|
|
885
|
+
Template generation following Rule 10.1 separation:
|
|
886
|
+
|
|
887
|
+
**Policy Functions**:
|
|
888
|
+
- `isTemplateFile(filename)` - Checks if file has template extension (`.tpl`, `.j2`)
|
|
889
|
+
- `getOutputFilename(template)` - Removes template extension from filename
|
|
890
|
+
|
|
891
|
+
**Execution Functions**:
|
|
892
|
+
- `discoverTemplateFiles(baseDir)` - Recursively finds template files
|
|
893
|
+
- `readTemplateFiles(modulePath, paths)` - Reads template content from disk
|
|
894
|
+
- `writeGeneratedFiles(outputPath, files)` - Writes generated files to disk
|
|
895
|
+
|
|
896
|
+
**Orchestration Function**:
|
|
897
|
+
- `generateTemplates(options)` - Main entry point, coordinates entire generation process
|
|
898
|
+
|
|
899
|
+
### Template Directories
|
|
900
|
+
|
|
901
|
+
Celilo looks for templates in:
|
|
902
|
+
- `terraform/` - Terraform configuration templates
|
|
903
|
+
- `ansible/` - Ansible playbook templates
|
|
904
|
+
|
|
905
|
+
Both directories are searched recursively.
|
|
906
|
+
|
|
907
|
+
### Template Extensions
|
|
908
|
+
|
|
909
|
+
- **`.tpl`** - Generic template files (Terraform style)
|
|
910
|
+
- **`.j2`** - Jinja2 template files (Ansible style)
|
|
911
|
+
|
|
912
|
+
Output files have extension removed:
|
|
913
|
+
- `main.tf.tpl` → `main.tf`
|
|
914
|
+
- `playbook.yml.j2` → `playbook.yml`
|
|
915
|
+
|
|
916
|
+
### Generation Process
|
|
917
|
+
|
|
918
|
+
1. **Discover** - Find all template files in `terraform/` and `ansible/` directories
|
|
919
|
+
2. **Build Context** - Load module config, secrets, and capabilities from database
|
|
920
|
+
3. **Read** - Load template content from files
|
|
921
|
+
4. **Resolve** - Replace variables using variable resolution system
|
|
922
|
+
5. **Write** - Save generated files to output directory
|
|
923
|
+
6. **Return** - Success with file list or detailed error messages
|
|
924
|
+
|
|
925
|
+
### Example Usage
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
import { generateTemplates } from './templates/generator';
|
|
929
|
+
|
|
930
|
+
const result = await generateTemplates({
|
|
931
|
+
moduleId: 'homebridge',
|
|
932
|
+
modulePath: '/data/modules/homebridge',
|
|
933
|
+
outputPath: '/data/modules/homebridge/generated',
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (result.success) {
|
|
937
|
+
console.log(`Generated ${result.files.length} files:`);
|
|
938
|
+
for (const file of result.files) {
|
|
939
|
+
console.log(` - ${file.path}`);
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
console.error('Generation failed:', result.error);
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Module Structure
|
|
947
|
+
|
|
948
|
+
```
|
|
949
|
+
modules/homebridge/
|
|
950
|
+
├── manifest.yml
|
|
951
|
+
├── terraform/
|
|
952
|
+
│ ├── main.tf.tpl # → generated/terraform/main.tf
|
|
953
|
+
│ ├── variables.tf.tpl # → generated/terraform/variables.tf
|
|
954
|
+
│ └── outputs.tf.tpl # → generated/terraform/outputs.tf
|
|
955
|
+
└── ansible/
|
|
956
|
+
├── playbook.yml.tpl # → generated/ansible/playbook.yml
|
|
957
|
+
└── roles/
|
|
958
|
+
└── homebridge/
|
|
959
|
+
└── tasks/
|
|
960
|
+
└── main.yml.tpl # → generated/ansible/roles/homebridge/tasks/main.yml
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Template Example
|
|
964
|
+
|
|
965
|
+
**Input** (`terraform/main.tf.tpl`):
|
|
966
|
+
```hcl
|
|
967
|
+
resource "proxmox_lxc" "container" {
|
|
968
|
+
hostname = "$self:hostname"
|
|
969
|
+
cores = $self:cores
|
|
970
|
+
memory = $self:memory
|
|
971
|
+
|
|
972
|
+
network {
|
|
973
|
+
name = "eth0"
|
|
974
|
+
bridge = "vmbr0"
|
|
975
|
+
ip = "$self:container_ip/24"
|
|
976
|
+
gw = "$system:management.ip"
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
provisioner "ansible" {
|
|
980
|
+
playbook = "./ansible/playbook.yml"
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**Output** (`generated/terraform/main.tf`):
|
|
986
|
+
```hcl
|
|
987
|
+
resource "proxmox_lxc" "container" {
|
|
988
|
+
hostname = "homebridge"
|
|
989
|
+
cores = 2
|
|
990
|
+
memory = 2048
|
|
991
|
+
|
|
992
|
+
network {
|
|
993
|
+
name = "eth0"
|
|
994
|
+
bridge = "vmbr0"
|
|
995
|
+
ip = "192.168.0.50/24"
|
|
996
|
+
gw = "192.168.0.10"
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
provisioner "ansible" {
|
|
1000
|
+
playbook = "./ansible/playbook.yml"
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Error Handling
|
|
1006
|
+
|
|
1007
|
+
Clear error messages for common failures:
|
|
1008
|
+
|
|
1009
|
+
```typescript
|
|
1010
|
+
// Module path doesn't exist
|
|
1011
|
+
{ success: false, error: "Module path does not exist: /path" }
|
|
1012
|
+
|
|
1013
|
+
// No templates found
|
|
1014
|
+
{ success: false, error: "No template files found in module" }
|
|
1015
|
+
|
|
1016
|
+
// Variable resolution failed
|
|
1017
|
+
{
|
|
1018
|
+
success: false,
|
|
1019
|
+
error: "Failed to resolve variables in templates:\n" +
|
|
1020
|
+
"terraform/main.tf:\n" +
|
|
1021
|
+
" $self:missing_var: Self variable 'missing_var' not found in module configuration"
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// File write failed
|
|
1025
|
+
{ success: false, error: "Failed to write generated files", details: Error }
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Integration
|
|
1029
|
+
|
|
1030
|
+
Template generation integrates with:
|
|
1031
|
+
- **Variable Resolution** - Resolves all `$self:`, `$system:`, `$secret:`, `$capability:` variables
|
|
1032
|
+
- **Database** - Loads module configuration and capability data
|
|
1033
|
+
- **File System** - Reads templates and writes generated code
|
|
1034
|
+
|
|
1035
|
+
## Next Steps
|
|
1036
|
+
|
|
1037
|
+
Phase 0 Part 1.7:
|
|
1038
|
+
1. ✅ Database layer
|
|
1039
|
+
2. ✅ Manifest validation
|
|
1040
|
+
3. ✅ Module import logic
|
|
1041
|
+
4. ✅ Variable resolution system
|
|
1042
|
+
5. ✅ Secret encryption (AES-256-GCM)
|
|
1043
|
+
6. ✅ Template generation (File I/O + variable resolution)
|
|
1044
|
+
7. CLI interface (Commands + user interaction)
|
|
1045
|
+
|
|
1046
|
+
## CLI Interface
|
|
1047
|
+
|
|
1048
|
+
### System Overview (`src/cli/`)
|
|
1049
|
+
|
|
1050
|
+
The CLI interface provides command-line tools for all Phase 0 operations following Rule 10.1 separation:
|
|
1051
|
+
|
|
1052
|
+
**Parser** (`parser.ts`) - Policy functions:
|
|
1053
|
+
- `parseArguments()` - Parses command-line arguments into structured format
|
|
1054
|
+
- `validateRequiredArgs()` - Validates argument count
|
|
1055
|
+
- `getArg()`, `getFlag()`, `hasFlag()` - Type-safe accessors
|
|
1056
|
+
|
|
1057
|
+
**Commands** (`commands/`) - Orchestration functions:
|
|
1058
|
+
- `module-import.ts` - Import modules from directory
|
|
1059
|
+
- `module-list.ts` - List installed modules
|
|
1060
|
+
- `module-config.ts` - Get/set module configuration
|
|
1061
|
+
- `module-generate.ts` - Generate templates for modules
|
|
1062
|
+
- `secret-set.ts` - Set encrypted secrets
|
|
1063
|
+
|
|
1064
|
+
**Entry Point** (`index.ts`) - Main orchestrator:
|
|
1065
|
+
- `runCli()` - Routes commands to handlers
|
|
1066
|
+
- `main()` - Entry point with error handling and exit codes
|
|
1067
|
+
|
|
1068
|
+
### Commands
|
|
1069
|
+
|
|
1070
|
+
**Module Import**
|
|
1071
|
+
```bash
|
|
1072
|
+
celilo module import <path> [--target <path>]
|
|
1073
|
+
|
|
1074
|
+
# Example
|
|
1075
|
+
celilo module import ./modules/homebridge
|
|
1076
|
+
celilo module import ./modules/homebridge --target /data/celilo/modules
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
**Module List**
|
|
1080
|
+
```bash
|
|
1081
|
+
celilo module list
|
|
1082
|
+
|
|
1083
|
+
# Output
|
|
1084
|
+
Installed modules:
|
|
1085
|
+
|
|
1086
|
+
homebridge (v1.0.0) - IMPORTED
|
|
1087
|
+
Bridge for HomeKit accessories
|
|
1088
|
+
|
|
1089
|
+
pihole (v2.1.0) - CONFIGURED
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
**Module Configuration**
|
|
1093
|
+
```bash
|
|
1094
|
+
# Set configuration value
|
|
1095
|
+
celilo module config set <module-id> <key> <value>
|
|
1096
|
+
|
|
1097
|
+
# Get specific value
|
|
1098
|
+
celilo module config get <module-id> [key]
|
|
1099
|
+
|
|
1100
|
+
# Examples
|
|
1101
|
+
celilo module config set homebridge hostname mybridge
|
|
1102
|
+
celilo module config set homebridge container_ip 192.168.0.50
|
|
1103
|
+
celilo module config get homebridge hostname
|
|
1104
|
+
celilo module config get homebridge # Get all config
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Module Generate**
|
|
1108
|
+
```bash
|
|
1109
|
+
celilo module generate <module-id> [--output <path>]
|
|
1110
|
+
|
|
1111
|
+
# Examples
|
|
1112
|
+
celilo module generate homebridge
|
|
1113
|
+
celilo module generate homebridge --output ./generated
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
**Secret Management**
|
|
1117
|
+
```bash
|
|
1118
|
+
celilo secret set <module-id> <name> <value>
|
|
1119
|
+
|
|
1120
|
+
# Example
|
|
1121
|
+
celilo secret set homebridge api_key mykey123
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
**System Configuration**
|
|
1125
|
+
```bash
|
|
1126
|
+
# Set system-wide config
|
|
1127
|
+
celilo system config set <key> <value>
|
|
1128
|
+
celilo system config get [key]
|
|
1129
|
+
|
|
1130
|
+
# Get Ansible Vault password
|
|
1131
|
+
celilo system vault-password
|
|
1132
|
+
|
|
1133
|
+
# Examples
|
|
1134
|
+
celilo system config set dns.primary 192.168.0.1
|
|
1135
|
+
celilo system config get dns.primary
|
|
1136
|
+
ansible-vault view secrets.yml --vault-password-file=<(celilo system vault-password)
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
**Help**
|
|
1140
|
+
```bash
|
|
1141
|
+
celilo help
|
|
1142
|
+
celilo --help
|
|
1143
|
+
celilo -h
|
|
1144
|
+
celilo module --help # Command-specific help
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
### Argument Parsing
|
|
1148
|
+
|
|
1149
|
+
The CLI uses a simple argument parser without external dependencies:
|
|
1150
|
+
|
|
1151
|
+
**Format**: `celilo <command> [subcommand] [args...] [--flags]`
|
|
1152
|
+
|
|
1153
|
+
**Parsing Rules**:
|
|
1154
|
+
- First argument is the command (`module`, `secret`, `help`)
|
|
1155
|
+
- Second argument is the subcommand if it doesn't start with `--`
|
|
1156
|
+
- Remaining arguments are positional args or flags
|
|
1157
|
+
- Flags start with `--` and can be boolean (`--verbose`) or string (`--target /path`)
|
|
1158
|
+
|
|
1159
|
+
**Examples**:
|
|
1160
|
+
```bash
|
|
1161
|
+
# Command only
|
|
1162
|
+
celilo help
|
|
1163
|
+
|
|
1164
|
+
# Command + subcommand
|
|
1165
|
+
celilo module list
|
|
1166
|
+
|
|
1167
|
+
# Command + subcommand + args
|
|
1168
|
+
celilo module import ./path
|
|
1169
|
+
|
|
1170
|
+
# Command + subcommand + args + flags
|
|
1171
|
+
celilo module generate homebridge --output ./out
|
|
1172
|
+
|
|
1173
|
+
# Nested subcommand
|
|
1174
|
+
celilo module config set homebridge hostname myhost
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
### Error Handling
|
|
1178
|
+
|
|
1179
|
+
All commands return structured results:
|
|
1180
|
+
```typescript
|
|
1181
|
+
interface CommandSuccess {
|
|
1182
|
+
success: true;
|
|
1183
|
+
message: string;
|
|
1184
|
+
data?: unknown;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
interface CommandError {
|
|
1188
|
+
success: false;
|
|
1189
|
+
error: string;
|
|
1190
|
+
details?: unknown;
|
|
1191
|
+
}
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
**Exit Codes**:
|
|
1195
|
+
- `0` - Success
|
|
1196
|
+
- `1` - Error (validation, not found, etc.)
|
|
1197
|
+
|
|
1198
|
+
**Error Messages** include usage hints:
|
|
1199
|
+
```
|
|
1200
|
+
Error: Missing required arguments. Expected 1, got 0
|
|
1201
|
+
|
|
1202
|
+
Usage: celilo module import <path> [--target <path>]
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### Testing
|
|
1206
|
+
|
|
1207
|
+
All CLI components have comprehensive tests:
|
|
1208
|
+
- **`parser.test.ts`** - Argument parsing (28 tests)
|
|
1209
|
+
- **`cli.test.ts`** - Integration tests (23 tests)
|
|
1210
|
+
|
|
1211
|
+
Tests use environment variables to configure database and master key paths for isolation.
|
|
1212
|
+
|
|
1213
|
+
### Entry Point
|
|
1214
|
+
|
|
1215
|
+
The main entry point is `src/cli/index.ts`, which exports `main()`:
|
|
1216
|
+
```typescript
|
|
1217
|
+
export async function main(): Promise<void> {
|
|
1218
|
+
const result = await runCli(process.argv);
|
|
1219
|
+
// Handle output and exit codes
|
|
1220
|
+
}
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
**Recommended:** Use the top-level wrapper script (from anywhere):
|
|
1224
|
+
```bash
|
|
1225
|
+
/path/to/celilo/celilo module list
|
|
1226
|
+
/path/to/celilo/celilo system config get
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
Or run directly from backend directory:
|
|
1230
|
+
```bash
|
|
1231
|
+
bun run src/cli/index.ts module list
|
|
1232
|
+
bun run src/cli/index.ts module import ./path
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
Or build a binary:
|
|
1236
|
+
```bash
|
|
1237
|
+
bun build src/cli/index.ts --compile --outfile celilo
|
|
1238
|
+
./celilo module list
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
### Architecture
|
|
1242
|
+
|
|
1243
|
+
**CLI Orchestration**:
|
|
1244
|
+
```
|
|
1245
|
+
User Input
|
|
1246
|
+
↓
|
|
1247
|
+
parseArguments() [Policy - parse and validate]
|
|
1248
|
+
↓
|
|
1249
|
+
runCli() [Orchestration - route to handler]
|
|
1250
|
+
↓
|
|
1251
|
+
handleModuleXxx() [Orchestration - coordinate operations]
|
|
1252
|
+
↓
|
|
1253
|
+
importModule() [Execution - database, filesystem]
|
|
1254
|
+
generateTemplates()
|
|
1255
|
+
etc.
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
**Separation of Concerns**:
|
|
1259
|
+
- **Parser** - Pure policy functions, no side effects
|
|
1260
|
+
- **Handlers** - Orchestrate multiple operations
|
|
1261
|
+
- **Core modules** - Execute actual work (import, generate, etc.)
|
|
1262
|
+
- **Entry point** - Handle errors and exit codes
|
|
1263
|
+
|
|
1264
|
+
All commands integrate with existing modules:
|
|
1265
|
+
- **Database** - Module metadata, configuration, secrets
|
|
1266
|
+
- **Manifest validation** - Parse and validate manifests
|
|
1267
|
+
- **Module import** - Copy files and insert to database
|
|
1268
|
+
- **Variable resolution** - Resolve template variables
|
|
1269
|
+
- **Secret encryption** - Encrypt/decrypt with master key
|
|
1270
|
+
- **Template generation** - Generate Terraform/Ansible code
|
|
1271
|
+
|
|
1272
|
+
## Troubleshooting
|
|
1273
|
+
|
|
1274
|
+
### Common Errors and Solutions
|
|
1275
|
+
|
|
1276
|
+
#### "Module ID must use kebab-case"
|
|
1277
|
+
|
|
1278
|
+
**Error**:
|
|
1279
|
+
```
|
|
1280
|
+
Error: Manifest validation failed
|
|
1281
|
+
Module ID must use kebab-case (lowercase letters, numbers, hyphens between segments)
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
**Cause**: Module ID uses underscores, uppercase, or invalid characters.
|
|
1285
|
+
|
|
1286
|
+
**Solution**: Use kebab-case for all module IDs:
|
|
1287
|
+
```yaml
|
|
1288
|
+
# ❌ WRONG
|
|
1289
|
+
metadata:
|
|
1290
|
+
name: dns_external # Underscores
|
|
1291
|
+
|
|
1292
|
+
# ✅ CORRECT
|
|
1293
|
+
metadata:
|
|
1294
|
+
name: dns-external # Kebab-case
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
**Pattern**: `/^[a-z0-9]+(-[a-z0-9]+)*$/`
|
|
1298
|
+
|
|
1299
|
+
---
|
|
1300
|
+
|
|
1301
|
+
#### "Variable resolution failed: Self variable not found"
|
|
1302
|
+
|
|
1303
|
+
**Error**:
|
|
1304
|
+
```
|
|
1305
|
+
Failed to resolve variables in templates:
|
|
1306
|
+
terraform/main.tf:
|
|
1307
|
+
$self:container_ip: Self variable 'container_ip' not found in module configuration
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
**Cause**: Template references variable that isn't configured.
|
|
1311
|
+
|
|
1312
|
+
**Solution**: Set the missing variable:
|
|
1313
|
+
```bash
|
|
1314
|
+
celilo module config set <module-id> container_ip 10.0.20.100
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
Or check for typos in template:
|
|
1318
|
+
```hcl
|
|
1319
|
+
# Check spelling
|
|
1320
|
+
ip = "$self:container_ip" # Not 'containerIP', 'container-ip', etc.
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
---
|
|
1324
|
+
|
|
1325
|
+
#### "ENOENT: no such file or directory" (path with spaces)
|
|
1326
|
+
|
|
1327
|
+
**Error**:
|
|
1328
|
+
```
|
|
1329
|
+
Error: ENOENT: no such file or directory, open '/Users/user/Application Support/celilo/...'
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
**Cause**: Unquoted path in shell command with spaces.
|
|
1333
|
+
|
|
1334
|
+
**Solution**: Always quote paths in shell commands:
|
|
1335
|
+
```typescript
|
|
1336
|
+
// ❌ WRONG
|
|
1337
|
+
execSync(`cd ${modulePath} && make build`);
|
|
1338
|
+
|
|
1339
|
+
// ✅ CORRECT
|
|
1340
|
+
execSync(`cd "${modulePath}" && make build`);
|
|
1341
|
+
|
|
1342
|
+
// ✅ BEST
|
|
1343
|
+
import { shellEscape } from '@/utils/shell';
|
|
1344
|
+
execSync(`cd ${shellEscape(modulePath)} && make build`);
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
---
|
|
1348
|
+
|
|
1349
|
+
#### "Drizzle where clause returns all rows"
|
|
1350
|
+
|
|
1351
|
+
**Error**: Query returns all modules instead of filtering by ID.
|
|
1352
|
+
|
|
1353
|
+
**Cause**: Using JavaScript operators instead of Drizzle operators.
|
|
1354
|
+
|
|
1355
|
+
**Solution**: Use Drizzle operator functions:
|
|
1356
|
+
```typescript
|
|
1357
|
+
import { eq } from 'drizzle-orm';
|
|
1358
|
+
|
|
1359
|
+
// ❌ WRONG - returns ALL rows!
|
|
1360
|
+
db.select().from(modules).where(modules.id === 'homebridge');
|
|
1361
|
+
|
|
1362
|
+
// ✅ CORRECT - filters correctly
|
|
1363
|
+
db.select().from(modules).where(eq(modules.id, 'homebridge'));
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
**Always use**: `eq()`, `ne()`, `gt()`, `lt()`, `and()`, `or()`, etc.
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1370
|
+
#### "ansible-vault: command not found"
|
|
1371
|
+
|
|
1372
|
+
**Error**:
|
|
1373
|
+
```
|
|
1374
|
+
Error: Failed to encrypt secrets with ansible-vault
|
|
1375
|
+
ansible-vault: command not found
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
**Cause**: Ansible is not installed or not in PATH.
|
|
1379
|
+
|
|
1380
|
+
**Solution**: Install Ansible:
|
|
1381
|
+
```bash
|
|
1382
|
+
# macOS
|
|
1383
|
+
brew install ansible
|
|
1384
|
+
|
|
1385
|
+
# Ubuntu/Debian
|
|
1386
|
+
sudo apt install ansible
|
|
1387
|
+
|
|
1388
|
+
# Verify
|
|
1389
|
+
ansible-vault --version
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
---
|
|
1393
|
+
|
|
1394
|
+
#### "Test passes alone, fails in suite"
|
|
1395
|
+
|
|
1396
|
+
**Error**: Test passes when run individually, fails when run with others.
|
|
1397
|
+
|
|
1398
|
+
**Cause**: State leakage between tests (shared database, temp files, environment).
|
|
1399
|
+
|
|
1400
|
+
**Solution**: Add proper setup/teardown:
|
|
1401
|
+
```typescript
|
|
1402
|
+
import { describe, test, beforeEach, afterEach } from 'bun:test';
|
|
1403
|
+
|
|
1404
|
+
describe('Feature Tests', () => {
|
|
1405
|
+
let testDb: Database;
|
|
1406
|
+
let tempDir: string;
|
|
1407
|
+
|
|
1408
|
+
beforeEach(async () => {
|
|
1409
|
+
testDb = await setupTestDatabase(); // Fresh database
|
|
1410
|
+
tempDir = await createTempDirectory(); // Fresh directory
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
afterEach(async () => {
|
|
1414
|
+
await cleanupTestDatabase(testDb); // MUST await!
|
|
1415
|
+
await removeTempDirectory(tempDir); // MUST await!
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
test('isolated test', async () => {
|
|
1419
|
+
// Test runs with clean state
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
---
|
|
1425
|
+
|
|
1426
|
+
#### "Cannot find module '@/something'"
|
|
1427
|
+
|
|
1428
|
+
**Error**:
|
|
1429
|
+
```
|
|
1430
|
+
Error: Cannot find module '@/module/import'
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
**Cause**: TypeScript path aliases not resolved.
|
|
1434
|
+
|
|
1435
|
+
**Solution**: Check `tsconfig.json` has correct paths:
|
|
1436
|
+
```json
|
|
1437
|
+
{
|
|
1438
|
+
"compilerOptions": {
|
|
1439
|
+
"baseUrl": ".",
|
|
1440
|
+
"paths": {
|
|
1441
|
+
"@/*": ["src/*"]
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
```
|
|
1446
|
+
|
|
1447
|
+
And run with Bun (which respects tsconfig paths):
|
|
1448
|
+
```bash
|
|
1449
|
+
bun run src/cli/index.ts # ✅ Works
|
|
1450
|
+
node src/cli/index.ts # ❌ Doesn't resolve @/ aliases
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
---
|
|
1454
|
+
|
|
1455
|
+
#### "Master key permission denied"
|
|
1456
|
+
|
|
1457
|
+
**Error**:
|
|
1458
|
+
```
|
|
1459
|
+
Error: EACCES: permission denied, open '/etc/celilo/master.key'
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
**Cause**: Master key file has wrong permissions or wrong owner.
|
|
1463
|
+
|
|
1464
|
+
**Solution**: Fix permissions:
|
|
1465
|
+
```bash
|
|
1466
|
+
# Check current permissions
|
|
1467
|
+
ls -la /tmp/celilo/master.key
|
|
1468
|
+
|
|
1469
|
+
# Fix permissions (owner read/write only)
|
|
1470
|
+
chmod 600 /tmp/celilo/master.key
|
|
1471
|
+
|
|
1472
|
+
# Or regenerate (WARNING: invalidates all encrypted secrets!)
|
|
1473
|
+
rm /tmp/celilo/master.key
|
|
1474
|
+
celilo system vault-password # Generates new key
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
---
|
|
1478
|
+
|
|
1479
|
+
#### "Migration already exists"
|
|
1480
|
+
|
|
1481
|
+
**Error**:
|
|
1482
|
+
```
|
|
1483
|
+
Error: Migration 0005_add_capability_secrets.sql already exists
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
**Cause**: Schema changed but migration not generated or conflicting migration number.
|
|
1487
|
+
|
|
1488
|
+
**Solution**: Check for duplicates:
|
|
1489
|
+
```bash
|
|
1490
|
+
# List migrations
|
|
1491
|
+
ls drizzle/
|
|
1492
|
+
|
|
1493
|
+
# Remove duplicate or conflicting migration
|
|
1494
|
+
rm drizzle/0005_duplicate.sql
|
|
1495
|
+
|
|
1496
|
+
# Regenerate
|
|
1497
|
+
bunx drizzle-kit generate
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
### Debug Checklist
|
|
1503
|
+
|
|
1504
|
+
When debugging issues, check:
|
|
1505
|
+
|
|
1506
|
+
1. **Test isolation**:
|
|
1507
|
+
- [ ] `beforeEach`/`afterEach` properly clean up
|
|
1508
|
+
- [ ] Tests can run in any order
|
|
1509
|
+
- [ ] Tests use isolated database/filesystem
|
|
1510
|
+
|
|
1511
|
+
2. **Path handling**:
|
|
1512
|
+
- [ ] All shell commands quote paths
|
|
1513
|
+
- [ ] Tested with paths containing spaces
|
|
1514
|
+
- [ ] No hardcoded absolute paths
|
|
1515
|
+
|
|
1516
|
+
3. **Drizzle queries**:
|
|
1517
|
+
- [ ] Using operator functions (`eq()`, not `===`)
|
|
1518
|
+
- [ ] Importing operators from `'drizzle-orm'`
|
|
1519
|
+
- [ ] Not using callback form in `where()`
|
|
1520
|
+
|
|
1521
|
+
4. **Identifier naming**:
|
|
1522
|
+
- [ ] Module IDs use kebab-case
|
|
1523
|
+
- [ ] No underscores or uppercase
|
|
1524
|
+
- [ ] Pattern: `/^[a-z0-9]+(-[a-z0-9]+)*$/`
|
|
1525
|
+
|
|
1526
|
+
5. **Variable resolution**:
|
|
1527
|
+
- [ ] Using colon syntax (`$self:var`, not `$self.var`)
|
|
1528
|
+
- [ ] Simple syntax for standalone values
|
|
1529
|
+
- [ ] Braced syntax for concatenation (`${self:disk}G`)
|
|
1530
|
+
|
|
1531
|
+
6. **Secrets**:
|
|
1532
|
+
- [ ] Master key exists and has 0600 permissions
|
|
1533
|
+
- [ ] Ansible installed for vault encryption
|
|
1534
|
+
- [ ] Secrets encrypted in database
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
### Getting Help
|
|
1539
|
+
|
|
1540
|
+
**Documentation**:
|
|
1541
|
+
- [CLAUDE.md](../../CLAUDE.md) - Engineering standards
|
|
1542
|
+
- [TESTING_STRATEGY.md](../../design/TESTING_STRATEGY.md) - Testing approach
|
|
1543
|
+
- [IDENTIFIER_NAMING_CONVENTIONS.md](../../design/IDENTIFIER_NAMING_CONVENTIONS.md) - Naming rules
|
|
1544
|
+
- [TEMPLATE_VARIABLE_SYNTAX.md](../../design/TEMPLATE_VARIABLE_SYNTAX.md) - Variable syntax
|
|
1545
|
+
|
|
1546
|
+
**Debugging Tools**:
|
|
1547
|
+
- Drizzle Studio: `bun run db:studio`
|
|
1548
|
+
- Test watch mode: `bun test --watch`
|
|
1549
|
+
- Verbose logs: `export CONDUCTOR_LOG_LEVEL=debug`
|
|
1550
|
+
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
## Next Steps
|
|
1554
|
+
|
|
1555
|
+
Phase 0 Part 1.7:
|
|
1556
|
+
1. ✅ Database layer
|
|
1557
|
+
2. ✅ Manifest validation
|
|
1558
|
+
3. ✅ Module import logic
|
|
1559
|
+
4. ✅ Variable resolution system
|
|
1560
|
+
5. ✅ Secret encryption (AES-256-GCM)
|
|
1561
|
+
6. ✅ Template generation (File I/O + variable resolution)
|
|
1562
|
+
7. ✅ CLI interface (Commands + user interaction)
|
|
1563
|
+
|
|
1564
|
+
**Phase 0 Part 1 is complete!**
|
|
1565
|
+
|
|
1566
|
+
Next: Phase 0 Part 2 - Web UI (React + tRPC)
|