Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/cli/src/commands/assign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
assignFiles,
assignFilesToNewWorkspace,
listUnassigned,
} from "@array/core/commands/assign";
import { cyan, dim, formatSuccess, green, message } from "../utils/output";
import { requireArg, unwrap } from "../utils/run";

export async function assign(args: string[]): Promise<void> {
if (args.length === 0) {
message("Usage: arr assign <file...> <workspace>");
message(" arr assign <file...> --new <workspace-name>");
message("");
message("Examples:");
message(" arr assign config.json agent-a");
message(" arr assign file1.txt file2.txt agent-b");
message(' arr assign "src/**/*.ts" --new refactor');
return;
}

// Check for --new flag
const newIndex = args.indexOf("--new");
const nIndex = args.indexOf("-n");
const newFlagIndex = newIndex !== -1 ? newIndex : nIndex;

if (newFlagIndex !== -1) {
// Everything before --new is files, next arg is workspace name
const files = args.slice(0, newFlagIndex);
const newWorkspaceName = args[newFlagIndex + 1];

requireArg(files[0], "Usage: arr assign <file...> --new <workspace-name>");
requireArg(
newWorkspaceName,
"Usage: arr assign <file...> --new <workspace-name>",
);

const result = unwrap(
await assignFilesToNewWorkspace(files, newWorkspaceName),
);

if (result.files.length === 1) {
message(
formatSuccess(
`Assigned ${cyan(result.files[0])} to new workspace ${green(result.to)}`,
),
);
} else {
message(
formatSuccess(
`Assigned ${result.files.length} files to new workspace ${green(result.to)}`,
),
);
for (const file of result.files) {
message(` ${cyan(file)}`);
}
}
return;
}

// Regular assign to existing workspace
// Last arg is workspace, everything else is files
if (args.length < 2) {
message("Usage: arr assign <file...> <workspace>");
return;
}

const files = args.slice(0, -1);
const targetWorkspace = args[args.length - 1];

requireArg(files[0], "Usage: arr assign <file...> <workspace>");
requireArg(targetWorkspace, "Usage: arr assign <file...> <workspace>");

const result = unwrap(await assignFiles(files, targetWorkspace));

if (result.files.length === 1) {
message(
formatSuccess(`Assigned ${cyan(result.files[0])} to ${green(result.to)}`),
);
} else {
message(
formatSuccess(
`Assigned ${result.files.length} files to ${green(result.to)}`,
),
);
for (const file of result.files) {
message(` ${cyan(file)}`);
}
}
}

export async function unassigned(
subcommand: string,
_args: string[],
): Promise<void> {
switch (subcommand) {
case "list":
case "ls": {
const result = unwrap(await listUnassigned());

if (result.files.length === 0) {
message(dim("No unassigned files"));
return;
}

message(
`${result.files.length} unassigned file${result.files.length === 1 ? "" : "s"}:`,
);
message("");

for (const file of result.files) {
message(` ${cyan(file)}`);
}

message("");
message(`Assign files: ${dim("arr assign <file...> <workspace>")}`);
break;
}

default:
message("Usage: arr unassigned <list>");
message("");
message("Subcommands:");
message(" list List files in unassigned workspace");
}
}
72 changes: 72 additions & 0 deletions apps/cli/src/commands/daemon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
daemonRestart,
daemonStart,
daemonStatus,
daemonStop,
} from "@array/core/commands/daemon";
import { cyan, dim, formatSuccess, green, message, red } from "../utils/output";
import { unwrap } from "../utils/run";

export async function daemon(subcommand: string): Promise<void> {
switch (subcommand) {
case "start": {
unwrap(await daemonStart());
message(formatSuccess("Daemon started"));
message(dim(" Watching workspaces for file changes"));
message(dim(" Stop with: arr daemon stop"));
break;
}

case "stop": {
unwrap(await daemonStop());
message(formatSuccess("Daemon stopped"));
break;
}

case "restart": {
unwrap(await daemonRestart());
message(formatSuccess("Daemon restarted"));
break;
}

case "status": {
const status = unwrap(await daemonStatus());
if (status.running) {
message(
`${green("●")} Daemon is ${green("running")} (PID: ${status.pid})`,
);
if (status.repos.length > 0) {
message("");
message("Watching repos:");
for (const repo of status.repos) {
message(` ${dim(repo.path)}`);
for (const ws of repo.workspaces) {
message(` └─ ${ws}`);
}
}
} else {
message("");
message(
dim("No repos registered. Use arr preview to register workspaces."),
);
}
message("");
message(`Logs: ${dim(status.logPath)}`);
} else {
message(`${red("○")} Daemon is ${dim("not running")}`);
message("");
message(`Start with: ${cyan("arr daemon start")}`);
}
break;
}

default:
message("Usage: arr daemon <start|stop|restart|status>");
message("");
message("Subcommands:");
message(" start Start the workspace sync daemon");
message(" stop Stop the daemon");
message(" restart Restart the daemon");
message(" status Check if daemon is running");
}
}
23 changes: 23 additions & 0 deletions apps/cli/src/commands/enter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { enter } from "@array/core/commands/enter";
import type { CommandMeta } from "@array/core/commands/types";
import { unwrap } from "@array/core/result";
import { blank, dim, green, hint, message } from "../utils/output";

export const meta: CommandMeta = {
name: "enter",
description: "Enter jj mode from git",
context: "none",
category: "management",
};

export async function run(): Promise<void> {
const result = unwrap(await enter(process.cwd()));

message(`${green(">")} jj ready`);
if (result.bookmark) {
message(dim(`On branch: ${result.bookmark}`));
}
message(dim(`Working copy: ${result.workingCopyChangeId}`));
blank();
hint("Run `arr exit` to switch git to a branch");
}
54 changes: 43 additions & 11 deletions apps/cli/src/commands/exit.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
import { exit } from "@array/core/commands/exit";
import { focusNone, focusStatus } from "@array/core/commands/focus";
import type { CommandMeta } from "@array/core/commands/types";
import { exitToGit } from "@array/core/git/branch";
import { getTrunk } from "@array/core/jj";
import { unwrap as coreUnwrap } from "@array/core/result";
import { COMMANDS } from "../registry";
import { arr, blank, cyan, green, hint, message } from "../utils/output";
import { unwrap } from "@array/core/result";
import {
blank,
cyan,
dim,
formatSuccess,
green,
hint,
message,
warning,
} from "../utils/output";

export const meta: CommandMeta = {
name: "exit",
description: "Exit to plain git on trunk (escape hatch if you need git)",
description: "Exit focus mode, or exit to plain git if not in focus",
context: "jj",
category: "management",
};

export async function exit(): Promise<void> {
const trunk = await getTrunk();
const result = coreUnwrap(await exitToGit(process.cwd(), trunk));
export async function run(): Promise<void> {
// Check if we're in focus mode - exit that first
const status = await focusStatus();
if (status.ok && status.value.isFocused) {
unwrap(await focusNone());
message(formatSuccess("Exited focus mode"));
}

// Exit to git
const result = unwrap(await exit(process.cwd()));

if (result.alreadyInGitMode) {
message(dim(`Already on git branch '${result.branch}'`));
return;
}

message(`${green(">")} Switched to git branch ${cyan(result.branch)}`);

if (result.syncedFiles > 0) {
message(
dim(`Synced ${result.syncedFiles} file(s) from unassigned workspace`),
);
}

if (result.usedFallback) {
blank();
warning("No bookmark found in ancestors, switched to trunk.");
}

message(`${green(">")} Switched to git branch ${cyan(result.trunk)}`);
blank();
hint("You're now using plain git. Your jj changes are still safe.");
hint(`To return to arr/jj, run: ${arr(COMMANDS.init)}`);
hint("Run `arr enter` to return to jj.");
}
Loading