@nexusmcp/cli 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/bin/cli.js +188 -66
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -15,8 +15,14 @@ token copy/paste is needed. The CLI creates a protected `.env.nexus`, project-lo
|
|
|
15
15
|
`.mcp.json`, `.nexus/project.json`, and agent guidance without printing or embedding the
|
|
16
16
|
credential in MCP configuration. For Codex, a local header helper reads `.env.nexus` at
|
|
17
17
|
connection time, so a restarted client does not depend on a globally exported token.
|
|
18
|
-
Existing configuration is preserved; conflicting project or credential context fails
|
|
19
|
-
closed.
|
|
18
|
+
Existing configuration is preserved; conflicting project or credential context fails
|
|
19
|
+
closed.
|
|
20
|
+
|
|
21
|
+
The CLI restricts `.env.nexus` to the current account (`0600` on POSIX and an
|
|
22
|
+
inheritance-disabled user/SYSTEM ACL on Windows). `doctor` fails when that protection
|
|
23
|
+
cannot be verified. The generated Codex setting uses the supported
|
|
24
|
+
`http_headers_helper` contract; it emits the Authorization header only to Codex and
|
|
25
|
+
never prints the credential in normal CLI output.
|
|
20
26
|
|
|
21
27
|
During the private beta the CLI connects to the hosted Cloud Run service by default.
|
|
22
28
|
Use `--server=<https-url>` only for an explicitly approved alternate deployment.
|
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
-
import { execSync, spawn } from 'child_process';
|
|
6
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
|
|
9
9
|
const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -25,7 +25,9 @@ const EXTENSION_TOOLS = [
|
|
|
25
25
|
'save_skill',
|
|
26
26
|
'search_skills',
|
|
27
27
|
].sort();
|
|
28
|
-
const CODEX_AUTH_HELPER_PATH = path.join('.nexus', 'auth-headers.cjs');
|
|
28
|
+
const CODEX_AUTH_HELPER_PATH = path.join('.nexus', 'auth-headers.cjs');
|
|
29
|
+
const CODEX_AUTH_HELPER_SETTING = 'http_headers_helper = "node .nexus/auth-headers.cjs"';
|
|
30
|
+
const CODEX_LEGACY_ENV_SETTING = 'bearer_token_env_var = "NEXUS_API_KEY"';
|
|
29
31
|
const CODEX_AUTH_HELPER_CONTENT = `#!/usr/bin/env node
|
|
30
32
|
// Nexus-owned Codex HTTP header helper. The bearer secret stays in .env.nexus.
|
|
31
33
|
const fs = require('node:fs');
|
|
@@ -105,6 +107,61 @@ function writeJsonFile(filePath, value) {
|
|
|
105
107
|
function sha256(content) {
|
|
106
108
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
107
109
|
}
|
|
110
|
+
|
|
111
|
+
function windowsUserSid() {
|
|
112
|
+
const result = spawnSync('whoami.exe', ['/user', '/fo', 'csv', '/nh'], {
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
116
|
+
});
|
|
117
|
+
if (result.status !== 0) return '';
|
|
118
|
+
return result.stdout.match(/S-1-(?:\d+-)+\d+/)?.[0] || '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function protectCredentialFile(filePath) {
|
|
122
|
+
const absolutePath = path.resolve(filePath);
|
|
123
|
+
if (process.platform !== 'win32') {
|
|
124
|
+
fs.chmodSync(absolutePath, 0o600);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const userSid = windowsUserSid();
|
|
129
|
+
if (!userSid) throw new Error('Unable to resolve the current Windows account SID.');
|
|
130
|
+
const result = spawnSync('icacls.exe', [
|
|
131
|
+
absolutePath,
|
|
132
|
+
'/inheritance:r',
|
|
133
|
+
'/grant:r', `*${userSid}:(F)`,
|
|
134
|
+
'/grant:r', '*S-1-5-18:(F)',
|
|
135
|
+
'/remove:g', '*S-1-1-0', '*S-1-5-11', '*S-1-5-32-545',
|
|
136
|
+
], {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
140
|
+
});
|
|
141
|
+
if (result.status !== 0) {
|
|
142
|
+
throw new Error('Windows refused the restricted credential-file ACL.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function credentialProtectionStatus(filePath) {
|
|
147
|
+
if (!fs.existsSync(filePath)) return { ok: false, detail: 'credential file is missing' };
|
|
148
|
+
if (process.platform !== 'win32') {
|
|
149
|
+
const permissions = fs.statSync(filePath).mode & 0o777;
|
|
150
|
+
return {
|
|
151
|
+
ok: (permissions & 0o077) === 0,
|
|
152
|
+
detail: `mode ${permissions.toString(8).padStart(3, '0')}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const result = spawnSync('icacls.exe', [path.resolve(filePath), '/verify'], {
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
ok: result.status === 0,
|
|
162
|
+
detail: result.status === 0 ? 'restricted Windows ACL is readable and valid' : 'Windows ACL verification failed',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
108
165
|
|
|
109
166
|
function writeTextIfAbsent(filePath, content, label) {
|
|
110
167
|
if (fs.existsSync(filePath) && !force) {
|
|
@@ -142,7 +199,7 @@ function ensureRemoteMcpJson(filePath, entry, label, explicitHttpType = false) {
|
|
|
142
199
|
}
|
|
143
200
|
}
|
|
144
201
|
|
|
145
|
-
function ensureCodexAuthHelper() {
|
|
202
|
+
function ensureCodexAuthHelper() {
|
|
146
203
|
const helperDir = path.dirname(CODEX_AUTH_HELPER_PATH);
|
|
147
204
|
if (!fs.existsSync(helperDir)) fs.mkdirSync(helperDir, { recursive: true });
|
|
148
205
|
if (fs.existsSync(CODEX_AUTH_HELPER_PATH)) {
|
|
@@ -154,17 +211,27 @@ function ensureCodexAuthHelper() {
|
|
|
154
211
|
}
|
|
155
212
|
fs.writeFileSync(CODEX_AUTH_HELPER_PATH, CODEX_AUTH_HELPER_CONTENT, { encoding: 'utf-8', mode: 0o700 });
|
|
156
213
|
try { fs.chmodSync(CODEX_AUTH_HELPER_PATH, 0o700); } catch (_) { /* Windows ACLs may ignore POSIX mode. */ }
|
|
157
|
-
console.log(' [OK] Created the local Codex credential helper.');
|
|
158
|
-
}
|
|
214
|
+
console.log(' [OK] Created the local Codex credential helper.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function inspectCodexAuthHelper() {
|
|
218
|
+
if (!fs.existsSync(CODEX_AUTH_HELPER_PATH)) {
|
|
219
|
+
return { action: 'create credential helper', label: '.nexus/auth-headers.cjs' };
|
|
220
|
+
}
|
|
221
|
+
const content = fs.readFileSync(CODEX_AUTH_HELPER_PATH, 'utf-8');
|
|
222
|
+
return content === CODEX_AUTH_HELPER_CONTENT
|
|
223
|
+
? { action: 'healthy', label: '.nexus/auth-headers.cjs' }
|
|
224
|
+
: { action: 'refused', label: '.nexus/auth-headers.cjs', detail: 'existing helper is not Nexus-owned' };
|
|
225
|
+
}
|
|
159
226
|
|
|
160
227
|
function ensureCodexProjectConfig(apiUrl) {
|
|
161
|
-
const codexDir = '.codex';
|
|
162
|
-
const configPath = path.join(codexDir, 'config.toml');
|
|
163
|
-
const block = `[mcp_servers.nexus-mcp]\nurl = "${apiUrl}/mcp"\
|
|
164
|
-
if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
|
|
165
|
-
if (!fs.existsSync(configPath)
|
|
166
|
-
fs.writeFileSync(configPath, block, 'utf-8');
|
|
167
|
-
console.log(' [OK] Created project-scoped Codex MCP configuration.');
|
|
228
|
+
const codexDir = '.codex';
|
|
229
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
230
|
+
const block = `[mcp_servers.nexus-mcp]\nurl = "${apiUrl}/mcp"\n${CODEX_AUTH_HELPER_SETTING}\nstartup_timeout_sec = 10\ntool_timeout_sec = 60\n`;
|
|
231
|
+
if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
|
|
232
|
+
if (!fs.existsSync(configPath)) {
|
|
233
|
+
fs.writeFileSync(configPath, block, 'utf-8');
|
|
234
|
+
console.log(' [OK] Created project-scoped Codex MCP configuration.');
|
|
168
235
|
return;
|
|
169
236
|
}
|
|
170
237
|
const existing = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -175,23 +242,28 @@ function ensureCodexProjectConfig(apiUrl) {
|
|
|
175
242
|
console.error('[NEXUS-CLI] Existing Nexus Codex entry points to another server; refusing to replace it.');
|
|
176
243
|
process.exit(1);
|
|
177
244
|
}
|
|
178
|
-
if (existingSection[1].includes(
|
|
179
|
-
console.log(' [NOTE] Preserved existing Nexus entry in .codex/config.toml.');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
245
|
+
if (existingSection[1].includes(CODEX_AUTH_HELPER_SETTING)) {
|
|
246
|
+
console.log(' [NOTE] Preserved existing Nexus entry in .codex/config.toml.');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!existingSection[1].includes(CODEX_LEGACY_ENV_SETTING)) {
|
|
250
|
+
console.error('[NEXUS-CLI] Existing Nexus Codex entry has an unrecognized credential source; refusing to replace it.');
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const prefix = existingSection[0].startsWith('\n') ? '\n' : '';
|
|
254
|
+
fs.writeFileSync(configPath, existing.replace(sectionPattern, `${prefix}${block.trimEnd()}`), 'utf-8');
|
|
255
|
+
console.log(' [OK] Migrated the Nexus Codex entry to its project-local credential helper.');
|
|
185
256
|
return;
|
|
186
257
|
}
|
|
187
258
|
fs.appendFileSync(configPath, `${existing.endsWith('\n') ? '' : '\n'}\n${block}`, 'utf-8');
|
|
188
259
|
console.log(' [OK] Added Nexus to existing .codex/config.toml without changing other servers.');
|
|
189
260
|
}
|
|
190
261
|
|
|
191
|
-
function readEnvCredential() {
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
262
|
+
function readEnvCredential() {
|
|
263
|
+
if (fs.existsSync('.env.nexus')) {
|
|
264
|
+
return fs.readFileSync('.env.nexus', 'utf-8').match(/^NEXUS_API_KEY=(.*)$/m)?.[1]?.trim() || '';
|
|
265
|
+
}
|
|
266
|
+
return process.env.NEXUS_API_KEY?.trim() || '';
|
|
195
267
|
}
|
|
196
268
|
|
|
197
269
|
function doctorCheck(ok, label, detail) {
|
|
@@ -420,8 +492,8 @@ function removeCodexEntry(apiUrl, apply) {
|
|
|
420
492
|
const sectionPattern = /(?:^|\n)\[mcp_servers\.nexus-mcp\]\s*\n([\s\S]*?)(?=\n\s*\[|(?![\s\S]))/m;
|
|
421
493
|
const match = content.match(sectionPattern);
|
|
422
494
|
if (!match) return { action: 'absent', label };
|
|
423
|
-
const ownedCredentialSource = match[1].includes(
|
|
424
|
-
|| match[1].includes(
|
|
495
|
+
const ownedCredentialSource = match[1].includes(CODEX_AUTH_HELPER_SETTING)
|
|
496
|
+
|| match[1].includes(CODEX_LEGACY_ENV_SETTING);
|
|
425
497
|
if (!match[1].includes(`url = "${apiUrl}/mcp"`) || !ownedCredentialSource) {
|
|
426
498
|
return { action: 'refused', label, detail: 'Nexus section does not match the recorded server and credential contract' };
|
|
427
499
|
}
|
|
@@ -578,18 +650,24 @@ function runRepair(title = 'Repair') {
|
|
|
578
650
|
repairJsonEntry(path.join('.cursor', 'mcp.json'), { url: `${project.api_url}/mcp`, headers: { Authorization: 'Bearer ${env:NEXUS_API_KEY}' } }, project.api_url, '.cursor/mcp.json', shouldApply),
|
|
579
651
|
repairJsonEntry(path.join('.agents', 'mcp_config.json'), { serverUrl: `${project.api_url}/mcp`, headers: { Authorization: 'Bearer ${NEXUS_API_KEY}' } }, project.api_url, '.agents/mcp_config.json', shouldApply),
|
|
580
652
|
];
|
|
581
|
-
let operations = collectJsonOperations(false);
|
|
582
|
-
const
|
|
653
|
+
let operations = collectJsonOperations(false);
|
|
654
|
+
const helperOperation = inspectCodexAuthHelper();
|
|
655
|
+
operations.push(helperOperation);
|
|
656
|
+
const codexPath = path.join('.codex', 'config.toml');
|
|
583
657
|
const codexContent = fs.existsSync(codexPath) ? fs.readFileSync(codexPath, 'utf-8') : '';
|
|
584
658
|
const codexHasSection = /^\s*\[mcp_servers\.nexus-mcp\]\s*$/m.test(codexContent);
|
|
585
659
|
const codexHealthy = codexHasSection
|
|
586
660
|
&& codexContent.includes(`url = "${project.api_url}/mcp"`)
|
|
587
|
-
&& codexContent.includes(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
661
|
+
&& codexContent.includes(CODEX_AUTH_HELPER_SETTING);
|
|
662
|
+
const codexOwned = !codexHasSection || (
|
|
663
|
+
codexContent.includes(`url = "${project.api_url}/mcp"`)
|
|
664
|
+
&& (codexContent.includes(CODEX_AUTH_HELPER_SETTING) || codexContent.includes(CODEX_LEGACY_ENV_SETTING))
|
|
665
|
+
);
|
|
666
|
+
operations.push(codexHealthy
|
|
667
|
+
? { action: 'healthy', label: '.codex/config.toml' }
|
|
668
|
+
: codexOwned
|
|
669
|
+
? { action: codexHasSection ? 'migrate credential helper' : 'add missing Nexus section', label: '.codex/config.toml' }
|
|
670
|
+
: { action: 'refused', label: '.codex/config.toml', detail: 'existing Nexus section is not owned by the recorded project context' });
|
|
593
671
|
|
|
594
672
|
const skillPath = path.join('.agents', 'skills', 'nexus', 'SKILL.md');
|
|
595
673
|
const skillContent = provisionedNexusSkillContent();
|
|
@@ -600,8 +678,10 @@ function runRepair(title = 'Repair') {
|
|
|
600
678
|
console.error(`[NEXUS-CLI] ${title} refused before making changes because ownership checks failed.`);
|
|
601
679
|
process.exit(1);
|
|
602
680
|
}
|
|
603
|
-
if (apply) {
|
|
681
|
+
if (apply) {
|
|
604
682
|
operations = collectJsonOperations(true);
|
|
683
|
+
if (helperOperation.action !== 'healthy') ensureCodexAuthHelper();
|
|
684
|
+
operations.push({ action: helperOperation.action === 'healthy' ? 'healthy' : 'create credential helper', label: '.nexus/auth-headers.cjs' });
|
|
605
685
|
if (!codexHealthy) {
|
|
606
686
|
ensureCodexProjectConfig(project.api_url);
|
|
607
687
|
}
|
|
@@ -636,8 +716,21 @@ async function runDoctor() {
|
|
|
636
716
|
healthy = doctorCheck(false, 'Project binding', 'run nexus init first') && healthy;
|
|
637
717
|
}
|
|
638
718
|
|
|
639
|
-
const token = readEnvCredential();
|
|
640
|
-
healthy = doctorCheck(Boolean(token), 'Scoped credential', token ? 'available without printing it' : 'set NEXUS_API_KEY or initialize .env.nexus') && healthy;
|
|
719
|
+
const token = readEnvCredential();
|
|
720
|
+
healthy = doctorCheck(Boolean(token), 'Scoped credential', token ? 'available without printing it' : 'set NEXUS_API_KEY or initialize .env.nexus') && healthy;
|
|
721
|
+
const credentialProtection = fs.existsSync('.env.nexus')
|
|
722
|
+
? credentialProtectionStatus('.env.nexus')
|
|
723
|
+
: {
|
|
724
|
+
ok: Boolean(process.env.NEXUS_API_KEY),
|
|
725
|
+
detail: process.env.NEXUS_API_KEY
|
|
726
|
+
? 'credential supplied by the process environment'
|
|
727
|
+
: 'credential file is missing',
|
|
728
|
+
};
|
|
729
|
+
healthy = doctorCheck(
|
|
730
|
+
credentialProtection.ok,
|
|
731
|
+
'Credential file permissions',
|
|
732
|
+
credentialProtection.detail,
|
|
733
|
+
) && healthy;
|
|
641
734
|
|
|
642
735
|
let mcpConfigured = false;
|
|
643
736
|
if (fs.existsSync('.mcp.json')) {
|
|
@@ -658,8 +751,9 @@ async function runDoctor() {
|
|
|
658
751
|
&& nexusRemoteEntryIsOwned(cursorEntry, apiUrl)
|
|
659
752
|
&& nexusRemoteEntryIsOwned(agentsEntry, apiUrl)
|
|
660
753
|
&& codexContent.includes(`url = "${apiUrl}/mcp"`)
|
|
661
|
-
&& codexContent.includes(
|
|
662
|
-
|
|
754
|
+
&& codexContent.includes(CODEX_AUTH_HELPER_SETTING)
|
|
755
|
+
&& inspectCodexAuthHelper().action === 'healthy';
|
|
756
|
+
healthy = doctorCheck(clientAuthHealthy, 'Supported client auth', clientAuthHealthy ? 'Cursor and agents use environment expansion; Codex uses the project-local header helper' : 'run repair to restore supported credential contracts') && healthy;
|
|
663
757
|
|
|
664
758
|
const gitignore = fs.existsSync('.gitignore') ? fs.readFileSync('.gitignore', 'utf-8') : '';
|
|
665
759
|
const secretsIgnored = gitignore.split(/\r?\n/).some((line) => line.trim() === '.env.nexus');
|
|
@@ -714,19 +808,28 @@ if (command === 'uninstall') runUninstall();
|
|
|
714
808
|
if (command === 'init') {
|
|
715
809
|
console.log('[NEXUS-CLI] 🚀 Initializing Nexus MCP in current repository...');
|
|
716
810
|
|
|
717
|
-
const apiUrl = (flags.server || flags.url || process.env.NEXUS_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
|
|
718
|
-
let token = flags.token || process.env.NEXUS_API_KEY;
|
|
719
|
-
let projectId = flags['project-id'] || process.env.NEXUS_PROJECT_ID;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
token
|
|
728
|
-
|
|
729
|
-
}
|
|
811
|
+
const apiUrl = (flags.server || flags.url || process.env.NEXUS_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
|
|
812
|
+
let token = flags.token || process.env.NEXUS_API_KEY;
|
|
813
|
+
let projectId = flags['project-id'] || process.env.NEXUS_PROJECT_ID;
|
|
814
|
+
const manualCredentialFlagProvided = Boolean(flags.token || flags['project-id']);
|
|
815
|
+
|
|
816
|
+
if (flags.token) {
|
|
817
|
+
console.warn('[NEXUS-CLI] Warning: --token may be retained in shell history. Prefer setting NEXUS_API_KEY in the process environment.');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (manualCredentialFlagProvided && (!token || !projectId)) {
|
|
821
|
+
console.error('[NEXUS-CLI] Manual credential mode requires both --token and --project-id (or the missing matching environment value).');
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!token || !projectId || token === 'nx_live_default_token') {
|
|
826
|
+
if (token || projectId) {
|
|
827
|
+
console.log('[NEXUS-CLI] Ignoring incomplete Nexus environment credentials and starting browser authorization.');
|
|
828
|
+
}
|
|
829
|
+
const authorized = await authorizeDevice(apiUrl);
|
|
830
|
+
token = authorized.token;
|
|
831
|
+
projectId = authorized.projectId;
|
|
832
|
+
}
|
|
730
833
|
if (!projectId) {
|
|
731
834
|
console.error('[NEXUS-CLI] Missing project id. Pass --project-id or set NEXUS_PROJECT_ID so hooks cannot ingest into the wrong tenant.');
|
|
732
835
|
process.exit(1);
|
|
@@ -758,22 +861,36 @@ if (command === 'init') {
|
|
|
758
861
|
},
|
|
759
862
|
'.agents/mcp_config.json'
|
|
760
863
|
);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
// 2. Write .env.nexus file with actual token
|
|
864
|
+
// 2. Write .env.nexus file with actual token
|
|
764
865
|
const envContent = `NEXUS_API_KEY=${token}\nNEXUS_API_URL=${apiUrl}\n`;
|
|
765
|
-
if (fs.existsSync('.env.nexus') && !force) {
|
|
766
|
-
const existingEnv = fs.readFileSync('.env.nexus', 'utf-8').match(/^NEXUS_API_KEY=(.*)$/m)?.[1]?.trim();
|
|
767
|
-
if (existingEnv
|
|
866
|
+
if (fs.existsSync('.env.nexus') && !force) {
|
|
867
|
+
const existingEnv = fs.readFileSync('.env.nexus', 'utf-8').match(/^NEXUS_API_KEY=(.*)$/m)?.[1]?.trim();
|
|
868
|
+
if (!existingEnv) {
|
|
869
|
+
console.error('[NEXUS-CLI] Existing .env.nexus has no valid Nexus credential; refusing to preserve it.');
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
if (existingEnv && existingEnv !== token) {
|
|
768
873
|
console.error('[NEXUS-CLI] Existing .env.nexus contains a different credential; refusing to replace it.');
|
|
769
874
|
process.exit(1);
|
|
770
875
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
876
|
+
try {
|
|
877
|
+
protectCredentialFile('.env.nexus');
|
|
878
|
+
} catch (error) {
|
|
879
|
+
console.error(`[NEXUS-CLI] ${error.message} Refusing to continue with an unprotected credential.`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
console.log(' [NOTE] Preserved existing .env.nexus credential file and refreshed its protection.');
|
|
883
|
+
} else {
|
|
884
|
+
fs.writeFileSync('.env.nexus', envContent, { encoding: 'utf-8', mode: 0o600 });
|
|
885
|
+
try {
|
|
886
|
+
protectCredentialFile('.env.nexus');
|
|
887
|
+
} catch (error) {
|
|
888
|
+
try { fs.unlinkSync('.env.nexus'); } catch (_) { /* best-effort secret cleanup */ }
|
|
889
|
+
console.error(`[NEXUS-CLI] ${error.message} The credential file was removed.`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
console.log(' [OK] Created protected .env.nexus credential file.');
|
|
893
|
+
}
|
|
777
894
|
|
|
778
895
|
// 3. Shield credentials in .gitignore
|
|
779
896
|
const gitignorePath = '.gitignore';
|
|
@@ -782,11 +899,16 @@ if (command === 'init') {
|
|
|
782
899
|
gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
783
900
|
}
|
|
784
901
|
|
|
785
|
-
if (!gitignoreContent.includes('.env.nexus')) {
|
|
902
|
+
if (!gitignoreContent.includes('.env.nexus')) {
|
|
786
903
|
gitignoreContent += '\n# Nexus Secrets & Cache Shielding\n.env.nexus\n.nexus/\n';
|
|
787
904
|
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
|
788
|
-
console.log(' [OK] Updated .gitignore to shield .env.nexus credentials.');
|
|
789
|
-
}
|
|
905
|
+
console.log(' [OK] Updated .gitignore to shield .env.nexus credentials.');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Codex supports a local HTTP-header helper. It reads the protected project
|
|
909
|
+
// credential at connection time, so Codex does not require a global env var.
|
|
910
|
+
ensureCodexAuthHelper();
|
|
911
|
+
ensureCodexProjectConfig(apiUrl);
|
|
790
912
|
|
|
791
913
|
// 4. Create .nexus/project.json
|
|
792
914
|
const nexusDir = '.nexus';
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexusmcp/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Nexus MCP Lightweight Onboarding CLI & Session Hook Assistant",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"bin",
|
|
8
|
-
"templates",
|
|
9
|
-
"README.md",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"templates",
|
|
9
|
+
"README.md",
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"bin": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"directory": "packages/cli"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node test/guidance-sync.cjs && node test/smoke.cjs && node test/session-end.cjs && node test/upgrade-doctor.cjs",
|
|
29
|
+
"test": "node test/guidance-sync.cjs && node test/smoke.cjs && node test/session-end.cjs && node test/upgrade-doctor.cjs",
|
|
30
30
|
"test:package": "node test/package-install.cjs",
|
|
31
|
-
"prepublishOnly": "npm test && npm run test:package && node --check bin/cli.js"
|
|
31
|
+
"prepublishOnly": "npm test && npm run test:package && node --check bin/cli.js"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"mcp",
|