@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
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`',
|
|
@@ -267,27 +255,21 @@ export const COMMANDS: CommandDef[] = [
|
|
|
267
255
|
subcommands: [
|
|
268
256
|
{
|
|
269
257
|
name: 'import',
|
|
270
|
-
description:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
name: 'file',
|
|
274
|
-
description: 'Import from local filesystem',
|
|
275
|
-
args: [{ name: 'path', description: 'Module path', completion: 'directories' }],
|
|
276
|
-
},
|
|
258
|
+
description:
|
|
259
|
+
'Import a module from the registry (bare name) or from a local path (./, /, ~, or *.netapp)',
|
|
260
|
+
args: [
|
|
277
261
|
{
|
|
278
|
-
name: '
|
|
279
|
-
description: '
|
|
280
|
-
|
|
281
|
-
flags: [
|
|
282
|
-
{
|
|
283
|
-
name: 'registry',
|
|
284
|
-
description: 'Registry URL (overrides default celilo.computer)',
|
|
285
|
-
takesValue: true,
|
|
286
|
-
},
|
|
287
|
-
],
|
|
262
|
+
name: 'name-or-path',
|
|
263
|
+
description: 'Module name (registry) or filesystem path',
|
|
264
|
+
completion: 'directories',
|
|
288
265
|
},
|
|
289
266
|
],
|
|
290
267
|
flags: [
|
|
268
|
+
{
|
|
269
|
+
name: 'registry',
|
|
270
|
+
description: 'Registry URL (overrides default celilo.computer)',
|
|
271
|
+
takesValue: true,
|
|
272
|
+
},
|
|
291
273
|
{
|
|
292
274
|
name: 'target',
|
|
293
275
|
description: 'Target directory',
|
|
@@ -489,12 +471,6 @@ export const COMMANDS: CommandDef[] = [
|
|
|
489
471
|
},
|
|
490
472
|
],
|
|
491
473
|
},
|
|
492
|
-
{
|
|
493
|
-
name: 'install',
|
|
494
|
-
description: 'Download and import a module from the registry',
|
|
495
|
-
args: [{ name: 'name', description: 'Module name' }],
|
|
496
|
-
flags: [{ name: 'registry', description: 'Registry URL', takesValue: true }],
|
|
497
|
-
},
|
|
498
474
|
{
|
|
499
475
|
name: 'search',
|
|
500
476
|
description: 'Search the module registry',
|
|
@@ -815,6 +791,18 @@ export const COMMANDS: CommandDef[] = [
|
|
|
815
791
|
},
|
|
816
792
|
],
|
|
817
793
|
},
|
|
794
|
+
{
|
|
795
|
+
name: 'doctor',
|
|
796
|
+
description: 'Diagnose system prerequisites and @celilo/* version drift',
|
|
797
|
+
flags: [
|
|
798
|
+
{
|
|
799
|
+
name: 'fix',
|
|
800
|
+
description:
|
|
801
|
+
'Repair drift by `bun link`-ing each drifted @celilo/* package from the workspace',
|
|
802
|
+
takesValue: false,
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
},
|
|
818
806
|
],
|
|
819
807
|
},
|
|
820
808
|
{
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the import-routing rule that disambiguates between
|
|
3
|
+
* `celilo module import caddy` → registry
|
|
4
|
+
* `celilo module import ./modules/caddy` → local path
|
|
5
|
+
*
|
|
6
|
+
* Pure function over the argument string; no network, no filesystem.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import { classifyImportArg } from './module-import';
|
|
11
|
+
|
|
12
|
+
describe('classifyImportArg', () => {
|
|
13
|
+
test('routes bare kebab names to the registry', () => {
|
|
14
|
+
expect(classifyImportArg('caddy')).toBe('name');
|
|
15
|
+
expect(classifyImportArg('homebridge')).toBe('name');
|
|
16
|
+
expect(classifyImportArg('dns-external')).toBe('name');
|
|
17
|
+
expect(classifyImportArg('a')).toBe('name');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('routes leading-dot relative paths to the filesystem', () => {
|
|
21
|
+
expect(classifyImportArg('./modules/caddy')).toBe('path');
|
|
22
|
+
expect(classifyImportArg('../caddy')).toBe('path');
|
|
23
|
+
expect(classifyImportArg('./caddy')).toBe('path');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('routes leading-slash absolute paths to the filesystem', () => {
|
|
27
|
+
expect(classifyImportArg('/tmp/caddy')).toBe('path');
|
|
28
|
+
expect(classifyImportArg('/abs/path/to/module')).toBe('path');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('routes tilde-expanded paths to the filesystem', () => {
|
|
32
|
+
expect(classifyImportArg('~')).toBe('path');
|
|
33
|
+
expect(classifyImportArg('~/dev/caddy')).toBe('path');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('routes any path containing / to the filesystem', () => {
|
|
37
|
+
expect(classifyImportArg('modules/caddy')).toBe('path');
|
|
38
|
+
expect(classifyImportArg('a/b/c')).toBe('path');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('routes .netapp filenames to the filesystem (registry never serves them by name)', () => {
|
|
42
|
+
expect(classifyImportArg('caddy.netapp')).toBe('path');
|
|
43
|
+
expect(classifyImportArg('homebridge-1.0.0.netapp')).toBe('path');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('does not confuse versioned names with paths (no slash, no .netapp)', () => {
|
|
47
|
+
// Future: registry may accept `name@version` syntax. Today this routes
|
|
48
|
+
// to "name" — KEBAB_NAME validation in handleModuleImport will reject
|
|
49
|
+
// until the syntax is implemented.
|
|
50
|
+
expect(classifyImportArg('caddy@1.0.0')).toBe('name');
|
|
51
|
+
});
|
|
52
|
+
});
|