@celilo/cli 0.3.24 → 0.3.25
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/manifest/schema.ts +11 -0
- package/src/services/bus-interview.ts +10 -0
- package/src/services/config-interview.ts +22 -0
- package/src/services/deploy-validation.test.ts +32 -0
- package/src/services/deploy-validation.ts +16 -0
- package/src/services/terminal-responder.ts +45 -2
package/package.json
CHANGED
package/src/manifest/schema.ts
CHANGED
|
@@ -112,6 +112,17 @@ export const SecretDeclareSchema = z.object({
|
|
|
112
112
|
// generic; namecheap wants 'Domain' / 'Password'.
|
|
113
113
|
key_label: z.string().optional(),
|
|
114
114
|
value_label: z.string().optional(),
|
|
115
|
+
// For `type: string-map` only: optional regex applied to each entered
|
|
116
|
+
// key. The responder rejects mismatches and re-prompts. Use for
|
|
117
|
+
// domain-shape validation (apex-only), hostname/email shape, etc.
|
|
118
|
+
// Pair with key_pattern_message to give the operator the rule in
|
|
119
|
+
// plain English when they hit it.
|
|
120
|
+
key_pattern: z.string().optional(),
|
|
121
|
+
key_pattern_message: z.string().optional(),
|
|
122
|
+
// Same idea for value validation. Less commonly useful for secrets
|
|
123
|
+
// (passwords vary), but provided for symmetry.
|
|
124
|
+
value_pattern: z.string().optional(),
|
|
125
|
+
value_pattern_message: z.string().optional(),
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
/**
|
|
@@ -87,6 +87,16 @@ export interface SecretRequiredPayload {
|
|
|
87
87
|
*/
|
|
88
88
|
key_label?: string;
|
|
89
89
|
value_label?: string;
|
|
90
|
+
/**
|
|
91
|
+
* For `type: string-map` only — optional regex applied to each
|
|
92
|
+
* entered key/value. Mismatches are rejected at input time and the
|
|
93
|
+
* prompt re-fires. Pair with the `_message` variants for a
|
|
94
|
+
* plain-English explanation when the operator hits one.
|
|
95
|
+
*/
|
|
96
|
+
key_pattern?: string;
|
|
97
|
+
key_pattern_message?: string;
|
|
98
|
+
value_pattern?: string;
|
|
99
|
+
value_pattern_message?: string;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export interface SecretAck {
|
|
@@ -70,6 +70,10 @@ interface SecretDeclareLike {
|
|
|
70
70
|
description?: string;
|
|
71
71
|
key_label?: string;
|
|
72
72
|
value_label?: string;
|
|
73
|
+
key_pattern?: string;
|
|
74
|
+
key_pattern_message?: string;
|
|
75
|
+
value_pattern?: string;
|
|
76
|
+
value_pattern_message?: string;
|
|
73
77
|
generate?: { method: string; length: number; encoding: string };
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -89,6 +93,11 @@ function coerceSecretDeclare(entry: unknown): SecretDeclareLike | null {
|
|
|
89
93
|
if (typeof e.description === 'string') out.description = e.description;
|
|
90
94
|
if (typeof e.key_label === 'string') out.key_label = e.key_label;
|
|
91
95
|
if (typeof e.value_label === 'string') out.value_label = e.value_label;
|
|
96
|
+
if (typeof e.key_pattern === 'string') out.key_pattern = e.key_pattern;
|
|
97
|
+
if (typeof e.key_pattern_message === 'string') out.key_pattern_message = e.key_pattern_message;
|
|
98
|
+
if (typeof e.value_pattern === 'string') out.value_pattern = e.value_pattern;
|
|
99
|
+
if (typeof e.value_pattern_message === 'string')
|
|
100
|
+
out.value_pattern_message = e.value_pattern_message;
|
|
92
101
|
if (typeof e.generate === 'object' && e.generate !== null) {
|
|
93
102
|
const g = e.generate as Record<string, unknown>;
|
|
94
103
|
if (
|
|
@@ -129,6 +138,10 @@ export async function findMissingSecrets(
|
|
|
129
138
|
type: secret.type,
|
|
130
139
|
key_label: secret.key_label,
|
|
131
140
|
value_label: secret.value_label,
|
|
141
|
+
key_pattern: secret.key_pattern,
|
|
142
|
+
key_pattern_message: secret.key_pattern_message,
|
|
143
|
+
value_pattern: secret.value_pattern,
|
|
144
|
+
value_pattern_message: secret.value_pattern_message,
|
|
132
145
|
generate: secret.generate,
|
|
133
146
|
});
|
|
134
147
|
}
|
|
@@ -161,6 +174,11 @@ export interface MissingVariable {
|
|
|
161
174
|
/** For `type: string-map` only — labels shown in the add-loop prompt. */
|
|
162
175
|
key_label?: string;
|
|
163
176
|
value_label?: string;
|
|
177
|
+
/** For `type: string-map` only — optional regex validation per entry. */
|
|
178
|
+
key_pattern?: string;
|
|
179
|
+
key_pattern_message?: string;
|
|
180
|
+
value_pattern?: string;
|
|
181
|
+
value_pattern_message?: string;
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
export interface InterviewResult {
|
|
@@ -735,6 +753,10 @@ export async function interviewForMissingSecrets(
|
|
|
735
753
|
: undefined,
|
|
736
754
|
key_label: variable.key_label,
|
|
737
755
|
value_label: variable.value_label,
|
|
756
|
+
key_pattern: variable.key_pattern,
|
|
757
|
+
key_pattern_message: variable.key_pattern_message,
|
|
758
|
+
value_pattern: variable.value_pattern,
|
|
759
|
+
value_pattern_message: variable.value_pattern_message,
|
|
738
760
|
};
|
|
739
761
|
await busInterview<SecretAck>(EVENT_TYPES.secretRequired(moduleId, variable.name), payload);
|
|
740
762
|
log.success(`Saved ${variable.name}`);
|
|
@@ -260,4 +260,36 @@ describe('findMissingSecrets (shared)', () => {
|
|
|
260
260
|
);
|
|
261
261
|
expect(missing.map((m) => m.name)).toEqual(['still_missing']);
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
test('passes through key_pattern / value_pattern + their messages', async () => {
|
|
265
|
+
// The terminal-responder reads these off the bus payload to apply
|
|
266
|
+
// input-time regex validation. Without manifest → MissingVariable
|
|
267
|
+
// propagation, the responder never sees them and operators end
|
|
268
|
+
// up entering invalid keys (e.g. 'www.lunacycle.net' instead of
|
|
269
|
+
// the apex 'lunacycle.net').
|
|
270
|
+
const missing = await findMissingSecrets(
|
|
271
|
+
'testmod',
|
|
272
|
+
{
|
|
273
|
+
secrets: {
|
|
274
|
+
declares: [
|
|
275
|
+
{
|
|
276
|
+
name: 'ddns_passwords',
|
|
277
|
+
type: 'string-map',
|
|
278
|
+
required: true,
|
|
279
|
+
key_pattern: '^[a-z0-9-]+\\.[a-z]{2,}$',
|
|
280
|
+
key_pattern_message: 'apex domain only — drop the www.',
|
|
281
|
+
value_pattern: '^.{8,}$',
|
|
282
|
+
value_pattern_message: 'min 8 chars',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
db,
|
|
288
|
+
);
|
|
289
|
+
expect(missing).toHaveLength(1);
|
|
290
|
+
expect(missing[0].key_pattern).toBe('^[a-z0-9-]+\\.[a-z]{2,}$');
|
|
291
|
+
expect(missing[0].key_pattern_message).toBe('apex domain only — drop the www.');
|
|
292
|
+
expect(missing[0].value_pattern).toBe('^.{8,}$');
|
|
293
|
+
expect(missing[0].value_pattern_message).toBe('min 8 chars');
|
|
294
|
+
});
|
|
263
295
|
});
|
|
@@ -33,6 +33,10 @@ export interface ValidationResult {
|
|
|
33
33
|
/** For `type: string-map` only — labels shown in the add-loop prompt. */
|
|
34
34
|
key_label?: string;
|
|
35
35
|
value_label?: string;
|
|
36
|
+
key_pattern?: string;
|
|
37
|
+
key_pattern_message?: string;
|
|
38
|
+
value_pattern?: string;
|
|
39
|
+
value_pattern_message?: string;
|
|
36
40
|
}>;
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -390,6 +394,10 @@ export async function findMissingRequiredVariables(
|
|
|
390
394
|
generate?: { method: string; length: number; encoding: string };
|
|
391
395
|
key_label?: string;
|
|
392
396
|
value_label?: string;
|
|
397
|
+
key_pattern?: string;
|
|
398
|
+
key_pattern_message?: string;
|
|
399
|
+
value_pattern?: string;
|
|
400
|
+
value_pattern_message?: string;
|
|
393
401
|
}>
|
|
394
402
|
> {
|
|
395
403
|
const missing: Array<{
|
|
@@ -403,6 +411,10 @@ export async function findMissingRequiredVariables(
|
|
|
403
411
|
generate?: { method: string; length: number; encoding: string };
|
|
404
412
|
key_label?: string;
|
|
405
413
|
value_label?: string;
|
|
414
|
+
key_pattern?: string;
|
|
415
|
+
key_pattern_message?: string;
|
|
416
|
+
value_pattern?: string;
|
|
417
|
+
value_pattern_message?: string;
|
|
406
418
|
}> = [];
|
|
407
419
|
|
|
408
420
|
// Check declared variables (user config, capability, system, infrastructure)
|
|
@@ -466,6 +478,10 @@ export async function findMissingRequiredVariables(
|
|
|
466
478
|
generate: s.generate,
|
|
467
479
|
key_label: s.key_label,
|
|
468
480
|
value_label: s.value_label,
|
|
481
|
+
key_pattern: s.key_pattern,
|
|
482
|
+
key_pattern_message: s.key_pattern_message,
|
|
483
|
+
value_pattern: s.value_pattern,
|
|
484
|
+
value_pattern_message: s.value_pattern_message,
|
|
469
485
|
});
|
|
470
486
|
}
|
|
471
487
|
|
|
@@ -397,6 +397,21 @@ async function promptForStringMap(payload: SecretRequiredPayload): Promise<strin
|
|
|
397
397
|
`Add ${keyLabel.toLowerCase()} → ${valueLabel.toLowerCase()} entries one at a time. Press Enter on an empty ${keyLabel.toLowerCase()} when done.`,
|
|
398
398
|
);
|
|
399
399
|
|
|
400
|
+
// Compile the optional regex validators once. An invalid regex (i.e.
|
|
401
|
+
// a typo in the manifest) gets surfaced once here; we treat it as
|
|
402
|
+
// "no validation" rather than wedging the whole interview.
|
|
403
|
+
const keyRegex = compileMaybeRegex(
|
|
404
|
+
payload.key_pattern,
|
|
405
|
+
`${payload.module}.${payload.key} key_pattern`,
|
|
406
|
+
);
|
|
407
|
+
const valueRegex = compileMaybeRegex(
|
|
408
|
+
payload.value_pattern,
|
|
409
|
+
`${payload.module}.${payload.key} value_pattern`,
|
|
410
|
+
);
|
|
411
|
+
const keyPatternMessage = payload.key_pattern_message ?? `must match: ${payload.key_pattern}`;
|
|
412
|
+
const valuePatternMessage =
|
|
413
|
+
payload.value_pattern_message ?? `must match: ${payload.value_pattern}`;
|
|
414
|
+
|
|
400
415
|
const collected: Record<string, string> = {};
|
|
401
416
|
|
|
402
417
|
while (true) {
|
|
@@ -405,7 +420,14 @@ async function promptForStringMap(payload: SecretRequiredPayload): Promise<strin
|
|
|
405
420
|
|
|
406
421
|
const key = await promptText({
|
|
407
422
|
message: keyMessage,
|
|
408
|
-
|
|
423
|
+
// promptText empties + falsy returns terminate the loop, so
|
|
424
|
+
// validate has to allow empty input. Apply the key regex only
|
|
425
|
+
// to non-empty values so the operator can still finish the loop.
|
|
426
|
+
validate: (val) => {
|
|
427
|
+
if (!val || val.trim() === '') return undefined;
|
|
428
|
+
if (keyRegex && !keyRegex.test(val.trim())) return keyPatternMessage;
|
|
429
|
+
return undefined;
|
|
430
|
+
},
|
|
409
431
|
});
|
|
410
432
|
if (key === undefined) {
|
|
411
433
|
// User cancelled (Ctrl-C). Return undefined so the responder
|
|
@@ -429,7 +451,11 @@ async function promptForStringMap(payload: SecretRequiredPayload): Promise<strin
|
|
|
429
451
|
|
|
430
452
|
const value = await promptPassword({
|
|
431
453
|
message: `${valueLabel} for '${trimmedKey}':`,
|
|
432
|
-
validate: (val) =>
|
|
454
|
+
validate: (val) => {
|
|
455
|
+
if (!val || val.trim() === '') return 'Required';
|
|
456
|
+
if (valueRegex && !valueRegex.test(val)) return valuePatternMessage;
|
|
457
|
+
return undefined;
|
|
458
|
+
},
|
|
433
459
|
});
|
|
434
460
|
if (value === undefined) {
|
|
435
461
|
// User cancelled mid-entry; abort the whole interview.
|
|
@@ -443,6 +469,23 @@ async function promptForStringMap(payload: SecretRequiredPayload): Promise<strin
|
|
|
443
469
|
return JSON.stringify(collected);
|
|
444
470
|
}
|
|
445
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Compile a regex pattern from manifest config, returning null on
|
|
474
|
+
* empty or invalid input. A bad regex emits a one-time warning and
|
|
475
|
+
* disables that validation rather than crashing the interview.
|
|
476
|
+
*/
|
|
477
|
+
function compileMaybeRegex(pattern: string | undefined, label: string): RegExp | null {
|
|
478
|
+
if (!pattern) return null;
|
|
479
|
+
try {
|
|
480
|
+
return new RegExp(pattern);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
log.warn(
|
|
483
|
+
`${label}: invalid regex '${pattern}' — ${err instanceof Error ? err.message : String(err)}. Skipping validation.`,
|
|
484
|
+
);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
446
489
|
/**
|
|
447
490
|
* Short hint shown in the prompt for non-string types so the operator
|
|
448
491
|
* isn't guessing what shape we want. Returns null for plain strings —
|