@nzpr/kb 0.1.1 → 0.1.3
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 +29 -5
- package/lib/cli-common.js +1 -1
- package/lib/cli.js +47 -13
- package/lib/init-repo-interactive.js +253 -0
- package/lib/repo-init.js +34 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ The public surface of `@nzpr/kb` is the `kb` CLI.
|
|
|
23
23
|
|
|
24
24
|
- `kb init-repo`
|
|
25
25
|
Initialize a knowledge base workspace.
|
|
26
|
+
Use `kb init-repo --interactive` for a guided setup flow.
|
|
27
|
+
Use `--layout repo-root` when this repository is itself the KB.
|
|
26
28
|
|
|
27
29
|
- `kb create --title TEXT --text TEXT`
|
|
28
30
|
Propose a new knowledge document or a substantial update.
|
|
@@ -95,6 +97,19 @@ Bootstrap that repo with:
|
|
|
95
97
|
kb init-repo --dir /path/to/knowledge-repo
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
If you want the CLI to walk you through setup and tell you what to do next:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
kb init-repo --interactive
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Layout options:
|
|
107
|
+
|
|
108
|
+
- `--layout repo-root` puts documents in `docs/`
|
|
109
|
+
- `--layout nested-kb` puts documents in `kb/docs/`
|
|
110
|
+
|
|
111
|
+
For a dedicated knowledge repository, prefer `--layout repo-root`.
|
|
112
|
+
|
|
98
113
|
Or bootstrap and configure it in one step:
|
|
99
114
|
|
|
100
115
|
```bash
|
|
@@ -113,15 +128,24 @@ Inside that knowledge repo, the normal flow is:
|
|
|
113
128
|
1. Run `kb init-repo` once to scaffold the repo. If you provide `--repo`, `--database-url`, and `GITHUB_TOKEN`, it also configures the target repo and preflights the database.
|
|
114
129
|
2. Open or update a knowledge proposal issue.
|
|
115
130
|
3. Review and approve it there.
|
|
116
|
-
4. Materialize it into
|
|
117
|
-
5. After merge, that repo's CI runs `kb publish
|
|
131
|
+
4. Materialize it into the configured docs directory.
|
|
132
|
+
5. After merge, that repo's CI runs `kb publish` against that docs directory.
|
|
118
133
|
|
|
119
134
|
`kb init-repo` is safe to rerun. It reports bootstrap status for:
|
|
120
135
|
|
|
121
136
|
- local scaffold creation
|
|
137
|
+
- knowledge document layout selection
|
|
122
138
|
- database preflight and schema initialization
|
|
123
139
|
- GitHub repo labels, variables, and secrets
|
|
124
140
|
|
|
141
|
+
In interactive mode it also:
|
|
142
|
+
|
|
143
|
+
- asks which repo and directory you want to initialize
|
|
144
|
+
- asks whether to wire GitHub setup now
|
|
145
|
+
- asks whether to verify the database now
|
|
146
|
+
- collects embedding settings if you want remote embeddings
|
|
147
|
+
- prints the next actions after initialization
|
|
148
|
+
|
|
125
149
|
If one remote step fails, the scaffold still stays in place and the command tells you what to rerun.
|
|
126
150
|
|
|
127
151
|
## Runtime
|
|
@@ -170,7 +194,7 @@ export KB_DATABASE_URL=postgresql://kb:kb@localhost:5432/kb
|
|
|
170
194
|
export KB_GITHUB_REPO=owner/repo
|
|
171
195
|
export GITHUB_TOKEN=...
|
|
172
196
|
docker compose -f docker-compose.pgvector.yml up -d
|
|
173
|
-
kb publish --docs-root ./
|
|
197
|
+
kb publish --docs-root ./docs
|
|
174
198
|
kb catalog --json
|
|
175
199
|
kb search "deployment rule"
|
|
176
200
|
```
|
|
@@ -200,13 +224,13 @@ The package is published as `@nzpr/kb`.
|
|
|
200
224
|
|
|
201
225
|
## Using From A Knowledge Repo
|
|
202
226
|
|
|
203
|
-
The knowledge-authority repo should install this package in CI and use it to sync
|
|
227
|
+
The knowledge-authority repo should install this package in CI and use it to sync its configured docs directory into the vector database. For a dedicated KB repo with `--layout repo-root`, that directory is `docs/`:
|
|
204
228
|
|
|
205
229
|
```bash
|
|
206
230
|
npm install -g @nzpr/kb
|
|
207
231
|
export KB_GITHUB_REPO=owner/repo
|
|
208
232
|
export GITHUB_TOKEN=...
|
|
209
|
-
kb publish --docs-root ./
|
|
233
|
+
kb publish --docs-root ./docs
|
|
210
234
|
```
|
|
211
235
|
|
|
212
236
|
Or scaffold that repo first:
|
package/lib/cli-common.js
CHANGED
package/lib/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./config.js";
|
|
10
10
|
import { connect, initDb } from "./db.js";
|
|
11
11
|
import { ingestDocuments } from "./index.js";
|
|
12
|
+
import { collectInitRepoInteractiveOptions } from "./init-repo-interactive.js";
|
|
12
13
|
import { createGitHubIssueFromText } from "./kb-proposals.js";
|
|
13
14
|
import { bootstrapKnowledgeRepo } from "./repo-init.js";
|
|
14
15
|
import { askIndex, doctor, knowledgeCatalog, listDocuments, searchIndex, snippet } from "./search.js";
|
|
@@ -60,21 +61,37 @@ export async function main(argv) {
|
|
|
60
61
|
return 0;
|
|
61
62
|
}
|
|
62
63
|
case "init-repo": {
|
|
64
|
+
const initRepoOptions = flags.interactive
|
|
65
|
+
? await collectInitRepoInteractiveOptions({ flags })
|
|
66
|
+
: {
|
|
67
|
+
targetDir: flags.dir ?? process.cwd(),
|
|
68
|
+
layout: flags.layout ?? "nested-kb",
|
|
69
|
+
repo: flags.repo ?? resolveGitHubRepository(),
|
|
70
|
+
databaseUrl: flags["database-url"] ?? process.env.KB_DATABASE_URL ?? null,
|
|
71
|
+
embeddingMode: flags["embedding-mode"] ?? process.env.KB_EMBEDDING_MODE ?? null,
|
|
72
|
+
embeddingApiUrl: flags["embedding-api-url"] ?? process.env.KB_EMBEDDING_API_URL ?? null,
|
|
73
|
+
embeddingModel: flags["embedding-model"] ?? process.env.KB_EMBEDDING_MODEL ?? null,
|
|
74
|
+
embeddingApiKey: flags["embedding-api-key"] ?? process.env.KB_EMBEDDING_API_KEY ?? null,
|
|
75
|
+
dbConnectTimeoutMs:
|
|
76
|
+
flags["db-connect-timeout-ms"] ?? process.env.KB_DB_CONNECT_TIMEOUT_MS ?? null,
|
|
77
|
+
repoAutomationToken:
|
|
78
|
+
flags["repo-automation-token"] ?? process.env.KB_REPO_AUTOMATION_TOKEN ?? null
|
|
79
|
+
};
|
|
63
80
|
const result = await bootstrapKnowledgeRepo({
|
|
64
|
-
targetDir:
|
|
65
|
-
|
|
81
|
+
targetDir: initRepoOptions.targetDir,
|
|
82
|
+
layout: initRepoOptions.layout,
|
|
83
|
+
repo: initRepoOptions.repo,
|
|
66
84
|
githubToken: process.env.GITHUB_TOKEN ?? null,
|
|
67
|
-
databaseUrl:
|
|
68
|
-
embeddingMode:
|
|
69
|
-
embeddingApiUrl:
|
|
70
|
-
embeddingModel:
|
|
71
|
-
embeddingApiKey:
|
|
72
|
-
dbConnectTimeoutMs:
|
|
73
|
-
|
|
74
|
-
repoAutomationToken:
|
|
75
|
-
flags["repo-automation-token"] ?? process.env.KB_REPO_AUTOMATION_TOKEN ?? null
|
|
85
|
+
databaseUrl: initRepoOptions.databaseUrl,
|
|
86
|
+
embeddingMode: initRepoOptions.embeddingMode,
|
|
87
|
+
embeddingApiUrl: initRepoOptions.embeddingApiUrl,
|
|
88
|
+
embeddingModel: initRepoOptions.embeddingModel,
|
|
89
|
+
embeddingApiKey: initRepoOptions.embeddingApiKey,
|
|
90
|
+
dbConnectTimeoutMs: initRepoOptions.dbConnectTimeoutMs,
|
|
91
|
+
repoAutomationToken: initRepoOptions.repoAutomationToken
|
|
76
92
|
});
|
|
77
93
|
console.log(`initialized knowledge repo scaffold in ${result.root}`);
|
|
94
|
+
console.log(`knowledge documents will live in ${result.docsRootRelative}/`);
|
|
78
95
|
for (const relativePath of result.created) {
|
|
79
96
|
console.log(`created ${relativePath}`);
|
|
80
97
|
}
|
|
@@ -87,6 +104,7 @@ export async function main(argv) {
|
|
|
87
104
|
printInitRepoStatus(result);
|
|
88
105
|
printRepoConfiguration(result.configuration);
|
|
89
106
|
printInitRepoNextStep(result);
|
|
107
|
+
printInitRepoChecklist(result);
|
|
90
108
|
return result.ok ? 0 : 1;
|
|
91
109
|
}
|
|
92
110
|
case "search": {
|
|
@@ -231,7 +249,7 @@ function printHelp() {
|
|
|
231
249
|
console.log(`usage: kb <command> [options]
|
|
232
250
|
|
|
233
251
|
commands:
|
|
234
|
-
init-repo [--dir PATH] [--repo OWNER/REPO]
|
|
252
|
+
init-repo [--interactive] [--layout repo-root|nested-kb] [--dir PATH] [--repo OWNER/REPO]
|
|
235
253
|
create --title TEXT --text TEXT [--path RELATIVE_PATH]
|
|
236
254
|
search <query>
|
|
237
255
|
ask <question>
|
|
@@ -256,7 +274,7 @@ search options:
|
|
|
256
274
|
function printCommandHelp(command) {
|
|
257
275
|
const commandHelp = {
|
|
258
276
|
"init-repo":
|
|
259
|
-
"usage: kb init-repo [--dir PATH] [--repo OWNER/REPO] [--database-url URL] [--embedding-mode MODE] [--embedding-api-url URL] [--embedding-model NAME] [--embedding-api-key KEY] [--db-connect-timeout-ms N] [--repo-automation-token TOKEN]\n\nScaffold a knowledge-authority repository, optionally configure its GitHub settings, and preflight the target database.",
|
|
277
|
+
"usage: kb init-repo [--interactive] [--layout repo-root|nested-kb] [--dir PATH] [--repo OWNER/REPO] [--database-url URL] [--embedding-mode MODE] [--embedding-api-url URL] [--embedding-model NAME] [--embedding-api-key KEY] [--db-connect-timeout-ms N] [--repo-automation-token TOKEN]\n\nScaffold a knowledge-authority repository, optionally configure its GitHub settings, and preflight the target database. Use --interactive for a guided terminal walkthrough.",
|
|
260
278
|
create: `usage: kb create --title TEXT --text TEXT [--path RELATIVE_PATH] [--repo OWNER/REPO]\n\n${githubCreationHelp()}`,
|
|
261
279
|
search: `usage: kb search <query> [options]\n\n${databaseHelp()}\n --limit N`,
|
|
262
280
|
ask: `usage: kb ask <question> [options]\n\n${databaseHelp()}\n --limit N`,
|
|
@@ -389,3 +407,19 @@ function printInitRepoNextStep(result) {
|
|
|
389
407
|
"next: rerun kb init-repo with the missing repo or database inputs when you are ready, or commit the scaffold now and finish remote setup later"
|
|
390
408
|
);
|
|
391
409
|
}
|
|
410
|
+
|
|
411
|
+
function printInitRepoChecklist(result) {
|
|
412
|
+
console.log("");
|
|
413
|
+
console.log("what to do next:");
|
|
414
|
+
if (result.created.length || result.skipped.length) {
|
|
415
|
+
console.log(` 1. commit and push the scaffolded knowledge repo files, including ${result.docsRootRelative}/`);
|
|
416
|
+
}
|
|
417
|
+
if (result.github.status !== "configured") {
|
|
418
|
+
console.log(" 2. make sure the target repo exists and rerun with --repo once GitHub setup is ready");
|
|
419
|
+
}
|
|
420
|
+
if (result.database.status !== "verified") {
|
|
421
|
+
console.log(" 3. rerun with --database-url once the target database is ready");
|
|
422
|
+
}
|
|
423
|
+
console.log(" 4. use kb create to open a proposal issue for the first knowledge entry");
|
|
424
|
+
console.log(" 5. review the issue, add kb-approved, merge the generated PR, and let publish sync the live KB");
|
|
425
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
4
|
+
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
5
|
+
import { resolveGitHubRepository } from "./config.js";
|
|
6
|
+
|
|
7
|
+
export async function collectInitRepoInteractiveOptions({
|
|
8
|
+
flags,
|
|
9
|
+
env = process.env,
|
|
10
|
+
cwd = process.cwd(),
|
|
11
|
+
stdin = defaultStdin,
|
|
12
|
+
stdout = defaultStdout,
|
|
13
|
+
prompter = null
|
|
14
|
+
}) {
|
|
15
|
+
if (!prompter && (!stdin.isTTY || !stdout.isTTY)) {
|
|
16
|
+
throw new Error("interactive init-repo requires a TTY; rerun in a terminal or pass flags directly");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const prompt = prompter ?? createReadlinePrompter({ stdin, stdout });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const defaults = buildInitRepoDefaults({ flags, env, cwd });
|
|
23
|
+
|
|
24
|
+
stdout.write("interactive kb init-repo\n");
|
|
25
|
+
stdout.write("this wizard scaffolds the knowledge repo and can also wire remote setup now.\n\n");
|
|
26
|
+
|
|
27
|
+
const targetDir = path.resolve(
|
|
28
|
+
cwd,
|
|
29
|
+
await prompt.askText("knowledge repo directory", defaults.targetDir, {
|
|
30
|
+
validate: requireValue
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
const layout = await askKnowledgeLayout(prompt, targetDir, defaults.layout);
|
|
34
|
+
|
|
35
|
+
const configureRepo = await prompt.askConfirm(
|
|
36
|
+
"configure the GitHub repo now",
|
|
37
|
+
Boolean(defaults.repo || env.GITHUB_TOKEN)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
let repo = null;
|
|
41
|
+
if (configureRepo) {
|
|
42
|
+
repo = await prompt.askText("target GitHub repo (OWNER/REPO)", defaults.repo, {
|
|
43
|
+
validate: requireValue
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const verifyDatabase = await prompt.askConfirm(
|
|
48
|
+
"verify the database and initialize schema now",
|
|
49
|
+
Boolean(defaults.databaseUrl)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
let databaseUrl = null;
|
|
53
|
+
if (verifyDatabase) {
|
|
54
|
+
databaseUrl = await prompt.askText("database URL", defaults.databaseUrl, {
|
|
55
|
+
validate: requireValue
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let embeddingMode = null;
|
|
60
|
+
let embeddingApiUrl = null;
|
|
61
|
+
let embeddingModel = null;
|
|
62
|
+
let embeddingApiKey = null;
|
|
63
|
+
let dbConnectTimeoutMs = null;
|
|
64
|
+
|
|
65
|
+
if (verifyDatabase) {
|
|
66
|
+
const useRemoteEmbeddings = await prompt.askConfirm(
|
|
67
|
+
"configure remote embeddings now",
|
|
68
|
+
defaults.embeddingMode === "bge-m3-openai"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (useRemoteEmbeddings) {
|
|
72
|
+
embeddingMode = "bge-m3-openai";
|
|
73
|
+
embeddingApiUrl = await prompt.askText(
|
|
74
|
+
"embedding API URL",
|
|
75
|
+
defaults.embeddingApiUrl || "https://your-embeddings-host/v1/embeddings",
|
|
76
|
+
{ validate: requireValue }
|
|
77
|
+
);
|
|
78
|
+
embeddingModel = await prompt.askText(
|
|
79
|
+
"embedding model",
|
|
80
|
+
defaults.embeddingModel || "BAAI/bge-m3",
|
|
81
|
+
{ validate: requireValue }
|
|
82
|
+
);
|
|
83
|
+
embeddingApiKey = await prompt.askText(
|
|
84
|
+
"embedding API key (optional)",
|
|
85
|
+
defaults.embeddingApiKey
|
|
86
|
+
);
|
|
87
|
+
dbConnectTimeoutMs = await prompt.askText(
|
|
88
|
+
"database connect timeout ms",
|
|
89
|
+
defaults.dbConnectTimeoutMs || "20000",
|
|
90
|
+
{ validate: requirePositiveNumber }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let repoAutomationToken = null;
|
|
96
|
+
if (configureRepo) {
|
|
97
|
+
const useAutomationToken = await prompt.askConfirm(
|
|
98
|
+
"set a dedicated repo automation token secret",
|
|
99
|
+
Boolean(defaults.repoAutomationToken)
|
|
100
|
+
);
|
|
101
|
+
if (useAutomationToken) {
|
|
102
|
+
repoAutomationToken = await prompt.askText(
|
|
103
|
+
"repo automation token",
|
|
104
|
+
defaults.repoAutomationToken
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stdout.write("\nplan:\n");
|
|
110
|
+
stdout.write(` local scaffold: ${targetDir}\n`);
|
|
111
|
+
stdout.write(` document layout: ${layout === "repo-root" ? "docs/" : "kb/docs/"}\n`);
|
|
112
|
+
stdout.write(` github repo setup: ${repo ? repo : "skip for now"}\n`);
|
|
113
|
+
stdout.write(` database preflight: ${databaseUrl ? "yes" : "skip for now"}\n`);
|
|
114
|
+
stdout.write(
|
|
115
|
+
` embeddings config: ${embeddingMode === "bge-m3-openai" ? `${embeddingMode}/${embeddingModel}` : "skip for now"}\n`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const proceed = await prompt.askConfirm("run init-repo with this plan", true);
|
|
119
|
+
if (!proceed) {
|
|
120
|
+
throw new Error("interactive init-repo cancelled");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
targetDir,
|
|
125
|
+
layout,
|
|
126
|
+
repo,
|
|
127
|
+
databaseUrl,
|
|
128
|
+
embeddingMode,
|
|
129
|
+
embeddingApiUrl,
|
|
130
|
+
embeddingModel,
|
|
131
|
+
embeddingApiKey: emptyToNull(embeddingApiKey),
|
|
132
|
+
dbConnectTimeoutMs: emptyToNull(dbConnectTimeoutMs),
|
|
133
|
+
repoAutomationToken: emptyToNull(repoAutomationToken)
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
await prompt.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildInitRepoDefaults({ flags = {}, env = process.env, cwd = process.cwd() }) {
|
|
141
|
+
return {
|
|
142
|
+
targetDir: flags.dir ?? cwd,
|
|
143
|
+
layout:
|
|
144
|
+
flags.layout ?? inferDefaultKnowledgeLayout(flags.dir ? path.resolve(cwd, flags.dir) : cwd),
|
|
145
|
+
repo: flags.repo ?? resolveGitHubRepository(env) ?? "",
|
|
146
|
+
databaseUrl: flags["database-url"] ?? env.KB_DATABASE_URL ?? "",
|
|
147
|
+
embeddingMode: flags["embedding-mode"] ?? env.KB_EMBEDDING_MODE ?? "",
|
|
148
|
+
embeddingApiUrl: flags["embedding-api-url"] ?? env.KB_EMBEDDING_API_URL ?? "",
|
|
149
|
+
embeddingModel: flags["embedding-model"] ?? env.KB_EMBEDDING_MODEL ?? "",
|
|
150
|
+
embeddingApiKey: flags["embedding-api-key"] ?? env.KB_EMBEDDING_API_KEY ?? "",
|
|
151
|
+
dbConnectTimeoutMs:
|
|
152
|
+
flags["db-connect-timeout-ms"] ?? env.KB_DB_CONNECT_TIMEOUT_MS ?? "",
|
|
153
|
+
repoAutomationToken:
|
|
154
|
+
flags["repo-automation-token"] ?? env.KB_REPO_AUTOMATION_TOKEN ?? ""
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function askKnowledgeLayout(prompt, targetDir, defaultLayout) {
|
|
159
|
+
const useRepoRoot = await prompt.askConfirm(
|
|
160
|
+
"store documents at repo root docs/ (recommended when this repo is the knowledge base)",
|
|
161
|
+
defaultLayout === "repo-root"
|
|
162
|
+
);
|
|
163
|
+
if (useRepoRoot) {
|
|
164
|
+
return "repo-root";
|
|
165
|
+
}
|
|
166
|
+
if (!fs.existsSync(targetDir) || fs.readdirSync(targetDir).length === 0) {
|
|
167
|
+
return "nested-kb";
|
|
168
|
+
}
|
|
169
|
+
return "nested-kb";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function inferDefaultKnowledgeLayout(targetDir) {
|
|
173
|
+
const root = path.resolve(targetDir);
|
|
174
|
+
if (fs.existsSync(path.join(root, "docs"))) {
|
|
175
|
+
return "repo-root";
|
|
176
|
+
}
|
|
177
|
+
if (fs.existsSync(path.join(root, "kb", "docs"))) {
|
|
178
|
+
return "nested-kb";
|
|
179
|
+
}
|
|
180
|
+
return "repo-root";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createReadlinePrompter({ stdin, stdout }) {
|
|
184
|
+
const rl = readline.createInterface({
|
|
185
|
+
input: stdin,
|
|
186
|
+
output: stdout
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
askText(label, defaultValue = "", options = {}) {
|
|
190
|
+
return askText(rl, label, defaultValue, options);
|
|
191
|
+
},
|
|
192
|
+
askConfirm(label, defaultValue = false) {
|
|
193
|
+
return askConfirm(rl, label, defaultValue);
|
|
194
|
+
},
|
|
195
|
+
async close() {
|
|
196
|
+
rl.close();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function askText(rl, label, defaultValue = "", { validate = null } = {}) {
|
|
202
|
+
while (true) {
|
|
203
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
204
|
+
const answer = (await rl.question(`${label}${suffix}: `)).trim();
|
|
205
|
+
const value = answer || String(defaultValue ?? "").trim();
|
|
206
|
+
|
|
207
|
+
if (!validate) {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
const error = validate(value);
|
|
211
|
+
if (!error) {
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
rl.output.write(`${error}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function askConfirm(rl, label, defaultValue = false) {
|
|
219
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
220
|
+
while (true) {
|
|
221
|
+
const answer = (await rl.question(`${label}? [${hint}]: `)).trim().toLowerCase();
|
|
222
|
+
if (!answer) {
|
|
223
|
+
return defaultValue;
|
|
224
|
+
}
|
|
225
|
+
if (["y", "yes"].includes(answer)) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (["n", "no"].includes(answer)) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
rl.output.write("please answer yes or no\n");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function requireValue(value) {
|
|
236
|
+
return String(value ?? "").trim() ? null : "a value is required";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function requirePositiveNumber(value) {
|
|
240
|
+
if (!String(value ?? "").trim()) {
|
|
241
|
+
return "a value is required";
|
|
242
|
+
}
|
|
243
|
+
const number = Number(value);
|
|
244
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
245
|
+
return "enter a positive number";
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function emptyToNull(value) {
|
|
251
|
+
const normalized = String(value ?? "").trim();
|
|
252
|
+
return normalized ? normalized : null;
|
|
253
|
+
}
|
package/lib/repo-init.js
CHANGED
|
@@ -5,6 +5,8 @@ import { connect, initDb } from "./db.js";
|
|
|
5
5
|
import { maskConnection } from "./cli-common.js";
|
|
6
6
|
|
|
7
7
|
const PACKAGE_NAME = "@nzpr/kb";
|
|
8
|
+
const REPO_ROOT_LAYOUT = "repo-root";
|
|
9
|
+
const NESTED_KB_LAYOUT = "nested-kb";
|
|
8
10
|
const LABELS = Object.freeze([
|
|
9
11
|
{
|
|
10
12
|
name: "kb-entry",
|
|
@@ -18,14 +20,19 @@ const LABELS = Object.freeze([
|
|
|
18
20
|
}
|
|
19
21
|
]);
|
|
20
22
|
|
|
21
|
-
export function initializeKnowledgeRepo({
|
|
23
|
+
export function initializeKnowledgeRepo({
|
|
24
|
+
targetDir = process.cwd(),
|
|
25
|
+
layout = NESTED_KB_LAYOUT
|
|
26
|
+
} = {}) {
|
|
22
27
|
const root = path.resolve(targetDir);
|
|
28
|
+
const normalizedLayout = normalizeKnowledgeLayout(layout);
|
|
29
|
+
const docsRootRelative = docsRootForLayout(normalizedLayout);
|
|
23
30
|
const files = new Map([
|
|
24
31
|
[".github/ISSUE_TEMPLATE/config.yml", renderIssueConfig()],
|
|
25
|
-
[".github/ISSUE_TEMPLATE/knowledge-document.md", renderIssueTemplate()],
|
|
26
|
-
[".github/workflows/kb-issue-to-pr.yml", renderIssueToPrWorkflow()],
|
|
27
|
-
[".github/workflows/kb-publish.yml", renderPublishWorkflow()],
|
|
28
|
-
[
|
|
32
|
+
[".github/ISSUE_TEMPLATE/knowledge-document.md", renderIssueTemplate({ docsRootRelative })],
|
|
33
|
+
[".github/workflows/kb-issue-to-pr.yml", renderIssueToPrWorkflow({ docsRootRelative })],
|
|
34
|
+
[".github/workflows/kb-publish.yml", renderPublishWorkflow({ docsRootRelative })],
|
|
35
|
+
[`${docsRootRelative}/.gitkeep`, ""]
|
|
29
36
|
]);
|
|
30
37
|
|
|
31
38
|
const created = [];
|
|
@@ -44,6 +51,8 @@ export function initializeKnowledgeRepo({ targetDir = process.cwd() } = {}) {
|
|
|
44
51
|
|
|
45
52
|
return {
|
|
46
53
|
root,
|
|
54
|
+
layout: normalizedLayout,
|
|
55
|
+
docsRootRelative,
|
|
47
56
|
created,
|
|
48
57
|
skipped,
|
|
49
58
|
configuration: buildConfigurationGuide()
|
|
@@ -52,6 +61,7 @@ export function initializeKnowledgeRepo({ targetDir = process.cwd() } = {}) {
|
|
|
52
61
|
|
|
53
62
|
export async function bootstrapKnowledgeRepo({
|
|
54
63
|
targetDir = process.cwd(),
|
|
64
|
+
layout = NESTED_KB_LAYOUT,
|
|
55
65
|
repo = null,
|
|
56
66
|
githubToken = null,
|
|
57
67
|
databaseUrl = null,
|
|
@@ -64,7 +74,7 @@ export async function bootstrapKnowledgeRepo({
|
|
|
64
74
|
runGitHubCommand = defaultRunGitHubCommand,
|
|
65
75
|
verifyDatabaseReady = defaultVerifyDatabaseReady
|
|
66
76
|
} = {}) {
|
|
67
|
-
const scaffold = initializeKnowledgeRepo({ targetDir });
|
|
77
|
+
const scaffold = initializeKnowledgeRepo({ targetDir, layout });
|
|
68
78
|
const result = {
|
|
69
79
|
...scaffold,
|
|
70
80
|
ok: true,
|
|
@@ -284,7 +294,7 @@ function renderIssueConfig() {
|
|
|
284
294
|
return ["blank_issues_enabled: false", ""].join("\n");
|
|
285
295
|
}
|
|
286
296
|
|
|
287
|
-
function renderIssueTemplate() {
|
|
297
|
+
function renderIssueTemplate({ docsRootRelative }) {
|
|
288
298
|
return [
|
|
289
299
|
"---",
|
|
290
300
|
"name: Knowledge Base Document",
|
|
@@ -313,12 +323,12 @@ function renderIssueTemplate() {
|
|
|
313
323
|
"",
|
|
314
324
|
"1. Open the issue with this template.",
|
|
315
325
|
"2. Review and edit the issue until the title and text are ready.",
|
|
316
|
-
|
|
326
|
+
`3. Add the \`kb-approved\` label to generate a PR that writes the Markdown file into \`${docsRootRelative}/\`.`,
|
|
317
327
|
""
|
|
318
328
|
].join("\n");
|
|
319
329
|
}
|
|
320
330
|
|
|
321
|
-
function renderIssueToPrWorkflow() {
|
|
331
|
+
function renderIssueToPrWorkflow({ docsRootRelative }) {
|
|
322
332
|
return [
|
|
323
333
|
"name: kb-issue-to-pr",
|
|
324
334
|
"",
|
|
@@ -353,7 +363,7 @@ function renderIssueToPrWorkflow() {
|
|
|
353
363
|
"",
|
|
354
364
|
" - name: Materialize approved issue",
|
|
355
365
|
" id: materialize",
|
|
356
|
-
|
|
366
|
+
` run: kb-admin issue-to-doc --issue-event "$GITHUB_EVENT_PATH" --docs-root ./${docsRootRelative}`,
|
|
357
367
|
"",
|
|
358
368
|
" - name: Create pull request",
|
|
359
369
|
" id: cpr",
|
|
@@ -382,7 +392,7 @@ function renderIssueToPrWorkflow() {
|
|
|
382
392
|
].join("\n");
|
|
383
393
|
}
|
|
384
394
|
|
|
385
|
-
function renderPublishWorkflow() {
|
|
395
|
+
function renderPublishWorkflow({ docsRootRelative }) {
|
|
386
396
|
return [
|
|
387
397
|
"name: kb-publish",
|
|
388
398
|
"",
|
|
@@ -391,7 +401,7 @@ function renderPublishWorkflow() {
|
|
|
391
401
|
" branches:",
|
|
392
402
|
" - main",
|
|
393
403
|
" paths:",
|
|
394
|
-
|
|
404
|
+
` - "${docsRootRelative}/**"`,
|
|
395
405
|
' - ".github/workflows/**"',
|
|
396
406
|
" workflow_dispatch:",
|
|
397
407
|
"",
|
|
@@ -432,7 +442,18 @@ function renderPublishWorkflow() {
|
|
|
432
442
|
` run: npm install -g ${PACKAGE_NAME}`,
|
|
433
443
|
"",
|
|
434
444
|
" - name: Publish knowledge",
|
|
435
|
-
|
|
445
|
+
` run: kb publish --docs-root ./${docsRootRelative}`,
|
|
436
446
|
""
|
|
437
447
|
].join("\n");
|
|
438
448
|
}
|
|
449
|
+
|
|
450
|
+
function normalizeKnowledgeLayout(layout) {
|
|
451
|
+
if (layout === REPO_ROOT_LAYOUT || layout === NESTED_KB_LAYOUT) {
|
|
452
|
+
return layout;
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`unsupported init-repo layout: ${layout}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function docsRootForLayout(layout) {
|
|
458
|
+
return layout === REPO_ROOT_LAYOUT ? "docs" : "kb/docs";
|
|
459
|
+
}
|