@magentrix-corp/magentrix-cli 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +258 -15
- package/actions/pull.js +520 -327
- package/actions/setup.js +62 -15
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/cli/helpers/ensureApiKey.js +28 -22
- package/utils/cli/helpers/ensureInstanceUrl.js +35 -27
- package/utils/cli/writeRecords.js +13 -2
- package/utils/config.js +76 -0
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/auth.js +45 -6
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +14 -2
- package/vars/global.js +1 -0
package/actions/setup.js
CHANGED
|
@@ -4,6 +4,7 @@ import Config from "../utils/config.js";
|
|
|
4
4
|
import { getAccessToken, tryAuthenticate } from "../utils/magentrix/api/auth.js";
|
|
5
5
|
import { ensureVSCodeFileAssociation } from "../utils/preferences.js";
|
|
6
6
|
import { EXPORT_ROOT, HASHED_CWD } from "../vars/global.js";
|
|
7
|
+
import { select } from "@inquirer/prompts";
|
|
7
8
|
|
|
8
9
|
const config = new Config();
|
|
9
10
|
|
|
@@ -35,24 +36,70 @@ export const setup = async (cliOptions = {}) => {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
if (cliOptions.instanceUrl) {
|
|
38
|
-
|
|
39
|
+
let trimmedUrl = cliOptions.instanceUrl.trim();
|
|
40
|
+
// Automatically strip trailing slashes
|
|
41
|
+
trimmedUrl = trimmedUrl.replace(/\/+$/, '');
|
|
42
|
+
|
|
39
43
|
if (!urlRegex.test(trimmedUrl)) {
|
|
40
|
-
throw new Error('--instance-url must be in the form: https://subdomain.domain.com (
|
|
44
|
+
throw new Error('--instance-url must be in the form: https://subdomain.domain.com (no http, no extra path)');
|
|
41
45
|
}
|
|
46
|
+
// Update the option with the cleaned URL
|
|
47
|
+
cliOptions.instanceUrl = trimmedUrl;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
// Retry loop for authentication
|
|
51
|
+
let authSuccess = false;
|
|
52
|
+
let apiKey, instanceUrl, tokenData;
|
|
53
|
+
|
|
54
|
+
while (!authSuccess) {
|
|
55
|
+
// Get API key (from CLI or prompt)
|
|
56
|
+
apiKey = cliOptions.apiKey
|
|
57
|
+
? cliOptions.apiKey.trim()
|
|
58
|
+
: await ensureApiKey(true);
|
|
59
|
+
|
|
60
|
+
// Get instance URL (from CLI or prompt)
|
|
61
|
+
instanceUrl = cliOptions.instanceUrl
|
|
62
|
+
? cliOptions.instanceUrl.trim()
|
|
63
|
+
: await ensureInstanceUrl(true);
|
|
64
|
+
|
|
65
|
+
// Validate credentials by attempting to fetch an access token
|
|
66
|
+
try {
|
|
67
|
+
tokenData = await tryAuthenticate(apiKey, instanceUrl);
|
|
68
|
+
authSuccess = true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.log(error.message);
|
|
71
|
+
console.log();
|
|
72
|
+
|
|
73
|
+
// Ask user if they want to retry
|
|
74
|
+
try {
|
|
75
|
+
const retry = await select({
|
|
76
|
+
message: 'What would you like to do?',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: 'Try again', value: 'retry' },
|
|
79
|
+
{ name: 'Exit setup', value: 'exit' }
|
|
80
|
+
],
|
|
81
|
+
default: 'retry'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (retry !== 'retry') {
|
|
85
|
+
console.log('❌ Setup cancelled.');
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Reset CLI options so user can re-enter them
|
|
90
|
+
cliOptions.apiKey = null;
|
|
91
|
+
cliOptions.instanceUrl = null;
|
|
92
|
+
console.log(); // Blank line for spacing
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Handle Ctrl+C or other cancellation
|
|
95
|
+
if (error.name === 'ExitPromptError') {
|
|
96
|
+
console.log('\n❌ Setup cancelled.');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
56
103
|
|
|
57
104
|
console.log(); // Blank line for spacing
|
|
58
105
|
|
|
@@ -69,7 +116,7 @@ export const setup = async (cliOptions = {}) => {
|
|
|
69
116
|
{ global: true, pathHash: HASHED_CWD }
|
|
70
117
|
);
|
|
71
118
|
|
|
72
|
-
// Set up the editor
|
|
119
|
+
// Set up the editor
|
|
73
120
|
await ensureVSCodeFileAssociation('./');
|
|
74
121
|
|
|
75
122
|
console.log(); // Blank line for spacing
|
package/bin/magentrix.js
CHANGED
|
@@ -15,6 +15,7 @@ import { EXPORT_ROOT } from '../vars/global.js';
|
|
|
15
15
|
import { publish } from '../actions/publish.js';
|
|
16
16
|
import { update } from '../actions/update.js';
|
|
17
17
|
import { configWizard } from '../actions/config.js';
|
|
18
|
+
import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
|
|
18
19
|
|
|
19
20
|
// ── Middleware ────────────────────────────────
|
|
20
21
|
async function preMiddleware() {
|
|
@@ -60,7 +61,12 @@ program
|
|
|
60
61
|
{ name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
|
|
61
62
|
{ name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
|
|
62
63
|
{ name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' },
|
|
63
|
-
{ name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' }
|
|
64
|
+
{ name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' },
|
|
65
|
+
{ name: 'iris-link', desc: 'Link a Vue project to the CLI', icon: '🔗 ' },
|
|
66
|
+
{ name: 'vue-build-stage', desc: 'Build Vue project and stage for publish', icon: '🏗️ ' },
|
|
67
|
+
{ name: 'iris-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
|
|
68
|
+
{ name: 'iris-delete', desc: 'Delete an Iris app with backup', icon: '🗑️ ' },
|
|
69
|
+
{ name: 'iris-recover', desc: 'Recover a deleted Iris app from backup', icon: '♻️ ' }
|
|
64
70
|
];
|
|
65
71
|
|
|
66
72
|
const maxNameLen = Math.max(...commands.map(c => c.name.length));
|
|
@@ -191,6 +197,42 @@ program
|
|
|
191
197
|
.description('Configure CLI settings')
|
|
192
198
|
.action(configWizard);
|
|
193
199
|
|
|
200
|
+
// Iris commands for Vue.js app management
|
|
201
|
+
program
|
|
202
|
+
.command('iris-link')
|
|
203
|
+
.description('Link a Vue project to the CLI for deployment')
|
|
204
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
205
|
+
.option('--unlink', 'Unlink a project instead of linking')
|
|
206
|
+
.option('--list', 'List all linked projects')
|
|
207
|
+
.option('--cleanup', 'Remove invalid (non-existent) linked projects')
|
|
208
|
+
.action(irisLink);
|
|
209
|
+
|
|
210
|
+
program
|
|
211
|
+
.command('vue-build-stage')
|
|
212
|
+
.description('Build a Vue project and stage it for publishing')
|
|
213
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
214
|
+
.option('--skip-build', 'Skip build step and use existing dist/')
|
|
215
|
+
.action(vueBuildStage);
|
|
216
|
+
|
|
217
|
+
program
|
|
218
|
+
.command('iris-dev')
|
|
219
|
+
.description('Start Vue dev server with platform assets injected')
|
|
220
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
221
|
+
.option('--no-inject', 'Skip asset injection')
|
|
222
|
+
.option('--restore', 'Restore config.ts from backup')
|
|
223
|
+
.action(irisDev);
|
|
224
|
+
|
|
225
|
+
program
|
|
226
|
+
.command('iris-delete')
|
|
227
|
+
.description('Delete a published Iris app with recovery backup')
|
|
228
|
+
.action(irisDelete);
|
|
229
|
+
|
|
230
|
+
program
|
|
231
|
+
.command('iris-recover')
|
|
232
|
+
.description('Recover a deleted Iris app from backup')
|
|
233
|
+
.option('--list', 'List available recovery backups')
|
|
234
|
+
.action(irisRecover);
|
|
235
|
+
|
|
194
236
|
// ── Unknown Command Handler ──────────────────
|
|
195
237
|
program.argument('[command]', 'command to run').action((cmd) => {
|
|
196
238
|
const runMain = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magentrix-corp/magentrix-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI tool for synchronizing local files with Magentrix cloud platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@inquirer/prompts": "^7.6.0",
|
|
39
|
+
"archiver": "^7.0.1",
|
|
39
40
|
"chalk": "^5.4.1",
|
|
40
41
|
"chokidar": "^4.0.3",
|
|
41
42
|
"commander": "^14.0.0",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const LOCK_FILE_NAME = 'autopublish.lock';
|
|
5
|
+
const LOCK_EXPIRY_MS = 3600000; // 1 hour
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the path to the autopublish lock file.
|
|
9
|
+
* @returns {string} - Path to lock file
|
|
10
|
+
*/
|
|
11
|
+
export function getLockFilePath() {
|
|
12
|
+
return path.join(process.cwd(), '.magentrix', LOCK_FILE_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if autopublish is currently running.
|
|
17
|
+
* @returns {boolean} - True if autopublish is running (lock file exists and is not stale)
|
|
18
|
+
*/
|
|
19
|
+
export function isAutopublishRunning() {
|
|
20
|
+
const lockFile = getLockFilePath();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(lockFile)) return false;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
|
26
|
+
const lockAge = Date.now() - lockData.timestamp;
|
|
27
|
+
|
|
28
|
+
// If lock is older than 1 hour, consider it stale
|
|
29
|
+
return lockAge < LOCK_EXPIRY_MS;
|
|
30
|
+
} catch {
|
|
31
|
+
// If we can't read the lock file, assume it's not running
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a lock file to prevent multiple autopublish instances.
|
|
38
|
+
* @returns {boolean} - True if lock was acquired, false if already locked
|
|
39
|
+
*/
|
|
40
|
+
export function acquireAutopublishLock() {
|
|
41
|
+
const lockFile = getLockFilePath();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(lockFile)) {
|
|
45
|
+
// Check if the lock is stale (process might have crashed)
|
|
46
|
+
if (isAutopublishRunning()) {
|
|
47
|
+
return false; // Lock is active
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create lock file
|
|
52
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
53
|
+
fs.writeFileSync(lockFile, JSON.stringify({
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Release the autopublish lock file.
|
|
66
|
+
*/
|
|
67
|
+
export function releaseAutopublishLock() {
|
|
68
|
+
const lockFile = getLockFilePath();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(lockFile)) {
|
|
72
|
+
fs.unlinkSync(lockFile);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore errors during cleanup
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -243,13 +243,12 @@ export async function showCurrentConflicts(rootDir, instanceUrl, token, forceCon
|
|
|
243
243
|
export async function promptConflictResolution(fileIssues) {
|
|
244
244
|
if (!fileIssues.length) return 'skip';
|
|
245
245
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
246
|
+
// Add spacing instead of clearing (clearing causes flickering with progress tracker)
|
|
247
|
+
console.log('\n');
|
|
248
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
250
249
|
console.log(
|
|
251
250
|
chalk.bold.yellow(
|
|
252
|
-
|
|
251
|
+
`${fileIssues.length} file${fileIssues.length > 1 ? 's' : ''} require conflict resolution:\n`
|
|
253
252
|
)
|
|
254
253
|
);
|
|
255
254
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { password } from "@inquirer/prompts";
|
|
2
2
|
import Config from "../../config.js";
|
|
3
3
|
import { HASHED_CWD } from "../../../vars/global.js";
|
|
4
4
|
|
|
@@ -27,27 +27,33 @@ export const ensureApiKey = async (forcePrompt = false) => {
|
|
|
27
27
|
console.log('\n🚀 Magentrix CLI Setup:');
|
|
28
28
|
|
|
29
29
|
while (true) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
try {
|
|
31
|
+
const apiKeyInput = await password({
|
|
32
|
+
message: 'Enter your Magentrix API key (no spaces):',
|
|
33
|
+
mask: '*',
|
|
34
|
+
validate: (value) => {
|
|
35
|
+
if (!value || value.trim().length < 12) {
|
|
36
|
+
return "API key must be at least 12 characters.";
|
|
37
|
+
}
|
|
38
|
+
if (/\s/.test(value)) {
|
|
39
|
+
return "API key cannot contain any spaces.";
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const trimmed = apiKeyInput.trim();
|
|
46
|
+
|
|
47
|
+
if (isValid(trimmed)) {
|
|
48
|
+
return trimmed;
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// Handle Ctrl+C or other cancellation
|
|
52
|
+
if (error.name === 'ExitPromptError') {
|
|
53
|
+
console.error('\n❌ API key is required. Exiting.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
51
57
|
}
|
|
52
58
|
// Invalid: will re-prompt.
|
|
53
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { input } from "@inquirer/prompts";
|
|
2
2
|
import Config from "../../config.js";
|
|
3
3
|
import { checkInstanceUrl } from "../checkInstanceUrl.js";
|
|
4
4
|
import { withSpinner } from "../../spinner.js";
|
|
@@ -27,37 +27,45 @@ export const ensureInstanceUrl = async (forcePrompt = false) => {
|
|
|
27
27
|
console.log('\n🚀 Magentrix CLI Setup: Instance URL');
|
|
28
28
|
|
|
29
29
|
while (true) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
30
|
+
try {
|
|
31
|
+
// Prompt: Only validate format, NOT reachability
|
|
32
|
+
const inputUrl = await input({
|
|
33
|
+
message: 'Enter your Magentrix instance URL (e.g., https://example.magentrix.com):',
|
|
34
|
+
validate: (value) => {
|
|
35
|
+
// Automatically strip trailing slashes for validation
|
|
36
|
+
const cleaned = value.trim().replace(/\/+$/, '');
|
|
37
|
+
if (!urlRegex.test(cleaned)) {
|
|
38
|
+
return 'Instance URL must be in the form: https://subdomain.domain.com (no http, no extra path)';
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
// Trim and automatically strip trailing slashes
|
|
45
|
+
const trimmed = inputUrl.trim().replace(/\/+$/, '');
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
// Now check reachability WITH a spinner
|
|
48
|
+
try {
|
|
49
|
+
await withSpinner('Checking instance URL...', () =>
|
|
50
|
+
checkInstanceUrl(trimmed)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return trimmed;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Print error AFTER spinner, then re-prompt
|
|
56
|
+
console.error('❌ Instance URL not reachable. Try again.');
|
|
57
|
+
}
|
|
55
58
|
} catch (error) {
|
|
56
|
-
//
|
|
57
|
-
|
|
59
|
+
// Handle Ctrl+C or other cancellation
|
|
60
|
+
if (error.name === 'ExitPromptError') {
|
|
61
|
+
console.error('\n❌ Instance URL is required. Exiting.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
// Trim and automatically strip trailing slashes from existing URL
|
|
70
|
+
return instanceUrl.trim().replace(/\/+$/, '');
|
|
63
71
|
};
|
|
@@ -39,7 +39,18 @@ export const mapRecordToFile = (record) => {
|
|
|
39
39
|
? path.join(mapping.directory, foundPathByRecordId.split(mapping.directory)[1] || "")
|
|
40
40
|
: "";
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Fix for Bug 1: If the file exists but the name doesn't match the record name,
|
|
43
|
+
// we should treat it as a rename and use the new name.
|
|
44
|
+
let useExistingPath = false;
|
|
45
|
+
if (foundPathByRecordId) {
|
|
46
|
+
const existingName = path.basename(foundPathByRecordId);
|
|
47
|
+
// Check if existing name matches the expected filename (ignoring extension case if needed, but strict for now)
|
|
48
|
+
if (existingName === filename) {
|
|
49
|
+
useExistingPath = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filePath = useExistingPath ? relativeFoundPath : path.join(mapping.directory, filename);
|
|
43
54
|
|
|
44
55
|
return {
|
|
45
56
|
...record,
|
|
@@ -198,7 +209,7 @@ export const writeRecords = async (records, resolutionMethod, progress = null, l
|
|
|
198
209
|
try {
|
|
199
210
|
// Lookup a matching file dir in our cache, in case the file was renamed
|
|
200
211
|
const cachedFilePath = findFileByTag(record.Id) || path.resolve(filePath);
|
|
201
|
-
|
|
212
|
+
|
|
202
213
|
// Ensure the directory exists (paranoia for deeply nested files)
|
|
203
214
|
fs.mkdirSync(path.dirname(cachedFilePath), { recursive: true });
|
|
204
215
|
fs.writeFileSync(cachedFilePath, finalContent, "utf8");
|
package/utils/config.js
CHANGED
|
@@ -200,6 +200,82 @@ class Config {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Remove multiple keys from config (global or project) in a single save operation.
|
|
205
|
+
*
|
|
206
|
+
* @param {string[]} keys - Array of keys to remove.
|
|
207
|
+
* @param {Object} opts
|
|
208
|
+
* @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
|
|
209
|
+
* @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
|
|
210
|
+
* @param {string} [opts.filename] - Optional. Custom filename for project config.
|
|
211
|
+
*/
|
|
212
|
+
removeKeys(keys, opts = {}) {
|
|
213
|
+
const isGlobal = opts.global === true;
|
|
214
|
+
const filename = opts.filename;
|
|
215
|
+
const pathHash = opts.pathHash;
|
|
216
|
+
|
|
217
|
+
if (!Array.isArray(keys) || keys.length === 0) return;
|
|
218
|
+
|
|
219
|
+
if (isGlobal) {
|
|
220
|
+
if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
|
|
221
|
+
|
|
222
|
+
let changed = false;
|
|
223
|
+
if (pathHash) {
|
|
224
|
+
if (!this._globalConfig[pathHash]) return;
|
|
225
|
+
for (const key of keys) {
|
|
226
|
+
if (key in this._globalConfig[pathHash]) {
|
|
227
|
+
delete this._globalConfig[pathHash][key];
|
|
228
|
+
changed = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
for (const key of keys) {
|
|
233
|
+
if (key in this._globalConfig) {
|
|
234
|
+
delete this._globalConfig[key];
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (changed) {
|
|
241
|
+
this._saveConfig(this._globalConfig, 'global');
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
if (filename) {
|
|
245
|
+
const projectFolderPath = path.join(this.projectDir, this.projectFolder);
|
|
246
|
+
if (!fs.existsSync(projectFolderPath)) return;
|
|
247
|
+
const customPath = path.join(projectFolderPath, filename);
|
|
248
|
+
const customConfig = this._loadConfig('project', customPath) || {};
|
|
249
|
+
|
|
250
|
+
let changed = false;
|
|
251
|
+
for (const key of keys) {
|
|
252
|
+
if (key in customConfig) {
|
|
253
|
+
delete customConfig[key];
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (changed) {
|
|
259
|
+
this._saveConfig(customConfig, 'project', customPath);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
|
|
263
|
+
|
|
264
|
+
let changed = false;
|
|
265
|
+
for (const key of keys) {
|
|
266
|
+
if (key in this._projectConfig) {
|
|
267
|
+
delete this._projectConfig[key];
|
|
268
|
+
changed = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (changed) {
|
|
273
|
+
this._saveConfig(this._projectConfig, 'project');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
203
279
|
|
|
204
280
|
/**
|
|
205
281
|
* Save a key-value pair to config (global or project).
|