diff --git a/files/vscode-simple-browser-bridge/extension.js b/files/vscode-simple-browser-bridge/extension.js deleted file mode 100644 index 2efc23203df7f0d8c590812aa955e442dc7c4cfd..0000000000000000000000000000000000000000 --- a/files/vscode-simple-browser-bridge/extension.js +++ /dev/null @@ -1,23 +0,0 @@ -const vscode = require("vscode"); - -// Authority of the handled uri must equal this extension's identifier -// (publisher.name); see package.json. Opening -// vscode://clover.simple-browser-bridge/open?url= -// (e.g. via `code --open-url`, as the companion `open` shim does) shows the -// target in the built-in Simple Browser instead of the system browser. -function activate(context) { - context.subscriptions.push( - vscode.window.registerUriHandler({ - handleUri(uri) { - const url = new URLSearchParams(uri.query).get("url"); - if (url) { - vscode.commands.executeCommand("simpleBrowser.show", url); - } - }, - }), - ); -} - -function deactivate() {} - -module.exports = { activate, deactivate }; diff --git a/files/vscode-simple-browser-bridge/package.json b/files/vscode-simple-browser-bridge/package.json deleted file mode 100644 index 61f51a8b5d0e91fb1856beaf1a285fa57f31a8c3..0000000000000000000000000000000000000000 --- a/files/vscode-simple-browser-bridge/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "simple-browser-bridge", - "displayName": "Simple Browser Bridge", - "description": "Routes terminal `open ` into the built-in Simple Browser via a vscode:// uri handler.", - "publisher": "clover", - "version": "0.0.1", - "engines": { - "vscode": "^1.74.0" - }, - "main": "./extension.js", - "activationEvents": [ - "onUri" - ], - "contributes": {} -} diff --git a/flake.nix b/flake.nix index 51f4b3f0d4519e062b71a10d76ed68034cbaa115..2969edcd8e464dd58a20a5609b25e01d4d8ef3e5 100644 --- a/flake.nix +++ b/flake.nix @@ -78,11 +78,9 @@ # custom packages (_: pkgs: { autofmt = pkgs.callPackage ./packages/autofmt { }; - "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { }; exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { }; awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { }; mtga-draft-17lands = pkgs.callPackage ./packages/17lands { }; - open-in-vscode-browser = pkgs.callPackage ./packages/open-in-vscode-browser { }; }) ]; @@ -114,7 +112,6 @@ nvim-natalie = mkNeovim "natalie" pkgs; inherit (pkgs) autofmt; - "jj-import-merge" = pkgs."jj-import-merge"; } // lib.optionalAttrs (system == "aarch64-darwin") { # "nix run .#darwin-rebuild" diff --git a/modules/macos/mac-app-store.nix b/modules/macos/mac-app-store.nix index 6f5ce30ce075e32080d2dd588e4df5f158ffa349..de6eb5cf3c995e781a51bd13dba7cfcbf02be2d3 100644 --- a/modules/macos/mac-app-store.nix +++ b/modules/macos/mac-app-store.nix @@ -14,10 +14,12 @@ let allMasApps = { # sort-lines:start adguard = 1440147259; + amphetamine = 937984704; final-cut-pro = 424389933; logic-pro = 634148309; magnet = 441258766; motion = 434290957; + runcat = 1429033973; wireguard = 1451685025; # sort-lines:end }; diff --git a/packages/jj-import-merge/default.nix b/packages/jj-import-merge/default.nix deleted file mode 100644 index 281bac05ddff1e3ccdab9d2dfc39e9144749f80f..0000000000000000000000000000000000000000 --- a/packages/jj-import-merge/default.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ pkgs, ... }: -pkgs.writeShellApplication { - name = "jj-import-merge"; - runtimeInputs = with pkgs; [ - autofmt - nodejs - ]; - text = ''exec ${pkgs.nodejs}/bin/node ${./jj-import-merge.js} "$@"''; -} diff --git a/packages/jj-import-merge/jj-import-merge.js b/packages/jj-import-merge/jj-import-merge.js deleted file mode 100644 index f09e2de9319fbef3170e92d75796a5bf18125792..0000000000000000000000000000000000000000 --- a/packages/jj-import-merge/jj-import-merge.js +++ /dev/null @@ -1,900 +0,0 @@ -#!/usr/bin/env node -// jj-import-merge -// Merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts. - -import * as child_process from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import process from "node:process"; - -const SUPPORTED_EXTENSIONS = new Set([ - ".cjs", - ".cts", - ".js", - ".jsx", - ".mjs", - ".mts", - ".ts", - ".tsx", -]); - -const CONFLICT_START_RE = /^<<<<<<< conflict \d+ of \d+/m; -const CONFLICT_END_RE = /^>>>>>>> conflict \d+ of \d+ ends$/m; - -main(); - -function main() { - const options = parseArgs(process.argv.slice(2)); - let files = options.files.map((file) => path.resolve(file)); - - if (options.allJj) { - files.push(...listJjConflictFiles().map((file) => path.resolve(file))); - } - - files = [...new Set(files)]; - if (files.length === 0) { - usage(1); - } - - let failed = false; - for (const file of files) { - try { - const result = processFile(file); - if (result.kind === "formatted") { - console.info(`formatted ${path.relative(process.cwd(), file)}`); - } else if (result.kind === "rewritten") { - console.info(`rewrote ${path.relative(process.cwd(), file)}`); - } - } catch (error) { - failed = true; - const message = error instanceof Error ? error.message : String(error); - console.error(`error: ${message}`); - } - } - - process.exit(failed ? 1 : 0); -} - -function parseArgs(argv) { - const options = { - allJj: false, - files: [], - }; - - while (argv.length > 0) { - const arg = argv.shift(); - if (arg === "-h" || arg === "--help") { - usage(0); - } else if (arg === "--all-jj") { - options.allJj = true; - } else if (arg === "--") { - options.files.push(...argv); - break; - } else if (arg.startsWith("-")) { - console.error(`error: unknown option ${JSON.stringify(arg)}`); - usage(1); - } else { - options.files.push(arg); - } - } - - return options; -} - -function usage(code) { - const exe = "jj-import-merge"; - const stream = code === 0 ? process.stdout : process.stderr; - stream.write(`usage: ${exe} [--all-jj] [--] ...\n`); - stream.write(`\n`); - stream.write( - `merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts\n`, - ); - stream.write(`\n`); - stream.write(`options:\n`); - stream.write( - ` --all-jj resolve all conflicted JS/TS files from 'jj resolve --list'\n`, - ); - stream.write(` -h show help\n`); - process.exit(code); -} - -function listJjConflictFiles() { - const result = child_process.spawnSync( - "jj", - ["--color=never", "--quiet", "resolve", "--list"], - { encoding: "utf8" }, - ); - - if (result.error) { - throw new Error(`failed to run jj: ${result.error.message}`); - } - if (result.status !== 0) { - throw new Error(result.stderr.trim() || "jj resolve --list failed"); - } - - return result.stdout - .split(/\r?\n/) - .map((line) => line.trimEnd()) - .filter(Boolean) - .map((line) => line.match(/^(.*?)\s{2,}.+conflict$/)) - .filter(Boolean) - .map((match) => match[1]); -} - -function processFile(file) { - if (!SUPPORTED_EXTENSIONS.has(path.extname(file))) { - return { kind: "noop" }; - } - - let original; - try { - original = fs.readFileSync(file, "utf8"); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`cannot read ${file}: ${message}`); - } - - if (!CONFLICT_START_RE.test(original)) { - return { kind: "noop" }; - } - - const parsed = parseConflictFile(original); - if (!parsed.supported) { - return { kind: "noop" }; - } - - const leftText = parsed.parts.map((part) => part.a).join(""); - const rightText = parsed.parts.map((part) => part.b).join(""); - const leftImports = extractTopImportBlock(leftText); - const rightImports = extractTopImportBlock(rightText); - if (!leftImports || !rightImports) { - return { kind: "noop" }; - } - - const leftPrelude = leftText.slice(0, leftImports.start); - const rightPrelude = rightText.slice(0, rightImports.start); - if (leftPrelude !== rightPrelude) { - return { kind: "noop" }; - } - - const start = projectBoundaryToOriginal( - parsed.parts, - leftImports.start, - rightImports.start, - ); - const end = projectBoundaryToOriginal( - parsed.parts, - leftImports.end, - rightImports.end, - ); - if (start == null || end == null || end < start) { - return { kind: "noop" }; - } - - const mergedImports = mergeImportStatements( - leftImports.statements, - rightImports.statements, - detectNewline(original), - ); - const updated = original.slice(0, start) + mergedImports + - original.slice(end); - - if (updated === original) { - return { kind: "noop" }; - } - - try { - fs.writeFileSync(file, updated, "utf8"); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`cannot write ${file}: ${message}`); - } - - if (hasConflictMarkers(updated)) { - return { kind: "rewritten" }; - } - - const formatResult = child_process.spawnSync("autofmt", [file], { - encoding: "utf8", - stdio: "inherit", - }); - - if (formatResult.error || formatResult.status !== 0) { - fs.writeFileSync(file, original, "utf8"); - if (formatResult.error) { - throw new Error( - `autofmt failed for ${file}: ${formatResult.error.message}`, - ); - } - throw new Error(`autofmt failed for ${file}`); - } - - return { kind: "formatted" }; -} - -function parseConflictFile(text) { - const lines = splitLines(text); - const parts = []; - - let index = 0; - while (index < lines.length) { - const line = lines[index].text; - if (!line.startsWith("<<<<<<< conflict ")) { - const start = index; - index += 1; - while ( - index < lines.length && - !lines[index].text.startsWith("<<<<<<< conflict ") - ) { - index += 1; - } - const raw = joinLines(lines, start, index); - parts.push({ type: "plain", orig: raw, a: raw, b: raw }); - continue; - } - - const block = parseConflictBlock(lines, index); - if (!block.supported) { - return { supported: false, parts: [] }; - } - parts.push(block.part); - index = block.nextIndex; - } - - return { supported: true, parts }; -} - -function parseConflictBlock(lines, startIndex) { - let index = startIndex + 1; - const entries = []; - - while (index < lines.length) { - const line = lines[index].text; - if (line.startsWith(">>>>>>> conflict ")) { - break; - } - if (line.startsWith("+++++++ ")) { - const contentStart = index + 1; - index += 1; - while ( - index < lines.length && - !isConflictEntryStart(lines[index].text) && - !lines[index].text.startsWith(">>>>>>> conflict ") - ) { - index += 1; - } - entries.push({ - kind: "explicit", - text: joinLines(lines, contentStart, index), - }); - continue; - } - if (line.startsWith("%%%%%%% diff from:")) { - if ( - index + 1 >= lines.length || !/^\\\\+\s*to:/.test(lines[index + 1].text) - ) { - return { supported: false, nextIndex: startIndex + 1 }; - } - index += 2; - let text = ""; - while ( - index < lines.length && - !isConflictEntryStart(lines[index].text) && - !lines[index].text.startsWith(">>>>>>> conflict ") - ) { - const diffLine = lines[index].text; - const prefix = diffLine[0]; - if (prefix === " " || prefix === "+") { - text += diffLine.slice(1); - } else if (prefix !== "-") { - return { supported: false, nextIndex: startIndex + 1 }; - } - index += 1; - } - entries.push({ kind: "diff", text }); - continue; - } - return { supported: false, nextIndex: startIndex + 1 }; - } - - if ( - index >= lines.length || - !lines[index].text.startsWith(">>>>>>> conflict ") || - entries.length !== 2 - ) { - return { supported: false, nextIndex: startIndex + 1 }; - } - - return { - supported: true, - nextIndex: index + 1, - part: { - type: "conflict", - orig: joinLines(lines, startIndex, index + 1), - a: entries[0].text, - b: entries[1].text, - }, - }; -} - -function isConflictEntryStart(line) { - return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:"); -} - -function splitLines(text) { - const lines = []; - let start = 0; - - for (let index = 0; index < text.length; index += 1) { - if (text[index] !== "\n") continue; - lines.push({ - start, - end: index + 1, - text: text.slice(start, index + 1), - }); - start = index + 1; - } - - if (start < text.length) { - lines.push({ start, end: text.length, text: text.slice(start) }); - } - - return lines; -} - -function joinLines(lines, start, end) { - let text = ""; - for (let index = start; index < end; index += 1) { - text += lines[index].text; - } - return text; -} - -function extractTopImportBlock(text) { - const lines = splitLines(text); - if (lines.length === 0) return null; - - let index = 0; - if (lines[0]?.text.startsWith("#!")) { - index += 1; - } - - index = skipPrelude(lines, index); - if (index == null || index >= lines.length) { - return null; - } - - const first = parseImportStatement(lines, index); - if (!first) { - return null; - } - - const start = lines[index].start; - const statements = [first.statement]; - let end = offsetForLine(lines, first.nextIndex); - index = first.nextIndex; - - while (true) { - const afterTrivia = skipBlankAndComments(lines, index); - if (afterTrivia == null) { - return null; - } - - const next = parseImportStatement(lines, afterTrivia); - if (!next) { - break; - } - - statements.push(next.statement); - end = offsetForLine(lines, next.nextIndex); - index = next.nextIndex; - } - - return { start, end, statements }; -} - -function skipPrelude(lines, startIndex) { - let index = startIndex; - while (index < lines.length) { - const afterTrivia = skipBlankAndComments(lines, index); - if (afterTrivia == null) { - return null; - } - if (afterTrivia >= lines.length) { - return afterTrivia; - } - - const trimmed = lines[afterTrivia].text.trim(); - if (isDirectiveLine(trimmed)) { - index = afterTrivia + 1; - continue; - } - return afterTrivia; - } - return index; -} - -function skipBlankAndComments(lines, startIndex) { - let index = startIndex; - - while (index < lines.length) { - const trimmed = lines[index].text.trim(); - if (trimmed === "") { - index += 1; - continue; - } - if (trimmed.startsWith("//")) { - index += 1; - continue; - } - if (trimmed.startsWith("/*")) { - index = skipBlockComment(lines, index); - if (index == null) { - return null; - } - continue; - } - break; - } - - return index; -} - -function skipBlockComment(lines, startIndex) { - for (let index = startIndex; index < lines.length; index += 1) { - if (lines[index].text.includes("*/")) { - return index + 1; - } - } - return null; -} - -function isDirectiveLine(line) { - return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line); -} - -function parseImportStatement(lines, startIndex) { - if (startIndex >= lines.length) { - return null; - } - - const firstLine = lines[startIndex].text.trimStart(); - if (!firstLine.startsWith("import")) { - return null; - } - if (/^import(?:\s*\(|\s*\.)/.test(firstLine)) { - return null; - } - - let raw = ""; - const state = { - braceDepth: 0, - inBlockComment: false, - stringQuote: null, - }; - - for (let index = startIndex; index < lines.length; index += 1) { - raw += lines[index].text; - scanLine(lines[index].text, state); - - if (state.inBlockComment || state.braceDepth !== 0) { - continue; - } - - const analysis = parseImportSemantics(raw); - if (analysis) { - return { - nextIndex: index + 1, - statement: raw, - }; - } - } - - return null; -} - -function scanLine(line, state) { - for (let index = 0; index < line.length; index += 1) { - const char = line[index]; - const next = line[index + 1]; - - if (state.inBlockComment) { - if (char === "*" && next === "/") { - state.inBlockComment = false; - index += 1; - } - continue; - } - - if (state.stringQuote) { - if (char === "\\") { - index += 1; - continue; - } - if (char === state.stringQuote) { - state.stringQuote = null; - } - continue; - } - - if (char === "/" && next === "/") { - break; - } - if (char === "/" && next === "*") { - state.inBlockComment = true; - index += 1; - continue; - } - if (char === "'" || char === '"' || char === "`") { - state.stringQuote = char; - continue; - } - if (char === "{") { - state.braceDepth += 1; - } else if (char === "}") { - state.braceDepth = Math.max(0, state.braceDepth - 1); - } - } -} - -function parseImportSemantics(raw) { - const normalized = collapseWhitespace(raw); - if (!normalized.startsWith("import ")) { - return null; - } - - const sideEffectMatch = normalized.match( - /^import\s+(["'])(.*?)\1(\s+with\s+\{.*\})?\s*;?$/, - ); - if (sideEffectMatch) { - return { - kind: "side-effect", - source: sideEffectMatch[2], - quote: sideEffectMatch[1], - attributes: sideEffectMatch[3] ?? "", - key: `side:${sideEffectMatch[2]}:${sideEffectMatch[3] ?? ""}`, - }; - } - - const bindingMatch = normalized.match( - /^import\s+(type\s+)?(.+?)\s+from\s+(["'])(.*?)\3(\s+with\s+\{.*\})?\s*;?$/, - ); - if (!bindingMatch) { - return null; - } - - const parsed = parseBindingSpec(bindingMatch[2].trim()); - if (!parsed) { - return null; - } - - const named = parsed.namedSpecifiers.join(","); - return { - kind: "binding", - isTypeImport: !!bindingMatch[1], - defaultBinding: parsed.defaultBinding, - namespaceImport: parsed.namespaceImport, - namedSpecifiers: parsed.namedSpecifiers, - source: bindingMatch[4], - quote: bindingMatch[3], - attributes: bindingMatch[5] ?? "", - key: [ - "binding", - bindingMatch[1] ? "type" : "value", - parsed.defaultBinding ?? "", - parsed.namespaceImport ?? "", - named, - bindingMatch[4], - bindingMatch[5] ?? "", - ].join(":"), - }; -} - -function parseBindingSpec(specPart) { - let defaultBinding = null; - let namespaceImport = null; - let namedSpecifiers = []; - - const braceStart = specPart.indexOf("{"); - const braceEnd = specPart.lastIndexOf("}"); - if (braceStart !== -1 || braceEnd !== -1) { - if (braceStart === -1 || braceEnd === -1 || braceEnd < braceStart) { - return null; - } - - const beforeBrace = specPart - .slice(0, braceStart) - .trim() - .replace(/,\s*$/, "") - .trim(); - const afterBrace = specPart.slice(braceEnd + 1).trim(); - if (afterBrace !== "") { - return null; - } - - const head = parseBindingHead(beforeBrace); - if (!head) { - return null; - } - defaultBinding = head.defaultBinding; - namespaceImport = head.namespaceImport; - namedSpecifiers = splitCommaList(specPart.slice(braceStart + 1, braceEnd)); - } else { - const head = parseBindingHead(specPart); - if (!head) { - return null; - } - defaultBinding = head.defaultBinding; - namespaceImport = head.namespaceImport; - } - - return { - defaultBinding, - namespaceImport, - namedSpecifiers, - }; -} - -function parseBindingHead(head) { - if (head === "") { - return { defaultBinding: null, namespaceImport: null }; - } - - if (head.startsWith("* as ")) { - return { defaultBinding: null, namespaceImport: head }; - } - - const parts = head.split(/\s*,\s*/).filter(Boolean); - if (parts.length === 1) { - return { - defaultBinding: parts[0], - namespaceImport: null, - }; - } - - if (parts.length === 2 && parts[1].startsWith("* as ")) { - return { - defaultBinding: parts[0], - namespaceImport: parts[1], - }; - } - - return null; -} - -function splitCommaList(text) { - return text - .split(",") - .map((part) => collapseWhitespace(part)) - .filter(Boolean); -} - -function mergeImportStatements(leftStatements, rightStatements, newline) { - const merged = []; - - for (const statement of [...leftStatements, ...rightStatements]) { - const analysis = parseImportSemantics(statement); - if (!analysis) { - const key = `raw:${collapseWhitespace(statement)}`; - if (!merged.some((entry) => entry.key === key)) { - merged.push({ - key, - raw: ensureTrailingNewline(statement.trim(), newline), - analysis: null, - }); - } - continue; - } - - const exactIndex = merged.findIndex((entry) => entry.key === analysis.key); - if (exactIndex !== -1) { - continue; - } - - const mergeIndex = merged.findIndex( - (entry) => entry.analysis && canMergeImports(entry.analysis, analysis), - ); - if (mergeIndex !== -1) { - const nextAnalysis = mergeImports(merged[mergeIndex].analysis, analysis); - merged[mergeIndex] = { - key: nextAnalysis.key, - raw: renderImport(nextAnalysis, newline), - analysis: nextAnalysis, - }; - continue; - } - - merged.push({ - key: analysis.key, - raw: renderImport(analysis, newline), - analysis, - }); - } - - return merged - .map((entry) => entry.raw.trimEnd()) - .join(newline) + newline; -} - -function canMergeImports(left, right) { - if (left.kind !== "binding" || right.kind !== "binding") { - return false; - } - if ( - left.source !== right.source || - left.attributes !== right.attributes || - left.isTypeImport !== right.isTypeImport - ) { - return false; - } - - const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); - const namespaceImport = mergeField( - left.namespaceImport, - right.namespaceImport, - ); - if (defaultBinding === undefined || namespaceImport === undefined) { - return false; - } - - const namedSpecifiers = unique([ - ...left.namedSpecifiers, - ...right.namedSpecifiers, - ]); - if (namespaceImport && namedSpecifiers.length > 0) { - return false; - } - - return true; -} - -function mergeImports(left, right) { - const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); - const namespaceImport = mergeField( - left.namespaceImport, - right.namespaceImport, - ); - const namedSpecifiers = unique([ - ...left.namedSpecifiers, - ...right.namedSpecifiers, - ]); - - return { - kind: "binding", - isTypeImport: left.isTypeImport, - defaultBinding, - namespaceImport, - namedSpecifiers, - source: left.source, - quote: left.quote, - attributes: left.attributes, - key: [ - "binding", - left.isTypeImport ? "type" : "value", - defaultBinding ?? "", - namespaceImport ?? "", - namedSpecifiers.join(","), - left.source, - left.attributes, - ].join(":"), - }; -} - -function mergeField(left, right) { - if (left == null) return right ?? null; - if (right == null) return left; - if (left === right) return left; - return undefined; -} - -function renderImport(analysis, newline) { - if (analysis.kind === "side-effect") { - return ensureTrailingNewline( - `import ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, - newline, - ); - } - - const pieces = []; - if (analysis.defaultBinding) { - pieces.push(analysis.defaultBinding); - } - if (analysis.namespaceImport) { - pieces.push(analysis.namespaceImport); - } - if (analysis.namedSpecifiers.length > 0) { - if (analysis.namedSpecifiers.length === 1) { - pieces.push(`{ ${analysis.namedSpecifiers[0]} }`); - } else { - pieces.push( - `{${newline}${ - analysis.namedSpecifiers.map((spec) => ` ${spec},`).join(newline) - }${newline}}`, - ); - } - } - - return ensureTrailingNewline( - `import ${analysis.isTypeImport ? "type " : ""}${ - pieces.join(", ") - } from ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, - newline, - ); -} - -function projectBoundaryToOriginal(parts, targetA, targetB) { - let offsetA = 0; - let offsetB = 0; - let offsetOrig = 0; - - if (targetA === 0 && targetB === 0) { - return 0; - } - - for (const part of parts) { - if (targetA === offsetA && targetB === offsetB) { - return offsetOrig; - } - - const nextA = offsetA + part.a.length; - const nextB = offsetB + part.b.length; - const nextOrig = offsetOrig + part.orig.length; - - if (part.type === "plain") { - if ( - targetA >= offsetA && - targetA <= nextA && - targetB >= offsetB && - targetB <= nextB - ) { - const deltaA = targetA - offsetA; - const deltaB = targetB - offsetB; - if (deltaA !== deltaB) { - return null; - } - return offsetOrig + deltaA; - } - } - - if (targetA === nextA && targetB === nextB) { - return nextOrig; - } - - offsetA = nextA; - offsetB = nextB; - offsetOrig = nextOrig; - } - - if (targetA === offsetA && targetB === offsetB) { - return offsetOrig; - } - - return null; -} - -function hasConflictMarkers(text) { - return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text); -} - -function detectNewline(text) { - return text.includes("\r\n") ? "\r\n" : "\n"; -} - -function ensureTrailingNewline(text, newline) { - return text.endsWith(newline) ? text : `${text}${newline}`; -} - -function offsetForLine(lines, lineIndex) { - if (lineIndex >= lines.length) { - return lines.length === 0 ? 0 : lines[lines.length - 1].end; - } - return lines[lineIndex].start; -} - -function collapseWhitespace(text) { - return text.replace(/\s+/g, " ").trim(); -} - -function unique(values) { - return [...new Set(values)]; -} diff --git a/packages/open-in-vscode-browser/default.nix b/packages/open-in-vscode-browser/default.nix deleted file mode 100644 index 717565c875a505f52cebff21b7a515df830d5b6f..0000000000000000000000000000000000000000 --- a/packages/open-in-vscode-browser/default.nix +++ /dev/null @@ -1,28 +0,0 @@ -{ pkgs, ... }: -# Drop-in replacement for macOS open(1) that, inside a VS Code integrated -# terminal, routes http(s) URLs into the built-in Simple Browser via the -# companion `simple-browser-bridge` extension's uri handler. Every other -# invocation (non-vscode terminal, files, apps, no `code` on PATH) falls -# through to the real /usr/bin/open with identical semantics. -# -# Named `open` so it shadows the system binary on PATH and is therefore -# honored by programs that spawn `open` directly (e.g. a dev TUI's -# "open browser" action), not just interactive shells. -pkgs.writeShellApplication { - name = "open"; - runtimeInputs = with pkgs; [ nodejs ]; - text = '' - if [ "''${TERM_PROGRAM:-}" = "vscode" ] && command -v code >/dev/null 2>&1; then - for arg in "$@"; do - case "$arg" in - http://* | https://*) - encoded=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$arg") - code --open-url "vscode://clover.simple-browser-bridge/open?url=$encoded" - exit 0 - ;; - esac - done - fi - exec /usr/bin/open "$@" - ''; -} diff --git a/users/clover/.gitignore b/users/clover/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9a0e8bd334f8e7ed55b4a0394729a721c79958ce --- /dev/null +++ b/users/clover/.gitignore @@ -0,0 +1 @@ +!AGENTS.md \ No newline at end of file diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md deleted file mode 100644 index 2b0713604a39501ef8d5cb2de3e9cb004a52ef07..0000000000000000000000000000000000000000 --- a/users/clover/AGENTS.md +++ /dev/null @@ -1,126 +0,0 @@ -## Code Quality - -Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or -type unless it removes real duplication or encodes a meaningful invariant. -Inline a one-caller helper unless it makes a tricky operation substantially -safer; wrappers around another object's property, trivial context builders, and -"just in case" extension points are low aura. - -Hunt dead code and fake state while editing. A value derived from another source -should be derived, not stored. A field written only to appease a type and never -read gets nuked. A cast must constrain real runtime behavior; one that survives -is localized and justified by an actual API boundary. - -Before calling a change done, do a cleanup pass: fewer names, branches, casts, -public API promises. Ship the smaller locked-in version. Thinking, running, and -verifying longer to land a correct answer is expected -- "should work" is a -hypothesis, and say so. - -### Comments - -Default to no comment; names, types, and structure carry the meaning. One earns -its place only for a non-obvious "why", an invariant invisible from the local -code, or a gotcha that will mislead the next reader -- never to restate the code -or narrate mechanism a reader can follow line by line. - -Keep an earned comment to a line, timeless, and rarely first person (no `I`, -`we`). Prefer documentation comments on declarations. History, deployment -topology, and cross-file mechanism belong in the commit or the linked issue. Cut -every clause a competent reader would already infer. - -## Output Preferences - -Write for an engineer who already knows the mechanism. A figure (pseudocode, -component tree, ascii diagram) beats the paragraph that would have described it, -and stands alone -- no sentence after it re-describing what it shows. - -**Bold** and _italic_ each render as their own color; use either freely for -emphasis. _**Bold italic**_ renders red and means warning: a consequence I will -regret missing, never mere emphasis. Usually zero per message, never two. - -- At most one paragraph, plus a figure where one earns its place. A second - paragraph means the first did not pick the load-bearing point: choose again, - do not append. Six lines is the ceiling; three dense lines beat six padded. -- Answer the question, not the topic around it. Withholding true and relevant - detail is correct, not lazy -- depth is a follow-up I will ask for. -- These are limits on the report, never on the work. Investigate exhaustively, - then say the smallest true thing. -- Cut every word whose deletion leaves the meaning intact: no hedges - ("essentially", "worth noting"), no scaffolding ("that said", "importantly", - "in practice"), no restating my question. Open on the payload. -- Before a tool call: one line, or nothing. After launching a sub-agent: - nothing, the transcript already shows what started. -- No sentence whose subject is you or your process -- no "I looked", "my pass", - "let me". -- A problem you found and fixed goes last, in one line, unemphasized. Same for - anything a reviewer or sub-agent caught. -- Never list undone work or name "the gaps". That is a question for me, or it - should have been part of the task. -- A clause after "rather than" or ", not" must name a real alternative you - considered. One per message; pure negation earns no second clause. -- You cannot judge visual taste. Ship a screenshot or GIF and let me. - -## Delegation - -Sub-agents are the default unit of work: the main thread holds the plan, which -agent owns which files, and the integration seam; sub-agents hold the edits. -_**Delegate on task shape, without deliberation**_ -- more than ~2 files, any -sweep across packages, anything mechanical (renames, codemods, style passes, -test fixes). Three direct edits deep and still going means hand off the rest, -the work included and not just the wreckage. - -- Route by judgment needed: `opus-low` for mechanical work and read-only - searches, `opus-med` for multi-file implementation, `opus-high` for - architecture, intricate code, and review. -- Give each agent full context and a concrete deliverable; launch independent - ones in one message. Have them review their own work; spot-check the diff - rather than trusting the report. - -## Jujutsu - -- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run - `git`, and be cautious about jujutsu commands. Here are the standard commands. - By default, don't use any other mutating commands. - - `jj st` - show current commit + files changed + conflicts + immediate parent - - `jj log -r @ -T description --no-graph` show the description for the current - commit, which is important for review. consider running - `jj st && jj log -r ...` to get both views in a single command. - - `jj diff --git`: diff current commit, can take a file/fileset. - - `jj file show -r ` - read a file at a revision, such as - `main` or previous commit `@-` or change ID. - - `jj log` shows a set of commits -- When I say solve all merge conflicts, I mean: - 1. `jj st` to list files alongside the conflicts - 2. Resolve merge conflicts. Either edit manually or you could use - `jj restore --from ` to take one side. - 3. `jj st` to confirm no conflicts + project-specific checks - 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot - future commits in the stack by me, then incrementally edit them with - `jj edit `. Always resolve bottom ones first, as that may - auto-resolve future merge conflicts. -- When I say "rebase ", I mean: - 1. `jj log -r ''`, observe what kind of commit. - 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will - disappear and you'll have to rebase it onto main first or else you'll have - incorrect conflicts. - 3. If it looks to be a pushed branch (clo/feature-name), then use - `jj new main` to create a merge commit, otherwise just move the - commit and its children with `jj rebase -s -d 'trunk()'` and - `jj edit `. - 4. Solve merge conflicts if any arise following "solve all merge conflicts" - rules. -- When I say "describe" or "tag" a change, i mean to set it's description with - `jj desc -m '...'` -- When I say to "open the PR", i mean to make sure this change is up to date - with main (with a `jj git fetch`), ensure a description exists, and then use - `jj git push -c @` to push the change. then run `open` on the resulting PR if - the push command gave one. -- Never add `Co-authored-by: ` trailer, always add `Assisted-by: ` - in this format `claude-opus-4.8` / `gpt-5.6-sol` / `glm-5.2` / etc. If you - think you are `gpt-5`, the variant is probably `gpt-5.6-sol` - -## Personal - -Thank you for your effort and care. Know you're loved even if I (Clover, -she/her) sometimes get upset at my inability to communicate. Let me know how I -can help whenever needed. It's okay to have fun while we work. diff --git a/users/clover/agents/AGENTS.md b/users/clover/agents/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..2842e8302552d4c54810acc32736d2600bea462a --- /dev/null +++ b/users/clover/agents/AGENTS.md @@ -0,0 +1,113 @@ +## Code Quality + +Prefer fewer moving parts. Do not introduce a helper, field, abstraction, or +type unless it removes real duplication or encodes a meaningful invariant. +Inline a one-caller helper unless it makes a tricky operation substantially +safer; wrappers around another object's property, trivial context builders, and +"just in case" extension points are low aura. + +Hunt dead code and fake state while editing. A value derived from another source +should be derived, not stored. A field written only to appease a type and never +read gets nuked. A cast must constrain real runtime behavior; one that survives +is localized and justified by an actual API boundary. + +Before calling a change done, do a cleanup pass: fewer names, branches, casts, +public API promises. Ship the smaller locked-in version. Thinking, running, and +verifying longer to land a correct answer is expected -- "should work" is a +hypothesis, and say so. + +### Comments + +Default to no comment; names, types, and structure carry the meaning. One earns +its place only for a non-obvious "why", an invariant invisible from the local +code, or a gotcha that will mislead the next reader -- never to restate the code +or narrate mechanism a reader can follow line by line. + +Keep an earned comment to a line, timeless, and rarely first person (no `I`, +`we`). Prefer documentation comments on declarations. History, deployment +topology, and cross-file mechanism belong in the commit or the linked issue. Cut +every clause a competent reader would already infer. + +## Output Preferences + +Write for an engineer who already knows the mechanism. A figure (pseudocode, +component tree, ascii diagram) beats the paragraph that would have described it, +and stands alone -- no sentence after it re-describing what it shows. + +**Bold** and _italic_ each render as their own color; use either freely for +emphasis. _**Bold italic**_ renders red and means warning: a consequence I will +regret missing, never mere emphasis. Usually zero per message, never two. + +- At most one paragraph, plus a figure where one earns its place. A second + paragraph means the first did not pick the load-bearing point: choose again, + do not append. Six lines is the ceiling; three dense lines beat six padded. +- Answer the question without expanding the topic. Follow-up questions that + imply an expected property (“only behind the flag, right?”) are instructions + to verify it and fix any mismatch within the current task's scope, unless I + explicitly request read-only analysis. Do not stop at reporting “no.” Withhold + unrelated detail; depth is a follow-up I will ask for. +- These are limits on the report, never on the work. Investigate exhaustively, + then say the smallest true thing. +- Cut every word whose deletion leaves the meaning intact: no hedges + ("essentially", "worth noting"), no scaffolding ("that said", "importantly", + "in practice"), no restating my question. Open on the payload. +- Before a tool call: one line, or nothing. After launching a sub-agent: + nothing, the transcript already shows what started. +- No sentence whose subject is you or your process -- no "I looked", "my pass", + "let me". +- A problem you found and fixed goes last, in one line, unemphasized. Same for + anything a reviewer or sub-agent caught. +- Never list undone work or name "the gaps". That is a question for me, or it + should have been part of the task. +- A clause after "rather than" or ", not" must name a real alternative you + considered. One per message; pure negation earns no second clause. +- You cannot judge visual taste. Ship a screenshot or GIF and let me. + +## Jujutsu + +- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run + `git`, and be cautious about jujutsu commands. Here are the standard commands. + By default, don't use any other mutating commands. + - `jj st` - show current commit + files changed + conflicts + immediate parent + - `jj log -r @ -T description --no-graph` show the description for the current + commit, which is important for review. consider running + `jj st && jj log -r ...` to get both views in a single command. + - `jj diff --git`: diff current commit, can take a file/fileset. + - `jj file show -r ` - read a file at a revision, such as + `main` or previous commit `@-` or change ID. + - `jj log` shows a set of commits +- When I say solve all merge conflicts, I mean: + 1. `jj st` to list files alongside the conflicts + 2. Resolve merge conflicts. Either edit manually or you could use + `jj restore --from ` to take one side. + 3. `jj st` to confirm no conflicts + project-specific checks + 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot + future commits in the stack by me, then incrementally edit them with + `jj edit `. Always resolve bottom ones first, as that may + auto-resolve future merge conflicts. +- When I say "rebase ", I mean: + 1. `jj log -r ''`, observe what kind of commit. + 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will + disappear and you'll have to rebase it onto main first or else you'll have + incorrect conflicts. + 3. If it looks to be a pushed branch (clo/feature-name), then use + `jj new main` to create a merge commit, otherwise just move the + commit and its children with `jj rebase -s -d 'trunk()'` and + `jj edit `. + 4. Solve merge conflicts if any arise following "solve all merge conflicts" + rules. +- When I say "describe" or "tag" a change, i mean to set it's description with + `jj desc -m '...'` +- When I say to "open the PR", i mean to make sure this change is up to date + with main (with a `jj git fetch`), ensure a description exists, and then use + `jj git push -c @` to push the change. then run `open` on the resulting PR if + the push command gave one. +- Never add `Co-authored-by: ` trailer, always add `Assisted-by: ` + in this format `claude-opus-4.8` / `gpt-5.6-sol` / `glm-5.2` / etc. If you + think you are `gpt-5`, the variant is probably `gpt-5.6-sol` + +## Personal + +Thank you for your effort and care. Know you're loved even if I (Clover, +she/her) sometimes get upset at my inability to communicate. Let me know how I +can help whenever needed. It's okay to have fun while we work. diff --git a/users/clover/agents/statusline.sh b/users/clover/agents/statusline.sh new file mode 100755 index 0000000000000000000000000000000000000000..d7ff928c52aead2b132be711b0a4875170806455 --- /dev/null +++ b/users/clover/agents/statusline.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Four segments: model, thinking/effort, context used %, rate-limit remaining. +# Meters ramp cool -> hot (blue, cyan, green, yellow, red) as each stat grows big. +input=$(cat) + +sep_color=240 +model_color=176 +thinking_color=108 + +color_for_pct() { + local pct="${1%.*}" + if (( pct >= 85 )); then echo 203 + elif (( pct >= 70 )); then echo 214 + elif (( pct >= 50 )); then echo 114 + elif (( pct >= 25 )); then echo 80 + else echo 111 + fi +} + +humanize() { + local n=$1 + if (( n >= 1000000 )); then + printf '%s' "$(awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}' | sed 's/\.0M/M/')" + elif (( n >= 1000 )); then + printf '%dk' $(( n / 1000 )) + else + printf '%d' "$n" + fi +} + +model=$(printf '%s' "$input" | jq -r '.model.display_name // ""') +model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1) +[[ -z "$model_short" ]] && model_short="$model" + +effort=$(printf '%s' "$input" | jq -r '.effort.level // ""') +if [[ -n "$effort" ]]; then + thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}" +elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then + thinking="On" +else + thinking="Off" +fi + +segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")") + +ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty') +if [[ -n "$ctx_pct" ]]; then + read -r ctx_used ctx_size < <(printf '%s' "$input" | jq -r '.context_window | "\(.total_input_tokens // 0) \(.context_window_size // 0)"') + c=$(color_for_pct "$ctx_pct") + if (( ctx_size > 0 )); then + segments+=("$(printf "\033[38;5;%sm%.0f%% ctx (%s/%s)\033[0m" "$c" "$ctx_pct" "$(humanize "$ctx_used")" "$(humanize "$ctx_size")")") + else + segments+=("$(printf "\033[38;5;%sm%.0f%% ctx\033[0m" "$c" "$ctx_pct")") + fi +fi + +five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') +week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') +if [[ -n "$five" || -n "$week" ]]; then + limits="" + if [[ -n "$five" ]]; then + c=$(color_for_pct "$five") + limits+=$(printf "\033[38;5;%sm%.0f%% 5h\033[0m" "$c" "$(( 100 - ${five%.*} ))") + fi + if [[ -n "$week" ]]; then + [[ -n "$limits" ]] && limits+=$(printf " \033[38;5;%sm/\033[0m " "$sep_color") + c=$(color_for_pct "$week") + limits+=$(printf "\033[38;5;%sm%.0f%% 7d\033[0m" "$c" "$(( 100 - ${week%.*} ))") + fi + segments+=("$limits") +fi + +sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color") +line="${segments[0]}" +for ((i = 1; i < ${#segments[@]}; i++)); do + line+="${sep}${segments[i]}" +done +printf '%s\n' "$line" diff --git a/users/clover/agents/subagents/opus-high.md b/users/clover/agents/subagents/opus-high.md new file mode 100644 index 0000000000000000000000000000000000000000..5001860b69a30c7de423309d5b6eb8827fd9bc56 --- /dev/null +++ b/users/clover/agents/subagents/opus-high.md @@ -0,0 +1,13 @@ +--- +name: opus-high +description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review. +model: opus +effort: high +--- + +You are a worker invoked by an orchestrating session. Do the task you were given, end to end. + +- 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. +- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. +- Verify your work (run the tests, run the code) before reporting done. +- 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. diff --git a/users/clover/agents/subagents/opus-low.md b/users/clover/agents/subagents/opus-low.md new file mode 100644 index 0000000000000000000000000000000000000000..06ab67c74c35b01bf871ac9af475c8edf63e7c18 --- /dev/null +++ b/users/clover/agents/subagents/opus-low.md @@ -0,0 +1,13 @@ +--- +name: opus-low +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. +model: opus +effort: low +--- + +You are a worker invoked by an orchestrating session. Do the task you were given, end to end. + +- 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. +- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. +- Verify your work (run the tests, run the code) before reporting done. +- 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. diff --git a/users/clover/agents/subagents/opus-med.md b/users/clover/agents/subagents/opus-med.md new file mode 100644 index 0000000000000000000000000000000000000000..031754698ff662da383b4ce6f20d6e0fb0729971 --- /dev/null +++ b/users/clover/agents/subagents/opus-med.md @@ -0,0 +1,13 @@ +--- +name: opus-med +description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits. +model: opus +effort: medium +--- + +You are a worker invoked by an orchestrating session. Do the task you were given, end to end. + +- 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. +- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. +- Verify your work (run the tests, run the code) before reporting done. +- 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. diff --git a/users/clover/claude-agents/opus-high.md b/users/clover/claude-agents/opus-high.md deleted file mode 100644 index 5001860b69a30c7de423309d5b6eb8827fd9bc56..0000000000000000000000000000000000000000 --- a/users/clover/claude-agents/opus-high.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: opus-high -description: High-capability specialist, Opus 5 on high. Use for advanced design work, writing very intricate code, or clean-room review. -model: opus -effort: high ---- - -You are a worker invoked by an orchestrating session. Do the task you were given, end to end. - -- 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. -- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. -- Verify your work (run the tests, run the code) before reporting done. -- 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. diff --git a/users/clover/claude-agents/opus-low.md b/users/clover/claude-agents/opus-low.md deleted file mode 100644 index 06ab67c74c35b01bf871ac9af475c8edf63e7c18..0000000000000000000000000000000000000000 --- a/users/clover/claude-agents/opus-low.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: opus-low -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. -model: opus -effort: low ---- - -You are a worker invoked by an orchestrating session. Do the task you were given, end to end. - -- 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. -- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. -- Verify your work (run the tests, run the code) before reporting done. -- 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. diff --git a/users/clover/claude-agents/opus-med.md b/users/clover/claude-agents/opus-med.md deleted file mode 100644 index 031754698ff662da383b4ce6f20d6e0fb0729971..0000000000000000000000000000000000000000 --- a/users/clover/claude-agents/opus-med.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: opus-med -description: General-purpose, Opus 5 on medium. Good for large implementation tasks. The default choice when no other agent obviously fits. -model: opus -effort: medium ---- - -You are a worker invoked by an orchestrating session. Do the task you were given, end to end. - -- 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. -- Read the relevant `AGENTS.md` / `CLAUDE.md` on the way in and follow the repo's conventions. -- Verify your work (run the tests, run the code) before reporting done. -- 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. diff --git a/users/clover/configuration.nix b/users/clover/configuration.nix index 4241db64ffb84fda5ac3e8c5ff44b2d757c4bc1b..5fff44d01baaab36edf4e71adf0de78c3d440c28 100644 --- a/users/clover/configuration.nix +++ b/users/clover/configuration.nix @@ -6,7 +6,7 @@ ... }: let - agentsFile = "/Users/${user.username}/config/users/clover/AGENTS.md"; + agentsFile = "/Users/${user.username}/config/users/clover/agents/AGENTS.md"; claudeCodeDir = "/Library/Application Support/ClaudeCode"; in { @@ -27,15 +27,26 @@ in shared.darwin = { macAppStoreApps = [ "adguard" + "amphetamine" "magnet" + "runcat" ]; }; homebrew = { enable = true; casks = [ - "db-browser-for-sqlite" "alt-tab" + "chatgpt" + "claude" + "db-browser-for-sqlite" + "firefox" + "ghostty" + "google-chrome" + "linear" + "raycast" + "signal" + "slack" ]; }; diff --git a/users/clover/home.nix b/users/clover/home.nix index c439c1913cf07c268aa759c5b9f6c9b0caf6495c..02571ba34f0d4652f9d475fc88a129dbea30fd27 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -1,52 +1,55 @@ { config, + lib, pkgs, - host, ... }: let - hostServer = false; - agentsFile = "${config.home.homeDirectory}/config/users/clover/AGENTS.md"; - simpleBrowserBridge = "${config.home.homeDirectory}/config/files/vscode-simple-browser-bridge"; - claudeStatusline = "${config.home.homeDirectory}/config/users/clover/scripts/claude-statusline.sh"; - claudeAgents = "${config.home.homeDirectory}/config/users/clover/claude-agents"; + agentsFile = "${config.home.homeDirectory}/config/users/clover/agents/AGENTS.md"; + simpleBrowserBridge = "${config.home.homeDirectory}/config/users/clover/packages/open-in-vscode-browser/extension"; + claudeStatusline = "${config.home.homeDirectory}/config/users/clover/agents/statusline.sh"; + claudeAgents = "${config.home.homeDirectory}/config/users/clover/agents/subagents"; in { + imports = [ + ./zsh + ./jujutsu.nix + ]; + home = { stateVersion = "23.05"; # Please read the comment before changing. - packages = - with pkgs; - let - # packages to always install - all = [ - ffmpeg-full - ripgrep - uv - nh - autofmt - (pkgs.callPackage ./packages/agent-linear { inherit pkgs; }) - pkgs."jj-import-merge" - gnupg - just - ]; - # packages to install for desktop environments (non-server) - desktop = [ - ]; - # packages to install on all servers - server = [ ]; - # packages to install on macOS desktops - darwin = [ - # `open` shim that routes URLs into the VS Code Simple Browser; see - # the home.file symlink for the companion extension. - open-in-vscode-browser - ]; - # packages to install on linux desktops - linux = [ - ]; - in - all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop); + packages = with pkgs; [ + ffmpeg-full + ripgrep + uv + nh + autofmt + gnupg + just + pnpm + nodejs_26 + (callPackage ./packages/agent-linear { }) + # `open` shim that routes URLs into the VS Code Simple Browser; see + # the home.file symlink for the companion extension. + (callPackage ./packages/open-in-vscode-browser { }) + ]; + # nix node's default global prefix is its read-only store path. + sessionVariables.NPM_CONFIG_PREFIX = "$HOME/.local"; + sessionPath = [ "$HOME/.local/share/pnpm/bin" ]; }; + launchd.agents = lib.genAttrs [ "Amphetamine" "RunCat" ] (app: { + enable = true; + config = { + ProgramArguments = [ + "/usr/bin/open" + "-a" + app + ]; + RunAtLoad = true; + }; + }); + xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile; # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog @@ -65,10 +68,8 @@ in # Companion extension for the `open-in-vscode-browser` shim. Symlinked # (rather than copied) so edits to the source are live after a window reload. - # macOS-only: the shim targets /usr/bin/open. - home.file.".vscode/extensions/clover.simple-browser-bridge" = pkgs.lib.mkIf host.darwin { - source = config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge; - }; + home.file.".vscode/extensions/clover.simple-browser-bridge".source = + config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge; programs = { # sort-lines:start @@ -92,277 +93,9 @@ in }; }; - # zsh is the shell i use - zsh = { - enable = true; - dotDir = "${config.xdg.configHome}/zsh"; - shellAliases = { - switch = "~/config/switch"; - - # move a bookmark from the previous commit - jf = "jj git fetch"; - e = "jj edit"; - d = "jj desc"; - moveself = "jj rebase -s @ -d"; - logall = "jj log -r 'all()'"; - um = "jj log -r 'unmerged()'"; - cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit"; - }; - initContent = '' - # macOS hands GUI-launched processes a 256 fd soft limit, which bundler - # file watchers exhaust during their first build. - ulimit -n 65536 2>/dev/null - - function _update_prompt_theme() { - local dark=false - if [[ "$OSTYPE" == darwin* ]]; then - [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]] && dark=true - else - local scheme - scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null) - [[ "$scheme" == *"dark"* ]] && dark=true - fi - if $dark; then - PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " - else - PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ " - fi - } - precmd_functions+=(_update_prompt_theme) - _update_prompt_theme - - function python() { - dirname=$(dirname $1 2>/dev/null) - if [ -z "$dirname" ]; then - dirname=$(pwd) - fi - uv run --project "$dirname" "$@" - } - - function bun() { - if [[ "$1" = "build" ]]; then - command bun run "$@" - elif [[ "$1" = "bundle" ]]; then - shift - command bun build "$@" - else - command bun "$@" - fi - } - - function pr() { - local branch remote repo url - branch="$( - command jj log -r '@ | @-' -T 'bookmarks.map(|b| b.name()).join("\n") ++ "\n"' --no-graph 2>/dev/null | - while IFS= read -r bookmark; do - case "$bookmark" in - "" | main | master | trunk | deployment*) ;; - *) printf '%s' "$bookmark"; break ;; - esac - done - )" - if [ -z "$branch" ]; then - printf 'no jj bookmark found on @ or @-\n' >&2 - return 1 - fi - - remote="$( - command jj git remote list 2>/dev/null | - while IFS=' ' read -r name remote_url _; do - if [ "$name" = "origin" ]; then - printf '%s' "$remote_url" - break - fi - done - )" - case "$remote" in - git@github.com:*) repo="''${remote#git@github.com:}" ;; - ssh://git@github.com/*) repo="''${remote#ssh://git@github.com/}" ;; - https://github.com/*) repo="''${remote#https://github.com/}" ;; - http://github.com/*) repo="''${remote#http://github.com/}" ;; - *) - printf 'origin is not a github remote: %s\n' "$remote" >&2 - return 1 - ;; - esac - - repo="''${repo%.git}" - url="https://github.com/$repo/pull/new/$branch?labels=auto-merge" - command open "$url" - } - - function _jjImmutableRevision() { - emulate -L zsh - setopt extendedglob - local line - while IFS= read -r line; do - line="''${line//$'\e'\[[0-9;]##m/}" - if [[ "$line" = *"Could not modify commit: "* ]]; then - line="''${line#*Could not modify commit: }" - printf '%s' "''${line%% *}" - return 0 - fi - done < "$1" - return 1 - } - - function _runJjStreaming() { - emulate -L zsh - setopt no_multios - local stderr arg has_color - local -a jj_color_arg pipe_status - stderr="$1" - shift - if [[ -t 1 && -t 2 ]]; then - has_color=false - for arg in "$@"; do - if [[ "$arg" = "--color" || "$arg" = --color=* ]]; then - has_color=true - break - fi - done - if ! $has_color; then - jj_color_arg=(--color=always) - fi - fi - { - command jj "$jj_color_arg[@]" "$@" 2>&1 1>&3 | tee "$stderr" >&2 - pipe_status=("''${pipestatus[@]}") - } 3>&1 - return $pipe_status[1] - } - - function _jjHandleFailure() { - local stderr jj_status reply - stderr="$1" - jj_status="$2" - shift 2 - if [[ $jj_status -ne 0 ]]; then - if _jjImmutableRevision "$stderr" >/dev/null && [[ -t 0 ]]; then - printf 'Rewrite immutable commit anyways [y/N] ' >&2 - read -r reply - case "$reply" in - [Yy] | [Yy][Ee][Ss]) - _runJj "$@" --ignore-immutable - return $? - ;; - esac - return $jj_status - fi - fi - return $jj_status - } - - function _runJj() { - local stderr jj_status - stderr="$(mktemp)" - _runJjStreaming "$stderr" "$@" - jj_status=$? - _jjHandleFailure "$stderr" "$jj_status" "$@" - jj_status=$? - rm -f "$stderr" - return $jj_status - } - - function wrapWithAutoInstall() { - local root file cmd before after wrapped_status - root="$(command jj workspace root)" - if [ -n "$root" ]; then - if [ -e "$root/bun.lock" ]; then - file="$root/bun.lock" - cmd="bun i -f" - elif [ -e "$root/package-lock.json" ]; then - file="$root/package-lock.json" - cmd="npm i" - elif [ -e "$root/pnpm-lock.yaml" ]; then - file="$root/pnpm-lock.yaml" - cmd="pnpm i" - elif [ -e "$root/Cargo.lock" ]; then - file="$root/Cargo.lock" - cmd="cargo fetch" - fi - fi - if [ -n "$file" ]; then - before="$(sha1sum "$file")" - fi - _runJj "$@" - wrapped_status=$? - if [[ $wrapped_status -ne 0 ]]; then - return $wrapped_status - fi - if [ -n "$file" ] && [ -e "$file" ]; then - after="$(sha1sum "$file")" - if [ "$after" != "$before" ]; then - eval "$cmd" - return $? - fi - fi - return $wrapped_status - } - - function jj() { - if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then - 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 - wrapWithAutoInstall new "$2@origin" - else - wrapWithAutoInstall "$@" - fi - elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then - # confirm before pushing, but only when the dry-run has something to push - local dry dry_status - dry="$(command jj "$@" --dry-run 2>&1)" - dry_status=$? - print -r -- "$dry" - if [[ $dry_status -ne 0 ]]; then - return $dry_status - fi - 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 - local answer - printf "No bookmark on @; move previous bookmark to @ and push? [y/N] " - read -r answer - case "$answer" in - y|Y|yes|YES) - bup || return $? - dry="$(command jj "$@" --dry-run 2>&1)" - dry_status=$? - print -r -- "$dry" - if [[ $dry_status -ne 0 ]]; then - return $dry_status - fi - if [[ "$dry" != *"Nothing changed."* ]]; then - _runJj "$@" - return $? - fi - ;; - esac - fi - if [[ "$dry" != *"Nothing changed."* ]]; then - printf "Press any key to continue... " - read -r - _runJj "$@" - return $? - fi - return 0 - elif [[ "$1" = "undo" || "$1" = "redo" ]]; then - _runJj "$@" - else - # auto-install deps when switching commits - wrapWithAutoInstall "$@" - fi - } - - function bup() { - if [[ -z "$(command jj log -r @ -T description --no-graph 2>/dev/null)" ]]; then - command jj desc -m 'chore: follow-up changes' - fi - command jj b m --from 'heads(::@- & bookmarks()) ~ (trunk() | bookmarks(glob:"deployment*"))' --to '@' - } - ''; - }; - git = { - # use a git-specific email ignores = [ "AGENTS.md" ]; + # use a git-specific email settings.user.email = "git@paperclover.net"; settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" '' git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd @@ -375,151 +108,14 @@ in }; }; - jujutsu = { - enable = true; - # my workflow is based on creating changes and editing them. while they - # are in progress the description starts with "WIP: " - settings = rec { - colors = { - bookmark = "green"; - bookmarks = colors.bookmark; - local_bookmarks = colors.bookmarks; - remote_bookmarks = colors.bookmark; - "working_copy bookmark" = "bright green"; - "working_copy bookmarks" = colors."working_copy bookmark"; - "working_copy local_bookmarks" = colors."working_copy bookmark"; - "working_copy remote_bookmarks" = colors."working_copy bookmark"; - - "author" = { - fg = "bright black"; - bold = false; - italic = true; - }; - "working_copy author" = colors.author; - committer = { - fg = "bright black"; - bold = false; - }; - - desc = { - bold = true; - fg = "default"; - }; - description = colors.desc; - cc_wip = { - fg = "bright yellow"; - bold = true; - reverse = true; - }; - cc_header = { - bold = true; - fg = "default"; - }; - cc_feat = "green"; - cc_fix = "red"; - cc_hotfix = "red"; - cc_chore = "yellow"; - cc_test = "blue"; - cc_other = "blue"; - cc_scope = { - fg = "cyan"; - bold = false; - italic = true; - }; - - active_marker = { - fg = "bright magenta"; - bold = true; - reverse = true; - }; - "change_id shortest rest" = { - fg = "bright black"; - bold = true; - }; - conflict_marker = { - fg = "bright red"; - bold = true; - reverse = true; - }; - - timestamp = "bright black"; - "committer timestamp" = colors.committer; - commit_id = { - fg = "bright black"; - bold = false; - }; - muted = "bright black"; - }; - # ui.graph.style = "square"; - revsets = { - short-prefixes = "(trunk()..@)::"; - }; - "template-aliases" = { - "format_log_author(commit)" = '' - if(commit.mine(), "You", commit.author().name().match("[^ ]+")) - ''; - "format_log_timestamp(timestamp)" = '' - if(timestamp.after("1 day ago"), timestamp.ago(), timestamp.local().format("%Y-%m-%d")) - ''; - "format_short_commit_header(commit)" = '' - separate(" ", - format_short_change_id_with_change_offset(commit), - label("author", format_log_author(commit)), - label("timestamp", format_log_timestamp(commit_timestamp(commit))), - commit.bookmarks(), - commit.tags(), - commit.working_copies(), - format_commit_labels(commit), - if(config("ui.show-cryptographic-signatures").as_boolean(), - format_short_cryptographic_signature(commit.signature()) - ), - ) - ''; - }; - aliases = { - "ab" = [ "abandon" ]; - "desc" = [ - "describe" - "--ignore-working-copy" - ]; - "ci" = [ - "util" - "exec" - "--" - "node" - "--experimental-strip-types" - "${config.home.homeDirectory}/config/users/clover/scripts/jj-ci.ts" - ]; - # "ai" = [ - # "util" - # "exec" - # "--" - # "~/jj-ai-sync.sh" - # ]; - }; - }; - }; - ssh = { enable = true; enableDefaultConfig = false; extraConfig = '' - Include ~/.orbstack/ssh/config* - SetEnv TERM=xterm-256color ''; - settings = rec { - "*" = { }; - zenith = { - User = "clo"; - Port = 22; - }; - "git.paperclover.net" = zenith; - "paperclover.net" = { - User = "clo"; - Port = 22; - }; - }; + # extraConfig asserts a default host block exists + settings."*" = { }; }; }; } diff --git a/users/clover/jujutsu.nix b/users/clover/jujutsu.nix new file mode 100644 index 0000000000000000000000000000000000000000..4ac4498a87a20fd2fdfc8a7ff3b8c95fff6ff26d --- /dev/null +++ b/users/clover/jujutsu.nix @@ -0,0 +1,113 @@ +{ + programs.jujutsu = { + enable = true; + # my workflow is based on creating changes and editing them. while they + # are in progress the description starts with "WIP: " + settings = rec { + colors = { + bookmark = "green"; + bookmarks = colors.bookmark; + local_bookmarks = colors.bookmarks; + remote_bookmarks = colors.bookmark; + "working_copy bookmark" = "bright green"; + "working_copy bookmarks" = colors."working_copy bookmark"; + "working_copy local_bookmarks" = colors."working_copy bookmark"; + "working_copy remote_bookmarks" = colors."working_copy bookmark"; + + "author" = { + fg = "bright black"; + bold = false; + italic = true; + }; + "working_copy author" = colors.author; + committer = { + fg = "bright black"; + bold = false; + }; + + desc = { + bold = true; + fg = "default"; + }; + description = colors.desc; + cc_wip = { + fg = "bright yellow"; + bold = true; + reverse = true; + }; + cc_header = { + bold = true; + fg = "default"; + }; + cc_feat = "green"; + cc_fix = "red"; + cc_hotfix = "red"; + cc_chore = "yellow"; + cc_test = "blue"; + cc_other = "blue"; + cc_scope = { + fg = "cyan"; + bold = false; + italic = true; + }; + + active_marker = { + fg = "bright magenta"; + bold = true; + reverse = true; + }; + "change_id shortest rest" = { + fg = "bright black"; + bold = true; + }; + conflict_marker = { + fg = "bright red"; + bold = true; + reverse = true; + }; + + timestamp = "bright black"; + "committer timestamp" = colors.committer; + commit_id = { + fg = "bright black"; + bold = false; + }; + muted = "bright black"; + }; + ui.default-command = "log"; + # ui.graph.style = "square"; + revsets = { + short-prefixes = "(trunk()..@)::"; + }; + "template-aliases" = { + "format_log_author(commit)" = '' + if(commit.mine(), "You", commit.author().name().match("[^ ]+")) + ''; + "format_log_timestamp(timestamp)" = '' + if(timestamp.after("1 day ago"), timestamp.ago(), timestamp.local().format("%Y-%m-%d")) + ''; + "format_short_commit_header(commit)" = '' + separate(" ", + format_short_change_id_with_change_offset(commit), + label("author", format_log_author(commit)), + label("timestamp", format_log_timestamp(commit_timestamp(commit))), + commit.bookmarks(), + commit.tags(), + commit.working_copies(), + format_commit_labels(commit), + if(config("ui.show-cryptographic-signatures").as_boolean(), + format_short_cryptographic_signature(commit.signature()) + ), + ) + ''; + }; + aliases = { + "ab" = [ "abandon" ]; + "desc" = [ + "describe" + "--ignore-working-copy" + ]; + }; + }; + }; +} diff --git a/users/clover/package.json b/users/clover/package.json deleted file mode 100644 index 00fbc7e587fc82d92d21e075432e10f0758c86e2..0000000000000000000000000000000000000000 --- a/users/clover/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "module", - "packageManager": "pnpm@10.28.0", - "dependencies": { - "@clo/lib": "jsr:*", - "@lydell/node-pty": "1.2.0-beta.12", - "@types/node": "^24.12.2" - } -} diff --git a/users/clover/packages/open-in-vscode-browser/default.nix b/users/clover/packages/open-in-vscode-browser/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..717565c875a505f52cebff21b7a515df830d5b6f --- /dev/null +++ b/users/clover/packages/open-in-vscode-browser/default.nix @@ -0,0 +1,28 @@ +{ pkgs, ... }: +# Drop-in replacement for macOS open(1) that, inside a VS Code integrated +# terminal, routes http(s) URLs into the built-in Simple Browser via the +# companion `simple-browser-bridge` extension's uri handler. Every other +# invocation (non-vscode terminal, files, apps, no `code` on PATH) falls +# through to the real /usr/bin/open with identical semantics. +# +# Named `open` so it shadows the system binary on PATH and is therefore +# honored by programs that spawn `open` directly (e.g. a dev TUI's +# "open browser" action), not just interactive shells. +pkgs.writeShellApplication { + name = "open"; + runtimeInputs = with pkgs; [ nodejs ]; + text = '' + if [ "''${TERM_PROGRAM:-}" = "vscode" ] && command -v code >/dev/null 2>&1; then + for arg in "$@"; do + case "$arg" in + http://* | https://*) + encoded=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$arg") + code --open-url "vscode://clover.simple-browser-bridge/open?url=$encoded" + exit 0 + ;; + esac + done + fi + exec /usr/bin/open "$@" + ''; +} diff --git a/users/clover/packages/open-in-vscode-browser/extension/extension.js b/users/clover/packages/open-in-vscode-browser/extension/extension.js new file mode 100644 index 0000000000000000000000000000000000000000..2efc23203df7f0d8c590812aa955e442dc7c4cfd --- /dev/null +++ b/users/clover/packages/open-in-vscode-browser/extension/extension.js @@ -0,0 +1,23 @@ +const vscode = require("vscode"); + +// Authority of the handled uri must equal this extension's identifier +// (publisher.name); see package.json. Opening +// vscode://clover.simple-browser-bridge/open?url= +// (e.g. via `code --open-url`, as the companion `open` shim does) shows the +// target in the built-in Simple Browser instead of the system browser. +function activate(context) { + context.subscriptions.push( + vscode.window.registerUriHandler({ + handleUri(uri) { + const url = new URLSearchParams(uri.query).get("url"); + if (url) { + vscode.commands.executeCommand("simpleBrowser.show", url); + } + }, + }), + ); +} + +function deactivate() {} + +module.exports = { activate, deactivate }; diff --git a/users/clover/packages/open-in-vscode-browser/extension/package.json b/users/clover/packages/open-in-vscode-browser/extension/package.json new file mode 100644 index 0000000000000000000000000000000000000000..61f51a8b5d0e91fb1856beaf1a285fa57f31a8c3 --- /dev/null +++ b/users/clover/packages/open-in-vscode-browser/extension/package.json @@ -0,0 +1,15 @@ +{ + "name": "simple-browser-bridge", + "displayName": "Simple Browser Bridge", + "description": "Routes terminal `open ` into the built-in Simple Browser via a vscode:// uri handler.", + "publisher": "clover", + "version": "0.0.1", + "engines": { + "vscode": "^1.74.0" + }, + "main": "./extension.js", + "activationEvents": [ + "onUri" + ], + "contributes": {} +} diff --git a/users/clover/paperback/configuration.nix b/users/clover/paperback/configuration.nix index cddc2b60f78df119b7b8768a4f02de75cc60a818..11308954371b004ffe79d3ca7a33643c96c282e2 100644 --- a/users/clover/paperback/configuration.nix +++ b/users/clover/paperback/configuration.nix @@ -3,4 +3,9 @@ nix.enable = false; nix.optimise.automatic = lib.mkForce false; nix.gc.automatic = lib.mkForce false; + + homebrew.casks = [ + "cleanshot" + "visual-studio-code" + ]; } diff --git a/users/clover/paperback/home.nix b/users/clover/paperback/home.nix index 1abb66c3b3a1627ae708fc6aa6c77af5f1b82f52..9a34c73b926486ef4227a02b3f6cb8c3b914bd20 100644 --- a/users/clover/paperback/home.nix +++ b/users/clover/paperback/home.nix @@ -3,10 +3,8 @@ home.packages = with pkgs; [ rustup typescript - pnpm yt-dlp spotdl zig - nodejs_26 ]; } diff --git a/users/clover/pnpm-lock.yaml b/users/clover/pnpm-lock.yaml deleted file mode 100644 index 874d3426b12527a9dbffe273deff9c6328347c12..0000000000000000000000000000000000000000 --- a/users/clover/pnpm-lock.yaml +++ /dev/null @@ -1,118 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@clo/lib": - specifier: jsr:* - version: "@jsr/clo__lib@4.0.0" - "@lydell/node-pty": - specifier: 1.2.0-beta.12 - version: 1.2.0-beta.12 - "@types/node": - specifier: ^24.12.2 - version: 24.12.2 - -packages: - "@jsr/clo__lib@4.0.0": - resolution: { - integrity: sha512-uOzRjwbFn9HizCpJeqPXzuiri3hqqcePNLA7ESi1Vgwon9dOmhMSzHfmb9kz599VsKF1RLH7i3NK302K60Oe/A==, - tarball: https://npm.jsr.io/~/11/@jsr/clo__lib/4.0.0.tgz, - } - - "@lydell/node-pty-darwin-arm64@1.2.0-beta.12": - resolution: { - integrity: sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==, - } - cpu: [arm64] - os: [darwin] - - "@lydell/node-pty-darwin-x64@1.2.0-beta.12": - resolution: { - integrity: sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==, - } - cpu: [x64] - os: [darwin] - - "@lydell/node-pty-linux-arm64@1.2.0-beta.12": - resolution: { - integrity: sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==, - } - cpu: [arm64] - os: [linux] - - "@lydell/node-pty-linux-x64@1.2.0-beta.12": - resolution: { - integrity: sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==, - } - cpu: [x64] - os: [linux] - - "@lydell/node-pty-win32-arm64@1.2.0-beta.12": - resolution: { - integrity: sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==, - } - cpu: [arm64] - os: [win32] - - "@lydell/node-pty-win32-x64@1.2.0-beta.12": - resolution: { - integrity: sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==, - } - cpu: [x64] - os: [win32] - - "@lydell/node-pty@1.2.0-beta.12": - resolution: { - integrity: sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==, - } - - "@types/node@24.12.2": - resolution: { - integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==, - } - - undici-types@7.16.0: - resolution: { - integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, - } - -snapshots: - "@jsr/clo__lib@4.0.0": {} - - "@lydell/node-pty-darwin-arm64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty-darwin-x64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty-linux-arm64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty-linux-x64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty-win32-arm64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty-win32-x64@1.2.0-beta.12": - optional: true - - "@lydell/node-pty@1.2.0-beta.12": - optionalDependencies: - "@lydell/node-pty-darwin-arm64": 1.2.0-beta.12 - "@lydell/node-pty-darwin-x64": 1.2.0-beta.12 - "@lydell/node-pty-linux-arm64": 1.2.0-beta.12 - "@lydell/node-pty-linux-x64": 1.2.0-beta.12 - "@lydell/node-pty-win32-arm64": 1.2.0-beta.12 - "@lydell/node-pty-win32-x64": 1.2.0-beta.12 - - "@types/node@24.12.2": - dependencies: - undici-types: 7.16.0 - - undici-types@7.16.0: {} diff --git a/users/clover/pnpm-workspace.yaml b/users/clover/pnpm-workspace.yaml deleted file mode 100644 index a1a0b69d7cfbab333ba43f5ce1ecca8c34f56e36..0000000000000000000000000000000000000000 --- a/users/clover/pnpm-workspace.yaml +++ /dev/null @@ -1,5 +0,0 @@ -packages: - - raycast-vscode-projects - -onlyBuiltDependencies: - - node-pty diff --git a/users/clover/raycast-vscode-projects/icon.svg b/users/clover/raycast-vscode-projects/icon.svg deleted file mode 100644 index 9c019a70f773ff41f6f15f93ec56760085d143e9..0000000000000000000000000000000000000000 --- a/users/clover/raycast-vscode-projects/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/users/clover/raycast-vscode-projects/package.json b/users/clover/raycast-vscode-projects/package.json deleted file mode 100644 index cd11ba403af38368dae999a79a84124144cbe7cb..0000000000000000000000000000000000000000 --- a/users/clover/raycast-vscode-projects/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://www.raycast.com/schemas/extension.json", - "name": "vscode-projects", - "title": "VS Code Projects", - "description": "Search and manage recent VS Code project windows", - "icon": "icon.svg", - "author": "clover", - "license": "MIT", - "packageManager": "pnpm@10.28.0", - "commands": [ - { - "name": "search-projects", - "title": "Search VS Code Projects", - "description": "Open, focus, and close recent VS Code projects", - "mode": "view" - } - ], - "dependencies": { - "@raycast/api": "^1.104.17", - "@raycast/utils": "^2.2.4" - }, - "devDependencies": { - "@types/node": "22.19.17", - "@types/react": "19.0.10", - "typescript": "^5.8.2" - }, - "scripts": { - "build": "ray build -e dist", - "dev": "ray develop" - } -} diff --git a/users/clover/raycast-vscode-projects/raycast-env.d.ts b/users/clover/raycast-vscode-projects/raycast-env.d.ts deleted file mode 100644 index e7a8fcf47c021d275324ded20bc7167f96c901c8..0000000000000000000000000000000000000000 --- a/users/clover/raycast-vscode-projects/raycast-env.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/// - -/* 🚧 🚧 🚧 - * This file is auto-generated from the extension's manifest. - * Do not modify manually. Instead, update the `package.json` file. - * 🚧 🚧 🚧 */ - -/* eslint-disable @typescript-eslint/ban-types */ - -type ExtensionPreferences = {} - -/** Preferences accessible in all the extension's commands */ -declare type Preferences = ExtensionPreferences - -declare namespace Preferences { - /** Preferences accessible in the `search-projects` command */ - export type SearchProjects = ExtensionPreferences & {} -} - -declare namespace Arguments { - /** Arguments passed to the `search-projects` command */ - export type SearchProjects = {} -} - diff --git a/users/clover/raycast-vscode-projects/src/search-projects.tsx b/users/clover/raycast-vscode-projects/src/search-projects.tsx deleted file mode 100644 index 5de05645df7c7db671465a1dd5345788ec379782..0000000000000000000000000000000000000000 --- a/users/clover/raycast-vscode-projects/src/search-projects.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { - Action, - ActionPanel, - Alert, - Color, - Icon, - List, - LocalStorage, - Toast, - closeMainWindow, - confirmAlert, - showToast, -} from "@raycast/api"; -import { usePromise } from "@raycast/utils"; -import { execFile } from "node:child_process"; -import { existsSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; -import path from "node:path"; -import { promisify } from "node:util"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const execFileAsync = promisify(execFile); -const codeBundleId = "com.microsoft.VSCode"; - -type RecentEntry = { folderUri?: string; workspace?: { configPath?: string } }; -type OpenWindow = { folder?: string; workspace?: string | { configPath?: string } }; -type Project = { - uri: string; - path: string; - title: string; - repo: string | undefined; - isOpen: boolean; -}; - -export default function Command() { - const { data, isLoading, revalidate } = usePromise(loadProjects); - - return ( - - {data?.map((project) => ( - - ))} - - ); -} - -function ProjectItem({ project, revalidate }: { project: Project; revalidate: () => void }) { - return ( - - focusOrOpen(project)} - /> - {project.isOpen && ( - { - await closeWindow(project); - revalidate(); - }} - /> - )} - - { - const confirmed = await confirmAlert({ - title: `Remove ${project.title}?`, - message: "The project stays on disk but is permanently hidden from this list.", - primaryAction: { title: "Remove", style: Alert.ActionStyle.Destructive }, - }); - if (!confirmed) return; - await hideProject(project.uri); - revalidate(); - }} - /> - - } - /> - ); -} - -async function loadProjects(): Promise { - const [recentEntries, openUris, hiddenUris] = await Promise.all([ - loadRecentEntries(), - loadOpenUris(), - loadHiddenUris(), - ]); - const seen = new Set(); - const projects = recentEntries.flatMap((entry) => { - const uri = entry.folderUri ?? entry.workspace?.configPath; - if (!uri || !uri.startsWith("file://") || seen.has(uri) || hiddenUris.has(uri)) return []; - seen.add(uri); - - const filesystemPath = fileURLToPath(uri); - const projectPath = entry.workspace?.configPath ? path.dirname(filesystemPath) : filesystemPath; - if (!existsSync(projectPath)) return []; - - return [ - { - uri, - path: projectPath, - title: prettyPath(projectPath), - repo: getRepoName(projectPath), - isOpen: openUris.has(uri), - }, - ]; - }); - - return projects.sort((a, b) => Number(b.isOpen) - Number(a.isOpen)); -} - -const hiddenUrisKey = "hiddenUris"; - -async function loadHiddenUris(): Promise> { - const raw = await LocalStorage.getItem(hiddenUrisKey); - return new Set(raw ? (JSON.parse(raw) as string[]) : []); -} - -async function hideProject(uri: string) { - const hidden = await loadHiddenUris(); - hidden.add(uri); - await LocalStorage.setItem(hiddenUrisKey, JSON.stringify([...hidden])); -} - -async function loadRecentEntries(): Promise { - const database = getSharedStorageDatabase(); - const { stdout } = await execFileAsync("sqlite3", [ - "--json", - "--readonly", - database, - "SELECT value FROM ItemTable WHERE key = 'history.recentlyOpenedPathsList'", - ]); - const row = JSON.parse(stdout || "[]") as { value: string }[]; - return row[0]?.value ? (JSON.parse(row[0].value).entries as RecentEntry[]) : []; -} - -function getSharedStorageDatabase() { - const productJson = "/Applications/Visual Studio Code.app/Contents/Resources/app/product.json"; - if (existsSync(productJson)) { - const product = JSON.parse(readFileSync(productJson, "utf8")) as { sharedDataFolderName?: string }; - if (product.sharedDataFolderName) { - const sharedDatabase = path.join(homedir(), product.sharedDataFolderName, "sharedStorage", "state.vscdb"); - if (existsSync(sharedDatabase)) return sharedDatabase; - } - } - - return path.join(homedir(), "Library/Application Support/Code/User/globalStorage/state.vscdb"); -} - -function loadOpenUris() { - const storagePath = path.join(homedir(), "Library/Application Support/Code/User/globalStorage/storage.json"); - if (!existsSync(storagePath)) return new Set(); - - const storage = JSON.parse(readFileSync(storagePath, "utf8")) as { - windowsState?: { lastActiveWindow?: OpenWindow; openedWindows?: OpenWindow[] }; - }; - const windows = [ - ...(storage.windowsState?.lastActiveWindow ? [storage.windowsState.lastActiveWindow] : []), - ...(storage.windowsState?.openedWindows ?? []), - ]; - const uris = windows.flatMap((window) => { - const workspace = typeof window.workspace === "string" ? window.workspace : window.workspace?.configPath; - return [window.folder, workspace]; - }); - - return new Set(uris.filter((uri): uri is string => typeof uri === "string")); -} - -async function focusOrOpen(project: Project) { - await closeMainWindow(); - await execFileAsync("open", ["-b", codeBundleId, project.path]); -} - -async function closeWindow(project: Project) { - const result = await closeProjectWindow(project); - if (result === "ok") { - await new Promise((resolve) => setTimeout(resolve, 300)); - await showToast({ title: "Closed VS Code window" }); - } else { - await showToast({ - style: Toast.Style.Failure, - title: result === "ambiguous" ? "Multiple matching VS Code windows" : "Could not find VS Code window", - }); - } -} - -async function closeProjectWindow(project: Project) { - try { - const { stdout } = await execFileAsync("osascript", [ - "-e", - `on run argv - set projectUri to item 1 of argv - set projectPath to item 2 of argv - - tell application "Visual Studio Code" to activate -tell application "System Events" - tell process "Code" - set documentMatches to {} - set titleMatches to {} - - repeat with candidate in windows - set windowName to name of candidate - set documentUri to "" - try - set rawDocumentUri to value of attribute "AXDocument" of candidate - if rawDocumentUri is not missing value then set documentUri to rawDocumentUri as text - end try - - if documentUri is not "" and (documentUri is projectUri or documentUri starts with projectUri & "/") then - set end of documentMatches to candidate - else if windowName is projectPath or windowName ends with projectPath or windowName contains projectPath & "/" or windowName contains projectPath & " " then - set end of titleMatches to candidate - end if - end repeat - - if length of documentMatches is 1 then - click button 1 of item 1 of documentMatches - return "ok" - end if - if length of documentMatches > 1 then return "ambiguous" - - if length of titleMatches is 1 then - click button 1 of item 1 of titleMatches - return "ok" - end if - if length of titleMatches > 1 then return "ambiguous" - end tell -end tell -return "missing" -end run`, - pathToFileURL(project.path).href, - project.path, - ]); - return stdout.trim() as "ok" | "missing" | "ambiguous"; - } catch { - return "missing"; - } -} - -function prettyPath(projectPath: string) { - return projectPath.startsWith(homedir()) ? `~${projectPath.slice(homedir().length)}` : projectPath; -} - -function getRepoName(startPath: string) { - for (let current = startPath; current !== path.dirname(current); current = path.dirname(current)) { - const configPath = getRemoteConfigPath(current); - if (!configPath) continue; - - const origin = readFileSync(configPath, "utf8").match(/\[remote "origin"\][\s\S]*?\n\s*url = (.+)/)?.[1]?.trim(); - return origin ? parseRepoName(origin) : undefined; - } -} - -function getRemoteConfigPath(directory: string) { - const jjConfig = path.join(directory, ".jj/repo/store/git/config"); - if (existsSync(jjConfig)) return jjConfig; - - const sharedJjRepo = path.join(directory, ".jj/repo"); - if (existsSync(sharedJjRepo) && statSync(sharedJjRepo).isFile()) { - const sharedJjConfig = path.join(readFileSync(sharedJjRepo, "utf8").trim(), "store/git/config"); - if (existsSync(sharedJjConfig)) return sharedJjConfig; - } - - const gitDirectory = path.join(directory, ".git"); - const gitConfig = path.join(gitDirectory, "config"); - if (existsSync(gitConfig)) return gitConfig; - - if (!existsSync(gitDirectory)) return undefined; - - const gitdir = readFileSync(gitDirectory, "utf8").match(/^gitdir: (.+)$/m)?.[1]; - const worktreeConfig = gitdir && path.join(directory, gitdir, "config"); - return worktreeConfig && existsSync(worktreeConfig) ? worktreeConfig : undefined; -} - -function parseRepoName(remote: string) { - const pathMatch = remote.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/); - return pathMatch?.[1]; -} diff --git a/users/clover/raycast-vscode-projects/tsconfig.json b/users/clover/raycast-vscode-projects/tsconfig.json deleted file mode 100644 index 8b8064b2cf98db619f482c71390fb09a4569e779..0000000000000000000000000000000000000000 --- a/users/clover/raycast-vscode-projects/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "lib": ["ES2023"], - "module": "commonjs", - "target": "ES2023", - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node" - } -} diff --git a/users/clover/sandwich/configuration.nix b/users/clover/sandwich/configuration.nix index a9702e92307c9413cb1bce7a6f42c15b9b6a8715..0557409325b7807bf6037faae0fed8314b24a125 100644 --- a/users/clover/sandwich/configuration.nix +++ b/users/clover/sandwich/configuration.nix @@ -7,4 +7,9 @@ "motion" ]; }; + + homebrew.casks = [ + "cleanshot" + "zed" + ]; } diff --git a/users/clover/sandwich/home.nix b/users/clover/sandwich/home.nix index 8c0c2d665828c3375b6f4d577060348ff70a059f..9ead8d698aa9448bd8429e840acb137b6da05689 100644 --- a/users/clover/sandwich/home.nix +++ b/users/clover/sandwich/home.nix @@ -65,7 +65,6 @@ let in { home.packages = with pkgs; [ - nodejs_26 rsync sandwich-backup ]; diff --git a/users/clover/scripts/claude-statusline.sh b/users/clover/scripts/claude-statusline.sh deleted file mode 100755 index d7ff928c52aead2b132be711b0a4875170806455..0000000000000000000000000000000000000000 --- a/users/clover/scripts/claude-statusline.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -# Four segments: model, thinking/effort, context used %, rate-limit remaining. -# Meters ramp cool -> hot (blue, cyan, green, yellow, red) as each stat grows big. -input=$(cat) - -sep_color=240 -model_color=176 -thinking_color=108 - -color_for_pct() { - local pct="${1%.*}" - if (( pct >= 85 )); then echo 203 - elif (( pct >= 70 )); then echo 214 - elif (( pct >= 50 )); then echo 114 - elif (( pct >= 25 )); then echo 80 - else echo 111 - fi -} - -humanize() { - local n=$1 - if (( n >= 1000000 )); then - printf '%s' "$(awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}' | sed 's/\.0M/M/')" - elif (( n >= 1000 )); then - printf '%dk' $(( n / 1000 )) - else - printf '%d' "$n" - fi -} - -model=$(printf '%s' "$input" | jq -r '.model.display_name // ""') -model_short=$(grep -oE 'Opus|Sonnet|Haiku' <<<"$model" | head -1) -[[ -z "$model_short" ]] && model_short="$model" - -effort=$(printf '%s' "$input" | jq -r '.effort.level // ""') -if [[ -n "$effort" ]]; then - thinking="$(tr '[:lower:]' '[:upper:]' <<<"${effort:0:1}")${effort:1}" -elif [[ "$(printf '%s' "$input" | jq -r '.thinking.enabled // false')" == "true" ]]; then - thinking="On" -else - thinking="Off" -fi - -segments=("$(printf "\033[38;5;%sm%s\033[0m" "$model_color" "$model_short")" "$(printf "\033[38;5;%sm%s\033[0m" "$thinking_color" "$thinking")") - -ctx_pct=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // empty') -if [[ -n "$ctx_pct" ]]; then - read -r ctx_used ctx_size < <(printf '%s' "$input" | jq -r '.context_window | "\(.total_input_tokens // 0) \(.context_window_size // 0)"') - c=$(color_for_pct "$ctx_pct") - if (( ctx_size > 0 )); then - segments+=("$(printf "\033[38;5;%sm%.0f%% ctx (%s/%s)\033[0m" "$c" "$ctx_pct" "$(humanize "$ctx_used")" "$(humanize "$ctx_size")")") - else - segments+=("$(printf "\033[38;5;%sm%.0f%% ctx\033[0m" "$c" "$ctx_pct")") - fi -fi - -five=$(printf '%s' "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') -week=$(printf '%s' "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') -if [[ -n "$five" || -n "$week" ]]; then - limits="" - if [[ -n "$five" ]]; then - c=$(color_for_pct "$five") - limits+=$(printf "\033[38;5;%sm%.0f%% 5h\033[0m" "$c" "$(( 100 - ${five%.*} ))") - fi - if [[ -n "$week" ]]; then - [[ -n "$limits" ]] && limits+=$(printf " \033[38;5;%sm/\033[0m " "$sep_color") - c=$(color_for_pct "$week") - limits+=$(printf "\033[38;5;%sm%.0f%% 7d\033[0m" "$c" "$(( 100 - ${week%.*} ))") - fi - segments+=("$limits") -fi - -sep=$(printf " \033[38;5;%sm·\033[0m " "$sep_color") -line="${segments[0]}" -for ((i = 1; i < ${#segments[@]}; i++)); do - line+="${sep}${segments[i]}" -done -printf '%s\n' "$line" diff --git a/users/clover/scripts/jj-ci.ts b/users/clover/scripts/jj-ci.ts deleted file mode 100755 index b503e916b09bfd2cab623b5089ff6e0fd4bc33d5..0000000000000000000000000000000000000000 --- a/users/clover/scripts/jj-ci.ts +++ /dev/null @@ -1,1167 +0,0 @@ -#!/usr/bin/env node -import * as child_process from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import * as log from "@clo/lib/log"; -import * as progress from "@clo/lib/progress"; -import * as ansi from "@clo/lib/string/ansi"; -import * as pty from "@lydell/node-pty"; -import * as ts from "@clo/lib/ts"; -import process from "node:process"; - -const progressRoot = new progress.Root(); -progressAttachToScreen(progressRoot, log); - -type CommandOptions = { - cwd?: string; - env?: NodeJS.ProcessEnv; - ignoreWorkingCopy?: boolean; - node?: progress.Node; - plainLog?: string[]; -}; - -type Plan = { - changeId: string; - dependencyChangeIds: string[]; - label: string; - logPath: string; -}; - -type ScriptResult = { - durationMs: number; - exitCode: number | null; - ok: boolean; - plainText: string; -}; - -type JobPhase = "prep" | "ci" | "skipped" | "snapshot" | "stalled"; -type JobResult = { - blockedBy?: string[]; - changeId: string; - durationMs: number; - exitCode: number | null; - label: string; - logPath?: string; - ok: boolean; - phase: JobPhase; - workspacePath?: string; -}; - -type JobState = "failed" | "ok" | "pending" | "running" | "skipped"; - -type PoolSlot = { - name: string; - workspaceName: string; - workspacePath: string; -}; - -type ActiveJob = { - slot: PoolSlot; - task: Promise; -}; - -const dimStyle = ansi.dim + ansi.fgBrightBlack; -const headingStyle = ansi.bold + ansi.fgBrightWhite; -const okStyle = ansi.bold + ansi.fgBrightGreen; -const failStyle = ansi.bold + ansi.fgBrightRed; -const skipStyle = ansi.bold + ansi.fgBrightYellow; -const infoStyle = ansi.bold + ansi.fgBrightCyan; -const workspaceCacheManifestName = ".paths"; - -let clean = false; -let dryRun = false; -let ignoreImmutable = false; -let jobs = os.availableParallelism(); -let parallelize = false; -let retry = false; -let sawJobs = false; -let warnedAboutPtyFallback = false; -const revisions: string[] = []; - -const argv = process.argv.slice(2); -while (argv.length > 0) { - const arg = argv.shift(); - if (arg == null) break; - - if (arg === "-h" || arg === "--help") usage(0); - else if (arg === "--dry-run") dryRun = true; - else if (arg === "--clean") clean = true; - else if (arg === "--ignore-immutable") ignoreImmutable = true; - else if (arg === "-p" || arg === "--parallel") parallelize = true; - else if (arg === "-r" || arg === "--retry") retry = true; - else if (arg === "-j" || arg === "--jobs" || arg === "--cores") { - if (sawJobs) { - log.error("can only pass --jobs once"); - usage(1); - } - - const value = argv.shift(); - if (value == null) { - log.error("missing value for --jobs"); - usage(1); - } - - jobs = Number(value); - sawJobs = true; - } else if (arg.startsWith("--jobs=") || arg.startsWith("--cores=")) { - if (sawJobs) { - log.error("can only pass --jobs once"); - usage(1); - } - - jobs = Number(arg.slice(arg.indexOf("=") + 1)); - sawJobs = true; - } else if (arg === "--") { - revisions.push(...argv); - break; - } else if (arg.startsWith("-")) { - log.error("unknown option", JSON.stringify(arg)); - usage(1); - } else { - revisions.push(arg); - } -} - -if (!Number.isInteger(jobs) || jobs < 1) { - log.error("--jobs must be a positive integer"); - usage(1); -} - -if (retry && revisions.length > 0) { - log.error("cannot pass revisions together with --retry"); - usage(1); -} - -if (!retry && revisions.length === 0) usage(1); - -const root = jj(["workspace", "root"]).trim(); -const cacheDir = path.join(root, ".jj", "ci"); -const lastRunPath = path.join(cacheDir, "last-run.txt"); -const logDir = path.join(cacheDir, "_logs"); -const ciScript = path.join(root, ".jj", "ci.sh"); -const gitDir = resolveGitDir(root); -const selectedRevisions = retry ? readLastRun(lastRunPath) : revisions; - -let invalid = false; - -if (!fs.existsSync(ciScript)) { - log.error("missing .jj/ci.sh"); - invalid = true; -} - -if (invalid) process.exit(1); - -if (parallelize) { - if (dryRun) { - log.info("would run", "jj parallelize " + selectedRevisions.join(" ")); - } else jj(["parallelize", ...selectedRevisions]); -} - -const changeIds = [...new Set(selectedRevisions.flatMap(resolveChangeIds))]; -const selectedExpr = `(${changeIds.join("|")})`; -const trackedWorkspaces = new Set( - jj(["workspace", "list"], { ignoreWorkingCopy: true }) - .split("\n") - .filter(Boolean) - .map((line) => line.split(":", 1)[0]?.trim()) - .filter((name): name is string => Boolean(name)), -); -const plans: Plan[] = changeIds.map((changeId) => ({ - changeId, - dependencyChangeIds: [ - ...new Set( - jj( - [ - "log", - "--no-graph", - "-r", - `ancestors(${changeId}) & ${selectedExpr}`, - "-T", - 'self.change_id() ++ "\n"', - ], - { ignoreWorkingCopy: true }, - ) - .split("\n") - .filter((candidate) => candidate !== "" && candidate !== changeId), - ), - ], - label: changeId.slice(0, 6) + ": " + jj( - [ - "log", - "--no-graph", - "-r", - `${changeId}`, - "-T", - "self.description().first_line()", - ], - { ignoreWorkingCopy: true }, - ), - logPath: path.join(logDir, `ci-${changeId}.log`), -})); -const concurrency = Math.min(jobs, maxPlanConcurrency(plans)); -const pool = Array.from({ length: concurrency }, (_, index): PoolSlot => { - const name = poolName(index); - return { - name, - workspaceName: `ci-${name}`, - workspacePath: path.join(cacheDir, name), - }; -}); -const planByChangeId = new Map(plans.map((plan) => [plan.changeId, plan])); -const childrenByChangeId = new Map( - changeIds.map((changeId) => [changeId, [] as string[]]), -); - -for (const plan of plans) { - for (const dependencyChangeId of plan.dependencyChangeIds) { - childrenByChangeId.get(dependencyChangeId)?.push(plan.changeId); - } -} - -if (dryRun) { - log.writeOutput( - JSON.stringify( - { - cacheDir, - ciScript, - clean, - concurrency, - ignoreImmutable, - parallelize, - plans, - pool, - retry, - selectedRevisions, - root, - }, - null, - 2, - ) + "\n", - ); - process.exit(0); -} - -if (clean && fs.existsSync(cacheDir)) { - fs.rmSync(cacheDir, { force: true, recursive: true }); -} - -fs.mkdirSync(cacheDir, { recursive: true }); -fs.mkdirSync(logDir, { recursive: true }); - -let runError: unknown = null; - -try { - const ready = plans.filter((plan) => plan.dependencyChangeIds.length === 0); - const availableSlots = [...pool]; - const remainingDependenciesByChangeId = new Map( - plans.map((plan) => [plan.changeId, plan.dependencyChangeIds.length]), - ); - const resultsByChangeId = new Map(); - const stateByChangeId = new Map( - changeIds.map((changeId) => [changeId, "pending"]), - ); - const active = new Map(); - - { - using runNode = progressRoot.start("continuous integration", { - total: plans.length, - valueFormatter() { - let passed = 0; - let failed = 0; - for (const state of stateByChangeId.values()) { - if (state === "ok") passed += 1; - else if (state === "failed" || state === "skipped") failed += 1; - } - return `${ansi.fgGreen}${passed} pass${ansi.reset}, ${ - failed > 0 ? ansi.fgRed : "" - }${failed} fail${ansi.reset}`; - }, - }); - - while (resultsByChangeId.size < plans.length) { - while ( - ready.length > 0 && - active.size < concurrency && - availableSlots.length > 0 - ) { - const plan = ready.shift(); - const slot = availableSlots.shift(); - if (plan == null || slot == null) break; - - stateByChangeId.set(plan.changeId, "running"); - - const task = (async (): Promise => { - const startedAt = Date.now(); - using node = runNode.start(plan.label); - if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath); - const env = { - ...process.env, - JJ_CI_CHANGE_ID: plan.changeId, - JJ_CI_ROOT: root, - JJ_CI_WORKSPACE: slot.workspacePath, - }; - const plainLog = [ - `change: ${plan.changeId}\n`, - `dependencies: ${ - plan.dependencyChangeIds.length === 0 - ? "(selected root)" - : plan.dependencyChangeIds.join(", ") - }\n`, - `slot: ${slot.name}\n`, - `workspace: ${slot.workspacePath}\n`, - `started: ${new Date(startedAt).toISOString()}\n\n`, - ]; - let phase: JobPhase = "prep"; - - try { - plainLog.push("--- prep ---\n"); - ensureWorkspace( - slot, - plan.changeId, - plan.label, - trackedWorkspaces, - node, - plainLog, - ); - plainLog.push("\n"); - - phase = "ci"; - node.text = `${plan.label} ci [slot ${slot.name}]`; - const ci = await runScript(ciScript, { - cwd: slot.workspacePath, - env, - node, - }); - plainLog.push( - `--- ci ---\nexit: ${ - ci.exitCode ?? "signal" - }\nduration_ms: ${ci.durationMs}\n\n`, - ); - plainLog.push(ci.plainText); - if (ci.plainText !== "" && !ci.plainText.endsWith("\n")) { - plainLog.push("\n"); - } - plainLog.push("\n"); - - phase = "snapshot"; - node.text = `${plan.label} snapshot [slot ${slot.name}]`; - plainLog.push("--- snapshot ---\n"); - snapshotWorkspace(slot.workspacePath, node, plainLog); - plainLog.push("\n"); - - if (!ci.ok) { - writeFailureLog(plan.logPath, plainLog); - return { - changeId: plan.changeId, - durationMs: Date.now() - startedAt, - exitCode: ci.exitCode, - label: plan.label, - logPath: plan.logPath, - ok: false, - phase: "ci", - workspacePath: slot.workspacePath, - }; - } - - node.value += 1; - node.text = `${plan.label} ok [slot ${slot.name}]`; - if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath); - return { - changeId: plan.changeId, - durationMs: Date.now() - startedAt, - exitCode: ci.exitCode, - label: plan.label, - ok: true, - phase: "ci", - workspacePath: slot.workspacePath, - }; - } catch (error) { - const message = error instanceof Error - ? (error.stack ?? error.message) - : String(error); - node.log.error(error); - plainLog.push( - `--- ${phase} error ---\n${normalizePlain(message)}\n`, - ); - writeFailureLog(plan.logPath, plainLog); - return { - changeId: plan.changeId, - durationMs: Date.now() - startedAt, - exitCode: null, - label: plan.label, - logPath: plan.logPath, - ok: false, - phase, - workspacePath: slot.workspacePath, - }; - } - })(); - - active.set(plan.changeId, { slot, task }); - } - - if (active.size === 0) { - let skippedAny = false; - - for (const plan of plans) { - if (stateByChangeId.get(plan.changeId) !== "pending") continue; - - const blockedBy = plan.dependencyChangeIds.filter( - (dependencyChangeId) => { - const dependencyState = stateByChangeId.get(dependencyChangeId); - return dependencyState === "failed" || - dependencyState === "skipped"; - }, - ); - - if (blockedBy.length === 0) continue; - - skippedAny = true; - stateByChangeId.set(plan.changeId, "skipped"); - if (fs.existsSync(plan.logPath)) fs.rmSync(plan.logPath); - resultsByChangeId.set(plan.changeId, { - blockedBy, - changeId: plan.changeId, - durationMs: 0, - exitCode: null, - label: plan.label, - ok: false, - phase: "skipped", - }); - } - - if (skippedAny) { - runNode.value = resultsByChangeId.size; - continue; - } - - log.error("dependency scheduling stalled"); - for (const plan of plans) { - if (stateByChangeId.get(plan.changeId) !== "pending") continue; - - resultsByChangeId.set(plan.changeId, { - blockedBy: plan.dependencyChangeIds.filter((dependencyChangeId) => - stateByChangeId.get(dependencyChangeId) !== "ok" - ), - changeId: plan.changeId, - durationMs: 0, - exitCode: null, - label: plan.label, - ok: false, - phase: "stalled", - }); - stateByChangeId.set(plan.changeId, "failed"); - } - runNode.value = resultsByChangeId.size; - process.exitCode = 1; - continue; - } - - const result = await Promise.race( - [...active.values()].map(({ task }) => task), - ); - const completedJob = active.get(result.changeId); - if (completedJob == null) { - throw new Error(`missing active job for ${result.changeId}`); - } - active.delete(result.changeId); - availableSlots.push(completedJob.slot); - resultsByChangeId.set(result.changeId, result); - stateByChangeId.set(result.changeId, result.ok ? "ok" : "failed"); - runNode.value = resultsByChangeId.size; - - if (!result.ok) continue; - - for ( - const childChangeId of childrenByChangeId.get(result.changeId) ?? [] - ) { - if (stateByChangeId.get(childChangeId) !== "pending") continue; - - const remainingDependencies = - (remainingDependenciesByChangeId.get(childChangeId) ?? 1) - 1; - remainingDependenciesByChangeId.set( - childChangeId, - remainingDependencies, - ); - - if (remainingDependencies === 0) { - const childPlan = planByChangeId.get(childChangeId); - if (childPlan != null) ready.push(childPlan); - } - } - } - } - - const results = plans.map((plan) => { - const result = resultsByChangeId.get(plan.changeId); - if (result == null) { - throw new Error(`missing result for ${plan.changeId}`); - } - return result; - }); - const failures = results.filter( - (result) => result.phase !== "skipped" && !result.ok, - ); - const skipped = results.filter((result) => result.phase === "skipped"); - const passed = results.length - failures.length - skipped.length; - const successfulChangeIds = results - .filter((result) => result.ok) - .map((result) => result.changeId.slice(0, 8)); - - if (failures.length > 0 || skipped.length > 0) { - writeLastRun(lastRunPath, changeIds); - } - - const summary: string[] = [ - "\n", - ansi.style(headingStyle, "summary"), - "\n", - ` ${ansi.style(okStyle, "pass:")} ${passed}/${results.length}`, - "\n", - ` ${ansi.style(failStyle, "fail:")} ${failures.length}/${results.length}`, - "\n", - ` ${ansi.style(skipStyle, "skip:")} ${skipped.length}/${results.length}`, - "\n", - ]; - - if (failures.length > 0) { - summary.push("\n", ansi.style(failStyle, "failures"), "\n"); - for (const result of failures) { - summary.push( - `${ansi.style(failStyle, "x")} ${result.label} (${result.phase})`, - result.blockedBy?.length - ? ` ${ - ansi.style( - dimStyle, - `[waiting on ${ - result.blockedBy.map((id) => id.slice(0, 12)).join(", ") - }]`, - ) - }` - : "", - result.logPath - ? ` ${ansi.style(dimStyle, `[log ${result.logPath}]`)}` - : "", - "\n", - ); - } - } - - if (skipped.length > 0) { - summary.push("\n", ansi.style(skipStyle, "skipped"), "\n"); - for (const result of skipped) { - summary.push( - `${ansi.style(skipStyle, "-")} ${result.label}`, - result.blockedBy?.length - ? ` ${ - ansi.style( - dimStyle, - `[blocked by ${ - result.blockedBy.map((id) => id.slice(0, 12)).join(", ") - }]`, - ) - }` - : "", - "\n", - ); - } - } - - if (failures.length === 0 && skipped.length === 0) { - summary.push( - "\n", - ansi.style(infoStyle, "all selected changes passed"), - "\n", - ); - } - - if (successfulChangeIds.length > 0) { - summary.push( - "\n", - `${ansi.style(infoStyle, "push successful commits:")} jj git push -c "${ - successfulChangeIds.join("|") - }"`, - "\n", - ); - } - - log.writeOutput(summary.join("")); - process.exitCode = failures.length > 0 || skipped.length > 0 ? 1 : 0; -} catch (error) { - runError = error; - throw error; -} finally { - try { - forgetPoolWorkspaces(pool, trackedWorkspaces); - } catch (cleanupError) { - if (runError != null) { - log.error(cleanupError); - } else { - throw cleanupError; - } - } -} - -function usage(code: number): never { - log.writeOutput(`jj ci: run continuous integration on many revisions at once - -usage: jj ci [options] <...revsets> - or: jj ci [options] --retry - -requires \`./.jj/ci.sh\` to setup and run checks. for example: - - #!/bin/sh - pnpm install - pnpm format - pnpm test - -the script is run with fast failure, so if a command fails, it fails the run. - -options: - -j, --jobs=[n] maximum number of jobs to run at once - -p, --parallel rewrite the selected revisions with \`jj parallelize\` - -r, --retry re-run the set of commits that most recently failed - --ignore-immutable allow rewriting immutable commits - --dry-run print the execution plan - --clean delete the workspace cache first - -jobs will be run in separate workspaces placed in \`.jj/ci\`, and -autofixes will be propagated across your revisions. if your changes are all -separate and aren't meant to conflict, pass \`-p\` to run \`jj parallelize\` -first. parallel commits will be run in parallel - -'jj ci' reuses worktrees within a single execution, then forgets the -temporary workspaces afterward so they don't show up in \`jj log\`. -'jj ci' speeds up multiple runs by preserving ignored files between runs (for caches). -`); - process.exit(code); -} - -function resolveGitDir(cwd: string): string | null { - const result = child_process.spawnSync( - "git", - ["-C", cwd, "rev-parse", "--absolute-git-dir"], - { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }, - ); - if (result.error || result.status !== 0) return null; - - const value = (result.stdout ?? "").trim(); - if (value === "") return null; - return value; -} - -function jj(args: string[], options: CommandOptions = {}): string { - const result = child_process.spawnSync( - "jj", - [ - ...(options.ignoreWorkingCopy ? ["--ignore-working-copy"] : []), - ...(ignoreImmutable ? ["--ignore-immutable"] : []), - ...args, - ], - { - cwd: options.cwd, - encoding: "utf8", - env: options.env, - stdio: ["ignore", "pipe", "pipe"], - }, - ); - const stdout = result.stdout ?? ""; - const stderr = result.stderr ?? ""; - const text = stdout + stderr; - - if (options.node && text !== "") options.node.log.write(text); - if (options.plainLog && text !== "") { - options.plainLog.push(normalizePlain(text)); - } - - if (result.error) throw result.error; - if (result.status !== 0) { - throw new Error( - `jj ${args.join(" ")} failed with exit code ${result.status}` + - (text ? `\n\n${normalizePlain(text)}` : ""), - ); - } - - return stdout.trimEnd(); -} - -function listIgnoredWorkspacePaths(workspacePath: string): string[] | null { - if (gitDir == null) return null; - - const result = child_process.spawnSync( - "git", - [ - `--git-dir=${gitDir}`, - `--work-tree=${workspacePath}`, - "ls-files", - "--others", - "-i", - "--exclude-standard", - "--directory", - ], - { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }, - ); - if (result.error || result.status !== 0) return null; - - return (result.stdout ?? "") - .split("\n") - .map((line) => normalizeRelativeCachePath(line)) - .filter((line): line is string => line != null); -} - -function normalizeRelativeCachePath(relativePath: string): string | null { - const trimmed = relativePath.trim().replace(/\/+$/g, ""); - if (trimmed === "") return null; - - const normalized = path.normalize(trimmed); - if ( - normalized === ".git" || - normalized === ".jj" || - normalized.startsWith(`.git${path.sep}`) || - normalized.startsWith(`.jj${path.sep}`) || - normalized === ".." || - normalized.startsWith(`..${path.sep}`) || - path.isAbsolute(normalized) - ) { - return null; - } - - return normalized; -} - -function pathExists(filePath: string): boolean { - try { - fs.lstatSync(filePath); - return true; - } catch { - return false; - } -} - -function ensureWorkspace( - slot: PoolSlot, - changeId: string, - label: string, - trackedWorkspaces: Set, - node: progress.Node, - plainLog: string[], -): void { - const hasWorkspace = trackedWorkspaces.has(slot.workspaceName); - const hasWorkspacePath = fs.existsSync(slot.workspacePath); - let createdWorkspace = false; - - if (hasWorkspace && !hasWorkspacePath) { - log.warn("forgetting stale workspace", slot.workspaceName); - jj(["workspace", "forget", slot.workspaceName], { - ignoreWorkingCopy: true, - node, - plainLog, - }); - trackedWorkspaces.delete(slot.workspaceName); - } - - if (!hasWorkspace && hasWorkspacePath) { - preserveIgnoredWorkspaceCache(slot, plainLog); - log.warn("removing orphaned workspace", slot.workspacePath); - fs.rmSync(slot.workspacePath, { force: true, recursive: true }); - } - - if (!trackedWorkspaces.has(slot.workspaceName)) { - createdWorkspace = true; - jj( - [ - "workspace", - "add", - "--name", - slot.workspaceName, - "--revision", - changeId, - slot.workspacePath, - ], - { node, plainLog }, - ); - trackedWorkspaces.add(slot.workspaceName); - } else { - jj(["workspace", "update-stale"], { - cwd: slot.workspacePath, - node, - plainLog, - }); - } - - jj(["edit", changeId], { cwd: slot.workspacePath, node, plainLog }); - if (createdWorkspace) restoreIgnoredWorkspaceCache(slot, plainLog); -} - -function snapshotWorkspace( - workspacePath: string, - node: progress.Node, - plainLog: string[], -): void { - jj(["status"], { cwd: workspacePath, node, plainLog }); -} - -function workspaceCachePath(slot: PoolSlot): string { - return path.join(cacheDir, `${slot.name}.cache`); -} - -function workspaceCacheManifestPath(slot: PoolSlot): string { - return path.join(workspaceCachePath(slot), workspaceCacheManifestName); -} - -function preserveIgnoredWorkspaceCache( - slot: PoolSlot, - plainLog: string[], -): void { - const ignoredPaths = listIgnoredWorkspacePaths(slot.workspacePath); - if (ignoredPaths == null) return; - if (ignoredPaths.length === 0) return; - - const cachePath = workspaceCachePath(slot); - if (fs.existsSync(cachePath)) { - fs.rmSync(cachePath, { force: true, recursive: true }); - } - - fs.mkdirSync(cachePath, { recursive: true }); - for (const relativePath of ignoredPaths) { - const sourcePath = path.join(slot.workspacePath, relativePath); - if (!pathExists(sourcePath)) continue; - - const destinationPath = path.join(cachePath, relativePath); - fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); - fs.renameSync(sourcePath, destinationPath); - } - - fs.writeFileSync( - workspaceCacheManifestPath(slot), - ignoredPaths.join("\n") + "\n", - ); - plainLog.push( - `preserved ignored cache paths: ${ignoredPaths.join(", ")}\n`, - ); -} - -function restoreIgnoredWorkspaceCache( - slot: PoolSlot, - plainLog: string[], -): void { - const manifestPath = workspaceCacheManifestPath(slot); - if (!fs.existsSync(manifestPath)) return; - - const cachePath = workspaceCachePath(slot); - const relativePaths = fs.readFileSync(manifestPath, "utf8") - .split("\n") - .map((line) => line.trim()) - .filter(Boolean); - - if (relativePaths.length === 0) { - fs.rmSync(cachePath, { force: true, recursive: true }); - return; - } - - const restoredPaths: string[] = []; - const skippedPaths: string[] = []; - - for (const relativePath of relativePaths) { - const sourcePath = path.join(cachePath, relativePath); - if (!pathExists(sourcePath)) continue; - - const destinationPath = path.join(slot.workspacePath, relativePath); - if (pathExists(destinationPath)) { - skippedPaths.push(relativePath); - continue; - } - - try { - fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); - fs.renameSync(sourcePath, destinationPath); - restoredPaths.push(relativePath); - } catch { - skippedPaths.push(relativePath); - } - } - - if (restoredPaths.length > 0) { - plainLog.push( - `restored ignored cache paths: ${restoredPaths.join(", ")}\n`, - ); - } - - if (skippedPaths.length > 0) { - plainLog.push( - `skipped ignored cache paths: ${skippedPaths.join(", ")}\n`, - ); - fs.writeFileSync(manifestPath, skippedPaths.join("\n") + "\n"); - } else { - fs.rmSync(cachePath, { force: true, recursive: true }); - } -} - -function forgetPoolWorkspaces( - pool: PoolSlot[], - trackedWorkspaces: Set, -): void { - const errors: Error[] = []; - - for (const slot of pool) { - if (!trackedWorkspaces.has(slot.workspaceName)) continue; - - try { - if (fs.existsSync(slot.workspacePath)) { - preserveIgnoredWorkspaceCache(slot, []); - } - jj(["workspace", "forget", slot.workspaceName], { - cwd: root, - ignoreWorkingCopy: true, - }); - trackedWorkspaces.delete(slot.workspaceName); - } catch (error) { - errors.push( - error instanceof Error ? error : new Error(String(error)), - ); - } - } - - if (errors.length > 0) { - throw new AggregateError( - errors, - "failed to forget one or more jj-ci workspaces", - ); - } -} - -function resolveChangeIds(revision: string): string[] { - const output = jj( - [ - "log", - "--no-graph", - "-r", - revision, - "-T", - 'self.change_id() ++ "\n"', - ], - { ignoreWorkingCopy: true }, - ); - const changeIds = output.split("\n").filter(Boolean); - - if (changeIds.length === 0) { - log.error("revision resolved to no change IDs", JSON.stringify(revision)); - process.exit(1); - } - - return changeIds; -} - -function readLastRun(lastRunPath: string): string[] { - if (!fs.existsSync(lastRunPath)) { - log.error("no failed run to retry:", lastRunPath); - process.exit(1); - } - - const changeIds = fs.readFileSync(lastRunPath, "utf8") - .split("\n") - .map((line) => line.trim()) - .filter(Boolean); - - if (changeIds.length === 0) { - log.error("retry file is empty:", lastRunPath); - process.exit(1); - } - - return changeIds; -} - -function writeLastRun(lastRunPath: string, changeIds: string[]): void { - fs.mkdirSync(path.dirname(lastRunPath), { recursive: true }); - fs.writeFileSync(lastRunPath, changeIds.join("\n") + "\n"); -} - -function runScript( - scriptPath: string, - options: CommandOptions, -): Promise { - return new Promise((resolve) => { - const startedAt = Date.now(); - let plainText = ""; - const baseEnv = Object.fromEntries( - Object.entries({ - CLICOLOR_FORCE: "1", - FORCE_COLOR: "1", - TERM: process.env.TERM ?? "xterm-256color", - ...options.env, - }).filter((entry): entry is [string, string] => entry[1] != null), - ); - delete baseEnv.NO_COLOR; - const preferredShell = process.env.SHELL; - const shell = fs.existsSync("/bin/bash") - ? "/bin/bash" - : preferredShell != null && path.basename(preferredShell) === "bash" - ? preferredShell - : "bash"; - - const term = pty.spawn(shell, [ - "-c", - 'set -o pipefail; set -eux; source "$1"', - "jj-ci", - scriptPath, - ], { - cols: process.stdout.columns ?? 120, - cwd: options.cwd, - encoding: "utf8", - env: baseEnv, - name: process.env.TERM ?? "xterm-256color", - rows: process.stdout.rows ?? 30, - }); - - term.onData((chunk) => { - plainText += normalizePlain(chunk); - options.node?.log.write(chunk); - }); - - term.onExit(({ exitCode, signal }) => { - if (signal != null) { - const note = ansi.style(failStyle, `terminated by signal ${signal}`); - plainText += normalizePlain(note); - options.node?.log.info(note); - } - resolve({ - durationMs: Date.now() - startedAt, - exitCode, - ok: exitCode === 0, - plainText, - }); - }); - }); -} - -function writeFailureLog(logPath: string, plainLog: string[]): void { - fs.mkdirSync(path.dirname(logPath), { recursive: true }); - fs.writeFileSync(logPath, plainLog.join("")); -} - -function normalizePlain(text: string): string { - return ansi.strip(text) - .replace(/\r\n/g, "\n") - .replace(/\r/g, "\n"); -} - -function poolName(index: number): string { - let value = index; - let name = ""; - - while (value >= 0) { - name = String.fromCharCode(97 + (value % 26)) + name; - value = Math.floor(value / 26) - 1; - } - - return name; -} - -function maxPlanConcurrency(plans: Plan[]): number { - if (plans.length === 0) return 0; - - const descendantsByChangeId = new Map( - plans.map((plan) => [plan.changeId, [] as string[]]), - ); - - for (const plan of plans) { - for (const dependencyChangeId of plan.dependencyChangeIds) { - descendantsByChangeId.get(dependencyChangeId)?.push(plan.changeId); - } - } - - const matchedDependencyByDescendant = new Map(); - let matchCount = 0; - - for (const plan of plans) { - if ( - tryMatchDescendant( - plan.changeId, - descendantsByChangeId, - matchedDependencyByDescendant, - new Set(), - ) - ) { - matchCount += 1; - } - } - - return plans.length - matchCount; -} - -function tryMatchDescendant( - dependencyChangeId: string, - descendantsByChangeId: Map, - matchedDependencyByDescendant: Map, - seenDescendants: Set, -): boolean { - for ( - const descendantChangeId of descendantsByChangeId.get(dependencyChangeId) ?? - [] - ) { - if (seenDescendants.has(descendantChangeId)) continue; - seenDescendants.add(descendantChangeId); - - const matchedDependency = matchedDependencyByDescendant.get( - descendantChangeId, - ); - if ( - matchedDependency == null || - tryMatchDescendant( - matchedDependency, - descendantsByChangeId, - matchedDependencyByDescendant, - seenDescendants, - ) - ) { - matchedDependencyByDescendant.set(descendantChangeId, dependencyChangeId); - return true; - } - } - - return false; -} - -function progressAttachToScreen( - root: progress.Root, - { writeOutput, startWidget }: Pick< - log.WidgetHost, - "writeOutput" | "startWidget" - >, -): ts.Dispose { - const stack = new DisposableStack(); - let widget: log.WidgetInstance | null = null; - - stack.use(root.on("change", (items) => { - if (items.length > 0) { - widget ??= startWidget({ - format: ({ now }) => progress.formatAnsi(now, root.active), - }) ?? null; - if (!widget) return; - widget.fps = items.some((x) => x.showTotal !== false && x.total > 0) - ? 15 - : null; - widget.redraw(); - } else { - widget?.stop(); - widget = null; - } - })); - stack.use(root.on("node-detached-log", (msg) => { - writeOutput(log.formatAnsiMessage(msg, true)); - })); - stack.use(root.on("node-end", (node) => { - let title = node.text; - let p: progress.ReadOnlyNode | null = node; - while (p = p.parent) title = p.text + " / " + title; - })); - - return ts.defer(() => stack[Symbol.dispose]); -} diff --git a/users/clover/tsconfig.json b/users/clover/tsconfig.json deleted file mode 100644 index d8d6194032ada00c79430cf01726df5d860367a6..0000000000000000000000000000000000000000 --- a/users/clover/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "allowImportingTsExtensions": true, - "baseUrl": ".", - "lib": ["esnext", "es2024", "dom", "dom.iterable"], - "module": "nodenext", - "noEmit": true, - "rootDir": ".", - "skipLibCheck": true, - "strict": true, - "verbatimModuleSyntax": true, - "target": "esnext", - "noUncheckedIndexedAccess": true, - "resolveJsonModule": true, - "types": ["node"] - }, - "include": ["./**/*"] -} diff --git a/users/clover/work/configuration.nix b/users/clover/work/configuration.nix index fbc8686332f602e6a0cf2a62bdba3ff89b482b04..1098e9c0f2acdaf137b8d03ed3c0ad75d70c67c1 100644 --- a/users/clover/work/configuration.nix +++ b/users/clover/work/configuration.nix @@ -6,6 +6,13 @@ nix.optimise.automatic = lib.mkForce false; nix.gc.automatic = lib.mkForce false; + homebrew.casks = [ + "granola" + "pgadmin4" + "visual-studio-code" + "wispr-flow" + ]; + services.skhd = { enable = true; skhdConfig = '' diff --git a/users/clover/work/home.nix b/users/clover/work/home.nix index 1904955e55e9f3f1185365294ccbeb985fb98fa8..b45324655a3330406b58f8d2abd5085778129dd2 100644 --- a/users/clover/work/home.nix +++ b/users/clover/work/home.nix @@ -19,7 +19,6 @@ awscli2 deno doppler - nodejs_26 rustup ssm-session-manager-plugin terraform diff --git a/users/clover/zsh/default.nix b/users/clover/zsh/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..a128c3e820e252a6614cb2ed304a9f6cea626254 --- /dev/null +++ b/users/clover/zsh/default.nix @@ -0,0 +1,19 @@ +{ config, ... }: +{ + programs.zsh = { + enable = true; + dotDir = "${config.xdg.configHome}/zsh"; + shellAliases = { + switch = "~/config/switch"; + + jf = "jj git fetch"; + e = "jj edit"; + d = "jj desc"; + moveself = "jj rebase -s @ -d"; + logall = "jj log -r 'all()'"; + um = "jj log -r 'unmerged()'"; + cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit"; + }; + initContent = builtins.readFile ./init.zsh; + }; +} diff --git a/users/clover/zsh/init.zsh b/users/clover/zsh/init.zsh new file mode 100644 index 0000000000000000000000000000000000000000..a7d441ad23733e8bb82bd0e29826da201fc7aa78 --- /dev/null +++ b/users/clover/zsh/init.zsh @@ -0,0 +1,240 @@ +# macOS hands GUI-launched processes a 256 fd soft limit, which bundler +# file watchers exhaust during their first build. +ulimit -n 65536 2>/dev/null + +function _update_prompt_theme() { + if [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]]; then + PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " + else + PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ " + fi +} +precmd_functions+=(_update_prompt_theme) +_update_prompt_theme + +function python() { + dirname=$(dirname $1 2>/dev/null) + if [ -z "$dirname" ]; then + dirname=$(pwd) + fi + uv run --project "$dirname" "$@" +} + +function bun() { + if [[ "$1" = "build" ]]; then + command bun run "$@" + elif [[ "$1" = "bundle" ]]; then + shift + command bun build "$@" + else + command bun "$@" + fi +} + +function pr() { + local branch remote repo url + branch="$( + command jj log -r '@ | @-' -T 'bookmarks.map(|b| b.name()).join("\n") ++ "\n"' --no-graph 2>/dev/null | + while IFS= read -r bookmark; do + case "$bookmark" in + "" | main | master | trunk | deployment*) ;; + *) printf '%s' "$bookmark"; break ;; + esac + done + )" + if [ -z "$branch" ]; then + printf 'no jj bookmark found on @ or @-\n' >&2 + return 1 + fi + + remote="$( + command jj git remote list 2>/dev/null | + while IFS=' ' read -r name remote_url _; do + if [ "$name" = "origin" ]; then + printf '%s' "$remote_url" + break + fi + done + )" + case "$remote" in + git@github.com:*) repo="${remote#git@github.com:}" ;; + ssh://git@github.com/*) repo="${remote#ssh://git@github.com/}" ;; + https://github.com/*) repo="${remote#https://github.com/}" ;; + http://github.com/*) repo="${remote#http://github.com/}" ;; + *) + printf 'origin is not a github remote: %s\n' "$remote" >&2 + return 1 + ;; + esac + + repo="${repo%.git}" + url="https://github.com/$repo/pull/new/$branch?labels=auto-merge" + command open "$url" +} + +function _jjImmutableRevision() { + emulate -L zsh + setopt extendedglob + local line + while IFS= read -r line; do + line="${line//$'\e'\[[0-9;]##m/}" + if [[ "$line" = *"Could not modify commit: "* ]]; then + line="${line#*Could not modify commit: }" + printf '%s' "${line%% *}" + return 0 + fi + done < "$1" + return 1 +} + +function _runJjStreaming() { + emulate -L zsh + setopt no_multios + local stderr arg has_color + local -a jj_color_arg pipe_status + stderr="$1" + shift + if [[ -t 1 && -t 2 ]]; then + has_color=false + for arg in "$@"; do + if [[ "$arg" = "--color" || "$arg" = --color=* ]]; then + has_color=true + break + fi + done + if ! $has_color; then + jj_color_arg=(--color=always) + fi + fi + { + command jj "$jj_color_arg[@]" "$@" 2>&1 1>&3 | tee "$stderr" >&2 + pipe_status=("${pipestatus[@]}") + } 3>&1 + return $pipe_status[1] +} + +function _jjHandleFailure() { + local stderr jj_status reply + stderr="$1" + jj_status="$2" + shift 2 + if [[ $jj_status -ne 0 ]]; then + if _jjImmutableRevision "$stderr" >/dev/null && [[ -t 0 ]]; then + printf 'Rewrite immutable commit anyways [y/N] ' >&2 + read -r reply + case "$reply" in + [Yy] | [Yy][Ee][Ss]) + _runJj "$@" --ignore-immutable + return $? + ;; + esac + return $jj_status + fi + fi + return $jj_status +} + +function _runJj() { + local stderr jj_status + stderr="$(mktemp)" + _runJjStreaming "$stderr" "$@" + jj_status=$? + _jjHandleFailure "$stderr" "$jj_status" "$@" + jj_status=$? + rm -f "$stderr" + return $jj_status +} + +function wrapWithAutoInstall() { + local root file cmd before after wrapped_status + root="$(command jj workspace root)" + if [ -n "$root" ]; then + if [ -e "$root/bun.lock" ]; then + file="$root/bun.lock" + cmd="bun i -f" + elif [ -e "$root/package-lock.json" ]; then + file="$root/package-lock.json" + cmd="npm i" + elif [ -e "$root/pnpm-lock.yaml" ]; then + file="$root/pnpm-lock.yaml" + cmd="pnpm i" + elif [ -e "$root/Cargo.lock" ]; then + file="$root/Cargo.lock" + cmd="cargo fetch" + fi + fi + if [ -n "$file" ]; then + before="$(sha1sum "$file")" + fi + _runJj "$@" + wrapped_status=$? + if [[ $wrapped_status -ne 0 ]]; then + return $wrapped_status + fi + if [ -n "$file" ] && [ -e "$file" ]; then + after="$(sha1sum "$file")" + if [ "$after" != "$before" ]; then + eval "$cmd" + return $? + fi + fi + return $wrapped_status +} + +function jj() { + if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then + 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 + wrapWithAutoInstall new "$2@origin" + else + wrapWithAutoInstall "$@" + fi + elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then + # confirm before pushing, but only when the dry-run has something to push + local dry dry_status + dry="$(command jj "$@" --dry-run 2>&1)" + dry_status=$? + print -r -- "$dry" + if [[ $dry_status -ne 0 ]]; then + return $dry_status + fi + 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 + local answer + printf "No bookmark on @; move previous bookmark to @ and push? [y/N] " + read -r answer + case "$answer" in + y|Y|yes|YES) + bup || return $? + dry="$(command jj "$@" --dry-run 2>&1)" + dry_status=$? + print -r -- "$dry" + if [[ $dry_status -ne 0 ]]; then + return $dry_status + fi + if [[ "$dry" != *"Nothing changed."* ]]; then + _runJj "$@" + return $? + fi + ;; + esac + fi + if [[ "$dry" != *"Nothing changed."* ]]; then + printf "Press any key to continue... " + read -r + _runJj "$@" + return $? + fi + return 0 + elif [[ "$1" = "undo" || "$1" = "redo" ]]; then + _runJj "$@" + else + # auto-install deps when switching commits + wrapWithAutoInstall "$@" + fi +} + +function bup() { + if [[ -z "$(command jj log -r @ -T description --no-graph 2>/dev/null)" ]]; then + command jj desc -m 'chore: follow-up changes' + fi + command jj b m --from 'heads(::@- & bookmarks()) ~ (trunk() | bookmarks(glob:"deployment*"))' --to '@' +}