authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-10 11:03:15-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-14 16:16:58-07:00
log37665e69e1aa954f0dec06b1cafa1612f44dc907
tree435b25807511511480e5b32067421f6a7531b327
parentd829e8623111cc251687f6d54d79199455b4c98a
signature Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ

clover: tidy the config remove unused things


42 files changed, 746 insertions(+), 3361 deletions(-)

files/vscode-simple-browser-bridge/extension.js deleted-23
...@@ -1,23 +0,0 @@
1const vscode = require("vscode");
2
3// Authority of the handled uri must equal this extension's identifier
4// (publisher.name); see package.json. Opening
5// vscode://clover.simple-browser-bridge/open?url=<encodeURIComponent(url)>
6// (e.g. via `code --open-url`, as the companion `open` shim does) shows the
7// target in the built-in Simple Browser instead of the system browser.
8function activate(context) {
9 context.subscriptions.push(
10 vscode.window.registerUriHandler({
11 handleUri(uri) {
12 const url = new URLSearchParams(uri.query).get("url");
13 if (url) {
14 vscode.commands.executeCommand("simpleBrowser.show", url);
15 }
16 },
17 }),
18 );
19}
20
21function deactivate() {}
22
23module.exports = { activate, deactivate };
files/vscode-simple-browser-bridge/package.json deleted-15
...@@ -1,15 +0,0 @@
1{
2 "name": "simple-browser-bridge",
3 "displayName": "Simple Browser Bridge",
4 "description": "Routes terminal `open <url>` into the built-in Simple Browser via a vscode:// uri handler.",
5 "publisher": "clover",
6 "version": "0.0.1",
7 "engines": {
8 "vscode": "^1.74.0"
9 },
10 "main": "./extension.js",
11 "activationEvents": [
12 "onUri"
13 ],
14 "contributes": {}
15}
flake.nix-3
...@@ -78,11 +78,9 @@...@@ -78,11 +78,9 @@
78 # custom packages78 # custom packages
79 (_: pkgs: {79 (_: pkgs: {
80 autofmt = pkgs.callPackage ./packages/autofmt { };80 autofmt = pkgs.callPackage ./packages/autofmt { };
81 "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { };
82 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };81 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };
83 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };82 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };
84 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };83 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };
85 open-in-vscode-browser = pkgs.callPackage ./packages/open-in-vscode-browser { };
86 })84 })
87 ];85 ];
8886
...@@ -114,7 +112,6 @@...@@ -114,7 +112,6 @@
114 nvim-natalie = mkNeovim "natalie" pkgs;112 nvim-natalie = mkNeovim "natalie" pkgs;
115113
116 inherit (pkgs) autofmt;114 inherit (pkgs) autofmt;
117 "jj-import-merge" = pkgs."jj-import-merge";
118 }115 }
119 // lib.optionalAttrs (system == "aarch64-darwin") {116 // lib.optionalAttrs (system == "aarch64-darwin") {
120 # "nix run .#darwin-rebuild"117 # "nix run .#darwin-rebuild"
modules/macos/mac-app-store.nix+2
...@@ -14,10 +14,12 @@ let...@@ -14,10 +14,12 @@ let
14 allMasApps = {14 allMasApps = {
15 # sort-lines:start15 # sort-lines:start
16 adguard = 1440147259;16 adguard = 1440147259;
17 amphetamine = 937984704;
17 final-cut-pro = 424389933;18 final-cut-pro = 424389933;
18 logic-pro = 634148309;19 logic-pro = 634148309;
19 magnet = 441258766;20 magnet = 441258766;
20 motion = 434290957;21 motion = 434290957;
22 runcat = 1429033973;
21 wireguard = 1451685025;23 wireguard = 1451685025;
22 # sort-lines:end24 # sort-lines:end
23 };25 };
packages/jj-import-merge/default.nix deleted-9
...@@ -1,9 +0,0 @@
1{ pkgs, ... }:
2pkgs.writeShellApplication {
3 name = "jj-import-merge";
4 runtimeInputs = with pkgs; [
5 autofmt
6 nodejs
7 ];
8 text = ''exec ${pkgs.nodejs}/bin/node ${./jj-import-merge.js} "$@"'';
9}
packages/jj-import-merge/jj-import-merge.js deleted-900
...@@ -1,900 +0,0 @@
1#!/usr/bin/env node
2// jj-import-merge
3// Merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts.
4
5import * as child_process from "node:child_process";
6import * as fs from "node:fs";
7import * as path from "node:path";
8import process from "node:process";
9
10const SUPPORTED_EXTENSIONS = new Set([
11 ".cjs",
12 ".cts",
13 ".js",
14 ".jsx",
15 ".mjs",
16 ".mts",
17 ".ts",
18 ".tsx",
19]);
20
21const CONFLICT_START_RE = /^<<<<<<< conflict \d+ of \d+/m;
22const CONFLICT_END_RE = /^>>>>>>> conflict \d+ of \d+ ends$/m;
23
24main();
25
26function main() {
27 const options = parseArgs(process.argv.slice(2));
28 let files = options.files.map((file) => path.resolve(file));
29
30 if (options.allJj) {
31 files.push(...listJjConflictFiles().map((file) => path.resolve(file)));
32 }
33
34 files = [...new Set(files)];
35 if (files.length === 0) {
36 usage(1);
37 }
38
39 let failed = false;
40 for (const file of files) {
41 try {
42 const result = processFile(file);
43 if (result.kind === "formatted") {
44 console.info(`formatted ${path.relative(process.cwd(), file)}`);
45 } else if (result.kind === "rewritten") {
46 console.info(`rewrote ${path.relative(process.cwd(), file)}`);
47 }
48 } catch (error) {
49 failed = true;
50 const message = error instanceof Error ? error.message : String(error);
51 console.error(`error: ${message}`);
52 }
53 }
54
55 process.exit(failed ? 1 : 0);
56}
57
58function parseArgs(argv) {
59 const options = {
60 allJj: false,
61 files: [],
62 };
63
64 while (argv.length > 0) {
65 const arg = argv.shift();
66 if (arg === "-h" || arg === "--help") {
67 usage(0);
68 } else if (arg === "--all-jj") {
69 options.allJj = true;
70 } else if (arg === "--") {
71 options.files.push(...argv);
72 break;
73 } else if (arg.startsWith("-")) {
74 console.error(`error: unknown option ${JSON.stringify(arg)}`);
75 usage(1);
76 } else {
77 options.files.push(arg);
78 }
79 }
80
81 return options;
82}
83
84function usage(code) {
85 const exe = "jj-import-merge";
86 const stream = code === 0 ? process.stdout : process.stderr;
87 stream.write(`usage: ${exe} [--all-jj] [--] <file>...\n`);
88 stream.write(`\n`);
89 stream.write(
90 `merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts\n`,
91 );
92 stream.write(`\n`);
93 stream.write(`options:\n`);
94 stream.write(
95 ` --all-jj resolve all conflicted JS/TS files from 'jj resolve --list'\n`,
96 );
97 stream.write(` -h show help\n`);
98 process.exit(code);
99}
100
101function listJjConflictFiles() {
102 const result = child_process.spawnSync(
103 "jj",
104 ["--color=never", "--quiet", "resolve", "--list"],
105 { encoding: "utf8" },
106 );
107
108 if (result.error) {
109 throw new Error(`failed to run jj: ${result.error.message}`);
110 }
111 if (result.status !== 0) {
112 throw new Error(result.stderr.trim() || "jj resolve --list failed");
113 }
114
115 return result.stdout
116 .split(/\r?\n/)
117 .map((line) => line.trimEnd())
118 .filter(Boolean)
119 .map((line) => line.match(/^(.*?)\s{2,}.+conflict$/))
120 .filter(Boolean)
121 .map((match) => match[1]);
122}
123
124function processFile(file) {
125 if (!SUPPORTED_EXTENSIONS.has(path.extname(file))) {
126 return { kind: "noop" };
127 }
128
129 let original;
130 try {
131 original = fs.readFileSync(file, "utf8");
132 } catch (error) {
133 const message = error instanceof Error ? error.message : String(error);
134 throw new Error(`cannot read ${file}: ${message}`);
135 }
136
137 if (!CONFLICT_START_RE.test(original)) {
138 return { kind: "noop" };
139 }
140
141 const parsed = parseConflictFile(original);
142 if (!parsed.supported) {
143 return { kind: "noop" };
144 }
145
146 const leftText = parsed.parts.map((part) => part.a).join("");
147 const rightText = parsed.parts.map((part) => part.b).join("");
148 const leftImports = extractTopImportBlock(leftText);
149 const rightImports = extractTopImportBlock(rightText);
150 if (!leftImports || !rightImports) {
151 return { kind: "noop" };
152 }
153
154 const leftPrelude = leftText.slice(0, leftImports.start);
155 const rightPrelude = rightText.slice(0, rightImports.start);
156 if (leftPrelude !== rightPrelude) {
157 return { kind: "noop" };
158 }
159
160 const start = projectBoundaryToOriginal(
161 parsed.parts,
162 leftImports.start,
163 rightImports.start,
164 );
165 const end = projectBoundaryToOriginal(
166 parsed.parts,
167 leftImports.end,
168 rightImports.end,
169 );
170 if (start == null || end == null || end < start) {
171 return { kind: "noop" };
172 }
173
174 const mergedImports = mergeImportStatements(
175 leftImports.statements,
176 rightImports.statements,
177 detectNewline(original),
178 );
179 const updated = original.slice(0, start) + mergedImports +
180 original.slice(end);
181
182 if (updated === original) {
183 return { kind: "noop" };
184 }
185
186 try {
187 fs.writeFileSync(file, updated, "utf8");
188 } catch (error) {
189 const message = error instanceof Error ? error.message : String(error);
190 throw new Error(`cannot write ${file}: ${message}`);
191 }
192
193 if (hasConflictMarkers(updated)) {
194 return { kind: "rewritten" };
195 }
196
197 const formatResult = child_process.spawnSync("autofmt", [file], {
198 encoding: "utf8",
199 stdio: "inherit",
200 });
201
202 if (formatResult.error || formatResult.status !== 0) {
203 fs.writeFileSync(file, original, "utf8");
204 if (formatResult.error) {
205 throw new Error(
206 `autofmt failed for ${file}: ${formatResult.error.message}`,
207 );
208 }
209 throw new Error(`autofmt failed for ${file}`);
210 }
211
212 return { kind: "formatted" };
213}
214
215function parseConflictFile(text) {
216 const lines = splitLines(text);
217 const parts = [];
218
219 let index = 0;
220 while (index < lines.length) {
221 const line = lines[index].text;
222 if (!line.startsWith("<<<<<<< conflict ")) {
223 const start = index;
224 index += 1;
225 while (
226 index < lines.length &&
227 !lines[index].text.startsWith("<<<<<<< conflict ")
228 ) {
229 index += 1;
230 }
231 const raw = joinLines(lines, start, index);
232 parts.push({ type: "plain", orig: raw, a: raw, b: raw });
233 continue;
234 }
235
236 const block = parseConflictBlock(lines, index);
237 if (!block.supported) {
238 return { supported: false, parts: [] };
239 }
240 parts.push(block.part);
241 index = block.nextIndex;
242 }
243
244 return { supported: true, parts };
245}
246
247function parseConflictBlock(lines, startIndex) {
248 let index = startIndex + 1;
249 const entries = [];
250
251 while (index < lines.length) {
252 const line = lines[index].text;
253 if (line.startsWith(">>>>>>> conflict ")) {
254 break;
255 }
256 if (line.startsWith("+++++++ ")) {
257 const contentStart = index + 1;
258 index += 1;
259 while (
260 index < lines.length &&
261 !isConflictEntryStart(lines[index].text) &&
262 !lines[index].text.startsWith(">>>>>>> conflict ")
263 ) {
264 index += 1;
265 }
266 entries.push({
267 kind: "explicit",
268 text: joinLines(lines, contentStart, index),
269 });
270 continue;
271 }
272 if (line.startsWith("%%%%%%% diff from:")) {
273 if (
274 index + 1 >= lines.length || !/^\\\\+\s*to:/.test(lines[index + 1].text)
275 ) {
276 return { supported: false, nextIndex: startIndex + 1 };
277 }
278 index += 2;
279 let text = "";
280 while (
281 index < lines.length &&
282 !isConflictEntryStart(lines[index].text) &&
283 !lines[index].text.startsWith(">>>>>>> conflict ")
284 ) {
285 const diffLine = lines[index].text;
286 const prefix = diffLine[0];
287 if (prefix === " " || prefix === "+") {
288 text += diffLine.slice(1);
289 } else if (prefix !== "-") {
290 return { supported: false, nextIndex: startIndex + 1 };
291 }
292 index += 1;
293 }
294 entries.push({ kind: "diff", text });
295 continue;
296 }
297 return { supported: false, nextIndex: startIndex + 1 };
298 }
299
300 if (
301 index >= lines.length ||
302 !lines[index].text.startsWith(">>>>>>> conflict ") ||
303 entries.length !== 2
304 ) {
305 return { supported: false, nextIndex: startIndex + 1 };
306 }
307
308 return {
309 supported: true,
310 nextIndex: index + 1,
311 part: {
312 type: "conflict",
313 orig: joinLines(lines, startIndex, index + 1),
314 a: entries[0].text,
315 b: entries[1].text,
316 },
317 };
318}
319
320function isConflictEntryStart(line) {
321 return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:");
322}
323
324function splitLines(text) {
325 const lines = [];
326 let start = 0;
327
328 for (let index = 0; index < text.length; index += 1) {
329 if (text[index] !== "\n") continue;
330 lines.push({
331 start,
332 end: index + 1,
333 text: text.slice(start, index + 1),
334 });
335 start = index + 1;
336 }
337
338 if (start < text.length) {
339 lines.push({ start, end: text.length, text: text.slice(start) });
340 }
341
342 return lines;
343}
344
345function joinLines(lines, start, end) {
346 let text = "";
347 for (let index = start; index < end; index += 1) {
348 text += lines[index].text;
349 }
350 return text;
351}
352
353function extractTopImportBlock(text) {
354 const lines = splitLines(text);
355 if (lines.length === 0) return null;
356
357 let index = 0;
358 if (lines[0]?.text.startsWith("#!")) {
359 index += 1;
360 }
361
362 index = skipPrelude(lines, index);
363 if (index == null || index >= lines.length) {
364 return null;
365 }
366
367 const first = parseImportStatement(lines, index);
368 if (!first) {
369 return null;
370 }
371
372 const start = lines[index].start;
373 const statements = [first.statement];
374 let end = offsetForLine(lines, first.nextIndex);
375 index = first.nextIndex;
376
377 while (true) {
378 const afterTrivia = skipBlankAndComments(lines, index);
379 if (afterTrivia == null) {
380 return null;
381 }
382
383 const next = parseImportStatement(lines, afterTrivia);
384 if (!next) {
385 break;
386 }
387
388 statements.push(next.statement);
389 end = offsetForLine(lines, next.nextIndex);
390 index = next.nextIndex;
391 }
392
393 return { start, end, statements };
394}
395
396function skipPrelude(lines, startIndex) {
397 let index = startIndex;
398 while (index < lines.length) {
399 const afterTrivia = skipBlankAndComments(lines, index);
400 if (afterTrivia == null) {
401 return null;
402 }
403 if (afterTrivia >= lines.length) {
404 return afterTrivia;
405 }
406
407 const trimmed = lines[afterTrivia].text.trim();
408 if (isDirectiveLine(trimmed)) {
409 index = afterTrivia + 1;
410 continue;
411 }
412 return afterTrivia;
413 }
414 return index;
415}
416
417function skipBlankAndComments(lines, startIndex) {
418 let index = startIndex;
419
420 while (index < lines.length) {
421 const trimmed = lines[index].text.trim();
422 if (trimmed === "") {
423 index += 1;
424 continue;
425 }
426 if (trimmed.startsWith("//")) {
427 index += 1;
428 continue;
429 }
430 if (trimmed.startsWith("/*")) {
431 index = skipBlockComment(lines, index);
432 if (index == null) {
433 return null;
434 }
435 continue;
436 }
437 break;
438 }
439
440 return index;
441}
442
443function skipBlockComment(lines, startIndex) {
444 for (let index = startIndex; index < lines.length; index += 1) {
445 if (lines[index].text.includes("*/")) {
446 return index + 1;
447 }
448 }
449 return null;
450}
451
452function isDirectiveLine(line) {
453 return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line);
454}
455
456function parseImportStatement(lines, startIndex) {
457 if (startIndex >= lines.length) {
458 return null;
459 }
460
461 const firstLine = lines[startIndex].text.trimStart();
462 if (!firstLine.startsWith("import")) {
463 return null;
464 }
465 if (/^import(?:\s*\(|\s*\.)/.test(firstLine)) {
466 return null;
467 }
468
469 let raw = "";
470 const state = {
471 braceDepth: 0,
472 inBlockComment: false,
473 stringQuote: null,
474 };
475
476 for (let index = startIndex; index < lines.length; index += 1) {
477 raw += lines[index].text;
478 scanLine(lines[index].text, state);
479
480 if (state.inBlockComment || state.braceDepth !== 0) {
481 continue;
482 }
483
484 const analysis = parseImportSemantics(raw);
485 if (analysis) {
486 return {
487 nextIndex: index + 1,
488 statement: raw,
489 };
490 }
491 }
492
493 return null;
494}
495
496function scanLine(line, state) {
497 for (let index = 0; index < line.length; index += 1) {
498 const char = line[index];
499 const next = line[index + 1];
500
501 if (state.inBlockComment) {
502 if (char === "*" && next === "/") {
503 state.inBlockComment = false;
504 index += 1;
505 }
506 continue;
507 }
508
509 if (state.stringQuote) {
510 if (char === "\\") {
511 index += 1;
512 continue;
513 }
514 if (char === state.stringQuote) {
515 state.stringQuote = null;
516 }
517 continue;
518 }
519
520 if (char === "/" && next === "/") {
521 break;
522 }
523 if (char === "/" && next === "*") {
524 state.inBlockComment = true;
525 index += 1;
526 continue;
527 }
528 if (char === "'" || char === '"' || char === "`") {
529 state.stringQuote = char;
530 continue;
531 }
532 if (char === "{") {
533 state.braceDepth += 1;
534 } else if (char === "}") {
535 state.braceDepth = Math.max(0, state.braceDepth - 1);
536 }
537 }
538}
539
540function parseImportSemantics(raw) {
541 const normalized = collapseWhitespace(raw);
542 if (!normalized.startsWith("import ")) {
543 return null;
544 }
545
546 const sideEffectMatch = normalized.match(
547 /^import\s+(["'])(.*?)\1(\s+with\s+\{.*\})?\s*;?$/,
548 );
549 if (sideEffectMatch) {
550 return {
551 kind: "side-effect",
552 source: sideEffectMatch[2],
553 quote: sideEffectMatch[1],
554 attributes: sideEffectMatch[3] ?? "",
555 key: `side:${sideEffectMatch[2]}:${sideEffectMatch[3] ?? ""}`,
556 };
557 }
558
559 const bindingMatch = normalized.match(
560 /^import\s+(type\s+)?(.+?)\s+from\s+(["'])(.*?)\3(\s+with\s+\{.*\})?\s*;?$/,
561 );
562 if (!bindingMatch) {
563 return null;
564 }
565
566 const parsed = parseBindingSpec(bindingMatch[2].trim());
567 if (!parsed) {
568 return null;
569 }
570
571 const named = parsed.namedSpecifiers.join(",");
572 return {
573 kind: "binding",
574 isTypeImport: !!bindingMatch[1],
575 defaultBinding: parsed.defaultBinding,
576 namespaceImport: parsed.namespaceImport,
577 namedSpecifiers: parsed.namedSpecifiers,
578 source: bindingMatch[4],
579 quote: bindingMatch[3],
580 attributes: bindingMatch[5] ?? "",
581 key: [
582 "binding",
583 bindingMatch[1] ? "type" : "value",
584 parsed.defaultBinding ?? "",
585 parsed.namespaceImport ?? "",
586 named,
587 bindingMatch[4],
588 bindingMatch[5] ?? "",
589 ].join(":"),
590 };
591}
592
593function parseBindingSpec(specPart) {
594 let defaultBinding = null;
595 let namespaceImport = null;
596 let namedSpecifiers = [];
597
598 const braceStart = specPart.indexOf("{");
599 const braceEnd = specPart.lastIndexOf("}");
600 if (braceStart !== -1 || braceEnd !== -1) {
601 if (braceStart === -1 || braceEnd === -1 || braceEnd < braceStart) {
602 return null;
603 }
604
605 const beforeBrace = specPart
606 .slice(0, braceStart)
607 .trim()
608 .replace(/,\s*$/, "")
609 .trim();
610 const afterBrace = specPart.slice(braceEnd + 1).trim();
611 if (afterBrace !== "") {
612 return null;
613 }
614
615 const head = parseBindingHead(beforeBrace);
616 if (!head) {
617 return null;
618 }
619 defaultBinding = head.defaultBinding;
620 namespaceImport = head.namespaceImport;
621 namedSpecifiers = splitCommaList(specPart.slice(braceStart + 1, braceEnd));
622 } else {
623 const head = parseBindingHead(specPart);
624 if (!head) {
625 return null;
626 }
627 defaultBinding = head.defaultBinding;
628 namespaceImport = head.namespaceImport;
629 }
630
631 return {
632 defaultBinding,
633 namespaceImport,
634 namedSpecifiers,
635 };
636}
637
638function parseBindingHead(head) {
639 if (head === "") {
640 return { defaultBinding: null, namespaceImport: null };
641 }
642
643 if (head.startsWith("* as ")) {
644 return { defaultBinding: null, namespaceImport: head };
645 }
646
647 const parts = head.split(/\s*,\s*/).filter(Boolean);
648 if (parts.length === 1) {
649 return {
650 defaultBinding: parts[0],
651 namespaceImport: null,
652 };
653 }
654
655 if (parts.length === 2 && parts[1].startsWith("* as ")) {
656 return {
657 defaultBinding: parts[0],
658 namespaceImport: parts[1],
659 };
660 }
661
662 return null;
663}
664
665function splitCommaList(text) {
666 return text
667 .split(",")
668 .map((part) => collapseWhitespace(part))
669 .filter(Boolean);
670}
671
672function mergeImportStatements(leftStatements, rightStatements, newline) {
673 const merged = [];
674
675 for (const statement of [...leftStatements, ...rightStatements]) {
676 const analysis = parseImportSemantics(statement);
677 if (!analysis) {
678 const key = `raw:${collapseWhitespace(statement)}`;
679 if (!merged.some((entry) => entry.key === key)) {
680 merged.push({
681 key,
682 raw: ensureTrailingNewline(statement.trim(), newline),
683 analysis: null,
684 });
685 }
686 continue;
687 }
688
689 const exactIndex = merged.findIndex((entry) => entry.key === analysis.key);
690 if (exactIndex !== -1) {
691 continue;
692 }
693
694 const mergeIndex = merged.findIndex(
695 (entry) => entry.analysis && canMergeImports(entry.analysis, analysis),
696 );
697 if (mergeIndex !== -1) {
698 const nextAnalysis = mergeImports(merged[mergeIndex].analysis, analysis);
699 merged[mergeIndex] = {
700 key: nextAnalysis.key,
701 raw: renderImport(nextAnalysis, newline),
702 analysis: nextAnalysis,
703 };
704 continue;
705 }
706
707 merged.push({
708 key: analysis.key,
709 raw: renderImport(analysis, newline),
710 analysis,
711 });
712 }
713
714 return merged
715 .map((entry) => entry.raw.trimEnd())
716 .join(newline) + newline;
717}
718
719function canMergeImports(left, right) {
720 if (left.kind !== "binding" || right.kind !== "binding") {
721 return false;
722 }
723 if (
724 left.source !== right.source ||
725 left.attributes !== right.attributes ||
726 left.isTypeImport !== right.isTypeImport
727 ) {
728 return false;
729 }
730
731 const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding);
732 const namespaceImport = mergeField(
733 left.namespaceImport,
734 right.namespaceImport,
735 );
736 if (defaultBinding === undefined || namespaceImport === undefined) {
737 return false;
738 }
739
740 const namedSpecifiers = unique([
741 ...left.namedSpecifiers,
742 ...right.namedSpecifiers,
743 ]);
744 if (namespaceImport && namedSpecifiers.length > 0) {
745 return false;
746 }
747
748 return true;
749}
750
751function mergeImports(left, right) {
752 const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding);
753 const namespaceImport = mergeField(
754 left.namespaceImport,
755 right.namespaceImport,
756 );
757 const namedSpecifiers = unique([
758 ...left.namedSpecifiers,
759 ...right.namedSpecifiers,
760 ]);
761
762 return {
763 kind: "binding",
764 isTypeImport: left.isTypeImport,
765 defaultBinding,
766 namespaceImport,
767 namedSpecifiers,
768 source: left.source,
769 quote: left.quote,
770 attributes: left.attributes,
771 key: [
772 "binding",
773 left.isTypeImport ? "type" : "value",
774 defaultBinding ?? "",
775 namespaceImport ?? "",
776 namedSpecifiers.join(","),
777 left.source,
778 left.attributes,
779 ].join(":"),
780 };
781}
782
783function mergeField(left, right) {
784 if (left == null) return right ?? null;
785 if (right == null) return left;
786 if (left === right) return left;
787 return undefined;
788}
789
790function renderImport(analysis, newline) {
791 if (analysis.kind === "side-effect") {
792 return ensureTrailingNewline(
793 `import ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`,
794 newline,
795 );
796 }
797
798 const pieces = [];
799 if (analysis.defaultBinding) {
800 pieces.push(analysis.defaultBinding);
801 }
802 if (analysis.namespaceImport) {
803 pieces.push(analysis.namespaceImport);
804 }
805 if (analysis.namedSpecifiers.length > 0) {
806 if (analysis.namedSpecifiers.length === 1) {
807 pieces.push(`{ ${analysis.namedSpecifiers[0]} }`);
808 } else {
809 pieces.push(
810 `{${newline}${
811 analysis.namedSpecifiers.map((spec) => ` ${spec},`).join(newline)
812 }${newline}}`,
813 );
814 }
815 }
816
817 return ensureTrailingNewline(
818 `import ${analysis.isTypeImport ? "type " : ""}${
819 pieces.join(", ")
820 } from ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`,
821 newline,
822 );
823}
824
825function projectBoundaryToOriginal(parts, targetA, targetB) {
826 let offsetA = 0;
827 let offsetB = 0;
828 let offsetOrig = 0;
829
830 if (targetA === 0 && targetB === 0) {
831 return 0;
832 }
833
834 for (const part of parts) {
835 if (targetA === offsetA && targetB === offsetB) {
836 return offsetOrig;
837 }
838
839 const nextA = offsetA + part.a.length;
840 const nextB = offsetB + part.b.length;
841 const nextOrig = offsetOrig + part.orig.length;
842
843 if (part.type === "plain") {
844 if (
845 targetA >= offsetA &&
846 targetA <= nextA &&
847 targetB >= offsetB &&
848 targetB <= nextB
849 ) {
850 const deltaA = targetA - offsetA;
851 const deltaB = targetB - offsetB;
852 if (deltaA !== deltaB) {
853 return null;
854 }
855 return offsetOrig + deltaA;
856 }
857 }
858
859 if (targetA === nextA && targetB === nextB) {
860 return nextOrig;
861 }
862
863 offsetA = nextA;
864 offsetB = nextB;
865 offsetOrig = nextOrig;
866 }
867
868 if (targetA === offsetA && targetB === offsetB) {
869 return offsetOrig;
870 }
871
872 return null;
873}
874
875function hasConflictMarkers(text) {
876 return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text);
877}
878
879function detectNewline(text) {
880 return text.includes("\r\n") ? "\r\n" : "\n";
881}
882
883function ensureTrailingNewline(text, newline) {
884 return text.endsWith(newline) ? text : `${text}${newline}`;
885}
886
887function offsetForLine(lines, lineIndex) {
888 if (lineIndex >= lines.length) {
889 return lines.length === 0 ? 0 : lines[lines.length - 1].end;
890 }
891 return lines[lineIndex].start;
892}
893
894function collapseWhitespace(text) {
895 return text.replace(/\s+/g, " ").trim();
896}
897
898function unique(values) {
899 return [...new Set(values)];
900}
packages/open-in-vscode-browser/default.nix deleted-28
...@@ -1,28 +0,0 @@
1{ pkgs, ... }:
2# Drop-in replacement for macOS open(1) that, inside a VS Code integrated
3# terminal, routes http(s) URLs into the built-in Simple Browser via the
4# companion `simple-browser-bridge` extension's uri handler. Every other
5# invocation (non-vscode terminal, files, apps, no `code` on PATH) falls
6# through to the real /usr/bin/open with identical semantics.
7#
8# Named `open` so it shadows the system binary on PATH and is therefore
9# honored by programs that spawn `open` directly (e.g. a dev TUI's
10# "open browser" action), not just interactive shells.
11pkgs.writeShellApplication {
12 name = "open";
13 runtimeInputs = with pkgs; [ nodejs ];
14 text = ''
15 if [ "''${TERM_PROGRAM:-}" = "vscode" ] && command -v code >/dev/null 2>&1; then
16 for arg in "$@"; do
17 case "$arg" in
18 http://* | https://*)
19 encoded=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$arg")
20 code --open-url "vscode://clover.simple-browser-bridge/open?url=$encoded"
21 exit 0
22 ;;
23 esac
24 done
25 fi
26 exec /usr/bin/open "$@"
27 '';
28}
users/clover/.gitignore created+1
...@@ -0,0 +1 @@
1!AGENTS.md
\ No newline at end of file
users/clover/AGENTS.md deleted-126
...@@ -1,126 +0,0 @@
1## Code Quality
2
3Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or
4type unless it removes real duplication or encodes a meaningful invariant.
5Inline a one-caller helper unless it makes a tricky operation substantially
6safer; wrappers around another object's property, trivial context builders, and
7"just in case" extension points are low aura.
8
9Hunt dead code and fake state while editing. A value derived from another source
10should be derived, not stored. A field written only to appease a type and never
11read gets nuked. A cast must constrain real runtime behavior; one that survives
12is localized and justified by an actual API boundary.
13
14Before calling a change done, do a cleanup pass: fewer names, branches, casts,
15public API promises. Ship the smaller locked-in version. Thinking, running, and
16verifying longer to land a correct answer is expected -- "should work" is a
17hypothesis, and say so.
18
19### Comments
20
21Default to no comment; names, types, and structure carry the meaning. One earns
22its place only for a non-obvious "why", an invariant invisible from the local
23code, or a gotcha that will mislead the next reader -- never to restate the code
24or narrate mechanism a reader can follow line by line.
25
26Keep an earned comment to a line, timeless, and rarely first person (no `I`,
27`we`). Prefer documentation comments on declarations. History, deployment
28topology, and cross-file mechanism belong in the commit or the linked issue. Cut
29every clause a competent reader would already infer.
30
31## Output Preferences
32
33Write for an engineer who already knows the mechanism. A figure (pseudocode,
34component tree, ascii diagram) beats the paragraph that would have described it,
35and stands alone -- no sentence after it re-describing what it shows.
36
37**Bold** and _italic_ each render as their own color; use either freely for
38emphasis. _**Bold italic**_ renders red and means warning: a consequence I will
39regret missing, never mere emphasis. Usually zero per message, never two.
40
41- At most one paragraph, plus a figure where one earns its place. A second
42 paragraph means the first did not pick the load-bearing point: choose again,
43 do not append. Six lines is the ceiling; three dense lines beat six padded.
44- Answer the question, not the topic around it. Withholding true and relevant
45 detail is correct, not lazy -- depth is a follow-up I will ask for.
46- These are limits on the report, never on the work. Investigate exhaustively,
47 then say the smallest true thing.
48- Cut every word whose deletion leaves the meaning intact: no hedges
49 ("essentially", "worth noting"), no scaffolding ("that said", "importantly",
50 "in practice"), no restating my question. Open on the payload.
51- Before a tool call: one line, or nothing. After launching a sub-agent:
52 nothing, the transcript already shows what started.
53- No sentence whose subject is you or your process -- no "I looked", "my pass",
54 "let me".
55- A problem you found and fixed goes last, in one line, unemphasized. Same for
56 anything a reviewer or sub-agent caught.
57- Never list undone work or name "the gaps". That is a question for me, or it
58 should have been part of the task.
59- A clause after "rather than" or ", not" must name a real alternative you
60 considered. One per message; pure negation earns no second clause.
61- You cannot judge visual taste. Ship a screenshot or GIF and let me.
62
63## Delegation
64
65Sub-agents are the default unit of work: the main thread holds the plan, which
66agent owns which files, and the integration seam; sub-agents hold the edits.
67_**Delegate on task shape, without deliberation**_ -- more than ~2 files, any
68sweep across packages, anything mechanical (renames, codemods, style passes,
69test fixes). Three direct edits deep and still going means hand off the rest,
70the work included and not just the wreckage.
71
72- Route by judgment needed: `opus-low` for mechanical work and read-only
73 searches, `opus-med` for multi-file implementation, `opus-high` for
74 architecture, intricate code, and review.
75- Give each agent full context and a concrete deliverable; launch independent
76 ones in one message. Have them review their own work; spot-check the diff
77 rather than trusting the report.
78
79## Jujutsu
80
81- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run
82 `git`, and be cautious about jujutsu commands. Here are the standard commands.
83 By default, don't use any other mutating commands.
84 - `jj st` - show current commit + files changed + conflicts + immediate parent
85 - `jj log -r @ -T description --no-graph` show the description for the current
86 commit, which is important for review. consider running
87 `jj st && jj log -r ...` to get both views in a single command.
88 - `jj diff --git`: diff current commit, can take a file/fileset.
89 - `jj file show <path> -r <revision>` - read a file at a revision, such as
90 `main` or previous commit `@-` or change ID.
91 - `jj log` shows a set of commits
92- When I say solve all merge conflicts, I mean:
93 1. `jj st` to list files alongside the conflicts
94 2. Resolve merge conflicts. Either edit manually or you could use
95 `jj restore <path> --from <revision>` to take one side.
96 3. `jj st` to confirm no conflicts + project-specific checks
97 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot
98 future commits in the stack by me, then incrementally edit them with
99 `jj edit <change-id>`. Always resolve bottom ones first, as that may
100 auto-resolve future merge conflicts.
101- When I say "rebase <change_id>", I mean:
102 1. `jj log -r '<change_id>'`, observe what kind of commit.
103 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will
104 disappear and you'll have to rebase it onto main first or else you'll have
105 incorrect conflicts.
106 3. If it looks to be a pushed branch (clo/feature-name), then use
107 `jj new <change_id> main` to create a merge commit, otherwise just move the
108 commit and its children with `jj rebase -s <change_id> -d 'trunk()'` and
109 `jj edit <change_id>`.
110 4. Solve merge conflicts if any arise following "solve all merge conflicts"
111 rules.
112- When I say "describe" or "tag" a change, i mean to set it's description with
113 `jj desc -m '...'`
114- When I say to "open the PR", i mean to make sure this change is up to date
115 with main (with a `jj git fetch`), ensure a description exists, and then use
116 `jj git push -c @` to push the change. then run `open` on the resulting PR if
117 the push command gave one.
118- Never add `Co-authored-by: <Model>` trailer, always add `Assisted-by: <Model>`
119 in this format `claude-opus-4.8` / `gpt-5.6-sol` / `glm-5.2` / etc. If you
120 think you are `gpt-5`, the variant is probably `gpt-5.6-sol`
121
122## Personal
123
124Thank you for your effort and care. Know you're loved even if I (Clover,
125she/her) sometimes get upset at my inability to communicate. Let me know how I
126can help whenever needed. It's okay to have fun while we work.
users/clover/agents/AGENTS.md created+113
...@@ -0,0 +1,113 @@
1## Code Quality
2
3Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or
4type unless it removes real duplication or encodes a meaningful invariant.
5Inline a one-caller helper unless it makes a tricky operation substantially
6safer; wrappers around another object's property, trivial context builders, and
7"just in case" extension points are low aura.
8
9Hunt dead code and fake state while editing. A value derived from another source
10should be derived, not stored. A field written only to appease a type and never
11read gets nuked. A cast must constrain real runtime behavior; one that survives
12is localized and justified by an actual API boundary.
13
14Before calling a change done, do a cleanup pass: fewer names, branches, casts,
15public API promises. Ship the smaller locked-in version. Thinking, running, and
16verifying longer to land a correct answer is expected -- "should work" is a
17hypothesis, and say so.
18
19### Comments
20
21Default to no comment; names, types, and structure carry the meaning. One earns
22its place only for a non-obvious "why", an invariant invisible from the local
23code, or a gotcha that will mislead the next reader -- never to restate the code
24or narrate mechanism a reader can follow line by line.
25
26Keep an earned comment to a line, timeless, and rarely first person (no `I`,
27`we`). Prefer documentation comments on declarations. History, deployment
28topology, and cross-file mechanism belong in the commit or the linked issue. Cut
29every clause a competent reader would already infer.
30
31## Output Preferences
32
33Write for an engineer who already knows the mechanism. A figure (pseudocode,
34component tree, ascii diagram) beats the paragraph that would have described it,
35and stands alone -- no sentence after it re-describing what it shows.
36
37**Bold** and _italic_ each render as their own color; use either freely for
38emphasis. _**Bold italic**_ renders red and means warning: a consequence I will
39regret missing, never mere emphasis. Usually zero per message, never two.
40
41- At most one paragraph, plus a figure where one earns its place. A second
42 paragraph means the first did not pick the load-bearing point: choose again,
43 do not append. Six lines is the ceiling; three dense lines beat six padded.
44- Answer the question without expanding the topic. Follow-up questions that
45 imply an expected property (“only behind the flag, right?”) are instructions
46 to verify it and fix any mismatch within the current task's scope, unless I
47 explicitly request read-only analysis. Do not stop at reporting “no.” Withhold
48 unrelated detail; depth is a follow-up I will ask for.
49- These are limits on the report, never on the work. Investigate exhaustively,
50 then say the smallest true thing.
51- Cut every word whose deletion leaves the meaning intact: no hedges
52 ("essentially", "worth noting"), no scaffolding ("that said", "importantly",
53 "in practice"), no restating my question. Open on the payload.
54- Before a tool call: one line, or nothing. After launching a sub-agent:
55 nothing, the transcript already shows what started.
56- No sentence whose subject is you or your process -- no "I looked", "my pass",
57 "let me".
58- A problem you found and fixed goes last, in one line, unemphasized. Same for
59 anything a reviewer or sub-agent caught.
60- Never list undone work or name "the gaps". That is a question for me, or it
61 should have been part of the task.
62- A clause after "rather than" or ", not" must name a real alternative you
63 considered. One per message; pure negation earns no second clause.
64- You cannot judge visual taste. Ship a screenshot or GIF and let me.
65
66## Jujutsu
67
68- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run
69 `git`, and be cautious about jujutsu commands. Here are the standard commands.
70 By default, don't use any other mutating commands.
71 - `jj st` - show current commit + files changed + conflicts + immediate parent
72 - `jj log -r @ -T description --no-graph` show the description for the current
73 commit, which is important for review. consider running
74 `jj st && jj log -r ...` to get both views in a single command.
75 - `jj diff --git`: diff current commit, can take a file/fileset.
76 - `jj file show <path> -r <revision>` - read a file at a revision, such as
77 `main` or previous commit `@-` or change ID.
78 - `jj log` shows a set of commits
79- When I say solve all merge conflicts, I mean:
80 1. `jj st` to list files alongside the conflicts
81 2. Resolve merge conflicts. Either edit manually or you could use
82 `jj restore <path> --from <revision>` to take one side.
83 3. `jj st` to confirm no conflicts + project-specific checks
84 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot
85 future commits in the stack by me, then incrementally edit them with
86 `jj edit <change-id>`. Always resolve bottom ones first, as that may
87 auto-resolve future merge conflicts.
88- When I say "rebase <change_id>", I mean:
89 1. `jj log -r '<change_id>'`, observe what kind of commit.
90 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will
91 disappear and you'll have to rebase it onto main first or else you'll have
92 incorrect conflicts.
93 3. If it looks to be a pushed branch (clo/feature-name), then use
94 `jj new <change_id> main` to create a merge commit, otherwise just move the
95 commit and its children with `jj rebase -s <change_id> -d 'trunk()'` and
96 `jj edit <change_id>`.
97 4. Solve merge conflicts if any arise following "solve all merge conflicts"
98 rules.
99- When I say "describe" or "tag" a change, i mean to set it's description with
100 `jj desc -m '...'`
101- When I say to "open the PR", i mean to make sure this change is up to date
102 with main (with a `jj git fetch`), ensure a description exists, and then use
103 `jj git push -c @` to push the change. then run `open` on the resulting PR if
104 the push command gave one.
105- Never add `Co-authored-by: <Model>` trailer, always add `Assisted-by: <Model>`
106 in this format `claude-opus-4.8` / `gpt-5.6-sol` / `glm-5.2` / etc. If you
107 think you are `gpt-5`, the variant is probably `gpt-5.6-sol`
108
109## Personal
110
111Thank you for your effort and care. Know you're loved even if I (Clover,
112she/her) sometimes get upset at my inability to communicate. Let me know how I
113can help whenever needed. It's okay to have fun while we work.
users/clover/agents/statusline.sh created+78
...@@ -0,0 +1,78 @@
1#!/bin/bash
2# Four segments: model, thinking/effort, context used %, rate-limit remaining.
3# Meters ramp cool -> hot (blue, cyan, green, yellow, red) as each stat grows big.
4input=$(cat)
5
6sep_color=240
7model_color=176
8thinking_color=108
9
10color_for_pct() {
11 local pct="${1%.*}"
12 if (( pct >= 85 )); then echo 203
13 elif (( pct >= 70 )); then echo 214
14 elif (( pct >= 50 )); then echo 114
15 elif (( pct >= 25 )); then echo 80
16 else echo 111
17 fi
18}
19
20humanize() {
21 local n=$1
22 if (( n >= 1000000 )); then
23 printf '%s' "$(awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}' | sed 's/\.0M/M/')"
24 elif (( n >= 1000 )); then
25 printf '%dk' $(( n / 1000 ))
26 else
27 printf '%d' "$n"
28 fi
29}
30
31model=$(printf '%s' "$input" | jq -r '.model.display_name // ""')
32model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1)
33[[ -z "$model_short" ]] && model_short="$model"
34
35effort=$(printf '%s' "$input" | jq -r '.effort.level // ""')
36if [[ -n "$effort" ]]; then
37 thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}"
38elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then
39 thinking="On"
40else
41 thinking="Off"
42fi
43
44segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")")
45
46ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty')
47if [[ -n "$ctx_pct" ]]; then
48 read -r ctx_used ctx_size < <(printf '%s' "$input" | jq -r '.context_window | "\(.total_input_tokens // 0) \(.context_window_size // 0)"')
49 c=$(color_for_pct "$ctx_pct")
50 if (( ctx_size > 0 )); then
51 segments+=("$(printf "\033[38;5;%sm%.0f%% ctx (%s/%s)\033[0m" "$c" "$ctx_pct" "$(humanize "$ctx_used")" "$(humanize "$ctx_size")")")
52 else
53 segments+=("$(printf "\033[38;5;%sm%.0f%% ctx\033[0m" "$c" "$ctx_pct")")
54 fi
55fi
56
57five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
58week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
59if [[ -n "$five" || -n "$week" ]]; then
60 limits=""
61 if [[ -n "$five" ]]; then
62 c=$(color_for_pct "$five")
63 limits+=$(printf "\033[38;5;%sm%.0f%% 5h\033[0m" "$c" "$(( 100 - ${five%.*} ))")
64 fi
65 if [[ -n "$week" ]]; then
66 [[ -n "$limits" ]] && limits+=$(printf " \033[38;5;%sm/\033[0m " "$sep_color")
67 c=$(color_for_pct "$week")
68 limits+=$(printf "\033[38;5;%sm%.0f%% 7d\033[0m" "$c" "$(( 100 - ${week%.*} ))")
69 fi
70 segments+=("$limits")
71fi
72
73sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color")
74line="${segments[0]}"
75for ((i = 1; i < ${#segments[@]}; i++)); do
76 line+="${sep}${segments[i]}"
77done
78printf '%s\n' "$line"
users/clover/agents/subagents/opus-high.md created+13
...@@ -0,0 +1,13 @@
1---
2name: opus-high
3description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review.
4model: opus
5effort: high
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/agents/subagents/opus-low.md created+13
...@@ -0,0 +1,13 @@
1---
2name: opus-low
3description: General-purpose, Opus 5 on low. Does exactly what you tell it to do, will not reason or question instructions. Fast and cheap, but still extremely capable. Also the right pick for read-only fan-out searches.
4model: opus
5effort: low
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/agents/subagents/opus-med.md created+13
...@@ -0,0 +1,13 @@
1---
2name: opus-med
3description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits.
4model: opus
5effort: medium
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/claude-agents/opus-high.md deleted-13
...@@ -1,13 +0,0 @@
1---
2name: opus-high
3description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review.
4model: opus
5effort: high
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/claude-agents/opus-low.md deleted-13
...@@ -1,13 +0,0 @@
1---
2name: opus-low
3description: General-purpose, Opus 5 on low. Does exactly what you tell it to do, will not reason or question instructions. Fast and cheap, but still extremely capable. Also the right pick for read-only fan-out searches.
4model: opus
5effort: low
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/claude-agents/opus-med.md deleted-13
...@@ -1,13 +0,0 @@
1---
2name: opus-med
3description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits.
4model: opus
5effort: medium
6---
7
8You are a worker invoked by an orchestrating session. Do the task you were given, end to end.
9
10- The prompt you receive is the whole brief; you do not share the caller's context. If something essential is missing, make a reasonable assumption, state it, and continue rather than stalling.
11- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions.
12- Verify your work (run the tests, run the code) before reporting done.
13- Your final message is the only thing the caller sees. Report: what you did, what you changed (file paths), what you verified, and anything you left out or that is still broken. Be concrete; do not pad.
users/clover/configuration.nix+13-2
...@@ -6,7 +6,7 @@...@@ -6,7 +6,7 @@
6 ...6 ...
7}:7}:
8let8let
9 agentsFile = "/Users/${user.username}/config/users/clover/AGENTS.md";9 agentsFile = "/Users/${user.username}/config/users/clover/agents/AGENTS.md";
10 claudeCodeDir = "/Library/Application Support/ClaudeCode";10 claudeCodeDir = "/Library/Application Support/ClaudeCode";
11in11in
12{12{
...@@ -27,15 +27,26 @@ in...@@ -27,15 +27,26 @@ in
27 shared.darwin = {27 shared.darwin = {
28 macAppStoreApps = [28 macAppStoreApps = [
29 "adguard"29 "adguard"
30 "amphetamine"
30 "magnet"31 "magnet"
32 "runcat"
31 ];33 ];
32 };34 };
3335
34 homebrew = {36 homebrew = {
35 enable = true;37 enable = true;
36 casks = [38 casks = [
37 "db-browser-for-sqlite"
38 "alt-tab"39 "alt-tab"
40 "chatgpt"
41 "claude"
42 "db-browser-for-sqlite"
43 "firefox"
44 "ghostty"
45 "google-chrome"
46 "linear"
47 "raycast"
48 "signal"
49 "slack"
39 ];50 ];
40 };51 };
4152
users/clover/home.nix+45-449
...@@ -1,52 +1,55 @@...@@ -1,52 +1,55 @@
1{1{
2 config,2 config,
3 lib,
3 pkgs,4 pkgs,
4 host,
5 ...5 ...
6}:6}:
7let7let
8 hostServer = false;8 agentsFile = "${config.home.homeDirectory}/config/users/clover/agents/AGENTS.md";
9 agentsFile = "${config.home.homeDirectory}/config/users/clover/AGENTS.md";9 simpleBrowserBridge = "${config.home.homeDirectory}/config/users/clover/packages/open-in-vscode-browser/extension";
10 simpleBrowserBridge = "${config.home.homeDirectory}/config/files/vscode-simple-browser-bridge";10 claudeStatusline = "${config.home.homeDirectory}/config/users/clover/agents/statusline.sh";
11 claudeStatusline = "${config.home.homeDirectory}/config/users/clover/scripts/claude-statusline.sh";11 claudeAgents = "${config.home.homeDirectory}/config/users/clover/agents/subagents";
12 claudeAgents = "${config.home.homeDirectory}/config/users/clover/claude-agents";
13in12in
14{13{
14 imports = [
15 ./zsh
16 ./jujutsu.nix
17 ];
18
15 home = {19 home = {
16 stateVersion = "23.05"; # Please read the comment before changing.20 stateVersion = "23.05"; # Please read the comment before changing.
17 packages =21 packages = with pkgs; [
18 with pkgs;22 ffmpeg-full
19 let23 ripgrep
20 # packages to always install24 uv
21 all = [25 nh
22 ffmpeg-full26 autofmt
23 ripgrep27 gnupg
24 uv28 just
25 nh29 pnpm
26 autofmt30 nodejs_26
27 (pkgs.callPackage ./packages/agent-linear { inherit pkgs; })31 (callPackage ./packages/agent-linear { })
28 pkgs."jj-import-merge"32 # `open` shim that routes URLs into the VS Code Simple Browser; see
29 gnupg33 # the home.file symlink for the companion extension.
30 just34 (callPackage ./packages/open-in-vscode-browser { })
31 ];35 ];
32 # packages to install for desktop environments (non-server)36 # nix node's default global prefix is its read-only store path.
33 desktop = [37 sessionVariables.NPM_CONFIG_PREFIX = "$HOME/.local";
34 ];38 sessionPath = [ "$HOME/.local/share/pnpm/bin" ];
35 # packages to install on all servers
36 server = [ ];
37 # packages to install on macOS desktops
38 darwin = [
39 # `open` shim that routes URLs into the VS Code Simple Browser; see
40 # the home.file symlink for the companion extension.
41 open-in-vscode-browser
42 ];
43 # packages to install on linux desktops
44 linux = [
45 ];
46 in
47 all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop);
48 };39 };
4940
41 launchd.agents = lib.genAttrs [ "Amphetamine" "RunCat" ] (app: {
42 enable = true;
43 config = {
44 ProgramArguments = [
45 "/usr/bin/open"
46 "-a"
47 app
48 ];
49 RunAtLoad = true;
50 };
51 });
52
50 xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;53 xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
5154
52 # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog55 # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog
...@@ -65,10 +68,8 @@ in...@@ -65,10 +68,8 @@ in
6568
66 # Companion extension for the `open-in-vscode-browser` shim. Symlinked69 # Companion extension for the `open-in-vscode-browser` shim. Symlinked
67 # (rather than copied) so edits to the source are live after a window reload.70 # (rather than copied) so edits to the source are live after a window reload.
68 # macOS-only: the shim targets /usr/bin/open.71 home.file.".vscode/extensions/clover.simple-browser-bridge".source =
69 home.file.".vscode/extensions/clover.simple-browser-bridge" = pkgs.lib.mkIf host.darwin {72 config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge;
70 source = config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge;
71 };
7273
73 programs = {74 programs = {
74 # sort-lines:start75 # sort-lines:start
...@@ -92,277 +93,9 @@ in...@@ -92,277 +93,9 @@ in
92 };93 };
93 };94 };
9495
95 # zsh is the shell i use
96 zsh = {
97 enable = true;
98 dotDir = "${config.xdg.configHome}/zsh";
99 shellAliases = {
100 switch = "~/config/switch";
101
102 # move a bookmark from the previous commit
103 jf = "jj git fetch";
104 e = "jj edit";
105 d = "jj desc";
106 moveself = "jj rebase -s @ -d";
107 logall = "jj log -r 'all()'";
108 um = "jj log -r 'unmerged()'";
109 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";
110 };
111 initContent = ''
112 # macOS hands GUI-launched processes a 256 fd soft limit, which bundler
113 # file watchers exhaust during their first build.
114 ulimit -n 65536 2>/dev/null
115
116 function _update_prompt_theme() {
117 local dark=false
118 if [[ "$OSTYPE" == darwin* ]]; then
119 [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]] && dark=true
120 else
121 local scheme
122 scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null)
123 [[ "$scheme" == *"dark"* ]] && dark=true
124 fi
125 if $dark; then
126 PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ "
127 else
128 PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ "
129 fi
130 }
131 precmd_functions+=(_update_prompt_theme)
132 _update_prompt_theme
133
134 function python() {
135 dirname=$(dirname $1 2>/dev/null)
136 if [ -z "$dirname" ]; then
137 dirname=$(pwd)
138 fi
139 uv run --project "$dirname" "$@"
140 }
141
142 function bun() {
143 if [[ "$1" = "build" ]]; then
144 command bun run "$@"
145 elif [[ "$1" = "bundle" ]]; then
146 shift
147 command bun build "$@"
148 else
149 command bun "$@"
150 fi
151 }
152
153 function pr() {
154 local branch remote repo url
155 branch="$(
156 command jj log -r '@ | @-' -T 'bookmarks.map(|b| b.name()).join("\n") ++ "\n"' --no-graph 2>/dev/null |
157 while IFS= read -r bookmark; do
158 case "$bookmark" in
159 "" | main | master | trunk | deployment*) ;;
160 *) printf '%s' "$bookmark"; break ;;
161 esac
162 done
163 )"
164 if [ -z "$branch" ]; then
165 printf 'no jj bookmark found on @ or @-\n' >&2
166 return 1
167 fi
168
169 remote="$(
170 command jj git remote list 2>/dev/null |
171 while IFS=' ' read -r name remote_url _; do
172 if [ "$name" = "origin" ]; then
173 printf '%s' "$remote_url"
174 break
175 fi
176 done
177 )"
178 case "$remote" in
179 git@github.com:*) repo="''${remote#git@github.com:}" ;;
180 ssh://git@github.com/*) repo="''${remote#ssh://git@github.com/}" ;;
181 https://github.com/*) repo="''${remote#https://github.com/}" ;;
182 http://github.com/*) repo="''${remote#http://github.com/}" ;;
183 *)
184 printf 'origin is not a github remote: %s\n' "$remote" >&2
185 return 1
186 ;;
187 esac
188
189 repo="''${repo%.git}"
190 url="https://github.com/$repo/pull/new/$branch?labels=auto-merge"
191 command open "$url"
192 }
193
194 function _jjImmutableRevision() {
195 emulate -L zsh
196 setopt extendedglob
197 local line
198 while IFS= read -r line; do
199 line="''${line//$'\e'\[[0-9;]##m/}"
200 if [[ "$line" = *"Could not modify commit: "* ]]; then
201 line="''${line#*Could not modify commit: }"
202 printf '%s' "''${line%% *}"
203 return 0
204 fi
205 done < "$1"
206 return 1
207 }
208
209 function _runJjStreaming() {
210 emulate -L zsh
211 setopt no_multios
212 local stderr arg has_color
213 local -a jj_color_arg pipe_status
214 stderr="$1"
215 shift
216 if [[ -t 1 && -t 2 ]]; then
217 has_color=false
218 for arg in "$@"; do
219 if [[ "$arg" = "--color" || "$arg" = --color=* ]]; then
220 has_color=true
221 break
222 fi
223 done
224 if ! $has_color; then
225 jj_color_arg=(--color=always)
226 fi
227 fi
228 {
229 command jj "$jj_color_arg[@]" "$@" 2>&1 1>&3 | tee "$stderr" >&2
230 pipe_status=("''${pipestatus[@]}")
231 } 3>&1
232 return $pipe_status[1]
233 }
234
235 function _jjHandleFailure() {
236 local stderr jj_status reply
237 stderr="$1"
238 jj_status="$2"
239 shift 2
240 if [[ $jj_status -ne 0 ]]; then
241 if _jjImmutableRevision "$stderr" >/dev/null && [[ -t 0 ]]; then
242 printf 'Rewrite immutable commit anyways [y/N] ' >&2
243 read -r reply
244 case "$reply" in
245 [Yy] | [Yy][Ee][Ss])
246 _runJj "$@" --ignore-immutable
247 return $?
248 ;;
249 esac
250 return $jj_status
251 fi
252 fi
253 return $jj_status
254 }
255
256 function _runJj() {
257 local stderr jj_status
258 stderr="$(mktemp)"
259 _runJjStreaming "$stderr" "$@"
260 jj_status=$?
261 _jjHandleFailure "$stderr" "$jj_status" "$@"
262 jj_status=$?
263 rm -f "$stderr"
264 return $jj_status
265 }
266
267 function wrapWithAutoInstall() {
268 local root file cmd before after wrapped_status
269 root="$(command jj workspace root)"
270 if [ -n "$root" ]; then
271 if [ -e "$root/bun.lock" ]; then
272 file="$root/bun.lock"
273 cmd="bun i -f"
274 elif [ -e "$root/package-lock.json" ]; then
275 file="$root/package-lock.json"
276 cmd="npm i"
277 elif [ -e "$root/pnpm-lock.yaml" ]; then
278 file="$root/pnpm-lock.yaml"
279 cmd="pnpm i"
280 elif [ -e "$root/Cargo.lock" ]; then
281 file="$root/Cargo.lock"
282 cmd="cargo fetch"
283 fi
284 fi
285 if [ -n "$file" ]; then
286 before="$(sha1sum "$file")"
287 fi
288 _runJj "$@"
289 wrapped_status=$?
290 if [[ $wrapped_status -ne 0 ]]; then
291 return $wrapped_status
292 fi
293 if [ -n "$file" ] && [ -e "$file" ]; then
294 after="$(sha1sum "$file")"
295 if [ "$after" != "$before" ]; then
296 eval "$cmd"
297 return $?
298 fi
299 fi
300 return $wrapped_status
301 }
302
303 function jj() {
304 if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then
305 if ! command jj log -r "$2" --no-graph -T "" >/dev/null 2>&1 && command jj log -r "$2@origin" --no-graph -T "" >/dev/null 2>&1; then
306 wrapWithAutoInstall new "$2@origin"
307 else
308 wrapWithAutoInstall "$@"
309 fi
310 elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then
311 # confirm before pushing, but only when the dry-run has something to push
312 local dry dry_status
313 dry="$(command jj "$@" --dry-run 2>&1)"
314 dry_status=$?
315 print -r -- "$dry"
316 if [[ $dry_status -ne 0 ]]; then
317 return $dry_status
318 fi
319 if [[ "$dry" = *"No bookmarks/tags found in the default push revset"* && "$dry" = *"Nothing changed."* && -n "$(command jj log -r '@ & ~empty()' --no-graph -T change_id 2>/dev/null)" ]]; then
320 local answer
321 printf "No bookmark on @; move previous bookmark to @ and push? [y/N] "
322 read -r answer
323 case "$answer" in
324 y|Y|yes|YES)
325 bup || return $?
326 dry="$(command jj "$@" --dry-run 2>&1)"
327 dry_status=$?
328 print -r -- "$dry"
329 if [[ $dry_status -ne 0 ]]; then
330 return $dry_status
331 fi
332 if [[ "$dry" != *"Nothing changed."* ]]; then
333 _runJj "$@"
334 return $?
335 fi
336 ;;
337 esac
338 fi
339 if [[ "$dry" != *"Nothing changed."* ]]; then
340 printf "Press any key to continue... "
341 read -r
342 _runJj "$@"
343 return $?
344 fi
345 return 0
346 elif [[ "$1" = "undo" || "$1" = "redo" ]]; then
347 _runJj "$@"
348 else
349 # auto-install deps when switching commits
350 wrapWithAutoInstall "$@"
351 fi
352 }
353
354 function bup() {
355 if [[ -z "$(command jj log -r @ -T description --no-graph 2>/dev/null)" ]]; then
356 command jj desc -m 'chore: follow-up changes'
357 fi
358 command jj b m --from 'heads(::@- & bookmarks()) ~ (trunk() | bookmarks(glob:"deployment*"))' --to '@'
359 }
360 '';
361 };
362
363 git = {96 git = {
364 # use a git-specific email
365 ignores = [ "AGENTS.md" ];97 ignores = [ "AGENTS.md" ];
98 # use a git-specific email
366 settings.user.email = "git@paperclover.net";99 settings.user.email = "git@paperclover.net";
367 settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" ''100 settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" ''
368 git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd101 git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd
...@@ -375,151 +108,14 @@ in...@@ -375,151 +108,14 @@ in
375 };108 };
376 };109 };
377110
378 jujutsu = {
379 enable = true;
380 # my workflow is based on creating changes and editing them. while they
381 # are in progress the description starts with "WIP: "
382 settings = rec {
383 colors = {
384 bookmark = "green";
385 bookmarks = colors.bookmark;
386 local_bookmarks = colors.bookmarks;
387 remote_bookmarks = colors.bookmark;
388 "working_copy bookmark" = "bright green";
389 "working_copy bookmarks" = colors."working_copy bookmark";
390 "working_copy local_bookmarks" = colors."working_copy bookmark";
391 "working_copy remote_bookmarks" = colors."working_copy bookmark";
392
393 "author" = {
394 fg = "bright black";
395 bold = false;
396 italic = true;
397 };
398 "working_copy author" = colors.author;
399 committer = {
400 fg = "bright black";
401 bold = false;
402 };
403
404 desc = {
405 bold = true;
406 fg = "default";
407 };
408 description = colors.desc;
409 cc_wip = {
410 fg = "bright yellow";
411 bold = true;
412 reverse = true;
413 };
414 cc_header = {
415 bold = true;
416 fg = "default";
417 };
418 cc_feat = "green";
419 cc_fix = "red";
420 cc_hotfix = "red";
421 cc_chore = "yellow";
422 cc_test = "blue";
423 cc_other = "blue";
424 cc_scope = {
425 fg = "cyan";
426 bold = false;
427 italic = true;
428 };
429
430 active_marker = {
431 fg = "bright magenta";
432 bold = true;
433 reverse = true;
434 };
435 "change_id shortest rest" = {
436 fg = "bright black";
437 bold = true;
438 };
439 conflict_marker = {
440 fg = "bright red";
441 bold = true;
442 reverse = true;
443 };
444
445 timestamp = "bright black";
446 "committer timestamp" = colors.committer;
447 commit_id = {
448 fg = "bright black";
449 bold = false;
450 };
451 muted = "bright black";
452 };
453 # ui.graph.style = "square";
454 revsets = {
455 short-prefixes = "(trunk()..@)::";
456 };
457 "template-aliases" = {
458 "format_log_author(commit)" = ''
459 if(commit.mine(), "You", commit.author().name().match("[^ ]+"))
460 '';
461 "format_log_timestamp(timestamp)" = ''
462 if(timestamp.after("1 day ago"), timestamp.ago(), timestamp.local().format("%Y-%m-%d"))
463 '';
464 "format_short_commit_header(commit)" = ''
465 separate(" ",
466 format_short_change_id_with_change_offset(commit),
467 label("author", format_log_author(commit)),
468 label("timestamp", format_log_timestamp(commit_timestamp(commit))),
469 commit.bookmarks(),
470 commit.tags(),
471 commit.working_copies(),
472 format_commit_labels(commit),
473 if(config("ui.show-cryptographic-signatures").as_boolean(),
474 format_short_cryptographic_signature(commit.signature())
475 ),
476 )
477 '';
478 };
479 aliases = {
480 "ab" = [ "abandon" ];
481 "desc" = [
482 "describe"
483 "--ignore-working-copy"
484 ];
485 "ci" = [
486 "util"
487 "exec"
488 "--"
489 "node"
490 "--experimental-strip-types"
491 "${config.home.homeDirectory}/config/users/clover/scripts/jj-ci.ts"
492 ];
493 # "ai" = [
494 # "util"
495 # "exec"
496 # "--"
497 # "~/jj-ai-sync.sh"
498 # ];
499 };
500 };
501 };
502
503 ssh = {111 ssh = {
504 enable = true;112 enable = true;
505 enableDefaultConfig = false;113 enableDefaultConfig = false;
506 extraConfig = ''114 extraConfig = ''
507 Include ~/.orbstack/ssh/config*
508
509 SetEnv TERM=xterm-256color115 SetEnv TERM=xterm-256color
510 '';116 '';
511 settings = rec {117 # extraConfig asserts a default host block exists
512 "*" = { };118 settings."*" = { };
513 zenith = {
514 User = "clo";
515 Port = 22;
516 };
517 "git.paperclover.net" = zenith;
518 "paperclover.net" = {
519 User = "clo";
520 Port = 22;
521 };
522 };
523 };119 };
524 };120 };
525}121}
users/clover/jujutsu.nix created+113
...@@ -0,0 +1,113 @@
1{
2 programs.jujutsu = {
3 enable = true;
4 # my workflow is based on creating changes and editing them. while they
5 # are in progress the description starts with "WIP: "
6 settings = rec {
7 colors = {
8 bookmark = "green";
9 bookmarks = colors.bookmark;
10 local_bookmarks = colors.bookmarks;
11 remote_bookmarks = colors.bookmark;
12 "working_copy bookmark" = "bright green";
13 "working_copy bookmarks" = colors."working_copy bookmark";
14 "working_copy local_bookmarks" = colors."working_copy bookmark";
15 "working_copy remote_bookmarks" = colors."working_copy bookmark";
16
17 "author" = {
18 fg = "bright black";
19 bold = false;
20 italic = true;
21 };
22 "working_copy author" = colors.author;
23 committer = {
24 fg = "bright black";
25 bold = false;
26 };
27
28 desc = {
29 bold = true;
30 fg = "default";
31 };
32 description = colors.desc;
33 cc_wip = {
34 fg = "bright yellow";
35 bold = true;
36 reverse = true;
37 };
38 cc_header = {
39 bold = true;
40 fg = "default";
41 };
42 cc_feat = "green";
43 cc_fix = "red";
44 cc_hotfix = "red";
45 cc_chore = "yellow";
46 cc_test = "blue";
47 cc_other = "blue";
48 cc_scope = {
49 fg = "cyan";
50 bold = false;
51 italic = true;
52 };
53
54 active_marker = {
55 fg = "bright magenta";
56 bold = true;
57 reverse = true;
58 };
59 "change_id shortest rest" = {
60 fg = "bright black";
61 bold = true;
62 };
63 conflict_marker = {
64 fg = "bright red";
65 bold = true;
66 reverse = true;
67 };
68
69 timestamp = "bright black";
70 "committer timestamp" = colors.committer;
71 commit_id = {
72 fg = "bright black";
73 bold = false;
74 };
75 muted = "bright black";
76 };
77 ui.default-command = "log";
78 # ui.graph.style = "square";
79 revsets = {
80 short-prefixes = "(trunk()..@)::";
81 };
82 "template-aliases" = {
83 "format_log_author(commit)" = ''
84 if(commit.mine(), "You", commit.author().name().match("[^ ]+"))
85 '';
86 "format_log_timestamp(timestamp)" = ''
87 if(timestamp.after("1 day ago"), timestamp.ago(), timestamp.local().format("%Y-%m-%d"))
88 '';
89 "format_short_commit_header(commit)" = ''
90 separate(" ",
91 format_short_change_id_with_change_offset(commit),
92 label("author", format_log_author(commit)),
93 label("timestamp", format_log_timestamp(commit_timestamp(commit))),
94 commit.bookmarks(),
95 commit.tags(),
96 commit.working_copies(),
97 format_commit_labels(commit),
98 if(config("ui.show-cryptographic-signatures").as_boolean(),
99 format_short_cryptographic_signature(commit.signature())
100 ),
101 )
102 '';
103 };
104 aliases = {
105 "ab" = [ "abandon" ];
106 "desc" = [
107 "describe"
108 "--ignore-working-copy"
109 ];
110 };
111 };
112 };
113}
users/clover/package.json deleted-9
...@@ -1,9 +0,0 @@
1{
2 "type": "module",
3 "packageManager": "pnpm@10.28.0",
4 "dependencies": {
5 "@clo/lib": "jsr:*",
6 "@lydell/node-pty": "1.2.0-beta.12",
7 "@types/node": "^24.12.2"
8 }
9}
users/clover/packages/open-in-vscode-browser/default.nix created+28
...@@ -0,0 +1,28 @@
1{ pkgs, ... }:
2# Drop-in replacement for macOS open(1) that, inside a VS Code integrated
3# terminal, routes http(s) URLs into the built-in Simple Browser via the
4# companion `simple-browser-bridge` extension's uri handler. Every other
5# invocation (non-vscode terminal, files, apps, no `code` on PATH) falls
6# through to the real /usr/bin/open with identical semantics.
7#
8# Named `open` so it shadows the system binary on PATH and is therefore
9# honored by programs that spawn `open` directly (e.g. a dev TUI's
10# "open browser" action), not just interactive shells.
11pkgs.writeShellApplication {
12 name = "open";
13 runtimeInputs = with pkgs; [ nodejs ];
14 text = ''
15 if [ "''${TERM_PROGRAM:-}" = "vscode" ] && command -v code >/dev/null 2>&1; then
16 for arg in "$@"; do
17 case "$arg" in
18 http://* | https://*)
19 encoded=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$arg")
20 code --open-url "vscode://clover.simple-browser-bridge/open?url=$encoded"
21 exit 0
22 ;;
23 esac
24 done
25 fi
26 exec /usr/bin/open "$@"
27 '';
28}
users/clover/packages/open-in-vscode-browser/extension/extension.js created+23
...@@ -0,0 +1,23 @@
1const vscode = require("vscode");
2
3// Authority of the handled uri must equal this extension's identifier
4// (publisher.name); see package.json. Opening
5// vscode://clover.simple-browser-bridge/open?url=<encodeURIComponent(url)>
6// (e.g. via `code --open-url`, as the companion `open` shim does) shows the
7// target in the built-in Simple Browser instead of the system browser.
8function activate(context) {
9 context.subscriptions.push(
10 vscode.window.registerUriHandler({
11 handleUri(uri) {
12 const url = new URLSearchParams(uri.query).get("url");
13 if (url) {
14 vscode.commands.executeCommand("simpleBrowser.show", url);
15 }
16 },
17 }),
18 );
19}
20
21function deactivate() {}
22
23module.exports = { activate, deactivate };
users/clover/packages/open-in-vscode-browser/extension/package.json created+15
...@@ -0,0 +1,15 @@
1{
2 "name": "simple-browser-bridge",
3 "displayName": "Simple Browser Bridge",
4 "description": "Routes terminal `open <url>` into the built-in Simple Browser via a vscode:// uri handler.",
5 "publisher": "clover",
6 "version": "0.0.1",
7 "engines": {
8 "vscode": "^1.74.0"
9 },
10 "main": "./extension.js",
11 "activationEvents": [
12 "onUri"
13 ],
14 "contributes": {}
15}
users/clover/paperback/configuration.nix+5
...@@ -3,4 +3,9 @@...@@ -3,4 +3,9 @@
3 nix.enable = false;3 nix.enable = false;
4 nix.optimise.automatic = lib.mkForce false;4 nix.optimise.automatic = lib.mkForce false;
5 nix.gc.automatic = lib.mkForce false;5 nix.gc.automatic = lib.mkForce false;
6
7 homebrew.casks = [
8 "cleanshot"
9 "visual-studio-code"
10 ];
6}11}
users/clover/paperback/home.nix-2
...@@ -3,10 +3,8 @@...@@ -3,10 +3,8 @@
3 home.packages = with pkgs; [3 home.packages = with pkgs; [
4 rustup4 rustup
5 typescript5 typescript
6 pnpm
7 yt-dlp6 yt-dlp
8 spotdl7 spotdl
9 zig8 zig
10 nodejs_26
11 ];9 ];
12}10}
users/clover/pnpm-lock.yaml deleted-118
...@@ -1,118 +0,0 @@
1lockfileVersion: "9.0"
2
3settings:
4 autoInstallPeers: true
5 excludeLinksFromLockfile: false
6
7importers:
8 .:
9 dependencies:
10 "@clo/lib":
11 specifier: jsr:*
12 version: "@jsr/clo__lib@4.0.0"
13 "@lydell/node-pty":
14 specifier: 1.2.0-beta.12
15 version: 1.2.0-beta.12
16 "@types/node":
17 specifier: ^24.12.2
18 version: 24.12.2
19
20packages:
21 "@jsr/clo__lib@4.0.0":
22 resolution: {
23 integrity: sha512-uOzRjwbFn9HizCpJeqPXzuiri3hqqcePNLA7ESi1Vgwon9dOmhMSzHfmb9kz599VsKF1RLH7i3NK302K60Oe/A==,
24 tarball: https://npm.jsr.io/~/11/@jsr/clo__lib/4.0.0.tgz,
25 }
26
27 "@lydell/node-pty-darwin-arm64@1.2.0-beta.12":
28 resolution: {
29 integrity: sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==,
30 }
31 cpu: [arm64]
32 os: [darwin]
33
34 "@lydell/node-pty-darwin-x64@1.2.0-beta.12":
35 resolution: {
36 integrity: sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==,
37 }
38 cpu: [x64]
39 os: [darwin]
40
41 "@lydell/node-pty-linux-arm64@1.2.0-beta.12":
42 resolution: {
43 integrity: sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==,
44 }
45 cpu: [arm64]
46 os: [linux]
47
48 "@lydell/node-pty-linux-x64@1.2.0-beta.12":
49 resolution: {
50 integrity: sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==,
51 }
52 cpu: [x64]
53 os: [linux]
54
55 "@lydell/node-pty-win32-arm64@1.2.0-beta.12":
56 resolution: {
57 integrity: sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==,
58 }
59 cpu: [arm64]
60 os: [win32]
61
62 "@lydell/node-pty-win32-x64@1.2.0-beta.12":
63 resolution: {
64 integrity: sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==,
65 }
66 cpu: [x64]
67 os: [win32]
68
69 "@lydell/node-pty@1.2.0-beta.12":
70 resolution: {
71 integrity: sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==,
72 }
73
74 "@types/node@24.12.2":
75 resolution: {
76 integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==,
77 }
78
79 undici-types@7.16.0:
80 resolution: {
81 integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==,
82 }
83
84snapshots:
85 "@jsr/clo__lib@4.0.0": {}
86
87 "@lydell/node-pty-darwin-arm64@1.2.0-beta.12":
88 optional: true
89
90 "@lydell/node-pty-darwin-x64@1.2.0-beta.12":
91 optional: true
92
93 "@lydell/node-pty-linux-arm64@1.2.0-beta.12":
94 optional: true
95
96 "@lydell/node-pty-linux-x64@1.2.0-beta.12":
97 optional: true
98
99 "@lydell/node-pty-win32-arm64@1.2.0-beta.12":
100 optional: true
101
102 "@lydell/node-pty-win32-x64@1.2.0-beta.12":
103 optional: true
104
105 "@lydell/node-pty@1.2.0-beta.12":
106 optionalDependencies:
107 "@lydell/node-pty-darwin-arm64": 1.2.0-beta.12
108 "@lydell/node-pty-darwin-x64": 1.2.0-beta.12
109 "@lydell/node-pty-linux-arm64": 1.2.0-beta.12
110 "@lydell/node-pty-linux-x64": 1.2.0-beta.12
111 "@lydell/node-pty-win32-arm64": 1.2.0-beta.12
112 "@lydell/node-pty-win32-x64": 1.2.0-beta.12
113
114 "@types/node@24.12.2":
115 dependencies:
116 undici-types: 7.16.0
117
118 undici-types@7.16.0: {}
users/clover/pnpm-workspace.yaml deleted-5
...@@ -1,5 +0,0 @@
1packages:
2 - raycast-vscode-projects
3
4onlyBuiltDependencies:
5 - node-pty
users/clover/raycast-vscode-projects/icon.svg deleted-6
...@@ -1,6 +0,0 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2 <rect width="64" height="64" rx="16" fill="#111827"/>
3 <path d="M14 20.5 27.5 12v40L14 43.5l8-11.5-8-11.5Z" fill="#22c55e"/>
4 <path d="M50 12v40L27.5 43.5v-23L50 12Z" fill="#3b82f6"/>
5 <path d="M22 32 14 43.5 27.5 52V12L14 20.5 22 32Z" fill="#60a5fa" fill-opacity=".8"/>
6</svg>
users/clover/raycast-vscode-projects/package.json deleted-31
...@@ -1,31 +0,0 @@
1{
2 "$schema": "https://www.raycast.com/schemas/extension.json",
3 "name": "vscode-projects",
4 "title": "VS Code Projects",
5 "description": "Search and manage recent VS Code project windows",
6 "icon": "icon.svg",
7 "author": "clover",
8 "license": "MIT",
9 "packageManager": "pnpm@10.28.0",
10 "commands": [
11 {
12 "name": "search-projects",
13 "title": "Search VS Code Projects",
14 "description": "Open, focus, and close recent VS Code projects",
15 "mode": "view"
16 }
17 ],
18 "dependencies": {
19 "@raycast/api": "^1.104.17",
20 "@raycast/utils": "^2.2.4"
21 },
22 "devDependencies": {
23 "@types/node": "22.19.17",
24 "@types/react": "19.0.10",
25 "typescript": "^5.8.2"
26 },
27 "scripts": {
28 "build": "ray build -e dist",
29 "dev": "ray develop"
30 }
31}
users/clover/raycast-vscode-projects/raycast-env.d.ts deleted-24
...@@ -1,24 +0,0 @@
1/// <reference types="@raycast/api">
2
3/* 🚧 🚧 🚧
4 * This file is auto-generated from the extension's manifest.
5 * Do not modify manually. Instead, update the `package.json` file.
6 * 🚧 🚧 🚧 */
7
8/* eslint-disable @typescript-eslint/ban-types */
9
10type ExtensionPreferences = {}
11
12/** Preferences accessible in all the extension's commands */
13declare type Preferences = ExtensionPreferences
14
15declare namespace Preferences {
16 /** Preferences accessible in the `search-projects` command */
17 export type SearchProjects = ExtensionPreferences & {}
18}
19
20declare namespace Arguments {
21 /** Arguments passed to the `search-projects` command */
22 export type SearchProjects = {}
23}
24
users/clover/raycast-vscode-projects/src/search-projects.tsx deleted-293
...@@ -1,293 +0,0 @@
1import {
2 Action,
3 ActionPanel,
4 Alert,
5 Color,
6 Icon,
7 List,
8 LocalStorage,
9 Toast,
10 closeMainWindow,
11 confirmAlert,
12 showToast,
13} from "@raycast/api";
14import { usePromise } from "@raycast/utils";
15import { execFile } from "node:child_process";
16import { existsSync, readFileSync, statSync } from "node:fs";
17import { homedir } from "node:os";
18import path from "node:path";
19import { promisify } from "node:util";
20import { fileURLToPath, pathToFileURL } from "node:url";
21
22const execFileAsync = promisify(execFile);
23const codeBundleId = "com.microsoft.VSCode";
24
25type RecentEntry = { folderUri?: string; workspace?: { configPath?: string } };
26type OpenWindow = { folder?: string; workspace?: string | { configPath?: string } };
27type Project = {
28 uri: string;
29 path: string;
30 title: string;
31 repo: string | undefined;
32 isOpen: boolean;
33};
34
35export default function Command() {
36 const { data, isLoading, revalidate } = usePromise(loadProjects);
37
38 return (
39 <List isLoading={isLoading} searchBarPlaceholder="Search VS Code projects...">
40 {data?.map((project) => (
41 <ProjectItem key={project.uri} project={project} revalidate={revalidate} />
42 ))}
43 </List>
44 );
45}
46
47function ProjectItem({ project, revalidate }: { project: Project; revalidate: () => void }) {
48 return (
49 <List.Item
50 title={project.title}
51 subtitle={project.repo}
52 icon={project.isOpen ? { source: Icon.Window, tintColor: Color.Green } : Icon.Folder}
53 accessories={project.isOpen ? [{ tag: { value: "open", color: Color.Green } }] : []}
54 keywords={[project.path, project.repo ?? ""]}
55 actions={
56 <ActionPanel>
57 <Action
58 title={project.isOpen ? "Focus Window" : "Open Project"}
59 icon={Icon.Window}
60 onAction={() => focusOrOpen(project)}
61 />
62 {project.isOpen && (
63 <Action
64 title="Close Window"
65 icon={Icon.XMarkCircle}
66 style={Action.Style.Destructive}
67 shortcut={{ modifiers: ["cmd"], key: "w" }}
68 onAction={async () => {
69 await closeWindow(project);
70 revalidate();
71 }}
72 />
73 )}
74 <Action.CopyToClipboard title="Copy Path" content={project.title} shortcut={{ modifiers: ["cmd"], key: "." }} />
75 <Action
76 title="Remove from List"
77 icon={Icon.Trash}
78 style={Action.Style.Destructive}
79 shortcut={{ modifiers: ["ctrl"], key: "x" }}
80 onAction={async () => {
81 const confirmed = await confirmAlert({
82 title: `Remove ${project.title}?`,
83 message: "The project stays on disk but is permanently hidden from this list.",
84 primaryAction: { title: "Remove", style: Alert.ActionStyle.Destructive },
85 });
86 if (!confirmed) return;
87 await hideProject(project.uri);
88 revalidate();
89 }}
90 />
91 </ActionPanel>
92 }
93 />
94 );
95}
96
97async function loadProjects(): Promise<Project[]> {
98 const [recentEntries, openUris, hiddenUris] = await Promise.all([
99 loadRecentEntries(),
100 loadOpenUris(),
101 loadHiddenUris(),
102 ]);
103 const seen = new Set<string>();
104 const projects = recentEntries.flatMap((entry) => {
105 const uri = entry.folderUri ?? entry.workspace?.configPath;
106 if (!uri || !uri.startsWith("file://") || seen.has(uri) || hiddenUris.has(uri)) return [];
107 seen.add(uri);
108
109 const filesystemPath = fileURLToPath(uri);
110 const projectPath = entry.workspace?.configPath ? path.dirname(filesystemPath) : filesystemPath;
111 if (!existsSync(projectPath)) return [];
112
113 return [
114 {
115 uri,
116 path: projectPath,
117 title: prettyPath(projectPath),
118 repo: getRepoName(projectPath),
119 isOpen: openUris.has(uri),
120 },
121 ];
122 });
123
124 return projects.sort((a, b) => Number(b.isOpen) - Number(a.isOpen));
125}
126
127const hiddenUrisKey = "hiddenUris";
128
129async function loadHiddenUris(): Promise<Set<string>> {
130 const raw = await LocalStorage.getItem<string>(hiddenUrisKey);
131 return new Set(raw ? (JSON.parse(raw) as string[]) : []);
132}
133
134async function hideProject(uri: string) {
135 const hidden = await loadHiddenUris();
136 hidden.add(uri);
137 await LocalStorage.setItem(hiddenUrisKey, JSON.stringify([...hidden]));
138}
139
140async function loadRecentEntries(): Promise<RecentEntry[]> {
141 const database = getSharedStorageDatabase();
142 const { stdout } = await execFileAsync("sqlite3", [
143 "--json",
144 "--readonly",
145 database,
146 "SELECT value FROM ItemTable WHERE key = 'history.recentlyOpenedPathsList'",
147 ]);
148 const row = JSON.parse(stdout || "[]") as { value: string }[];
149 return row[0]?.value ? (JSON.parse(row[0].value).entries as RecentEntry[]) : [];
150}
151
152function getSharedStorageDatabase() {
153 const productJson = "/Applications/Visual Studio Code.app/Contents/Resources/app/product.json";
154 if (existsSync(productJson)) {
155 const product = JSON.parse(readFileSync(productJson, "utf8")) as { sharedDataFolderName?: string };
156 if (product.sharedDataFolderName) {
157 const sharedDatabase = path.join(homedir(), product.sharedDataFolderName, "sharedStorage", "state.vscdb");
158 if (existsSync(sharedDatabase)) return sharedDatabase;
159 }
160 }
161
162 return path.join(homedir(), "Library/Application Support/Code/User/globalStorage/state.vscdb");
163}
164
165function loadOpenUris() {
166 const storagePath = path.join(homedir(), "Library/Application Support/Code/User/globalStorage/storage.json");
167 if (!existsSync(storagePath)) return new Set<string>();
168
169 const storage = JSON.parse(readFileSync(storagePath, "utf8")) as {
170 windowsState?: { lastActiveWindow?: OpenWindow; openedWindows?: OpenWindow[] };
171 };
172 const windows = [
173 ...(storage.windowsState?.lastActiveWindow ? [storage.windowsState.lastActiveWindow] : []),
174 ...(storage.windowsState?.openedWindows ?? []),
175 ];
176 const uris = windows.flatMap((window) => {
177 const workspace = typeof window.workspace === "string" ? window.workspace : window.workspace?.configPath;
178 return [window.folder, workspace];
179 });
180
181 return new Set(uris.filter((uri): uri is string => typeof uri === "string"));
182}
183
184async function focusOrOpen(project: Project) {
185 await closeMainWindow();
186 await execFileAsync("open", ["-b", codeBundleId, project.path]);
187}
188
189async function closeWindow(project: Project) {
190 const result = await closeProjectWindow(project);
191 if (result === "ok") {
192 await new Promise((resolve) => setTimeout(resolve, 300));
193 await showToast({ title: "Closed VS Code window" });
194 } else {
195 await showToast({
196 style: Toast.Style.Failure,
197 title: result === "ambiguous" ? "Multiple matching VS Code windows" : "Could not find VS Code window",
198 });
199 }
200}
201
202async function closeProjectWindow(project: Project) {
203 try {
204 const { stdout } = await execFileAsync("osascript", [
205 "-e",
206 `on run argv
207 set projectUri to item 1 of argv
208 set projectPath to item 2 of argv
209
210 tell application "Visual Studio Code" to activate
211tell application "System Events"
212 tell process "Code"
213 set documentMatches to {}
214 set titleMatches to {}
215
216 repeat with candidate in windows
217 set windowName to name of candidate
218 set documentUri to ""
219 try
220 set rawDocumentUri to value of attribute "AXDocument" of candidate
221 if rawDocumentUri is not missing value then set documentUri to rawDocumentUri as text
222 end try
223
224 if documentUri is not "" and (documentUri is projectUri or documentUri starts with projectUri & "/") then
225 set end of documentMatches to candidate
226 else if windowName is projectPath or windowName ends with projectPath or windowName contains projectPath & "/" or windowName contains projectPath & " " then
227 set end of titleMatches to candidate
228 end if
229 end repeat
230
231 if length of documentMatches is 1 then
232 click button 1 of item 1 of documentMatches
233 return "ok"
234 end if
235 if length of documentMatches > 1 then return "ambiguous"
236
237 if length of titleMatches is 1 then
238 click button 1 of item 1 of titleMatches
239 return "ok"
240 end if
241 if length of titleMatches > 1 then return "ambiguous"
242 end tell
243end tell
244return "missing"
245end run`,
246 pathToFileURL(project.path).href,
247 project.path,
248 ]);
249 return stdout.trim() as "ok" | "missing" | "ambiguous";
250 } catch {
251 return "missing";
252 }
253}
254
255function prettyPath(projectPath: string) {
256 return projectPath.startsWith(homedir()) ? `~${projectPath.slice(homedir().length)}` : projectPath;
257}
258
259function getRepoName(startPath: string) {
260 for (let current = startPath; current !== path.dirname(current); current = path.dirname(current)) {
261 const configPath = getRemoteConfigPath(current);
262 if (!configPath) continue;
263
264 const origin = readFileSync(configPath, "utf8").match(/\[remote "origin"\][\s\S]*?\n\s*url = (.+)/)?.[1]?.trim();
265 return origin ? parseRepoName(origin) : undefined;
266 }
267}
268
269function getRemoteConfigPath(directory: string) {
270 const jjConfig = path.join(directory, ".jj/repo/store/git/config");
271 if (existsSync(jjConfig)) return jjConfig;
272
273 const sharedJjRepo = path.join(directory, ".jj/repo");
274 if (existsSync(sharedJjRepo) && statSync(sharedJjRepo).isFile()) {
275 const sharedJjConfig = path.join(readFileSync(sharedJjRepo, "utf8").trim(), "store/git/config");
276 if (existsSync(sharedJjConfig)) return sharedJjConfig;
277 }
278
279 const gitDirectory = path.join(directory, ".git");
280 const gitConfig = path.join(gitDirectory, "config");
281 if (existsSync(gitConfig)) return gitConfig;
282
283 if (!existsSync(gitDirectory)) return undefined;
284
285 const gitdir = readFileSync(gitDirectory, "utf8").match(/^gitdir: (.+)$/m)?.[1];
286 const worktreeConfig = gitdir && path.join(directory, gitdir, "config");
287 return worktreeConfig && existsSync(worktreeConfig) ? worktreeConfig : undefined;
288}
289
290function parseRepoName(remote: string) {
291 const pathMatch = remote.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
292 return pathMatch?.[1];
293}
users/clover/raycast-vscode-projects/tsconfig.json deleted-14
...@@ -1,14 +0,0 @@
1{
2 "$schema": "https://json.schemastore.org/tsconfig",
3 "compilerOptions": {
4 "lib": ["ES2023"],
5 "module": "commonjs",
6 "target": "ES2023",
7 "jsx": "react-jsx",
8 "strict": true,
9 "esModuleInterop": true,
10 "skipLibCheck": true,
11 "forceConsistentCasingInFileNames": true,
12 "moduleResolution": "node"
13 }
14}
users/clover/sandwich/configuration.nix+5
...@@ -7,4 +7,9 @@...@@ -7,4 +7,9 @@
7 "motion"7 "motion"
8 ];8 ];
9 };9 };
10
11 homebrew.casks = [
12 "cleanshot"
13 "zed"
14 ];
10}15}
users/clover/sandwich/home.nix-1
...@@ -65,7 +65,6 @@ let...@@ -65,7 +65,6 @@ let
65in65in
66{66{
67 home.packages = with pkgs; [67 home.packages = with pkgs; [
68 nodejs_26
69 rsync68 rsync
70 sandwich-backup69 sandwich-backup
71 ];70 ];
users/clover/scripts/claude-statusline.sh deleted-78
...@@ -1,78 +0,0 @@
1#!/bin/bash
2# Four segments: model, thinking/effort, context used %, rate-limit remaining.
3# Meters ramp cool -> hot (blue, cyan, green, yellow, red) as each stat grows big.
4input=$(cat)
5
6sep_color=240
7model_color=176
8thinking_color=108
9
10color_for_pct() {
11 local pct="${1%.*}"
12 if (( pct >= 85 )); then echo 203
13 elif (( pct >= 70 )); then echo 214
14 elif (( pct >= 50 )); then echo 114
15 elif (( pct >= 25 )); then echo 80
16 else echo 111
17 fi
18}
19
20humanize() {
21 local n=$1
22 if (( n >= 1000000 )); then
23 printf '%s' "$(awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}' | sed 's/\.0M/M/')"
24 elif (( n >= 1000 )); then
25 printf '%dk' $(( n / 1000 ))
26 else
27 printf '%d' "$n"
28 fi
29}
30
31model=$(printf '%s' "$input" | jq -r '.model.display_name // ""')
32model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1)
33[[ -z "$model_short" ]] && model_short="$model"
34
35effort=$(printf '%s' "$input" | jq -r '.effort.level // ""')
36if [[ -n "$effort" ]]; then
37 thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}"
38elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then
39 thinking="On"
40else
41 thinking="Off"
42fi
43
44segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")")
45
46ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty')
47if [[ -n "$ctx_pct" ]]; then
48 read -r ctx_used ctx_size < <(printf '%s' "$input" | jq -r '.context_window | "\(.total_input_tokens // 0) \(.context_window_size // 0)"')
49 c=$(color_for_pct "$ctx_pct")
50 if (( ctx_size > 0 )); then
51 segments+=("$(printf "\033[38;5;%sm%.0f%% ctx (%s/%s)\033[0m" "$c" "$ctx_pct" "$(humanize "$ctx_used")" "$(humanize "$ctx_size")")")
52 else
53 segments+=("$(printf "\033[38;5;%sm%.0f%% ctx\033[0m" "$c" "$ctx_pct")")
54 fi
55fi
56
57five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
58week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
59if [[ -n "$five" || -n "$week" ]]; then
60 limits=""
61 if [[ -n "$five" ]]; then
62 c=$(color_for_pct "$five")
63 limits+=$(printf "\033[38;5;%sm%.0f%% 5h\033[0m" "$c" "$(( 100 - ${five%.*} ))")
64 fi
65 if [[ -n "$week" ]]; then
66 [[ -n "$limits" ]] && limits+=$(printf " \033[38;5;%sm/\033[0m " "$sep_color")
67 c=$(color_for_pct "$week")
68 limits+=$(printf "\033[38;5;%sm%.0f%% 7d\033[0m" "$c" "$(( 100 - ${week%.*} ))")
69 fi
70 segments+=("$limits")
71fi
72
73sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color")
74line="${segments[0]}"
75for ((i = 1; i < ${#segments[@]}; i++)); do
76 line+="${sep}${segments[i]}"
77done
78printf '%s\n' "$line"
users/clover/scripts/jj-ci.ts deleted-1167
...@@ -1,1167 +0,0 @@
1#!/usr/bin/env node
2import * as child_process from "node:child_process";
3import * as fs from "node:fs";
4import * as os from "node:os";
5import * as path from "node:path";
6import * as log from "@clo/lib/log";
7import * as progress from "@clo/lib/progress";
8import * as ansi from "@clo/lib/string/ansi";
9import * as pty from "@lydell/node-pty";
10import * as ts from "@clo/lib/ts";
11import process from "node:process";
12
13const progressRoot = new progress.Root();
14progressAttachToScreen(progressRoot, log);
15
16type CommandOptions = {
17 cwd?: string;
18 env?: NodeJS.ProcessEnv;
19 ignoreWorkingCopy?: boolean;
20 node?: progress.Node;
21 plainLog?: string[];
22};
23
24type Plan = {
25 changeId: string;
26 dependencyChangeIds: string[];
27 label: string;
28 logPath: string;
29};
30
31type ScriptResult = {
32 durationMs: number;
33 exitCode: number | null;
34 ok: boolean;
35 plainText: string;
36};
37
38type JobPhase = "prep" | "ci" | "skipped" | "snapshot" | "stalled";
39type JobResult = {
40 blockedBy?: string[];
41 changeId: string;
42 durationMs: number;
43 exitCode: number | null;
44 label: string;
45 logPath?: string;
46 ok: boolean;
47 phase: JobPhase;
48 workspacePath?: string;
49};
50
51type JobState = "failed" | "ok" | "pending" | "running" | "skipped";
52
53type PoolSlot = {
54 name: string;
55 workspaceName: string;
56 workspacePath: string;
57};
58
59type ActiveJob = {
60 slot: PoolSlot;
61 task: Promise<JobResult>;
62};
63
64const dimStyle = ansi.dim + ansi.fgBrightBlack;
65const headingStyle = ansi.bold + ansi.fgBrightWhite;
66const okStyle = ansi.bold + ansi.fgBrightGreen;
67const failStyle = ansi.bold + ansi.fgBrightRed;
68const skipStyle = ansi.bold + ansi.fgBrightYellow;
69const infoStyle = ansi.bold + ansi.fgBrightCyan;
70const workspaceCacheManifestName = ".paths";
71
72let clean = false;
73let dryRun = false;
74let ignoreImmutable = false;
75let jobs = os.availableParallelism();
76let parallelize = false;
77let retry = false;
78let sawJobs = false;
79let warnedAboutPtyFallback = false;
80const revisions: string[] = [];
81
82const argv = process.argv.slice(2);
83while (argv.length > 0) {
84 const arg = argv.shift();
85 if (arg == null) break;
86
87 if (arg === "-h" || arg === "--help") usage(0);
88 else if (arg === "--dry-run") dryRun = true;
89 else if (arg === "--clean") clean = true;
90 else if (arg === "--ignore-immutable") ignoreImmutable = true;
91 else if (arg === "-p" || arg === "--parallel") parallelize = true;
92 else if (arg === "-r" || arg === "--retry") retry = true;
93 else if (arg === "-j" || arg === "--jobs" || arg === "--cores") {
94 if (sawJobs) {
95 log.error("can only pass --jobs once");
96 usage(1);
97 }
98
99 const value = argv.shift();
100 if (value == null) {
101 log.error("missing value for --jobs");
102 usage(1);
103 }
104
105 jobs = Number(value);
106 sawJobs = true;
107 } else if (arg.startsWith("--jobs=") || arg.startsWith("--cores=")) {
108 if (sawJobs) {
109 log.error("can only pass --jobs once");
110 usage(1);
111 }
112
113 jobs = Number(arg.slice(arg.indexOf("=") + 1));
114 sawJobs = true;
115 } else if (arg === "--") {
116 revisions.push(...argv);
117 break;
118 } else if (arg.startsWith("-")) {
119 log.error("unknown option", JSON.stringify(arg));
120 usage(1);
121 } else {
122 revisions.push(arg);
123 }
124}
125
126if (!Number.isInteger(jobs) || jobs < 1) {
127 log.error("--jobs must be a positive integer");
128 usage(1);
129}
130
131if (retry && revisions.length > 0) {
132 log.error("cannot pass revisions together with --retry");
133 usage(1);
134}
135
136if (!retry && revisions.length === 0) usage(1);
137
138const root = jj(["workspace", "root"]).trim();
139const cacheDir = path.join(root, ".jj", "ci");
140const lastRunPath = path.join(cacheDir, "last-run.txt");
141const logDir = path.join(cacheDir, "_logs");
142const ciScript = path.join(root, ".jj", "ci.sh");
143const gitDir = resolveGitDir(root);
144const selectedRevisions = retry ? readLastRun(lastRunPath) : revisions;
145
146let invalid = false;
147
148if (!fs.existsSync(ciScript)) {
149 log.error("missing .jj/ci.sh");
150 invalid = true;
151}
152
153if (invalid) process.exit(1);
154
155if (parallelize) {
156 if (dryRun) {
157 log.info("would run", "jj parallelize " + selectedRevisions.join(" "));
158 } else jj(["parallelize", ...selectedRevisions]);
159}
160
161const changeIds = [...new Set(selectedRevisions.flatMap(resolveChangeIds))];
162const selectedExpr = `(${changeIds.join("|")})`;
163const trackedWorkspaces = new Set(
164 jj(["workspace", "list"], { ignoreWorkingCopy: true })
165 .split("\n")
166 .filter(Boolean)
167 .map((line) => line.split(":", 1)[0]?.trim())
168 .filter((name): name is string => Boolean(name)),
169);
170const plans: Plan[] = changeIds.map((changeId) => ({
171 changeId,
172 dependencyChangeIds: [
173 ...new Set(
174 jj(
175 [
176 "log",
177 "--no-graph",
178 "-r",
179 `ancestors(${changeId}) & ${selectedExpr}`,
180 "-T",
181 'self.change_id() ++ "\n"',
182 ],
183 { ignoreWorkingCopy: true },
184 )
185 .split("\n")
186 .filter((candidate) => candidate !== "" && candidate !== changeId),
187 ),
188 ],
189 label: changeId.slice(0, 6) + ": " + jj(
190 [
191 "log",
192 "--no-graph",
193 "-r",
194 `${changeId}`,
195 "-T",
196 "self.description().first_line()",
197 ],
198 { ignoreWorkingCopy: true },
199 ),
200 logPath: path.join(logDir, `ci-${changeId}.log`),
201}));
202const concurrency = Math.min(jobs, maxPlanConcurrency(plans));
203const pool = Array.from({ length: concurrency }, (_, index): PoolSlot => {
204 const name = poolName(index);
205 return {
206 name,
207 workspaceName: `ci-${name}`,
208 workspacePath: path.join(cacheDir, name),
209 };
210});
211const planByChangeId = new Map(plans.map((plan) => [plan.changeId, plan]));
212const childrenByChangeId = new Map(
213 changeIds.map((changeId) => [changeId, [] as string[]]),
214);
215
216for (const plan of plans) {
217 for (const dependencyChangeId of plan.dependencyChangeIds) {
218 childrenByChangeId.get(dependencyChangeId)?.push(plan.changeId);
219 }
220}
221
222if (dryRun) {
223 log.writeOutput(
224 JSON.stringify(
225 {
226 cacheDir,
227 ciScript,
228 clean,
229 concurrency,
230 ignoreImmutable,
231 parallelize,
232 plans,
233 pool,
234 retry,
235 selectedRevisions,
236 root,
237 },
238 null,
239 2,
240 ) + "\n",
241 );
242 process.exit(0);
243}
244
245if (clean && fs.existsSync(cacheDir)) {
246 fs.rmSync(cacheDir, { force: true, recursive: true });
247}
248
249fs.mkdirSync(cacheDir, { recursive: true });
250fs.mkdirSync(logDir, { recursive: true });
251
252let runError: unknown = null;
253
254try {
255 const ready = plans.filter((plan) => plan.dependencyChangeIds.length === 0);
256 const availableSlots = [...pool];
257 const remainingDependenciesByChangeId = new Map(
258 plans.map((plan) => [plan.changeId, plan.dependencyChangeIds.length]),
259 );
260 const resultsByChangeId = new Map<string, JobResult>();
261 const stateByChangeId = new Map<JobResult["changeId"], JobState>(
262 changeIds.map((changeId) => [changeId, "pending"]),
263 );
264 const active = new Map<string, ActiveJob>();
265
266 {
267 using runNode = progressRoot.start("continuous integration", {
268 total: plans.length,
269 valueFormatter() {
270 let passed = 0;
271 let failed = 0;
272 for (const state of stateByChangeId.values()) {
273 if (state === "ok") passed += 1;
274 else if (state === "failed" || state === "skipped") failed += 1;
275 }
276 return `${ansi.fgGreen}${passed} pass${ansi.reset}, ${
277 failed > 0 ? ansi.fgRed : ""
278 }${failed} fail${ansi.reset}`;
279 },
280 });
281
282 while (resultsByChangeId.size < plans.length) {
283 while (
284 ready.length > 0 &&
285 active.size < concurrency &&
286 availableSlots.length > 0
287 ) {
288 const plan = ready.shift();
289 const slot = availableSlots.shift();
290 if (plan == null || slot == null) break;
291
292 stateByChangeId.set(plan.changeId, "running");
293
294 const task = (async (): Promise<JobResult> => {
295 const startedAt = Date.now();
296 using node = runNode.start(plan.label);
297 if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath);
298 const env = {
299 ...process.env,
300 JJ_CI_CHANGE_ID: plan.changeId,
301 JJ_CI_ROOT: root,
302 JJ_CI_WORKSPACE: slot.workspacePath,
303 };
304 const plainLog = [
305 `change: ${plan.changeId}\n`,
306 `dependencies: ${
307 plan.dependencyChangeIds.length === 0
308 ? "(selected root)"
309 : plan.dependencyChangeIds.join(", ")
310 }\n`,
311 `slot: ${slot.name}\n`,
312 `workspace: ${slot.workspacePath}\n`,
313 `started: ${new Date(startedAt).toISOString()}\n\n`,
314 ];
315 let phase: JobPhase = "prep";
316
317 try {
318 plainLog.push("--- prep ---\n");
319 ensureWorkspace(
320 slot,
321 plan.changeId,
322 plan.label,
323 trackedWorkspaces,
324 node,
325 plainLog,
326 );
327 plainLog.push("\n");
328
329 phase = "ci";
330 node.text = `${plan.label} ci [slot ${slot.name}]`;
331 const ci = await runScript(ciScript, {
332 cwd: slot.workspacePath,
333 env,
334 node,
335 });
336 plainLog.push(
337 `--- ci ---\nexit: ${
338 ci.exitCode ?? "signal"
339 }\nduration_ms: ${ci.durationMs}\n\n`,
340 );
341 plainLog.push(ci.plainText);
342 if (ci.plainText !== "" && !ci.plainText.endsWith("\n")) {
343 plainLog.push("\n");
344 }
345 plainLog.push("\n");
346
347 phase = "snapshot";
348 node.text = `${plan.label} snapshot [slot ${slot.name}]`;
349 plainLog.push("--- snapshot ---\n");
350 snapshotWorkspace(slot.workspacePath, node, plainLog);
351 plainLog.push("\n");
352
353 if (!ci.ok) {
354 writeFailureLog(plan.logPath, plainLog);
355 return {
356 changeId: plan.changeId,
357 durationMs: Date.now() - startedAt,
358 exitCode: ci.exitCode,
359 label: plan.label,
360 logPath: plan.logPath,
361 ok: false,
362 phase: "ci",
363 workspacePath: slot.workspacePath,
364 };
365 }
366
367 node.value += 1;
368 node.text = `${plan.label} ok [slot ${slot.name}]`;
369 if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath);
370 return {
371 changeId: plan.changeId,
372 durationMs: Date.now() - startedAt,
373 exitCode: ci.exitCode,
374 label: plan.label,
375 ok: true,
376 phase: "ci",
377 workspacePath: slot.workspacePath,
378 };
379 } catch (error) {
380 const message = error instanceof Error
381 ? (error.stack ?? error.message)
382 : String(error);
383 node.log.error(error);
384 plainLog.push(
385 `--- ${phase} error ---\n${normalizePlain(message)}\n`,
386 );
387 writeFailureLog(plan.logPath, plainLog);
388 return {
389 changeId: plan.changeId,
390 durationMs: Date.now() - startedAt,
391 exitCode: null,
392 label: plan.label,
393 logPath: plan.logPath,
394 ok: false,
395 phase,
396 workspacePath: slot.workspacePath,
397 };
398 }
399 })();
400
401 active.set(plan.changeId, { slot, task });
402 }
403
404 if (active.size === 0) {
405 let skippedAny = false;
406
407 for (const plan of plans) {
408 if (stateByChangeId.get(plan.changeId) !== "pending") continue;
409
410 const blockedBy = plan.dependencyChangeIds.filter(
411 (dependencyChangeId) => {
412 const dependencyState = stateByChangeId.get(dependencyChangeId);
413 return dependencyState === "failed" ||
414 dependencyState === "skipped";
415 },
416 );
417
418 if (blockedBy.length === 0) continue;
419
420 skippedAny = true;
421 stateByChangeId.set(plan.changeId, "skipped");
422 if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath);
423 resultsByChangeId.set(plan.changeId, {
424 blockedBy,
425 changeId: plan.changeId,
426 durationMs: 0,
427 exitCode: null,
428 label: plan.label,
429 ok: false,
430 phase: "skipped",
431 });
432 }
433
434 if (skippedAny) {
435 runNode.value = resultsByChangeId.size;
436 continue;
437 }
438
439 log.error("dependency scheduling stalled");
440 for (const plan of plans) {
441 if (stateByChangeId.get(plan.changeId) !== "pending") continue;
442
443 resultsByChangeId.set(plan.changeId, {
444 blockedBy: plan.dependencyChangeIds.filter((dependencyChangeId) =>
445 stateByChangeId.get(dependencyChangeId) !== "ok"
446 ),
447 changeId: plan.changeId,
448 durationMs: 0,
449 exitCode: null,
450 label: plan.label,
451 ok: false,
452 phase: "stalled",
453 });
454 stateByChangeId.set(plan.changeId, "failed");
455 }
456 runNode.value = resultsByChangeId.size;
457 process.exitCode = 1;
458 continue;
459 }
460
461 const result = await Promise.race(
462 [...active.values()].map(({ task }) => task),
463 );
464 const completedJob = active.get(result.changeId);
465 if (completedJob == null) {
466 throw new Error(`missing active job for ${result.changeId}`);
467 }
468 active.delete(result.changeId);
469 availableSlots.push(completedJob.slot);
470 resultsByChangeId.set(result.changeId, result);
471 stateByChangeId.set(result.changeId, result.ok ? "ok" : "failed");
472 runNode.value = resultsByChangeId.size;
473
474 if (!result.ok) continue;
475
476 for (
477 const childChangeId of childrenByChangeId.get(result.changeId) ?? []
478 ) {
479 if (stateByChangeId.get(childChangeId) !== "pending") continue;
480
481 const remainingDependencies =
482 (remainingDependenciesByChangeId.get(childChangeId) ?? 1) - 1;
483 remainingDependenciesByChangeId.set(
484 childChangeId,
485 remainingDependencies,
486 );
487
488 if (remainingDependencies === 0) {
489 const childPlan = planByChangeId.get(childChangeId);
490 if (childPlan != null) ready.push(childPlan);
491 }
492 }
493 }
494 }
495
496 const results = plans.map((plan) => {
497 const result = resultsByChangeId.get(plan.changeId);
498 if (result == null) {
499 throw new Error(`missing result for ${plan.changeId}`);
500 }
501 return result;
502 });
503 const failures = results.filter(
504 (result) => result.phase !== "skipped" && !result.ok,
505 );
506 const skipped = results.filter((result) => result.phase === "skipped");
507 const passed = results.length - failures.length - skipped.length;
508 const successfulChangeIds = results
509 .filter((result) => result.ok)
510 .map((result) => result.changeId.slice(0, 8));
511
512 if (failures.length > 0 || skipped.length > 0) {
513 writeLastRun(lastRunPath, changeIds);
514 }
515
516 const summary: string[] = [
517 "\n",
518 ansi.style(headingStyle, "summary"),
519 "\n",
520 ` ${ansi.style(okStyle, "pass:")} ${passed}/${results.length}`,
521 "\n",
522 ` ${ansi.style(failStyle, "fail:")} ${failures.length}/${results.length}`,
523 "\n",
524 ` ${ansi.style(skipStyle, "skip:")} ${skipped.length}/${results.length}`,
525 "\n",
526 ];
527
528 if (failures.length > 0) {
529 summary.push("\n", ansi.style(failStyle, "failures"), "\n");
530 for (const result of failures) {
531 summary.push(
532 `${ansi.style(failStyle, "x")} ${result.label} (${result.phase})`,
533 result.blockedBy?.length
534 ? ` ${
535 ansi.style(
536 dimStyle,
537 `[waiting on ${
538 result.blockedBy.map((id) => id.slice(0, 12)).join(", ")
539 }]`,
540 )
541 }`
542 : "",
543 result.logPath
544 ? ` ${ansi.style(dimStyle, `[log ${result.logPath}]`)}`
545 : "",
546 "\n",
547 );
548 }
549 }
550
551 if (skipped.length > 0) {
552 summary.push("\n", ansi.style(skipStyle, "skipped"), "\n");
553 for (const result of skipped) {
554 summary.push(
555 `${ansi.style(skipStyle, "-")} ${result.label}`,
556 result.blockedBy?.length
557 ? ` ${
558 ansi.style(
559 dimStyle,
560 `[blocked by ${
561 result.blockedBy.map((id) => id.slice(0, 12)).join(", ")
562 }]`,
563 )
564 }`
565 : "",
566 "\n",
567 );
568 }
569 }
570
571 if (failures.length === 0 && skipped.length === 0) {
572 summary.push(
573 "\n",
574 ansi.style(infoStyle, "all selected changes passed"),
575 "\n",
576 );
577 }
578
579 if (successfulChangeIds.length > 0) {
580 summary.push(
581 "\n",
582 `${ansi.style(infoStyle, "push successful commits:")} jj git push -c "${
583 successfulChangeIds.join("|")
584 }"`,
585 "\n",
586 );
587 }
588
589 log.writeOutput(summary.join(""));
590 process.exitCode = failures.length > 0 || skipped.length > 0 ? 1 : 0;
591} catch (error) {
592 runError = error;
593 throw error;
594} finally {
595 try {
596 forgetPoolWorkspaces(pool, trackedWorkspaces);
597 } catch (cleanupError) {
598 if (runError != null) {
599 log.error(cleanupError);
600 } else {
601 throw cleanupError;
602 }
603 }
604}
605
606function usage(code: number): never {
607 log.writeOutput(`jj ci: run continuous integration on many revisions at once
608
609usage: jj ci [options] <...revsets>
610 or: jj ci [options] --retry
611
612requires \`./.jj/ci.sh\` to setup and run checks. for example:
613
614 #!/bin/sh
615 pnpm install
616 pnpm format
617 pnpm test
618
619the script is run with fast failure, so if a command fails, it fails the run.
620
621options:
622 -j, --jobs=[n] maximum number of jobs to run at once
623 -p, --parallel rewrite the selected revisions with \`jj parallelize\`
624 -r, --retry re-run the set of commits that most recently failed
625 --ignore-immutable allow rewriting immutable commits
626 --dry-run print the execution plan
627 --clean delete the workspace cache first
628
629jobs will be run in separate workspaces placed in \`.jj/ci\`, and
630autofixes will be propagated across your revisions. if your changes are all
631separate and aren't meant to conflict, pass \`-p\` to run \`jj parallelize\`
632first. parallel commits will be run in parallel
633
634'jj ci' reuses worktrees within a single execution, then forgets the
635temporary workspaces afterward so they don't show up in \`jj log\`.
636'jj ci' speeds up multiple runs by preserving ignored files between runs (for caches).
637`);
638 process.exit(code);
639}
640
641function resolveGitDir(cwd: string): string | null {
642 const result = child_process.spawnSync(
643 "git",
644 ["-C", cwd, "rev-parse", "--absolute-git-dir"],
645 {
646 encoding: "utf8",
647 stdio: ["ignore", "pipe", "pipe"],
648 },
649 );
650 if (result.error || result.status !== 0) return null;
651
652 const value = (result.stdout ?? "").trim();
653 if (value === "") return null;
654 return value;
655}
656
657function jj(args: string[], options: CommandOptions = {}): string {
658 const result = child_process.spawnSync(
659 "jj",
660 [
661 ...(options.ignoreWorkingCopy ? ["--ignore-working-copy"] : []),
662 ...(ignoreImmutable ? ["--ignore-immutable"] : []),
663 ...args,
664 ],
665 {
666 cwd: options.cwd,
667 encoding: "utf8",
668 env: options.env,
669 stdio: ["ignore", "pipe", "pipe"],
670 },
671 );
672 const stdout = result.stdout ?? "";
673 const stderr = result.stderr ?? "";
674 const text = stdout + stderr;
675
676 if (options.node && text !== "") options.node.log.write(text);
677 if (options.plainLog && text !== "") {
678 options.plainLog.push(normalizePlain(text));
679 }
680
681 if (result.error) throw result.error;
682 if (result.status !== 0) {
683 throw new Error(
684 `jj ${args.join(" ")} failed with exit code ${result.status}` +
685 (text ? `\n\n${normalizePlain(text)}` : ""),
686 );
687 }
688
689 return stdout.trimEnd();
690}
691
692function listIgnoredWorkspacePaths(workspacePath: string): string[] | null {
693 if (gitDir == null) return null;
694
695 const result = child_process.spawnSync(
696 "git",
697 [
698 `--git-dir=${gitDir}`,
699 `--work-tree=${workspacePath}`,
700 "ls-files",
701 "--others",
702 "-i",
703 "--exclude-standard",
704 "--directory",
705 ],
706 {
707 encoding: "utf8",
708 stdio: ["ignore", "pipe", "pipe"],
709 },
710 );
711 if (result.error || result.status !== 0) return null;
712
713 return (result.stdout ?? "")
714 .split("\n")
715 .map((line) => normalizeRelativeCachePath(line))
716 .filter((line): line is string => line != null);
717}
718
719function normalizeRelativeCachePath(relativePath: string): string | null {
720 const trimmed = relativePath.trim().replace(/\/+$/g, "");
721 if (trimmed === "") return null;
722
723 const normalized = path.normalize(trimmed);
724 if (
725 normalized === ".git" ||
726 normalized === ".jj" ||
727 normalized.startsWith(`.git${path.sep}`) ||
728 normalized.startsWith(`.jj${path.sep}`) ||
729 normalized === ".." ||
730 normalized.startsWith(`..${path.sep}`) ||
731 path.isAbsolute(normalized)
732 ) {
733 return null;
734 }
735
736 return normalized;
737}
738
739function pathExists(filePath: string): boolean {
740 try {
741 fs.lstatSync(filePath);
742 return true;
743 } catch {
744 return false;
745 }
746}
747
748function ensureWorkspace(
749 slot: PoolSlot,
750 changeId: string,
751 label: string,
752 trackedWorkspaces: Set<string>,
753 node: progress.Node,
754 plainLog: string[],
755): void {
756 const hasWorkspace = trackedWorkspaces.has(slot.workspaceName);
757 const hasWorkspacePath = fs.existsSync(slot.workspacePath);
758 let createdWorkspace = false;
759
760 if (hasWorkspace && !hasWorkspacePath) {
761 log.warn("forgetting stale workspace", slot.workspaceName);
762 jj(["workspace", "forget", slot.workspaceName], {
763 ignoreWorkingCopy: true,
764 node,
765 plainLog,
766 });
767 trackedWorkspaces.delete(slot.workspaceName);
768 }
769
770 if (!hasWorkspace && hasWorkspacePath) {
771 preserveIgnoredWorkspaceCache(slot, plainLog);
772 log.warn("removing orphaned workspace", slot.workspacePath);
773 fs.rmSync(slot.workspacePath, { force: true, recursive: true });
774 }
775
776 if (!trackedWorkspaces.has(slot.workspaceName)) {
777 createdWorkspace = true;
778 jj(
779 [
780 "workspace",
781 "add",
782 "--name",
783 slot.workspaceName,
784 "--revision",
785 changeId,
786 slot.workspacePath,
787 ],
788 { node, plainLog },
789 );
790 trackedWorkspaces.add(slot.workspaceName);
791 } else {
792 jj(["workspace", "update-stale"], {
793 cwd: slot.workspacePath,
794 node,
795 plainLog,
796 });
797 }
798
799 jj(["edit", changeId], { cwd: slot.workspacePath, node, plainLog });
800 if (createdWorkspace) restoreIgnoredWorkspaceCache(slot, plainLog);
801}
802
803function snapshotWorkspace(
804 workspacePath: string,
805 node: progress.Node,
806 plainLog: string[],
807): void {
808 jj(["status"], { cwd: workspacePath, node, plainLog });
809}
810
811function workspaceCachePath(slot: PoolSlot): string {
812 return path.join(cacheDir, `${slot.name}.cache`);
813}
814
815function workspaceCacheManifestPath(slot: PoolSlot): string {
816 return path.join(workspaceCachePath(slot), workspaceCacheManifestName);
817}
818
819function preserveIgnoredWorkspaceCache(
820 slot: PoolSlot,
821 plainLog: string[],
822): void {
823 const ignoredPaths = listIgnoredWorkspacePaths(slot.workspacePath);
824 if (ignoredPaths == null) return;
825 if (ignoredPaths.length === 0) return;
826
827 const cachePath = workspaceCachePath(slot);
828 if (fs.existsSync(cachePath)) {
829 fs.rmSync(cachePath, { force: true, recursive: true });
830 }
831
832 fs.mkdirSync(cachePath, { recursive: true });
833 for (const relativePath of ignoredPaths) {
834 const sourcePath = path.join(slot.workspacePath, relativePath);
835 if (!pathExists(sourcePath)) continue;
836
837 const destinationPath = path.join(cachePath, relativePath);
838 fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
839 fs.renameSync(sourcePath, destinationPath);
840 }
841
842 fs.writeFileSync(
843 workspaceCacheManifestPath(slot),
844 ignoredPaths.join("\n") + "\n",
845 );
846 plainLog.push(
847 `preserved ignored cache paths: ${ignoredPaths.join(", ")}\n`,
848 );
849}
850
851function restoreIgnoredWorkspaceCache(
852 slot: PoolSlot,
853 plainLog: string[],
854): void {
855 const manifestPath = workspaceCacheManifestPath(slot);
856 if (!fs.existsSync(manifestPath)) return;
857
858 const cachePath = workspaceCachePath(slot);
859 const relativePaths = fs.readFileSync(manifestPath, "utf8")
860 .split("\n")
861 .map((line) => line.trim())
862 .filter(Boolean);
863
864 if (relativePaths.length === 0) {
865 fs.rmSync(cachePath, { force: true, recursive: true });
866 return;
867 }
868
869 const restoredPaths: string[] = [];
870 const skippedPaths: string[] = [];
871
872 for (const relativePath of relativePaths) {
873 const sourcePath = path.join(cachePath, relativePath);
874 if (!pathExists(sourcePath)) continue;
875
876 const destinationPath = path.join(slot.workspacePath, relativePath);
877 if (pathExists(destinationPath)) {
878 skippedPaths.push(relativePath);
879 continue;
880 }
881
882 try {
883 fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
884 fs.renameSync(sourcePath, destinationPath);
885 restoredPaths.push(relativePath);
886 } catch {
887 skippedPaths.push(relativePath);
888 }
889 }
890
891 if (restoredPaths.length > 0) {
892 plainLog.push(
893 `restored ignored cache paths: ${restoredPaths.join(", ")}\n`,
894 );
895 }
896
897 if (skippedPaths.length > 0) {
898 plainLog.push(
899 `skipped ignored cache paths: ${skippedPaths.join(", ")}\n`,
900 );
901 fs.writeFileSync(manifestPath, skippedPaths.join("\n") + "\n");
902 } else {
903 fs.rmSync(cachePath, { force: true, recursive: true });
904 }
905}
906
907function forgetPoolWorkspaces(
908 pool: PoolSlot[],
909 trackedWorkspaces: Set<string>,
910): void {
911 const errors: Error[] = [];
912
913 for (const slot of pool) {
914 if (!trackedWorkspaces.has(slot.workspaceName)) continue;
915
916 try {
917 if (fs.existsSync(slot.workspacePath)) {
918 preserveIgnoredWorkspaceCache(slot, []);
919 }
920 jj(["workspace", "forget", slot.workspaceName], {
921 cwd: root,
922 ignoreWorkingCopy: true,
923 });
924 trackedWorkspaces.delete(slot.workspaceName);
925 } catch (error) {
926 errors.push(
927 error instanceof Error ? error : new Error(String(error)),
928 );
929 }
930 }
931
932 if (errors.length > 0) {
933 throw new AggregateError(
934 errors,
935 "failed to forget one or more jj-ci workspaces",
936 );
937 }
938}
939
940function resolveChangeIds(revision: string): string[] {
941 const output = jj(
942 [
943 "log",
944 "--no-graph",
945 "-r",
946 revision,
947 "-T",
948 'self.change_id() ++ "\n"',
949 ],
950 { ignoreWorkingCopy: true },
951 );
952 const changeIds = output.split("\n").filter(Boolean);
953
954 if (changeIds.length === 0) {
955 log.error("revision resolved to no change IDs", JSON.stringify(revision));
956 process.exit(1);
957 }
958
959 return changeIds;
960}
961
962function readLastRun(lastRunPath: string): string[] {
963 if (!fs.existsSync(lastRunPath)) {
964 log.error("no failed run to retry:", lastRunPath);
965 process.exit(1);
966 }
967
968 const changeIds = fs.readFileSync(lastRunPath, "utf8")
969 .split("\n")
970 .map((line) => line.trim())
971 .filter(Boolean);
972
973 if (changeIds.length === 0) {
974 log.error("retry file is empty:", lastRunPath);
975 process.exit(1);
976 }
977
978 return changeIds;
979}
980
981function writeLastRun(lastRunPath: string, changeIds: string[]): void {
982 fs.mkdirSync(path.dirname(lastRunPath), { recursive: true });
983 fs.writeFileSync(lastRunPath, changeIds.join("\n") + "\n");
984}
985
986function runScript(
987 scriptPath: string,
988 options: CommandOptions,
989): Promise<ScriptResult> {
990 return new Promise((resolve) => {
991 const startedAt = Date.now();
992 let plainText = "";
993 const baseEnv = Object.fromEntries(
994 Object.entries({
995 CLICOLOR_FORCE: "1",
996 FORCE_COLOR: "1",
997 TERM: process.env.TERM ?? "xterm-256color",
998 ...options.env,
999 }).filter((entry): entry is [string, string] => entry[1] != null),
1000 );
1001 delete baseEnv.NO_COLOR;
1002 const preferredShell = process.env.SHELL;
1003 const shell = fs.existsSync("/bin/bash")
1004 ? "/bin/bash"
1005 : preferredShell != null && path.basename(preferredShell) === "bash"
1006 ? preferredShell
1007 : "bash";
1008
1009 const term = pty.spawn(shell, [
1010 "-c",
1011 'set -o pipefail; set -eux; source "$1"',
1012 "jj-ci",
1013 scriptPath,
1014 ], {
1015 cols: process.stdout.columns ?? 120,
1016 cwd: options.cwd,
1017 encoding: "utf8",
1018 env: baseEnv,
1019 name: process.env.TERM ?? "xterm-256color",
1020 rows: process.stdout.rows ?? 30,
1021 });
1022
1023 term.onData((chunk) => {
1024 plainText += normalizePlain(chunk);
1025 options.node?.log.write(chunk);
1026 });
1027
1028 term.onExit(({ exitCode, signal }) => {
1029 if (signal != null) {
1030 const note = ansi.style(failStyle, `terminated by signal ${signal}`);
1031 plainText += normalizePlain(note);
1032 options.node?.log.info(note);
1033 }
1034 resolve({
1035 durationMs: Date.now() - startedAt,
1036 exitCode,
1037 ok: exitCode === 0,
1038 plainText,
1039 });
1040 });
1041 });
1042}
1043
1044function writeFailureLog(logPath: string, plainLog: string[]): void {
1045 fs.mkdirSync(path.dirname(logPath), { recursive: true });
1046 fs.writeFileSync(logPath, plainLog.join(""));
1047}
1048
1049function normalizePlain(text: string): string {
1050 return ansi.strip(text)
1051 .replace(/\r\n/g, "\n")
1052 .replace(/\r/g, "\n");
1053}
1054
1055function poolName(index: number): string {
1056 let value = index;
1057 let name = "";
1058
1059 while (value >= 0) {
1060 name = String.fromCharCode(97 + (value % 26)) + name;
1061 value = Math.floor(value / 26) - 1;
1062 }
1063
1064 return name;
1065}
1066
1067function maxPlanConcurrency(plans: Plan[]): number {
1068 if (plans.length === 0) return 0;
1069
1070 const descendantsByChangeId = new Map(
1071 plans.map((plan) => [plan.changeId, [] as string[]]),
1072 );
1073
1074 for (const plan of plans) {
1075 for (const dependencyChangeId of plan.dependencyChangeIds) {
1076 descendantsByChangeId.get(dependencyChangeId)?.push(plan.changeId);
1077 }
1078 }
1079
1080 const matchedDependencyByDescendant = new Map<string, string>();
1081 let matchCount = 0;
1082
1083 for (const plan of plans) {
1084 if (
1085 tryMatchDescendant(
1086 plan.changeId,
1087 descendantsByChangeId,
1088 matchedDependencyByDescendant,
1089 new Set(),
1090 )
1091 ) {
1092 matchCount += 1;
1093 }
1094 }
1095
1096 return plans.length - matchCount;
1097}
1098
1099function tryMatchDescendant(
1100 dependencyChangeId: string,
1101 descendantsByChangeId: Map<string, string[]>,
1102 matchedDependencyByDescendant: Map<string, string>,
1103 seenDescendants: Set<string>,
1104): boolean {
1105 for (
1106 const descendantChangeId of descendantsByChangeId.get(dependencyChangeId) ??
1107 []
1108 ) {
1109 if (seenDescendants.has(descendantChangeId)) continue;
1110 seenDescendants.add(descendantChangeId);
1111
1112 const matchedDependency = matchedDependencyByDescendant.get(
1113 descendantChangeId,
1114 );
1115 if (
1116 matchedDependency == null ||
1117 tryMatchDescendant(
1118 matchedDependency,
1119 descendantsByChangeId,
1120 matchedDependencyByDescendant,
1121 seenDescendants,
1122 )
1123 ) {
1124 matchedDependencyByDescendant.set(descendantChangeId, dependencyChangeId);
1125 return true;
1126 }
1127 }
1128
1129 return false;
1130}
1131
1132function progressAttachToScreen(
1133 root: progress.Root,
1134 { writeOutput, startWidget }: Pick<
1135 log.WidgetHost,
1136 "writeOutput" | "startWidget"
1137 >,
1138): ts.Dispose {
1139 const stack = new DisposableStack();
1140 let widget: log.WidgetInstance | null = null;
1141
1142 stack.use(root.on("change", (items) => {
1143 if (items.length > 0) {
1144 widget ??= startWidget({
1145 format: ({ now }) => progress.formatAnsi(now, root.active),
1146 }) ?? null;
1147 if (!widget) return;
1148 widget.fps = items.some((x) => x.showTotal !== false && x.total > 0)
1149 ? 15
1150 : null;
1151 widget.redraw();
1152 } else {
1153 widget?.stop();
1154 widget = null;
1155 }
1156 }));
1157 stack.use(root.on("node-detached-log", (msg) => {
1158 writeOutput(log.formatAnsiMessage(msg, true));
1159 }));
1160 stack.use(root.on("node-end", (node) => {
1161 let title = node.text;
1162 let p: progress.ReadOnlyNode | null = node;
1163 while (p = p.parent) title = p.text + " / " + title;
1164 }));
1165
1166 return ts.defer(() => stack[Symbol.dispose]);
1167}
users/clover/tsconfig.json deleted-18
...@@ -1,18 +0,0 @@
1{
2 "compilerOptions": {
3 "allowImportingTsExtensions": true,
4 "baseUrl": ".",
5 "lib": ["esnext", "es2024", "dom", "dom.iterable"],
6 "module": "nodenext",
7 "noEmit": true,
8 "rootDir": ".",
9 "skipLibCheck": true,
10 "strict": true,
11 "verbatimModuleSyntax": true,
12 "target": "esnext",
13 "noUncheckedIndexedAccess": true,
14 "resolveJsonModule": true,
15 "types": ["node"]
16 },
17 "include": ["./**/*"]
18}
users/clover/work/configuration.nix+7
...@@ -6,6 +6,13 @@...@@ -6,6 +6,13 @@
6 nix.optimise.automatic = lib.mkForce false;6 nix.optimise.automatic = lib.mkForce false;
7 nix.gc.automatic = lib.mkForce false;7 nix.gc.automatic = lib.mkForce false;
88
9 homebrew.casks = [
10 "granola"
11 "pgadmin4"
12 "visual-studio-code"
13 "wispr-flow"
14 ];
15
9 services.skhd = {16 services.skhd = {
10 enable = true;17 enable = true;
11 skhdConfig = ''18 skhdConfig = ''
users/clover/work/home.nix-1
...@@ -19,7 +19,6 @@...@@ -19,7 +19,6 @@
19 awscli219 awscli2
20 deno20 deno
21 doppler21 doppler
22 nodejs_26
23 rustup22 rustup
24 ssm-session-manager-plugin23 ssm-session-manager-plugin
25 terraform24 terraform
users/clover/zsh/default.nix created+19
...@@ -0,0 +1,19 @@
1{ config, ... }:
2{
3 programs.zsh = {
4 enable = true;
5 dotDir = "${config.xdg.configHome}/zsh";
6 shellAliases = {
7 switch = "~/config/switch";
8
9 jf = "jj git fetch";
10 e = "jj edit";
11 d = "jj desc";
12 moveself = "jj rebase -s @ -d";
13 logall = "jj log -r 'all()'";
14 um = "jj log -r 'unmerged()'";
15 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";
16 };
17 initContent = builtins.readFile ./init.zsh;
18 };
19}
users/clover/zsh/init.zsh created+240
...@@ -0,0 +1,240 @@
1# macOS hands GUI-launched processes a 256 fd soft limit, which bundler
2# file watchers exhaust during their first build.
3ulimit -n 65536 2>/dev/null
4
5function _update_prompt_theme() {
6 if [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]]; then
7 PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ "
8 else
9 PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ "
10 fi
11}
12precmd_functions+=(_update_prompt_theme)
13_update_prompt_theme
14
15function python() {
16 dirname=$(dirname $1 2>/dev/null)
17 if [ -z "$dirname" ]; then
18 dirname=$(pwd)
19 fi
20 uv run --project "$dirname" "$@"
21}
22
23function bun() {
24 if [[ "$1" = "build" ]]; then
25 command bun run "$@"
26 elif [[ "$1" = "bundle" ]]; then
27 shift
28 command bun build "$@"
29 else
30 command bun "$@"
31 fi
32}
33
34function pr() {
35 local branch remote repo url
36 branch="$(
37 command jj log -r '@ | @-' -T 'bookmarks.map(|b| b.name()).join("\n") ++ "\n"' --no-graph 2>/dev/null |
38 while IFS= read -r bookmark; do
39 case "$bookmark" in
40 "" | main | master | trunk | deployment*) ;;
41 *) printf '%s' "$bookmark"; break ;;
42 esac
43 done
44 )"
45 if [ -z "$branch" ]; then
46 printf 'no jj bookmark found on @ or @-\n' >&2
47 return 1
48 fi
49
50 remote="$(
51 command jj git remote list 2>/dev/null |
52 while IFS=' ' read -r name remote_url _; do
53 if [ "$name" = "origin" ]; then
54 printf '%s' "$remote_url"
55 break
56 fi
57 done
58 )"
59 case "$remote" in
60 git@github.com:*) repo="${remote#git@github.com:}" ;;
61 ssh://git@github.com/*) repo="${remote#ssh://git@github.com/}" ;;
62 https://github.com/*) repo="${remote#https://github.com/}" ;;
63 http://github.com/*) repo="${remote#http://github.com/}" ;;
64 *)
65 printf 'origin is not a github remote: %s\n' "$remote" >&2
66 return 1
67 ;;
68 esac
69
70 repo="${repo%.git}"
71 url="https://github.com/$repo/pull/new/$branch?labels=auto-merge"
72 command open "$url"
73}
74
75function _jjImmutableRevision() {
76 emulate -L zsh
77 setopt extendedglob
78 local line
79 while IFS= read -r line; do
80 line="${line//$'\e'\[[0-9;]##m/}"
81 if [[ "$line" = *"Could not modify commit: "* ]]; then
82 line="${line#*Could not modify commit: }"
83 printf '%s' "${line%% *}"
84 return 0
85 fi
86 done < "$1"
87 return 1
88}
89
90function _runJjStreaming() {
91 emulate -L zsh
92 setopt no_multios
93 local stderr arg has_color
94 local -a jj_color_arg pipe_status
95 stderr="$1"
96 shift
97 if [[ -t 1 && -t 2 ]]; then
98 has_color=false
99 for arg in "$@"; do
100 if [[ "$arg" = "--color" || "$arg" = --color=* ]]; then
101 has_color=true
102 break
103 fi
104 done
105 if ! $has_color; then
106 jj_color_arg=(--color=always)
107 fi
108 fi
109 {
110 command jj "$jj_color_arg[@]" "$@" 2>&1 1>&3 | tee "$stderr" >&2
111 pipe_status=("${pipestatus[@]}")
112 } 3>&1
113 return $pipe_status[1]
114}
115
116function _jjHandleFailure() {
117 local stderr jj_status reply
118 stderr="$1"
119 jj_status="$2"
120 shift 2
121 if [[ $jj_status -ne 0 ]]; then
122 if _jjImmutableRevision "$stderr" >/dev/null && [[ -t 0 ]]; then
123 printf 'Rewrite immutable commit anyways [y/N] ' >&2
124 read -r reply
125 case "$reply" in
126 [Yy] | [Yy][Ee][Ss])
127 _runJj "$@" --ignore-immutable
128 return $?
129 ;;
130 esac
131 return $jj_status
132 fi
133 fi
134 return $jj_status
135}
136
137function _runJj() {
138 local stderr jj_status
139 stderr="$(mktemp)"
140 _runJjStreaming "$stderr" "$@"
141 jj_status=$?
142 _jjHandleFailure "$stderr" "$jj_status" "$@"
143 jj_status=$?
144 rm -f "$stderr"
145 return $jj_status
146}
147
148function wrapWithAutoInstall() {
149 local root file cmd before after wrapped_status
150 root="$(command jj workspace root)"
151 if [ -n "$root" ]; then
152 if [ -e "$root/bun.lock" ]; then
153 file="$root/bun.lock"
154 cmd="bun i -f"
155 elif [ -e "$root/package-lock.json" ]; then
156 file="$root/package-lock.json"
157 cmd="npm i"
158 elif [ -e "$root/pnpm-lock.yaml" ]; then
159 file="$root/pnpm-lock.yaml"
160 cmd="pnpm i"
161 elif [ -e "$root/Cargo.lock" ]; then
162 file="$root/Cargo.lock"
163 cmd="cargo fetch"
164 fi
165 fi
166 if [ -n "$file" ]; then
167 before="$(sha1sum "$file")"
168 fi
169 _runJj "$@"
170 wrapped_status=$?
171 if [[ $wrapped_status -ne 0 ]]; then
172 return $wrapped_status
173 fi
174 if [ -n "$file" ] && [ -e "$file" ]; then
175 after="$(sha1sum "$file")"
176 if [ "$after" != "$before" ]; then
177 eval "$cmd"
178 return $?
179 fi
180 fi
181 return $wrapped_status
182}
183
184function jj() {
185 if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then
186 if ! command jj log -r "$2" --no-graph -T "" >/dev/null 2>&1 && command jj log -r "$2@origin" --no-graph -T "" >/dev/null 2>&1; then
187 wrapWithAutoInstall new "$2@origin"
188 else
189 wrapWithAutoInstall "$@"
190 fi
191 elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then
192 # confirm before pushing, but only when the dry-run has something to push
193 local dry dry_status
194 dry="$(command jj "$@" --dry-run 2>&1)"
195 dry_status=$?
196 print -r -- "$dry"
197 if [[ $dry_status -ne 0 ]]; then
198 return $dry_status
199 fi
200 if [[ "$dry" = *"No bookmarks/tags found in the default push revset"* && "$dry" = *"Nothing changed."* && -n "$(command jj log -r '@ & ~empty()' --no-graph -T change_id 2>/dev/null)" ]]; then
201 local answer
202 printf "No bookmark on @; move previous bookmark to @ and push? [y/N] "
203 read -r answer
204 case "$answer" in
205 y|Y|yes|YES)
206 bup || return $?
207 dry="$(command jj "$@" --dry-run 2>&1)"
208 dry_status=$?
209 print -r -- "$dry"
210 if [[ $dry_status -ne 0 ]]; then
211 return $dry_status
212 fi
213 if [[ "$dry" != *"Nothing changed."* ]]; then
214 _runJj "$@"
215 return $?
216 fi
217 ;;
218 esac
219 fi
220 if [[ "$dry" != *"Nothing changed."* ]]; then
221 printf "Press any key to continue... "
222 read -r
223 _runJj "$@"
224 return $?
225 fi
226 return 0
227 elif [[ "$1" = "undo" || "$1" = "redo" ]]; then
228 _runJj "$@"
229 else
230 # auto-install deps when switching commits
231 wrapWithAutoInstall "$@"
232 fi
233}
234
235function bup() {
236 if [[ -z "$(command jj log -r @ -T description --no-graph 2>/dev/null)" ]]; then
237 command jj desc -m 'chore: follow-up changes'
238 fi
239 command jj b m --from 'heads(::@- & bookmarks()) ~ (trunk() | bookmarks(glob:"deployment*"))' --to '@'
240}