@theholocron/cli 2.0.0-alpha.45 → 2.0.0-alpha.46

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 (3) hide show
  1. package/README.md +2 -4
  2. package/dist/cli.mjs +83 -133
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -107,8 +107,6 @@ export default acmeConfig;
107
107
 
108
108
  ## Status
109
109
 
110
- **`v2.0.0-alpha.0`** — published on npm under the `alpha` dist-tag.
111
- [Release notes](https://github.com/theholocron/holocron/releases/tag/v2.0.0-alpha.0).
112
- Design in
110
+ Published on npm under the `alpha` dist-tag. APIs may still shift before
111
+ stable v2.0.0. Design in
113
112
  [`.notes/archive/tech-architecture.spec.md`](../../.notes/archive/tech-architecture.spec.md).
114
- APIs may still shift before stable v2.0.0.
package/dist/cli.mjs CHANGED
@@ -3,9 +3,10 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
3
3
  import path, { basename, dirname, join } from "node:path";
4
4
  import yargs from "yargs";
5
5
  import { hideBin } from "yargs/helpers";
6
- import { ProviderApiError } from "@theholocron/http-client";
6
+ import { ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
7
7
  import { Entry, findCredentials } from "@napi-rs/keyring";
8
8
  import { createHash } from "node:crypto";
9
+ import { createGitHubClient } from "@theholocron/github-client";
9
10
  import { spawnSync } from "node:child_process";
10
11
  import { access, readFile, stat } from "node:fs/promises";
11
12
  import { pathToFileURL } from "node:url";
@@ -1233,8 +1234,8 @@ jobs:
1233
1234
  env.GPG_KEY_SET == 'true'
1234
1235
  with:
1235
1236
  branch: \${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
1236
- commit_message: "chore: fix linting issues"
1237
- commit_options: "--no-verify --signoff"
1237
+ commit_message: "chore: fix linting issues\\n\\nSigned-off-by: super-linter <super-linter@super-linter.dev>"
1238
+ commit_options: "--no-verify"
1238
1239
  commit_user_name: super-linter
1239
1240
  commit_user_email: super-linter@super-linter.dev
1240
1241
  `,
@@ -2005,7 +2006,6 @@ function generateThinCallerContent(name, withOverrides) {
2005
2006
  //#endregion
2006
2007
  //#region src/commands/sync-github.ts
2007
2008
  const DEFAULT_REPO = "theholocron/.github";
2008
- const API_BASE = "https://api.github.com";
2009
2009
  /**
2010
2010
  * Extracts the `project.workflows` array from a `holocron.config.ts` source string.
2011
2011
  * Handles both plain string entries and `{ name, with }` object entries.
@@ -2111,16 +2111,12 @@ function gitBlobSha(content) {
2111
2111
  async function runSyncGithub(input) {
2112
2112
  const print = input.print ?? ((line) => console.log(line));
2113
2113
  const repo = input.repo ?? DEFAULT_REPO;
2114
- const [owner, repoName] = repo.split("/");
2115
2114
  const { token, dryRun = false, branch, createPr = false } = input;
2116
2115
  const message = input.message ?? `chore: sync from theholocron/holocron`;
2117
- const fetchFn = input.fetch ?? globalThis.fetch;
2118
- const headers = {
2119
- Authorization: `Bearer ${token}`,
2120
- Accept: "application/vnd.github+json",
2121
- "Content-Type": "application/json",
2122
- "X-GitHub-Api-Version": "2022-11-28"
2123
- };
2116
+ const client = createGitHubClient({
2117
+ token,
2118
+ fetch: input.fetch
2119
+ });
2124
2120
  print(`holocron sync-github${dryRun ? " (dry-run)" : ""}`);
2125
2121
  print(` repo: ${repo}`);
2126
2122
  if (branch) print(` branch: ${branch}`);
@@ -2142,26 +2138,31 @@ async function runSyncGithub(input) {
2142
2138
  }
2143
2139
  let targetBranch = branch;
2144
2140
  let defaultBranch;
2145
- if (!targetBranch || createPr) {
2146
- const repoRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}`, { headers });
2147
- if (!repoRes.ok) {
2148
- const msg = "failed to fetch repo metadata";
2149
- print(` ✗ ${msg}`);
2150
- return {
2151
- status: "fail",
2152
- created: 0,
2153
- updated: 0,
2154
- unchanged: 0,
2155
- message: msg
2156
- };
2157
- }
2158
- defaultBranch = (await repoRes.json()).default_branch;
2141
+ if (!targetBranch || createPr) try {
2142
+ defaultBranch = (await client.repos.getRepo(repo)).default_branch;
2159
2143
  if (!targetBranch) targetBranch = defaultBranch;
2144
+ } catch {
2145
+ const msg = "failed to fetch repo metadata";
2146
+ print(` ✗ ${msg}`);
2147
+ return {
2148
+ status: "fail",
2149
+ created: 0,
2150
+ updated: 0,
2151
+ unchanged: 0,
2152
+ message: msg
2153
+ };
2160
2154
  }
2161
2155
  const baseBranch = createPr && defaultBranch ? defaultBranch : targetBranch;
2162
- const refRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/ref/heads/${baseBranch}`, { headers });
2163
- if (!refRes.ok) {
2164
- const msg = `Branch ${baseBranch} not found`;
2156
+ let headSha;
2157
+ let baseTreeSha;
2158
+ let existingBlobs;
2159
+ try {
2160
+ headSha = (await client.git.getRef(repo, baseBranch)).object.sha;
2161
+ baseTreeSha = (await client.git.getCommit(repo, headSha)).tree.sha;
2162
+ const treeData = await client.git.getTree(repo, baseTreeSha, true);
2163
+ existingBlobs = new Map(treeData.tree.filter((i) => i.type === "blob").map((i) => [i.path, i.sha]));
2164
+ } catch (err) {
2165
+ const msg = err instanceof Error ? err.message : `Branch ${baseBranch} not found`;
2165
2166
  print(` ✗ ${msg}`);
2166
2167
  return {
2167
2168
  status: "fail",
@@ -2171,24 +2172,19 @@ async function runSyncGithub(input) {
2171
2172
  message: msg
2172
2173
  };
2173
2174
  }
2174
- const { object: { sha: headSha } } = await refRes.json();
2175
- const { tree: { sha: baseTreeSha } } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/commits/${headSha}`, { headers })).json();
2176
- const { tree: existingTree } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees/${baseTreeSha}?recursive=1`, { headers })).json();
2177
- const existingBlobs = new Map(existingTree.filter((i) => i.type === "blob").map((i) => [i.path, i.sha]));
2178
2175
  let allowedWorkflows;
2179
2176
  let withOverrides;
2180
2177
  if (repo !== DEFAULT_REPO) try {
2181
2178
  let entries = [];
2182
- const jsonRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
2183
- if (jsonRes.ok) {
2184
- const data = await jsonRes.json();
2179
+ try {
2180
+ const data = await client.git.getContents(repo, "holocron.config.json");
2185
2181
  entries = (JSON.parse(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? []).map((w) => typeof w === "string" ? { name: w } : w);
2186
- } else {
2187
- const tsRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.ts`, { headers });
2188
- if (tsRes.ok) {
2189
- const data = await tsRes.json();
2182
+ } catch (err) {
2183
+ if (!(err instanceof ProviderApiError) || err.status !== 404) throw err;
2184
+ try {
2185
+ const data = await client.git.getContents(repo, "holocron.config.ts");
2190
2186
  entries = parseWorkflowsFromTs(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"));
2191
- }
2187
+ } catch {}
2192
2188
  }
2193
2189
  if (entries.length > 0) {
2194
2190
  allowedWorkflows = new Set(entries.map((e) => e.name));
@@ -2226,45 +2222,30 @@ async function runSyncGithub(input) {
2226
2222
  unchanged
2227
2223
  };
2228
2224
  const treeEntries = [];
2229
- for (const file of changedFiles) {
2230
- const blobRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/blobs`, {
2231
- method: "POST",
2232
- headers,
2233
- body: JSON.stringify({
2234
- content: file.content,
2235
- encoding: "utf-8"
2236
- })
2237
- });
2238
- if (!blobRes.ok) {
2239
- const err = await blobRes.json();
2240
- const msg = `failed to create blob for ${file.path}: ${err.message ?? blobRes.status}`;
2241
- print(` ✗ ${msg}`);
2242
- return {
2243
- status: "fail",
2244
- created,
2245
- updated,
2246
- unchanged,
2247
- message: msg
2248
- };
2249
- }
2250
- const { sha: blobSha } = await blobRes.json();
2225
+ for (const file of changedFiles) try {
2226
+ const blob = await client.git.createBlob(repo, file.content);
2251
2227
  treeEntries.push({
2252
2228
  path: file.path,
2253
2229
  mode: "100644",
2254
2230
  type: "blob",
2255
- sha: blobSha
2231
+ sha: blob.sha
2256
2232
  });
2233
+ } catch (err) {
2234
+ const msg = `failed to create blob for ${file.path}: ${err instanceof Error ? err.message : String(err)}`;
2235
+ print(` ✗ ${msg}`);
2236
+ return {
2237
+ status: "fail",
2238
+ created,
2239
+ updated,
2240
+ unchanged,
2241
+ message: msg
2242
+ };
2257
2243
  }
2258
- const newTreeRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees`, {
2259
- method: "POST",
2260
- headers,
2261
- body: JSON.stringify({
2262
- base_tree: baseTreeSha,
2263
- tree: treeEntries
2264
- })
2265
- });
2266
- if (!newTreeRes.ok) {
2267
- const msg = `failed to create tree: ${(await newTreeRes.json()).message ?? newTreeRes.status}`;
2244
+ let newTreeSha;
2245
+ try {
2246
+ newTreeSha = (await client.git.createTree(repo, treeEntries, baseTreeSha)).sha;
2247
+ } catch (err) {
2248
+ const msg = `failed to create tree: ${err instanceof Error ? err.message : String(err)}`;
2268
2249
  print(` ✗ ${msg}`);
2269
2250
  return {
2270
2251
  status: "fail",
@@ -2274,18 +2255,11 @@ async function runSyncGithub(input) {
2274
2255
  message: msg
2275
2256
  };
2276
2257
  }
2277
- const { sha: newTreeSha } = await newTreeRes.json();
2278
- const newCommitRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/commits`, {
2279
- method: "POST",
2280
- headers,
2281
- body: JSON.stringify({
2282
- message,
2283
- tree: newTreeSha,
2284
- parents: [headSha]
2285
- })
2286
- });
2287
- if (!newCommitRes.ok) {
2288
- const msg = `failed to create commit: ${(await newCommitRes.json()).message ?? newCommitRes.status}`;
2258
+ let newCommitSha;
2259
+ try {
2260
+ newCommitSha = (await client.git.createCommit(repo, message, newTreeSha, [headSha])).sha;
2261
+ } catch (err) {
2262
+ const msg = `failed to create commit: ${err instanceof Error ? err.message : String(err)}`;
2289
2263
  print(` ✗ ${msg}`);
2290
2264
  return {
2291
2265
  status: "fail",
@@ -2295,32 +2269,16 @@ async function runSyncGithub(input) {
2295
2269
  message: msg
2296
2270
  };
2297
2271
  }
2298
- const { sha: newCommitSha } = await newCommitRes.json();
2299
- let refUpdateRes;
2300
- if (createPr && branch) {
2301
- refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs`, {
2302
- method: "POST",
2303
- headers,
2304
- body: JSON.stringify({
2305
- ref: `refs/heads/${branch}`,
2306
- sha: newCommitSha
2307
- })
2308
- });
2309
- if (refUpdateRes.status === 422) refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs/heads/${branch}`, {
2310
- method: "PATCH",
2311
- headers,
2312
- body: JSON.stringify({
2313
- sha: newCommitSha,
2314
- force: true
2315
- })
2316
- });
2317
- } else refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs/heads/${targetBranch}`, {
2318
- method: "PATCH",
2319
- headers,
2320
- body: JSON.stringify({ sha: newCommitSha })
2321
- });
2322
- if (!refUpdateRes.ok) {
2323
- const msg = `failed to update ref: ${(await refUpdateRes.json()).message ?? refUpdateRes.status}`;
2272
+ try {
2273
+ if (createPr && branch) try {
2274
+ await client.git.createRef(repo, `refs/heads/${branch}`, newCommitSha);
2275
+ } catch (err) {
2276
+ if (!(err instanceof ProviderApiError) || err.status !== 422) throw err;
2277
+ await client.git.updateRef(repo, `heads/${branch}`, newCommitSha, true);
2278
+ }
2279
+ else await client.git.updateRef(repo, `heads/${targetBranch}`, newCommitSha);
2280
+ } catch (err) {
2281
+ const msg = `failed to update ref: ${err instanceof Error ? err.message : String(err)}`;
2324
2282
  print(` ✗ ${msg}`);
2325
2283
  return {
2326
2284
  status: "fail",
@@ -2331,25 +2289,17 @@ async function runSyncGithub(input) {
2331
2289
  };
2332
2290
  }
2333
2291
  let prUrl;
2334
- if (branch && createPr && !dryRun) {
2335
- const prRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/pulls`, {
2336
- method: "POST",
2337
- headers,
2338
- body: JSON.stringify({
2339
- title: message.split("\n")[0],
2340
- head: branch,
2341
- base: "main",
2342
- body: "Auto-generated by `holocron sync-github`. Review and merge to apply template updates."
2343
- })
2344
- });
2345
- if (prRes.ok) {
2346
- prUrl = (await prRes.json()).html_url;
2347
- print(` → PR opened: ${prUrl}`);
2348
- } else {
2349
- const err = await prRes.json();
2350
- if (err.errors?.some((e) => e.message.includes("already exists"))) print(` → PR already open for ${branch} — branch updated, ready to merge`);
2351
- else print(` ⚠ PR creation failed: ${err.message ?? prRes.status}`);
2352
- }
2292
+ if (branch && createPr && !dryRun) try {
2293
+ prUrl = (await client.git.createPull(repo, {
2294
+ title: message.split("\n")[0],
2295
+ head: branch,
2296
+ base: "main",
2297
+ body: "Auto-generated by `holocron sync-github`. Review and merge to apply template updates."
2298
+ })).html_url;
2299
+ print(` → PR opened: ${prUrl}`);
2300
+ } catch (err) {
2301
+ if (err instanceof ProviderApiError && err.status === 422 && String(err.details).includes("already exists")) print(` → PR already open for ${branch} — branch updated, ready to merge`);
2302
+ else print(` ⚠ PR creation failed: ${err instanceof Error ? err.message : String(err)}`);
2353
2303
  }
2354
2304
  return {
2355
2305
  status: "ok",
@@ -4019,7 +3969,7 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
4019
3969
  message: "created"
4020
3970
  };
4021
3971
  } catch (err) {
4022
- if (!(err instanceof ProviderApiError) || err.status !== 403) return {
3972
+ if (!(err instanceof ProviderApiError$1) || err.status !== 403) return {
4023
3973
  capability: "source",
4024
3974
  step,
4025
3975
  status: "fail",
@@ -4036,7 +3986,7 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
4036
3986
  message: `classic protection on ${repo.defaultBranch}`
4037
3987
  };
4038
3988
  } catch (err) {
4039
- if (err instanceof ProviderApiError && err.status === 403) return {
3989
+ if (err instanceof ProviderApiError$1 && err.status === 403) return {
4040
3990
  capability: "source",
4041
3991
  step,
4042
3992
  status: "skip",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.45",
3
+ "version": "2.0.0-alpha.46",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -34,7 +34,8 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@napi-rs/keyring": "^1.3.0",
37
- "@theholocron/http-client": "^0.1.0",
37
+ "@theholocron/github-client": "^0.3.2",
38
+ "@theholocron/http-client": "^0.3.2",
38
39
  "tsx": "^4.22.4",
39
40
  "yargs": "^18.0.0"
40
41
  },