@4xeoz/re-entry 0.2.3 → 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 +7 -7
- package/package.json +1 -1
- package/src/main.mjs +17 -2
- package/src/workspace-picker.mjs +112 -43
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
|
|
23
|
-
the
|
|
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
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
->
|
|
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
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({
|
|
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();
|
|
@@ -271,11 +277,16 @@ async function connect(flags, ui, options = {}) {
|
|
|
271
277
|
ui.step("Re-entry", receiver);
|
|
272
278
|
ui.step("This Mac", flags["device-name"] ?? defaultDeviceName());
|
|
273
279
|
}
|
|
274
|
-
const client = new LocalConnectorPairingClient({
|
|
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…");
|
|
275
285
|
const credentials = await client.connect(
|
|
276
286
|
{ deviceName: flags["device-name"] ?? defaultDeviceName() },
|
|
277
287
|
async ({ verificationUri }) => {
|
|
278
288
|
if (ui.interactive) {
|
|
289
|
+
ui.stopWait("Re-entry ready", "secure approval link created");
|
|
279
290
|
ui.step("Browser", "press Enter to open Re-entry sign-in and approval");
|
|
280
291
|
ui.info("If it does not open", verificationUri);
|
|
281
292
|
await waitForEnterToOpenBrowser();
|
|
@@ -802,6 +813,10 @@ function errorHint(error) {
|
|
|
802
813
|
pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
|
|
803
814
|
host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
|
|
804
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",
|
|
805
820
|
device_authorization_expired: "run `re-entry connect` again and approve within ten minutes",
|
|
806
821
|
device_authorization_denied: "run `re-entry connect` again when you are ready to approve this Mac",
|
|
807
822
|
connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
|
package/src/workspace-picker.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
54
|
+
const choices = createBrowserChoices({ current, directories });
|
|
33
55
|
selected = Math.min(selected, choices.length - 1);
|
|
34
|
-
renderPicker(output,
|
|
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
|
|
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
|
|
84
|
+
async function createQuickChoices({ homeDirectory, preferredDirectory }) {
|
|
59
85
|
const choices = [];
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
choices.push({ label:
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
104
|
-
output.write(`Current folder: ${current}\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
|
-
|
|
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;
|