@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,74 +1,92 @@
|
|
|
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
|
|
|
11
|
-
const ensureDefaultConfig = async (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
const ensureDefaultConfig = async (
|
|
12
|
+
manager: ContextManager,
|
|
13
|
+
configsDir: string,
|
|
14
|
+
): Promise<boolean> => {
|
|
15
|
+
const defaultPath = path.join(configsDir, "default.json");
|
|
16
|
+
if (existsSync(defaultPath)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const currentContext = await manager.getCurrentContext();
|
|
20
|
+
if (currentContext) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const claudeSettings = path.join(homedir(), ".claude", "settings.json");
|
|
24
|
+
if (!existsSync(claudeSettings)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
mkdirSync(configsDir, { recursive: true });
|
|
28
|
+
const content = await Bun.file(claudeSettings).text();
|
|
29
|
+
await Bun.write(defaultPath, content);
|
|
30
|
+
return true;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
const loadDefaultConfig = async (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const loadDefaultConfig = async (
|
|
34
|
+
configsDir: string,
|
|
35
|
+
): Promise<Record<string, unknown>> => {
|
|
36
|
+
const defaultPath = path.join(configsDir, "default.json");
|
|
37
|
+
return readJsonIfExists<Record<string, unknown>>(defaultPath, {});
|
|
33
38
|
};
|
|
34
39
|
|
|
35
|
-
const importProfile = async (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const importProfile = async (
|
|
41
|
+
manager: ContextManager,
|
|
42
|
+
profileName: string,
|
|
43
|
+
merged: Record<string, unknown>,
|
|
44
|
+
): Promise<void> => {
|
|
45
|
+
await manager.deleteContext(profileName).catch(() => undefined);
|
|
46
|
+
const input = `${JSON.stringify(merged, null, 2)}\n`;
|
|
47
|
+
await manager.importContextFromString(profileName, input);
|
|
39
48
|
};
|
|
40
49
|
|
|
41
|
-
export const importFromConfigs = async (
|
|
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
|
-
|
|
50
|
+
export const importFromConfigs = async (
|
|
51
|
+
manager: ContextManager,
|
|
52
|
+
configsDir?: string,
|
|
53
|
+
): Promise<void> => {
|
|
54
|
+
const dir = configsDir ?? defaultConfigsDir();
|
|
55
|
+
console.log(`📥 Importing profiles from configs directory...`);
|
|
56
|
+
const created = await ensureDefaultConfig(manager, dir);
|
|
57
|
+
const defaultConfig = await loadDefaultConfig(dir);
|
|
58
|
+
const defaultProfile = await readJsonIfExists<Record<string, unknown>>(
|
|
59
|
+
path.join(dir, "default.json"),
|
|
60
|
+
defaultConfig,
|
|
61
|
+
);
|
|
62
|
+
const currentContext = await manager.getCurrentContext();
|
|
63
|
+
let entries: string[] = [];
|
|
64
|
+
try {
|
|
65
|
+
entries = readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
66
|
+
} catch {
|
|
67
|
+
entries = [];
|
|
68
|
+
}
|
|
69
|
+
let importedCount = 0;
|
|
70
|
+
for (const fileName of entries) {
|
|
71
|
+
const configPath = path.join(dir, fileName);
|
|
72
|
+
const profileName = path.basename(fileName, ".json");
|
|
73
|
+
console.log(` 📦 Importing '${colors.cyan(profileName)}'...`);
|
|
74
|
+
const config = await readJson<Record<string, unknown>>(configPath);
|
|
75
|
+
const merged =
|
|
76
|
+
fileName === "default.json" ? config : deepMerge(defaultConfig, config);
|
|
77
|
+
if (currentContext && currentContext === profileName) {
|
|
78
|
+
await manager.unsetContext();
|
|
79
|
+
}
|
|
80
|
+
await importProfile(manager, profileName, merged);
|
|
81
|
+
importedCount++;
|
|
82
|
+
}
|
|
83
|
+
if (created) {
|
|
84
|
+
await importProfile(manager, "default", defaultProfile);
|
|
85
|
+
}
|
|
86
|
+
if (currentContext) {
|
|
87
|
+
await manager.switchContext(currentContext);
|
|
88
|
+
}
|
|
89
|
+
console.log(
|
|
90
|
+
`✅ Imported ${colors.bold(colors.green(String(importedCount)))} profiles from configs`,
|
|
91
|
+
);
|
|
74
92
|
};
|
package/src/commands/import.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const importCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export const importCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (!name) {
|
|
8
|
+
throw new Error("error: context name required for import");
|
|
9
|
+
}
|
|
10
|
+
await manager.importContext(name);
|
|
8
11
|
};
|
package/src/commands/list.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const listCommand = async (
|
|
4
|
-
|
|
3
|
+
export const listCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
quiet: boolean,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
await manager.listContextsWithCurrent(quiet);
|
|
5
8
|
};
|
package/src/commands/merge.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const mergeCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export const mergeCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
source?: string,
|
|
6
|
+
target?: string,
|
|
7
|
+
mergeFull?: boolean,
|
|
8
|
+
): Promise<void> => {
|
|
9
|
+
if (!source) {
|
|
10
|
+
throw new Error("error: source required for merge");
|
|
11
|
+
}
|
|
12
|
+
await manager.mergeFrom(target ?? "current", source, Boolean(mergeFull));
|
|
8
13
|
};
|
|
9
14
|
|
|
10
|
-
export const unmergeCommand = async (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
export const unmergeCommand = async (
|
|
16
|
+
manager: ContextManager,
|
|
17
|
+
source?: string,
|
|
18
|
+
target?: string,
|
|
19
|
+
mergeFull?: boolean,
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
if (!source) {
|
|
22
|
+
throw new Error("error: source required for unmerge");
|
|
23
|
+
}
|
|
24
|
+
await manager.unmergeFrom(target ?? "current", source, Boolean(mergeFull));
|
|
15
25
|
};
|
|
16
26
|
|
|
17
|
-
export const mergeHistoryCommand = async (
|
|
18
|
-
|
|
27
|
+
export const mergeHistoryCommand = async (
|
|
28
|
+
manager: ContextManager,
|
|
29
|
+
target?: string,
|
|
30
|
+
): Promise<void> => {
|
|
31
|
+
await manager.showMergeHistory(target);
|
|
19
32
|
};
|
package/src/commands/rename.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const renameCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
export const renameCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (name) {
|
|
8
|
+
const { promptInput } = await import("../utils/interactive.js");
|
|
9
|
+
const input = await promptInput("New name");
|
|
10
|
+
if (!input) {
|
|
11
|
+
throw new Error("error: new name required");
|
|
12
|
+
}
|
|
13
|
+
await manager.renameContext(name, input);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await manager.interactiveRename();
|
|
14
17
|
};
|
package/src/commands/show.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const showCommand = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
export const showCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name?: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
if (name) {
|
|
8
|
+
await manager.showContext(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.showContext(current);
|
|
13
16
|
};
|
package/src/commands/switch.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ContextManager } from "../core/context-manager.js";
|
|
2
2
|
|
|
3
|
-
export const switchCommand = async (
|
|
4
|
-
|
|
3
|
+
export const switchCommand = async (
|
|
4
|
+
manager: ContextManager,
|
|
5
|
+
name: string,
|
|
6
|
+
): Promise<void> => {
|
|
7
|
+
await manager.switchContext(name);
|
|
5
8
|
};
|
|
6
9
|
|
|
7
|
-
export const switchPreviousCommand = async (
|
|
8
|
-
|
|
10
|
+
export const switchPreviousCommand = async (
|
|
11
|
+
manager: ContextManager,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
await manager.switchToPrevious();
|
|
9
14
|
};
|
package/src/commands/unset.ts
CHANGED
|
@@ -1,74 +1,76 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { ContextManager } from "./context-manager.js";
|
|
6
5
|
import type { SettingsLevel } from "../types/index.js";
|
|
6
|
+
import { ContextManager } from "./context-manager.js";
|
|
7
7
|
|
|
8
8
|
const tempRoot = path.join(tmpdir(), "ccst-tests");
|
|
9
9
|
|
|
10
10
|
const makePaths = (root: string, level: SettingsLevel) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
const claudeDir = path.join(root, ".claude");
|
|
12
|
+
const contextsDir = path.join(claudeDir, "settings");
|
|
13
|
+
if (level === "user") {
|
|
14
|
+
return {
|
|
15
|
+
contextsDir,
|
|
16
|
+
settingsPath: path.join(claudeDir, "settings.json"),
|
|
17
|
+
statePath: path.join(contextsDir, ".cctx-state.json"),
|
|
18
|
+
settingsLevel: level,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (level === "project") {
|
|
22
|
+
return {
|
|
23
|
+
contextsDir,
|
|
24
|
+
settingsPath: path.join(claudeDir, "settings.json"),
|
|
25
|
+
statePath: path.join(contextsDir, ".cctx-state.json"),
|
|
26
|
+
settingsLevel: level,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
contextsDir,
|
|
31
|
+
settingsPath: path.join(claudeDir, "settings.local.json"),
|
|
32
|
+
statePath: path.join(contextsDir, ".cctx-state.local.json"),
|
|
33
|
+
settingsLevel: level,
|
|
34
|
+
};
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const createManager = (root: string): ContextManager => {
|
|
38
|
-
|
|
38
|
+
return new ContextManager(makePaths(root, "user"));
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
beforeEach(() => {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
43
|
+
mkdirSync(tempRoot, { recursive: true });
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
afterEach(() => {
|
|
47
|
-
|
|
47
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test("creates context from empty when no settings.json", async () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const manager = createManager(tempRoot);
|
|
52
|
+
await manager.createContext("alpha");
|
|
53
|
+
const contextPath = path.join(tempRoot, ".claude", "settings", "alpha.json");
|
|
54
|
+
const content = await Bun.file(contextPath).text();
|
|
55
|
+
expect(content.trim()).toBe("{}");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
test("switches context and updates settings.json", async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
const manager = createManager(tempRoot);
|
|
60
|
+
const contextPath = path.join(tempRoot, ".claude", "settings", "beta.json");
|
|
61
|
+
writeFileSync(contextPath, '{"name":"beta"}');
|
|
62
|
+
await manager.switchContext("beta");
|
|
63
|
+
const settingsPath = path.join(tempRoot, ".claude", "settings.json");
|
|
64
|
+
const settings = await Bun.file(settingsPath).text();
|
|
65
|
+
expect(settings).toContain("beta");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
test("importContextFromString validates json", async () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const manager = createManager(tempRoot);
|
|
70
|
+
await expect(manager.importContextFromString("gamma", "{")).rejects.toThrow(
|
|
71
|
+
"invalid JSON",
|
|
72
|
+
);
|
|
73
|
+
await manager.importContextFromString("gamma", '{"ok":true}\n');
|
|
74
|
+
const contextPath = path.join(tempRoot, ".claude", "settings", "gamma.json");
|
|
75
|
+
expect(await Bun.file(contextPath).exists()).toBe(true);
|
|
74
76
|
});
|