@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.
Files changed (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Backup List Command
3
+ * Lists available backups with metadata.
4
+ */
5
+
6
+ import { formatSize, getBackup, listBackups } from '../../services/backup-metadata';
7
+ import { getBackupStorage } from '../../services/backup-storage';
8
+ import { celiloIntro } from '../prompts';
9
+ import type { CommandResult } from '../types';
10
+
11
+ /**
12
+ * Format a timestamp as a human-friendly relative string.
13
+ * Uses relative terms for recent backups and dates for older ones.
14
+ */
15
+ function formatRelativeDate(date: Date): string {
16
+ const now = Date.now();
17
+ const diffMs = now - date.getTime();
18
+ const diffMins = Math.floor(diffMs / 60_000);
19
+ const diffHours = Math.floor(diffMs / 3_600_000);
20
+ const diffDays = Math.floor(diffMs / 86_400_000);
21
+
22
+ if (diffMins < 1) return 'just now';
23
+ if (diffMins < 60) return `${diffMins}m ago`;
24
+ if (diffHours < 24) return `${diffHours}h ago`;
25
+ if (diffDays === 1) return 'yesterday';
26
+ if (diffDays < 7) return `${diffDays} days ago`;
27
+ if (diffDays < 14) return 'last week';
28
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
29
+ if (diffDays < 60) return 'last month';
30
+ return `${Math.floor(diffDays / 30)} months ago`;
31
+ }
32
+
33
+ /**
34
+ * Show detailed info for a single backup
35
+ */
36
+ function showBackupDetail(backupId: string): CommandResult {
37
+ const backup = getBackup(backupId);
38
+ if (!backup) {
39
+ return { success: false, error: `Backup not found: ${backupId}` };
40
+ }
41
+
42
+ const storage = getBackupStorage(backup.storageId);
43
+ const isSystem = backup.backupType === 'system_state';
44
+
45
+ console.log('');
46
+ console.log(` ID: ${backup.id}`);
47
+ console.log(` Short ID: ${backup.id.substring(0, 8)}`);
48
+ if (backup.name) {
49
+ console.log(` Name: ${backup.name}`);
50
+ }
51
+ console.log(` Type: ${isSystem ? 'System State' : 'Module Data'}`);
52
+ if (!isSystem) {
53
+ console.log(` Module: ${backup.moduleId ?? 'unknown'}`);
54
+ }
55
+ if (backup.moduleVersion) {
56
+ console.log(` Module Version: ${backup.moduleVersion}`);
57
+ }
58
+ if (backup.schemaVersion) {
59
+ console.log(` Schema Version: ${backup.schemaVersion}`);
60
+ }
61
+ console.log(` Status: ${backup.status}`);
62
+ console.log(` Size: ${formatSize(backup.sizeBytes)}`);
63
+ console.log(
64
+ ` Date: ${new Date(backup.startedAt).toISOString().replace('T', ' ').substring(0, 19)} UTC`,
65
+ );
66
+ console.log(` Storage: ${storage?.storageId ?? 'unknown'} (${storage?.name ?? '?'})`);
67
+ console.log(` Path: ${backup.storagePath}`);
68
+ if (backup.errorMessage) {
69
+ console.log(` Error: ${backup.errorMessage}`);
70
+ }
71
+ console.log('');
72
+
73
+ return { success: true, message: `Backup detail: ${backupId}` };
74
+ }
75
+
76
+ export async function handleBackupList(
77
+ args: string[],
78
+ flags: Record<string, boolean | string> = {},
79
+ ): Promise<CommandResult> {
80
+ try {
81
+ const moduleIdOrBackupId = args[0];
82
+ const limit = typeof flags.limit === 'string' ? Number.parseInt(flags.limit, 10) : 20;
83
+
84
+ // If the argument looks like a backup ID (hex chars), show detail view
85
+ if (moduleIdOrBackupId && /^[0-9a-f]{8,}$/i.test(moduleIdOrBackupId)) {
86
+ const backup = getBackup(moduleIdOrBackupId);
87
+ if (backup) {
88
+ celiloIntro('Backup Detail');
89
+ return showBackupDetail(moduleIdOrBackupId);
90
+ }
91
+ // Fall through to module filter if not a backup ID
92
+ }
93
+
94
+ celiloIntro('Available Backups');
95
+
96
+ const moduleId = moduleIdOrBackupId;
97
+ const backupList = listBackups({ moduleId, limit });
98
+
99
+ if (backupList.length === 0) {
100
+ console.log('No backups found.\n');
101
+ console.log('Create a backup:');
102
+ console.log(' celilo backup create');
103
+ return { success: true, message: 'No backups found' };
104
+ }
105
+
106
+ console.log('');
107
+
108
+ for (const backup of backupList) {
109
+ const id = backup.id.substring(0, 8);
110
+ const module = backup.backupType === 'system_state' ? '[system]' : (backup.moduleId ?? '?');
111
+ const when = formatRelativeDate(new Date(backup.startedAt));
112
+ const size = formatSize(backup.sizeBytes);
113
+ const statusIcon =
114
+ backup.status === 'completed' ? '✓' : backup.status === 'failed' ? '✗' : '…';
115
+
116
+ const nameTag = backup.name ? ` "${backup.name}"` : '';
117
+ console.log(
118
+ ` ${statusIcon} ${id} ${module.padEnd(12)} ${size.padEnd(10)} ${when}${nameTag}`,
119
+ );
120
+ }
121
+
122
+ console.log(`\n${backupList.length} backup${backupList.length === 1 ? '' : 's'} shown.`);
123
+ console.log('Run "celilo backup list <id>" for details.\n');
124
+
125
+ return { success: true, message: `Found ${backupList.length} backup(s)` };
126
+ } catch (error) {
127
+ return {
128
+ success: false,
129
+ error: `Failed to list backups: ${error instanceof Error ? error.message : String(error)}`,
130
+ };
131
+ }
132
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Backup Name Command
3
+ * Set or view a human-readable name/annotation on a backup.
4
+ *
5
+ * Usage:
6
+ * celilo backup name <backup-id> <name...> Set a name
7
+ * celilo backup name <backup-id> Show current name
8
+ * celilo backup name <backup-id> --clear Remove the name
9
+ */
10
+
11
+ import { formatSize, getBackup, updateBackupName } from '../../services/backup-metadata';
12
+ import { celiloIntro, celiloOutro } from '../prompts';
13
+ import type { CommandResult } from '../types';
14
+
15
+ export async function handleBackupName(
16
+ args: string[],
17
+ flags: Record<string, boolean | string> = {},
18
+ ): Promise<CommandResult> {
19
+ try {
20
+ const backupId = args[0];
21
+
22
+ if (!backupId) {
23
+ return {
24
+ success: false,
25
+ error:
26
+ 'Backup ID is required\n\nUsage:\n celilo backup name <backup-id> <name...>\n celilo backup name <backup-id> --clear',
27
+ };
28
+ }
29
+
30
+ const backup = getBackup(backupId);
31
+ if (!backup) {
32
+ return { success: false, error: `Backup not found: ${backupId}` };
33
+ }
34
+
35
+ const shortId = backup.id.substring(0, 8);
36
+
37
+ // Clear the name
38
+ if (flags.clear) {
39
+ celiloIntro('Clear Backup Name');
40
+ updateBackupName(backup.id, null);
41
+ celiloOutro(`Name cleared for backup ${shortId}.`);
42
+ return { success: true, message: `Cleared name for ${shortId}` };
43
+ }
44
+
45
+ // Remaining args after the backup ID form the name
46
+ const nameParts = args.slice(1);
47
+
48
+ // No name provided — show current name
49
+ if (nameParts.length === 0) {
50
+ celiloIntro('Backup Name');
51
+ const module =
52
+ backup.backupType === 'system_state' ? '[system]' : (backup.moduleId ?? 'unknown');
53
+ console.log('');
54
+ console.log(` Backup: ${shortId} (${module}, ${formatSize(backup.sizeBytes)})`);
55
+ console.log(` Name: ${backup.name ?? '(none)'}`);
56
+ console.log('');
57
+ return { success: true, message: backup.name ?? '(none)' };
58
+ }
59
+
60
+ // Set the name (join all remaining args — supports both quoted and unquoted)
61
+ const name = nameParts.join(' ');
62
+ celiloIntro('Set Backup Name');
63
+ updateBackupName(backup.id, name);
64
+ celiloOutro(`Backup ${shortId} named: "${name}"`);
65
+
66
+ return { success: true, message: `Named ${shortId}: ${name}` };
67
+ } catch (error) {
68
+ return {
69
+ success: false,
70
+ error: `Failed to update backup name: ${error instanceof Error ? error.message : String(error)}`,
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Backup Prune Command
3
+ * Remove old backups according to retention policies defined in module manifests.
4
+ */
5
+
6
+ import { eq } from 'drizzle-orm';
7
+ import { getDb } from '../../db/client';
8
+ import { modules } from '../../db/schema';
9
+ import type { ModuleManifest } from '../../manifest/schema';
10
+ import { findBackupEligibleModules } from '../../services/backup-create';
11
+ import { pruneBackupsForModule } from '../../services/backup-retention';
12
+ import { celiloIntro, celiloOutro } from '../prompts';
13
+ import type { CommandResult } from '../types';
14
+
15
+ export async function handleBackupPrune(
16
+ args: string[],
17
+ flags: Record<string, boolean | string> = {},
18
+ ): Promise<CommandResult> {
19
+ try {
20
+ celiloIntro('Prune Old Backups');
21
+
22
+ const specificModule = args[0];
23
+ const dryRun = Boolean(flags['dry-run']);
24
+
25
+ if (dryRun) {
26
+ console.log('(dry run — no backups will be deleted)\n');
27
+ }
28
+
29
+ let totalDeleted = 0;
30
+
31
+ if (specificModule) {
32
+ // Prune a specific module
33
+ const db = getDb();
34
+ const mod = db.select().from(modules).where(eq(modules.id, specificModule)).get();
35
+ if (!mod) {
36
+ return { success: false, error: `Module not found: ${specificModule}` };
37
+ }
38
+
39
+ const manifest = mod.manifestData as unknown as ModuleManifest;
40
+ const retention = manifest.backup?.retention;
41
+ if (!retention) {
42
+ console.log(`Module '${specificModule}' has no retention policy defined.`);
43
+ return { success: true, message: 'No retention policy' };
44
+ }
45
+
46
+ const result = await pruneBackupsForModule(
47
+ specificModule,
48
+ { count: retention.count, maxAgeDays: retention.max_age_days },
49
+ dryRun,
50
+ );
51
+
52
+ if (result.deleted > 0) {
53
+ const verb = dryRun ? 'Would delete' : 'Deleted';
54
+ console.log(`${verb} ${result.deleted} backup(s) for ${specificModule}:`);
55
+ for (const path of result.deletedPaths) {
56
+ console.log(` - ${path}`);
57
+ }
58
+ } else {
59
+ console.log(`No expired backups for ${specificModule}.`);
60
+ }
61
+ totalDeleted = result.deleted;
62
+ } else {
63
+ // Prune all modules with retention policies
64
+ const eligible = findBackupEligibleModules();
65
+
66
+ for (const { module: mod, manifest } of eligible) {
67
+ const retention = manifest.backup?.retention;
68
+ if (!retention) continue;
69
+
70
+ const result = await pruneBackupsForModule(
71
+ mod.id,
72
+ { count: retention.count, maxAgeDays: retention.max_age_days },
73
+ dryRun,
74
+ );
75
+
76
+ if (result.deleted > 0) {
77
+ const verb = dryRun ? 'Would delete' : 'Deleted';
78
+ console.log(`${verb} ${result.deleted} backup(s) for ${mod.id}`);
79
+ totalDeleted += result.deleted;
80
+ }
81
+ }
82
+
83
+ if (totalDeleted === 0) {
84
+ console.log('No expired backups found.');
85
+ }
86
+ }
87
+
88
+ const verb = dryRun ? 'would be deleted' : 'deleted';
89
+ celiloOutro(totalDeleted > 0 ? `${totalDeleted} backup(s) ${verb}.` : 'No backups to prune.');
90
+
91
+ return { success: true, message: `Pruned ${totalDeleted} backup(s)` };
92
+ } catch (error) {
93
+ return {
94
+ success: false,
95
+ error: `Failed to prune backups: ${error instanceof Error ? error.message : String(error)}`,
96
+ };
97
+ }
98
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Backup Restore Command
3
+ * Restores from a backup archive, executing the module's on_restore hook.
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { formatSize, getBackup } from '../../services/backup-metadata';
8
+ import { restoreModuleBackup, restoreSystemStateBackup } from '../../services/backup-restore';
9
+ import { getBackupStorage } from '../../services/backup-storage';
10
+ import { celiloIntro, celiloOutro } from '../prompts';
11
+ import type { CommandResult } from '../types';
12
+
13
+ export async function handleBackupRestore(
14
+ args: string[],
15
+ flags: Record<string, boolean | string> = {},
16
+ ): Promise<CommandResult> {
17
+ try {
18
+ celiloIntro('Restore Backup');
19
+
20
+ const backupId = args[0];
21
+ if (!backupId) {
22
+ return {
23
+ success: false,
24
+ error: 'Backup ID is required\n\nUsage: celilo backup restore <backup-id>',
25
+ };
26
+ }
27
+
28
+ const backup = getBackup(backupId);
29
+ if (!backup) {
30
+ return { success: false, error: `Backup not found: ${backupId}` };
31
+ }
32
+
33
+ if (backup.status !== 'completed') {
34
+ return {
35
+ success: false,
36
+ error: `Cannot restore backup with status '${backup.status}'. Only completed backups can be restored.`,
37
+ };
38
+ }
39
+
40
+ // Show backup details
41
+ const storage = getBackupStorage(backup.storageId);
42
+ const isSystem = backup.backupType === 'system_state';
43
+
44
+ console.log('\nBackup Details:');
45
+ console.log(` Type: ${isSystem ? 'System State' : 'Module Data'}`);
46
+ if (!isSystem) {
47
+ console.log(` Module: ${backup.moduleId ?? 'unknown'}`);
48
+ if (backup.moduleVersion) {
49
+ console.log(` Module Version: ${backup.moduleVersion}`);
50
+ }
51
+ if (backup.schemaVersion) {
52
+ console.log(` Schema Version: ${backup.schemaVersion}`);
53
+ }
54
+ }
55
+ console.log(
56
+ ` Date: ${new Date(backup.startedAt).toISOString().replace('T', ' ').substring(0, 19)} UTC`,
57
+ );
58
+ console.log(` Size: ${formatSize(backup.sizeBytes)}`);
59
+ console.log(` Storage: ${storage?.storageId ?? 'unknown'}`);
60
+ console.log('');
61
+
62
+ if (isSystem) {
63
+ console.log('⚠ This will replace the current Celilo database with the backup version.');
64
+ console.log(' All module configs, secrets, and state will be overwritten.\n');
65
+ }
66
+
67
+ // Confirm unless --yes
68
+ if (!flags.yes) {
69
+ const confirmed = await p.confirm({
70
+ message: 'Proceed with restore?',
71
+ initialValue: false,
72
+ });
73
+
74
+ if (p.isCancel(confirmed) || !confirmed) {
75
+ p.cancel('Restore cancelled');
76
+ return { success: false, error: 'Cancelled by user' };
77
+ }
78
+ }
79
+
80
+ if (isSystem) {
81
+ // System state restore
82
+ console.log('\n▸ Downloading and decrypting backup...');
83
+ const result = await restoreSystemStateBackup(backup);
84
+
85
+ if (!result.success) {
86
+ console.log(`✗ ${result.error}`);
87
+ return { success: false, error: result.error ?? 'Restore failed' };
88
+ }
89
+
90
+ console.log('✓ System state restored');
91
+ celiloOutro('Restore complete. The database has been replaced with the backup version.');
92
+ } else {
93
+ // Module data restore
94
+ console.log('\n▸ Downloading and decrypting backup...');
95
+ console.log('▸ Executing on_restore hook...');
96
+
97
+ const result = await restoreModuleBackup(backup, { runHealthCheck: true });
98
+
99
+ if (!result.success) {
100
+ console.log(`✗ ${result.error}`);
101
+ return { success: false, error: result.error ?? 'Restore failed' };
102
+ }
103
+
104
+ console.log('✓ on_restore completed');
105
+
106
+ if (result.healthCheckPassed === true) {
107
+ console.log('✓ Health check passed');
108
+ } else if (result.healthCheckPassed === false) {
109
+ console.log('⚠ Health check failed after restore');
110
+ }
111
+
112
+ celiloOutro('Restore complete.');
113
+ }
114
+
115
+ return { success: true, message: `Restored backup: ${backupId}` };
116
+ } catch (error) {
117
+ return {
118
+ success: false,
119
+ error: `Restore failed: ${error instanceof Error ? error.message : String(error)}`,
120
+ };
121
+ }
122
+ }
@@ -0,0 +1,121 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb } from '../../db/client';
3
+ import { capabilities, capabilitySecrets, modules } from '../../db/schema';
4
+ import { getArg, validateRequiredArgs } from '../parser';
5
+ import type { CommandResult } from '../types';
6
+
7
+ /**
8
+ * Handle capability info command
9
+ *
10
+ * Usage: celilo capability info <name>
11
+ *
12
+ * @param args - Command arguments
13
+ * @returns Command result with capability details
14
+ */
15
+ export async function handleCapabilityInfo(args: string[]): Promise<CommandResult> {
16
+ const error = validateRequiredArgs(args, 1);
17
+ if (error) {
18
+ return {
19
+ success: false,
20
+ error: `${error}\n\nUsage: celilo capability info <name>`,
21
+ };
22
+ }
23
+
24
+ const capabilityName = getArg(args, 0);
25
+
26
+ if (!capabilityName) {
27
+ return {
28
+ success: false,
29
+ error: 'Capability name is required',
30
+ };
31
+ }
32
+
33
+ const db = getDb();
34
+
35
+ const capability = db
36
+ .select({
37
+ id: capabilities.id,
38
+ capabilityName: capabilities.capabilityName,
39
+ version: capabilities.version,
40
+ data: capabilities.data,
41
+ moduleId: capabilities.moduleId,
42
+ moduleState: modules.state,
43
+ registeredAt: capabilities.registeredAt,
44
+ })
45
+ .from(capabilities)
46
+ .innerJoin(modules, eq(capabilities.moduleId, modules.id))
47
+ .where(eq(capabilities.capabilityName, capabilityName))
48
+ .get();
49
+
50
+ if (!capability) {
51
+ return {
52
+ success: false,
53
+ error: `Capability not found: ${capabilityName}`,
54
+ };
55
+ }
56
+
57
+ // Get capability secrets
58
+ const secretRows = db
59
+ .select({
60
+ name: capabilitySecrets.name,
61
+ description: capabilitySecrets.description,
62
+ hasValue: capabilitySecrets.encryptedValue,
63
+ })
64
+ .from(capabilitySecrets)
65
+ .where(eq(capabilitySecrets.capabilityId, capability.id))
66
+ .all();
67
+
68
+ const sections: string[] = [];
69
+
70
+ // Section 1: Capability metadata
71
+ const metaLines = [
72
+ `Capability: ${capability.capabilityName}`,
73
+ `Version: ${capability.version}`,
74
+ `Provider: ${capability.moduleId} (state: ${capability.moduleState})`,
75
+ `Registered: ${capability.registeredAt.toISOString()}`,
76
+ ];
77
+ sections.push(metaLines.join('\n'));
78
+
79
+ // Section 2: Capability data
80
+ if (capability.data && Object.keys(capability.data).length > 0) {
81
+ const dataLines = ['Data:'];
82
+ formatData(capability.data, dataLines, 1);
83
+ sections.push(dataLines.join('\n'));
84
+ } else {
85
+ sections.push('Data: (none)');
86
+ }
87
+
88
+ // Section 3: Secrets
89
+ if (secretRows.length > 0) {
90
+ const secretLines = ['Secrets:'];
91
+ for (const secret of secretRows) {
92
+ const status = secret.hasValue ? '[configured]' : '[not set]';
93
+ const desc = secret.description ? ` - ${secret.description}` : '';
94
+ secretLines.push(` ${secret.name}: ${status}${desc}`);
95
+ }
96
+ sections.push(secretLines.join('\n'));
97
+ }
98
+
99
+ return {
100
+ success: true,
101
+ message: sections.join('\n\n'),
102
+ data: { capability, secrets: secretRows },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Format nested data object for display
108
+ */
109
+ function formatData(data: Record<string, unknown>, lines: string[], depth: number): void {
110
+ const indent = ' '.repeat(depth);
111
+ for (const [key, value] of Object.entries(data)) {
112
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
113
+ lines.push(`${indent}${key}:`);
114
+ formatData(value as Record<string, unknown>, lines, depth + 1);
115
+ } else if (Array.isArray(value)) {
116
+ lines.push(`${indent}${key}: [${value.join(', ')}]`);
117
+ } else {
118
+ lines.push(`${indent}${key}: ${value}`);
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,47 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb } from '../../db/client';
3
+ import { capabilities, modules } from '../../db/schema';
4
+ import type { CommandResult } from '../types';
5
+
6
+ /**
7
+ * Handle capability list command
8
+ *
9
+ * Usage: celilo capability list
10
+ *
11
+ * @returns Command result with all registered capabilities
12
+ */
13
+ export async function handleCapabilityList(): Promise<CommandResult> {
14
+ const db = getDb();
15
+
16
+ const rows = db
17
+ .select({
18
+ capabilityName: capabilities.capabilityName,
19
+ version: capabilities.version,
20
+ moduleId: capabilities.moduleId,
21
+ moduleState: modules.state,
22
+ })
23
+ .from(capabilities)
24
+ .innerJoin(modules, eq(capabilities.moduleId, modules.id))
25
+ .all();
26
+
27
+ if (rows.length === 0) {
28
+ return {
29
+ success: true,
30
+ message:
31
+ 'No capabilities registered\n\nCapabilities are registered when modules with "provides.capabilities" are imported.',
32
+ };
33
+ }
34
+
35
+ const lines = ['Registered capabilities:', ''];
36
+ for (const row of rows) {
37
+ lines.push(
38
+ `${row.capabilityName} v${row.version} (provided by: ${row.moduleId}, state: ${row.moduleState})`,
39
+ );
40
+ }
41
+
42
+ return {
43
+ success: true,
44
+ message: lines.join('\n'),
45
+ data: rows,
46
+ };
47
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Completion Command
3
+ * Generate shell completion scripts for bash/zsh
4
+ */
5
+
6
+ import { COMMANDS } from '../command-registry';
7
+ import { generateBashCompletion } from '../completion';
8
+ import { generateRichZshCompletion } from '../generate-zsh-completion';
9
+ import { celiloIntro } from '../prompts';
10
+ import type { CommandResult } from '../types';
11
+
12
+ /**
13
+ * Handle completion command
14
+ *
15
+ * @param args - Command arguments: [shell]
16
+ * @param flags - Command flags
17
+ */
18
+ export async function handleCompletion(
19
+ args: string[],
20
+ _flags: Record<string, boolean | string> = {},
21
+ ): Promise<CommandResult> {
22
+ try {
23
+ const shell = args[0];
24
+
25
+ if (!shell) {
26
+ celiloIntro('Shell Completion');
27
+
28
+ return {
29
+ success: false,
30
+ error: `Shell argument required.
31
+
32
+ Usage:
33
+ celilo completion bash Generate bash completion script
34
+ celilo completion zsh Generate zsh completion script
35
+
36
+ Install zsh completions:
37
+ celilo completion zsh > ~/.zsh/completions/_celilo
38
+ exec zsh
39
+ `,
40
+ };
41
+ }
42
+
43
+ if (shell === 'bash') {
44
+ const script = generateBashCompletion();
45
+ return {
46
+ success: true,
47
+ message: script,
48
+ };
49
+ }
50
+
51
+ if (shell === 'zsh') {
52
+ const script = generateRichZshCompletion(COMMANDS);
53
+ return {
54
+ success: true,
55
+ message: script,
56
+ };
57
+ }
58
+
59
+ celiloIntro('Shell Completion');
60
+
61
+ return {
62
+ success: false,
63
+ error: `Unknown shell: ${shell}
64
+
65
+ Supported shells:
66
+ bash Generate bash completion script
67
+ zsh Generate zsh completion script
68
+
69
+ Usage:
70
+ # Bash
71
+ celilo completion bash > /etc/bash_completion.d/celilo
72
+ # OR for user install:
73
+ celilo completion bash >> ~/.bashrc
74
+
75
+ # Zsh
76
+ celilo completion zsh > ~/.zsh/completions/_celilo
77
+ # OR for user install:
78
+ celilo completion zsh > /usr/local/share/zsh/site-functions/_celilo
79
+ `,
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ success: false,
84
+ error: `Failed to generate completion script: ${error instanceof Error ? error.message : String(error)}`,
85
+ };
86
+ }
87
+ }