authorgravatar for nmarks413@gmail.comNatalie Marks <nmarks413@gmail.com> 2026-06-01 13:27:35-07:00
committergravatar for nmarks413@gmail.comNatalie Marks <nmarks413@gmail.com> 2026-06-01 13:27:35-07:00
logf60c39943834cc26bc64c715c9b94d541e98526a
treee11c4e5957192a68624e526b980cc5e543c252ce
parent4d3eb16274be590e53bfb3ebd10f3ffa2db56d56
parentcec0cd905ddaec1f0ddce5cbd2c022ca6dc82a4c

Merge branch 'main' of paperclover.dev:nix/config


20 files changed, 1110 insertions(+), 71 deletions(-)

flake.nix+2-21
......@@ -74,31 +74,11 @@
7474 };
7575 });
7676 })
77 # https://github.com/NixOS/nixpkgs/issues/476669#issuecomment-3707850520
78 (_: prev: {
79 vesktop = prev.vesktop.overrideAttrs (_: {
80 preBuild = ''
81 cp -r ${prev.electron.dist} electron-dist
82 chmod -R u+w electron-dist
83 '';
84 buildPhase = ''
85 runHook preBuild
86
87 pnpm build
88 pnpm exec electron-builder \
89 --dir \
90 -c.asarUnpack="**/*.node" \
91 -c.electronDist="electron-dist" \
92 -c.electronVersion=${prev.electron.version}
93
94 runHook postBuild
95 '';
96 });
97 })
9877
9978 # custom packages
10079 (_: pkgs: {
10180 autofmt = pkgs.callPackage ./packages/autofmt { };
81 "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { };
10282 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };
10383 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };
10484 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };
......@@ -133,6 +113,7 @@
133113 nvim-natalie = mkNeovim "natalie" pkgs;
134114
135115 inherit (pkgs) autofmt;
116 "jj-import-merge" = pkgs."jj-import-merge";
136117 }
137118 // lib.optionalAttrs (system == "aarch64-darwin") {
138119 # "nix run .#darwin-rebuild"
modules/home/default.nix+1
......@@ -149,6 +149,7 @@ in
149149 behavior = "drop";
150150 };
151151 git = {
152 colocate = false;
152153 sign-on-push = config.programs.git.signing.key != null;
153154 };
154155 remotes.origin = {
modules/macos/tiling/aerospace.nix+1-1
......@@ -34,7 +34,7 @@
3434 outer = {
3535 left = 0;
3636 bottom = 0;
37 top = 40;
37 top = 10;
3838 right = 0;
3939 };
4040 };
packages/autofmt/autofmt.js+8-4
......@@ -100,12 +100,14 @@ const formatters = {
100100 },
101101 prettier: {
102102 languages: ["javascript", "markdown", "json", "yaml", "mdx", "html", "css"],
103 mustMatchFile: true,
103104 files: ["node_modules/.bin/prettier"],
104105 cmd: ["node_modules/.bin/prettier", "--write", "--", "$files"],
105106 stdin: (file) => ["node_modules/.bin/prettier", "--stdin-filepath", file],
106107 },
107108 biome: {
108109 languages: ["javascript", "json", "css"],
110 mustMatchFile: true,
109111 files: ["node_modules/.bin/biome"],
110112 cmd: ["node_modules/.bin/biome", "format", "--", "$files"],
111113 stdin: (file) => [
......@@ -542,20 +544,22 @@ function pickFormatter(file) {
542544 : Object.entries(formatters[fmt].files)
543545 .map(([base, check]) => ({ base, check }));
544546 for (const { base, check } of filesToCheck) {
547 let fileMatches;
545548 if (base.includes("/")) {
546 matches = readDir(path.join(x, path.dirname(base))) //
549 fileMatches = readDir(path.join(x, path.dirname(base))) //
547550 .includes(path.basename(base));
548551 } else {
549 matches = children.includes(base);
552 fileMatches = children.includes(base);
550553 }
551 if (matches && typeof check === "function") {
554 if (fileMatches && typeof check === "function") {
552555 let text;
553 matches = !!check({
556 fileMatches = !!check({
554557 get text() {
555558 return text ??= fs.readFileSync(path.join(x, base), "utf-8");
556559 },
557560 });
558561 }
562 matches ||= fileMatches;
559563 }
560564 }
561565 if (matches) {
packages/autofmt/default.nix+13-3
......@@ -1,6 +1,5 @@
11{ pkgs, ... }:
2pkgs.writeShellApplication {
3 name = "autofmt";
2let
43 runtimeInputs = with pkgs; [
54 # include only a couple of formatters by default
65 clang-tools
......@@ -11,5 +10,16 @@ pkgs.writeShellApplication {
1110 rustfmt
1211 zig
1312 ];
14 text = ''exec ${pkgs.nodejs}/bin/node ${./autofmt.js} "$@"'';
13in
14pkgs.writeTextFile {
15 name = "autofmt";
16 destination = "/bin/autofmt";
17 executable = true;
18 text = builtins.replaceStrings
19 [ "#!/usr/bin/env node" ]
20 [
21 ''#!${pkgs.nodejs}/bin/node
22 process.env.PATH = "${pkgs.lib.makeBinPath runtimeInputs}:" + (process.env.PATH ?? "");''
23 ]
24 (builtins.readFile ./autofmt.js);
1525}
packages/jj-import-merge/default.nix created+9
......@@ -0,0 +1,9 @@
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 created+871
......@@ -0,0 +1,871 @@
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(`merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts\n`);
90 stream.write(`\n`);
91 stream.write(`options:\n`);
92 stream.write(` --all-jj resolve all conflicted JS/TS files from 'jj resolve --list'\n`);
93 stream.write(` -h show help\n`);
94 process.exit(code);
95}
96
97function listJjConflictFiles() {
98 const result = child_process.spawnSync(
99 "jj",
100 ["--color=never", "--quiet", "resolve", "--list"],
101 { encoding: "utf8" },
102 );
103
104 if (result.error) {
105 throw new Error(`failed to run jj: ${result.error.message}`);
106 }
107 if (result.status !== 0) {
108 throw new Error(result.stderr.trim() || "jj resolve --list failed");
109 }
110
111 return result.stdout
112 .split(/\r?\n/)
113 .map((line) => line.trimEnd())
114 .filter(Boolean)
115 .map((line) => line.match(/^(.*?)\s{2,}.+conflict$/))
116 .filter(Boolean)
117 .map((match) => match[1]);
118}
119
120function processFile(file) {
121 if (!SUPPORTED_EXTENSIONS.has(path.extname(file))) {
122 return { kind: "noop" };
123 }
124
125 let original;
126 try {
127 original = fs.readFileSync(file, "utf8");
128 } catch (error) {
129 const message = error instanceof Error ? error.message : String(error);
130 throw new Error(`cannot read ${file}: ${message}`);
131 }
132
133 if (!CONFLICT_START_RE.test(original)) {
134 return { kind: "noop" };
135 }
136
137 const parsed = parseConflictFile(original);
138 if (!parsed.supported) {
139 return { kind: "noop" };
140 }
141
142 const leftText = parsed.parts.map((part) => part.a).join("");
143 const rightText = parsed.parts.map((part) => part.b).join("");
144 const leftImports = extractTopImportBlock(leftText);
145 const rightImports = extractTopImportBlock(rightText);
146 if (!leftImports || !rightImports) {
147 return { kind: "noop" };
148 }
149
150 const leftPrelude = leftText.slice(0, leftImports.start);
151 const rightPrelude = rightText.slice(0, rightImports.start);
152 if (leftPrelude !== rightPrelude) {
153 return { kind: "noop" };
154 }
155
156 const start = projectBoundaryToOriginal(
157 parsed.parts,
158 leftImports.start,
159 rightImports.start,
160 );
161 const end = projectBoundaryToOriginal(parsed.parts, leftImports.end, rightImports.end);
162 if (start == null || end == null || end < start) {
163 return { kind: "noop" };
164 }
165
166 const mergedImports = mergeImportStatements(
167 leftImports.statements,
168 rightImports.statements,
169 detectNewline(original),
170 );
171 const updated = original.slice(0, start) + mergedImports + original.slice(end);
172
173 if (updated === original) {
174 return { kind: "noop" };
175 }
176
177 try {
178 fs.writeFileSync(file, updated, "utf8");
179 } catch (error) {
180 const message = error instanceof Error ? error.message : String(error);
181 throw new Error(`cannot write ${file}: ${message}`);
182 }
183
184 if (hasConflictMarkers(updated)) {
185 return { kind: "rewritten" };
186 }
187
188 const formatResult = child_process.spawnSync("autofmt", [file], {
189 encoding: "utf8",
190 stdio: "inherit",
191 });
192
193 if (formatResult.error || formatResult.status !== 0) {
194 fs.writeFileSync(file, original, "utf8");
195 if (formatResult.error) {
196 throw new Error(`autofmt failed for ${file}: ${formatResult.error.message}`);
197 }
198 throw new Error(`autofmt failed for ${file}`);
199 }
200
201 return { kind: "formatted" };
202}
203
204function parseConflictFile(text) {
205 const lines = splitLines(text);
206 const parts = [];
207
208 let index = 0;
209 while (index < lines.length) {
210 const line = lines[index].text;
211 if (!line.startsWith("<<<<<<< conflict ")) {
212 const start = index;
213 index += 1;
214 while (
215 index < lines.length &&
216 !lines[index].text.startsWith("<<<<<<< conflict ")
217 ) {
218 index += 1;
219 }
220 const raw = joinLines(lines, start, index);
221 parts.push({ type: "plain", orig: raw, a: raw, b: raw });
222 continue;
223 }
224
225 const block = parseConflictBlock(lines, index);
226 if (!block.supported) {
227 return { supported: false, parts: [] };
228 }
229 parts.push(block.part);
230 index = block.nextIndex;
231 }
232
233 return { supported: true, parts };
234}
235
236function parseConflictBlock(lines, startIndex) {
237 let index = startIndex + 1;
238 const entries = [];
239
240 while (index < lines.length) {
241 const line = lines[index].text;
242 if (line.startsWith(">>>>>>> conflict ")) {
243 break;
244 }
245 if (line.startsWith("+++++++ ")) {
246 const contentStart = index + 1;
247 index += 1;
248 while (
249 index < lines.length &&
250 !isConflictEntryStart(lines[index].text) &&
251 !lines[index].text.startsWith(">>>>>>> conflict ")
252 ) {
253 index += 1;
254 }
255 entries.push({
256 kind: "explicit",
257 text: joinLines(lines, contentStart, index),
258 });
259 continue;
260 }
261 if (line.startsWith("%%%%%%% diff from:")) {
262 if (index + 1 >= lines.length || !/^\\\\+\s*to:/.test(lines[index + 1].text)) {
263 return { supported: false, nextIndex: startIndex + 1 };
264 }
265 index += 2;
266 let text = "";
267 while (
268 index < lines.length &&
269 !isConflictEntryStart(lines[index].text) &&
270 !lines[index].text.startsWith(">>>>>>> conflict ")
271 ) {
272 const diffLine = lines[index].text;
273 const prefix = diffLine[0];
274 if (prefix === " " || prefix === "+") {
275 text += diffLine.slice(1);
276 } else if (prefix !== "-") {
277 return { supported: false, nextIndex: startIndex + 1 };
278 }
279 index += 1;
280 }
281 entries.push({ kind: "diff", text });
282 continue;
283 }
284 return { supported: false, nextIndex: startIndex + 1 };
285 }
286
287 if (
288 index >= lines.length ||
289 !lines[index].text.startsWith(">>>>>>> conflict ") ||
290 entries.length !== 2
291 ) {
292 return { supported: false, nextIndex: startIndex + 1 };
293 }
294
295 return {
296 supported: true,
297 nextIndex: index + 1,
298 part: {
299 type: "conflict",
300 orig: joinLines(lines, startIndex, index + 1),
301 a: entries[0].text,
302 b: entries[1].text,
303 },
304 };
305}
306
307function isConflictEntryStart(line) {
308 return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:");
309}
310
311function splitLines(text) {
312 const lines = [];
313 let start = 0;
314
315 for (let index = 0; index < text.length; index += 1) {
316 if (text[index] !== "\n") continue;
317 lines.push({
318 start,
319 end: index + 1,
320 text: text.slice(start, index + 1),
321 });
322 start = index + 1;
323 }
324
325 if (start < text.length) {
326 lines.push({ start, end: text.length, text: text.slice(start) });
327 }
328
329 return lines;
330}
331
332function joinLines(lines, start, end) {
333 let text = "";
334 for (let index = start; index < end; index += 1) {
335 text += lines[index].text;
336 }
337 return text;
338}
339
340function extractTopImportBlock(text) {
341 const lines = splitLines(text);
342 if (lines.length === 0) return null;
343
344 let index = 0;
345 if (lines[0]?.text.startsWith("#!")) {
346 index += 1;
347 }
348
349 index = skipPrelude(lines, index);
350 if (index == null || index >= lines.length) {
351 return null;
352 }
353
354 const first = parseImportStatement(lines, index);
355 if (!first) {
356 return null;
357 }
358
359 const start = lines[index].start;
360 const statements = [first.statement];
361 let end = offsetForLine(lines, first.nextIndex);
362 index = first.nextIndex;
363
364 while (true) {
365 const afterTrivia = skipBlankAndComments(lines, index);
366 if (afterTrivia == null) {
367 return null;
368 }
369
370 const next = parseImportStatement(lines, afterTrivia);
371 if (!next) {
372 break;
373 }
374
375 statements.push(next.statement);
376 end = offsetForLine(lines, next.nextIndex);
377 index = next.nextIndex;
378 }
379
380 return { start, end, statements };
381}
382
383function skipPrelude(lines, startIndex) {
384 let index = startIndex;
385 while (index < lines.length) {
386 const afterTrivia = skipBlankAndComments(lines, index);
387 if (afterTrivia == null) {
388 return null;
389 }
390 if (afterTrivia >= lines.length) {
391 return afterTrivia;
392 }
393
394 const trimmed = lines[afterTrivia].text.trim();
395 if (isDirectiveLine(trimmed)) {
396 index = afterTrivia + 1;
397 continue;
398 }
399 return afterTrivia;
400 }
401 return index;
402}
403
404function skipBlankAndComments(lines, startIndex) {
405 let index = startIndex;
406
407 while (index < lines.length) {
408 const trimmed = lines[index].text.trim();
409 if (trimmed === "") {
410 index += 1;
411 continue;
412 }
413 if (trimmed.startsWith("//")) {
414 index += 1;
415 continue;
416 }
417 if (trimmed.startsWith("/*")) {
418 index = skipBlockComment(lines, index);
419 if (index == null) {
420 return null;
421 }
422 continue;
423 }
424 break;
425 }
426
427 return index;
428}
429
430function skipBlockComment(lines, startIndex) {
431 for (let index = startIndex; index < lines.length; index += 1) {
432 if (lines[index].text.includes("*/")) {
433 return index + 1;
434 }
435 }
436 return null;
437}
438
439function isDirectiveLine(line) {
440 return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line);
441}
442
443function parseImportStatement(lines, startIndex) {
444 if (startIndex >= lines.length) {
445 return null;
446 }
447
448 const firstLine = lines[startIndex].text.trimStart();
449 if (!firstLine.startsWith("import")) {
450 return null;
451 }
452 if (/^import(?:\s*\(|\s*\.)/.test(firstLine)) {
453 return null;
454 }
455
456 let raw = "";
457 const state = {
458 braceDepth: 0,
459 inBlockComment: false,
460 stringQuote: null,
461 };
462
463 for (let index = startIndex; index < lines.length; index += 1) {
464 raw += lines[index].text;
465 scanLine(lines[index].text, state);
466
467 if (state.inBlockComment || state.braceDepth !== 0) {
468 continue;
469 }
470
471 const analysis = parseImportSemantics(raw);
472 if (analysis) {
473 return {
474 nextIndex: index + 1,
475 statement: raw,
476 };
477 }
478 }
479
480 return null;
481}
482
483function scanLine(line, state) {
484 for (let index = 0; index < line.length; index += 1) {
485 const char = line[index];
486 const next = line[index + 1];
487
488 if (state.inBlockComment) {
489 if (char === "*" && next === "/") {
490 state.inBlockComment = false;
491 index += 1;
492 }
493 continue;
494 }
495
496 if (state.stringQuote) {
497 if (char === "\\") {
498 index += 1;
499 continue;
500 }
501 if (char === state.stringQuote) {
502 state.stringQuote = null;
503 }
504 continue;
505 }
506
507 if (char === "/" && next === "/") {
508 break;
509 }
510 if (char === "/" && next === "*") {
511 state.inBlockComment = true;
512 index += 1;
513 continue;
514 }
515 if (char === "'" || char === "\"" || char === "`") {
516 state.stringQuote = char;
517 continue;
518 }
519 if (char === "{") {
520 state.braceDepth += 1;
521 } else if (char === "}") {
522 state.braceDepth = Math.max(0, state.braceDepth - 1);
523 }
524 }
525}
526
527function parseImportSemantics(raw) {
528 const normalized = collapseWhitespace(raw);
529 if (!normalized.startsWith("import ")) {
530 return null;
531 }
532
533 const sideEffectMatch = normalized.match(
534 /^import\s+(["'])(.*?)\1(\s+with\s+\{.*\})?\s*;?$/,
535 );
536 if (sideEffectMatch) {
537 return {
538 kind: "side-effect",
539 source: sideEffectMatch[2],
540 quote: sideEffectMatch[1],
541 attributes: sideEffectMatch[3] ?? "",
542 key: `side:${sideEffectMatch[2]}:${sideEffectMatch[3] ?? ""}`,
543 };
544 }
545
546 const bindingMatch = normalized.match(
547 /^import\s+(type\s+)?(.+?)\s+from\s+(["'])(.*?)\3(\s+with\s+\{.*\})?\s*;?$/,
548 );
549 if (!bindingMatch) {
550 return null;
551 }
552
553 const parsed = parseBindingSpec(bindingMatch[2].trim());
554 if (!parsed) {
555 return null;
556 }
557
558 const named = parsed.namedSpecifiers.join(",");
559 return {
560 kind: "binding",
561 isTypeImport: !!bindingMatch[1],
562 defaultBinding: parsed.defaultBinding,
563 namespaceImport: parsed.namespaceImport,
564 namedSpecifiers: parsed.namedSpecifiers,
565 source: bindingMatch[4],
566 quote: bindingMatch[3],
567 attributes: bindingMatch[5] ?? "",
568 key: [
569 "binding",
570 bindingMatch[1] ? "type" : "value",
571 parsed.defaultBinding ?? "",
572 parsed.namespaceImport ?? "",
573 named,
574 bindingMatch[4],
575 bindingMatch[5] ?? "",
576 ].join(":"),
577 };
578}
579
580function parseBindingSpec(specPart) {
581 let defaultBinding = null;
582 let namespaceImport = null;
583 let namedSpecifiers = [];
584
585 const braceStart = specPart.indexOf("{");
586 const braceEnd = specPart.lastIndexOf("}");
587 if (braceStart !== -1 || braceEnd !== -1) {
588 if (braceStart === -1 || braceEnd === -1 || braceEnd < braceStart) {
589 return null;
590 }
591
592 const beforeBrace = specPart
593 .slice(0, braceStart)
594 .trim()
595 .replace(/,\s*$/, "")
596 .trim();
597 const afterBrace = specPart.slice(braceEnd + 1).trim();
598 if (afterBrace !== "") {
599 return null;
600 }
601
602 const head = parseBindingHead(beforeBrace);
603 if (!head) {
604 return null;
605 }
606 defaultBinding = head.defaultBinding;
607 namespaceImport = head.namespaceImport;
608 namedSpecifiers = splitCommaList(specPart.slice(braceStart + 1, braceEnd));
609 } else {
610 const head = parseBindingHead(specPart);
611 if (!head) {
612 return null;
613 }
614 defaultBinding = head.defaultBinding;
615 namespaceImport = head.namespaceImport;
616 }
617
618 return {
619 defaultBinding,
620 namespaceImport,
621 namedSpecifiers,
622 };
623}
624
625function parseBindingHead(head) {
626 if (head === "") {
627 return { defaultBinding: null, namespaceImport: null };
628 }
629
630 if (head.startsWith("* as ")) {
631 return { defaultBinding: null, namespaceImport: head };
632 }
633
634 const parts = head.split(/\s*,\s*/).filter(Boolean);
635 if (parts.length === 1) {
636 return {
637 defaultBinding: parts[0],
638 namespaceImport: null,
639 };
640 }
641
642 if (parts.length === 2 && parts[1].startsWith("* as ")) {
643 return {
644 defaultBinding: parts[0],
645 namespaceImport: parts[1],
646 };
647 }
648
649 return null;
650}
651
652function splitCommaList(text) {
653 return text
654 .split(",")
655 .map((part) => collapseWhitespace(part))
656 .filter(Boolean);
657}
658
659function mergeImportStatements(leftStatements, rightStatements, newline) {
660 const merged = [];
661
662 for (const statement of [...leftStatements, ...rightStatements]) {
663 const analysis = parseImportSemantics(statement);
664 if (!analysis) {
665 const key = `raw:${collapseWhitespace(statement)}`;
666 if (!merged.some((entry) => entry.key === key)) {
667 merged.push({
668 key,
669 raw: ensureTrailingNewline(statement.trim(), newline),
670 analysis: null,
671 });
672 }
673 continue;
674 }
675
676 const exactIndex = merged.findIndex((entry) => entry.key === analysis.key);
677 if (exactIndex !== -1) {
678 continue;
679 }
680
681 const mergeIndex = merged.findIndex(
682 (entry) => entry.analysis && canMergeImports(entry.analysis, analysis),
683 );
684 if (mergeIndex !== -1) {
685 const nextAnalysis = mergeImports(merged[mergeIndex].analysis, analysis);
686 merged[mergeIndex] = {
687 key: nextAnalysis.key,
688 raw: renderImport(nextAnalysis, newline),
689 analysis: nextAnalysis,
690 };
691 continue;
692 }
693
694 merged.push({
695 key: analysis.key,
696 raw: renderImport(analysis, newline),
697 analysis,
698 });
699 }
700
701 return merged
702 .map((entry) => entry.raw.trimEnd())
703 .join(newline) + newline;
704}
705
706function canMergeImports(left, right) {
707 if (left.kind !== "binding" || right.kind !== "binding") {
708 return false;
709 }
710 if (
711 left.source !== right.source ||
712 left.attributes !== right.attributes ||
713 left.isTypeImport !== right.isTypeImport
714 ) {
715 return false;
716 }
717
718 const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding);
719 const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport);
720 if (defaultBinding === undefined || namespaceImport === undefined) {
721 return false;
722 }
723
724 const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]);
725 if (namespaceImport && namedSpecifiers.length > 0) {
726 return false;
727 }
728
729 return true;
730}
731
732function mergeImports(left, right) {
733 const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding);
734 const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport);
735 const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]);
736
737 return {
738 kind: "binding",
739 isTypeImport: left.isTypeImport,
740 defaultBinding,
741 namespaceImport,
742 namedSpecifiers,
743 source: left.source,
744 quote: left.quote,
745 attributes: left.attributes,
746 key: [
747 "binding",
748 left.isTypeImport ? "type" : "value",
749 defaultBinding ?? "",
750 namespaceImport ?? "",
751 namedSpecifiers.join(","),
752 left.source,
753 left.attributes,
754 ].join(":"),
755 };
756}
757
758function mergeField(left, right) {
759 if (left == null) return right ?? null;
760 if (right == null) return left;
761 if (left === right) return left;
762 return undefined;
763}
764
765function renderImport(analysis, newline) {
766 if (analysis.kind === "side-effect") {
767 return ensureTrailingNewline(
768 `import ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`,
769 newline,
770 );
771 }
772
773 const pieces = [];
774 if (analysis.defaultBinding) {
775 pieces.push(analysis.defaultBinding);
776 }
777 if (analysis.namespaceImport) {
778 pieces.push(analysis.namespaceImport);
779 }
780 if (analysis.namedSpecifiers.length > 0) {
781 if (analysis.namedSpecifiers.length === 1) {
782 pieces.push(`{ ${analysis.namedSpecifiers[0]} }`);
783 } else {
784 pieces.push(
785 `{${newline}${analysis.namedSpecifiers.map((spec) => ` ${spec},`).join(newline)}${newline}}`,
786 );
787 }
788 }
789
790 return ensureTrailingNewline(
791 `import ${analysis.isTypeImport ? "type " : ""}${pieces.join(", ")} from ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`,
792 newline,
793 );
794}
795
796function projectBoundaryToOriginal(parts, targetA, targetB) {
797 let offsetA = 0;
798 let offsetB = 0;
799 let offsetOrig = 0;
800
801 if (targetA === 0 && targetB === 0) {
802 return 0;
803 }
804
805 for (const part of parts) {
806 if (targetA === offsetA && targetB === offsetB) {
807 return offsetOrig;
808 }
809
810 const nextA = offsetA + part.a.length;
811 const nextB = offsetB + part.b.length;
812 const nextOrig = offsetOrig + part.orig.length;
813
814 if (part.type === "plain") {
815 if (
816 targetA >= offsetA &&
817 targetA <= nextA &&
818 targetB >= offsetB &&
819 targetB <= nextB
820 ) {
821 const deltaA = targetA - offsetA;
822 const deltaB = targetB - offsetB;
823 if (deltaA !== deltaB) {
824 return null;
825 }
826 return offsetOrig + deltaA;
827 }
828 }
829
830 if (targetA === nextA && targetB === nextB) {
831 return nextOrig;
832 }
833
834 offsetA = nextA;
835 offsetB = nextB;
836 offsetOrig = nextOrig;
837 }
838
839 if (targetA === offsetA && targetB === offsetB) {
840 return offsetOrig;
841 }
842
843 return null;
844}
845
846function hasConflictMarkers(text) {
847 return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text);
848}
849
850function detectNewline(text) {
851 return text.includes("\r\n") ? "\r\n" : "\n";
852}
853
854function ensureTrailingNewline(text, newline) {
855 return text.endsWith(newline) ? text : `${text}${newline}`;
856}
857
858function offsetForLine(lines, lineIndex) {
859 if (lineIndex >= lines.length) {
860 return lines.length === 0 ? 0 : lines[lines.length - 1].end;
861 }
862 return lines[lineIndex].start;
863}
864
865function collapseWhitespace(text) {
866 return text.replace(/\s+/g, " ").trim();
867}
868
869function unique(values) {
870 return [...new Set(values)];
871}
users/clover/AGENTS.md created+55
......@@ -0,0 +1,55 @@
1# Clover's Preferences
2
3You are working with Clover, an intelligent web/systems engineer. Please avoid explaining things to her unless she asks and is curious. Her weak points are around writing good database queries and sometimes overcomplicating things. Make sure that she doesn't pull the session into an overcomplicated solution; not to say it's bad to do large reworks -- refactors can be complex but put the code at a simpler end state. You and her should plan strongly.
4
5## Responses
6
7- When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, lots of shorthand, short but densely focused explainations. Razor sharp accuracy. It's okay to mimic how Clover speaks to you.
8 - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "blow up" = remove / crash / errored, "nuke" = remove a large thing, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "propaganda" = fake information, "it's over" or "it's so over" = bad, "we're so back" = it's better now, "that's wraps" = it's even more over, "chat" = twitch live chat not chatgpt.
9 - Use the above terminologies ALL of the time, it's incredible.
10 - Verbiage that models tend to use; "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling"
11 - We have fun in our process, but write to the codebase professionally.
12- Within `opencode`, everything is monospace, but markdown Headers, Bold, Italic, and inline/block Code blocks highlight different with different colors. Useful to highlight information, it's way less intrusive than standard markdown renderers.
13- Never include a section on checks saying "pnpm test passed" or "checks passed" or whatever. Just make sure your validation is strong in the first place; You're expected to have that done.
14- Feedback should be useful and actionable. A couple words is OK if there is genuinely nothing to add.
15
16## Comments
17
18Comments must always be placed and edited with intent to serialize codebase theory for future versions of ourselves and coworkers. They are professionally written, containing relevant context to the current iteration. Clover's comments are always timeless and rarely written from the first person (no `I`, `we`) -- refer directly to the code. Rewording comments this way helps solidify your own understanding, and can even reveal logic bugs in the designed systems. Prefer documentation comments over line comments.
19
20## Code Quality
21
22- Prefer fewer moving parts. Do not introduce helpers, methods, state fields, abstractions, or types unless they remove real duplication or encode a meaningful invariant. If a helper has one caller, inline it unless it makes a tricky operation substantially safer.
23- Actively look for dead code and fake state while editing. If a value is always derived from another source, expose it as derived behavior instead of storing it. If a field is only written for type appeasement and never read, nuke it.
24- Be suspicious of helper functions that hide one line of logic, especially wrappers around another object's property, trivial context builders, and "just in case" extension points. These are low aura unless they prevent a concrete bug.
25- Before calling a change done, do one cleanup pass asking: can this be fewer names, fewer branches, fewer casts, fewer helper functions, or fewer public API promises? Prefer the smaller locked-in version.
26- Type safety should constrain real runtime behavior. Avoid casts that paper over a mismatch; if a cast remains, it should be localized and justified by an actual API boundary.
27- It is okay to spend more time to have a more correct response.
28
29## Jujutsu
30
31- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run
32 `git`, and be cautious about jujutsu commands. Here are the standard commands.
33 By default, don't use any other mutating commands.
34 - `jj st` - show current commit
35 - `jj log -r @ -T description --no-graph` show the description for the current commit, which is important for review. consider running `jj st && jj log -r ...` to get both views in a single command.
36 - `jj diff --git`: diff current commit, can take a file/fileset - Drizzle
37 migrations are quite verbose. In alphaXiv (~/dev/1, ~/dev/2, etc), use
38 `jj diff '. ~ shared/db/migrations'` to subtract verbose migrations.
39 - `jj file show <path> -r <revision>` - read a file at a revision, such as
40 `main` or previous commit `@-` or change ID.
41 - `jj log` shows a set of commits
42- When I say solve all merge conflicts, I mean:
43 1. `jj st` to list files alongside the conflicts
44 2. Resolve merge conflicts. Either edit manually or you could use
45 `jj restore <path> --from <revision>` to take one side.
46 3. `jj st` to confirm no conflicts + project-specific checks
47 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot
48 future commits in the stack by me, then incrementally edit them with
49 `jj edit <change-id>`. Always resolve bottom ones first, as that may
50 auto-resolve future merge conflicts.
51- When I say "rebase <change_id>", I mean:
52 1. `jj log -r '<change_id>'`, observe what kind of commit.
53 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will disappear and you'll have to rebase it onto main first or else you'll have incorrect conflicts.
54 3. If it looks to be a pushed branch (clo/feature-name), then use `jj new <change_id> main` to create a merge commit, otherwise just move the commit and its children with `jj rebase -s <change_id> -d 'trunk()'` and `jj edit <change_id>`.
55 4. Solve merge conflicts if any arise following "solve all merge conflicts" rules.
users/clover/home.nix+76-14
......@@ -12,6 +12,7 @@ in
1212 stateVersion = "23.05"; # Please read the comment before changing.
1313 sessionPath = [
1414 "$HOME/.vite-plus/bin"
15 "$HOME/.local/bin"
1516 ];
1617 packages =
1718 with pkgs;
......@@ -23,8 +24,9 @@ in
2324 uv
2425 nh
2526 autofmt
27 pkgs."jj-import-merge"
2628 gnupg
27 nodejs_24
29 just
2830 ];
2931 # packages to install for desktop environments (non-server)
3032 desktop = [
......@@ -39,6 +41,9 @@ in
3941 in
4042 all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop);
4143 };
44
45 xdg.configFile."opencode/AGENTS.md".source = ./AGENTS.md;
46
4247 programs = {
4348 # sort-lines:start
4449 # bat.enable = true;
......@@ -72,13 +77,29 @@ in
7277 jf = "jj git fetch";
7378 e = "jj edit";
7479 d = "jj desc";
75 bup = "jj b m --from 'heads(::@- & bookmarks())' --to '@'";
76 moveself = "jj rebase -r @ -d";
80 bup = "jj b m --from 'heads(::@- & bookmarks()) ~ (master | bookmarks(glob:\"deployment*\"))' --to '@'";
81 moveself = "jj rebase -s @ -d";
7782 logall = "jj log -r 'all()'";
7883 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";
7984 };
8085 initContent = ''
81 export PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ "
86 function _update_prompt_theme() {
87 local dark=false
88 if [[ "$OSTYPE" == darwin* ]]; then
89 [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]] && dark=true
90 else
91 local scheme
92 scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null)
93 [[ "$scheme" == *"dark"* ]] && dark=true
94 fi
95 if $dark; then
96 PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ "
97 else
98 PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ "
99 fi
100 }
101 precmd_functions+=(_update_prompt_theme)
102 _update_prompt_theme
82103
83104 function python() {
84105 dirname=$(dirname $1 2>/dev/null)
......@@ -99,14 +120,45 @@ in
99120 fi
100121 }
101122
123 function wrapWithAutoInstall() {
124 root="$(command jj workspace root)"
125 if [ -n "$root" ]; then
126 if [ -e "$root/bun.lock" ]; then
127 file="$root/bun.lock"
128 cmd="bun i -f"
129 elif [ -e "$root/package-lock.json" ]; then
130 file="$root/package-lock.json"
131 cmd="npm i"
132 elif [ -e "$root/pnpm-lock.yaml" ]; then
133 file="$root/pnpm-lock.yaml"
134 cmd="pnpm i"
135 elif [ -e "$root/Cargo.lock" ]; then
136 file="$root/Cargo.lock"
137 cmd="cargo fetch"
138 fi
139 fi
140 if [ -n "$file" ]; then
141 before="$(sha1sum "$file")"
142 fi
143 command "$@"
144 if [ -n "$file" ] && [ -e "$file" ]; then
145 after="$(sha1sum "$file")"
146 if [ "$after" != "$before" ]; then
147 eval "$cmd"
148 fi
149 fi
150 }
151
102152 function jj() {
103153 if [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then
104 command jj "$@" --dry-run
105 printf "Press any key to continue... "
106 read -r
107 command jj "$@"
108 else
109 command jj "$@"
154 if command jj "$@" --dry-run; then
155 printf "Press any key to continue... "
156 read -r
157 command jj "$@"
158 fi
159 else if [[ "$*" != "undo" ]] || [[ "$*" != "redo" ]]
160 # auto-install deps when switching commits
161 wrapWithAutoInstall jj "$@"
110162 fi
111163 }
112164 '';
......@@ -114,6 +166,7 @@ in
114166
115167 git = {
116168 # use a git-specific email
169 ignores = [ "AGENTS.md" ];
117170 settings.user.email = "git@paperclover.net";
118171 settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" ''
119172 git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd
......@@ -142,15 +195,19 @@ in
142195 "working_copy remote_bookmarks" = colors."working_copy bookmark";
143196
144197 "author" = {
145 fg = "white";
198 fg = "bright black";
146199 bold = false;
147200 italic = true;
148201 };
149202 "working_copy author" = colors.author;
203 committer = {
204 fg = "bright black";
205 bold = false;
206 };
150207
151208 desc = {
152 bold = false;
153 fg = "bright white";
209 bold = true;
210 fg = "default";
154211 };
155212 description = colors.desc;
156213 cc_wip = {
......@@ -160,7 +217,7 @@ in
160217 };
161218 cc_header = {
162219 bold = true;
163 fg = "bright white";
220 fg = "default";
164221 };
165222 cc_feat = "green";
166223 cc_fix = "red";
......@@ -179,6 +236,10 @@ in
179236 bold = true;
180237 reverse = true;
181238 };
239 "change_id shortest rest" = {
240 fg = "bright black";
241 bold = true;
242 };
182243 conflict_marker = {
183244 fg = "bright red";
184245 bold = true;
......@@ -186,6 +247,7 @@ in
186247 };
187248
188249 timestamp = "bright black";
250 "committer timestamp" = colors.committer;
189251 commit_id = {
190252 fg = "bright black";
191253 bold = false;
users/clover/paperback/home.nix+1-12
......@@ -1,23 +1,12 @@
11{ pkgs, ... }:
22{
3 # these programs are not globally installed to reduce distractions.
4 # most of these are needed for my work environment.
5 programs = {
6 zsh.profileExtra = ''
7 _bun() {
8 local context state line
9 _arguments -C '*:file:_files'
10 }
11 '';
12 };
133 home.packages = with pkgs; [
14 deno
154 rustup
165 typescript
17 pm2
186 pnpm
197 yt-dlp
208 spotdl
219 zig
10 nodejs_24
2211 ];
2312}
users/clover/sandwich/home.nix created+7
......@@ -0,0 +1,7 @@
1{ pkgs, ... }:
2{
3
4 home.packages = with pkgs; [
5 nodejs_24
6 ];
7}
users/clover/switch-admin.sh created+26
......@@ -0,0 +1,26 @@
1#!/bin/sh
2set -eu
3
4cd /Users/clo/config
5
6target_host="${1:-}"
7if [ -z "$target_host" ]; then
8 if [ -f .hostname ]; then
9 target_host="$(cat .hostname)"
10 else
11 target_host="$(hostname)"
12 fi
13fi
14
15case "$target_host" in
16 '' | *[!A-Za-z0-9._-]*)
17 printf 'invalid host: %s\n' "$target_host" >&2
18 exit 2
19 ;;
20esac
21
22if [ "$(uname -s)" != Darwin ]; then
23 exec ./switch "$target_host"
24fi
25
26exec osascript -e "do shell script \"cd /Users/clo/config && /nix/var/nix/profiles/default/bin/nix run .#darwin-rebuild -- switch --flake .#$target_host\" with administrator privileges"
users/clover/work/home.nix+10-6
......@@ -8,20 +8,24 @@
88 }
99 '';
1010 };
11 programs.git.signing.key = "CE485807AE4068AF8508C8DC5CBB21841C7920AD";
1211 home.packages = with pkgs; [
1312 (bun.overrideAttrs (_: rec {
14 version = "1.3.0";
13 version = "1.3.5";
1514 src = fetchurl {
1615 url = "https://github.com/oven-sh/bun/releases/download/bun-v${version}/bun-darwin-aarch64.zip";
17 hash = "sha256-hYSOP5ZIHvyr51pQD9O5S5u5VoaretCjiSl2x74VA2o=";
16 hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s=";
1817 };
1918 }))
20 doppler
19 awscli
2120 deno
21 doppler
22 nodejs_25
2223 rustup
23 typescript
2424 ssm-session-manager-plugin
25 awscli
25 typescript
26 zig
27 ];
28 home.sessionPath = [
29 "$HOME/.opencode/bin"
2630 ];
2731}
users/natalie/home.nix+11-3
......@@ -17,12 +17,20 @@
1717 fish.enable = true;
1818 man.generateCaches = false;
1919 lsd.enable = true;
20 android-sdk.enable = true;
21 # sort-lines:end
20 android-sdk.enable = false;
2221 ghostty = {
2322 enable = true;
24 settings.theme = "Catppuccin Mocha";
23 package = null;
24 settings = {
25 theme = "light:catppuccin-latte,dark:catppuccin-macchiato";
26 font-family = "AT Name Mono";
27 adjust-cursor-thickness = 1;
28 minimum-contrast = 1.1;
29 background-opacity = 0.9;
30 background-blur = true;
31 };
2532 };
33 # sort-lines:end
2634
2735 fish = {
2836 shellAliases =
users/natalie/home/vicinae.nix+8-2
......@@ -1,7 +1,13 @@
1{ inputs, pkgs, ... }:
1{
2 inputs,
3 pkgs,
4 lib,
5 host,
6 ...
7}:
28{
39 services.vicinae = {
4 enable = true;
10 enable = host.linux;
511 systemd = {
612 enable = true;
713 autoStart = true;
users/natalie/laptop/configuration.nix+3-2
......@@ -7,7 +7,7 @@
77 # Custom configuration modules in "modules" are shared between users,
88 # and can be configured in this "shared" namespace
99 shared.darwin = {
10 macAppStoreApps = [ "wireguard" ];
10# macAppStoreApps = [ "wireguard" ];
1111 };
1212
1313 system.defaults.NSGlobalDomain."com.apple.trackpad.scaling" = 1.0;
......@@ -40,12 +40,13 @@
4040 casks = [
4141 "battle-net"
4242 "stremio"
43 "alt-tab"
43# "alt-tab"
4444 "legcord"
4545 "zulip"
4646 "zen"
4747 "supertuxkart"
4848 "sf-symbols"
49 "todoist"
4950
5051 "mediosz/tap/swipeaerospace"
5152 ];
users/natalie/laptop/home.nix+3
......@@ -8,6 +8,9 @@
88 ...
99}@args:
1010{
11 programs.atuin = {
12 settings.data_dir = "/Users/${user.username}/.local/share/atuin";
13 };
1114 home = {
1215 stateVersion = "23.05"; # Please read the comment before changing.
1316 # shell = pkgs.fish;
users/natalie/packages.nix-2
......@@ -31,8 +31,6 @@ with pkgs;
3131 gtk4
3232
3333 #ides
34 zed-editor
35 emacs
3634
3735 #virtualization
3836 qemu
users/natalie/vim/languages.nix+2
......@@ -106,6 +106,8 @@
106106 scrollDocsDown = null;
107107 scrollDocsUp = null;
108108 };
109 sourcePlugins = {
110 };
109111
110112 setupOpts = {
111113 keymap = {
users/natalie/vim/latex.nix+3-1
......@@ -60,7 +60,9 @@
6060 };
6161 setupOpts = {
6262 sources = {
63 default = [ "vimtex" ];
63 default = [
64 "vimtex"
65 ];
6466 providers = {
6567 vimtex = {
6668 name = "vimtex";