@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/drizzle/0009_dns_registrations.sql +13 -0
- package/drizzle/0010_dns_internal_records.sql +12 -0
- package/drizzle/0011_backups_name.sql +1 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +3 -3
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +211 -1
- package/src/api-clients/proxmox.ts +399 -8
- package/src/cli/command-registry.ts +83 -6
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +89 -24
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-publish.ts +24 -0
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +26 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
- package/src/cli/commands/publish/alpha.test.ts +26 -0
- package/src/cli/commands/publish/alpha.ts +23 -0
- package/src/cli/commands/publish/types.ts +7 -2
- package/src/cli/commands/publish/workspace.ts +11 -1
- package/src/cli/commands/restore.ts +29 -0
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +283 -209
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +252 -129
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-doctor.ts +135 -40
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/commands/system-migrate.test.ts +40 -0
- package/src/cli/commands/system-migrate.ts +65 -0
- package/src/cli/completion.ts +23 -0
- package/src/cli/index.ts +91 -7
- package/src/cli/service-credential.ts +54 -0
- package/src/config/paths.test.ts +61 -48
- package/src/db/client.ts +15 -146
- package/src/db/migrate.ts +14 -6
- package/src/db/schema-introspection.ts +88 -0
- package/src/db/schema.ts +74 -0
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/hooks/capability-loader.ts +43 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +17 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-interview.ts +232 -0
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-preflight.ts +25 -0
- package/src/services/deploy-validation.test.ts +54 -4
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/dns-internal-records.test.ts +126 -0
- package/src/services/dns-internal-records.ts +119 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-provider-backfill.ts +14 -2
- package/src/services/dns-registrations.test.ts +120 -0
- package/src/services/dns-registrations.ts +108 -0
- package/src/services/events-daemon.test.ts +59 -0
- package/src/services/events-daemon.ts +191 -57
- package/src/services/fleet-checks.test.ts +508 -0
- package/src/services/fleet-checks.ts +678 -0
- package/src/services/module-build.test.ts +43 -38
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/module-subscriptions.test.ts +88 -0
- package/src/services/module-subscriptions.ts +50 -1
- package/src/services/module-validator/bundled-deps.test.ts +55 -0
- package/src/services/module-validator/bundled-deps.ts +115 -0
- package/src/services/module-validator/capability-versions.test.ts +1 -1
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +92 -12
- package/src/templates/generator.ts +165 -80
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
package/src/cli/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { COMMANDS, type CommandDef } from './command-registry';
|
|
|
10
10
|
import { handleCapabilityInfo } from './commands/capability-info';
|
|
11
11
|
import { handleCapabilityList } from './commands/capability-list';
|
|
12
12
|
import { handleCompletion } from './commands/completion';
|
|
13
|
+
import { handleDnsRegistrations } from './commands/dns';
|
|
13
14
|
import {
|
|
14
15
|
handleEventsAck,
|
|
15
16
|
handleEventsDrain,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
handleEventsRepair,
|
|
22
23
|
handleEventsReply,
|
|
23
24
|
handleEventsRespond,
|
|
25
|
+
handleEventsResyncSubscriptions,
|
|
24
26
|
handleEventsRun,
|
|
25
27
|
handleEventsRunHook,
|
|
26
28
|
handleEventsShowDaemon,
|
|
@@ -64,6 +66,7 @@ import { handleModuleTypesCheck, handleModuleTypesGenerate } from './commands/mo
|
|
|
64
66
|
import { handleModuleUpgrade } from './commands/module-upgrade';
|
|
65
67
|
import { moduleVerify } from './commands/module-verify';
|
|
66
68
|
import { handlePackage } from './commands/package';
|
|
69
|
+
import { handleProxmoxNodeList } from './commands/proxmox-node-list';
|
|
67
70
|
import { main as runPublish } from './commands/publish';
|
|
68
71
|
import { handleSecretList } from './commands/secret-list';
|
|
69
72
|
import { handleSecretSet } from './commands/secret-set';
|
|
@@ -87,6 +90,7 @@ import { handleSystemAudit } from './commands/system-audit';
|
|
|
87
90
|
import { handleSystemConfigGet, handleSystemConfigSet } from './commands/system-config';
|
|
88
91
|
import { handleSystemDoctor } from './commands/system-doctor';
|
|
89
92
|
import { handleSystemInit } from './commands/system-init';
|
|
93
|
+
import { handleSystemMigrate } from './commands/system-migrate';
|
|
90
94
|
import { handleSystemSecretGet } from './commands/system-secret-get';
|
|
91
95
|
import { handleSystemSecretSet } from './commands/system-secret-set';
|
|
92
96
|
import { handleSystemUpdate } from './commands/system-update';
|
|
@@ -162,6 +166,7 @@ Commands:
|
|
|
162
166
|
audit Top-level alias for 'system audit'
|
|
163
167
|
events SQLite event-bus operations (status, tail, run dispatcher, etc.)
|
|
164
168
|
capability View registered module capabilities
|
|
169
|
+
dns View DNS bookkeeping (registrations ledger)
|
|
165
170
|
package Create distributable .netapp packages from module source
|
|
166
171
|
module Manage modules (import, list, configure, build, generate)
|
|
167
172
|
service Manage container services (Proxmox, Digital Ocean)
|
|
@@ -171,6 +176,7 @@ Commands:
|
|
|
171
176
|
machine Manage machine pool (bring-your-own-hardware)
|
|
172
177
|
system Manage system configuration
|
|
173
178
|
ipam Manage IP address and VMID allocations and reservations
|
|
179
|
+
proxmox Proxmox cluster introspection (proxmox node list)
|
|
174
180
|
publish Publish workspace packages to npm and modules to celilo.computer
|
|
175
181
|
subscribers Manage build-bus subscribers (cross-machine publish-event delivery)
|
|
176
182
|
completion Generate shell completion scripts (bash/zsh)
|
|
@@ -255,6 +261,7 @@ Subcommands:
|
|
|
255
261
|
status Print bus health as JSON
|
|
256
262
|
tail [--type T] [--limit N] Recent events as JSON
|
|
257
263
|
list-subscribers List persistent bus subscribers
|
|
264
|
+
resync-subscriptions Rebuild subscribers from deployed modules' manifests (after a restore/migration)
|
|
258
265
|
list-pending [--subscriber] List pending deliveries
|
|
259
266
|
drain [--concurrency N] Process pending deliveries once and return
|
|
260
267
|
run [--poll-ms N] Run the long-running dispatcher (foreground)
|
|
@@ -265,9 +272,9 @@ Subcommands:
|
|
|
265
272
|
repair Crash-recovery sweep without starting the dispatcher
|
|
266
273
|
resume Alias for repair (acknowledges halt-on-recovery)
|
|
267
274
|
respond Run the terminal responder; answer deploy prompts from another shell
|
|
268
|
-
install-daemon
|
|
269
|
-
uninstall-daemon
|
|
270
|
-
show-daemon
|
|
275
|
+
install-daemon [--system] Write a systemd/launchd unit for the dispatcher (--system: management-plane scope)
|
|
276
|
+
uninstall-daemon [--system] Remove the installed supervisor unit
|
|
277
|
+
show-daemon [--system] Print the currently installed unit file
|
|
271
278
|
|
|
272
279
|
Description:
|
|
273
280
|
The event bus is a SQLite-backed pub/sub layer for celilo modules.
|
|
@@ -282,7 +289,7 @@ Examples:
|
|
|
282
289
|
celilo events tail --type deploy.completed.lunacycle # filter by type
|
|
283
290
|
celilo events emit deploy.completed.lunacycle '{}' # operator-fired event
|
|
284
291
|
celilo events tail --type 'config.required.*' # see pending deploy questions
|
|
285
|
-
celilo events reply 42 '"
|
|
292
|
+
celilo events reply 42 '"example.net"' # answer query #42 (config)
|
|
286
293
|
`;
|
|
287
294
|
return { success: true, message: helpText.trim() };
|
|
288
295
|
}
|
|
@@ -380,6 +387,32 @@ Related Commands:
|
|
|
380
387
|
};
|
|
381
388
|
}
|
|
382
389
|
|
|
390
|
+
function displayDnsHelp(): CommandResult {
|
|
391
|
+
const helpText = `
|
|
392
|
+
Celilo - DNS Bookkeeping
|
|
393
|
+
|
|
394
|
+
Usage:
|
|
395
|
+
celilo dns <subcommand> [options]
|
|
396
|
+
|
|
397
|
+
Subcommands:
|
|
398
|
+
registrations List the DNS registration ledger
|
|
399
|
+
|
|
400
|
+
Description:
|
|
401
|
+
Every successful dns_registrar.registerHost (e.g. a deploy registering
|
|
402
|
+
its public hostname with namecheap) is recorded in the registration
|
|
403
|
+
ledger. The provider's refresh_registrations hook re-asserts these on
|
|
404
|
+
a timer; REFRESHED shows when that last succeeded.
|
|
405
|
+
|
|
406
|
+
Options:
|
|
407
|
+
--provider <module-id> Only show registrations owned by one provider
|
|
408
|
+
|
|
409
|
+
Examples:
|
|
410
|
+
celilo dns registrations
|
|
411
|
+
celilo dns registrations --provider namecheap
|
|
412
|
+
`;
|
|
413
|
+
return { success: true, message: helpText };
|
|
414
|
+
}
|
|
415
|
+
|
|
383
416
|
function displayCapabilityHelp(): CommandResult {
|
|
384
417
|
const helpText = `
|
|
385
418
|
Celilo - Capability Management
|
|
@@ -1060,7 +1093,7 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1060
1093
|
'',
|
|
1061
1094
|
'Examples:',
|
|
1062
1095
|
' celilo hook run namecheap validate_config --debug',
|
|
1063
|
-
' celilo hook run namecheap container_created vps_ip=
|
|
1096
|
+
' celilo hook run namecheap container_created vps_ip=192.0.2.30',
|
|
1064
1097
|
].join('\n'),
|
|
1065
1098
|
};
|
|
1066
1099
|
}
|
|
@@ -1075,6 +1108,28 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1075
1108
|
};
|
|
1076
1109
|
}
|
|
1077
1110
|
|
|
1111
|
+
if (parsed.command === 'dns') {
|
|
1112
|
+
if (parsed.flags.help || parsed.flags.h) {
|
|
1113
|
+
return displayDnsHelp();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (!parsed.subcommand) {
|
|
1117
|
+
return {
|
|
1118
|
+
success: false,
|
|
1119
|
+
error: 'DNS subcommand required\n\nRun "celilo dns --help" for usage',
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (parsed.subcommand === 'registrations') {
|
|
1124
|
+
return handleDnsRegistrations(parsed.args, parsed.flags);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return {
|
|
1128
|
+
success: false,
|
|
1129
|
+
error: `Unknown dns subcommand: ${parsed.subcommand}\n\nRun "celilo dns --help" for usage`,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1078
1133
|
if (parsed.command === 'capability') {
|
|
1079
1134
|
if (parsed.flags.help || parsed.flags.h) {
|
|
1080
1135
|
return displayCapabilityHelp();
|
|
@@ -1144,6 +1199,8 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1144
1199
|
return handleEventsTail(parsed.args, parsed.flags);
|
|
1145
1200
|
case 'list-subscribers':
|
|
1146
1201
|
return handleEventsListSubscribers();
|
|
1202
|
+
case 'resync-subscriptions':
|
|
1203
|
+
return handleEventsResyncSubscriptions();
|
|
1147
1204
|
case 'list-pending':
|
|
1148
1205
|
return handleEventsListPending(parsed.args, parsed.flags);
|
|
1149
1206
|
case 'drain':
|
|
@@ -1168,9 +1225,9 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1168
1225
|
case 'install-daemon':
|
|
1169
1226
|
return handleEventsInstallDaemon(parsed.args, parsed.flags);
|
|
1170
1227
|
case 'uninstall-daemon':
|
|
1171
|
-
return handleEventsUninstallDaemon();
|
|
1228
|
+
return handleEventsUninstallDaemon(parsed.args, parsed.flags);
|
|
1172
1229
|
case 'show-daemon':
|
|
1173
|
-
return handleEventsShowDaemon();
|
|
1230
|
+
return handleEventsShowDaemon(parsed.args, parsed.flags);
|
|
1174
1231
|
default:
|
|
1175
1232
|
return {
|
|
1176
1233
|
success: false,
|
|
@@ -1729,12 +1786,39 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1729
1786
|
return handleSystemDoctor(parsed.args, parsed.flags);
|
|
1730
1787
|
}
|
|
1731
1788
|
|
|
1789
|
+
if (parsed.subcommand === 'migrate') {
|
|
1790
|
+
return handleSystemMigrate();
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1732
1793
|
return {
|
|
1733
1794
|
success: false,
|
|
1734
1795
|
error: `Unknown system subcommand: ${parsed.subcommand}\n\nRun "celilo system --help" for usage`,
|
|
1735
1796
|
};
|
|
1736
1797
|
}
|
|
1737
1798
|
|
|
1799
|
+
if (parsed.command === 'proxmox') {
|
|
1800
|
+
if (!parsed.subcommand) {
|
|
1801
|
+
return {
|
|
1802
|
+
success: false,
|
|
1803
|
+
error: 'Proxmox subcommand required (node)\n\nRun "celilo proxmox --help" for usage',
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (parsed.subcommand === 'node') {
|
|
1807
|
+
const nodeSubcommand = parsed.args[0];
|
|
1808
|
+
if (nodeSubcommand === 'list') {
|
|
1809
|
+
return handleProxmoxNodeList(parsed.args.slice(1), parsed.flags);
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
success: false,
|
|
1813
|
+
error: 'Proxmox node action required (list)\n\nRun "celilo proxmox --help" for usage',
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
return {
|
|
1817
|
+
success: false,
|
|
1818
|
+
error: `Unknown proxmox subcommand: ${parsed.subcommand}\n\nRun "celilo proxmox --help" for usage`,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1738
1822
|
if (parsed.command === 'ipam') {
|
|
1739
1823
|
// Handle ipam --help
|
|
1740
1824
|
if (parsed.flags.help || parsed.flags.h) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-credential secret resolution (ISS-0127, D7).
|
|
3
|
+
*
|
|
4
|
+
* Service credentials (a Proxmox API token, a DigitalOcean API token, an SSH
|
|
5
|
+
* password) are a special kind of secret: at `service add` time the service
|
|
6
|
+
* does not exist yet, so there is no encrypted store to write the value to
|
|
7
|
+
* out-of-band and no safe sink for a bus reply. Per design decision D7 these
|
|
8
|
+
* secrets therefore travel by **flag or env var**, never over the event bus.
|
|
9
|
+
*
|
|
10
|
+
* The resolution order is: explicit `--<flag>` → `$ENV` → (only when stdin is
|
|
11
|
+
* a TTY) a local clack `password` prompt → otherwise a fail-fast error naming
|
|
12
|
+
* the flag and env var. This keeps a zero-TTY `service add` possible while the
|
|
13
|
+
* credential never lands on the bus, and the local password prompt is the one
|
|
14
|
+
* clack input the recurrence gate (D6) allow-lists — precisely because a
|
|
15
|
+
* flag/env path always exists alongside it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { promptPassword } from './prompts';
|
|
19
|
+
|
|
20
|
+
export interface ServiceCredentialSpec {
|
|
21
|
+
/** Human-readable field name used in the prompt + error, e.g. "API token secret". */
|
|
22
|
+
field: string;
|
|
23
|
+
/** The flag value (already resolved from `flags[<flag>]`), if the operator passed `--<flag>`. */
|
|
24
|
+
flagValue: string | boolean | undefined;
|
|
25
|
+
/** The CLI flag the operator would pass, e.g. `api-token-secret`. */
|
|
26
|
+
flag: string;
|
|
27
|
+
/** The environment variable that supplies the value headlessly, e.g. `PROXMOX_API_TOKEN_SECRET`. */
|
|
28
|
+
envVar: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a service-credential secret from flag → env → (TTY) clack password →
|
|
33
|
+
* error. Returns the secret value as a string. Throws an actionable error when
|
|
34
|
+
* no value is available on a non-TTY run.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveServiceCredential(spec: ServiceCredentialSpec): Promise<string> {
|
|
37
|
+
if (typeof spec.flagValue === 'string' && spec.flagValue.trim() !== '') {
|
|
38
|
+
return spec.flagValue.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fromEnv = process.env[spec.envVar];
|
|
42
|
+
if (fromEnv && fromEnv.trim() !== '') {
|
|
43
|
+
return fromEnv.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.stdin.isTTY) {
|
|
47
|
+
return promptPassword({
|
|
48
|
+
message: `${spec.field}:`,
|
|
49
|
+
validate: (val) => (!val || val.trim() === '' ? `${spec.field} is required` : undefined),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(`${spec.field} required: pass --${spec.flag} or set $${spec.envVar} (no TTY)`);
|
|
54
|
+
}
|
package/src/config/paths.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { skipIntegration } from '../test-utils/integration-guard';
|
|
2
3
|
import { getDataDir, getDbPath, getMasterKeyPath, getModuleStoragePath } from './paths';
|
|
3
4
|
|
|
4
5
|
describe('paths configuration', () => {
|
|
@@ -34,18 +35,21 @@ describe('paths configuration', () => {
|
|
|
34
35
|
expect(path).toMatch(/celilo-data\/modules$/);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
it(
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
39
|
+
'returns platform-specific path by default',
|
|
40
|
+
() => {
|
|
41
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
42
|
+
process.env.ENVIRONMENT = undefined;
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
const path = getModuleStoragePath();
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/modules$/);
|
|
48
|
+
} else {
|
|
49
|
+
expect(path).toBe('/var/lib/celilo/modules');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
);
|
|
49
53
|
|
|
50
54
|
it('prioritizes CELILO_DATA_DIR over ENVIRONMENT=dev', () => {
|
|
51
55
|
process.env.CELILO_DATA_DIR = '/custom/data';
|
|
@@ -73,18 +77,21 @@ describe('paths configuration', () => {
|
|
|
73
77
|
expect(path).toMatch(/celilo-data$/);
|
|
74
78
|
});
|
|
75
79
|
|
|
76
|
-
it(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
81
|
+
'returns platform-specific path by default',
|
|
82
|
+
() => {
|
|
83
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
84
|
+
process.env.ENVIRONMENT = undefined;
|
|
85
|
+
|
|
86
|
+
const path = getDataDir();
|
|
87
|
+
|
|
88
|
+
if (process.platform === 'darwin') {
|
|
89
|
+
expect(path).toMatch(/Library\/Application Support\/celilo$/);
|
|
90
|
+
} else {
|
|
91
|
+
expect(path).toBe('/var/lib/celilo');
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
describe('getMasterKeyPath', () => {
|
|
@@ -103,19 +110,22 @@ describe('paths configuration', () => {
|
|
|
103
110
|
expect(path).toBe('/custom/data/master.key');
|
|
104
111
|
});
|
|
105
112
|
|
|
106
|
-
it(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
114
|
+
'uses platform-specific data dir by default',
|
|
115
|
+
() => {
|
|
116
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
117
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
118
|
+
process.env.ENVIRONMENT = undefined;
|
|
119
|
+
|
|
120
|
+
const path = getMasterKeyPath();
|
|
121
|
+
|
|
122
|
+
if (process.platform === 'darwin') {
|
|
123
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/master\.key$/);
|
|
124
|
+
} else {
|
|
125
|
+
expect(path).toBe('/var/lib/celilo/master.key');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
);
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
describe('getDbPath', () => {
|
|
@@ -128,19 +138,22 @@ describe('paths configuration', () => {
|
|
|
128
138
|
expect(path).toBe('/custom/path/celilo.db');
|
|
129
139
|
});
|
|
130
140
|
|
|
131
|
-
it(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
142
|
+
'returns data dir + celilo.db on macOS in production',
|
|
143
|
+
() => {
|
|
144
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
145
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
146
|
+
process.env.ENVIRONMENT = undefined;
|
|
147
|
+
|
|
148
|
+
const path = getDbPath();
|
|
149
|
+
|
|
150
|
+
if (process.platform === 'darwin') {
|
|
151
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/celilo.db$/);
|
|
152
|
+
} else {
|
|
153
|
+
expect(path).toBe('/var/lib/celilo/celilo.db');
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
);
|
|
144
157
|
|
|
145
158
|
it('returns celilo-data/celilo.db in development mode', () => {
|
|
146
159
|
process.env.CELILO_DB_PATH = undefined;
|
package/src/db/client.ts
CHANGED
|
@@ -41,145 +41,6 @@ export function findMigrationsFolder(): string {
|
|
|
41
41
|
throw new Error(`Could not find drizzle migrations folder. Tried: ${candidates.join(', ')}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* Check if database needs initialization (tables don't exist)
|
|
46
|
-
*/
|
|
47
|
-
function needsMigration(sqlite: Database): boolean {
|
|
48
|
-
try {
|
|
49
|
-
// Check if the modules table exists at all (new database)
|
|
50
|
-
const result = sqlite
|
|
51
|
-
.query("SELECT name FROM sqlite_master WHERE type='table' AND name='modules'")
|
|
52
|
-
.get();
|
|
53
|
-
if (!result) return true; // New database — run full migrations
|
|
54
|
-
|
|
55
|
-
// Existing database — apply incremental schema updates
|
|
56
|
-
// Each statement is wrapped in try/catch (no-op if already applied)
|
|
57
|
-
const alterStatements = [
|
|
58
|
-
'ALTER TABLE capabilities ADD zones text',
|
|
59
|
-
'ALTER TABLE machines ADD earmarked_module text',
|
|
60
|
-
// web_routes' subdomain/custom_domain columns were folded into a
|
|
61
|
-
// single `hostname` field by migration 0004. Don't re-add them
|
|
62
|
-
// here — the migration drops and recreates the table.
|
|
63
|
-
// Backup system tables (Phase 1)
|
|
64
|
-
`CREATE TABLE IF NOT EXISTS backup_storages (
|
|
65
|
-
id text PRIMARY KEY NOT NULL,
|
|
66
|
-
storage_id text NOT NULL UNIQUE,
|
|
67
|
-
name text NOT NULL,
|
|
68
|
-
provider_name text NOT NULL,
|
|
69
|
-
credentials_encrypted text NOT NULL,
|
|
70
|
-
provider_config text NOT NULL,
|
|
71
|
-
verified integer DEFAULT 0 NOT NULL,
|
|
72
|
-
verified_at integer,
|
|
73
|
-
verification_error text,
|
|
74
|
-
is_default integer DEFAULT 0 NOT NULL,
|
|
75
|
-
created_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
76
|
-
updated_at integer DEFAULT (unixepoch()) NOT NULL
|
|
77
|
-
)`,
|
|
78
|
-
`CREATE TABLE IF NOT EXISTS backups (
|
|
79
|
-
id text PRIMARY KEY NOT NULL,
|
|
80
|
-
module_id text REFERENCES modules(id) ON DELETE SET NULL,
|
|
81
|
-
storage_id text NOT NULL REFERENCES backup_storages(id),
|
|
82
|
-
storage_path text NOT NULL,
|
|
83
|
-
backup_type text NOT NULL,
|
|
84
|
-
module_version text,
|
|
85
|
-
schema_version text,
|
|
86
|
-
size_bytes integer,
|
|
87
|
-
metadata text DEFAULT '{}' NOT NULL,
|
|
88
|
-
status text DEFAULT 'in_progress' NOT NULL,
|
|
89
|
-
error_message text,
|
|
90
|
-
started_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
91
|
-
completed_at integer
|
|
92
|
-
)`,
|
|
93
|
-
// Backup naming support
|
|
94
|
-
'ALTER TABLE backups ADD name text',
|
|
95
|
-
// Module systems (v2/MODULE_SYSTEMS_ADDRESSING.md) — a module's 0..N
|
|
96
|
-
// deployed hosts. Fresh DBs get this via migration 0007; existing installs
|
|
97
|
-
// get it here. Replaces the scalar target_ip/vmid rows in module_configs.
|
|
98
|
-
`CREATE TABLE IF NOT EXISTS module_systems (
|
|
99
|
-
module_id text NOT NULL REFERENCES modules(id) ON DELETE cascade,
|
|
100
|
-
name text NOT NULL,
|
|
101
|
-
hostname text NOT NULL,
|
|
102
|
-
ipv4_address text NOT NULL,
|
|
103
|
-
zone text NOT NULL,
|
|
104
|
-
infra_type text NOT NULL,
|
|
105
|
-
machine_id text REFERENCES machines(id),
|
|
106
|
-
service_id text REFERENCES container_services(id),
|
|
107
|
-
vmid integer,
|
|
108
|
-
created_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
109
|
-
updated_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
110
|
-
PRIMARY KEY (module_id, name)
|
|
111
|
-
)`,
|
|
112
|
-
// Aspect consent decision (ISS-0027). Fresh DBs get this via migration
|
|
113
|
-
// 0008; existing installs get it here. Defaults to true so pre-existing
|
|
114
|
-
// rows (all approvals) keep running; false = a durable refusal.
|
|
115
|
-
'ALTER TABLE aspect_approvals ADD consented integer DEFAULT true NOT NULL',
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
for (const stmt of alterStatements) {
|
|
119
|
-
try {
|
|
120
|
-
sqlite.exec(stmt);
|
|
121
|
-
} catch {
|
|
122
|
-
// Column already exists — fine
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// web_routes hostname migration (CADDY_HOSTNAME_LIST design).
|
|
127
|
-
// Drizzle's auto-migrate path (`migrate()`) only runs for fresh
|
|
128
|
-
// databases — for existing celilo installs we apply the schema
|
|
129
|
-
// change here. Phase 0 + no production users = destructive
|
|
130
|
-
// rebuild; modules repopulate routes on their next deploy.
|
|
131
|
-
try {
|
|
132
|
-
const cols = sqlite.query("SELECT name FROM pragma_table_info('web_routes')").all() as Array<{
|
|
133
|
-
name: string;
|
|
134
|
-
}>;
|
|
135
|
-
const hasHostname = cols.some((c) => c.name === 'hostname');
|
|
136
|
-
const hasOldColumns = cols.some((c) => c.name === 'subdomain' || c.name === 'custom_domain');
|
|
137
|
-
if (!hasHostname && cols.length > 0) {
|
|
138
|
-
// Old shape detected (or missing hostname) — drop and recreate.
|
|
139
|
-
// Wrap in a transaction so the table is never half-migrated.
|
|
140
|
-
sqlite.exec('BEGIN');
|
|
141
|
-
try {
|
|
142
|
-
sqlite.exec('DROP TABLE web_routes');
|
|
143
|
-
sqlite.exec(`CREATE TABLE web_routes (
|
|
144
|
-
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
145
|
-
slug text NOT NULL,
|
|
146
|
-
module_id text NOT NULL,
|
|
147
|
-
type text NOT NULL,
|
|
148
|
-
path text NOT NULL,
|
|
149
|
-
hostname text NOT NULL,
|
|
150
|
-
target_host text,
|
|
151
|
-
target_port integer,
|
|
152
|
-
websocket integer DEFAULT false NOT NULL,
|
|
153
|
-
content_hash text,
|
|
154
|
-
created_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
155
|
-
updated_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
156
|
-
FOREIGN KEY (module_id) REFERENCES modules(id) ON UPDATE no action ON DELETE cascade
|
|
157
|
-
)`);
|
|
158
|
-
sqlite.exec(
|
|
159
|
-
'CREATE UNIQUE INDEX web_routes_hostname_path_idx ON web_routes (hostname, path)',
|
|
160
|
-
);
|
|
161
|
-
sqlite.exec('COMMIT');
|
|
162
|
-
if (hasOldColumns) {
|
|
163
|
-
console.log(
|
|
164
|
-
'web_routes migrated to hostname-based schema. Modules will repopulate their routes on next deploy.',
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
sqlite.exec('ROLLBACK');
|
|
169
|
-
throw err;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
} catch (err) {
|
|
173
|
-
console.error('Failed to migrate web_routes schema:', err);
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return false; // Schema is up to date
|
|
178
|
-
} catch {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
44
|
/**
|
|
184
45
|
* Create database client and run migrations if needed
|
|
185
46
|
*/
|
|
@@ -209,12 +70,21 @@ export function createDbClient(config?: Partial<DatabaseConfig>) {
|
|
|
209
70
|
|
|
210
71
|
const db = drizzle(sqlite, { schema });
|
|
211
72
|
|
|
212
|
-
//
|
|
213
|
-
|
|
73
|
+
// Apply migrations on open (ISS-0100). Drizzle's migrator is the single
|
|
74
|
+
// migration mechanism for ALL DBs — fresh and existing — and the only place
|
|
75
|
+
// schema changes live (the old imperative hand-list is gone). migrate() is
|
|
76
|
+
// idempotent: it applies every migration newer than the latest recorded in
|
|
77
|
+
// `__drizzle_migrations` and no-ops once current.
|
|
78
|
+
//
|
|
79
|
+
// One-time caveat (ISS-0100): an existing DB from the hand-list era has a
|
|
80
|
+
// frozen `__drizzle_migrations` watermark; it must be remediated by hand
|
|
81
|
+
// (stamp the watermark to the latest migration + create any missing table)
|
|
82
|
+
// BEFORE this code opens it, or migrate() re-runs already-applied migrations
|
|
83
|
+
// and throws. `celilo system doctor` (checkSchemaDrift) detects the drift.
|
|
84
|
+
if (!readonly) {
|
|
214
85
|
try {
|
|
215
86
|
const migrationsFolder = findMigrationsFolder();
|
|
216
87
|
migrate(db, { migrationsFolder });
|
|
217
|
-
console.log('Database initialized with migrations');
|
|
218
88
|
} catch (error) {
|
|
219
89
|
console.error('Failed to run migrations:', error);
|
|
220
90
|
throw error;
|
|
@@ -222,10 +92,9 @@ export function createDbClient(config?: Partial<DatabaseConfig>) {
|
|
|
222
92
|
}
|
|
223
93
|
|
|
224
94
|
// One-time upgrade backfill for the target_ip → module_systems refactor
|
|
225
|
-
// (v2/MODULE_SYSTEMS_ADDRESSING.md).
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// created before the refactor has its host data only in module_configs /
|
|
95
|
+
// (v2/MODULE_SYSTEMS_ADDRESSING.md). migrate() above has ensured the
|
|
96
|
+
// module_systems table exists (migration 0007). A deployment created before
|
|
97
|
+
// the refactor has its host data only in module_configs /
|
|
229
98
|
// ip_allocations / module_infrastructure and an EMPTY module_systems, so its
|
|
230
99
|
// modules resolve to no system and the migrated hooks throw "No deployed
|
|
231
100
|
// system found". This lifts that state across. Idempotent (skips modules
|
package/src/db/migrate.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
|
2
|
-
import { closeDb, createDbClient, findMigrationsFolder } from './client';
|
|
2
|
+
import { type DbClient, closeDb, createDbClient, findMigrationsFolder } from './client';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Apply pending drizzle migrations to an open DB. Idempotent — drizzle applies
|
|
6
|
+
* only migrations newer than the latest recorded in `__drizzle_migrations`.
|
|
7
|
+
* The single migration mechanism (ISS-0100); createDbClient also calls this
|
|
8
|
+
* shape on open (auto-migrate).
|
|
9
|
+
*/
|
|
10
|
+
export function runMigrationsOn(db: DbClient): void {
|
|
11
|
+
migrate(db, { migrationsFolder: findMigrationsFolder() });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run database migrations (standalone entrypoint — `bun run src/db/migrate.ts`).
|
|
16
|
+
* createDbClient already migrates on open; this re-asserts for explicit use.
|
|
6
17
|
*/
|
|
7
18
|
export async function runMigrations(dbPath?: string) {
|
|
8
19
|
console.log('Running database migrations...');
|
|
9
|
-
|
|
10
20
|
const db = createDbClient(dbPath ? { path: dbPath } : undefined);
|
|
11
|
-
|
|
12
21
|
try {
|
|
13
|
-
|
|
14
|
-
await migrate(db, { migrationsFolder });
|
|
22
|
+
runMigrationsOn(db);
|
|
15
23
|
console.log('Migrations completed successfully');
|
|
16
24
|
} catch (error) {
|
|
17
25
|
console.error('Migration failed:', error);
|