claude-code-extensions 0.1.0 → 0.1.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.
Files changed (115) hide show
  1. package/dist/ast.d.ts +43 -0
  2. package/dist/ast.js +308 -0
  3. package/dist/ast.js.map +1 -0
  4. package/dist/cli-setup.d.ts +14 -0
  5. package/dist/cli-setup.js +110 -0
  6. package/dist/cli-setup.js.map +1 -0
  7. package/dist/cli.d.ts +18 -0
  8. package/dist/cli.js +218 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/patch-worker.d.ts +6 -0
  11. package/dist/patch-worker.js +27 -0
  12. package/dist/patch-worker.js.map +1 -0
  13. package/dist/patches/always-show-context.d.ts +23 -0
  14. package/dist/patches/always-show-context.js +97 -0
  15. package/dist/patches/always-show-context.js.map +1 -0
  16. package/dist/patches/always-show-thinking.d.ts +18 -0
  17. package/dist/patches/always-show-thinking.js +55 -0
  18. package/dist/patches/always-show-thinking.js.map +1 -0
  19. package/dist/patches/banner.d.ts +10 -0
  20. package/dist/patches/banner.js +60 -0
  21. package/dist/patches/banner.js.map +1 -0
  22. package/dist/patches/cd-command.d.ts +16 -0
  23. package/dist/patches/cd-command.js +89 -0
  24. package/dist/patches/cd-command.js.map +1 -0
  25. package/dist/patches/cx-badge.d.ts +10 -0
  26. package/dist/patches/cx-badge.js +115 -0
  27. package/dist/patches/cx-badge.js.map +1 -0
  28. package/dist/patches/cx-resume-commands.d.ts +14 -0
  29. package/dist/patches/cx-resume-commands.js +53 -0
  30. package/dist/patches/cx-resume-commands.js.map +1 -0
  31. package/dist/patches/disable-paste-collapse.d.ts +16 -0
  32. package/dist/patches/disable-paste-collapse.js +49 -0
  33. package/dist/patches/disable-paste-collapse.js.map +1 -0
  34. package/dist/patches/disable-telemetry.d.ts +13 -0
  35. package/dist/patches/disable-telemetry.js +76 -0
  36. package/dist/patches/disable-telemetry.js.map +1 -0
  37. package/{patches/index.js → dist/patches/index.d.ts} +4 -0
  38. package/dist/patches/index.js +22 -0
  39. package/dist/patches/index.js.map +1 -0
  40. package/dist/patches/no-attribution.d.ts +15 -0
  41. package/dist/patches/no-attribution.js +42 -0
  42. package/dist/patches/no-attribution.js.map +1 -0
  43. package/dist/patches/no-feedback.d.ts +12 -0
  44. package/dist/patches/no-feedback.js +31 -0
  45. package/dist/patches/no-feedback.js.map +1 -0
  46. package/dist/patches/no-npm-warning.d.ts +9 -0
  47. package/dist/patches/no-npm-warning.js +31 -0
  48. package/dist/patches/no-npm-warning.js.map +1 -0
  49. package/dist/patches/no-tips.d.ts +10 -0
  50. package/dist/patches/no-tips.js +26 -0
  51. package/dist/patches/no-tips.js.map +1 -0
  52. package/dist/patches/persist-max-effort.d.ts +15 -0
  53. package/dist/patches/persist-max-effort.js +75 -0
  54. package/dist/patches/persist-max-effort.js.map +1 -0
  55. package/dist/patches/queue.d.ts +10 -0
  56. package/dist/patches/queue.js +202 -0
  57. package/dist/patches/queue.js.map +1 -0
  58. package/dist/patches/random-clawd.d.ts +9 -0
  59. package/dist/patches/random-clawd.js +49 -0
  60. package/dist/patches/random-clawd.js.map +1 -0
  61. package/dist/patches/reload.d.ts +10 -0
  62. package/dist/patches/reload.js +50 -0
  63. package/dist/patches/reload.js.map +1 -0
  64. package/dist/patches/session-export.d.ts +16 -0
  65. package/dist/patches/session-export.js +93 -0
  66. package/dist/patches/session-export.js.map +1 -0
  67. package/dist/patches/session-timer.d.ts +18 -0
  68. package/dist/patches/session-timer.js +217 -0
  69. package/dist/patches/session-timer.js.map +1 -0
  70. package/dist/patches/show-file-in-collapsed-read.d.ts +17 -0
  71. package/dist/patches/show-file-in-collapsed-read.js +151 -0
  72. package/dist/patches/show-file-in-collapsed-read.js.map +1 -0
  73. package/dist/patches/simple-spinner.d.ts +12 -0
  74. package/dist/patches/simple-spinner.js +30 -0
  75. package/dist/patches/simple-spinner.js.map +1 -0
  76. package/dist/patches/swap-enter-submit.d.ts +26 -0
  77. package/dist/patches/swap-enter-submit.js +155 -0
  78. package/dist/patches/swap-enter-submit.js.map +1 -0
  79. package/dist/setup.d.ts +11 -0
  80. package/dist/setup.js +268 -0
  81. package/dist/setup.js.map +1 -0
  82. package/dist/transform-worker.d.ts +10 -0
  83. package/dist/transform-worker.js +35 -0
  84. package/dist/transform-worker.js.map +1 -0
  85. package/dist/transform.d.ts +12 -0
  86. package/dist/transform.js +83 -0
  87. package/dist/transform.js.map +1 -0
  88. package/dist/types.d.ts +105 -0
  89. package/dist/types.js +8 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +16 -11
  92. package/ast.js +0 -276
  93. package/cx +0 -228
  94. package/cx-setup +0 -101
  95. package/patch-worker.js +0 -31
  96. package/patches/always-show-context.js +0 -123
  97. package/patches/always-show-thinking.js +0 -65
  98. package/patches/banner.js +0 -58
  99. package/patches/cd-command.js +0 -104
  100. package/patches/cx-badge.js +0 -112
  101. package/patches/cx-resume-commands.js +0 -58
  102. package/patches/disable-paste-collapse.js +0 -52
  103. package/patches/disable-telemetry.js +0 -84
  104. package/patches/no-attribution.js +0 -55
  105. package/patches/no-npm-warning.js +0 -32
  106. package/patches/no-tips.js +0 -29
  107. package/patches/persist-max-effort.js +0 -70
  108. package/patches/queue.js +0 -215
  109. package/patches/random-clawd.js +0 -52
  110. package/patches/reload.js +0 -68
  111. package/patches/show-file-in-collapsed-read.js +0 -178
  112. package/patches/swap-enter-submit.js +0 -188
  113. package/setup.js +0 -222
  114. package/transform-worker.js +0 -38
  115. package/transform.js +0 -99
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Core type definitions for cx — Claude Code Extensions.
3
+ *
4
+ * All types used across the framework are defined here to avoid
5
+ * circular dependencies between modules.
6
+ */
7
+ import type { ASTIndex } from './ast.js';
8
+ /**
9
+ * Loose AST node type for patch authors.
10
+ * Extends acorn positional info with an index signature so ESTree
11
+ * properties (value, elements, properties, callee, etc.) are
12
+ * accessible without casts.
13
+ */
14
+ export interface ASTNode {
15
+ type: string;
16
+ start: number;
17
+ end: number;
18
+ [key: string]: any;
19
+ }
20
+ /** Possible values of a JS Literal node. */
21
+ export type LiteralValue = string | number | boolean | null | RegExp;
22
+ export interface SourceEdit {
23
+ pos: number;
24
+ deleteCount: number;
25
+ text: string;
26
+ }
27
+ /** Minimal editor interface exposed to patches. */
28
+ export interface PatchEditor {
29
+ insertAt(pos: number, text: string): void;
30
+ replaceRange(start: number, end: number, text: string): void;
31
+ }
32
+ export interface FindHelpers {
33
+ findFirst(root: ASTNode, predicate: (node: ASTNode) => boolean): ASTNode | null;
34
+ findAll(root: ASTNode, predicate: (node: ASTNode) => boolean): ASTNode[];
35
+ walkAST: (node: ASTNode) => Generator<ASTNode>;
36
+ }
37
+ export interface QueryHelpers {
38
+ findArrayWithConsecutiveStrings(root: ASTNode, str1: string, str2: string): ASTNode | null;
39
+ findObjectWithStringProps(root: ASTNode, propPairs: [string, string][]): ASTNode | null;
40
+ findHookCallWithObjectKeys(root: ASTNode, hookName: string, keys: string[]): ASTNode | null;
41
+ findFunctionsContainingStrings(root: ASTNode, ...strings: string[]): ASTNode[];
42
+ getDestructuredName(objPattern: ASTNode, propKey: string): string | null;
43
+ }
44
+ export interface PatchContext {
45
+ ast: ASTNode;
46
+ source: string;
47
+ editor: PatchEditor;
48
+ index: ASTIndex;
49
+ find: FindHelpers;
50
+ query: QueryHelpers;
51
+ src(node: ASTNode): string;
52
+ assert(condition: unknown, message: string): void;
53
+ getFunctionName(fn: ASTNode): string | null;
54
+ }
55
+ export interface Patch {
56
+ id: string;
57
+ name: string;
58
+ description: string;
59
+ defaultEnabled?: boolean;
60
+ apply(ctx: PatchContext): void;
61
+ }
62
+ export interface PatchInfo {
63
+ id: string;
64
+ name: string;
65
+ description: string;
66
+ defaultEnabled?: boolean;
67
+ }
68
+ export interface WorkerData {
69
+ source: string;
70
+ patchIds: string[];
71
+ patchesDir: string;
72
+ }
73
+ export interface PatchWorkerData {
74
+ source: string;
75
+ patchId: string;
76
+ patchesDir: string;
77
+ }
78
+ export type WorkerMessage = {
79
+ type: 'ready';
80
+ } | {
81
+ type: 'done';
82
+ id: string;
83
+ } | {
84
+ type: 'complete';
85
+ patched: string;
86
+ } | {
87
+ type: 'error';
88
+ error: string;
89
+ };
90
+ export type PatchWorkerMessage = {
91
+ type: 'ready';
92
+ } | {
93
+ type: 'done';
94
+ edits: SourceEdit[];
95
+ } | {
96
+ type: 'error';
97
+ error: string;
98
+ };
99
+ export interface CxConfig {
100
+ patches: Record<string, boolean>;
101
+ }
102
+ export interface TransformCallbacks {
103
+ onReady?(): void;
104
+ onDone?(id: string): void;
105
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Core type definitions for cx — Claude Code Extensions.
3
+ *
4
+ * All types used across the framework are defined here to avoid
5
+ * circular dependencies between modules.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
package/package.json CHANGED
@@ -1,25 +1,26 @@
1
1
  {
2
2
  "name": "claude-code-extensions",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Modular, opt-in patches for @anthropic-ai/claude-code applied at runtime via AST transformation",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cx": "./cx",
8
- "cx-setup": "./cx-setup"
7
+ "cx": "./dist/cli.js",
8
+ "cx-setup": "./dist/cli-setup.js"
9
9
  },
10
10
  "files": [
11
- "cx",
12
- "cx-setup",
13
- "ast.js",
14
- "transform.js",
15
- "transform-worker.js",
16
- "patch-worker.js",
17
- "setup.js",
18
- "patches/"
11
+ "dist/"
19
12
  ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
20
17
  "dependencies": {
21
18
  "acorn": "^8.16.0"
22
19
  },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.7.0"
23
+ },
23
24
  "peerDependencies": {
24
25
  "@anthropic-ai/claude-code": "*"
25
26
  },
@@ -34,6 +35,10 @@
34
35
  "extensions",
35
36
  "patches"
36
37
  ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/magidandrew/cx"
41
+ },
37
42
  "license": "MIT",
38
43
  "engines": {
39
44
  "node": ">=18"
package/ast.js DELETED
@@ -1,276 +0,0 @@
1
- /**
2
- * AST indexing and query helpers for cx patches.
3
- *
4
- * ASTIndex builds lookup tables in a single walk so that subsequent
5
- * queries are O(matches) instead of O(all nodes). The generator-based
6
- * walkAST is retained for backward compat — some patches iterate it
7
- * directly over subtrees.
8
- */
9
-
10
- // ═══════════════════════════════════════════════════════════════════════════
11
- // Walk (generator, for direct use by patches)
12
- // ═══════════════════════════════════════════════════════════════════════════
13
-
14
- export function* walkAST(node) {
15
- if (!node || typeof node !== 'object') return;
16
- if (node.type) yield node;
17
- for (const key of Object.keys(node)) {
18
- if (key === 'type' || key === 'start' || key === 'end' || key === 'raw') continue;
19
- const child = node[key];
20
- if (Array.isArray(child)) {
21
- for (const item of child) {
22
- if (item && typeof item === 'object' && item.type) yield* walkAST(item);
23
- }
24
- } else if (child && typeof child === 'object' && child.type) {
25
- yield* walkAST(child);
26
- }
27
- }
28
- }
29
-
30
- // ═══════════════════════════════════════════════════════════════════════════
31
- // Index
32
- // ═══════════════════════════════════════════════════════════════════════════
33
-
34
- export class ASTIndex {
35
- constructor(ast) {
36
- this.ast = ast;
37
- this.nodesByType = new Map();
38
- this.literalsByValue = new Map();
39
- this.parentMap = new WeakMap();
40
- /** Pre-order flat list — sorted by start position. */
41
- this.allNodes = [];
42
- this._build(ast, null);
43
- }
44
-
45
- _build(node, parent) {
46
- if (!node || typeof node !== 'object' || !node.type) return;
47
-
48
- this.allNodes.push(node);
49
- if (parent) this.parentMap.set(node, parent);
50
-
51
- let byType = this.nodesByType.get(node.type);
52
- if (!byType) { byType = []; this.nodesByType.set(node.type, byType); }
53
- byType.push(node);
54
-
55
- if (node.type === 'Literal' && node.value != null) {
56
- let byVal = this.literalsByValue.get(node.value);
57
- if (!byVal) { byVal = []; this.literalsByValue.set(node.value, byVal); }
58
- byVal.push(node);
59
- }
60
-
61
- for (const key of Object.keys(node)) {
62
- if (key === 'type' || key === 'start' || key === 'end' || key === 'raw') continue;
63
- const child = node[key];
64
- if (Array.isArray(child)) {
65
- for (const item of child) this._build(item, node);
66
- } else {
67
- this._build(child, node);
68
- }
69
- }
70
- }
71
-
72
- // ── Generic queries ────────────────────────────────────────────────────
73
-
74
- findFirst(root, predicate) {
75
- if (!root || root === this.ast) {
76
- for (const n of this.allNodes) if (predicate(n)) return n;
77
- return null;
78
- }
79
- const lo = this._lowerBound(root.start);
80
- for (let i = lo; i < this.allNodes.length; i++) {
81
- const n = this.allNodes[i];
82
- if (n.start >= root.end) break;
83
- if (n.end <= root.end && predicate(n)) return n;
84
- }
85
- return null;
86
- }
87
-
88
- findAll(root, predicate) {
89
- const results = [];
90
- if (!root || root === this.ast) {
91
- for (const n of this.allNodes) if (predicate(n)) results.push(n);
92
- return results;
93
- }
94
- const lo = this._lowerBound(root.start);
95
- for (let i = lo; i < this.allNodes.length; i++) {
96
- const n = this.allNodes[i];
97
- if (n.start >= root.end) break;
98
- if (n.end <= root.end && predicate(n)) results.push(n);
99
- }
100
- return results;
101
- }
102
-
103
- // ── Specialized queries (indexed) ──────────────────────────────────────
104
-
105
- findArrayWithConsecutiveStrings(root, str1, str2) {
106
- for (const lit of this._inRange(this.literalsByValue.get(str1), root)) {
107
- const parent = this.parentMap.get(lit);
108
- if (!parent || parent.type !== 'ArrayExpression') continue;
109
- const idx = parent.elements.indexOf(lit);
110
- if (idx >= 0 && idx < parent.elements.length - 1) {
111
- const next = parent.elements[idx + 1];
112
- if (next?.type === 'Literal' && next.value === str2) return parent;
113
- }
114
- }
115
- return null;
116
- }
117
-
118
- findObjectWithStringProps(root, propPairs) {
119
- let bestVal = propPairs[0][1], bestCount = Infinity;
120
- for (const [, v] of propPairs) {
121
- const c = (this.literalsByValue.get(v) || []).length;
122
- if (c < bestCount) { bestVal = v; bestCount = c; }
123
- }
124
- for (const lit of this._inRange(this.literalsByValue.get(bestVal), root)) {
125
- let obj = this.parentMap.get(lit);
126
- if (obj?.type === 'Property') obj = this.parentMap.get(obj);
127
- if (!obj || obj.type !== 'ObjectExpression') continue;
128
- if (propPairs.every(([key, value]) =>
129
- obj.properties.some(prop => {
130
- if (prop.type !== 'Property') return false;
131
- const kMatch = (prop.key.type === 'Identifier' && prop.key.name === key) ||
132
- (prop.key.type === 'Literal' && prop.key.value === key);
133
- return kMatch && prop.value.type === 'Literal' && prop.value.value === value;
134
- })
135
- )) return obj;
136
- }
137
- return null;
138
- }
139
-
140
- findHookCallWithObjectKeys(root, hookName, keys) {
141
- for (const node of this._inRange(this.nodesByType.get('CallExpression'), root)) {
142
- const c = node.callee;
143
- if (c.type !== 'MemberExpression' || c.property.name !== hookName) continue;
144
- const firstArg = node.arguments[0];
145
- if (!firstArg) continue;
146
- for (const obj of this._inRange(this.nodesByType.get('ObjectExpression'), firstArg)) {
147
- if (keys.every(k => obj.properties.some(p =>
148
- p.type === 'Property' &&
149
- ((p.key.type === 'Literal' && p.key.value === k) || (p.key.type === 'Identifier' && p.key.name === k))
150
- ))) return node;
151
- }
152
- }
153
- return null;
154
- }
155
-
156
- findFunctionsContainingStrings(root, ...strings) {
157
- let rarest = strings[0];
158
- let rarestCount = (this.literalsByValue.get(rarest) || []).length;
159
- for (let i = 1; i < strings.length; i++) {
160
- const count = (this.literalsByValue.get(strings[i]) || []).length;
161
- if (count < rarestCount) { rarest = strings[i]; rarestCount = count; }
162
- }
163
- const seen = new Set();
164
- const results = [];
165
- for (const lit of this._inRange(this.literalsByValue.get(rarest), root)) {
166
- const fn = this.enclosingFunction(lit);
167
- if (!fn || seen.has(fn)) continue;
168
- seen.add(fn);
169
- if (strings.every(s =>
170
- (this.literalsByValue.get(s) || []).some(l => l.start >= fn.start && l.end <= fn.end)
171
- )) results.push(fn);
172
- }
173
- return results;
174
- }
175
-
176
- getDestructuredName(objPattern, propKey) {
177
- for (const prop of objPattern.properties) {
178
- if (prop.type === 'RestElement') continue;
179
- const k = prop.key;
180
- if ((k.type === 'Identifier' && k.name === propKey) || (k.type === 'Literal' && k.value === propKey)) {
181
- if (prop.value.type === 'Identifier') return prop.value.name;
182
- if (prop.value.type === 'AssignmentPattern' && prop.value.left.type === 'Identifier') return prop.value.left.name;
183
- }
184
- }
185
- return null;
186
- }
187
-
188
- // ── Internal helpers ───────────────────────────────────────────────────
189
-
190
- _inRange(nodes, root) {
191
- if (!nodes) return [];
192
- if (!root || root === this.ast) return nodes;
193
- return nodes.filter(n => n.start >= root.start && n.end <= root.end);
194
- }
195
-
196
- _lowerBound(target) {
197
- let lo = 0, hi = this.allNodes.length;
198
- while (lo < hi) {
199
- const mid = (lo + hi) >> 1;
200
- if (this.allNodes[mid].start < target) lo = mid + 1; else hi = mid;
201
- }
202
- return lo;
203
- }
204
-
205
- /** Walk up the parent chain to find the nearest ancestor of a given type. */
206
- ancestor(node, type) {
207
- let current = this.parentMap.get(node);
208
- while (current) {
209
- if (current.type === type) return current;
210
- current = this.parentMap.get(current);
211
- }
212
- return null;
213
- }
214
-
215
- enclosingFunction(node) {
216
- let current = this.parentMap.get(node);
217
- while (current) {
218
- if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression' || current.type === 'ArrowFunctionExpression') return current;
219
- current = this.parentMap.get(current);
220
- }
221
- return null;
222
- }
223
- }
224
-
225
- // ═══════════════════════════════════════════════════════════════════════════
226
- // Source Editor
227
- // ═══════════════════════════════════════════════════════════════════════════
228
-
229
- export class SourceEditor {
230
- constructor() { this.edits = []; }
231
- insertAt(pos, text) { this.edits.push({ pos, deleteCount: 0, text }); }
232
- replaceRange(start, end, text) { this.edits.push({ pos: start, deleteCount: end - start, text }); }
233
- apply(src) {
234
- const sorted = [...this.edits].sort((a, b) => b.pos - a.pos);
235
- let result = src;
236
- for (const edit of sorted) {
237
- result = result.slice(0, edit.pos) + edit.text + result.slice(edit.pos + edit.deleteCount);
238
- }
239
- return result;
240
- }
241
- }
242
-
243
- /**
244
- * Build the standard patch context from source + AST index + editor.
245
- * Shared by transform.js (sequential) and patch-worker.js (parallel).
246
- */
247
- export function buildContext(source, index, editor) {
248
- return {
249
- ast: index.ast,
250
- source,
251
- editor,
252
- index,
253
- find: {
254
- findFirst: (root, pred) => index.findFirst(root, pred),
255
- findAll: (root, pred) => index.findAll(root, pred),
256
- walkAST,
257
- },
258
- query: {
259
- findArrayWithConsecutiveStrings: (root, s1, s2) => index.findArrayWithConsecutiveStrings(root, s1, s2),
260
- findObjectWithStringProps: (root, pp) => index.findObjectWithStringProps(root, pp),
261
- findHookCallWithObjectKeys: (root, hn, keys) => index.findHookCallWithObjectKeys(root, hn, keys),
262
- findFunctionsContainingStrings: (root, ...strings) => index.findFunctionsContainingStrings(root, ...strings),
263
- getDestructuredName: (obj, key) => index.getDestructuredName(obj, key),
264
- },
265
- src: node => source.slice(node.start, node.end),
266
- assert(cond, msg) {
267
- if (!cond) throw new Error(`transform: ${msg}`);
268
- },
269
- getFunctionName(fn) {
270
- if (fn.type === 'FunctionDeclaration' && fn.id) return fn.id.name;
271
- const before = source.slice(Math.max(0, fn.start - 100), fn.start);
272
- const m = before.match(/(?:^|[,;{}\s])(\w+)\s*=\s*$/);
273
- return m ? m[1] : null;
274
- },
275
- };
276
- }
package/cx DELETED
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * cx — Claude Code Extensions
4
- *
5
- * Drop-in replacement for `claude`. Applies enabled patches at runtime
6
- * via AST transformation. The original cli.js is never modified.
7
- * All arguments pass through to claude untouched.
8
- *
9
- * Subcommands:
10
- * cx setup — interactive patch configurator
11
- * cx list — show patch status
12
- * cx reload — signal a running cx instance to reload Claude
13
- * cx [args] — run patched Claude (with auto-reload support)
14
- *
15
- * Reload: type `! cx reload` inside Claude to restart the session
16
- * with fresh patches applied. The conversation resumes via --continue.
17
- */
18
-
19
- import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, unlinkSync } from 'fs';
20
- import { execSync, spawn as nodeSpawn } from 'child_process';
21
- import { resolve, dirname } from 'path';
22
- import { fileURLToPath } from 'url';
23
- import { transformAsync, listPatches } from './transform.js';
24
-
25
- const __dirname = dirname(fileURLToPath(import.meta.url));
26
- const CONFIG_PATH = resolve(__dirname, '.cx-patches.json');
27
- const cacheDir = resolve(__dirname, '.cache');
28
- const cachedCliPath = resolve(cacheDir, 'cli.mjs');
29
- const metaPath = resolve(cacheDir, 'meta.json');
30
- const PID_FILE = resolve(__dirname, '.cx-pid');
31
- const RELOAD_EXIT_CODE = 75;
32
-
33
- // ── Subcommands (fast path, before any heavy work) ───────────────────────
34
-
35
- const sub = process.argv[2];
36
-
37
- if (sub === 'reload') {
38
- try {
39
- const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
40
- process.kill(pid, 'SIGUSR1');
41
- process.stderr.write('\x1b[2mcx: reload signal sent\x1b[0m\n');
42
- } catch (e) {
43
- const msg = e.code === 'ENOENT' ? 'no cx instance running'
44
- : e.code === 'ESRCH' ? 'cx process not running'
45
- : e.message;
46
- process.stderr.write(`cx reload: ${msg}\n`);
47
- process.exit(1);
48
- }
49
- process.exit(0);
50
- }
51
-
52
- // ── Config ───────────────────────────────────────────────────────────────
53
-
54
- function loadConfig() {
55
- if (!existsSync(CONFIG_PATH)) return null;
56
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
57
- }
58
-
59
- function getEnabledPatches() {
60
- const config = loadConfig();
61
- const all = listPatches();
62
- if (!config?.patches) return all.map(p => p.id);
63
- return all.filter(p => {
64
- if (p.id in config.patches) return config.patches[p.id] !== false;
65
- return p.defaultEnabled !== false;
66
- }).map(p => p.id);
67
- }
68
-
69
- if (sub === 'list') {
70
- const all = listPatches();
71
- const enabled = getEnabledPatches();
72
- for (const p of all) {
73
- const on = enabled.includes(p.id);
74
- process.stdout.write(` ${on ? '\x1b[32m✓\x1b[0m' : '\x1b[90m✗\x1b[0m'} ${p.id} — ${p.description ?? p.name}\n`);
75
- }
76
- process.exit(0);
77
- }
78
-
79
- if (sub === 'setup') {
80
- await import('./setup.js');
81
- process.exit(0);
82
- }
83
-
84
- // ── First run ────────────────────────────────────────────────────────────
85
-
86
- if (!existsSync(CONFIG_PATH)) {
87
- process.stderr.write('\x1b[2mcx: first run — all patches enabled. run cx setup to configure.\x1b[0m\n');
88
- }
89
-
90
- // ── Locate cli.js ────────────────────────────────────────────────────────
91
-
92
- let cliPath;
93
- try {
94
- const npmRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
95
- cliPath = resolve(npmRoot, '@anthropic-ai/claude-code/cli.js');
96
- } catch { /* fallthrough */ }
97
-
98
- if (!cliPath || !existsSync(cliPath)) {
99
- console.error('cx: could not find @anthropic-ai/claude-code. Install it:');
100
- console.error(' npm install -g @anthropic-ai/claude-code');
101
- process.exit(1);
102
- }
103
-
104
- // ── Cache ────────────────────────────────────────────────────────────────
105
-
106
- async function ensureCache() {
107
- const enabled = getEnabledPatches();
108
- const stat = statSync(cliPath);
109
- const key = `${stat.size}:${stat.mtimeMs}:${[...enabled].sort().join(',')}`;
110
-
111
- let valid = false;
112
- if (existsSync(cachedCliPath) && existsSync(metaPath)) {
113
- try { valid = JSON.parse(readFileSync(metaPath, 'utf-8')).key === key; } catch {}
114
- }
115
-
116
- if (!valid) {
117
- const t0 = performance.now();
118
- const total = enabled.length;
119
-
120
- // Line 0: prepare with elapsed timer, Lines 1..N: patch checklist
121
- process.stderr.write(`\x1b[2m ◇ preparing (0s)\x1b[0m\n`);
122
- for (const id of enabled) {
123
- process.stderr.write(`\x1b[2m ◻ ${id}\x1b[0m\n`);
124
- }
125
-
126
- const timer = setInterval(() => {
127
- const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
128
- const up = total + 1;
129
- process.stderr.write(`\x1b[${up}A\r\x1b[2m ◇ preparing (${elapsed}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
130
- }, 100);
131
-
132
- const original = readFileSync(cliPath, 'utf-8');
133
- const patched = await transformAsync(original, enabled, {
134
- onReady() {
135
- clearInterval(timer);
136
- const up = total + 1;
137
- const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
138
- process.stderr.write(`\x1b[${up}A\r\x1b[2m ◇ ready (${elapsed}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
139
- },
140
- onDone(id) {
141
- const idx = enabled.indexOf(id);
142
- const up = total - idx;
143
- process.stderr.write(`\x1b[${up}A\r\x1b[32m ✔ ${id}\x1b[0m\x1b[K\x1b[${up}B\r`);
144
- },
145
- });
146
-
147
- // Replace prepare line with summary
148
- const up = total + 1;
149
- process.stderr.write(`\x1b[${up}A\r\x1b[2m ◆ ${total} patches applied (${((performance.now() - t0) / 1000).toFixed(1)}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
150
-
151
- mkdirSync(cacheDir, { recursive: true });
152
- writeFileSync(cachedCliPath, patched);
153
- writeFileSync(metaPath, JSON.stringify({ key, ts: new Date().toISOString() }));
154
- }
155
- }
156
-
157
- // ── PID file ─────────────────────────────────────────────────────────────
158
-
159
- writeFileSync(PID_FILE, String(process.pid));
160
- function cleanPid() { try { unlinkSync(PID_FILE); } catch {} }
161
- process.on('exit', cleanPid);
162
-
163
- // ── Reload loop ──────────────────────────────────────────────────────────
164
-
165
- let child = null;
166
- let shouldReload = false;
167
-
168
- process.on('SIGUSR1', () => {
169
- shouldReload = true;
170
- if (child) child.kill('SIGTERM');
171
- });
172
-
173
- // Let child handle Ctrl+C — don't let it kill the wrapper
174
- process.on('SIGINT', () => {
175
- if (!child) { cleanPid(); process.exit(130); }
176
- });
177
-
178
- process.on('SIGTERM', () => {
179
- if (child) child.kill('SIGTERM');
180
- cleanPid();
181
- process.exit(143);
182
- });
183
-
184
- /**
185
- * Build args for a reload: strip --continue/--resume, prepend --continue.
186
- */
187
- function reloadArgs(original) {
188
- const skip = new Set(['--continue', '-c', '--resume', '-r']);
189
- const result = ['--continue'];
190
- for (let i = 0; i < original.length; i++) {
191
- if (skip.has(original[i])) {
192
- // --resume/-r may have an optional value; skip it too
193
- if ((original[i] === '--resume' || original[i] === '-r') &&
194
- i + 1 < original.length && !original[i + 1].startsWith('-')) {
195
- i++;
196
- }
197
- continue;
198
- }
199
- result.push(original[i]);
200
- }
201
- return result;
202
- }
203
-
204
- const userArgs = process.argv.slice(2);
205
- let isReload = false;
206
-
207
- while (true) {
208
- await ensureCache();
209
-
210
- const args = isReload ? reloadArgs(userArgs) : userArgs;
211
-
212
- child = nodeSpawn(process.execPath, [cachedCliPath, ...args], {
213
- stdio: 'inherit',
214
- env: process.env,
215
- });
216
-
217
- const code = await new Promise(r => child.on('close', (c) => r(c)));
218
- child = null;
219
-
220
- if (shouldReload || code === RELOAD_EXIT_CODE) {
221
- shouldReload = false;
222
- isReload = true;
223
- process.stderr.write('\n\x1b[2mcx: reloading…\x1b[0m\n');
224
- continue;
225
- }
226
-
227
- process.exit(code ?? 0);
228
- }