@nzpr/kb 0.1.3 → 0.1.5
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/lib/cli.js +22 -4
- package/lib/init-repo-interactive.js +143 -7
- package/lib/repo-init.js +67 -1
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function main(argv) {
|
|
|
81
81
|
targetDir: initRepoOptions.targetDir,
|
|
82
82
|
layout: initRepoOptions.layout,
|
|
83
83
|
repo: initRepoOptions.repo,
|
|
84
|
-
githubToken: process.env.GITHUB_TOKEN ?? null,
|
|
84
|
+
githubToken: initRepoOptions.githubToken ?? process.env.GITHUB_TOKEN ?? null,
|
|
85
85
|
databaseUrl: initRepoOptions.databaseUrl,
|
|
86
86
|
embeddingMode: initRepoOptions.embeddingMode,
|
|
87
87
|
embeddingApiUrl: initRepoOptions.embeddingApiUrl,
|
|
@@ -330,7 +330,7 @@ async function requirePublishAccess({ repo, token, apiBaseUrl }) {
|
|
|
330
330
|
|
|
331
331
|
function printRepoConfiguration(configuration) {
|
|
332
332
|
console.log("");
|
|
333
|
-
console.log("
|
|
333
|
+
console.log("configuration reference:");
|
|
334
334
|
console.log("");
|
|
335
335
|
console.log("required secrets:");
|
|
336
336
|
for (const entry of configuration.requiredSecrets) {
|
|
@@ -354,7 +354,10 @@ function printRepoConfiguration(configuration) {
|
|
|
354
354
|
|
|
355
355
|
function printInitRepoStatus(result) {
|
|
356
356
|
console.log("");
|
|
357
|
-
console.log("
|
|
357
|
+
console.log("summary:");
|
|
358
|
+
console.log(` docs layout: ${result.docsRootRelative}/`);
|
|
359
|
+
console.log(` files created: ${result.created.length}`);
|
|
360
|
+
console.log(` files kept: ${result.skipped.length}`);
|
|
358
361
|
printInitRepoDatabaseStatus(result.database);
|
|
359
362
|
printInitRepoGitHubStatus(result.github);
|
|
360
363
|
}
|
|
@@ -376,6 +379,11 @@ function printInitRepoDatabaseStatus(database) {
|
|
|
376
379
|
function printInitRepoGitHubStatus(github) {
|
|
377
380
|
if (github.status === "configured") {
|
|
378
381
|
console.log(` github: configured ${github.repo}`);
|
|
382
|
+
if (github.actions) {
|
|
383
|
+
console.log(
|
|
384
|
+
` actions: enabled=${github.actions.enabled} workflow_permissions=${github.actions.defaultWorkflowPermissions} pr_creation=${github.actions.canApprovePullRequestReviews}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
379
387
|
console.log(` labels: ${github.labels.join(", ")}`);
|
|
380
388
|
if (github.secrets.length) {
|
|
381
389
|
console.log(` secrets: ${github.secrets.join(", ")}`);
|
|
@@ -410,7 +418,7 @@ function printInitRepoNextStep(result) {
|
|
|
410
418
|
|
|
411
419
|
function printInitRepoChecklist(result) {
|
|
412
420
|
console.log("");
|
|
413
|
-
console.log("
|
|
421
|
+
console.log("next steps:");
|
|
414
422
|
if (result.created.length || result.skipped.length) {
|
|
415
423
|
console.log(` 1. commit and push the scaffolded knowledge repo files, including ${result.docsRootRelative}/`);
|
|
416
424
|
}
|
|
@@ -422,4 +430,14 @@ function printInitRepoChecklist(result) {
|
|
|
422
430
|
}
|
|
423
431
|
console.log(" 4. use kb create to open a proposal issue for the first knowledge entry");
|
|
424
432
|
console.log(" 5. review the issue, add kb-approved, merge the generated PR, and let publish sync the live KB");
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log("done in this run:");
|
|
435
|
+
if (result.created.length) {
|
|
436
|
+
console.log(` created: ${result.created.join(", ")}`);
|
|
437
|
+
} else {
|
|
438
|
+
console.log(" created: no new files");
|
|
439
|
+
}
|
|
440
|
+
if (result.skipped.length) {
|
|
441
|
+
console.log(` kept: ${result.skipped.join(", ")}`);
|
|
442
|
+
}
|
|
425
443
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import readline from "node:readline/promises";
|
|
4
5
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
5
6
|
import { resolveGitHubRepository } from "./config.js";
|
|
@@ -10,7 +11,8 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
10
11
|
cwd = process.cwd(),
|
|
11
12
|
stdin = defaultStdin,
|
|
12
13
|
stdout = defaultStdout,
|
|
13
|
-
prompter = null
|
|
14
|
+
prompter = null,
|
|
15
|
+
auth = defaultInteractiveGitHubAuth
|
|
14
16
|
}) {
|
|
15
17
|
if (!prompter && (!stdin.isTTY || !stdout.isTTY)) {
|
|
16
18
|
throw new Error("interactive init-repo requires a TTY; rerun in a terminal or pass flags directly");
|
|
@@ -21,8 +23,8 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
21
23
|
try {
|
|
22
24
|
const defaults = buildInitRepoDefaults({ flags, env, cwd });
|
|
23
25
|
|
|
24
|
-
stdout.write("
|
|
25
|
-
stdout.write("
|
|
26
|
+
stdout.write("KB Init Wizard\n");
|
|
27
|
+
stdout.write("This walkthrough prepares the knowledge repo and can wire GitHub and database setup now.\n\n");
|
|
26
28
|
|
|
27
29
|
const targetDir = path.resolve(
|
|
28
30
|
cwd,
|
|
@@ -42,6 +44,15 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
42
44
|
repo = await prompt.askText("target GitHub repo (OWNER/REPO)", defaults.repo, {
|
|
43
45
|
validate: requireValue
|
|
44
46
|
});
|
|
47
|
+
if (!defaults.githubToken) {
|
|
48
|
+
const authorized = await ensureInteractiveGitHubAccess({ prompt, stdout, auth });
|
|
49
|
+
if (authorized) {
|
|
50
|
+
defaults.githubToken = authorized;
|
|
51
|
+
} else {
|
|
52
|
+
stdout.write("skipping GitHub repo setup for now because GitHub auth is not ready.\n");
|
|
53
|
+
repo = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
const verifyDatabase = await prompt.askConfirm(
|
|
@@ -95,7 +106,7 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
95
106
|
let repoAutomationToken = null;
|
|
96
107
|
if (configureRepo) {
|
|
97
108
|
const useAutomationToken = await prompt.askConfirm(
|
|
98
|
-
"
|
|
109
|
+
"use a custom automation token instead of the default GitHub Actions token",
|
|
99
110
|
Boolean(defaults.repoAutomationToken)
|
|
100
111
|
);
|
|
101
112
|
if (useAutomationToken) {
|
|
@@ -106,10 +117,10 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
stdout.write("\
|
|
110
|
-
stdout.write(`
|
|
120
|
+
stdout.write("\nPlan\n");
|
|
121
|
+
stdout.write(` target directory: ${targetDir}\n`);
|
|
111
122
|
stdout.write(` document layout: ${layout === "repo-root" ? "docs/" : "kb/docs/"}\n`);
|
|
112
|
-
stdout.write(`
|
|
123
|
+
stdout.write(` GitHub repo setup: ${repo ? repo : "skip for now"}\n`);
|
|
113
124
|
stdout.write(` database preflight: ${databaseUrl ? "yes" : "skip for now"}\n`);
|
|
114
125
|
stdout.write(
|
|
115
126
|
` embeddings config: ${embeddingMode === "bge-m3-openai" ? `${embeddingMode}/${embeddingModel}` : "skip for now"}\n`
|
|
@@ -124,6 +135,7 @@ export async function collectInitRepoInteractiveOptions({
|
|
|
124
135
|
targetDir,
|
|
125
136
|
layout,
|
|
126
137
|
repo,
|
|
138
|
+
githubToken: defaults.githubToken || null,
|
|
127
139
|
databaseUrl,
|
|
128
140
|
embeddingMode,
|
|
129
141
|
embeddingApiUrl,
|
|
@@ -142,6 +154,7 @@ export function buildInitRepoDefaults({ flags = {}, env = process.env, cwd = pro
|
|
|
142
154
|
targetDir: flags.dir ?? cwd,
|
|
143
155
|
layout:
|
|
144
156
|
flags.layout ?? inferDefaultKnowledgeLayout(flags.dir ? path.resolve(cwd, flags.dir) : cwd),
|
|
157
|
+
githubToken: env.GITHUB_TOKEN ?? "",
|
|
145
158
|
repo: flags.repo ?? resolveGitHubRepository(env) ?? "",
|
|
146
159
|
databaseUrl: flags["database-url"] ?? env.KB_DATABASE_URL ?? "",
|
|
147
160
|
embeddingMode: flags["embedding-mode"] ?? env.KB_EMBEDDING_MODE ?? "",
|
|
@@ -189,6 +202,9 @@ function createReadlinePrompter({ stdin, stdout }) {
|
|
|
189
202
|
askText(label, defaultValue = "", options = {}) {
|
|
190
203
|
return askText(rl, label, defaultValue, options);
|
|
191
204
|
},
|
|
205
|
+
askSecret(label, options = {}) {
|
|
206
|
+
return askSecret({ stdin, stdout, label, ...options });
|
|
207
|
+
},
|
|
192
208
|
askConfirm(label, defaultValue = false) {
|
|
193
209
|
return askConfirm(rl, label, defaultValue);
|
|
194
210
|
},
|
|
@@ -232,6 +248,20 @@ async function askConfirm(rl, label, defaultValue = false) {
|
|
|
232
248
|
}
|
|
233
249
|
}
|
|
234
250
|
|
|
251
|
+
async function askSecret({ stdin, stdout, label, validate = null }) {
|
|
252
|
+
while (true) {
|
|
253
|
+
const value = await readHiddenInput({ stdin, stdout, prompt: `${label}: ` });
|
|
254
|
+
if (!validate) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
const error = validate(value);
|
|
258
|
+
if (!error) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
stdout.write(`${error}\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
235
265
|
function requireValue(value) {
|
|
236
266
|
return String(value ?? "").trim() ? null : "a value is required";
|
|
237
267
|
}
|
|
@@ -251,3 +281,109 @@ function emptyToNull(value) {
|
|
|
251
281
|
const normalized = String(value ?? "").trim();
|
|
252
282
|
return normalized ? normalized : null;
|
|
253
283
|
}
|
|
284
|
+
|
|
285
|
+
async function ensureInteractiveGitHubAccess({ prompt, stdout, auth }) {
|
|
286
|
+
const existingToken = auth.getToken();
|
|
287
|
+
if (existingToken) {
|
|
288
|
+
stdout.write("using existing GitHub CLI authentication for repo setup.\n");
|
|
289
|
+
return existingToken;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
stdout.write("\nGitHub repo setup needs GitHub authentication.\n");
|
|
293
|
+
stdout.write("You can paste a GitHub token now, use GitHub CLI browser login, or skip repo setup for now.\n");
|
|
294
|
+
|
|
295
|
+
while (true) {
|
|
296
|
+
const method = (
|
|
297
|
+
await prompt.askText("authentication method (token/login/skip)", "token", {
|
|
298
|
+
validate: validateAuthMethod
|
|
299
|
+
})
|
|
300
|
+
).toLowerCase();
|
|
301
|
+
if (method === "skip") {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (method === "token") {
|
|
305
|
+
const token = await prompt.askSecret("GITHUB_TOKEN", {
|
|
306
|
+
validate: requireValue
|
|
307
|
+
});
|
|
308
|
+
if (token) {
|
|
309
|
+
stdout.write("using the provided GitHub token for repo setup.\n");
|
|
310
|
+
return token;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
auth.login();
|
|
315
|
+
const token = auth.getToken();
|
|
316
|
+
if (token) {
|
|
317
|
+
stdout.write("GitHub CLI authentication is ready.\n");
|
|
318
|
+
return token;
|
|
319
|
+
}
|
|
320
|
+
stdout.write("GitHub CLI login did not yield a usable token.\n");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const defaultInteractiveGitHubAuth = {
|
|
325
|
+
getToken() {
|
|
326
|
+
try {
|
|
327
|
+
return execFileSync("gh", ["auth", "token"], {
|
|
328
|
+
encoding: "utf8",
|
|
329
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
330
|
+
}).trim();
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
login() {
|
|
336
|
+
execFileSync("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
|
|
337
|
+
stdio: "inherit"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
function validateAuthMethod(value) {
|
|
343
|
+
if (["token", "login", "skip"].includes(String(value ?? "").trim().toLowerCase())) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
return "enter token, login, or skip";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function readHiddenInput({ stdin, stdout, prompt }) {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
stdout.write(prompt);
|
|
352
|
+
let value = "";
|
|
353
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
354
|
+
|
|
355
|
+
const cleanup = () => {
|
|
356
|
+
stdin.off("data", onData);
|
|
357
|
+
if (typeof stdin.setRawMode === "function") {
|
|
358
|
+
stdin.setRawMode(wasRaw);
|
|
359
|
+
}
|
|
360
|
+
stdout.write("\n");
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const onData = (chunk) => {
|
|
364
|
+
const text = chunk.toString("utf8");
|
|
365
|
+
for (const char of text) {
|
|
366
|
+
if (char === "\u0003") {
|
|
367
|
+
cleanup();
|
|
368
|
+
reject(new Error("interactive init-repo cancelled"));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (char === "\r" || char === "\n") {
|
|
372
|
+
cleanup();
|
|
373
|
+
resolve(value.trim());
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (char === "\u007f") {
|
|
377
|
+
value = value.slice(0, -1);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
value += char;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (typeof stdin.setRawMode === "function") {
|
|
385
|
+
stdin.setRawMode(true);
|
|
386
|
+
}
|
|
387
|
+
stdin.on("data", onData);
|
|
388
|
+
});
|
|
389
|
+
}
|
package/lib/repo-init.js
CHANGED
|
@@ -112,7 +112,8 @@ export async function bootstrapKnowledgeRepo({
|
|
|
112
112
|
result.github = {
|
|
113
113
|
status: "failed",
|
|
114
114
|
repo,
|
|
115
|
-
error:
|
|
115
|
+
error:
|
|
116
|
+
"GITHUB_TOKEN is required when configuring a knowledge repo; set GITHUB_TOKEN or run gh auth login before rerunning"
|
|
116
117
|
};
|
|
117
118
|
return result;
|
|
118
119
|
}
|
|
@@ -226,6 +227,11 @@ async function configureKnowledgeRepo({
|
|
|
226
227
|
runGitHubCommand
|
|
227
228
|
}) {
|
|
228
229
|
await runGitHubCommand(["repo", "edit", repo, "--enable-issues"], { githubToken });
|
|
230
|
+
const actions = await ensureGitHubActionsPermissions({
|
|
231
|
+
repo,
|
|
232
|
+
githubToken,
|
|
233
|
+
runGitHubCommand
|
|
234
|
+
});
|
|
229
235
|
|
|
230
236
|
for (const label of LABELS) {
|
|
231
237
|
await runGitHubCommand(
|
|
@@ -259,12 +265,72 @@ async function configureKnowledgeRepo({
|
|
|
259
265
|
|
|
260
266
|
return {
|
|
261
267
|
repo,
|
|
268
|
+
actions,
|
|
262
269
|
labels: LABELS.map((label) => label.name),
|
|
263
270
|
secrets: [...secrets.keys()],
|
|
264
271
|
variables: [...variables.keys()]
|
|
265
272
|
};
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
async function ensureGitHubActionsPermissions({ repo, githubToken, runGitHubCommand }) {
|
|
276
|
+
const actionsPermissions = await runGitHubJson(
|
|
277
|
+
["api", `repos/${repo}/actions/permissions`],
|
|
278
|
+
{ githubToken, runGitHubCommand }
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (actionsPermissions.enabled === false) {
|
|
282
|
+
await runGitHubCommand(
|
|
283
|
+
[
|
|
284
|
+
"api",
|
|
285
|
+
"--method",
|
|
286
|
+
"PUT",
|
|
287
|
+
`repos/${repo}/actions/permissions`,
|
|
288
|
+
"-F",
|
|
289
|
+
"enabled=true",
|
|
290
|
+
"-f",
|
|
291
|
+
`allowed_actions=${actionsPermissions.allowed_actions ?? "all"}`
|
|
292
|
+
],
|
|
293
|
+
{ githubToken }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const workflowPermissions = await runGitHubJson(
|
|
298
|
+
["api", `repos/${repo}/actions/permissions/workflow`],
|
|
299
|
+
{ githubToken, runGitHubCommand }
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
workflowPermissions.default_workflow_permissions !== "write" ||
|
|
304
|
+
workflowPermissions.can_approve_pull_request_reviews !== true
|
|
305
|
+
) {
|
|
306
|
+
await runGitHubCommand(
|
|
307
|
+
[
|
|
308
|
+
"api",
|
|
309
|
+
"--method",
|
|
310
|
+
"PUT",
|
|
311
|
+
`repos/${repo}/actions/permissions/workflow`,
|
|
312
|
+
"-f",
|
|
313
|
+
"default_workflow_permissions=write",
|
|
314
|
+
"-F",
|
|
315
|
+
"can_approve_pull_request_reviews=true"
|
|
316
|
+
],
|
|
317
|
+
{ githubToken }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
enabled: true,
|
|
323
|
+
allowedActions: actionsPermissions.allowed_actions ?? "all",
|
|
324
|
+
defaultWorkflowPermissions: "write",
|
|
325
|
+
canApprovePullRequestReviews: true
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function runGitHubJson(args, { githubToken, runGitHubCommand }) {
|
|
330
|
+
const output = await runGitHubCommand(args, { githubToken });
|
|
331
|
+
return JSON.parse(output);
|
|
332
|
+
}
|
|
333
|
+
|
|
268
334
|
async function defaultVerifyDatabaseReady({ databaseUrl }) {
|
|
269
335
|
const client = await connect(databaseUrl);
|
|
270
336
|
try {
|