@theholocron/cli 2.0.0-alpha.44 → 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.
package/README.md CHANGED
@@ -101,13 +101,12 @@ export default acmeConfig;
101
101
  - `src/loader.ts` — `PluginLoader` — dynamic-imports plugins, resolves
102
102
  capability config packages, builds the capability registry
103
103
  - `src/cli.ts` — yargs entry, dispatches subcommands
104
- - `src/commands/` — `setup`, `doctor`, `deploy`, `secret set`,
105
- `secrets sync`, `npm publish-initial`
104
+ - `src/commands/` — `setup`, `sync`, `doctor`, `deploy`, `secret set`,
105
+ `secrets sync`, `npm publish-initial`, `sync-github`, `upgrade node`,
106
+ `plugin create`, `auth`
106
107
 
107
108
  ## Status
108
109
 
109
- **`v2.0.0-alpha.0`** — published on npm under the `alpha` dist-tag.
110
- [Release notes](https://github.com/theholocron/holocron/releases/tag/v2.0.0-alpha.0).
111
- Design in
112
- [`.notes/tech-architecture.spec.md`](../../.notes/archive/tech-architecture.spec.md).
113
- APIs may still shift before stable v2.0.0.
110
+ Published on npm under the `alpha` dist-tag. APIs may still shift before
111
+ stable v2.0.0. Design in
112
+ [`.notes/archive/tech-architecture.spec.md`](../../.notes/archive/tech-architecture.spec.md).
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";
@@ -464,8 +465,7 @@ var PluginLoader = class {
464
465
  */
465
466
  projectDefaults() {
466
467
  const defaults = {};
467
- const repo = this.config.project.repo;
468
- if (repo) defaults.repo = typeof repo === "string" ? repo : repo.name;
468
+ if (this.config.project.repo) defaults.repo = this.config.project.repo.name;
469
469
  return defaults;
470
470
  }
471
471
  };
@@ -1059,7 +1059,7 @@ jobs:
1059
1059
  id: metadata
1060
1060
 
1061
1061
  - run: gh pr merge --auto --squash "$PR_URL"
1062
- # --squash is intentional: repoPolicy sets allow_merge_commit: false,
1062
+ # --squash is intentional: repo protection sets allow_merge_commit: false,
1063
1063
  # so --merge would fail on any repo using the standard preset.
1064
1064
  name: Enable auto-merge for Dependabot PRs
1065
1065
  if: steps.metadata.outputs.update-type == 'version-update:semver-patch'
@@ -1234,8 +1234,8 @@ jobs:
1234
1234
  env.GPG_KEY_SET == 'true'
1235
1235
  with:
1236
1236
  branch: \${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
1237
- commit_message: "chore: fix linting issues"
1238
- 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"
1239
1239
  commit_user_name: super-linter
1240
1240
  commit_user_email: super-linter@super-linter.dev
1241
1241
  `,
@@ -2006,7 +2006,6 @@ function generateThinCallerContent(name, withOverrides) {
2006
2006
  //#endregion
2007
2007
  //#region src/commands/sync-github.ts
2008
2008
  const DEFAULT_REPO = "theholocron/.github";
2009
- const API_BASE = "https://api.github.com";
2010
2009
  /**
2011
2010
  * Extracts the `project.workflows` array from a `holocron.config.ts` source string.
2012
2011
  * Handles both plain string entries and `{ name, with }` object entries.
@@ -2112,16 +2111,12 @@ function gitBlobSha(content) {
2112
2111
  async function runSyncGithub(input) {
2113
2112
  const print = input.print ?? ((line) => console.log(line));
2114
2113
  const repo = input.repo ?? DEFAULT_REPO;
2115
- const [owner, repoName] = repo.split("/");
2116
2114
  const { token, dryRun = false, branch, createPr = false } = input;
2117
2115
  const message = input.message ?? `chore: sync from theholocron/holocron`;
2118
- const fetchFn = input.fetch ?? globalThis.fetch;
2119
- const headers = {
2120
- Authorization: `Bearer ${token}`,
2121
- Accept: "application/vnd.github+json",
2122
- "Content-Type": "application/json",
2123
- "X-GitHub-Api-Version": "2022-11-28"
2124
- };
2116
+ const client = createGitHubClient({
2117
+ token,
2118
+ fetch: input.fetch
2119
+ });
2125
2120
  print(`holocron sync-github${dryRun ? " (dry-run)" : ""}`);
2126
2121
  print(` repo: ${repo}`);
2127
2122
  if (branch) print(` branch: ${branch}`);
@@ -2143,26 +2138,31 @@ async function runSyncGithub(input) {
2143
2138
  }
2144
2139
  let targetBranch = branch;
2145
2140
  let defaultBranch;
2146
- if (!targetBranch || createPr) {
2147
- const repoRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}`, { headers });
2148
- if (!repoRes.ok) {
2149
- const msg = "failed to fetch repo metadata";
2150
- print(` ✗ ${msg}`);
2151
- return {
2152
- status: "fail",
2153
- created: 0,
2154
- updated: 0,
2155
- unchanged: 0,
2156
- message: msg
2157
- };
2158
- }
2159
- defaultBranch = (await repoRes.json()).default_branch;
2141
+ if (!targetBranch || createPr) try {
2142
+ defaultBranch = (await client.repos.getRepo(repo)).default_branch;
2160
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
+ };
2161
2154
  }
2162
2155
  const baseBranch = createPr && defaultBranch ? defaultBranch : targetBranch;
2163
- const refRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/ref/heads/${baseBranch}`, { headers });
2164
- if (!refRes.ok) {
2165
- 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`;
2166
2166
  print(` ✗ ${msg}`);
2167
2167
  return {
2168
2168
  status: "fail",
@@ -2172,24 +2172,19 @@ async function runSyncGithub(input) {
2172
2172
  message: msg
2173
2173
  };
2174
2174
  }
2175
- const { object: { sha: headSha } } = await refRes.json();
2176
- const { tree: { sha: baseTreeSha } } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/commits/${headSha}`, { headers })).json();
2177
- const { tree: existingTree } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees/${baseTreeSha}?recursive=1`, { headers })).json();
2178
- const existingBlobs = new Map(existingTree.filter((i) => i.type === "blob").map((i) => [i.path, i.sha]));
2179
2175
  let allowedWorkflows;
2180
2176
  let withOverrides;
2181
2177
  if (repo !== DEFAULT_REPO) try {
2182
2178
  let entries = [];
2183
- const jsonRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
2184
- if (jsonRes.ok) {
2185
- const data = await jsonRes.json();
2179
+ try {
2180
+ const data = await client.git.getContents(repo, "holocron.config.json");
2186
2181
  entries = (JSON.parse(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? []).map((w) => typeof w === "string" ? { name: w } : w);
2187
- } else {
2188
- const tsRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.ts`, { headers });
2189
- if (tsRes.ok) {
2190
- 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");
2191
2186
  entries = parseWorkflowsFromTs(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"));
2192
- }
2187
+ } catch {}
2193
2188
  }
2194
2189
  if (entries.length > 0) {
2195
2190
  allowedWorkflows = new Set(entries.map((e) => e.name));
@@ -2227,45 +2222,30 @@ async function runSyncGithub(input) {
2227
2222
  unchanged
2228
2223
  };
2229
2224
  const treeEntries = [];
2230
- for (const file of changedFiles) {
2231
- const blobRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/blobs`, {
2232
- method: "POST",
2233
- headers,
2234
- body: JSON.stringify({
2235
- content: file.content,
2236
- encoding: "utf-8"
2237
- })
2238
- });
2239
- if (!blobRes.ok) {
2240
- const err = await blobRes.json();
2241
- const msg = `failed to create blob for ${file.path}: ${err.message ?? blobRes.status}`;
2242
- print(` ✗ ${msg}`);
2243
- return {
2244
- status: "fail",
2245
- created,
2246
- updated,
2247
- unchanged,
2248
- message: msg
2249
- };
2250
- }
2251
- const { sha: blobSha } = await blobRes.json();
2225
+ for (const file of changedFiles) try {
2226
+ const blob = await client.git.createBlob(repo, file.content);
2252
2227
  treeEntries.push({
2253
2228
  path: file.path,
2254
2229
  mode: "100644",
2255
2230
  type: "blob",
2256
- sha: blobSha
2231
+ sha: blob.sha
2257
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
+ };
2258
2243
  }
2259
- const newTreeRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees`, {
2260
- method: "POST",
2261
- headers,
2262
- body: JSON.stringify({
2263
- base_tree: baseTreeSha,
2264
- tree: treeEntries
2265
- })
2266
- });
2267
- if (!newTreeRes.ok) {
2268
- 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)}`;
2269
2249
  print(` ✗ ${msg}`);
2270
2250
  return {
2271
2251
  status: "fail",
@@ -2275,18 +2255,11 @@ async function runSyncGithub(input) {
2275
2255
  message: msg
2276
2256
  };
2277
2257
  }
2278
- const { sha: newTreeSha } = await newTreeRes.json();
2279
- const newCommitRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/commits`, {
2280
- method: "POST",
2281
- headers,
2282
- body: JSON.stringify({
2283
- message,
2284
- tree: newTreeSha,
2285
- parents: [headSha]
2286
- })
2287
- });
2288
- if (!newCommitRes.ok) {
2289
- 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)}`;
2290
2263
  print(` ✗ ${msg}`);
2291
2264
  return {
2292
2265
  status: "fail",
@@ -2296,32 +2269,16 @@ async function runSyncGithub(input) {
2296
2269
  message: msg
2297
2270
  };
2298
2271
  }
2299
- const { sha: newCommitSha } = await newCommitRes.json();
2300
- let refUpdateRes;
2301
- if (createPr && branch) {
2302
- refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs`, {
2303
- method: "POST",
2304
- headers,
2305
- body: JSON.stringify({
2306
- ref: `refs/heads/${branch}`,
2307
- sha: newCommitSha
2308
- })
2309
- });
2310
- if (refUpdateRes.status === 422) refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs/heads/${branch}`, {
2311
- method: "PATCH",
2312
- headers,
2313
- body: JSON.stringify({
2314
- sha: newCommitSha,
2315
- force: true
2316
- })
2317
- });
2318
- } else refUpdateRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/refs/heads/${targetBranch}`, {
2319
- method: "PATCH",
2320
- headers,
2321
- body: JSON.stringify({ sha: newCommitSha })
2322
- });
2323
- if (!refUpdateRes.ok) {
2324
- 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)}`;
2325
2282
  print(` ✗ ${msg}`);
2326
2283
  return {
2327
2284
  status: "fail",
@@ -2332,25 +2289,17 @@ async function runSyncGithub(input) {
2332
2289
  };
2333
2290
  }
2334
2291
  let prUrl;
2335
- if (branch && createPr && !dryRun) {
2336
- const prRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/pulls`, {
2337
- method: "POST",
2338
- headers,
2339
- body: JSON.stringify({
2340
- title: message.split("\n")[0],
2341
- head: branch,
2342
- base: "main",
2343
- body: "Auto-generated by `holocron sync-github`. Review and merge to apply template updates."
2344
- })
2345
- });
2346
- if (prRes.ok) {
2347
- prUrl = (await prRes.json()).html_url;
2348
- print(` → PR opened: ${prUrl}`);
2349
- } else {
2350
- const err = await prRes.json();
2351
- if (err.errors?.some((e) => e.message.includes("already exists"))) print(` → PR already open for ${branch} — branch updated, ready to merge`);
2352
- else print(` ⚠ PR creation failed: ${err.message ?? prRes.status}`);
2353
- }
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)}`);
2354
2303
  }
2355
2304
  return {
2356
2305
  status: "ok",
@@ -4020,7 +3969,7 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
4020
3969
  message: "created"
4021
3970
  };
4022
3971
  } catch (err) {
4023
- if (!(err instanceof ProviderApiError) || err.status !== 403) return {
3972
+ if (!(err instanceof ProviderApiError$1) || err.status !== 403) return {
4024
3973
  capability: "source",
4025
3974
  step,
4026
3975
  status: "fail",
@@ -4037,7 +3986,7 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
4037
3986
  message: `classic protection on ${repo.defaultBranch}`
4038
3987
  };
4039
3988
  } catch (err) {
4040
- if (err instanceof ProviderApiError && err.status === 403) return {
3989
+ if (err instanceof ProviderApiError$1 && err.status === 403) return {
4041
3990
  capability: "source",
4042
3991
  step,
4043
3992
  status: "skip",
@@ -4059,7 +4008,7 @@ async function runSetup(input) {
4059
4008
  const dryRun = input.context.dryRun ?? false;
4060
4009
  const steps = [];
4061
4010
  const repo = config.project.repo;
4062
- const effectivePreset = repo?.protection ?? config.project.repoPolicy?.preset;
4011
+ const effectivePreset = repo?.protection;
4063
4012
  print(`Holocron setup — ${config.project.name}${dryRun ? " (dry-run)" : ""}`);
4064
4013
  print(` config: ${input.loaded.filepath}`);
4065
4014
  print("");
@@ -4096,7 +4045,7 @@ async function runSetup(input) {
4096
4045
  const ctx = WORKFLOW_CHECK_CONTEXTS[name];
4097
4046
  return ctx ? [ctx] : [];
4098
4047
  }),
4099
- ...repo?.requiredChecks ?? config.project.repoPolicy?.requiredChecks ?? []
4048
+ ...repo?.requiredChecks ?? []
4100
4049
  ] : [];
4101
4050
  steps.push(await upsertBranchProtection(source, dryRun, requiredChecks));
4102
4051
  print(formatStep(steps[steps.length - 1]));
@@ -4310,6 +4259,148 @@ function formatStep(step) {
4310
4259
  return ` ${icon} ${step.step}${detail}`;
4311
4260
  }
4312
4261
  //#endregion
4262
+ //#region src/commands/sync.ts
4263
+ const SYNC_STEPS = [
4264
+ "labels",
4265
+ "properties",
4266
+ "topics"
4267
+ ];
4268
+ async function runSync(input) {
4269
+ const print = input.print ?? ((line) => console.log(line));
4270
+ const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
4271
+ await loader.load();
4272
+ const config = input.loaded.resolved;
4273
+ const dryRun = input.context.dryRun ?? false;
4274
+ const requestedSteps = input.steps;
4275
+ const steps = [];
4276
+ print(`Holocron sync — ${config.project.name}${dryRun ? " (dry-run)" : ""}`);
4277
+ print(` config: ${input.loaded.filepath}`);
4278
+ print("");
4279
+ if (loader.has("source")) {
4280
+ const source = loader.get("source");
4281
+ print(" → source");
4282
+ for (const stepName of SYNC_STEPS) {
4283
+ if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
4284
+ if (stepName === "labels") if (source.syncLabels) {
4285
+ steps.push(await runSyncStep("source", "sync labels", dryRun, () => source.syncLabels(CANONICAL_LABELS, STALE_LABELS)));
4286
+ print(formatSyncStep(steps[steps.length - 1]));
4287
+ } else {
4288
+ steps.push({
4289
+ capability: "source",
4290
+ step: "sync labels",
4291
+ status: "skip",
4292
+ message: "provider does not implement syncLabels"
4293
+ });
4294
+ print(formatSyncStep(steps[steps.length - 1]));
4295
+ }
4296
+ if (stepName === "properties") if (source.syncProperties) {
4297
+ const repo = config.project.repo;
4298
+ const properties = {};
4299
+ const effectivePreset = repo?.protection;
4300
+ if (effectivePreset && effectivePreset !== "none") properties["branch_protection_level"] = effectivePreset;
4301
+ const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
4302
+ properties["monorepo"] = String(isMonorepo);
4303
+ const manual = repo?.properties ?? {};
4304
+ if (manual.lifecycle) properties["lifecycle"] = manual.lifecycle;
4305
+ if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
4306
+ if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
4307
+ if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
4308
+ steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
4309
+ print(formatSyncStep(steps[steps.length - 1]));
4310
+ } else {
4311
+ steps.push({
4312
+ capability: "source",
4313
+ step: "sync properties",
4314
+ status: "skip",
4315
+ message: "provider does not implement syncProperties"
4316
+ });
4317
+ print(formatSyncStep(steps[steps.length - 1]));
4318
+ }
4319
+ if (stepName === "topics") {
4320
+ const topics = config.project.repo?.topics ?? [];
4321
+ if (topics.length === 0) {
4322
+ steps.push({
4323
+ capability: "source",
4324
+ step: "sync topics",
4325
+ status: "skip",
4326
+ message: "no topics configured"
4327
+ });
4328
+ print(formatSyncStep(steps[steps.length - 1]));
4329
+ } else if (source.syncTopics) {
4330
+ steps.push(await runSyncStep("source", "sync topics", dryRun, () => source.syncTopics(topics)));
4331
+ print(formatSyncStep(steps[steps.length - 1]));
4332
+ } else {
4333
+ steps.push({
4334
+ capability: "source",
4335
+ step: "sync topics",
4336
+ status: "skip",
4337
+ message: "provider does not implement syncTopics"
4338
+ });
4339
+ print(formatSyncStep(steps[steps.length - 1]));
4340
+ }
4341
+ }
4342
+ }
4343
+ if (requestedSteps) {
4344
+ for (const name of requestedSteps) if (!SYNC_STEPS.includes(name)) {
4345
+ steps.push({
4346
+ capability: "source",
4347
+ step: `sync ${name}`,
4348
+ status: "skip",
4349
+ message: `unknown step "${name}"`
4350
+ });
4351
+ print(formatSyncStep(steps[steps.length - 1]));
4352
+ }
4353
+ }
4354
+ }
4355
+ const summary = steps.reduce((acc, s) => {
4356
+ if (s.status === "ok") acc.ok += 1;
4357
+ else if (s.status === "fail") acc.fail += 1;
4358
+ else if (s.status === "skip") acc.skip += 1;
4359
+ else if (s.status === "dry-run") acc.dryRun += 1;
4360
+ return acc;
4361
+ }, {
4362
+ ok: 0,
4363
+ fail: 0,
4364
+ skip: 0,
4365
+ dryRun: 0
4366
+ });
4367
+ print("");
4368
+ print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`);
4369
+ return {
4370
+ steps,
4371
+ summary
4372
+ };
4373
+ }
4374
+ async function runSyncStep(capability, step, dryRun, body) {
4375
+ if (dryRun) return {
4376
+ capability,
4377
+ step,
4378
+ status: "dry-run"
4379
+ };
4380
+ try {
4381
+ const note = await body();
4382
+ const result = {
4383
+ capability,
4384
+ step,
4385
+ status: "ok"
4386
+ };
4387
+ if (typeof note === "string") result.message = note;
4388
+ return result;
4389
+ } catch (err) {
4390
+ return {
4391
+ capability,
4392
+ step,
4393
+ status: "fail",
4394
+ message: err instanceof Error ? err.message : String(err)
4395
+ };
4396
+ }
4397
+ }
4398
+ function formatSyncStep(step) {
4399
+ const icon = step.status === "ok" ? "✓" : step.status === "fail" ? "✗" : step.status === "dry-run" ? "…" : "·";
4400
+ const detail = step.message ? ` (${step.message})` : "";
4401
+ return ` ${icon} ${step.step}${detail}`;
4402
+ }
4403
+ //#endregion
4313
4404
  //#region src/load-config.ts
4314
4405
  /**
4315
4406
  * `holocron.config.{json,js,ts}` file loader.
@@ -4532,7 +4623,25 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4532
4623
  dryRun: argv.dryRun,
4533
4624
  ...argv.otp ? { otp: argv.otp } : {}
4534
4625
  })).status === "fail") process.exitCode = 1;
4535
- }).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github via the GitHub API", (y) => y.option("repo", {
4626
+ }).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync [steps..]", "Sync source-level state (labels, properties, topics) from config to the provider", (y) => y.positional("steps", {
4627
+ type: "string",
4628
+ array: true,
4629
+ describe: "Steps to run: labels, properties, topics (default: all)"
4630
+ }).option("repo", {
4631
+ type: "string",
4632
+ describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
4633
+ }), async (argv) => {
4634
+ if ((await runSync({
4635
+ loaded: await loadConfig(argv.cwd),
4636
+ context: {
4637
+ repoRoot: argv.cwd,
4638
+ dryRun: argv.dryRun,
4639
+ ...argv.repo ? { repo: argv.repo } : {},
4640
+ ...argv.token ? { cliToken: argv.token } : {}
4641
+ },
4642
+ ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
4643
+ })).summary.fail > 0) process.exitCode = 1;
4644
+ }).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github via the GitHub API", (y) => y.option("repo", {
4536
4645
  type: "string",
4537
4646
  default: "theholocron/.github",
4538
4647
  describe: "Target org/repo (default: theholocron/.github)"
package/dist/index.d.mts CHANGED
@@ -30,26 +30,6 @@ type SingleEntry = string | [provider: string, options: ProviderOptions];
30
30
  type MultiEntry = Array<string | [provider: string, options: ProviderOptions]>;
31
31
  type RawProviderEntry = SingleEntry | MultiEntry;
32
32
  type RawProvidersConfig = Partial<Record<CapabilityKey, RawProviderEntry>>;
33
- interface RepoPolicyConfig {
34
- /**
35
- * "balanced" — squash-only merges, delete-branch-on-merge, issues/discussions/projects
36
- * enabled, auto-merge enabled, web sign-off required, always suggest updating, no wiki;
37
- * plus a ruleset that blocks force-push + deletion and requires a pull request (0 reviews).
38
- *
39
- * "strict" — everything in "balanced" plus required status checks from `requiredChecks`.
40
- *
41
- * "none" — skips repo settings + ruleset entirely.
42
- *
43
- * @default "balanced"
44
- * @deprecated Use `project.repo.protection` instead.
45
- */
46
- preset?: "balanced" | "strict" | "none";
47
- /**
48
- * CI check context names required on the default branch (used by "strict").
49
- * @deprecated Use `project.repo.requiredChecks` instead.
50
- */
51
- requiredChecks?: string[];
52
- }
53
33
  type RepoProtection = "balanced" | "strict" | "none";
54
34
  interface RepoProperties {
55
35
  lifecycle?: "active" | "experimental" | "deprecated";
@@ -91,12 +71,6 @@ interface HolocronConfig {
91
71
  * `--repo` on the command line still overrides.
92
72
  */
93
73
  repo?: RepoConfig;
94
- /**
95
- * Repo-level policy applied by `holocron setup`. Defines merge
96
- * strategy, branch protection rulesets, and security defaults.
97
- * Requires `source` capability to be configured.
98
- */
99
- repoPolicy?: RepoPolicyConfig;
100
74
  /**
101
75
  * CI workflow names to install as thin wrappers during `holocron setup`.
102
76
  * Each name maps to a reusable workflow in `theholocron/.github`.
@@ -216,4 +190,4 @@ interface LoadedConfig {
216
190
  */
217
191
  declare function loadConfig(cwd: string): Promise<LoadedConfig>;
218
192
  //#endregion
219
- export { Analytics, AppConfig, Auth, AuthDescription, AuthError, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoConfig, RepoPolicyConfig, RepoProperties, RepoProtection, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
193
+ export { Analytics, AppConfig, Auth, AuthDescription, AuthError, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoConfig, RepoProperties, RepoProtection, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.44",
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
  },