@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,104 +1,119 @@
|
|
|
1
|
-
import type { CliOptions } from "../../domain/models/cli-options";
|
|
2
|
-
import { DEFAULTS } from "./schema";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"--
|
|
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
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.filter((a) =>
|
|
48
|
-
.filter((a) => !
|
|
49
|
-
.
|
|
50
|
-
.filter(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
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
|
-
|
|
1
|
+
import type { CliOptions } from "../../domain/models/cli-options";
|
|
2
|
+
import { DEFAULTS } from "./schema";
|
|
3
|
+
import { normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
|
|
4
|
+
|
|
5
|
+
const KNOWN_FLAGS = new Set([
|
|
6
|
+
"--build",
|
|
7
|
+
"--serve",
|
|
8
|
+
"--layoutconfig",
|
|
9
|
+
"--full",
|
|
10
|
+
"--push",
|
|
11
|
+
"--home",
|
|
12
|
+
"--search",
|
|
13
|
+
"--path",
|
|
14
|
+
"--output",
|
|
15
|
+
"--layouts-dir",
|
|
16
|
+
"--interactive",
|
|
17
|
+
"-i",
|
|
18
|
+
"--no-interactive",
|
|
19
|
+
"--yes",
|
|
20
|
+
"-y",
|
|
21
|
+
"--ai",
|
|
22
|
+
"--pages-actions",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function readOptionValue(args: string[], optionName: string): string {
|
|
26
|
+
const equalsArg = args.find((a) => a.startsWith(`${optionName}=`));
|
|
27
|
+
if (equalsArg) return equalsArg.slice(optionName.length + 1).trim();
|
|
28
|
+
const index = args.indexOf(optionName);
|
|
29
|
+
if (index >= 0) {
|
|
30
|
+
const next = args[index + 1];
|
|
31
|
+
if (next && !next.startsWith("--")) return next.trim();
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCliOptions(argv: string[], env: NodeJS.ProcessEnv): CliOptions {
|
|
37
|
+
const args = argv.slice(2);
|
|
38
|
+
|
|
39
|
+
let githubOwner = readOptionValue(args, "--owner");
|
|
40
|
+
let githubRepo = readOptionValue(args, "--repo");
|
|
41
|
+
const docsPath = readOptionValue(args, "--path");
|
|
42
|
+
const outputDir = readOptionValue(args, "--output");
|
|
43
|
+
const layoutsDir = readOptionValue(args, "--layouts-dir");
|
|
44
|
+
const searchRaw = readOptionValue(args, "--search");
|
|
45
|
+
|
|
46
|
+
const fallbackDashedArgs = args
|
|
47
|
+
.filter((a) => a.startsWith("--"))
|
|
48
|
+
.filter((a) => !KNOWN_FLAGS.has(a))
|
|
49
|
+
.filter((a) => !a.startsWith("--owner"))
|
|
50
|
+
.filter((a) => !a.startsWith("--repo"))
|
|
51
|
+
.filter((a) => !a.startsWith("--path"))
|
|
52
|
+
.filter((a) => !a.startsWith("--home"))
|
|
53
|
+
.filter((a) => !a.startsWith("--output"))
|
|
54
|
+
.filter((a) => !a.startsWith("--layouts-dir"))
|
|
55
|
+
.filter((a) => !a.startsWith("--search"))
|
|
56
|
+
.map((a) => a.replace(/^--/, "").trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
|
|
59
|
+
if (!githubOwner && fallbackDashedArgs[0]) githubOwner = fallbackDashedArgs[0];
|
|
60
|
+
if (!githubRepo && fallbackDashedArgs[1]) githubRepo = fallbackDashedArgs[1];
|
|
61
|
+
|
|
62
|
+
// New verb aliases (Phase 6) that map onto existing modes without new modes.
|
|
63
|
+
const command = args[0];
|
|
64
|
+
const isDocument = command === "document" || (command?.startsWith("document:") ?? false);
|
|
65
|
+
const isDeploy = command === "deploy" || command === "pages";
|
|
66
|
+
|
|
67
|
+
const isBuild = args.includes("--build") || env.GITPAGEDOCS_BUILD === "1";
|
|
68
|
+
const isServe = args.includes("--serve");
|
|
69
|
+
const useLocalLayoutConfig = args.includes("--layoutconfig");
|
|
70
|
+
const shouldPush = args.includes("--push") || isDeploy;
|
|
71
|
+
const isHome = args.includes("--home");
|
|
72
|
+
const isInteractive = args.includes("--interactive") || args.includes("-i");
|
|
73
|
+
const isAi = command === "ai" || args.includes("--ai") || isDocument;
|
|
74
|
+
const aiCommand =
|
|
75
|
+
command === "ai"
|
|
76
|
+
? args[1]
|
|
77
|
+
: command === "document"
|
|
78
|
+
? args[1] ?? "repo"
|
|
79
|
+
: command?.startsWith("document:")
|
|
80
|
+
? command.split(":")[1]
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
const mode: import("../../domain/models/cli-options").CliMode = isAi ? "ai" : isHome ? "home" : args.includes("--full") ? "full" : "config-only";
|
|
84
|
+
const repositorySearch =
|
|
85
|
+
searchRaw === "true"
|
|
86
|
+
? true
|
|
87
|
+
: searchRaw === "false"
|
|
88
|
+
? false
|
|
89
|
+
: isHome
|
|
90
|
+
? DEFAULTS.home.repositorySearch
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
const normalizedPath = docsPath.trim();
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
isBuild,
|
|
97
|
+
isServe,
|
|
98
|
+
mode,
|
|
99
|
+
aiCommand,
|
|
100
|
+
outputDir: outputDir || (isHome ? DEFAULTS.home.outputDir : DEFAULTS.outputDir),
|
|
101
|
+
layoutsDir: normalizeLayoutsDir(layoutsDir || DEFAULTS.layoutsDir),
|
|
102
|
+
useLocalLayoutConfig,
|
|
103
|
+
shouldPush,
|
|
104
|
+
githubOwner,
|
|
105
|
+
githubRepo,
|
|
106
|
+
docsPath: normalizedPath,
|
|
107
|
+
basePath: normalizedPath,
|
|
108
|
+
repositorySearch,
|
|
109
|
+
isInteractive,
|
|
110
|
+
hasArgs: args.length > 0,
|
|
111
|
+
explicit: {
|
|
112
|
+
useLocalLayoutConfig,
|
|
113
|
+
layoutsDir: Boolean(layoutsDir),
|
|
114
|
+
githubOwner: Boolean(githubOwner),
|
|
115
|
+
githubRepo: Boolean(githubRepo),
|
|
116
|
+
outputDir: Boolean(outputDir),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -1,48 +1,46 @@
|
|
|
1
|
-
import type { CliOptions } from "../../domain/models/cli-options";
|
|
2
|
-
import { parseCliOptions } from "./parser";
|
|
3
|
-
import {
|
|
4
|
-
shouldRunInteractive,
|
|
5
|
-
promptConfigOnlyOptions,
|
|
6
|
-
promptHomeOptions,
|
|
7
|
-
promptDeployOptions,
|
|
8
|
-
ensureGitRepoInteractive,
|
|
9
|
-
interactivePromptsAvailable,
|
|
10
|
-
} from "../ui/prompts";
|
|
11
|
-
import { DEFAULTS } from "./schema";
|
|
12
|
-
// @ts-expect-error .mjs runtime module is type-less in this package.
|
|
13
|
-
import { detectRepoFromGit } from "../../runtime/git-ops.mjs";
|
|
14
|
-
|
|
15
|
-
export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Promise<CliOptions> {
|
|
16
|
-
const parsed = parseCliOptions(argv, env);
|
|
17
|
-
|
|
18
|
-
if (parsed.mode === "home") {
|
|
19
|
-
parsed.repositorySearch = parsed.repositorySearch ?? DEFAULTS.home.repositorySearch;
|
|
20
|
-
parsed.basePath = parsed.basePath ?? parsed.docsPath ?? DEFAULTS.home.basePath;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// `deploy` / `--push` need owner + repo AND a git repo. If anything is
|
|
24
|
-
// missing, prompt interactively (owner/repo pre-filled from the git origin
|
|
25
|
-
// remote; offer to `git init`) instead of throwing. In CI/non-TTY we fall
|
|
26
|
-
// through so the explicit guards in the push flow still fire clearly.
|
|
27
|
-
if (parsed.shouldPush && interactivePromptsAvailable()) {
|
|
28
|
-
let resolved = parsed;
|
|
29
|
-
if (!parsed.githubOwner || !parsed.githubRepo) {
|
|
30
|
-
const detected = detectRepoFromGit(process.cwd()) as { owner: string; repo: string } | null;
|
|
31
|
-
resolved = await promptDeployOptions(parsed, detected);
|
|
32
|
-
}
|
|
33
|
-
await ensureGitRepoInteractive(process.cwd());
|
|
34
|
-
return resolved;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
1
|
+
import type { CliOptions } from "../../domain/models/cli-options";
|
|
2
|
+
import { parseCliOptions } from "./parser";
|
|
3
|
+
import {
|
|
4
|
+
shouldRunInteractive,
|
|
5
|
+
promptConfigOnlyOptions,
|
|
6
|
+
promptHomeOptions,
|
|
7
|
+
promptDeployOptions,
|
|
8
|
+
ensureGitRepoInteractive,
|
|
9
|
+
interactivePromptsAvailable,
|
|
10
|
+
} from "../ui/prompts";
|
|
11
|
+
import { DEFAULTS } from "./schema";
|
|
12
|
+
// @ts-expect-error .mjs runtime module is type-less in this package.
|
|
13
|
+
import { detectRepoFromGit } from "../../runtime/git-ops.mjs";
|
|
14
|
+
|
|
15
|
+
export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Promise<CliOptions> {
|
|
16
|
+
const parsed = parseCliOptions(argv, env);
|
|
17
|
+
|
|
18
|
+
if (parsed.mode === "home") {
|
|
19
|
+
parsed.repositorySearch = parsed.repositorySearch ?? DEFAULTS.home.repositorySearch;
|
|
20
|
+
parsed.basePath = parsed.basePath ?? parsed.docsPath ?? DEFAULTS.home.basePath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// `deploy` / `--push` need owner + repo AND a git repo. If anything is
|
|
24
|
+
// missing, prompt interactively (owner/repo pre-filled from the git origin
|
|
25
|
+
// remote; offer to `git init`) instead of throwing. In CI/non-TTY we fall
|
|
26
|
+
// through so the explicit guards in the push flow still fire clearly.
|
|
27
|
+
if (parsed.shouldPush && interactivePromptsAvailable()) {
|
|
28
|
+
let resolved = parsed;
|
|
29
|
+
if (!parsed.githubOwner || !parsed.githubRepo) {
|
|
30
|
+
const detected = detectRepoFromGit(process.cwd()) as { owner: string; repo: string } | null;
|
|
31
|
+
resolved = await promptDeployOptions(parsed, detected);
|
|
32
|
+
}
|
|
33
|
+
await ensureGitRepoInteractive(process.cwd());
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Every command prompts in a real terminal; shouldRunInteractive owns the
|
|
38
|
+
// CI / piped-stdin / --no-interactive opt-outs, and each prompt asks only for
|
|
39
|
+
// what the command line left unanswered.
|
|
40
|
+
if (!shouldRunInteractive(argv)) return parsed;
|
|
41
|
+
|
|
42
|
+
if (parsed.mode === "home") return promptHomeOptions(parsed);
|
|
43
|
+
if (parsed.mode === "config-only") return promptConfigOnlyOptions(parsed);
|
|
44
|
+
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_LAYOUTS_DIR } from "../../contracts/layouts-paths.mjs";
|
|
2
|
+
|
|
1
3
|
export const DEFAULTS = {
|
|
2
4
|
outputDir: "gitpagedocs",
|
|
5
|
+
/** Standalone layouts home; local layouts are generated here, never in outputDir. */
|
|
6
|
+
layoutsDir: DEFAULT_LAYOUTS_DIR as string,
|
|
3
7
|
repositorySearch: false,
|
|
4
8
|
basePath: "",
|
|
5
9
|
home: {
|
package/presentation/ui/clack.ts
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin, cancellation-aware wrappers over @clack/prompts. Centralizes the
|
|
3
|
-
* isCancel handling so prompt modules stay declarative and a Ctrl-C exits
|
|
4
|
-
* cleanly (mirroring the previous inquirer behaviour).
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
text,
|
|
8
|
-
password,
|
|
9
|
-
confirm,
|
|
10
|
-
select,
|
|
11
|
-
multiselect,
|
|
12
|
-
isCancel,
|
|
13
|
-
cancel,
|
|
14
|
-
} from "@clack/prompts";
|
|
15
|
-
|
|
16
|
-
function exitIfCancelled<T>(value: T | symbol): T {
|
|
17
|
-
if (isCancel(value)) {
|
|
18
|
-
cancel("Operation cancelled.");
|
|
19
|
-
process.exit(0);
|
|
20
|
-
}
|
|
21
|
-
return value as T;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface AskTextOptions {
|
|
25
|
-
message: string;
|
|
26
|
-
defaultValue?: string;
|
|
27
|
-
placeholder?: string;
|
|
28
|
-
validate?: (value: string) => string | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function askText(options: AskTextOptions): Promise<string> {
|
|
32
|
-
const result = await text({
|
|
33
|
-
message: options.message,
|
|
34
|
-
placeholder: options.placeholder,
|
|
35
|
-
defaultValue: options.defaultValue,
|
|
36
|
-
initialValue: undefined,
|
|
37
|
-
validate: options.validate
|
|
38
|
-
? (value) => options.validate?.(value ?? "")
|
|
39
|
-
: undefined,
|
|
40
|
-
});
|
|
41
|
-
return exitIfCancelled(result);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AskPasswordOptions {
|
|
45
|
-
message: string;
|
|
46
|
-
validate?: (value: string) => string | undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Masked password prompt (input hidden as the user types). */
|
|
50
|
-
export async function askPassword(options: AskPasswordOptions): Promise<string> {
|
|
51
|
-
const result = await password({
|
|
52
|
-
message: options.message,
|
|
53
|
-
validate: options.validate
|
|
54
|
-
? (value) => options.validate?.(value ?? "")
|
|
55
|
-
: undefined,
|
|
56
|
-
});
|
|
57
|
-
return exitIfCancelled(result);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function askConfirm(message: string, initialValue = false): Promise<boolean> {
|
|
61
|
-
const result = await confirm({ message, initialValue });
|
|
62
|
-
return exitIfCancelled(result);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface Choice<T extends string> {
|
|
66
|
-
value: T;
|
|
67
|
-
label: string;
|
|
68
|
-
hint?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function askSelect<T extends string>(
|
|
72
|
-
message: string,
|
|
73
|
-
options: Choice<T>[],
|
|
74
|
-
initialValue?: T,
|
|
75
|
-
): Promise<T> {
|
|
76
|
-
// clack's Option<Value> is a conditional type TS can't match against a generic
|
|
77
|
-
// Choice<T> (identical shape); cast at the boundary, inference flows from initialValue.
|
|
78
|
-
const result = await select<T>({ message, options: options as never, initialValue });
|
|
79
|
-
return exitIfCancelled(result);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function askMultiSelect<T extends string>(
|
|
83
|
-
message: string,
|
|
84
|
-
options: Choice<T>[],
|
|
85
|
-
initialValues?: T[],
|
|
86
|
-
): Promise<T[]> {
|
|
87
|
-
const result = await multiselect<T>({ message, options: options as never, initialValues, required: true });
|
|
88
|
-
return exitIfCancelled(result);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export { intro, outro, note, spinner } from "@clack/prompts";
|
|
1
|
+
/**
|
|
2
|
+
* Thin, cancellation-aware wrappers over @clack/prompts. Centralizes the
|
|
3
|
+
* isCancel handling so prompt modules stay declarative and a Ctrl-C exits
|
|
4
|
+
* cleanly (mirroring the previous inquirer behaviour).
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
text,
|
|
8
|
+
password,
|
|
9
|
+
confirm,
|
|
10
|
+
select,
|
|
11
|
+
multiselect,
|
|
12
|
+
isCancel,
|
|
13
|
+
cancel,
|
|
14
|
+
} from "@clack/prompts";
|
|
15
|
+
|
|
16
|
+
function exitIfCancelled<T>(value: T | symbol): T {
|
|
17
|
+
if (isCancel(value)) {
|
|
18
|
+
cancel("Operation cancelled.");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
return value as T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AskTextOptions {
|
|
25
|
+
message: string;
|
|
26
|
+
defaultValue?: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
validate?: (value: string) => string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function askText(options: AskTextOptions): Promise<string> {
|
|
32
|
+
const result = await text({
|
|
33
|
+
message: options.message,
|
|
34
|
+
placeholder: options.placeholder,
|
|
35
|
+
defaultValue: options.defaultValue,
|
|
36
|
+
initialValue: undefined,
|
|
37
|
+
validate: options.validate
|
|
38
|
+
? (value) => options.validate?.(value ?? "")
|
|
39
|
+
: undefined,
|
|
40
|
+
});
|
|
41
|
+
return exitIfCancelled(result);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AskPasswordOptions {
|
|
45
|
+
message: string;
|
|
46
|
+
validate?: (value: string) => string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Masked password prompt (input hidden as the user types). */
|
|
50
|
+
export async function askPassword(options: AskPasswordOptions): Promise<string> {
|
|
51
|
+
const result = await password({
|
|
52
|
+
message: options.message,
|
|
53
|
+
validate: options.validate
|
|
54
|
+
? (value) => options.validate?.(value ?? "")
|
|
55
|
+
: undefined,
|
|
56
|
+
});
|
|
57
|
+
return exitIfCancelled(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function askConfirm(message: string, initialValue = false): Promise<boolean> {
|
|
61
|
+
const result = await confirm({ message, initialValue });
|
|
62
|
+
return exitIfCancelled(result);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Choice<T extends string> {
|
|
66
|
+
value: T;
|
|
67
|
+
label: string;
|
|
68
|
+
hint?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function askSelect<T extends string>(
|
|
72
|
+
message: string,
|
|
73
|
+
options: Choice<T>[],
|
|
74
|
+
initialValue?: T,
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
// clack's Option<Value> is a conditional type TS can't match against a generic
|
|
77
|
+
// Choice<T> (identical shape); cast at the boundary, inference flows from initialValue.
|
|
78
|
+
const result = await select<T>({ message, options: options as never, initialValue });
|
|
79
|
+
return exitIfCancelled(result);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function askMultiSelect<T extends string>(
|
|
83
|
+
message: string,
|
|
84
|
+
options: Choice<T>[],
|
|
85
|
+
initialValues?: T[],
|
|
86
|
+
): Promise<T[]> {
|
|
87
|
+
const result = await multiselect<T>({ message, options: options as never, initialValues, required: true });
|
|
88
|
+
return exitIfCancelled(result);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { intro, outro, note, spinner } from "@clack/prompts";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout-related prompts: where layouts come from, where they are written, and
|
|
3
|
+
* the opt-in migration away from the legacy `<outputDir>/layouts/` folder.
|
|
4
|
+
*/
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { askConfirm, askSelect, askText, note } from "./clack";
|
|
7
|
+
import { interactivePromptsAvailable } from "./tty";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_LAYOUTS_DIR,
|
|
10
|
+
normalizeLayoutsDir,
|
|
11
|
+
legacyLayoutsDir,
|
|
12
|
+
} from "../../contracts/layouts-paths.mjs";
|
|
13
|
+
import { planLayoutsMigration } from "../../domain/services/layouts-migration.mjs";
|
|
14
|
+
import { listFilesRecursively, executeLayoutsMigration } from "../../runtime/layouts-migrator.mjs";
|
|
15
|
+
|
|
16
|
+
type LayoutsSource = "official" | "local";
|
|
17
|
+
|
|
18
|
+
/** Outcome of a completed migration, for the caller to report. */
|
|
19
|
+
export interface LayoutsMigrationOutcome {
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
movedCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Ask whether layouts come from the official remote source or a local folder. */
|
|
26
|
+
export async function askLayoutsSource(useLocal: boolean): Promise<boolean> {
|
|
27
|
+
const choice = await askSelect<LayoutsSource>(
|
|
28
|
+
"Layout source",
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
value: "official",
|
|
32
|
+
label: "Official layouts (remote)",
|
|
33
|
+
hint: "nothing is written to your repository",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: "local",
|
|
37
|
+
label: "Local layouts",
|
|
38
|
+
hint: `generated into ${DEFAULT_LAYOUTS_DIR}/`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
useLocal ? "local" : "official",
|
|
42
|
+
);
|
|
43
|
+
return choice === "local";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Ask which folder should hold the generated layouts. */
|
|
47
|
+
export async function askLayoutsDir(current: string): Promise<string> {
|
|
48
|
+
const answer = await askText({
|
|
49
|
+
message: "Layouts folder:",
|
|
50
|
+
defaultValue: normalizeLayoutsDir(current),
|
|
51
|
+
validate: (value) => (value.trim() ? undefined : "A folder name is required."),
|
|
52
|
+
});
|
|
53
|
+
return normalizeLayoutsDir(answer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toAbsolute(root: string, repoRelative: string): string {
|
|
57
|
+
return path.join(root, ...repoRelative.split("/"));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Offer to move layouts left behind in the legacy folder.
|
|
62
|
+
*
|
|
63
|
+
* Nothing is moved without an explicit confirmation. Outside a TTY the legacy
|
|
64
|
+
* folder is reported and left alone — the viewer still reads it, so declining
|
|
65
|
+
* (or automating) never breaks an existing site.
|
|
66
|
+
*
|
|
67
|
+
* @returns The outcome when files were moved, otherwise `null`.
|
|
68
|
+
*/
|
|
69
|
+
export async function migrateLegacyLayoutsInteractive(
|
|
70
|
+
root: string,
|
|
71
|
+
options: { outputDir: string; layoutsDir: string },
|
|
72
|
+
): Promise<LayoutsMigrationOutcome | null> {
|
|
73
|
+
const from = legacyLayoutsDir(options.outputDir);
|
|
74
|
+
const to = normalizeLayoutsDir(options.layoutsDir);
|
|
75
|
+
|
|
76
|
+
const plan = planLayoutsMigration({
|
|
77
|
+
outputDir: options.outputDir,
|
|
78
|
+
layoutsDir: to,
|
|
79
|
+
legacyFiles: listFilesRecursively(toAbsolute(root, from)),
|
|
80
|
+
targetFiles: listFilesRecursively(toAbsolute(root, to)),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!plan.required) return null;
|
|
84
|
+
|
|
85
|
+
if (!interactivePromptsAvailable()) {
|
|
86
|
+
note(
|
|
87
|
+
`${plan.files.length} layout file(s) still live in ${plan.from}/.\n` +
|
|
88
|
+
`The viewer still reads them. Run this in a terminal to move them to ${plan.to}/.`,
|
|
89
|
+
"Legacy layouts",
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const overwriteWarning =
|
|
95
|
+
plan.overwrites.length > 0
|
|
96
|
+
? `\n${plan.overwrites.length} file(s) already in ${plan.to}/ would be replaced.`
|
|
97
|
+
: "";
|
|
98
|
+
note(
|
|
99
|
+
`Found ${plan.files.length} layout file(s) in ${plan.from}/.\n` +
|
|
100
|
+
`Layouts are now generated in ${plan.to}/.${overwriteWarning}`,
|
|
101
|
+
"Legacy layouts",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const confirmed = await askConfirm(
|
|
105
|
+
`Move them to ${plan.to}/ now?`,
|
|
106
|
+
plan.overwrites.length === 0,
|
|
107
|
+
);
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
note(`Left untouched — the viewer still reads ${plan.from}/.`, "Legacy layouts");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = executeLayoutsMigration(root, plan);
|
|
114
|
+
|
|
115
|
+
if (result.failed.length > 0) {
|
|
116
|
+
note(
|
|
117
|
+
`${result.failed.length} file(s) could not be moved:\n` +
|
|
118
|
+
result.failed.map((entry) => `- ${entry.file}: ${entry.reason}`).join("\n"),
|
|
119
|
+
"Migration incomplete",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { from: plan.from, to: plan.to, movedCount: result.moved.length };
|
|
124
|
+
}
|