@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.
Files changed (159) hide show
  1. package/README.md +3 -3
  2. package/config/base-codex.settings.json +4 -4
  3. package/dist/ccs.js +68 -97
  4. package/dist/ccs.js.map +1 -1
  5. package/dist/cliproxy/config/env-builder.d.ts.map +1 -1
  6. package/dist/cliproxy/config/env-builder.js +77 -15
  7. package/dist/cliproxy/config/env-builder.js.map +1 -1
  8. package/dist/cliproxy/config/extended-context-config.d.ts +1 -8
  9. package/dist/cliproxy/config/extended-context-config.d.ts.map +1 -1
  10. package/dist/cliproxy/config/extended-context-config.js +8 -24
  11. package/dist/cliproxy/config/extended-context-config.js.map +1 -1
  12. package/dist/cliproxy/config/generator.d.ts +3 -1
  13. package/dist/cliproxy/config/generator.d.ts.map +1 -1
  14. package/dist/cliproxy/config/generator.js +210 -39
  15. package/dist/cliproxy/config/generator.js.map +1 -1
  16. package/dist/cliproxy/config/thinking-config.d.ts.map +1 -1
  17. package/dist/cliproxy/config/thinking-config.js +15 -4
  18. package/dist/cliproxy/config/thinking-config.js.map +1 -1
  19. package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
  20. package/dist/cliproxy/executor/env-resolver.js +38 -2
  21. package/dist/cliproxy/executor/env-resolver.js.map +1 -1
  22. package/dist/cliproxy/executor/index.d.ts.map +1 -1
  23. package/dist/cliproxy/executor/index.js +7 -4
  24. package/dist/cliproxy/executor/index.js.map +1 -1
  25. package/dist/cliproxy/executor/retry-handler.d.ts +1 -1
  26. package/dist/cliproxy/executor/retry-handler.js +2 -2
  27. package/dist/cliproxy/executor/retry-handler.js.map +1 -1
  28. package/dist/cliproxy/index.d.ts +1 -0
  29. package/dist/cliproxy/index.d.ts.map +1 -1
  30. package/dist/cliproxy/index.js +12 -3
  31. package/dist/cliproxy/index.js.map +1 -1
  32. package/dist/cliproxy/model-catalog.d.ts.map +1 -1
  33. package/dist/cliproxy/model-catalog.js +6 -1
  34. package/dist/cliproxy/model-catalog.js.map +1 -1
  35. package/dist/cliproxy/model-config.d.ts.map +1 -1
  36. package/dist/cliproxy/model-config.js +6 -6
  37. package/dist/cliproxy/model-config.js.map +1 -1
  38. package/dist/cliproxy/model-id-normalizer.d.ts +70 -0
  39. package/dist/cliproxy/model-id-normalizer.d.ts.map +1 -0
  40. package/dist/cliproxy/model-id-normalizer.js +120 -0
  41. package/dist/cliproxy/model-id-normalizer.js.map +1 -0
  42. package/dist/cliproxy/provider-capabilities.d.ts +6 -0
  43. package/dist/cliproxy/provider-capabilities.d.ts.map +1 -1
  44. package/dist/cliproxy/provider-capabilities.js +24 -2
  45. package/dist/cliproxy/provider-capabilities.js.map +1 -1
  46. package/dist/cliproxy/quota-fetcher-claude-normalizer.d.ts +18 -0
  47. package/dist/cliproxy/quota-fetcher-claude-normalizer.d.ts.map +1 -0
  48. package/dist/cliproxy/quota-fetcher-claude-normalizer.js +291 -0
  49. package/dist/cliproxy/quota-fetcher-claude-normalizer.js.map +1 -0
  50. package/dist/cliproxy/quota-fetcher-claude.d.ts +21 -0
  51. package/dist/cliproxy/quota-fetcher-claude.d.ts.map +1 -0
  52. package/dist/cliproxy/quota-fetcher-claude.js +263 -0
  53. package/dist/cliproxy/quota-fetcher-claude.js.map +1 -0
  54. package/dist/cliproxy/quota-manager.d.ts +7 -4
  55. package/dist/cliproxy/quota-manager.d.ts.map +1 -1
  56. package/dist/cliproxy/quota-manager.js +80 -21
  57. package/dist/cliproxy/quota-manager.js.map +1 -1
  58. package/dist/cliproxy/quota-types.d.ts +74 -2
  59. package/dist/cliproxy/quota-types.d.ts.map +1 -1
  60. package/dist/cliproxy/quota-types.js +1 -1
  61. package/dist/cliproxy/remote-proxy-client.d.ts +3 -10
  62. package/dist/cliproxy/remote-proxy-client.d.ts.map +1 -1
  63. package/dist/cliproxy/remote-proxy-client.js +32 -29
  64. package/dist/cliproxy/remote-proxy-client.js.map +1 -1
  65. package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
  66. package/dist/cliproxy/services/variant-settings.js +12 -13
  67. package/dist/cliproxy/services/variant-settings.js.map +1 -1
  68. package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
  69. package/dist/cliproxy/tool-sanitization-proxy.js +15 -5
  70. package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
  71. package/dist/cliproxy/types.d.ts +1 -1
  72. package/dist/cliproxy/types.d.ts.map +1 -1
  73. package/dist/commands/api-command.d.ts +13 -0
  74. package/dist/commands/api-command.d.ts.map +1 -1
  75. package/dist/commands/api-command.js +85 -24
  76. package/dist/commands/api-command.js.map +1 -1
  77. package/dist/commands/arg-extractor.d.ts +29 -0
  78. package/dist/commands/arg-extractor.d.ts.map +1 -0
  79. package/dist/commands/arg-extractor.js +81 -0
  80. package/dist/commands/arg-extractor.js.map +1 -0
  81. package/dist/commands/cliproxy/help-subcommand.d.ts.map +1 -1
  82. package/dist/commands/cliproxy/help-subcommand.js +3 -2
  83. package/dist/commands/cliproxy/help-subcommand.js.map +1 -1
  84. package/dist/commands/cliproxy/index.d.ts +13 -0
  85. package/dist/commands/cliproxy/index.d.ts.map +1 -1
  86. package/dist/commands/cliproxy/index.js +66 -126
  87. package/dist/commands/cliproxy/index.js.map +1 -1
  88. package/dist/commands/cliproxy/quota-subcommand.d.ts +2 -1
  89. package/dist/commands/cliproxy/quota-subcommand.d.ts.map +1 -1
  90. package/dist/commands/cliproxy/quota-subcommand.js +172 -46
  91. package/dist/commands/cliproxy/quota-subcommand.js.map +1 -1
  92. package/dist/commands/config-command.d.ts.map +1 -1
  93. package/dist/commands/config-command.js +17 -16
  94. package/dist/commands/config-command.js.map +1 -1
  95. package/dist/commands/config-image-analysis-command.d.ts.map +1 -1
  96. package/dist/commands/config-image-analysis-command.js +37 -27
  97. package/dist/commands/config-image-analysis-command.js.map +1 -1
  98. package/dist/commands/persist-command.d.ts.map +1 -1
  99. package/dist/commands/persist-command.js +455 -139
  100. package/dist/commands/persist-command.js.map +1 -1
  101. package/dist/management/checks/config-check.d.ts.map +1 -1
  102. package/dist/management/checks/config-check.js +2 -2
  103. package/dist/management/checks/config-check.js.map +1 -1
  104. package/dist/shared/extended-context-utils.d.ts +14 -0
  105. package/dist/shared/extended-context-utils.d.ts.map +1 -0
  106. package/dist/shared/extended-context-utils.js +35 -0
  107. package/dist/shared/extended-context-utils.js.map +1 -0
  108. package/dist/ui/assets/{accounts-BiL_RD2P.js → accounts-7x9sEJ6E.js} +1 -1
  109. package/dist/ui/assets/{alert-dialog-BoSPpkH2.js → alert-dialog-CSG7wAU8.js} +1 -1
  110. package/dist/ui/assets/{api-BrHcZxrF.js → api-BF6fKWuJ.js} +1 -1
  111. package/dist/ui/assets/{auth-section-CNEQOji9.js → auth-section-BrRo7P5e.js} +1 -1
  112. package/dist/ui/assets/{backups-section-CYPJq2Y6.js → backups-section-BvoQ6sjn.js} +1 -1
  113. package/dist/ui/assets/cliproxy-Dp-l74Ht.js +3 -0
  114. package/dist/ui/assets/cliproxy-control-panel-D3_E45dZ.js +1 -0
  115. package/dist/ui/assets/{confirm-dialog-JGLPn0Pg.js → confirm-dialog-QJFlAs8u.js} +1 -1
  116. package/dist/ui/assets/{copilot-Bt-S8kHA.js → copilot-CRxuFhMs.js} +2 -2
  117. package/dist/ui/assets/{cursor-Qg3y30M0.js → cursor-Dsbkv6Oq.js} +1 -1
  118. package/dist/ui/assets/{globalenv-section-Dxrj87dw.js → globalenv-section-Cf_HiHep.js} +1 -1
  119. package/dist/ui/assets/{health-Bjnrp81E.js → health-CgHX0qCf.js} +1 -1
  120. package/dist/ui/assets/index-BNJ3rHVd.js +47 -0
  121. package/dist/ui/assets/{index-ApptKWow.js → index-BUC-Zfgc.js} +1 -1
  122. package/dist/ui/assets/{index-XJ9726WB.js → index-C-4XwF_5.js} +1 -1
  123. package/dist/ui/assets/{index-DOQkTkq-.js → index-D9YYXEsW.js} +1 -1
  124. package/dist/ui/assets/{index-UVFLMRYY.js → index-yfs5e5sm.js} +1 -1
  125. package/dist/ui/assets/{proxy-status-widget-PLX0fi09.js → proxy-status-widget-CNesfhAI.js} +1 -1
  126. package/dist/ui/assets/{separator-yZDNbi3M.js → separator-9gFbzNO-.js} +1 -1
  127. package/dist/ui/assets/{shared-DZ3QOOgF.js → shared-DfHvplPN.js} +1 -1
  128. package/dist/ui/assets/{switch-DsTWD8-1.js → switch-BMNi4Qdv.js} +1 -1
  129. package/dist/ui/index.html +1 -1
  130. package/dist/utils/claude-config-path.d.ts +11 -0
  131. package/dist/utils/claude-config-path.d.ts.map +1 -0
  132. package/dist/utils/claude-config-path.js +51 -0
  133. package/dist/utils/claude-config-path.js.map +1 -0
  134. package/dist/utils/websearch/hook-config.d.ts.map +1 -1
  135. package/dist/utils/websearch/hook-config.js +9 -23
  136. package/dist/utils/websearch/hook-config.js.map +1 -1
  137. package/dist/web-server/jsonl-parser.d.ts +0 -4
  138. package/dist/web-server/jsonl-parser.d.ts.map +1 -1
  139. package/dist/web-server/jsonl-parser.js +52 -29
  140. package/dist/web-server/jsonl-parser.js.map +1 -1
  141. package/dist/web-server/routes/cliproxy-stats-routes.d.ts.map +1 -1
  142. package/dist/web-server/routes/cliproxy-stats-routes.js +105 -3
  143. package/dist/web-server/routes/cliproxy-stats-routes.js.map +1 -1
  144. package/dist/web-server/routes/persist-routes.d.ts.map +1 -1
  145. package/dist/web-server/routes/persist-routes.js +54 -44
  146. package/dist/web-server/routes/persist-routes.js.map +1 -1
  147. package/dist/web-server/routes/route-helpers.d.ts +0 -5
  148. package/dist/web-server/routes/route-helpers.d.ts.map +1 -1
  149. package/dist/web-server/routes/route-helpers.js +52 -6
  150. package/dist/web-server/routes/route-helpers.js.map +1 -1
  151. package/dist/web-server/routes/settings-routes.d.ts.map +1 -1
  152. package/dist/web-server/routes/settings-routes.js +72 -10
  153. package/dist/web-server/routes/settings-routes.js.map +1 -1
  154. package/dist/web-server/shared-routes.js +2 -2
  155. package/dist/web-server/shared-routes.js.map +1 -1
  156. package/package.json +1 -1
  157. package/dist/ui/assets/cliproxy-CDDlPp51.js +0 -3
  158. package/dist/ui/assets/cliproxy-control-panel-XSSdoJ3f.js +0 -1
  159. 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
- for (let i = 0; i < args.length; i++) {
49
- const arg = args[i];
50
- if (arg === '--yes' || arg === '-y') {
51
- result.yes = true;
52
- }
53
- else if (arg === '--help' || arg === '-h') {
54
- // Will be handled in main function
55
- }
56
- else if (arg === '--list-backups') {
57
- result.listBackups = true;
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 (arg === '--restore') {
60
- // Check if next arg is a timestamp (not a flag)
61
- const nextArg = args[i + 1];
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.restore = true; // Use latest
101
+ result.permissionMode = permissionModeOption.value;
68
102
  }
69
103
  }
70
- else if (!arg.startsWith('-') && !result.profile) {
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
- /** Get Claude settings.json path */
77
- function getClaudeSettingsPath() {
78
- return path.join(os.homedir(), '.claude', 'settings.json');
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
- /** Read existing Claude settings.json with validation */
81
- function readClaudeSettings() {
82
- const settingsPath = getClaudeSettingsPath();
150
+ async function pathExists(filePath) {
83
151
  try {
84
- const content = fs.readFileSync(settingsPath, 'utf8');
85
- // Handle empty file (0 bytes)
86
- if (!content.trim()) {
87
- return {};
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 parsed = JSON.parse(content);
90
- // Validate parsed value is a plain object (not array, null, or primitive)
91
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
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
- return parsed;
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
- * Write settings back to settings.json
106
- * Note: mode 0o600 only applies when creating a new file.
107
- * Existing file permissions are preserved (acceptable behavior).
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 dir = path.dirname(settingsPath);
116
- if (!fs.existsSync(dir)) {
117
- fs.mkdirSync(dir, { recursive: true });
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
- const stats = fs.lstatSync(filePath);
127
- return stats.isSymbolicLink();
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
- return false;
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 (!fs.existsSync(settingsPath)) {
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
- // Security: Reject symlinks to prevent writing to unexpected locations
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.copyFileSync(settingsPath, backupPath);
153
- // Security: Set restrictive permissions on backup (contains API keys)
154
- fs.chmodSync(backupPath, 0o600);
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
- // Ignore deletion errors (file may be locked or already deleted)
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
- // Parse YYYYMMDD_HHMMSS
191
- const year = parseInt(timestamp.slice(0, 4));
192
- const month = parseInt(timestamp.slice(4, 6)) - 1;
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: new Date(year, month, day, hour, min, sec),
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)('This will replace ~/.claude/settings.json'));
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
- // Validate backup JSON integrity before restore
568
+ let parsedBackupSettings;
325
569
  try {
326
- const backupContent = fs.readFileSync(backup.path, 'utf8');
327
- const parsed = JSON.parse(backupContent);
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
- // Security: Reject symlink backup files
338
- if (isSymlink(backup.path)) {
339
- console.log((0, ui_1.fail)('Backup file is a symlink - refusing to restore for security'));
340
- process.exit(1);
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
- // Copy backup over settings.json
343
- const settingsPath = getClaudeSettingsPath();
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(' ~/.claude/settings.json for native Claude Code usage.');
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(' [i] Backups are saved as ~/.claude/settings.json.backup.YYYYMMDD_HHMMSS');
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('The following env vars will be written to ~/.claude/settings.json:');
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.includes('TOKEN') || key.includes('KEY') || key.includes('SECRET')
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)('This will modify ~/.claude/settings.json'));
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
- let createBackupFlag = parsedArgs.yes === true; // Auto-backup with --yes
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
- writeClaudeSettings(mergedSettings);
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
- console.log((0, ui_1.fail)(`Failed to write settings: ${error.message}`));
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.replace(os.homedir(), '~')}`);
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 ~/.claude/settings.json`));
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.'));