@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.
- package/README.md +193 -191
- package/ai/application/ai-command.ts +24 -24
- package/ai/application/ai-provider-factory.ts +22 -22
- package/ai/application/docs-pattern.ts +68 -68
- package/ai/application/run-ai-cli-command.ts +167 -167
- package/ai/config.ts +50 -50
- package/ai/core/models/ai-cli-config.ts +33 -33
- package/ai/core/models/parse-ai-cli-config.ts +78 -78
- package/ai/core/ports/illm-provider.ts +32 -32
- package/ai/infrastructure/ai-config-file.ts +101 -101
- package/ai/infrastructure/file-system-adapter.ts +85 -85
- package/ai/infrastructure/llm/tools-llm-provider.ts +54 -54
- package/ai/infrastructure/version-docs-writer.ts +143 -143
- package/ai/presentation/ai-prompts.ts +239 -239
- package/application/config-only/handler.mjs +2 -0
- package/application/home/handler.mjs +2 -0
- package/application/ports/cli-runtime-ports.ts +91 -87
- package/application/report/config-only-reporter.mjs +64 -63
- package/application/use-cases/dispatch-mode.ts +18 -18
- package/builders/config-orchestrator.mjs +34 -34
- package/builders/project-links.mjs +50 -26
- package/builders/root-config-builder.d.mts +16 -0
- package/builders/root-config-builder.mjs +61 -53
- package/builders/route-builders.mjs +136 -136
- package/builders/theme-template.mjs +76 -76
- package/builders/version-config-builder.mjs +186 -186
- package/content/docs-en.mjs +549 -545
- package/content/docs-es.mjs +462 -460
- package/content/docs-pt.mjs +462 -460
- package/contracts/doc-versions.mjs +3 -3
- package/contracts/index.mjs +10 -0
- package/contracts/layouts-paths.d.mts +17 -0
- package/contracts/layouts-paths.mjs +68 -0
- package/data/default-site-config.mjs +321 -321
- package/data/i18n-langmenu.mjs +347 -347
- package/data/layouts.mjs +436 -436
- package/data/path-mappings.mjs +26 -26
- package/data/route-metas.mjs +93 -93
- package/data/theme-colors.mjs +443 -443
- package/data/urls.mjs +12 -12
- package/domain/models/cli-options.ts +33 -18
- package/domain/services/layouts-migration.d.mts +15 -0
- package/domain/services/layouts-migration.mjs +60 -0
- package/home/constants.mjs +4 -4
- package/index.mjs +37 -37
- package/infrastructure/gitpagedocs-config-file.ts +47 -47
- package/infrastructure/home/runtime.mjs +37 -37
- package/package.json +3 -3
- package/presentation/commands/ai-info.ts +57 -57
- package/presentation/commands/config-clear.ts +23 -23
- package/presentation/commands/config-info.ts +24 -24
- package/presentation/commands/diagnostics.ts +164 -164
- package/presentation/commands/docs.ts +37 -37
- package/presentation/commands/mcp.ts +20 -20
- package/presentation/commands/pages.ts +101 -101
- package/presentation/commands/password.ts +62 -62
- package/presentation/commands/run-command.ts +70 -70
- package/presentation/index.ts +124 -116
- package/presentation/options/parser.ts +119 -104
- package/presentation/options/resolver.ts +46 -48
- package/presentation/options/schema.ts +4 -0
- package/presentation/ui/clack.ts +91 -91
- package/presentation/ui/layouts-prompts.ts +124 -0
- package/presentation/ui/prompts.ts +146 -138
- package/presentation/ui/tty.ts +37 -0
- package/runtime/git-ops.mjs +110 -110
- package/runtime/layouts-migrator.d.mts +11 -0
- package/runtime/layouts-migrator.mjs +103 -0
- package/runtime/output.mjs +159 -146
- package/runtime/workflow.mjs +129 -129
- package/start.mjs +12 -12
- 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
note("
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
package/runtime/git-ops.mjs
CHANGED
|
@@ -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
|
+
}
|