@kaitranntt/ccs 7.47.0 → 7.48.0-dev.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 +3 -3
- package/config/base-codex.settings.json +4 -4
- package/dist/ccs.js +68 -97
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/config/env-builder.d.ts.map +1 -1
- package/dist/cliproxy/config/env-builder.js +77 -15
- package/dist/cliproxy/config/env-builder.js.map +1 -1
- package/dist/cliproxy/config/extended-context-config.d.ts +1 -8
- package/dist/cliproxy/config/extended-context-config.d.ts.map +1 -1
- package/dist/cliproxy/config/extended-context-config.js +8 -24
- package/dist/cliproxy/config/extended-context-config.js.map +1 -1
- package/dist/cliproxy/config/generator.d.ts +3 -1
- package/dist/cliproxy/config/generator.d.ts.map +1 -1
- package/dist/cliproxy/config/generator.js +210 -39
- package/dist/cliproxy/config/generator.js.map +1 -1
- package/dist/cliproxy/config/thinking-config.d.ts.map +1 -1
- package/dist/cliproxy/config/thinking-config.js +15 -4
- package/dist/cliproxy/config/thinking-config.js.map +1 -1
- package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
- package/dist/cliproxy/executor/env-resolver.js +38 -2
- package/dist/cliproxy/executor/env-resolver.js.map +1 -1
- package/dist/cliproxy/executor/index.d.ts.map +1 -1
- package/dist/cliproxy/executor/index.js +7 -4
- package/dist/cliproxy/executor/index.js.map +1 -1
- package/dist/cliproxy/executor/retry-handler.d.ts +1 -1
- package/dist/cliproxy/executor/retry-handler.js +2 -2
- package/dist/cliproxy/executor/retry-handler.js.map +1 -1
- package/dist/cliproxy/index.d.ts +1 -0
- package/dist/cliproxy/index.d.ts.map +1 -1
- package/dist/cliproxy/index.js +12 -3
- package/dist/cliproxy/index.js.map +1 -1
- package/dist/cliproxy/model-catalog.d.ts.map +1 -1
- package/dist/cliproxy/model-catalog.js +6 -1
- package/dist/cliproxy/model-catalog.js.map +1 -1
- package/dist/cliproxy/model-config.d.ts.map +1 -1
- package/dist/cliproxy/model-config.js +6 -6
- package/dist/cliproxy/model-config.js.map +1 -1
- package/dist/cliproxy/model-id-normalizer.d.ts +70 -0
- package/dist/cliproxy/model-id-normalizer.d.ts.map +1 -0
- package/dist/cliproxy/model-id-normalizer.js +120 -0
- package/dist/cliproxy/model-id-normalizer.js.map +1 -0
- package/dist/cliproxy/provider-capabilities.d.ts +6 -0
- package/dist/cliproxy/provider-capabilities.d.ts.map +1 -1
- package/dist/cliproxy/provider-capabilities.js +24 -2
- package/dist/cliproxy/provider-capabilities.js.map +1 -1
- package/dist/cliproxy/quota-fetcher-claude-normalizer.d.ts +18 -0
- package/dist/cliproxy/quota-fetcher-claude-normalizer.d.ts.map +1 -0
- package/dist/cliproxy/quota-fetcher-claude-normalizer.js +291 -0
- package/dist/cliproxy/quota-fetcher-claude-normalizer.js.map +1 -0
- package/dist/cliproxy/quota-fetcher-claude.d.ts +21 -0
- package/dist/cliproxy/quota-fetcher-claude.d.ts.map +1 -0
- package/dist/cliproxy/quota-fetcher-claude.js +263 -0
- package/dist/cliproxy/quota-fetcher-claude.js.map +1 -0
- package/dist/cliproxy/quota-manager.d.ts +7 -4
- package/dist/cliproxy/quota-manager.d.ts.map +1 -1
- package/dist/cliproxy/quota-manager.js +80 -21
- package/dist/cliproxy/quota-manager.js.map +1 -1
- package/dist/cliproxy/quota-types.d.ts +74 -2
- package/dist/cliproxy/quota-types.d.ts.map +1 -1
- package/dist/cliproxy/quota-types.js +1 -1
- package/dist/cliproxy/remote-proxy-client.d.ts +3 -10
- package/dist/cliproxy/remote-proxy-client.d.ts.map +1 -1
- package/dist/cliproxy/remote-proxy-client.js +32 -29
- package/dist/cliproxy/remote-proxy-client.js.map +1 -1
- package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.js +12 -13
- package/dist/cliproxy/services/variant-settings.js.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.js +15 -5
- package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
- package/dist/cliproxy/types.d.ts +1 -1
- package/dist/cliproxy/types.d.ts.map +1 -1
- package/dist/commands/api-command.d.ts +13 -0
- package/dist/commands/api-command.d.ts.map +1 -1
- package/dist/commands/api-command.js +85 -24
- package/dist/commands/api-command.js.map +1 -1
- package/dist/commands/arg-extractor.d.ts +29 -0
- package/dist/commands/arg-extractor.d.ts.map +1 -0
- package/dist/commands/arg-extractor.js +81 -0
- package/dist/commands/arg-extractor.js.map +1 -0
- package/dist/commands/cliproxy/help-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/help-subcommand.js +3 -2
- package/dist/commands/cliproxy/help-subcommand.js.map +1 -1
- package/dist/commands/cliproxy/index.d.ts +13 -0
- package/dist/commands/cliproxy/index.d.ts.map +1 -1
- package/dist/commands/cliproxy/index.js +66 -126
- package/dist/commands/cliproxy/index.js.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.d.ts +2 -1
- package/dist/commands/cliproxy/quota-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.js +172 -46
- package/dist/commands/cliproxy/quota-subcommand.js.map +1 -1
- package/dist/commands/config-command.d.ts.map +1 -1
- package/dist/commands/config-command.js +17 -16
- package/dist/commands/config-command.js.map +1 -1
- package/dist/commands/config-image-analysis-command.d.ts.map +1 -1
- package/dist/commands/config-image-analysis-command.js +37 -27
- package/dist/commands/config-image-analysis-command.js.map +1 -1
- package/dist/commands/persist-command.d.ts.map +1 -1
- package/dist/commands/persist-command.js +455 -139
- package/dist/commands/persist-command.js.map +1 -1
- package/dist/management/checks/config-check.d.ts.map +1 -1
- package/dist/management/checks/config-check.js +2 -2
- package/dist/management/checks/config-check.js.map +1 -1
- package/dist/shared/extended-context-utils.d.ts +14 -0
- package/dist/shared/extended-context-utils.d.ts.map +1 -0
- package/dist/shared/extended-context-utils.js +35 -0
- package/dist/shared/extended-context-utils.js.map +1 -0
- package/dist/ui/assets/{accounts-BiL_RD2P.js → accounts-7x9sEJ6E.js} +1 -1
- package/dist/ui/assets/{alert-dialog-BoSPpkH2.js → alert-dialog-CSG7wAU8.js} +1 -1
- package/dist/ui/assets/{api-BrHcZxrF.js → api-BF6fKWuJ.js} +1 -1
- package/dist/ui/assets/{auth-section-CNEQOji9.js → auth-section-BrRo7P5e.js} +1 -1
- package/dist/ui/assets/{backups-section-CYPJq2Y6.js → backups-section-BvoQ6sjn.js} +1 -1
- package/dist/ui/assets/cliproxy-Dp-l74Ht.js +3 -0
- package/dist/ui/assets/cliproxy-control-panel-D3_E45dZ.js +1 -0
- package/dist/ui/assets/{confirm-dialog-JGLPn0Pg.js → confirm-dialog-QJFlAs8u.js} +1 -1
- package/dist/ui/assets/{copilot-Bt-S8kHA.js → copilot-CRxuFhMs.js} +2 -2
- package/dist/ui/assets/{cursor-Qg3y30M0.js → cursor-Dsbkv6Oq.js} +1 -1
- package/dist/ui/assets/{globalenv-section-Dxrj87dw.js → globalenv-section-Cf_HiHep.js} +1 -1
- package/dist/ui/assets/{health-Bjnrp81E.js → health-CgHX0qCf.js} +1 -1
- package/dist/ui/assets/index-BNJ3rHVd.js +47 -0
- package/dist/ui/assets/{index-ApptKWow.js → index-BUC-Zfgc.js} +1 -1
- package/dist/ui/assets/{index-XJ9726WB.js → index-C-4XwF_5.js} +1 -1
- package/dist/ui/assets/{index-DOQkTkq-.js → index-D9YYXEsW.js} +1 -1
- package/dist/ui/assets/{index-UVFLMRYY.js → index-yfs5e5sm.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-PLX0fi09.js → proxy-status-widget-CNesfhAI.js} +1 -1
- package/dist/ui/assets/{separator-yZDNbi3M.js → separator-9gFbzNO-.js} +1 -1
- package/dist/ui/assets/{shared-DZ3QOOgF.js → shared-DfHvplPN.js} +1 -1
- package/dist/ui/assets/{switch-DsTWD8-1.js → switch-BMNi4Qdv.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/utils/claude-config-path.d.ts +11 -0
- package/dist/utils/claude-config-path.d.ts.map +1 -0
- package/dist/utils/claude-config-path.js +51 -0
- package/dist/utils/claude-config-path.js.map +1 -0
- package/dist/utils/websearch/hook-config.d.ts.map +1 -1
- package/dist/utils/websearch/hook-config.js +9 -23
- package/dist/utils/websearch/hook-config.js.map +1 -1
- package/dist/web-server/jsonl-parser.d.ts +0 -4
- package/dist/web-server/jsonl-parser.d.ts.map +1 -1
- package/dist/web-server/jsonl-parser.js +52 -29
- package/dist/web-server/jsonl-parser.js.map +1 -1
- package/dist/web-server/routes/cliproxy-stats-routes.d.ts.map +1 -1
- package/dist/web-server/routes/cliproxy-stats-routes.js +105 -3
- package/dist/web-server/routes/cliproxy-stats-routes.js.map +1 -1
- package/dist/web-server/routes/persist-routes.d.ts.map +1 -1
- package/dist/web-server/routes/persist-routes.js +54 -44
- package/dist/web-server/routes/persist-routes.js.map +1 -1
- package/dist/web-server/routes/route-helpers.d.ts +0 -5
- package/dist/web-server/routes/route-helpers.d.ts.map +1 -1
- package/dist/web-server/routes/route-helpers.js +52 -6
- package/dist/web-server/routes/route-helpers.js.map +1 -1
- package/dist/web-server/routes/settings-routes.d.ts.map +1 -1
- package/dist/web-server/routes/settings-routes.js +72 -10
- package/dist/web-server/routes/settings-routes.js.map +1 -1
- package/dist/web-server/shared-routes.js +2 -2
- package/dist/web-server/shared-routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/cliproxy-CDDlPp51.js +0 -3
- package/dist/ui/assets/cliproxy-control-panel-XSSdoJ3f.js +0 -1
- package/dist/ui/assets/index-Ce5AiHY_.js +0 -47
|
@@ -36,110 +36,287 @@ exports.handlePersistCommand = void 0;
|
|
|
36
36
|
const fs = __importStar(require("fs"));
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const os = __importStar(require("os"));
|
|
39
|
+
const lockfile = __importStar(require("proper-lockfile"));
|
|
39
40
|
const ui_1 = require("../utils/ui");
|
|
40
41
|
const prompt_1 = require("../utils/prompt");
|
|
41
42
|
const profile_detector_1 = __importStar(require("../auth/profile-detector"));
|
|
42
43
|
const config_generator_1 = require("../cliproxy/config-generator");
|
|
43
44
|
const copilot_executor_1 = require("../copilot/copilot-executor");
|
|
44
45
|
const helpers_1 = require("../utils/helpers");
|
|
46
|
+
const claude_config_path_1 = require("../utils/claude-config-path");
|
|
47
|
+
const arg_extractor_1 = require("./arg-extractor");
|
|
48
|
+
const PERSIST_KNOWN_FLAGS = [
|
|
49
|
+
'--yes',
|
|
50
|
+
'-y',
|
|
51
|
+
'--list-backups',
|
|
52
|
+
'--restore',
|
|
53
|
+
'--permission-mode',
|
|
54
|
+
'--dangerously-skip-permissions',
|
|
55
|
+
'--auto-approve',
|
|
56
|
+
'--help',
|
|
57
|
+
'-h',
|
|
58
|
+
];
|
|
59
|
+
const VALID_PERMISSION_MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
|
60
|
+
const PERSIST_LOCK_STALE_MS = 10000;
|
|
61
|
+
const PERSIST_LOCK_RETRIES = 5;
|
|
62
|
+
const PERSIST_LOCK_RETRY_MIN_MS = 100;
|
|
63
|
+
const PERSIST_LOCK_RETRY_MAX_MS = 500;
|
|
64
|
+
function isPermissionMode(value) {
|
|
65
|
+
return VALID_PERMISSION_MODES.includes(value);
|
|
66
|
+
}
|
|
67
|
+
function isKnownPersistFlagToken(token) {
|
|
68
|
+
return PERSIST_KNOWN_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`));
|
|
69
|
+
}
|
|
70
|
+
function resolvePermissionMode(parsedArgs) {
|
|
71
|
+
if (!parsedArgs.dangerouslySkipPermissions) {
|
|
72
|
+
return parsedArgs.permissionMode;
|
|
73
|
+
}
|
|
74
|
+
if (parsedArgs.permissionMode && parsedArgs.permissionMode !== 'bypassPermissions') {
|
|
75
|
+
throw new Error('--dangerously-skip-permissions conflicts with --permission-mode. Use bypassPermissions or remove one flag.');
|
|
76
|
+
}
|
|
77
|
+
return 'bypassPermissions';
|
|
78
|
+
}
|
|
45
79
|
/** Parse command line arguments */
|
|
46
80
|
function parseArgs(args) {
|
|
47
|
-
const result = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
const result = {
|
|
82
|
+
yes: (0, arg_extractor_1.hasAnyFlag)(args, ['--yes', '-y']),
|
|
83
|
+
listBackups: (0, arg_extractor_1.hasAnyFlag)(args, ['--list-backups']),
|
|
84
|
+
};
|
|
85
|
+
const restoreOption = (0, arg_extractor_1.extractOption)(args, ['--restore']);
|
|
86
|
+
if (restoreOption.found) {
|
|
87
|
+
result.restore = restoreOption.missingValue ? true : restoreOption.value || true;
|
|
88
|
+
}
|
|
89
|
+
const permissionModeOption = (0, arg_extractor_1.extractOption)(restoreOption.remainingArgs, ['--permission-mode'], {
|
|
90
|
+
knownFlags: PERSIST_KNOWN_FLAGS,
|
|
91
|
+
});
|
|
92
|
+
if (permissionModeOption.found) {
|
|
93
|
+
if (permissionModeOption.missingValue) {
|
|
94
|
+
result.parseError = 'Missing value for --permission-mode';
|
|
58
95
|
}
|
|
59
|
-
else if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
63
|
-
result.restore = nextArg;
|
|
64
|
-
i++; // Skip next arg
|
|
96
|
+
else if (permissionModeOption.value) {
|
|
97
|
+
if (!isPermissionMode(permissionModeOption.value)) {
|
|
98
|
+
result.parseError = `Invalid --permission-mode "${permissionModeOption.value}". Valid modes: ${VALID_PERMISSION_MODES.join(', ')}`;
|
|
65
99
|
}
|
|
66
100
|
else {
|
|
67
|
-
result.
|
|
101
|
+
result.permissionMode = permissionModeOption.value;
|
|
68
102
|
}
|
|
69
103
|
}
|
|
70
|
-
|
|
104
|
+
}
|
|
105
|
+
result.dangerouslySkipPermissions = (0, arg_extractor_1.hasAnyFlag)(permissionModeOption.remainingArgs, [
|
|
106
|
+
'--dangerously-skip-permissions',
|
|
107
|
+
'--auto-approve',
|
|
108
|
+
]);
|
|
109
|
+
const unknownFlags = permissionModeOption.remainingArgs.filter((arg) => arg.startsWith('-') && !isKnownPersistFlagToken(arg));
|
|
110
|
+
if (!result.parseError && unknownFlags.length > 0) {
|
|
111
|
+
const unknownList = unknownFlags.map((flag) => `"${flag}"`).join(', ');
|
|
112
|
+
result.parseError = `Unknown option(s): ${unknownList}. Run 'ccs persist --help' for usage.`;
|
|
113
|
+
}
|
|
114
|
+
if (!result.parseError && result.listBackups && result.restore) {
|
|
115
|
+
result.parseError = '--list-backups cannot be used with --restore';
|
|
116
|
+
}
|
|
117
|
+
if (!result.parseError &&
|
|
118
|
+
(result.listBackups || result.restore) &&
|
|
119
|
+
(result.permissionMode || result.dangerouslySkipPermissions)) {
|
|
120
|
+
result.parseError =
|
|
121
|
+
'Permission flags are not valid with backup operations. Use them only with ccs persist <profile>.';
|
|
122
|
+
}
|
|
123
|
+
for (const arg of permissionModeOption.remainingArgs) {
|
|
124
|
+
if (!arg.startsWith('-')) {
|
|
71
125
|
result.profile = arg;
|
|
126
|
+
break;
|
|
72
127
|
}
|
|
73
128
|
}
|
|
74
129
|
return result;
|
|
75
130
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
131
|
+
function formatDisplayPath(filePath) {
|
|
132
|
+
const defaultClaudeDir = path.join(os.homedir(), '.claude');
|
|
133
|
+
const claudeDir = (0, claude_config_path_1.getClaudeConfigDir)();
|
|
134
|
+
// Keep real path when user overrides Claude directory.
|
|
135
|
+
if (path.resolve(claudeDir) !== path.resolve(defaultClaudeDir)) {
|
|
136
|
+
return filePath;
|
|
137
|
+
}
|
|
138
|
+
if (filePath === claudeDir) {
|
|
139
|
+
return '~/.claude';
|
|
140
|
+
}
|
|
141
|
+
const claudePrefix = `${claudeDir}${path.sep}`;
|
|
142
|
+
if (filePath.startsWith(claudePrefix)) {
|
|
143
|
+
return filePath.replace(claudePrefix, '~/.claude/');
|
|
144
|
+
}
|
|
145
|
+
return filePath;
|
|
146
|
+
}
|
|
147
|
+
function getClaudeSettingsDisplayPath() {
|
|
148
|
+
return formatDisplayPath((0, claude_config_path_1.getClaudeSettingsPath)());
|
|
79
149
|
}
|
|
80
|
-
|
|
81
|
-
function readClaudeSettings() {
|
|
82
|
-
const settingsPath = getClaudeSettingsPath();
|
|
150
|
+
async function pathExists(filePath) {
|
|
83
151
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
152
|
+
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function isSymlinkAsync(filePath) {
|
|
160
|
+
try {
|
|
161
|
+
const stats = await fs.promises.lstat(filePath);
|
|
162
|
+
return stats.isSymbolicLink();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function getNoFollowFlag() {
|
|
169
|
+
const candidate = fs.constants['O_NOFOLLOW'];
|
|
170
|
+
if (process.platform !== 'win32' && typeof candidate === 'number') {
|
|
171
|
+
return candidate;
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
function createSymlinkReadError(filePath) {
|
|
176
|
+
const error = new Error(`Refusing to read symlinked file for security: ${formatDisplayPath(filePath)}`);
|
|
177
|
+
error.code = 'ELOOP';
|
|
178
|
+
return error;
|
|
179
|
+
}
|
|
180
|
+
async function readFileUtf8NoFollow(filePath) {
|
|
181
|
+
if (await isSymlinkAsync(filePath)) {
|
|
182
|
+
throw createSymlinkReadError(filePath);
|
|
183
|
+
}
|
|
184
|
+
const noFollowFlag = getNoFollowFlag();
|
|
185
|
+
const flags = fs.constants.O_RDONLY | noFollowFlag;
|
|
186
|
+
const handle = await fs.promises.open(filePath, flags);
|
|
187
|
+
try {
|
|
188
|
+
// Best-effort fallback for platforms without O_NOFOLLOW (notably Windows).
|
|
189
|
+
// Re-check symlink status after open to reduce check-then-use windows.
|
|
190
|
+
if (noFollowFlag === 0 && (await isSymlinkAsync(filePath))) {
|
|
191
|
+
throw createSymlinkReadError(filePath);
|
|
88
192
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
throw new Error('settings.json must contain a JSON object, not an array or primitive');
|
|
193
|
+
const stats = await handle.stat();
|
|
194
|
+
if (!stats.isFile()) {
|
|
195
|
+
throw new Error('Path is not a regular file');
|
|
93
196
|
}
|
|
94
|
-
|
|
197
|
+
if (noFollowFlag === 0) {
|
|
198
|
+
const latestStats = await fs.promises.stat(filePath);
|
|
199
|
+
if (latestStats.dev !== stats.dev || latestStats.ino !== stats.ino) {
|
|
200
|
+
throw new Error('Path changed during secure read');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return await handle.readFile({ encoding: 'utf8' });
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
await handle.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function parseSettingsObject(content, sourceLabel) {
|
|
210
|
+
if (!content.trim()) {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
const parsed = JSON.parse(content);
|
|
214
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
215
|
+
throw new Error(`${sourceLabel} must contain a JSON object, not an array or primitive`);
|
|
216
|
+
}
|
|
217
|
+
return parsed;
|
|
218
|
+
}
|
|
219
|
+
async function withPersistSettingsLock(operation) {
|
|
220
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
221
|
+
const settingsDir = path.dirname(settingsPath);
|
|
222
|
+
await fs.promises.mkdir(settingsDir, { recursive: true });
|
|
223
|
+
let release;
|
|
224
|
+
try {
|
|
225
|
+
release = await lockfile.lock(settingsDir, {
|
|
226
|
+
stale: PERSIST_LOCK_STALE_MS,
|
|
227
|
+
retries: {
|
|
228
|
+
retries: PERSIST_LOCK_RETRIES,
|
|
229
|
+
minTimeout: PERSIST_LOCK_RETRY_MIN_MS,
|
|
230
|
+
maxTimeout: PERSIST_LOCK_RETRY_MAX_MS,
|
|
231
|
+
},
|
|
232
|
+
realpath: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw new Error(`Failed to lock Claude settings directory (${formatDisplayPath(settingsDir)}): ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
return await operation();
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
if (release) {
|
|
243
|
+
try {
|
|
244
|
+
await release();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Best-effort release.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Read existing Claude settings.json with validation */
|
|
253
|
+
async function readClaudeSettings() {
|
|
254
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
255
|
+
try {
|
|
256
|
+
const content = await readFileUtf8NoFollow(settingsPath);
|
|
257
|
+
return parseSettingsObject(content, 'settings.json');
|
|
95
258
|
}
|
|
96
259
|
catch (error) {
|
|
97
260
|
const nodeError = error;
|
|
98
261
|
if (nodeError.code === 'ENOENT') {
|
|
99
262
|
return {};
|
|
100
263
|
}
|
|
264
|
+
if (nodeError.code === 'ELOOP') {
|
|
265
|
+
throw new Error('settings.json is a symlink - refusing to read for security');
|
|
266
|
+
}
|
|
101
267
|
throw new Error(`Failed to parse settings.json: ${error.message}`);
|
|
102
268
|
}
|
|
103
269
|
}
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
function writeClaudeSettings(settings) {
|
|
110
|
-
const settingsPath = getClaudeSettingsPath();
|
|
111
|
-
// Security: Reject symlinks to prevent writing to unexpected locations
|
|
112
|
-
if (isSymlink(settingsPath)) {
|
|
270
|
+
/** Write settings back to settings.json with atomic replace semantics. */
|
|
271
|
+
async function writeClaudeSettings(settings) {
|
|
272
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
273
|
+
if (await isSymlinkAsync(settingsPath)) {
|
|
113
274
|
throw new Error('settings.json is a symlink - refusing to write for security');
|
|
114
275
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
276
|
+
const settingsDir = path.dirname(settingsPath);
|
|
277
|
+
await fs.promises.mkdir(settingsDir, { recursive: true });
|
|
278
|
+
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
279
|
+
const tmpPath = path.join(settingsDir, `settings.json.tmp-${nonce}`);
|
|
280
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | getNoFollowFlag();
|
|
281
|
+
let handle;
|
|
282
|
+
try {
|
|
283
|
+
handle = await fs.promises.open(tmpPath, flags, 0o600);
|
|
284
|
+
await handle.writeFile(JSON.stringify(settings, null, 2) + '\n', { encoding: 'utf8' });
|
|
285
|
+
await handle.sync();
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
if (handle) {
|
|
289
|
+
await handle.close();
|
|
290
|
+
}
|
|
118
291
|
}
|
|
119
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
|
|
120
|
-
}
|
|
121
|
-
/** Maximum number of backups to keep (oldest are deleted) */
|
|
122
|
-
const MAX_BACKUPS = 10;
|
|
123
|
-
/** Check if path is a symlink (security check) */
|
|
124
|
-
function isSymlink(filePath) {
|
|
125
292
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
293
|
+
await fs.promises.rename(tmpPath, settingsPath);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
try {
|
|
297
|
+
await fs.promises.unlink(tmpPath);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Best-effort cleanup.
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await fs.promises.chmod(settingsPath, 0o600);
|
|
128
306
|
}
|
|
129
307
|
catch {
|
|
130
|
-
|
|
308
|
+
// Best-effort permission hardening.
|
|
131
309
|
}
|
|
132
310
|
}
|
|
311
|
+
/** Maximum number of backups to keep (oldest are deleted) */
|
|
312
|
+
const MAX_BACKUPS = 10;
|
|
133
313
|
/** Create backup of settings.json with proper permissions and rotation */
|
|
134
|
-
function createBackup() {
|
|
135
|
-
const settingsPath = getClaudeSettingsPath();
|
|
136
|
-
if (!
|
|
314
|
+
async function createBackup() {
|
|
315
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
316
|
+
if (!(await pathExists(settingsPath))) {
|
|
137
317
|
throw new Error('No settings.json to backup');
|
|
138
318
|
}
|
|
139
|
-
|
|
140
|
-
if (isSymlink(settingsPath)) {
|
|
141
|
-
throw new Error('settings.json is a symlink - refusing to backup for security');
|
|
142
|
-
}
|
|
319
|
+
const settingsContent = await readFileUtf8NoFollow(settingsPath);
|
|
143
320
|
const now = new Date();
|
|
144
321
|
const timestamp = now.getFullYear().toString() +
|
|
145
322
|
(now.getMonth() + 1).toString().padStart(2, '0') +
|
|
@@ -149,9 +326,24 @@ function createBackup() {
|
|
|
149
326
|
now.getMinutes().toString().padStart(2, '0') +
|
|
150
327
|
now.getSeconds().toString().padStart(2, '0');
|
|
151
328
|
const backupPath = `${settingsPath}.backup.${timestamp}`;
|
|
152
|
-
fs.
|
|
153
|
-
|
|
154
|
-
|
|
329
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | getNoFollowFlag();
|
|
330
|
+
let handle;
|
|
331
|
+
try {
|
|
332
|
+
handle = await fs.promises.open(backupPath, flags, 0o600);
|
|
333
|
+
await handle.writeFile(settingsContent, { encoding: 'utf8' });
|
|
334
|
+
await handle.sync();
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (handle) {
|
|
338
|
+
await handle.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
await fs.promises.chmod(backupPath, 0o600);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Best-effort permission hardening.
|
|
346
|
+
}
|
|
155
347
|
// Cleanup: Rotate old backups (keep only MAX_BACKUPS)
|
|
156
348
|
cleanupOldBackups();
|
|
157
349
|
return backupPath;
|
|
@@ -165,15 +357,37 @@ function cleanupOldBackups() {
|
|
|
165
357
|
try {
|
|
166
358
|
fs.unlinkSync(backup.path);
|
|
167
359
|
}
|
|
168
|
-
catch {
|
|
169
|
-
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.log((0, ui_1.warn)(`Failed to delete old backup ${formatDisplayPath(backup.path)}: ${error.message}`));
|
|
170
362
|
}
|
|
171
363
|
}
|
|
172
364
|
}
|
|
173
365
|
}
|
|
366
|
+
function parseBackupTimestamp(timestamp) {
|
|
367
|
+
const year = parseInt(timestamp.slice(0, 4), 10);
|
|
368
|
+
const month = parseInt(timestamp.slice(4, 6), 10);
|
|
369
|
+
const day = parseInt(timestamp.slice(6, 8), 10);
|
|
370
|
+
const hour = parseInt(timestamp.slice(9, 11), 10);
|
|
371
|
+
const minute = parseInt(timestamp.slice(11, 13), 10);
|
|
372
|
+
const second = parseInt(timestamp.slice(13, 15), 10);
|
|
373
|
+
const date = new Date(year, month - 1, day, hour, minute, second);
|
|
374
|
+
if (date.getFullYear() !== year)
|
|
375
|
+
return null;
|
|
376
|
+
if (date.getMonth() !== month - 1)
|
|
377
|
+
return null;
|
|
378
|
+
if (date.getDate() !== day)
|
|
379
|
+
return null;
|
|
380
|
+
if (date.getHours() !== hour)
|
|
381
|
+
return null;
|
|
382
|
+
if (date.getMinutes() !== minute)
|
|
383
|
+
return null;
|
|
384
|
+
if (date.getSeconds() !== second)
|
|
385
|
+
return null;
|
|
386
|
+
return date;
|
|
387
|
+
}
|
|
174
388
|
/** Get all backup files sorted by date (newest first) */
|
|
175
389
|
function getBackupFiles() {
|
|
176
|
-
const settingsPath = getClaudeSettingsPath();
|
|
390
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
177
391
|
const dir = path.dirname(settingsPath);
|
|
178
392
|
if (!fs.existsSync(dir)) {
|
|
179
393
|
return [];
|
|
@@ -187,17 +401,13 @@ function getBackupFiles() {
|
|
|
187
401
|
if (!match)
|
|
188
402
|
return null;
|
|
189
403
|
const timestamp = match[1];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const day = parseInt(timestamp.slice(6, 8));
|
|
194
|
-
const hour = parseInt(timestamp.slice(9, 11));
|
|
195
|
-
const min = parseInt(timestamp.slice(11, 13));
|
|
196
|
-
const sec = parseInt(timestamp.slice(13, 15));
|
|
404
|
+
const date = parseBackupTimestamp(timestamp);
|
|
405
|
+
if (!date)
|
|
406
|
+
return null;
|
|
197
407
|
return {
|
|
198
408
|
path: path.join(dir, f),
|
|
199
409
|
timestamp,
|
|
200
|
-
date
|
|
410
|
+
date,
|
|
201
411
|
};
|
|
202
412
|
})
|
|
203
413
|
.filter((f) => f !== null)
|
|
@@ -211,6 +421,40 @@ function maskApiKey(key) {
|
|
|
211
421
|
}
|
|
212
422
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
213
423
|
}
|
|
424
|
+
const SENSITIVE_ENV_PARTS = new Set([
|
|
425
|
+
'TOKEN',
|
|
426
|
+
'KEY',
|
|
427
|
+
'SECRET',
|
|
428
|
+
'PASSWORD',
|
|
429
|
+
'PASS',
|
|
430
|
+
'AUTH',
|
|
431
|
+
'CREDENTIAL',
|
|
432
|
+
'PRIVATE',
|
|
433
|
+
'ACCESS',
|
|
434
|
+
'REFRESH',
|
|
435
|
+
'APIKEY',
|
|
436
|
+
]);
|
|
437
|
+
function splitSensitiveKeyParts(key) {
|
|
438
|
+
const withCamelCaseBoundaries = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
|
|
439
|
+
return withCamelCaseBoundaries
|
|
440
|
+
.toUpperCase()
|
|
441
|
+
.split(/[^A-Z0-9]+/)
|
|
442
|
+
.filter(Boolean);
|
|
443
|
+
}
|
|
444
|
+
function isSensitiveEnvKey(key) {
|
|
445
|
+
const parts = splitSensitiveKeyParts(key);
|
|
446
|
+
if (parts.some((part) => SENSITIVE_ENV_PARTS.has(part))) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
const compact = parts.join('');
|
|
450
|
+
return (compact.includes('TOKEN') ||
|
|
451
|
+
compact.includes('APIKEY') ||
|
|
452
|
+
compact.includes('ACCESSKEY') ||
|
|
453
|
+
compact.includes('AUTHKEY') ||
|
|
454
|
+
compact.includes('SECRET') ||
|
|
455
|
+
compact.includes('PASSWORD') ||
|
|
456
|
+
compact.includes('CREDENTIAL'));
|
|
457
|
+
}
|
|
214
458
|
/** Resolve env vars for a profile */
|
|
215
459
|
async function resolveProfileEnvVars(profileName, profileResult) {
|
|
216
460
|
switch (profileResult.type) {
|
|
@@ -312,7 +556,7 @@ async function handleRestore(timestamp, yes) {
|
|
|
312
556
|
console.log(`Backup: ${(0, ui_1.color)(backup.timestamp, 'command')}`);
|
|
313
557
|
console.log(`Date: ${backup.date.toLocaleString()}`);
|
|
314
558
|
console.log('');
|
|
315
|
-
console.log((0, ui_1.warn)(
|
|
559
|
+
console.log((0, ui_1.warn)(`This will replace ${getClaudeSettingsDisplayPath()}`));
|
|
316
560
|
console.log('');
|
|
317
561
|
if (!yes) {
|
|
318
562
|
const proceed = await prompt_1.InteractivePrompt.confirm('Proceed with restore?', { default: false });
|
|
@@ -321,32 +565,57 @@ async function handleRestore(timestamp, yes) {
|
|
|
321
565
|
process.exit(0);
|
|
322
566
|
}
|
|
323
567
|
}
|
|
324
|
-
|
|
568
|
+
let parsedBackupSettings;
|
|
325
569
|
try {
|
|
326
|
-
const backupContent =
|
|
327
|
-
|
|
328
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
329
|
-
console.log((0, ui_1.fail)('Backup file is corrupted: not a valid JSON object'));
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
570
|
+
const backupContent = await readFileUtf8NoFollow(backup.path);
|
|
571
|
+
parsedBackupSettings = parseSettingsObject(backupContent, 'Backup file');
|
|
332
572
|
}
|
|
333
573
|
catch (error) {
|
|
574
|
+
const nodeError = error;
|
|
575
|
+
if (nodeError.code === 'ENOENT') {
|
|
576
|
+
console.log((0, ui_1.fail)('Backup was deleted during restore'));
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
if (nodeError.code === 'ELOOP') {
|
|
580
|
+
console.log((0, ui_1.fail)('Backup file is a symlink - refusing to restore for security'));
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
334
583
|
console.log((0, ui_1.fail)(`Backup file is corrupted: ${error.message}`));
|
|
335
584
|
process.exit(1);
|
|
336
585
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
586
|
+
try {
|
|
587
|
+
await withPersistSettingsLock(async () => {
|
|
588
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
589
|
+
if (await isSymlinkAsync(settingsPath)) {
|
|
590
|
+
throw new Error('settings.json is a symlink - refusing to restore for security');
|
|
591
|
+
}
|
|
592
|
+
let rollbackBackupPath = null;
|
|
593
|
+
if (await pathExists(settingsPath)) {
|
|
594
|
+
rollbackBackupPath = await createBackup();
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
await writeClaudeSettings(parsedBackupSettings);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
const writeError = error;
|
|
601
|
+
if (rollbackBackupPath) {
|
|
602
|
+
try {
|
|
603
|
+
const rollbackContent = await readFileUtf8NoFollow(rollbackBackupPath);
|
|
604
|
+
const rollbackSettings = parseSettingsObject(rollbackContent, 'Rollback backup');
|
|
605
|
+
await writeClaudeSettings(rollbackSettings);
|
|
606
|
+
}
|
|
607
|
+
catch (rollbackError) {
|
|
608
|
+
throw new Error(`Restore failed: ${writeError.message}. Rollback also failed: ${rollbackError.message}. Manual recovery backup: ${formatDisplayPath(rollbackBackupPath)}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
throw new Error(`Restore failed: ${writeError.message}`);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
341
614
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// Security: Reject symlink target
|
|
345
|
-
if (isSymlink(settingsPath)) {
|
|
346
|
-
console.log((0, ui_1.fail)('settings.json is a symlink - refusing to restore for security'));
|
|
615
|
+
catch (error) {
|
|
616
|
+
console.log((0, ui_1.fail)(error.message));
|
|
347
617
|
process.exit(1);
|
|
348
618
|
}
|
|
349
|
-
fs.copyFileSync(backup.path, settingsPath);
|
|
350
619
|
console.log((0, ui_1.ok)(`Restored from backup: ${backup.timestamp}`));
|
|
351
620
|
}
|
|
352
621
|
/** Show help for persist command */
|
|
@@ -361,13 +630,16 @@ async function showHelp() {
|
|
|
361
630
|
console.log('');
|
|
362
631
|
console.log((0, ui_1.subheader)('Description'));
|
|
363
632
|
console.log(" Writes a profile's environment variables directly to");
|
|
364
|
-
console.log(
|
|
633
|
+
console.log(` ${getClaudeSettingsDisplayPath()} for native Claude Code usage.`);
|
|
365
634
|
console.log('');
|
|
366
635
|
console.log(' This allows Claude Code to use the profile without CCS,');
|
|
367
636
|
console.log(' enabling compatibility with IDEs and extensions.');
|
|
368
637
|
console.log('');
|
|
369
638
|
console.log((0, ui_1.subheader)('Options'));
|
|
370
639
|
console.log(` ${(0, ui_1.color)('--yes, -y', 'command')} Skip confirmation prompts (auto-backup)`);
|
|
640
|
+
console.log(` ${(0, ui_1.color)('--permission-mode <mode>', 'command')} Set default permission mode in settings.json`);
|
|
641
|
+
console.log(` ${(0, ui_1.color)('--dangerously-skip-permissions', 'command')} Persist auto-approve (bypassPermissions)`);
|
|
642
|
+
console.log(` ${(0, ui_1.color)('--auto-approve', 'command')} Alias for --dangerously-skip-permissions`);
|
|
371
643
|
console.log(` ${(0, ui_1.color)('--help, -h', 'command')} Show this help message`);
|
|
372
644
|
console.log('');
|
|
373
645
|
console.log((0, ui_1.subheader)('Backup Management'));
|
|
@@ -388,6 +660,12 @@ async function showHelp() {
|
|
|
388
660
|
console.log(` ${(0, ui_1.dim)('# Persist with auto-confirmation')}`);
|
|
389
661
|
console.log(` ${(0, ui_1.color)('ccs persist gemini --yes', 'command')}`);
|
|
390
662
|
console.log('');
|
|
663
|
+
console.log(` ${(0, ui_1.dim)('# Persist with default permission mode')}`);
|
|
664
|
+
console.log(` ${(0, ui_1.color)('ccs persist glm --permission-mode acceptEdits', 'command')}`);
|
|
665
|
+
console.log('');
|
|
666
|
+
console.log(` ${(0, ui_1.dim)('# Persist with auto-approve enabled')}`);
|
|
667
|
+
console.log(` ${(0, ui_1.color)('ccs persist codex --dangerously-skip-permissions', 'command')}`);
|
|
668
|
+
console.log('');
|
|
391
669
|
console.log(` ${(0, ui_1.dim)('# List all backups')}`);
|
|
392
670
|
console.log(` ${(0, ui_1.color)('ccs persist --list-backups', 'command')}`);
|
|
393
671
|
console.log('');
|
|
@@ -400,7 +678,7 @@ async function showHelp() {
|
|
|
400
678
|
console.log((0, ui_1.subheader)('Notes'));
|
|
401
679
|
console.log(' [i] CLIProxy profiles require the proxy to be running.');
|
|
402
680
|
console.log(' [i] Copilot profiles require copilot-api daemon.');
|
|
403
|
-
console.log(
|
|
681
|
+
console.log(` [i] Backups are saved as ${getClaudeSettingsDisplayPath()}.backup.YYYYMMDD_HHMMSS`);
|
|
404
682
|
console.log('');
|
|
405
683
|
}
|
|
406
684
|
/** Main persist command handler */
|
|
@@ -411,6 +689,9 @@ async function handlePersistCommand(args) {
|
|
|
411
689
|
return;
|
|
412
690
|
}
|
|
413
691
|
const parsedArgs = parseArgs(args);
|
|
692
|
+
if (parsedArgs.parseError) {
|
|
693
|
+
throw new Error(parsedArgs.parseError);
|
|
694
|
+
}
|
|
414
695
|
// Handle --list-backups
|
|
415
696
|
if (parsedArgs.listBackups) {
|
|
416
697
|
await handleListBackups();
|
|
@@ -422,6 +703,7 @@ async function handlePersistCommand(args) {
|
|
|
422
703
|
return;
|
|
423
704
|
}
|
|
424
705
|
await (0, ui_1.initUI)();
|
|
706
|
+
const resolvedPermissionMode = resolvePermissionMode(parsedArgs);
|
|
425
707
|
if (!parsedArgs.profile) {
|
|
426
708
|
console.log((0, ui_1.fail)('Profile name is required'));
|
|
427
709
|
console.log('');
|
|
@@ -461,7 +743,7 @@ async function handlePersistCommand(args) {
|
|
|
461
743
|
console.log('');
|
|
462
744
|
console.log(`Profile type: ${(0, ui_1.color)(resolved.profileType, 'command')}`);
|
|
463
745
|
console.log('');
|
|
464
|
-
console.log(
|
|
746
|
+
console.log(`The following env vars will be written to ${getClaudeSettingsDisplayPath()}:`);
|
|
465
747
|
console.log('');
|
|
466
748
|
// Display env vars (mask sensitive values)
|
|
467
749
|
const envKeys = Object.keys(resolved.env);
|
|
@@ -472,45 +754,40 @@ async function handlePersistCommand(args) {
|
|
|
472
754
|
const maxKeyLen = Math.max(...envKeys.map((k) => k.length));
|
|
473
755
|
for (const [key, value] of Object.entries(resolved.env)) {
|
|
474
756
|
const paddedKey = key.padEnd(maxKeyLen + 2);
|
|
475
|
-
const displayValue = key
|
|
476
|
-
? maskApiKey(value)
|
|
477
|
-
: value;
|
|
757
|
+
const displayValue = isSensitiveEnvKey(key) ? maskApiKey(value) : value;
|
|
478
758
|
console.log(` ${(0, ui_1.color)(paddedKey, 'command')} = ${displayValue}`);
|
|
479
759
|
}
|
|
480
760
|
console.log('');
|
|
761
|
+
if (resolvedPermissionMode) {
|
|
762
|
+
console.log(`Default permission mode: ${(0, ui_1.color)(resolvedPermissionMode, 'command')}`);
|
|
763
|
+
if (resolvedPermissionMode === 'bypassPermissions') {
|
|
764
|
+
console.log((0, ui_1.warn)('Auto-approve enabled: Claude will skip permission prompts by default.'));
|
|
765
|
+
}
|
|
766
|
+
console.log('');
|
|
767
|
+
}
|
|
481
768
|
// Show warning if applicable
|
|
482
769
|
if (resolved.warning) {
|
|
483
770
|
console.log((0, ui_1.warn)(resolved.warning));
|
|
484
771
|
console.log('');
|
|
485
772
|
}
|
|
486
773
|
// Warning about modification
|
|
487
|
-
console.log((0, ui_1.warn)(
|
|
774
|
+
console.log((0, ui_1.warn)(`This will modify ${getClaudeSettingsDisplayPath()}`));
|
|
488
775
|
console.log((0, ui_1.dim)(' Existing hooks and other settings will be preserved.'));
|
|
489
776
|
console.log('');
|
|
490
777
|
// Check if settings.json exists for backup
|
|
491
|
-
const settingsPath = getClaudeSettingsPath();
|
|
778
|
+
const settingsPath = (0, claude_config_path_1.getClaudeSettingsPath)();
|
|
492
779
|
const settingsExist = fs.existsSync(settingsPath);
|
|
780
|
+
let createBackupFlag = false;
|
|
493
781
|
// Track backup path for error recovery guidance
|
|
494
782
|
let createdBackupPath = null;
|
|
495
783
|
// Backup prompt (unless --yes)
|
|
496
784
|
if (settingsExist) {
|
|
497
|
-
|
|
785
|
+
createBackupFlag = parsedArgs.yes === true; // Auto-backup with --yes
|
|
498
786
|
if (!parsedArgs.yes) {
|
|
499
787
|
createBackupFlag = await prompt_1.InteractivePrompt.confirm('Create backup before modifying?', {
|
|
500
788
|
default: true,
|
|
501
789
|
});
|
|
502
790
|
}
|
|
503
|
-
if (createBackupFlag) {
|
|
504
|
-
try {
|
|
505
|
-
createdBackupPath = createBackup();
|
|
506
|
-
console.log((0, ui_1.ok)(`Backup created: ${createdBackupPath.replace(os.homedir(), '~')}`));
|
|
507
|
-
console.log('');
|
|
508
|
-
}
|
|
509
|
-
catch (error) {
|
|
510
|
-
console.log((0, ui_1.fail)(`Failed to create backup: ${error.message}`));
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
791
|
}
|
|
515
792
|
// Proceed confirmation (unless --yes)
|
|
516
793
|
if (!parsedArgs.yes) {
|
|
@@ -520,42 +797,81 @@ async function handlePersistCommand(args) {
|
|
|
520
797
|
process.exit(0);
|
|
521
798
|
}
|
|
522
799
|
}
|
|
523
|
-
// Read existing settings and merge
|
|
524
|
-
const existingSettings = readClaudeSettings();
|
|
525
|
-
// Validate existing env is an object (not array/primitive)
|
|
526
|
-
const rawEnv = existingSettings.env;
|
|
527
|
-
let existingEnv = {};
|
|
528
|
-
if (rawEnv !== undefined && rawEnv !== null) {
|
|
529
|
-
if (typeof rawEnv !== 'object' || Array.isArray(rawEnv)) {
|
|
530
|
-
console.log((0, ui_1.warn)('Existing env in settings.json is not an object - it will be replaced'));
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
existingEnv = rawEnv;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
const mergedSettings = {
|
|
537
|
-
...existingSettings,
|
|
538
|
-
env: {
|
|
539
|
-
...existingEnv,
|
|
540
|
-
...resolved.env,
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
// Write merged settings
|
|
544
800
|
try {
|
|
545
|
-
|
|
801
|
+
await withPersistSettingsLock(async () => {
|
|
802
|
+
if (createBackupFlag && (await pathExists(settingsPath))) {
|
|
803
|
+
try {
|
|
804
|
+
createdBackupPath = await createBackup();
|
|
805
|
+
console.log((0, ui_1.ok)(`Backup created: ${formatDisplayPath(createdBackupPath)}`));
|
|
806
|
+
console.log('');
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
throw new Error(`Failed to create backup: ${error.message}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// Read existing settings and merge
|
|
813
|
+
const existingSettings = await readClaudeSettings();
|
|
814
|
+
// Validate existing env is an object (not array/primitive)
|
|
815
|
+
const rawEnv = existingSettings.env;
|
|
816
|
+
let existingEnv = {};
|
|
817
|
+
if (rawEnv !== undefined) {
|
|
818
|
+
if (rawEnv === null) {
|
|
819
|
+
console.log((0, ui_1.warn)('Existing env in settings.json is null - it will be replaced'));
|
|
820
|
+
}
|
|
821
|
+
else if (typeof rawEnv !== 'object' || Array.isArray(rawEnv)) {
|
|
822
|
+
console.log((0, ui_1.warn)('Existing env in settings.json is not an object - it will be replaced'));
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
existingEnv = rawEnv;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const mergedSettings = {
|
|
829
|
+
...existingSettings,
|
|
830
|
+
env: {
|
|
831
|
+
...existingEnv,
|
|
832
|
+
...resolved.env,
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
if (resolvedPermissionMode) {
|
|
836
|
+
const rawPermissions = existingSettings.permissions;
|
|
837
|
+
let existingPermissions = {};
|
|
838
|
+
if (rawPermissions !== undefined) {
|
|
839
|
+
if (rawPermissions === null) {
|
|
840
|
+
console.log((0, ui_1.warn)('Existing permissions in settings.json is null - it will be replaced'));
|
|
841
|
+
}
|
|
842
|
+
else if (typeof rawPermissions !== 'object' || Array.isArray(rawPermissions)) {
|
|
843
|
+
console.log((0, ui_1.warn)('Existing permissions in settings.json is not an object - it will be replaced'));
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
existingPermissions = rawPermissions;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
mergedSettings.permissions = {
|
|
850
|
+
...existingPermissions,
|
|
851
|
+
defaultMode: resolvedPermissionMode,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
await writeClaudeSettings(mergedSettings);
|
|
855
|
+
});
|
|
546
856
|
}
|
|
547
857
|
catch (error) {
|
|
548
|
-
|
|
858
|
+
const message = error.message;
|
|
859
|
+
if (message.startsWith('Failed to create backup:')) {
|
|
860
|
+
console.log((0, ui_1.fail)(message));
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
console.log((0, ui_1.fail)(`Failed to write settings: ${message}`));
|
|
864
|
+
}
|
|
549
865
|
if (createdBackupPath) {
|
|
550
866
|
console.log('');
|
|
551
867
|
console.log((0, ui_1.info)(`A backup was created before this error:`));
|
|
552
|
-
console.log(` ${createdBackupPath
|
|
868
|
+
console.log(` ${formatDisplayPath(createdBackupPath)}`);
|
|
553
869
|
console.log((0, ui_1.dim)(' To restore: ccs persist --restore'));
|
|
554
870
|
}
|
|
555
871
|
process.exit(1);
|
|
556
872
|
}
|
|
557
873
|
console.log('');
|
|
558
|
-
console.log((0, ui_1.ok)(`Profile '${parsedArgs.profile}' written to
|
|
874
|
+
console.log((0, ui_1.ok)(`Profile '${parsedArgs.profile}' written to ${getClaudeSettingsDisplayPath()}`));
|
|
559
875
|
console.log('');
|
|
560
876
|
console.log((0, ui_1.info)('Claude Code will now use this profile by default.'));
|
|
561
877
|
console.log((0, ui_1.dim)(' To revert, restore the backup or edit settings.json manually.'));
|