@4xeoz/re-entry 0.2.1 → 0.2.3

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.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
@@ -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);
@@ -223,7 +248,8 @@ async function connect(flags, ui, options = {}) {
223
248
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
224
249
  const store = new LocalConnectorCredentialStore({ filename: credentialFile });
225
250
  const current = await store.load();
226
- if (current && Date.parse(current.connector_expires_at) > Date.now()) {
251
+ const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
252
+ if (currentIsValid && current.receiver_origin === receiver) {
227
253
  if (ui.interactive) {
228
254
  ui.success("Already connected", `${current.connector_id} · ${current.receiver_origin}`);
229
255
  ui.info("Next", "run `re-entry start` to wait for approved work");
@@ -237,6 +263,10 @@ async function connect(flags, ui, options = {}) {
237
263
  return current;
238
264
  }
239
265
 
266
+ if (currentIsValid && ui.interactive) {
267
+ ui.warning("Receiver changed", `${current.receiver_origin} → ${receiver}; approve this Mac again`);
268
+ }
269
+
240
270
  if (ui.interactive) {
241
271
  ui.step("Re-entry", receiver);
242
272
  ui.step("This Mac", flags["device-name"] ?? defaultDeviceName());
@@ -318,14 +348,54 @@ async function status(flags, ui) {
318
348
  }
319
349
  }
320
350
 
351
+ async function stop(flags, ui) {
352
+ if (ui.interactive) {
353
+ ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
354
+ }
355
+ const result = await stopMacConnectorService();
356
+ if (ui.interactive) {
357
+ if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
358
+ else if (result.stopped) ui.success("Stopped", "the background Connector is no longer running");
359
+ else ui.info("Already stopped", "no running background Connector was found");
360
+ } else {
361
+ process.stdout.write(`${JSON.stringify({
362
+ event: "connector_stopped",
363
+ supported: result.supported,
364
+ stopped: result.stopped,
365
+ })}\n`);
366
+ }
367
+ }
368
+
369
+ async function uninstall(flags, ui) {
370
+ if (ui.interactive) {
371
+ ui.begin("Uninstall Re-entry", "Stop the Connector and remove only its local service data");
372
+ ui.warning("This removes", "the LaunchAgent, saved Connector credential, and Connector logs");
373
+ await askForUninstallConfirmation();
374
+ } else if (flags.yes !== true) {
375
+ throw cliFailure("connector_uninstall_confirmation_required");
376
+ }
377
+ const result = await uninstallMacConnectorService({
378
+ credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
379
+ });
380
+ if (ui.interactive) {
381
+ ui.success("Uninstalled", "local Connector service data was removed");
382
+ ui.info("Package", "remove the npm package separately with `npm uninstall --global @4xeoz/re-entry`");
383
+ } else {
384
+ process.stdout.write(`${JSON.stringify({
385
+ event: "connector_uninstalled",
386
+ supported: result.supported,
387
+ removed_paths: result.removedPaths,
388
+ })}\n`);
389
+ }
390
+ }
391
+
321
392
  async function install(flags, ui) {
322
393
  if (ui.interactive) {
323
394
  ui.begin("Install Re-entry", "Check Codex, connect your account, then start at login");
395
+ ui.info("Setup", "choose a workspace, approve this Mac, then leave the Connector running");
324
396
  }
325
- const runtimeFlags = {
326
- ...flags,
327
- "codex-cd": flags["codex-cd"] ?? process.cwd(),
328
- };
397
+ const runtimeFlags = await withWorkspaceDirectory(flags, ui);
398
+ if (ui.interactive) ui.info("Receiver", runtimeFlags.receiver ?? DEFAULT_RECEIVER_ORIGIN);
329
399
  const readiness = inspectReadiness(runtimeFlags);
330
400
  showReadiness(readiness, ui);
331
401
  const credentials = await connect(runtimeFlags, ui, { quietHeader: true });
@@ -340,6 +410,7 @@ async function install(flags, ui) {
340
410
  ui.stopWait("Installed", "Re-entry will start automatically when you log in");
341
411
  ui.success("Account", credentials.connector_id);
342
412
  ui.info("Logs", service.stdoutPath);
413
+ ui.info("Commands", "status to inspect · stop to pause · uninstall to remove local setup");
343
414
  ui.info("You are done", "the Connector now waits in the background");
344
415
  } else {
345
416
  process.stdout.write(`${JSON.stringify({
@@ -356,10 +427,7 @@ async function start(flags, ui) {
356
427
  if (ui.interactive) {
357
428
  ui.begin("Re-entry is starting", "Connect once, then wait quietly for work you approve");
358
429
  }
359
- const runtimeFlags = {
360
- ...flags,
361
- "codex-cd": flags["codex-cd"] ?? process.cwd(),
362
- };
430
+ const runtimeFlags = await withWorkspaceDirectory(flags, ui);
363
431
  const readiness = inspectReadiness(runtimeFlags);
364
432
  showReadiness(readiness, ui);
365
433
  if (!ui.interactive) {
@@ -572,7 +640,7 @@ function parseArguments(argumentsList) {
572
640
  const value = rest[index];
573
641
  if (!value.startsWith("--")) throw cliFailure("connector_argument_invalid");
574
642
  const name = value.slice(2);
575
- if (name === "json") {
643
+ if (name === "json" || name === "yes") {
576
644
  if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
577
645
  flags[name] = true;
578
646
  continue;
@@ -591,6 +659,8 @@ function validateCommandFlags(command, flags) {
591
659
  pair: new Set(["receiver", "code", "credential-file", "json"]),
592
660
  connect: new Set(["receiver", "device-name", "credential-file", "json"]),
593
661
  status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
662
+ stop: new Set(["json"]),
663
+ uninstall: new Set(["credential-file", "yes", "json"]),
594
664
  install: new Set([
595
665
  "receiver",
596
666
  "device-name",
@@ -708,6 +778,17 @@ async function askForPairingCode(ui) {
708
778
  }
709
779
  }
710
780
 
781
+ async function askForUninstallConfirmation() {
782
+ if (process.stdin.isTTY !== true) throw cliFailure("connector_uninstall_confirmation_required");
783
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
784
+ try {
785
+ const answer = await readline.question(" Type DELETE to remove local Connector data: ");
786
+ if (answer.trim() !== "DELETE") throw cliFailure("connector_uninstall_confirmation_required");
787
+ } finally {
788
+ readline.close();
789
+ }
790
+ }
791
+
711
792
  function errorHint(error) {
712
793
  const hints = {
713
794
  connector_codex_cd_missing: "pass --codex-cd /absolute/path/to/your/Host-project",
@@ -726,6 +807,8 @@ function errorHint(error) {
726
807
  connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
727
808
  connector_max_errors_invalid: "use a maximum error count between 1 and 20",
728
809
  connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
810
+ connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
811
+ connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
729
812
  };
730
813
  return hints[error?.code] ?? "check the Receiver address and try again";
731
814
  }
@@ -8,6 +8,19 @@ 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
+ ];
18
+ const REENTRY_PANEL = [
19
+ " +-----------------------------------------------+",
20
+ " | RE-ENTRY |",
21
+ " | LOCAL CONNECTOR |",
22
+ " +-----------------------------------------------+",
23
+ ];
11
24
 
12
25
  /**
13
26
  * Small dependency-free terminal presentation for the Local Connector CLI.
@@ -41,6 +54,10 @@ export function createTerminalUi(options = {}) {
41
54
  begin(title, subtitle) {
42
55
  if (!interactive) return;
43
56
  write("");
57
+ for (const line of REENTRY_ASCII) write(style(line, BOLD));
58
+ write("");
59
+ for (const line of REENTRY_PANEL) write(style(line, BOLD));
60
+ write("");
44
61
  write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
45
62
  write(` ${style(title, BOLD)}`);
46
63
  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
+ }