@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.
- package/README.md +2 -4
- package/dist/cli.mjs +83 -133
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -107,8 +107,6 @@ export default acmeConfig;
|
|
|
107
107
|
|
|
108
108
|
## Status
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
-
|
|
2183
|
-
|
|
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
|
-
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
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");
|
|
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
|
|
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:
|
|
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
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
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
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
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.
|
|
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
|
},
|