@rathnasgala/cli 0.0.17 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/cli",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { describeHttpFailure } from './http-failure.js';
2
3
 
3
4
  const API_VERSION = '2026-03-10';
4
5
 
@@ -11,7 +12,7 @@ export async function verifyEmptyRepository({ owner, repository, accessToken, fe
11
12
  const response = await fetchImpl(repositoryUrl, {
12
13
  headers
13
14
  });
14
- if (!response.ok) throw new Error(`GitHub repository lookup failed with HTTP ${response.status}`);
15
+ if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub repository lookup'));
15
16
  const payload = await response.json();
16
17
  if (payload.full_name?.toLowerCase() !== `${owner}/${repository}`.toLowerCase()) {
17
18
  throw new TypeError('GitHub returned an unexpected repository');
@@ -21,7 +22,7 @@ export async function verifyEmptyRepository({ owner, repository, accessToken, fe
21
22
  ...headers
22
23
  }
23
24
  });
24
- if (!branchesResponse.ok) throw new Error(`GitHub branch lookup failed with HTTP ${branchesResponse.status}`);
25
+ if (!branchesResponse.ok) throw new Error(await describeHttpFailure(branchesResponse, 'GitHub branch lookup'));
25
26
  const branches = await branchesResponse.json();
26
27
  if (payload.size !== 0 || !Array.isArray(branches) || branches.length !== 0) {
27
28
  throw new Error('Existing repository is not empty; explicit non-empty adoption is not implemented');
@@ -1,3 +1,4 @@
1
+ import { describeHttpFailure } from './http-failure.js';
1
2
  const GITHUB_API_VERSION = '2026-03-10';
2
3
 
3
4
  /**
@@ -19,7 +20,7 @@ export async function resolveGithubLogin({ accessToken, fetchImpl = fetch }) {
19
20
  'x-github-api-version': GITHUB_API_VERSION
20
21
  }
21
22
  });
22
- if (!response.ok) throw new Error(`GitHub account lookup failed with HTTP ${response.status}`);
23
+ if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub account lookup'));
23
24
  const payload = await response.json();
24
25
  const login = payload?.login;
25
26
  // The same shape `scaffold` demands of `--owner`. Refusing here beats a confusing failure four
@@ -1,3 +1,4 @@
1
+ import { describeHttpFailure } from './http-failure.js';
1
2
  const GITHUB_API_VERSION = '2026-03-10';
2
3
  const SEGMENT = /^[A-Za-z0-9_.-]+$/;
3
4
  const SHA = /^[0-9a-f]{40}$/;
@@ -19,7 +20,7 @@ function headers(accessToken) {
19
20
  }
20
21
 
21
22
  async function json(response, operation) {
22
- if (!response.ok) throw new Error(`GitHub ${operation} failed with HTTP ${response.status}`);
23
+ if (!response.ok) throw new Error(await describeHttpFailure(response, `GitHub ${operation}`));
23
24
  return response.json();
24
25
  }
25
26
 
@@ -86,7 +87,7 @@ export async function provisionGithubPages({
86
87
  return Object.freeze({ created: false, url: configuration.html_url, runUrl: run.html_url });
87
88
  }
88
89
  if (current.status !== 404) {
89
- throw new Error(`GitHub Pages configuration request failed with HTTP ${current.status}`);
90
+ throw new Error(await describeHttpFailure(current, 'GitHub Pages configuration request'));
90
91
  }
91
92
  const created = await fetchImpl(`${repositoryUrl}/pages`, {
92
93
  method: 'POST', headers: requestHeaders,
@@ -1,3 +1,4 @@
1
+ import { describeHttpFailure } from './http-failure.js';
1
2
  const GITHUB_API_VERSION = '2026-03-10';
2
3
  const OWNER_OR_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
3
4
  const VARIABLE_NAME = /^[A-Z_][A-Z0-9_]*$/;
@@ -42,7 +43,7 @@ export async function installRepositoryVariable({
42
43
  });
43
44
  if (update.ok) return;
44
45
  if (update.status !== 404) {
45
- throw new Error(`GitHub repository variable update failed with HTTP ${update.status}`);
46
+ throw new Error(await describeHttpFailure(update, 'GitHub repository variable update'));
46
47
  }
47
48
  const create = await fetchImpl(baseUrl, {
48
49
  method: 'POST',
@@ -50,6 +51,6 @@ export async function installRepositoryVariable({
50
51
  body: JSON.stringify({ name: normalizedName, value })
51
52
  });
52
53
  if (!create.ok) {
53
- throw new Error(`GitHub repository variable creation failed with HTTP ${create.status}`);
54
+ throw new Error(await describeHttpFailure(create, 'GitHub repository variable creation'));
54
55
  }
55
56
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
+ import { describeHttpFailure } from './http-failure.js';
3
4
 
4
5
  const GITHUB_API_VERSION = '2026-03-10';
5
6
  const REPOSITORY_IDENTITY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -57,7 +58,7 @@ export async function generateRepositoryFromTemplate({
57
58
  }
58
59
  );
59
60
  if (response.status !== 201) {
60
- throw new Error(`GitHub template generation failed with HTTP ${response.status}`);
61
+ throw new Error(await describeHttpFailure(response, 'GitHub template generation'));
61
62
  }
62
63
  const payload = await response.json();
63
64
  if (payload == null || Array.isArray(payload) || typeof payload !== 'object') {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What the server actually said, instead of only what it scored.
3
+ *
4
+ * Every failure in this CLI reported `failed with HTTP 403` and discarded the response body. GitHub
5
+ * puts the reason there and nowhere else — an organisation's OAuth App restrictions, a missing
6
+ * scope, a rename, a rate limit all arrive as 403 with a sentence explaining which — so the one
7
+ * fact worth having was the one being thrown away. Diagnosing anything meant guessing between
8
+ * causes the server had already distinguished.
9
+ *
10
+ * Only the response is read. Request bodies, tokens and secrets never pass through here.
11
+ */
12
+ const MAX_DETAIL = 400;
13
+
14
+ export async function describeHttpFailure(response, action) {
15
+ const status = response?.status ?? 0;
16
+ let detail = '';
17
+ try {
18
+ // Cloned so a caller that also reads the body still can; a response whose body is already
19
+ // consumed simply yields no detail rather than a second failure on top of the first.
20
+ const text = await (typeof response.clone === 'function' ? response.clone() : response).text();
21
+ detail = extract(text);
22
+ } catch {
23
+ detail = '';
24
+ }
25
+ return `${action} failed with HTTP ${status}${detail === '' ? '' : `: ${detail}`}`;
26
+ }
27
+
28
+ function extract(text) {
29
+ if (typeof text !== 'string' || text.trim() === '') return '';
30
+ let payload;
31
+ try {
32
+ payload = JSON.parse(text);
33
+ } catch {
34
+ return truncate(text);
35
+ }
36
+ if (payload == null || typeof payload !== 'object') return truncate(text);
37
+ const parts = [];
38
+ if (typeof payload.message === 'string' && payload.message.trim() !== '') parts.push(payload.message.trim());
39
+ // GitHub's `errors` array carries the specific field or reason behind a generic message.
40
+ if (Array.isArray(payload.errors)) {
41
+ for (const error of payload.errors) {
42
+ const reason = typeof error === 'string' ? error : error?.message ?? error?.code;
43
+ if (typeof reason === 'string' && reason.trim() !== '') parts.push(reason.trim());
44
+ }
45
+ }
46
+ if (typeof payload.error_description === 'string') parts.push(payload.error_description.trim());
47
+ if (typeof payload.code === 'string' && parts.length === 0) parts.push(payload.code);
48
+ if (typeof payload.documentation_url === 'string') parts.push(`See ${payload.documentation_url}`);
49
+ return truncate(parts.length === 0 ? text : parts.join(' — '));
50
+ }
51
+
52
+ function truncate(value) {
53
+ const flattened = value.replace(/\s+/g, ' ').trim();
54
+ return flattened.length > MAX_DETAIL ? `${flattened.slice(0, MAX_DETAIL)}…` : flattened;
55
+ }
@@ -1,3 +1,4 @@
1
+ import { describeHttpFailure } from './http-failure.js';
1
2
  const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
2
3
  const REPOSITORY_PART = /^[A-Za-z0-9_.-]+$/;
3
4
  const IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{16,128}$/;
@@ -34,7 +35,7 @@ async function authorizeGitHub({ apiBaseUrl, galaAccessToken, githubAccessToken,
34
35
  throw new Error('GitHub or Gala authentication expired; run `gala auth` again');
35
36
  }
36
37
  if (response.status !== 200) {
37
- throw new Error(`GitHub repository authorization failed with HTTP ${response.status}`);
38
+ throw new Error(await describeHttpFailure(response, 'GitHub repository authorization'));
38
39
  }
39
40
  const payload = await response.json();
40
41
  return required(payload?.authorization, 'GitHub authorization', /^[A-Za-z0-9_-]{43}$/);
@@ -103,7 +104,7 @@ export async function registerSite({
103
104
  }
104
105
  throw new Error('Site registration conflicts with existing protected state; use the recovery command');
105
106
  }
106
- if (response.status !== 201) throw new Error(`Gala site registration failed with HTTP ${response.status}`);
107
+ if (response.status !== 201) throw new Error(await describeHttpFailure(response, 'Gala site registration'));
107
108
  const payload = await response.json();
108
109
  if (!ULID.test(payload?.siteId) || typeof payload.siteSecret !== 'string' || payload.siteSecret === '') {
109
110
  throw new TypeError('Gala site registration response is invalid');