rulesync-cli 0.1.4 → 0.1.9
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/dist/index.js +598 -54
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/login.ts
|
|
8
|
-
import {
|
|
8
|
+
import { createServer } from "http";
|
|
9
|
+
import { execFile } from "child_process";
|
|
9
10
|
|
|
10
11
|
// src/config.ts
|
|
11
12
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -30,38 +31,89 @@ async function loadAuthConfig() {
|
|
|
30
31
|
return JSON.parse(raw);
|
|
31
32
|
}
|
|
32
33
|
async function saveAuthConfig(config) {
|
|
33
|
-
const { mkdirSync } = await import("fs");
|
|
34
|
-
|
|
34
|
+
const { mkdirSync: mkdirSync2 } = await import("fs");
|
|
35
|
+
mkdirSync2(AUTH_DIR, { recursive: true });
|
|
35
36
|
writeFileSync(AUTH_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// src/commands/login.ts
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
40
|
+
var PROD_URL = "https://rulesync.dev";
|
|
41
|
+
function openBrowser(url) {
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
if (platform === "darwin") {
|
|
44
|
+
execFile("open", [url]);
|
|
45
|
+
} else if (platform === "win32") {
|
|
46
|
+
execFile("cmd", ["/c", "start", "", url]);
|
|
47
|
+
} else {
|
|
48
|
+
execFile("xdg-open", [url]);
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
|
-
async function login() {
|
|
51
|
+
async function login(options) {
|
|
52
|
+
const devPort = options.port || "3000";
|
|
53
|
+
const apiUrl = options.dev ? `http://localhost:${devPort}` : PROD_URL;
|
|
49
54
|
console.log("RuleSync Login\n");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
const apiUrl = await prompt("API URL [http://localhost:3000]: ");
|
|
56
|
-
await saveAuthConfig({
|
|
57
|
-
apiKey,
|
|
58
|
-
apiUrl: apiUrl || "http://localhost:3000"
|
|
59
|
-
});
|
|
55
|
+
console.log(`Authenticating with ${apiUrl}...
|
|
56
|
+
`);
|
|
57
|
+
const key = await waitForBrowserAuth(apiUrl);
|
|
58
|
+
await saveAuthConfig({ apiKey: key, apiUrl });
|
|
60
59
|
console.log("\nAuthenticated! Config saved to ~/.rulesync/auth.json");
|
|
61
60
|
}
|
|
61
|
+
function waitForBrowserAuth(apiUrl) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let timeout;
|
|
64
|
+
const server = createServer((req, res) => {
|
|
65
|
+
const url = new URL(req.url, `http://localhost`);
|
|
66
|
+
if (url.pathname === "/callback") {
|
|
67
|
+
const key = url.searchParams.get("key");
|
|
68
|
+
if (key) {
|
|
69
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
70
|
+
res.end(`
|
|
71
|
+
<html>
|
|
72
|
+
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #ededed;">
|
|
73
|
+
<div style="text-align: center;">
|
|
74
|
+
<h1 style="font-size: 1.25rem; margin-bottom: 0.5rem;">CLI Authorized</h1>
|
|
75
|
+
<p style="color: #888;">You can close this tab and return to the terminal.</p>
|
|
76
|
+
</div>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
79
|
+
`);
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
server.close();
|
|
82
|
+
resolve(key);
|
|
83
|
+
} else {
|
|
84
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
85
|
+
res.end("Missing key parameter");
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
res.writeHead(404);
|
|
90
|
+
res.end();
|
|
91
|
+
});
|
|
92
|
+
server.listen(0, () => {
|
|
93
|
+
const address = server.address();
|
|
94
|
+
if (!address || typeof address === "string") {
|
|
95
|
+
reject(new Error("Failed to start local server"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const port = address.port;
|
|
99
|
+
const authUrl = `${apiUrl}/cli/auth?callback_port=${port}`;
|
|
100
|
+
console.log(`Opening browser to authorize...
|
|
101
|
+
`);
|
|
102
|
+
console.log(`If the browser doesn't open, visit:
|
|
103
|
+
${authUrl}
|
|
104
|
+
`);
|
|
105
|
+
console.log("Waiting for authorization...");
|
|
106
|
+
openBrowser(authUrl);
|
|
107
|
+
});
|
|
108
|
+
timeout = setTimeout(() => {
|
|
109
|
+
server.close();
|
|
110
|
+
reject(new Error("Login timed out after 5 minutes"));
|
|
111
|
+
}, 5 * 60 * 1e3);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
62
114
|
|
|
63
115
|
// src/commands/init.ts
|
|
64
|
-
import { createInterface
|
|
116
|
+
import { createInterface } from "readline";
|
|
65
117
|
|
|
66
118
|
// src/api.ts
|
|
67
119
|
async function apiRequest(path, options = {}) {
|
|
@@ -86,8 +138,8 @@ async function apiRequest(path, options = {}) {
|
|
|
86
138
|
}
|
|
87
139
|
|
|
88
140
|
// src/commands/init.ts
|
|
89
|
-
async function
|
|
90
|
-
const rl =
|
|
141
|
+
async function prompt(question, defaultValue) {
|
|
142
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
143
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
92
144
|
return new Promise((resolve) => {
|
|
93
145
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
@@ -98,8 +150,8 @@ async function prompt2(question, defaultValue) {
|
|
|
98
150
|
}
|
|
99
151
|
async function init() {
|
|
100
152
|
console.log("RuleSync Init\n");
|
|
101
|
-
const projectName = await
|
|
102
|
-
const outputFile = await
|
|
153
|
+
const projectName = await prompt("Project name (optional, press Enter to skip)");
|
|
154
|
+
const outputFile = await prompt("Output file", "CLAUDE.md");
|
|
103
155
|
let rulesets = [];
|
|
104
156
|
try {
|
|
105
157
|
rulesets = await apiRequest("/api/rulesets");
|
|
@@ -112,7 +164,7 @@ async function init() {
|
|
|
112
164
|
rulesets.forEach((rs, i) => {
|
|
113
165
|
console.log(` ${i + 1}. ${rs.name} (${rs.slug})`);
|
|
114
166
|
});
|
|
115
|
-
const selection = await
|
|
167
|
+
const selection = await prompt(
|
|
116
168
|
"\nSelect rulesets (comma-separated numbers)"
|
|
117
169
|
);
|
|
118
170
|
if (selection) {
|
|
@@ -120,39 +172,531 @@ async function init() {
|
|
|
120
172
|
selectedSlugs = indices.filter((i) => i >= 0 && i < rulesets.length).map((i) => rulesets[i].slug);
|
|
121
173
|
}
|
|
122
174
|
}
|
|
123
|
-
|
|
124
|
-
project: projectName,
|
|
175
|
+
const config = {
|
|
125
176
|
output: outputFile,
|
|
126
177
|
rulesets: selectedSlugs
|
|
127
|
-
}
|
|
178
|
+
};
|
|
179
|
+
if (projectName) {
|
|
180
|
+
config.project = projectName;
|
|
181
|
+
}
|
|
182
|
+
await saveProjectConfig(config);
|
|
128
183
|
console.log(`
|
|
129
184
|
Created .rulesync.json`);
|
|
130
185
|
console.log('Run "rulesync-cli pull" to sync your rules file.');
|
|
131
186
|
}
|
|
132
187
|
|
|
133
188
|
// src/commands/pull.ts
|
|
134
|
-
import { writeFileSync as writeFileSync2 } from "fs";
|
|
135
|
-
import { join as join2 } from "path";
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
190
|
+
import { join as join2, dirname } from "path";
|
|
191
|
+
import { createInterface as createInterface2 } from "readline";
|
|
192
|
+
|
|
193
|
+
// ../../node_modules/.pnpm/diff@8.0.4/node_modules/diff/libesm/diff/base.js
|
|
194
|
+
var Diff = class {
|
|
195
|
+
diff(oldStr, newStr, options = {}) {
|
|
196
|
+
let callback;
|
|
197
|
+
if (typeof options === "function") {
|
|
198
|
+
callback = options;
|
|
199
|
+
options = {};
|
|
200
|
+
} else if ("callback" in options) {
|
|
201
|
+
callback = options.callback;
|
|
202
|
+
}
|
|
203
|
+
const oldString = this.castInput(oldStr, options);
|
|
204
|
+
const newString = this.castInput(newStr, options);
|
|
205
|
+
const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
|
|
206
|
+
const newTokens = this.removeEmpty(this.tokenize(newString, options));
|
|
207
|
+
return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
|
|
141
208
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
209
|
+
diffWithOptionsObj(oldTokens, newTokens, options, callback) {
|
|
210
|
+
var _a;
|
|
211
|
+
const done = (value) => {
|
|
212
|
+
value = this.postProcess(value, options);
|
|
213
|
+
if (callback) {
|
|
214
|
+
setTimeout(function() {
|
|
215
|
+
callback(value);
|
|
216
|
+
}, 0);
|
|
217
|
+
return void 0;
|
|
218
|
+
} else {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
223
|
+
let editLength = 1;
|
|
224
|
+
let maxEditLength = newLen + oldLen;
|
|
225
|
+
if (options.maxEditLength != null) {
|
|
226
|
+
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
227
|
+
}
|
|
228
|
+
const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
|
|
229
|
+
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
230
|
+
const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
|
|
231
|
+
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
232
|
+
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
233
|
+
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
234
|
+
}
|
|
235
|
+
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
236
|
+
const execEditLength = () => {
|
|
237
|
+
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
238
|
+
let basePath;
|
|
239
|
+
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
240
|
+
if (removePath) {
|
|
241
|
+
bestPath[diagonalPath - 1] = void 0;
|
|
242
|
+
}
|
|
243
|
+
let canAdd = false;
|
|
244
|
+
if (addPath) {
|
|
245
|
+
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
246
|
+
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
247
|
+
}
|
|
248
|
+
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
249
|
+
if (!canAdd && !canRemove) {
|
|
250
|
+
bestPath[diagonalPath] = void 0;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
254
|
+
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
255
|
+
} else {
|
|
256
|
+
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
257
|
+
}
|
|
258
|
+
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
259
|
+
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
260
|
+
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
261
|
+
} else {
|
|
262
|
+
bestPath[diagonalPath] = basePath;
|
|
263
|
+
if (basePath.oldPos + 1 >= oldLen) {
|
|
264
|
+
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
265
|
+
}
|
|
266
|
+
if (newPos + 1 >= newLen) {
|
|
267
|
+
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
editLength++;
|
|
272
|
+
};
|
|
273
|
+
if (callback) {
|
|
274
|
+
(function exec() {
|
|
275
|
+
setTimeout(function() {
|
|
276
|
+
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
277
|
+
return callback(void 0);
|
|
278
|
+
}
|
|
279
|
+
if (!execEditLength()) {
|
|
280
|
+
exec();
|
|
281
|
+
}
|
|
282
|
+
}, 0);
|
|
283
|
+
})();
|
|
284
|
+
} else {
|
|
285
|
+
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
286
|
+
const ret = execEditLength();
|
|
287
|
+
if (ret) {
|
|
288
|
+
return ret;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
addToPath(path, added, removed, oldPosInc, options) {
|
|
294
|
+
const last = path.lastComponent;
|
|
295
|
+
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
296
|
+
return {
|
|
297
|
+
oldPos: path.oldPos + oldPosInc,
|
|
298
|
+
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
299
|
+
};
|
|
300
|
+
} else {
|
|
301
|
+
return {
|
|
302
|
+
oldPos: path.oldPos + oldPosInc,
|
|
303
|
+
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
|
|
308
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
309
|
+
let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
|
310
|
+
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
|
|
311
|
+
newPos++;
|
|
312
|
+
oldPos++;
|
|
313
|
+
commonCount++;
|
|
314
|
+
if (options.oneChangePerToken) {
|
|
315
|
+
basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (commonCount && !options.oneChangePerToken) {
|
|
319
|
+
basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
320
|
+
}
|
|
321
|
+
basePath.oldPos = oldPos;
|
|
322
|
+
return newPos;
|
|
323
|
+
}
|
|
324
|
+
equals(left, right, options) {
|
|
325
|
+
if (options.comparator) {
|
|
326
|
+
return options.comparator(left, right);
|
|
327
|
+
} else {
|
|
328
|
+
return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
removeEmpty(array) {
|
|
332
|
+
const ret = [];
|
|
333
|
+
for (let i = 0; i < array.length; i++) {
|
|
334
|
+
if (array[i]) {
|
|
335
|
+
ret.push(array[i]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return ret;
|
|
339
|
+
}
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
341
|
+
castInput(value, options) {
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
345
|
+
tokenize(value, options) {
|
|
346
|
+
return Array.from(value);
|
|
347
|
+
}
|
|
348
|
+
join(chars) {
|
|
349
|
+
return chars.join("");
|
|
350
|
+
}
|
|
351
|
+
postProcess(changeObjects, options) {
|
|
352
|
+
return changeObjects;
|
|
353
|
+
}
|
|
354
|
+
get useLongestToken() {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
buildValues(lastComponent, newTokens, oldTokens) {
|
|
358
|
+
const components = [];
|
|
359
|
+
let nextComponent;
|
|
360
|
+
while (lastComponent) {
|
|
361
|
+
components.push(lastComponent);
|
|
362
|
+
nextComponent = lastComponent.previousComponent;
|
|
363
|
+
delete lastComponent.previousComponent;
|
|
364
|
+
lastComponent = nextComponent;
|
|
365
|
+
}
|
|
366
|
+
components.reverse();
|
|
367
|
+
const componentLen = components.length;
|
|
368
|
+
let componentPos = 0, newPos = 0, oldPos = 0;
|
|
369
|
+
for (; componentPos < componentLen; componentPos++) {
|
|
370
|
+
const component = components[componentPos];
|
|
371
|
+
if (!component.removed) {
|
|
372
|
+
if (!component.added && this.useLongestToken) {
|
|
373
|
+
let value = newTokens.slice(newPos, newPos + component.count);
|
|
374
|
+
value = value.map(function(value2, i) {
|
|
375
|
+
const oldValue = oldTokens[oldPos + i];
|
|
376
|
+
return oldValue.length > value2.length ? oldValue : value2;
|
|
377
|
+
});
|
|
378
|
+
component.value = this.join(value);
|
|
379
|
+
} else {
|
|
380
|
+
component.value = this.join(newTokens.slice(newPos, newPos + component.count));
|
|
381
|
+
}
|
|
382
|
+
newPos += component.count;
|
|
383
|
+
if (!component.added) {
|
|
384
|
+
oldPos += component.count;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
|
|
388
|
+
oldPos += component.count;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return components;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// ../../node_modules/.pnpm/diff@8.0.4/node_modules/diff/libesm/diff/line.js
|
|
396
|
+
var LineDiff = class extends Diff {
|
|
397
|
+
constructor() {
|
|
398
|
+
super(...arguments);
|
|
399
|
+
this.tokenize = tokenize;
|
|
400
|
+
}
|
|
401
|
+
equals(left, right, options) {
|
|
402
|
+
if (options.ignoreWhitespace) {
|
|
403
|
+
if (!options.newlineIsToken || !left.includes("\n")) {
|
|
404
|
+
left = left.trim();
|
|
405
|
+
}
|
|
406
|
+
if (!options.newlineIsToken || !right.includes("\n")) {
|
|
407
|
+
right = right.trim();
|
|
408
|
+
}
|
|
409
|
+
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
410
|
+
if (left.endsWith("\n")) {
|
|
411
|
+
left = left.slice(0, -1);
|
|
412
|
+
}
|
|
413
|
+
if (right.endsWith("\n")) {
|
|
414
|
+
right = right.slice(0, -1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return super.equals(left, right, options);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var lineDiff = new LineDiff();
|
|
421
|
+
function diffLines(oldStr, newStr, options) {
|
|
422
|
+
return lineDiff.diff(oldStr, newStr, options);
|
|
423
|
+
}
|
|
424
|
+
function tokenize(value, options) {
|
|
425
|
+
if (options.stripTrailingCr) {
|
|
426
|
+
value = value.replace(/\r\n/g, "\n");
|
|
427
|
+
}
|
|
428
|
+
const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
|
429
|
+
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
|
430
|
+
linesAndNewlines.pop();
|
|
431
|
+
}
|
|
432
|
+
for (let i = 0; i < linesAndNewlines.length; i++) {
|
|
433
|
+
const line = linesAndNewlines[i];
|
|
434
|
+
if (i % 2 && !options.newlineIsToken) {
|
|
435
|
+
retLines[retLines.length - 1] += line;
|
|
436
|
+
} else {
|
|
437
|
+
retLines.push(line);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return retLines;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ../../node_modules/.pnpm/diff@8.0.4/node_modules/diff/libesm/patch/create.js
|
|
444
|
+
function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
|
|
445
|
+
let optionsObj;
|
|
446
|
+
if (!options) {
|
|
447
|
+
optionsObj = {};
|
|
448
|
+
} else if (typeof options === "function") {
|
|
449
|
+
optionsObj = { callback: options };
|
|
450
|
+
} else {
|
|
451
|
+
optionsObj = options;
|
|
452
|
+
}
|
|
453
|
+
if (typeof optionsObj.context === "undefined") {
|
|
454
|
+
optionsObj.context = 4;
|
|
455
|
+
}
|
|
456
|
+
const context = optionsObj.context;
|
|
457
|
+
if (optionsObj.newlineIsToken) {
|
|
458
|
+
throw new Error("newlineIsToken may not be used with patch-generation functions, only with diffing functions");
|
|
459
|
+
}
|
|
460
|
+
if (!optionsObj.callback) {
|
|
461
|
+
return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj));
|
|
462
|
+
} else {
|
|
463
|
+
const { callback } = optionsObj;
|
|
464
|
+
diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => {
|
|
465
|
+
const patch = diffLinesResultToPatch(diff);
|
|
466
|
+
callback(patch);
|
|
467
|
+
} }));
|
|
468
|
+
}
|
|
469
|
+
function diffLinesResultToPatch(diff) {
|
|
470
|
+
if (!diff) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
diff.push({ value: "", lines: [] });
|
|
474
|
+
function contextLines(lines) {
|
|
475
|
+
return lines.map(function(entry) {
|
|
476
|
+
return " " + entry;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const hunks = [];
|
|
480
|
+
let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1;
|
|
481
|
+
for (let i = 0; i < diff.length; i++) {
|
|
482
|
+
const current = diff[i], lines = current.lines || splitLines(current.value);
|
|
483
|
+
current.lines = lines;
|
|
484
|
+
if (current.added || current.removed) {
|
|
485
|
+
if (!oldRangeStart) {
|
|
486
|
+
const prev = diff[i - 1];
|
|
487
|
+
oldRangeStart = oldLine;
|
|
488
|
+
newRangeStart = newLine;
|
|
489
|
+
if (prev) {
|
|
490
|
+
curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
|
|
491
|
+
oldRangeStart -= curRange.length;
|
|
492
|
+
newRangeStart -= curRange.length;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const line of lines) {
|
|
496
|
+
curRange.push((current.added ? "+" : "-") + line);
|
|
497
|
+
}
|
|
498
|
+
if (current.added) {
|
|
499
|
+
newLine += lines.length;
|
|
500
|
+
} else {
|
|
501
|
+
oldLine += lines.length;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
if (oldRangeStart) {
|
|
505
|
+
if (lines.length <= context * 2 && i < diff.length - 2) {
|
|
506
|
+
for (const line of contextLines(lines)) {
|
|
507
|
+
curRange.push(line);
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
const contextSize = Math.min(lines.length, context);
|
|
511
|
+
for (const line of contextLines(lines.slice(0, contextSize))) {
|
|
512
|
+
curRange.push(line);
|
|
513
|
+
}
|
|
514
|
+
const hunk = {
|
|
515
|
+
oldStart: oldRangeStart,
|
|
516
|
+
oldLines: oldLine - oldRangeStart + contextSize,
|
|
517
|
+
newStart: newRangeStart,
|
|
518
|
+
newLines: newLine - newRangeStart + contextSize,
|
|
519
|
+
lines: curRange
|
|
520
|
+
};
|
|
521
|
+
hunks.push(hunk);
|
|
522
|
+
oldRangeStart = 0;
|
|
523
|
+
newRangeStart = 0;
|
|
524
|
+
curRange = [];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
oldLine += lines.length;
|
|
528
|
+
newLine += lines.length;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const hunk of hunks) {
|
|
532
|
+
for (let i = 0; i < hunk.lines.length; i++) {
|
|
533
|
+
if (hunk.lines[i].endsWith("\n")) {
|
|
534
|
+
hunk.lines[i] = hunk.lines[i].slice(0, -1);
|
|
535
|
+
} else {
|
|
536
|
+
hunk.lines.splice(i + 1, 0, "\");
|
|
537
|
+
i++;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
oldFileName,
|
|
543
|
+
newFileName,
|
|
544
|
+
oldHeader,
|
|
545
|
+
newHeader,
|
|
546
|
+
hunks
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function splitLines(text) {
|
|
551
|
+
const hasTrailingNl = text.endsWith("\n");
|
|
552
|
+
const result = text.split("\n").map((line) => line + "\n");
|
|
553
|
+
if (hasTrailingNl) {
|
|
554
|
+
result.pop();
|
|
555
|
+
} else {
|
|
556
|
+
result.push(result.pop().slice(0, -1));
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/pull.ts
|
|
562
|
+
var RED = "\x1B[31m";
|
|
563
|
+
var GREEN = "\x1B[32m";
|
|
564
|
+
var CYAN = "\x1B[36m";
|
|
565
|
+
var DIM = "\x1B[2m";
|
|
566
|
+
var BOLD = "\x1B[1m";
|
|
567
|
+
var RESET = "\x1B[0m";
|
|
568
|
+
async function prompt2(question) {
|
|
569
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
570
|
+
return new Promise((resolve) => {
|
|
571
|
+
rl.question(question, (answer) => {
|
|
572
|
+
rl.close();
|
|
573
|
+
resolve(answer.trim().toLowerCase());
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function printDiff(filePath, oldContent, newContent) {
|
|
578
|
+
const patch = structuredPatch(filePath, filePath, oldContent, newContent, "current", "incoming");
|
|
579
|
+
for (const hunk of patch.hunks) {
|
|
580
|
+
console.log(`${CYAN}@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@${RESET}`);
|
|
581
|
+
for (const line of hunk.lines) {
|
|
582
|
+
if (line.startsWith("+")) {
|
|
583
|
+
console.log(`${GREEN}${line}${RESET}`);
|
|
584
|
+
} else if (line.startsWith("-")) {
|
|
585
|
+
console.log(`${RED}${line}${RESET}`);
|
|
586
|
+
} else {
|
|
587
|
+
console.log(`${DIM}${line}${RESET}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function promptAction(filePath, fileIndex, totalFiles) {
|
|
593
|
+
const label = `${DIM}[${fileIndex + 1}/${totalFiles}]${RESET}`;
|
|
594
|
+
const answer = await prompt2(
|
|
595
|
+
`${label} Apply changes to ${BOLD}${filePath}${RESET}? [${GREEN}y${RESET}]es / [${RED}n${RESET}]o / [${CYAN}a${RESET}]ll / [${RED}c${RESET}]ancel: `
|
|
151
596
|
);
|
|
597
|
+
switch (answer) {
|
|
598
|
+
case "y":
|
|
599
|
+
case "yes":
|
|
600
|
+
return "approve";
|
|
601
|
+
case "n":
|
|
602
|
+
case "no":
|
|
603
|
+
return "skip";
|
|
604
|
+
case "a":
|
|
605
|
+
case "all":
|
|
606
|
+
return "approve-all";
|
|
607
|
+
case "c":
|
|
608
|
+
case "cancel":
|
|
609
|
+
return "cancel";
|
|
610
|
+
default:
|
|
611
|
+
return "approve";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function writeWithApproval(files) {
|
|
615
|
+
let approveAll = false;
|
|
616
|
+
let written = 0;
|
|
617
|
+
let skipped = 0;
|
|
618
|
+
for (let i = 0; i < files.length; i++) {
|
|
619
|
+
const file = files[i];
|
|
620
|
+
const outputPath = join2(process.cwd(), file.path);
|
|
621
|
+
const oldContent = existsSync2(outputPath) ? readFileSync2(outputPath, "utf-8") : "";
|
|
622
|
+
const stripTimestamp = (s) => s.replace(/^<!-- Last synced: .* -->\n/m, "");
|
|
623
|
+
if (stripTimestamp(oldContent) === stripTimestamp(file.content)) {
|
|
624
|
+
console.log(`${DIM}${file.path} \u2014 no changes (${file.info})${RESET}`);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const isNew = !existsSync2(outputPath);
|
|
628
|
+
console.log(`
|
|
629
|
+
${BOLD}${isNew ? "New file" : "Modified"}: ${file.path}${RESET} (${file.info})`);
|
|
630
|
+
if (isNew) {
|
|
631
|
+
console.log(`${GREEN}+ ${file.content.split("\n").length} lines${RESET}`);
|
|
632
|
+
} else {
|
|
633
|
+
printDiff(file.path, oldContent, file.content);
|
|
634
|
+
}
|
|
635
|
+
if (!approveAll) {
|
|
636
|
+
const action = await promptAction(file.path, i, files.length);
|
|
637
|
+
if (action === "cancel") {
|
|
638
|
+
console.log(`
|
|
639
|
+
Cancelled. ${written} file(s) written, ${files.length - i} skipped.`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (action === "skip") {
|
|
643
|
+
skipped++;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (action === "approve-all") {
|
|
647
|
+
approveAll = true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
651
|
+
writeFileSync2(outputPath, file.content);
|
|
652
|
+
written++;
|
|
653
|
+
console.log(`${GREEN} \u2713 Wrote ${file.path}${RESET}`);
|
|
654
|
+
}
|
|
655
|
+
console.log(`
|
|
656
|
+
Done. ${written} written, ${skipped} skipped.`);
|
|
657
|
+
}
|
|
658
|
+
async function pull(options) {
|
|
659
|
+
const config = await loadProjectConfig();
|
|
660
|
+
let data;
|
|
661
|
+
if (config && config.rulesets.length > 0) {
|
|
662
|
+
console.log(`Fetching rulesets: ${config.rulesets.join(", ")}...`);
|
|
663
|
+
if (config.project) {
|
|
664
|
+
data = await apiRequest(`/api/sync/${encodeURIComponent(config.project)}`);
|
|
665
|
+
} else {
|
|
666
|
+
data = await apiRequest("/api/sync", {
|
|
667
|
+
method: "POST",
|
|
668
|
+
body: JSON.stringify({ rulesets: config.rulesets })
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
console.log("No .rulesync.json found \u2014 syncing all rulesets in your account...");
|
|
673
|
+
data = await apiRequest("/api/sync");
|
|
674
|
+
}
|
|
675
|
+
const filesToWrite = [];
|
|
676
|
+
if (data.files && data.files.length > 0) {
|
|
677
|
+
for (const file of data.files) {
|
|
678
|
+
const info = file.rulesets.map((r) => `${r.slug}@v${r.version}`).join(", ");
|
|
679
|
+
filesToWrite.push({ path: file.path, content: file.content, info });
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
const output = config?.output || "CLAUDE.md";
|
|
683
|
+
const info = data.rulesets.map((r) => `${r.slug}@v${r.version}`).join(", ");
|
|
684
|
+
filesToWrite.push({ path: output, content: data.content, info });
|
|
685
|
+
}
|
|
686
|
+
if (options.yes) {
|
|
687
|
+
for (const file of filesToWrite) {
|
|
688
|
+
const outputPath = join2(process.cwd(), file.path);
|
|
689
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
690
|
+
writeFileSync2(outputPath, file.content);
|
|
691
|
+
console.log(`Wrote ${file.path} (${file.info})`);
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
await writeWithApproval(filesToWrite);
|
|
695
|
+
}
|
|
152
696
|
}
|
|
153
697
|
|
|
154
698
|
// src/commands/push.ts
|
|
155
|
-
import { readFileSync as
|
|
699
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
156
700
|
import { join as join3 } from "path";
|
|
157
701
|
import { createInterface as createInterface3 } from "readline";
|
|
158
702
|
async function prompt3(question) {
|
|
@@ -171,11 +715,11 @@ async function push() {
|
|
|
171
715
|
process.exit(1);
|
|
172
716
|
}
|
|
173
717
|
const filePath = join3(process.cwd(), config.output);
|
|
174
|
-
if (!
|
|
718
|
+
if (!existsSync3(filePath)) {
|
|
175
719
|
console.error(`File not found: ${config.output}`);
|
|
176
720
|
process.exit(1);
|
|
177
721
|
}
|
|
178
|
-
const content =
|
|
722
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
179
723
|
const slug = await prompt3("Upload to ruleset slug: ");
|
|
180
724
|
if (!slug) {
|
|
181
725
|
console.error("Slug is required.");
|
|
@@ -210,12 +754,12 @@ async function status() {
|
|
|
210
754
|
}
|
|
211
755
|
|
|
212
756
|
// src/index.ts
|
|
213
|
-
var pkg = JSON.parse(
|
|
757
|
+
var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf-8"));
|
|
214
758
|
var program = new Command();
|
|
215
759
|
program.name("rulesync-cli").description("Sync CLAUDE.md files across repos via RuleSync").version(pkg.version);
|
|
216
|
-
program.command("login").description("Authenticate
|
|
760
|
+
program.command("login").description("Authenticate via browser login").option("--dev", "Use localhost instead of rulesync.dev").option("--port <port>", "Dev server port (default: 3000, requires --dev)").action(login);
|
|
217
761
|
program.command("init").description("Initialize .rulesync.json in the current project").action(init);
|
|
218
|
-
program.command("pull").description("Fetch rulesets and write the rules file").action(pull);
|
|
762
|
+
program.command("pull").description("Fetch rulesets and write the rules file").option("-y, --yes", "Skip interactive approval, write all files").action(pull);
|
|
219
763
|
program.command("push").description("Upload local rules file as a new ruleset version").action(push);
|
|
220
764
|
program.command("status").description("Show current config and auth status").action(status);
|
|
221
765
|
program.action(pull);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulesync-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "CLI to sync CLAUDE.md files across repos via RuleSync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
30
|
"prepublishOnly": "tsup",
|
|
31
|
-
"release": "npm version patch && npm publish --access public"
|
|
31
|
+
"release": "npm version patch --no-git-tag-version && npm publish --access public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"commander": "^13.0.0"
|
|
34
|
+
"commander": "^13.0.0",
|
|
35
|
+
"diff": "^8.0.4"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"tsup": "^8.0.0",
|