@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,239 @@
1
+ import {
2
+ AI_MODEL_DEFAULTS,
3
+ OLLAMA_DEFAULT_BASE_URL,
4
+ type AiProviderId,
5
+ } from '../config';
6
+ import { PROVIDER_CATALOG, legacyProviderToCatalogId } from '@gitpagedocs/tools/ai';
7
+ import type { AiCliConfig, AiCliRunPlan } from '../core/models/ai-cli-config';
8
+ import {
9
+ askText,
10
+ askConfirm,
11
+ askSelect,
12
+ askMultiSelect,
13
+ intro,
14
+ } from '../../presentation/ui/clack';
15
+
16
+ export interface InteractiveCliAnswers {
17
+ provider: AiProviderId;
18
+ apiKeyOrHost: string;
19
+ model: string;
20
+ targetDirs: string[];
21
+ languages: Array<'pt' | 'en' | 'es'>;
22
+ outputDir: string;
23
+ filePrefix: string;
24
+ saveConfig: boolean;
25
+ runConfigScaffold: boolean;
26
+ contextPrompt: string;
27
+ }
28
+
29
+ function parseCommaSeparated(value: string): string[] {
30
+ return value
31
+ .split(',')
32
+ .map((item) => item.trim())
33
+ .filter(Boolean);
34
+ }
35
+
36
+ function validateCommaSeparatedDirectories(value: string): string | undefined {
37
+ const values = parseCommaSeparated(value);
38
+ if (!values.length) return 'Provide at least one path.';
39
+ return undefined;
40
+ }
41
+
42
+ const CUSTOM_MODEL = '__custom_model__';
43
+
44
+ /**
45
+ * Ask for the model as a SELECT of the provider's real catalog models (with the
46
+ * default marked), plus a "Custom" escape hatch — so an invalid id like "4.8"
47
+ * can't be typed by mistake and rejected later as a 404 by the provider.
48
+ */
49
+ async function askModel(provider: AiProviderId): Promise<string> {
50
+ const catalogId = legacyProviderToCatalogId(provider);
51
+ const spec = catalogId ? PROVIDER_CATALOG[catalogId] : undefined;
52
+ const fallback = AI_MODEL_DEFAULTS[provider] || '';
53
+
54
+ if (!spec || spec.models.length === 0) {
55
+ return String(await askText({ message: 'Which model do you want to use?', defaultValue: fallback }));
56
+ }
57
+
58
+ const picked = await askSelect<string>(
59
+ 'Which model do you want to use?',
60
+ [
61
+ ...spec.models.map((m) => ({
62
+ value: m.id,
63
+ label: m.id === spec.defaultModel ? `${m.id} (default)` : m.id,
64
+ })),
65
+ { value: CUSTOM_MODEL, label: 'Custom — enter a model id manually' },
66
+ ],
67
+ spec.defaultModel,
68
+ );
69
+
70
+ if (picked !== CUSTOM_MODEL) return picked;
71
+
72
+ return String(
73
+ await askText({
74
+ message: 'Enter the model id:',
75
+ defaultValue: spec.defaultModel,
76
+ validate: (value) => (value.trim() ? undefined : 'Provide a model id'),
77
+ }),
78
+ );
79
+ }
80
+
81
+ function toPlan(answers: InteractiveCliAnswers): AiCliRunPlan {
82
+ const config: AiCliConfig = {
83
+ version: 1,
84
+ ai: {
85
+ provider: answers.provider,
86
+ model: answers.model,
87
+ apiKey: answers.provider === 'ollama' ? undefined : answers.apiKeyOrHost,
88
+ baseUrl: answers.provider === 'ollama' ? answers.apiKeyOrHost : undefined,
89
+ paths: answers.targetDirs,
90
+ languages: answers.languages,
91
+ outputDir: answers.outputDir,
92
+ filePrefix: answers.filePrefix,
93
+ contextPrompt: answers.contextPrompt,
94
+ },
95
+ };
96
+
97
+ return {
98
+ config,
99
+ saveConfig: answers.saveConfig,
100
+ runConfigScaffold: answers.runConfigScaffold,
101
+ };
102
+ }
103
+
104
+ export async function promptMissingDirectories(
105
+ missingDirectories: string[],
106
+ ): Promise<{ replacementPaths: string[]; abort: boolean }> {
107
+ const recoveryMode = await askSelect(
108
+ `I couldn't find ${missingDirectories.length} directory(ies): ${missingDirectories.join(', ')}`,
109
+ [
110
+ { value: 'fix', label: 'Fix paths now (recommended)' },
111
+ { value: 'skip', label: 'Skip missing paths and continue' },
112
+ { value: 'abort', label: 'Abort execution' },
113
+ ],
114
+ 'fix',
115
+ );
116
+
117
+ if (recoveryMode === 'abort') {
118
+ return { replacementPaths: [], abort: true };
119
+ }
120
+ if (recoveryMode === 'skip') {
121
+ return { replacementPaths: [], abort: false };
122
+ }
123
+
124
+ const fixedPaths = await askText({
125
+ message: 'Enter new paths (comma-separated):',
126
+ validate: validateCommaSeparatedDirectories,
127
+ });
128
+
129
+ return {
130
+ replacementPaths: parseCommaSeparated(String(fixedPaths || '')),
131
+ abort: false,
132
+ };
133
+ }
134
+
135
+ export async function promptUseExistingConfig(existingConfig: AiCliConfig): Promise<AiCliRunPlan | null> {
136
+ const useExistingConfig = await askConfirm(
137
+ 'Found .gitpagedocsconfig. Do you want to use this configuration?',
138
+ true,
139
+ );
140
+
141
+ if (!useExistingConfig) return null;
142
+
143
+ const runConfigScaffold = await askConfirm(
144
+ 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?',
145
+ true,
146
+ );
147
+
148
+ return {
149
+ config: existingConfig,
150
+ saveConfig: false,
151
+ runConfigScaffold,
152
+ };
153
+ }
154
+
155
+ export async function runAiInteractivePrompt(existingConfig?: AiCliConfig | null): Promise<AiCliRunPlan> {
156
+ intro('GitPageDocs AI CLI - interactive mode');
157
+
158
+ if (existingConfig) {
159
+ const existingPlan = await promptUseExistingConfig(existingConfig);
160
+ if (existingPlan) return existingPlan;
161
+ }
162
+
163
+ const provider = await askSelect<AiProviderId>(
164
+ 'Which AI provider should be used to generate documentation?',
165
+ [
166
+ { value: 'openai', label: 'OpenAI (GPT)' },
167
+ { value: 'claude', label: 'Anthropic (Claude)' },
168
+ { value: 'gemini', label: 'Google Gemini' },
169
+ { value: 'ollama', label: 'Ollama (Local Open Source)' },
170
+ ],
171
+ 'openai',
172
+ );
173
+
174
+ const apiKeyOrHost = await askText({
175
+ message: provider === 'ollama' ? 'Ollama host URL:' : 'Provider API key:',
176
+ defaultValue: provider === 'ollama' ? OLLAMA_DEFAULT_BASE_URL : undefined,
177
+ validate: (input) => (input.length > 0 ? undefined : 'Required field'),
178
+ });
179
+
180
+ const model = await askModel(provider);
181
+
182
+ const targetDirsRaw = await askText({
183
+ message:
184
+ 'Paths to analyze (comma-separated). You can include multiple repositories (e.g., src,cli,../other-repo/src):',
185
+ defaultValue: 'src,cli',
186
+ validate: validateCommaSeparatedDirectories,
187
+ });
188
+
189
+ const languages = await askMultiSelect<'en' | 'pt' | 'es'>(
190
+ 'Documentation languages to generate:',
191
+ [
192
+ { value: 'en', label: 'English (en)' },
193
+ { value: 'pt', label: 'Portuguese (pt)' },
194
+ { value: 'es', label: 'Español (es)' },
195
+ ],
196
+ ['en', 'pt', 'es'],
197
+ );
198
+
199
+ const outputDir = await askText({
200
+ message: 'Output directory for generated markdown files:',
201
+ defaultValue: 'gitpagedocs/docs',
202
+ validate: (value) => (value.trim() ? undefined : 'Provide an output directory'),
203
+ });
204
+
205
+ const filePrefix = await askText({
206
+ message: 'Output file prefix:',
207
+ defaultValue: 'ai-generated',
208
+ validate: (value) => (value.trim() ? undefined : 'Provide a prefix'),
209
+ });
210
+
211
+ const contextPrompt = await askText({
212
+ message: 'Extra instructions for AI (optional):',
213
+ defaultValue:
214
+ 'You are a senior technical writer. Generate professional, clear documentation with architectural insight following FSD, SOLID, and Clean Code principles.',
215
+ });
216
+
217
+ const saveConfig = await askConfirm(
218
+ 'Save this configuration to .gitpagedocsconfig for future manual use?',
219
+ true,
220
+ );
221
+
222
+ const runConfigScaffold = await askConfirm(
223
+ 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?',
224
+ true,
225
+ );
226
+
227
+ return toPlan({
228
+ provider,
229
+ apiKeyOrHost,
230
+ model,
231
+ targetDirs: parseCommaSeparated(targetDirsRaw),
232
+ languages,
233
+ outputDir,
234
+ filePrefix,
235
+ contextPrompt,
236
+ saveConfig,
237
+ runConfigScaffold,
238
+ });
239
+ }
@@ -0,0 +1,57 @@
1
+ /** Config-only use case - orchestrates build, write, and optional push */
2
+
3
+ import { sanitizeSegment } from "../../domain/services/sanitize-segment.mjs";
4
+ import { reportAll } from "../report/config-only-reporter.mjs";
5
+
6
+ /**
7
+ * Execute config-only use case
8
+ * @param {Object} params
9
+ * @param {Object} params.options - Parsed CLI options
10
+ * @param {string} params.root - Project root
11
+ * @param {string} params.pkgRoot - Package root
12
+ * @param {string} params.prebuiltDir - Prebuilt directory path
13
+ * @param {Function} params.buildConfigArtifacts - Artifact builder
14
+ * @param {Function} params.createThemeTemplate - Theme template creator
15
+ * @param {Array} params.layouts - Layout definitions
16
+ * @param {import("../ports/cli-runtime-ports").ConfigOnlyRuntimePort} runtime
17
+ */
18
+ export async function executeConfigOnly(params, runtime) {
19
+ if (!runtime) {
20
+ throw new Error("Config-only runtime is required.");
21
+ }
22
+
23
+ const { options, root, pkgRoot, prebuiltDir, buildConfigArtifacts, createThemeTemplate, layouts } = params;
24
+
25
+ const artifacts = buildConfigArtifacts({
26
+ useLocalLayoutConfig: options.useLocalLayoutConfig,
27
+ layoutsDir: options.layoutsDir,
28
+ githubOwner: sanitizeSegment(options.githubOwner),
29
+ githubRepo: sanitizeSegment(options.githubRepo),
30
+ root,
31
+ });
32
+
33
+ await runtime.writeConfigOnlyOutput({
34
+ root,
35
+ pkgRoot,
36
+ outputDir: options.outputDir,
37
+ layoutsDir: options.layoutsDir,
38
+ artifacts,
39
+ useLocalLayoutConfig: options.useLocalLayoutConfig,
40
+ layouts,
41
+ createThemeTemplate,
42
+ });
43
+
44
+ if (options.shouldPush) {
45
+ await runtime.ensureGitHubPagesWorkflow(
46
+ () => runtime.getCurrentGitBranch(root),
47
+ (relativePath, content) => runtime.writeText(root, relativePath, content),
48
+ options.docsPath || "",
49
+ );
50
+ runtime.runGitPushForGeneratedArtifacts(options, root, sanitizeSegment);
51
+ }
52
+
53
+ const reportLines = reportAll(options, runtime.hasPath(prebuiltDir));
54
+ for (const line of reportLines) {
55
+ runtime.logInfo(line);
56
+ }
57
+ }
@@ -0,0 +1,87 @@
1
+ /** Home distribution use case - static site + auxiliary files */
2
+
3
+ import { STATIC_OUTPUT_DIR, ARTIFACTS_DIR } from "../../home/constants.mjs";
4
+ import { trimSlashes } from "../../contracts/path-segments.mjs";
5
+
6
+ /**
7
+ * Execute home distribution use case
8
+ * @param {Object} params
9
+ * @param {Object} params.options - Parsed CLI options
10
+ * @param {string} params.root - Project root
11
+ * @param {string} params.pkgRoot - Package root
12
+ * @param {Function} params.buildConfigArtifacts - Artifact builder
13
+ * @param {Function} params.createThemeTemplate - Theme template creator
14
+ * @param {Array} params.layouts - Layout definitions
15
+ * @param {import("../ports/cli-runtime-ports").HomeRuntimePort} runtime
16
+ */
17
+ export async function executeHome(params, runtime) {
18
+ if (!runtime) {
19
+ throw new Error("Home runtime is required.");
20
+ }
21
+
22
+ const { options, root, pkgRoot, buildConfigArtifacts, createThemeTemplate, layouts } = params;
23
+ const outputDir = options.outputDir || "gitpagedocshome";
24
+ const repositorySearch = options.repositorySearch ?? false;
25
+ const basePath = options.basePath ?? "";
26
+
27
+ const isCurrentDir = outputDir === "." || outputDir === "./";
28
+ const targetDir = isCurrentDir ? root : runtime.joinPath(root, outputDir);
29
+
30
+ if (!isCurrentDir) {
31
+ runtime.ensureDirEmpty(root, outputDir);
32
+ }
33
+
34
+ const pathSegment = basePath ? trimSlashes(basePath) : "";
35
+ const buildEnv = {
36
+ ...process.env,
37
+ GITHUB_ACTIONS: "true",
38
+ GITPAGEDOCS_REPOSITORY_SEARCH: String(repositorySearch),
39
+ GITPAGEDOCS_BASE_PATH: pathSegment ? `/${pathSegment}` : "/",
40
+ GITPAGEDOCS_PATH: pathSegment,
41
+ };
42
+
43
+ const artifacts = buildConfigArtifacts({
44
+ useLocalLayoutConfig: false,
45
+ layoutsDir: options.layoutsDir,
46
+ githubOwner: "",
47
+ githubRepo: "",
48
+ root,
49
+ });
50
+
51
+ await runtime.writeConfigOnlyOutput({
52
+ root,
53
+ pkgRoot,
54
+ outputDir: ARTIFACTS_DIR,
55
+ layoutsDir: options.layoutsDir,
56
+ artifacts,
57
+ useLocalLayoutConfig: false,
58
+ layouts,
59
+ createThemeTemplate,
60
+ });
61
+
62
+ runtime.runNextBuild(root, buildEnv);
63
+
64
+ const outPath = runtime.joinPath(root, STATIC_OUTPUT_DIR);
65
+ if (!runtime.existsPath(outPath)) {
66
+ throw new Error(`Static export not found at ${STATIC_OUTPUT_DIR}/. Ensure next.config exports when GITHUB_ACTIONS=true.`);
67
+ }
68
+
69
+ const destBase = isCurrentDir ? root : targetDir;
70
+ if (isCurrentDir) {
71
+ for (const name of runtime.readDirNames(outPath)) {
72
+ runtime.copyRecursive(runtime.joinPath(outPath, name), runtime.joinPath(destBase, name));
73
+ }
74
+ } else {
75
+ runtime.copyRecursive(outPath, destBase);
76
+ }
77
+ runtime.copyRecursive(runtime.joinPath(root, ARTIFACTS_DIR), runtime.joinPath(destBase, ARTIFACTS_DIR));
78
+
79
+ await runtime.writeHomeFiles(root, isCurrentDir ? "." : outputDir, { repositorySearch, basePath });
80
+ await runtime.writeText(root, (isCurrentDir ? "" : outputDir + "/") + ".nojekyll", "");
81
+
82
+ const cdDir = isCurrentDir ? "." : outputDir;
83
+ runtime.logSuccess(`Generated: ${cdDir}/ (static site + .env + Dockerfile + README.md)`);
84
+ runtime.logInfo(`Serve: cd ${cdDir} && npx serve .`);
85
+ runtime.logInfo(`Docker: cd ${cdDir} && docker build -t gitpagedocshome . && docker run -p 3000:80 gitpagedocshome`);
86
+ }
87
+
@@ -0,0 +1,93 @@
1
+ import type { CliOptions } from "../../domain/models/cli-options";
2
+
3
+ export interface BuildArtifactsInput {
4
+ useLocalLayoutConfig: boolean;
5
+ /** Repo-relative layouts home the generated config should reference. */
6
+ layoutsDir: string;
7
+ githubOwner: string;
8
+ githubRepo: string;
9
+ root: string;
10
+ }
11
+
12
+ export interface LayoutDefinition {
13
+ file: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export interface BuiltConfigArtifacts {
18
+ rootConfig: unknown;
19
+ /** UI-string bundle per language, written to `<outputDir>/langs/<lang>.json`. */
20
+ languageBundles: Record<string, unknown>;
21
+ layoutsConfig: unknown;
22
+ fallbackLayoutsConfig: unknown;
23
+ docs?: Record<string, Record<string, string>>;
24
+ docsHtml?: Record<string, Record<string, string>>;
25
+ versionConfigs: Record<string, Record<string, unknown>>;
26
+ }
27
+
28
+ export interface CliRuntimeParams {
29
+ root: string;
30
+ pkgRoot: string;
31
+ prebuiltDir: string;
32
+ buildConfigArtifacts: (input: BuildArtifactsInput) => BuiltConfigArtifacts;
33
+ createThemeTemplate: (layout: LayoutDefinition) => unknown;
34
+ layouts: LayoutDefinition[];
35
+ }
36
+
37
+ export interface CliCommandContext extends CliRuntimeParams {
38
+ options: CliOptions;
39
+ }
40
+
41
+ export interface CliCommandRunner {
42
+ runConfigOnly: (params: CliCommandContext) => Promise<void>;
43
+ runHome: (params: CliCommandContext) => Promise<void>;
44
+ runAi: (params: CliCommandContext) => Promise<void>;
45
+ }
46
+
47
+ export interface ConfigOnlyOutputInput {
48
+ root: string;
49
+ pkgRoot: string;
50
+ outputDir: string;
51
+ /** Repo-relative layouts home local layouts are written to. */
52
+ layoutsDir: string;
53
+ artifacts: BuiltConfigArtifacts;
54
+ useLocalLayoutConfig: boolean;
55
+ layouts: LayoutDefinition[];
56
+ createThemeTemplate: (layout: LayoutDefinition) => unknown;
57
+ }
58
+
59
+ export interface ConfigOnlyRuntimePort {
60
+ hasPath: (absolutePath: string) => boolean;
61
+ writeConfigOnlyOutput: (input: ConfigOnlyOutputInput) => Promise<void>;
62
+ writeText: (root: string, relativePath: string, data: string) => Promise<void>;
63
+ ensureGitHubPagesWorkflow: (
64
+ getCurrentGitBranch: () => string,
65
+ writeText: (relativePath: string, content: string) => Promise<void>,
66
+ docsPath?: string,
67
+ ) => Promise<void>;
68
+ getCurrentGitBranch: (root: string) => string;
69
+ runGitPushForGeneratedArtifacts: (
70
+ options: CliOptions,
71
+ root: string,
72
+ sanitizeSegment: (value: string | undefined | null) => string,
73
+ ) => void;
74
+ logInfo: (message: string) => void;
75
+ }
76
+
77
+ export interface HomeRuntimePort {
78
+ joinPath: (...parts: string[]) => string;
79
+ ensureDirEmpty: (root: string, relativeDir: string) => void;
80
+ copyRecursive: (src: string, dest: string) => void;
81
+ readDirNames: (absolutePath: string) => string[];
82
+ existsPath: (absolutePath: string) => boolean;
83
+ runNextBuild: (root: string, env: NodeJS.ProcessEnv) => void;
84
+ writeConfigOnlyOutput: (input: ConfigOnlyOutputInput) => Promise<void>;
85
+ writeHomeFiles: (
86
+ root: string,
87
+ outputDir: string,
88
+ options: { repositorySearch?: boolean; basePath?: string },
89
+ ) => Promise<void>;
90
+ writeText: (root: string, relativePath: string, data: string) => Promise<void>;
91
+ logSuccess: (message: string) => void;
92
+ logInfo: (message: string) => void;
93
+ }
@@ -0,0 +1,68 @@
1
+ /** Config-only command output messages - SRP: single place for all report messages */
2
+ import { resolveRenderingUrl } from "../../builders/project-links.mjs";
3
+ import { normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
4
+ import { languageArtifactPaths } from "../../contracts/langs-paths.mjs";
5
+ import { SUPPORTED_LANGUAGES } from "../../contracts/languages.mjs";
6
+
7
+ export function reportConfigOnlySuccess(options) {
8
+ const langs = languageArtifactPaths(options.outputDir);
9
+ return [
10
+ `Generated: ${options.outputDir}/ (config-only)`,
11
+ `UI strings: ${langs.dir}/{${SUPPORTED_LANGUAGES.join(",")}}.json (enable/disable each language in ${langs.config} -> site.languages)`,
12
+ "No index.html/index.js generated.",
13
+ ];
14
+ }
15
+
16
+ export function reportLayoutConfig(options) {
17
+ const layoutsDir = normalizeLayoutsDir(options.layoutsDir);
18
+ if (options.useLocalLayoutConfig) {
19
+ return [`Local layouts generated in ${layoutsDir}/ (--layoutconfig).`];
20
+ }
21
+ return [`Using official remote layouts config by default (no local ${layoutsDir}/ generated).`];
22
+ }
23
+
24
+ export function reportRenderingUrl(options) {
25
+ if (options.githubOwner && options.githubRepo) {
26
+ return [
27
+ `Configured rendering URL: ${resolveRenderingUrl(options.githubOwner, options.githubRepo)}`,
28
+ `Official viewer remains available: https://vidigal-code.github.io/git-page-docs/${options.githubOwner}/${options.githubRepo}?modetheme=light&lang=pt`,
29
+ ];
30
+ }
31
+ return [];
32
+ }
33
+
34
+ export function reportPushMode() {
35
+ return [
36
+ "Generated: .github/workflows/gitpagedocs-pages.yml",
37
+ "Push mode enabled: committed and pushed gitpagedocs/ + workflow to origin.",
38
+ ];
39
+ }
40
+
41
+ export function reportBuildFlag() {
42
+ return ["`--build` keeps compatibility flag but output remains gitpagedocs/."];
43
+ }
44
+
45
+ export function reportFullOrServeSkipped() {
46
+ return ["External commands were skipped (no prebuilt copy and no local serve spawn)."];
47
+ }
48
+
49
+ export function reportPrebuiltDetected() {
50
+ return ["`prebuilt/` detected, but ignored by config-only generator."];
51
+ }
52
+
53
+ export function reportAll(options, prebuiltDetected) {
54
+ const lines = [...reportConfigOnlySuccess(options), ...reportLayoutConfig(options), ...reportRenderingUrl(options)];
55
+ if (options.shouldPush) {
56
+ lines.push(...reportPushMode());
57
+ }
58
+ if (options.isBuild) {
59
+ lines.push(...reportBuildFlag());
60
+ }
61
+ if (options.mode === "full" || options.isServe) {
62
+ lines.push(...reportFullOrServeSkipped());
63
+ }
64
+ if (prebuiltDetected) {
65
+ lines.push(...reportPrebuiltDetected());
66
+ }
67
+ return lines;
68
+ }
@@ -0,0 +1,18 @@
1
+ import type { CliOptions } from "../../domain/models/cli-options";
2
+ import type { CliCommandRunner, CliRuntimeParams } from "../ports/cli-runtime-ports";
3
+
4
+ export async function dispatchMode(
5
+ options: CliOptions,
6
+ params: CliRuntimeParams,
7
+ runner: CliCommandRunner,
8
+ ): Promise<void> {
9
+ if (options.mode === "ai") {
10
+ await runner.runAi({ ...params, options });
11
+ return;
12
+ }
13
+ if (options.mode === "home") {
14
+ await runner.runHome({ ...params, options });
15
+ return;
16
+ }
17
+ await runner.runConfigOnly({ ...params, options });
18
+ }
@@ -0,0 +1,35 @@
1
+ /** Orchestrate build of config artifacts for gitpagedocs */
2
+
3
+ import { LAYOUTS, FALLBACK_LAYOUTS } from "../data/layouts.mjs";
4
+ import { DOCS } from "../content/docs.mjs";
5
+ import { DOC_VERSIONS } from "../data/version-constants.mjs";
6
+ import { buildRootConfig } from "./root-config-builder.mjs";
7
+ import { buildVersionConfig } from "./version-config-builder.mjs";
8
+ import { buildLanguageArtifacts } from "./language-bundles-builder.mjs";
9
+
10
+ /**
11
+ * Build all config artifacts (root, languages, layouts, versions, docs).
12
+ * @param {object} options - { useLocalLayoutConfig, githubOwner, githubRepo, layoutsDir }
13
+ * @returns {object} { rootConfig, languageBundles, layoutsConfig, fallbackLayoutsConfig, docs, docsHtml, versionConfigs }
14
+ */
15
+ export function buildConfigArtifacts(options = {}) {
16
+ const { languageToggles, languageBundles } = buildLanguageArtifacts();
17
+ const rootConfig = buildRootConfig({ ...options, languageToggles });
18
+ const layoutsConfig = { layouts: LAYOUTS };
19
+ const fallbackLayoutsConfig = { layouts: FALLBACK_LAYOUTS };
20
+
21
+ const versionConfigs = {};
22
+ for (const versionId of DOC_VERSIONS) {
23
+ versionConfigs[versionId] = buildVersionConfig(versionId, options);
24
+ }
25
+
26
+ return {
27
+ rootConfig,
28
+ languageBundles,
29
+ layoutsConfig,
30
+ fallbackLayoutsConfig,
31
+ docs: DOCS,
32
+ docsHtml: {},
33
+ versionConfigs,
34
+ };
35
+ }
@@ -0,0 +1,24 @@
1
+ /** Build the `site.languages` toggles + per-language UI string bundles for gitpagedocs */
2
+ import { buildLanguageToggles, splitLanguageBundles } from "@gitpagedocs/tools/i18n";
3
+ import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
4
+ import { defaultLangMenu } from "../data/i18n-langmenu.mjs";
5
+ import { defaultTranslations } from "../data/i18n-translations.mjs";
6
+
7
+ function assertLanguageStrings(languages) {
8
+ const missing = languages.filter((language) => !defaultLangMenu[language]);
9
+ if (missing.length > 0) {
10
+ throw new Error(`No UI strings (i18n-langmenu) for supported language(s): ${missing.join(", ")}`);
11
+ }
12
+ }
13
+
14
+ /**
15
+ * @returns {{ languageToggles: Record<string, boolean>, languageBundles: Record<string, object> }}
16
+ */
17
+ export function buildLanguageArtifacts() {
18
+ const languages = [...SUPPORTED_LANGUAGES];
19
+ assertLanguageStrings(languages);
20
+ return {
21
+ languageToggles: buildLanguageToggles(languages),
22
+ languageBundles: splitLanguageBundles(languages, defaultLangMenu, defaultTranslations),
23
+ };
24
+ }
@@ -0,0 +1,50 @@
1
+ /** Shared derivation of project URLs from CLI options (single source of truth
2
+ * for root and version config builders). */
3
+
4
+ import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
5
+
6
+ export const DEFAULT_PROJECT_LINK = "https://github.com/Vidigal-code/git-page-docs";
7
+ export const DEFAULT_RENDERING_URL = "https://vidigal-code.github.io/git-page-docs/";
8
+
9
+ export function resolveProjectLink(githubOwner, githubRepo) {
10
+ return githubOwner && githubRepo
11
+ ? `https://github.com/${githubOwner}/${githubRepo}`
12
+ : DEFAULT_PROJECT_LINK;
13
+ }
14
+
15
+ export function resolveRenderingUrl(githubOwner, githubRepo) {
16
+ return githubOwner && githubRepo
17
+ ? `https://${githubOwner}.github.io/${githubRepo}/`
18
+ : DEFAULT_RENDERING_URL;
19
+ }
20
+
21
+ /**
22
+ * Where the viewer should look for locally generated layouts.
23
+ *
24
+ * Mirrors resolveSourceViewerPath: user repositories use the symbolic HEAD ref
25
+ * so any default branch resolves. Without an owner/repo pair there is no URL to
26
+ * build, so the repo-relative path is returned - the viewer reads it directly in
27
+ * local mode and still discovers it by probing the repository otherwise.
28
+ *
29
+ * @param {string} githubOwner
30
+ * @param {string} githubRepo
31
+ * @param {string} layoutsDir Repo-relative layouts home.
32
+ * @returns {{ config: string, templates: string }}
33
+ */
34
+ export function resolveLayoutsLinks(githubOwner, githubRepo, layoutsDir) {
35
+ const paths = layoutsArtifactPaths(layoutsDir);
36
+ if (!githubOwner || !githubRepo) {
37
+ return { config: paths.config, templates: paths.templates };
38
+ }
39
+ const base = `${resolveProjectLink(githubOwner, githubRepo)}/blob/HEAD`;
40
+ return { config: `${base}/${paths.config}`, templates: `${base}/${paths.templates}` };
41
+ }
42
+
43
+ export function resolveSourceViewerPath(githubOwner, githubRepo) {
44
+ // User repositories use the symbolic HEAD ref so any default branch works
45
+ // (github.com, the trees API and raw.githubusercontent all accept it);
46
+ // the official repository keeps its stable main link.
47
+ return githubOwner && githubRepo
48
+ ? `${resolveProjectLink(githubOwner, githubRepo)}/tree/HEAD`
49
+ : `${DEFAULT_PROJECT_LINK}/tree/main`;
50
+ }