@celilo/cli 0.11.0 → 0.12.1

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 (137) hide show
  1. package/AGENTS.md +7 -4
  2. package/CELILO_CORE_MODULES.md +7 -6
  3. package/CELILO_SUBSYSTEMS.md +52 -6
  4. package/MODULE_PRIMITIVES.md +164 -0
  5. package/README.md +5 -5
  6. package/drizzle/0015_port_forwards.sql +12 -0
  7. package/drizzle/meta/_journal.json +7 -0
  8. package/package.json +13 -5
  9. package/schemas/system_config.json +1 -1
  10. package/src/ansible/inventory.test.ts +1 -1
  11. package/src/ansible/inventory.ts +3 -3
  12. package/src/api/protocol.test.ts +1 -1
  13. package/src/api/remote-client.test.ts +1 -1
  14. package/src/api/serve.ts +5 -5
  15. package/src/capabilities/public-web-publish.test.ts +209 -0
  16. package/src/capabilities/validation.test.ts +1 -1
  17. package/src/capabilities/well-known.test.ts +1 -1
  18. package/src/capabilities/well-known.ts +2 -2
  19. package/src/cli/command-tree-parser.ts +3 -1
  20. package/src/cli/commands/api.ts +1 -1
  21. package/src/cli/commands/apt-upgrade.test.ts +33 -0
  22. package/src/cli/commands/apt-upgrade.ts +63 -0
  23. package/src/cli/commands/backup-pull.ts +1 -1
  24. package/src/cli/commands/commands-json.ts +29 -0
  25. package/src/cli/commands/completion.ts +1 -1
  26. package/src/cli/commands/events.ts +1 -1
  27. package/src/cli/commands/module-changeset.ts +1 -1
  28. package/src/cli/commands/module-import.ts +1 -1
  29. package/src/cli/commands/module-list.ts +16 -2
  30. package/src/cli/commands/module-publish.ts +1 -1
  31. package/src/cli/commands/module-remove.ts +26 -1
  32. package/src/cli/commands/module-types.ts +1 -1
  33. package/src/cli/commands/module-upgrade.ts +1 -1
  34. package/src/cli/commands/module-version.ts +1 -1
  35. package/src/cli/commands/module-where.test.ts +26 -0
  36. package/src/cli/commands/module-where.ts +130 -0
  37. package/src/cli/commands/proxmox-node-list.ts +1 -1
  38. package/src/cli/commands/publish/alpha.test.ts +1 -1
  39. package/src/cli/commands/publish/alpha.ts +3 -3
  40. package/src/cli/commands/publish/changesets.ts +1 -1
  41. package/src/cli/commands/publish/global-install.ts +6 -2
  42. package/src/cli/commands/publish/helpers.ts +21 -14
  43. package/src/cli/commands/publish/index.ts +4 -4
  44. package/src/cli/commands/publish/plan.ts +3 -3
  45. package/src/cli/commands/publish/preflight.ts +2 -2
  46. package/src/cli/commands/publish/types.ts +2 -2
  47. package/src/cli/commands/publish/workspace.test.ts +1 -1
  48. package/src/cli/commands/publish/workspace.ts +2 -2
  49. package/src/cli/commands/registry-owner.test.ts +166 -0
  50. package/src/cli/commands/registry-owner.ts +124 -0
  51. package/src/cli/commands/registry-token.test.ts +109 -0
  52. package/src/cli/commands/registry-token.ts +194 -0
  53. package/src/cli/commands/restore.ts +1 -1
  54. package/src/cli/commands/service-add-proxmox.ts +1 -1
  55. package/src/cli/commands/service-list.ts +15 -2
  56. package/src/cli/commands/subscribers-list.ts +1 -1
  57. package/src/cli/commands/system-apply-config-equivalence.test.ts +1 -1
  58. package/src/cli/commands/system-apply-config.ts +1 -1
  59. package/src/cli/commands/system-init-deprecation.test.ts +1 -1
  60. package/src/cli/commands/system-init.ts +2 -2
  61. package/src/cli/commands/token.test.ts +26 -0
  62. package/src/cli/commands/token.ts +160 -0
  63. package/src/cli/completion.ts +21 -0
  64. package/src/cli/generate-zsh-completion.test.ts +22 -4
  65. package/src/cli/generate-zsh-completion.ts +7 -3
  66. package/src/cli/index.ts +170 -7
  67. package/src/cli/parser.ts +1 -1
  68. package/src/cli/restore-command.test.ts +1 -1
  69. package/src/db/client.ts +1 -1
  70. package/src/db/schema.ts +50 -7
  71. package/src/hooks/capability-loader-firewall.test.ts +4 -3
  72. package/src/hooks/capability-loader.ts +100 -10
  73. package/src/hooks/define-hook.test.ts +24 -0
  74. package/src/hooks/executor.test.ts +1 -1
  75. package/src/hooks/executor.ts +2 -2
  76. package/src/hooks/types.ts +1 -1
  77. package/src/manifest/contracts/v1.ts +2 -2
  78. package/src/manifest/schema.ts +13 -13
  79. package/src/manifest/template-validator.ts +1 -1
  80. package/src/module/packaging/build.ts +19 -0
  81. package/src/module/packaging/workspace-deps.test.ts +94 -0
  82. package/src/module/packaging/workspace-deps.ts +185 -0
  83. package/src/module/versioning/changeset-version.ts +1 -1
  84. package/src/policy/no-hand-built-ssh.test.ts +90 -0
  85. package/src/registry/client.test.ts +86 -0
  86. package/src/registry/client.ts +67 -1
  87. package/src/services/api-access.ts +1 -1
  88. package/src/services/aspect-approvals.ts +1 -1
  89. package/src/services/aspect-runner.ts +3 -3
  90. package/src/services/aspect-template-resolver.test.ts +1 -1
  91. package/src/services/aspect-template-resolver.ts +1 -1
  92. package/src/services/build-bus/delivery-events.ts +1 -1
  93. package/src/services/build-bus/fan-out.ts +1 -1
  94. package/src/services/build-bus/hook-dispatch.ts +1 -1
  95. package/src/services/build-bus/receiver-server.ts +1 -1
  96. package/src/services/build-bus/status.test.ts +1 -1
  97. package/src/services/build-bus/status.ts +1 -1
  98. package/src/services/build-bus/subscriber-store.ts +1 -1
  99. package/src/services/bus-interview.ts +1 -1
  100. package/src/services/celilo-events.ts +1 -1
  101. package/src/services/celilo-mgmt-hooks.test.ts +1 -1
  102. package/src/services/cross-module-read.ts +1 -1
  103. package/src/services/deploy-posture.ts +1 -1
  104. package/src/services/deployed-systems.test.ts +1 -1
  105. package/src/services/deployed-systems.ts +4 -4
  106. package/src/services/dns-provider-backfill.ts +2 -2
  107. package/src/services/events-daemon.ts +1 -1
  108. package/src/services/machine-pool.ts +3 -3
  109. package/src/services/module-deploy.ts +93 -8
  110. package/src/services/module-subscriptions.ts +1 -1
  111. package/src/services/module-types-generator.ts +1 -1
  112. package/src/services/module-validator/git-hygiene.ts +1 -1
  113. package/src/services/port-forwards.test.ts +92 -0
  114. package/src/services/port-forwards.ts +86 -0
  115. package/src/services/proxmox-reconcile.ts +2 -2
  116. package/src/services/public-web-republish.test.ts +189 -0
  117. package/src/services/public-web-republish.ts +84 -0
  118. package/src/services/restore-from-file.ts +1 -1
  119. package/src/services/restore-preflight.ts +1 -1
  120. package/src/services/system-identity.ts +1 -1
  121. package/src/services/system-init.test.ts +2 -2
  122. package/src/services/system-init.ts +2 -2
  123. package/src/services/web-route-cleanup.test.ts +250 -0
  124. package/src/services/web-route-cleanup.ts +144 -0
  125. package/src/templates/generator.ts +2 -2
  126. package/src/types/infrastructure.ts +1 -1
  127. package/src/variables/computed/computed-integration.test.ts +1 -1
  128. package/src/variables/computed/evaluate.ts +1 -1
  129. package/src/variables/computed/parse.ts +1 -1
  130. package/src/variables/context.test.ts +1 -1
  131. package/src/variables/context.ts +5 -5
  132. package/src/variables/lxc-nameserver.test.ts +1 -1
  133. package/src/variables/resolver.ts +2 -2
  134. package/src/variables/types.ts +1 -1
  135. package/src/api/protocol.ts +0 -159
  136. package/src/api/remote-client.ts +0 -218
  137. package/src/cli/command-registry.ts +0 -1488
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Contributor identity tokens (idp-issued, per-user)
3
+ *
4
+ * `celilo token obtain|list|revoke` mints, lists, and revokes per-user API
5
+ * tokens from the idp (authentik) so a module contributor authenticates
6
+ * publishes AS THEMSELVES — no admin/shared token ever reaches their machine
7
+ * (SECURE_MODULE_PUBLISH.md). The token consumed by `celilo author init`.
8
+ *
9
+ * Distinct from `celilo registry token add/rm`, which manages the raw
10
+ * bootstrap `publish_tokens` list — a different trust/storage model. Keep the
11
+ * two families separate.
12
+ *
13
+ * Runs on-mgr where the idp provider + bootstrap token live (same model as
14
+ * registry-token.ts). The token string is shown once at mint; the idp stores
15
+ * it hashed and celilo persists nothing.
16
+ */
17
+
18
+ import type { IdpCapability } from '@celilo/capabilities';
19
+ import { getDb } from '../../db/client';
20
+ import { loadCapabilityFunctions } from '../../hooks/capability-loader';
21
+ import { createCapturingLogger } from '../../hooks/logger';
22
+ import { getArg, getFlag } from '../parser';
23
+ import type { CommandError, CommandResult } from '../types';
24
+
25
+ /** Default identifier for a contributor's publish token — stable per user so obtain is idempotent. */
26
+ function defaultIdentifier(username: string): string {
27
+ return `celilo-publish-${username}`;
28
+ }
29
+
30
+ function isError(v: unknown): v is CommandError {
31
+ return typeof v === 'object' && v !== null && 'success' in v && v.success === false;
32
+ }
33
+
34
+ /**
35
+ * Load the idp capability. Runs where the idp provider (authentik) is
36
+ * deployed; errors clearly when it isn't. The capturing logger discards the
37
+ * capability's own progress markers so command output stays clean.
38
+ */
39
+ async function loadIdp(): Promise<IdpCapability | CommandError> {
40
+ const db = getDb();
41
+ const { logger } = createCapturingLogger();
42
+ const caps = await loadCapabilityFunctions('celilo-token', db, logger);
43
+ const idp = caps.idp as IdpCapability | undefined;
44
+ if (!idp) {
45
+ return {
46
+ success: false,
47
+ error:
48
+ 'No idp provider found. Run this on the host where the idp (authentik) is deployed (celilo-mgr).',
49
+ };
50
+ }
51
+ return idp;
52
+ }
53
+
54
+ /** Resolve the target username from `--user` (or first positional arg). */
55
+ function resolveUser(args: string[], flags: Record<string, string | boolean>): string | undefined {
56
+ const flag = getFlag(flags, 'user').trim();
57
+ if (flag) return flag;
58
+ return getArg(args, 0)?.trim();
59
+ }
60
+
61
+ /**
62
+ * `celilo token obtain --user <username> [--identifier <id>] [--description <text>]`
63
+ * Mints (or returns the existing) per-user token. Idempotent on identifier.
64
+ */
65
+ export async function handleTokenObtain(
66
+ args: string[],
67
+ flags: Record<string, string | boolean>,
68
+ ): Promise<CommandResult> {
69
+ const username = resolveUser(args, flags);
70
+ if (!username) {
71
+ return {
72
+ success: false,
73
+ error:
74
+ 'Username required\n\nUsage: celilo token obtain --user <username> [--identifier <id>]',
75
+ };
76
+ }
77
+ const identifier = getFlag(flags, 'identifier').trim() || defaultIdentifier(username);
78
+ const description = getFlag(flags, 'description').trim() || undefined;
79
+
80
+ const idp = await loadIdp();
81
+ if (isError(idp)) return idp;
82
+
83
+ const result = await idp.create_token({ username, identifier, description });
84
+ const verb = result.created ? 'Minted new' : 'Returned existing';
85
+
86
+ return {
87
+ success: true,
88
+ message: `${verb} per-user token for '${username}' (identifier: ${identifier})
89
+
90
+ ${result.token}
91
+
92
+ Treat as a secret — store it now (CELILO_PUBLISH_TOKEN, or \`celilo author init --token …\`).
93
+ celilo persists nothing; the idp stores it hashed.
94
+ Revoke with: celilo token revoke ${identifier}`,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * `celilo token list --user <username>`
100
+ * Lists the user's API tokens (metadata only — never the bearer string).
101
+ */
102
+ export async function handleTokenList(
103
+ args: string[],
104
+ flags: Record<string, string | boolean>,
105
+ ): Promise<CommandResult> {
106
+ const username = resolveUser(args, flags);
107
+ if (!username) {
108
+ return {
109
+ success: false,
110
+ error: 'Username required\n\nUsage: celilo token list --user <username>',
111
+ };
112
+ }
113
+
114
+ const idp = await loadIdp();
115
+ if (isError(idp)) return idp;
116
+
117
+ const tokens = await idp.list_tokens({ username });
118
+ if (tokens.length === 0) {
119
+ return { success: true, message: `No API tokens for '${username}'.` };
120
+ }
121
+
122
+ const lines = tokens.map((t) => {
123
+ const desc = t.description ? ` — ${t.description}` : '';
124
+ const exp = t.expiring && t.expires ? ` (expires ${t.expires})` : '';
125
+ return ` ${t.identifier}${desc}${exp}`;
126
+ });
127
+ return {
128
+ success: true,
129
+ message: `API tokens for '${username}':\n${lines.join('\n')}`,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * `celilo token revoke <identifier>` (or `--identifier <id>`)
135
+ * Deletes a token at the idp; the next publish with it is denied.
136
+ */
137
+ export async function handleTokenRevoke(
138
+ args: string[],
139
+ flags: Record<string, string | boolean>,
140
+ ): Promise<CommandResult> {
141
+ const identifier = getArg(args, 0)?.trim() || getFlag(flags, 'identifier').trim();
142
+ if (!identifier) {
143
+ return {
144
+ success: false,
145
+ error: 'Identifier required\n\nUsage: celilo token revoke <identifier>',
146
+ };
147
+ }
148
+
149
+ const idp = await loadIdp();
150
+ if (isError(idp)) return idp;
151
+
152
+ const { revoked } = await idp.revoke_token({ identifier });
153
+ if (!revoked) {
154
+ return { success: false, error: `No token with identifier '${identifier}' found.` };
155
+ }
156
+ return {
157
+ success: true,
158
+ message: `Revoked token '${identifier}'. The next publish using it will be denied.`,
159
+ };
160
+ }
@@ -30,9 +30,11 @@ export async function getCompletions(words: string[], current: number): Promise<
30
30
  if (currentIndex === 0) {
31
31
  const commands = [
32
32
  'api',
33
+ 'apt-upgrade',
33
34
  'audit',
34
35
  'backup',
35
36
  'capability',
37
+ 'commands',
36
38
  'dns',
37
39
  'completion',
38
40
  'events',
@@ -44,12 +46,14 @@ export async function getCompletions(words: string[], current: number): Promise<
44
46
  'package',
45
47
  'proxmox',
46
48
  'publish',
49
+ 'registry',
47
50
  'restore',
48
51
  'service',
49
52
  'status',
50
53
  'storage',
51
54
  'subscribers',
52
55
  'system',
56
+ 'token',
53
57
  'version',
54
58
  ];
55
59
  return filterSuggestions(commands, args[0] || '');
@@ -68,6 +72,22 @@ export async function getCompletions(words: string[], current: number): Promise<
68
72
  return filterSuggestions(subcommands, args[1] || '');
69
73
  }
70
74
 
75
+ // Registry subcommands
76
+ if (command === 'registry' && currentIndex === 1) {
77
+ return filterSuggestions(['token', 'owner'], args[1] || '');
78
+ }
79
+ if (command === 'registry' && args[1] === 'token' && currentIndex === 2) {
80
+ return filterSuggestions(['add', 'rm'], args[2] || '');
81
+ }
82
+ if (command === 'registry' && args[1] === 'owner' && currentIndex === 2) {
83
+ return filterSuggestions(['list', 'show', 'set'], args[2] || '');
84
+ }
85
+
86
+ // Token subcommands (contributor identity tokens)
87
+ if (command === 'token' && currentIndex === 1) {
88
+ return filterSuggestions(['obtain', 'list', 'revoke'], args[1] || '');
89
+ }
90
+
71
91
  // Capability info - complete with capability names
72
92
  if (command === 'capability' && args[1] === 'info' && currentIndex === 2) {
73
93
  const db = getDb();
@@ -159,6 +179,7 @@ export async function getCompletions(words: string[], current: number): Promise<
159
179
  'run-hook',
160
180
  'secret',
161
181
  'status',
182
+ 'where',
162
183
  'terraform-unlock',
163
184
  'types',
164
185
  'validate',
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { COMMANDS } from './command-registry';
3
- import type { CommandDef } from './command-registry';
2
+ import { COMMANDS } from '@celilo/core';
3
+ import type { CommandDef } from '@celilo/core';
4
4
  import { generateRichZshCompletion } from './generate-zsh-completion';
5
5
 
6
6
  describe('Zsh Completion Generator', () => {
@@ -79,12 +79,15 @@ describe('Zsh Completion Generator', () => {
79
79
  });
80
80
 
81
81
  test('all subcommand descriptions are present', () => {
82
+ function escapeDesc(text: string): string {
83
+ return text.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "'\\''");
84
+ }
82
85
  function checkDescriptions(commands: CommandDef[]): void {
83
86
  for (const cmd of commands) {
84
87
  if (cmd.subcommands) {
85
88
  for (const sub of cmd.subcommands) {
86
- // Description should appear in the _commands function
87
- expect(output).toContain(sub.description.replace(/:/g, '\\:'));
89
+ // Description should appear (escaped) in the _commands function
90
+ expect(output).toContain(escapeDesc(sub.description));
88
91
  }
89
92
  checkDescriptions(cmd.subcommands);
90
93
  }
@@ -92,4 +95,19 @@ describe('Zsh Completion Generator', () => {
92
95
  }
93
96
  checkDescriptions(COMMANDS);
94
97
  });
98
+
99
+ test('generated completion is valid zsh syntax', () => {
100
+ // Recurrence gate: any unescaped quote/redirect in a description breaks the
101
+ // whole file (an apostrophe in "account's" once did). zsh -n parses without
102
+ // executing. Skip cleanly where zsh is unavailable (minimal CI images).
103
+ let zsh: ReturnType<typeof Bun.spawnSync>;
104
+ try {
105
+ zsh = Bun.spawnSync(['zsh', '-nc', output]);
106
+ } catch {
107
+ return; // ponytail: no zsh here — nothing to check
108
+ }
109
+ if (zsh.exitCode === null) return;
110
+ expect(zsh.stderr?.toString() ?? '').toBe('');
111
+ expect(zsh.exitCode).toBe(0);
112
+ });
95
113
  });
@@ -9,14 +9,18 @@
9
9
  * should never be hand-edited.
10
10
  */
11
11
 
12
- import type { ArgDef, CommandDef, FlagDef } from './command-registry';
12
+ import type { ArgDef, CommandDef, FlagDef } from '@celilo/core';
13
13
 
14
14
  /**
15
15
  * Escape a string for use in zsh completion descriptions.
16
- * Colons and backslashes need escaping in _describe arrays.
16
+ * Descriptions are emitted inside single-quoted zsh strings, so:
17
+ * - backslash and colon need escaping in _describe / _arguments specs
18
+ * - a literal apostrophe must close-quote, emit an escaped quote, and reopen
19
+ * ('...'\''...') — otherwise it terminates the string early and the rest of
20
+ * the file mis-parses (e.g. "account's" broke completion at the next `->`).
17
21
  */
18
22
  function escapeZshDescription(text: string): string {
19
- return text.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
23
+ return text.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "'\\''");
20
24
  }
21
25
 
22
26
  /**
package/src/cli/index.ts CHANGED
@@ -4,10 +4,9 @@
4
4
  * Orchestration function (Rule 10.1) - routes commands to handlers
5
5
  */
6
6
 
7
+ import { COMMANDS, type CommandDef, resolveRemote, runRemoteClient } from '@celilo/core';
7
8
  import * as p from '@clack/prompts';
8
- import { resolveRemote, runRemoteClient } from '../api/remote-client';
9
9
  import { CLIServerRequestSchema, parseJsonWithValidation } from '../validation/schemas';
10
- import { COMMANDS, type CommandDef } from './command-registry';
11
10
  import {
12
11
  handleApiAuthorizedKeys,
13
12
  handleApiGrant,
@@ -15,8 +14,10 @@ import {
15
14
  handleApiList,
16
15
  handleApiRevoke,
17
16
  } from './commands/api';
17
+ import { handleAptUpgrade } from './commands/apt-upgrade';
18
18
  import { handleCapabilityInfo } from './commands/capability-info';
19
19
  import { handleCapabilityList } from './commands/capability-list';
20
+ import { handleCommands } from './commands/commands-json';
20
21
  import { handleCompletion } from './commands/completion';
21
22
  import { handleDnsRegistrations } from './commands/dns';
22
23
  import {
@@ -76,11 +77,11 @@ import { handleModuleUpdate } from './commands/module-update';
76
77
  import { handleModuleUpgrade } from './commands/module-upgrade';
77
78
  import { moduleVerify } from './commands/module-verify';
78
79
  import { handleModuleVersion } from './commands/module-version';
80
+ import { handleModuleWhere } from './commands/module-where';
79
81
  import { handlePackage } from './commands/package';
80
82
  import { handleProxmoxInstanceList } from './commands/proxmox-instance-list';
81
83
  import { handleProxmoxInstanceResize } from './commands/proxmox-instance-resize';
82
84
  import { handleProxmoxNodeList } from './commands/proxmox-node-list';
83
- import { main as runPublish } from './commands/publish';
84
85
  import { handleSecretList } from './commands/secret-list';
85
86
  import { handleSecretSet } from './commands/secret-set';
86
87
  import { handleServiceAddDigitalOcean } from './commands/service-add-digitalocean';
@@ -188,12 +189,16 @@ Commands:
188
189
  restore Restore a celilo-mgmt backup from a local file (fresh-bootstrap path)
189
190
  machine Manage machine pool (bring-your-own-hardware)
190
191
  system Manage system configuration
192
+ apt-upgrade Upgrade the deb-installed celilo packages + apply migrations
191
193
  ipam Manage IP address and VMID allocations and reservations
192
194
  proxmox Proxmox cluster introspection (proxmox node list)
193
195
  publish Publish workspace packages to npm and modules to celilo.computer
196
+ registry Administer the module registry (append-safe publish-token management)
197
+ token Manage contributor identity tokens (idp-issued per-user publish tokens)
194
198
  subscribers Manage build-bus subscribers (cross-machine publish-event delivery)
195
199
  api Manage remote-API access (principals, grants, authorized_keys)
196
200
  completion Generate shell completion scripts (bash/zsh)
201
+ commands Print the CLI command registry as JSON (drives @celilo/mcp)
197
202
 
198
203
  help, --help, -h Show this help message
199
204
 
@@ -298,7 +303,7 @@ Description:
298
303
  Modules declare \`subscriptions:\` in their manifests; \`celilo module deploy\`
299
304
  emits lifecycle events that subscribers react to.
300
305
 
301
- See infra/design/SQLITE_EVENT_BUS.md for the full design.
306
+ See infra/openspec/specs/event-bus/spec.md for the full design.
302
307
 
303
308
  Examples:
304
309
  celilo events run # foreground dispatcher
@@ -337,7 +342,7 @@ Description:
337
342
  package name glob). The publisher signs every event with the
338
343
  subscriber's secret; the receiver verifies before reacting.
339
344
 
340
- This is the static-config first cut of v2/BUILD_BUS.md — operators
345
+ This is the static-config first cut of openspec/changes/build-bus-poll-cd/proposal.md — operators
341
346
  manage subscribers explicitly. Auto-discovery via the celilo
342
347
  registry switchboard is a follow-on.
343
348
 
@@ -430,6 +435,79 @@ Examples:
430
435
  return { success: true, message: helpText };
431
436
  }
432
437
 
438
+ function displayRegistryHelp(): CommandResult {
439
+ const helpText = `
440
+ Celilo - Module Registry Administration
441
+
442
+ Usage:
443
+ celilo registry <subcommand> [args...]
444
+
445
+ Subcommands:
446
+ token add <token> Append a publish token (append-safe; never clobbers others)
447
+ token rm <token> Remove one publish token, leaving the rest intact
448
+ owner list List the module-owner table (who may publish what)
449
+ owner show <module> Show the owner of one module name
450
+ owner set <module> <sub> Reassign a module name to a new owner (admin)
451
+
452
+ Description:
453
+ The celilo-registry module's publish_tokens secret is a newline-separated list
454
+ of bootstrap/admin publish tokens. Editing it with 'module secret set' OVERWRITES
455
+ the whole list, clobbering every other holder. These commands read-modify-write
456
+ the list instead.
457
+
458
+ The module-owner table (owner *) gates which module NAMES a verified publisher
459
+ may publish: the first verified publisher of an unclaimed name owns it
460
+ (first-publish-claims); thereafter only that owner or an admin may publish it.
461
+ 'owner set' is the admin reassignment. These read the admin token from the local
462
+ publish_tokens list; pass --registry to target a non-default registry.
463
+
464
+ Run on the host where celilo-registry is deployed (where the master key lives).
465
+
466
+ Examples:
467
+ celilo registry token add ci-token-abc123
468
+ celilo registry owner list
469
+ celilo registry owner show homebridge
470
+ celilo registry owner set homebridge alice
471
+ `;
472
+
473
+ return { success: true, message: helpText.trim() };
474
+ }
475
+
476
+ function displayTokenHelp(): CommandResult {
477
+ const helpText = `
478
+ Celilo - Contributor Identity Tokens (idp-issued, per-user)
479
+
480
+ Usage:
481
+ celilo token <subcommand> [args...]
482
+
483
+ Subcommands:
484
+ obtain --user <username> Mint (or return) a per-user API token for publishing
485
+ list --user <username> List a user's API tokens (metadata only, no secret)
486
+ revoke <identifier> Revoke a token by identifier; next publish denied
487
+
488
+ Options:
489
+ --user <username> The idp user the token is bound to
490
+ --identifier <id> Token identifier (default: celilo-publish-<user>)
491
+ --description <text> Human-readable description (obtain only)
492
+
493
+ Description:
494
+ Issues per-user API tokens from the idp (authentik) so a module contributor
495
+ authenticates publishes AS THEMSELVES — no admin/shared token on their machine.
496
+ The token is consumed by 'celilo author init'. Shown once at mint; the idp
497
+ stores it hashed and celilo persists nothing.
498
+
499
+ Distinct from 'celilo registry token add/rm', which manages the raw bootstrap
500
+ publish_tokens list. Run on the host where the idp is deployed (celilo-mgr).
501
+
502
+ Examples:
503
+ celilo token obtain --user alice
504
+ celilo token list --user alice
505
+ celilo token revoke celilo-publish-alice
506
+ `;
507
+
508
+ return { success: true, message: helpText.trim() };
509
+ }
510
+
433
511
  function displayCapabilityHelp(): CommandResult {
434
512
  const helpText = `
435
513
  Celilo - Capability Management
@@ -1096,11 +1174,20 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1096
1174
  return handleStatus();
1097
1175
  }
1098
1176
 
1177
+ // Handle commands command (serialize the registry; drives @celilo/mcp)
1178
+ if (parsed.command === 'commands') {
1179
+ return handleCommands(parsed.args, parsed.flags);
1180
+ }
1181
+
1099
1182
  // Top-level alias: `celilo audit` → `celilo system audit`
1100
1183
  if (parsed.command === 'audit') {
1101
1184
  return handleSystemAudit(parsed.args, parsed.flags);
1102
1185
  }
1103
1186
 
1187
+ if (parsed.command === 'apt-upgrade') {
1188
+ return handleAptUpgrade(parsed.args, parsed.flags);
1189
+ }
1190
+
1104
1191
  // Route commands
1105
1192
  if (parsed.command === 'package') {
1106
1193
  // Handle package --help
@@ -1194,6 +1281,71 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1194
1281
  };
1195
1282
  }
1196
1283
 
1284
+ if (parsed.command === 'registry') {
1285
+ if (parsed.flags.help || parsed.flags.h || !parsed.subcommand) {
1286
+ return displayRegistryHelp();
1287
+ }
1288
+
1289
+ const registryFlagError = checkFlags('registry', parsed.subcommand, parsed.flags, parsed.args);
1290
+ if (registryFlagError) return registryFlagError;
1291
+
1292
+ if (parsed.subcommand === 'token') {
1293
+ const action = parsed.args[0];
1294
+ const tokenArgs = parsed.args.slice(1);
1295
+ const { handleRegistryTokenAdd, handleRegistryTokenRm } = await import(
1296
+ './commands/registry-token'
1297
+ );
1298
+ if (action === 'add') return handleRegistryTokenAdd(tokenArgs);
1299
+ if (action === 'rm') return handleRegistryTokenRm(tokenArgs);
1300
+ return {
1301
+ success: false,
1302
+ error:
1303
+ 'Token action required (add or rm)\n\nUsage:\n celilo registry token add <token>\n celilo registry token rm <token>',
1304
+ };
1305
+ }
1306
+
1307
+ if (parsed.subcommand === 'owner') {
1308
+ const action = parsed.args[0];
1309
+ const ownerArgs = parsed.args.slice(1);
1310
+ const { handleRegistryOwnerList, handleRegistryOwnerShow, handleRegistryOwnerSet } =
1311
+ await import('./commands/registry-owner');
1312
+ if (action === 'list') return handleRegistryOwnerList(parsed.flags);
1313
+ if (action === 'show') return handleRegistryOwnerShow(ownerArgs, parsed.flags);
1314
+ if (action === 'set') return handleRegistryOwnerSet(ownerArgs, parsed.flags);
1315
+ return {
1316
+ success: false,
1317
+ error:
1318
+ 'Owner action required (list, show, or set)\n\nUsage:\n celilo registry owner list\n celilo registry owner show <module>\n celilo registry owner set <module> <owner-sub>',
1319
+ };
1320
+ }
1321
+
1322
+ return {
1323
+ success: false,
1324
+ error: `Unknown registry subcommand: ${parsed.subcommand}\n\nRun "celilo registry --help" for usage`,
1325
+ };
1326
+ }
1327
+
1328
+ if (parsed.command === 'token') {
1329
+ if (parsed.flags.help || parsed.flags.h || !parsed.subcommand) {
1330
+ return displayTokenHelp();
1331
+ }
1332
+
1333
+ const tokenFlagError = checkFlags('token', parsed.subcommand, parsed.flags, parsed.args);
1334
+ if (tokenFlagError) return tokenFlagError;
1335
+
1336
+ const { handleTokenObtain, handleTokenList, handleTokenRevoke } = await import(
1337
+ './commands/token'
1338
+ );
1339
+ if (parsed.subcommand === 'obtain') return handleTokenObtain(parsed.args, parsed.flags);
1340
+ if (parsed.subcommand === 'list') return handleTokenList(parsed.args, parsed.flags);
1341
+ if (parsed.subcommand === 'revoke') return handleTokenRevoke(parsed.args, parsed.flags);
1342
+
1343
+ return {
1344
+ success: false,
1345
+ error: `Unknown token subcommand: ${parsed.subcommand}\n\nRun "celilo token --help" for usage`,
1346
+ };
1347
+ }
1348
+
1197
1349
  if (parsed.command === 'publish') {
1198
1350
  if (parsed.flags.help || parsed.flags.h) {
1199
1351
  return displayPublishHelp();
@@ -1206,6 +1358,15 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1206
1358
  // flag identity since the parser canonicalises those.
1207
1359
  const publishIdx = process.argv.indexOf('publish');
1208
1360
  const publishArgv = publishIdx >= 0 ? process.argv.slice(publishIdx + 1) : [...parsed.args];
1361
+ // Lazy-load the publish command: it's monorepo-only release tooling whose
1362
+ // transitive import chain reaches scripts/workspace-graph.ts, which does NOT
1363
+ // ship in @celilo/cli's files[]. A static top-level import pulled that into
1364
+ // EVERY command's load graph, so `celilo --version` in a consumer install
1365
+ // crashed on the unresolvable module (WORKSPACE_PACKAGE_GRAPH.md — the D3
1366
+ // failure, deeper than the module-load throw). Loading it only when the
1367
+ // operator actually runs `celilo publish` (which only happens in the
1368
+ // monorepo) keeps every other command free of the dependency.
1369
+ const { main: runPublish } = await import('./commands/publish');
1209
1370
  await runPublish(publishArgv);
1210
1371
  // runPublish handles its own console output (multi-phase, multi-line);
1211
1372
  // returning an empty success message tells the outer CLI loop to skip
@@ -1291,9 +1452,11 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1291
1452
  case 'import':
1292
1453
  return handleModuleImport(parsed.args, parsed.flags);
1293
1454
  case 'list':
1294
- return handleModuleList();
1455
+ return handleModuleList(parsed.flags);
1295
1456
  case 'status':
1296
1457
  return handleModuleStatus(parsed.args);
1458
+ case 'where':
1459
+ return handleModuleWhere(parsed.args, parsed.flags);
1297
1460
  case 'logs':
1298
1461
  return handleModuleLogs(parsed.args, parsed.flags);
1299
1462
  case 'health':
@@ -1685,7 +1848,7 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1685
1848
  }
1686
1849
 
1687
1850
  if (parsed.command === 'restore') {
1688
- // Phase 4 of v2/SYSTEM_BACKUP_TERRAFORM_STATE.md — top-level
1851
+ // Phase 4 of openspec/specs/management-server-backup/spec.md — top-level
1689
1852
  // fresh-bootstrap restore. Distinct from `celilo backup restore
1690
1853
  // <id>` (which restores from a configured storage destination
1691
1854
  // using an existing backup record); this one takes a local file
package/src/cli/parser.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Policy functions (Rule 10.1) - parsing and validation only
5
5
  */
6
6
 
7
- import type { CommandDef } from './command-registry';
7
+ import type { CommandDef } from '@celilo/core';
8
8
  import type { ParsedCommand } from './types';
9
9
 
10
10
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tests for the `celilo restore` top-level command (Phase 4 of
3
- * v2/SYSTEM_BACKUP_TERRAFORM_STATE.md).
3
+ * openspec/specs/management-server-backup/spec.md).
4
4
  *
5
5
  * Covers the CLI surface: routing, argument parsing, pre-flight
6
6
  * gating, error propagation. The actual file-direct restore +
package/src/db/client.ts CHANGED
@@ -92,7 +92,7 @@ export function createDbClient(config?: Partial<DatabaseConfig>) {
92
92
  }
93
93
 
94
94
  // One-time upgrade backfill for the target_ip → module_systems refactor
95
- // (v2/MODULE_SYSTEMS_ADDRESSING.md). migrate() above has ensured the
95
+ // (openspec/specs/module-systems-addressing/spec.md). migrate() above has ensured the
96
96
  // module_systems table exists (migration 0007). A deployment created before
97
97
  // the refactor has its host data only in module_configs /
98
98
  // ip_allocations / module_infrastructure and an EMPTY module_systems, so its