@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.
Files changed (91) hide show
  1. package/README.md +193 -0
  2. package/ai/application/ai-command.ts +24 -0
  3. package/ai/application/ai-provider-factory.ts +22 -0
  4. package/ai/application/docs-pattern.ts +72 -0
  5. package/ai/application/resolve-chat-credentials.ts +69 -0
  6. package/ai/application/run-ai-cli-command.ts +167 -0
  7. package/ai/config.ts +50 -0
  8. package/ai/core/chat-session.ts +44 -0
  9. package/ai/core/models/ai-cli-config.ts +33 -0
  10. package/ai/core/models/parse-ai-cli-config.ts +76 -0
  11. package/ai/core/ports/illm-provider.ts +32 -0
  12. package/ai/infrastructure/ai-config-file.ts +101 -0
  13. package/ai/infrastructure/file-system-adapter.ts +85 -0
  14. package/ai/infrastructure/llm/tools-llm-provider.ts +54 -0
  15. package/ai/infrastructure/version-docs-writer.ts +143 -0
  16. package/ai/presentation/ai-prompts.ts +239 -0
  17. package/application/config-only/handler.mjs +57 -0
  18. package/application/home/handler.mjs +87 -0
  19. package/application/ports/cli-runtime-ports.ts +93 -0
  20. package/application/report/config-only-reporter.mjs +68 -0
  21. package/application/use-cases/dispatch-mode.ts +18 -0
  22. package/builders/config-orchestrator.mjs +35 -0
  23. package/builders/language-bundles-builder.mjs +24 -0
  24. package/builders/project-links.mjs +50 -0
  25. package/builders/root-config-builder.d.mts +16 -0
  26. package/builders/root-config-builder.mjs +56 -0
  27. package/builders/route-builders.mjs +136 -0
  28. package/builders/theme-template.mjs +76 -0
  29. package/builders/version-config-builder.mjs +186 -0
  30. package/content/docs-en.mjs +556 -0
  31. package/content/docs-es.mjs +469 -0
  32. package/content/docs-pt.mjs +469 -0
  33. package/content/docs.mjs +9 -0
  34. package/contracts/doc-versions.mjs +3 -0
  35. package/contracts/index.mjs +11 -0
  36. package/contracts/langs-paths.mjs +28 -0
  37. package/contracts/languages.mjs +6 -0
  38. package/contracts/layouts-paths.d.mts +17 -0
  39. package/contracts/layouts-paths.mjs +68 -0
  40. package/contracts/path-segments.d.mts +2 -0
  41. package/contracts/path-segments.mjs +15 -0
  42. package/data/default-site-config.mjs +328 -0
  43. package/data/i18n-langmenu.mjs +350 -0
  44. package/data/i18n-translations.mjs +20 -0
  45. package/data/layouts.mjs +437 -0
  46. package/data/path-mappings.mjs +26 -0
  47. package/data/route-metas.mjs +93 -0
  48. package/data/theme-colors.mjs +444 -0
  49. package/data/urls.mjs +12 -0
  50. package/data/version-constants.mjs +3 -0
  51. package/domain/models/cli-options.ts +33 -0
  52. package/domain/services/layouts-migration.d.mts +15 -0
  53. package/domain/services/layouts-migration.mjs +74 -0
  54. package/domain/services/sanitize-segment.mjs +7 -0
  55. package/home/constants.mjs +4 -0
  56. package/home/home-file-writer.mjs +19 -0
  57. package/home/templates.mjs +51 -0
  58. package/index.mjs +38 -0
  59. package/infrastructure/config-only/runtime.mjs +17 -0
  60. package/infrastructure/gitpagedocs-config-file.ts +47 -0
  61. package/infrastructure/home/runtime.mjs +37 -0
  62. package/package.json +60 -0
  63. package/presentation/commands/ai-info.ts +57 -0
  64. package/presentation/commands/chat.ts +259 -0
  65. package/presentation/commands/config-clear.ts +23 -0
  66. package/presentation/commands/config-info.ts +72 -0
  67. package/presentation/commands/diagnostics.ts +174 -0
  68. package/presentation/commands/docs.ts +42 -0
  69. package/presentation/commands/mcp.ts +20 -0
  70. package/presentation/commands/pages.ts +103 -0
  71. package/presentation/commands/password.ts +62 -0
  72. package/presentation/commands/run-command.ts +72 -0
  73. package/presentation/index.ts +125 -0
  74. package/presentation/options/parser.ts +120 -0
  75. package/presentation/options/resolver.ts +46 -0
  76. package/presentation/options/schema.ts +14 -0
  77. package/presentation/ui/banner.ts +13 -0
  78. package/presentation/ui/clack.ts +91 -0
  79. package/presentation/ui/layouts-prompts.ts +124 -0
  80. package/presentation/ui/prompts.ts +147 -0
  81. package/presentation/ui/terminal-stream.ts +97 -0
  82. package/presentation/ui/tty.ts +37 -0
  83. package/runtime/doc-path-resolver.mjs +59 -0
  84. package/runtime/exec.mjs +168 -0
  85. package/runtime/git-ops.mjs +112 -0
  86. package/runtime/layouts-migrator.d.mts +11 -0
  87. package/runtime/layouts-migrator.mjs +117 -0
  88. package/runtime/output.mjs +201 -0
  89. package/runtime/workflow.mjs +131 -0
  90. package/start.mjs +14 -0
  91. package/ui/logger.mjs +9 -0
@@ -0,0 +1,174 @@
1
+ import path from "node:path";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { defaultConfigLoader } from "@gitpagedocs/tools";
4
+ import type { CommandContext } from "./run-command";
5
+ // @ts-expect-error .mjs runtime module is type-less in this package.
6
+ import { runExecutableCapture } from "../../runtime/exec.mjs";
7
+
8
+ interface DoctorCheck {
9
+ label: string;
10
+ ok: boolean;
11
+ detail: string;
12
+ }
13
+
14
+ function readVersion(pkgRoot: string): string {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
17
+ return String(pkg.version ?? "unknown");
18
+ } catch {
19
+ return "unknown";
20
+ }
21
+ }
22
+
23
+ export async function runVersion(ctx: CommandContext): Promise<void> {
24
+ // eslint-disable-next-line no-console
25
+ console.log(`gitpagedocs ${readVersion(ctx.pkgRoot)} (node ${process.version})`);
26
+ }
27
+
28
+ /** Run `<program> --version` (argv array, no shell) and keep its first output line. */
29
+ function probe(label: string, program: string): DoctorCheck {
30
+ try {
31
+ const out = String(runExecutableCapture(program, ["--version"], { stdio: ["ignore", "pipe", "ignore"] }));
32
+ return { label, ok: true, detail: out.split("\n")[0] };
33
+ } catch {
34
+ return { label, ok: false, detail: "not found" };
35
+ }
36
+ }
37
+
38
+ export async function runDoctor(ctx: CommandContext): Promise<void> {
39
+ const configPath = await defaultConfigLoader.resolveConfigPath(ctx.cwd);
40
+ const hasDocsDirectory = existsSync(path.join(ctx.cwd, "gitpagedocs"));
41
+ const checks: DoctorCheck[] = [
42
+ { label: "node", ok: true, detail: process.version },
43
+ probe("git", "git"),
44
+ probe("gh (GitHub CLI)", "gh"),
45
+ probe("pnpm", "pnpm"),
46
+ {
47
+ label: "gitpagedocs config",
48
+ ok: Boolean(configPath),
49
+ detail: configPath ? path.relative(ctx.cwd, configPath) : "not found (run `gitpagedocs init`)",
50
+ },
51
+ {
52
+ label: "gitpagedocs/ directory",
53
+ ok: hasDocsDirectory,
54
+ detail: hasDocsDirectory ? "present" : "missing",
55
+ },
56
+ ];
57
+
58
+ // eslint-disable-next-line no-console
59
+ console.log("\n gitpagedocs doctor\n");
60
+ for (const c of checks) {
61
+ // eslint-disable-next-line no-console
62
+ console.log(` ${c.ok ? "[ok]" : "[x] "} ${c.label.padEnd(22)} ${c.detail}`);
63
+ }
64
+ const failed = checks.filter((c) => !c.ok && c.label !== "gh (GitHub CLI)").length;
65
+ const summary = failed === 0 ? "All required checks passed." : `${failed} check(s) need attention.`;
66
+ // eslint-disable-next-line no-console
67
+ console.log(`\n ${summary}\n`);
68
+ }
69
+
70
+ /** Used only when package.json is unreadable — the CLI's published name. */
71
+ const FALLBACK_PACKAGE = "@gitpagedocs/cli";
72
+ /** The bin this package installs; shown alongside the package name. */
73
+ const BIN_NAME = "gitpagedocs";
74
+
75
+ interface PackageIdentity {
76
+ name: string;
77
+ version: string;
78
+ }
79
+
80
+ /**
81
+ * Read the identity from the installed package.json rather than hardcoding it,
82
+ * so the hint always names the package the user actually has. A hardcoded name
83
+ * is how `update` ended up advertising an unpublished package.
84
+ */
85
+ function readPackageIdentity(pkgRoot: string): PackageIdentity {
86
+ try {
87
+ const pkg = JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
88
+ const name = typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : FALLBACK_PACKAGE;
89
+ return { name, version: String(pkg.version ?? "unknown") };
90
+ } catch {
91
+ return { name: FALLBACK_PACKAGE, version: "unknown" };
92
+ }
93
+ }
94
+
95
+ type RegistryLookup =
96
+ | { status: "ok"; latest: string }
97
+ | { status: "missing" }
98
+ | { status: "unreachable" };
99
+
100
+ function registryBase(): string {
101
+ const configured = process.env.npm_config_registry || "https://registry.npmjs.org";
102
+ return configured.endsWith("/") ? configured.slice(0, -1) : configured;
103
+ }
104
+
105
+ /** Resolve the dist-tag `latest` for a package. Never throws. */
106
+ async function fetchLatest(name: string, timeoutMs = 6000): Promise<RegistryLookup> {
107
+ const encoded = name.split("/").map(encodeURIComponent).join("/");
108
+ try {
109
+ const response = await fetch(`${registryBase()}/${encoded}/latest`, {
110
+ signal: AbortSignal.timeout(timeoutMs),
111
+ });
112
+ if (response.status === 404) return { status: "missing" };
113
+ if (!response.ok) return { status: "unreachable" };
114
+ const body = (await response.json()) as { version?: unknown };
115
+ if (typeof body.version === "string" && body.version.length > 0) {
116
+ return { status: "ok", latest: body.version };
117
+ }
118
+ return { status: "unreachable" };
119
+ } catch {
120
+ return { status: "unreachable" };
121
+ }
122
+ }
123
+
124
+ /** Numeric version compare, prerelease suffix ignored. */
125
+ function compareVersions(a: string, b: string): number {
126
+ const parts = (value: string): number[] =>
127
+ value.split("-")[0].split(".").map((piece) => Number.parseInt(piece, 10) || 0);
128
+ const left = parts(a);
129
+ const right = parts(b);
130
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
131
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
132
+ if (diff !== 0) return diff < 0 ? -1 : 1;
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ export async function runUpdate(ctx: CommandContext): Promise<void> {
138
+ const { name, version } = readPackageIdentity(ctx.pkgRoot);
139
+ const lookup = await fetchLatest(name);
140
+ const label = name === BIN_NAME ? `${BIN_NAME} ${version}` : `${BIN_NAME} ${version} (${name})`;
141
+ const lines = ["", ` Installed: ${label}`];
142
+
143
+ if (lookup.status === "ok") {
144
+ lines.push(` Latest: ${lookup.latest}`);
145
+ } else if (lookup.status === "missing") {
146
+ lines.push(` Latest: unavailable — the registry has no published ${name}`);
147
+ } else {
148
+ lines.push(" Latest: unknown — could not reach the npm registry");
149
+ }
150
+
151
+ if (lookup.status === "missing") {
152
+ lines.push(
153
+ "",
154
+ " Nothing can be installed under that name. If the package was renamed",
155
+ " or unpublished, check the current one:",
156
+ " https://www.npmjs.com/package/@gitpagedocs/cli",
157
+ );
158
+ } else if (lookup.status === "ok" && compareVersions(version, lookup.latest) >= 0) {
159
+ lines.push("", " Already on the latest published version.");
160
+ } else {
161
+ const target = lookup.status === "ok" ? lookup.latest : "latest";
162
+ lines.push(
163
+ "",
164
+ " To update the global CLI:",
165
+ ` npm i -g ${name}@${target}`,
166
+ " Or, in a project:",
167
+ ` pnpm add -D ${name}@${target}`,
168
+ );
169
+ }
170
+
171
+ lines.push("");
172
+ // eslint-disable-next-line no-console
173
+ console.log(lines.join("\n"));
174
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ FileService,
3
+ DocUpdater,
4
+ providersSection,
5
+ cliCommandsSection,
6
+ devWorkflowSection,
7
+ securityNoteSection,
8
+ CLI_COMMANDS,
9
+ } from "@gitpagedocs/tools";
10
+ import type { CommandContext } from "./run-command";
11
+
12
+ /** Human-readable outcome of one managed-region update. */
13
+ function describeUpdate(result: { changed: boolean; replaced: boolean }): string {
14
+ if (!result.changed) return "already up to date";
15
+ return result.replaced ? "managed region updated" : "managed region added";
16
+ }
17
+
18
+ /**
19
+ * `gitpagedocs docs` — refresh the managed regions of the standard docs files
20
+ * with deterministic content. Idempotent and marker-bounded: manual content
21
+ * outside `<!-- gitpagedocs:start/end -->` is preserved; missing files are
22
+ * created. Covers README, CONTRIBUTING and SECURITY (master-spec docs set).
23
+ */
24
+ export async function runDocs(ctx: CommandContext): Promise<void> {
25
+ const updater = new DocUpdater(new FileService(ctx.cwd));
26
+
27
+ const updates = [
28
+ { path: "README.md", generated: `${providersSection()}\n\n${cliCommandsSection(CLI_COMMANDS)}` },
29
+ { path: "CONTRIBUTING.md", generated: `${devWorkflowSection()}\n\n${cliCommandsSection(CLI_COMMANDS)}` },
30
+ { path: "SECURITY.md", generated: securityNoteSection() },
31
+ ];
32
+
33
+ const results = await updater.updateManagedFiles(updates);
34
+ // eslint-disable-next-line no-console
35
+ console.log("");
36
+ for (const r of results) {
37
+ // eslint-disable-next-line no-console
38
+ console.log(` ${r.path}: ${describeUpdate(r)}.`);
39
+ }
40
+ // eslint-disable-next-line no-console
41
+ console.log("");
42
+ }
@@ -0,0 +1,20 @@
1
+ import type { CommandContext } from "./run-command";
2
+
3
+ /**
4
+ * `gitpagedocs mcp start` — entry point for the MCP server. The server package
5
+ * (@gitpagedocs/mcp) is implemented in Phase 7; this command is wired to launch
6
+ * it there. Until then it reports status honestly rather than failing silently.
7
+ */
8
+ export async function runMcp(ctx: CommandContext): Promise<void> {
9
+ const sub = ctx.args[1] ?? "start";
10
+ if (sub !== "start") {
11
+ // eslint-disable-next-line no-console
12
+ console.log(`\n Unknown mcp subcommand: ${sub}\n Usage: gitpagedocs mcp start\n`);
13
+ return;
14
+ }
15
+ // stderr only: stdout is the MCP stdio channel and must stay protocol-clean.
16
+ console.error("[gitpagedocs] Starting MCP server over stdio (Ctrl-C to stop)...");
17
+ // Dynamic import keeps the MCP SDK out of the frontend bundle.
18
+ const { startMcpServer } = await import("@gitpagedocs/mcp");
19
+ await startMcpServer(ctx.cwd);
20
+ }
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import type { CommandContext } from "./run-command";
4
+ import { askConfirm } from "../ui/clack";
5
+ import { askOwnerRepo, interactivePromptsAvailable } from "../ui/prompts";
6
+ // @ts-expect-error .mjs runtime module
7
+ import { detectRepoFromGit, getCurrentGitBranch, tryConfigurePagesToGitHubActions } from "../../runtime/git-ops.mjs";
8
+ import { trimSlashes } from "../../contracts/path-segments.mjs";
9
+
10
+ /** Read `--name value` (or `--name=value`) from a raw arg list. */
11
+ function readFlag(args: string[], name: string): string {
12
+ const eq = args.find((a) => a.startsWith(`${name}=`));
13
+ if (eq) return eq.slice(name.length + 1).trim();
14
+ const i = args.indexOf(name);
15
+ if (i >= 0 && args[i + 1] && !args[i + 1].startsWith("--")) return args[i + 1].trim();
16
+ return "";
17
+ }
18
+
19
+ /**
20
+ * `gitpagedocs pages actions` / `--pages-actions` — switch the repository's
21
+ * GitHub Pages source to GitHub Actions. Detects owner/repo/branch from git,
22
+ * validates the gh CLI, and confirms before changing anything (no silent
23
+ * overwrite). Does NOT generate docs or push (that is `pages deploy`).
24
+ */
25
+ export async function runPagesActions(ctx: CommandContext): Promise<void> {
26
+ let repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
27
+ if (!repo) {
28
+ if (!interactivePromptsAvailable()) {
29
+ // eslint-disable-next-line no-console
30
+ console.log("\n Could not detect a GitHub repo from the git 'origin' remote.\n Add a remote or use `--push --owner <o> --repo <r>`.\n");
31
+ return;
32
+ }
33
+ repo = await askOwnerRepo(null);
34
+ }
35
+ const branch = getCurrentGitBranch(ctx.cwd) as string;
36
+ // eslint-disable-next-line no-console
37
+ console.log(`\n Detected ${repo.owner}/${repo.repo} (branch ${branch}).`);
38
+ const ok = await askConfirm(`Switch GitHub Pages source to GitHub Actions for ${repo.owner}/${repo.repo}?`, false);
39
+ if (!ok) {
40
+ // eslint-disable-next-line no-console
41
+ console.log(" Aborted. No changes made.\n");
42
+ return;
43
+ }
44
+ tryConfigurePagesToGitHubActions(repo.owner, repo.repo, branch, ctx.cwd);
45
+ }
46
+
47
+ /**
48
+ * `gitpagedocs pages deploy` — the full documented deploy flow: resolve
49
+ * owner/repo (from --owner/--repo or the git remote), confirm, then run the
50
+ * proven `--push` path (generate docs + workflow → commit → push → configure
51
+ * Pages→Actions) and print the final site URL. Reuses the legacy push verbatim
52
+ * (spawned), so `--push` behavior stays byte-identical.
53
+ */
54
+ export async function runPagesDeploy(ctx: CommandContext): Promise<void> {
55
+ let owner = readFlag(ctx.args, "--owner");
56
+ let repo = readFlag(ctx.args, "--repo");
57
+ const docsPath = readFlag(ctx.args, "--path");
58
+
59
+ if (!owner || !repo) {
60
+ const detected = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
61
+ if (detected) {
62
+ owner = owner || detected.owner;
63
+ repo = repo || detected.repo;
64
+ }
65
+ }
66
+ if (!owner || !repo) {
67
+ if (!interactivePromptsAvailable()) {
68
+ // eslint-disable-next-line no-console
69
+ console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n");
70
+ return;
71
+ }
72
+ const answered = await askOwnerRepo(null, { owner, repo });
73
+ owner = answered.owner;
74
+ repo = answered.repo;
75
+ }
76
+
77
+ const pathSuffix = docsPath ? ` (path: ${docsPath})` : "";
78
+ // eslint-disable-next-line no-console
79
+ console.log(`\n Deploy ${owner}/${repo} to GitHub Pages via Actions${pathSuffix}.`);
80
+ const ok = await askConfirm(
81
+ `Generate docs + workflow, commit, push, and switch Pages to Actions for ${owner}/${repo}?`,
82
+ false,
83
+ );
84
+ if (!ok) {
85
+ // eslint-disable-next-line no-console
86
+ console.log(" Aborted. No changes made.\n");
87
+ return;
88
+ }
89
+
90
+ // pkgRoot is the cli package root (cli/), where the bin lives as index.mjs.
91
+ const bin = path.join(ctx.pkgRoot, "index.mjs");
92
+ const pushArgs = ["--push", "--owner", owner, "--repo", repo, ...(docsPath ? ["--path", docsPath] : [])];
93
+ const result = spawnSync(process.execPath, [bin, ...pushArgs], { stdio: "inherit", cwd: ctx.cwd, env: process.env });
94
+
95
+ if (result.status === 0) {
96
+ const seg = docsPath ? `/${trimSlashes(docsPath)}` : "";
97
+ // eslint-disable-next-line no-console
98
+ console.log(`\n Deployed. Final URL: https://${owner}.github.io/${repo}${seg}/\n`);
99
+ } else {
100
+ // eslint-disable-next-line no-console
101
+ console.log("\n Deploy did not complete (push step failed). See output above.\n");
102
+ }
103
+ }
@@ -0,0 +1,62 @@
1
+ import { NodeCryptoService, deriveDocAccessKeys } from "@gitpagedocs/tools";
2
+ import type { CommandContext } from "./run-command";
3
+ import { askPassword, note, outro } from "../ui/clack";
4
+ import { interactivePromptsAvailable } from "../ui/prompts";
5
+ import { GitPageDocsConfigRepository } from "../../infrastructure/gitpagedocs-config-file";
6
+
7
+ const MIN_PASSWORD_LENGTH = 4;
8
+
9
+ /**
10
+ * `gitpagedocs password` — interactively set a documentation access password.
11
+ *
12
+ * Type + confirm a password, derive the double-hash key pair, write the PUBLIC
13
+ * key into gitpagedocs/config.json (which makes the frontend gate the docs), and
14
+ * print the PRIVATE key for the user to copy. The password itself is never stored.
15
+ */
16
+ export async function runPassword(ctx: CommandContext): Promise<void> {
17
+ if (!interactivePromptsAvailable()) {
18
+ // eslint-disable-next-line no-console
19
+ console.log("\n `gitpagedocs password` is interactive — run it in a terminal (TTY).\n");
20
+ return;
21
+ }
22
+
23
+ const password = await askPassword({
24
+ message: "Documentation password:",
25
+ validate: (v) =>
26
+ v && v.trim().length >= MIN_PASSWORD_LENGTH
27
+ ? undefined
28
+ : `Use at least ${MIN_PASSWORD_LENGTH} characters.`,
29
+ });
30
+ const confirmation = await askPassword({
31
+ message: "Confirm password:",
32
+ validate: (v) => (v ? undefined : "Required."),
33
+ });
34
+
35
+ if (password !== confirmation) {
36
+ note("Passwords do not match. Nothing was changed.", "Aborted");
37
+ return;
38
+ }
39
+
40
+ const { privateKey, publicKey } = await deriveDocAccessKeys(password, new NodeCryptoService());
41
+
42
+ let configPath: string;
43
+ try {
44
+ configPath = await new GitPageDocsConfigRepository(ctx.cwd).patchDocsAccess(publicKey);
45
+ } catch (error) {
46
+ note(error instanceof Error ? error.message : String(error), "Could not save");
47
+ return;
48
+ }
49
+
50
+ note(
51
+ `Public key saved to ${configPath}\nThe documentation is now password-protected.`,
52
+ "Saved",
53
+ );
54
+ // eslint-disable-next-line no-console
55
+ console.log(
56
+ "\n PRIVATE KEY (copy & keep it safe — it also unlocks the docs):\n\n" +
57
+ ` ${privateKey}\n\n` +
58
+ " Readers can unlock with the password OR this private key.\n" +
59
+ " To remove protection, set site.docsAccess.enabled to false in config.json.\n",
60
+ );
61
+ outro("Done.");
62
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * New-verb command layer (Phase 6). Intercepts the informational/utility
3
+ * commands that are NOT part of the legacy flag flow (version, doctor, provider,
4
+ * models, config, mcp, update) and routes them to thin handlers that consume
5
+ * @gitpagedocs/tools. Alias verbs (init, document, deploy, pages) are mapped in
6
+ * the parser and fall through to the legacy dispatch, so this returns false for
7
+ * them.
8
+ */
9
+ export interface CommandContext {
10
+ argv: string[];
11
+ args: string[];
12
+ pkgRoot: string;
13
+ cwd: string;
14
+ }
15
+
16
+ export type CommandHandler = (ctx: CommandContext) => Promise<void>;
17
+
18
+ import { runVersion, runDoctor, runUpdate } from "./diagnostics";
19
+ import { runProvider, runModels } from "./ai-info";
20
+ import { runConfig } from "./config-info";
21
+ import { runConfigClear } from "./config-clear";
22
+ import { runMcp } from "./mcp";
23
+ import { runDocs } from "./docs";
24
+ import { runPagesActions, runPagesDeploy } from "./pages";
25
+ import { runPassword } from "./password";
26
+ import { runChat } from "./chat";
27
+
28
+ const REGISTRY: Record<string, CommandHandler> = {
29
+ version: runVersion,
30
+ "--version": runVersion,
31
+ "-v": runVersion,
32
+ doctor: runDoctor,
33
+ update: runUpdate,
34
+ provider: runProvider,
35
+ providers: runProvider,
36
+ models: runModels,
37
+ config: runConfig,
38
+ mcp: runMcp,
39
+ docs: runDocs,
40
+ password: runPassword,
41
+ chat: runChat,
42
+ };
43
+
44
+ /** Returns true if a new command handled the invocation (skip legacy flow). */
45
+ export async function runNewCommand(argv: string[], pkgRoot: string): Promise<boolean> {
46
+ const args = argv.slice(2);
47
+ const ctx: CommandContext = { argv, args, pkgRoot, cwd: process.cwd() };
48
+
49
+ // Configure-only Pages→Actions: `--pages-actions` flag or `pages actions` verb.
50
+ if (args.includes("--pages-actions") || (args[0] === "pages" && args[1] === "actions")) {
51
+ await runPagesActions(ctx);
52
+ return true;
53
+ }
54
+ // Full deploy: `pages deploy` (detect owner/repo, confirm, run the proven
55
+ // push flow, print URL). Bare `pages`/`deploy` still fall through to legacy.
56
+ if (args[0] === "pages" && args[1] === "deploy") {
57
+ await runPagesDeploy(ctx);
58
+ return true;
59
+ }
60
+ // Wipe the stored AI credentials config: `config clear`.
61
+ if (args[0] === "config" && args[1] === "clear") {
62
+ await runConfigClear(ctx);
63
+ return true;
64
+ }
65
+
66
+ const handler = REGISTRY[args[0] ?? ""];
67
+ if (!handler) return false;
68
+ await handler(ctx);
69
+ return true;
70
+ }
71
+
72
+ export const NEW_COMMAND_NAMES = Object.keys(REGISTRY).filter((n) => !n.startsWith("-"));
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { printBanner, printCredits } from "./ui/banner";
6
+ import { resolveOptions } from "./options/resolver";
7
+ import { migrateLegacyLayoutsInteractive } from "./ui/layouts-prompts";
8
+ import { runNewCommand } from "./commands/run-command";
9
+ import { dispatchMode } from "../application/use-cases/dispatch-mode";
10
+ import type {
11
+ CliRuntimeParams,
12
+ ConfigOnlyRuntimePort,
13
+ HomeRuntimePort,
14
+ } from "../application/ports/cli-runtime-ports";
15
+ // @ts-expect-error .mjs modules are runtime-only in this package.
16
+ import { buildConfigArtifacts } from "../builders/config-orchestrator.mjs";
17
+ // @ts-expect-error .mjs modules are runtime-only in this package.
18
+ import { createThemeTemplate } from "../builders/theme-template.mjs";
19
+ // @ts-expect-error .mjs modules are runtime-only in this package.
20
+ import { LAYOUTS } from "../data/layouts.mjs";
21
+ // @ts-expect-error .mjs modules are runtime-only in this package.
22
+ import { executeConfigOnly } from "../application/config-only/handler.mjs";
23
+ // @ts-expect-error .mjs modules are runtime-only in this package.
24
+ import { executeHome } from "../application/home/handler.mjs";
25
+ // @ts-expect-error .mjs modules are runtime-only in this package.
26
+ import { createConfigOnlyRuntime } from "../infrastructure/config-only/runtime.mjs";
27
+ // @ts-expect-error .mjs modules are runtime-only in this package.
28
+ import { createHomeRuntime } from "../infrastructure/home/runtime.mjs";
29
+
30
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
31
+ const root = process.cwd();
32
+ // cli/ is the published package root (bin = cli/index.mjs). scriptDir is
33
+ // cli/presentation, so pkgRoot = cli/ and the bundled prebuilt/ ships at
34
+ // cli/prebuilt — works identically in-repo and when installed via npm.
35
+ const pkgRoot = path.join(scriptDir, "..");
36
+ const prebuiltDir = path.join(pkgRoot, "prebuilt");
37
+
38
+ const params: CliRuntimeParams = {
39
+ root,
40
+ pkgRoot,
41
+ prebuiltDir,
42
+ buildConfigArtifacts,
43
+ createThemeTemplate,
44
+ layouts: LAYOUTS,
45
+ };
46
+
47
+ async function main(): Promise<void> {
48
+ // Commands that own stdout get no banner/credits: `mcp start` (the MCP stdio
49
+ // channel) and `chat` (streamed model output must be the only thing on stdout).
50
+ const stdoutOwnedByCommand = process.argv[2] === "mcp" || process.argv[2] === "chat";
51
+ if (!stdoutOwnedByCommand) printBanner();
52
+
53
+ // New informational/utility commands (version, doctor, provider, models,
54
+ // config, mcp, update) are handled before the legacy flag flow. Alias verbs
55
+ // (init, document, deploy, pages) are mapped in the parser and fall through.
56
+ if (await runNewCommand(process.argv, pkgRoot)) {
57
+ if (!stdoutOwnedByCommand) printCredits();
58
+ return;
59
+ }
60
+
61
+ const options = await resolveOptions(process.argv, process.env);
62
+
63
+ // Layouts moved out of <outputDir>/layouts/. Offer to relocate anything left
64
+ // behind before generating, so the config written next points at one folder.
65
+ if (options.mode === "config-only") {
66
+ await migrateLegacyLayoutsInteractive(root, options);
67
+ }
68
+
69
+ const configOnlyRuntime = createConfigOnlyRuntime() as ConfigOnlyRuntimePort;
70
+ const homeRuntime = createHomeRuntime() as HomeRuntimePort;
71
+ await dispatchMode(options, params, {
72
+ runConfigOnly: async (context) => {
73
+ const safePrebuiltDir =
74
+ typeof context.prebuiltDir === "string"
75
+ ? context.prebuiltDir
76
+ : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt");
77
+ await executeConfigOnly(
78
+ {
79
+ ...context,
80
+ prebuiltDir: safePrebuiltDir,
81
+ },
82
+ configOnlyRuntime,
83
+ );
84
+ },
85
+ runHome: async (context) => {
86
+ await executeHome(context, homeRuntime);
87
+ },
88
+ runAi: async (context) => {
89
+ console.log("[gitpagedocs] Iniciando Módulo de IA na CLI...");
90
+ const safePrebuiltDir =
91
+ typeof context.prebuiltDir === "string"
92
+ ? context.prebuiltDir
93
+ : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt");
94
+ // @ts-ignore
95
+ const { runAiCliCommand } = await import("../ai/application/run-ai-cli-command.ts");
96
+ const result = await runAiCliCommand({
97
+ cwd: process.cwd(),
98
+ onInfo: (message: string) => console.log(message),
99
+ // Scaffold the base gitpagedocs/ structure before AI pages are wired in.
100
+ onScaffold: async () => {
101
+ await executeConfigOnly({ ...context, prebuiltDir: safePrebuiltDir }, configOnlyRuntime);
102
+ },
103
+ });
104
+
105
+ if (result.summary.scannedFilesCount === 0) {
106
+ console.log("[gitpagedocs:ai] Nenhum arquivo elegível foi encontrado nos paths informados.");
107
+ return;
108
+ }
109
+
110
+ const pages = result.summary.pages ?? [];
111
+ console.log(
112
+ `\nProcesso completo. Páginas no padrão gitpagedocs: ${pages.join(", ") || "(nenhuma)"}`,
113
+ );
114
+ console.log(
115
+ `[gitpagedocs:ai] ${result.summary.outputs.length} arquivos markdown gerados e conectados ao config.json da versão mais recente.`,
116
+ );
117
+ },
118
+ });
119
+ printCredits();
120
+ }
121
+
122
+ main().catch((err: unknown) => {
123
+ console.error("Failed to create Git Page Docs scaffold.", err);
124
+ process.exitCode = 1;
125
+ });