|
| 1 | +import { triggerBackgroundRefresh } from "@array/core/background-refresh"; |
| 2 | +import { type ArrContext, initContext } from "@array/core/engine"; |
| 3 | +import { dumpRefs } from "./commands/hidden/dump-refs"; |
| 4 | +import { refreshPRInfo } from "./commands/hidden/refresh-pr-info"; |
| 5 | +import { |
| 6 | + CATEGORY_LABELS, |
| 7 | + CATEGORY_ORDER, |
| 8 | + COMMANDS as COMMAND_INFO, |
| 9 | + type CommandInfo, |
| 10 | + getCommandsByCategory, |
| 11 | + getCoreCommands, |
| 12 | + getRequiredContext, |
| 13 | + HANDLERS, |
| 14 | + resolveCommandAlias, |
| 15 | +} from "./registry"; |
| 16 | +import { parseArgs } from "./utils/args"; |
| 17 | +import { |
| 18 | + checkContext, |
| 19 | + isContextValid, |
| 20 | + printContextError, |
| 21 | +} from "./utils/context"; |
| 22 | +import { |
| 23 | + arr, |
| 24 | + bold, |
| 25 | + cyan, |
| 26 | + dim, |
| 27 | + formatError, |
| 28 | + hint, |
| 29 | + message, |
| 30 | +} from "./utils/output"; |
| 31 | + |
| 32 | +const CLI_NAME = "arr"; |
| 33 | +const CLI_VERSION = "0.0.1"; |
| 34 | +const CMD_WIDTH = 22; |
| 35 | + |
| 36 | +const TAGLINE = `arr is a CLI for stacked PRs using jj. |
| 37 | +It enables stacking changes on top of each other to keep you unblocked |
| 38 | +and your changes small, focused, and reviewable.`; |
| 39 | + |
| 40 | +const USAGE = `${bold("USAGE")} |
| 41 | + $ arr <command> [flags]`; |
| 42 | + |
| 43 | +const TERMS = `${bold("TERMS")} |
| 44 | + stack: A sequence of changes, each building off of its parent. |
| 45 | + ex: main <- "add API" <- "update frontend" <- "docs" |
| 46 | + trunk: The branch that stacks are merged into (e.g., main). |
| 47 | + change: A jj commit/revision. Unlike git, jj tracks the working |
| 48 | + copy as a change automatically.`; |
| 49 | + |
| 50 | +const GLOBAL_OPTIONS = `${bold("GLOBAL OPTIONS")} |
| 51 | + --help Show help for a command. |
| 52 | + --help --all Show full command reference. |
| 53 | + --version Show arr version number.`; |
| 54 | + |
| 55 | +const DOCS = `${bold("DOCS")} |
| 56 | + Get started: https://github.com/posthog/array`; |
| 57 | + |
| 58 | +function formatCommand( |
| 59 | + c: CommandInfo, |
| 60 | + showAliases = true, |
| 61 | + showFlags = false, |
| 62 | +): string { |
| 63 | + const full = c.args ? `${c.name} ${c.args}` : c.name; |
| 64 | + const aliasStr = |
| 65 | + showAliases && c.aliases?.length |
| 66 | + ? ` ${dim(`[aliases: ${c.aliases.join(", ")}]`)}` |
| 67 | + : ""; |
| 68 | + let result = ` ${cyan(full.padEnd(CMD_WIDTH))}${c.description}.${aliasStr}`; |
| 69 | + |
| 70 | + if (showFlags && c.flags?.length) { |
| 71 | + for (const flag of c.flags) { |
| 72 | + const flagName = flag.short |
| 73 | + ? `-${flag.short}, --${flag.name}` |
| 74 | + : `--${flag.name}`; |
| 75 | + result += `\n ${dim(flagName.padEnd(CMD_WIDTH - 2))}${dim(flag.description)}`; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + return result; |
| 80 | +} |
| 81 | + |
| 82 | +function printHelp(): void { |
| 83 | + const coreCommands = getCoreCommands(); |
| 84 | + |
| 85 | + console.log(`${TAGLINE} |
| 86 | +
|
| 87 | +${USAGE} |
| 88 | +
|
| 89 | +${TERMS} |
| 90 | +
|
| 91 | +${bold("CORE COMMANDS")} |
| 92 | +${coreCommands.map((c) => formatCommand(c, false)).join("\n")} |
| 93 | +
|
| 94 | + Run ${arr(COMMAND_INFO.help, "--all")} for a full command reference. |
| 95 | +
|
| 96 | +${bold("CORE WORKFLOW")} |
| 97 | + 1. ${dim("(make edits)")}\t\t\tno need to stage, jj tracks automatically |
| 98 | + 2. ${arr(COMMAND_INFO.create, '"add user model"')}\tSave as a change |
| 99 | + 3. ${dim("(make more edits)")}\t\t\tStack more work |
| 100 | + 4. ${arr(COMMAND_INFO.create, '"add user api"')}\t\tSave as another change |
| 101 | + 5. ${arr(COMMAND_INFO.submit)}\t\t\t\tCreate PRs for the stack |
| 102 | + 6. ${arr(COMMAND_INFO.merge)}\t\t\t\tMerge PRs from the CLI |
| 103 | + 7. ${arr(COMMAND_INFO.sync)}\t\t\t\tFetch & rebase after reviews |
| 104 | +
|
| 105 | +${bold("ESCAPE HATCH")} |
| 106 | + ${arr(COMMAND_INFO.exit)}\t\t\t\tSwitch back to plain git if you need it. |
| 107 | + \t\t\t\t\tYour jj changes are preserved and you can return anytime. |
| 108 | +
|
| 109 | +${bold("LEARN MORE")} |
| 110 | + Documentation\t\t\thttps://github.com/posthog/array |
| 111 | + jj documentation\t\thttps://www.jj-vcs.dev/latest/ |
| 112 | +`); |
| 113 | +} |
| 114 | + |
| 115 | +function printHelpAll(): void { |
| 116 | + const hidden = new Set(["help", "version", "config"]); |
| 117 | + const sections = CATEGORY_ORDER.map((category) => { |
| 118 | + const commands = getCommandsByCategory(category).filter( |
| 119 | + (c) => !hidden.has(c.name), |
| 120 | + ); |
| 121 | + if (commands.length === 0) return ""; |
| 122 | + return `${bold(CATEGORY_LABELS[category])}\n${commands.map((c) => formatCommand(c, true, true)).join("\n")}`; |
| 123 | + }).filter(Boolean); |
| 124 | + |
| 125 | + console.log(`${TAGLINE} |
| 126 | +
|
| 127 | +${USAGE} |
| 128 | +
|
| 129 | +${TERMS} |
| 130 | +
|
| 131 | +${sections.join("\n\n")} |
| 132 | +
|
| 133 | +${GLOBAL_OPTIONS} |
| 134 | +
|
| 135 | +${DOCS} |
| 136 | +`); |
| 137 | +} |
| 138 | + |
| 139 | +function printVersion(): void { |
| 140 | + console.log(`${CLI_NAME} ${CLI_VERSION}`); |
| 141 | +} |
| 142 | + |
| 143 | +export async function main(): Promise<void> { |
| 144 | + const parsed = parseArgs(Bun.argv); |
| 145 | + const command = resolveCommandAlias(parsed.name); |
| 146 | + |
| 147 | + if (parsed.name && parsed.name !== command) { |
| 148 | + message(dim(`(${parsed.name} → ${command})`)); |
| 149 | + } |
| 150 | + |
| 151 | + if (parsed.flags.help || parsed.flags.h) { |
| 152 | + if (parsed.flags.all) { |
| 153 | + printHelpAll(); |
| 154 | + } else { |
| 155 | + printHelp(); |
| 156 | + } |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + if (parsed.flags.version || parsed.flags.v) { |
| 161 | + printVersion(); |
| 162 | + return; |
| 163 | + } |
| 164 | + |
| 165 | + // No command provided - show help |
| 166 | + if (command === "__guided") { |
| 167 | + printHelp(); |
| 168 | + return; |
| 169 | + } |
| 170 | + |
| 171 | + // Built-in commands |
| 172 | + if (command === "help") { |
| 173 | + parsed.flags.all ? printHelpAll() : printHelp(); |
| 174 | + return; |
| 175 | + } |
| 176 | + if (command === "version") { |
| 177 | + printVersion(); |
| 178 | + return; |
| 179 | + } |
| 180 | + |
| 181 | + // Hidden commands |
| 182 | + if (command === "__refresh-pr-info") { |
| 183 | + await refreshPRInfo(); |
| 184 | + return; |
| 185 | + } |
| 186 | + if (command === "__dump-refs") { |
| 187 | + await dumpRefs(); |
| 188 | + return; |
| 189 | + } |
| 190 | + |
| 191 | + const handler = HANDLERS[command]; |
| 192 | + if (handler) { |
| 193 | + const requiredLevel = getRequiredContext(command); |
| 194 | + |
| 195 | + // Commands that don't need context (auth, help, etc.) |
| 196 | + if (requiredLevel === "none") { |
| 197 | + await handler(parsed, null); |
| 198 | + return; |
| 199 | + } |
| 200 | + |
| 201 | + // Check prerequisites (git, jj, arr initialized) |
| 202 | + const debug = !!parsed.flags.debug; |
| 203 | + let t0 = Date.now(); |
| 204 | + const prereqs = await checkContext(); |
| 205 | + if (debug) console.log(` checkContext: ${Date.now() - t0}ms`); |
| 206 | + if (!isContextValid(prereqs, requiredLevel)) { |
| 207 | + printContextError(prereqs, requiredLevel); |
| 208 | + process.exit(1); |
| 209 | + } |
| 210 | + |
| 211 | + // Initialize context with engine |
| 212 | + let context: ArrContext | null = null; |
| 213 | + try { |
| 214 | + t0 = Date.now(); |
| 215 | + context = await initContext(); |
| 216 | + if (debug) console.log(` initContext: ${Date.now() - t0}ms`); |
| 217 | + |
| 218 | + // Trigger background PR refresh (rate-limited) |
| 219 | + triggerBackgroundRefresh(context.cwd); |
| 220 | + |
| 221 | + t0 = Date.now(); |
| 222 | + await handler(parsed, context); |
| 223 | + if (debug) console.log(` handler: ${Date.now() - t0}ms`); |
| 224 | + } finally { |
| 225 | + // Auto-persist engine changes |
| 226 | + context?.engine.persist(); |
| 227 | + } |
| 228 | + return; |
| 229 | + } |
| 230 | + |
| 231 | + console.error(formatError(`Unknown command: ${command}`)); |
| 232 | + hint(`Run '${arr(COMMAND_INFO.help)}' to see available commands.`); |
| 233 | + process.exit(1); |
| 234 | +} |
0 commit comments