@ship-et/buildatlas-cli 0.0.0-stage → 0.11.0

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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/browser-login.d.ts +12 -0
  4. package/dist/browser-login.d.ts.map +1 -0
  5. package/dist/browser-login.js +94 -0
  6. package/dist/browser-login.js.map +1 -0
  7. package/dist/client.d.ts +33 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +115 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/client.test.d.ts +2 -0
  12. package/dist/client.test.d.ts.map +1 -0
  13. package/dist/client.test.js +102 -0
  14. package/dist/client.test.js.map +1 -0
  15. package/dist/commands/auth.d.ts +3 -0
  16. package/dist/commands/auth.d.ts.map +1 -0
  17. package/dist/commands/auth.js +88 -0
  18. package/dist/commands/auth.js.map +1 -0
  19. package/dist/commands/build.d.ts +4 -0
  20. package/dist/commands/build.d.ts.map +1 -0
  21. package/dist/commands/build.js +297 -0
  22. package/dist/commands/build.js.map +1 -0
  23. package/dist/commands/builds.d.ts +3 -0
  24. package/dist/commands/builds.d.ts.map +1 -0
  25. package/dist/commands/builds.js +102 -0
  26. package/dist/commands/builds.js.map +1 -0
  27. package/dist/commands/credentials-wizard.d.ts +25 -0
  28. package/dist/commands/credentials-wizard.d.ts.map +1 -0
  29. package/dist/commands/credentials-wizard.js +274 -0
  30. package/dist/commands/credentials-wizard.js.map +1 -0
  31. package/dist/commands/credentials.d.ts +3 -0
  32. package/dist/commands/credentials.d.ts.map +1 -0
  33. package/dist/commands/credentials.js +249 -0
  34. package/dist/commands/credentials.js.map +1 -0
  35. package/dist/commands/env.d.ts +37 -0
  36. package/dist/commands/env.d.ts.map +1 -0
  37. package/dist/commands/env.js +220 -0
  38. package/dist/commands/env.js.map +1 -0
  39. package/dist/commands/expo-tokens.d.ts +3 -0
  40. package/dist/commands/expo-tokens.d.ts.map +1 -0
  41. package/dist/commands/expo-tokens.js +103 -0
  42. package/dist/commands/expo-tokens.js.map +1 -0
  43. package/dist/commands/init.d.ts +6 -0
  44. package/dist/commands/init.d.ts.map +1 -0
  45. package/dist/commands/init.js +204 -0
  46. package/dist/commands/init.js.map +1 -0
  47. package/dist/commands/projects.d.ts +11 -0
  48. package/dist/commands/projects.d.ts.map +1 -0
  49. package/dist/commands/projects.js +148 -0
  50. package/dist/commands/projects.js.map +1 -0
  51. package/dist/commands/submit.d.ts +10 -0
  52. package/dist/commands/submit.d.ts.map +1 -0
  53. package/dist/commands/submit.js +118 -0
  54. package/dist/commands/submit.js.map +1 -0
  55. package/dist/commands/update.d.ts +10 -0
  56. package/dist/commands/update.d.ts.map +1 -0
  57. package/dist/commands/update.js +93 -0
  58. package/dist/commands/update.js.map +1 -0
  59. package/dist/config.d.ts +66 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +255 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/detach.d.ts +18 -0
  64. package/dist/detach.d.ts.map +1 -0
  65. package/dist/detach.js +42 -0
  66. package/dist/detach.js.map +1 -0
  67. package/dist/eas-credentials-zip.d.ts +38 -0
  68. package/dist/eas-credentials-zip.d.ts.map +1 -0
  69. package/dist/eas-credentials-zip.js +63 -0
  70. package/dist/eas-credentials-zip.js.map +1 -0
  71. package/dist/eas-json.d.ts +21 -0
  72. package/dist/eas-json.d.ts.map +1 -0
  73. package/dist/eas-json.js +32 -0
  74. package/dist/eas-json.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +67 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/logs.d.ts +13 -0
  80. package/dist/logs.d.ts.map +1 -0
  81. package/dist/logs.js +70 -0
  82. package/dist/logs.js.map +1 -0
  83. package/dist/packaging.test.d.ts +2 -0
  84. package/dist/packaging.test.d.ts.map +1 -0
  85. package/dist/packaging.test.js +79 -0
  86. package/dist/packaging.test.js.map +1 -0
  87. package/dist/profiles.test.d.ts +2 -0
  88. package/dist/profiles.test.d.ts.map +1 -0
  89. package/dist/profiles.test.js +56 -0
  90. package/dist/profiles.test.js.map +1 -0
  91. package/dist/project-hint.d.ts +12 -0
  92. package/dist/project-hint.d.ts.map +1 -0
  93. package/dist/project-hint.js +19 -0
  94. package/dist/project-hint.js.map +1 -0
  95. package/dist/project-type.d.ts +43 -0
  96. package/dist/project-type.d.ts.map +1 -0
  97. package/dist/project-type.js +88 -0
  98. package/dist/project-type.js.map +1 -0
  99. package/dist/project-type.test.d.ts +2 -0
  100. package/dist/project-type.test.d.ts.map +1 -0
  101. package/dist/project-type.test.js +77 -0
  102. package/dist/project-type.test.js.map +1 -0
  103. package/dist/prompt.d.ts +56 -0
  104. package/dist/prompt.d.ts.map +1 -0
  105. package/dist/prompt.js +157 -0
  106. package/dist/prompt.js.map +1 -0
  107. package/dist/tarball.d.ts +39 -0
  108. package/dist/tarball.d.ts.map +1 -0
  109. package/dist/tarball.js +208 -0
  110. package/dist/tarball.js.map +1 -0
  111. package/dist/tarball.test.d.ts +2 -0
  112. package/dist/tarball.test.d.ts.map +1 -0
  113. package/dist/tarball.test.js +97 -0
  114. package/dist/tarball.test.js.map +1 -0
  115. package/package.json +56 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digicom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # buildatlas
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ CLI for [BuildAtlas](https://github.com/YohannesTz/BuildAtlas) — a self-hosted
4
+ replacement for EAS Build and EAS Update that runs on a Mac you own.
5
+
6
+ This package is the client. It talks to a BuildAtlas server that you host; it is
7
+ not useful on its own. If nobody has set one up yet, start with
8
+ [self-hosting](https://github.com/YohannesTz/BuildAtlas/blob/main/docs/self-hosting.md).
9
+
10
+ ```bash
11
+ npm install -g buildatlas
12
+ ```
13
+
14
+ Installs two commands: `buildatlas`, and `ba` as a shorter alias.
15
+
16
+ ## Use
17
+
18
+ ```bash
19
+ buildatlas login --api https://builds.yourcompany.com
20
+ export BUILDATLAS_PROJECT=my-app
21
+
22
+ buildatlas build --platform android --profile production # → signed .aab
23
+ buildatlas build --platform ios --profile production # → signed .ipa
24
+ buildatlas update --branch production # → OTA, no rebuild
25
+ ```
26
+
27
+ Your existing `eas.json` profiles work unchanged — BuildAtlas reads the same
28
+ file, because each build runs `eas build --local` under the hood. Builds happen
29
+ on your hardware, artifacts land in your object storage, and signing
30
+ certificates never leave your network.
31
+
32
+ ## Commands
33
+
34
+ | | |
35
+ |---|---|
36
+ | `login` / `logout` / `whoami` | Authentication for this machine |
37
+ | `build` | Queue a build and follow the log |
38
+ | `builds` / `logs` / `cancel` | Inspect and manage builds |
39
+ | `credentials` | Signing material and build-time environment variables |
40
+ | `expo-tokens` | Expo access tokens, which every build needs |
41
+ | `update` | Publish an OTA update |
42
+
43
+ Full reference: [docs/cli.md](https://github.com/YohannesTz/BuildAtlas/blob/main/docs/cli.md).
44
+
45
+ New to Expo? [Getting started](https://github.com/YohannesTz/BuildAtlas/blob/main/docs/getting-started.md)
46
+ walks from an empty folder to an app on a phone.
47
+
48
+ ## Configuration
49
+
50
+ | | Env var | Falls back to |
51
+ |---|---|---|
52
+ | API URL | `BUILDATLAS_API_URL` | `~/.buildatlas/auth.json`, then `--api` |
53
+ | Token | `BUILDATLAS_TOKEN` | `~/.buildatlas/auth.json` |
54
+ | Project | `BUILDATLAS_PROJECT` | `--project` |
55
+
56
+ Env vars win over the file, so CI can inject a token without writing to disk.
57
+ The config file is created at mode 0600.
58
+
59
+ Requires Node 20.11 or newer.
60
+
61
+ ## Developing against a local checkout
62
+
63
+ ```bash
64
+ npm link -w buildatlas # from the repo root
65
+ ```
66
+
67
+ Prefer `npm link` over `npm install -g ./packages/cli` while iterating. A global
68
+ install is keyed on the version number, so npm reports "up to date" and silently
69
+ skips the copy when you rebuild without bumping it — leaving an old binary on your
70
+ PATH while the source looks correct. `npm link` symlinks instead, so a rebuild is
71
+ picked up immediately.
72
+
73
+ If you do use a global install, `npm rm -g buildatlas` first.
74
+
75
+ The build script sets the executable bit explicitly. `tsc` emits mode 0644, and a
76
+ bin symlinked into `dist/` stops being runnable after every rebuild — npm sets the
77
+ bit when installing from a tarball, but not for a linked workspace, which is exactly
78
+ how you develop against it. The symptom is `permission denied: buildatlas`.
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,12 @@
1
+ export interface BrowserLogin {
2
+ /** Resolves with the token, or null if it timed out / was aborted. */
3
+ readonly done: Promise<string | null>;
4
+ /** The URL the user can open by hand if the browser did not launch. */
5
+ readonly url: string;
6
+ /** Stop the local server (used when a pasted token wins the race instead). */
7
+ abort(): void;
8
+ }
9
+ export declare function startBrowserLogin(apiUrl: string, tokenName: string, timeoutMs?: number): BrowserLogin;
10
+ /** Default token name for a machine: the hostname, trimmed to something sane. */
11
+ export declare function defaultTokenName(): string;
12
+ //# sourceMappingURL=browser-login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-login.d.ts","sourceRoot":"","sources":["../src/browser-login.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtC,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,SAAU,GAAG,YAAY,CAsDtG;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,IAAI,MAAM,CAGzC"}
@@ -0,0 +1,94 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { hostname } from 'node:os';
5
+ import pc from 'picocolors';
6
+ /**
7
+ * Browser login over a localhost callback — the "gh auth login" pattern.
8
+ *
9
+ * The CLI starts a one-shot HTTP server on 127.0.0.1, opens the dashboard's
10
+ * `/cli-auth` page, and waits. The page mints a token (as the logged-in user) and
11
+ * redirects the browser to `http://127.0.0.1:<port>/cb?token=…&state=…`; this server
12
+ * catches it. No copy-paste, and the token never travels anywhere but loopback.
13
+ *
14
+ * A random `state` ties the browser round-trip to this exact invocation, so a stray
15
+ * request to the port cannot inject a token. The callback host is loopback by
16
+ * construction — the page is only ever told a port, never a full URL — so a token
17
+ * cannot be redirected off the machine.
18
+ */
19
+ /** Opens a URL in the default browser, best-effort. A headless box just gets the printed URL. */
20
+ function openBrowser(url) {
21
+ const [cmd, args] = process.platform === 'darwin'
22
+ ? ['open', [url]]
23
+ : process.platform === 'win32'
24
+ ? ['cmd', ['/c', 'start', '', url]]
25
+ : ['xdg-open', [url]];
26
+ try {
27
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
28
+ }
29
+ catch {
30
+ // No browser launcher (SSH, minimal box). The URL is printed; open it elsewhere.
31
+ }
32
+ }
33
+ const DONE_PAGE = '<!doctype html><meta charset="utf-8"><title>BuildAtlas</title>' +
34
+ '<body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:64px 24px;color:#15122b">' +
35
+ '<div style="font-size:40px">✓</div><h1 style="margin:.3em 0">Connected</h1>' +
36
+ '<p style="color:#6f6890">You can close this tab and return to the terminal.</p></body>';
37
+ export function startBrowserLogin(apiUrl, tokenName, timeoutMs = 180_000) {
38
+ const state = randomBytes(16).toString('hex');
39
+ let settle;
40
+ const done = new Promise((r) => {
41
+ settle = r;
42
+ });
43
+ const server = createServer((req, res) => {
44
+ const reqUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
45
+ if (reqUrl.pathname !== '/cb') {
46
+ res.writeHead(404).end();
47
+ return;
48
+ }
49
+ const token = reqUrl.searchParams.get('token');
50
+ const returned = reqUrl.searchParams.get('state');
51
+ if (!token || returned !== state) {
52
+ res
53
+ .writeHead(400, { 'content-type': 'text/html' })
54
+ .end('<!doctype html><meta charset="utf-8"><body>Login failed — you can close this tab.</body>');
55
+ return;
56
+ }
57
+ res.writeHead(200, { 'content-type': 'text/html' }).end(DONE_PAGE);
58
+ finish(token);
59
+ });
60
+ let finished = false;
61
+ const finish = (token) => {
62
+ if (finished)
63
+ return;
64
+ finished = true;
65
+ clearTimeout(timer);
66
+ server.close();
67
+ settle(token);
68
+ };
69
+ const timer = setTimeout(() => finish(null), timeoutMs);
70
+ server.on('error', () => finish(null));
71
+ // Bind synchronously-ish: listen then compute the URL in the callback.
72
+ let url = '';
73
+ server.listen(0, '127.0.0.1', () => {
74
+ const addr = server.address();
75
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
76
+ url = `${apiUrl.replace(/\/+$/, '')}/cli-auth?port=${port}&state=${state}&name=${encodeURIComponent(tokenName)}`;
77
+ console.log(`\n→ Opening ${pc.cyan(url)}`);
78
+ console.log(pc.dim(' Approve in the browser and the token returns here automatically.'));
79
+ openBrowser(url);
80
+ });
81
+ return {
82
+ done,
83
+ get url() {
84
+ return url;
85
+ },
86
+ abort: () => finish(null),
87
+ };
88
+ }
89
+ /** Default token name for a machine: the hostname, trimmed to something sane. */
90
+ export function defaultTokenName() {
91
+ const host = hostname().split('.')[0] || 'cli';
92
+ return `cli-${host}`;
93
+ }
94
+ //# sourceMappingURL=browser-login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-login.js","sourceRoot":"","sources":["../src/browser-login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { AtlasConfig } from './config.ts';
2
+ export declare class AtlasApiError extends Error {
3
+ readonly status: number;
4
+ readonly code: string;
5
+ constructor(status: number, code: string, message: string);
6
+ }
7
+ /**
8
+ * Whether this request may be sent twice.
9
+ *
10
+ * It matters because the link between a developer and a self-hosted server is not the
11
+ * one in the demo. Measured against a server in Germany from an office in Addis Ababa,
12
+ * roughly one request in thirty either never connects or gets a 504 out of the reverse
13
+ * proxy — and with no retry at all that is one `buildatlas build` in thirty dying after
14
+ * the source archive has already been packed, and one `cancel` in thirty reporting
15
+ * "Gateway Time-out" at a person who then has no idea whether the build was cancelled.
16
+ *
17
+ * `safe` is the default for GET, and is also right for the calls that state a fact
18
+ * rather than add one — enqueue a build that exists, cancel a build. `once` is for
19
+ * creating things: a repeated POST /builds is a second build, a second queue slot and a
20
+ * second set of build minutes, which is worse than an error message.
21
+ */
22
+ export type Retry = 'safe' | 'once';
23
+ /**
24
+ * Minimal API client over the platform fetch. Takes an explicit token rather
25
+ * than reading config itself, so `buildatlas login` can validate a token before it
26
+ * is ever written to disk.
27
+ */
28
+ export declare function apiRequest<T>(config: Pick<AtlasConfig, 'apiUrl'>, path: string, init?: RequestInit & {
29
+ token?: string;
30
+ raw?: boolean;
31
+ retry?: Retry;
32
+ }): Promise<T>;
33
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,aAAc,SAAQ,KAAK;IAGtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxD;CACF;AAID;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAYpC;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAO,GACxE,OAAO,CAAC,CAAC,CAAC,CAiEZ"}
package/dist/client.js ADDED
@@ -0,0 +1,115 @@
1
+ export class AtlasApiError extends Error {
2
+ // Explicit fields: Node's type-stripping mode does not support parameter
3
+ // properties, and the built CLI runs under plain Node.
4
+ status;
5
+ code;
6
+ constructor(status, code, message) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ /** Backoff between attempts. Short: a person is watching a prompt, not a daemon log. */
14
+ const BACKOFF_MS = [500, 1500, 3000];
15
+ function retryable(status) {
16
+ // Gateway and transport failures only. A 4xx is the server understanding the request
17
+ // and refusing it, and 500 is the API itself having failed — repeating either changes
18
+ // nothing but the wait.
19
+ return status === 502 || status === 503 || status === 504;
20
+ }
21
+ /**
22
+ * Minimal API client over the platform fetch. Takes an explicit token rather
23
+ * than reading config itself, so `buildatlas login` can validate a token before it
24
+ * is ever written to disk.
25
+ */
26
+ export async function apiRequest(config, path, init = {}) {
27
+ // `raw` returns the body as text. Used for archived logs, which are plain text
28
+ // rather than JSON so they can be piped straight into a pager.
29
+ const { token, raw, retry, ...rest } = init;
30
+ const method = (rest.method ?? 'GET').toUpperCase();
31
+ const mode = retry ?? (method === 'GET' ? 'safe' : 'once');
32
+ let res;
33
+ for (let attempt = 0;; attempt += 1) {
34
+ try {
35
+ res = await fetch(new URL(`/api${path}`, config.apiUrl), {
36
+ ...rest,
37
+ headers: {
38
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
39
+ ...(rest.body ? { 'content-type': 'application/json' } : {}),
40
+ ...rest.headers,
41
+ },
42
+ });
43
+ if (mode === 'safe' && retryable(res.status) && attempt < BACKOFF_MS.length) {
44
+ await delay(BACKOFF_MS[attempt]);
45
+ continue;
46
+ }
47
+ }
48
+ catch (err) {
49
+ // No response at all: the connection failed, so for a `once` request we cannot
50
+ // know whether the server acted on it. Reported rather than repeated.
51
+ if (mode !== 'safe' || attempt >= BACKOFF_MS.length) {
52
+ throw new AtlasApiError(0, 'network_error', `Could not reach ${new URL(config.apiUrl).host} (${err.message}).`);
53
+ }
54
+ await delay(BACKOFF_MS[attempt]);
55
+ continue;
56
+ }
57
+ break;
58
+ }
59
+ if (res.status === 204)
60
+ return undefined;
61
+ if (raw) {
62
+ const text = await res.text();
63
+ if (!res.ok) {
64
+ // An error is still JSON even when the success path is not — unless it came from
65
+ // a proxy, in which case it is an HTML page and JSON.parse would throw a
66
+ // SyntaxError over the top of the real failure.
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(text);
70
+ }
71
+ catch {
72
+ parsed = undefined;
73
+ }
74
+ if (!parsed?.error)
75
+ throw gatewayError(res, config);
76
+ throw new AtlasApiError(res.status, parsed.error.code, parsed.error.message);
77
+ }
78
+ return text;
79
+ }
80
+ const body = await res.json().catch(() => undefined);
81
+ if (!res.ok) {
82
+ const err = body?.error;
83
+ if (!err)
84
+ throw gatewayError(res, config);
85
+ throw new AtlasApiError(res.status, err.code, err.message);
86
+ }
87
+ return body;
88
+ }
89
+ /**
90
+ * An error the API did not write.
91
+ *
92
+ * Every API error is JSON with a code. A reply without one came from something in
93
+ * between — nginx, a load balancer, Cloudflare — and `✖ Gateway Time-out (unknown)`,
94
+ * which is what this used to print, tells a person nothing about who timed out or
95
+ * whether their build was created. Name the host and say the API may never have seen
96
+ * the request, because that is the question they are about to ask.
97
+ */
98
+ function gatewayError(res, config) {
99
+ const host = (() => {
100
+ try {
101
+ return new URL(config.apiUrl).host;
102
+ }
103
+ catch {
104
+ return config.apiUrl;
105
+ }
106
+ })();
107
+ const what = res.status === 413
108
+ ? `${host} refused the request as too large (413). A reverse proxy in front of BuildAtlas ` +
109
+ 'needs client_max_body_size raised — see docs/deployment.md.'
110
+ : `${host} answered ${res.status} ${res.statusText} without reaching BuildAtlas. ` +
111
+ 'A proxy in front of the API timed out or is down, so the request may not have been ' +
112
+ 'processed at all. Check and retry.';
113
+ return new AtlasApiError(res.status, 'gateway_error', what);
114
+ }
115
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../src/client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createServer } from 'node:http';
3
+ import { afterEach, describe, it } from 'node:test';
4
+ import { AtlasApiError, apiRequest } from './client.js';
5
+ /**
6
+ * How the CLI behaves when the network between a developer and their server is the
7
+ * real one.
8
+ *
9
+ * Measured from Addis Ababa against a server in Germany, roughly one request in thirty
10
+ * either never connects or comes back as a 504 from the reverse proxy. With no retries
11
+ * that was a `buildatlas build` dying after the source archive was packed, and a
12
+ * `cancel` printing "Gateway Time-out (unknown)" — which does not say whether the build
13
+ * was cancelled, who timed out, or what to do next.
14
+ */
15
+ let server;
16
+ async function serve(handler) {
17
+ let calls = 0;
18
+ server = createServer((req, res) => {
19
+ calls += 1;
20
+ const { status, body, type } = handler(calls);
21
+ res.writeHead(status, { 'content-type': type ?? 'application/json' });
22
+ res.end(body);
23
+ });
24
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
25
+ const { port } = server.address();
26
+ return { apiUrl: `http://127.0.0.1:${port}`, calls: () => calls };
27
+ }
28
+ afterEach(() => {
29
+ server?.close();
30
+ server = undefined;
31
+ });
32
+ describe('apiRequest', () => {
33
+ it('retries a GET through a gateway failure', async () => {
34
+ const { apiUrl, calls } = await serve((n) => n < 3
35
+ ? { status: 504, body: '<html><head><title>504 Gateway Time-out</title></head></html>', type: 'text/html' }
36
+ : { status: 200, body: JSON.stringify({ builds: [] }) });
37
+ const result = await apiRequest({ apiUrl }, '/projects/x/builds');
38
+ assert.deepEqual(result, { builds: [] });
39
+ assert.equal(calls(), 3);
40
+ });
41
+ it('does not retry a POST that creates something', async () => {
42
+ const { apiUrl, calls } = await serve(() => ({
43
+ status: 504,
44
+ body: '<html><title>504</title></html>',
45
+ type: 'text/html',
46
+ }));
47
+ await assert.rejects(apiRequest({ apiUrl }, '/projects/x/builds', { method: 'POST', body: '{}' }), (err) => err.code === 'gateway_error');
48
+ // One build requested, one build created. A retry here is a second set of build
49
+ // minutes for an app nobody asked to build twice.
50
+ assert.equal(calls(), 1);
51
+ });
52
+ it('retries a POST that only restates a fact', async () => {
53
+ const { apiUrl, calls } = await serve((n) => n < 2 ? { status: 502, body: 'nope', type: 'text/html' } : { status: 200, body: '{"ok":true}' });
54
+ await apiRequest({ apiUrl }, '/builds/b1/enqueue', { method: 'POST', retry: 'safe' });
55
+ assert.equal(calls(), 2);
56
+ });
57
+ it('names the host and says the API may not have seen it', async () => {
58
+ const { apiUrl } = await serve(() => ({
59
+ status: 504,
60
+ body: '<html><title>504 Gateway Time-out</title></html>',
61
+ type: 'text/html',
62
+ }));
63
+ await assert.rejects(apiRequest({ apiUrl }, '/builds/b1/cancel', { method: 'POST' }), (err) => {
64
+ assert.equal(err.code, 'gateway_error');
65
+ assert.match(err.message, /127\.0\.0\.1/);
66
+ assert.match(err.message, /may not have been/);
67
+ // The old message. It told a person nothing.
68
+ assert.doesNotMatch(err.message, /\(unknown\)/);
69
+ return true;
70
+ });
71
+ });
72
+ it('points a 413 at the proxy setting that causes it', async () => {
73
+ const { apiUrl } = await serve(() => ({ status: 413, body: '<html>too big</html>', type: 'text/html' }));
74
+ await assert.rejects(apiRequest({ apiUrl }, '/x', { method: 'POST' }), (err) => {
75
+ assert.match(err.message, /client_max_body_size/);
76
+ return true;
77
+ });
78
+ });
79
+ it('keeps the API’s own error when there is one', async () => {
80
+ const { apiUrl, calls } = await serve(() => ({
81
+ status: 404,
82
+ body: JSON.stringify({ error: { code: 'not_found', message: 'No such build' } }),
83
+ }));
84
+ await assert.rejects(apiRequest({ apiUrl }, '/builds/nope'), (err) => {
85
+ assert.equal(err.code, 'not_found');
86
+ assert.equal(err.message, 'No such build');
87
+ return true;
88
+ });
89
+ // A 404 is an answer, not a blip.
90
+ assert.equal(calls(), 1);
91
+ });
92
+ it('reports an unreachable host rather than a bare "fetch failed"', async () => {
93
+ await assert.rejects(
94
+ // Port 1 on the loopback: nothing listens, and the connection is refused at once.
95
+ apiRequest({ apiUrl: 'http://127.0.0.1:1' }, '/projects', { method: 'POST' }), (err) => {
96
+ assert.equal(err.code, 'network_error');
97
+ assert.match(err.message, /Could not reach 127\.0\.0\.1:1/);
98
+ return true;
99
+ });
100
+ });
101
+ });
102
+ //# sourceMappingURL=client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.js","sourceRoot":"","sources":["../src/client.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerAuthCommands(program: Command): void;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BzC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoE3D"}
@@ -0,0 +1,88 @@
1
+ import pc from 'picocolors';
2
+ import { startBrowserLogin, defaultTokenName } from '../browser-login.js';
3
+ import { AtlasApiError, apiRequest } from '../client.js';
4
+ import { DEFAULT_API_URL, configPath, loadConfig, saveConfig, storedApiUrl } from '../config.js';
5
+ import { withPrompter } from '../prompt.js';
6
+ /**
7
+ * Validate a token against the server, then persist it. Shared by the browser flow, a
8
+ * pasted token and the `--token` CI path — a dead token fails here rather than on the
9
+ * next command, where it is harder to diagnose.
10
+ */
11
+ async function finishLogin(apiUrl, token) {
12
+ try {
13
+ const { user } = await apiRequest({ apiUrl }, '/auth/me', {
14
+ token,
15
+ });
16
+ await saveConfig({ apiUrl, token, email: user.email });
17
+ console.log(pc.green('✔'), `Logged in as ${pc.bold(user.email)} at ${pc.dim(apiUrl)}`);
18
+ console.log(pc.dim(` token stored in ${configPath()} (mode 0600)`));
19
+ }
20
+ catch (err) {
21
+ if (err instanceof AtlasApiError) {
22
+ console.error(pc.red('✖'), `Token rejected: ${err.message}`);
23
+ }
24
+ else {
25
+ console.error(pc.red('✖'), `Could not reach ${apiUrl}: ${err.message}`);
26
+ }
27
+ process.exitCode = 1;
28
+ }
29
+ }
30
+ export function registerAuthCommands(program) {
31
+ program
32
+ .command('login')
33
+ .description('Log in — opens the browser, mints a token, and captures it automatically')
34
+ .option('--api <url>', 'API base URL (default: your instance, remembered after first login)')
35
+ .option('--name <name>', 'Name for the token that gets created (default: this machine)')
36
+ .option('--create', 'Prompt for a token name instead of defaulting to this machine')
37
+ .option('--token <token>', 'Use this token directly and skip the browser (for CI)')
38
+ .action(async (opts) => {
39
+ // The server: an explicit flag, then a previous login, then the instance this CLI
40
+ // ships pointed at. No prompt in the common case.
41
+ const apiUrl = (opts.api ?? (await storedApiUrl()) ?? DEFAULT_API_URL).replace(/\/+$/, '');
42
+ // CI path: a token was supplied, so there is nothing to open.
43
+ if (opts.token) {
44
+ await finishLogin(apiUrl, opts.token);
45
+ return;
46
+ }
47
+ await withPrompter(async (prompter) => {
48
+ const chosen = opts.name ?? (opts.create ? (await prompter.ask('Token name: ')).trim() : '');
49
+ const tokenName = chosen || defaultTokenName();
50
+ const login = startBrowserLogin(apiUrl, tokenName);
51
+ // Race the browser callback against a paste, so a box that cannot open a browser
52
+ // (SSH, locked down) still has a way through. An empty line is ignored — it just
53
+ // means "keep waiting for the browser".
54
+ const paste = prompter
55
+ .ask(pc.dim(' …or paste a token here and press enter: '))
56
+ .then((v) => v.trim())
57
+ .catch(() => '');
58
+ const winner = await Promise.race([
59
+ login.done.then((t) => ({ src: 'browser', t })),
60
+ paste.then((t) => ({ src: 'paste', t })),
61
+ ]);
62
+ let token;
63
+ if (winner.src === 'paste' && winner.t) {
64
+ login.abort();
65
+ token = winner.t;
66
+ }
67
+ else {
68
+ token = winner.src === 'browser' ? winner.t : await login.done;
69
+ }
70
+ if (!token) {
71
+ console.error(pc.red('✖'), 'No token received (the browser login timed out).');
72
+ console.error(pc.dim(' Retry, or create one at'), pc.cyan(`${apiUrl}/settings`), pc.dim('and paste it.'));
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ await finishLogin(apiUrl, token);
77
+ });
78
+ });
79
+ program
80
+ .command('logout')
81
+ .description('Remove the stored token')
82
+ .action(async () => {
83
+ const config = await loadConfig();
84
+ await saveConfig({ apiUrl: config.apiUrl });
85
+ console.log(pc.green('✔'), 'Token removed');
86
+ });
87
+ }
88
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ export declare function assertProfileExists(projectDir: string, profile: string): Promise<void>;
3
+ export declare function registerBuildCommand(program: Command): void;
4
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgEzC,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5F;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiD3D"}