@4xeoz/re-entry 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,14 +13,37 @@ Requirements: macOS, Node.js 24+, Codex installed and signed in, and an absolute
13
13
  Codex may read and write.
14
14
 
15
15
  ```sh
16
- npx @4xeoz/re-entry install --codex-cd /absolute/path/to/your/project
16
+ npx @4xeoz/re-entry install
17
+ ```
18
+
19
+ This uses the deployed Re-entry Cloud Receiver by default:
20
+ `https://reentry-cloud.vercel.app`.
21
+
22
+ The interactive CLI lets you choose the Codex workspace with ↑/↓ and Enter. If you want to choose
23
+ the workspace explicitly, pass it directly:
24
+
25
+ ```sh
26
+ npx @4xeoz/re-entry install \
27
+ --codex-cd /absolute/path/to/your/project
28
+ ```
29
+
30
+ Run this from the Host project directory, your home directory, or another normal working
31
+ directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
32
+ source directory as the package itself and fail to create the temporary executable link.
33
+
34
+ If npm prints `sh: re-entry: command not found`, the package is present but that npm installation
35
+ has an `npx` temporary-bin `PATH` bug. Use the reliable one-time global installation instead:
36
+
37
+ ```sh
38
+ npm install --global @4xeoz/re-entry@0.2.1
39
+ re-entry install --codex-cd /absolute/path/to/your/project
17
40
  ```
18
41
 
19
42
  The published package uses the hosted Receiver by default. Use `--receiver` only when testing a
20
43
  different Receiver, such as the local preview:
21
44
 
22
45
  ```sh
23
- npx @4xeoz/re-entry install \
46
+ npx --yes --package=@4xeoz/re-entry re-entry install \
24
47
  --receiver http://127.0.0.1:43224 \
25
48
  --codex-cd /absolute/path/to/your/project
26
49
  ```
@@ -29,6 +52,7 @@ npx @4xeoz/re-entry install \
29
52
 
30
53
  ```text
31
54
  check Node + find Codex + validate project directory
55
+ -> choose the Codex workspace with ↑/↓ and Enter when --codex-cd is omitted
32
56
  -> request a device authorization from Re-entry
33
57
  -> show the verification URL and wait for the user to press Enter
34
58
  -> open Re-entry in the default browser
@@ -59,6 +83,18 @@ re-entry claim-once --codex-cd /absolute/path/to/project
59
83
  re-entry start --codex-cd /absolute/path/to/project
60
84
  ```
61
85
 
86
+ To pause or remove the local Connector:
87
+
88
+ ```sh
89
+ re-entry stop
90
+ re-entry uninstall
91
+ ```
92
+
93
+ `stop` pauses the macOS background service and keeps the account connection. `uninstall` requires
94
+ typing `DELETE`, then removes only the LaunchAgent, saved Connector credential, and Connector logs.
95
+ It does not recursively delete folders or uninstall the npm package. To remove a global npm
96
+ installation separately, run `npm uninstall --global @4xeoz/re-entry`.
97
+
62
98
  `connect` repeats only account authorization. `install` is the normal product path because it also
63
99
  installs the background job. `claim-once` is the smallest manual delivery test. The legacy
64
100
  Host-code `pair` command remains only for compatibility tests.
@@ -69,7 +105,7 @@ Copy this prompt into a coding-agent task:
69
105
 
70
106
  ```text
71
107
  Install the Re-entry Local Connector on this Mac. First read the package README. Verify Node.js 24
72
- or newer and locate the installed Codex executable. Run `npx @4xeoz/re-entry install` with an absolute
108
+ or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
73
109
  project directory. Let the human complete the Re-entry
74
110
  browser approval; never copy browser cookies, organization keys, Connector tokens, or private keys
75
111
  into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
@@ -81,7 +117,8 @@ results without claiming Browser/WebMCP or production deployment.
81
117
  - `src/main.mjs` — `re-entry` CLI and long-running poll loop.
82
118
  - `src/pairing-client.mjs` — account-first browser device authorization.
83
119
  - `src/credentials.mjs` — atomic local credential storage.
84
- - `src/macos-service.mjs` — per-user LaunchAgent install and status.
120
+ - `src/macos-service.mjs` — per-user LaunchAgent install, stop, uninstall, and status.
121
+ - `src/workspace-picker.mjs` — interactive Codex workspace selection.
85
122
  - `src/local-connector.mjs` — one claim and activation boundary.
86
123
  - `src/codex-exec-adapter.mjs` — fresh local Codex process adapter.
87
124
  - `src/terminal-ui.mjs` — dependency-free human CLI presentation.
@@ -137,9 +174,13 @@ npm login
137
174
  npm publish --access public
138
175
  ```
139
176
 
140
- After publishing, users run `npx @4xeoz/re-entry install`. The executable inside the package is
177
+ After publishing, users run `npx --yes --package=@4xeoz/re-entry re-entry install`. The explicit
178
+ `--package` form works across npm versions when the package is scoped; the executable itself is
141
179
  still named `re-entry`.
142
180
 
181
+ On npm installations affected by the temporary-bin `PATH` bug, use `npm install --global
182
+ @4xeoz/re-entry` once and then run `re-entry install` directly.
183
+
143
184
  ## Check a Mac before pairing
144
185
 
145
186
  Run the read-only readiness check before connecting the machine:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
@@ -10,9 +10,7 @@
10
10
  "README.md",
11
11
  "src"
12
12
  ],
13
- "bin": {
14
- "re-entry": "./src/main.mjs"
15
- },
13
+ "bin": "./src/main.mjs",
16
14
  "exports": {
17
15
  ".": "./src/index.mjs",
18
16
  "./connector": "./src/local-connector.mjs",
@@ -96,6 +96,58 @@ export async function inspectMacConnectorService(options = {}) {
96
96
  });
97
97
  }
98
98
 
99
+ export async function stopMacConnectorService(options = {}) {
100
+ const configuration = requireServiceConfiguration(options, "stop");
101
+ if (process.platform !== "darwin" && configuration.allowNonMacForTest !== true) {
102
+ return Object.freeze({ supported: false, stopped: false, installed: false, running: false });
103
+ }
104
+ const status = await inspectMacConnectorService(configuration);
105
+ if (!status.installed || !status.running) {
106
+ return Object.freeze({ ...status, stopped: false });
107
+ }
108
+ const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : 0}`;
109
+ const result = await configuration.runCommand(["bootout", `${domain}/${LABEL}`]);
110
+ if (result.code !== 0) {
111
+ throw serviceFailure("connector_service_stop_failed", "Background service could not be stopped");
112
+ }
113
+ return Object.freeze({ ...status, stopped: true, running: false });
114
+ }
115
+
116
+ export async function uninstallMacConnectorService(options = {}) {
117
+ const configuration = requireServiceConfiguration(options, "uninstall");
118
+ if (process.platform !== "darwin" && configuration.allowNonMacForTest !== true) {
119
+ return Object.freeze({ supported: false, removedPaths: [] });
120
+ }
121
+ const service = await stopMacConnectorService(configuration);
122
+ const stateDirectory = configuration.stateDirectory ?? join(homedir(), ".webmcp-connector");
123
+ const credentialFile = requireAbsolutePath(
124
+ configuration.credentialFile ?? join(stateDirectory, "credentials.json"),
125
+ "Credential file",
126
+ );
127
+ const paths = [...new Set([
128
+ service.plistPath,
129
+ credentialFile,
130
+ join(stateDirectory, "connector.log"),
131
+ join(stateDirectory, "connector-error.log"),
132
+ ])];
133
+ const removedPaths = [];
134
+ for (const path of paths) {
135
+ try {
136
+ await unlink(path);
137
+ removedPaths.push(path);
138
+ } catch (error) {
139
+ if (error?.code !== "ENOENT") {
140
+ throw serviceFailure("connector_service_remove_failed", "Connector data could not be removed", error);
141
+ }
142
+ }
143
+ }
144
+ return Object.freeze({
145
+ supported: true,
146
+ removedPaths,
147
+ plistPath: service.plistPath,
148
+ });
149
+ }
150
+
99
151
  export function renderLaunchAgent(options) {
100
152
  const argumentsList = [
101
153
  options.nodeExecutable,
@@ -156,6 +208,18 @@ function runLaunchctl(argumentsList) {
156
208
  });
157
209
  }
158
210
 
211
+ function requireServiceConfiguration(options, operation) {
212
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
213
+ throw serviceFailure("connector_service_input_invalid", `Service ${operation} options are invalid`);
214
+ }
215
+ const launchAgentsDirectory = options.launchAgentsDirectory ?? join(homedir(), "Library", "LaunchAgents");
216
+ const runCommand = options.runCommand ?? runLaunchctl;
217
+ if (typeof runCommand !== "function") {
218
+ throw new TypeError(`Service ${operation} runCommand must be a function`);
219
+ }
220
+ return { ...options, launchAgentsDirectory, runCommand };
221
+ }
222
+
159
223
  function requireAbsolutePath(value, label) {
160
224
  if (
161
225
  typeof value !== "string" ||
package/src/main.mjs CHANGED
@@ -20,9 +20,12 @@ import { LocalConnector } from "./local-connector.mjs";
20
20
  import { LocalConnectorCredentialStore } from "./credentials.mjs";
21
21
  import { LocalConnectorPairingClient } from "./pairing-client.mjs";
22
22
  import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
23
+ import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
23
24
  import {
24
25
  inspectMacConnectorService,
25
26
  installMacConnectorService,
27
+ stopMacConnectorService,
28
+ uninstallMacConnectorService,
26
29
  } from "./macos-service.mjs";
27
30
  import { createTerminalUi } from "./terminal-ui.mjs";
28
31
 
@@ -63,6 +66,14 @@ async function main() {
63
66
  await status(flags, ui);
64
67
  return;
65
68
  }
69
+ if (command === "stop") {
70
+ await stop(flags, ui);
71
+ return;
72
+ }
73
+ if (command === "uninstall") {
74
+ await uninstall(flags, ui);
75
+ return;
76
+ }
66
77
  if (command === "install") {
67
78
  await install(flags, ui);
68
79
  return;
@@ -98,13 +109,15 @@ function printHelp() {
98
109
  Connect this Mac once, then receive approved work in Codex in the background.
99
110
 
100
111
  Usage:
101
- re-entry install --receiver <url> --codex-cd <project> Recommended first run
112
+ re-entry install Recommended first run
102
113
  re-entry status Check the connection
103
- re-entry start --codex-cd <project> Run in this terminal
114
+ re-entry start Run in this terminal
104
115
 
105
116
  Commands:
106
117
  install Check this Mac, connect the Re-entry account, and start at login
107
118
  status Show account, Receiver, service, Node.js, and Codex readiness
119
+ stop Stop the background Connector without removing its credential
120
+ uninstall Stop the Connector and remove its local service data
108
121
  connect Authorize this Mac without installing the background service
109
122
  start Poll for approved work until stopped
110
123
  doctor Check Node.js, Codex, and the optional project directory
@@ -113,9 +126,10 @@ Commands:
113
126
 
114
127
  Common options:
115
128
  --receiver <url> Re-entry Receiver origin
116
- --codex-cd <path> Project opened for the fresh Codex session
129
+ --codex-cd <path> Workspace for Codex; interactive mode offers a folder picker if omitted
117
130
  --codex-binary <path> Explicit Codex executable
118
131
  --json Machine-readable output
132
+ --yes Confirm uninstall in a non-interactive script
119
133
  --help Show this help
120
134
  --version Show the installed version
121
135
 
@@ -160,6 +174,17 @@ function inspectReadiness(flags) {
160
174
  return Object.freeze({ workingDirectory, installation });
161
175
  }
162
176
 
177
+ async function withWorkspaceDirectory(flags, ui) {
178
+ if (flags["codex-cd"] !== undefined || !ui.interactive) {
179
+ return { ...flags, "codex-cd": flags["codex-cd"] ?? process.cwd() };
180
+ }
181
+ ui.info("Workspace", "choose the folder Codex will open");
182
+ const selected = await chooseWorkspaceDirectory({ startDirectory: process.cwd() });
183
+ if (!selected) throw cliFailure("connector_codex_cd_missing");
184
+ ui.success("Workspace", selected);
185
+ return { ...flags, "codex-cd": selected };
186
+ }
187
+
163
188
  function showReadiness(readiness, ui) {
164
189
  if (!ui.interactive) return;
165
190
  ui.success("Node.js", process.versions.node);
@@ -318,14 +343,53 @@ async function status(flags, ui) {
318
343
  }
319
344
  }
320
345
 
346
+ async function stop(flags, ui) {
347
+ if (ui.interactive) {
348
+ ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
349
+ }
350
+ const result = await stopMacConnectorService();
351
+ if (ui.interactive) {
352
+ if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
353
+ else if (result.stopped) ui.success("Stopped", "the background Connector is no longer running");
354
+ else ui.info("Already stopped", "no running background Connector was found");
355
+ } else {
356
+ process.stdout.write(`${JSON.stringify({
357
+ event: "connector_stopped",
358
+ supported: result.supported,
359
+ stopped: result.stopped,
360
+ })}\n`);
361
+ }
362
+ }
363
+
364
+ async function uninstall(flags, ui) {
365
+ if (ui.interactive) {
366
+ ui.begin("Uninstall Re-entry", "Stop the Connector and remove only its local service data");
367
+ ui.warning("This removes", "the LaunchAgent, saved Connector credential, and Connector logs");
368
+ await askForUninstallConfirmation();
369
+ } else if (flags.yes !== true) {
370
+ throw cliFailure("connector_uninstall_confirmation_required");
371
+ }
372
+ const result = await uninstallMacConnectorService({
373
+ credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
374
+ });
375
+ if (ui.interactive) {
376
+ ui.success("Uninstalled", "local Connector service data was removed");
377
+ ui.info("Package", "remove the npm package separately with `npm uninstall --global @4xeoz/re-entry`");
378
+ } else {
379
+ process.stdout.write(`${JSON.stringify({
380
+ event: "connector_uninstalled",
381
+ supported: result.supported,
382
+ removed_paths: result.removedPaths,
383
+ })}\n`);
384
+ }
385
+ }
386
+
321
387
  async function install(flags, ui) {
322
388
  if (ui.interactive) {
323
389
  ui.begin("Install Re-entry", "Check Codex, connect your account, then start at login");
390
+ ui.info("Setup", "choose a workspace, approve this Mac, then leave the Connector running");
324
391
  }
325
- const runtimeFlags = {
326
- ...flags,
327
- "codex-cd": flags["codex-cd"] ?? process.cwd(),
328
- };
392
+ const runtimeFlags = await withWorkspaceDirectory(flags, ui);
329
393
  const readiness = inspectReadiness(runtimeFlags);
330
394
  showReadiness(readiness, ui);
331
395
  const credentials = await connect(runtimeFlags, ui, { quietHeader: true });
@@ -356,10 +420,7 @@ async function start(flags, ui) {
356
420
  if (ui.interactive) {
357
421
  ui.begin("Re-entry is starting", "Connect once, then wait quietly for work you approve");
358
422
  }
359
- const runtimeFlags = {
360
- ...flags,
361
- "codex-cd": flags["codex-cd"] ?? process.cwd(),
362
- };
423
+ const runtimeFlags = await withWorkspaceDirectory(flags, ui);
363
424
  const readiness = inspectReadiness(runtimeFlags);
364
425
  showReadiness(readiness, ui);
365
426
  if (!ui.interactive) {
@@ -572,7 +633,7 @@ function parseArguments(argumentsList) {
572
633
  const value = rest[index];
573
634
  if (!value.startsWith("--")) throw cliFailure("connector_argument_invalid");
574
635
  const name = value.slice(2);
575
- if (name === "json") {
636
+ if (name === "json" || name === "yes") {
576
637
  if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
577
638
  flags[name] = true;
578
639
  continue;
@@ -591,6 +652,8 @@ function validateCommandFlags(command, flags) {
591
652
  pair: new Set(["receiver", "code", "credential-file", "json"]),
592
653
  connect: new Set(["receiver", "device-name", "credential-file", "json"]),
593
654
  status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
655
+ stop: new Set(["json"]),
656
+ uninstall: new Set(["credential-file", "yes", "json"]),
594
657
  install: new Set([
595
658
  "receiver",
596
659
  "device-name",
@@ -708,6 +771,17 @@ async function askForPairingCode(ui) {
708
771
  }
709
772
  }
710
773
 
774
+ async function askForUninstallConfirmation() {
775
+ if (process.stdin.isTTY !== true) throw cliFailure("connector_uninstall_confirmation_required");
776
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
777
+ try {
778
+ const answer = await readline.question(" Type DELETE to remove local Connector data: ");
779
+ if (answer.trim() !== "DELETE") throw cliFailure("connector_uninstall_confirmation_required");
780
+ } finally {
781
+ readline.close();
782
+ }
783
+ }
784
+
711
785
  function errorHint(error) {
712
786
  const hints = {
713
787
  connector_codex_cd_missing: "pass --codex-cd /absolute/path/to/your/Host-project",
@@ -726,6 +800,8 @@ function errorHint(error) {
726
800
  connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
727
801
  connector_max_errors_invalid: "use a maximum error count between 1 and 20",
728
802
  connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
803
+ connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
804
+ connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
729
805
  };
730
806
  return hints[error?.code] ?? "check the Receiver address and try again";
731
807
  }
@@ -8,6 +8,13 @@ const YELLOW = "\u001b[33m";
8
8
  const RED = "\u001b[31m";
9
9
  const CYAN = "\u001b[36m";
10
10
  const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
11
+ const REENTRY_ASCII = [
12
+ " ____ _____ _____ _ _ _____ ____ __ __",
13
+ " | _ \\| ____| | ____| \\ | | ____| _ \\ \\ \\ / /",
14
+ " | |_) | _| _____ | _| | \\| | _| | |_) | \\ V /",
15
+ " | _ <| |__|_____| | |___| |\\ | |___| _ < | |",
16
+ " |_| \\_\\_____| |_____|_| \\_|_____|_| \\_\\ |_|",
17
+ ];
11
18
 
12
19
  /**
13
20
  * Small dependency-free terminal presentation for the Local Connector CLI.
@@ -41,6 +48,8 @@ export function createTerminalUi(options = {}) {
41
48
  begin(title, subtitle) {
42
49
  if (!interactive) return;
43
50
  write("");
51
+ for (const line of REENTRY_ASCII) write(style(line, BOLD));
52
+ write("");
44
53
  write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
45
54
  write(` ${style(title, BOLD)}`);
46
55
  if (subtitle) write(` ${style(subtitle, DIM)}`);
@@ -0,0 +1,125 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { emitKeypressEvents } from "node:readline";
5
+ import process from "node:process";
6
+
7
+ const MAX_VISIBLE_DIRECTORIES = 40;
8
+
9
+ /**
10
+ * Let an interactive user choose the directory passed to Codex as its workspace.
11
+ * The picker only selects existing directories; it never creates or deletes files.
12
+ */
13
+ export async function chooseWorkspaceDirectory(options = {}) {
14
+ const input = options.input ?? process.stdin;
15
+ const output = options.output ?? process.stdout;
16
+ if (input.isTTY !== true || output.isTTY !== true || typeof input.setRawMode !== "function") {
17
+ return null;
18
+ }
19
+
20
+ const homeDirectory = resolve(options.homeDirectory ?? homedir());
21
+ const preferredDirectory = resolve(options.startDirectory ?? process.cwd());
22
+ let current = homeDirectory;
23
+ const shortcuts = shortcutDirectories(homeDirectory);
24
+ let selected = 0;
25
+ const wasRaw = Boolean(input.isRaw);
26
+ emitKeypressEvents(input);
27
+ input.setRawMode(true);
28
+
29
+ try {
30
+ while (true) {
31
+ const directories = await readableDirectories(current);
32
+ const choices = createChoices({ current, homeDirectory, preferredDirectory, shortcuts, directories });
33
+ selected = Math.min(selected, choices.length - 1);
34
+ renderPicker(output, current, choices, selected);
35
+ const key = await readKey(input);
36
+ if (key.name === "up") {
37
+ selected = (selected - 1 + choices.length) % choices.length;
38
+ } else if (key.name === "down") {
39
+ selected = (selected + 1) % choices.length;
40
+ } else if (key.name === "return" || key.name === "enter") {
41
+ const choice = choices[selected];
42
+ if (choice.action === "use") return choice.path;
43
+ if (choice.action === "cancel") {
44
+ throw pickerFailure("workspace_selection_cancelled", "Workspace selection was cancelled");
45
+ }
46
+ current = choice.path;
47
+ selected = 0;
48
+ } else if (key.name === "escape" || key.sequence === "\u0003") {
49
+ throw pickerFailure("workspace_selection_cancelled", "Workspace selection was cancelled");
50
+ }
51
+ }
52
+ } finally {
53
+ input.setRawMode(wasRaw);
54
+ output.write("\n");
55
+ }
56
+ }
57
+
58
+ function createChoices({ current, homeDirectory, preferredDirectory, shortcuts, directories }) {
59
+ const choices = [];
60
+ if (current === homeDirectory && preferredDirectory !== homeDirectory) {
61
+ choices.push({ label: `Use current folder ${preferredDirectory}`, action: "use", path: preferredDirectory });
62
+ }
63
+ choices.push({ label: `Use this folder ${current}`, action: "use", path: current });
64
+ if (current === homeDirectory) {
65
+ for (const shortcut of shortcuts) {
66
+ if (directories.some((directory) => directory.path === shortcut.path)) {
67
+ choices.push({ label: `Open ${shortcut.label}`, action: "open", path: shortcut.path });
68
+ }
69
+ }
70
+ }
71
+ const parent = dirname(current);
72
+ if (parent !== current) choices.push({ label: `Go up ${parent}`, action: "open", path: parent });
73
+ const shortcutPaths = new Set(shortcuts.map((shortcut) => shortcut.path));
74
+ for (const directory of directories) {
75
+ if (current === homeDirectory && shortcutPaths.has(directory.path)) continue;
76
+ choices.push({ label: `Open ${directory.name}`, action: "open", path: directory.path });
77
+ }
78
+ choices.push({ label: "Cancel", action: "cancel", path: null });
79
+ return choices;
80
+ }
81
+
82
+ async function readableDirectories(directory) {
83
+ const entries = await readdir(directory, { withFileTypes: true });
84
+ return entries
85
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
86
+ .sort((left, right) => left.name.localeCompare(right.name))
87
+ .slice(0, MAX_VISIBLE_DIRECTORIES)
88
+ .map((entry) => ({ name: entry.name, path: join(directory, entry.name) }));
89
+ }
90
+
91
+ function shortcutDirectories(home) {
92
+ return [
93
+ { label: "Desktop", path: join(home, "Desktop") },
94
+ { label: "Documents", path: join(home, "Documents") },
95
+ { label: "Downloads", path: join(home, "Downloads") },
96
+ { label: "Home", path: home },
97
+ ];
98
+ }
99
+
100
+ function renderPicker(output, current, choices, selected) {
101
+ output.write("\u001b[2J\u001b[H");
102
+ output.write("RE-ENTRY WORKSPACE\n\n");
103
+ output.write("Choose the folder where Codex will work. Files stay inside this workspace.\n");
104
+ output.write(`Current folder: ${current}\n\n`);
105
+ for (let index = 0; index < choices.length; index += 1) {
106
+ output.write(`${index === selected ? "❯" : " "} ${choices[index].label}\n`);
107
+ }
108
+ output.write("\n↑/↓ Move Enter Select Esc Cancel");
109
+ }
110
+
111
+ function readKey(input) {
112
+ return new Promise((resolveKey) => {
113
+ function onKeypress(_sequence, key = {}) {
114
+ input.removeListener("keypress", onKeypress);
115
+ resolveKey(key);
116
+ }
117
+ input.on("keypress", onKeypress);
118
+ });
119
+ }
120
+
121
+ function pickerFailure(code, message) {
122
+ const error = new Error(message);
123
+ error.code = code;
124
+ return error;
125
+ }