@gitpagedocs/cli 0.0.1
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 +193 -0
- package/ai/application/ai-command.ts +24 -0
- package/ai/application/ai-provider-factory.ts +22 -0
- package/ai/application/docs-pattern.ts +72 -0
- package/ai/application/resolve-chat-credentials.ts +69 -0
- package/ai/application/run-ai-cli-command.ts +167 -0
- package/ai/config.ts +50 -0
- package/ai/core/chat-session.ts +44 -0
- package/ai/core/models/ai-cli-config.ts +33 -0
- package/ai/core/models/parse-ai-cli-config.ts +76 -0
- package/ai/core/ports/illm-provider.ts +32 -0
- package/ai/infrastructure/ai-config-file.ts +101 -0
- package/ai/infrastructure/file-system-adapter.ts +85 -0
- package/ai/infrastructure/llm/tools-llm-provider.ts +54 -0
- package/ai/infrastructure/version-docs-writer.ts +143 -0
- package/ai/presentation/ai-prompts.ts +239 -0
- package/application/config-only/handler.mjs +57 -0
- package/application/home/handler.mjs +87 -0
- package/application/ports/cli-runtime-ports.ts +93 -0
- package/application/report/config-only-reporter.mjs +68 -0
- package/application/use-cases/dispatch-mode.ts +18 -0
- package/builders/config-orchestrator.mjs +35 -0
- package/builders/language-bundles-builder.mjs +24 -0
- package/builders/project-links.mjs +50 -0
- package/builders/root-config-builder.d.mts +16 -0
- package/builders/root-config-builder.mjs +56 -0
- package/builders/route-builders.mjs +136 -0
- package/builders/theme-template.mjs +76 -0
- package/builders/version-config-builder.mjs +186 -0
- package/content/docs-en.mjs +556 -0
- package/content/docs-es.mjs +469 -0
- package/content/docs-pt.mjs +469 -0
- package/content/docs.mjs +9 -0
- package/contracts/doc-versions.mjs +3 -0
- package/contracts/index.mjs +11 -0
- package/contracts/langs-paths.mjs +28 -0
- package/contracts/languages.mjs +6 -0
- package/contracts/layouts-paths.d.mts +17 -0
- package/contracts/layouts-paths.mjs +68 -0
- package/contracts/path-segments.d.mts +2 -0
- package/contracts/path-segments.mjs +15 -0
- package/data/default-site-config.mjs +328 -0
- package/data/i18n-langmenu.mjs +350 -0
- package/data/i18n-translations.mjs +20 -0
- package/data/layouts.mjs +437 -0
- package/data/path-mappings.mjs +26 -0
- package/data/route-metas.mjs +93 -0
- package/data/theme-colors.mjs +444 -0
- package/data/urls.mjs +12 -0
- package/data/version-constants.mjs +3 -0
- package/domain/models/cli-options.ts +33 -0
- package/domain/services/layouts-migration.d.mts +15 -0
- package/domain/services/layouts-migration.mjs +74 -0
- package/domain/services/sanitize-segment.mjs +7 -0
- package/home/constants.mjs +4 -0
- package/home/home-file-writer.mjs +19 -0
- package/home/templates.mjs +51 -0
- package/index.mjs +38 -0
- package/infrastructure/config-only/runtime.mjs +17 -0
- package/infrastructure/gitpagedocs-config-file.ts +47 -0
- package/infrastructure/home/runtime.mjs +37 -0
- package/package.json +60 -0
- package/presentation/commands/ai-info.ts +57 -0
- package/presentation/commands/chat.ts +259 -0
- package/presentation/commands/config-clear.ts +23 -0
- package/presentation/commands/config-info.ts +72 -0
- package/presentation/commands/diagnostics.ts +174 -0
- package/presentation/commands/docs.ts +42 -0
- package/presentation/commands/mcp.ts +20 -0
- package/presentation/commands/pages.ts +103 -0
- package/presentation/commands/password.ts +62 -0
- package/presentation/commands/run-command.ts +72 -0
- package/presentation/index.ts +125 -0
- package/presentation/options/parser.ts +120 -0
- package/presentation/options/resolver.ts +46 -0
- package/presentation/options/schema.ts +14 -0
- package/presentation/ui/banner.ts +13 -0
- package/presentation/ui/clack.ts +91 -0
- package/presentation/ui/layouts-prompts.ts +124 -0
- package/presentation/ui/prompts.ts +147 -0
- package/presentation/ui/terminal-stream.ts +97 -0
- package/presentation/ui/tty.ts +37 -0
- package/runtime/doc-path-resolver.mjs +59 -0
- package/runtime/exec.mjs +168 -0
- package/runtime/git-ops.mjs +112 -0
- package/runtime/layouts-migrator.d.mts +11 -0
- package/runtime/layouts-migrator.mjs +117 -0
- package/runtime/output.mjs +201 -0
- package/runtime/workflow.mjs +131 -0
- package/start.mjs +14 -0
- package/ui/logger.mjs +9 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
8
|
+
const scriptDir = path.dirname(scriptPath);
|
|
9
|
+
const tsEntry = path.join(scriptDir, "presentation", "index.ts");
|
|
10
|
+
|
|
11
|
+
// Resolve tsx from THIS package's location, not the user's cwd. `node --import
|
|
12
|
+
// tsx` would otherwise resolve the bare "tsx" specifier relative to the current
|
|
13
|
+
// working directory, which fails when the CLI is installed globally and run
|
|
14
|
+
// from a project that doesn't depend on tsx (ERR_MODULE_NOT_FOUND 'tsx').
|
|
15
|
+
let tsxLoader = "tsx";
|
|
16
|
+
try {
|
|
17
|
+
tsxLoader = import.meta.resolve("tsx");
|
|
18
|
+
} catch {
|
|
19
|
+
// Older Node without a stable import.meta.resolve — fall back to the bare
|
|
20
|
+
// specifier (works when tsx is resolvable from the cwd or globally).
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = spawnSync(
|
|
24
|
+
process.execPath,
|
|
25
|
+
["--import", tsxLoader, tsEntry, ...process.argv.slice(2)],
|
|
26
|
+
{
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
env: process.env,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
console.error("[gitpagedocs] Failed to run TypeScript CLI runtime.", result.error);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
} else if (typeof result.status === "number" && result.status !== 0) {
|
|
37
|
+
process.exitCode = result.status;
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getCurrentGitBranch, runGitPushForGeneratedArtifacts } from "../../runtime/git-ops.mjs";
|
|
3
|
+
import { writeConfigOnlyOutput, writeText } from "../../runtime/output.mjs";
|
|
4
|
+
import { ensureGitHubPagesWorkflow } from "../../runtime/workflow.mjs";
|
|
5
|
+
import { logInfo } from "../../ui/logger.mjs";
|
|
6
|
+
|
|
7
|
+
export function createConfigOnlyRuntime() {
|
|
8
|
+
return {
|
|
9
|
+
hasPath: existsSync,
|
|
10
|
+
writeConfigOnlyOutput,
|
|
11
|
+
writeText,
|
|
12
|
+
ensureGitHubPagesWorkflow,
|
|
13
|
+
getCurrentGitBranch,
|
|
14
|
+
runGitPushForGeneratedArtifacts,
|
|
15
|
+
logInfo,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { defaultConfigLoader } from "@gitpagedocs/tools";
|
|
3
|
+
|
|
4
|
+
export interface DocsAccessConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
publicKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads and patches the generated gitpagedocs/config.json IN PLACE, preserving
|
|
11
|
+
* every existing key (raw-JSON round-trip — it never reserializes through a typed
|
|
12
|
+
* model that could drop unknown fields). Used by interactive commands that need
|
|
13
|
+
* to persist a single value (e.g. the documentation password public key).
|
|
14
|
+
*/
|
|
15
|
+
export class GitPageDocsConfigRepository {
|
|
16
|
+
constructor(private readonly cwd: string = process.cwd()) {}
|
|
17
|
+
|
|
18
|
+
/** Absolute path to the JSON config, or throws a friendly, actionable error. */
|
|
19
|
+
async resolvePath(): Promise<string> {
|
|
20
|
+
const resolved = await defaultConfigLoader.resolveConfigPath(this.cwd);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
throw new Error("gitpagedocs config.json not found. Run `gitpagedocs` first to generate it.");
|
|
23
|
+
}
|
|
24
|
+
if (!resolved.endsWith(".json")) {
|
|
25
|
+
throw new Error(`Expected a JSON config to patch, found "${resolved}". Use a gitpagedocs/config.json.`);
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set `site.docsAccess = { enabled, publicKey }`, leaving all other config keys
|
|
32
|
+
* untouched. Returns the path that was written.
|
|
33
|
+
*/
|
|
34
|
+
async patchDocsAccess(publicKey: string, enabled = true): Promise<string> {
|
|
35
|
+
const configPath = await this.resolvePath();
|
|
36
|
+
const raw = await fs.readFile(configPath, "utf-8");
|
|
37
|
+
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
38
|
+
const site =
|
|
39
|
+
config.site && typeof config.site === "object"
|
|
40
|
+
? (config.site as Record<string, unknown>)
|
|
41
|
+
: {};
|
|
42
|
+
site.docsAccess = { enabled, publicKey } satisfies DocsAccessConfig;
|
|
43
|
+
config.site = site;
|
|
44
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
45
|
+
return configPath;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { runExecutable } from "../../runtime/exec.mjs";
|
|
4
|
+
import { writeConfigOnlyOutput, writeText } from "../../runtime/output.mjs";
|
|
5
|
+
import { writeHomeFiles } from "../../home/home-file-writer.mjs";
|
|
6
|
+
import { logInfo, logSuccess } from "../../ui/logger.mjs";
|
|
7
|
+
|
|
8
|
+
function ensureDirEmpty(root, relativeDir) {
|
|
9
|
+
const target = path.join(root, relativeDir);
|
|
10
|
+
if (existsSync(target)) {
|
|
11
|
+
rmSync(target, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function copyRecursive(src, dest) {
|
|
16
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runNextBuild(root, env) {
|
|
20
|
+
runExecutable("npx", ["next", "build", "frontend"], { cwd: root, env, stdio: "inherit" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createHomeRuntime() {
|
|
24
|
+
return {
|
|
25
|
+
joinPath: (...parts) => path.join(...parts),
|
|
26
|
+
ensureDirEmpty,
|
|
27
|
+
copyRecursive,
|
|
28
|
+
readDirNames: (absolutePath) => readdirSync(absolutePath),
|
|
29
|
+
existsPath: existsSync,
|
|
30
|
+
runNextBuild,
|
|
31
|
+
writeConfigOnlyOutput,
|
|
32
|
+
writeHomeFiles,
|
|
33
|
+
writeText,
|
|
34
|
+
logSuccess,
|
|
35
|
+
logInfo,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gitpagedocs/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gitpagedocs": "index.mjs"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.mjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"start.mjs",
|
|
12
|
+
"ai",
|
|
13
|
+
"application",
|
|
14
|
+
"builders",
|
|
15
|
+
"content",
|
|
16
|
+
"contracts",
|
|
17
|
+
"data",
|
|
18
|
+
"domain",
|
|
19
|
+
"home",
|
|
20
|
+
"infrastructure",
|
|
21
|
+
"presentation",
|
|
22
|
+
"runtime",
|
|
23
|
+
"ui",
|
|
24
|
+
"prebuilt",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@clack/prompts": "^1.5.1",
|
|
32
|
+
"@gitpagedocs/mcp": "^0.0.1",
|
|
33
|
+
"@gitpagedocs/tools": "^0.0.1",
|
|
34
|
+
"tsx": "^4.21.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/Vidigal-code/git-page-docs.git",
|
|
39
|
+
"directory": "cli"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"gitpagedocs",
|
|
43
|
+
"documentation",
|
|
44
|
+
"markdown",
|
|
45
|
+
"spa",
|
|
46
|
+
"cli",
|
|
47
|
+
"github-pages",
|
|
48
|
+
"create-gitpagedocs"
|
|
49
|
+
],
|
|
50
|
+
"author": "Vidigal-code",
|
|
51
|
+
"license": "ISC",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Vidigal-code/git-page-docs/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://vidigal-code.github.io/git-page-docs/",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"registry": "https://registry.npmjs.org/",
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PROVIDER_CATALOG, ALL_PROVIDER_IDS, createModelRegistry, type AiProviderId } from "@gitpagedocs/tools";
|
|
2
|
+
import type { CommandContext } from "./run-command";
|
|
3
|
+
|
|
4
|
+
function capsLabel(caps: { streaming: boolean; vision: boolean; audio: boolean }): string {
|
|
5
|
+
const parts: string[] = [];
|
|
6
|
+
if (caps.streaming) parts.push("stream");
|
|
7
|
+
if (caps.vision) parts.push("vision");
|
|
8
|
+
if (caps.audio) parts.push("audio");
|
|
9
|
+
return parts.join(", ") || "text";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `gitpagedocs provider [id]` — list providers or show one provider's detail. */
|
|
13
|
+
export async function runProvider(ctx: CommandContext): Promise<void> {
|
|
14
|
+
const requested = ctx.args[1] as AiProviderId | undefined;
|
|
15
|
+
if (requested && PROVIDER_CATALOG[requested]) {
|
|
16
|
+
const spec = PROVIDER_CATALOG[requested];
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.log(
|
|
19
|
+
`\n ${spec.label} (${spec.id})\n` +
|
|
20
|
+
` default model : ${spec.defaultModel}\n` +
|
|
21
|
+
` capabilities : ${capsLabel(spec.capabilities)}\n` +
|
|
22
|
+
` base url : ${spec.baseUrl || "(user-provided)"}\n` +
|
|
23
|
+
` models : ${spec.models.map((m) => m.id).join(", ")}\n`,
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.log(`\n AI providers (${ALL_PROVIDER_IDS.length}):\n`);
|
|
30
|
+
for (const id of ALL_PROVIDER_IDS) {
|
|
31
|
+
const spec = PROVIDER_CATALOG[id];
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.log(` ${id.padEnd(14)} ${spec.label.padEnd(18)} ${spec.defaultModel}`);
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.log("\n Detail: gitpagedocs provider <id>\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `gitpagedocs models [provider]` — list models for one or all providers. */
|
|
40
|
+
export async function runModels(ctx: CommandContext): Promise<void> {
|
|
41
|
+
const models = createModelRegistry();
|
|
42
|
+
const requested = ctx.args[1] as AiProviderId | undefined;
|
|
43
|
+
const ids = requested && PROVIDER_CATALOG[requested] ? [requested] : ALL_PROVIDER_IDS;
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.log("");
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
const list = models.list(id);
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.log(` ${PROVIDER_CATALOG[id].label} (${id}):`);
|
|
50
|
+
for (const m of list) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log(` - ${m.id}${m.id === PROVIDER_CATALOG[id].defaultModel ? " (default)" : ""}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log("");
|
|
57
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { createDefaultFactory } from "@gitpagedocs/tools/ai";
|
|
3
|
+
import { PROVIDER_CATALOG } from "@gitpagedocs/tools";
|
|
4
|
+
import type { AIProvider, ProviderConfig } from "@gitpagedocs/tools/ports";
|
|
5
|
+
import { ChatSession } from "../../ai/core/chat-session";
|
|
6
|
+
import { resolveChatCredentials, type ResolvedChatCredentials } from "../../ai/application/resolve-chat-credentials";
|
|
7
|
+
import {
|
|
8
|
+
ElapsedSpinner,
|
|
9
|
+
accent,
|
|
10
|
+
danger,
|
|
11
|
+
writeChrome,
|
|
12
|
+
writeContent,
|
|
13
|
+
} from "../ui/terminal-stream";
|
|
14
|
+
import type { CommandContext } from "./run-command";
|
|
15
|
+
|
|
16
|
+
/** Positions the model as a documentation assistant for the current project. */
|
|
17
|
+
const DEFAULT_CHAT_SYSTEM_PROMPT = [
|
|
18
|
+
"You are a concise documentation and coding assistant embedded in the gitpagedocs CLI.",
|
|
19
|
+
"Answer questions about the user's project, source code and documentation.",
|
|
20
|
+
"Prefer short, correct, actionable answers; use GitHub-flavored markdown for code.",
|
|
21
|
+
].join(" ");
|
|
22
|
+
|
|
23
|
+
const EXIT_REARM_MS = 3000;
|
|
24
|
+
|
|
25
|
+
interface ChatFlags {
|
|
26
|
+
provider?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
system?: string;
|
|
29
|
+
question?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseChatArgs(args: readonly string[]): ChatFlags {
|
|
33
|
+
// args[0] is the verb ("chat"); parse flags and collect the free-text question.
|
|
34
|
+
const flags: ChatFlags = {};
|
|
35
|
+
const positional: string[] = [];
|
|
36
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
37
|
+
const arg = args[i];
|
|
38
|
+
if (arg === "--provider" || arg === "--model" || arg === "--system") {
|
|
39
|
+
const value = args[i + 1];
|
|
40
|
+
if (value !== undefined) {
|
|
41
|
+
if (arg === "--provider") flags.provider = value;
|
|
42
|
+
else if (arg === "--model") flags.model = value;
|
|
43
|
+
else flags.system = value;
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg.startsWith("--")) continue;
|
|
49
|
+
positional.push(arg);
|
|
50
|
+
}
|
|
51
|
+
const question = positional.join(" ").trim();
|
|
52
|
+
if (question) flags.question = question;
|
|
53
|
+
return flags;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readPipedStdin(): Promise<string> {
|
|
57
|
+
if (process.stdin.isTTY) return "";
|
|
58
|
+
const chunks: Buffer[] = [];
|
|
59
|
+
for await (const chunk of process.stdin) {
|
|
60
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
61
|
+
}
|
|
62
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Streams one assistant turn to stdout. Cancellation is a scoped concern: the
|
|
67
|
+
* caller owns the AbortController and the stream stops the moment it aborts
|
|
68
|
+
* (the async generator's cleanup tears down the HTTP request). Returns whether
|
|
69
|
+
* the turn completed (false = aborted or errored).
|
|
70
|
+
*/
|
|
71
|
+
async function streamTurn(
|
|
72
|
+
provider: AIProvider,
|
|
73
|
+
config: ProviderConfig,
|
|
74
|
+
session: ChatSession,
|
|
75
|
+
userText: string,
|
|
76
|
+
signal: AbortSignal,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
session.addUser(userText);
|
|
79
|
+
const spinner = new ElapsedSpinner("Thinking");
|
|
80
|
+
spinner.start();
|
|
81
|
+
let full = "";
|
|
82
|
+
let started = false;
|
|
83
|
+
try {
|
|
84
|
+
for await (const delta of provider.stream(
|
|
85
|
+
{ messages: session.buildRequestMessages(), system: session.system, signal },
|
|
86
|
+
config,
|
|
87
|
+
)) {
|
|
88
|
+
if (!started) {
|
|
89
|
+
spinner.stop();
|
|
90
|
+
started = true;
|
|
91
|
+
}
|
|
92
|
+
full += delta;
|
|
93
|
+
writeContent(delta);
|
|
94
|
+
}
|
|
95
|
+
if (started) writeContent("\n");
|
|
96
|
+
session.addAssistant(full);
|
|
97
|
+
writeChrome(
|
|
98
|
+
`${PROVIDER_CATALOG[config.providerId].label} · ${config.model} · ${spinner.elapsedSeconds().toFixed(1)}s`,
|
|
99
|
+
);
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
spinner.stop();
|
|
103
|
+
if (signal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
104
|
+
if (started) writeContent("\n");
|
|
105
|
+
writeChrome(danger("Interrupted."));
|
|
106
|
+
// Keep the partial reply in history so follow-ups have context.
|
|
107
|
+
session.addAssistant(full);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
writeChrome(danger(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
111
|
+
return false;
|
|
112
|
+
} finally {
|
|
113
|
+
spinner.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One-shot mode: a single question in, the answer out, then exit. */
|
|
118
|
+
async function runOneShot(
|
|
119
|
+
provider: AIProvider,
|
|
120
|
+
config: ProviderConfig,
|
|
121
|
+
session: ChatSession,
|
|
122
|
+
question: string,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const onSigint = () => controller.abort();
|
|
126
|
+
process.on("SIGINT", onSigint);
|
|
127
|
+
try {
|
|
128
|
+
const ok = await streamTurn(provider, config, session, question, controller.signal);
|
|
129
|
+
if (!ok) process.exitCode = 1;
|
|
130
|
+
} finally {
|
|
131
|
+
process.off("SIGINT", onSigint);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Interactive REPL: requires a TTY on both ends. */
|
|
136
|
+
async function runRepl(
|
|
137
|
+
provider: AIProvider,
|
|
138
|
+
config: ProviderConfig,
|
|
139
|
+
session: ChatSession,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const spec = PROVIDER_CATALOG[config.providerId];
|
|
142
|
+
writeChrome(`Chat — ${spec.label} · ${config.model}`);
|
|
143
|
+
writeChrome("Commands: /clear reset · /help · /exit (or Ctrl+C twice)");
|
|
144
|
+
|
|
145
|
+
const rl = readline.createInterface({
|
|
146
|
+
input: process.stdin,
|
|
147
|
+
output: process.stderr,
|
|
148
|
+
prompt: accent("› "),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let generating: AbortController | null = null;
|
|
152
|
+
let exitArmed = false;
|
|
153
|
+
let exitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
154
|
+
|
|
155
|
+
rl.on("SIGINT", () => {
|
|
156
|
+
if (generating) {
|
|
157
|
+
generating.abort();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (exitArmed) {
|
|
161
|
+
rl.close();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
exitArmed = true;
|
|
165
|
+
writeChrome("Press Ctrl+C again to exit.");
|
|
166
|
+
exitTimer = setTimeout(() => {
|
|
167
|
+
exitArmed = false;
|
|
168
|
+
}, EXIT_REARM_MS);
|
|
169
|
+
rl.prompt();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
rl.prompt();
|
|
173
|
+
for await (const line of rl) {
|
|
174
|
+
if (exitTimer) {
|
|
175
|
+
clearTimeout(exitTimer);
|
|
176
|
+
exitTimer = null;
|
|
177
|
+
}
|
|
178
|
+
exitArmed = false;
|
|
179
|
+
const input = line.trim();
|
|
180
|
+
if (!input) {
|
|
181
|
+
rl.prompt();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (input === "/exit" || input === "/quit") break;
|
|
185
|
+
if (input === "/clear") {
|
|
186
|
+
session.clear();
|
|
187
|
+
writeChrome("Conversation cleared.");
|
|
188
|
+
rl.prompt();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (input === "/help") {
|
|
192
|
+
writeChrome("Type a message to chat. /clear resets history, /exit quits, Ctrl+C interrupts a reply.");
|
|
193
|
+
rl.prompt();
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
generating = new AbortController();
|
|
197
|
+
await streamTurn(provider, config, session, input, generating.signal);
|
|
198
|
+
generating = null;
|
|
199
|
+
rl.prompt();
|
|
200
|
+
}
|
|
201
|
+
rl.close();
|
|
202
|
+
writeChrome("Bye.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printSetupHelp(providerHint: string): void {
|
|
206
|
+
writeChrome(danger("No AI credentials found."));
|
|
207
|
+
writeChrome(`Set an API key via environment variable (e.g. ${providerHint}) or run: gitpagedocs ai`);
|
|
208
|
+
writeChrome("Then: gitpagedocs chat \"your question\" · or just: gitpagedocs chat");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `gitpagedocs chat [question]` — interactive AI chat in the terminal.
|
|
213
|
+
* Flags: --provider <id>, --model <id>, --system <prompt>.
|
|
214
|
+
* One-shot when a question is given or stdin is piped; a REPL on a TTY.
|
|
215
|
+
*/
|
|
216
|
+
export async function runChat(ctx: CommandContext): Promise<void> {
|
|
217
|
+
const flags = parseChatArgs(ctx.args);
|
|
218
|
+
const creds: ResolvedChatCredentials | null = await resolveChatCredentials({
|
|
219
|
+
cwd: ctx.cwd,
|
|
220
|
+
providerOverride: flags.provider,
|
|
221
|
+
modelOverride: flags.model,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!creds) {
|
|
225
|
+
const hintProvider = flags.provider ?? "openai";
|
|
226
|
+
const hint = PROVIDER_CATALOG[
|
|
227
|
+
(hintProvider in PROVIDER_CATALOG ? hintProvider : "openai") as keyof typeof PROVIDER_CATALOG
|
|
228
|
+
].envVars[0] ?? "OPENAI_API_KEY";
|
|
229
|
+
printSetupHelp(hint);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const provider = createDefaultFactory().create(creds.providerId);
|
|
235
|
+
const config: ProviderConfig = {
|
|
236
|
+
providerId: creds.providerId,
|
|
237
|
+
model: creds.model,
|
|
238
|
+
apiKey: creds.apiKey,
|
|
239
|
+
baseUrl: creds.baseUrl,
|
|
240
|
+
};
|
|
241
|
+
const session = new ChatSession(flags.system ?? DEFAULT_CHAT_SYSTEM_PROMPT);
|
|
242
|
+
|
|
243
|
+
const piped = await readPipedStdin();
|
|
244
|
+
const oneShot = [flags.question, piped].filter(Boolean).join("\n\n").trim();
|
|
245
|
+
|
|
246
|
+
if (oneShot) {
|
|
247
|
+
await runOneShot(provider, config, session, oneShot);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
252
|
+
writeChrome(danger("No question given and no interactive terminal available."));
|
|
253
|
+
writeChrome('Usage: gitpagedocs chat "your question"');
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await runRepl(provider, config, session);
|
|
259
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AiConfigFileRepository } from "../../ai/infrastructure/ai-config-file";
|
|
2
|
+
import type { CommandContext } from "./run-command";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `gitpagedocs config clear` — delete the stored `.gitpagedocsconfig` from the
|
|
6
|
+
* per-user config directory and any legacy repo-root copy, wiping the saved AI
|
|
7
|
+
* credentials (API key, provider, scan paths).
|
|
8
|
+
*/
|
|
9
|
+
export async function runConfigClear(ctx: CommandContext): Promise<void> {
|
|
10
|
+
const repository = new AiConfigFileRepository({ cwd: ctx.cwd });
|
|
11
|
+
const removed = await repository.clear();
|
|
12
|
+
|
|
13
|
+
if (!removed.length) {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log("\n No stored .gitpagedocsconfig found - nothing to clear.\n");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(
|
|
21
|
+
["", " Stored AI configuration removed (credentials wiped):", ...removed.map((p) => ` - ${p}`), ""].join("\n"),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_LANGS_MANIFEST_PATH,
|
|
5
|
+
defaultConfigLoader,
|
|
6
|
+
getEnabledLanguages,
|
|
7
|
+
isAppError,
|
|
8
|
+
parseLanguageManifest,
|
|
9
|
+
parseLanguageToggles,
|
|
10
|
+
} from "@gitpagedocs/tools";
|
|
11
|
+
import type { CommandContext } from "./run-command";
|
|
12
|
+
|
|
13
|
+
const UNKNOWN = "—";
|
|
14
|
+
|
|
15
|
+
interface LanguageSummary {
|
|
16
|
+
enabled: string[];
|
|
17
|
+
disabled: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The 1.1.68 manifest, read only for sites whose config.json predates `site.languages`. */
|
|
21
|
+
async function readLegacyManifestLanguages(cwd: string): Promise<string[]> {
|
|
22
|
+
try {
|
|
23
|
+
const manifest = parseLanguageManifest(JSON.parse(await readFile(path.join(cwd, DEFAULT_LANGS_MANIFEST_PATH), "utf-8")));
|
|
24
|
+
if (manifest) return manifest.languages;
|
|
25
|
+
} catch {
|
|
26
|
+
// No manifest: nothing to report.
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `site.languages` toggles from config.json, else the legacy inline list, else the legacy manifest. */
|
|
32
|
+
async function summarizeLanguages(cwd: string, site: Record<string, unknown>): Promise<LanguageSummary> {
|
|
33
|
+
const toggles = parseLanguageToggles(site.languages);
|
|
34
|
+
if (toggles) {
|
|
35
|
+
const enabled = getEnabledLanguages(toggles);
|
|
36
|
+
return { enabled, disabled: Object.keys(toggles).filter((language) => !enabled.includes(language)) };
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(site.supportedLanguages)) {
|
|
39
|
+
return { enabled: site.supportedLanguages as string[], disabled: [] };
|
|
40
|
+
}
|
|
41
|
+
return { enabled: await readLegacyManifestLanguages(cwd), disabled: [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatList(languages: string[]): string {
|
|
45
|
+
return languages.length > 0 ? languages.join(", ") : UNKNOWN;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Scalars print as-is; anything else (missing, object) prints as unknown instead of "[object Object]". */
|
|
49
|
+
function formatScalar(value: unknown): string {
|
|
50
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : UNKNOWN;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `gitpagedocs config` — resolve and summarize the active gitpagedocs config. */
|
|
54
|
+
export async function runConfig(ctx: CommandContext): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const { config, sourcePath, extension } = await defaultConfigLoader.loadGitPageDocsConfig(ctx.cwd);
|
|
57
|
+
const site = (config as { site?: Record<string, unknown> }).site ?? {};
|
|
58
|
+
const languages = await summarizeLanguages(ctx.cwd, site);
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.log(
|
|
61
|
+
`\n Config: ${path.relative(ctx.cwd, sourcePath)} (${extension})\n` +
|
|
62
|
+
` site name : ${formatScalar(site.name)}\n` +
|
|
63
|
+
` default language : ${formatScalar(site.defaultLanguage)}\n` +
|
|
64
|
+
` languages : ${formatList(languages.enabled)}\n` +
|
|
65
|
+
` disabled : ${formatList(languages.disabled)}\n`,
|
|
66
|
+
);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = isAppError(error) ? error.message : String(error);
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.log(`\n No usable config found: ${message}\n Run \`gitpagedocs init\` to generate one.\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|