@celilo/cli 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-clients/proxmox.ts +77 -45
- package/src/cli/command-registry.ts +23 -35
- package/src/cli/commands/completion.ts +12 -11
- package/src/cli/commands/module-check.ts +158 -0
- package/src/cli/commands/module-import-routing.test.ts +52 -0
- package/src/cli/commands/module-import.ts +70 -27
- package/src/cli/commands/module-publish.test.ts +3 -90
- package/src/cli/commands/module-publish.ts +14 -118
- package/src/cli/commands/proxmox-template-selection.test.ts +150 -0
- package/src/cli/commands/proxmox-template-selection.ts +258 -0
- package/src/cli/commands/service-add-proxmox.ts +49 -127
- package/src/cli/commands/service-reconfigure.ts +36 -79
- package/src/cli/commands/service-verify.ts +20 -79
- package/src/cli/commands/{doctor.test.ts → system-doctor.test.ts} +1 -1
- package/src/cli/commands/{doctor.ts → system-doctor.ts} +93 -18
- package/src/cli/commands/system-update.ts +1 -1
- package/src/cli/completion.ts +29 -8
- package/src/cli/index.ts +25 -30
- package/src/manifest/schema.ts +9 -1
- package/src/module/import.ts +4 -2
- package/src/registry/client.ts +14 -1
- package/src/services/bus-interview.ts +13 -1
- package/src/services/bus-secret-flow.test.ts +94 -0
- package/src/services/config-interview.ts +66 -6
- package/src/services/module-deploy.ts +19 -1
- package/src/services/module-validator/capability-versions.test.ts +90 -0
- package/src/services/module-validator/capability-versions.ts +115 -0
- package/src/services/module-validator/contract-version.test.ts +24 -0
- package/src/services/module-validator/contract-version.ts +69 -0
- package/src/services/module-validator/git-hygiene.test.ts +141 -0
- package/src/services/module-validator/git-hygiene.ts +144 -0
- package/src/services/module-validator/index.test.ts +67 -0
- package/src/services/module-validator/index.ts +74 -0
- package/src/services/module-validator/manifest-schema.ts +42 -0
- package/src/services/module-validator/types.ts +43 -0
- package/src/services/module-validator/typescript-build.test.ts +58 -0
- package/src/services/module-validator/typescript-build.ts +115 -0
- package/src/services/module-validator/workspace-deps.test.ts +137 -0
- package/src/services/module-validator/workspace-deps.ts +187 -0
- package/src/services/terminal-responder.ts +75 -0
- package/src/system/prereqs.test.ts +374 -0
- package/src/system/prereqs.ts +377 -0
|
@@ -4,21 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
|
-
import {
|
|
8
|
-
buildTemplateUrl,
|
|
9
|
-
checkTaskStatus,
|
|
10
|
-
downloadTemplate,
|
|
11
|
-
extractTemplateFilename,
|
|
12
|
-
} from '../../api-clients/proxmox';
|
|
7
|
+
import { extractTemplateFilename } from '../../api-clients/proxmox';
|
|
13
8
|
import {
|
|
14
9
|
type ProxmoxCredentials,
|
|
15
10
|
getContainerServiceByServiceId,
|
|
16
11
|
getServiceCredentials,
|
|
17
12
|
verifyContainerService,
|
|
18
13
|
} from '../../services/container-service';
|
|
19
|
-
import { FuelGauge } from '../fuel-gauge';
|
|
20
14
|
import { celiloIntro, celiloOutro } from '../prompts';
|
|
21
15
|
import type { CommandResult } from '../types';
|
|
16
|
+
import { runApplianceDownload } from './proxmox-template-selection';
|
|
22
17
|
|
|
23
18
|
/**
|
|
24
19
|
* Handle service verify command
|
|
@@ -108,7 +103,6 @@ export async function handleServiceVerify(
|
|
|
108
103
|
|
|
109
104
|
if (shouldDownload) {
|
|
110
105
|
try {
|
|
111
|
-
// Get credentials and config
|
|
112
106
|
const credentials = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
|
|
113
107
|
const providerConfig = service.providerConfig as {
|
|
114
108
|
default_target_node: string;
|
|
@@ -116,86 +110,33 @@ export async function handleServiceVerify(
|
|
|
116
110
|
storage: string;
|
|
117
111
|
};
|
|
118
112
|
|
|
119
|
-
// Extract template info
|
|
120
113
|
const templateFilename = extractTemplateFilename(providerConfig.lxc_template);
|
|
121
|
-
const
|
|
122
|
-
const templateStorage = templateParts[0] || 'local';
|
|
123
|
-
|
|
124
|
-
// Detect Ubuntu version from template filename
|
|
125
|
-
const versionMatch = templateFilename.match(/ubuntu-(\d+\.\d+)-/);
|
|
126
|
-
if (!versionMatch) {
|
|
127
|
-
return {
|
|
128
|
-
success: false,
|
|
129
|
-
error: 'Cannot auto-download: unrecognized template format',
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const ubuntuVersion = versionMatch[1];
|
|
133
|
-
|
|
134
|
-
const templateUrl = buildTemplateUrl(ubuntuVersion);
|
|
114
|
+
const templateStorage = providerConfig.lxc_template.split(':')[0] || 'local';
|
|
135
115
|
|
|
136
|
-
|
|
116
|
+
// The saved volid was the canonical filename when the service was
|
|
117
|
+
// created; if Proxmox refreshed the revision since then, this download
|
|
118
|
+
// will fail with `started-failed` and the user can `service reconfigure`
|
|
119
|
+
// to pick a fresh filename from the catalog.
|
|
120
|
+
const outcome = await runApplianceDownload({
|
|
137
121
|
credentials,
|
|
138
|
-
providerConfig.default_target_node,
|
|
122
|
+
targetNode: providerConfig.default_target_node,
|
|
139
123
|
templateStorage,
|
|
140
|
-
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
if (!
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Wait for download to complete with fuel-gauge progress
|
|
151
|
-
const upid = downloadResult.data;
|
|
152
|
-
const gauge = new FuelGauge(`Downloading ${templateFilename}`);
|
|
153
|
-
gauge.start();
|
|
154
|
-
|
|
155
|
-
let downloadComplete = false;
|
|
156
|
-
let attempts = 0;
|
|
157
|
-
const maxAttempts = 60; // 5 minutes
|
|
158
|
-
|
|
159
|
-
while (!downloadComplete && attempts < maxAttempts) {
|
|
160
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
161
|
-
|
|
162
|
-
const statusResult = await checkTaskStatus(
|
|
163
|
-
credentials,
|
|
164
|
-
providerConfig.default_target_node,
|
|
165
|
-
upid,
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
if (statusResult.success) {
|
|
169
|
-
if (statusResult.data.status === 'stopped') {
|
|
170
|
-
if (statusResult.data.exitstatus === 'OK') {
|
|
171
|
-
downloadComplete = true;
|
|
172
|
-
gauge.stop(true);
|
|
173
|
-
} else {
|
|
174
|
-
gauge.stop(false);
|
|
175
|
-
return {
|
|
176
|
-
success: false,
|
|
177
|
-
error: `Template download failed: ${statusResult.data.exitstatus}\n\nThis usually means the Proxmox host cannot reach download.proxmox.com.\n\nTroubleshooting:\n 1. SSH into your Proxmox host and test: curl -I https://download.proxmox.com\n 2. Try downloading manually: pveam download ${templateStorage} ${templateFilename}\n 3. Choose a different Ubuntu version: celilo service reconfigure ${service.serviceId}\n 4. Check DNS and firewall settings on the Proxmox host`,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
attempts++;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!downloadComplete) {
|
|
191
|
-
gauge.stop(false);
|
|
124
|
+
templateFilename,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!outcome.ready) {
|
|
128
|
+
const detail =
|
|
129
|
+
outcome.reason === 'task-failed'
|
|
130
|
+
? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}\n\nThis usually means the saved template revision is no longer available on Proxmox's mirror.`
|
|
131
|
+
: outcome.reason === 'started-failed'
|
|
132
|
+
? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}\n\nThis usually means the saved template name does not match Proxmox's current catalog.`
|
|
133
|
+
: 'Template download did not complete in time. The Proxmox host may have slow internet or connectivity issues.';
|
|
192
134
|
return {
|
|
193
135
|
success: false,
|
|
194
|
-
error:
|
|
136
|
+
error: `${detail}\n\nTroubleshooting:\n 1. Pick a fresh template version: celilo service reconfigure ${service.serviceId}\n 2. SSH into your Proxmox host and run: pveam update && pveam download ${templateStorage} ${templateFilename}\n 3. Check DNS and firewall settings on the Proxmox host`,
|
|
195
137
|
};
|
|
196
138
|
}
|
|
197
139
|
|
|
198
|
-
// Retry verification
|
|
199
140
|
console.log('\nRetrying verification...');
|
|
200
141
|
const retryResult = await verifyContainerService(service.id);
|
|
201
142
|
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `celilo doctor` — diagnose
|
|
3
|
-
*
|
|
2
|
+
* `celilo system doctor` — diagnose system-level health for celilo:
|
|
3
|
+
* 1. System prerequisites (ansible, terraform, ssh, etc.) present
|
|
4
|
+
* and at supported versions.
|
|
5
|
+
* 2. @celilo/* version drift between the running CLI and the
|
|
6
|
+
* surrounding workspace (if any).
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* The canonical failures this catches:
|
|
9
|
+
* - "I just installed celilo on a fresh box but module import is
|
|
10
|
+
* erroring with a child-process exit 127." → prereq section.
|
|
11
|
+
* - "I edited the workspace but my global celilo is still running
|
|
12
|
+
* an older published version." → drift section.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* Renamed from top-level `celilo doctor` (Phase 0; no alias kept) to
|
|
15
|
+
* fit alongside `system init`, `system audit`, `system config`. See
|
|
16
|
+
* apps/celilo/designs/PREREQ_DETECTION.md.
|
|
17
|
+
*
|
|
18
|
+
* Drift resolution strategy:
|
|
19
|
+
* - The running CLI's package.json comes from a relative import —
|
|
20
|
+
* that anchors us to whatever copy of `@celilo/cli` is actually
|
|
21
|
+
* executing (workspace TS source or globally-installed
|
|
22
|
+
* node_modules tree).
|
|
12
23
|
* - For each `@celilo/*` dependency, we ask the runtime where it
|
|
13
|
-
* resolves the package's `package.json` and read the version
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
24
|
+
* resolves the package's `package.json` and read the version
|
|
25
|
+
* there.
|
|
26
|
+
* - If we can find a workspace root by walking up from
|
|
27
|
+
* `process.cwd()`, we read each `packages/*\/package.json` and
|
|
28
|
+
* flag anything where the loaded version is older than the
|
|
29
|
+
* workspace.
|
|
17
30
|
*/
|
|
18
31
|
|
|
19
32
|
import { spawnSync } from 'node:child_process';
|
|
@@ -21,6 +34,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
21
34
|
import { createRequire } from 'node:module';
|
|
22
35
|
import { dirname, join, resolve } from 'node:path';
|
|
23
36
|
import cliPkg from '../../../package.json' with { type: 'json' };
|
|
37
|
+
import { checkAllPrerequisites, failingPrerequisites } from '../../system/prereqs';
|
|
24
38
|
import type { CommandResult } from '../types';
|
|
25
39
|
|
|
26
40
|
interface CeliloPkgInfo {
|
|
@@ -247,7 +261,42 @@ function applyFix(drifted: DriftedDep[], cliRoot: string): string[] {
|
|
|
247
261
|
return lines;
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Render the system-prerequisites block: every tool from the
|
|
266
|
+
* PREREQUISITES table with its detection result, formatted into the
|
|
267
|
+
* doctor's output column-aligned style.
|
|
268
|
+
*/
|
|
269
|
+
function renderPrereqSection(): { lines: string[]; failingCount: number } {
|
|
270
|
+
const checks = checkAllPrerequisites();
|
|
271
|
+
const lines: string[] = [];
|
|
272
|
+
|
|
273
|
+
lines.push('System prerequisites');
|
|
274
|
+
// Right-pad column for alignment. Take the longest tool name +1
|
|
275
|
+
// for spacing.
|
|
276
|
+
const nameCol = Math.max(...checks.map((c) => c.name.length), 12);
|
|
277
|
+
|
|
278
|
+
for (const c of checks) {
|
|
279
|
+
if (c.present && c.meetsMinimum) {
|
|
280
|
+
const version = c.version ? c.version : `${ANSI.dim}(version unknown)${ANSI.reset}`;
|
|
281
|
+
lines.push(` ${ANSI.green}✔${ANSI.reset} ${c.name.padEnd(nameCol)} ${version}`);
|
|
282
|
+
} else if (c.present && !c.meetsMinimum) {
|
|
283
|
+
// Present but below minimum (or version-parse failed with a minimum).
|
|
284
|
+
const detail = c.version ? `${c.version} — below minimum required` : 'version unreadable';
|
|
285
|
+
lines.push(` ${ANSI.yellow}⚠${ANSI.reset} ${c.name.padEnd(nameCol)} ${detail}`);
|
|
286
|
+
lines.push(` ${ANSI.dim}install: ${c.installHint}${ANSI.reset}`);
|
|
287
|
+
} else {
|
|
288
|
+
// Missing entirely.
|
|
289
|
+
lines.push(
|
|
290
|
+
` ${ANSI.red}✗${ANSI.reset} ${c.name.padEnd(nameCol)} ${ANSI.dim}not installed${ANSI.reset}`,
|
|
291
|
+
);
|
|
292
|
+
lines.push(` ${ANSI.dim}install: ${c.installHint}${ANSI.reset}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { lines, failingCount: failingPrerequisites(checks).length };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function handleSystemDoctor(
|
|
251
300
|
_args: string[],
|
|
252
301
|
flags: Record<string, string | boolean>,
|
|
253
302
|
): Promise<CommandResult> {
|
|
@@ -261,6 +310,12 @@ export async function handleDoctor(
|
|
|
261
310
|
lines.push(`${ANSI.dim}running from ${cliRoot}${ANSI.reset}`);
|
|
262
311
|
lines.push('');
|
|
263
312
|
|
|
313
|
+
// System prerequisites first — they're the most common reason a
|
|
314
|
+
// fresh management box can't run modules.
|
|
315
|
+
const prereqResult = renderPrereqSection();
|
|
316
|
+
lines.push(...prereqResult.lines);
|
|
317
|
+
lines.push('');
|
|
318
|
+
|
|
264
319
|
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
265
320
|
const workspaceVersions = workspaceRoot ? collectWorkspaceVersions(workspaceRoot) : [];
|
|
266
321
|
const workspaceMap = new Map(workspaceVersions.map((w) => [w.name, w]));
|
|
@@ -347,8 +402,17 @@ export async function handleDoctor(
|
|
|
347
402
|
lines.push(...applyFix(drifted, cliRoot));
|
|
348
403
|
lines.push('');
|
|
349
404
|
lines.push(
|
|
350
|
-
`${ANSI.dim}Re-run \`celilo doctor\` to verify; \`bun unlink\` from each workspace dir reverses.${ANSI.reset}`,
|
|
405
|
+
`${ANSI.dim}Re-run \`celilo system doctor\` to verify; \`bun unlink\` from each workspace dir reverses.${ANSI.reset}`,
|
|
351
406
|
);
|
|
407
|
+
// Note: --fix only addresses drift, not missing prereqs. If prereqs
|
|
408
|
+
// failed, surface that even on a successful --fix run.
|
|
409
|
+
if (prereqResult.failingCount > 0) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: `${prereqResult.failingCount} system prerequisite(s) missing or below minimum — install before running celilo`,
|
|
413
|
+
details: lines.join('\n'),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
352
416
|
return {
|
|
353
417
|
success: true,
|
|
354
418
|
message: lines.join('\n'),
|
|
@@ -360,23 +424,34 @@ export async function handleDoctor(
|
|
|
360
424
|
lines.push(`${ANSI.dim}--fix: nothing to repair.${ANSI.reset}`);
|
|
361
425
|
}
|
|
362
426
|
|
|
363
|
-
if (driftCount > 0 || unresolvedCount > 0) {
|
|
427
|
+
if (driftCount > 0 || unresolvedCount > 0 || prereqResult.failingCount > 0) {
|
|
364
428
|
const summary: string[] = [];
|
|
429
|
+
if (prereqResult.failingCount > 0) {
|
|
430
|
+
summary.push(`${prereqResult.failingCount} prerequisite(s) missing/below-minimum`);
|
|
431
|
+
}
|
|
365
432
|
if (driftCount > 0) summary.push(`${driftCount} package(s) behind workspace`);
|
|
366
433
|
if (unresolvedCount > 0) summary.push(`${unresolvedCount} unresolved`);
|
|
367
434
|
if (drifted.length > 0) {
|
|
368
435
|
lines.push(
|
|
369
|
-
`${ANSI.dim}Run \`celilo doctor --fix\` to bun-link drifted packages from the workspace.${ANSI.reset}`,
|
|
436
|
+
`${ANSI.dim}Run \`celilo system doctor --fix\` to bun-link drifted packages from the workspace.${ANSI.reset}`,
|
|
370
437
|
);
|
|
371
438
|
}
|
|
439
|
+
// Distinguish prereq-only from drift-only from both — the
|
|
440
|
+
// operator's next move is different.
|
|
441
|
+
const errorPrefix =
|
|
442
|
+
driftCount === 0 && unresolvedCount === 0
|
|
443
|
+
? 'System prerequisites missing'
|
|
444
|
+
: prereqResult.failingCount === 0
|
|
445
|
+
? 'Drift detected'
|
|
446
|
+
: 'Issues detected';
|
|
372
447
|
return {
|
|
373
448
|
success: false,
|
|
374
|
-
error:
|
|
449
|
+
error: `${errorPrefix}: ${summary.join(', ')}`,
|
|
375
450
|
details: lines.join('\n'),
|
|
376
451
|
};
|
|
377
452
|
}
|
|
378
453
|
|
|
379
|
-
lines.push(`${ANSI.green}OK${ANSI.reset} — no
|
|
454
|
+
lines.push(`${ANSI.green}OK${ANSI.reset} — no issues detected`);
|
|
380
455
|
return {
|
|
381
456
|
success: true,
|
|
382
457
|
message: lines.join('\n'),
|
|
@@ -142,7 +142,7 @@ async function buildSnapshots(
|
|
|
142
142
|
* - `backup` calls `createModuleBackup`. Modules without an
|
|
143
143
|
* `on_backup` hook return ok (nothing to back up; not an error).
|
|
144
144
|
* - `upgrade` is a no-op for now — the in-place upgrade path lives
|
|
145
|
-
* in `module
|
|
145
|
+
* in `module import <name>` and needs a refactor before the
|
|
146
146
|
* orchestrator can drive it cleanly. Deploy will re-converge
|
|
147
147
|
* against whatever's installed, which is the right behavior for
|
|
148
148
|
* "redeploy what's there."
|
package/src/cli/completion.ts
CHANGED
|
@@ -32,7 +32,6 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
32
32
|
'backup',
|
|
33
33
|
'capability',
|
|
34
34
|
'completion',
|
|
35
|
-
'doctor',
|
|
36
35
|
'events',
|
|
37
36
|
'help',
|
|
38
37
|
'hook',
|
|
@@ -122,8 +121,8 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
122
121
|
// Module subcommands
|
|
123
122
|
if (command === 'module' && currentIndex === 1) {
|
|
124
123
|
const subcommands = [
|
|
124
|
+
'check',
|
|
125
125
|
'import',
|
|
126
|
-
'install',
|
|
127
126
|
'list',
|
|
128
127
|
'publish',
|
|
129
128
|
'remove',
|
|
@@ -150,11 +149,6 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
150
149
|
return filterSuggestions(subcommands, args[1] || '');
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
// Module import subcommands (celilo module import file|public-registry)
|
|
154
|
-
if (command === 'module' && args[1] === 'import' && currentIndex === 2) {
|
|
155
|
-
return filterSuggestions(['file', 'public-registry'], args[2] || '');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
152
|
// Module config subcommands (celilo module config set/get)
|
|
159
153
|
if (command === 'module' && args[1] === 'config' && currentIndex === 2) {
|
|
160
154
|
const subcommands = ['set', 'get'];
|
|
@@ -437,7 +431,7 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
437
431
|
|
|
438
432
|
// System subcommands
|
|
439
433
|
if (command === 'system' && currentIndex === 1) {
|
|
440
|
-
const subcommands = ['init', 'config', 'secret', 'vault-password', 'audit', 'update'];
|
|
434
|
+
const subcommands = ['init', 'config', 'secret', 'vault-password', 'audit', 'update', 'doctor'];
|
|
441
435
|
return filterSuggestions(subcommands, args[1] || '');
|
|
442
436
|
}
|
|
443
437
|
|
|
@@ -518,3 +512,30 @@ _celilo() {
|
|
|
518
512
|
compdef _celilo celilo
|
|
519
513
|
`;
|
|
520
514
|
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate fish completion script.
|
|
518
|
+
*
|
|
519
|
+
* Fish doesn't have an equivalent of bash's `compgen -W` or zsh's `compadd`,
|
|
520
|
+
* so we register a single dynamic completer that calls the CLI's
|
|
521
|
+
* --get-completions hook on each TAB. The CLI emits one completion per line;
|
|
522
|
+
* fish picks them up as candidates.
|
|
523
|
+
*
|
|
524
|
+
* The shell context fish exposes is `commandline -opc` (tokens before the
|
|
525
|
+
* in-progress word) plus `commandline -ct` (the in-progress word). We
|
|
526
|
+
* recombine them into the same words + cword shape the bash/zsh wrappers
|
|
527
|
+
* use, so the same TypeScript completion logic serves all three shells.
|
|
528
|
+
*/
|
|
529
|
+
export function generateFishCompletion(): string {
|
|
530
|
+
return `# Celilo fish completion
|
|
531
|
+
function __celilo_complete
|
|
532
|
+
set -l tokens (commandline -opc)
|
|
533
|
+
set -l current (commandline -ct)
|
|
534
|
+
set -l words celilo $tokens[2..-1] $current
|
|
535
|
+
set -l cword (math (count $words) - 1)
|
|
536
|
+
celilo --get-completions $words $cword 2>/dev/null
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
complete -c celilo -f -a '(__celilo_complete)'
|
|
540
|
+
`;
|
|
541
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { COMMANDS, type CommandDef } from './command-registry';
|
|
|
10
10
|
import { handleCapabilityInfo } from './commands/capability-info';
|
|
11
11
|
import { handleCapabilityList } from './commands/capability-list';
|
|
12
12
|
import { handleCompletion } from './commands/completion';
|
|
13
|
-
import { handleDoctor } from './commands/doctor';
|
|
14
13
|
import {
|
|
15
14
|
handleEventsAck,
|
|
16
15
|
handleEventsDrain,
|
|
@@ -45,11 +44,12 @@ import { handleMachineRemove } from './commands/machine-remove';
|
|
|
45
44
|
import { handleMachineStatus } from './commands/machine-status';
|
|
46
45
|
import { moduleAudit } from './commands/module-audit';
|
|
47
46
|
import { handleModuleBuild } from './commands/module-build';
|
|
47
|
+
import { handleModuleCheck } from './commands/module-check';
|
|
48
48
|
import { handleModuleConfigGet, handleModuleConfigSet } from './commands/module-config';
|
|
49
49
|
import { handleModuleDeploy } from './commands/module-deploy';
|
|
50
50
|
import { handleModuleGenerate } from './commands/module-generate';
|
|
51
51
|
import { handleModuleHealth } from './commands/module-health';
|
|
52
|
-
import { handleModuleImport
|
|
52
|
+
import { handleModuleImport } from './commands/module-import';
|
|
53
53
|
import { handleModuleList } from './commands/module-list';
|
|
54
54
|
import { handleModuleLogs } from './commands/module-logs';
|
|
55
55
|
import { handleModulePublish } from './commands/module-publish';
|
|
@@ -75,6 +75,7 @@ import { handleServiceVerify } from './commands/service-verify';
|
|
|
75
75
|
import { handleStatus } from './commands/status';
|
|
76
76
|
import { handleSystemAudit } from './commands/system-audit';
|
|
77
77
|
import { handleSystemConfigGet, handleSystemConfigSet } from './commands/system-config';
|
|
78
|
+
import { handleSystemDoctor } from './commands/system-doctor';
|
|
78
79
|
import { handleSystemInit } from './commands/system-init';
|
|
79
80
|
import { handleSystemSecretGet } from './commands/system-secret-get';
|
|
80
81
|
import { handleSystemSecretSet } from './commands/system-secret-set';
|
|
@@ -148,7 +149,6 @@ Usage:
|
|
|
148
149
|
|
|
149
150
|
Commands:
|
|
150
151
|
status Show system and module status
|
|
151
|
-
doctor Diagnose @celilo/* version drift between the running CLI and the workspace
|
|
152
152
|
audit Top-level alias for 'system audit'
|
|
153
153
|
events SQLite event-bus operations (status, tail, run dispatcher, etc.)
|
|
154
154
|
capability View registered module capabilities
|
|
@@ -218,7 +218,7 @@ Examples:
|
|
|
218
218
|
celilo package ../custom-module
|
|
219
219
|
|
|
220
220
|
Related Commands:
|
|
221
|
-
celilo module import <
|
|
221
|
+
celilo module import <name-or-path> Import a module (registry name or local path)
|
|
222
222
|
celilo module verify <module-id> Verify package integrity
|
|
223
223
|
`;
|
|
224
224
|
|
|
@@ -313,8 +313,9 @@ Usage:
|
|
|
313
313
|
celilo module <subcommand> [args...] [options]
|
|
314
314
|
|
|
315
315
|
Subcommands:
|
|
316
|
-
import <path>
|
|
316
|
+
import <name-or-path> Import a module from the registry (bare name) or a local path
|
|
317
317
|
Options:
|
|
318
|
+
--registry <url> Use a custom registry (default: https://celilo.computer/registry)
|
|
318
319
|
--target <path> Target base directory (default: platform-specific)
|
|
319
320
|
--auto-generate-secrets Auto-generate all secrets without prompting
|
|
320
321
|
|
|
@@ -359,10 +360,6 @@ Subcommands:
|
|
|
359
360
|
update <path> [path...] Update module code while preserving state (configs, secrets, infra)
|
|
360
361
|
|
|
361
362
|
Registry:
|
|
362
|
-
install <name> Download and import a module from the registry
|
|
363
|
-
Options:
|
|
364
|
-
--registry <url> Use a custom registry (default: https://celilo.computer/registry)
|
|
365
|
-
|
|
366
363
|
search [query] Search the registry for modules
|
|
367
364
|
Options:
|
|
368
365
|
--registry <url> Use a custom registry
|
|
@@ -377,15 +374,21 @@ Registry:
|
|
|
377
374
|
--allow-dirty Permit publishing from a dirty git tree
|
|
378
375
|
--allow-stale Skip the manifest-vs-src stale-check. Use sparingly.
|
|
379
376
|
|
|
377
|
+
check [path] Check a module for drift against the framework (default: .)
|
|
378
|
+
Options:
|
|
379
|
+
--no-build Skip the TypeScript build check
|
|
380
|
+
--json Emit a structured Check[] payload (CI-friendly)
|
|
381
|
+
--strict Treat warnings as failures
|
|
382
|
+
|
|
380
383
|
Examples:
|
|
381
|
-
celilo module
|
|
382
|
-
celilo module
|
|
384
|
+
celilo module import caddy # registry (bare name)
|
|
385
|
+
celilo module import namecheap --registry https://my-registry.example.com/registry
|
|
386
|
+
celilo module import ./modules/homebridge # local directory
|
|
387
|
+
celilo module import homebridge.netapp # local .netapp file
|
|
388
|
+
celilo module import /abs/path/to/module --target /custom/location
|
|
383
389
|
celilo module search dns
|
|
384
390
|
celilo module publish ./modules/caddy --token mytoken
|
|
385
391
|
celilo module publish ./modules/* # publish every module in a dir
|
|
386
|
-
celilo module import ./modules/homebridge
|
|
387
|
-
celilo module import homebridge.netapp
|
|
388
|
-
celilo module import /abs/path/to/module --target /custom/location
|
|
389
392
|
celilo module list
|
|
390
393
|
celilo module remove homebridge
|
|
391
394
|
celilo module verify homebridge
|
|
@@ -787,6 +790,8 @@ Subcommands:
|
|
|
787
790
|
update [--module <id>] [--no-backup] [--allow-destructive] [--dry-run] [--json]
|
|
788
791
|
Bring the system to the audit-determined READY state
|
|
789
792
|
|
|
793
|
+
doctor [--fix] Diagnose system prerequisites and @celilo/* version drift
|
|
794
|
+
|
|
790
795
|
Runbook:
|
|
791
796
|
https://celilo.computer/docs/system-update
|
|
792
797
|
(offline summary in apps/celilo/docs/INDEX.md)
|
|
@@ -907,11 +912,6 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
907
912
|
return handleStatus();
|
|
908
913
|
}
|
|
909
914
|
|
|
910
|
-
// Handle doctor command
|
|
911
|
-
if (parsed.command === 'doctor') {
|
|
912
|
-
return handleDoctor(parsed.args, parsed.flags);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
915
|
// Top-level alias: `celilo audit` → `celilo system audit`
|
|
916
916
|
if (parsed.command === 'audit') {
|
|
917
917
|
return handleSystemAudit(parsed.args, parsed.flags);
|
|
@@ -1162,19 +1162,10 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1162
1162
|
return handleModuleShowZone(parsed.args);
|
|
1163
1163
|
case 'search':
|
|
1164
1164
|
return handleModuleSearch(parsed.args, parsed.flags);
|
|
1165
|
-
case 'install': {
|
|
1166
|
-
// `celilo module install <name>` — shorthand for `module import public-registry <name>`
|
|
1167
|
-
const name = parsed.args[0] ?? parsed.subcommand;
|
|
1168
|
-
if (!name) {
|
|
1169
|
-
return {
|
|
1170
|
-
success: false,
|
|
1171
|
-
error: 'Module name required\n\nUsage: celilo module install <name> [--registry <url>]',
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
return handlePublicRegistryImport(name, parsed.flags);
|
|
1175
|
-
}
|
|
1176
1165
|
case 'publish':
|
|
1177
1166
|
return handleModulePublish(parsed.args, parsed.flags);
|
|
1167
|
+
case 'check':
|
|
1168
|
+
return handleModuleCheck(parsed.args, parsed.flags);
|
|
1178
1169
|
default:
|
|
1179
1170
|
return {
|
|
1180
1171
|
success: false,
|
|
@@ -1512,6 +1503,10 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1512
1503
|
return handleSystemUpdate(parsed.args, parsed.flags);
|
|
1513
1504
|
}
|
|
1514
1505
|
|
|
1506
|
+
if (parsed.subcommand === 'doctor') {
|
|
1507
|
+
return handleSystemDoctor(parsed.args, parsed.flags);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1515
1510
|
return {
|
|
1516
1511
|
success: false,
|
|
1517
1512
|
error: `Unknown system subcommand: ${parsed.subcommand}\n\nRun "celilo system --help" for usage`,
|
package/src/manifest/schema.ts
CHANGED
|
@@ -95,7 +95,10 @@ export const SecretGenerateSchema = z.object({
|
|
|
95
95
|
|
|
96
96
|
export const SecretDeclareSchema = z.object({
|
|
97
97
|
name: z.string().min(1),
|
|
98
|
-
|
|
98
|
+
// `string-map` is `Record<string, string>` (e.g. domain → password). It's
|
|
99
|
+
// stored as JSON.stringify'd text on disk, but the interview uses an
|
|
100
|
+
// add-loop UX so the operator never has to type JSON braces.
|
|
101
|
+
type: z.enum(['string', 'integer', 'number', 'string-map']).default('string'),
|
|
99
102
|
required: z.boolean().default(false),
|
|
100
103
|
description: z.string().optional(),
|
|
101
104
|
sensitive: z.boolean().default(true), // Don't log in CLI output
|
|
@@ -104,6 +107,11 @@ export const SecretDeclareSchema = z.object({
|
|
|
104
107
|
minimum: z.number().optional(),
|
|
105
108
|
maximum: z.number().optional(),
|
|
106
109
|
pattern: z.string().optional(),
|
|
110
|
+
// For `type: string-map` only: human-readable labels shown in the
|
|
111
|
+
// add-loop prompt. Defaults are 'key' / 'value' which are usually too
|
|
112
|
+
// generic; namecheap wants 'Domain' / 'Password'.
|
|
113
|
+
key_label: z.string().optional(),
|
|
114
|
+
value_label: z.string().optional(),
|
|
107
115
|
});
|
|
108
116
|
|
|
109
117
|
/**
|
package/src/module/import.ts
CHANGED
|
@@ -417,8 +417,10 @@ async function installScriptDependencies(
|
|
|
417
417
|
return;
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
// Run bun install in the scripts directory
|
|
421
|
-
|
|
420
|
+
// Run bun install in the scripts directory. The bun-install can take a
|
|
421
|
+
// few seconds on a cold cache, so we surface a status line — silent
|
|
422
|
+
// pauses look like hangs.
|
|
423
|
+
log.info('Installing dependencies for module hook scripts...');
|
|
422
424
|
execSync('bun install', {
|
|
423
425
|
cwd: scriptsDir,
|
|
424
426
|
timeout: 120_000,
|
package/src/registry/client.ts
CHANGED
|
@@ -115,11 +115,24 @@ export class RegistryClient {
|
|
|
115
115
|
version: string;
|
|
116
116
|
netappPath: string;
|
|
117
117
|
token: string;
|
|
118
|
+
/**
|
|
119
|
+
* One-line description from the module's manifest.yml. Optional
|
|
120
|
+
* (server tolerates absence) but recommended — it's what shows
|
|
121
|
+
* up in the registry's browse UI per
|
|
122
|
+
* apps/celilo/designs/REGISTRY_BROWSE_UI.md (Phase 2 step 0).
|
|
123
|
+
*/
|
|
124
|
+
description?: string;
|
|
118
125
|
}): Promise<{ ok: boolean; name: string; vers: string }> {
|
|
119
126
|
const fileData = await readFile(opts.netappPath);
|
|
120
127
|
const cksum = `sha256:${createHash('sha256').update(fileData).digest('hex')}`;
|
|
121
128
|
|
|
122
|
-
const meta = JSON.stringify({
|
|
129
|
+
const meta = JSON.stringify({
|
|
130
|
+
name: opts.name,
|
|
131
|
+
vers: opts.version,
|
|
132
|
+
deps: [],
|
|
133
|
+
cksum,
|
|
134
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
135
|
+
});
|
|
123
136
|
const metaBuf = Buffer.from(meta, 'utf-8');
|
|
124
137
|
|
|
125
138
|
const body = Buffer.allocUnsafe(4 + metaBuf.length + 4 + fileData.length);
|
|
@@ -70,11 +70,23 @@ export interface ConfigReply {
|
|
|
70
70
|
export interface SecretRequiredPayload {
|
|
71
71
|
module: string;
|
|
72
72
|
key: string;
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* `string-map` = Record<string, string>; the responder runs an
|
|
75
|
+
* add-loop and serializes to JSON before storing, so the operator
|
|
76
|
+
* never types braces or commas. See terminal-responder.ts for the UX.
|
|
77
|
+
*/
|
|
78
|
+
type: 'string' | 'integer' | 'number' | 'string-map';
|
|
74
79
|
required: boolean;
|
|
75
80
|
description?: string;
|
|
76
81
|
style?: 'user_provided' | 'user_password' | 'generated_optional';
|
|
77
82
|
generate?: { format: string; length: number };
|
|
83
|
+
/**
|
|
84
|
+
* For `type: string-map` only — labels shown in the add-loop prompt.
|
|
85
|
+
* Defaults to 'key' / 'value' when absent. namecheap uses
|
|
86
|
+
* 'Domain' / 'Password'.
|
|
87
|
+
*/
|
|
88
|
+
key_label?: string;
|
|
89
|
+
value_label?: string;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
export interface SecretAck {
|