@leynier/ccst 0.2.1 → 0.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/package.json +57 -56
- package/src/commands/completions.ts +53 -47
- package/src/commands/config/dump.ts +40 -0
- package/src/commands/config/load.ts +122 -0
- package/src/commands/create.ts +5 -2
- package/src/commands/delete.ts +9 -6
- package/src/commands/edit.ts +13 -10
- package/src/commands/export.ts +13 -10
- package/src/commands/import-profiles/ccs.ts +94 -69
- package/src/commands/import-profiles/configs.ts +78 -60
- package/src/commands/import.ts +8 -5
- package/src/commands/list.ts +5 -2
- package/src/commands/merge.ts +25 -12
- package/src/commands/rename.ts +14 -11
- package/src/commands/show.ts +13 -10
- package/src/commands/switch.ts +9 -4
- package/src/commands/unset.ts +1 -1
- package/src/core/context-manager.test.ts +49 -47
- package/src/core/context-manager.ts +484 -389
- package/src/core/merge-manager.test.ts +40 -25
- package/src/core/merge-manager.ts +182 -132
- package/src/core/settings-level.ts +11 -8
- package/src/core/state.ts +22 -17
- package/src/index.ts +169 -130
- package/src/types/index.ts +5 -5
- package/src/utils/ccs-paths.ts +120 -0
- package/src/utils/colors.ts +6 -6
- package/src/utils/deep-merge.ts +21 -18
- package/src/utils/interactive.ts +68 -56
- package/src/utils/json.ts +17 -11
- package/src/utils/paths.ts +46 -44
|
@@ -1,397 +1,492 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
1
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
5
4
|
import type { SettingsLevel } from "../types/index.js";
|
|
6
5
|
import { colors } from "../utils/colors.js";
|
|
7
|
-
import { hasLocalContexts, hasProjectContexts } from "../utils/paths.js";
|
|
8
6
|
import { readJson, writeJson } from "../utils/json.js";
|
|
7
|
+
import type { Paths } from "../utils/paths.js";
|
|
8
|
+
import { hasLocalContexts, hasProjectContexts } from "../utils/paths.js";
|
|
9
|
+
import {
|
|
10
|
+
formatHistory,
|
|
11
|
+
loadHistory,
|
|
12
|
+
mergeFull,
|
|
13
|
+
mergePermissions,
|
|
14
|
+
saveHistory,
|
|
15
|
+
unmergeFull,
|
|
16
|
+
unmergePermissions,
|
|
17
|
+
} from "./merge-manager.js";
|
|
9
18
|
import { loadState, saveState, setCurrent, unsetCurrent } from "./state.js";
|
|
10
|
-
import { mergePermissions, mergeFull, unmergePermissions, unmergeFull, loadHistory, saveHistory, formatHistory } from "./merge-manager.js";
|
|
11
19
|
|
|
12
20
|
export class ContextManager {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
21
|
+
public readonly contextsDir: string;
|
|
22
|
+
public readonly settingsPath: string;
|
|
23
|
+
public readonly statePath: string;
|
|
24
|
+
public readonly settingsLevel: SettingsLevel;
|
|
25
|
+
|
|
26
|
+
public constructor(paths: Paths) {
|
|
27
|
+
this.contextsDir = paths.contextsDir;
|
|
28
|
+
this.settingsPath = paths.settingsPath;
|
|
29
|
+
this.statePath = paths.statePath;
|
|
30
|
+
this.settingsLevel = paths.settingsLevel;
|
|
31
|
+
mkdirSync(this.contextsDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public contextPath(name: string): string {
|
|
35
|
+
return path.join(this.contextsDir, `${name}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async listContexts(): Promise<string[]> {
|
|
39
|
+
let entries: string[] = [];
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync(this.contextsDir);
|
|
42
|
+
} catch {
|
|
43
|
+
entries = [];
|
|
44
|
+
}
|
|
45
|
+
const contexts = entries
|
|
46
|
+
.filter((name) => name.endsWith(".json") && !name.startsWith("."))
|
|
47
|
+
.map((name) => name.replace(/\.json$/u, ""));
|
|
48
|
+
contexts.sort();
|
|
49
|
+
return contexts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async getCurrentContext(): Promise<string | undefined> {
|
|
53
|
+
const state = await loadState(this.statePath);
|
|
54
|
+
return state.current;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async switchContext(name: string): Promise<void> {
|
|
58
|
+
const contexts = await this.listContexts();
|
|
59
|
+
if (!contexts.includes(name)) {
|
|
60
|
+
throw new Error(`error: no context exists with the name "${name}"`);
|
|
61
|
+
}
|
|
62
|
+
const state = await loadState(this.statePath);
|
|
63
|
+
const nextState = setCurrent(state, name);
|
|
64
|
+
const content = await Bun.file(this.contextPath(name)).text();
|
|
65
|
+
mkdirSync(path.dirname(this.settingsPath), { recursive: true });
|
|
66
|
+
await Bun.write(this.settingsPath, content);
|
|
67
|
+
await saveState(this.statePath, nextState);
|
|
68
|
+
console.log(`Switched to context "${colors.bold(colors.green(name))}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async switchToPrevious(): Promise<void> {
|
|
72
|
+
const state = await loadState(this.statePath);
|
|
73
|
+
if (!state.previous) {
|
|
74
|
+
throw new Error("error: no previous context");
|
|
75
|
+
}
|
|
76
|
+
await this.switchContext(state.previous);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async createContext(name: string): Promise<void> {
|
|
80
|
+
this.validateContextName(name);
|
|
81
|
+
const contexts = await this.listContexts();
|
|
82
|
+
if (contexts.includes(name)) {
|
|
83
|
+
throw new Error(`error: context "${name}" already exists`);
|
|
84
|
+
}
|
|
85
|
+
const contextPath = this.contextPath(name);
|
|
86
|
+
if (existsSync(this.settingsPath)) {
|
|
87
|
+
await Bun.write(contextPath, await Bun.file(this.settingsPath).text());
|
|
88
|
+
console.log(
|
|
89
|
+
`Context "${colors.bold(colors.green(name))}" created from current settings`,
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await writeJson(contextPath, {});
|
|
94
|
+
console.log(`Context "${colors.bold(colors.green(name))}" created (empty)`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async deleteContext(name: string): Promise<void> {
|
|
98
|
+
const state = await loadState(this.statePath);
|
|
99
|
+
if (state.current === name) {
|
|
100
|
+
throw new Error(`error: cannot delete the active context "${name}"`);
|
|
101
|
+
}
|
|
102
|
+
const contextPath = this.contextPath(name);
|
|
103
|
+
if (!existsSync(contextPath)) {
|
|
104
|
+
throw new Error(`error: no context exists with the name "${name}"`);
|
|
105
|
+
}
|
|
106
|
+
await Bun.remove(contextPath);
|
|
107
|
+
if (state.previous === name) {
|
|
108
|
+
const next = { ...state };
|
|
109
|
+
delete next.previous;
|
|
110
|
+
await saveState(this.statePath, next);
|
|
111
|
+
}
|
|
112
|
+
console.log(`Context "${colors.red(name)}" deleted`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async renameContext(oldName: string, newName: string): Promise<void> {
|
|
116
|
+
this.validateContextName(newName);
|
|
117
|
+
const contexts = await this.listContexts();
|
|
118
|
+
if (!contexts.includes(oldName)) {
|
|
119
|
+
throw new Error(`error: no context exists with the name "${oldName}"`);
|
|
120
|
+
}
|
|
121
|
+
if (contexts.includes(newName)) {
|
|
122
|
+
throw new Error(`error: context "${newName}" already exists`);
|
|
123
|
+
}
|
|
124
|
+
const oldPath = this.contextPath(oldName);
|
|
125
|
+
const newPath = this.contextPath(newName);
|
|
126
|
+
await Bun.rename(oldPath, newPath);
|
|
127
|
+
const state = await loadState(this.statePath);
|
|
128
|
+
let updated = false;
|
|
129
|
+
const next: typeof state = { ...state };
|
|
130
|
+
if (next.current === oldName) {
|
|
131
|
+
next.current = newName;
|
|
132
|
+
updated = true;
|
|
133
|
+
}
|
|
134
|
+
if (next.previous === oldName) {
|
|
135
|
+
next.previous = newName;
|
|
136
|
+
updated = true;
|
|
137
|
+
}
|
|
138
|
+
if (updated) {
|
|
139
|
+
await saveState(this.statePath, next);
|
|
140
|
+
}
|
|
141
|
+
console.log(
|
|
142
|
+
`Context "${oldName}" renamed to "${colors.bold(colors.green(newName))}"`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async showContext(name: string): Promise<void> {
|
|
147
|
+
const contextPath = this.contextPath(name);
|
|
148
|
+
if (!existsSync(contextPath)) {
|
|
149
|
+
throw new Error(`error: no context exists with the name "${name}"`);
|
|
150
|
+
}
|
|
151
|
+
const json = await readJson<Record<string, unknown>>(contextPath);
|
|
152
|
+
console.log(JSON.stringify(json, null, 2));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public async editContext(name: string): Promise<void> {
|
|
156
|
+
const contextPath = this.contextPath(name);
|
|
157
|
+
if (!existsSync(contextPath)) {
|
|
158
|
+
throw new Error(`error: no context exists with the name "${name}"`);
|
|
159
|
+
}
|
|
160
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
161
|
+
const status = spawnSync(editor, [contextPath], { stdio: "inherit" });
|
|
162
|
+
if (status.status !== 0) {
|
|
163
|
+
throw new Error("error: editor exited with non-zero status");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public async exportContext(name: string): Promise<void> {
|
|
168
|
+
const contextPath = this.contextPath(name);
|
|
169
|
+
if (!existsSync(contextPath)) {
|
|
170
|
+
throw new Error(`error: no context exists with the name "${name}"`);
|
|
171
|
+
}
|
|
172
|
+
const content = await Bun.file(contextPath).text();
|
|
173
|
+
process.stdout.write(content);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public async importContext(name: string): Promise<void> {
|
|
177
|
+
const input = await new Response(process.stdin).text();
|
|
178
|
+
await this.importContextFromString(name, input);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async importContextFromString(
|
|
182
|
+
name: string,
|
|
183
|
+
input: string,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
this.validateContextName(name);
|
|
186
|
+
const contexts = await this.listContexts();
|
|
187
|
+
if (contexts.includes(name)) {
|
|
188
|
+
throw new Error(`error: context "${name}" already exists`);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
JSON.parse(input);
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error("error: invalid JSON input");
|
|
194
|
+
}
|
|
195
|
+
await Bun.write(this.contextPath(name), input);
|
|
196
|
+
console.log(`Context "${colors.bold(colors.green(name))}" imported`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public async unsetContext(): Promise<void> {
|
|
200
|
+
if (existsSync(this.settingsPath)) {
|
|
201
|
+
await Bun.remove(this.settingsPath);
|
|
202
|
+
}
|
|
203
|
+
const state = await loadState(this.statePath);
|
|
204
|
+
const { state: next } = unsetCurrent(state);
|
|
205
|
+
await saveState(this.statePath, next);
|
|
206
|
+
console.log("Unset current context");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public async listContextsWithCurrent(quiet: boolean): Promise<void> {
|
|
210
|
+
const contexts = await this.listContexts();
|
|
211
|
+
const current = await this.getCurrentContext();
|
|
212
|
+
if (quiet) {
|
|
213
|
+
if (current) {
|
|
214
|
+
console.log(current);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (this.settingsLevel === "user") {
|
|
219
|
+
if (hasProjectContexts()) {
|
|
220
|
+
console.log(
|
|
221
|
+
`💡 Project contexts available: run 'ccst --in-project' to manage`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (hasLocalContexts()) {
|
|
225
|
+
console.log(
|
|
226
|
+
`💡 Local contexts available: run 'ccst --local' to manage`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (contexts.length === 0) {
|
|
231
|
+
const label =
|
|
232
|
+
this.settingsLevel === "user"
|
|
233
|
+
? "👤 User"
|
|
234
|
+
: this.settingsLevel === "project"
|
|
235
|
+
? "📁 Project"
|
|
236
|
+
: "💻 Local";
|
|
237
|
+
console.log(
|
|
238
|
+
`${label} contexts: No contexts found. Create one with: ccst -n <name>`,
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const label =
|
|
243
|
+
this.settingsLevel === "user"
|
|
244
|
+
? "👤 User"
|
|
245
|
+
: this.settingsLevel === "project"
|
|
246
|
+
? "📁 Project"
|
|
247
|
+
: "💻 Local";
|
|
248
|
+
console.log(`${colors.bold(colors.cyan(label))} contexts:`);
|
|
249
|
+
for (const ctx of contexts) {
|
|
250
|
+
if (ctx === current) {
|
|
251
|
+
console.log(
|
|
252
|
+
` ${colors.bold(colors.green(ctx))} ${colors.dim("(current)")}`,
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` ${ctx}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public async interactiveSelect(): Promise<void> {
|
|
261
|
+
const { selectContext } = await import("../utils/interactive.js");
|
|
262
|
+
const contexts = await this.listContexts();
|
|
263
|
+
if (contexts.length === 0) {
|
|
264
|
+
console.log("No contexts found");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const current = await this.getCurrentContext();
|
|
268
|
+
const selected = await selectContext(contexts, current);
|
|
269
|
+
if (!selected || selected === current) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
await this.switchContext(selected);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public async interactiveDelete(): Promise<void> {
|
|
276
|
+
const { selectContext } = await import("../utils/interactive.js");
|
|
277
|
+
const contexts = await this.listContexts();
|
|
278
|
+
if (contexts.length === 0) {
|
|
279
|
+
console.log("No contexts found");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const current = await this.getCurrentContext();
|
|
283
|
+
const selected = await selectContext(contexts, current);
|
|
284
|
+
if (!selected) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
await this.deleteContext(selected);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public async interactiveRename(): Promise<void> {
|
|
291
|
+
const { selectContext, promptInput } = await import(
|
|
292
|
+
"../utils/interactive.js"
|
|
293
|
+
);
|
|
294
|
+
const contexts = await this.listContexts();
|
|
295
|
+
if (contexts.length === 0) {
|
|
296
|
+
console.log("No contexts found");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const current = await this.getCurrentContext();
|
|
300
|
+
const selected = await selectContext(contexts, current);
|
|
301
|
+
if (!selected) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const newName = await promptInput("New name");
|
|
305
|
+
if (!newName) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
await this.renameContext(selected, newName);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public async interactiveCreateContext(): Promise<void> {
|
|
312
|
+
const { promptInput } = await import("../utils/interactive.js");
|
|
313
|
+
const name = await promptInput("Context name");
|
|
314
|
+
if (!name) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await this.createContext(name);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public async mergeFrom(
|
|
321
|
+
target: string,
|
|
322
|
+
source: string,
|
|
323
|
+
mergeFullFlag: boolean,
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
if (mergeFullFlag) {
|
|
326
|
+
await this.mergeFromFull(target, source);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const { targetPath, targetJson, sourceJson } = await this.getMergePayload(
|
|
330
|
+
target,
|
|
331
|
+
source,
|
|
332
|
+
);
|
|
333
|
+
const entry = mergePermissions(targetJson, sourceJson, source);
|
|
334
|
+
await writeJson(targetPath, targetJson);
|
|
335
|
+
await this.appendHistory(target, entry);
|
|
336
|
+
console.log(
|
|
337
|
+
`✅ Merged ${entry.mergedItems.length} permissions from '${colors.green(source)}' into '${colors.bold(colors.green(target))}'`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public async mergeFromFull(target: string, source: string): Promise<void> {
|
|
342
|
+
const { targetPath, targetJson, sourceJson } = await this.getMergePayload(
|
|
343
|
+
target,
|
|
344
|
+
source,
|
|
345
|
+
);
|
|
346
|
+
const entry = mergeFull(targetJson, sourceJson, source);
|
|
347
|
+
await writeJson(targetPath, targetJson);
|
|
348
|
+
await this.appendHistory(target, entry);
|
|
349
|
+
console.log(
|
|
350
|
+
`✅ Full merge completed: ${entry.mergedItems.length} items from '${colors.green(source)}' into '${colors.bold(colors.green(target))}'`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public async unmergeFrom(
|
|
355
|
+
target: string,
|
|
356
|
+
source: string,
|
|
357
|
+
mergeFullFlag: boolean,
|
|
358
|
+
): Promise<void> {
|
|
359
|
+
if (mergeFullFlag) {
|
|
360
|
+
await this.unmergeFromFull(target, source);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const { targetPath, targetJson, contextName } =
|
|
364
|
+
await this.getUnmergePayload(target, source);
|
|
365
|
+
const entries = await loadHistory(this.contextsDir, contextName);
|
|
366
|
+
const nextEntries = unmergePermissions(targetJson, entries, source);
|
|
367
|
+
await writeJson(targetPath, targetJson);
|
|
368
|
+
await saveHistory(this.contextsDir, contextName, nextEntries);
|
|
369
|
+
console.log(
|
|
370
|
+
`✅ Removed permissions previously merged from '${colors.red(source)}' in '${colors.bold(colors.green(target))}'`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public async unmergeFromFull(target: string, source: string): Promise<void> {
|
|
375
|
+
const { targetPath, targetJson, contextName } =
|
|
376
|
+
await this.getUnmergePayload(target, source);
|
|
377
|
+
const entries = await loadHistory(this.contextsDir, contextName);
|
|
378
|
+
const nextEntries = unmergeFull(targetJson, entries, source);
|
|
379
|
+
await writeJson(targetPath, targetJson);
|
|
380
|
+
await saveHistory(this.contextsDir, contextName, nextEntries);
|
|
381
|
+
console.log(
|
|
382
|
+
`✅ Removed all settings previously merged from '${colors.red(source)}' in '${colors.bold(colors.green(target))}'`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public async showMergeHistory(name?: string): Promise<void> {
|
|
387
|
+
const contextName = name ?? (await this.getCurrentContext());
|
|
388
|
+
if (!contextName) {
|
|
389
|
+
throw new Error("error: no current context set");
|
|
390
|
+
}
|
|
391
|
+
const entries = await loadHistory(this.contextsDir, contextName);
|
|
392
|
+
console.log(formatHistory(contextName, entries));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async getMergePayload(
|
|
396
|
+
target: string,
|
|
397
|
+
source: string,
|
|
398
|
+
): Promise<{
|
|
399
|
+
targetPath: string;
|
|
400
|
+
targetJson: Record<string, unknown>;
|
|
401
|
+
sourceJson: Record<string, unknown>;
|
|
402
|
+
}> {
|
|
403
|
+
const targetPath = await this.resolveTargetPath(target);
|
|
404
|
+
const targetJson = await readJson<Record<string, unknown>>(targetPath);
|
|
405
|
+
const sourceJson = await this.resolveSourceJson(source);
|
|
406
|
+
return { targetPath, targetJson, sourceJson };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async getUnmergePayload(
|
|
410
|
+
target: string,
|
|
411
|
+
source: string,
|
|
412
|
+
): Promise<{
|
|
413
|
+
targetPath: string;
|
|
414
|
+
targetJson: Record<string, unknown>;
|
|
415
|
+
contextName: string;
|
|
416
|
+
}> {
|
|
417
|
+
const targetPath = await this.resolveTargetPath(target);
|
|
418
|
+
const targetJson = await readJson<Record<string, unknown>>(targetPath);
|
|
419
|
+
const contextName =
|
|
420
|
+
target === "current"
|
|
421
|
+
? ((await this.getCurrentContext()) ?? "current")
|
|
422
|
+
: target;
|
|
423
|
+
return { targetPath, targetJson, contextName };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private async resolveTargetPath(target: string): Promise<string> {
|
|
427
|
+
if (target === "current") {
|
|
428
|
+
if (!existsSync(this.settingsPath)) {
|
|
429
|
+
throw new Error("error: no current context is set");
|
|
430
|
+
}
|
|
431
|
+
return this.settingsPath;
|
|
432
|
+
}
|
|
433
|
+
const contextPath = this.contextPath(target);
|
|
434
|
+
if (!existsSync(contextPath)) {
|
|
435
|
+
throw new Error(`error: no context exists with the name "${target}"`);
|
|
436
|
+
}
|
|
437
|
+
return contextPath;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async resolveSourceJson(
|
|
441
|
+
source: string,
|
|
442
|
+
): Promise<Record<string, unknown>> {
|
|
443
|
+
if (source === "user") {
|
|
444
|
+
const homeSettings = path.join(
|
|
445
|
+
path.dirname(path.dirname(this.contextsDir)),
|
|
446
|
+
"settings.json",
|
|
447
|
+
);
|
|
448
|
+
if (!existsSync(homeSettings)) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`error: user settings file not found at ${homeSettings}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return readJson<Record<string, unknown>>(homeSettings);
|
|
454
|
+
}
|
|
455
|
+
if (source.endsWith(".json")) {
|
|
456
|
+
if (!existsSync(source)) {
|
|
457
|
+
throw new Error(`error: source file not found at ${source}`);
|
|
458
|
+
}
|
|
459
|
+
return readJson<Record<string, unknown>>(source);
|
|
460
|
+
}
|
|
461
|
+
const contextPath = this.contextPath(source);
|
|
462
|
+
if (!existsSync(contextPath)) {
|
|
463
|
+
throw new Error(`error: no context exists with the name "${source}"`);
|
|
464
|
+
}
|
|
465
|
+
return readJson<Record<string, unknown>>(contextPath);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private async appendHistory(
|
|
469
|
+
target: string,
|
|
470
|
+
entry: ReturnType<typeof mergePermissions>,
|
|
471
|
+
): Promise<void> {
|
|
472
|
+
const contextName =
|
|
473
|
+
target === "current"
|
|
474
|
+
? ((await this.getCurrentContext()) ?? "current")
|
|
475
|
+
: target;
|
|
476
|
+
const history = await loadHistory(this.contextsDir, contextName);
|
|
477
|
+
history.push(entry);
|
|
478
|
+
await saveHistory(this.contextsDir, contextName, history);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private validateContextName(name: string): void {
|
|
482
|
+
if (
|
|
483
|
+
!name ||
|
|
484
|
+
name === "-" ||
|
|
485
|
+
name === "." ||
|
|
486
|
+
name === ".." ||
|
|
487
|
+
name.includes("/")
|
|
488
|
+
) {
|
|
489
|
+
throw new Error(`error: invalid context name "${name}"`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
397
492
|
}
|