@leynier/ccst 0.2.1 → 0.3.1
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 +58 -57
- 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
package/package.json
CHANGED
|
@@ -1,58 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
2
|
+
"name": "@leynier/ccst",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Claude Code Switch Tools for managing contexts",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"cli",
|
|
9
|
+
"contexts",
|
|
10
|
+
"config",
|
|
11
|
+
"switcher"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/leynier/ccst",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/leynier/ccst.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/leynier/ccst/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"readme": "README.md",
|
|
23
|
+
"module": "index.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"ccst": "./src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"format": "biome format --write",
|
|
30
|
+
"lint": "biome check --write",
|
|
31
|
+
"validate": "bun run format && bun run lint"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"jszip": "^3.10.1",
|
|
36
|
+
"picocolors": "^1.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "2.3.11",
|
|
40
|
+
"@types/bun": "latest"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"typescript": "^5"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"src",
|
|
47
|
+
"index.ts",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"AGENTS.md",
|
|
51
|
+
"CLAUDE.md",
|
|
52
|
+
"GEMINI.md",
|
|
53
|
+
"biome.json",
|
|
54
|
+
"tsconfig.json"
|
|
55
|
+
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
export const completionsCommand = (shell: string | undefined): void => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
2
|
+
if (!shell) {
|
|
3
|
+
throw new Error("error: shell required for completions");
|
|
4
|
+
}
|
|
5
|
+
const available = ["bash", "zsh", "fish", "powershell", "elvish"];
|
|
6
|
+
if (!available.includes(shell)) {
|
|
7
|
+
throw new Error(`error: unsupported shell ${shell}`);
|
|
8
|
+
}
|
|
9
|
+
const options = [
|
|
10
|
+
"-d",
|
|
11
|
+
"--delete",
|
|
12
|
+
"-c",
|
|
13
|
+
"--current",
|
|
14
|
+
"-r",
|
|
15
|
+
"--rename",
|
|
16
|
+
"-n",
|
|
17
|
+
"--new",
|
|
18
|
+
"-e",
|
|
19
|
+
"--edit",
|
|
20
|
+
"-s",
|
|
21
|
+
"--show",
|
|
22
|
+
"--export",
|
|
23
|
+
"--import",
|
|
24
|
+
"-u",
|
|
25
|
+
"--unset",
|
|
26
|
+
"--completions",
|
|
27
|
+
"-q",
|
|
28
|
+
"--quiet",
|
|
29
|
+
"--in-project",
|
|
30
|
+
"--local",
|
|
31
|
+
"--merge-from",
|
|
32
|
+
"--unmerge",
|
|
33
|
+
"--merge-history",
|
|
34
|
+
"--merge-full",
|
|
35
|
+
].join(" ");
|
|
36
|
+
if (shell === "bash") {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`# ccst bash completions\ncomplete -W "${options}" ccst\n`,
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (shell === "zsh") {
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
`#compdef ccst\n_arguments '*::options:(${options})'\n`,
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (shell === "fish") {
|
|
49
|
+
process.stdout.write(`complete -c ccst -f -a "${options}"\n`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
`# ${shell} completions not implemented; use bash/zsh/fish output\n`,
|
|
54
|
+
);
|
|
49
55
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import JSZip from "jszip";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import {
|
|
5
|
+
getAllBackupFilePaths,
|
|
6
|
+
getRelativePath,
|
|
7
|
+
} from "../../utils/ccs-paths.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OUTPUT = "ccs-config.zip";
|
|
10
|
+
|
|
11
|
+
export const configDumpCommand = async (outputPath?: string): Promise<void> => {
|
|
12
|
+
const output = resolve(outputPath ?? DEFAULT_OUTPUT);
|
|
13
|
+
const files = getAllBackupFilePaths();
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log(pc.yellow("No CCS configuration files found to backup"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const zip = new JSZip();
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
try {
|
|
21
|
+
const relativePath = getRelativePath(filePath);
|
|
22
|
+
const content = await Bun.file(filePath).arrayBuffer();
|
|
23
|
+
zip.file(relativePath, content);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(`Failed to read ${filePath}: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const zipContent = await zip.generateAsync({ type: "uint8array" });
|
|
31
|
+
await Bun.write(output, zipContent);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(`Failed to write ${output}: ${message}`);
|
|
35
|
+
}
|
|
36
|
+
console.log(pc.green(`Exported ${files.length} files to ${output}`));
|
|
37
|
+
for (const filePath of files) {
|
|
38
|
+
console.log(pc.dim(` - ${getRelativePath(filePath)}`));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import JSZip from "jszip";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import {
|
|
6
|
+
ensureDirectoryExists,
|
|
7
|
+
getCcsHome,
|
|
8
|
+
getFilesToDelete,
|
|
9
|
+
} from "../../utils/ccs-paths.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_INPUT = "ccs-config.zip";
|
|
12
|
+
|
|
13
|
+
type LoadOptions = {
|
|
14
|
+
replace?: boolean;
|
|
15
|
+
yes?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const confirmReplace = async (
|
|
19
|
+
profiles: string[],
|
|
20
|
+
authFiles: string[],
|
|
21
|
+
): Promise<boolean> => {
|
|
22
|
+
const totalFiles = profiles.length + authFiles.length;
|
|
23
|
+
if (totalFiles === 0) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
console.log(pc.yellow("\nThe following files will be deleted:"));
|
|
27
|
+
if (profiles.length > 0) {
|
|
28
|
+
console.log(pc.dim("\nProfiles:"));
|
|
29
|
+
for (const file of profiles) {
|
|
30
|
+
console.log(pc.red(` - ${file}`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (authFiles.length > 0) {
|
|
34
|
+
console.log(pc.dim("\nAuth files:"));
|
|
35
|
+
for (const file of authFiles) {
|
|
36
|
+
console.log(pc.red(` - ${file}`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log();
|
|
40
|
+
// Dynamic import for interactive prompt
|
|
41
|
+
const readline = await import("node:readline");
|
|
42
|
+
const rl = readline.createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout,
|
|
45
|
+
});
|
|
46
|
+
return new Promise((resolvePromise) => {
|
|
47
|
+
rl.question(
|
|
48
|
+
pc.yellow(`Delete ${totalFiles} files and replace? [y/N] `),
|
|
49
|
+
(answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolvePromise(answer.toLowerCase() === "y");
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const configLoadCommand = async (
|
|
58
|
+
inputPath?: string,
|
|
59
|
+
options?: LoadOptions,
|
|
60
|
+
): Promise<void> => {
|
|
61
|
+
const input = resolve(inputPath ?? DEFAULT_INPUT);
|
|
62
|
+
if (!existsSync(input)) {
|
|
63
|
+
throw new Error(`File not found: ${input}`);
|
|
64
|
+
}
|
|
65
|
+
const zipData = await Bun.file(input).arrayBuffer();
|
|
66
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
67
|
+
const fileEntries = Object.keys(zip.files).filter((name) => {
|
|
68
|
+
const file = zip.files[name];
|
|
69
|
+
return file && !file.dir;
|
|
70
|
+
});
|
|
71
|
+
if (fileEntries.length === 0) {
|
|
72
|
+
console.log(pc.yellow("No files found in zip archive"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const ccsHome = getCcsHome();
|
|
76
|
+
// Handle replace mode
|
|
77
|
+
if (options?.replace) {
|
|
78
|
+
const { profiles, authFiles } = getFilesToDelete();
|
|
79
|
+
if (!options.yes) {
|
|
80
|
+
const confirmed = await confirmReplace(profiles, authFiles);
|
|
81
|
+
if (!confirmed) {
|
|
82
|
+
console.log(pc.dim("Operation cancelled"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Delete existing files
|
|
87
|
+
for (const file of [...profiles, ...authFiles]) {
|
|
88
|
+
if (existsSync(file)) {
|
|
89
|
+
unlinkSync(file);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (profiles.length + authFiles.length > 0) {
|
|
93
|
+
console.log(
|
|
94
|
+
pc.dim(`Deleted ${profiles.length + authFiles.length} existing files`),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Extract files
|
|
99
|
+
let extractedCount = 0;
|
|
100
|
+
for (const relativePath of fileEntries) {
|
|
101
|
+
// Handle both forward and back slashes for cross-platform compatibility
|
|
102
|
+
const pathParts = relativePath.split(/[/\\]/);
|
|
103
|
+
const absolutePath = join(ccsHome, ...pathParts);
|
|
104
|
+
// Validate path is within ccsHome (prevent path traversal attacks)
|
|
105
|
+
if (!absolutePath.startsWith(ccsHome)) {
|
|
106
|
+
console.warn(pc.yellow(`Skipping suspicious path: ${relativePath}`));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const zipFile = zip.files[relativePath];
|
|
110
|
+
if (!zipFile) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const fileData = await zipFile.async("uint8array");
|
|
114
|
+
ensureDirectoryExists(absolutePath);
|
|
115
|
+
await Bun.write(absolutePath, fileData);
|
|
116
|
+
extractedCount++;
|
|
117
|
+
}
|
|
118
|
+
console.log(pc.green(`Imported ${extractedCount} files from ${input}`));
|
|
119
|
+
for (const relativePath of fileEntries) {
|
|
120
|
+
console.log(pc.dim(` - ${relativePath}`));
|
|
121
|
+
}
|
|
122
|
+
};
|
package/src/commands/create.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const createCommand = async (
|
|
4
|
-
|
|
3
|
+
export const createCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
await manager.createContext(name);
|
|
5
8
|
};
|
package/src/commands/delete.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const deleteCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const deleteCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (name) {
|
|
8
|
+
await manager.deleteContext(name);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await manager.interactiveDelete();
|
|
9
12
|
};
|
package/src/commands/edit.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const editCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
export const editCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (name) {
|
|
8
|
+
await manager.editContext(name);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const current = await manager.getCurrentContext();
|
|
12
|
+
if (!current) {
|
|
13
|
+
throw new Error("error: no current context set");
|
|
14
|
+
}
|
|
15
|
+
await manager.editContext(current);
|
|
13
16
|
};
|
package/src/commands/export.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const exportCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
export const exportCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (name) {
|
|
8
|
+
await manager.exportContext(name);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const current = await manager.getCurrentContext();
|
|
12
|
+
if (!current) {
|
|
13
|
+
throw new Error("error: no current context set");
|
|
14
|
+
}
|
|
15
|
+
await manager.exportContext(current);
|
|
13
16
|
};
|
|
@@ -1,88 +1,113 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
1
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
4
|
import type { ContextManager } from "../../core/context-manager.js";
|
|
5
|
+
import { colors } from "../../utils/colors.js";
|
|
5
6
|
import { deepMerge } from "../../utils/deep-merge.js";
|
|
6
7
|
import { readJson, readJsonIfExists } from "../../utils/json.js";
|
|
7
|
-
import { colors } from "../../utils/colors.js";
|
|
8
8
|
|
|
9
9
|
const defaultConfigsDir = (): string => path.join(homedir(), ".ccst");
|
|
10
10
|
const ccsDir = (): string => path.join(homedir(), ".ccs");
|
|
11
11
|
|
|
12
|
-
const ensureDefaultConfig = async (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
const ensureDefaultConfig = async (
|
|
13
|
+
manager: ContextManager,
|
|
14
|
+
configsDir: string,
|
|
15
|
+
): Promise<{ created: boolean; defaultPath: string }> => {
|
|
16
|
+
const defaultPath = path.join(configsDir, "default.json");
|
|
17
|
+
if (existsSync(defaultPath)) {
|
|
18
|
+
return { created: false, defaultPath };
|
|
19
|
+
}
|
|
20
|
+
const currentContext = await getCurrentContext(manager);
|
|
21
|
+
if (currentContext) {
|
|
22
|
+
return { created: false, defaultPath };
|
|
23
|
+
}
|
|
24
|
+
const claudeSettings = path.join(homedir(), ".claude", "settings.json");
|
|
25
|
+
if (!existsSync(claudeSettings)) {
|
|
26
|
+
return { created: false, defaultPath };
|
|
27
|
+
}
|
|
28
|
+
mkdirSync(configsDir, { recursive: true });
|
|
29
|
+
const content = await Bun.file(claudeSettings).text();
|
|
30
|
+
await Bun.write(defaultPath, content);
|
|
31
|
+
return { created: true, defaultPath };
|
|
29
32
|
};
|
|
30
33
|
|
|
31
|
-
const getCurrentContext = async (
|
|
32
|
-
|
|
34
|
+
const getCurrentContext = async (
|
|
35
|
+
manager: ContextManager,
|
|
36
|
+
): Promise<string | undefined> => {
|
|
37
|
+
return manager.getCurrentContext();
|
|
33
38
|
};
|
|
34
39
|
|
|
35
|
-
const loadDefaultConfig = async (
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const loadDefaultConfig = async (
|
|
41
|
+
configsDir: string,
|
|
42
|
+
): Promise<Record<string, unknown>> => {
|
|
43
|
+
const defaultPath = path.join(configsDir, "default.json");
|
|
44
|
+
return readJsonIfExists<Record<string, unknown>>(defaultPath, {});
|
|
38
45
|
};
|
|
39
46
|
|
|
40
|
-
const importProfile = async (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
const importProfile = async (
|
|
48
|
+
manager: ContextManager,
|
|
49
|
+
profileName: string,
|
|
50
|
+
merged: Record<string, unknown>,
|
|
51
|
+
): Promise<void> => {
|
|
52
|
+
await manager.deleteContext(profileName).catch(() => undefined);
|
|
53
|
+
const input = `${JSON.stringify(merged, null, 2)}\n`;
|
|
54
|
+
await manager.importContextFromString(profileName, input);
|
|
44
55
|
};
|
|
45
56
|
|
|
46
|
-
const createDefaultProfileIfNeeded = async (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const createDefaultProfileIfNeeded = async (
|
|
58
|
+
manager: ContextManager,
|
|
59
|
+
created: boolean,
|
|
60
|
+
defaultProfile: Record<string, unknown>,
|
|
61
|
+
): Promise<void> => {
|
|
62
|
+
if (!created) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await importProfile(manager, "default", defaultProfile);
|
|
51
66
|
};
|
|
52
67
|
|
|
53
|
-
export const importFromCcs = async (
|
|
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
|
-
|
|
68
|
+
export const importFromCcs = async (
|
|
69
|
+
manager: ContextManager,
|
|
70
|
+
configsDir?: string,
|
|
71
|
+
): Promise<void> => {
|
|
72
|
+
const ccsPath = ccsDir();
|
|
73
|
+
if (!existsSync(ccsPath)) {
|
|
74
|
+
throw new Error(`CCS directory not found: ${ccsPath}`);
|
|
75
|
+
}
|
|
76
|
+
console.log(`📥 Importing profiles from CCS settings...`);
|
|
77
|
+
const dir = configsDir ?? defaultConfigsDir();
|
|
78
|
+
const { created } = await ensureDefaultConfig(manager, dir);
|
|
79
|
+
const defaultConfig = await loadDefaultConfig(dir);
|
|
80
|
+
const defaultProfile = await readJsonIfExists<Record<string, unknown>>(
|
|
81
|
+
path.join(dir, "default.json"),
|
|
82
|
+
defaultConfig,
|
|
83
|
+
);
|
|
84
|
+
await createDefaultProfileIfNeeded(manager, created, defaultProfile);
|
|
85
|
+
const currentContext = await manager.getCurrentContext();
|
|
86
|
+
let entries: string[] = [];
|
|
87
|
+
try {
|
|
88
|
+
entries = readdirSync(ccsPath).filter((name) =>
|
|
89
|
+
name.endsWith(".settings.json"),
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
entries = [];
|
|
93
|
+
}
|
|
94
|
+
let importedCount = 0;
|
|
95
|
+
for (const fileName of entries) {
|
|
96
|
+
const settingsPath = path.join(ccsPath, fileName);
|
|
97
|
+
const profileName = fileName.replace(/\.settings\.json$/u, "");
|
|
98
|
+
console.log(` 📦 Importing '${colors.cyan(profileName)}'...`);
|
|
99
|
+
const settings = await readJson<Record<string, unknown>>(settingsPath);
|
|
100
|
+
const merged = deepMerge(defaultConfig, settings);
|
|
101
|
+
if (currentContext && currentContext === profileName) {
|
|
102
|
+
await manager.unsetContext();
|
|
103
|
+
}
|
|
104
|
+
await importProfile(manager, profileName, merged);
|
|
105
|
+
importedCount++;
|
|
106
|
+
}
|
|
107
|
+
if (currentContext) {
|
|
108
|
+
await manager.switchContext(currentContext);
|
|
109
|
+
}
|
|
110
|
+
console.log(
|
|
111
|
+
`✅ Imported ${colors.bold(colors.green(String(importedCount)))} profiles from CCS`,
|
|
112
|
+
);
|
|
88
113
|
};
|