@atlassian-dc-mcp/common 0.17.1 → 0.19.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/CHANGELOG.md +22 -0
- package/build/__tests__/args.test.d.ts +2 -0
- package/build/__tests__/args.test.d.ts.map +1 -0
- package/build/__tests__/args.test.js +117 -0
- package/build/__tests__/args.test.js.map +1 -0
- package/build/__tests__/describe-error.test.d.ts +2 -0
- package/build/__tests__/describe-error.test.d.ts.map +1 -0
- package/build/__tests__/describe-error.test.js +54 -0
- package/build/__tests__/describe-error.test.js.map +1 -0
- package/build/__tests__/setup-cli.test.js +337 -44
- package/build/__tests__/setup-cli.test.js.map +1 -1
- package/build/config/source.d.ts +1 -0
- package/build/config/source.d.ts.map +1 -1
- package/build/config/source.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/setup/args.d.ts +14 -0
- package/build/setup/args.d.ts.map +1 -0
- package/build/setup/args.js +66 -0
- package/build/setup/args.js.map +1 -0
- package/build/setup/describe-error.d.ts +2 -0
- package/build/setup/describe-error.d.ts.map +1 -0
- package/build/setup/describe-error.js +50 -0
- package/build/setup/describe-error.js.map +1 -0
- package/build/setup/value-validator.d.ts +8 -0
- package/build/setup/value-validator.d.ts.map +1 -0
- package/build/setup/value-validator.js +57 -0
- package/build/setup/value-validator.js.map +1 -0
- package/build/setup-cli.d.ts +18 -0
- package/build/setup-cli.d.ts.map +1 -1
- package/build/setup-cli.js +241 -33
- package/build/setup-cli.js.map +1 -1
- package/jest.config.js +1 -0
- package/package.json +2 -2
- package/src/__tests__/args.test.ts +132 -0
- package/src/__tests__/describe-error.test.ts +70 -0
- package/src/__tests__/setup-cli.test.ts +393 -38
- package/src/config/source.ts +1 -0
- package/src/index.ts +3 -1
- package/src/setup/args.ts +78 -0
- package/src/setup/describe-error.ts +67 -0
- package/src/setup/value-validator.ts +59 -0
- package/src/setup-cli.ts +318 -36
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ValidationResult = true | string;
|
|
2
|
+
|
|
3
|
+
export class SetupValueValidator {
|
|
4
|
+
static host(raw: string): ValidationResult {
|
|
5
|
+
const value = raw.trim();
|
|
6
|
+
if (value.length === 0) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (/\s/.test(value)) {
|
|
10
|
+
return 'must not contain whitespace';
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
|
14
|
+
if (!url.hostname) {
|
|
15
|
+
return 'must include a host name';
|
|
16
|
+
}
|
|
17
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
18
|
+
return 'must use http or https';
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return 'enter a host name or http(s) URL';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static apiBasePath(raw: string): ValidationResult {
|
|
27
|
+
const value = raw.trim();
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (/\s/.test(value)) {
|
|
32
|
+
return 'must not contain whitespace';
|
|
33
|
+
}
|
|
34
|
+
if (/^https?:\/\//i.test(value)) {
|
|
35
|
+
try {
|
|
36
|
+
new URL(value);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return 'enter a valid http(s) URL';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return value.startsWith('/') ? true : 'enter a path starting with / or a full http(s) URL';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static token(raw: string): ValidationResult {
|
|
46
|
+
const value = raw.trim();
|
|
47
|
+
if (value.length === 0) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return /\s/.test(value) ? 'must not contain whitespace' : true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static pageSize(raw: string): ValidationResult {
|
|
54
|
+
const trimmed = raw.trim();
|
|
55
|
+
return /^\d+$/.test(trimmed) && Number.parseInt(trimmed, 10) > 0
|
|
56
|
+
? true
|
|
57
|
+
: 'enter a positive integer';
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/setup-cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
|
|
2
2
|
import { buildDefaultRegistry, type ConfigRegistry } from './config/registry.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { ProductRuntimeConfig } from './config/runtime-config.js';
|
|
4
4
|
import type {
|
|
5
5
|
ConfigKey,
|
|
6
6
|
ProductDefinition,
|
|
@@ -8,19 +8,46 @@ import type {
|
|
|
8
8
|
} from './config/source.js';
|
|
9
9
|
import { HomeFileSource, getHomeFilePath } from './config/sources/home-file.js';
|
|
10
10
|
import { MacosKeychainSource } from './config/sources/macos-keychain.js';
|
|
11
|
+
import { parseSetupArgs, printSetupHelp, SetupArgsError, type ParsedSetupArgs } from './setup/args.js';
|
|
12
|
+
import { SetupValueValidator } from './setup/value-validator.js';
|
|
11
13
|
|
|
12
14
|
const FALLBACK_PAGE_SIZE = 25;
|
|
15
|
+
const MAX_VALIDATION_ATTEMPTS = 3;
|
|
16
|
+
|
|
17
|
+
type PromptDefaults = {
|
|
18
|
+
host?: string;
|
|
19
|
+
apiBasePath?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
defaultPageSize?: number;
|
|
22
|
+
};
|
|
13
23
|
|
|
14
24
|
type PromptResult = {
|
|
15
25
|
host: string;
|
|
16
26
|
apiBasePath: string;
|
|
17
27
|
defaultPageSize: string;
|
|
18
|
-
|
|
28
|
+
tokenToWrite: string | undefined;
|
|
29
|
+
tokenForValidation: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TokenPromptResult = Pick<PromptResult, 'tokenToWrite' | 'tokenForValidation'>;
|
|
33
|
+
|
|
34
|
+
export type CredentialValidationContext = {
|
|
35
|
+
host: string;
|
|
36
|
+
apiBasePath: string;
|
|
37
|
+
token: string;
|
|
19
38
|
};
|
|
20
39
|
|
|
40
|
+
export type CredentialValidationResult =
|
|
41
|
+
| { ok: true; detail?: string }
|
|
42
|
+
| { ok: false; message: string };
|
|
43
|
+
|
|
44
|
+
export type ValidateCredentials = (
|
|
45
|
+
context: CredentialValidationContext,
|
|
46
|
+
) => Promise<CredentialValidationResult>;
|
|
47
|
+
|
|
21
48
|
export type SetupPrompts = {
|
|
22
49
|
input: (opts: { message: string; default?: string; validate?: (raw: string) => true | string }) => Promise<string>;
|
|
23
|
-
password: (opts: { message: string; mask?: string }) => Promise<string>;
|
|
50
|
+
password: (opts: { message: string; mask?: string; validate?: (raw: string) => true | string }) => Promise<string>;
|
|
24
51
|
confirm: (opts: { message: string; default?: boolean }) => Promise<boolean>;
|
|
25
52
|
};
|
|
26
53
|
|
|
@@ -29,6 +56,8 @@ export type SetupDeps = {
|
|
|
29
56
|
log?: (message: string) => void;
|
|
30
57
|
exit?: (code: number) => void;
|
|
31
58
|
prompts?: SetupPrompts;
|
|
59
|
+
validateCredentials?: ValidateCredentials;
|
|
60
|
+
args?: ParsedSetupArgs;
|
|
32
61
|
};
|
|
33
62
|
|
|
34
63
|
const DEFAULT_PROMPTS: SetupPrompts = {
|
|
@@ -37,6 +66,36 @@ const DEFAULT_PROMPTS: SetupPrompts = {
|
|
|
37
66
|
confirm: (opts) => inquirerConfirm(opts as any),
|
|
38
67
|
};
|
|
39
68
|
|
|
69
|
+
export async function runSetupCli(
|
|
70
|
+
product: ProductDefinition,
|
|
71
|
+
deps: Omit<SetupDeps, 'args'> = {},
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const rawArgv = process.argv.slice(2);
|
|
74
|
+
const argv = rawArgv[0] === 'setup' ? rawArgv.slice(1) : rawArgv;
|
|
75
|
+
const exit = deps.exit ?? ((code: number) => { process.exit(code); });
|
|
76
|
+
|
|
77
|
+
let args: ParsedSetupArgs;
|
|
78
|
+
try {
|
|
79
|
+
args = parseSetupArgs(argv);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof SetupArgsError) {
|
|
82
|
+
process.stderr.write(`${error.message}\n\n`);
|
|
83
|
+
printSetupHelp(product.id, (m) => process.stderr.write(`${m}\n`));
|
|
84
|
+
exit(1);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (args.help) {
|
|
91
|
+
printSetupHelp(product.id, (m) => process.stdout.write(`${m}\n`));
|
|
92
|
+
exit(0);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await runSetup(product, { ...deps, args });
|
|
97
|
+
}
|
|
98
|
+
|
|
40
99
|
export async function runSetup(product: ProductDefinition, deps: SetupDeps = {}): Promise<void> {
|
|
41
100
|
const registry = deps.registry ?? buildDefaultRegistry();
|
|
42
101
|
const log = deps.log ?? ((message: string) => process.stdout.write(`${message}\n`));
|
|
@@ -48,26 +107,195 @@ export async function runSetup(product: ProductDefinition, deps: SetupDeps = {})
|
|
|
48
107
|
log(`Atlassian DC MCP setup — ${product.id}`);
|
|
49
108
|
log('');
|
|
50
109
|
|
|
51
|
-
const current =
|
|
110
|
+
const current = readCurrentConfig(registry, product);
|
|
52
111
|
printCurrent(log, registry, product, current);
|
|
53
112
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
exit(130);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
113
|
+
const answers = deps.args?.nonInteractive
|
|
114
|
+
? await runNonInteractive(product, deps, current, log, exit, deps.args)
|
|
115
|
+
: await collectAnswersWithValidation(product, deps, prompts, current, log, exit, deps.args);
|
|
116
|
+
if (!answers) {
|
|
117
|
+
return;
|
|
63
118
|
}
|
|
64
119
|
|
|
65
120
|
const homeFile = requireHomeFile(registry);
|
|
66
121
|
writeNonSecretFields(registry, product, answers, homeFile, log);
|
|
67
|
-
const tokenWriter = await writeToken(registry, product, answers.
|
|
122
|
+
const tokenWriter = await writeToken(registry, product, answers.tokenToWrite, homeFile, log, prompts);
|
|
68
123
|
printSummary(log, product, answers, tokenWriter);
|
|
69
124
|
}
|
|
70
125
|
|
|
126
|
+
async function collectAnswersWithValidation(
|
|
127
|
+
product: ProductDefinition,
|
|
128
|
+
deps: SetupDeps,
|
|
129
|
+
prompts: SetupPrompts,
|
|
130
|
+
current: ProductRuntimeConfig,
|
|
131
|
+
log: (message: string) => void,
|
|
132
|
+
exit: (code: number) => void,
|
|
133
|
+
args: ParsedSetupArgs | undefined,
|
|
134
|
+
): Promise<PromptResult | undefined> {
|
|
135
|
+
let defaults: PromptDefaults = current;
|
|
136
|
+
|
|
137
|
+
for (let attempt = 1; ; attempt++) {
|
|
138
|
+
let answers: PromptResult;
|
|
139
|
+
try {
|
|
140
|
+
answers = await promptForValues(prompts, product, defaults, args);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isUserCancel(error)) {
|
|
143
|
+
exit(130);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const answerErrors = validateAnswers(product, answers);
|
|
150
|
+
if (answerErrors.length > 0) {
|
|
151
|
+
for (const message of answerErrors) {
|
|
152
|
+
log(`Validation failed: ${message}`);
|
|
153
|
+
}
|
|
154
|
+
const retry = await confirmRetry(prompts, 'Try again?');
|
|
155
|
+
if (retry) {
|
|
156
|
+
defaults = answersAsDefaults(answers);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
exit(1);
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!deps.validateCredentials || !answers.tokenForValidation) {
|
|
164
|
+
return answers;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await deps.validateCredentials({
|
|
168
|
+
host: answers.host,
|
|
169
|
+
apiBasePath: answers.apiBasePath,
|
|
170
|
+
token: answers.tokenForValidation,
|
|
171
|
+
});
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
log(result.detail ? `Validation succeeded: ${result.detail}` : 'Validation succeeded.');
|
|
174
|
+
return answers;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
log(`Validation failed: ${result.message}`);
|
|
178
|
+
const outcome = await offerRetryAfterFailure(prompts, attempt);
|
|
179
|
+
if (outcome === 'retry') {
|
|
180
|
+
defaults = answersAsDefaults(answers);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (outcome === 'save-anyway') {
|
|
184
|
+
return answers;
|
|
185
|
+
}
|
|
186
|
+
exit(1);
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runNonInteractive(
|
|
192
|
+
product: ProductDefinition,
|
|
193
|
+
deps: SetupDeps,
|
|
194
|
+
current: ProductRuntimeConfig,
|
|
195
|
+
log: (message: string) => void,
|
|
196
|
+
exit: (code: number) => void,
|
|
197
|
+
args: ParsedSetupArgs,
|
|
198
|
+
): Promise<PromptResult | undefined> {
|
|
199
|
+
let tokenToWrite: string | undefined;
|
|
200
|
+
let tokenForValidation: string | undefined;
|
|
201
|
+
if (args.token) {
|
|
202
|
+
tokenToWrite = args.token;
|
|
203
|
+
tokenForValidation = args.token;
|
|
204
|
+
} else if (current.token) {
|
|
205
|
+
tokenForValidation = current.token;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const answers: PromptResult = {
|
|
209
|
+
host: args.host ?? current.host ?? '',
|
|
210
|
+
apiBasePath: args.apiBasePath ?? current.apiBasePath ?? product.defaultApiBasePath ?? '',
|
|
211
|
+
defaultPageSize: args.defaultPageSize ?? String(current.defaultPageSize ?? FALLBACK_PAGE_SIZE),
|
|
212
|
+
tokenToWrite,
|
|
213
|
+
tokenForValidation,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const formatErrors = validateAnswers(product, answers);
|
|
217
|
+
if (formatErrors.length > 0) {
|
|
218
|
+
for (const message of formatErrors) {
|
|
219
|
+
log(`Validation failed: ${message}`);
|
|
220
|
+
}
|
|
221
|
+
exit(1);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (deps.validateCredentials && answers.tokenForValidation) {
|
|
226
|
+
const result = await deps.validateCredentials({
|
|
227
|
+
host: answers.host,
|
|
228
|
+
apiBasePath: answers.apiBasePath,
|
|
229
|
+
token: answers.tokenForValidation,
|
|
230
|
+
});
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
log(`Validation failed: ${result.message}`);
|
|
233
|
+
exit(1);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
log(result.detail ? `Validation succeeded: ${result.detail}` : 'Validation succeeded.');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return answers;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function answersAsDefaults(answers: PromptResult): PromptDefaults {
|
|
243
|
+
const pageSize = Number.parseInt(answers.defaultPageSize, 10);
|
|
244
|
+
return {
|
|
245
|
+
host: answers.host,
|
|
246
|
+
apiBasePath: answers.apiBasePath,
|
|
247
|
+
token: answers.tokenForValidation,
|
|
248
|
+
defaultPageSize: Number.isFinite(pageSize) && pageSize > 0 ? pageSize : undefined,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function confirmRetry(prompts: SetupPrompts, message: string): Promise<boolean> {
|
|
253
|
+
return prompts.confirm({ message, default: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function offerRetryAfterFailure(
|
|
257
|
+
prompts: SetupPrompts,
|
|
258
|
+
attempt: number,
|
|
259
|
+
): Promise<'retry' | 'save-anyway' | 'abort'> {
|
|
260
|
+
if (attempt < MAX_VALIDATION_ATTEMPTS) {
|
|
261
|
+
const retry = await confirmRetry(prompts, 'Try again with different values?');
|
|
262
|
+
if (retry) {
|
|
263
|
+
return 'retry';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const saveAnyway = await prompts.confirm({
|
|
267
|
+
message: 'Save configuration anyway?',
|
|
268
|
+
default: false,
|
|
269
|
+
});
|
|
270
|
+
return saveAnyway ? 'save-anyway' : 'abort';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readCurrentConfig(
|
|
274
|
+
registry: ConfigRegistry,
|
|
275
|
+
product: ProductDefinition,
|
|
276
|
+
): ProductRuntimeConfig {
|
|
277
|
+
const pageSizeRaw = registry.resolve(product, 'defaultPageSize').value;
|
|
278
|
+
const pageSize = parsePositiveInteger(pageSizeRaw) ?? FALLBACK_PAGE_SIZE;
|
|
279
|
+
return {
|
|
280
|
+
host: registry.resolve(product, 'host').value,
|
|
281
|
+
apiBasePath: registry.resolve(product, 'apiBasePath').value,
|
|
282
|
+
token: registry.resolve(product, 'token').value,
|
|
283
|
+
defaultPageSize: pageSize,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parsePositiveInteger(value: string | undefined): number | undefined {
|
|
288
|
+
if (!value) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const trimmed = value.trim();
|
|
292
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
296
|
+
return parsed > 0 ? parsed : undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
71
299
|
function requireHomeFile(registry: ConfigRegistry): HomeFileSource {
|
|
72
300
|
const homeFile = registry.getWritableSource(
|
|
73
301
|
(s): s is HomeFileSource => s instanceof HomeFileSource,
|
|
@@ -82,7 +310,7 @@ function printCurrent(
|
|
|
82
310
|
log: (message: string) => void,
|
|
83
311
|
registry: ConfigRegistry,
|
|
84
312
|
product: ProductDefinition,
|
|
85
|
-
current:
|
|
313
|
+
current: ProductRuntimeConfig,
|
|
86
314
|
): void {
|
|
87
315
|
const keys: ConfigKey[] = ['host', 'apiBasePath', 'token', 'defaultPageSize'];
|
|
88
316
|
for (const key of keys) {
|
|
@@ -98,42 +326,93 @@ function printCurrent(
|
|
|
98
326
|
async function promptForValues(
|
|
99
327
|
prompts: SetupPrompts,
|
|
100
328
|
product: ProductDefinition,
|
|
101
|
-
|
|
329
|
+
defaults: PromptDefaults,
|
|
330
|
+
args: ParsedSetupArgs | undefined,
|
|
102
331
|
): Promise<PromptResult> {
|
|
103
|
-
const host = await prompts.input({
|
|
332
|
+
const host = args?.host ?? await prompts.input({
|
|
104
333
|
message: 'Host (e.g. jira.example.com):',
|
|
105
|
-
default:
|
|
334
|
+
default: defaults.host ?? '',
|
|
335
|
+
validate: SetupValueValidator.host,
|
|
106
336
|
});
|
|
107
|
-
const apiBasePath = await prompts.input({
|
|
337
|
+
const apiBasePath = args?.apiBasePath ?? await prompts.input({
|
|
108
338
|
message: 'API base path:',
|
|
109
|
-
default:
|
|
339
|
+
default: defaults.apiBasePath ?? product.defaultApiBasePath ?? '',
|
|
340
|
+
validate: SetupValueValidator.apiBasePath,
|
|
110
341
|
});
|
|
111
|
-
const defaultPageSize = await prompts.input({
|
|
342
|
+
const defaultPageSize = args?.defaultPageSize ?? await prompts.input({
|
|
112
343
|
message: 'Default page size:',
|
|
113
|
-
default: String(
|
|
114
|
-
validate:
|
|
115
|
-
/^\d+$/.test(raw.trim()) && Number.parseInt(raw.trim(), 10) > 0
|
|
116
|
-
? true
|
|
117
|
-
: 'Enter a positive integer',
|
|
344
|
+
default: String(defaults.defaultPageSize ?? FALLBACK_PAGE_SIZE),
|
|
345
|
+
validate: SetupValueValidator.pageSize,
|
|
118
346
|
});
|
|
119
|
-
const token = await promptForToken(prompts,
|
|
120
|
-
return {
|
|
347
|
+
const token = await promptForToken(prompts, defaults.token, args?.token);
|
|
348
|
+
return {
|
|
349
|
+
host: host.trim(),
|
|
350
|
+
apiBasePath: apiBasePath.trim(),
|
|
351
|
+
defaultPageSize: defaultPageSize.trim(),
|
|
352
|
+
...token,
|
|
353
|
+
};
|
|
121
354
|
}
|
|
122
355
|
|
|
123
356
|
async function promptForToken(
|
|
124
357
|
prompts: SetupPrompts,
|
|
125
358
|
existing: string | undefined,
|
|
126
|
-
|
|
127
|
-
|
|
359
|
+
fromArgs: string | undefined,
|
|
360
|
+
): Promise<TokenPromptResult> {
|
|
361
|
+
if (fromArgs) {
|
|
362
|
+
return { tokenToWrite: fromArgs, tokenForValidation: fromArgs };
|
|
363
|
+
}
|
|
364
|
+
const entered = await prompts.password({
|
|
365
|
+
message: 'API token:',
|
|
366
|
+
mask: '*',
|
|
367
|
+
validate: SetupValueValidator.token,
|
|
368
|
+
});
|
|
128
369
|
const trimmed = entered.trim();
|
|
129
370
|
if (trimmed.length > 0) {
|
|
130
|
-
return trimmed;
|
|
371
|
+
return { tokenToWrite: trimmed, tokenForValidation: trimmed };
|
|
131
372
|
}
|
|
132
373
|
if (!existing) {
|
|
133
|
-
return undefined;
|
|
374
|
+
return { tokenToWrite: undefined, tokenForValidation: undefined };
|
|
134
375
|
}
|
|
135
|
-
await prompts.confirm({ message: 'Keep existing token?', default: true });
|
|
136
|
-
return
|
|
376
|
+
const keepExisting = await prompts.confirm({ message: 'Keep existing token?', default: true });
|
|
377
|
+
return {
|
|
378
|
+
tokenToWrite: undefined,
|
|
379
|
+
tokenForValidation: keepExisting ? existing : undefined,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validateAnswers(product: ProductDefinition, answers: PromptResult): string[] {
|
|
384
|
+
const errors: string[] = [];
|
|
385
|
+
for (const [label, value, validator] of [
|
|
386
|
+
['host', answers.host, SetupValueValidator.host],
|
|
387
|
+
['API base path', answers.apiBasePath, SetupValueValidator.apiBasePath],
|
|
388
|
+
] as const) {
|
|
389
|
+
const result = validator(value);
|
|
390
|
+
if (result !== true) {
|
|
391
|
+
errors.push(`${label}: ${result}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const pageSize = SetupValueValidator.pageSize(answers.defaultPageSize);
|
|
396
|
+
if (pageSize !== true) {
|
|
397
|
+
errors.push(`default page size: ${pageSize}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!answers.tokenForValidation) {
|
|
401
|
+
errors.push(`API token is required (${product.envVars.token}).`);
|
|
402
|
+
} else {
|
|
403
|
+
const tokenResult = SetupValueValidator.token(answers.tokenForValidation);
|
|
404
|
+
if (tokenResult !== true) {
|
|
405
|
+
errors.push(`API token: ${tokenResult}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const hasHost = answers.host.length > 0;
|
|
410
|
+
const hasFullApiBasePath = /^https?:\/\//i.test(answers.apiBasePath);
|
|
411
|
+
if (!hasHost && !hasFullApiBasePath) {
|
|
412
|
+
errors.push(`Enter ${product.envVars.host}, or enter a full URL for ${product.envVars.apiBasePath}.`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return errors;
|
|
137
416
|
}
|
|
138
417
|
|
|
139
418
|
function writeNonSecretFields(
|
|
@@ -208,7 +487,10 @@ async function tryWrite(
|
|
|
208
487
|
message: 'Fall back to plaintext home file with mode 0600?',
|
|
209
488
|
default: false,
|
|
210
489
|
});
|
|
211
|
-
|
|
490
|
+
if (fallback) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
throw new Error('Token was not saved because keychain write failed and plaintext fallback was declined');
|
|
212
494
|
}
|
|
213
495
|
return false;
|
|
214
496
|
}
|
|
@@ -254,7 +536,7 @@ function printSummary(
|
|
|
254
536
|
log(` apiBasePath: ${answers.apiBasePath || '(unchanged)'}`);
|
|
255
537
|
log(` defaultPageSize: ${answers.defaultPageSize || '(unchanged)'}`);
|
|
256
538
|
if (tokenWriter) {
|
|
257
|
-
log(` token: ${maskToken(answers.
|
|
539
|
+
log(` token: ${maskToken(answers.tokenToWrite)} (stored in ${describeWriter(tokenWriter, product)})`);
|
|
258
540
|
} else {
|
|
259
541
|
log(' token: (unchanged)');
|
|
260
542
|
}
|