@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 +46 -5
- package/package.json +1 -1
- package/src/macos-service.mjs +64 -0
- package/src/main.mjs +96 -13
- package/src/terminal-ui.mjs +17 -0
- package/src/workspace-picker.mjs +125 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
package/src/macos-service.mjs
CHANGED
|
@@ -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
|
|
112
|
+
re-entry install Recommended first run
|
|
102
113
|
re-entry status Check the connection
|
|
103
|
-
re-entry start
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -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
|
+
}
|