@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,189 @@
1
+ /**
2
+ * Module types command
3
+ *
4
+ * Generates TypeScript `<module>/celilo/types.d.ts` files from a
5
+ * module's `variables.owns` / `variables.imports` declarations in
6
+ * manifest.yml. The generated file exposes a `<ModuleName>Config`
7
+ * interface that hook scripts import via `defineHook<ConfigT, ...>`.
8
+ *
9
+ * See `design/TECHNICAL_DESIGN_HOOK_API_V2.md` D2 for the design rationale.
10
+ *
11
+ * Subcommands:
12
+ * celilo module types generate <module-dir> — write types.d.ts
13
+ * celilo module types check <module-dir> — CI drift check
14
+ */
15
+
16
+ import { existsSync } from 'node:fs';
17
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import type { ModuleManifest } from '../../manifest/schema';
20
+ import { validateManifest } from '../../manifest/validate';
21
+ import { generateModuleTypes } from '../../services/module-types-generator';
22
+ import { getArg, validateRequiredArgs } from '../parser';
23
+ import type { CommandResult } from '../types';
24
+
25
+ const TYPES_FILE_RELATIVE_PATH = join('celilo', 'types.d.ts');
26
+
27
+ type LoadManifestResult =
28
+ | { ok: true; manifest: ModuleManifest }
29
+ | { ok: false; failure: CommandResult };
30
+
31
+ /**
32
+ * Load and validate a module's manifest.yml from a directory path.
33
+ *
34
+ * Returns a discriminated result: either the parsed manifest on success,
35
+ * or a CommandResult describing the failure that callers can return
36
+ * directly.
37
+ */
38
+ async function loadManifest(modulePath: string): Promise<LoadManifestResult> {
39
+ const absolute = resolve(modulePath);
40
+ if (!existsSync(absolute)) {
41
+ return {
42
+ ok: false,
43
+ failure: {
44
+ success: false,
45
+ error: `Module directory does not exist: ${modulePath}`,
46
+ },
47
+ };
48
+ }
49
+
50
+ const manifestPath = join(absolute, 'manifest.yml');
51
+ if (!existsSync(manifestPath)) {
52
+ return {
53
+ ok: false,
54
+ failure: {
55
+ success: false,
56
+ error: `No manifest.yml found in ${modulePath}`,
57
+ },
58
+ };
59
+ }
60
+
61
+ const yamlContent = await readFile(manifestPath, 'utf-8');
62
+ const result = validateManifest(yamlContent);
63
+ if (!result.success) {
64
+ const errorMessages = result.errors.map((e) => ` ${e.path}: ${e.message}`).join('\n');
65
+ return {
66
+ ok: false,
67
+ failure: {
68
+ success: false,
69
+ error: `Manifest validation failed:\n${errorMessages}`,
70
+ },
71
+ };
72
+ }
73
+
74
+ return { ok: true, manifest: result.data };
75
+ }
76
+
77
+ /**
78
+ * Handle `celilo module types generate <module-dir>`.
79
+ *
80
+ * Reads the manifest, generates the types file content, writes it to
81
+ * `<module-dir>/celilo/types.d.ts`, creating the `celilo/`
82
+ * subdirectory if needed.
83
+ */
84
+ export async function handleModuleTypesGenerate(args: string[]): Promise<CommandResult> {
85
+ const err = validateRequiredArgs(args, 1);
86
+ if (err) {
87
+ return {
88
+ success: false,
89
+ error: `${err}\n\nUsage: celilo module types generate <module-dir>`,
90
+ };
91
+ }
92
+
93
+ const modulePath = getArg(args, 0);
94
+ if (!modulePath) {
95
+ return {
96
+ success: false,
97
+ error: 'Module directory is required\n\nUsage: celilo module types generate <module-dir>',
98
+ };
99
+ }
100
+
101
+ const loaded = await loadManifest(modulePath);
102
+ if (!loaded.ok) return loaded.failure;
103
+
104
+ const content = generateModuleTypes(loaded.manifest);
105
+ const outputPath = resolve(modulePath, TYPES_FILE_RELATIVE_PATH);
106
+
107
+ await mkdir(dirname(outputPath), { recursive: true });
108
+ await writeFile(outputPath, content, 'utf-8');
109
+
110
+ return {
111
+ success: true,
112
+ message: `Generated ${outputPath}`,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Handle `celilo module types check <module-dir>`.
118
+ *
119
+ * Regenerates the expected types file content in-memory and compares it
120
+ * byte-for-byte to the committed file. Fails if they differ or if the
121
+ * committed file is missing. Used as a CI drift check.
122
+ */
123
+ export async function handleModuleTypesCheck(args: string[]): Promise<CommandResult> {
124
+ const err = validateRequiredArgs(args, 1);
125
+ if (err) {
126
+ return {
127
+ success: false,
128
+ error: `${err}\n\nUsage: celilo module types check <module-dir>`,
129
+ };
130
+ }
131
+
132
+ const modulePath = getArg(args, 0);
133
+ if (!modulePath) {
134
+ return {
135
+ success: false,
136
+ error: 'Module directory is required\n\nUsage: celilo module types check <module-dir>',
137
+ };
138
+ }
139
+
140
+ const loaded = await loadManifest(modulePath);
141
+ if (!loaded.ok) return loaded.failure;
142
+
143
+ const expected = generateModuleTypes(loaded.manifest);
144
+ const outputPath = resolve(modulePath, TYPES_FILE_RELATIVE_PATH);
145
+
146
+ if (!existsSync(outputPath)) {
147
+ return {
148
+ success: false,
149
+ error: `Types file missing: ${outputPath}\n\nRun 'celilo module types generate ${modulePath}' to create it.`,
150
+ };
151
+ }
152
+
153
+ const actual = await readFile(outputPath, 'utf-8');
154
+ if (actual !== expected) {
155
+ return {
156
+ success: false,
157
+ error: `Types file is stale: ${outputPath}\n\nIt does not match what would be generated from the current manifest.yml.\nRun 'celilo module types generate ${modulePath}' to regenerate.`,
158
+ };
159
+ }
160
+
161
+ return {
162
+ success: true,
163
+ message: `Types file is in sync: ${outputPath}`,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Silent generator used as the belt-and-suspenders step in
169
+ * `celilo module import`. Writes types to the imported module's
170
+ * directory and swallows errors with a warning — failing to regenerate
171
+ * types should not abort an import.
172
+ *
173
+ * Returns a string describing what was done (for logging), or `null` if
174
+ * the operation was skipped or failed silently.
175
+ */
176
+ export async function generateTypesForImportedModule(modulePath: string): Promise<string | null> {
177
+ try {
178
+ const loaded = await loadManifest(modulePath);
179
+ if (!loaded.ok) return null;
180
+
181
+ const content = generateModuleTypes(loaded.manifest);
182
+ const outputPath = resolve(modulePath, TYPES_FILE_RELATIVE_PATH);
183
+ await mkdir(dirname(outputPath), { recursive: true });
184
+ await writeFile(outputPath, content, 'utf-8');
185
+ return outputPath;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Module update command
3
+ *
4
+ * Updates module code (manifest, scripts, templates) while preserving state
5
+ * (configs, secrets, infrastructure, capabilities).
6
+ *
7
+ * Usage: celilo module update <path>
8
+ *
9
+ * The module ID is read from the manifest at the given path.
10
+ */
11
+
12
+ import { cpSync, existsSync, readFileSync, readdirSync } from 'node:fs';
13
+ import { join, resolve } from 'node:path';
14
+ import { eq } from 'drizzle-orm';
15
+ import { parse as parseYaml } from 'yaml';
16
+ import { registerModuleCapabilities } from '../../capabilities/registration';
17
+ import { getDb } from '../../db/client';
18
+ import { capabilities, modules } from '../../db/schema';
19
+ import { ModuleManifestSchema } from '../../manifest/schema';
20
+ import type { ModuleManifest } from '../../manifest/schema';
21
+ import { cleanupTempDir, extractPackage } from '../../module/packaging/extract';
22
+ import { log } from '../prompts';
23
+ import type { CommandResult } from '../types';
24
+
25
+ /**
26
+ * Upgrade a single module from a source path
27
+ */
28
+ async function upgradeOne(
29
+ sourcePath: string,
30
+ db: ReturnType<typeof getDb>,
31
+ flags: Record<string, string | boolean> = {},
32
+ ): Promise<{ moduleId: string; success: boolean; error?: string }> {
33
+ const originalCwd = process.env.CELILO_ORIGINAL_CWD || process.cwd();
34
+ const importPath = resolve(originalCwd, sourcePath);
35
+ if (!existsSync(importPath)) {
36
+ return { moduleId: sourcePath, success: false, error: `Source path not found: ${importPath}` };
37
+ }
38
+
39
+ // Handle .netapp packages: extract to temp dir
40
+ let actualPath = importPath;
41
+ let tempDir: string | null = null;
42
+
43
+ if (importPath.endsWith('.netapp')) {
44
+ const extractResult = await extractPackage(importPath);
45
+ if (!extractResult.success || !extractResult.tempDir) {
46
+ return {
47
+ moduleId: sourcePath,
48
+ success: false,
49
+ error: extractResult.error || 'Failed to extract package',
50
+ };
51
+ }
52
+ tempDir = extractResult.tempDir;
53
+ actualPath = tempDir;
54
+
55
+ // Skip signature verification if --skip-verify
56
+ if (flags['skip-verify'] !== true) {
57
+ const { verifyPackageIntegrity } = await import('../../module/packaging/extract');
58
+ const verifyResult = await verifyPackageIntegrity(tempDir);
59
+ if (!verifyResult.success) {
60
+ await cleanupTempDir(tempDir);
61
+ return {
62
+ moduleId: sourcePath,
63
+ success: false,
64
+ error: verifyResult.error || 'Package verification failed',
65
+ };
66
+ }
67
+ } else {
68
+ log.warn('Skipping package signature verification (--skip-verify)');
69
+ }
70
+ }
71
+
72
+ const manifestPath = join(actualPath, 'manifest.yml');
73
+ if (!existsSync(manifestPath)) {
74
+ if (tempDir) await cleanupTempDir(tempDir);
75
+ return {
76
+ moduleId: sourcePath,
77
+ success: false,
78
+ error: `No manifest.yml found at ${actualPath}`,
79
+ };
80
+ }
81
+
82
+ let newManifest: ModuleManifest;
83
+ try {
84
+ const raw = readFileSync(manifestPath, 'utf-8');
85
+ const parsed = parseYaml(raw);
86
+ newManifest = ModuleManifestSchema.parse(parsed);
87
+ } catch (err) {
88
+ if (tempDir) await cleanupTempDir(tempDir);
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ return { moduleId: sourcePath, success: false, error: `Invalid manifest: ${msg}` };
91
+ }
92
+
93
+ const moduleId = newManifest.id;
94
+
95
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
96
+ if (!module) {
97
+ return {
98
+ moduleId,
99
+ success: false,
100
+ error: `Module '${moduleId}' is not installed. Use 'celilo module import ${sourcePath}' first.`,
101
+ };
102
+ }
103
+
104
+ const oldManifest = module.manifestData as ModuleManifest;
105
+ log.info(`Upgrading ${moduleId}: v${oldManifest.version} → v${newManifest.version}`);
106
+
107
+ // Copy new module files, preserving generated output and state
108
+ const installedPath = module.sourcePath;
109
+ const preserveDirs = new Set(['generated', 'screenshots', 'cookies.json']);
110
+
111
+ const skipDirs = new Set(['.git', 'node_modules', '.next', '.cache']);
112
+ const entries = readdirSync(actualPath);
113
+ for (const entry of entries) {
114
+ if (preserveDirs.has(entry) || skipDirs.has(entry)) continue;
115
+ const src = join(actualPath, entry);
116
+ const dest = join(installedPath, entry);
117
+ cpSync(src, dest, { recursive: true, force: true });
118
+ }
119
+
120
+ // Clean up temp dir if we extracted a .netapp
121
+ if (tempDir) await cleanupTempDir(tempDir);
122
+
123
+ // Update manifest in database
124
+ db.update(modules)
125
+ .set({
126
+ manifestData: newManifest as unknown as Record<string, unknown>,
127
+ version: newManifest.version,
128
+ name: newManifest.name,
129
+ })
130
+ .where(eq(modules.id, moduleId))
131
+ .run();
132
+
133
+ // Re-register capabilities
134
+ db.delete(capabilities).where(eq(capabilities.moduleId, moduleId)).run();
135
+
136
+ if (newManifest.provides?.capabilities && newManifest.provides.capabilities.length > 0) {
137
+ const regResult = await registerModuleCapabilities(moduleId, newManifest, db.$client);
138
+ if (!regResult.success) {
139
+ log.warn(` ${moduleId}: capability re-registration warning: ${regResult.error}`);
140
+ }
141
+ }
142
+
143
+ log.success(`Upgraded ${moduleId} (v${oldManifest.version} → v${newManifest.version})`);
144
+ return { moduleId, success: true };
145
+ }
146
+
147
+ /**
148
+ * Handle module upgrade command
149
+ *
150
+ * @param args - Command arguments: [path, path, ...]
151
+ * @returns Command result
152
+ */
153
+ export async function handleModuleUpgrade(
154
+ args: string[],
155
+ flags: Record<string, string | boolean> = {},
156
+ ): Promise<CommandResult> {
157
+ if (args.length === 0) {
158
+ return {
159
+ success: false,
160
+ error: 'At least one module path is required\n\nUsage: celilo module update <path> [path...]',
161
+ };
162
+ }
163
+
164
+ const db = getDb();
165
+ const results: Array<{ moduleId: string; success: boolean; error?: string }> = [];
166
+
167
+ for (const path of args) {
168
+ const result = await upgradeOne(path, db, flags);
169
+ results.push(result);
170
+ }
171
+
172
+ const succeeded = results.filter((r) => r.success);
173
+ const failed = results.filter((r) => !r.success);
174
+
175
+ if (failed.length > 0) {
176
+ const errors = failed.map((r) => ` ${r.moduleId}: ${r.error}`).join('\n');
177
+ if (succeeded.length > 0) {
178
+ const names = succeeded.map((r) => r.moduleId).join(', ');
179
+ return {
180
+ success: false,
181
+ error: `Upgraded ${succeeded.length} module(s): ${names}\n\nFailed ${failed.length}:\n${errors}`,
182
+ };
183
+ }
184
+ return { success: false, error: `Upgrade failed:\n${errors}` };
185
+ }
186
+
187
+ const names = succeeded.map((r) => r.moduleId).join(', ');
188
+ return {
189
+ success: true,
190
+ message: `Successfully updated ${succeeded.length} module(s): ${names}`,
191
+ };
192
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Package command
3
+ *
4
+ * Creates distributable .netapp packages from module source directories
5
+ */
6
+
7
+ import { resolve } from 'node:path';
8
+ import { buildModule } from '../../module/packaging/build';
9
+ import { getArg, getFlag, validateRequiredArgs } from '../parser';
10
+ import type { CommandResult } from '../types';
11
+
12
+ /**
13
+ * Handle package command
14
+ *
15
+ * Usage:
16
+ * celilo package <source-directory> [--output <path>]
17
+ *
18
+ * @param args - Command arguments
19
+ * @param flags - Command flags
20
+ * @returns Command result
21
+ */
22
+ export async function handlePackage(
23
+ args: string[],
24
+ flags: Record<string, string | boolean>,
25
+ ): Promise<CommandResult> {
26
+ // Validate arguments
27
+ const error = validateRequiredArgs(args, 1);
28
+ if (error) {
29
+ return {
30
+ success: false,
31
+ error: `${error}\n\nUsage:\n celilo package <source-directory> [--output <path>]`,
32
+ };
33
+ }
34
+
35
+ const sourceDir = getArg(args, 0);
36
+ if (!sourceDir) {
37
+ return {
38
+ success: false,
39
+ error: 'Source directory is required',
40
+ };
41
+ }
42
+
43
+ // Resolve paths relative to the user's original cwd, not the backend directory
44
+ const originalCwd = process.env.CELILO_ORIGINAL_CWD || process.cwd();
45
+ const resolvedSourceDir = resolve(originalCwd, sourceDir);
46
+ const outputPath = getFlag(flags, 'output');
47
+ const resolvedOutputPath = outputPath ? resolve(originalCwd, outputPath as string) : undefined;
48
+
49
+ const result = await buildModule({
50
+ sourceDir: resolvedSourceDir,
51
+ outputPath: resolvedOutputPath,
52
+ });
53
+
54
+ if (!result.success) {
55
+ return {
56
+ success: false,
57
+ error: result.error || 'Package creation failed',
58
+ };
59
+ }
60
+
61
+ return {
62
+ success: true,
63
+ message: `Successfully created module package: ${result.packagePath}`,
64
+ data: {
65
+ packagePath: result.packagePath,
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Secret list command
3
+ */
4
+
5
+ import { eq } from 'drizzle-orm';
6
+ import { getDb } from '../../db/client';
7
+ import { modules, secrets } from '../../db/schema';
8
+ import { getArg, validateRequiredArgs } from '../parser';
9
+ import type { CommandResult } from '../types';
10
+
11
+ /**
12
+ * Handle secret list command
13
+ *
14
+ * Usage: celilo secret list <module-id>
15
+ *
16
+ * Lists all secrets declared in the module manifest and shows which ones are configured.
17
+ *
18
+ * @param args - Command arguments
19
+ * @returns Command result
20
+ */
21
+ export async function handleSecretList(args: string[]): Promise<CommandResult> {
22
+ // Validate arguments
23
+ const error = validateRequiredArgs(args, 1);
24
+ if (error) {
25
+ return {
26
+ success: false,
27
+ error: `${error}\n\nUsage: celilo secret list <module-id>`,
28
+ };
29
+ }
30
+
31
+ const moduleId = getArg(args, 0);
32
+ if (!moduleId) {
33
+ return {
34
+ success: false,
35
+ error: 'Module ID is required',
36
+ };
37
+ }
38
+
39
+ const db = getDb();
40
+
41
+ // Check if module exists
42
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
43
+ if (!module) {
44
+ return {
45
+ success: false,
46
+ error: `Module not found: ${moduleId}`,
47
+ };
48
+ }
49
+
50
+ // Get declared secrets from manifest
51
+ const manifest = module.manifestData as Record<string, unknown>;
52
+ const secretsSection = manifest.secrets as
53
+ | {
54
+ declares?: Array<{ name: string; type?: string; required?: boolean; description?: string }>;
55
+ }
56
+ | undefined;
57
+ const declaredSecrets = secretsSection?.declares || [];
58
+
59
+ if (declaredSecrets.length === 0) {
60
+ return {
61
+ success: true,
62
+ message: `Module '${moduleId}' declares no secrets`,
63
+ };
64
+ }
65
+
66
+ // Get configured secrets from database
67
+ const configuredSecrets = db
68
+ .select({ name: secrets.name })
69
+ .from(secrets)
70
+ .where(eq(secrets.moduleId, moduleId))
71
+ .all();
72
+
73
+ const configuredNames = new Set(configuredSecrets.map((s) => s.name));
74
+
75
+ // Build output
76
+ const lines: string[] = [`\nSecrets for module '${moduleId}':\n`];
77
+
78
+ for (const secret of declaredSecrets) {
79
+ const isConfigured = configuredNames.has(secret.name);
80
+ const status = isConfigured ? '✓' : '✗';
81
+ const statusText = isConfigured ? 'configured' : 'not configured';
82
+ const requiredText = secret.required ? ' (required)' : '';
83
+
84
+ lines.push(` ${status} ${secret.name} - ${statusText}${requiredText}`);
85
+ if (secret.description) {
86
+ lines.push(` ${secret.description}`);
87
+ }
88
+ }
89
+
90
+ // Summary
91
+ const configuredCount = declaredSecrets.filter((s) => configuredNames.has(s.name)).length;
92
+ const totalCount = declaredSecrets.length;
93
+ lines.push(`\nConfigured: ${configuredCount}/${totalCount}`);
94
+
95
+ return {
96
+ success: true,
97
+ message: lines.join('\n'),
98
+ };
99
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Secret set command
3
+ */
4
+
5
+ import { and, eq } from 'drizzle-orm';
6
+ import { getDb } from '../../db/client';
7
+ import { modules, secrets } from '../../db/schema';
8
+ import { encryptSecret } from '../../secrets/encryption';
9
+ import { getOrCreateMasterKey } from '../../secrets/master-key';
10
+ import { getArg, validateRequiredArgs } from '../parser';
11
+ import type { CommandResult } from '../types';
12
+
13
+ /**
14
+ * Handle secret set command
15
+ *
16
+ * Usage: celilo secret set <module-id> <name> <value>
17
+ *
18
+ * @param args - Command arguments
19
+ * @returns Command result
20
+ */
21
+ export async function handleSecretSet(args: string[]): Promise<CommandResult> {
22
+ // Validate arguments
23
+ const error = validateRequiredArgs(args, 3);
24
+ if (error) {
25
+ return {
26
+ success: false,
27
+ error: `${error}\n\nUsage: celilo secret set <module-id> <name> <value>`,
28
+ };
29
+ }
30
+
31
+ const moduleId = getArg(args, 0);
32
+ const name = getArg(args, 1);
33
+ const value = getArg(args, 2);
34
+
35
+ if (!moduleId || !name || !value) {
36
+ return {
37
+ success: false,
38
+ error: 'Module ID, secret name, and value are required',
39
+ };
40
+ }
41
+
42
+ const db = getDb();
43
+
44
+ // Check if module exists
45
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
46
+ if (!module) {
47
+ return {
48
+ success: false,
49
+ error: `Module not found: ${moduleId}`,
50
+ };
51
+ }
52
+
53
+ // Validate secret name against manifest
54
+ const manifest = module.manifestData as Record<string, unknown>;
55
+ const secretsSection = manifest.secrets as
56
+ | { declares?: Array<{ name: string; type?: string; required?: boolean }> }
57
+ | undefined;
58
+ const declaredSecrets = secretsSection?.declares || [];
59
+
60
+ // Check if secret is declared in manifest
61
+ const declaredSecret = declaredSecrets.find((s) => s.name === name);
62
+ if (!declaredSecret) {
63
+ const validSecrets = declaredSecrets.map((s) => s.name).join(', ');
64
+ return {
65
+ success: false,
66
+ error: `Invalid secret '${name}' for module ${moduleId}.\n\nValid secrets: ${validSecrets || '(none declared)'}`,
67
+ };
68
+ }
69
+
70
+ // Get or create master key
71
+ let masterKey: Buffer;
72
+ try {
73
+ masterKey = await getOrCreateMasterKey();
74
+ } catch (err) {
75
+ return {
76
+ success: false,
77
+ error: 'Failed to access master key',
78
+ details: err,
79
+ };
80
+ }
81
+
82
+ // Encrypt secret
83
+ let encrypted: { encryptedValue: string; iv: string; authTag: string };
84
+ try {
85
+ encrypted = encryptSecret(value, masterKey);
86
+ } catch (err) {
87
+ return {
88
+ success: false,
89
+ error: 'Failed to encrypt secret',
90
+ details: err,
91
+ };
92
+ }
93
+
94
+ // Check if secret already exists
95
+ const existingSecret = db
96
+ .select()
97
+ .from(secrets)
98
+ .where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name)))
99
+ .get();
100
+
101
+ if (existingSecret) {
102
+ // Update existing secret
103
+ db.update(secrets)
104
+ .set({
105
+ encryptedValue: encrypted.encryptedValue,
106
+ iv: encrypted.iv,
107
+ authTag: encrypted.authTag,
108
+ updatedAt: new Date(),
109
+ })
110
+ .where(eq(secrets.id, existingSecret.id))
111
+ .run();
112
+
113
+ return {
114
+ success: true,
115
+ message: `Updated secret for ${moduleId}: ${name}`,
116
+ };
117
+ }
118
+
119
+ // Insert new secret
120
+ db.insert(secrets)
121
+ .values({
122
+ moduleId,
123
+ name,
124
+ encryptedValue: encrypted.encryptedValue,
125
+ iv: encrypted.iv,
126
+ authTag: encrypted.authTag,
127
+ })
128
+ .run();
129
+
130
+ return {
131
+ success: true,
132
+ message: `Set secret for ${moduleId}: ${name}`,
133
+ };
134
+ }