@4xeoz/re-entry 0.2.4 → 0.2.6
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 +26 -1
- package/package.json +5 -2
- package/src/activity-monitor.mjs +107 -0
- package/src/browser-prompt.mjs +1 -1
- package/src/codex-exec-adapter.mjs +40 -2
- package/src/main.mjs +252 -68
- package/src/terminal-ui.mjs +61 -31
- package/src/workspace-picker.mjs +13 -8
package/README.md
CHANGED
|
@@ -16,6 +16,16 @@ Codex may read and write.
|
|
|
16
16
|
npx @4xeoz/re-entry install
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`npx` runs a temporary copy, so it does not leave a permanent shell command behind. Install the
|
|
20
|
+
small CLI globally once if you want to use `re-entry` from any folder afterward:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install --global @4xeoz/re-entry
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The package installs both `re-entry` and the older `reentry` spelling; `re-entry` is the documented
|
|
27
|
+
command.
|
|
28
|
+
|
|
19
29
|
This uses the deployed Re-entry Cloud Receiver by default:
|
|
20
30
|
`https://reentry-cloud.vercel.app`.
|
|
21
31
|
|
|
@@ -27,6 +37,10 @@ npx @4xeoz/re-entry install \
|
|
|
27
37
|
--codex-cd /absolute/path/to/your/project
|
|
28
38
|
```
|
|
29
39
|
|
|
40
|
+
The guided screen stays intentionally small: **Workspace → System check → Connect Re-entry**. It
|
|
41
|
+
shows one clear next command when setup finishes; internal Connector IDs, credential paths, and log
|
|
42
|
+
paths stay out of the normal success screen.
|
|
43
|
+
|
|
30
44
|
Run this from the Host project directory, your home directory, or another normal working
|
|
31
45
|
directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
|
|
32
46
|
source directory as the package itself and fail to create the temporary executable link.
|
|
@@ -70,11 +84,13 @@ preview.
|
|
|
70
84
|
|
|
71
85
|
```sh
|
|
72
86
|
re-entry status
|
|
87
|
+
re-entry listen
|
|
73
88
|
re-entry --help
|
|
74
89
|
```
|
|
75
90
|
|
|
76
91
|
The status view checks the local authorization, background job, Receiver reachability, Node, and
|
|
77
|
-
Codex.
|
|
92
|
+
Codex. `listen` watches the already-running background Connector and displays new activity until
|
|
93
|
+
you press Ctrl+C; it does not start a competing second poller. Useful development commands are:
|
|
78
94
|
|
|
79
95
|
```sh
|
|
80
96
|
re-entry doctor --codex-cd /absolute/path/to/project
|
|
@@ -83,6 +99,15 @@ re-entry claim-once --codex-cd /absolute/path/to/project
|
|
|
83
99
|
re-entry start --codex-cd /absolute/path/to/project
|
|
84
100
|
```
|
|
85
101
|
|
|
102
|
+
Test the local Codex handoff without waiting for Cloud work:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
re-entry test "Reply with: Re-entry is working."
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This starts one fresh local Codex process through the same adapter seam used by real deliveries. It
|
|
109
|
+
does not create a Grant, claim Receiver work, or prove the browser/WebMCP return path.
|
|
110
|
+
|
|
86
111
|
To pause or remove the local Connector:
|
|
87
112
|
|
|
88
113
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4xeoz/re-entry",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outbound Local Connector process for the Re-entry Core local preview.",
|
|
6
6
|
"publishConfig": {
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"README.md",
|
|
11
11
|
"src"
|
|
12
12
|
],
|
|
13
|
-
"bin":
|
|
13
|
+
"bin": {
|
|
14
|
+
"re-entry": "./src/main.mjs",
|
|
15
|
+
"reentry": "./src/main.mjs"
|
|
16
|
+
},
|
|
14
17
|
"exports": {
|
|
15
18
|
".": "./src/index.mjs",
|
|
16
19
|
"./connector": "./src/local-connector.mjs",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { open, stat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const MAX_LINE_BYTES = 16 * 1_024;
|
|
4
|
+
const MAX_READ_BYTES = 64 * 1_024;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Follow new JSON events written by the installed background Connector.
|
|
8
|
+
* The monitor starts at the end of each file and never starts another Connector poller.
|
|
9
|
+
*/
|
|
10
|
+
export async function followConnectorActivity(options) {
|
|
11
|
+
requireOptions(options);
|
|
12
|
+
const cursors = await Promise.all(options.paths.map(initializeCursor));
|
|
13
|
+
const pollIntervalMs = options.pollIntervalMs ?? 350;
|
|
14
|
+
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 10 || pollIntervalMs > 5_000) {
|
|
15
|
+
throw new TypeError("Activity monitor poll interval is invalid");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
while (!options.signal.aborted) {
|
|
19
|
+
for (const cursor of cursors) {
|
|
20
|
+
await readNewEvents(cursor, options.onEvent);
|
|
21
|
+
}
|
|
22
|
+
await waitForNextRead(pollIntervalMs, options.signal);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function initializeCursor(path) {
|
|
27
|
+
try {
|
|
28
|
+
const metadata = await stat(path);
|
|
29
|
+
return { path, offset: metadata.size, pending: "" };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code === "ENOENT") return { path, offset: 0, pending: "" };
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readNewEvents(cursor, onEvent) {
|
|
37
|
+
let handle;
|
|
38
|
+
try {
|
|
39
|
+
handle = await open(cursor.path, "r");
|
|
40
|
+
const metadata = await handle.stat();
|
|
41
|
+
if (metadata.size < cursor.offset) {
|
|
42
|
+
cursor.offset = 0;
|
|
43
|
+
cursor.pending = "";
|
|
44
|
+
}
|
|
45
|
+
if (metadata.size === cursor.offset) return;
|
|
46
|
+
const length = Math.min(metadata.size - cursor.offset, MAX_READ_BYTES);
|
|
47
|
+
const buffer = Buffer.alloc(length);
|
|
48
|
+
const { bytesRead } = await handle.read(buffer, 0, length, cursor.offset);
|
|
49
|
+
cursor.offset += bytesRead;
|
|
50
|
+
const lines = `${cursor.pending}${buffer.subarray(0, bytesRead).toString("utf8")}`.split("\n");
|
|
51
|
+
cursor.pending = lines.pop() ?? "";
|
|
52
|
+
if (Buffer.byteLength(cursor.pending, "utf8") > MAX_LINE_BYTES) cursor.pending = "";
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const event = parseEvent(line);
|
|
55
|
+
if (event) await onEvent(event);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error?.code !== "ENOENT") throw error;
|
|
59
|
+
} finally {
|
|
60
|
+
await handle?.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseEvent(line) {
|
|
65
|
+
if (line.length === 0 || Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) return null;
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(line);
|
|
68
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.event !== "string") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function waitForNextRead(milliseconds, signal) {
|
|
78
|
+
if (signal.aborted) return Promise.resolve();
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const timer = setTimeout(done, milliseconds);
|
|
81
|
+
function done() {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
signal.removeEventListener("abort", done);
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
signal.addEventListener("abort", done, { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireOptions(options) {
|
|
91
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
92
|
+
throw new TypeError("Activity monitor options are required");
|
|
93
|
+
}
|
|
94
|
+
const fields = Object.keys(options);
|
|
95
|
+
if (fields.some((field) => !["paths", "signal", "onEvent", "pollIntervalMs"].includes(field))) {
|
|
96
|
+
throw new TypeError("Activity monitor options contain an unsupported field");
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
!Array.isArray(options.paths) ||
|
|
100
|
+
options.paths.length !== 2 ||
|
|
101
|
+
options.paths.some((path) => typeof path !== "string" || path.length === 0) ||
|
|
102
|
+
!(options.signal instanceof AbortSignal) ||
|
|
103
|
+
typeof options.onEvent !== "function"
|
|
104
|
+
) {
|
|
105
|
+
throw new TypeError("Activity monitor options are invalid");
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/browser-prompt.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import process from "node:process";
|
|
|
8
8
|
export async function waitForEnterToOpenBrowser(options = {}) {
|
|
9
9
|
const input = options.input ?? process.stdin;
|
|
10
10
|
const output = options.output ?? process.stdout;
|
|
11
|
-
const prompt = options.prompt ?? " Press Enter to open Re-entry
|
|
11
|
+
const prompt = options.prompt ?? "\n Press Enter to open Re-entry → ";
|
|
12
12
|
const readline = createInterface({ input, output });
|
|
13
13
|
try {
|
|
14
14
|
await readline.question(prompt);
|
|
@@ -22,6 +22,7 @@ const MIN_COMMAND_TIMEOUT_MS = 100;
|
|
|
22
22
|
const MAX_COMMAND_TIMEOUT_MS = 60_000;
|
|
23
23
|
const MAX_REFERENCE_BYTES = 4 * 1_024;
|
|
24
24
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
25
|
+
const PROMPT_CONTROL_CHARACTER_PATTERN = /[\u0000-\u0009\u000b-\u001f\u007f]/;
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Create the fresh-session Codex adapter that lives inside the Local Connector process.
|
|
@@ -54,11 +55,11 @@ export function createCodexExecAdapter(options) {
|
|
|
54
55
|
return activationResult(activation, "rejected", "activation_rejected", null);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
await
|
|
58
|
+
await runCodexPrompt({
|
|
58
59
|
executable,
|
|
59
60
|
workingDirectory,
|
|
60
61
|
prompt: buildContinuationPrompt(activation),
|
|
61
|
-
|
|
62
|
+
commandTimeoutMs,
|
|
62
63
|
spawnCommand,
|
|
63
64
|
});
|
|
64
65
|
return activationResult(
|
|
@@ -71,6 +72,30 @@ export function createCodexExecAdapter(options) {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Run one local prompt through the same fresh Codex process seam used by real activations.
|
|
77
|
+
* This helper carries no Receiver authority and is intended only for an explicit local smoke test.
|
|
78
|
+
*/
|
|
79
|
+
export async function runCodexPrompt(options) {
|
|
80
|
+
requireExactRecord(
|
|
81
|
+
options,
|
|
82
|
+
["workingDirectory", "prompt", "executable", "commandTimeoutMs", "spawnCommand"],
|
|
83
|
+
["workingDirectory", "prompt"],
|
|
84
|
+
"Codex prompt options",
|
|
85
|
+
);
|
|
86
|
+
const workingDirectory = validateCodexWorkingDirectory(options.workingDirectory);
|
|
87
|
+
const executable = options.executable === undefined
|
|
88
|
+
? discoverCodexExecutable()
|
|
89
|
+
: requireReference(options.executable, "Codex executable");
|
|
90
|
+
const prompt = requirePrompt(options.prompt);
|
|
91
|
+
const timeoutMs = requireTimeout(options.commandTimeoutMs ?? 60_000);
|
|
92
|
+
const spawnCommand = options.spawnCommand ?? spawn;
|
|
93
|
+
if (typeof spawnCommand !== "function") {
|
|
94
|
+
throw new TypeError("Codex prompt spawnCommand must be a function");
|
|
95
|
+
}
|
|
96
|
+
await runCodexExec({ executable, workingDirectory, prompt, timeoutMs, spawnCommand });
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
function buildContinuationPrompt(activation) {
|
|
75
100
|
return [
|
|
76
101
|
"You are a Re-entry continuation agent.",
|
|
@@ -183,6 +208,19 @@ function requireReference(value, label) {
|
|
|
183
208
|
return value;
|
|
184
209
|
}
|
|
185
210
|
|
|
211
|
+
function requirePrompt(value) {
|
|
212
|
+
if (
|
|
213
|
+
typeof value !== "string" ||
|
|
214
|
+
value.length === 0 ||
|
|
215
|
+
value.trim() !== value ||
|
|
216
|
+
Buffer.byteLength(value, "utf8") > MAX_REFERENCE_BYTES ||
|
|
217
|
+
PROMPT_CONTROL_CHARACTER_PATTERN.test(value)
|
|
218
|
+
) {
|
|
219
|
+
throw new TypeError("Codex prompt is invalid");
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
|
|
186
224
|
function requireExactRecord(value, allowedFields, requiredFields, label) {
|
|
187
225
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
188
226
|
throw new TypeError(`${label} must be an object`);
|
package/src/main.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { randomBytes } from "node:crypto";
|
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
|
|
11
11
|
import { LocalConnectorClient } from "@webmcp-challenge/reentry-core/local-connector-client";
|
|
12
|
-
import {
|
|
12
|
+
import { followConnectorActivity } from "./activity-monitor.mjs";
|
|
13
|
+
import { createCodexExecAdapter, runCodexPrompt } from "./codex-exec-adapter.mjs";
|
|
13
14
|
import {
|
|
14
15
|
discoverCodexExecutable,
|
|
15
16
|
requireSupportedNode,
|
|
@@ -47,8 +48,8 @@ async function main() {
|
|
|
47
48
|
process.stdout.write(`${CONNECTOR_VERSION}\n`);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
const { command, flags } = parseArguments(argumentsList);
|
|
51
|
-
validateCommandFlags(command, flags);
|
|
51
|
+
const { command, flags, positionals } = parseArguments(argumentsList);
|
|
52
|
+
validateCommandFlags(command, flags, positionals);
|
|
52
53
|
ui = createTerminalUi({ interactive: flags.json !== true && process.stdout.isTTY === true });
|
|
53
54
|
requireSupportedNode();
|
|
54
55
|
if (command === "doctor") {
|
|
@@ -67,6 +68,14 @@ async function main() {
|
|
|
67
68
|
await status(flags, ui);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
71
|
+
if (command === "listen") {
|
|
72
|
+
await listen(flags, ui);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (command === "test") {
|
|
76
|
+
await testCodex(flags, positionals, ui);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
70
79
|
if (command === "stop") {
|
|
71
80
|
await stop(flags, ui);
|
|
72
81
|
return;
|
|
@@ -111,12 +120,14 @@ Connect this Mac once, then receive approved work in Codex in the background.
|
|
|
111
120
|
|
|
112
121
|
Usage:
|
|
113
122
|
re-entry install Recommended first run
|
|
114
|
-
re-entry
|
|
115
|
-
re-entry
|
|
123
|
+
re-entry listen Watch live activity
|
|
124
|
+
re-entry test "Reply with hello" Test Codex locally
|
|
116
125
|
|
|
117
126
|
Commands:
|
|
118
127
|
install Check this Mac, connect the Re-entry account, and start at login
|
|
119
128
|
status Show account, Receiver, service, Node.js, and Codex readiness
|
|
129
|
+
listen Watch the background Connector until you press Ctrl+C
|
|
130
|
+
test Start one fresh local Codex session with the supplied prompt
|
|
120
131
|
stop Stop the background Connector without removing its credential
|
|
121
132
|
uninstall Stop the Connector and remove its local service data
|
|
122
133
|
connect Authorize this Mac without installing the background service
|
|
@@ -134,6 +145,7 @@ Common options:
|
|
|
134
145
|
--help Show this help
|
|
135
146
|
--version Show the installed version
|
|
136
147
|
|
|
148
|
+
Both commands are installed: re-entry and reentry.
|
|
137
149
|
The Connector opens no inbound port. Host keys never belong on this Mac.
|
|
138
150
|
`);
|
|
139
151
|
}
|
|
@@ -149,9 +161,12 @@ function readConnectorVersion() {
|
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
function doctor(flags, ui) {
|
|
152
|
-
if (ui.interactive)
|
|
164
|
+
if (ui.interactive) {
|
|
165
|
+
ui.begin("System check", "Confirm that this Mac is ready for Re-entry.");
|
|
166
|
+
ui.section("CHECK", "Requirements");
|
|
167
|
+
}
|
|
153
168
|
const readiness = inspectReadiness(flags);
|
|
154
|
-
showReadiness(readiness, ui);
|
|
169
|
+
showReadiness(readiness, ui, { detailed: true });
|
|
155
170
|
if (!ui.interactive) {
|
|
156
171
|
process.stdout.write(`${JSON.stringify({
|
|
157
172
|
event: "connector_ready",
|
|
@@ -179,21 +194,26 @@ async function withWorkspaceDirectory(flags, ui) {
|
|
|
179
194
|
if (flags["codex-cd"] !== undefined || !ui.interactive) {
|
|
180
195
|
return { ...flags, "codex-cd": flags["codex-cd"] ?? process.cwd() };
|
|
181
196
|
}
|
|
182
|
-
ui.info("Workspace", "choose the folder Codex will open");
|
|
183
197
|
const selected = await chooseWorkspaceDirectory({ startDirectory: process.cwd() });
|
|
184
198
|
if (!selected) throw cliFailure("connector_codex_cd_missing");
|
|
185
|
-
ui.success("Workspace", selected);
|
|
186
199
|
return { ...flags, "codex-cd": selected };
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
function showReadiness(readiness, ui) {
|
|
202
|
+
function showReadiness(readiness, ui, options = {}) {
|
|
190
203
|
if (!ui.interactive) return;
|
|
191
|
-
ui.success("Node.js", process.versions.node);
|
|
192
|
-
ui.success(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
ui.success("Node.js", `v${process.versions.node}`);
|
|
205
|
+
ui.success(
|
|
206
|
+
"Codex",
|
|
207
|
+
options.detailed
|
|
208
|
+
? `${readiness.installation.version} · ${readiness.installation.executable}`
|
|
209
|
+
: "ready",
|
|
210
|
+
);
|
|
211
|
+
if (options.includeWorkspace !== false) {
|
|
212
|
+
if (readiness.workingDirectory) {
|
|
213
|
+
ui.success("Workspace", readiness.workingDirectory);
|
|
214
|
+
} else {
|
|
215
|
+
ui.info("Workspace", "selected automatically when Re-entry starts");
|
|
216
|
+
}
|
|
197
217
|
}
|
|
198
218
|
}
|
|
199
219
|
|
|
@@ -215,18 +235,16 @@ async function pair(flags, ui, options = {}) {
|
|
|
215
235
|
if (ui.interactive) ui.wait("Contacting Re-entry…");
|
|
216
236
|
const credentials = await client.pair({ userCode }, async ({ verificationUri }) => {
|
|
217
237
|
if (ui.interactive) {
|
|
218
|
-
ui.stopWait("
|
|
219
|
-
ui.
|
|
220
|
-
ui.info("Open this URL", verificationUri);
|
|
238
|
+
ui.stopWait("Secure link", "ready");
|
|
239
|
+
ui.info("Browser link", verificationUri);
|
|
221
240
|
await waitForEnterToOpenBrowser();
|
|
222
|
-
ui.
|
|
223
|
-
ui.wait("Waiting for you to approve this Mac…");
|
|
241
|
+
ui.wait("Waiting for approval in your browser…");
|
|
224
242
|
} else {
|
|
225
243
|
process.stdout.write(`${JSON.stringify({ event: "pairing_waiting", verification_uri: verificationUri })}\n`);
|
|
226
244
|
}
|
|
227
245
|
});
|
|
228
246
|
if (ui.interactive) {
|
|
229
|
-
ui.stopWait("
|
|
247
|
+
ui.stopWait("Approved", "this Mac is connected");
|
|
230
248
|
if (!credentials.browserOpened) {
|
|
231
249
|
ui.warning("Browser", "did not open automatically; use the URL above");
|
|
232
250
|
}
|
|
@@ -240,7 +258,8 @@ async function pair(flags, ui, options = {}) {
|
|
|
240
258
|
connector_expires_at: credentials.connector_expires_at,
|
|
241
259
|
});
|
|
242
260
|
if (ui.interactive) {
|
|
243
|
-
ui.
|
|
261
|
+
ui.complete("Pairing complete", "Re-entry can now deliver approved work to this Mac.");
|
|
262
|
+
ui.next("re-entry start", "Wait for approved work in this terminal.");
|
|
244
263
|
} else {
|
|
245
264
|
process.stdout.write(`${JSON.stringify({ event: "connector_paired", connector_id: credentials.connector_id })}\n`);
|
|
246
265
|
}
|
|
@@ -257,8 +276,10 @@ async function connect(flags, ui, options = {}) {
|
|
|
257
276
|
const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
|
|
258
277
|
if (currentIsValid && current.receiver_origin === receiver) {
|
|
259
278
|
if (ui.interactive) {
|
|
260
|
-
ui.success("
|
|
261
|
-
|
|
279
|
+
ui.success("Account", "already connected");
|
|
280
|
+
if (!options.guidedInstall) {
|
|
281
|
+
ui.next("re-entry start", "Wait for approved work in this terminal.");
|
|
282
|
+
}
|
|
262
283
|
} else {
|
|
263
284
|
process.stdout.write(`${JSON.stringify({
|
|
264
285
|
event: "connector_already_connected",
|
|
@@ -274,8 +295,8 @@ async function connect(flags, ui, options = {}) {
|
|
|
274
295
|
}
|
|
275
296
|
|
|
276
297
|
if (ui.interactive) {
|
|
277
|
-
ui.
|
|
278
|
-
ui.
|
|
298
|
+
if (!options.guidedInstall) ui.info("Re-entry", displayReceiver(receiver));
|
|
299
|
+
ui.info("This Mac", flags["device-name"] ?? defaultDeviceName());
|
|
279
300
|
}
|
|
280
301
|
const client = new LocalConnectorPairingClient({
|
|
281
302
|
baseUrl: receiver,
|
|
@@ -286,12 +307,10 @@ async function connect(flags, ui, options = {}) {
|
|
|
286
307
|
{ deviceName: flags["device-name"] ?? defaultDeviceName() },
|
|
287
308
|
async ({ verificationUri }) => {
|
|
288
309
|
if (ui.interactive) {
|
|
289
|
-
ui.stopWait("
|
|
290
|
-
ui.
|
|
291
|
-
ui.info("If it does not open", verificationUri);
|
|
310
|
+
ui.stopWait("Secure link", "ready");
|
|
311
|
+
ui.info("Browser link", verificationUri);
|
|
292
312
|
await waitForEnterToOpenBrowser();
|
|
293
|
-
ui.
|
|
294
|
-
ui.wait("Waiting for you to connect this Mac…");
|
|
313
|
+
ui.wait("Waiting for approval in your browser…");
|
|
295
314
|
} else {
|
|
296
315
|
process.stdout.write(`${JSON.stringify({
|
|
297
316
|
event: "connector_authorization_waiting",
|
|
@@ -301,7 +320,7 @@ async function connect(flags, ui, options = {}) {
|
|
|
301
320
|
},
|
|
302
321
|
);
|
|
303
322
|
if (ui.interactive) {
|
|
304
|
-
ui.stopWait("
|
|
323
|
+
ui.stopWait("Account", "connected");
|
|
305
324
|
if (!credentials.browserOpened) {
|
|
306
325
|
ui.warning("Browser", "did not open automatically; use the URL shown above");
|
|
307
326
|
}
|
|
@@ -315,8 +334,10 @@ async function connect(flags, ui, options = {}) {
|
|
|
315
334
|
};
|
|
316
335
|
await store.save(saved);
|
|
317
336
|
if (ui.interactive) {
|
|
318
|
-
|
|
319
|
-
|
|
337
|
+
if (!options.guidedInstall) {
|
|
338
|
+
ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
|
|
339
|
+
ui.next("re-entry start", "Wait for approved work in this terminal.");
|
|
340
|
+
}
|
|
320
341
|
} else {
|
|
321
342
|
process.stdout.write(`${JSON.stringify({
|
|
322
343
|
event: "connector_connected",
|
|
@@ -327,22 +348,33 @@ async function connect(flags, ui, options = {}) {
|
|
|
327
348
|
}
|
|
328
349
|
|
|
329
350
|
async function status(flags, ui) {
|
|
330
|
-
if (ui.interactive) ui.begin("
|
|
351
|
+
if (ui.interactive) ui.begin("Status", "A quick check of this Mac and Re-entry.");
|
|
331
352
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
332
353
|
const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
|
|
333
354
|
const readiness = inspectReadiness(flags);
|
|
334
355
|
const service = await inspectMacConnectorService();
|
|
335
356
|
const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
|
|
336
|
-
showReadiness(readiness, ui);
|
|
337
357
|
const connected = Boolean(credentials && Date.parse(credentials.connector_expires_at) > Date.now());
|
|
338
358
|
if (ui.interactive) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
else ui.warning("
|
|
344
|
-
if (
|
|
345
|
-
else if (
|
|
359
|
+
ui.section("SYSTEM", "This Mac");
|
|
360
|
+
showReadiness(readiness, ui);
|
|
361
|
+
ui.section("CONNECTION", "Re-entry");
|
|
362
|
+
if (connected) ui.success("Account", "connected");
|
|
363
|
+
else ui.warning("Account", "not connected");
|
|
364
|
+
if (service.running) ui.success("Background", "running");
|
|
365
|
+
else if (service.installed) ui.warning("Background", "stopped");
|
|
366
|
+
else ui.warning("Background", "not installed");
|
|
367
|
+
if (connected && receiverReady) ui.success("Cloud", "online");
|
|
368
|
+
else if (connected) ui.warning("Cloud", "unavailable");
|
|
369
|
+
|
|
370
|
+
if (!connected || !service.running) {
|
|
371
|
+
ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
|
|
372
|
+
} else if (!receiverReady) {
|
|
373
|
+
ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
|
|
374
|
+
} else {
|
|
375
|
+
ui.complete("Everything looks good", "Re-entry is ready for approved work.");
|
|
376
|
+
ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
|
|
377
|
+
}
|
|
346
378
|
} else {
|
|
347
379
|
process.stdout.write(`${JSON.stringify({
|
|
348
380
|
event: "connector_status",
|
|
@@ -359,6 +391,109 @@ async function status(flags, ui) {
|
|
|
359
391
|
}
|
|
360
392
|
}
|
|
361
393
|
|
|
394
|
+
async function listen(flags, ui) {
|
|
395
|
+
if (ui.interactive) {
|
|
396
|
+
ui.begin("Live activity", "Watch the background Connector. Ctrl+C closes this view.");
|
|
397
|
+
}
|
|
398
|
+
const service = await inspectMacConnectorService();
|
|
399
|
+
const credentials = await new LocalConnectorCredentialStore({
|
|
400
|
+
filename: defaultCredentialFile(),
|
|
401
|
+
}).load();
|
|
402
|
+
if (!service.running || !credentials) {
|
|
403
|
+
if (ui.interactive) {
|
|
404
|
+
if (!credentials) ui.warning("Account", "not connected");
|
|
405
|
+
if (!service.running) ui.warning("Background", "not running");
|
|
406
|
+
ui.next("re-entry install", "Finish setup and start the background Connector.");
|
|
407
|
+
} else {
|
|
408
|
+
process.stdout.write(`${JSON.stringify({
|
|
409
|
+
event: "connector_listener_unavailable",
|
|
410
|
+
connected: Boolean(credentials),
|
|
411
|
+
service_running: service.running,
|
|
412
|
+
})}\n`);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (ui.interactive) {
|
|
418
|
+
ui.success("Background", "running");
|
|
419
|
+
ui.success("Account", "connected");
|
|
420
|
+
ui.wait("Listening for approved work…");
|
|
421
|
+
} else {
|
|
422
|
+
process.stdout.write('{"event":"connector_listener_started"}\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const stopSignal = createStopSignal();
|
|
426
|
+
try {
|
|
427
|
+
await followConnectorActivity({
|
|
428
|
+
paths: defaultConnectorLogFiles(),
|
|
429
|
+
signal: stopSignal.signal,
|
|
430
|
+
onEvent(event) {
|
|
431
|
+
if (ui.interactive) reportLiveActivity(event, ui);
|
|
432
|
+
else process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
} finally {
|
|
436
|
+
stopSignal.close();
|
|
437
|
+
if (ui.interactive) {
|
|
438
|
+
ui.stopWait("Live view closed", "the background Connector is still running", "info");
|
|
439
|
+
} else {
|
|
440
|
+
process.stdout.write('{"event":"connector_listener_stopped"}\n');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function testCodex(flags, positionals, ui) {
|
|
446
|
+
const prompt = requireTestPrompt(positionals);
|
|
447
|
+
const runtimeFlags = await withWorkspaceDirectory(flags, ui);
|
|
448
|
+
const readiness = inspectReadiness(runtimeFlags);
|
|
449
|
+
if (ui.interactive) {
|
|
450
|
+
ui.begin("Test Codex", "Run one local prompt through the Re-entry Codex adapter.");
|
|
451
|
+
ui.success("Codex", "ready");
|
|
452
|
+
ui.success("Workspace", readiness.workingDirectory);
|
|
453
|
+
ui.info("Prompt", prompt);
|
|
454
|
+
ui.wait("Starting a fresh Codex session…");
|
|
455
|
+
}
|
|
456
|
+
await runCodexPrompt({
|
|
457
|
+
workingDirectory: readiness.workingDirectory,
|
|
458
|
+
executable: readiness.installation.executable,
|
|
459
|
+
prompt,
|
|
460
|
+
commandTimeoutMs: readBoundedNumber(
|
|
461
|
+
flags["activation-timeout"] ?? 60_000,
|
|
462
|
+
100,
|
|
463
|
+
60_000,
|
|
464
|
+
"connector_activation_timeout_invalid",
|
|
465
|
+
),
|
|
466
|
+
});
|
|
467
|
+
if (ui.interactive) {
|
|
468
|
+
ui.stopWait("Codex", "completed the local test");
|
|
469
|
+
ui.complete("Test passed", "The same fresh-session process seam is ready for Re-entry work.");
|
|
470
|
+
ui.next("re-entry listen", "Watch the background Connector for approved work.");
|
|
471
|
+
} else {
|
|
472
|
+
process.stdout.write('{"event":"connector_codex_test_passed"}\n');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function reportLiveActivity(event, ui) {
|
|
477
|
+
if (event.event === "connector_waiting") return;
|
|
478
|
+
if (event.event === "connector_activation_result") {
|
|
479
|
+
const accepted = event.outcome === "accepted";
|
|
480
|
+
ui.stopWait(
|
|
481
|
+
accepted ? "Work received" : "Work finished",
|
|
482
|
+
accepted ? "a fresh Codex session was started" : String(event.outcome ?? "unknown"),
|
|
483
|
+
accepted ? "success" : "warning",
|
|
484
|
+
);
|
|
485
|
+
} else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
|
|
486
|
+
ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
|
|
487
|
+
} else if (event.event === "connector_ready") {
|
|
488
|
+
ui.stopWait("Connector", "ready", "success");
|
|
489
|
+
} else if (event.event === "connector_stopped") {
|
|
490
|
+
ui.stopWait("Background", "stopped", "warning");
|
|
491
|
+
} else {
|
|
492
|
+
ui.stopWait("Activity", event.event.replaceAll("_", " "), "info");
|
|
493
|
+
}
|
|
494
|
+
ui.wait("Listening for approved work…");
|
|
495
|
+
}
|
|
496
|
+
|
|
362
497
|
async function stop(flags, ui) {
|
|
363
498
|
if (ui.interactive) {
|
|
364
499
|
ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
|
|
@@ -366,8 +501,9 @@ async function stop(flags, ui) {
|
|
|
366
501
|
const result = await stopMacConnectorService();
|
|
367
502
|
if (ui.interactive) {
|
|
368
503
|
if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
|
|
369
|
-
else if (result.stopped) ui.
|
|
504
|
+
else if (result.stopped) ui.complete("Re-entry is paused", "Your account connection is still saved.");
|
|
370
505
|
else ui.info("Already stopped", "no running background Connector was found");
|
|
506
|
+
if (result.supported) ui.next("re-entry install", "Start Re-entry in the background again.");
|
|
371
507
|
} else {
|
|
372
508
|
process.stdout.write(`${JSON.stringify({
|
|
373
509
|
event: "connector_stopped",
|
|
@@ -389,8 +525,8 @@ async function uninstall(flags, ui) {
|
|
|
389
525
|
credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
|
|
390
526
|
});
|
|
391
527
|
if (ui.interactive) {
|
|
392
|
-
ui.
|
|
393
|
-
ui.
|
|
528
|
+
ui.complete("Removed from this Mac", "The local service, connection, and logs are gone.");
|
|
529
|
+
ui.next("npx @4xeoz/re-entry install", "Connect this Mac again whenever you are ready.");
|
|
394
530
|
} else {
|
|
395
531
|
process.stdout.write(`${JSON.stringify({
|
|
396
532
|
event: "connector_uninstalled",
|
|
@@ -401,16 +537,18 @@ async function uninstall(flags, ui) {
|
|
|
401
537
|
}
|
|
402
538
|
|
|
403
539
|
async function install(flags, ui) {
|
|
404
|
-
if (ui.interactive) {
|
|
405
|
-
ui.begin("Install Re-entry", "Check Codex, connect your account, then start at login");
|
|
406
|
-
ui.info("Setup", "choose a workspace, approve this Mac, then leave the Connector running");
|
|
407
|
-
}
|
|
408
540
|
const runtimeFlags = await withWorkspaceDirectory(flags, ui);
|
|
409
|
-
if (ui.interactive) ui.info("Receiver", runtimeFlags.receiver ?? DEFAULT_RECEIVER_ORIGIN);
|
|
410
541
|
const readiness = inspectReadiness(runtimeFlags);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
542
|
+
if (ui.interactive) {
|
|
543
|
+
ui.begin("Set up this Mac", "Three quick steps. You only do this once.");
|
|
544
|
+
ui.section("1 OF 3", "Workspace");
|
|
545
|
+
ui.success("Selected", readiness.workingDirectory);
|
|
546
|
+
ui.section("2 OF 3", "System check");
|
|
547
|
+
showReadiness(readiness, ui, { includeWorkspace: false });
|
|
548
|
+
ui.section("3 OF 3", "Connect Re-entry", "Approve this Mac in your browser.");
|
|
549
|
+
}
|
|
550
|
+
const credentials = await connect(runtimeFlags, ui, { quietHeader: true, guidedInstall: true });
|
|
551
|
+
if (ui.interactive) ui.wait("Starting Re-entry in the background…");
|
|
414
552
|
const service = await installMacConnectorService({
|
|
415
553
|
nodeExecutable: process.execPath,
|
|
416
554
|
entrypoint: fileURLToPath(import.meta.url),
|
|
@@ -418,11 +556,9 @@ async function install(flags, ui) {
|
|
|
418
556
|
credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
|
|
419
557
|
});
|
|
420
558
|
if (ui.interactive) {
|
|
421
|
-
ui.stopWait("
|
|
422
|
-
ui.
|
|
423
|
-
ui.
|
|
424
|
-
ui.info("Commands", "status to inspect · stop to pause · uninstall to remove local setup");
|
|
425
|
-
ui.info("You are done", "the Connector now waits in the background");
|
|
559
|
+
ui.stopWait("Background", "running at login");
|
|
560
|
+
ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
|
|
561
|
+
ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
|
|
426
562
|
} else {
|
|
427
563
|
process.stdout.write(`${JSON.stringify({
|
|
428
564
|
event: "connector_service_installed",
|
|
@@ -435,12 +571,13 @@ async function install(flags, ui) {
|
|
|
435
571
|
}
|
|
436
572
|
|
|
437
573
|
async function start(flags, ui) {
|
|
438
|
-
if (ui.interactive) {
|
|
439
|
-
ui.begin("Re-entry is starting", "Connect once, then wait quietly for work you approve");
|
|
440
|
-
}
|
|
441
574
|
const runtimeFlags = await withWorkspaceDirectory(flags, ui);
|
|
442
575
|
const readiness = inspectReadiness(runtimeFlags);
|
|
443
|
-
|
|
576
|
+
if (ui.interactive) {
|
|
577
|
+
ui.begin("Start Re-entry", "Wait for work you have approved.");
|
|
578
|
+
ui.section("READY", "This Mac");
|
|
579
|
+
showReadiness(readiness, ui);
|
|
580
|
+
}
|
|
444
581
|
if (!ui.interactive) {
|
|
445
582
|
process.stdout.write(`${JSON.stringify({
|
|
446
583
|
event: "connector_ready",
|
|
@@ -647,9 +784,14 @@ function parseArguments(argumentsList) {
|
|
|
647
784
|
...(hasExplicitCommand ? argumentsList.slice(commandIndex + 1) : argumentsList.slice(commandIndex)),
|
|
648
785
|
];
|
|
649
786
|
const flags = {};
|
|
787
|
+
const positionals = [];
|
|
650
788
|
for (let index = 0; index < rest.length; index += 1) {
|
|
651
789
|
const value = rest[index];
|
|
652
|
-
if (!value.startsWith("--"))
|
|
790
|
+
if (!value.startsWith("--")) {
|
|
791
|
+
if (command !== "test") throw cliFailure("connector_argument_invalid");
|
|
792
|
+
positionals.push(value);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
653
795
|
const name = value.slice(2);
|
|
654
796
|
if (name === "json" || name === "yes") {
|
|
655
797
|
if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
|
|
@@ -661,15 +803,17 @@ function parseArguments(argumentsList) {
|
|
|
661
803
|
flags[name] = next;
|
|
662
804
|
index += 1;
|
|
663
805
|
}
|
|
664
|
-
return { command, flags };
|
|
806
|
+
return { command, flags, positionals };
|
|
665
807
|
}
|
|
666
808
|
|
|
667
|
-
function validateCommandFlags(command, flags) {
|
|
809
|
+
function validateCommandFlags(command, flags, positionals) {
|
|
668
810
|
const allowedByCommand = {
|
|
669
811
|
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
670
812
|
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
671
813
|
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
672
814
|
status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
|
|
815
|
+
listen: new Set(["json"]),
|
|
816
|
+
test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
|
|
673
817
|
stop: new Set(["json"]),
|
|
674
818
|
uninstall: new Set(["credential-file", "yes", "json"]),
|
|
675
819
|
install: new Set([
|
|
@@ -704,7 +848,11 @@ function validateCommandFlags(command, flags) {
|
|
|
704
848
|
]),
|
|
705
849
|
};
|
|
706
850
|
const allowed = allowedByCommand[command];
|
|
707
|
-
if (
|
|
851
|
+
if (
|
|
852
|
+
!allowed ||
|
|
853
|
+
Object.keys(flags).some((name) => !allowed.has(name)) ||
|
|
854
|
+
(command !== "test" && positionals.length > 0)
|
|
855
|
+
) {
|
|
708
856
|
throw cliFailure("connector_argument_invalid");
|
|
709
857
|
}
|
|
710
858
|
}
|
|
@@ -719,6 +867,14 @@ function defaultCredentialFile() {
|
|
|
719
867
|
return join(homedir(), ".webmcp-connector", "credentials.json");
|
|
720
868
|
}
|
|
721
869
|
|
|
870
|
+
function defaultConnectorLogFiles() {
|
|
871
|
+
const stateDirectory = join(homedir(), ".webmcp-connector");
|
|
872
|
+
return [
|
|
873
|
+
join(stateDirectory, "connector.log"),
|
|
874
|
+
join(stateDirectory, "connector-error.log"),
|
|
875
|
+
];
|
|
876
|
+
}
|
|
877
|
+
|
|
722
878
|
function defaultDeviceName() {
|
|
723
879
|
const value = hostname().trim();
|
|
724
880
|
return value.length >= 2 && Buffer.byteLength(value, "utf8") <= 80
|
|
@@ -726,6 +882,31 @@ function defaultDeviceName() {
|
|
|
726
882
|
: "This Mac";
|
|
727
883
|
}
|
|
728
884
|
|
|
885
|
+
function displayReceiver(origin) {
|
|
886
|
+
try {
|
|
887
|
+
return new URL(origin).host;
|
|
888
|
+
} catch {
|
|
889
|
+
return origin;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function requireTestPrompt(positionals) {
|
|
894
|
+
if (!Array.isArray(positionals) || positionals.length !== 1) {
|
|
895
|
+
throw cliFailure("connector_test_prompt_missing");
|
|
896
|
+
}
|
|
897
|
+
const value = positionals[0];
|
|
898
|
+
if (
|
|
899
|
+
typeof value !== "string" ||
|
|
900
|
+
value.trim() !== value ||
|
|
901
|
+
value.length === 0 ||
|
|
902
|
+
Buffer.byteLength(value, "utf8") > 4 * 1_024 ||
|
|
903
|
+
/[\u0000-\u001f\u007f]/.test(value)
|
|
904
|
+
) {
|
|
905
|
+
throw cliFailure("connector_test_prompt_invalid");
|
|
906
|
+
}
|
|
907
|
+
return value;
|
|
908
|
+
}
|
|
909
|
+
|
|
729
910
|
function readBoundedNumber(value, minimum, maximum, code) {
|
|
730
911
|
const number = typeof value === "number" ? value : Number(value);
|
|
731
912
|
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
@@ -824,6 +1005,9 @@ function errorHint(error) {
|
|
|
824
1005
|
connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
|
|
825
1006
|
connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
|
|
826
1007
|
connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
|
|
1008
|
+
connector_test_prompt_missing: "use `re-entry test \"Reply with: Re-entry is working.\"`",
|
|
1009
|
+
connector_test_prompt_invalid: "use one short, single-line prompt inside quotes",
|
|
1010
|
+
connector_activation_timeout_invalid: "use an activation timeout between 100 and 60000 milliseconds",
|
|
827
1011
|
};
|
|
828
1012
|
return hints[error?.code] ?? "check the Receiver address and try again";
|
|
829
1013
|
}
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -8,19 +8,15 @@ const YELLOW = "\u001b[33m";
|
|
|
8
8
|
const RED = "\u001b[31m";
|
|
9
9
|
const CYAN = "\u001b[36m";
|
|
10
10
|
const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
" |_|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
" | RE-ENTRY |",
|
|
21
|
-
" | LOCAL CONNECTOR |",
|
|
22
|
-
" +-----------------------------------------------+",
|
|
23
|
-
];
|
|
11
|
+
const RULE = " ─────────────────────────────────────────────";
|
|
12
|
+
|
|
13
|
+
export const REENTRY_WORDMARK = Object.freeze([
|
|
14
|
+
" ____ _____ _____ _ _ _____ ______ __",
|
|
15
|
+
" | _ \\| ____| | ____| \\ | |_ _| _ \\ \\ / /",
|
|
16
|
+
" | |_) | _| _____| _| | \\| | | | | |_) | \\ V /",
|
|
17
|
+
" | _ <| |__|_____| |___| |\\ | | | | _ < | |",
|
|
18
|
+
" |_| \\_\\_____| |_____|_| \\_| |_| |_| \\_\\ |_|",
|
|
19
|
+
]);
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* Small dependency-free terminal presentation for the Local Connector CLI.
|
|
@@ -36,7 +32,7 @@ export function createTerminalUi(options = {}) {
|
|
|
36
32
|
let spinnerFrame = 0;
|
|
37
33
|
|
|
38
34
|
const style = (value, code) => color ? `${code}${value}${RESET}` : value;
|
|
39
|
-
const write = (value) => output.write(`${value}\n`);
|
|
35
|
+
const write = (value = "") => output.write(`${value}\n`);
|
|
40
36
|
const clearSpinnerLine = () => {
|
|
41
37
|
if (spinnerTimer === null) return;
|
|
42
38
|
output.write("\r\u001b[2K");
|
|
@@ -44,44 +40,56 @@ export function createTerminalUi(options = {}) {
|
|
|
44
40
|
spinnerTimer = null;
|
|
45
41
|
};
|
|
46
42
|
const renderSpinner = () => {
|
|
47
|
-
output.write(`\r\u001b[2K ${style(SPINNER_FRAMES[spinnerFrame], CYAN)}
|
|
43
|
+
output.write(`\r\u001b[2K ${style(SPINNER_FRAMES[spinnerFrame], CYAN)} ${spinnerMessage}`);
|
|
48
44
|
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
49
45
|
};
|
|
46
|
+
const renderState = (symbol, symbolColor, label, detail) => {
|
|
47
|
+
clearSpinnerLine();
|
|
48
|
+
write(` ${style(symbol, symbolColor)} ${style(label, BOLD)}${detail ? ` ${style(detail, DIM)}` : ""}`);
|
|
49
|
+
};
|
|
50
50
|
|
|
51
51
|
return Object.freeze({
|
|
52
52
|
interactive,
|
|
53
53
|
|
|
54
54
|
begin(title, subtitle) {
|
|
55
55
|
if (!interactive) return;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
write(
|
|
59
|
-
|
|
60
|
-
write(
|
|
61
|
-
write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
|
|
56
|
+
clearSpinnerLine();
|
|
57
|
+
write();
|
|
58
|
+
for (const line of REENTRY_WORDMARK) write(style(line, `${BOLD}${CYAN}`));
|
|
59
|
+
write(` ${style("LOCAL CONNECTOR", DIM)}`);
|
|
60
|
+
write();
|
|
62
61
|
write(` ${style(title, BOLD)}`);
|
|
63
62
|
if (subtitle) write(` ${style(subtitle, DIM)}`);
|
|
64
|
-
write(
|
|
63
|
+
write(RULE);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
section(step, title, detail) {
|
|
67
|
+
if (!interactive) return;
|
|
68
|
+
clearSpinnerLine();
|
|
69
|
+
write();
|
|
70
|
+
write(` ${style(step, CYAN)} ${style(title, BOLD)}`);
|
|
71
|
+
if (detail) write(` ${style(detail, DIM)}`);
|
|
72
|
+
write();
|
|
65
73
|
},
|
|
66
74
|
|
|
67
75
|
step(label, detail) {
|
|
68
76
|
if (!interactive) return;
|
|
69
|
-
|
|
77
|
+
renderState("→", CYAN, label, detail);
|
|
70
78
|
},
|
|
71
79
|
|
|
72
80
|
success(label, detail) {
|
|
73
81
|
if (!interactive) return;
|
|
74
|
-
|
|
82
|
+
renderState("✓", GREEN, label, detail);
|
|
75
83
|
},
|
|
76
84
|
|
|
77
85
|
info(label, detail) {
|
|
78
86
|
if (!interactive) return;
|
|
79
|
-
|
|
87
|
+
renderState("·", CYAN, label, detail);
|
|
80
88
|
},
|
|
81
89
|
|
|
82
90
|
warning(label, detail) {
|
|
83
91
|
if (!interactive) return;
|
|
84
|
-
|
|
92
|
+
renderState("!", YELLOW, label, detail);
|
|
85
93
|
},
|
|
86
94
|
|
|
87
95
|
wait(message) {
|
|
@@ -97,16 +105,38 @@ export function createTerminalUi(options = {}) {
|
|
|
97
105
|
stopWait(label, detail, outcome = "success") {
|
|
98
106
|
if (!interactive) return;
|
|
99
107
|
clearSpinnerLine();
|
|
100
|
-
if (outcome === "warning")
|
|
101
|
-
else if (outcome === "info")
|
|
102
|
-
else
|
|
108
|
+
if (outcome === "warning") renderState("!", YELLOW, label, detail);
|
|
109
|
+
else if (outcome === "info") renderState("·", CYAN, label, detail);
|
|
110
|
+
else renderState("✓", GREEN, label, detail);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
complete(title, detail) {
|
|
114
|
+
if (!interactive) return;
|
|
115
|
+
clearSpinnerLine();
|
|
116
|
+
write();
|
|
117
|
+
write(` ${style("✓", GREEN)} ${style(title, BOLD)}`);
|
|
118
|
+
if (detail) write(` ${style(detail, DIM)}`);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
next(command, detail) {
|
|
122
|
+
if (!interactive) return;
|
|
123
|
+
clearSpinnerLine();
|
|
124
|
+
write();
|
|
125
|
+
write(` ${style("NEXT", CYAN)}`);
|
|
126
|
+
write(` ${style("$", CYAN)} ${style(command, BOLD)}`);
|
|
127
|
+
if (detail) write(` ${style(detail, DIM)}`);
|
|
128
|
+
write();
|
|
103
129
|
},
|
|
104
130
|
|
|
105
131
|
error(label, detail, hint) {
|
|
106
132
|
if (!interactive) return;
|
|
107
133
|
clearSpinnerLine();
|
|
108
|
-
errorOutput.write(
|
|
109
|
-
if (
|
|
134
|
+
errorOutput.write(`\n ${style("✕", RED)} ${style(label, BOLD)}\n`);
|
|
135
|
+
if (detail) errorOutput.write(` ${style(detail, DIM)}\n`);
|
|
136
|
+
if (hint) {
|
|
137
|
+
errorOutput.write(`\n ${style("NEXT", CYAN)}\n`);
|
|
138
|
+
errorOutput.write(` ${style("→", CYAN)} ${hint}\n\n`);
|
|
139
|
+
}
|
|
110
140
|
},
|
|
111
141
|
|
|
112
142
|
close() {
|
package/src/workspace-picker.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { dirname, join, resolve } from "node:path";
|
|
|
4
4
|
import { emitKeypressEvents } from "node:readline";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
|
|
7
|
+
import { REENTRY_WORDMARK } from "./terminal-ui.mjs";
|
|
8
|
+
|
|
7
9
|
const MAX_VISIBLE_DIRECTORIES = 12;
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -77,7 +79,7 @@ export async function chooseWorkspaceDirectory(options = {}) {
|
|
|
77
79
|
}
|
|
78
80
|
} finally {
|
|
79
81
|
input.setRawMode(wasRaw);
|
|
80
|
-
output.write("\
|
|
82
|
+
output.write("\u001b[2J\u001b[H");
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -158,19 +160,22 @@ async function readableDirectories(directory) {
|
|
|
158
160
|
|
|
159
161
|
function renderPicker(output, { title, current, choices, selected, truncated = false }) {
|
|
160
162
|
output.write("\u001b[2J\u001b[H");
|
|
161
|
-
output.write(
|
|
162
|
-
output.write(
|
|
163
|
-
|
|
163
|
+
for (const line of REENTRY_WORDMARK) output.write(`${line}\n`);
|
|
164
|
+
output.write(" LOCAL CONNECTOR\n\n");
|
|
165
|
+
output.write(" 1 OF 3 WORKSPACE\n");
|
|
166
|
+
output.write(` ${title}\n`);
|
|
167
|
+
if (current) output.write(` Current: ${current}\n`);
|
|
168
|
+
output.write(" ─────────────────────────────────────────────\n");
|
|
164
169
|
output.write("\n");
|
|
165
170
|
for (let index = 0; index < choices.length; index += 1) {
|
|
166
171
|
const choice = choices[index];
|
|
167
|
-
output.write(
|
|
168
|
-
if (choice.detail) output.write(`
|
|
172
|
+
output.write(` ${index === selected ? "❯" : " "} ${choice.label}\n`);
|
|
173
|
+
if (choice.detail) output.write(` ${choice.detail}\n`);
|
|
169
174
|
}
|
|
170
175
|
if (truncated) {
|
|
171
|
-
output.write(`\
|
|
176
|
+
output.write(`\n Showing the first ${MAX_VISIBLE_DIRECTORIES} folders.\n`);
|
|
172
177
|
}
|
|
173
|
-
output.write("\n
|
|
178
|
+
output.write("\n ↑↓ Move Enter Select Esc Cancel");
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
function readKey(input) {
|