Skip to content

Commit e6b9217

Browse files
authored
feat(cli): workspaces (#487)
1 parent b6e0947 commit e6b9217

32 files changed

+4373
-41
lines changed

apps/cli/src/commands/assign.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
assignFiles,
3+
assignFilesToNewWorkspace,
4+
listUnassigned,
5+
} from "@array/core/commands/assign";
6+
import { cyan, dim, formatSuccess, green, message } from "../utils/output";
7+
import { requireArg, unwrap } from "../utils/run";
8+
9+
export async function assign(args: string[]): Promise<void> {
10+
if (args.length === 0) {
11+
message("Usage: arr assign <file...> <workspace>");
12+
message(" arr assign <file...> --new <workspace-name>");
13+
message("");
14+
message("Examples:");
15+
message(" arr assign config.json agent-a");
16+
message(" arr assign file1.txt file2.txt agent-b");
17+
message(' arr assign "src/**/*.ts" --new refactor');
18+
return;
19+
}
20+
21+
// Check for --new flag
22+
const newIndex = args.indexOf("--new");
23+
const nIndex = args.indexOf("-n");
24+
const newFlagIndex = newIndex !== -1 ? newIndex : nIndex;
25+
26+
if (newFlagIndex !== -1) {
27+
// Everything before --new is files, next arg is workspace name
28+
const files = args.slice(0, newFlagIndex);
29+
const newWorkspaceName = args[newFlagIndex + 1];
30+
31+
requireArg(files[0], "Usage: arr assign <file...> --new <workspace-name>");
32+
requireArg(
33+
newWorkspaceName,
34+
"Usage: arr assign <file...> --new <workspace-name>",
35+
);
36+
37+
const result = unwrap(
38+
await assignFilesToNewWorkspace(files, newWorkspaceName),
39+
);
40+
41+
if (result.files.length === 1) {
42+
message(
43+
formatSuccess(
44+
`Assigned ${cyan(result.files[0])} to new workspace ${green(result.to)}`,
45+
),
46+
);
47+
} else {
48+
message(
49+
formatSuccess(
50+
`Assigned ${result.files.length} files to new workspace ${green(result.to)}`,
51+
),
52+
);
53+
for (const file of result.files) {
54+
message(` ${cyan(file)}`);
55+
}
56+
}
57+
return;
58+
}
59+
60+
// Regular assign to existing workspace
61+
// Last arg is workspace, everything else is files
62+
if (args.length < 2) {
63+
message("Usage: arr assign <file...> <workspace>");
64+
return;
65+
}
66+
67+
const files = args.slice(0, -1);
68+
const targetWorkspace = args[args.length - 1];
69+
70+
requireArg(files[0], "Usage: arr assign <file...> <workspace>");
71+
requireArg(targetWorkspace, "Usage: arr assign <file...> <workspace>");
72+
73+
const result = unwrap(await assignFiles(files, targetWorkspace));
74+
75+
if (result.files.length === 1) {
76+
message(
77+
formatSuccess(`Assigned ${cyan(result.files[0])} to ${green(result.to)}`),
78+
);
79+
} else {
80+
message(
81+
formatSuccess(
82+
`Assigned ${result.files.length} files to ${green(result.to)}`,
83+
),
84+
);
85+
for (const file of result.files) {
86+
message(` ${cyan(file)}`);
87+
}
88+
}
89+
}
90+
91+
export async function unassigned(
92+
subcommand: string,
93+
_args: string[],
94+
): Promise<void> {
95+
switch (subcommand) {
96+
case "list":
97+
case "ls": {
98+
const result = unwrap(await listUnassigned());
99+
100+
if (result.files.length === 0) {
101+
message(dim("No unassigned files"));
102+
return;
103+
}
104+
105+
message(
106+
`${result.files.length} unassigned file${result.files.length === 1 ? "" : "s"}:`,
107+
);
108+
message("");
109+
110+
for (const file of result.files) {
111+
message(` ${cyan(file)}`);
112+
}
113+
114+
message("");
115+
message(`Assign files: ${dim("arr assign <file...> <workspace>")}`);
116+
break;
117+
}
118+
119+
default:
120+
message("Usage: arr unassigned <list>");
121+
message("");
122+
message("Subcommands:");
123+
message(" list List files in unassigned workspace");
124+
}
125+
}

apps/cli/src/commands/daemon.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
daemonRestart,
3+
daemonStart,
4+
daemonStatus,
5+
daemonStop,
6+
} from "@array/core/commands/daemon";
7+
import { cyan, dim, formatSuccess, green, message, red } from "../utils/output";
8+
import { unwrap } from "../utils/run";
9+
10+
export async function daemon(subcommand: string): Promise<void> {
11+
switch (subcommand) {
12+
case "start": {
13+
unwrap(await daemonStart());
14+
message(formatSuccess("Daemon started"));
15+
message(dim(" Watching workspaces for file changes"));
16+
message(dim(" Stop with: arr daemon stop"));
17+
break;
18+
}
19+
20+
case "stop": {
21+
unwrap(await daemonStop());
22+
message(formatSuccess("Daemon stopped"));
23+
break;
24+
}
25+
26+
case "restart": {
27+
unwrap(await daemonRestart());
28+
message(formatSuccess("Daemon restarted"));
29+
break;
30+
}
31+
32+
case "status": {
33+
const status = unwrap(await daemonStatus());
34+
if (status.running) {
35+
message(
36+
`${green("●")} Daemon is ${green("running")} (PID: ${status.pid})`,
37+
);
38+
if (status.repos.length > 0) {
39+
message("");
40+
message("Watching repos:");
41+
for (const repo of status.repos) {
42+
message(` ${dim(repo.path)}`);
43+
for (const ws of repo.workspaces) {
44+
message(` └─ ${ws}`);
45+
}
46+
}
47+
} else {
48+
message("");
49+
message(
50+
dim("No repos registered. Use arr preview to register workspaces."),
51+
);
52+
}
53+
message("");
54+
message(`Logs: ${dim(status.logPath)}`);
55+
} else {
56+
message(`${red("○")} Daemon is ${dim("not running")}`);
57+
message("");
58+
message(`Start with: ${cyan("arr daemon start")}`);
59+
}
60+
break;
61+
}
62+
63+
default:
64+
message("Usage: arr daemon <start|stop|restart|status>");
65+
message("");
66+
message("Subcommands:");
67+
message(" start Start the workspace sync daemon");
68+
message(" stop Stop the daemon");
69+
message(" restart Restart the daemon");
70+
message(" status Check if daemon is running");
71+
}
72+
}

apps/cli/src/commands/enter.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { enter } from "@array/core/commands/enter";
2+
import type { CommandMeta } from "@array/core/commands/types";
3+
import { unwrap } from "@array/core/result";
4+
import { blank, dim, green, hint, message } from "../utils/output";
5+
6+
export const meta: CommandMeta = {
7+
name: "enter",
8+
description: "Enter jj mode from git",
9+
context: "none",
10+
category: "management",
11+
};
12+
13+
export async function run(): Promise<void> {
14+
const result = unwrap(await enter(process.cwd()));
15+
16+
message(`${green(">")} jj ready`);
17+
if (result.bookmark) {
18+
message(dim(`On branch: ${result.bookmark}`));
19+
}
20+
message(dim(`Working copy: ${result.workingCopyChangeId}`));
21+
blank();
22+
hint("Run `arr exit` to switch git to a branch");
23+
}

apps/cli/src/commands/exit.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,55 @@
1+
import { exit } from "@array/core/commands/exit";
2+
import { focusNone, focusStatus } from "@array/core/commands/focus";
13
import type { CommandMeta } from "@array/core/commands/types";
2-
import { exitToGit } from "@array/core/git/branch";
3-
import { getTrunk } from "@array/core/jj";
4-
import { unwrap as coreUnwrap } from "@array/core/result";
5-
import { COMMANDS } from "../registry";
6-
import { arr, blank, cyan, green, hint, message } from "../utils/output";
4+
import { unwrap } from "@array/core/result";
5+
import {
6+
blank,
7+
cyan,
8+
dim,
9+
formatSuccess,
10+
green,
11+
hint,
12+
message,
13+
warning,
14+
} from "../utils/output";
715

816
export const meta: CommandMeta = {
917
name: "exit",
10-
description: "Exit to plain git on trunk (escape hatch if you need git)",
18+
description: "Exit focus mode, or exit to plain git if not in focus",
1119
context: "jj",
1220
category: "management",
1321
};
1422

15-
export async function exit(): Promise<void> {
16-
const trunk = await getTrunk();
17-
const result = coreUnwrap(await exitToGit(process.cwd(), trunk));
23+
export async function run(): Promise<void> {
24+
// Check if we're in focus mode - exit that first
25+
const status = await focusStatus();
26+
if (status.ok && status.value.isFocused) {
27+
unwrap(await focusNone());
28+
message(formatSuccess("Exited focus mode"));
29+
}
30+
31+
// Exit to git
32+
const result = unwrap(await exit(process.cwd()));
33+
34+
if (result.alreadyInGitMode) {
35+
message(dim(`Already on git branch '${result.branch}'`));
36+
return;
37+
}
38+
39+
message(`${green(">")} Switched to git branch ${cyan(result.branch)}`);
40+
41+
if (result.syncedFiles > 0) {
42+
message(
43+
dim(`Synced ${result.syncedFiles} file(s) from unassigned workspace`),
44+
);
45+
}
46+
47+
if (result.usedFallback) {
48+
blank();
49+
warning("No bookmark found in ancestors, switched to trunk.");
50+
}
1851

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

0 commit comments

Comments
 (0)