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.
Files changed (2) hide show
  1. package/dist/index.js +598 -54
  2. 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 readFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/login.ts
8
- import { createInterface } from "readline";
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
- mkdirSync(AUTH_DIR, { recursive: true });
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
- async function prompt(question) {
40
- const rl = createInterface({ input: process.stdin, output: process.stdout });
41
- return new Promise((resolve) => {
42
- rl.question(question, (answer) => {
43
- rl.close();
44
- resolve(answer.trim());
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
- const apiKey = await prompt("API Key (from Settings page): ");
51
- if (!apiKey.startsWith("rs_")) {
52
- console.error("Invalid key format. Keys start with rs_");
53
- process.exit(1);
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 as createInterface2 } from "readline";
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 prompt2(question, defaultValue) {
90
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
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 prompt2("Project name");
102
- const outputFile = await prompt2("Output file", "CLAUDE.md");
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 prompt2(
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
- await saveProjectConfig({
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
- async function pull() {
137
- const config = await loadProjectConfig();
138
- if (!config) {
139
- console.error("No .rulesync.json found. Run: rulesync-cli init");
140
- process.exit(1);
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
- console.log(
143
- `Fetching rulesets: ${config.rulesets.join(", ")}...`
144
- );
145
- const data = await apiRequest(`/api/sync/${encodeURIComponent(config.project)}`);
146
- const outputPath = join2(process.cwd(), config.output);
147
- writeFileSync2(outputPath, data.content);
148
- const rulesetInfo = data.rulesets.map((r) => `${r.slug}@v${r.version}`).join(", ");
149
- console.log(
150
- `Wrote ${config.output} (${data.rulesets.length} rulesets: ${rulesetInfo})`
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 readFileSync2, existsSync as existsSync2 } from "fs";
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 (!existsSync2(filePath)) {
718
+ if (!existsSync3(filePath)) {
175
719
  console.error(`File not found: ${config.output}`);
176
720
  process.exit(1);
177
721
  }
178
- const content = readFileSync2(filePath, "utf-8");
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(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
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 with your RuleSync API key").action(login);
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.4",
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",