@atlassian-dc-mcp/common 0.18.0 → 0.19.1
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 +19 -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__/setup-cli.test.js +186 -0
- package/build/__tests__/setup-cli.test.js.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -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-cli.d.ts +3 -0
- package/build/setup-cli.d.ts.map +1 -1
- package/build/setup-cli.js +107 -12
- package/build/setup-cli.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/args.test.ts +132 -0
- package/src/__tests__/setup-cli.test.ts +212 -0
- package/src/index.ts +2 -1
- package/src/setup/args.ts +78 -0
- package/src/setup-cli.ts +132 -11
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
|
|
3
|
+
export type ParsedSetupArgs = {
|
|
4
|
+
host?: string;
|
|
5
|
+
apiBasePath?: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
defaultPageSize?: string;
|
|
8
|
+
nonInteractive: boolean;
|
|
9
|
+
help: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class SetupArgsError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'SetupArgsError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseSetupArgs(argv: readonly string[]): ParsedSetupArgs {
|
|
20
|
+
let values;
|
|
21
|
+
try {
|
|
22
|
+
({ values } = parseArgs({
|
|
23
|
+
args: [...argv],
|
|
24
|
+
options: {
|
|
25
|
+
host: { type: 'string', short: 'H' },
|
|
26
|
+
'api-base-path': { type: 'string', short: 'b' },
|
|
27
|
+
token: { type: 'string', short: 't' },
|
|
28
|
+
'default-page-size': { type: 'string', short: 's' },
|
|
29
|
+
'non-interactive': { type: 'boolean', short: 'n', default: false },
|
|
30
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
31
|
+
},
|
|
32
|
+
strict: true,
|
|
33
|
+
allowPositionals: false,
|
|
34
|
+
}));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new SetupArgsError((error as Error).message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
host: trimToUndefined(values.host),
|
|
41
|
+
apiBasePath: trimToUndefined(values['api-base-path']),
|
|
42
|
+
token: trimToUndefined(values.token),
|
|
43
|
+
defaultPageSize: trimToUndefined(values['default-page-size']),
|
|
44
|
+
nonInteractive: values['non-interactive'] === true,
|
|
45
|
+
help: values.help === true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function trimToUndefined(value: string | undefined): string | undefined {
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function printSetupHelp(productId: string, log: (message: string) => void): void {
|
|
58
|
+
const lines = [
|
|
59
|
+
`Usage: @atlassian-dc-mcp/${productId} setup [options]`,
|
|
60
|
+
'',
|
|
61
|
+
'Options:',
|
|
62
|
+
' -H, --host <value> Host (e.g. jira.example.com)',
|
|
63
|
+
' -b, --api-base-path <value> API base path or full URL',
|
|
64
|
+
' -t, --token <value> API token',
|
|
65
|
+
' -s, --default-page-size <n> Default page size (positive integer)',
|
|
66
|
+
' -n, --non-interactive Skip prompts; fail if a required value is missing',
|
|
67
|
+
' -h, --help Show this help and exit',
|
|
68
|
+
'',
|
|
69
|
+
'In interactive mode (default), any value not passed as a flag is collected via prompts.',
|
|
70
|
+
'In --non-interactive mode, missing values fall back to existing configuration',
|
|
71
|
+
`(process env, ~/.atlassian-dc-mcp/${productId}.env, or macOS Keychain), and the run`,
|
|
72
|
+
'fails if a host (or full-URL --api-base-path) and token cannot be resolved.',
|
|
73
|
+
'An existing token is reused when --token is omitted.',
|
|
74
|
+
];
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
log(line);
|
|
77
|
+
}
|
|
78
|
+
}
|
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,6 +8,7 @@ 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';
|
|
11
12
|
import { SetupValueValidator } from './setup/value-validator.js';
|
|
12
13
|
|
|
13
14
|
const FALLBACK_PAGE_SIZE = 25;
|
|
@@ -56,6 +57,7 @@ export type SetupDeps = {
|
|
|
56
57
|
exit?: (code: number) => void;
|
|
57
58
|
prompts?: SetupPrompts;
|
|
58
59
|
validateCredentials?: ValidateCredentials;
|
|
60
|
+
args?: ParsedSetupArgs;
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
const DEFAULT_PROMPTS: SetupPrompts = {
|
|
@@ -64,6 +66,36 @@ const DEFAULT_PROMPTS: SetupPrompts = {
|
|
|
64
66
|
confirm: (opts) => inquirerConfirm(opts as any),
|
|
65
67
|
};
|
|
66
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
|
+
|
|
67
99
|
export async function runSetup(product: ProductDefinition, deps: SetupDeps = {}): Promise<void> {
|
|
68
100
|
const registry = deps.registry ?? buildDefaultRegistry();
|
|
69
101
|
const log = deps.log ?? ((message: string) => process.stdout.write(`${message}\n`));
|
|
@@ -75,10 +107,12 @@ export async function runSetup(product: ProductDefinition, deps: SetupDeps = {})
|
|
|
75
107
|
log(`Atlassian DC MCP setup — ${product.id}`);
|
|
76
108
|
log('');
|
|
77
109
|
|
|
78
|
-
const current =
|
|
110
|
+
const current = readCurrentConfig(registry, product);
|
|
79
111
|
printCurrent(log, registry, product, current);
|
|
80
112
|
|
|
81
|
-
const answers =
|
|
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);
|
|
82
116
|
if (!answers) {
|
|
83
117
|
return;
|
|
84
118
|
}
|
|
@@ -93,16 +127,17 @@ async function collectAnswersWithValidation(
|
|
|
93
127
|
product: ProductDefinition,
|
|
94
128
|
deps: SetupDeps,
|
|
95
129
|
prompts: SetupPrompts,
|
|
96
|
-
current:
|
|
130
|
+
current: ProductRuntimeConfig,
|
|
97
131
|
log: (message: string) => void,
|
|
98
132
|
exit: (code: number) => void,
|
|
133
|
+
args: ParsedSetupArgs | undefined,
|
|
99
134
|
): Promise<PromptResult | undefined> {
|
|
100
135
|
let defaults: PromptDefaults = current;
|
|
101
136
|
|
|
102
137
|
for (let attempt = 1; ; attempt++) {
|
|
103
138
|
let answers: PromptResult;
|
|
104
139
|
try {
|
|
105
|
-
answers = await promptForValues(prompts, product, defaults);
|
|
140
|
+
answers = await promptForValues(prompts, product, defaults, args);
|
|
106
141
|
} catch (error) {
|
|
107
142
|
if (isUserCancel(error)) {
|
|
108
143
|
exit(130);
|
|
@@ -153,6 +188,57 @@ async function collectAnswersWithValidation(
|
|
|
153
188
|
}
|
|
154
189
|
}
|
|
155
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
|
+
|
|
156
242
|
function answersAsDefaults(answers: PromptResult): PromptDefaults {
|
|
157
243
|
const pageSize = Number.parseInt(answers.defaultPageSize, 10);
|
|
158
244
|
return {
|
|
@@ -184,6 +270,32 @@ async function offerRetryAfterFailure(
|
|
|
184
270
|
return saveAnyway ? 'save-anyway' : 'abort';
|
|
185
271
|
}
|
|
186
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
|
+
|
|
187
299
|
function requireHomeFile(registry: ConfigRegistry): HomeFileSource {
|
|
188
300
|
const homeFile = registry.getWritableSource(
|
|
189
301
|
(s): s is HomeFileSource => s instanceof HomeFileSource,
|
|
@@ -198,7 +310,7 @@ function printCurrent(
|
|
|
198
310
|
log: (message: string) => void,
|
|
199
311
|
registry: ConfigRegistry,
|
|
200
312
|
product: ProductDefinition,
|
|
201
|
-
current:
|
|
313
|
+
current: ProductRuntimeConfig,
|
|
202
314
|
): void {
|
|
203
315
|
const keys: ConfigKey[] = ['host', 'apiBasePath', 'token', 'defaultPageSize'];
|
|
204
316
|
for (const key of keys) {
|
|
@@ -215,23 +327,24 @@ async function promptForValues(
|
|
|
215
327
|
prompts: SetupPrompts,
|
|
216
328
|
product: ProductDefinition,
|
|
217
329
|
defaults: PromptDefaults,
|
|
330
|
+
args: ParsedSetupArgs | undefined,
|
|
218
331
|
): Promise<PromptResult> {
|
|
219
|
-
const host = await prompts.input({
|
|
332
|
+
const host = args?.host ?? await prompts.input({
|
|
220
333
|
message: 'Host (e.g. jira.example.com):',
|
|
221
334
|
default: defaults.host ?? '',
|
|
222
335
|
validate: SetupValueValidator.host,
|
|
223
336
|
});
|
|
224
|
-
const apiBasePath = await prompts.input({
|
|
337
|
+
const apiBasePath = args?.apiBasePath ?? await prompts.input({
|
|
225
338
|
message: 'API base path:',
|
|
226
339
|
default: defaults.apiBasePath ?? product.defaultApiBasePath ?? '',
|
|
227
340
|
validate: SetupValueValidator.apiBasePath,
|
|
228
341
|
});
|
|
229
|
-
const defaultPageSize = await prompts.input({
|
|
342
|
+
const defaultPageSize = args?.defaultPageSize ?? await prompts.input({
|
|
230
343
|
message: 'Default page size:',
|
|
231
344
|
default: String(defaults.defaultPageSize ?? FALLBACK_PAGE_SIZE),
|
|
232
345
|
validate: SetupValueValidator.pageSize,
|
|
233
346
|
});
|
|
234
|
-
const token = await promptForToken(prompts, defaults.token);
|
|
347
|
+
const token = await promptForToken(prompts, defaults.token, args?.token);
|
|
235
348
|
return {
|
|
236
349
|
host: host.trim(),
|
|
237
350
|
apiBasePath: apiBasePath.trim(),
|
|
@@ -243,7 +356,11 @@ async function promptForValues(
|
|
|
243
356
|
async function promptForToken(
|
|
244
357
|
prompts: SetupPrompts,
|
|
245
358
|
existing: string | undefined,
|
|
359
|
+
fromArgs: string | undefined,
|
|
246
360
|
): Promise<TokenPromptResult> {
|
|
361
|
+
if (fromArgs) {
|
|
362
|
+
return { tokenToWrite: fromArgs, tokenForValidation: fromArgs };
|
|
363
|
+
}
|
|
247
364
|
const entered = await prompts.password({
|
|
248
365
|
message: 'API token:',
|
|
249
366
|
mask: '*',
|
|
@@ -268,7 +385,6 @@ function validateAnswers(product: ProductDefinition, answers: PromptResult): str
|
|
|
268
385
|
for (const [label, value, validator] of [
|
|
269
386
|
['host', answers.host, SetupValueValidator.host],
|
|
270
387
|
['API base path', answers.apiBasePath, SetupValueValidator.apiBasePath],
|
|
271
|
-
['API token', answers.tokenForValidation ?? '', SetupValueValidator.token],
|
|
272
388
|
] as const) {
|
|
273
389
|
const result = validator(value);
|
|
274
390
|
if (result !== true) {
|
|
@@ -283,6 +399,11 @@ function validateAnswers(product: ProductDefinition, answers: PromptResult): str
|
|
|
283
399
|
|
|
284
400
|
if (!answers.tokenForValidation) {
|
|
285
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
|
+
}
|
|
286
407
|
}
|
|
287
408
|
|
|
288
409
|
const hasHost = answers.host.length > 0;
|