@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 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
- const response = await fetchFn(`${trimTrailingSlash(apiUrl)}/api/cli/device-login/start`, {
64
- method: "POST"
65
- });
66
- if (!response.ok) {
67
- throw new Error(`Unable to start CLI login (${response.status})`);
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
- return parseStartResponse(await response.json());
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 (isTransientPollStatus(response.status)) {
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 isTransientPollStatus(status) {
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
- const response = await fetchFn(`${trimTrailingSlash(input.apiUrl)}/api/hosts/register`, {
23
- method: "POST",
24
- headers: {
25
- authorization: `Bearer ${input.cliToken}`,
26
- "content-type": "application/json"
27
- },
28
- body: JSON.stringify({
29
- machineName: input.machineName,
30
- platform: input.platform
31
- })
32
- });
33
- if (!response.ok) {
34
- throw new Error(`Unable to register host (${response.status})`);
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
- const body = (await response.json());
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.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.1"
18
+ "@abitat_reece/host-daemon": "0.1.3"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "25.6.0",