@quonfig/cli 0.0.11 → 0.0.12
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/README.md +154 -28
- package/dist/commands/migrate/doctor.d.ts +12 -0
- package/dist/commands/migrate/doctor.js +63 -0
- package/dist/commands/migrate/status.d.ts +10 -0
- package/dist/commands/migrate/status.js +112 -0
- package/dist/commands/migrate.d.ts +20 -0
- package/dist/commands/migrate.js +256 -0
- package/dist/migrate/doctor.d.ts +27 -0
- package/dist/migrate/doctor.js +220 -0
- package/dist/migrate/identifier-map.d.ts +14 -0
- package/dist/migrate/identifier-map.js +55 -0
- package/dist/migrate/import-state.d.ts +14 -0
- package/dist/migrate/import-state.js +60 -0
- package/dist/migrate/migration-report.d.ts +42 -0
- package/dist/migrate/migration-report.js +93 -0
- package/dist/migrate/push-strategy.d.ts +34 -0
- package/dist/migrate/push-strategy.js +133 -0
- package/dist/migrate/push-to-cloud.d.ts +15 -0
- package/dist/migrate/push-to-cloud.js +31 -0
- package/dist/migrate/registry.d.ts +6 -0
- package/dist/migrate/registry.js +25 -0
- package/dist/migrate/source.d.ts +22 -0
- package/dist/migrate/source.js +12 -0
- package/dist/migrate/sources/flagsmith.d.ts +2 -0
- package/dist/migrate/sources/flagsmith.js +18 -0
- package/dist/migrate/sources/launch/api.d.ts +7 -0
- package/dist/migrate/sources/launch/api.js +75 -0
- package/dist/migrate/sources/launch/translate.d.ts +6 -0
- package/dist/migrate/sources/launch/translate.js +190 -0
- package/dist/migrate/sources/launch/types.d.ts +68 -0
- package/dist/migrate/sources/launch/types.js +1 -0
- package/dist/migrate/sources/launch.d.ts +3 -0
- package/dist/migrate/sources/launch.js +70 -0
- package/dist/migrate/sources/launchdarkly.d.ts +2 -0
- package/dist/migrate/sources/launchdarkly.js +18 -0
- package/dist/ui/get-confirmation.d.ts +2 -1
- package/dist/ui/get-confirmation.js +9 -1
- package/dist/ui/get-value.d.ts +1 -1
- package/dist/ui/get-value.js +3 -2
- package/dist/util/is-interactive.d.ts +21 -1
- package/dist/util/is-interactive.js +21 -1
- package/dist/verify/validate.js +6 -1
- package/oclif.manifest.json +287 -1
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export function slugify(value) {
|
|
2
|
+
return value
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replaceAll(/[^\da-z]+/g, '-')
|
|
5
|
+
.replaceAll(/^-+|-+$/g, '');
|
|
6
|
+
}
|
|
7
|
+
export function isLegacyLogLevel(type) {
|
|
8
|
+
return type.toUpperCase() === 'LOG_LEVEL';
|
|
9
|
+
}
|
|
10
|
+
function zeroValue(valueType) {
|
|
11
|
+
switch (valueType) {
|
|
12
|
+
case 'bool': {
|
|
13
|
+
return { type: 'bool', value: false };
|
|
14
|
+
}
|
|
15
|
+
case 'double': {
|
|
16
|
+
return { type: 'double', value: '0' };
|
|
17
|
+
}
|
|
18
|
+
case 'duration': {
|
|
19
|
+
return { type: 'duration', value: 'PT0S' };
|
|
20
|
+
}
|
|
21
|
+
case 'int': {
|
|
22
|
+
return { type: 'int', value: '0' };
|
|
23
|
+
}
|
|
24
|
+
case 'json': {
|
|
25
|
+
return { type: 'json', value: {} };
|
|
26
|
+
}
|
|
27
|
+
case 'log_level': {
|
|
28
|
+
return { type: 'log_level', value: 'WARN' };
|
|
29
|
+
}
|
|
30
|
+
case 'string': {
|
|
31
|
+
return { type: 'string', value: '' };
|
|
32
|
+
}
|
|
33
|
+
case 'string_list': {
|
|
34
|
+
return { type: 'string_list', value: [] };
|
|
35
|
+
}
|
|
36
|
+
default: {
|
|
37
|
+
return { type: 'string', value: '' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function parseJsonString(raw, configKey, path) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
throw new Error(`Failed to parse stringified json value for config "${configKey}" at ${path}: ${message}. Raw value: ${JSON.stringify(raw)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeLaunchValue(value, configKey, path) {
|
|
51
|
+
if (!value || typeof value !== 'object')
|
|
52
|
+
return;
|
|
53
|
+
const v = value;
|
|
54
|
+
if (v.type === 'json' && typeof v.value === 'string') {
|
|
55
|
+
v.value = parseJsonString(v.value, configKey, path);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (v.type === 'weighted_values' && Array.isArray(v.value)) {
|
|
59
|
+
const entries = v.value;
|
|
60
|
+
for (const [i, entry] of entries.entries()) {
|
|
61
|
+
if (entry && typeof entry === 'object' && entry.value) {
|
|
62
|
+
normalizeLaunchValue(entry.value, configKey, `${path}.weightedValues[${i}].value`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function normalizeRules(rules, configKey, path) {
|
|
68
|
+
if (!Array.isArray(rules))
|
|
69
|
+
return;
|
|
70
|
+
for (const [i, rule] of rules.entries()) {
|
|
71
|
+
if (!rule || typeof rule !== 'object')
|
|
72
|
+
continue;
|
|
73
|
+
const r = rule;
|
|
74
|
+
if (r.value) {
|
|
75
|
+
normalizeLaunchValue(r.value, configKey, `${path}[${i}].value`);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(r.criteria)) {
|
|
78
|
+
for (const [j, crit] of r.criteria.entries()) {
|
|
79
|
+
if (!crit || typeof crit !== 'object')
|
|
80
|
+
continue;
|
|
81
|
+
const c = crit;
|
|
82
|
+
if (c.valueToMatch) {
|
|
83
|
+
normalizeLaunchValue(c.valueToMatch, configKey, `${path}[${i}].criteria[${j}].valueToMatch`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function normalizeJsonValuesInConfig(out) {
|
|
90
|
+
const configKey = String(out.key ?? 'unknown');
|
|
91
|
+
const defaultSection = out.default;
|
|
92
|
+
if (defaultSection && Array.isArray(defaultSection.rules)) {
|
|
93
|
+
normalizeRules(defaultSection.rules, configKey, 'default.rules');
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(out.environments)) {
|
|
96
|
+
for (const [i, env] of out.environments.entries()) {
|
|
97
|
+
const envId = typeof env.id === 'string' ? env.id : String(i);
|
|
98
|
+
normalizeRules(env.rules, configKey, `environments[${envId}].rules`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(out.variants)) {
|
|
102
|
+
for (const [i, variant] of out.variants.entries()) {
|
|
103
|
+
if (variant && variant.value) {
|
|
104
|
+
normalizeLaunchValue(variant.value, configKey, `variants[${i}].value`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function transformConfig(config, envIdMap) {
|
|
110
|
+
const out = JSON.parse(JSON.stringify(config));
|
|
111
|
+
delete out.changedBy;
|
|
112
|
+
delete out.modifiedAt;
|
|
113
|
+
delete out.createdAt;
|
|
114
|
+
delete out.deleted;
|
|
115
|
+
if (out.type === 'feature_flag') {
|
|
116
|
+
delete out.sendToClientSdk;
|
|
117
|
+
}
|
|
118
|
+
if (typeof out.type === 'string' && out.type.toUpperCase() === 'LOG_LEVEL_V2') {
|
|
119
|
+
out.type = 'log_level';
|
|
120
|
+
}
|
|
121
|
+
if (typeof out.valueType === 'string' && out.valueType.toUpperCase() === 'LOG_LEVEL_V2') {
|
|
122
|
+
out.valueType = 'log_level';
|
|
123
|
+
}
|
|
124
|
+
if (typeof out.type === 'string' && out.type.toLowerCase() === 'schema') {
|
|
125
|
+
throw new Error(`Schema-type configs are not yet supported by qfg migrate --from launch. ` +
|
|
126
|
+
`File a bead to prioritize this: https://github.com/quonfig/cli/issues/new?title=qfg+migrate+schema+configs`);
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(out.environments)) {
|
|
129
|
+
for (const env of out.environments) {
|
|
130
|
+
if (typeof env.id === 'string') {
|
|
131
|
+
const name = envIdMap[env.id] ?? `env-${env.id}`;
|
|
132
|
+
env.id = slugify(name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!out.default && typeof out.valueType === 'string') {
|
|
137
|
+
out.default = {
|
|
138
|
+
rules: [
|
|
139
|
+
{
|
|
140
|
+
criteria: [{ operator: 'ALWAYS_TRUE' }],
|
|
141
|
+
value: zeroValue(out.valueType),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
normalizeJsonValuesInConfig(out);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
export function getOutputPath(type, key) {
|
|
150
|
+
const normalizedType = type.toUpperCase();
|
|
151
|
+
switch (normalizedType) {
|
|
152
|
+
case 'FEATURE_FLAG': {
|
|
153
|
+
return `feature-flags/${key}.json`;
|
|
154
|
+
}
|
|
155
|
+
case 'LOG_LEVEL_V2': {
|
|
156
|
+
return `log-levels/${key}.json`;
|
|
157
|
+
}
|
|
158
|
+
case 'SCHEMA': {
|
|
159
|
+
return `schemas/${key}.json`;
|
|
160
|
+
}
|
|
161
|
+
case 'SEGMENT': {
|
|
162
|
+
return `segments/${key}.json`;
|
|
163
|
+
}
|
|
164
|
+
default: {
|
|
165
|
+
return `configs/${key}.json`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function groupChanges(changes) {
|
|
170
|
+
const groups = new Map();
|
|
171
|
+
for (const change of changes) {
|
|
172
|
+
const groupKey = `${change.changedAt}:${change.changedBy.id}`;
|
|
173
|
+
if (!groups.has(groupKey)) {
|
|
174
|
+
groups.set(groupKey, {
|
|
175
|
+
changedAt: change.changedAt,
|
|
176
|
+
changedBy: change.changedBy,
|
|
177
|
+
changes: [],
|
|
178
|
+
groupKey,
|
|
179
|
+
summary: '',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
groups.get(groupKey).changes.push(change);
|
|
183
|
+
}
|
|
184
|
+
for (const group of groups.values()) {
|
|
185
|
+
group.summary = group.changes
|
|
186
|
+
.map((c) => (c.deleted ? `Delete ${c.key}` : (c.summary ?? `Update ${c.key}`)))
|
|
187
|
+
.join('\n');
|
|
188
|
+
}
|
|
189
|
+
return [...groups.values()].sort((a, b) => a.changedAt - b.changedAt);
|
|
190
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface LaunchValue {
|
|
2
|
+
type: string;
|
|
3
|
+
value: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface LaunchCriterion {
|
|
6
|
+
operator: string;
|
|
7
|
+
propertyName?: string;
|
|
8
|
+
valueToMatch?: LaunchValue;
|
|
9
|
+
}
|
|
10
|
+
export interface LaunchRule {
|
|
11
|
+
criteria: LaunchCriterion[];
|
|
12
|
+
value: LaunchValue;
|
|
13
|
+
}
|
|
14
|
+
export interface LaunchEnvironmentRow {
|
|
15
|
+
id: string;
|
|
16
|
+
rules: LaunchRule[];
|
|
17
|
+
}
|
|
18
|
+
export interface LaunchChangedBy {
|
|
19
|
+
email: string;
|
|
20
|
+
fullName?: string;
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
export interface LaunchConfig {
|
|
25
|
+
allowableValues?: LaunchValue[];
|
|
26
|
+
changedBy?: LaunchChangedBy;
|
|
27
|
+
default?: {
|
|
28
|
+
rules: LaunchRule[];
|
|
29
|
+
};
|
|
30
|
+
environments: LaunchEnvironmentRow[];
|
|
31
|
+
id: string;
|
|
32
|
+
key: string;
|
|
33
|
+
projectId: string;
|
|
34
|
+
schemaKey?: string;
|
|
35
|
+
sendToClientSdk?: boolean;
|
|
36
|
+
type: string;
|
|
37
|
+
valueType: string;
|
|
38
|
+
}
|
|
39
|
+
export interface LaunchChangeEntry {
|
|
40
|
+
changedAt: number;
|
|
41
|
+
changedBy: LaunchChangedBy;
|
|
42
|
+
deleted: boolean;
|
|
43
|
+
key: string;
|
|
44
|
+
newConfig?: LaunchConfig;
|
|
45
|
+
newConfigId: number | string;
|
|
46
|
+
previousConfigId?: number | string;
|
|
47
|
+
summary?: string;
|
|
48
|
+
type: string;
|
|
49
|
+
version?: number | string;
|
|
50
|
+
}
|
|
51
|
+
export interface LaunchChangeHistoryResponse {
|
|
52
|
+
changes: LaunchChangeEntry[];
|
|
53
|
+
cursor?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface LaunchProjectEnvironmentsResponse {
|
|
56
|
+
envs: Array<{
|
|
57
|
+
id: number;
|
|
58
|
+
name: string;
|
|
59
|
+
}>;
|
|
60
|
+
projectId: number;
|
|
61
|
+
}
|
|
62
|
+
export interface LaunchChangeGroup {
|
|
63
|
+
changedAt: number;
|
|
64
|
+
changedBy: LaunchChangedBy;
|
|
65
|
+
changes: LaunchChangeEntry[];
|
|
66
|
+
groupKey: string;
|
|
67
|
+
summary: string;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fetchAllChangeHistory, fetchEnvironments } from './launch/api.js';
|
|
2
|
+
import { getOutputPath, isLegacyLogLevel, slugify, transformConfig } from './launch/translate.js';
|
|
3
|
+
const SOURCE_NAME = 'launch';
|
|
4
|
+
const state = { apiKey: null, envIdMap: null };
|
|
5
|
+
class MissingAuthError extends Error {
|
|
6
|
+
constructor(operation) {
|
|
7
|
+
super(`launch source ${operation} requires validateAuth(apiKey) to be called first (no API key configured).`);
|
|
8
|
+
this.name = 'MissingAuthError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function requireApiKey(operation) {
|
|
12
|
+
if (!state.apiKey)
|
|
13
|
+
throw new MissingAuthError(operation);
|
|
14
|
+
return state.apiKey;
|
|
15
|
+
}
|
|
16
|
+
async function* fetchChangesImpl(sinceEpochMs) {
|
|
17
|
+
const apiKey = requireApiKey('fetchChanges');
|
|
18
|
+
const since = sinceEpochMs === null ? undefined : sinceEpochMs;
|
|
19
|
+
const changes = await fetchAllChangeHistory(apiKey, since);
|
|
20
|
+
for (const change of changes) {
|
|
21
|
+
yield {
|
|
22
|
+
changedAt: change.changedAt,
|
|
23
|
+
key: change.key,
|
|
24
|
+
raw: change,
|
|
25
|
+
source: SOURCE_NAME,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function listEnvironmentsImpl() {
|
|
30
|
+
const apiKey = requireApiKey('listEnvironments');
|
|
31
|
+
const rawEnvIdMap = await fetchEnvironments(apiKey);
|
|
32
|
+
const envIdMap = {};
|
|
33
|
+
for (const [id, name] of Object.entries(rawEnvIdMap)) {
|
|
34
|
+
envIdMap[id] = slugify(name);
|
|
35
|
+
}
|
|
36
|
+
state.envIdMap = envIdMap;
|
|
37
|
+
return [...new Set(Object.values(envIdMap))];
|
|
38
|
+
}
|
|
39
|
+
function translateImpl(change) {
|
|
40
|
+
const raw = change.raw;
|
|
41
|
+
if (!raw || typeof raw !== 'object')
|
|
42
|
+
return [];
|
|
43
|
+
if (typeof raw.type === 'string' && isLegacyLogLevel(raw.type))
|
|
44
|
+
return [];
|
|
45
|
+
if (raw.deleted)
|
|
46
|
+
return [];
|
|
47
|
+
if (!raw.newConfig)
|
|
48
|
+
return [];
|
|
49
|
+
const envIdMap = state.envIdMap ?? {};
|
|
50
|
+
const transformed = transformConfig(raw.newConfig, envIdMap);
|
|
51
|
+
const path = getOutputPath(raw.type, raw.key);
|
|
52
|
+
return [{ contents: JSON.stringify(transformed, null, 2), path }];
|
|
53
|
+
}
|
|
54
|
+
async function validateAuthImpl(apiKey) {
|
|
55
|
+
await fetchEnvironments(apiKey);
|
|
56
|
+
state.apiKey = apiKey;
|
|
57
|
+
}
|
|
58
|
+
export const launchSource = {
|
|
59
|
+
fetchChanges(sinceEpochMs) {
|
|
60
|
+
return fetchChangesImpl(sinceEpochMs);
|
|
61
|
+
},
|
|
62
|
+
listEnvironments: listEnvironmentsImpl,
|
|
63
|
+
name: SOURCE_NAME,
|
|
64
|
+
translate: translateImpl,
|
|
65
|
+
validateAuth: validateAuthImpl,
|
|
66
|
+
};
|
|
67
|
+
export function __resetLaunchSourceForTests() {
|
|
68
|
+
state.apiKey = null;
|
|
69
|
+
state.envIdMap = null;
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NotYetImplementedError } from '../source.js';
|
|
2
|
+
const SOURCE_NAME = 'launchdarkly';
|
|
3
|
+
export const launchdarklySource = {
|
|
4
|
+
// eslint-disable-next-line require-yield
|
|
5
|
+
async *fetchChanges() {
|
|
6
|
+
throw new NotYetImplementedError(SOURCE_NAME, 'fetchChanges');
|
|
7
|
+
},
|
|
8
|
+
listEnvironments() {
|
|
9
|
+
throw new NotYetImplementedError(SOURCE_NAME, 'listEnvironments');
|
|
10
|
+
},
|
|
11
|
+
name: SOURCE_NAME,
|
|
12
|
+
translate() {
|
|
13
|
+
throw new NotYetImplementedError(SOURCE_NAME, 'translate');
|
|
14
|
+
},
|
|
15
|
+
validateAuth() {
|
|
16
|
+
throw new NotYetImplementedError(SOURCE_NAME, 'validateAuth');
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare const confirmFlag: {
|
|
2
2
|
confirm: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
3
3
|
};
|
|
4
|
-
declare const getConfirmation: ({ flags, message }: {
|
|
4
|
+
declare const getConfirmation: ({ flags, message, }: {
|
|
5
5
|
flags: {
|
|
6
6
|
confirm?: boolean;
|
|
7
|
+
interactive?: boolean;
|
|
7
8
|
};
|
|
8
9
|
message: string;
|
|
9
10
|
}) => Promise<boolean>;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { Flags, ux } from '@oclif/core';
|
|
2
|
+
import isInteractive from '../util/is-interactive.js';
|
|
2
3
|
export const confirmFlag = { confirm: Flags.boolean({ description: 'confirm without prompt' }) };
|
|
3
|
-
const getConfirmation = async ({ flags, message }) =>
|
|
4
|
+
const getConfirmation = async ({ flags, message, }) => {
|
|
5
|
+
if (flags.confirm)
|
|
6
|
+
return true;
|
|
7
|
+
if (!isInteractive(flags)) {
|
|
8
|
+
throw new Error('Cannot prompt for confirmation in a non-interactive context. Pass --confirm to skip the confirmation prompt.');
|
|
9
|
+
}
|
|
10
|
+
return ux.confirm(message);
|
|
11
|
+
};
|
|
4
12
|
export default getConfirmation;
|
package/dist/ui/get-value.d.ts
CHANGED
package/dist/ui/get-value.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { defaultValueFor } from '../quonfig.js';
|
|
2
2
|
import { failure, noop, success } from '../result.js';
|
|
3
|
+
import isInteractive from '../util/is-interactive.js';
|
|
3
4
|
import validateValue from '../validations/value.js';
|
|
4
5
|
import getString from './get-string.js';
|
|
5
6
|
const getValue = async ({ allowBlank = true, desiredValue, environment, flags, key, message, quonfig, }) => {
|
|
6
|
-
if (desiredValue === undefined && !flags
|
|
7
|
-
return failure(`No value provided for ${key}
|
|
7
|
+
if (desiredValue === undefined && !isInteractive(flags)) {
|
|
8
|
+
return failure(`No value provided for ${key ?? message}. Pass --value=<value> (or use --env-var) when running non-interactively.`);
|
|
8
9
|
}
|
|
9
10
|
if (!key) {
|
|
10
11
|
if (desiredValue !== undefined) {
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
type Flags = {
|
|
2
2
|
interactive?: boolean;
|
|
3
3
|
};
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Returns true when the CLI can safely prompt the user.
|
|
6
|
+
*
|
|
7
|
+
* Rules, in order:
|
|
8
|
+
* 1. `--no-interactive` (flags.interactive === false) always wins → non-interactive.
|
|
9
|
+
* 2. Otherwise require both stdin and stdout to be a TTY.
|
|
10
|
+
*
|
|
11
|
+
* Historical bug: this used to return `true` whenever `flags.interactive` was
|
|
12
|
+
* truthy, which effectively bypassed the TTY check because the global
|
|
13
|
+
* `--interactive` flag defaults to `true`. Under non-TTY stdio (CI, piped
|
|
14
|
+
* stdin, `nohup`) the prompt helpers (`ux.prompt`, inquirer autocomplete)
|
|
15
|
+
* would be invoked with no way to read input, leaving an unsettled
|
|
16
|
+
* top-level await and emitting a raw Node warning (`Detected unsettled
|
|
17
|
+
* top-level await`) with exit code 13. Now we always AND with the TTY check
|
|
18
|
+
* so non-TTY contexts fall through to the command-level "pass --value and
|
|
19
|
+
* --no-interactive" error message instead of hanging.
|
|
20
|
+
*
|
|
21
|
+
* @param flags - the parsed oclif flags (at minimum `interactive` is inspected)
|
|
22
|
+
* @returns true if prompting is safe, false otherwise
|
|
23
|
+
*/
|
|
24
|
+
declare const isInteractive: (flags: Flags) => boolean;
|
|
5
25
|
export default isInteractive;
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when the CLI can safely prompt the user.
|
|
3
|
+
*
|
|
4
|
+
* Rules, in order:
|
|
5
|
+
* 1. `--no-interactive` (flags.interactive === false) always wins → non-interactive.
|
|
6
|
+
* 2. Otherwise require both stdin and stdout to be a TTY.
|
|
7
|
+
*
|
|
8
|
+
* Historical bug: this used to return `true` whenever `flags.interactive` was
|
|
9
|
+
* truthy, which effectively bypassed the TTY check because the global
|
|
10
|
+
* `--interactive` flag defaults to `true`. Under non-TTY stdio (CI, piped
|
|
11
|
+
* stdin, `nohup`) the prompt helpers (`ux.prompt`, inquirer autocomplete)
|
|
12
|
+
* would be invoked with no way to read input, leaving an unsettled
|
|
13
|
+
* top-level await and emitting a raw Node warning (`Detected unsettled
|
|
14
|
+
* top-level await`) with exit code 13. Now we always AND with the TTY check
|
|
15
|
+
* so non-TTY contexts fall through to the command-level "pass --value and
|
|
16
|
+
* --no-interactive" error message instead of hanging.
|
|
17
|
+
*
|
|
18
|
+
* @param flags - the parsed oclif flags (at minimum `interactive` is inspected)
|
|
19
|
+
* @returns true if prompting is safe, false otherwise
|
|
20
|
+
*/
|
|
1
21
|
const isInteractive = (flags) => {
|
|
2
22
|
if (flags.interactive === false) {
|
|
3
23
|
return false;
|
|
4
24
|
}
|
|
5
|
-
return (process.stdout.isTTY && process.stdin.isTTY)
|
|
25
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
6
26
|
};
|
|
7
27
|
export default isInteractive;
|
package/dist/verify/validate.js
CHANGED
|
@@ -74,7 +74,12 @@ const BoolValueSchema = z.object({ type: z.literal("bool"), value: z.boolean() }
|
|
|
74
74
|
const StringValueSchema = z.object({ type: z.literal("string"), value: z.string() });
|
|
75
75
|
const IntValueSchema = z.object({ type: z.literal("int"), value: z.union([z.number(), z.string()]) });
|
|
76
76
|
const DoubleValueSchema = z.object({ type: z.literal("double"), value: z.union([z.number(), z.string()]) });
|
|
77
|
-
const JsonValueSchema = z.object({
|
|
77
|
+
const JsonValueSchema = z.object({
|
|
78
|
+
type: z.literal("json"),
|
|
79
|
+
value: z.any().refine((v) => typeof v !== "string", {
|
|
80
|
+
message: 'json values must be native JSON (object/array/number/boolean/null). Stringified JSON is no longer allowed — use { "a": 1 } instead of "{\\"a\\":1}".',
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
78
83
|
const StringListValueSchema = z.object({ type: z.literal("string_list"), value: z.array(z.string()) });
|
|
79
84
|
const DurationValueSchema = z.object({ type: z.literal("duration"), value: z.string() });
|
|
80
85
|
const LogLevelValueSchema = z.object({ type: z.literal("log_level"), value: LogLevelSchema });
|