@gitpagedocs/cli 1.1.67 → 1.1.69

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 (38) hide show
  1. package/ai/application/ai-command.ts +1 -1
  2. package/ai/application/docs-pattern.ts +9 -5
  3. package/ai/application/resolve-chat-credentials.ts +13 -6
  4. package/ai/core/models/parse-ai-cli-config.ts +6 -8
  5. package/application/home/handler.mjs +3 -1
  6. package/application/ports/cli-runtime-ports.ts +2 -0
  7. package/application/report/config-only-reporter.mjs +9 -5
  8. package/builders/config-orchestrator.mjs +7 -6
  9. package/builders/language-bundles-builder.mjs +24 -0
  10. package/builders/root-config-builder.mjs +1 -6
  11. package/content/docs-en.mjs +11 -4
  12. package/content/docs-es.mjs +11 -4
  13. package/content/docs-pt.mjs +11 -4
  14. package/contracts/doc-versions.mjs +1 -1
  15. package/contracts/langs-paths.mjs +28 -0
  16. package/contracts/languages.mjs +6 -0
  17. package/contracts/layouts-paths.mjs +4 -4
  18. package/contracts/path-segments.d.mts +2 -0
  19. package/contracts/path-segments.mjs +15 -0
  20. package/data/default-site-config.mjs +9 -2
  21. package/domain/services/layouts-migration.mjs +15 -1
  22. package/home/home-file-writer.mjs +2 -3
  23. package/home/templates.mjs +5 -2
  24. package/infrastructure/home/runtime.mjs +2 -2
  25. package/package.json +3 -3
  26. package/presentation/commands/config-info.ts +54 -6
  27. package/presentation/commands/diagnostics.ts +30 -20
  28. package/presentation/commands/docs.ts +7 -2
  29. package/presentation/commands/pages.ts +4 -2
  30. package/presentation/options/parser.ts +46 -45
  31. package/presentation/ui/prompts.ts +6 -5
  32. package/runtime/doc-path-resolver.mjs +8 -6
  33. package/runtime/exec.mjs +168 -0
  34. package/runtime/git-ops.mjs +22 -20
  35. package/runtime/layouts-migrator.mjs +15 -1
  36. package/runtime/output.mjs +99 -57
  37. package/runtime/workflow.mjs +3 -1
  38. package/start.mjs +8 -6
@@ -1,22 +1,22 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { execSync } from "node:child_process";
3
+ import { runExecutable, runExecutableCapture } from "./exec.mjs";
4
4
 
5
5
  /** Detect { owner, repo } from the git `origin` remote URL, or null. */
6
6
  export function detectRepoFromGit(root) {
7
7
  try {
8
- const url = execSync("git remote get-url origin", { cwd: root, stdio: "pipe" }).toString().trim();
8
+ const url = runExecutableCapture("git", ["remote", "get-url", "origin"], { cwd: root });
9
9
  const match = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/);
10
10
  if (match) return { owner: match[1], repo: match[2] };
11
11
  } catch {
12
- // no remote / not a repo
12
+ // no remote / not a repo / git not installed
13
13
  }
14
14
  return null;
15
15
  }
16
16
 
17
17
  export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
18
18
  try {
19
- execSync("gh --version", { cwd: root, stdio: "ignore" });
19
+ runExecutable("gh", ["--version"], { cwd: root, stdio: "ignore" });
20
20
  } catch {
21
21
  console.warn(
22
22
  "GitHub CLI (gh) not found. Could not auto-configure Pages source. Set GitHub Pages source to 'GitHub Actions' manually in repository settings.",
@@ -24,16 +24,19 @@ export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
24
24
  return;
25
25
  }
26
26
 
27
+ // Each candidate is an argv array: owner/repo/branch travel as discrete
28
+ // arguments, never through a shell string.
29
+ const pagesEndpoint = `repos/${owner}/${repo}/pages`;
27
30
  const candidates = [
28
- `gh api -X PUT "repos/${owner}/${repo}/pages" -f build_type=workflow`,
29
- `gh api -X POST "repos/${owner}/${repo}/pages" -f build_type=workflow`,
30
- `gh api -X POST "repos/${owner}/${repo}/pages" -f source[branch]=${branch} -f source[path]=/`,
31
- `gh api -X PUT "repos/${owner}/${repo}/pages" -f source[branch]=${branch} -f source[path]=/ -f build_type=workflow`,
31
+ ["api", "-X", "PUT", pagesEndpoint, "-f", "build_type=workflow"],
32
+ ["api", "-X", "POST", pagesEndpoint, "-f", "build_type=workflow"],
33
+ ["api", "-X", "POST", pagesEndpoint, "-f", `source[branch]=${branch}`, "-f", "source[path]=/"],
34
+ ["api", "-X", "PUT", pagesEndpoint, "-f", `source[branch]=${branch}`, "-f", "source[path]=/", "-f", "build_type=workflow"],
32
35
  ];
33
36
 
34
- for (const command of candidates) {
37
+ for (const args of candidates) {
35
38
  try {
36
- execSync(command, { cwd: root, stdio: "ignore" });
39
+ runExecutable("gh", args, { cwd: root, stdio: "ignore" });
37
40
  console.log("GitHub Pages source configured for GitHub Actions.");
38
41
  return;
39
42
  } catch {
@@ -48,7 +51,7 @@ export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
48
51
 
49
52
  export function getCurrentGitBranch(root) {
50
53
  try {
51
- const branch = execSync("git branch --show-current", { cwd: root, stdio: "pipe" }).toString().trim();
54
+ const branch = runExecutableCapture("git", ["branch", "--show-current"], { cwd: root });
52
55
  return branch || "main";
53
56
  } catch {
54
57
  return "main";
@@ -67,17 +70,16 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
67
70
 
68
71
  const repoUrl = `https://github.com/${owner}/${repo}.git`;
69
72
  try {
70
- execSync("git remote get-url origin", { cwd: root, stdio: "ignore" });
73
+ runExecutable("git", ["remote", "get-url", "origin"], { cwd: root, stdio: "ignore" });
71
74
  } catch {
72
- execSync(`git remote add origin "${repoUrl}"`, { cwd: root, stdio: "inherit" });
75
+ runExecutable("git", ["remote", "add", "origin", repoUrl], { cwd: root, stdio: "inherit" });
73
76
  }
74
77
 
75
- execSync('git add "gitpagedocs" ".github/workflows/gitpagedocs-pages.yml"', { cwd: root, stdio: "inherit" });
78
+ runExecutable("git", ["add", "gitpagedocs", ".github/workflows/gitpagedocs-pages.yml"], { cwd: root, stdio: "inherit" });
76
79
 
77
80
  let hasStagedChanges = false;
78
81
  try {
79
- execSync("git diff --cached --quiet", { cwd: root, stdio: "ignore" });
80
- hasStagedChanges = false;
82
+ runExecutable("git", ["diff", "--cached", "--quiet"], { cwd: root, stdio: "ignore" });
81
83
  } catch {
82
84
  hasStagedChanges = true;
83
85
  }
@@ -86,11 +88,11 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
86
88
  return;
87
89
  }
88
90
 
89
- execSync('git commit -m "chore: setup gitpagedocs pages workflow"', { cwd: root, stdio: "inherit" });
91
+ runExecutable("git", ["commit", "-m", "chore: setup gitpagedocs pages workflow"], { cwd: root, stdio: "inherit" });
90
92
 
91
93
  const currentBranch = getCurrentGitBranch(root);
92
94
  try {
93
- execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
95
+ runExecutable("git", ["push", "-u", "origin", currentBranch], { cwd: root, stdio: "inherit" });
94
96
  tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
95
97
  return;
96
98
  } catch {
@@ -98,8 +100,8 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
98
100
  }
99
101
 
100
102
  try {
101
- execSync(`git pull --rebase origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
102
- execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
103
+ runExecutable("git", ["pull", "--rebase", "origin", currentBranch], { cwd: root, stdio: "inherit" });
104
+ runExecutable("git", ["push", "-u", "origin", currentBranch], { cwd: root, stdio: "inherit" });
103
105
  } catch {
104
106
  throw new Error(
105
107
  `Failed to push after automatic rebase on branch '${currentBranch}'. Resolve conflicts and run 'git push -u origin ${currentBranch}' manually.`,
@@ -48,7 +48,21 @@ export function listFilesRecursively(absoluteDir) {
48
48
  };
49
49
 
50
50
  walk(absoluteDir, "");
51
- return found.sort();
51
+ return found.sort(compareCodeUnits);
52
+ }
53
+
54
+ /**
55
+ * Plain UTF-16 code-unit order (what `sort()` does by default), spelled out so
56
+ * the listing never depends on the runtime locale.
57
+ *
58
+ * @param {string} a
59
+ * @param {string} b
60
+ * @returns {number}
61
+ */
62
+ function compareCodeUnits(a, b) {
63
+ if (a < b) return -1;
64
+ if (a > b) return 1;
65
+ return 0;
52
66
  }
53
67
 
54
68
  /**
@@ -9,6 +9,8 @@ import {
9
9
  normalizeToOutputPath,
10
10
  } from "./doc-path-resolver.mjs";
11
11
  import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
12
+ import { languageArtifactPaths } from "../contracts/langs-paths.mjs";
13
+ import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
12
14
 
13
15
  const DEFAULT_ICON_SVG = `<?xml version="1.0" encoding="utf-8"?>
14
16
  <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -29,7 +31,7 @@ function listExistingVersionIds(root, outputDir) {
29
31
  function removeLegacySourceViewerFiles(root, outputDir) {
30
32
  const versionIds = listExistingVersionIds(root, outputDir);
31
33
  for (const versionId of versionIds) {
32
- for (const language of ["pt", "en", "es"]) {
34
+ for (const language of SUPPORTED_LANGUAGES) {
33
35
  for (const fileName of ["source-viewer", "source-viewer.html"]) {
34
36
  const legacyPath = path.join(root, outputDir, "docs", "versions", versionId, language, fileName);
35
37
  if (existsSync(legacyPath)) {
@@ -75,10 +77,101 @@ async function writeLayoutArtifacts(options) {
75
77
  }
76
78
  }
77
79
 
80
+ /**
81
+ * Write one UI-strings bundle per shipped language. Which ones are on is
82
+ * `site.languages` in config.json, so the 1.1.68 `langs.json` manifest is
83
+ * removed: a stale copy would only mislead.
84
+ */
85
+ async function writeLanguageArtifacts(root, outputDir, artifacts) {
86
+ const paths = languageArtifactPaths(outputDir);
87
+ rmSync(path.join(root, paths.legacyManifest), { force: true });
88
+ for (const [language, bundle] of Object.entries(artifacts.languageBundles)) {
89
+ await writeJson(root, paths.bundle(language), bundle);
90
+ }
91
+ }
92
+
93
+ /** Keep only versioned docs output in docs/, removing legacy root language folders. */
94
+ function removeLegacyLanguageDirs(root, outputDir) {
95
+ for (const legacyLanguageDir of SUPPORTED_LANGUAGES) {
96
+ const legacyPath = path.join(root, outputDir, "docs", legacyLanguageDir);
97
+ if (existsSync(legacyPath)) {
98
+ rmSync(legacyPath, { recursive: true, force: true });
99
+ }
100
+ }
101
+ }
102
+
103
+ async function writeVersionConfigs(root, outputDir, versionConfigs) {
104
+ for (const [versionId, versionConfig] of Object.entries(versionConfigs)) {
105
+ await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);
106
+ }
107
+ }
108
+
109
+ async function writeMarkdownDoc(root, outputDir, versionId, docPath, docs) {
110
+ const key = parseDocFileToKey(path.basename(docPath));
111
+ const language = extractLanguageFromPath(docPath);
112
+ const content = key && language ? docs?.[language]?.[key] : undefined;
113
+ if (!content) return;
114
+ const versionedContent = withVersionBadge(content, versionId, language);
115
+ await writeText(root, normalizeToOutputPath(outputDir, docPath), versionedContent);
116
+ }
117
+
118
+ async function writeVersionMarkdownDocs(root, outputDir, versionId, versionConfig, docs) {
119
+ const versionRoutesMd = versionConfig["routes-md"] ?? versionConfig.routes ?? [];
120
+ for (const route of versionRoutesMd) {
121
+ for (const docPath of Object.values(route.path ?? {})) {
122
+ await writeMarkdownDoc(root, outputDir, versionId, docPath, docs);
123
+ }
124
+ }
125
+ }
126
+
127
+ async function writeHtmlDoc(root, outputDir, docPath, docsHtml) {
128
+ const key = parseHtmlFileToKey(path.basename(docPath));
129
+ const language = extractLanguageFromPath(docPath);
130
+ const content = key && language ? docsHtml[key]?.[language] : undefined;
131
+ if (!content) return;
132
+ const normalizedHtmlPath = normalizeToOutputPath(outputDir, docPath);
133
+ await writeText(root, normalizedHtmlPath, content);
134
+ // Backward-compatible alias for tools or users that still expect ".html".
135
+ if (!path.extname(docPath)) {
136
+ await writeText(root, `${normalizedHtmlPath}.html`, content);
137
+ }
138
+ }
139
+
140
+ async function writeVersionHtmlDocs(root, outputDir, versionConfig, docsHtml) {
141
+ const versionRoutesHtml = versionConfig["routes-html"] ?? [];
142
+ for (const route of versionRoutesHtml) {
143
+ if (route.url) continue;
144
+ for (const docPath of Object.values(route.path ?? {})) {
145
+ await writeHtmlDoc(root, outputDir, docPath, docsHtml);
146
+ }
147
+ }
148
+ }
149
+
150
+ /** Ensure version folders always include an index file. */
151
+ async function writeVersionFallbackIndexes(root, outputDir, versionId, docs) {
152
+ for (const language of SUPPORTED_LANGUAGES) {
153
+ const fallbackIndex = docs?.[language]?.index;
154
+ if (!fallbackIndex) continue;
155
+ const versionIndexPath = `${outputDir}/docs/versions/${versionId}/${language}/index.md`;
156
+ if (!existsSync(path.join(root, versionIndexPath))) {
157
+ const versionedFallbackIndex = withVersionBadge(fallbackIndex, versionId, language);
158
+ await writeText(root, versionIndexPath, versionedFallbackIndex);
159
+ }
160
+ }
161
+ }
162
+
163
+ async function writeVersionedDocs(root, outputDir, artifacts) {
164
+ const docsHtml = artifacts.docsHtml ?? {};
165
+ for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
166
+ await writeVersionMarkdownDocs(root, outputDir, versionId, versionConfig, artifacts.docs);
167
+ await writeVersionHtmlDocs(root, outputDir, versionConfig, docsHtml);
168
+ await writeVersionFallbackIndexes(root, outputDir, versionId, artifacts.docs);
169
+ }
170
+ }
171
+
78
172
  export async function writeConfigOnlyOutput(options) {
79
173
  const {
80
174
  root,
81
- pkgRoot,
82
175
  outputDir,
83
176
  artifacts,
84
177
  useLocalLayoutConfig,
@@ -87,15 +180,10 @@ export async function writeConfigOnlyOutput(options) {
87
180
  createThemeTemplate,
88
181
  } = options;
89
182
 
90
- // Keep only versioned docs output in docs/, removing legacy root language folders.
91
- for (const legacyLanguageDir of ["en", "pt", "es"]) {
92
- const legacyPath = path.join(root, outputDir, "docs", legacyLanguageDir);
93
- if (existsSync(legacyPath)) {
94
- rmSync(legacyPath, { recursive: true, force: true });
95
- }
96
- }
183
+ removeLegacyLanguageDirs(root, outputDir);
97
184
 
98
185
  await writeJson(root, `${outputDir}/config.json`, artifacts.rootConfig);
186
+ await writeLanguageArtifacts(root, outputDir, artifacts);
99
187
  await writeText(root, `${outputDir}/icon.svg`, DEFAULT_ICON_SVG);
100
188
  await writeLayoutArtifacts({
101
189
  root,
@@ -105,55 +193,9 @@ export async function writeConfigOnlyOutput(options) {
105
193
  artifacts,
106
194
  createThemeTemplate,
107
195
  });
108
-
109
- for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
110
- await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);
111
- }
196
+ await writeVersionConfigs(root, outputDir, artifacts.versionConfigs);
112
197
 
113
198
  removeLegacySourceViewerFiles(root, outputDir);
114
199
 
115
- for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
116
- const versionRoutesMd = versionConfig["routes-md"] ?? versionConfig.routes ?? [];
117
- for (const route of versionRoutesMd) {
118
- for (const docPath of Object.values(route.path ?? {})) {
119
- const fileName = path.basename(docPath);
120
- const key = parseDocFileToKey(fileName);
121
- const language = extractLanguageFromPath(docPath);
122
- const content = key && language ? artifacts.docs?.[language]?.[key] : undefined;
123
- if (!content) continue;
124
- const versionedContent = withVersionBadge(content, versionId, language);
125
- await writeText(root, normalizeToOutputPath(outputDir, docPath), versionedContent);
126
- }
127
- }
128
-
129
- const versionRoutesHtml = versionConfig["routes-html"] ?? [];
130
- const docsHtml = artifacts.docsHtml ?? {};
131
- for (const route of versionRoutesHtml) {
132
- if (route.url) continue;
133
- for (const docPath of Object.values(route.path ?? {})) {
134
- const fileName = path.basename(docPath);
135
- const key = parseHtmlFileToKey(fileName);
136
- const language = extractLanguageFromPath(docPath);
137
- const content = key && language ? docsHtml[key]?.[language] : undefined;
138
- if (!content) continue;
139
- const normalizedHtmlPath = normalizeToOutputPath(outputDir, docPath);
140
- await writeText(root, normalizedHtmlPath, content);
141
- // Backward-compatible alias for tools or users that still expect ".html".
142
- if (!path.extname(docPath)) {
143
- await writeText(root, `${normalizedHtmlPath}.html`, content);
144
- }
145
- }
146
- }
147
-
148
- // Ensure version folders always include an index file.
149
- for (const language of ["pt", "en", "es"]) {
150
- const fallbackIndex = artifacts.docs?.[language]?.index;
151
- if (!fallbackIndex) continue;
152
- const versionIndexPath = `${outputDir}/docs/versions/${versionId}/${language}/index.md`;
153
- if (!existsSync(path.join(root, versionIndexPath))) {
154
- const versionedFallbackIndex = withVersionBadge(fallbackIndex, versionId, language);
155
- await writeText(root, versionIndexPath, versionedFallbackIndex);
156
- }
157
- }
158
- }
200
+ await writeVersionedDocs(root, outputDir, artifacts);
159
201
  }
@@ -1,6 +1,8 @@
1
+ import { trimSlashes } from "../contracts/path-segments.mjs";
2
+
1
3
  function sanitizeDocsPath(docsPath) {
2
4
  if (!docsPath || typeof docsPath !== "string") return "";
3
- const segment = docsPath.replace(/^\/+|\/+$/g, "").trim();
5
+ const segment = trimSlashes(docsPath).trim();
4
6
  return /^[A-Za-z0-9._-]+$/.test(segment) ? segment : "";
5
7
  }
6
8
 
package/start.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "child_process";
2
+ import { spawnExecutable } from "./runtime/exec.mjs";
3
3
 
4
4
  const env = { ...process.env, GITPAGEDOCS_REPOSITORY_SEARCH: "true" };
5
5
  // The Next app lives in frontend/, so `next build` emits to frontend/.next —
6
6
  // point `next start` at the same directory (matches `next build frontend`).
7
- const child = spawn("npx", ["next", "start", "frontend"], {
8
- stdio: "inherit",
9
- env,
10
- shell: true,
11
- });
7
+ let child;
8
+ try {
9
+ child = spawnExecutable("npx", ["next", "start", "frontend"], { stdio: "inherit", env });
10
+ } catch (error) {
11
+ console.error(`[gitpagedocs] ${error instanceof Error ? error.message : String(error)}`);
12
+ process.exit(1);
13
+ }
12
14
  child.on("exit", (code) => process.exit(code ?? 0));