@celilo/cli 0.3.16 → 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 +12 -12
- 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
package/package.json
CHANGED
|
@@ -501,40 +501,29 @@ export async function listAvailableTemplates(
|
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
504
|
+
* Make an authenticated POST request to the Proxmox API.
|
|
505
|
+
* Shares connection/auth handling with makeProxmoxRequest; the only differences
|
|
506
|
+
* are the verb and the form-encoded body.
|
|
506
507
|
*/
|
|
507
|
-
|
|
508
|
+
async function makeProxmoxPost<T>(
|
|
508
509
|
credentials: ProxmoxCredentials,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
): Promise<ProxmoxResult<string>> {
|
|
510
|
+
path: string,
|
|
511
|
+
params: Record<string, string>,
|
|
512
|
+
): Promise<ProxmoxResult<T>> {
|
|
513
513
|
return new Promise((resolve) => {
|
|
514
514
|
try {
|
|
515
515
|
const { api_url, api_token_id, api_token_secret } = credentials;
|
|
516
516
|
const authHeader = `PVEAPIToken=${api_token_id}=${api_token_secret}`;
|
|
517
|
-
const fullUrl = `${api_url}
|
|
517
|
+
const fullUrl = `${api_url}${path}`;
|
|
518
518
|
const url = new URL(fullUrl);
|
|
519
|
+
const postData = new URLSearchParams(params).toString();
|
|
519
520
|
|
|
520
521
|
if (process.env.DEBUG) {
|
|
521
|
-
console.log(`[Proxmox]
|
|
522
|
-
console.log(`[Proxmox]
|
|
523
|
-
console.log(`[Proxmox] Storage: ${storageName}`);
|
|
522
|
+
console.log(`[Proxmox] POST: ${fullUrl}`);
|
|
523
|
+
console.log(`[Proxmox] Body: ${postData}`);
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
const agent = new https.Agent({
|
|
527
|
-
rejectUnauthorized: false,
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// POST request with form data
|
|
531
|
-
// Note: Proxmox requires 'filename' parameter for download-url endpoint
|
|
532
|
-
const filename = templateUrl.split('/').pop() || 'template.tar.zst';
|
|
533
|
-
const postData = new URLSearchParams({
|
|
534
|
-
url: templateUrl,
|
|
535
|
-
content: 'vztmpl',
|
|
536
|
-
filename: filename,
|
|
537
|
-
}).toString();
|
|
526
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
538
527
|
|
|
539
528
|
const req = https.request(
|
|
540
529
|
{
|
|
@@ -551,33 +540,31 @@ export async function downloadTemplate(
|
|
|
551
540
|
},
|
|
552
541
|
(res) => {
|
|
553
542
|
let body = '';
|
|
554
|
-
|
|
555
543
|
res.on('data', (chunk) => {
|
|
556
544
|
body += chunk;
|
|
557
545
|
});
|
|
558
|
-
|
|
559
546
|
res.on('end', () => {
|
|
560
547
|
const statusCode = res.statusCode || 0;
|
|
561
548
|
|
|
562
549
|
if (statusCode < 200 || statusCode >= 300) {
|
|
563
|
-
if (process.env.DEBUG || statusCode
|
|
564
|
-
console.error(`[Proxmox]
|
|
550
|
+
if (process.env.DEBUG || statusCode >= 400) {
|
|
551
|
+
console.error(`[Proxmox] POST ${path} failed (${statusCode}): ${body}`);
|
|
565
552
|
}
|
|
566
553
|
resolve({
|
|
567
554
|
success: false,
|
|
568
|
-
message: `
|
|
555
|
+
message: `Request failed with status ${statusCode}: ${body}`,
|
|
569
556
|
details: { status: statusCode, response: body },
|
|
570
557
|
});
|
|
571
558
|
return;
|
|
572
559
|
}
|
|
573
560
|
|
|
574
561
|
try {
|
|
575
|
-
const data = JSON.parse(body) as ProxmoxApiResponse<
|
|
562
|
+
const data = JSON.parse(body) as ProxmoxApiResponse<T>;
|
|
576
563
|
resolve({ success: true, data: data.data });
|
|
577
564
|
} catch (error) {
|
|
578
565
|
resolve({
|
|
579
566
|
success: false,
|
|
580
|
-
message: 'Failed to parse
|
|
567
|
+
message: 'Failed to parse response',
|
|
581
568
|
details: { error: String(error), body },
|
|
582
569
|
});
|
|
583
570
|
}
|
|
@@ -588,7 +575,7 @@ export async function downloadTemplate(
|
|
|
588
575
|
req.on('error', (error) => {
|
|
589
576
|
resolve({
|
|
590
577
|
success: false,
|
|
591
|
-
message: `
|
|
578
|
+
message: `Request failed: ${error.message}`,
|
|
592
579
|
details: { error: String(error) },
|
|
593
580
|
});
|
|
594
581
|
});
|
|
@@ -598,13 +585,67 @@ export async function downloadTemplate(
|
|
|
598
585
|
} catch (error) {
|
|
599
586
|
resolve({
|
|
600
587
|
success: false,
|
|
601
|
-
message: `
|
|
588
|
+
message: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
589
|
details: { error: String(error) },
|
|
603
590
|
});
|
|
604
591
|
}
|
|
605
592
|
});
|
|
606
593
|
}
|
|
607
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Entry from Proxmox's appliance catalog (`pveam available`). The `template`
|
|
597
|
+
* field is the canonical filename (revision included) that should be passed to
|
|
598
|
+
* downloadAppliance; constructing it ourselves is a known foot-gun because
|
|
599
|
+
* Proxmox refreshes revisions over time.
|
|
600
|
+
*/
|
|
601
|
+
export interface ProxmoxAppliance {
|
|
602
|
+
/** Full canonical filename, e.g. "ubuntu-24.04-standard_24.04-2_amd64.tar.zst" */
|
|
603
|
+
template: string;
|
|
604
|
+
/** Package family, e.g. "ubuntu-24.04-standard" */
|
|
605
|
+
package: string;
|
|
606
|
+
/** Version including revision, e.g. "24.04-2" */
|
|
607
|
+
version: string;
|
|
608
|
+
/** Template type, typically "lxc" */
|
|
609
|
+
type?: string;
|
|
610
|
+
/** Operating system, e.g. "ubuntu" */
|
|
611
|
+
os?: string;
|
|
612
|
+
/** Section, e.g. "system" — what `pveam available --section system` filters on */
|
|
613
|
+
section?: string;
|
|
614
|
+
/** Display headline */
|
|
615
|
+
headline?: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* List the LXC templates Proxmox knows are downloadable from its mirror.
|
|
620
|
+
* Wraps `GET /nodes/{node}/aplinfo` — the same data source `pveam available`
|
|
621
|
+
* uses. Use this rather than building URLs against download.proxmox.com so
|
|
622
|
+
* that revision bumps (e.g. ubuntu-24.04 -1 → -2) are picked up automatically.
|
|
623
|
+
*/
|
|
624
|
+
export async function listAvailableAppliances(
|
|
625
|
+
credentials: ProxmoxCredentials,
|
|
626
|
+
nodeName: string,
|
|
627
|
+
): Promise<ProxmoxResult<ProxmoxAppliance[]>> {
|
|
628
|
+
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/aplinfo`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Start a download of an appliance template from Proxmox's mirror.
|
|
633
|
+
* `templateName` must be the exact `template` field from listAvailableAppliances
|
|
634
|
+
* (the same string `pveam download <storage> <template>` accepts).
|
|
635
|
+
* Returns a UPID for status polling via checkTaskStatus.
|
|
636
|
+
*/
|
|
637
|
+
export async function downloadAppliance(
|
|
638
|
+
credentials: ProxmoxCredentials,
|
|
639
|
+
nodeName: string,
|
|
640
|
+
storageName: string,
|
|
641
|
+
templateName: string,
|
|
642
|
+
): Promise<ProxmoxResult<string>> {
|
|
643
|
+
return makeProxmoxPost<string>(credentials, `/nodes/${nodeName}/aplinfo`, {
|
|
644
|
+
storage: storageName,
|
|
645
|
+
template: templateName,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
608
649
|
/**
|
|
609
650
|
* Check status of a running task (UPID)
|
|
610
651
|
* Returns task status and completion percentage
|
|
@@ -619,15 +660,6 @@ export async function checkTaskStatus(
|
|
|
619
660
|
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/tasks/${encodedUpid}/status`);
|
|
620
661
|
}
|
|
621
662
|
|
|
622
|
-
/**
|
|
623
|
-
* Build template URL for downloading from Proxmox repository
|
|
624
|
-
*/
|
|
625
|
-
export function buildTemplateUrl(ubuntuVersion: string): string {
|
|
626
|
-
// Proxmox mirrors Ubuntu cloud images
|
|
627
|
-
// Format: http://download.proxmox.com/images/system/ubuntu-{version}-standard_{version}-1_amd64.tar.zst
|
|
628
|
-
return `http://download.proxmox.com/images/system/ubuntu-${ubuntuVersion}-standard_${ubuntuVersion}-1_amd64.tar.zst`;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
663
|
/**
|
|
632
664
|
* Extract template filename from full template path
|
|
633
665
|
* Format: "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst" -> "ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
|
|
@@ -639,11 +671,11 @@ export function extractTemplateFilename(templatePath: string): string {
|
|
|
639
671
|
}
|
|
640
672
|
|
|
641
673
|
/**
|
|
642
|
-
* Build full template path from
|
|
674
|
+
* Build full template path (volid) from a storage name and the canonical
|
|
675
|
+
* template filename returned by listAvailableAppliances.
|
|
643
676
|
*/
|
|
644
|
-
export function buildTemplatePath(storageName: string,
|
|
645
|
-
|
|
646
|
-
return `${storageName}:vztmpl/${filename}`;
|
|
677
|
+
export function buildTemplatePath(storageName: string, templateFilename: string): string {
|
|
678
|
+
return `${storageName}:vztmpl/${templateFilename}`;
|
|
647
679
|
}
|
|
648
680
|
|
|
649
681
|
/**
|
|
@@ -66,18 +66,6 @@ export const COMMANDS: CommandDef[] = [
|
|
|
66
66
|
name: 'status',
|
|
67
67
|
description: 'Show system and module status',
|
|
68
68
|
},
|
|
69
|
-
{
|
|
70
|
-
name: 'doctor',
|
|
71
|
-
description: 'Diagnose @celilo/* version drift between the running CLI and the workspace',
|
|
72
|
-
flags: [
|
|
73
|
-
{
|
|
74
|
-
name: 'fix',
|
|
75
|
-
description:
|
|
76
|
-
'Repair drift by `bun link`-ing each drifted @celilo/* package from the workspace',
|
|
77
|
-
takesValue: false,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
69
|
{
|
|
82
70
|
name: 'audit',
|
|
83
71
|
description: 'Top-level alias for `system audit`',
|
|
@@ -815,6 +803,18 @@ export const COMMANDS: CommandDef[] = [
|
|
|
815
803
|
},
|
|
816
804
|
],
|
|
817
805
|
},
|
|
806
|
+
{
|
|
807
|
+
name: 'doctor',
|
|
808
|
+
description: 'Diagnose system prerequisites and @celilo/* version drift',
|
|
809
|
+
flags: [
|
|
810
|
+
{
|
|
811
|
+
name: 'fix',
|
|
812
|
+
description:
|
|
813
|
+
'Repair drift by `bun link`-ing each drifted @celilo/* package from the workspace',
|
|
814
|
+
takesValue: false,
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
},
|
|
818
818
|
],
|
|
819
819
|
},
|
|
820
820
|
{
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { COMMANDS } from '../command-registry';
|
|
7
|
-
import { generateBashCompletion } from '../completion';
|
|
7
|
+
import { generateBashCompletion, generateFishCompletion } from '../completion';
|
|
8
8
|
import { generateRichZshCompletion } from '../generate-zsh-completion';
|
|
9
9
|
import { celiloIntro } from '../prompts';
|
|
10
10
|
import type { CommandResult } from '../types';
|
|
@@ -32,6 +32,7 @@ export async function handleCompletion(
|
|
|
32
32
|
Usage:
|
|
33
33
|
celilo completion bash Generate bash completion script
|
|
34
34
|
celilo completion zsh Generate zsh completion script
|
|
35
|
+
celilo completion fish Generate fish completion script
|
|
35
36
|
|
|
36
37
|
Install zsh completions:
|
|
37
38
|
celilo completion zsh > ~/.zsh/completions/_celilo
|
|
@@ -41,19 +42,15 @@ Install zsh completions:
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
if (shell === 'bash') {
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
success: true,
|
|
47
|
-
message: script,
|
|
48
|
-
};
|
|
45
|
+
return { success: true, message: generateBashCompletion() };
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
if (shell === 'zsh') {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
49
|
+
return { success: true, message: generateRichZshCompletion(COMMANDS) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (shell === 'fish') {
|
|
53
|
+
return { success: true, message: generateFishCompletion() };
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
celiloIntro('Shell Completion');
|
|
@@ -65,6 +62,7 @@ Install zsh completions:
|
|
|
65
62
|
Supported shells:
|
|
66
63
|
bash Generate bash completion script
|
|
67
64
|
zsh Generate zsh completion script
|
|
65
|
+
fish Generate fish completion script
|
|
68
66
|
|
|
69
67
|
Usage:
|
|
70
68
|
# Bash
|
|
@@ -76,6 +74,9 @@ Usage:
|
|
|
76
74
|
celilo completion zsh > ~/.zsh/completions/_celilo
|
|
77
75
|
# OR for user install:
|
|
78
76
|
celilo completion zsh > /usr/local/share/zsh/site-functions/_celilo
|
|
77
|
+
|
|
78
|
+
# Fish
|
|
79
|
+
celilo completion fish > ~/.config/fish/completions/celilo.fish
|
|
79
80
|
`,
|
|
80
81
|
};
|
|
81
82
|
} catch (error) {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celilo module check <path>` — drift detection for third-party modules.
|
|
3
|
+
*
|
|
4
|
+
* Runs every checker in services/module-validator against the module
|
|
5
|
+
* at <path> (defaults to ".") and reports a human-friendly summary or
|
|
6
|
+
* a structured JSON payload.
|
|
7
|
+
*
|
|
8
|
+
* Flags:
|
|
9
|
+
* --no-build Skip `bunx tsc --noEmit`. Useful for fast iteration
|
|
10
|
+
* or when the module has no TypeScript surface.
|
|
11
|
+
* --json Emit the structured Check[] payload instead of the
|
|
12
|
+
* formatted text. Good for CI.
|
|
13
|
+
* --strict Treat warnings as failures (any non-OK is non-zero).
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* - all checks pass (or only warns) → success
|
|
17
|
+
* - one or more fails → CommandError (CLI exits 1)
|
|
18
|
+
* - --strict turns warns into fails too
|
|
19
|
+
*
|
|
20
|
+
* Pure filesystem + manifest + npm. No DB writes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { resolve } from 'node:path';
|
|
24
|
+
import { type Check, runChecks } from '../../services/module-validator';
|
|
25
|
+
import { hasFlag } from '../parser';
|
|
26
|
+
import type { CommandResult } from '../types';
|
|
27
|
+
|
|
28
|
+
interface CheckOptions {
|
|
29
|
+
noBuild: boolean;
|
|
30
|
+
json: boolean;
|
|
31
|
+
strict: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseOptions(flags: Record<string, string | boolean>): CheckOptions {
|
|
35
|
+
return {
|
|
36
|
+
noBuild: hasFlag(flags, 'no-build'),
|
|
37
|
+
json: hasFlag(flags, 'json'),
|
|
38
|
+
strict: hasFlag(flags, 'strict'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function summarize(checks: Check[]): { ok: number; warn: number; fail: number } {
|
|
43
|
+
const summary = { ok: 0, warn: 0, fail: 0 };
|
|
44
|
+
for (const c of checks) summary[c.status]++;
|
|
45
|
+
return summary;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function statusIcon(status: Check['status']): string {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'ok':
|
|
51
|
+
return '✓';
|
|
52
|
+
case 'warn':
|
|
53
|
+
return '!';
|
|
54
|
+
case 'fail':
|
|
55
|
+
return '✗';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatTextReport(modulePath: string, checks: Check[]): string {
|
|
60
|
+
const lines: string[] = [`Module check: ${modulePath}`, ''];
|
|
61
|
+
const byCategory = new Map<string, Check[]>();
|
|
62
|
+
for (const check of checks) {
|
|
63
|
+
const list = byCategory.get(check.category) ?? [];
|
|
64
|
+
list.push(check);
|
|
65
|
+
byCategory.set(check.category, list);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const order: Check['category'][] = [
|
|
69
|
+
'manifest_schema',
|
|
70
|
+
'contract_version',
|
|
71
|
+
'capability',
|
|
72
|
+
'workspace_dep',
|
|
73
|
+
'git_hygiene',
|
|
74
|
+
'typescript_build',
|
|
75
|
+
];
|
|
76
|
+
for (const category of order) {
|
|
77
|
+
const items = byCategory.get(category);
|
|
78
|
+
if (!items || items.length === 0) continue;
|
|
79
|
+
lines.push(formatCategoryHeader(category));
|
|
80
|
+
for (const c of items) {
|
|
81
|
+
lines.push(` ${statusIcon(c.status)} ${c.name}`);
|
|
82
|
+
lines.push(` ${c.message}`);
|
|
83
|
+
if (c.suggestedValue && c.status !== 'ok') {
|
|
84
|
+
lines.push(` suggest: ${c.suggestedValue}`);
|
|
85
|
+
}
|
|
86
|
+
if (c.migrationUrl) {
|
|
87
|
+
lines.push(` migration: ${c.migrationUrl}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const summary = summarize(checks);
|
|
94
|
+
lines.push(`Summary: ${summary.ok} ok, ${summary.warn} warn, ${summary.fail} fail`);
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatCategoryHeader(category: Check['category']): string {
|
|
99
|
+
switch (category) {
|
|
100
|
+
case 'manifest_schema':
|
|
101
|
+
return 'Manifest schema:';
|
|
102
|
+
case 'contract_version':
|
|
103
|
+
return 'Contract version:';
|
|
104
|
+
case 'capability':
|
|
105
|
+
return 'Capability versions:';
|
|
106
|
+
case 'workspace_dep':
|
|
107
|
+
return 'Workspace deps (@celilo/*):';
|
|
108
|
+
case 'git_hygiene':
|
|
109
|
+
return 'Publish readiness (git):';
|
|
110
|
+
case 'typescript_build':
|
|
111
|
+
return 'TypeScript build:';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatJsonReport(modulePath: string, checks: Check[]): string {
|
|
116
|
+
return JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
module: { path: modulePath },
|
|
119
|
+
checks,
|
|
120
|
+
summary: summarize(checks),
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function handleModuleCheck(
|
|
128
|
+
args: string[],
|
|
129
|
+
flags: Record<string, string | boolean>,
|
|
130
|
+
): Promise<CommandResult> {
|
|
131
|
+
const options = parseOptions(flags);
|
|
132
|
+
const modulePath = resolve(args[0] ?? '.');
|
|
133
|
+
|
|
134
|
+
const checks = await runChecks(modulePath, { noBuild: options.noBuild });
|
|
135
|
+
const summary = summarize(checks);
|
|
136
|
+
|
|
137
|
+
const message = options.json
|
|
138
|
+
? formatJsonReport(modulePath, checks)
|
|
139
|
+
: formatTextReport(modulePath, checks);
|
|
140
|
+
|
|
141
|
+
const hasFails = summary.fail > 0;
|
|
142
|
+
const hasWarns = summary.warn > 0;
|
|
143
|
+
const failed = hasFails || (options.strict && hasWarns);
|
|
144
|
+
|
|
145
|
+
if (failed) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: message,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message,
|
|
155
|
+
rawOutput: options.json,
|
|
156
|
+
data: { checks, summary },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -141,17 +141,17 @@ async function handlePublicRegistryImport(
|
|
|
141
141
|
return { success: false, error: result.error, details: result.details };
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// Type-generation is a developer-mode aid for module AUTHORS editing
|
|
145
|
+
// hook scripts in the source tree. Registry installs are black-box
|
|
146
|
+
// dependencies — the operator isn't editing them — so skipping the
|
|
147
|
+
// type-gen pass saves the work and keeps the install output clean.
|
|
147
148
|
return {
|
|
148
149
|
success: true,
|
|
149
|
-
message: `Imported ${result.moduleId}@${latest.vers}\nFiles: ${shortenPath(result.targetPath)}
|
|
150
|
+
message: `Imported ${result.moduleId}@${latest.vers}\nFiles: ${shortenPath(result.targetPath)}`,
|
|
150
151
|
data: {
|
|
151
152
|
moduleId: result.moduleId,
|
|
152
153
|
version: latest.vers,
|
|
153
154
|
targetPath: result.targetPath,
|
|
154
|
-
typesPath: typesPath ?? undefined,
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
} finally {
|
|
@@ -15,7 +15,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
15
15
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import type { IndexEntry } from '../../registry/client';
|
|
18
|
-
import { handleModulePublish, resolveToken
|
|
18
|
+
import { handleModulePublish, resolveToken } from './module-publish';
|
|
19
19
|
|
|
20
20
|
const TEST_DIR = `/tmp/test-module-publish-${Date.now()}`;
|
|
21
21
|
|
|
@@ -155,95 +155,8 @@ describe('revision auto-detection algorithm', () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// Manifest-claimed versions for known capabilities must match the framework's
|
|
161
|
-
// runtime registry; mismatches refuse the publish.
|
|
162
|
-
|
|
163
|
-
describe('validateCapabilityVersions', () => {
|
|
164
|
-
test('no errors when no capabilities are declared', () => {
|
|
165
|
-
expect(validateCapabilityVersions({ id: 'x', version: '1.0.0' })).toEqual([]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('no errors when provides matches the runtime registry exactly', () => {
|
|
169
|
-
expect(
|
|
170
|
-
validateCapabilityVersions({
|
|
171
|
-
id: 'caddy',
|
|
172
|
-
version: '3.0.0',
|
|
173
|
-
provides: { capabilities: [{ name: 'public_web', version: '3.0.0' }] },
|
|
174
|
-
}),
|
|
175
|
-
).toEqual([]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('error when provides[X].version differs from runtime', () => {
|
|
179
|
-
// The runtime is set to 3.0.0 for public_web; claiming 1.0.0 is a bug.
|
|
180
|
-
const errors = validateCapabilityVersions({
|
|
181
|
-
id: 'caddy',
|
|
182
|
-
version: '1.0.0',
|
|
183
|
-
provides: { capabilities: [{ name: 'public_web', version: '1.0.0' }] },
|
|
184
|
-
});
|
|
185
|
-
expect(errors).toHaveLength(1);
|
|
186
|
-
expect(errors[0]).toContain('provides[public_web]');
|
|
187
|
-
expect(errors[0]).toContain('1.0.0');
|
|
188
|
-
expect(errors[0]).toContain('3.0.0');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('error when provides[X].version is newer than runtime', () => {
|
|
192
|
-
const errors = validateCapabilityVersions({
|
|
193
|
-
id: 'caddy',
|
|
194
|
-
version: '4.0.0',
|
|
195
|
-
provides: { capabilities: [{ name: 'public_web', version: '4.0.0' }] },
|
|
196
|
-
});
|
|
197
|
-
expect(errors).toHaveLength(1);
|
|
198
|
-
expect(errors[0]).toContain('provides[public_web]');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('skips capabilities not in the framework registry (third-party)', () => {
|
|
202
|
-
expect(
|
|
203
|
-
validateCapabilityVersions({
|
|
204
|
-
id: 'odd',
|
|
205
|
-
version: '1.0.0',
|
|
206
|
-
provides: { capabilities: [{ name: 'custom_metric', version: '99.0.0' }] },
|
|
207
|
-
}),
|
|
208
|
-
).toEqual([]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('error when requires[X].version is a higher major than runtime', () => {
|
|
212
|
-
// Framework can't satisfy a requirement it doesn't know about.
|
|
213
|
-
const errors = validateCapabilityVersions({
|
|
214
|
-
id: 'lunacycle',
|
|
215
|
-
version: '1.0.0',
|
|
216
|
-
requires: { capabilities: [{ name: 'idp', version: '2.0.0' }] },
|
|
217
|
-
});
|
|
218
|
-
expect(errors).toHaveLength(1);
|
|
219
|
-
expect(errors[0]).toContain('requires[idp]');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('no error when requires[X].version is at most the runtime version', () => {
|
|
223
|
-
// dns_registrar runtime is 4.0.0; consumer requiring 4.0.0 is fine.
|
|
224
|
-
expect(
|
|
225
|
-
validateCapabilityVersions({
|
|
226
|
-
id: 'caddy',
|
|
227
|
-
version: '1.0.0',
|
|
228
|
-
requires: { capabilities: [{ name: 'dns_registrar', version: '4.0.0' }] },
|
|
229
|
-
}),
|
|
230
|
-
).toEqual([]);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('reports multiple errors at once', () => {
|
|
234
|
-
const errors = validateCapabilityVersions({
|
|
235
|
-
id: 'broken',
|
|
236
|
-
version: '1.0.0',
|
|
237
|
-
provides: {
|
|
238
|
-
capabilities: [
|
|
239
|
-
{ name: 'public_web', version: '99.0.0' },
|
|
240
|
-
{ name: 'idp', version: '99.0.0' },
|
|
241
|
-
],
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
expect(errors).toHaveLength(2);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
158
|
+
// validateCapabilityVersions tests live with the function in
|
|
159
|
+
// services/module-validator/capability-versions.test.ts.
|
|
247
160
|
|
|
248
161
|
// ── Token resolution: --token → env → secret store ────────────────────────────
|
|
249
162
|
|