@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/build/__tests__/runtime-config.test.js +65 -14
  3. package/build/__tests__/runtime-config.test.js.map +1 -1
  4. package/build/__tests__/setup-cli.test.d.ts +2 -0
  5. package/build/__tests__/setup-cli.test.d.ts.map +1 -0
  6. package/build/__tests__/setup-cli.test.js +166 -0
  7. package/build/__tests__/setup-cli.test.js.map +1 -0
  8. package/build/config/__tests__/registry.test.d.ts +2 -0
  9. package/build/config/__tests__/registry.test.d.ts.map +1 -0
  10. package/build/config/__tests__/registry.test.js +105 -0
  11. package/build/config/__tests__/registry.test.js.map +1 -0
  12. package/build/config/__tests__/resolve-base.test.d.ts +2 -0
  13. package/build/config/__tests__/resolve-base.test.d.ts.map +1 -0
  14. package/build/config/__tests__/resolve-base.test.js +89 -0
  15. package/build/config/__tests__/resolve-base.test.js.map +1 -0
  16. package/build/config/__tests__/sources/env-file.test.d.ts +2 -0
  17. package/build/config/__tests__/sources/env-file.test.d.ts.map +1 -0
  18. package/build/config/__tests__/sources/env-file.test.js +93 -0
  19. package/build/config/__tests__/sources/env-file.test.js.map +1 -0
  20. package/build/config/__tests__/sources/home-file.test.d.ts +2 -0
  21. package/build/config/__tests__/sources/home-file.test.d.ts.map +1 -0
  22. package/build/config/__tests__/sources/home-file.test.js +114 -0
  23. package/build/config/__tests__/sources/home-file.test.js.map +1 -0
  24. package/build/config/__tests__/sources/macos-keychain.test.d.ts +2 -0
  25. package/build/config/__tests__/sources/macos-keychain.test.d.ts.map +1 -0
  26. package/build/config/__tests__/sources/macos-keychain.test.js +109 -0
  27. package/build/config/__tests__/sources/macos-keychain.test.js.map +1 -0
  28. package/build/config/__tests__/sources/process-env.test.d.ts +2 -0
  29. package/build/config/__tests__/sources/process-env.test.d.ts.map +1 -0
  30. package/build/config/__tests__/sources/process-env.test.js +51 -0
  31. package/build/config/__tests__/sources/process-env.test.js.map +1 -0
  32. package/build/config/index.d.ts +9 -0
  33. package/build/config/index.d.ts.map +1 -0
  34. package/build/config/index.js +9 -0
  35. package/build/config/index.js.map +1 -0
  36. package/build/config/registry.d.ts +26 -0
  37. package/build/config/registry.d.ts.map +1 -0
  38. package/build/config/registry.js +51 -0
  39. package/build/config/registry.js.map +1 -0
  40. package/build/config/resolve-base.d.ts +8 -0
  41. package/build/config/resolve-base.d.ts.map +1 -0
  42. package/build/config/resolve-base.js +31 -0
  43. package/build/config/resolve-base.js.map +1 -0
  44. package/build/config/runtime-config.d.ts +14 -0
  45. package/build/config/runtime-config.d.ts.map +1 -0
  46. package/build/config/runtime-config.js +41 -0
  47. package/build/config/runtime-config.js.map +1 -0
  48. package/build/config/source.d.ts +28 -0
  49. package/build/config/source.d.ts.map +1 -0
  50. package/build/config/source.js +11 -0
  51. package/build/config/source.js.map +1 -0
  52. package/build/config/sources/dotenv-file-cache.d.ts +12 -0
  53. package/build/config/sources/dotenv-file-cache.d.ts.map +1 -0
  54. package/build/config/sources/dotenv-file-cache.js +41 -0
  55. package/build/config/sources/dotenv-file-cache.js.map +1 -0
  56. package/build/config/sources/env-file.d.ts +16 -0
  57. package/build/config/sources/env-file.d.ts.map +1 -0
  58. package/build/config/sources/env-file.js +48 -0
  59. package/build/config/sources/env-file.js.map +1 -0
  60. package/build/config/sources/home-file.d.ts +19 -0
  61. package/build/config/sources/home-file.d.ts.map +1 -0
  62. package/build/config/sources/home-file.js +102 -0
  63. package/build/config/sources/home-file.js.map +1 -0
  64. package/build/config/sources/macos-keychain.d.ts +26 -0
  65. package/build/config/sources/macos-keychain.d.ts.map +1 -0
  66. package/build/config/sources/macos-keychain.js +88 -0
  67. package/build/config/sources/macos-keychain.js.map +1 -0
  68. package/build/config/sources/process-env.d.ts +8 -0
  69. package/build/config/sources/process-env.d.ts.map +1 -0
  70. package/build/config/sources/process-env.js +12 -0
  71. package/build/config/sources/process-env.js.map +1 -0
  72. package/build/index.d.ts +2 -1
  73. package/build/index.d.ts.map +1 -1
  74. package/build/index.js +2 -1
  75. package/build/index.js.map +1 -1
  76. package/build/setup-cli.d.ts +25 -0
  77. package/build/setup-cli.d.ts.map +1 -0
  78. package/build/setup-cli.js +215 -0
  79. package/build/setup-cli.js.map +1 -0
  80. package/package.json +5 -4
  81. package/src/__tests__/runtime-config.test.ts +75 -27
  82. package/src/__tests__/setup-cli.test.ts +175 -0
  83. package/src/config/__tests__/registry.test.ts +118 -0
  84. package/src/config/__tests__/resolve-base.test.ts +101 -0
  85. package/src/config/__tests__/sources/env-file.test.ts +113 -0
  86. package/src/config/__tests__/sources/home-file.test.ts +129 -0
  87. package/src/config/__tests__/sources/macos-keychain.test.ts +136 -0
  88. package/src/config/__tests__/sources/process-env.test.ts +59 -0
  89. package/src/config/index.ts +8 -0
  90. package/src/config/registry.ts +79 -0
  91. package/src/config/resolve-base.ts +43 -0
  92. package/src/config/runtime-config.ts +60 -0
  93. package/src/config/source.ts +37 -0
  94. package/src/config/sources/dotenv-file-cache.ts +54 -0
  95. package/src/config/sources/env-file.ts +61 -0
  96. package/src/config/sources/home-file.ts +121 -0
  97. package/src/config/sources/macos-keychain.ts +117 -0
  98. package/src/config/sources/process-env.ts +15 -0
  99. package/src/index.ts +2 -1
  100. package/src/setup-cli.ts +308 -0
  101. package/tsconfig.tsbuildinfo +1 -1
  102. package/build/runtime-config.d.ts +0 -14
  103. package/build/runtime-config.d.ts.map +0 -1
  104. package/build/runtime-config.js +0 -123
  105. package/build/runtime-config.js.map +0 -1
  106. 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.16.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": "^16.4.7",
31
- "zod": "^3.24.2"
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": "eb8394fc8317b2d986b03de9853edf47aa77566a"
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
- describe('runtime config loader', () => {
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('../runtime-config.js');
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 environment variables higher priority than file values', async () => {
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('../runtime-config.js');
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 the cwd .env file when no explicit shared file is configured', async () => {
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('../runtime-config.js');
106
-
140
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../index.js');
107
141
  initializeRuntimeConfig({ cwd: tempDir });
108
- expect(getProductRuntimeConfig('bitbucket').token).toBe('token-a');
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('bitbucket').token).toBe('token-b');
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
+ });