@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.
@@ -1,38 +1,53 @@
1
- import { test, expect } from "bun:test";
2
- import { mergePermissions, mergeFull, unmergePermissions, unmergeFull } from "./merge-manager.js";
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 = () => ({ permissions: { allow: ["a"], deny: [] }, env: { A: "1" }, other: 1 });
5
- const source = () => ({ permissions: { allow: ["b"], deny: ["x"] }, env: { B: "2" }, other2: true });
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
- const target = base();
9
- const entry = mergePermissions(target, source(), "source");
10
- expect((target.permissions as { allow: string[] }).allow).toContain("b");
11
- expect((target.permissions as { deny: string[] }).deny).toContain("x");
12
- expect(entry.mergedItems.some((item) => item.includes("permissions.allow"))).toBe(true);
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
- const target = base();
17
- const entry = mergeFull(target, source(), "source");
18
- expect((target.env as Record<string, string>).B).toBe("2");
19
- expect(target.other2).toBe(true);
20
- expect(entry.mergedItems.some((item) => item.startsWith("env:"))).toBe(true);
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
- const target = base();
25
- const history = [mergePermissions(target, source(), "source")];
26
- const remaining = unmergePermissions(target, history, "source");
27
- expect(remaining.length).toBe(0);
28
- expect((target.permissions as { allow: string[] }).allow).not.toContain("b");
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
- const target = base();
33
- const history = [mergeFull(target, source(), "source")];
34
- const remaining = unmergeFull(target, history, "source");
35
- expect(remaining.length).toBe(0);
36
- expect((target.env as Record<string, string>).B).toBeUndefined();
37
- expect(target.other2).toBeUndefined();
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 = (json: Record<string, unknown>): { allow: string[]; deny: string[] } => {
6
- const permissions = (json.permissions as Record<string, unknown> | undefined) ?? {};
7
- const allow = Array.isArray(permissions.allow) ? permissions.allow.slice() : [];
8
- const deny = Array.isArray(permissions.deny) ? permissions.deny.slice() : [];
9
- json.permissions = { ...permissions, allow, deny };
10
- return { allow, deny };
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 = (target: Record<string, unknown>, source: Record<string, unknown>, sourceLabel: string): MergeHistoryEntry => {
14
- const { allow, deny } = ensurePermissions(target);
15
- const sourcePermissions = (source.permissions as Record<string, unknown> | undefined) ?? {};
16
- const sourceAllow = Array.isArray(sourcePermissions.allow) ? sourcePermissions.allow : [];
17
- const sourceDeny = Array.isArray(sourcePermissions.deny) ? sourcePermissions.deny : [];
18
- const mergedItems: string[] = [];
19
- const allowSet = new Set(allow);
20
- const denySet = new Set(deny);
21
- for (const item of sourceAllow) {
22
- if (!allowSet.has(item)) {
23
- allowSet.add(item);
24
- mergedItems.push(`permissions.allow:${item}`);
25
- }
26
- }
27
- for (const item of sourceDeny) {
28
- if (!denySet.has(item)) {
29
- denySet.add(item);
30
- mergedItems.push(`permissions.deny:${item}`);
31
- }
32
- }
33
- (target.permissions as Record<string, unknown>).allow = Array.from(allowSet);
34
- (target.permissions as Record<string, unknown>).deny = Array.from(denySet);
35
- return {
36
- source: sourceLabel,
37
- mergedItems,
38
- timestamp: new Date().toISOString(),
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 = (target: Record<string, unknown>, source: Record<string, unknown>, sourceLabel: string): MergeHistoryEntry => {
43
- const mergedItems: string[] = [];
44
- const permEntry = mergePermissions(target, source, sourceLabel);
45
- mergedItems.push(...permEntry.mergedItems);
46
- const targetEnv = (target.env as Record<string, unknown> | undefined) ?? {};
47
- const sourceEnv = (source.env as Record<string, unknown> | undefined) ?? {};
48
- const nextEnv: Record<string, unknown> = { ...targetEnv };
49
- for (const [key, value] of Object.entries(sourceEnv)) {
50
- if (!(key in nextEnv)) {
51
- nextEnv[key] = value;
52
- mergedItems.push(`env:${key}`);
53
- }
54
- }
55
- if (Object.keys(nextEnv).length > 0) {
56
- target.env = nextEnv;
57
- }
58
- for (const [key, value] of Object.entries(source)) {
59
- if (key === "permissions" || key === "env") {
60
- continue;
61
- }
62
- if (!(key in target)) {
63
- target[key] = value;
64
- mergedItems.push(key);
65
- }
66
- }
67
- return {
68
- source: sourceLabel,
69
- mergedItems,
70
- timestamp: permEntry.timestamp,
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 = (target: Record<string, unknown>, history: MergeHistory, sourceLabel: string): MergeHistory => {
75
- const { allow, deny } = ensurePermissions(target);
76
- const allowSet = new Set(allow);
77
- const denySet = new Set(deny);
78
- const remainingHistory = history.filter((entry) => entry.source !== sourceLabel);
79
- const removedEntries = history.filter((entry) => entry.source === sourceLabel);
80
- for (const entry of removedEntries) {
81
- for (const item of entry.mergedItems) {
82
- if (item.startsWith("permissions.allow:")) {
83
- allowSet.delete(item.replace("permissions.allow:", ""));
84
- }
85
- if (item.startsWith("permissions.deny:")) {
86
- denySet.delete(item.replace("permissions.deny:", ""));
87
- }
88
- }
89
- }
90
- (target.permissions as Record<string, unknown>).allow = Array.from(allowSet);
91
- (target.permissions as Record<string, unknown>).deny = Array.from(denySet);
92
- return remainingHistory;
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 = (target: Record<string, unknown>, history: MergeHistory, sourceLabel: string): MergeHistory => {
96
- const remainingHistory = history.filter((entry) => entry.source !== sourceLabel);
97
- const removedEntries = history.filter((entry) => entry.source === sourceLabel);
98
- const targetEnv = (target.env as Record<string, unknown> | undefined) ?? {};
99
- for (const entry of removedEntries) {
100
- for (const item of entry.mergedItems) {
101
- if (item.startsWith("permissions.allow:") || item.startsWith("permissions.deny:")) {
102
- continue;
103
- }
104
- if (item.startsWith("env:")) {
105
- const key = item.replace("env:", "");
106
- delete targetEnv[key];
107
- continue;
108
- }
109
- delete target[item];
110
- }
111
- }
112
- if (Object.keys(targetEnv).length > 0) {
113
- target.env = targetEnv;
114
- } else {
115
- delete target.env;
116
- }
117
- return unmergePermissions(target, remainingHistory, sourceLabel);
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 = (contextsDir: string, contextName: string): string => {
121
- return path.join(contextsDir, `.${contextName}-merge-history.json`);
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 (contextsDir: string, contextName: string): Promise<MergeHistory> => {
125
- const filePath = historyPath(contextsDir, contextName);
126
- if (!existsSync(filePath)) {
127
- return [];
128
- }
129
- const text = await Bun.file(filePath).text();
130
- return JSON.parse(text) as MergeHistory;
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 (contextsDir: string, contextName: string, history: MergeHistory): Promise<void> => {
134
- const filePath = historyPath(contextsDir, contextName);
135
- await Bun.write(filePath, `${JSON.stringify(history, null, 2)}\n`);
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 = (contextName: string, history: MergeHistory): string => {
139
- if (history.length === 0) {
140
- return `No merge history for ${contextName}`;
141
- }
142
- const lines: string[] = [];
143
- lines.push(`📋 Merge history for context '${contextName}':`);
144
- lines.push("");
145
- for (const entry of history) {
146
- lines.push(` 📅 ${entry.timestamp}`);
147
- lines.push(` 📁 Source: ${entry.source}`);
148
- lines.push(` 📝 Merged ${entry.mergedItems.length} items`);
149
- lines.push("");
150
- }
151
- return lines.join("\n");
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: { local?: boolean; inProject?: boolean }): SettingsLevel => {
4
- if (opts.local) {
5
- return "local";
6
- }
7
- if (opts.inProject) {
8
- return "project";
9
- }
10
- return "user";
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
- return readJsonIfExists<State>(statePath, {});
5
+ return readJsonIfExists<State>(statePath, {});
6
6
  };
7
7
 
8
- export const saveState = async (statePath: string, state: State): Promise<void> => {
9
- await writeJson(statePath, state);
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
- const next: State = { ...state };
14
- if (next.current && next.current !== context) {
15
- next.previous = next.current;
16
- }
17
- next.current = context;
18
- return next;
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 = (state: State): { state: State; previous?: string } => {
22
- const next: State = { ...state };
23
- const current = next.current;
24
- delete next.current;
25
- if (current) {
26
- next.previous = current;
27
- }
28
- return { state: next, previous: current };
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
  };