@gitpagedocs/cli 1.1.61 → 1.1.63

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 (72) hide show
  1. package/README.md +193 -191
  2. package/ai/application/ai-command.ts +24 -24
  3. package/ai/application/ai-provider-factory.ts +22 -22
  4. package/ai/application/docs-pattern.ts +68 -68
  5. package/ai/application/run-ai-cli-command.ts +167 -167
  6. package/ai/config.ts +50 -50
  7. package/ai/core/models/ai-cli-config.ts +33 -33
  8. package/ai/core/models/parse-ai-cli-config.ts +78 -78
  9. package/ai/core/ports/illm-provider.ts +32 -32
  10. package/ai/infrastructure/ai-config-file.ts +101 -101
  11. package/ai/infrastructure/file-system-adapter.ts +85 -85
  12. package/ai/infrastructure/llm/tools-llm-provider.ts +54 -54
  13. package/ai/infrastructure/version-docs-writer.ts +143 -143
  14. package/ai/presentation/ai-prompts.ts +239 -239
  15. package/application/config-only/handler.mjs +2 -0
  16. package/application/home/handler.mjs +2 -0
  17. package/application/ports/cli-runtime-ports.ts +91 -87
  18. package/application/report/config-only-reporter.mjs +64 -63
  19. package/application/use-cases/dispatch-mode.ts +18 -18
  20. package/builders/config-orchestrator.mjs +34 -34
  21. package/builders/project-links.mjs +50 -26
  22. package/builders/root-config-builder.d.mts +16 -0
  23. package/builders/root-config-builder.mjs +61 -53
  24. package/builders/route-builders.mjs +136 -136
  25. package/builders/theme-template.mjs +76 -76
  26. package/builders/version-config-builder.mjs +186 -186
  27. package/content/docs-en.mjs +549 -545
  28. package/content/docs-es.mjs +462 -460
  29. package/content/docs-pt.mjs +462 -460
  30. package/contracts/doc-versions.mjs +3 -3
  31. package/contracts/index.mjs +10 -0
  32. package/contracts/layouts-paths.d.mts +17 -0
  33. package/contracts/layouts-paths.mjs +68 -0
  34. package/data/default-site-config.mjs +321 -321
  35. package/data/i18n-langmenu.mjs +347 -347
  36. package/data/layouts.mjs +436 -436
  37. package/data/path-mappings.mjs +26 -26
  38. package/data/route-metas.mjs +93 -93
  39. package/data/theme-colors.mjs +443 -443
  40. package/data/urls.mjs +12 -12
  41. package/domain/models/cli-options.ts +33 -18
  42. package/domain/services/layouts-migration.d.mts +15 -0
  43. package/domain/services/layouts-migration.mjs +60 -0
  44. package/home/constants.mjs +4 -4
  45. package/index.mjs +37 -37
  46. package/infrastructure/gitpagedocs-config-file.ts +47 -47
  47. package/infrastructure/home/runtime.mjs +37 -37
  48. package/package.json +3 -3
  49. package/presentation/commands/ai-info.ts +57 -57
  50. package/presentation/commands/config-clear.ts +23 -23
  51. package/presentation/commands/config-info.ts +24 -24
  52. package/presentation/commands/diagnostics.ts +164 -164
  53. package/presentation/commands/docs.ts +37 -37
  54. package/presentation/commands/mcp.ts +20 -20
  55. package/presentation/commands/pages.ts +101 -101
  56. package/presentation/commands/password.ts +62 -62
  57. package/presentation/commands/run-command.ts +70 -70
  58. package/presentation/index.ts +124 -116
  59. package/presentation/options/parser.ts +119 -104
  60. package/presentation/options/resolver.ts +46 -48
  61. package/presentation/options/schema.ts +4 -0
  62. package/presentation/ui/clack.ts +91 -91
  63. package/presentation/ui/layouts-prompts.ts +124 -0
  64. package/presentation/ui/prompts.ts +146 -138
  65. package/presentation/ui/tty.ts +37 -0
  66. package/runtime/git-ops.mjs +110 -110
  67. package/runtime/layouts-migrator.d.mts +11 -0
  68. package/runtime/layouts-migrator.mjs +103 -0
  69. package/runtime/output.mjs +159 -146
  70. package/runtime/workflow.mjs +129 -129
  71. package/start.mjs +12 -12
  72. package/ui/logger.mjs +9 -9
@@ -1,138 +1,146 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import { execSync } from "node:child_process";
4
- import type { CliOptions } from "../../domain/models/cli-options";
5
- import { DEFAULTS } from "../options/schema";
6
- import { askText, askConfirm, note } from "./clack";
7
-
8
- function isCiOrNonTty(): boolean {
9
- if (process.env.CI === "true") return true;
10
- if (process.env.GITHUB_ACTIONS === "true") return true;
11
- if (process.stdin && !process.stdin.isTTY) return true;
12
- return false;
13
- }
14
-
15
- /** True when clack prompts can run (interactive TTY, not CI). */
16
- export function interactivePromptsAvailable(): boolean {
17
- return !isCiOrNonTty();
18
- }
19
-
20
- export function shouldRunInteractive(argv: string[]): boolean {
21
- if (isCiOrNonTty()) return false;
22
- const args = argv.slice(2);
23
- if (args.includes("--interactive") || args.includes("-i")) return true;
24
- if (args.length === 0) return true;
25
- if (args.length === 1 && args[0] === "--home") return true;
26
- return false;
27
- }
28
-
29
- export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions> {
30
- const outputDir = await askText({
31
- message: "Output directory:",
32
- defaultValue: parsed.outputDir ?? DEFAULTS.home.outputDir,
33
- validate: (v) => (v && v.trim() ? undefined : "Required"),
34
- });
35
- const repositorySearch = await askConfirm(
36
- "Enable repository search home?",
37
- parsed.repositorySearch ?? DEFAULTS.repositorySearch,
38
- );
39
- const basePath = await askText({
40
- message: "Base path (e.g. git-page-docs, empty for root):",
41
- defaultValue: parsed.basePath ?? DEFAULTS.basePath,
42
- });
43
-
44
- return {
45
- ...parsed,
46
- outputDir: outputDir.trim(),
47
- repositorySearch,
48
- basePath: basePath?.trim() ?? "",
49
- };
50
- }
51
-
52
- /**
53
- * Deploy/--push needs a GitHub owner + repo. When they are missing, prompt for
54
- * them (pre-filled from the git `origin` remote when available) instead of
55
- * crashing with "`--push` requires owner and repo".
56
- */
57
- export async function askOwnerRepo(
58
- detected: { owner: string; repo: string } | null,
59
- defaults?: { owner?: string; repo?: string },
60
- ): Promise<{ owner: string; repo: string }> {
61
- const owner = await askText({
62
- message: "GitHub owner (user or organization):",
63
- defaultValue: defaults?.owner || detected?.owner || "",
64
- validate: (v) => (v && v.trim() ? undefined : "Owner is required."),
65
- });
66
- const repo = await askText({
67
- message: "GitHub repository name:",
68
- defaultValue: defaults?.repo || detected?.repo || "",
69
- validate: (v) => (v && v.trim() ? undefined : "Repository is required."),
70
- });
71
- return { owner: owner.trim(), repo: repo.trim() };
72
- }
73
-
74
- export async function promptDeployOptions(
75
- parsed: CliOptions,
76
- detected: { owner: string; repo: string } | null,
77
- ): Promise<CliOptions> {
78
- note(
79
- "Deploy publishes gitpagedocs to GitHub Pages and needs the target\nrepository (owner/repo). Press Enter to accept a detected value.",
80
- "GitHub Pages deploy",
81
- );
82
- const { owner, repo } = await askOwnerRepo(detected, {
83
- owner: parsed.githubOwner,
84
- repo: parsed.githubRepo,
85
- });
86
-
87
- return {
88
- ...parsed,
89
- githubOwner: owner,
90
- githubRepo: repo,
91
- };
92
- }
93
-
94
- /**
95
- * Deploy/--push needs a git repository in the current folder. When there isn't
96
- * one, offer to run `git init` instead of crashing with "not a git repository".
97
- * In CI/non-TTY this is a no-op so the downstream guard still reports clearly.
98
- */
99
- export async function ensureGitRepoInteractive(root: string): Promise<void> {
100
- if (existsSync(path.join(root, ".git"))) return;
101
- if (!interactivePromptsAvailable()) return;
102
- const init = await askConfirm(
103
- "This folder is not a git repository yet. Initialize one here (git init)?",
104
- true,
105
- );
106
- if (!init) {
107
- note("Deploy needs a git repository. Run `git init`, then retry.", "Heads up");
108
- return;
109
- }
110
- try {
111
- execSync("git init", { cwd: root, stdio: "ignore" });
112
- note("Initialized an empty git repository.", "git");
113
- } catch {
114
- note("Could not run `git init`. Initialize git manually, then retry.", "git");
115
- }
116
- }
117
-
118
- export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> {
119
- const useLocalLayoutConfig = await askConfirm(
120
- "Generate local layout templates?",
121
- parsed.useLocalLayoutConfig ?? false,
122
- );
123
- const githubOwner = await askText({
124
- message: "GitHub owner (optional):",
125
- defaultValue: parsed.githubOwner ?? "",
126
- });
127
- const githubRepo = await askText({
128
- message: "GitHub repo (optional):",
129
- defaultValue: parsed.githubRepo ?? "",
130
- });
131
-
132
- return {
133
- ...parsed,
134
- useLocalLayoutConfig,
135
- githubOwner: githubOwner?.trim() ?? "",
136
- githubRepo: githubRepo?.trim() ?? "",
137
- };
138
- }
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import type { CliOptions } from "../../domain/models/cli-options";
5
+ import { DEFAULTS } from "../options/schema";
6
+ import { askText, askConfirm, note } from "./clack";
7
+ import { interactivePromptsAvailable, promptingEnabled } from "./tty";
8
+ import { askLayoutsSource, askLayoutsDir } from "./layouts-prompts";
9
+
10
+ export { interactivePromptsAvailable };
11
+
12
+ /**
13
+ * Every command prompts when it runs in a real terminal, so a bare invocation
14
+ * reflects what the user actually needs instead of silently taking defaults.
15
+ * CI, a piped stdin, and an explicit `--no-interactive` / `--yes` / `-y` opt out;
16
+ * `--interactive` / `-i` remains accepted and reads the same either way.
17
+ */
18
+ export function shouldRunInteractive(argv: string[]): boolean {
19
+ return promptingEnabled(argv.slice(2));
20
+ }
21
+
22
+ export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions> {
23
+ const outputDir = await askText({
24
+ message: "Output directory:",
25
+ defaultValue: parsed.outputDir ?? DEFAULTS.home.outputDir,
26
+ validate: (v) => (v && v.trim() ? undefined : "Required"),
27
+ });
28
+ const repositorySearch = await askConfirm(
29
+ "Enable repository search home?",
30
+ parsed.repositorySearch ?? DEFAULTS.repositorySearch,
31
+ );
32
+ const basePath = await askText({
33
+ message: "Base path (e.g. git-page-docs, empty for root):",
34
+ defaultValue: parsed.basePath ?? DEFAULTS.basePath,
35
+ });
36
+
37
+ return {
38
+ ...parsed,
39
+ outputDir: outputDir.trim(),
40
+ repositorySearch,
41
+ basePath: basePath?.trim() ?? "",
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Deploy/--push needs a GitHub owner + repo. When they are missing, prompt for
47
+ * them (pre-filled from the git `origin` remote when available) instead of
48
+ * crashing with "`--push` requires owner and repo".
49
+ */
50
+ export async function askOwnerRepo(
51
+ detected: { owner: string; repo: string } | null,
52
+ defaults?: { owner?: string; repo?: string },
53
+ ): Promise<{ owner: string; repo: string }> {
54
+ const owner = await askText({
55
+ message: "GitHub owner (user or organization):",
56
+ defaultValue: defaults?.owner || detected?.owner || "",
57
+ validate: (v) => (v && v.trim() ? undefined : "Owner is required."),
58
+ });
59
+ const repo = await askText({
60
+ message: "GitHub repository name:",
61
+ defaultValue: defaults?.repo || detected?.repo || "",
62
+ validate: (v) => (v && v.trim() ? undefined : "Repository is required."),
63
+ });
64
+ return { owner: owner.trim(), repo: repo.trim() };
65
+ }
66
+
67
+ export async function promptDeployOptions(
68
+ parsed: CliOptions,
69
+ detected: { owner: string; repo: string } | null,
70
+ ): Promise<CliOptions> {
71
+ note(
72
+ "Deploy publishes gitpagedocs to GitHub Pages and needs the target\nrepository (owner/repo). Press Enter to accept a detected value.",
73
+ "GitHub Pages deploy",
74
+ );
75
+ const { owner, repo } = await askOwnerRepo(detected, {
76
+ owner: parsed.githubOwner,
77
+ repo: parsed.githubRepo,
78
+ });
79
+
80
+ return {
81
+ ...parsed,
82
+ githubOwner: owner,
83
+ githubRepo: repo,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Deploy/--push needs a git repository in the current folder. When there isn't
89
+ * one, offer to run `git init` instead of crashing with "not a git repository".
90
+ * In CI/non-TTY this is a no-op so the downstream guard still reports clearly.
91
+ */
92
+ export async function ensureGitRepoInteractive(root: string): Promise<void> {
93
+ if (existsSync(path.join(root, ".git"))) return;
94
+ if (!interactivePromptsAvailable()) return;
95
+ const init = await askConfirm(
96
+ "This folder is not a git repository yet. Initialize one here (git init)?",
97
+ true,
98
+ );
99
+ if (!init) {
100
+ note("Deploy needs a git repository. Run `git init`, then retry.", "Heads up");
101
+ return;
102
+ }
103
+ try {
104
+ execSync("git init", { cwd: root, stdio: "ignore" });
105
+ note("Initialized an empty git repository.", "git");
106
+ } catch {
107
+ note("Could not run `git init`. Initialize git manually, then retry.", "git");
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Ask only for what the command line did not already answer, so an explicit
113
+ * flag is never second-guessed by a prompt.
114
+ */
115
+ export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> {
116
+ const useLocalLayoutConfig = parsed.explicit.useLocalLayoutConfig
117
+ ? parsed.useLocalLayoutConfig
118
+ : await askLayoutsSource(parsed.useLocalLayoutConfig ?? false);
119
+
120
+ const layoutsDir =
121
+ useLocalLayoutConfig && !parsed.explicit.layoutsDir
122
+ ? await askLayoutsDir(parsed.layoutsDir)
123
+ : parsed.layoutsDir;
124
+
125
+ const githubOwner = parsed.explicit.githubOwner
126
+ ? parsed.githubOwner
127
+ : await askText({
128
+ message: "GitHub owner (optional):",
129
+ defaultValue: parsed.githubOwner ?? "",
130
+ });
131
+
132
+ const githubRepo = parsed.explicit.githubRepo
133
+ ? parsed.githubRepo
134
+ : await askText({
135
+ message: "GitHub repo (optional):",
136
+ defaultValue: parsed.githubRepo ?? "",
137
+ });
138
+
139
+ return {
140
+ ...parsed,
141
+ useLocalLayoutConfig,
142
+ layoutsDir,
143
+ githubOwner: githubOwner?.trim() ?? "",
144
+ githubRepo: githubRepo?.trim() ?? "",
145
+ };
146
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Terminal capability detection.
3
+ *
4
+ * Kept in its own module so every prompt module can ask "may I prompt?" without
5
+ * importing the prompt modules themselves (which would be circular).
6
+ */
7
+
8
+ /** Environment variables that mark a non-interactive automated run. */
9
+ const CI_ENV_FLAGS = ["CI", "GITHUB_ACTIONS"] as const;
10
+
11
+ /** Flags that explicitly opt out of prompting. */
12
+ export const NON_INTERACTIVE_FLAGS = ["--no-interactive", "--yes", "-y"] as const;
13
+
14
+ function isCiOrNonTty(): boolean {
15
+ for (const flag of CI_ENV_FLAGS) {
16
+ if (process.env[flag] === "true") return true;
17
+ }
18
+ return Boolean(process.stdin && !process.stdin.isTTY);
19
+ }
20
+
21
+ /** True when clack prompts can run (interactive TTY, not CI). */
22
+ export function interactivePromptsAvailable(): boolean {
23
+ return !isCiOrNonTty();
24
+ }
25
+
26
+ /** True when the user explicitly asked for a non-interactive run. */
27
+ export function promptingOptedOut(args: readonly string[]): boolean {
28
+ return NON_INTERACTIVE_FLAGS.some((flag) => args.includes(flag));
29
+ }
30
+
31
+ /**
32
+ * True when prompts should run for this invocation: a real terminal, and no
33
+ * explicit opt-out.
34
+ */
35
+ export function promptingEnabled(args: readonly string[]): boolean {
36
+ return interactivePromptsAvailable() && !promptingOptedOut(args);
37
+ }
@@ -1,110 +1,110 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import { execSync } from "node:child_process";
4
-
5
- /** Detect { owner, repo } from the git `origin` remote URL, or null. */
6
- export function detectRepoFromGit(root) {
7
- try {
8
- const url = execSync("git remote get-url origin", { cwd: root, stdio: "pipe" }).toString().trim();
9
- const match = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/);
10
- if (match) return { owner: match[1], repo: match[2] };
11
- } catch {
12
- // no remote / not a repo
13
- }
14
- return null;
15
- }
16
-
17
- export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
18
- try {
19
- execSync("gh --version", { cwd: root, stdio: "ignore" });
20
- } catch {
21
- console.warn(
22
- "GitHub CLI (gh) not found. Could not auto-configure Pages source. Set GitHub Pages source to 'GitHub Actions' manually in repository settings.",
23
- );
24
- return;
25
- }
26
-
27
- 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`,
32
- ];
33
-
34
- for (const command of candidates) {
35
- try {
36
- execSync(command, { cwd: root, stdio: "ignore" });
37
- console.log("GitHub Pages source configured for GitHub Actions.");
38
- return;
39
- } catch {
40
- // Try next candidate endpoint.
41
- }
42
- }
43
-
44
- console.warn(
45
- "Could not auto-configure Pages source through GitHub API. Set repository Pages source to 'GitHub Actions' manually.",
46
- );
47
- }
48
-
49
- export function getCurrentGitBranch(root) {
50
- try {
51
- const branch = execSync("git branch --show-current", { cwd: root, stdio: "pipe" }).toString().trim();
52
- return branch || "main";
53
- } catch {
54
- return "main";
55
- }
56
- }
57
-
58
- export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment) {
59
- const owner = sanitizeSegment(options.githubOwner);
60
- const repo = sanitizeSegment(options.githubRepo);
61
- if (!owner || !repo) {
62
- throw new Error("`--push` requires owner and repo. Use `--owner <owner> --repo <repo>` or `--<owner> --<repo>`.");
63
- }
64
- if (!existsSync(path.join(root, ".git"))) {
65
- throw new Error("Current directory is not a git repository. Initialize git before using --push.");
66
- }
67
-
68
- const repoUrl = `https://github.com/${owner}/${repo}.git`;
69
- try {
70
- execSync("git remote get-url origin", { cwd: root, stdio: "ignore" });
71
- } catch {
72
- execSync(`git remote add origin "${repoUrl}"`, { cwd: root, stdio: "inherit" });
73
- }
74
-
75
- execSync('git add "gitpagedocs" ".github/workflows/gitpagedocs-pages.yml"', { cwd: root, stdio: "inherit" });
76
-
77
- let hasStagedChanges = false;
78
- try {
79
- execSync("git diff --cached --quiet", { cwd: root, stdio: "ignore" });
80
- hasStagedChanges = false;
81
- } catch {
82
- hasStagedChanges = true;
83
- }
84
-
85
- if (!hasStagedChanges) {
86
- return;
87
- }
88
-
89
- execSync('git commit -m "chore: setup gitpagedocs pages workflow"', { cwd: root, stdio: "inherit" });
90
-
91
- const currentBranch = getCurrentGitBranch(root);
92
- try {
93
- execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
94
- tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
95
- return;
96
- } catch {
97
- console.warn("Initial push failed. Trying automatic rebase with remote branch...");
98
- }
99
-
100
- try {
101
- execSync(`git pull --rebase origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
102
- execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
103
- } catch {
104
- throw new Error(
105
- `Failed to push after automatic rebase on branch '${currentBranch}'. Resolve conflicts and run 'git push -u origin ${currentBranch}' manually.`,
106
- );
107
- }
108
-
109
- tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
110
- }
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+
5
+ /** Detect { owner, repo } from the git `origin` remote URL, or null. */
6
+ export function detectRepoFromGit(root) {
7
+ try {
8
+ const url = execSync("git remote get-url origin", { cwd: root, stdio: "pipe" }).toString().trim();
9
+ const match = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/);
10
+ if (match) return { owner: match[1], repo: match[2] };
11
+ } catch {
12
+ // no remote / not a repo
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
18
+ try {
19
+ execSync("gh --version", { cwd: root, stdio: "ignore" });
20
+ } catch {
21
+ console.warn(
22
+ "GitHub CLI (gh) not found. Could not auto-configure Pages source. Set GitHub Pages source to 'GitHub Actions' manually in repository settings.",
23
+ );
24
+ return;
25
+ }
26
+
27
+ 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`,
32
+ ];
33
+
34
+ for (const command of candidates) {
35
+ try {
36
+ execSync(command, { cwd: root, stdio: "ignore" });
37
+ console.log("GitHub Pages source configured for GitHub Actions.");
38
+ return;
39
+ } catch {
40
+ // Try next candidate endpoint.
41
+ }
42
+ }
43
+
44
+ console.warn(
45
+ "Could not auto-configure Pages source through GitHub API. Set repository Pages source to 'GitHub Actions' manually.",
46
+ );
47
+ }
48
+
49
+ export function getCurrentGitBranch(root) {
50
+ try {
51
+ const branch = execSync("git branch --show-current", { cwd: root, stdio: "pipe" }).toString().trim();
52
+ return branch || "main";
53
+ } catch {
54
+ return "main";
55
+ }
56
+ }
57
+
58
+ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment) {
59
+ const owner = sanitizeSegment(options.githubOwner);
60
+ const repo = sanitizeSegment(options.githubRepo);
61
+ if (!owner || !repo) {
62
+ throw new Error("`--push` requires owner and repo. Use `--owner <owner> --repo <repo>` or `--<owner> --<repo>`.");
63
+ }
64
+ if (!existsSync(path.join(root, ".git"))) {
65
+ throw new Error("Current directory is not a git repository. Initialize git before using --push.");
66
+ }
67
+
68
+ const repoUrl = `https://github.com/${owner}/${repo}.git`;
69
+ try {
70
+ execSync("git remote get-url origin", { cwd: root, stdio: "ignore" });
71
+ } catch {
72
+ execSync(`git remote add origin "${repoUrl}"`, { cwd: root, stdio: "inherit" });
73
+ }
74
+
75
+ execSync('git add "gitpagedocs" ".github/workflows/gitpagedocs-pages.yml"', { cwd: root, stdio: "inherit" });
76
+
77
+ let hasStagedChanges = false;
78
+ try {
79
+ execSync("git diff --cached --quiet", { cwd: root, stdio: "ignore" });
80
+ hasStagedChanges = false;
81
+ } catch {
82
+ hasStagedChanges = true;
83
+ }
84
+
85
+ if (!hasStagedChanges) {
86
+ return;
87
+ }
88
+
89
+ execSync('git commit -m "chore: setup gitpagedocs pages workflow"', { cwd: root, stdio: "inherit" });
90
+
91
+ const currentBranch = getCurrentGitBranch(root);
92
+ try {
93
+ execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
94
+ tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
95
+ return;
96
+ } catch {
97
+ console.warn("Initial push failed. Trying automatic rebase with remote branch...");
98
+ }
99
+
100
+ try {
101
+ execSync(`git pull --rebase origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
102
+ execSync(`git push -u origin ${currentBranch}`, { cwd: root, stdio: "inherit" });
103
+ } catch {
104
+ throw new Error(
105
+ `Failed to push after automatic rebase on branch '${currentBranch}'. Resolve conflicts and run 'git push -u origin ${currentBranch}' manually.`,
106
+ );
107
+ }
108
+
109
+ tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
110
+ }
@@ -0,0 +1,11 @@
1
+ export interface LayoutsMigrationResult {
2
+ moved: string[];
3
+ failed: Array<{ file: string; reason: string }>;
4
+ }
5
+
6
+ export declare function listFilesRecursively(absoluteDir: string): string[];
7
+
8
+ export declare function executeLayoutsMigration(
9
+ root: string,
10
+ plan: { from: string; to: string; files: string[] },
11
+ ): LayoutsMigrationResult;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Filesystem side of the legacy layouts migration.
3
+ *
4
+ * The decision of *what* moves lives in the domain planner; this module only
5
+ * reads directories and performs the move, so both halves stay testable on
6
+ * their own.
7
+ */
8
+ import {
9
+ existsSync,
10
+ readdirSync,
11
+ statSync,
12
+ mkdirSync,
13
+ renameSync,
14
+ copyFileSync,
15
+ rmSync,
16
+ } from "node:fs";
17
+ import path from "node:path";
18
+
19
+ /**
20
+ * List every file under a directory, as POSIX paths relative to it.
21
+ *
22
+ * @param {string} absoluteDir
23
+ * @returns {string[]} Empty when the directory is absent or unreadable.
24
+ */
25
+ export function listFilesRecursively(absoluteDir) {
26
+ if (!absoluteDir || !existsSync(absoluteDir)) return [];
27
+
28
+ const found = [];
29
+ const walk = (currentDir, prefix) => {
30
+ let entries;
31
+ try {
32
+ entries = readdirSync(currentDir);
33
+ } catch {
34
+ return;
35
+ }
36
+ for (const entry of entries) {
37
+ const absolute = path.join(currentDir, entry);
38
+ const relative = prefix ? `${prefix}/${entry}` : entry;
39
+ let isDirectory = false;
40
+ try {
41
+ isDirectory = statSync(absolute).isDirectory();
42
+ } catch {
43
+ continue;
44
+ }
45
+ if (isDirectory) walk(absolute, relative);
46
+ else found.push(relative);
47
+ }
48
+ };
49
+
50
+ walk(absoluteDir, "");
51
+ return found.sort();
52
+ }
53
+
54
+ /**
55
+ * Move one file, falling back to copy + delete when rename is not possible
56
+ * (different volumes, or a locked handle on Windows).
57
+ *
58
+ * @param {string} fromAbsolute
59
+ * @param {string} toAbsolute
60
+ */
61
+ function moveFile(fromAbsolute, toAbsolute) {
62
+ mkdirSync(path.dirname(toAbsolute), { recursive: true });
63
+ try {
64
+ renameSync(fromAbsolute, toAbsolute);
65
+ } catch {
66
+ copyFileSync(fromAbsolute, toAbsolute);
67
+ rmSync(fromAbsolute, { force: true });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Execute a migration plan produced by `planLayoutsMigration`.
73
+ *
74
+ * Files are moved one by one so a single failure is reported without aborting
75
+ * the rest; the legacy folder is removed only once it is genuinely empty.
76
+ *
77
+ * @param {string} root Project root the plan's paths are relative to.
78
+ * @param {{ from: string, to: string, files: string[] }} plan
79
+ * @returns {{ moved: string[], failed: Array<{ file: string, reason: string }> }}
80
+ */
81
+ export function executeLayoutsMigration(root, plan) {
82
+ const moved = [];
83
+ const failed = [];
84
+
85
+ const fromRoot = path.join(root, ...plan.from.split("/"));
86
+ const toRoot = path.join(root, ...plan.to.split("/"));
87
+
88
+ for (const file of plan.files) {
89
+ const segments = file.split("/");
90
+ try {
91
+ moveFile(path.join(fromRoot, ...segments), path.join(toRoot, ...segments));
92
+ moved.push(file);
93
+ } catch (error) {
94
+ failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
95
+ }
96
+ }
97
+
98
+ if (failed.length === 0 && listFilesRecursively(fromRoot).length === 0) {
99
+ rmSync(fromRoot, { recursive: true, force: true });
100
+ }
101
+
102
+ return { moved, failed };
103
+ }