@celilo/core 0.5.0 → 0.7.0
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 -3
- package/src/command-registry.ts +68 -0
- package/src/protocol.ts +14 -0
- package/src/remote-client.ts +26 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Lightweight shared core for Celilo CLI tools — command registry, NDJSON API protocol, and remote SSH client. No Ink/React/drizzle/aws-sdk transitive deps, so an MCP server can reach the transport without installing the full CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"directory": "packages/core"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@celilo/cli-display": "^0.
|
|
28
|
-
"@clack/prompts": "^1.1.0",
|
|
27
|
+
"@celilo/cli-display": "^0.2.0",
|
|
29
28
|
"zod": "^3.24.1"
|
|
30
29
|
}
|
|
31
30
|
}
|
package/src/command-registry.ts
CHANGED
|
@@ -168,6 +168,18 @@ export const COMMANDS: CommandDef[] = [
|
|
|
168
168
|
{ name: 'limit', description: 'Max rows to return', takesValue: true },
|
|
169
169
|
],
|
|
170
170
|
},
|
|
171
|
+
{
|
|
172
|
+
name: 'list-failed',
|
|
173
|
+
description: 'List failed/abandoned deliveries with a true total',
|
|
174
|
+
flags: [
|
|
175
|
+
{
|
|
176
|
+
name: 'subscriber',
|
|
177
|
+
description: 'Restrict to a subscriber by name',
|
|
178
|
+
takesValue: true,
|
|
179
|
+
},
|
|
180
|
+
{ name: 'limit', description: 'Max rows to return', takesValue: true },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
171
183
|
{
|
|
172
184
|
name: 'list-unanswered',
|
|
173
185
|
description: 'List interview questions nobody has answered yet',
|
|
@@ -569,6 +581,62 @@ export const COMMANDS: CommandDef[] = [
|
|
|
569
581
|
},
|
|
570
582
|
],
|
|
571
583
|
},
|
|
584
|
+
{
|
|
585
|
+
name: 'pause',
|
|
586
|
+
description:
|
|
587
|
+
"Take a module out of celilo's control plane without uninstalling it (keeps running unless --stop-infra)",
|
|
588
|
+
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
589
|
+
flags: [
|
|
590
|
+
{
|
|
591
|
+
name: 'cascade',
|
|
592
|
+
description: 'Also pause every module that depends on it, consumers first',
|
|
593
|
+
takesValue: false,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: 'stop-infra',
|
|
597
|
+
description: "Also stop the module's container or service",
|
|
598
|
+
takesValue: false,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'reason',
|
|
602
|
+
description: 'Why the pause was taken, recorded on the module',
|
|
603
|
+
takesValue: true,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'dry-run',
|
|
607
|
+
description: 'Print the ordered plan and change nothing',
|
|
608
|
+
takesValue: false,
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'yes',
|
|
612
|
+
description: 'Skip the cascade confirmation',
|
|
613
|
+
takesValue: false,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'unpause',
|
|
619
|
+
description:
|
|
620
|
+
'Return a paused module to service by redeploying it, rebinding it to the providers present now',
|
|
621
|
+
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
622
|
+
flags: [
|
|
623
|
+
{
|
|
624
|
+
name: 'cascade',
|
|
625
|
+
description: 'Also unpause everything that depends on it, providers first',
|
|
626
|
+
takesValue: false,
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'dry-run',
|
|
630
|
+
description: 'Print the ordered plan and change nothing',
|
|
631
|
+
takesValue: false,
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'yes',
|
|
635
|
+
description: 'Skip the cascade confirmation',
|
|
636
|
+
takesValue: false,
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
572
640
|
{
|
|
573
641
|
name: 'update',
|
|
574
642
|
description:
|
package/src/protocol.ts
CHANGED
|
@@ -108,6 +108,20 @@ export const ResultMessageSchema = z.object({
|
|
|
108
108
|
type: z.literal('result'),
|
|
109
109
|
success: z.boolean(),
|
|
110
110
|
exitCode: z.number(),
|
|
111
|
+
/**
|
|
112
|
+
* Fleet-level conditions the operator should know about, regardless of what
|
|
113
|
+
* they ran. Today that is exactly one thing: modules are paused
|
|
114
|
+
* (openspec/changes/module-pause-lifecycle, design D7).
|
|
115
|
+
*
|
|
116
|
+
* Attached to EVERY result, including commands unrelated to the paused
|
|
117
|
+
* modules, and that is the point — a pause switches off the alerting that
|
|
118
|
+
* would otherwise report those modules as down, so the only remaining defence
|
|
119
|
+
* is that the paused-ness is discovered incidentally, while doing something
|
|
120
|
+
* else. A warning the operator has to go looking for does not do that job.
|
|
121
|
+
*
|
|
122
|
+
* Optional so an older client parses a newer server's result unchanged.
|
|
123
|
+
*/
|
|
124
|
+
warnings: z.array(z.string()).optional(),
|
|
111
125
|
});
|
|
112
126
|
export type ResultMessage = z.infer<typeof ResultMessageSchema>;
|
|
113
127
|
|
package/src/remote-client.ts
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
type DisplayWriter,
|
|
15
|
+
ProgressDisplay,
|
|
16
|
+
isCancel,
|
|
17
|
+
cancel as uiCancel,
|
|
18
|
+
confirm as uiConfirm,
|
|
19
|
+
multiselect as uiMultiselect,
|
|
20
|
+
select as uiSelect,
|
|
21
|
+
text as uiText,
|
|
22
|
+
} from '@celilo/cli-display';
|
|
15
23
|
import {
|
|
16
24
|
type ClientMessage,
|
|
17
25
|
type InterviewMessage,
|
|
@@ -40,15 +48,17 @@ export class InterviewUnanswerableError extends Error {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
|
-
* Render a forwarded interview
|
|
51
|
+
* Render a forwarded interview on the terminal, returning the operator's answer.
|
|
44
52
|
*
|
|
45
|
-
* Refuses to prompt when stdin isn't a TTY
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* Refuses to prompt when stdin isn't a TTY, and keeps its own check rather than
|
|
54
|
+
* relying on the prompt layer's: this is the boundary that must produce an
|
|
55
|
+
* `unanswerable` message, not an exception. The hazard it guards against is
|
|
56
|
+
* specific — a prompt that reads keypresses off a non-terminal stdin (an MCP
|
|
57
|
+
* server's JSON-RPC stream, a CI harness) takes the very next newline as a
|
|
58
|
+
* submit at the question's `defaultValue`, and the answer is indistinguishable
|
|
59
|
+
* from a considered human decision. That is how a live rollout came to report
|
|
60
|
+
* "operator declined" for a breaking update nobody was asked about. A client
|
|
61
|
+
* with no terminal must say so instead of guessing.
|
|
52
62
|
*/
|
|
53
63
|
async function defaultRenderInterview(iv: InterviewMessage): Promise<unknown> {
|
|
54
64
|
if (!process.stdin.isTTY) {
|
|
@@ -61,30 +71,30 @@ async function defaultRenderInterview(iv: InterviewMessage): Promise<unknown> {
|
|
|
61
71
|
let result: unknown;
|
|
62
72
|
switch (iv.kind) {
|
|
63
73
|
case 'confirm':
|
|
64
|
-
result = await
|
|
74
|
+
result = await uiConfirm({
|
|
65
75
|
message: iv.message,
|
|
66
76
|
initialValue: iv.defaultValue === 'true',
|
|
67
77
|
});
|
|
68
78
|
break;
|
|
69
79
|
case 'select':
|
|
70
|
-
result = await
|
|
80
|
+
result = await uiSelect({ message: iv.message, options });
|
|
71
81
|
break;
|
|
72
82
|
case 'multiselect':
|
|
73
|
-
result = await
|
|
83
|
+
result = await uiMultiselect({
|
|
74
84
|
message: iv.message,
|
|
75
85
|
options,
|
|
76
86
|
required: iv.required ?? false,
|
|
77
87
|
});
|
|
78
88
|
break;
|
|
79
89
|
default:
|
|
80
|
-
result = await
|
|
90
|
+
result = await uiText({
|
|
81
91
|
message: iv.message,
|
|
82
92
|
defaultValue: iv.defaultValue,
|
|
83
93
|
placeholder: iv.placeholder,
|
|
84
94
|
});
|
|
85
95
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
96
|
+
if (isCancel(result)) {
|
|
97
|
+
uiCancel('Cancelled.');
|
|
88
98
|
process.exit(130);
|
|
89
99
|
}
|
|
90
100
|
return result;
|