@atlassian-dc-mcp/common 0.16.0 → 0.17.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 +23 -0
- package/build/__tests__/runtime-config.test.js +65 -14
- package/build/__tests__/runtime-config.test.js.map +1 -1
- package/build/__tests__/setup-cli.test.d.ts +2 -0
- package/build/__tests__/setup-cli.test.d.ts.map +1 -0
- package/build/__tests__/setup-cli.test.js +166 -0
- package/build/__tests__/setup-cli.test.js.map +1 -0
- package/build/config/__tests__/registry.test.d.ts +2 -0
- package/build/config/__tests__/registry.test.d.ts.map +1 -0
- package/build/config/__tests__/registry.test.js +105 -0
- package/build/config/__tests__/registry.test.js.map +1 -0
- package/build/config/__tests__/resolve-base.test.d.ts +2 -0
- package/build/config/__tests__/resolve-base.test.d.ts.map +1 -0
- package/build/config/__tests__/resolve-base.test.js +89 -0
- package/build/config/__tests__/resolve-base.test.js.map +1 -0
- package/build/config/__tests__/sources/env-file.test.d.ts +2 -0
- package/build/config/__tests__/sources/env-file.test.d.ts.map +1 -0
- package/build/config/__tests__/sources/env-file.test.js +93 -0
- package/build/config/__tests__/sources/env-file.test.js.map +1 -0
- package/build/config/__tests__/sources/home-file.test.d.ts +2 -0
- package/build/config/__tests__/sources/home-file.test.d.ts.map +1 -0
- package/build/config/__tests__/sources/home-file.test.js +114 -0
- package/build/config/__tests__/sources/home-file.test.js.map +1 -0
- package/build/config/__tests__/sources/macos-keychain.test.d.ts +2 -0
- package/build/config/__tests__/sources/macos-keychain.test.d.ts.map +1 -0
- package/build/config/__tests__/sources/macos-keychain.test.js +109 -0
- package/build/config/__tests__/sources/macos-keychain.test.js.map +1 -0
- package/build/config/__tests__/sources/process-env.test.d.ts +2 -0
- package/build/config/__tests__/sources/process-env.test.d.ts.map +1 -0
- package/build/config/__tests__/sources/process-env.test.js +51 -0
- package/build/config/__tests__/sources/process-env.test.js.map +1 -0
- package/build/config/index.d.ts +9 -0
- package/build/config/index.d.ts.map +1 -0
- package/build/config/index.js +9 -0
- package/build/config/index.js.map +1 -0
- package/build/config/registry.d.ts +26 -0
- package/build/config/registry.d.ts.map +1 -0
- package/build/config/registry.js +51 -0
- package/build/config/registry.js.map +1 -0
- package/build/config/resolve-base.d.ts +8 -0
- package/build/config/resolve-base.d.ts.map +1 -0
- package/build/config/resolve-base.js +31 -0
- package/build/config/resolve-base.js.map +1 -0
- package/build/config/runtime-config.d.ts +14 -0
- package/build/config/runtime-config.d.ts.map +1 -0
- package/build/config/runtime-config.js +41 -0
- package/build/config/runtime-config.js.map +1 -0
- package/build/config/source.d.ts +28 -0
- package/build/config/source.d.ts.map +1 -0
- package/build/config/source.js +11 -0
- package/build/config/source.js.map +1 -0
- package/build/config/sources/dotenv-file-cache.d.ts +12 -0
- package/build/config/sources/dotenv-file-cache.d.ts.map +1 -0
- package/build/config/sources/dotenv-file-cache.js +41 -0
- package/build/config/sources/dotenv-file-cache.js.map +1 -0
- package/build/config/sources/env-file.d.ts +16 -0
- package/build/config/sources/env-file.d.ts.map +1 -0
- package/build/config/sources/env-file.js +48 -0
- package/build/config/sources/env-file.js.map +1 -0
- package/build/config/sources/home-file.d.ts +19 -0
- package/build/config/sources/home-file.d.ts.map +1 -0
- package/build/config/sources/home-file.js +102 -0
- package/build/config/sources/home-file.js.map +1 -0
- package/build/config/sources/macos-keychain.d.ts +26 -0
- package/build/config/sources/macos-keychain.d.ts.map +1 -0
- package/build/config/sources/macos-keychain.js +88 -0
- package/build/config/sources/macos-keychain.js.map +1 -0
- package/build/config/sources/process-env.d.ts +8 -0
- package/build/config/sources/process-env.d.ts.map +1 -0
- package/build/config/sources/process-env.js +12 -0
- package/build/config/sources/process-env.js.map +1 -0
- 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-cli.d.ts +25 -0
- package/build/setup-cli.d.ts.map +1 -0
- package/build/setup-cli.js +215 -0
- package/build/setup-cli.js.map +1 -0
- package/package.json +5 -4
- package/src/__tests__/runtime-config.test.ts +75 -27
- package/src/__tests__/setup-cli.test.ts +175 -0
- package/src/config/__tests__/registry.test.ts +118 -0
- package/src/config/__tests__/resolve-base.test.ts +101 -0
- package/src/config/__tests__/sources/env-file.test.ts +113 -0
- package/src/config/__tests__/sources/home-file.test.ts +129 -0
- package/src/config/__tests__/sources/macos-keychain.test.ts +136 -0
- package/src/config/__tests__/sources/process-env.test.ts +59 -0
- package/src/config/index.ts +8 -0
- package/src/config/registry.ts +79 -0
- package/src/config/resolve-base.ts +43 -0
- package/src/config/runtime-config.ts +60 -0
- package/src/config/source.ts +37 -0
- package/src/config/sources/dotenv-file-cache.ts +54 -0
- package/src/config/sources/env-file.ts +61 -0
- package/src/config/sources/home-file.ts +121 -0
- package/src/config/sources/macos-keychain.ts +117 -0
- package/src/config/sources/process-env.ts +15 -0
- package/src/index.ts +2 -1
- package/src/setup-cli.ts +308 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/build/runtime-config.d.ts +0 -14
- package/build/runtime-config.d.ts.map +0 -1
- package/build/runtime-config.js +0 -123
- package/build/runtime-config.js.map +0 -1
- package/src/runtime-config.ts +0 -176
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
|
|
2
|
+
import { buildDefaultRegistry } from './config/registry.js';
|
|
3
|
+
import { getProductRuntimeConfig } from './config/runtime-config.js';
|
|
4
|
+
import { HomeFileSource, getHomeFilePath } from './config/sources/home-file.js';
|
|
5
|
+
import { MacosKeychainSource } from './config/sources/macos-keychain.js';
|
|
6
|
+
const FALLBACK_PAGE_SIZE = 25;
|
|
7
|
+
const DEFAULT_PROMPTS = {
|
|
8
|
+
input: (opts) => inquirerInput(opts),
|
|
9
|
+
password: (opts) => inquirerPassword(opts),
|
|
10
|
+
confirm: (opts) => inquirerConfirm(opts),
|
|
11
|
+
};
|
|
12
|
+
export async function runSetup(product, deps = {}) {
|
|
13
|
+
const registry = deps.registry ?? buildDefaultRegistry();
|
|
14
|
+
const log = deps.log ?? ((message) => process.stdout.write(`${message}\n`));
|
|
15
|
+
const exit = deps.exit ?? ((code) => { process.exit(code); });
|
|
16
|
+
const prompts = deps.prompts ?? DEFAULT_PROMPTS;
|
|
17
|
+
registry.initialize();
|
|
18
|
+
log(`Atlassian DC MCP setup — ${product.id}`);
|
|
19
|
+
log('');
|
|
20
|
+
const current = getProductRuntimeConfig(product);
|
|
21
|
+
printCurrent(log, registry, product, current);
|
|
22
|
+
let answers;
|
|
23
|
+
try {
|
|
24
|
+
answers = await promptForValues(prompts, product, current);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (isUserCancel(error)) {
|
|
28
|
+
exit(130);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const homeFile = requireHomeFile(registry);
|
|
34
|
+
writeNonSecretFields(registry, product, answers, homeFile, log);
|
|
35
|
+
const tokenWriter = await writeToken(registry, product, answers.token, homeFile, log, prompts);
|
|
36
|
+
printSummary(log, product, answers, tokenWriter);
|
|
37
|
+
}
|
|
38
|
+
function requireHomeFile(registry) {
|
|
39
|
+
const homeFile = registry.getWritableSource((s) => s instanceof HomeFileSource);
|
|
40
|
+
if (!homeFile) {
|
|
41
|
+
throw new Error('No writable source available for non-secret fields');
|
|
42
|
+
}
|
|
43
|
+
return homeFile;
|
|
44
|
+
}
|
|
45
|
+
function printCurrent(log, registry, product, current) {
|
|
46
|
+
const keys = ['host', 'apiBasePath', 'token', 'defaultPageSize'];
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
const primary = registry.locate(product, key)[0];
|
|
49
|
+
const label = displayLabel(key);
|
|
50
|
+
const displayValue = key === 'token' ? maskToken(current.token) : readable(current[key]);
|
|
51
|
+
const origin = primary ? `from ${primary.detail ?? primary.sourceId}` : 'not set';
|
|
52
|
+
log(`Current ${label}: ${displayValue} (${origin})`);
|
|
53
|
+
}
|
|
54
|
+
log('');
|
|
55
|
+
}
|
|
56
|
+
async function promptForValues(prompts, product, current) {
|
|
57
|
+
const host = await prompts.input({
|
|
58
|
+
message: 'Host (e.g. jira.example.com):',
|
|
59
|
+
default: current.host ?? '',
|
|
60
|
+
});
|
|
61
|
+
const apiBasePath = await prompts.input({
|
|
62
|
+
message: 'API base path:',
|
|
63
|
+
default: current.apiBasePath ?? product.defaultApiBasePath ?? '',
|
|
64
|
+
});
|
|
65
|
+
const defaultPageSize = await prompts.input({
|
|
66
|
+
message: 'Default page size:',
|
|
67
|
+
default: String(current.defaultPageSize ?? FALLBACK_PAGE_SIZE),
|
|
68
|
+
validate: (raw) => /^\d+$/.test(raw.trim()) && Number.parseInt(raw.trim(), 10) > 0
|
|
69
|
+
? true
|
|
70
|
+
: 'Enter a positive integer',
|
|
71
|
+
});
|
|
72
|
+
const token = await promptForToken(prompts, current.token);
|
|
73
|
+
return { host: host.trim(), apiBasePath: apiBasePath.trim(), defaultPageSize: defaultPageSize.trim(), token };
|
|
74
|
+
}
|
|
75
|
+
async function promptForToken(prompts, existing) {
|
|
76
|
+
const entered = await prompts.password({ message: 'API token:', mask: '*' });
|
|
77
|
+
const trimmed = entered.trim();
|
|
78
|
+
if (trimmed.length > 0) {
|
|
79
|
+
return trimmed;
|
|
80
|
+
}
|
|
81
|
+
if (!existing) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
await prompts.confirm({ message: 'Keep existing token?', default: true });
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function writeNonSecretFields(registry, product, answers, homeFile, log) {
|
|
88
|
+
for (const key of ['host', 'apiBasePath', 'defaultPageSize']) {
|
|
89
|
+
warnIfShadowed(registry, product, key, homeFile, log);
|
|
90
|
+
const value = answers[key];
|
|
91
|
+
if (value.length > 0) {
|
|
92
|
+
homeFile.write(product, key, value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function writeToken(registry, product, token, homeFile, log, prompts) {
|
|
97
|
+
if (!token) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const keychain = registry.getWritableSource((s) => s instanceof MacosKeychainSource);
|
|
101
|
+
const candidates = [];
|
|
102
|
+
if (keychain?.isAvailable()) {
|
|
103
|
+
candidates.push(keychain);
|
|
104
|
+
}
|
|
105
|
+
candidates.push(homeFile);
|
|
106
|
+
warnIfShadowed(registry, product, 'token', candidates[0], log);
|
|
107
|
+
for (const writer of candidates) {
|
|
108
|
+
const written = await tryWrite(writer, product, token, log, prompts);
|
|
109
|
+
if (written) {
|
|
110
|
+
if (writer instanceof MacosKeychainSource) {
|
|
111
|
+
homeFile.clear(product, 'token');
|
|
112
|
+
}
|
|
113
|
+
return writer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error('No writable source succeeded for token');
|
|
117
|
+
}
|
|
118
|
+
async function tryWrite(writer, product, token, log, prompts) {
|
|
119
|
+
try {
|
|
120
|
+
writer.write(product, 'token', token);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const stderr = error.stderr?.toString() ?? '';
|
|
125
|
+
log(`Failed to write token to ${writer.describe()}: ${error.message}`);
|
|
126
|
+
if (stderr) {
|
|
127
|
+
log(stderr.trim());
|
|
128
|
+
}
|
|
129
|
+
if (writer instanceof MacosKeychainSource) {
|
|
130
|
+
const fallback = await prompts.confirm({
|
|
131
|
+
message: 'Fall back to plaintext home file with mode 0600?',
|
|
132
|
+
default: false,
|
|
133
|
+
});
|
|
134
|
+
return !fallback;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function warnIfShadowed(registry, product, key, target, log) {
|
|
140
|
+
const shadowing = registry
|
|
141
|
+
.locate(product, key)
|
|
142
|
+
.filter((loc) => priorityOf(registry, loc.sourceId) > target.priority);
|
|
143
|
+
if (shadowing.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const envVar = product.envVars[key];
|
|
147
|
+
for (const loc of shadowing) {
|
|
148
|
+
if (loc.sourceId === 'process-env') {
|
|
149
|
+
log(`Warning: process.env[${envVar}] is set — runtime will prefer it over the value you're saving to ${target.describe()}. Unset it to use the new value.`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
log(`Warning: ${loc.detail ?? loc.sourceId} provides ${envVar} — runtime will prefer it over the value you're saving to ${target.describe()}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function priorityOf(registry, sourceId) {
|
|
157
|
+
const match = registry.sources.find((s) => s.id === sourceId);
|
|
158
|
+
return match?.priority ?? 0;
|
|
159
|
+
}
|
|
160
|
+
function printSummary(log, product, answers, tokenWriter) {
|
|
161
|
+
log('');
|
|
162
|
+
log('Saved configuration:');
|
|
163
|
+
log(` host: ${answers.host || '(unchanged)'}`);
|
|
164
|
+
log(` apiBasePath: ${answers.apiBasePath || '(unchanged)'}`);
|
|
165
|
+
log(` defaultPageSize: ${answers.defaultPageSize || '(unchanged)'}`);
|
|
166
|
+
if (tokenWriter) {
|
|
167
|
+
log(` token: ${maskToken(answers.token)} (stored in ${describeWriter(tokenWriter, product)})`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
log(' token: (unchanged)');
|
|
171
|
+
}
|
|
172
|
+
log(`Home file: ${getHomeFilePath(product)}`);
|
|
173
|
+
}
|
|
174
|
+
function describeWriter(writer, product) {
|
|
175
|
+
if (writer instanceof MacosKeychainSource) {
|
|
176
|
+
return `macOS Keychain (service atlassian-dc-mcp, account ${product.id}-token)`;
|
|
177
|
+
}
|
|
178
|
+
if (writer instanceof HomeFileSource) {
|
|
179
|
+
return writer.describeForProduct(product);
|
|
180
|
+
}
|
|
181
|
+
return writer.describe();
|
|
182
|
+
}
|
|
183
|
+
function displayLabel(key) {
|
|
184
|
+
switch (key) {
|
|
185
|
+
case 'host':
|
|
186
|
+
return 'host';
|
|
187
|
+
case 'apiBasePath':
|
|
188
|
+
return 'API base path';
|
|
189
|
+
case 'token':
|
|
190
|
+
return 'token';
|
|
191
|
+
case 'defaultPageSize':
|
|
192
|
+
return 'page size';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function readable(value) {
|
|
196
|
+
if (value === undefined || value === '') {
|
|
197
|
+
return '(not set)';
|
|
198
|
+
}
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
function maskToken(token) {
|
|
202
|
+
if (!token) {
|
|
203
|
+
return '(not set)';
|
|
204
|
+
}
|
|
205
|
+
const last4 = token.slice(-4);
|
|
206
|
+
return `••••${last4}`;
|
|
207
|
+
}
|
|
208
|
+
function isUserCancel(error) {
|
|
209
|
+
if (!error || typeof error !== 'object') {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const err = error;
|
|
213
|
+
return err.name === 'ExitPromptError' || /force closed/i.test(err.message ?? '');
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=setup-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-cli.js","sourceRoot":"","sources":["../src/setup-cli.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlassian-dc-mcp/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"dev": "nodemon --exec ts-node --esm src/index.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "7.10.1",
|
|
29
30
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
|
-
"dotenv": "
|
|
31
|
-
"zod": "
|
|
31
|
+
"dotenv": "16.4.7",
|
|
32
|
+
"zod": "3.25.76"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/node": "^22.13.5",
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "eb8dccc2a1ba6eebf65925209f23ed269fec3550"
|
|
42
43
|
}
|
|
@@ -1,20 +1,66 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import type { ProductDefinition } from '../config/source.js';
|
|
5
|
+
|
|
6
|
+
const JIRA: ProductDefinition = {
|
|
7
|
+
id: 'jira',
|
|
8
|
+
envVars: {
|
|
9
|
+
host: 'JIRA_HOST',
|
|
10
|
+
apiBasePath: 'JIRA_API_BASE_PATH',
|
|
11
|
+
token: 'JIRA_API_TOKEN',
|
|
12
|
+
defaultPageSize: 'JIRA_DEFAULT_PAGE_SIZE',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CONFLUENCE: ProductDefinition = {
|
|
17
|
+
id: 'confluence',
|
|
18
|
+
envVars: {
|
|
19
|
+
host: 'CONFLUENCE_HOST',
|
|
20
|
+
apiBasePath: 'CONFLUENCE_API_BASE_PATH',
|
|
21
|
+
token: 'CONFLUENCE_API_TOKEN',
|
|
22
|
+
defaultPageSize: 'CONFLUENCE_DEFAULT_PAGE_SIZE',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BITBUCKET: ProductDefinition = {
|
|
27
|
+
id: 'bitbucket',
|
|
28
|
+
envVars: {
|
|
29
|
+
host: 'BITBUCKET_HOST',
|
|
30
|
+
apiBasePath: 'BITBUCKET_API_BASE_PATH',
|
|
31
|
+
token: 'BITBUCKET_API_TOKEN',
|
|
32
|
+
defaultPageSize: 'BITBUCKET_DEFAULT_PAGE_SIZE',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('runtime config facade', () => {
|
|
6
37
|
const originalEnv = process.env;
|
|
7
38
|
let tempDir: string;
|
|
39
|
+
let tempHome: string;
|
|
40
|
+
let homedirSpy: jest.SpyInstance;
|
|
8
41
|
|
|
9
42
|
beforeEach(() => {
|
|
10
43
|
jest.resetModules();
|
|
11
44
|
process.env = { ...originalEnv };
|
|
12
45
|
delete process.env.ATLASSIAN_DC_MCP_CONFIG_FILE;
|
|
46
|
+
delete process.env.JIRA_HOST;
|
|
47
|
+
delete process.env.JIRA_API_TOKEN;
|
|
48
|
+
delete process.env.JIRA_API_BASE_PATH;
|
|
49
|
+
delete process.env.JIRA_DEFAULT_PAGE_SIZE;
|
|
50
|
+
delete process.env.CONFLUENCE_HOST;
|
|
51
|
+
delete process.env.CONFLUENCE_API_TOKEN;
|
|
52
|
+
delete process.env.CONFLUENCE_DEFAULT_PAGE_SIZE;
|
|
53
|
+
delete process.env.BITBUCKET_HOST;
|
|
54
|
+
delete process.env.BITBUCKET_API_TOKEN;
|
|
13
55
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atlassian-dc-mcp-config-'));
|
|
56
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'atlassian-dc-mcp-home-'));
|
|
57
|
+
homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome);
|
|
14
58
|
});
|
|
15
59
|
|
|
16
60
|
afterEach(() => {
|
|
61
|
+
homedirSpy.mockRestore();
|
|
17
62
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
18
64
|
});
|
|
19
65
|
|
|
20
66
|
afterAll(() => {
|
|
@@ -26,11 +72,9 @@ describe('runtime config loader', () => {
|
|
|
26
72
|
fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\n');
|
|
27
73
|
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
|
|
28
74
|
|
|
29
|
-
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../
|
|
30
|
-
|
|
75
|
+
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
|
|
31
76
|
initializeRuntimeConfig({ cwd: tempDir });
|
|
32
|
-
|
|
33
|
-
expect(getProductRuntimeConfig('jira')).toEqual({
|
|
77
|
+
expect(getProductRuntimeConfig(JIRA)).toEqual({
|
|
34
78
|
host: 'file-host',
|
|
35
79
|
apiBasePath: undefined,
|
|
36
80
|
token: 'file-token',
|
|
@@ -40,9 +84,7 @@ describe('runtime config loader', () => {
|
|
|
40
84
|
|
|
41
85
|
it('throws when the explicit shared config file is missing', async () => {
|
|
42
86
|
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = path.join(tempDir, 'missing.env');
|
|
43
|
-
|
|
44
|
-
const { initializeRuntimeConfig } = await import('../runtime-config.js');
|
|
45
|
-
|
|
87
|
+
const { initializeRuntimeConfig } = await import('../index.js');
|
|
46
88
|
expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow(
|
|
47
89
|
`ATLASSIAN_DC_MCP_CONFIG_FILE points to a missing file: ${path.join(tempDir, 'missing.env')}`,
|
|
48
90
|
);
|
|
@@ -50,26 +92,22 @@ describe('runtime config loader', () => {
|
|
|
50
92
|
|
|
51
93
|
it('requires an absolute shared config file path', async () => {
|
|
52
94
|
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = 'relative/shared.env';
|
|
53
|
-
|
|
54
|
-
const { initializeRuntimeConfig } = await import('../runtime-config.js');
|
|
55
|
-
|
|
95
|
+
const { initializeRuntimeConfig } = await import('../index.js');
|
|
56
96
|
expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow(
|
|
57
97
|
'ATLASSIAN_DC_MCP_CONFIG_FILE must be an absolute path: relative/shared.env',
|
|
58
98
|
);
|
|
59
99
|
});
|
|
60
100
|
|
|
61
|
-
it('keeps
|
|
101
|
+
it('keeps env variables higher priority than file values', async () => {
|
|
62
102
|
const sharedConfigPath = path.join(tempDir, 'shared.env');
|
|
63
103
|
fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\nJIRA_DEFAULT_PAGE_SIZE=50\n');
|
|
64
104
|
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
|
|
65
105
|
process.env.JIRA_API_TOKEN = 'env-token';
|
|
66
106
|
process.env.JIRA_DEFAULT_PAGE_SIZE = '10';
|
|
67
107
|
|
|
68
|
-
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../
|
|
69
|
-
|
|
108
|
+
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
|
|
70
109
|
initializeRuntimeConfig({ cwd: tempDir });
|
|
71
|
-
|
|
72
|
-
expect(getProductRuntimeConfig('jira')).toEqual({
|
|
110
|
+
expect(getProductRuntimeConfig(JIRA)).toEqual({
|
|
73
111
|
host: 'file-host',
|
|
74
112
|
apiBasePath: undefined,
|
|
75
113
|
token: 'env-token',
|
|
@@ -77,17 +115,14 @@ describe('runtime config loader', () => {
|
|
|
77
115
|
});
|
|
78
116
|
});
|
|
79
117
|
|
|
80
|
-
it('falls back to
|
|
118
|
+
it('falls back to cwd/.env when no explicit shared file is configured', async () => {
|
|
81
119
|
fs.writeFileSync(
|
|
82
120
|
path.join(tempDir, '.env'),
|
|
83
121
|
'CONFLUENCE_HOST=cwd-host\nCONFLUENCE_API_TOKEN=cwd-token\nCONFLUENCE_DEFAULT_PAGE_SIZE=30\n',
|
|
84
122
|
);
|
|
85
|
-
|
|
86
|
-
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
|
|
87
|
-
|
|
123
|
+
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
|
|
88
124
|
initializeRuntimeConfig({ cwd: tempDir });
|
|
89
|
-
|
|
90
|
-
expect(getProductRuntimeConfig('confluence')).toEqual({
|
|
125
|
+
expect(getProductRuntimeConfig(CONFLUENCE)).toEqual({
|
|
91
126
|
host: 'cwd-host',
|
|
92
127
|
apiBasePath: undefined,
|
|
93
128
|
token: 'cwd-token',
|
|
@@ -102,15 +137,28 @@ describe('runtime config loader', () => {
|
|
|
102
137
|
fs.utimesSync(sharedConfigPath, initialTime, initialTime);
|
|
103
138
|
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
|
|
104
139
|
|
|
105
|
-
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../
|
|
106
|
-
|
|
140
|
+
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
|
|
107
141
|
initializeRuntimeConfig({ cwd: tempDir });
|
|
108
|
-
expect(getProductRuntimeConfig(
|
|
142
|
+
expect(getProductRuntimeConfig(BITBUCKET).token).toBe('token-a');
|
|
109
143
|
|
|
110
144
|
fs.writeFileSync(sharedConfigPath, 'BITBUCKET_HOST=file-host\nBITBUCKET_API_TOKEN=token-b\n');
|
|
111
145
|
const updatedTime = new Date('2026-01-01T00:00:01.000Z');
|
|
112
146
|
fs.utimesSync(sharedConfigPath, updatedTime, updatedTime);
|
|
113
147
|
|
|
114
|
-
expect(getProductRuntimeConfig(
|
|
148
|
+
expect(getProductRuntimeConfig(BITBUCKET).token).toBe('token-b');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('reads the home file when env and env-file are empty', async () => {
|
|
152
|
+
fs.mkdirSync(path.join(tempHome, '.atlassian-dc-mcp'), { recursive: true });
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(tempHome, '.atlassian-dc-mcp', 'jira.env'),
|
|
155
|
+
'JIRA_HOST=home-host\nJIRA_API_TOKEN=home-token\n',
|
|
156
|
+
{ mode: 0o600 },
|
|
157
|
+
);
|
|
158
|
+
const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
|
|
159
|
+
initializeRuntimeConfig({ cwd: tempDir });
|
|
160
|
+
const config = getProductRuntimeConfig(JIRA);
|
|
161
|
+
expect(config.host).toBe('home-host');
|
|
162
|
+
expect(config.token).toBe('home-token');
|
|
115
163
|
});
|
|
116
164
|
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { DefaultConfigRegistry } from '../config/registry.js';
|
|
2
|
+
import { HomeFileSource } from '../config/sources/home-file.js';
|
|
3
|
+
import { MacosKeychainSource, type KeychainDeps } from '../config/sources/macos-keychain.js';
|
|
4
|
+
import { ProcessEnvSource } from '../config/sources/process-env.js';
|
|
5
|
+
import { runSetup, type SetupPrompts } from '../setup-cli.js';
|
|
6
|
+
import type { ConfigKey, ProductDefinition } from '../config/source.js';
|
|
7
|
+
|
|
8
|
+
const JIRA: ProductDefinition = {
|
|
9
|
+
id: 'jira',
|
|
10
|
+
envVars: {
|
|
11
|
+
host: 'JIRA_HOST',
|
|
12
|
+
apiBasePath: 'JIRA_API_BASE_PATH',
|
|
13
|
+
token: 'JIRA_API_TOKEN',
|
|
14
|
+
defaultPageSize: 'JIRA_DEFAULT_PAGE_SIZE',
|
|
15
|
+
},
|
|
16
|
+
defaultApiBasePath: '/rest/api/2',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class FakeKeychain extends MacosKeychainSource {
|
|
20
|
+
available = true;
|
|
21
|
+
store: string | undefined;
|
|
22
|
+
writeCalls = 0;
|
|
23
|
+
clearCalls = 0;
|
|
24
|
+
constructor() {
|
|
25
|
+
const deps: Partial<KeychainDeps> = {
|
|
26
|
+
execFileSync: (() => '') as unknown as KeychainDeps['execFileSync'],
|
|
27
|
+
existsSync: () => true,
|
|
28
|
+
getPlatform: () => 'darwin' as NodeJS.Platform,
|
|
29
|
+
};
|
|
30
|
+
super(deps);
|
|
31
|
+
}
|
|
32
|
+
override isAvailable(): boolean {
|
|
33
|
+
return this.available;
|
|
34
|
+
}
|
|
35
|
+
override read(_p: ProductDefinition, key: ConfigKey): string | undefined {
|
|
36
|
+
return key === 'token' ? this.store : undefined;
|
|
37
|
+
}
|
|
38
|
+
override write(_p: ProductDefinition, _k: ConfigKey, v: string): void {
|
|
39
|
+
this.writeCalls++;
|
|
40
|
+
this.store = v;
|
|
41
|
+
}
|
|
42
|
+
override clear(_p: ProductDefinition, _k: ConfigKey): void {
|
|
43
|
+
this.clearCalls++;
|
|
44
|
+
this.store = undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class FakeHomeFile extends HomeFileSource {
|
|
49
|
+
values: Record<string, Partial<Record<ConfigKey, string>>> = {
|
|
50
|
+
jira: {},
|
|
51
|
+
confluence: {},
|
|
52
|
+
bitbucket: {},
|
|
53
|
+
};
|
|
54
|
+
writes: Array<[string, ConfigKey, string]> = [];
|
|
55
|
+
clears: Array<[string, ConfigKey]> = [];
|
|
56
|
+
override read(product: ProductDefinition, key: ConfigKey): string | undefined {
|
|
57
|
+
return this.values[product.id]?.[key];
|
|
58
|
+
}
|
|
59
|
+
override write(product: ProductDefinition, key: ConfigKey, value: string): void {
|
|
60
|
+
this.values[product.id] = this.values[product.id] ?? {};
|
|
61
|
+
this.values[product.id][key] = value;
|
|
62
|
+
this.writes.push([product.id, key, value]);
|
|
63
|
+
}
|
|
64
|
+
override clear(product: ProductDefinition, key: ConfigKey): void {
|
|
65
|
+
if (this.values[product.id]) {
|
|
66
|
+
delete this.values[product.id][key];
|
|
67
|
+
}
|
|
68
|
+
this.clears.push([product.id, key]);
|
|
69
|
+
}
|
|
70
|
+
override describeForProduct(product: ProductDefinition): string {
|
|
71
|
+
return `home-file-fake(${product.id})`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeRegistry(keychain: FakeKeychain, home: FakeHomeFile) {
|
|
76
|
+
return new DefaultConfigRegistry([new ProcessEnvSource(), home, keychain]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makePrompts(overrides: Partial<SetupPrompts> = {}): SetupPrompts {
|
|
80
|
+
return {
|
|
81
|
+
input: async (opts) => opts.default ?? '',
|
|
82
|
+
password: async () => '',
|
|
83
|
+
confirm: async (opts) => opts.default ?? false,
|
|
84
|
+
...overrides,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('runSetup', () => {
|
|
89
|
+
const originalEnv = process.env;
|
|
90
|
+
let keychain: FakeKeychain;
|
|
91
|
+
let home: FakeHomeFile;
|
|
92
|
+
let logs: string[];
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
process.env = { ...originalEnv };
|
|
96
|
+
delete process.env.JIRA_API_TOKEN;
|
|
97
|
+
delete process.env.JIRA_HOST;
|
|
98
|
+
keychain = new FakeKeychain();
|
|
99
|
+
home = new FakeHomeFile();
|
|
100
|
+
logs = [];
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterAll(() => {
|
|
104
|
+
process.env = originalEnv;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('writes the token to keychain first on darwin and clears home file token', async () => {
|
|
108
|
+
const prompts = makePrompts({
|
|
109
|
+
input: async (opts) => {
|
|
110
|
+
if (opts.message.startsWith('Host')) return 'j-host';
|
|
111
|
+
if (opts.message.startsWith('API base path')) return '/rest/api/2';
|
|
112
|
+
return '25';
|
|
113
|
+
},
|
|
114
|
+
password: async () => 'secret',
|
|
115
|
+
});
|
|
116
|
+
const registry = makeRegistry(keychain, home);
|
|
117
|
+
await runSetup(JIRA, { registry, log: (m) => logs.push(m), exit: () => undefined, prompts });
|
|
118
|
+
|
|
119
|
+
expect(keychain.writeCalls).toBe(1);
|
|
120
|
+
expect(keychain.store).toBe('secret');
|
|
121
|
+
expect(home.clears).toContainEqual(['jira', 'token']);
|
|
122
|
+
expect(home.values.jira.host).toBe('j-host');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('falls back to home file when keychain is unavailable', async () => {
|
|
126
|
+
keychain.available = false;
|
|
127
|
+
const prompts = makePrompts({
|
|
128
|
+
input: async (opts) => {
|
|
129
|
+
if (opts.message.startsWith('Host')) return 'j-host';
|
|
130
|
+
if (opts.message.startsWith('API base path')) return '/rest/api/2';
|
|
131
|
+
return '25';
|
|
132
|
+
},
|
|
133
|
+
password: async () => 'secret',
|
|
134
|
+
});
|
|
135
|
+
const registry = makeRegistry(keychain, home);
|
|
136
|
+
await runSetup(JIRA, { registry, log: (m) => logs.push(m), exit: () => undefined, prompts });
|
|
137
|
+
|
|
138
|
+
expect(keychain.writeCalls).toBe(0);
|
|
139
|
+
const tokenWrites = home.writes.filter(([, k]) => k === 'token');
|
|
140
|
+
expect(tokenWrites).toHaveLength(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('prints shadowing warning when env var is set higher than target', async () => {
|
|
144
|
+
process.env.JIRA_API_TOKEN = 'shadow';
|
|
145
|
+
const prompts = makePrompts({
|
|
146
|
+
input: async (opts) => {
|
|
147
|
+
if (opts.message.startsWith('Host')) return 'j-host';
|
|
148
|
+
if (opts.message.startsWith('API base path')) return '/rest/api/2';
|
|
149
|
+
return '25';
|
|
150
|
+
},
|
|
151
|
+
password: async () => 'secret',
|
|
152
|
+
});
|
|
153
|
+
const registry = makeRegistry(keychain, home);
|
|
154
|
+
await runSetup(JIRA, { registry, log: (m) => logs.push(m), exit: () => undefined, prompts });
|
|
155
|
+
|
|
156
|
+
expect(logs.some((l) => l.includes('JIRA_API_TOKEN') && l.includes('Warning'))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('exits 130 on SIGINT cancellation before any write', async () => {
|
|
160
|
+
const exitErr: Error & { name: string } = Object.assign(new Error('closed'), {
|
|
161
|
+
name: 'ExitPromptError',
|
|
162
|
+
});
|
|
163
|
+
const prompts = makePrompts({
|
|
164
|
+
input: async () => {
|
|
165
|
+
throw exitErr;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const exitFn = jest.fn();
|
|
169
|
+
const registry = makeRegistry(keychain, home);
|
|
170
|
+
await runSetup(JIRA, { registry, log: (m) => logs.push(m), exit: exitFn, prompts });
|
|
171
|
+
expect(exitFn).toHaveBeenCalledWith(130);
|
|
172
|
+
expect(keychain.writeCalls).toBe(0);
|
|
173
|
+
expect(home.writes).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
});
|