@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 +7 -7
- package/package.json +1 -1
- package/src/main.mjs +25 -3
- package/src/terminal-ui.mjs +8 -0
- 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();
|
|
@@ -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
|
-
|
|
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({
|
|
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",
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -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)}`);
|
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;
|