@celilo/cli 0.3.2 → 0.3.4
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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.1.10",
|
|
56
56
|
"@celilo/cli-display": "^0.1.6",
|
|
57
|
-
"@celilo/event-bus": "^0.1.
|
|
57
|
+
"@celilo/event-bus": "^0.1.3",
|
|
58
58
|
"@clack/prompts": "^1.1.0",
|
|
59
59
|
"ajv": "^8.18.0",
|
|
60
60
|
"drizzle-orm": "^0.36.4",
|
|
@@ -442,29 +442,13 @@ export async function interviewForMissingConfig(
|
|
|
442
442
|
payload,
|
|
443
443
|
);
|
|
444
444
|
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
await db
|
|
452
|
-
.insert(moduleConfigs)
|
|
453
|
-
.values({
|
|
454
|
-
moduleId,
|
|
455
|
-
key: variable.name,
|
|
456
|
-
value,
|
|
457
|
-
valueJson: null,
|
|
458
|
-
createdAt: new Date(),
|
|
459
|
-
updatedAt: new Date(),
|
|
460
|
-
})
|
|
461
|
-
.onConflictDoUpdate({
|
|
462
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
463
|
-
set: {
|
|
464
|
-
value,
|
|
465
|
-
updatedAt: new Date(),
|
|
466
|
-
},
|
|
467
|
-
});
|
|
445
|
+
// Persist via writeModuleConfigKey so the (string value,
|
|
446
|
+
// typed valueJson) pair is set correctly. Downstream readers
|
|
447
|
+
// (validate_config hooks, capability resolvers) deserialize
|
|
448
|
+
// typed values from valueJson — earlier I was only writing
|
|
449
|
+
// value with valueJson:null, which broke `array`/`object`
|
|
450
|
+
// typed configs that consumers expected to be JSON-parsed.
|
|
451
|
+
await writeModuleConfigKey(moduleId, variable.name, reply.value, db);
|
|
468
452
|
|
|
469
453
|
configured.push(variable.name);
|
|
470
454
|
}
|
|
@@ -54,50 +54,70 @@ export function startTerminalResponder(): TerminalResponderHandle {
|
|
|
54
54
|
// re-fire (e.g. validation re-emit) doesn't double-prompt.
|
|
55
55
|
const handled = new Set<number>();
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// Pattern matches exactly `config.required.<module>.<key>` (4 dot
|
|
58
|
+
// segments). The reply we emit is `config.required.<m>.<k>.reply`
|
|
59
|
+
// (5 segments) — wouldn't match — but `**` would have matched, and
|
|
60
|
+
// we'd recursively try to prompt for our own reply event. The
|
|
61
|
+
// explicit replyFor === null check below is defense in depth in
|
|
62
|
+
// case any responder ever emits a reply with the same shape.
|
|
63
|
+
const watch = bus.watch('config.required.*.*', async (event) => {
|
|
64
|
+
// Reply events carry replyFor; queries don't. Skip anything that
|
|
65
|
+
// looks like a reply, even if it accidentally matches the pattern.
|
|
66
|
+
if (event.replyFor !== null) return;
|
|
58
67
|
if (handled.has(event.id)) return;
|
|
59
68
|
handled.add(event.id);
|
|
60
69
|
|
|
61
70
|
const payload = event.payload as ConfigRequiredPayload;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const value = await promptText({
|
|
68
|
-
message,
|
|
69
|
-
validate: (val) => {
|
|
70
|
-
if (payload.required && (!val || val.trim() === '')) {
|
|
71
|
-
return 'This field is required';
|
|
72
|
-
}
|
|
73
|
-
if (payload.pattern && val) {
|
|
74
|
-
const re = new RegExp(payload.pattern);
|
|
75
|
-
if (!re.test(val)) return `Value must match: ${payload.pattern}`;
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Coerce to the declared type. clack returns string; the
|
|
81
|
-
// responder honors the type contract so the deploy doesn't
|
|
82
|
-
// have to second-guess the reply shape.
|
|
83
|
-
const coerced = coerceValue(value, payload.type);
|
|
84
|
-
|
|
85
|
-
bus.emitRaw(
|
|
86
|
-
`${event.type}.reply`,
|
|
87
|
-
{ value: coerced },
|
|
88
|
-
{
|
|
89
|
-
replyFor: event.id,
|
|
90
|
-
emittedBy: me,
|
|
91
|
-
},
|
|
71
|
+
// Defensive: if the payload is malformed (no module/key), don't
|
|
72
|
+
// render a "undefined.undefined:" prompt — log and skip.
|
|
73
|
+
if (!payload || typeof payload.module !== 'string' || typeof payload.key !== 'string') {
|
|
74
|
+
log.warn(
|
|
75
|
+
`Terminal responder skipped malformed event ${event.type} (id ${event.id}): missing module/key`,
|
|
92
76
|
);
|
|
93
|
-
|
|
94
|
-
// Reply with an error-shaped payload so the deploy can surface
|
|
95
|
-
// it cleanly. The deploy can decide whether to re-emit (for
|
|
96
|
-
// recoverable errors) or fail.
|
|
97
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
-
log.warn(`Terminal responder error for ${event.type}: ${message}`);
|
|
99
|
-
bus.emitRaw(`${event.type}.reply`, { error: message }, { replyFor: event.id, emittedBy: me });
|
|
77
|
+
return;
|
|
100
78
|
}
|
|
79
|
+
|
|
80
|
+
const message = payload.description
|
|
81
|
+
? `${payload.module}.${payload.key} — ${payload.description}:`
|
|
82
|
+
: `${payload.module}.${payload.key}:`;
|
|
83
|
+
|
|
84
|
+
// Hint shown next to the prompt. For non-string types, tell the
|
|
85
|
+
// user what shape we're expecting so they don't get a coercion
|
|
86
|
+
// failure mid-deploy.
|
|
87
|
+
const typeHint = describeTypeHint(payload.type);
|
|
88
|
+
|
|
89
|
+
const value = await promptText({
|
|
90
|
+
message: typeHint ? `${message} (${typeHint})` : message,
|
|
91
|
+
validate: (val) => {
|
|
92
|
+
if (payload.required && (!val || val.trim() === '')) {
|
|
93
|
+
return 'This field is required';
|
|
94
|
+
}
|
|
95
|
+
if (payload.pattern && val) {
|
|
96
|
+
const re = new RegExp(payload.pattern);
|
|
97
|
+
if (!re.test(val)) return `Value must match: ${payload.pattern}`;
|
|
98
|
+
}
|
|
99
|
+
// Type-shape validation runs HERE, at submit time, so clack
|
|
100
|
+
// re-prompts on bad input rather than letting a malformed
|
|
101
|
+
// value through that fails post-submit.
|
|
102
|
+
try {
|
|
103
|
+
coerceValue(val, payload.type);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return err instanceof Error ? err.message : String(err);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Already validated above; coerce can't throw here.
|
|
111
|
+
const coerced = coerceValue(value, payload.type);
|
|
112
|
+
|
|
113
|
+
bus.emitRaw(
|
|
114
|
+
`${event.type}.reply`,
|
|
115
|
+
{ value: coerced },
|
|
116
|
+
{
|
|
117
|
+
replyFor: event.id,
|
|
118
|
+
emittedBy: me,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
101
121
|
});
|
|
102
122
|
|
|
103
123
|
return {
|
|
@@ -108,7 +128,37 @@ export function startTerminalResponder(): TerminalResponderHandle {
|
|
|
108
128
|
};
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Short hint shown in the prompt for non-string types so the operator
|
|
133
|
+
* isn't guessing what shape we want. Returns null for plain strings —
|
|
134
|
+
* no hint needed.
|
|
135
|
+
*/
|
|
136
|
+
function describeTypeHint(type: ConfigRequiredPayload['type']): string | null {
|
|
137
|
+
switch (type) {
|
|
138
|
+
case 'string':
|
|
139
|
+
return null;
|
|
140
|
+
case 'integer':
|
|
141
|
+
return 'integer';
|
|
142
|
+
case 'number':
|
|
143
|
+
return 'number';
|
|
144
|
+
case 'boolean':
|
|
145
|
+
return 'true/false/yes/no/y/n/1/0';
|
|
146
|
+
case 'array':
|
|
147
|
+
return 'JSON array, e.g. ["a","b"]';
|
|
148
|
+
case 'object':
|
|
149
|
+
return 'JSON object, e.g. {"k":"v"}';
|
|
150
|
+
default:
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function coerceValue(raw: string | undefined, type: ConfigRequiredPayload['type']): unknown {
|
|
156
|
+
// clack returns undefined when the user cancels (Ctrl+C). Bubble
|
|
157
|
+
// that up as an error so validate sees it and the responder can
|
|
158
|
+
// skip the reply rather than emit a malformed one.
|
|
159
|
+
if (raw === undefined) {
|
|
160
|
+
throw new Error('Cancelled');
|
|
161
|
+
}
|
|
112
162
|
switch (type) {
|
|
113
163
|
case 'string':
|
|
114
164
|
return raw;
|