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