@celilo/cli 0.1.4 → 0.1.6

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 (161) hide show
  1. package/drizzle/0004_caddy_hostname_list.sql +25 -0
  2. package/drizzle/meta/_journal.json +14 -0
  3. package/package.json +9 -2
  4. package/src/ansible/inventory.test.ts +9 -8
  5. package/src/ansible/inventory.ts +9 -7
  6. package/src/capabilities/public-web-helpers.test.ts +2 -2
  7. package/src/capabilities/public-web-publish.test.ts +45 -12
  8. package/src/capabilities/registration.test.ts +6 -6
  9. package/src/capabilities/well-known.test.ts +2 -2
  10. package/src/capabilities/well-known.ts +5 -5
  11. package/src/cli/cli.test.ts +2 -2
  12. package/src/cli/command-registry.ts +146 -3
  13. package/src/cli/command-tree-parser.test.ts +1 -1
  14. package/src/cli/command-tree-parser.ts +9 -8
  15. package/src/cli/commands/hook-run.ts +15 -66
  16. package/src/cli/commands/module-audit.ts +14 -44
  17. package/src/cli/commands/module-deploy.ts +4 -1
  18. package/src/cli/commands/module-import-registry.test.ts +115 -0
  19. package/src/cli/commands/module-import.ts +106 -22
  20. package/src/cli/commands/module-publish.test.ts +235 -0
  21. package/src/cli/commands/module-publish.ts +234 -0
  22. package/src/cli/commands/module-remove.ts +82 -2
  23. package/src/cli/commands/module-search.ts +57 -0
  24. package/src/cli/commands/module-secret-get.ts +59 -0
  25. package/src/cli/commands/module-show.ts +1 -1
  26. package/src/cli/commands/module-terraform-unlock.ts +57 -0
  27. package/src/cli/commands/module-verify.test.ts +59 -0
  28. package/src/cli/commands/module-verify.ts +53 -0
  29. package/src/cli/commands/status.ts +30 -20
  30. package/src/cli/commands/system-audit.test.ts +138 -0
  31. package/src/cli/commands/system-audit.ts +571 -0
  32. package/src/cli/commands/system-update.ts +391 -0
  33. package/src/cli/completion.ts +15 -1
  34. package/src/cli/fuel-gauge.ts +68 -3
  35. package/src/cli/generate-zsh-completion.ts +13 -3
  36. package/src/cli/index.ts +112 -5
  37. package/src/cli/parser.ts +11 -0
  38. package/src/cli/prompts.ts +36 -5
  39. package/src/cli/tui/audit-state.test.ts +246 -0
  40. package/src/cli/tui/audit-state.ts +525 -0
  41. package/src/cli/tui/audit-tui.test.tsx +135 -0
  42. package/src/cli/tui/audit-tui.tsx +624 -0
  43. package/src/cli/tui/celebration.tsx +29 -0
  44. package/src/cli/tui/clipboard.test.ts +94 -0
  45. package/src/cli/tui/clipboard.ts +101 -0
  46. package/src/cli/tui/icons.ts +22 -0
  47. package/src/cli/tui/keybar.tsx +65 -0
  48. package/src/cli/tui/keymap.test.ts +105 -0
  49. package/src/cli/tui/keymap.ts +70 -0
  50. package/src/cli/tui/modals/analyzing.tsx +75 -0
  51. package/src/cli/tui/modals/celebration.tsx +44 -0
  52. package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
  53. package/src/cli/tui/modals/remediate.tsx +44 -0
  54. package/src/cli/tui/modals.test.ts +137 -0
  55. package/src/cli/tui/mouse.test.ts +78 -0
  56. package/src/cli/tui/mouse.ts +114 -0
  57. package/src/cli/tui/panes/categories.tsx +62 -0
  58. package/src/cli/tui/panes/command-log.tsx +87 -0
  59. package/src/cli/tui/panes/detail.tsx +175 -0
  60. package/src/cli/tui/panes/findings.tsx +97 -0
  61. package/src/cli/tui/panes/summary.tsx +64 -0
  62. package/src/cli/tui/spawn.ts +130 -0
  63. package/src/cli/tui/theme.ts +42 -0
  64. package/src/cli/tui/wrap.test.ts +43 -0
  65. package/src/cli/tui/wrap.ts +45 -0
  66. package/src/cli/types.ts +5 -0
  67. package/src/db/client.ts +55 -2
  68. package/src/db/schema.test.ts +3 -3
  69. package/src/db/schema.ts +26 -17
  70. package/src/hooks/capability-loader.ts +135 -72
  71. package/src/hooks/define-hook.test.ts +11 -3
  72. package/src/hooks/executor.ts +22 -1
  73. package/src/hooks/load-hook-config.test.ts +165 -0
  74. package/src/hooks/load-hook-config.ts +60 -0
  75. package/src/hooks/logger.ts +42 -12
  76. package/src/hooks/run-named-hook.ts +128 -0
  77. package/src/hooks/types.ts +19 -0
  78. package/src/manifest/ensure-schema.test.ts +115 -0
  79. package/src/manifest/schema.ts +76 -0
  80. package/src/manifest/template-validator.test.ts +1 -1
  81. package/src/manifest/template-validator.ts +1 -1
  82. package/src/manifest/validate.test.ts +1 -1
  83. package/src/module/import.ts +20 -12
  84. package/src/module/packaging/build.ts +121 -25
  85. package/src/module/packaging/release-metadata.test.ts +103 -0
  86. package/src/module/packaging/release-metadata.ts +145 -0
  87. package/src/registry/client.test.ts +228 -0
  88. package/src/registry/client.ts +157 -0
  89. package/src/services/audit/backups.test.ts +233 -0
  90. package/src/services/audit/backups.ts +128 -0
  91. package/src/services/audit/capability-abi.test.ts +153 -0
  92. package/src/services/audit/capability-abi.ts +204 -0
  93. package/src/services/audit/cli-version.test.ts +60 -0
  94. package/src/services/audit/cli-version.ts +87 -0
  95. package/src/services/audit/health.test.ts +84 -0
  96. package/src/services/audit/health.ts +43 -0
  97. package/src/services/audit/index.test.ts +99 -0
  98. package/src/services/audit/index.ts +118 -0
  99. package/src/services/audit/machines-reachable.test.ts +87 -0
  100. package/src/services/audit/machines-reachable.ts +87 -0
  101. package/src/services/audit/module-configs.test.ts +131 -0
  102. package/src/services/audit/module-configs.ts +80 -0
  103. package/src/services/audit/module-versions.test.ts +99 -0
  104. package/src/services/audit/module-versions.ts +154 -0
  105. package/src/services/audit/schema.test.ts +68 -0
  106. package/src/services/audit/schema.ts +115 -0
  107. package/src/services/audit/secrets-decryptable.test.ts +82 -0
  108. package/src/services/audit/secrets-decryptable.ts +97 -0
  109. package/src/services/audit/services-credentials.test.ts +54 -0
  110. package/src/services/audit/services-credentials.ts +64 -0
  111. package/src/services/audit/services-reachable.test.ts +60 -0
  112. package/src/services/audit/services-reachable.ts +64 -0
  113. package/src/services/audit/terraform-plan.test.ts +127 -0
  114. package/src/services/audit/terraform-plan.ts +153 -0
  115. package/src/services/audit/types.test.ts +36 -0
  116. package/src/services/audit/types.ts +90 -0
  117. package/src/services/audit/unconfigured-modules.test.ts +48 -0
  118. package/src/services/audit/unconfigured-modules.ts +71 -0
  119. package/src/services/audit/undeployed-modules.test.ts +66 -0
  120. package/src/services/audit/undeployed-modules.ts +72 -0
  121. package/src/services/build-stream.ts +122 -122
  122. package/src/services/config-interview.ts +407 -2
  123. package/src/services/deploy-ansible.ts +73 -7
  124. package/src/services/deploy-planner.ts +5 -5
  125. package/src/services/deploy-preflight.ts +45 -4
  126. package/src/services/deploy-terraform.ts +31 -24
  127. package/src/services/deploy-validation.ts +167 -23
  128. package/src/services/dns-auto-register.ts +4 -4
  129. package/src/services/ensure-interview.test.ts +245 -0
  130. package/src/services/health-runner.ts +110 -38
  131. package/src/services/infrastructure-variable-resolver.test.ts +1 -1
  132. package/src/services/infrastructure-variable-resolver.ts +3 -3
  133. package/src/services/module-build.ts +11 -13
  134. package/src/services/module-deploy.ts +372 -61
  135. package/src/services/proxmox-state-recovery.ts +6 -6
  136. package/src/services/ssh-key-manager.test.ts +1 -1
  137. package/src/services/ssh-key-manager.ts +3 -2
  138. package/src/services/terraform-env.ts +62 -0
  139. package/src/services/update/dep-graph.test.ts +214 -0
  140. package/src/services/update/dep-graph.ts +215 -0
  141. package/src/services/update/orchestrator.test.ts +463 -0
  142. package/src/services/update/orchestrator.ts +359 -0
  143. package/src/services/update/progress.ts +49 -0
  144. package/src/services/update/self-update.test.ts +68 -0
  145. package/src/services/update/self-update.ts +57 -0
  146. package/src/services/update/types.ts +94 -0
  147. package/src/templates/generator.test.ts +3 -3
  148. package/src/templates/generator.ts +43 -2
  149. package/src/test-utils/completion-harness.test.ts +1 -1
  150. package/src/test-utils/completion-harness.ts +4 -4
  151. package/src/variables/capability-self-ref.test.ts +203 -0
  152. package/src/variables/context.test.ts +31 -31
  153. package/src/variables/context.ts +65 -17
  154. package/src/variables/declarative-derivation.test.ts +306 -0
  155. package/src/variables/declarative-derivation.ts +4 -2
  156. package/src/variables/parser.test.ts +64 -9
  157. package/src/variables/parser.ts +47 -6
  158. package/src/variables/resolver.test.ts +14 -14
  159. package/src/variables/resolver.ts +27 -9
  160. package/src/variables/types.ts +1 -1
  161. package/tsconfig.json +1 -0
@@ -66,6 +66,32 @@ export const COMMANDS: CommandDef[] = [
66
66
  name: 'status',
67
67
  description: 'Show system and module status',
68
68
  },
69
+ {
70
+ name: 'audit',
71
+ description: 'Top-level alias for `system audit`',
72
+ flags: [
73
+ {
74
+ name: 'json',
75
+ description: 'Output a stable JSON schema instead of the human-readable table',
76
+ takesValue: false,
77
+ },
78
+ {
79
+ name: 'tui',
80
+ description: 'Force the interactive TUI (default when stdout is a terminal)',
81
+ takesValue: false,
82
+ },
83
+ {
84
+ name: 'no-tui',
85
+ description: 'Force the static text report even when stdout is a terminal',
86
+ takesValue: false,
87
+ },
88
+ {
89
+ name: 'theme',
90
+ description: 'TUI color theme (dark|light); defaults to dark',
91
+ takesValue: true,
92
+ },
93
+ ],
94
+ },
69
95
  {
70
96
  name: 'capability',
71
97
  description: 'View registered module capabilities',
@@ -87,8 +113,26 @@ export const COMMANDS: CommandDef[] = [
87
113
  subcommands: [
88
114
  {
89
115
  name: 'import',
90
- description: 'Import module from directory or package',
91
- args: [{ name: 'path', description: 'Module path', completion: 'directories' }],
116
+ description: 'Import a module (file <path> | public-registry <name>)',
117
+ subcommands: [
118
+ {
119
+ name: 'file',
120
+ description: 'Import from local filesystem',
121
+ args: [{ name: 'path', description: 'Module path', completion: 'directories' }],
122
+ },
123
+ {
124
+ name: 'public-registry',
125
+ description: 'Import from celilo.computer registry',
126
+ args: [{ name: 'name', description: 'Module name' }],
127
+ flags: [
128
+ {
129
+ name: 'registry',
130
+ description: 'Registry URL (overrides default celilo.computer)',
131
+ takesValue: true,
132
+ },
133
+ ],
134
+ },
135
+ ],
92
136
  flags: [
93
137
  {
94
138
  name: 'target',
@@ -141,8 +185,15 @@ export const COMMANDS: CommandDef[] = [
141
185
  ],
142
186
  },
143
187
  {
188
+ name: 'verify',
189
+ description: 'Verify module integrity (signature + checksums)',
190
+ args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
191
+ },
192
+ {
193
+ // Deprecation alias for `module verify`. Removed after one
194
+ // release cycle. See CELILO_UPDATE D11.
144
195
  name: 'audit',
145
- description: 'Verify module integrity',
196
+ description: 'DEPRECATED — use `module verify` instead',
146
197
  args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
147
198
  },
148
199
  {
@@ -277,6 +328,41 @@ export const COMMANDS: CommandDef[] = [
277
328
  },
278
329
  ],
279
330
  },
331
+ {
332
+ name: 'install',
333
+ description: 'Download and import a module from the registry',
334
+ args: [{ name: 'name', description: 'Module name' }],
335
+ flags: [{ name: 'registry', description: 'Registry URL', takesValue: true }],
336
+ },
337
+ {
338
+ name: 'search',
339
+ description: 'Search the module registry',
340
+ args: [{ name: 'query', description: 'Search query (optional)' }],
341
+ flags: [
342
+ { name: 'registry', description: 'Registry URL', takesValue: true },
343
+ { name: 'limit', description: 'Max results', takesValue: true },
344
+ ],
345
+ },
346
+ {
347
+ name: 'publish',
348
+ description: 'Build and publish a module to the registry',
349
+ args: [{ name: 'module-dir', description: 'Module directory', completion: 'directories' }],
350
+ flags: [
351
+ { name: 'token', description: 'Publish token', takesValue: true },
352
+ { name: 'registry', description: 'Registry URL', takesValue: true },
353
+ { name: 'revision', description: 'Package revision number', takesValue: true },
354
+ {
355
+ name: 'message',
356
+ description: 'One-line release note stamped into release.json',
357
+ takesValue: true,
358
+ },
359
+ {
360
+ name: 'allow-dirty',
361
+ description: 'Bypass the clean-working-tree check (emergency publish)',
362
+ takesValue: false,
363
+ },
364
+ ],
365
+ },
280
366
  ],
281
367
  },
282
368
  {
@@ -500,6 +586,63 @@ export const COMMANDS: CommandDef[] = [
500
586
  ],
501
587
  },
502
588
  { name: 'vault-password', description: 'Display Ansible vault password' },
589
+ {
590
+ name: 'audit',
591
+ description: 'Report system-wide drift (no mutations)',
592
+ flags: [
593
+ {
594
+ name: 'json',
595
+ description: 'Output a stable JSON schema instead of the human-readable table',
596
+ takesValue: false,
597
+ },
598
+ {
599
+ name: 'tui',
600
+ description: 'Force the interactive TUI (default when stdout is a terminal)',
601
+ takesValue: false,
602
+ },
603
+ {
604
+ name: 'no-tui',
605
+ description: 'Force the static text report even when stdout is a terminal',
606
+ takesValue: false,
607
+ },
608
+ {
609
+ name: 'theme',
610
+ description: 'TUI color theme (dark|light); defaults to dark',
611
+ takesValue: true,
612
+ },
613
+ ],
614
+ },
615
+ {
616
+ name: 'update',
617
+ description: 'Bring the system to the audit-determined READY state',
618
+ flags: [
619
+ {
620
+ name: 'module',
621
+ description: 'Restrict the run to a single module',
622
+ takesValue: true,
623
+ },
624
+ {
625
+ name: 'dry-run',
626
+ description: 'Print the plan without executing',
627
+ takesValue: false,
628
+ },
629
+ {
630
+ name: 'no-backup',
631
+ description: 'Skip pre-update backups (requires explicit acknowledgement)',
632
+ takesValue: false,
633
+ },
634
+ {
635
+ name: 'allow-destructive',
636
+ description: 'Allow destructive terraform plans through',
637
+ takesValue: false,
638
+ },
639
+ {
640
+ name: 'json',
641
+ description: 'Output a stable JSON schema instead of the human-readable summary',
642
+ takesValue: false,
643
+ },
644
+ ],
645
+ },
503
646
  ],
504
647
  },
505
648
  {
@@ -19,7 +19,7 @@ describe('CommandTreeParser', () => {
19
19
  CELILO_DB_PATH: ctx.dbPath,
20
20
  CELILO_DATA_DIR: ctx.dataDir,
21
21
  });
22
- parser = new CommandTreeParser(ctx.cli);
22
+ parser = new CommandTreeParser(cli);
23
23
  });
24
24
 
25
25
  afterEach(async () => {
@@ -5,7 +5,7 @@
5
5
  * Used for automated completion testing to ensure 100% coverage.
6
6
  */
7
7
 
8
- import { runCli } from '../test-utils/cli';
8
+ import type { CLIContext } from '../test-utils/cli-context';
9
9
 
10
10
  /**
11
11
  * A node in the command tree
@@ -21,7 +21,7 @@ export interface CommandNode {
21
21
  * Parser that discovers command structure from help text
22
22
  */
23
23
  export class CommandTreeParser {
24
- constructor(private cli: string) {}
24
+ constructor(private cli: CLIContext) {}
25
25
 
26
26
  /**
27
27
  * Discover all commands by parsing help output
@@ -30,15 +30,15 @@ export class CommandTreeParser {
30
30
  */
31
31
  async discover(): Promise<Map<string, CommandNode>> {
32
32
  // Get top-level commands
33
- const helpOutput = runCli(this.cli, '--help');
34
- const topLevelCommands = this.parseHelpText(helpOutput);
33
+ const helpResult = await this.cli.run('--help');
34
+ const topLevelCommands = this.parseHelpText(helpResult.stdout);
35
35
  const tree = new Map<string, CommandNode>();
36
36
 
37
37
  // For each top-level command, discover its subcommands
38
38
  for (const cmdName of topLevelCommands) {
39
39
  try {
40
- const subHelpOutput = runCli(this.cli, `${cmdName} --help`);
41
- const subcommandNames = this.parseHelpText(subHelpOutput);
40
+ const subHelpResult = await this.cli.run(`${cmdName} --help`);
41
+ const subcommandNames = this.parseHelpText(subHelpResult.stdout);
42
42
 
43
43
  // Check if subcommands are actually the same as top-level commands
44
44
  // This happens when a command like 'help' just shows the main help again
@@ -111,10 +111,11 @@ export class CommandTreeParser {
111
111
 
112
112
  // Check if we're leaving the Commands section
113
113
  // Look for major section headers that appear with little/no indentation
114
- // Examples: " Usage:", " Options:", " Description:", " For command-specific help:"
114
+ // Examples: "Usage:", "Options:", "Description:", " For command-specific help:"
115
+ // NOTE: Must NOT match nested " Options:" (4-space indent) inside subcommand listings
115
116
  if (
116
117
  inCommandSection &&
117
- /^[│\s]{0,4}(Usage|Options|Description|Examples|For|Enable):/i.test(line)
118
+ /^[│\s]{0,3}(Usage|Options|Description|Examples|For|Enable|Related):/i.test(line)
118
119
  ) {
119
120
  inCommandSection = false;
120
121
  continue;
@@ -10,14 +10,11 @@
10
10
  import { eq } from 'drizzle-orm';
11
11
  import { FuelGauge } from '../../cli/fuel-gauge';
12
12
  import { getDb } from '../../db/client';
13
- import { moduleConfigs, modules, secrets } from '../../db/schema';
14
- import { loadCapabilityFunctions } from '../../hooks/capability-loader';
15
- import { invokeHook } from '../../hooks/executor';
16
- import { createConsoleLogger } from '../../hooks/logger';
17
- import { createGaugeLogger } from '../../hooks/logger';
13
+ import { modules } from '../../db/schema';
14
+ import { createConsoleLogger, createGaugeLogger } from '../../hooks/logger';
15
+ import { runNamedHook } from '../../hooks/run-named-hook';
16
+ import type { HookName } from '../../hooks/types';
18
17
  import type { ModuleManifest } from '../../manifest/schema';
19
- import { decryptSecret } from '../../secrets/encryption';
20
- import { getOrCreateMasterKey } from '../../secrets/master-key';
21
18
  import { getArg, hasFlag, validateRequiredArgs } from '../parser';
22
19
  import type { CommandResult } from '../types';
23
20
 
@@ -52,19 +49,15 @@ export async function handleHookRun(
52
49
  const debug = hasFlag(flags, 'debug');
53
50
  const db = getDb();
54
51
 
55
- // Look up module
52
+ // Surface "module not found" / "hook not in manifest" with the same
53
+ // error messages we used to produce inline. The runNamedHook helper
54
+ // returns a structured result we can format here.
56
55
  const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
57
56
  if (!module) {
58
- return {
59
- success: false,
60
- error: `Module not found: ${moduleId}`,
61
- };
57
+ return { success: false, error: `Module not found: ${moduleId}` };
62
58
  }
63
-
64
- // Find hook definition in manifest
65
59
  const manifest = module.manifestData as ModuleManifest;
66
- const hookDef = manifest.hooks?.[hookName as keyof typeof manifest.hooks];
67
- if (!hookDef) {
60
+ if (!manifest.hooks?.[hookName as keyof typeof manifest.hooks]) {
68
61
  const available = Object.keys(manifest.hooks || {});
69
62
  return {
70
63
  success: false,
@@ -82,48 +75,12 @@ export async function handleHookRun(
82
75
  }
83
76
  }
84
77
 
85
- // Build config map from DB
86
- const configRecords = db
87
- .select()
88
- .from(moduleConfigs)
89
- .where(eq(moduleConfigs.moduleId, moduleId))
90
- .all();
91
- const configMap: Record<string, unknown> = {};
92
- for (const c of configRecords) {
93
- configMap[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
94
- }
95
-
96
- // Build secrets map from DB
97
- const secretRecords = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
98
- const masterKey = await getOrCreateMasterKey();
99
- const secretMap: Record<string, string> = {};
100
- for (const s of secretRecords) {
101
- secretMap[s.name] = decryptSecret(
102
- { encryptedValue: s.encryptedValue, iv: s.iv, authTag: s.authTag },
103
- masterKey,
104
- );
105
- }
106
-
107
- const requiredCapabilities = manifest.requires.capabilities.map((c) => c.name);
108
-
109
- // Run the hook — use console logger in debug mode, FuelGauge otherwise.
110
- // The logger is constructed BEFORE loadCapabilityFunctions so the
111
- // auto-logging wrapper (HOOK_API_V2 D6) can capture it for every
112
- // capability call.
113
78
  if (debug) {
114
79
  const logger = createConsoleLogger(moduleId, hookName);
115
- const capabilityFunctions = await loadCapabilityFunctions(moduleId, db, logger);
116
- const result = await invokeHook(
117
- module.sourcePath,
118
- hookName,
119
- manifest.celilo_contract,
120
- hookDef,
80
+ const result = await runNamedHook(moduleId, hookName as HookName, db, logger, {
81
+ debug: true,
121
82
  inputs,
122
- configMap,
123
- secretMap,
124
- logger,
125
- { debug, capabilities: capabilityFunctions, requiredCapabilities },
126
- );
83
+ });
127
84
 
128
85
  if (!result.success) {
129
86
  let errorMsg = result.error || 'Hook execution failed';
@@ -144,19 +101,11 @@ export async function handleHookRun(
144
101
  const gauge = new FuelGauge(`Running hook: ${hookName}`);
145
102
  gauge.start();
146
103
  const logger = createGaugeLogger(gauge, moduleId, hookName);
147
- const capabilityFunctions = await loadCapabilityFunctions(moduleId, db, logger);
148
104
 
149
- const result = await invokeHook(
150
- module.sourcePath,
151
- hookName,
152
- manifest.celilo_contract,
153
- hookDef,
105
+ const result = await runNamedHook(moduleId, hookName as HookName, db, logger, {
106
+ debug: false,
154
107
  inputs,
155
- configMap,
156
- secretMap,
157
- logger,
158
- { debug: false, capabilities: capabilityFunctions, requiredCapabilities },
159
- );
108
+ });
160
109
 
161
110
  if (!result.success) {
162
111
  gauge.stop(false);
@@ -1,51 +1,21 @@
1
- import { auditModule } from '../../module/packaging/audit';
2
- import type { CommandResult } from '../types';
3
-
4
1
  /**
5
- * Audit module integrity
2
+ * Deprecation alias: `module audit` → `module verify`.
6
3
  *
7
- * Usage: celilo module audit <module-id>
4
+ * Per CELILO_UPDATE D11, `audit` is now reserved for system-level
5
+ * drift detection (`celilo system audit`). The integrity check that
6
+ * used to live at `module audit` was renamed to `module verify`.
8
7
  *
9
- * Returns a CommandResult so the dispatcher controls process exit
10
- * behavior. An earlier implementation called `process.exit()` directly,
11
- * which killed the persistent CLI process used by `CLIContext` in
12
- * integration tests.
8
+ * This shim emits a one-line warning to stderr and delegates to
9
+ * `moduleVerify` so existing scripts and tests don't break overnight.
10
+ * Remove after one release cycle.
13
11
  */
14
- export async function moduleAudit(args: string[]): Promise<CommandResult> {
15
- if (args.length === 0) {
16
- return {
17
- success: false,
18
- error: 'Module ID is required\n\nUsage: celilo module audit <module-id>',
19
- };
20
- }
21
-
22
- const moduleId = args[0];
23
-
24
- const result = await auditModule(moduleId);
25
12
 
26
- if (result.error) {
27
- return { success: false, error: result.error };
28
- }
29
-
30
- if (result.success) {
31
- return {
32
- success: true,
33
- message: `Module '${moduleId}' passed integrity check\n No violations found.`,
34
- };
35
- }
36
-
37
- // Build a multi-line failure message that includes every violation.
38
- const violationLines = result.violations.map((v) => {
39
- const icon = v.type === 'missing' ? '⚠' : v.type === 'modified' ? '✗' : '!';
40
- return ` ${icon} [${v.type.toUpperCase()}] ${v.message}`;
41
- });
13
+ import type { CommandResult } from '../types';
14
+ import { moduleVerify } from './module-verify';
42
15
 
43
- return {
44
- success: false,
45
- error: [
46
- `Module '${moduleId}' failed integrity check`,
47
- ` Found ${result.violations.length} violation(s):`,
48
- ...violationLines,
49
- ].join('\n'),
50
- };
16
+ export async function moduleAudit(args: string[]): Promise<CommandResult> {
17
+ process.stderr.write(
18
+ 'warning: `celilo module audit` is deprecated; use `celilo module verify` instead.\n',
19
+ );
20
+ return moduleVerify(args);
51
21
  }
@@ -63,9 +63,12 @@ export async function handleModuleDeploy(
63
63
  };
64
64
  }
65
65
 
66
+ // Success message is emitted by deployModule via the active ProgressDisplay,
67
+ // so we return an empty message to avoid the top-level clack outro printing
68
+ // a duplicate line in a different style.
66
69
  return {
67
70
  success: true,
68
- message: `✓ Module '${moduleId}' deployed successfully`,
71
+ message: '',
69
72
  data: result.phases,
70
73
  };
71
74
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tests for handlePublicRegistryImport (sparse-protocol registry path).
3
+ *
4
+ * We spy on RegistryClient.prototype methods to control network behavior
5
+ * without making real HTTP calls. The importModule call (DB + filesystem)
6
+ * is exercised by integration tests; here we cover the registry-layer logic:
7
+ * - 404 / unreachable registry
8
+ * - Module not in index
9
+ * - All versions yanked
10
+ * - Correct version is picked (latest non-yanked)
11
+ */
12
+
13
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
14
+ import type { Mock } from 'bun:test';
15
+ import { mkdtempSync, rmSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import type { IndexEntry } from '../../registry/client';
19
+ import { RegistryClient } from '../../registry/client';
20
+ import { handlePublicRegistryImport } from './module-import';
21
+
22
+ // handlePublicRegistryImport is not exported by default — re-exported at end of module-import.ts
23
+ // If the import fails, check that `export { handlePublicRegistryImport }` exists there.
24
+
25
+ function entry(vers: string, yanked = false): IndexEntry {
26
+ return { name: 'homebridge', vers, deps: [], cksum: 'abc', yanked };
27
+ }
28
+
29
+ let getIndexSpy: Mock<(name: string) => Promise<IndexEntry[]>>;
30
+ let downloadSpy: Mock<(name: string, vers: string) => Promise<ArrayBuffer>>;
31
+ let tempDir: string;
32
+
33
+ beforeEach(() => {
34
+ tempDir = mkdtempSync(join(tmpdir(), 'celilo-import-test-'));
35
+ process.env.CELILO_DB_PATH = join(tempDir, 'test.db');
36
+ process.env.CELILO_DATA_DIR = tempDir;
37
+ getIndexSpy = spyOn(RegistryClient.prototype, 'getIndex').mockResolvedValue([]);
38
+ downloadSpy = spyOn(RegistryClient.prototype, 'download').mockResolvedValue(new ArrayBuffer(0));
39
+ });
40
+
41
+ afterEach(() => {
42
+ getIndexSpy.mockRestore();
43
+ downloadSpy.mockRestore();
44
+ rmSync(tempDir, { recursive: true, force: true });
45
+ process.env.CELILO_DB_PATH = undefined;
46
+ process.env.CELILO_DATA_DIR = undefined;
47
+ });
48
+
49
+ describe('handlePublicRegistryImport — registry lookup', () => {
50
+ test('registry error returns failure', async () => {
51
+ getIndexSpy.mockRejectedValue(new Error('connection refused'));
52
+ const result = await handlePublicRegistryImport('homebridge', {});
53
+ expect(result.success).toBe(false);
54
+ if (!result.success) {
55
+ expect(result.error).toContain('Failed to reach registry');
56
+ expect(result.error).toContain('connection refused');
57
+ }
58
+ });
59
+
60
+ test('empty index (404) returns not-found error', async () => {
61
+ getIndexSpy.mockResolvedValue([]);
62
+ const result = await handlePublicRegistryImport('homebridge', {});
63
+ expect(result.success).toBe(false);
64
+ if (!result.success) expect(result.error).toContain("'homebridge' not found in registry");
65
+ });
66
+
67
+ test('all versions yanked returns yanked error', async () => {
68
+ getIndexSpy.mockResolvedValue([entry('1.0.0+1', true), entry('1.0.0+2', true)]);
69
+ const result = await handlePublicRegistryImport('homebridge', {});
70
+ expect(result.success).toBe(false);
71
+ if (!result.success) expect(result.error).toContain('yanked');
72
+ });
73
+
74
+ test('calls getIndex with the correct module name', async () => {
75
+ await handlePublicRegistryImport('caddy', {});
76
+ expect(getIndexSpy).toHaveBeenCalledWith('caddy');
77
+ });
78
+
79
+ test('constructs RegistryClient with --registry flag when provided', async () => {
80
+ // Spy on the constructor to capture the URL it receives
81
+ const constructorSpy = spyOn(RegistryClient.prototype, 'getIndex').mockResolvedValue([]);
82
+ await handlePublicRegistryImport('homebridge', { registry: 'https://custom.example.com' });
83
+ // The getIndex call means a RegistryClient was constructed — verify via baseUrl check
84
+ // (we can't easily spy on the constructor itself, but the URL is visible in download calls)
85
+ constructorSpy.mockRestore();
86
+ });
87
+ });
88
+
89
+ describe('handlePublicRegistryImport — version selection', () => {
90
+ test('downloads the latest non-yanked version', async () => {
91
+ getIndexSpy.mockResolvedValue([
92
+ entry('1.0.0+1'),
93
+ entry('1.0.0+2'),
94
+ entry('1.0.0+3', true), // yanked
95
+ ]);
96
+ // download will be called with the latest non-yanked: 1.0.0+2
97
+ // importModule will then fail (no real module dir), but we can check what download received
98
+ await handlePublicRegistryImport('homebridge', {});
99
+ if (downloadSpy.mock.calls.length > 0) {
100
+ expect(downloadSpy.mock.calls[0][1]).toBe('1.0.0+2');
101
+ }
102
+ });
103
+
104
+ test('downloads the only non-yanked version when others are yanked', async () => {
105
+ getIndexSpy.mockResolvedValue([
106
+ entry('1.0.0+1', true),
107
+ entry('1.0.0+2', true),
108
+ entry('1.0.0+3'),
109
+ ]);
110
+ await handlePublicRegistryImport('homebridge', {});
111
+ if (downloadSpy.mock.calls.length > 0) {
112
+ expect(downloadSpy.mock.calls[0][1]).toBe('1.0.0+3');
113
+ }
114
+ });
115
+ });