@celilo/cli 0.5.0-alpha.7 → 0.5.0-alpha.9
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 +2 -2
- package/src/api-clients/proxmox.test.ts +78 -0
- package/src/api-clients/proxmox.ts +96 -1
- package/src/cli/command-registry.ts +32 -3
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/events.ts +8 -3
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +10 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +275 -260
- package/src/cli/commands/service-reconfigure.ts +171 -153
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/completion.ts +15 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/service-credential.ts +54 -0
- package/src/services/bus-interview.ts +232 -0
- package/src/services/deploy-validation.test.ts +52 -2
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/fleet-checks.test.ts +13 -0
- package/src/services/fleet-checks.ts +15 -0
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +30 -0
- package/src/templates/generator.ts +86 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/cli",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.9",
|
|
4
4
|
"description": "Celilo — home lab orchestration CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@aws-sdk/client-s3": "^3.1024.0",
|
|
55
55
|
"@celilo/capabilities": "^0.4.2",
|
|
56
56
|
"@celilo/cli-display": "^0.1.9",
|
|
57
|
-
"@celilo/event-bus": "^0.1.
|
|
57
|
+
"@celilo/event-bus": "^0.1.7",
|
|
58
58
|
"@clack/prompts": "^1.1.0",
|
|
59
59
|
"ajv": "^8.18.0",
|
|
60
60
|
"drizzle-orm": "^0.36.4",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import {
|
|
3
|
+
type ProxmoxClusterResource,
|
|
3
4
|
buildCloudImageVolid,
|
|
4
5
|
filterVmTemplates,
|
|
5
6
|
findNodeForVmid,
|
|
6
7
|
pollTaskUntilDone,
|
|
7
8
|
selectFreeVmid,
|
|
8
9
|
selectIsoStorage,
|
|
10
|
+
summarizeNodeCapacities,
|
|
9
11
|
} from './proxmox';
|
|
10
12
|
|
|
11
13
|
describe('findNodeForVmid (ISS-0090 — Proxmox is source of truth for current location)', () => {
|
|
@@ -36,6 +38,82 @@ describe('findNodeForVmid (ISS-0090 — Proxmox is source of truth for current l
|
|
|
36
38
|
});
|
|
37
39
|
});
|
|
38
40
|
|
|
41
|
+
describe('summarizeNodeCapacities (ISS-0060 — per-node capacity reconciled from Proxmox)', () => {
|
|
42
|
+
const GB = 1024 * 1024 * 1024;
|
|
43
|
+
const resources: ProxmoxClusterResource[] = [
|
|
44
|
+
{
|
|
45
|
+
type: 'node',
|
|
46
|
+
node: 'node3',
|
|
47
|
+
status: 'online',
|
|
48
|
+
maxmem: 16 * GB,
|
|
49
|
+
mem: 4 * GB,
|
|
50
|
+
maxcpu: 8,
|
|
51
|
+
cpu: 0.25,
|
|
52
|
+
maxdisk: 200 * GB,
|
|
53
|
+
disk: 50 * GB,
|
|
54
|
+
uptime: 3600,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'node',
|
|
58
|
+
node: 'node2',
|
|
59
|
+
status: 'offline',
|
|
60
|
+
maxmem: 8 * GB,
|
|
61
|
+
mem: 7 * GB,
|
|
62
|
+
maxcpu: 4,
|
|
63
|
+
cpu: 0.9,
|
|
64
|
+
maxdisk: 100 * GB,
|
|
65
|
+
disk: 90 * GB,
|
|
66
|
+
uptime: 0,
|
|
67
|
+
},
|
|
68
|
+
// Guest + storage rows must be ignored.
|
|
69
|
+
{ type: 'lxc', vmid: 200, node: 'node2', status: 'running' },
|
|
70
|
+
{ type: 'storage', node: 'node3', status: 'available' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
test('derives free RAM/CPU/disk and online state from node rows only', () => {
|
|
74
|
+
const caps = summarizeNodeCapacities(resources);
|
|
75
|
+
expect(caps).toHaveLength(2); // guest + storage rows dropped
|
|
76
|
+
// Sorted by node name → node2 first.
|
|
77
|
+
expect(caps[0]).toEqual({
|
|
78
|
+
node: 'node2',
|
|
79
|
+
online: false,
|
|
80
|
+
memTotalMb: 8192,
|
|
81
|
+
memFreeMb: 1024,
|
|
82
|
+
cpuCores: 4,
|
|
83
|
+
cpuUsedPct: 90,
|
|
84
|
+
diskTotalGb: 100,
|
|
85
|
+
diskFreeGb: 10,
|
|
86
|
+
uptimeSec: 0,
|
|
87
|
+
});
|
|
88
|
+
expect(caps[1]).toMatchObject({
|
|
89
|
+
node: 'node3',
|
|
90
|
+
online: true,
|
|
91
|
+
memFreeMb: 12288,
|
|
92
|
+
cpuUsedPct: 25,
|
|
93
|
+
diskFreeGb: 150,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('tolerates missing fields (treats absent capacity as 0, offline unless status==online)', () => {
|
|
98
|
+
const caps = summarizeNodeCapacities([{ type: 'node', node: 'bare' }]);
|
|
99
|
+
expect(caps[0]).toEqual({
|
|
100
|
+
node: 'bare',
|
|
101
|
+
online: false,
|
|
102
|
+
memTotalMb: 0,
|
|
103
|
+
memFreeMb: 0,
|
|
104
|
+
cpuCores: 0,
|
|
105
|
+
cpuUsedPct: 0,
|
|
106
|
+
diskTotalGb: 0,
|
|
107
|
+
diskFreeGb: 0,
|
|
108
|
+
uptimeSec: 0,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('empty inventory → no nodes', () => {
|
|
113
|
+
expect(summarizeNodeCapacities([])).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
39
117
|
describe('filterVmTemplates — extract VM templates from a /nodes/{node}/qemu listing', () => {
|
|
40
118
|
test('keeps only guests flagged template===1 and projects to {vmid, name}', () => {
|
|
41
119
|
const guests = [
|
|
@@ -28,7 +28,7 @@ interface ProxmoxError {
|
|
|
28
28
|
details?: Record<string, unknown>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
type ProxmoxResult<T> = { success: true; data: T } | ProxmoxError;
|
|
31
|
+
export type ProxmoxResult<T> = { success: true; data: T } | ProxmoxError;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Make an authenticated API request to Proxmox
|
|
@@ -543,6 +543,101 @@ export function findNodeForVmid(
|
|
|
543
543
|
return match?.node ?? null;
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
/**
|
|
547
|
+
* One row from `GET /cluster/resources`. The list is heterogeneous — `type`
|
|
548
|
+
* discriminates node / storage / guest rows. For `type: 'node'`, `status` is
|
|
549
|
+
* 'online'/'offline' and the mem/cpu/disk fields describe the node's capacity;
|
|
550
|
+
* for guests it's 'running'/'stopped' and `vmid` is set.
|
|
551
|
+
*/
|
|
552
|
+
export interface ProxmoxClusterResource {
|
|
553
|
+
type: string;
|
|
554
|
+
node?: string;
|
|
555
|
+
status?: string;
|
|
556
|
+
vmid?: number;
|
|
557
|
+
maxmem?: number; // bytes (node total RAM)
|
|
558
|
+
mem?: number; // bytes (node RAM in use)
|
|
559
|
+
maxcpu?: number; // cores
|
|
560
|
+
cpu?: number; // load fraction 0..1
|
|
561
|
+
maxdisk?: number; // bytes (node total disk on the relevant storage)
|
|
562
|
+
disk?: number; // bytes (disk in use)
|
|
563
|
+
uptime?: number; // seconds
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Per-node capacity reconciled from Proxmox (reality, never a cached DB value). */
|
|
567
|
+
export interface ProxmoxNodeCapacity {
|
|
568
|
+
node: string;
|
|
569
|
+
online: boolean;
|
|
570
|
+
memTotalMb: number;
|
|
571
|
+
memFreeMb: number;
|
|
572
|
+
cpuCores: number;
|
|
573
|
+
cpuUsedPct: number; // 0..100
|
|
574
|
+
diskTotalGb: number;
|
|
575
|
+
diskFreeGb: number;
|
|
576
|
+
uptimeSec: number;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
580
|
+
const BYTES_PER_GB = 1024 * 1024 * 1024;
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Summarize per-node capacity from a `/cluster/resources` list. Pure (Rule 10) —
|
|
584
|
+
* split from the network call for unit testing. Only `type: 'node'` rows count;
|
|
585
|
+
* guest and storage rows are ignored. Sorted by node name for stable output.
|
|
586
|
+
*/
|
|
587
|
+
export function summarizeNodeCapacities(
|
|
588
|
+
resources: ProxmoxClusterResource[],
|
|
589
|
+
): ProxmoxNodeCapacity[] {
|
|
590
|
+
return resources
|
|
591
|
+
.filter((r): r is ProxmoxClusterResource & { node: string } => r.type === 'node' && !!r.node)
|
|
592
|
+
.map((r) => {
|
|
593
|
+
const maxmem = r.maxmem ?? 0;
|
|
594
|
+
const mem = r.mem ?? 0;
|
|
595
|
+
const maxdisk = r.maxdisk ?? 0;
|
|
596
|
+
const disk = r.disk ?? 0;
|
|
597
|
+
return {
|
|
598
|
+
node: r.node,
|
|
599
|
+
online: r.status === 'online',
|
|
600
|
+
memTotalMb: Math.round(maxmem / BYTES_PER_MB),
|
|
601
|
+
memFreeMb: Math.round((maxmem - mem) / BYTES_PER_MB),
|
|
602
|
+
cpuCores: r.maxcpu ?? 0,
|
|
603
|
+
cpuUsedPct: Math.round((r.cpu ?? 0) * 100),
|
|
604
|
+
diskTotalGb: Math.round(maxdisk / BYTES_PER_GB),
|
|
605
|
+
diskFreeGb: Math.round((maxdisk - disk) / BYTES_PER_GB),
|
|
606
|
+
uptimeSec: r.uptime ?? 0,
|
|
607
|
+
};
|
|
608
|
+
})
|
|
609
|
+
.sort((a, b) => a.node.localeCompare(b.node));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Cohesive Proxmox introspection client (ISS-0060). Wraps the credentials so
|
|
614
|
+
* callers don't thread them through every call, and centralizes the live reads
|
|
615
|
+
* that let celilo treat Proxmox — not a cached DB row — as the source of truth
|
|
616
|
+
* for where containers live and how much room each node has.
|
|
617
|
+
*/
|
|
618
|
+
export class ProxmoxClient {
|
|
619
|
+
constructor(private readonly credentials: ProxmoxCredentials) {}
|
|
620
|
+
|
|
621
|
+
/** Raw cluster resource inventory (node + guest + storage rows). */
|
|
622
|
+
async clusterResources(): Promise<ProxmoxResult<ProxmoxClusterResource[]>> {
|
|
623
|
+
return makeProxmoxRequest<ProxmoxClusterResource[]>(this.credentials, '/cluster/resources');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Live per-node capacity (RAM/CPU/disk/online), reconciled from Proxmox. */
|
|
627
|
+
async nodeCapacities(): Promise<ProxmoxResult<ProxmoxNodeCapacity[]>> {
|
|
628
|
+
const result = await this.clusterResources();
|
|
629
|
+
if (!result.success) return result;
|
|
630
|
+
return { success: true, data: summarizeNodeCapacities(result.data) };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** The node a VMID currently lives on, or null if it isn't created yet. */
|
|
634
|
+
async nodeForVmid(vmid: number): Promise<ProxmoxResult<string | null>> {
|
|
635
|
+
const result = await this.clusterResources();
|
|
636
|
+
if (!result.success) return result;
|
|
637
|
+
return { success: true, data: findNodeForVmid(result.data, vmid) };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
546
641
|
/**
|
|
547
642
|
* List available LXC templates in storage
|
|
548
643
|
*/
|
|
@@ -666,8 +666,16 @@ export const COMMANDS: CommandDef[] = [
|
|
|
666
666
|
description: 'Add Proxmox container service',
|
|
667
667
|
flags: [
|
|
668
668
|
{ name: 'api-url', description: 'Proxmox API URL', takesValue: true },
|
|
669
|
-
{
|
|
670
|
-
|
|
669
|
+
{
|
|
670
|
+
name: 'api-token-id',
|
|
671
|
+
description: 'API token ID (or $PROXMOX_API_TOKEN_ID)',
|
|
672
|
+
takesValue: true,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: 'api-token-secret',
|
|
676
|
+
description: 'API token secret (or $PROXMOX_API_TOKEN_SECRET)',
|
|
677
|
+
takesValue: true,
|
|
678
|
+
},
|
|
671
679
|
{ name: 'node', description: 'Target node', takesValue: true },
|
|
672
680
|
{
|
|
673
681
|
name: 'zone',
|
|
@@ -682,7 +690,11 @@ export const COMMANDS: CommandDef[] = [
|
|
|
682
690
|
name: 'digitalocean',
|
|
683
691
|
description: 'Add DigitalOcean container service',
|
|
684
692
|
flags: [
|
|
685
|
-
{
|
|
693
|
+
{
|
|
694
|
+
name: 'api-token',
|
|
695
|
+
description: 'DigitalOcean API token (or $DIGITALOCEAN_API_TOKEN)',
|
|
696
|
+
takesValue: true,
|
|
697
|
+
},
|
|
686
698
|
{ name: 'region', description: 'Region', takesValue: true },
|
|
687
699
|
{
|
|
688
700
|
name: 'zone',
|
|
@@ -1057,6 +1069,23 @@ export const COMMANDS: CommandDef[] = [
|
|
|
1057
1069
|
},
|
|
1058
1070
|
],
|
|
1059
1071
|
},
|
|
1072
|
+
{
|
|
1073
|
+
name: 'proxmox',
|
|
1074
|
+
description: 'Proxmox cluster introspection (nodes, capacity)',
|
|
1075
|
+
subcommands: [
|
|
1076
|
+
{
|
|
1077
|
+
name: 'node',
|
|
1078
|
+
description: 'Proxmox node operations',
|
|
1079
|
+
subcommands: [
|
|
1080
|
+
{
|
|
1081
|
+
name: 'list',
|
|
1082
|
+
description: 'List cluster nodes with live capacity (RAM/CPU/disk)',
|
|
1083
|
+
args: [{ name: 'service-id', description: 'Proxmox service (optional if only one)' }],
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
},
|
|
1087
|
+
],
|
|
1088
|
+
},
|
|
1060
1089
|
{
|
|
1061
1090
|
name: 'subscribers',
|
|
1062
1091
|
description: 'Manage build-bus subscribers (cross-machine publish-event delivery)',
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Delete a specific backup entry and its storage file.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import { deleteBackupRecord, formatSize, getBackup } from '../../services/backup-metadata';
|
|
8
7
|
import { createStorageProvider, getBackupStorage } from '../../services/backup-storage';
|
|
8
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
9
9
|
import { celiloIntro, celiloOutro } from '../prompts';
|
|
10
10
|
import type { CommandResult } from '../types';
|
|
11
11
|
|
|
@@ -40,13 +40,16 @@ export async function handleBackupDelete(
|
|
|
40
40
|
console.log(` → ${storage?.storageId ?? '?'}: ${backup.storagePath}\n`);
|
|
41
41
|
|
|
42
42
|
if (!flags.force) {
|
|
43
|
-
const confirmed = await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const confirmed = await withInterviewSession(() =>
|
|
44
|
+
askConfirm({
|
|
45
|
+
scope: `backup:${backupId}`,
|
|
46
|
+
key: 'delete',
|
|
47
|
+
message: 'Delete this backup?',
|
|
48
|
+
defaultValue: false,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
47
51
|
|
|
48
|
-
if (
|
|
49
|
-
p.cancel('Operation cancelled');
|
|
52
|
+
if (!confirmed) {
|
|
50
53
|
return { success: false, error: 'Cancelled by user' };
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, statSync } from 'node:fs';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
|
-
import * as p from '@clack/prompts';
|
|
9
8
|
import { formatSize } from '../../services/backup-metadata';
|
|
9
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
10
10
|
import { celiloIntro, celiloOutro } from '../prompts';
|
|
11
11
|
import type { CommandResult } from '../types';
|
|
12
12
|
|
|
@@ -54,13 +54,16 @@ export async function handleBackupImport(
|
|
|
54
54
|
|
|
55
55
|
// Confirm unless --yes
|
|
56
56
|
if (!flags.yes) {
|
|
57
|
-
const confirmed = await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const confirmed = await withInterviewSession(() =>
|
|
58
|
+
askConfirm({
|
|
59
|
+
scope: `backup-import:${moduleId}`,
|
|
60
|
+
key: 'import',
|
|
61
|
+
message: `Import this file as a backup for ${moduleId}?`,
|
|
62
|
+
defaultValue: true,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!confirmed) {
|
|
64
67
|
return { success: false, error: 'Cancelled by user' };
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Restores from a backup archive, executing the module's on_restore hook.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import { formatSize, getBackup } from '../../services/backup-metadata';
|
|
8
7
|
import { restoreModuleBackup, restoreSystemStateBackup } from '../../services/backup-restore';
|
|
9
8
|
import { getBackupStorage } from '../../services/backup-storage';
|
|
9
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
10
10
|
import { celiloIntro, celiloOutro } from '../prompts';
|
|
11
11
|
import type { CommandResult } from '../types';
|
|
12
12
|
|
|
@@ -66,13 +66,16 @@ export async function handleBackupRestore(
|
|
|
66
66
|
|
|
67
67
|
// Confirm unless --yes
|
|
68
68
|
if (!flags.yes) {
|
|
69
|
-
const confirmed = await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
const confirmed = await withInterviewSession(() =>
|
|
70
|
+
askConfirm({
|
|
71
|
+
scope: `backup-restore:${backupId}`,
|
|
72
|
+
key: 'proceed',
|
|
73
|
+
message: 'Proceed with restore?',
|
|
74
|
+
defaultValue: false,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!confirmed) {
|
|
76
79
|
return { success: false, error: 'Cancelled by user' };
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -389,7 +389,7 @@ export async function handleEventsRepair(): Promise<CommandResult> {
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
const INTERVIEW_FAMILIES = ['config', 'secret', 'ensure', 'aspect'] as const;
|
|
392
|
+
const INTERVIEW_FAMILIES = ['config', 'secret', 'ensure', 'aspect', 'interview'] as const;
|
|
393
393
|
type InterviewFamily = (typeof INTERVIEW_FAMILIES)[number];
|
|
394
394
|
|
|
395
395
|
/** Classify a query event type into its interview family, or null if it isn't one. */
|
|
@@ -398,6 +398,7 @@ function interviewFamily(type: string): InterviewFamily | null {
|
|
|
398
398
|
if (type.startsWith('secret.required.')) return 'secret';
|
|
399
399
|
if (type.startsWith('ensure.required.')) return 'ensure';
|
|
400
400
|
if (type.startsWith('aspect.required.')) return 'aspect';
|
|
401
|
+
if (type.startsWith('interview.required.')) return 'interview';
|
|
401
402
|
return null;
|
|
402
403
|
}
|
|
403
404
|
|
|
@@ -428,6 +429,10 @@ function interviewFamily(type: string): InterviewFamily | null {
|
|
|
428
429
|
* base-module aspect, 'false' refuses it (ISS-0027).
|
|
429
430
|
* Replies { consented }; the deploy records the
|
|
430
431
|
* approval/denial.
|
|
432
|
+
* - interview.required.<scope>.<key> → value is the answer itself, shaped per
|
|
433
|
+
* the question's kind ('"node3"', 'true',
|
|
434
|
+
* '["a","b"]'); replies { value }. The generic
|
|
435
|
+
* operator-command interview family (ISS-0127).
|
|
431
436
|
*/
|
|
432
437
|
export async function handleEventsReply(
|
|
433
438
|
args: string[],
|
|
@@ -476,7 +481,7 @@ export async function handleEventsReply(
|
|
|
476
481
|
return {
|
|
477
482
|
success: false,
|
|
478
483
|
error: `Event ${queryId} is type '${query.type}', not an interview query.
|
|
479
|
-
Expected one of: config.required.* / secret.required.* / ensure.required.* / aspect.required.*`,
|
|
484
|
+
Expected one of: config.required.* / secret.required.* / ensure.required.* / aspect.required.* / interview.required.*`,
|
|
480
485
|
};
|
|
481
486
|
}
|
|
482
487
|
|
|
@@ -496,7 +501,7 @@ export async function handleEventsReply(
|
|
|
496
501
|
});
|
|
497
502
|
}
|
|
498
503
|
|
|
499
|
-
if (family === 'config') {
|
|
504
|
+
if (family === 'config' || family === 'interview') {
|
|
500
505
|
bus.emitRaw(`${query.type}.reply`, { value }, { replyFor: queryId, emittedBy });
|
|
501
506
|
return jsonResult({ ok: true, status: 'replied', queryId, type: query.type, family, value });
|
|
502
507
|
}
|