@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
|
@@ -1,38 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mergeFull,
|
|
4
|
+
mergePermissions,
|
|
5
|
+
unmergeFull,
|
|
6
|
+
unmergePermissions,
|
|
7
|
+
} from "./merge-manager.js";
|
|
3
8
|
|
|
4
|
-
const base = () => ({
|
|
5
|
-
|
|
9
|
+
const base = () => ({
|
|
10
|
+
permissions: { allow: ["a"], deny: [] },
|
|
11
|
+
env: { A: "1" },
|
|
12
|
+
other: 1,
|
|
13
|
+
});
|
|
14
|
+
const source = () => ({
|
|
15
|
+
permissions: { allow: ["b"], deny: ["x"] },
|
|
16
|
+
env: { B: "2" },
|
|
17
|
+
other2: true,
|
|
18
|
+
});
|
|
6
19
|
|
|
7
20
|
test("mergePermissions adds allow/deny and records items", () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
const target = base();
|
|
22
|
+
const entry = mergePermissions(target, source(), "source");
|
|
23
|
+
expect((target.permissions as { allow: string[] }).allow).toContain("b");
|
|
24
|
+
expect((target.permissions as { deny: string[] }).deny).toContain("x");
|
|
25
|
+
expect(
|
|
26
|
+
entry.mergedItems.some((item) => item.includes("permissions.allow")),
|
|
27
|
+
).toBe(true);
|
|
13
28
|
});
|
|
14
29
|
|
|
15
30
|
test("mergeFull merges env and top-level keys", () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
const target = base();
|
|
32
|
+
const entry = mergeFull(target, source(), "source");
|
|
33
|
+
expect((target.env as Record<string, string>).B).toBe("2");
|
|
34
|
+
expect(target.other2).toBe(true);
|
|
35
|
+
expect(entry.mergedItems.some((item) => item.startsWith("env:"))).toBe(true);
|
|
21
36
|
});
|
|
22
37
|
|
|
23
38
|
test("unmergePermissions removes merged items", () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const target = base();
|
|
40
|
+
const history = [mergePermissions(target, source(), "source")];
|
|
41
|
+
const remaining = unmergePermissions(target, history, "source");
|
|
42
|
+
expect(remaining.length).toBe(0);
|
|
43
|
+
expect((target.permissions as { allow: string[] }).allow).not.toContain("b");
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
test("unmergeFull removes merged items and history", () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
const target = base();
|
|
48
|
+
const history = [mergeFull(target, source(), "source")];
|
|
49
|
+
const remaining = unmergeFull(target, history, "source");
|
|
50
|
+
expect(remaining.length).toBe(0);
|
|
51
|
+
expect((target.env as Record<string, string>).B).toBeUndefined();
|
|
52
|
+
expect(target.other2).toBeUndefined();
|
|
38
53
|
});
|
|
@@ -1,152 +1,202 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import type { MergeHistory, MergeHistoryEntry } from "../types/index.js";
|
|
4
4
|
|
|
5
|
-
const ensurePermissions = (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const ensurePermissions = (
|
|
6
|
+
json: Record<string, unknown>,
|
|
7
|
+
): { allow: string[]; deny: string[] } => {
|
|
8
|
+
const permissions =
|
|
9
|
+
(json.permissions as Record<string, unknown> | undefined) ?? {};
|
|
10
|
+
const allow = Array.isArray(permissions.allow)
|
|
11
|
+
? permissions.allow.slice()
|
|
12
|
+
: [];
|
|
13
|
+
const deny = Array.isArray(permissions.deny) ? permissions.deny.slice() : [];
|
|
14
|
+
json.permissions = { ...permissions, allow, deny };
|
|
15
|
+
return { allow, deny };
|
|
11
16
|
};
|
|
12
17
|
|
|
13
|
-
export const mergePermissions = (
|
|
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
|
-
|
|
18
|
+
export const mergePermissions = (
|
|
19
|
+
target: Record<string, unknown>,
|
|
20
|
+
source: Record<string, unknown>,
|
|
21
|
+
sourceLabel: string,
|
|
22
|
+
): MergeHistoryEntry => {
|
|
23
|
+
const { allow, deny } = ensurePermissions(target);
|
|
24
|
+
const sourcePermissions =
|
|
25
|
+
(source.permissions as Record<string, unknown> | undefined) ?? {};
|
|
26
|
+
const sourceAllow = Array.isArray(sourcePermissions.allow)
|
|
27
|
+
? sourcePermissions.allow
|
|
28
|
+
: [];
|
|
29
|
+
const sourceDeny = Array.isArray(sourcePermissions.deny)
|
|
30
|
+
? sourcePermissions.deny
|
|
31
|
+
: [];
|
|
32
|
+
const mergedItems: string[] = [];
|
|
33
|
+
const allowSet = new Set(allow);
|
|
34
|
+
const denySet = new Set(deny);
|
|
35
|
+
for (const item of sourceAllow) {
|
|
36
|
+
if (!allowSet.has(item)) {
|
|
37
|
+
allowSet.add(item);
|
|
38
|
+
mergedItems.push(`permissions.allow:${item}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const item of sourceDeny) {
|
|
42
|
+
if (!denySet.has(item)) {
|
|
43
|
+
denySet.add(item);
|
|
44
|
+
mergedItems.push(`permissions.deny:${item}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
(target.permissions as Record<string, unknown>).allow = Array.from(allowSet);
|
|
48
|
+
(target.permissions as Record<string, unknown>).deny = Array.from(denySet);
|
|
49
|
+
return {
|
|
50
|
+
source: sourceLabel,
|
|
51
|
+
mergedItems,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
};
|
|
40
54
|
};
|
|
41
55
|
|
|
42
|
-
export const mergeFull = (
|
|
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
|
-
|
|
56
|
+
export const mergeFull = (
|
|
57
|
+
target: Record<string, unknown>,
|
|
58
|
+
source: Record<string, unknown>,
|
|
59
|
+
sourceLabel: string,
|
|
60
|
+
): MergeHistoryEntry => {
|
|
61
|
+
const mergedItems: string[] = [];
|
|
62
|
+
const permEntry = mergePermissions(target, source, sourceLabel);
|
|
63
|
+
mergedItems.push(...permEntry.mergedItems);
|
|
64
|
+
const targetEnv = (target.env as Record<string, unknown> | undefined) ?? {};
|
|
65
|
+
const sourceEnv = (source.env as Record<string, unknown> | undefined) ?? {};
|
|
66
|
+
const nextEnv: Record<string, unknown> = { ...targetEnv };
|
|
67
|
+
for (const [key, value] of Object.entries(sourceEnv)) {
|
|
68
|
+
if (!(key in nextEnv)) {
|
|
69
|
+
nextEnv[key] = value;
|
|
70
|
+
mergedItems.push(`env:${key}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(nextEnv).length > 0) {
|
|
74
|
+
target.env = nextEnv;
|
|
75
|
+
}
|
|
76
|
+
for (const [key, value] of Object.entries(source)) {
|
|
77
|
+
if (key === "permissions" || key === "env") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!(key in target)) {
|
|
81
|
+
target[key] = value;
|
|
82
|
+
mergedItems.push(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
source: sourceLabel,
|
|
87
|
+
mergedItems,
|
|
88
|
+
timestamp: permEntry.timestamp,
|
|
89
|
+
};
|
|
72
90
|
};
|
|
73
91
|
|
|
74
|
-
export const unmergePermissions = (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
export const unmergePermissions = (
|
|
93
|
+
target: Record<string, unknown>,
|
|
94
|
+
history: MergeHistory,
|
|
95
|
+
sourceLabel: string,
|
|
96
|
+
): MergeHistory => {
|
|
97
|
+
const { allow, deny } = ensurePermissions(target);
|
|
98
|
+
const allowSet = new Set(allow);
|
|
99
|
+
const denySet = new Set(deny);
|
|
100
|
+
const remainingHistory = history.filter(
|
|
101
|
+
(entry) => entry.source !== sourceLabel,
|
|
102
|
+
);
|
|
103
|
+
const removedEntries = history.filter(
|
|
104
|
+
(entry) => entry.source === sourceLabel,
|
|
105
|
+
);
|
|
106
|
+
for (const entry of removedEntries) {
|
|
107
|
+
for (const item of entry.mergedItems) {
|
|
108
|
+
if (item.startsWith("permissions.allow:")) {
|
|
109
|
+
allowSet.delete(item.replace("permissions.allow:", ""));
|
|
110
|
+
}
|
|
111
|
+
if (item.startsWith("permissions.deny:")) {
|
|
112
|
+
denySet.delete(item.replace("permissions.deny:", ""));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
(target.permissions as Record<string, unknown>).allow = Array.from(allowSet);
|
|
117
|
+
(target.permissions as Record<string, unknown>).deny = Array.from(denySet);
|
|
118
|
+
return remainingHistory;
|
|
93
119
|
};
|
|
94
120
|
|
|
95
|
-
export const unmergeFull = (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
export const unmergeFull = (
|
|
122
|
+
target: Record<string, unknown>,
|
|
123
|
+
history: MergeHistory,
|
|
124
|
+
sourceLabel: string,
|
|
125
|
+
): MergeHistory => {
|
|
126
|
+
const remainingHistory = history.filter(
|
|
127
|
+
(entry) => entry.source !== sourceLabel,
|
|
128
|
+
);
|
|
129
|
+
const removedEntries = history.filter(
|
|
130
|
+
(entry) => entry.source === sourceLabel,
|
|
131
|
+
);
|
|
132
|
+
const targetEnv = (target.env as Record<string, unknown> | undefined) ?? {};
|
|
133
|
+
for (const entry of removedEntries) {
|
|
134
|
+
for (const item of entry.mergedItems) {
|
|
135
|
+
if (
|
|
136
|
+
item.startsWith("permissions.allow:") ||
|
|
137
|
+
item.startsWith("permissions.deny:")
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (item.startsWith("env:")) {
|
|
142
|
+
const key = item.replace("env:", "");
|
|
143
|
+
delete targetEnv[key];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
delete target[item];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (Object.keys(targetEnv).length > 0) {
|
|
150
|
+
target.env = targetEnv;
|
|
151
|
+
} else {
|
|
152
|
+
delete target.env;
|
|
153
|
+
}
|
|
154
|
+
return unmergePermissions(target, remainingHistory, sourceLabel);
|
|
118
155
|
};
|
|
119
156
|
|
|
120
|
-
export const historyPath = (
|
|
121
|
-
|
|
157
|
+
export const historyPath = (
|
|
158
|
+
contextsDir: string,
|
|
159
|
+
contextName: string,
|
|
160
|
+
): string => {
|
|
161
|
+
return path.join(contextsDir, `.${contextName}-merge-history.json`);
|
|
122
162
|
};
|
|
123
163
|
|
|
124
|
-
export const loadHistory = async (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
164
|
+
export const loadHistory = async (
|
|
165
|
+
contextsDir: string,
|
|
166
|
+
contextName: string,
|
|
167
|
+
): Promise<MergeHistory> => {
|
|
168
|
+
const filePath = historyPath(contextsDir, contextName);
|
|
169
|
+
if (!existsSync(filePath)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const text = await Bun.file(filePath).text();
|
|
173
|
+
return JSON.parse(text) as MergeHistory;
|
|
131
174
|
};
|
|
132
175
|
|
|
133
|
-
export const saveHistory = async (
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
export const saveHistory = async (
|
|
177
|
+
contextsDir: string,
|
|
178
|
+
contextName: string,
|
|
179
|
+
history: MergeHistory,
|
|
180
|
+
): Promise<void> => {
|
|
181
|
+
const filePath = historyPath(contextsDir, contextName);
|
|
182
|
+
await Bun.write(filePath, `${JSON.stringify(history, null, 2)}\n`);
|
|
136
183
|
};
|
|
137
184
|
|
|
138
|
-
export const formatHistory = (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
185
|
+
export const formatHistory = (
|
|
186
|
+
contextName: string,
|
|
187
|
+
history: MergeHistory,
|
|
188
|
+
): string => {
|
|
189
|
+
if (history.length === 0) {
|
|
190
|
+
return `No merge history for ${contextName}`;
|
|
191
|
+
}
|
|
192
|
+
const lines: string[] = [];
|
|
193
|
+
lines.push(`📋 Merge history for context '${contextName}':`);
|
|
194
|
+
lines.push("");
|
|
195
|
+
for (const entry of history) {
|
|
196
|
+
lines.push(` 📅 ${entry.timestamp}`);
|
|
197
|
+
lines.push(` 📁 Source: ${entry.source}`);
|
|
198
|
+
lines.push(` 📝 Merged ${entry.mergedItems.length} items`);
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
return lines.join("\n");
|
|
152
202
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { SettingsLevel } from "../types/index.js";
|
|
2
2
|
|
|
3
|
-
export const resolveSettingsLevel = (opts: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
export const resolveSettingsLevel = (opts: {
|
|
4
|
+
local?: boolean;
|
|
5
|
+
inProject?: boolean;
|
|
6
|
+
}): SettingsLevel => {
|
|
7
|
+
if (opts.local) {
|
|
8
|
+
return "local";
|
|
9
|
+
}
|
|
10
|
+
if (opts.inProject) {
|
|
11
|
+
return "project";
|
|
12
|
+
}
|
|
13
|
+
return "user";
|
|
11
14
|
};
|
package/src/core/state.ts
CHANGED
|
@@ -2,28 +2,33 @@ import type { State } from "../types/index.js";
|
|
|
2
2
|
import { readJsonIfExists, writeJson } from "../utils/json.js";
|
|
3
3
|
|
|
4
4
|
export const loadState = async (statePath: string): Promise<State> => {
|
|
5
|
-
|
|
5
|
+
return readJsonIfExists<State>(statePath, {});
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export const saveState = async (
|
|
9
|
-
|
|
8
|
+
export const saveState = async (
|
|
9
|
+
statePath: string,
|
|
10
|
+
state: State,
|
|
11
|
+
): Promise<void> => {
|
|
12
|
+
await writeJson(statePath, state);
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export const setCurrent = (state: State, context: string): State => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const next: State = { ...state };
|
|
17
|
+
if (next.current && next.current !== context) {
|
|
18
|
+
next.previous = next.current;
|
|
19
|
+
}
|
|
20
|
+
next.current = context;
|
|
21
|
+
return next;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
|
-
export const unsetCurrent = (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export const unsetCurrent = (
|
|
25
|
+
state: State,
|
|
26
|
+
): { state: State; previous?: string } => {
|
|
27
|
+
const next: State = { ...state };
|
|
28
|
+
const current = next.current;
|
|
29
|
+
delete next.current;
|
|
30
|
+
if (current) {
|
|
31
|
+
next.previous = current;
|
|
32
|
+
}
|
|
33
|
+
return { state: next, previous: current };
|
|
29
34
|
};
|