@4xeoz/re-entry 0.2.2 → 0.2.4

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
@@ -19,8 +19,8 @@ npx @4xeoz/re-entry install
19
19
  This uses the deployed Re-entry Cloud Receiver by default:
20
20
  `https://reentry-cloud.vercel.app`.
21
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:
22
+ The interactive CLI first offers Desktop, the current folder, or a small folder browser. Choose
23
+ with ↑/↓ and Enter. If you want to skip the picker, pass the workspace directly:
24
24
 
25
25
  ```sh
26
26
  npx @4xeoz/re-entry install \
@@ -31,11 +31,11 @@ Run this from the Host project directory, your home directory, or another normal
31
31
  directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
32
32
  source directory as the package itself and fail to create the temporary executable link.
33
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:
34
+ If your npm installation cannot create the temporary `npx` executable, use the one-time global
35
+ installation instead:
36
36
 
37
37
  ```sh
38
- npm install --global @4xeoz/re-entry@0.2.1
38
+ npm install --global @4xeoz/re-entry
39
39
  re-entry install --codex-cd /absolute/path/to/your/project
40
40
  ```
41
41
 
@@ -51,8 +51,8 @@ npx --yes --package=@4xeoz/re-entry re-entry install \
51
51
  `re-entry install` performs the whole user setup:
52
52
 
53
53
  ```text
54
- check Node + find Codex + validate project directory
55
- -> choose the Codex workspace with ↑/↓ and Enter when --codex-cd is omitted
54
+ choose Desktop, the current folder, or another Codex workspace
55
+ -> check Node + find Codex + validate the selected directory
56
56
  -> request a device authorization from Re-entry
57
57
  -> show the verification URL and wait for the user to press Enter
58
58
  -> open Re-entry in the default browser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
package/src/main.mjs CHANGED
@@ -32,6 +32,7 @@ import { createTerminalUi } from "./terminal-ui.mjs";
32
32
  const DEFAULT_RECEIVER_ORIGIN = process.env.REENTRY_RECEIVER_ORIGIN ?? "https://reentry-cloud.vercel.app";
33
33
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
34
34
  const DEFAULT_MAX_CONSECUTIVE_ERRORS = 5;
35
+ const DEFAULT_PAIRING_REQUEST_TIMEOUT_MS = 20_000;
35
36
  const CONNECTOR_VERSION = readConnectorVersion();
36
37
 
37
38
  async function main() {
@@ -207,9 +208,14 @@ async function pair(flags, ui, options = {}) {
207
208
  }
208
209
  const userCode = flags.code ?? await askForPairingCode(ui);
209
210
  if (ui.interactive) ui.step("Pairing code", "received");
210
- const client = new LocalConnectorPairingClient({ baseUrl: receiver });
211
+ const client = new LocalConnectorPairingClient({
212
+ baseUrl: receiver,
213
+ requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
214
+ });
215
+ if (ui.interactive) ui.wait("Contacting Re-entry…");
211
216
  const credentials = await client.pair({ userCode }, async ({ verificationUri }) => {
212
217
  if (ui.interactive) {
218
+ ui.stopWait("Re-entry ready", "approval link created");
213
219
  ui.step("Browser", "press Enter to open the Re-entry approval page");
214
220
  ui.info("Open this URL", verificationUri);
215
221
  await waitForEnterToOpenBrowser();
@@ -248,7 +254,8 @@ async function connect(flags, ui, options = {}) {
248
254
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
249
255
  const store = new LocalConnectorCredentialStore({ filename: credentialFile });
250
256
  const current = await store.load();
251
- if (current && Date.parse(current.connector_expires_at) > Date.now()) {
257
+ const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
258
+ if (currentIsValid && current.receiver_origin === receiver) {
252
259
  if (ui.interactive) {
253
260
  ui.success("Already connected", `${current.connector_id} · ${current.receiver_origin}`);
254
261
  ui.info("Next", "run `re-entry start` to wait for approved work");
@@ -262,15 +269,24 @@ async function connect(flags, ui, options = {}) {
262
269
  return current;
263
270
  }
264
271
 
272
+ if (currentIsValid && ui.interactive) {
273
+ ui.warning("Receiver changed", `${current.receiver_origin} → ${receiver}; approve this Mac again`);
274
+ }
275
+
265
276
  if (ui.interactive) {
266
277
  ui.step("Re-entry", receiver);
267
278
  ui.step("This Mac", flags["device-name"] ?? defaultDeviceName());
268
279
  }
269
- const client = new LocalConnectorPairingClient({ baseUrl: receiver });
280
+ const client = new LocalConnectorPairingClient({
281
+ baseUrl: receiver,
282
+ requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
283
+ });
284
+ if (ui.interactive) ui.wait("Contacting Re-entry…");
270
285
  const credentials = await client.connect(
271
286
  { deviceName: flags["device-name"] ?? defaultDeviceName() },
272
287
  async ({ verificationUri }) => {
273
288
  if (ui.interactive) {
289
+ ui.stopWait("Re-entry ready", "secure approval link created");
274
290
  ui.step("Browser", "press Enter to open Re-entry sign-in and approval");
275
291
  ui.info("If it does not open", verificationUri);
276
292
  await waitForEnterToOpenBrowser();
@@ -390,6 +406,7 @@ async function install(flags, ui) {
390
406
  ui.info("Setup", "choose a workspace, approve this Mac, then leave the Connector running");
391
407
  }
392
408
  const runtimeFlags = await withWorkspaceDirectory(flags, ui);
409
+ if (ui.interactive) ui.info("Receiver", runtimeFlags.receiver ?? DEFAULT_RECEIVER_ORIGIN);
393
410
  const readiness = inspectReadiness(runtimeFlags);
394
411
  showReadiness(readiness, ui);
395
412
  const credentials = await connect(runtimeFlags, ui, { quietHeader: true });
@@ -404,6 +421,7 @@ async function install(flags, ui) {
404
421
  ui.stopWait("Installed", "Re-entry will start automatically when you log in");
405
422
  ui.success("Account", credentials.connector_id);
406
423
  ui.info("Logs", service.stdoutPath);
424
+ ui.info("Commands", "status to inspect · stop to pause · uninstall to remove local setup");
407
425
  ui.info("You are done", "the Connector now waits in the background");
408
426
  } else {
409
427
  process.stdout.write(`${JSON.stringify({
@@ -795,6 +813,10 @@ function errorHint(error) {
795
813
  pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
796
814
  host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
797
815
  pairing_expired: "ask the Host backend for a new pairing code",
816
+ pairing_request_timeout: "the Receiver took too long to answer; check your connection and run the command again",
817
+ pairing_network_error: "check your internet connection and the Receiver address, then try again",
818
+ workspace_directory_unavailable: "choose a readable folder or pass --codex-cd /absolute/path",
819
+ workspace_selection_cancelled: "run the command again when you are ready to choose a workspace",
798
820
  device_authorization_expired: "run `re-entry connect` again and approve within ten minutes",
799
821
  device_authorization_denied: "run `re-entry connect` again when you are ready to approve this Mac",
800
822
  connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
@@ -15,6 +15,12 @@ const REENTRY_ASCII = [
15
15
  " | _ <| |__|_____| | |___| |\\ | |___| _ < | |",
16
16
  " |_| \\_\\_____| |_____|_| \\_|_____|_| \\_\\ |_|",
17
17
  ];
18
+ const REENTRY_PANEL = [
19
+ " +-----------------------------------------------+",
20
+ " | RE-ENTRY |",
21
+ " | LOCAL CONNECTOR |",
22
+ " +-----------------------------------------------+",
23
+ ];
18
24
 
19
25
  /**
20
26
  * Small dependency-free terminal presentation for the Local Connector CLI.
@@ -50,6 +56,8 @@ export function createTerminalUi(options = {}) {
50
56
  write("");
51
57
  for (const line of REENTRY_ASCII) write(style(line, BOLD));
52
58
  write("");
59
+ for (const line of REENTRY_PANEL) write(style(line, BOLD));
60
+ write("");
53
61
  write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
54
62
  write(` ${style(title, BOLD)}`);
55
63
  if (subtitle) write(` ${style(subtitle, DIM)}`);
@@ -1,10 +1,10 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readdir, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { emitKeypressEvents } from "node:readline";
5
5
  import process from "node:process";
6
6
 
7
- const MAX_VISIBLE_DIRECTORIES = 40;
7
+ const MAX_VISIBLE_DIRECTORIES = 12;
8
8
 
9
9
  /**
10
10
  * Let an interactive user choose the directory passed to Codex as its workspace.
@@ -19,19 +19,47 @@ export async function chooseWorkspaceDirectory(options = {}) {
19
19
 
20
20
  const homeDirectory = resolve(options.homeDirectory ?? homedir());
21
21
  const preferredDirectory = resolve(options.startDirectory ?? process.cwd());
22
- let current = homeDirectory;
23
- const shortcuts = shortcutDirectories(homeDirectory);
24
22
  let selected = 0;
25
23
  const wasRaw = Boolean(input.isRaw);
26
24
  emitKeypressEvents(input);
27
25
  input.setRawMode(true);
28
26
 
29
27
  try {
28
+ const quickChoices = await createQuickChoices({ homeDirectory, preferredDirectory });
29
+ while (true) {
30
+ renderPicker(output, {
31
+ title: "Choose where Codex should open for approved work.",
32
+ choices: quickChoices,
33
+ selected,
34
+ });
35
+ const key = await readKey(input);
36
+ if (key.name === "up") {
37
+ selected = (selected - 1 + quickChoices.length) % quickChoices.length;
38
+ } else if (key.name === "down") {
39
+ selected = (selected + 1) % quickChoices.length;
40
+ } else if (key.name === "return" || key.name === "enter") {
41
+ const choice = quickChoices[selected];
42
+ if (choice.action === "use") return choice.path;
43
+ if (choice.action === "cancel") throw cancelledFailure();
44
+ break;
45
+ } else if (key.name === "escape" || key.sequence === "\u0003") {
46
+ throw cancelledFailure();
47
+ }
48
+ }
49
+
50
+ let current = homeDirectory;
51
+ selected = 0;
30
52
  while (true) {
31
53
  const directories = await readableDirectories(current);
32
- const choices = createChoices({ current, homeDirectory, preferredDirectory, shortcuts, directories });
54
+ const choices = createBrowserChoices({ current, directories });
33
55
  selected = Math.min(selected, choices.length - 1);
34
- renderPicker(output, current, choices, selected);
56
+ renderPicker(output, {
57
+ title: "Choose another folder.",
58
+ current,
59
+ choices,
60
+ selected,
61
+ truncated: directories.truncated,
62
+ });
35
63
  const key = await readKey(input);
36
64
  if (key.name === "up") {
37
65
  selected = (selected - 1 + choices.length) % choices.length;
@@ -40,13 +68,11 @@ export async function chooseWorkspaceDirectory(options = {}) {
40
68
  } else if (key.name === "return" || key.name === "enter") {
41
69
  const choice = choices[selected];
42
70
  if (choice.action === "use") return choice.path;
43
- if (choice.action === "cancel") {
44
- throw pickerFailure("workspace_selection_cancelled", "Workspace selection was cancelled");
45
- }
71
+ if (choice.action === "cancel") throw cancelledFailure();
46
72
  current = choice.path;
47
73
  selected = 0;
48
74
  } else if (key.name === "escape" || key.sequence === "\u0003") {
49
- throw pickerFailure("workspace_selection_cancelled", "Workspace selection was cancelled");
75
+ throw cancelledFailure();
50
76
  }
51
77
  }
52
78
  } finally {
@@ -55,55 +81,94 @@ export async function chooseWorkspaceDirectory(options = {}) {
55
81
  }
56
82
  }
57
83
 
58
- function createChoices({ current, homeDirectory, preferredDirectory, shortcuts, directories }) {
84
+ async function createQuickChoices({ homeDirectory, preferredDirectory }) {
59
85
  const choices = [];
60
- if (current === homeDirectory && preferredDirectory !== homeDirectory) {
61
- choices.push({ label: `Use current folder ${preferredDirectory}`, action: "use", path: preferredDirectory });
86
+ const desktop = join(homeDirectory, "Desktop");
87
+ if (await isDirectory(desktop)) {
88
+ choices.push({
89
+ label: "Use Desktop (recommended)",
90
+ detail: desktop,
91
+ action: "use",
92
+ path: desktop,
93
+ });
62
94
  }
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
- }
95
+ if (preferredDirectory !== desktop && await isDirectory(preferredDirectory)) {
96
+ choices.push({
97
+ label: "Use current folder",
98
+ detail: preferredDirectory,
99
+ action: "use",
100
+ path: preferredDirectory,
101
+ });
70
102
  }
103
+ if (choices.length === 0) {
104
+ choices.push({
105
+ label: "Use Home",
106
+ detail: homeDirectory,
107
+ action: "use",
108
+ path: homeDirectory,
109
+ });
110
+ }
111
+ choices.push({ label: "Choose another folder…", action: "browse", path: homeDirectory });
112
+ choices.push({ label: "Cancel", action: "cancel", path: null });
113
+ return choices;
114
+ }
115
+
116
+ function createBrowserChoices({ current, directories }) {
117
+ const choices = [
118
+ { label: "Use this folder", detail: current, action: "use", path: current },
119
+ ];
71
120
  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 });
121
+ if (parent !== current) {
122
+ choices.push({ label: "Go up", detail: parent, action: "open", path: parent });
123
+ }
124
+ for (const directory of directories.items) {
125
+ choices.push({ label: directory.name, action: "open", path: directory.path });
77
126
  }
78
127
  choices.push({ label: "Cancel", action: "cancel", path: null });
79
128
  return choices;
80
129
  }
81
130
 
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) }));
131
+ async function isDirectory(path) {
132
+ try {
133
+ return (await stat(path)).isDirectory();
134
+ } catch {
135
+ return false;
136
+ }
89
137
  }
90
138
 
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
- ];
139
+ async function readableDirectories(directory) {
140
+ let entries;
141
+ try {
142
+ entries = await readdir(directory, { withFileTypes: true });
143
+ } catch (error) {
144
+ const failure = pickerFailure("workspace_directory_unavailable", `Cannot open ${directory}`);
145
+ failure.cause = error;
146
+ throw failure;
147
+ }
148
+ const directories = entries
149
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
150
+ .sort((left, right) => left.name.localeCompare(right.name));
151
+ return {
152
+ items: directories
153
+ .slice(0, MAX_VISIBLE_DIRECTORIES)
154
+ .map((entry) => ({ name: entry.name, path: join(directory, entry.name) })),
155
+ truncated: directories.length > MAX_VISIBLE_DIRECTORIES,
156
+ };
98
157
  }
99
158
 
100
- function renderPicker(output, current, choices, selected) {
159
+ function renderPicker(output, { title, current, choices, selected, truncated = false }) {
101
160
  output.write("\u001b[2J\u001b[H");
102
161
  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`);
162
+ output.write(`${title}\n`);
163
+ if (current) output.write(`Current folder: ${current}\n`);
164
+ output.write("\n");
105
165
  for (let index = 0; index < choices.length; index += 1) {
106
- output.write(`${index === selected ? "❯" : " "} ${choices[index].label}\n`);
166
+ const choice = choices[index];
167
+ output.write(`${index === selected ? "❯" : " "} ${choice.label}\n`);
168
+ if (choice.detail) output.write(` ${choice.detail}\n`);
169
+ }
170
+ if (truncated) {
171
+ output.write(`\nShowing the first ${MAX_VISIBLE_DIRECTORIES} folders. Open a folder to continue.\n`);
107
172
  }
108
173
  output.write("\n↑/↓ Move Enter Select Esc Cancel");
109
174
  }
@@ -118,6 +183,10 @@ function readKey(input) {
118
183
  });
119
184
  }
120
185
 
186
+ function cancelledFailure() {
187
+ return pickerFailure("workspace_selection_cancelled", "Workspace selection was cancelled");
188
+ }
189
+
121
190
  function pickerFailure(code, message) {
122
191
  const error = new Error(message);
123
192
  error.code = code;