@abitat_reece/cli 0.1.1 → 0.1.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/dist/auth.js +38 -10
- package/dist/iphone.js +49 -15
- package/package.json +2 -2
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
const DEFAULT_START_ATTEMPTS = 4;
|
|
5
|
+
const DEFAULT_START_RETRY_DELAY_MS = 1000;
|
|
4
6
|
export function defaultApiUrl(env) {
|
|
5
7
|
return env.ABITAT_API_URL ?? "https://workspace.abitat.io";
|
|
6
8
|
}
|
|
@@ -38,7 +40,10 @@ export async function deleteCliSession(configPath) {
|
|
|
38
40
|
}
|
|
39
41
|
export async function runLoginCommand(input) {
|
|
40
42
|
const fetchFn = input.fetchFn ?? fetch;
|
|
41
|
-
const start = await startDeviceLogin(input.apiUrl, fetchFn
|
|
43
|
+
const start = await startDeviceLogin(input.apiUrl, fetchFn, {
|
|
44
|
+
maxAttempts: input.maxStartAttempts ?? DEFAULT_START_ATTEMPTS,
|
|
45
|
+
retryDelayMs: input.startRetryDelayMs ?? DEFAULT_START_RETRY_DELAY_MS
|
|
46
|
+
});
|
|
42
47
|
input.openUrl(new URL(start.verificationPath, input.apiUrl).toString());
|
|
43
48
|
const maxPolls = input.maxPolls ?? 120;
|
|
44
49
|
const pollIntervalMs = input.pollIntervalMs ?? 1000;
|
|
@@ -59,14 +64,34 @@ export async function runLoginCommand(input) {
|
|
|
59
64
|
}
|
|
60
65
|
throw new Error("Timed out waiting for browser login approval");
|
|
61
66
|
}
|
|
62
|
-
async function startDeviceLogin(apiUrl, fetchFn) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
async function startDeviceLogin(apiUrl, fetchFn, options) {
|
|
68
|
+
let lastError;
|
|
69
|
+
const maxAttempts = Math.max(1, options.maxAttempts);
|
|
70
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetchFn(`${trimTrailingSlash(apiUrl)}/api/cli/device-login/start`, {
|
|
73
|
+
method: "POST"
|
|
74
|
+
});
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
return parseStartResponse(await response.json());
|
|
77
|
+
}
|
|
78
|
+
const error = new Error(`Unable to start CLI login (${response.status})`);
|
|
79
|
+
if (!isTransientHostedStatus(response.status) || attempt >= maxAttempts) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
lastError = error;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!isTransientFetchError(error) || attempt >= maxAttempts) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
lastError = error;
|
|
89
|
+
}
|
|
90
|
+
if (options.retryDelayMs > 0) {
|
|
91
|
+
await delay(options.retryDelayMs);
|
|
92
|
+
}
|
|
68
93
|
}
|
|
69
|
-
|
|
94
|
+
throw lastError instanceof Error ? lastError : new Error("Unable to start CLI login");
|
|
70
95
|
}
|
|
71
96
|
async function pollDeviceLogin(apiUrl, deviceLoginId, fetchFn) {
|
|
72
97
|
const response = await fetchFn(`${trimTrailingSlash(apiUrl)}/api/cli/device-login/poll`, {
|
|
@@ -78,16 +103,19 @@ async function pollDeviceLogin(apiUrl, deviceLoginId, fetchFn) {
|
|
|
78
103
|
return { status: "pending" };
|
|
79
104
|
}
|
|
80
105
|
if (!response.ok) {
|
|
81
|
-
if (
|
|
106
|
+
if (isTransientHostedStatus(response.status)) {
|
|
82
107
|
return { status: "pending" };
|
|
83
108
|
}
|
|
84
109
|
throw new Error(`Unable to poll CLI login (${response.status})`);
|
|
85
110
|
}
|
|
86
111
|
return parsePollResponse(await response.json());
|
|
87
112
|
}
|
|
88
|
-
function
|
|
113
|
+
function isTransientHostedStatus(status) {
|
|
89
114
|
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
90
115
|
}
|
|
116
|
+
function isTransientFetchError(error) {
|
|
117
|
+
return error instanceof TypeError;
|
|
118
|
+
}
|
|
91
119
|
function parseStartResponse(value) {
|
|
92
120
|
const candidate = value;
|
|
93
121
|
if (typeof candidate.code !== "string" ||
|
package/dist/iphone.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
const DEFAULT_HOST_REGISTER_ATTEMPTS = 4;
|
|
3
|
+
const DEFAULT_HOST_REGISTER_RETRY_DELAY_MS = 1000;
|
|
1
4
|
export function createIphoneStartupPlan(input) {
|
|
2
5
|
return [
|
|
3
6
|
{
|
|
@@ -12,28 +15,53 @@ export function createIphoneStartupPlan(input) {
|
|
|
12
15
|
env: {
|
|
13
16
|
ABITAT_API_URL: input.apiUrl,
|
|
14
17
|
ABITAT_HOST_TOKEN: input.hostToken,
|
|
15
|
-
ABITAT_MACHINE_ID: input.machineId
|
|
18
|
+
ABITAT_MACHINE_ID: input.machineId,
|
|
19
|
+
CODEX_APP_SERVER_URL: input.codexServerUrl
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
];
|
|
19
23
|
}
|
|
20
24
|
export async function registerHost(input) {
|
|
21
25
|
const fetchFn = input.fetchFn ?? fetch;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
let lastError;
|
|
27
|
+
const maxAttempts = Math.max(1, input.maxAttempts ?? DEFAULT_HOST_REGISTER_ATTEMPTS);
|
|
28
|
+
const retryDelayMs = input.retryDelayMs ?? DEFAULT_HOST_REGISTER_RETRY_DELAY_MS;
|
|
29
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetchFn(`${trimTrailingSlash(input.apiUrl)}/api/hosts/register`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
authorization: `Bearer ${input.cliToken}`,
|
|
35
|
+
"content-type": "application/json"
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
machineName: input.machineName,
|
|
39
|
+
platform: input.platform
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
return parseHostRegistration(await response.json());
|
|
44
|
+
}
|
|
45
|
+
const error = new Error(`Unable to register host (${response.status})`);
|
|
46
|
+
if (!isTransientHostedStatus(response.status) || attempt >= maxAttempts) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
lastError = error;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isTransientFetchError(error) || attempt >= maxAttempts) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
lastError = error;
|
|
56
|
+
}
|
|
57
|
+
if (retryDelayMs > 0) {
|
|
58
|
+
await delay(retryDelayMs);
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
|
-
|
|
61
|
+
throw lastError instanceof Error ? lastError : new Error("Unable to register host");
|
|
62
|
+
}
|
|
63
|
+
function parseHostRegistration(value) {
|
|
64
|
+
const body = value;
|
|
37
65
|
if (typeof body.machineId !== "string" ||
|
|
38
66
|
typeof body.workspaceId !== "string" ||
|
|
39
67
|
typeof body.hostToken !== "string") {
|
|
@@ -66,3 +94,9 @@ export async function prepareIphoneCommand(input) {
|
|
|
66
94
|
function trimTrailingSlash(value) {
|
|
67
95
|
return value.replace(/\/+$/u, "");
|
|
68
96
|
}
|
|
97
|
+
function isTransientHostedStatus(status) {
|
|
98
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
99
|
+
}
|
|
100
|
+
function isTransientFetchError(error) {
|
|
101
|
+
return error instanceof TypeError;
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abitat_reece/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"description": "Remote Codex control from Mac and iPhone",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"node": ">=22"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@abitat_reece/host-daemon": "0.1.
|
|
18
|
+
"@abitat_reece/host-daemon": "0.1.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "25.6.0",
|