@celilo/cli 0.3.15 → 0.3.17
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 +25 -14
- package/src/cli/commands/completion.ts +12 -11
- package/src/cli/commands/module-check.ts +158 -0
- package/src/cli/commands/module-import.ts +5 -5
- 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/completion.ts +29 -2
- package/src/cli/index.ts +16 -7
- package/src/module/import.ts +4 -2
- package/src/registry/client.ts +14 -1
- 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/system/prereqs.test.ts +374 -0
- package/src/system/prereqs.ts +377 -0
|
@@ -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 install 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'),
|
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,6 +121,7 @@ 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
126
|
'install',
|
|
127
127
|
'list',
|
|
@@ -437,7 +437,7 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
437
437
|
|
|
438
438
|
// System subcommands
|
|
439
439
|
if (command === 'system' && currentIndex === 1) {
|
|
440
|
-
const subcommands = ['init', 'config', 'secret', 'vault-password', 'audit', 'update'];
|
|
440
|
+
const subcommands = ['init', 'config', 'secret', 'vault-password', 'audit', 'update', 'doctor'];
|
|
441
441
|
return filterSuggestions(subcommands, args[1] || '');
|
|
442
442
|
}
|
|
443
443
|
|
|
@@ -518,3 +518,30 @@ _celilo() {
|
|
|
518
518
|
compdef _celilo celilo
|
|
519
519
|
`;
|
|
520
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Generate fish completion script.
|
|
524
|
+
*
|
|
525
|
+
* Fish doesn't have an equivalent of bash's `compgen -W` or zsh's `compadd`,
|
|
526
|
+
* so we register a single dynamic completer that calls the CLI's
|
|
527
|
+
* --get-completions hook on each TAB. The CLI emits one completion per line;
|
|
528
|
+
* fish picks them up as candidates.
|
|
529
|
+
*
|
|
530
|
+
* The shell context fish exposes is `commandline -opc` (tokens before the
|
|
531
|
+
* in-progress word) plus `commandline -ct` (the in-progress word). We
|
|
532
|
+
* recombine them into the same words + cword shape the bash/zsh wrappers
|
|
533
|
+
* use, so the same TypeScript completion logic serves all three shells.
|
|
534
|
+
*/
|
|
535
|
+
export function generateFishCompletion(): string {
|
|
536
|
+
return `# Celilo fish completion
|
|
537
|
+
function __celilo_complete
|
|
538
|
+
set -l tokens (commandline -opc)
|
|
539
|
+
set -l current (commandline -ct)
|
|
540
|
+
set -l words celilo $tokens[2..-1] $current
|
|
541
|
+
set -l cword (math (count $words) - 1)
|
|
542
|
+
celilo --get-completions $words $cword 2>/dev/null
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
complete -c celilo -f -a '(__celilo_complete)'
|
|
546
|
+
`;
|
|
547
|
+
}
|
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,6 +44,7 @@ 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';
|
|
@@ -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
|
|
@@ -377,6 +377,12 @@ Registry:
|
|
|
377
377
|
--allow-dirty Permit publishing from a dirty git tree
|
|
378
378
|
--allow-stale Skip the manifest-vs-src stale-check. Use sparingly.
|
|
379
379
|
|
|
380
|
+
check [path] Check a module for drift against the framework (default: .)
|
|
381
|
+
Options:
|
|
382
|
+
--no-build Skip the TypeScript build check
|
|
383
|
+
--json Emit a structured Check[] payload (CI-friendly)
|
|
384
|
+
--strict Treat warnings as failures
|
|
385
|
+
|
|
380
386
|
Examples:
|
|
381
387
|
celilo module install caddy
|
|
382
388
|
celilo module install namecheap --registry https://my-registry.example.com/registry
|
|
@@ -787,6 +793,8 @@ Subcommands:
|
|
|
787
793
|
update [--module <id>] [--no-backup] [--allow-destructive] [--dry-run] [--json]
|
|
788
794
|
Bring the system to the audit-determined READY state
|
|
789
795
|
|
|
796
|
+
doctor [--fix] Diagnose system prerequisites and @celilo/* version drift
|
|
797
|
+
|
|
790
798
|
Runbook:
|
|
791
799
|
https://celilo.computer/docs/system-update
|
|
792
800
|
(offline summary in apps/celilo/docs/INDEX.md)
|
|
@@ -907,11 +915,6 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
907
915
|
return handleStatus();
|
|
908
916
|
}
|
|
909
917
|
|
|
910
|
-
// Handle doctor command
|
|
911
|
-
if (parsed.command === 'doctor') {
|
|
912
|
-
return handleDoctor(parsed.args, parsed.flags);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
918
|
// Top-level alias: `celilo audit` → `celilo system audit`
|
|
916
919
|
if (parsed.command === 'audit') {
|
|
917
920
|
return handleSystemAudit(parsed.args, parsed.flags);
|
|
@@ -1175,6 +1178,8 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1175
1178
|
}
|
|
1176
1179
|
case 'publish':
|
|
1177
1180
|
return handleModulePublish(parsed.args, parsed.flags);
|
|
1181
|
+
case 'check':
|
|
1182
|
+
return handleModuleCheck(parsed.args, parsed.flags);
|
|
1178
1183
|
default:
|
|
1179
1184
|
return {
|
|
1180
1185
|
success: false,
|
|
@@ -1512,6 +1517,10 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1512
1517
|
return handleSystemUpdate(parsed.args, parsed.flags);
|
|
1513
1518
|
}
|
|
1514
1519
|
|
|
1520
|
+
if (parsed.subcommand === 'doctor') {
|
|
1521
|
+
return handleSystemDoctor(parsed.args, parsed.flags);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1515
1524
|
return {
|
|
1516
1525
|
success: false,
|
|
1517
1526
|
error: `Unknown system subcommand: ${parsed.subcommand}\n\nRun "celilo system --help" for usage`,
|
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);
|
|
@@ -1137,13 +1137,31 @@ async function deployModuleImpl(
|
|
|
1137
1137
|
}
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
//
|
|
1140
|
+
// Persist target_ip to moduleConfigs (not just inject into the
|
|
1141
|
+
// in-memory hook context). Later hooks like on_backup build
|
|
1142
|
+
// their context from the DB only, so without this they'd see
|
|
1143
|
+
// no target_ip and fail with "No container IP configured".
|
|
1144
|
+
// Mirrors proxmox-state-recovery.ts for the machine-deployed
|
|
1145
|
+
// case.
|
|
1141
1146
|
if (machineId) {
|
|
1142
1147
|
const { getMachine } = await import('./machine-pool');
|
|
1143
1148
|
const deployMachine = await getMachine(machineId);
|
|
1144
1149
|
if (deployMachine) {
|
|
1145
1150
|
installConfigMap['ip.primary'] = deployMachine.ipAddress;
|
|
1146
1151
|
installConfigMap.target_ip = deployMachine.ipAddress;
|
|
1152
|
+
|
|
1153
|
+
await db
|
|
1154
|
+
.insert(pcTable)
|
|
1155
|
+
.values({
|
|
1156
|
+
moduleId,
|
|
1157
|
+
key: 'target_ip',
|
|
1158
|
+
value: deployMachine.ipAddress,
|
|
1159
|
+
})
|
|
1160
|
+
.onConflictDoUpdate({
|
|
1161
|
+
target: [pcTable.moduleId, pcTable.key],
|
|
1162
|
+
set: { value: deployMachine.ipAddress },
|
|
1163
|
+
})
|
|
1164
|
+
.run();
|
|
1147
1165
|
}
|
|
1148
1166
|
}
|
|
1149
1167
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict-publish: manifest-claimed versions for known capabilities must
|
|
3
|
+
* match the framework's runtime registry. Mismatches refuse the publish
|
|
4
|
+
* and surface as failed checks in `module check`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { validateCapabilityVersions } from './capability-versions';
|
|
9
|
+
|
|
10
|
+
describe('validateCapabilityVersions', () => {
|
|
11
|
+
test('no errors when no capabilities are declared', () => {
|
|
12
|
+
expect(validateCapabilityVersions({ id: 'x', version: '1.0.0' })).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('no errors when provides matches the runtime registry exactly', () => {
|
|
16
|
+
expect(
|
|
17
|
+
validateCapabilityVersions({
|
|
18
|
+
id: 'caddy',
|
|
19
|
+
version: '3.0.0',
|
|
20
|
+
provides: { capabilities: [{ name: 'public_web', version: '3.0.0' }] },
|
|
21
|
+
}),
|
|
22
|
+
).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('error when provides[X].version differs from runtime', () => {
|
|
26
|
+
const errors = validateCapabilityVersions({
|
|
27
|
+
id: 'caddy',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
provides: { capabilities: [{ name: 'public_web', version: '1.0.0' }] },
|
|
30
|
+
});
|
|
31
|
+
expect(errors).toHaveLength(1);
|
|
32
|
+
expect(errors[0]).toContain('provides[public_web]');
|
|
33
|
+
expect(errors[0]).toContain('1.0.0');
|
|
34
|
+
expect(errors[0]).toContain('3.0.0');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('error when provides[X].version is newer than runtime', () => {
|
|
38
|
+
const errors = validateCapabilityVersions({
|
|
39
|
+
id: 'caddy',
|
|
40
|
+
version: '4.0.0',
|
|
41
|
+
provides: { capabilities: [{ name: 'public_web', version: '4.0.0' }] },
|
|
42
|
+
});
|
|
43
|
+
expect(errors).toHaveLength(1);
|
|
44
|
+
expect(errors[0]).toContain('provides[public_web]');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('skips capabilities not in the framework registry (third-party)', () => {
|
|
48
|
+
expect(
|
|
49
|
+
validateCapabilityVersions({
|
|
50
|
+
id: 'odd',
|
|
51
|
+
version: '1.0.0',
|
|
52
|
+
provides: { capabilities: [{ name: 'custom_metric', version: '99.0.0' }] },
|
|
53
|
+
}),
|
|
54
|
+
).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('error when requires[X].version is a higher major than runtime', () => {
|
|
58
|
+
const errors = validateCapabilityVersions({
|
|
59
|
+
id: 'lunacycle',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
requires: { capabilities: [{ name: 'idp', version: '2.0.0' }] },
|
|
62
|
+
});
|
|
63
|
+
expect(errors).toHaveLength(1);
|
|
64
|
+
expect(errors[0]).toContain('requires[idp]');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('no error when requires[X].version is at most the runtime version', () => {
|
|
68
|
+
expect(
|
|
69
|
+
validateCapabilityVersions({
|
|
70
|
+
id: 'caddy',
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
requires: { capabilities: [{ name: 'dns_registrar', version: '4.0.0' }] },
|
|
73
|
+
}),
|
|
74
|
+
).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('reports multiple errors at once', () => {
|
|
78
|
+
const errors = validateCapabilityVersions({
|
|
79
|
+
id: 'broken',
|
|
80
|
+
version: '1.0.0',
|
|
81
|
+
provides: {
|
|
82
|
+
capabilities: [
|
|
83
|
+
{ name: 'public_web', version: '99.0.0' },
|
|
84
|
+
{ name: 'idp', version: '99.0.0' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
expect(errors).toHaveLength(2);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAPABILITY_CONTRACT_VERSIONS,
|
|
3
|
+
type KnownCapabilityName,
|
|
4
|
+
compareProviderToRuntime,
|
|
5
|
+
} from '@celilo/capabilities';
|
|
6
|
+
import type { Check } from './types';
|
|
7
|
+
|
|
8
|
+
export interface ManifestCapabilityClaim {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ManifestForCapabilityCheck {
|
|
14
|
+
id?: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
provides?: { capabilities?: ManifestCapabilityClaim[] };
|
|
17
|
+
requires?: { capabilities?: ManifestCapabilityClaim[] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isKnownCapability(name: string): name is KnownCapabilityName {
|
|
21
|
+
return name in CAPABILITY_CONTRACT_VERSIONS;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strict-publish: every `provides[X].version` must match the framework's
|
|
26
|
+
* runtime registry; every `requires[X].version` must be at most the
|
|
27
|
+
* framework's runtime version (consumers can require older minors of
|
|
28
|
+
* still-supported majors).
|
|
29
|
+
*
|
|
30
|
+
* Returns a list of human-readable error messages — empty list means OK.
|
|
31
|
+
* Used both by `module publish` (which fails on any error) and
|
|
32
|
+
* `module check` (which surfaces them as failed checks).
|
|
33
|
+
*/
|
|
34
|
+
export function validateCapabilityVersions(manifest: ManifestForCapabilityCheck): string[] {
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const p of manifest.provides?.capabilities ?? []) {
|
|
38
|
+
if (!isKnownCapability(p.name)) continue;
|
|
39
|
+
const runtime = CAPABILITY_CONTRACT_VERSIONS[p.name];
|
|
40
|
+
const r = compareProviderToRuntime(p.version, runtime);
|
|
41
|
+
if (!r.compatible) {
|
|
42
|
+
errors.push(
|
|
43
|
+
`provides[${p.name}].version is ${p.version} but framework registry is ${runtime} ` +
|
|
44
|
+
`(${r.reason}). Update the manifest, then retry.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const need of manifest.requires?.capabilities ?? []) {
|
|
50
|
+
if (!isKnownCapability(need.name)) continue;
|
|
51
|
+
const runtime = CAPABILITY_CONTRACT_VERSIONS[need.name];
|
|
52
|
+
const r = compareProviderToRuntime(need.version, runtime);
|
|
53
|
+
if (!r.compatible && r.reason === 'major_mismatch_higher') {
|
|
54
|
+
errors.push(
|
|
55
|
+
`requires[${need.name}].version is ${need.version} but framework registry is ${runtime}. Bump the framework before publishing, or lower the manifest version.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return errors;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Per-capability `Check[]` form for `module check`. Emits one OK row for
|
|
65
|
+
* every known capability the manifest references and one FAIL row for
|
|
66
|
+
* every mismatch. A manifest with no known-capability claims yields a
|
|
67
|
+
* single synthetic OK so the report is never empty.
|
|
68
|
+
*/
|
|
69
|
+
export function checkCapabilityVersions(manifest: ManifestForCapabilityCheck): Check[] {
|
|
70
|
+
const checks: Check[] = [];
|
|
71
|
+
|
|
72
|
+
for (const p of manifest.provides?.capabilities ?? []) {
|
|
73
|
+
if (!isKnownCapability(p.name)) continue;
|
|
74
|
+
const runtime = CAPABILITY_CONTRACT_VERSIONS[p.name];
|
|
75
|
+
const r = compareProviderToRuntime(p.version, runtime);
|
|
76
|
+
checks.push({
|
|
77
|
+
category: 'capability',
|
|
78
|
+
name: `provides[${p.name}]`,
|
|
79
|
+
status: r.compatible ? 'ok' : 'fail',
|
|
80
|
+
message: r.compatible
|
|
81
|
+
? `provides ${p.name}@${p.version} matches framework registry ${runtime}`
|
|
82
|
+
: `provides ${p.name}@${p.version} but framework registry is ${runtime} (${r.reason}). Update the manifest.`,
|
|
83
|
+
currentValue: p.version,
|
|
84
|
+
suggestedValue: r.compatible ? undefined : runtime,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const need of manifest.requires?.capabilities ?? []) {
|
|
89
|
+
if (!isKnownCapability(need.name)) continue;
|
|
90
|
+
const runtime = CAPABILITY_CONTRACT_VERSIONS[need.name];
|
|
91
|
+
const r = compareProviderToRuntime(need.version, runtime);
|
|
92
|
+
const tooNew = !r.compatible && r.reason === 'major_mismatch_higher';
|
|
93
|
+
checks.push({
|
|
94
|
+
category: 'capability',
|
|
95
|
+
name: `requires[${need.name}]`,
|
|
96
|
+
status: tooNew ? 'fail' : 'ok',
|
|
97
|
+
message: tooNew
|
|
98
|
+
? `requires ${need.name}@${need.version} but framework registry is ${runtime}. Bump the framework, or lower the manifest version.`
|
|
99
|
+
: `requires ${need.name}@${need.version} can be satisfied by framework registry ${runtime}`,
|
|
100
|
+
currentValue: need.version,
|
|
101
|
+
suggestedValue: tooNew ? runtime : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (checks.length === 0) {
|
|
106
|
+
checks.push({
|
|
107
|
+
category: 'capability',
|
|
108
|
+
name: 'no known-capability claims',
|
|
109
|
+
status: 'ok',
|
|
110
|
+
message: 'manifest declares no provides/requires for known framework capabilities',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return checks;
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { checkContractVersion } from './contract-version';
|
|
3
|
+
|
|
4
|
+
describe('checkContractVersion', () => {
|
|
5
|
+
test('ok when manifest declares the latest supported contract', () => {
|
|
6
|
+
const r = checkContractVersion({ celilo_contract: '1.0' });
|
|
7
|
+
expect(r.status).toBe('ok');
|
|
8
|
+
expect(r.message).toContain('latest');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('fail when celilo_contract field is missing entirely', () => {
|
|
12
|
+
const r = checkContractVersion({});
|
|
13
|
+
expect(r.status).toBe('fail');
|
|
14
|
+
expect(r.message).toContain('missing');
|
|
15
|
+
expect(r.suggestedValue).toBe('1.0');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('fail when celilo_contract claims an unknown version', () => {
|
|
19
|
+
const r = checkContractVersion({ celilo_contract: '99.0' });
|
|
20
|
+
expect(r.status).toBe('fail');
|
|
21
|
+
expect(r.message).toContain('unknown');
|
|
22
|
+
expect(r.suggestedValue).toBe('1.0');
|
|
23
|
+
});
|
|
24
|
+
});
|