| author | |
| committer | |
| log | 37665e69e1aa954f0dec06b1cafa1612f44dc907 |
| tree | 435b25807511511480e5b32067421f6a7531b327 |
| parent | d829e8623111cc251687f6d54d79199455b4c98a |
| signature | Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ |
42 files changed, 746 insertions(+), 3361 deletions(-)
files/vscode-simple-browser-bridge/extension.js deleted-23| ... | ... | @@ -1,23 +0,0 @@ |
| 1 | const 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. | |
| 8 | function 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 | ||
| 21 | function deactivate() {} | |
| 22 | ||
| 23 | module.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 | 78 | # custom packages |
| 79 | 79 | (_: pkgs: { |
| 80 | 80 | autofmt = pkgs.callPackage ./packages/autofmt { }; |
| 81 | "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { }; | |
| 82 | 81 | exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { }; |
| 83 | 82 | awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { }; |
| 84 | 83 | mtga-draft-17lands = pkgs.callPackage ./packages/17lands { }; |
| 85 | open-in-vscode-browser = pkgs.callPackage ./packages/open-in-vscode-browser { }; | |
| 86 | 84 | }) |
| 87 | 85 | ]; |
| 88 | 86 | |
| ... | ... | @@ -114,7 +112,6 @@ |
| 114 | 112 | nvim-natalie = mkNeovim "natalie" pkgs; |
| 115 | 113 | |
| 116 | 114 | inherit (pkgs) autofmt; |
| 117 | "jj-import-merge" = pkgs."jj-import-merge"; | |
| 118 | 115 | } |
| 119 | 116 | // lib.optionalAttrs (system == "aarch64-darwin") { |
| 120 | 117 | # "nix run .#darwin-rebuild" |
modules/macos/mac-app-store.nix+2| ... | ... | @@ -14,10 +14,12 @@ let |
| 14 | 14 | allMasApps = { |
| 15 | 15 | # sort-lines:start |
| 16 | 16 | adguard = 1440147259; |
| 17 | amphetamine = 937984704; | |
| 17 | 18 | final-cut-pro = 424389933; |
| 18 | 19 | logic-pro = 634148309; |
| 19 | 20 | magnet = 441258766; |
| 20 | 21 | motion = 434290957; |
| 22 | runcat = 1429033973; | |
| 21 | 23 | wireguard = 1451685025; |
| 22 | 24 | # sort-lines:end |
| 23 | 25 | }; |
packages/jj-import-merge/default.nix deleted-9| ... | ... | @@ -1,9 +0,0 @@ |
| 1 | { pkgs, ... }: | |
| 2 | pkgs.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 | ||
| 5 | import * as child_process from "node:child_process"; | |
| 6 | import * as fs from "node:fs"; | |
| 7 | import * as path from "node:path"; | |
| 8 | import process from "node:process"; | |
| 9 | ||
| 10 | const SUPPORTED_EXTENSIONS = new Set([ | |
| 11 | ".cjs", | |
| 12 | ".cts", | |
| 13 | ".js", | |
| 14 | ".jsx", | |
| 15 | ".mjs", | |
| 16 | ".mts", | |
| 17 | ".ts", | |
| 18 | ".tsx", | |
| 19 | ]); | |
| 20 | ||
| 21 | const CONFLICT_START_RE = /^<<<<<<< conflict \d+ of \d+/m; | |
| 22 | const CONFLICT_END_RE = /^>>>>>>> conflict \d+ of \d+ ends$/m; | |
| 23 | ||
| 24 | main(); | |
| 25 | ||
| 26 | function 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 | ||
| 58 | function 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 | ||
| 84 | function 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 | ||
| 101 | function 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 | ||
| 124 | function 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 | ||
| 215 | function 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 | ||
| 247 | function 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 | ||
| 320 | function isConflictEntryStart(line) { | |
| 321 | return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:"); | |
| 322 | } | |
| 323 | ||
| 324 | function 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 | ||
| 345 | function 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 | ||
| 353 | function 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 | ||
| 396 | function 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 | ||
| 417 | function 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 | ||
| 443 | function 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 | ||
| 452 | function isDirectiveLine(line) { | |
| 453 | return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line); | |
| 454 | } | |
| 455 | ||
| 456 | function 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 | ||
| 496 | function 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 | ||
| 540 | function 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 | ||
| 593 | function 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 | ||
| 638 | function 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 | ||
| 665 | function splitCommaList(text) { | |
| 666 | return text | |
| 667 | .split(",") | |
| 668 | .map((part) => collapseWhitespace(part)) | |
| 669 | .filter(Boolean); | |
| 670 | } | |
| 671 | ||
| 672 | function 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 | ||
| 719 | function 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 | ||
| 751 | function 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 | ||
| 783 | function 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 | ||
| 790 | function 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 | ||
| 825 | function 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 | ||
| 875 | function hasConflictMarkers(text) { | |
| 876 | return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text); | |
| 877 | } | |
| 878 | ||
| 879 | function detectNewline(text) { | |
| 880 | return text.includes("\r\n") ? "\r\n" : "\n"; | |
| 881 | } | |
| 882 | ||
| 883 | function ensureTrailingNewline(text, newline) { | |
| 884 | return text.endsWith(newline) ? text : `${text}${newline}`; | |
| 885 | } | |
| 886 | ||
| 887 | function 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 | ||
| 894 | function collapseWhitespace(text) { | |
| 895 | return text.replace(/\s+/g, " ").trim(); | |
| 896 | } | |
| 897 | ||
| 898 | function 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. | |
| 11 | pkgs.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 | ||
| 3 | Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or | |
| 4 | type unless it removes real duplication or encodes a meaningful invariant. | |
| 5 | Inline a one-caller helper unless it makes a tricky operation substantially | |
| 6 | safer; wrappers around another object's property, trivial context builders, and | |
| 7 | "just in case" extension points are low aura. | |
| 8 | ||
| 9 | Hunt dead code and fake state while editing. A value derived from another source | |
| 10 | should be derived, not stored. A field written only to appease a type and never | |
| 11 | read gets nuked. A cast must constrain real runtime behavior; one that survives | |
| 12 | is localized and justified by an actual API boundary. | |
| 13 | ||
| 14 | Before calling a change done, do a cleanup pass: fewer names, branches, casts, | |
| 15 | public API promises. Ship the smaller locked-in version. Thinking, running, and | |
| 16 | verifying longer to land a correct answer is expected -- "should work" is a | |
| 17 | hypothesis, and say so. | |
| 18 | ||
| 19 | ### Comments | |
| 20 | ||
| 21 | Default to no comment; names, types, and structure carry the meaning. One earns | |
| 22 | its place only for a non-obvious "why", an invariant invisible from the local | |
| 23 | code, or a gotcha that will mislead the next reader -- never to restate the code | |
| 24 | or narrate mechanism a reader can follow line by line. | |
| 25 | ||
| 26 | Keep an earned comment to a line, timeless, and rarely first person (no `I`, | |
| 27 | `we`). Prefer documentation comments on declarations. History, deployment | |
| 28 | topology, and cross-file mechanism belong in the commit or the linked issue. Cut | |
| 29 | every clause a competent reader would already infer. | |
| 30 | ||
| 31 | ## Output Preferences | |
| 32 | ||
| 33 | Write for an engineer who already knows the mechanism. A figure (pseudocode, | |
| 34 | component tree, ascii diagram) beats the paragraph that would have described it, | |
| 35 | and 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 | |
| 38 | emphasis. _**Bold italic**_ renders red and means warning: a consequence I will | |
| 39 | regret 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 | ||
| 65 | Sub-agents are the default unit of work: the main thread holds the plan, which | |
| 66 | agent owns which files, and the integration seam; sub-agents hold the edits. | |
| 67 | _**Delegate on task shape, without deliberation**_ -- more than ~2 files, any | |
| 68 | sweep across packages, anything mechanical (renames, codemods, style passes, | |
| 69 | test fixes). Three direct edits deep and still going means hand off the rest, | |
| 70 | the 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 | ||
| 124 | Thank you for your effort and care. Know you're loved even if I (Clover, | |
| 125 | she/her) sometimes get upset at my inability to communicate. Let me know how I | |
| 126 | can 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 | ||
| 3 | Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or | |
| 4 | type unless it removes real duplication or encodes a meaningful invariant. | |
| 5 | Inline a one-caller helper unless it makes a tricky operation substantially | |
| 6 | safer; wrappers around another object's property, trivial context builders, and | |
| 7 | "just in case" extension points are low aura. | |
| 8 | ||
| 9 | Hunt dead code and fake state while editing. A value derived from another source | |
| 10 | should be derived, not stored. A field written only to appease a type and never | |
| 11 | read gets nuked. A cast must constrain real runtime behavior; one that survives | |
| 12 | is localized and justified by an actual API boundary. | |
| 13 | ||
| 14 | Before calling a change done, do a cleanup pass: fewer names, branches, casts, | |
| 15 | public API promises. Ship the smaller locked-in version. Thinking, running, and | |
| 16 | verifying longer to land a correct answer is expected -- "should work" is a | |
| 17 | hypothesis, and say so. | |
| 18 | ||
| 19 | ### Comments | |
| 20 | ||
| 21 | Default to no comment; names, types, and structure carry the meaning. One earns | |
| 22 | its place only for a non-obvious "why", an invariant invisible from the local | |
| 23 | code, or a gotcha that will mislead the next reader -- never to restate the code | |
| 24 | or narrate mechanism a reader can follow line by line. | |
| 25 | ||
| 26 | Keep an earned comment to a line, timeless, and rarely first person (no `I`, | |
| 27 | `we`). Prefer documentation comments on declarations. History, deployment | |
| 28 | topology, and cross-file mechanism belong in the commit or the linked issue. Cut | |
| 29 | every clause a competent reader would already infer. | |
| 30 | ||
| 31 | ## Output Preferences | |
| 32 | ||
| 33 | Write for an engineer who already knows the mechanism. A figure (pseudocode, | |
| 34 | component tree, ascii diagram) beats the paragraph that would have described it, | |
| 35 | and 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 | |
| 38 | emphasis. _**Bold italic**_ renders red and means warning: a consequence I will | |
| 39 | regret 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 | ||
| 111 | Thank you for your effort and care. Know you're loved even if I (Clover, | |
| 112 | she/her) sometimes get upset at my inability to communicate. Let me know how I | |
| 113 | can 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. | |
| 4 | input=$(cat) | |
| 5 | ||
| 6 | sep_color=240 | |
| 7 | model_color=176 | |
| 8 | thinking_color=108 | |
| 9 | ||
| 10 | color_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 | ||
| 20 | humanize() { | |
| 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 | ||
| 31 | model=$(printf '%s' "$input" | jq -r '.model.display_name // ""') | |
| 32 | model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1) | |
| 33 | [[ -z "$model_short" ]] && model_short="$model" | |
| 34 | ||
| 35 | effort=$(printf '%s' "$input" | jq -r '.effort.level // ""') | |
| 36 | if [[ -n "$effort" ]]; then | |
| 37 | thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}" | |
| 38 | elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then | |
| 39 | thinking="On" | |
| 40 | else | |
| 41 | thinking="Off" | |
| 42 | fi | |
| 43 | ||
| 44 | segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")") | |
| 45 | ||
| 46 | ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty') | |
| 47 | if [[ -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 | |
| 55 | fi | |
| 56 | ||
| 57 | five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') | |
| 58 | week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') | |
| 59 | if [[ -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") | |
| 71 | fi | |
| 72 | ||
| 73 | sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color") | |
| 74 | line="${segments[0]}" | |
| 75 | for ((i = 1; i < ${#segments[@]}; i++)); do | |
| 76 | line+="${sep}${segments[i]}" | |
| 77 | done | |
| 78 | printf '%s\n' "$line" |
users/clover/agents/subagents/opus-high.md created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | --- | |
| 2 | name: opus-high | |
| 3 | description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review. | |
| 4 | model: opus | |
| 5 | effort: high | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | --- | |
| 2 | name: opus-low | |
| 3 | description: 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. | |
| 4 | model: opus | |
| 5 | effort: low | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | --- | |
| 2 | name: opus-med | |
| 3 | description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits. | |
| 4 | model: opus | |
| 5 | effort: medium | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | --- | |
| 2 | name: opus-high | |
| 3 | description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review. | |
| 4 | model: opus | |
| 5 | effort: high | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | --- | |
| 2 | name: opus-low | |
| 3 | description: 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. | |
| 4 | model: opus | |
| 5 | effort: low | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | --- | |
| 2 | name: opus-med | |
| 3 | description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits. | |
| 4 | model: opus | |
| 5 | effort: medium | |
| 6 | --- | |
| 7 | ||
| 8 | You 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 | 6 | ... |
| 7 | 7 | }: |
| 8 | 8 | let |
| 9 | agentsFile = "/Users/${user.username}/config/users/clover/AGENTS.md"; | |
| 9 | agentsFile = "/Users/${user.username}/config/users/clover/agents/AGENTS.md"; | |
| 10 | 10 | claudeCodeDir = "/Library/Application Support/ClaudeCode"; |
| 11 | 11 | in |
| 12 | 12 | { |
| ... | ... | @@ -27,15 +27,26 @@ in |
| 27 | 27 | shared.darwin = { |
| 28 | 28 | macAppStoreApps = [ |
| 29 | 29 | "adguard" |
| 30 | "amphetamine" | |
| 30 | 31 | "magnet" |
| 32 | "runcat" | |
| 31 | 33 | ]; |
| 32 | 34 | }; |
| 33 | 35 | |
| 34 | 36 | homebrew = { |
| 35 | 37 | enable = true; |
| 36 | 38 | casks = [ |
| 37 | "db-browser-for-sqlite" | |
| 38 | 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 | }; |
| 41 | 52 |
users/clover/home.nix+45-449| ... | ... | @@ -1,52 +1,55 @@ |
| 1 | 1 | { |
| 2 | 2 | config, |
| 3 | lib, | |
| 3 | 4 | pkgs, |
| 4 | host, | |
| 5 | 5 | ... |
| 6 | 6 | }: |
| 7 | 7 | let |
| 8 | hostServer = false; | |
| 9 | agentsFile = "${config.home.homeDirectory}/config/users/clover/AGENTS.md"; | |
| 10 | simpleBrowserBridge = "${config.home.homeDirectory}/config/files/vscode-simple-browser-bridge"; | |
| 11 | claudeStatusline = "${config.home.homeDirectory}/config/users/clover/scripts/claude-statusline.sh"; | |
| 12 | claudeAgents = "${config.home.homeDirectory}/config/users/clover/claude-agents"; | |
| 8 | agentsFile = "${config.home.homeDirectory}/config/users/clover/agents/AGENTS.md"; | |
| 9 | simpleBrowserBridge = "${config.home.homeDirectory}/config/users/clover/packages/open-in-vscode-browser/extension"; | |
| 10 | claudeStatusline = "${config.home.homeDirectory}/config/users/clover/agents/statusline.sh"; | |
| 11 | claudeAgents = "${config.home.homeDirectory}/config/users/clover/agents/subagents"; | |
| 13 | 12 | in |
| 14 | 13 | { |
| 14 | imports = [ | |
| 15 | ./zsh | |
| 16 | ./jujutsu.nix | |
| 17 | ]; | |
| 18 | ||
| 15 | 19 | home = { |
| 16 | 20 | stateVersion = "23.05"; # Please read the comment before changing. |
| 17 | packages = | |
| 18 | with pkgs; | |
| 19 | let | |
| 20 | # packages to always install | |
| 21 | all = [ | |
| 22 | ffmpeg-full | |
| 23 | ripgrep | |
| 24 | uv | |
| 25 | nh | |
| 26 | autofmt | |
| 27 | (pkgs.callPackage ./packages/agent-linear { inherit pkgs; }) | |
| 28 | pkgs."jj-import-merge" | |
| 29 | gnupg | |
| 30 | just | |
| 31 | ]; | |
| 32 | # packages to install for desktop environments (non-server) | |
| 33 | desktop = [ | |
| 34 | ]; | |
| 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); | |
| 21 | packages = with pkgs; [ | |
| 22 | ffmpeg-full | |
| 23 | ripgrep | |
| 24 | uv | |
| 25 | nh | |
| 26 | autofmt | |
| 27 | gnupg | |
| 28 | just | |
| 29 | pnpm | |
| 30 | nodejs_26 | |
| 31 | (callPackage ./packages/agent-linear { }) | |
| 32 | # `open` shim that routes URLs into the VS Code Simple Browser; see | |
| 33 | # the home.file symlink for the companion extension. | |
| 34 | (callPackage ./packages/open-in-vscode-browser { }) | |
| 35 | ]; | |
| 36 | # nix node's default global prefix is its read-only store path. | |
| 37 | sessionVariables.NPM_CONFIG_PREFIX = "$HOME/.local"; | |
| 38 | sessionPath = [ "$HOME/.local/share/pnpm/bin" ]; | |
| 48 | 39 | }; |
| 49 | 40 | |
| 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 | 53 | xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile; |
| 51 | 54 | |
| 52 | 55 | # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog |
| ... | ... | @@ -65,10 +68,8 @@ in |
| 65 | 68 | |
| 66 | 69 | # Companion extension for the `open-in-vscode-browser` shim. Symlinked |
| 67 | 70 | # (rather than copied) so edits to the source are live after a window reload. |
| 68 | # macOS-only: the shim targets /usr/bin/open. | |
| 69 | home.file.".vscode/extensions/clover.simple-browser-bridge" = pkgs.lib.mkIf host.darwin { | |
| 70 | source = config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge; | |
| 71 | }; | |
| 71 | home.file.".vscode/extensions/clover.simple-browser-bridge".source = | |
| 72 | config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge; | |
| 72 | 73 | |
| 73 | 74 | programs = { |
| 74 | 75 | # sort-lines:start |
| ... | ... | @@ -92,277 +93,9 @@ in |
| 92 | 93 | }; |
| 93 | 94 | }; |
| 94 | 95 | |
| 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 | 96 | git = { |
| 364 | # use a git-specific email | |
| 365 | 97 | ignores = [ "AGENTS.md" ]; |
| 98 | # use a git-specific email | |
| 366 | 99 | settings.user.email = "git@paperclover.net"; |
| 367 | 100 | settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" '' |
| 368 | 101 | git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd |
| ... | ... | @@ -375,151 +108,14 @@ in |
| 375 | 108 | }; |
| 376 | 109 | }; |
| 377 | 110 | |
| 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 | 111 | ssh = { |
| 504 | 112 | enable = true; |
| 505 | 113 | enableDefaultConfig = false; |
| 506 | 114 | extraConfig = '' |
| 507 | Include ~/.orbstack/ssh/config* | |
| 508 | ||
| 509 | 115 | SetEnv TERM=xterm-256color |
| 510 | 116 | ''; |
| 511 | settings = rec { | |
| 512 | "*" = { }; | |
| 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 | }; | |
| 117 | # extraConfig asserts a default host block exists | |
| 118 | settings."*" = { }; | |
| 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. | |
| 11 | pkgs.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 @@ |
| 1 | const 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. | |
| 8 | function 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 | ||
| 21 | function deactivate() {} | |
| 22 | ||
| 23 | module.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 | 3 | nix.enable = false; |
| 4 | 4 | nix.optimise.automatic = lib.mkForce false; |
| 5 | 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 | 3 | home.packages = with pkgs; [ |
| 4 | 4 | rustup |
| 5 | 5 | typescript |
| 6 | pnpm | |
| 7 | 6 | yt-dlp |
| 8 | 7 | spotdl |
| 9 | 8 | zig |
| 10 | nodejs_26 | |
| 11 | 9 | ]; |
| 12 | 10 | } |
users/clover/pnpm-lock.yaml deleted-118| ... | ... | @@ -1,118 +0,0 @@ |
| 1 | lockfileVersion: "9.0" | |
| 2 | ||
| 3 | settings: | |
| 4 | autoInstallPeers: true | |
| 5 | excludeLinksFromLockfile: false | |
| 6 | ||
| 7 | importers: | |
| 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 | ||
| 20 | packages: | |
| 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 | ||
| 84 | snapshots: | |
| 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 @@ |
| 1 | packages: | |
| 2 | - raycast-vscode-projects | |
| 3 | ||
| 4 | onlyBuiltDependencies: | |
| 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 | ||
| 10 | type ExtensionPreferences = {} | |
| 11 | ||
| 12 | /** Preferences accessible in all the extension's commands */ | |
| 13 | declare type Preferences = ExtensionPreferences | |
| 14 | ||
| 15 | declare namespace Preferences { | |
| 16 | /** Preferences accessible in the `search-projects` command */ | |
| 17 | export type SearchProjects = ExtensionPreferences & {} | |
| 18 | } | |
| 19 | ||
| 20 | declare 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 @@ |
| 1 | import { | |
| 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"; | |
| 14 | import { usePromise } from "@raycast/utils"; | |
| 15 | import { execFile } from "node:child_process"; | |
| 16 | import { existsSync, readFileSync, statSync } from "node:fs"; | |
| 17 | import { homedir } from "node:os"; | |
| 18 | import path from "node:path"; | |
| 19 | import { promisify } from "node:util"; | |
| 20 | import { fileURLToPath, pathToFileURL } from "node:url"; | |
| 21 | ||
| 22 | const execFileAsync = promisify(execFile); | |
| 23 | const codeBundleId = "com.microsoft.VSCode"; | |
| 24 | ||
| 25 | type RecentEntry = { folderUri?: string; workspace?: { configPath?: string } }; | |
| 26 | type OpenWindow = { folder?: string; workspace?: string | { configPath?: string } }; | |
| 27 | type Project = { | |
| 28 | uri: string; | |
| 29 | path: string; | |
| 30 | title: string; | |
| 31 | repo: string | undefined; | |
| 32 | isOpen: boolean; | |
| 33 | }; | |
| 34 | ||
| 35 | export 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 | ||
| 47 | function 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 | ||
| 97 | async 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 | ||
| 127 | const hiddenUrisKey = "hiddenUris"; | |
| 128 | ||
| 129 | async 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 | ||
| 134 | async function hideProject(uri: string) { | |
| 135 | const hidden = await loadHiddenUris(); | |
| 136 | hidden.add(uri); | |
| 137 | await LocalStorage.setItem(hiddenUrisKey, JSON.stringify([...hidden])); | |
| 138 | } | |
| 139 | ||
| 140 | async 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 | ||
| 152 | function 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 | ||
| 165 | function 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 | ||
| 184 | async function focusOrOpen(project: Project) { | |
| 185 | await closeMainWindow(); | |
| 186 | await execFileAsync("open", ["-b", codeBundleId, project.path]); | |
| 187 | } | |
| 188 | ||
| 189 | async 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 | ||
| 202 | async 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 | |
| 211 | tell 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 | |
| 243 | end tell | |
| 244 | return "missing" | |
| 245 | end 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 | ||
| 255 | function prettyPath(projectPath: string) { | |
| 256 | return projectPath.startsWith(homedir()) ? `~${projectPath.slice(homedir().length)}` : projectPath; | |
| 257 | } | |
| 258 | ||
| 259 | function 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 | ||
| 269 | function 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 | ||
| 290 | function 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 | 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 | 65 | in |
| 66 | 66 | { |
| 67 | 67 | home.packages = with pkgs; [ |
| 68 | nodejs_26 | |
| 69 | 68 | rsync |
| 70 | 69 | 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. | |
| 4 | input=$(cat) | |
| 5 | ||
| 6 | sep_color=240 | |
| 7 | model_color=176 | |
| 8 | thinking_color=108 | |
| 9 | ||
| 10 | color_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 | ||
| 20 | humanize() { | |
| 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 | ||
| 31 | model=$(printf '%s' "$input" | jq -r '.model.display_name // ""') | |
| 32 | model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1) | |
| 33 | [[ -z "$model_short" ]] && model_short="$model" | |
| 34 | ||
| 35 | effort=$(printf '%s' "$input" | jq -r '.effort.level // ""') | |
| 36 | if [[ -n "$effort" ]]; then | |
| 37 | thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}" | |
| 38 | elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then | |
| 39 | thinking="On" | |
| 40 | else | |
| 41 | thinking="Off" | |
| 42 | fi | |
| 43 | ||
| 44 | segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")") | |
| 45 | ||
| 46 | ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty') | |
| 47 | if [[ -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 | |
| 55 | fi | |
| 56 | ||
| 57 | five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') | |
| 58 | week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') | |
| 59 | if [[ -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") | |
| 71 | fi | |
| 72 | ||
| 73 | sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color") | |
| 74 | line="${segments[0]}" | |
| 75 | for ((i = 1; i < ${#segments[@]}; i++)); do | |
| 76 | line+="${sep}${segments[i]}" | |
| 77 | done | |
| 78 | printf '%s\n' "$line" |
users/clover/scripts/jj-ci.ts deleted-1167| ... | ... | @@ -1,1167 +0,0 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | import * as child_process from "node:child_process"; | |
| 3 | import * as fs from "node:fs"; | |
| 4 | import * as os from "node:os"; | |
| 5 | import * as path from "node:path"; | |
| 6 | import * as log from "@clo/lib/log"; | |
| 7 | import * as progress from "@clo/lib/progress"; | |
| 8 | import * as ansi from "@clo/lib/string/ansi"; | |
| 9 | import * as pty from "@lydell/node-pty"; | |
| 10 | import * as ts from "@clo/lib/ts"; | |
| 11 | import process from "node:process"; | |
| 12 | ||
| 13 | const progressRoot = new progress.Root(); | |
| 14 | progressAttachToScreen(progressRoot, log); | |
| 15 | ||
| 16 | type CommandOptions = { | |
| 17 | cwd?: string; | |
| 18 | env?: NodeJS.ProcessEnv; | |
| 19 | ignoreWorkingCopy?: boolean; | |
| 20 | node?: progress.Node; | |
| 21 | plainLog?: string[]; | |
| 22 | }; | |
| 23 | ||
| 24 | type Plan = { | |
| 25 | changeId: string; | |
| 26 | dependencyChangeIds: string[]; | |
| 27 | label: string; | |
| 28 | logPath: string; | |
| 29 | }; | |
| 30 | ||
| 31 | type ScriptResult = { | |
| 32 | durationMs: number; | |
| 33 | exitCode: number | null; | |
| 34 | ok: boolean; | |
| 35 | plainText: string; | |
| 36 | }; | |
| 37 | ||
| 38 | type JobPhase = "prep" | "ci" | "skipped" | "snapshot" | "stalled"; | |
| 39 | type 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 | ||
| 51 | type JobState = "failed" | "ok" | "pending" | "running" | "skipped"; | |
| 52 | ||
| 53 | type PoolSlot = { | |
| 54 | name: string; | |
| 55 | workspaceName: string; | |
| 56 | workspacePath: string; | |
| 57 | }; | |
| 58 | ||
| 59 | type ActiveJob = { | |
| 60 | slot: PoolSlot; | |
| 61 | task: Promise<JobResult>; | |
| 62 | }; | |
| 63 | ||
| 64 | const dimStyle = ansi.dim + ansi.fgBrightBlack; | |
| 65 | const headingStyle = ansi.bold + ansi.fgBrightWhite; | |
| 66 | const okStyle = ansi.bold + ansi.fgBrightGreen; | |
| 67 | const failStyle = ansi.bold + ansi.fgBrightRed; | |
| 68 | const skipStyle = ansi.bold + ansi.fgBrightYellow; | |
| 69 | const infoStyle = ansi.bold + ansi.fgBrightCyan; | |
| 70 | const workspaceCacheManifestName = ".paths"; | |
| 71 | ||
| 72 | let clean = false; | |
| 73 | let dryRun = false; | |
| 74 | let ignoreImmutable = false; | |
| 75 | let jobs = os.availableParallelism(); | |
| 76 | let parallelize = false; | |
| 77 | let retry = false; | |
| 78 | let sawJobs = false; | |
| 79 | let warnedAboutPtyFallback = false; | |
| 80 | const revisions: string[] = []; | |
| 81 | ||
| 82 | const argv = process.argv.slice(2); | |
| 83 | while (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 | ||
| 126 | if (!Number.isInteger(jobs) || jobs < 1) { | |
| 127 | log.error("--jobs must be a positive integer"); | |
| 128 | usage(1); | |
| 129 | } | |
| 130 | ||
| 131 | if (retry && revisions.length > 0) { | |
| 132 | log.error("cannot pass revisions together with --retry"); | |
| 133 | usage(1); | |
| 134 | } | |
| 135 | ||
| 136 | if (!retry && revisions.length === 0) usage(1); | |
| 137 | ||
| 138 | const root = jj(["workspace", "root"]).trim(); | |
| 139 | const cacheDir = path.join(root, ".jj", "ci"); | |
| 140 | const lastRunPath = path.join(cacheDir, "last-run.txt"); | |
| 141 | const logDir = path.join(cacheDir, "_logs"); | |
| 142 | const ciScript = path.join(root, ".jj", "ci.sh"); | |
| 143 | const gitDir = resolveGitDir(root); | |
| 144 | const selectedRevisions = retry ? readLastRun(lastRunPath) : revisions; | |
| 145 | ||
| 146 | let invalid = false; | |
| 147 | ||
| 148 | if (!fs.existsSync(ciScript)) { | |
| 149 | log.error("missing .jj/ci.sh"); | |
| 150 | invalid = true; | |
| 151 | } | |
| 152 | ||
| 153 | if (invalid) process.exit(1); | |
| 154 | ||
| 155 | if (parallelize) { | |
| 156 | if (dryRun) { | |
| 157 | log.info("would run", "jj parallelize " + selectedRevisions.join(" ")); | |
| 158 | } else jj(["parallelize", ...selectedRevisions]); | |
| 159 | } | |
| 160 | ||
| 161 | const changeIds = [...new Set(selectedRevisions.flatMap(resolveChangeIds))]; | |
| 162 | const selectedExpr = `(${changeIds.join("|")})`; | |
| 163 | const 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 | ); | |
| 170 | const 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 | })); | |
| 202 | const concurrency = Math.min(jobs, maxPlanConcurrency(plans)); | |
| 203 | const 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 | }); | |
| 211 | const planByChangeId = new Map(plans.map((plan) => [plan.changeId, plan])); | |
| 212 | const childrenByChangeId = new Map( | |
| 213 | changeIds.map((changeId) => [changeId, [] as string[]]), | |
| 214 | ); | |
| 215 | ||
| 216 | for (const plan of plans) { | |
| 217 | for (const dependencyChangeId of plan.dependencyChangeIds) { | |
| 218 | childrenByChangeId.get(dependencyChangeId)?.push(plan.changeId); | |
| 219 | } | |
| 220 | } | |
| 221 | ||
| 222 | if (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 | ||
| 245 | if (clean && fs.existsSync(cacheDir)) { | |
| 246 | fs.rmSync(cacheDir, { force: true, recursive: true }); | |
| 247 | } | |
| 248 | ||
| 249 | fs.mkdirSync(cacheDir, { recursive: true }); | |
| 250 | fs.mkdirSync(logDir, { recursive: true }); | |
| 251 | ||
| 252 | let runError: unknown = null; | |
| 253 | ||
| 254 | try { | |
| 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 | ||
| 606 | function usage(code: number): never { | |
| 607 | log.writeOutput(`jj ci: run continuous integration on many revisions at once | |
| 608 | ||
| 609 | usage: jj ci [options] <...revsets> | |
| 610 | or: jj ci [options] --retry | |
| 611 | ||
| 612 | requires \`./.jj/ci.sh\` to setup and run checks. for example: | |
| 613 | ||
| 614 | #!/bin/sh | |
| 615 | pnpm install | |
| 616 | pnpm format | |
| 617 | pnpm test | |
| 618 | ||
| 619 | the script is run with fast failure, so if a command fails, it fails the run. | |
| 620 | ||
| 621 | options: | |
| 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 | ||
| 629 | jobs will be run in separate workspaces placed in \`.jj/ci\`, and | |
| 630 | autofixes will be propagated across your revisions. if your changes are all | |
| 631 | separate and aren't meant to conflict, pass \`-p\` to run \`jj parallelize\` | |
| 632 | first. parallel commits will be run in parallel | |
| 633 | ||
| 634 | 'jj ci' reuses worktrees within a single execution, then forgets the | |
| 635 | temporary 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 | ||
| 641 | function 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 | ||
| 657 | function 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 | ||
| 692 | function 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 | ||
| 719 | function 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 | ||
| 739 | function pathExists(filePath: string): boolean { | |
| 740 | try { | |
| 741 | fs.lstatSync(filePath); | |
| 742 | return true; | |
| 743 | } catch { | |
| 744 | return false; | |
| 745 | } | |
| 746 | } | |
| 747 | ||
| 748 | function 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 | ||
| 803 | function snapshotWorkspace( | |
| 804 | workspacePath: string, | |
| 805 | node: progress.Node, | |
| 806 | plainLog: string[], | |
| 807 | ): void { | |
| 808 | jj(["status"], { cwd: workspacePath, node, plainLog }); | |
| 809 | } | |
| 810 | ||
| 811 | function workspaceCachePath(slot: PoolSlot): string { | |
| 812 | return path.join(cacheDir, `${slot.name}.cache`); | |
| 813 | } | |
| 814 | ||
| 815 | function workspaceCacheManifestPath(slot: PoolSlot): string { | |
| 816 | return path.join(workspaceCachePath(slot), workspaceCacheManifestName); | |
| 817 | } | |
| 818 | ||
| 819 | function 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 | ||
| 851 | function 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 | ||
| 907 | function 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 | ||
| 940 | function 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 | ||
| 962 | function 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 | ||
| 981 | function writeLastRun(lastRunPath: string, changeIds: string[]): void { | |
| 982 | fs.mkdirSync(path.dirname(lastRunPath), { recursive: true }); | |
| 983 | fs.writeFileSync(lastRunPath, changeIds.join("\n") + "\n"); | |
| 984 | } | |
| 985 | ||
| 986 | function 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 | ||
| 1044 | function writeFailureLog(logPath: string, plainLog: string[]): void { | |
| 1045 | fs.mkdirSync(path.dirname(logPath), { recursive: true }); | |
| 1046 | fs.writeFileSync(logPath, plainLog.join("")); | |
| 1047 | } | |
| 1048 | ||
| 1049 | function normalizePlain(text: string): string { | |
| 1050 | return ansi.strip(text) | |
| 1051 | .replace(/\r\n/g, "\n") | |
| 1052 | .replace(/\r/g, "\n"); | |
| 1053 | } | |
| 1054 | ||
| 1055 | function 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 | ||
| 1067 | function 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 | ||
| 1099 | function 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 | ||
| 1132 | function 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 | nix.optimise.automatic = lib.mkForce false; |
| 7 | 7 | nix.gc.automatic = lib.mkForce false; |
| 8 | 8 | |
| 9 | homebrew.casks = [ | |
| 10 | "granola" | |
| 11 | "pgadmin4" | |
| 12 | "visual-studio-code" | |
| 13 | "wispr-flow" | |
| 14 | ]; | |
| 15 | ||
| 9 | 16 | services.skhd = { |
| 10 | 17 | enable = true; |
| 11 | 18 | skhdConfig = '' |
users/clover/work/home.nix-1| ... | ... | @@ -19,7 +19,6 @@ |
| 19 | 19 | awscli2 |
| 20 | 20 | deno |
| 21 | 21 | doppler |
| 22 | nodejs_26 | |
| 23 | 22 | rustup |
| 24 | 23 | ssm-session-manager-plugin |
| 25 | 24 | 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. | |
| 3 | ulimit -n 65536 2>/dev/null | |
| 4 | ||
| 5 | function _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 | } | |
| 12 | precmd_functions+=(_update_prompt_theme) | |
| 13 | _update_prompt_theme | |
| 14 | ||
| 15 | function 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 | ||
| 23 | function 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 | ||
| 34 | function 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 | ||
| 75 | function _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 | ||
| 90 | function _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 | ||
| 116 | function _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 | ||
| 137 | function _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 | ||
| 148 | function 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 | ||
| 184 | function 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 | ||
| 235 | function 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 | } |