@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 +6 -7
- package/dist/cli.mjs +248 -139
- package/dist/index.d.mts +1 -27
- package/package.json +3 -2
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
-
|
|
2184
|
-
|
|
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
|
-
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
const data = await
|
|
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
|
|
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:
|
|
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
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
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
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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,
|
|
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.
|
|
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/
|
|
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
|
},
|