@specglass/cli 0.0.4 → 0.0.6

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.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Source platform detection for migration.
3
+ *
4
+ * Detects which documentation platform is being used in a project
5
+ * by looking for platform-specific configuration files and content.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from "fs";
8
+ import { join, resolve } from "path";
9
+ /** Supported source platforms for migration. */
10
+ export const SUPPORTED_PLATFORMS = [
11
+ "mintlify",
12
+ "docusaurus",
13
+ "readme",
14
+ "gitbook",
15
+ ];
16
+ // ─── Platform-specific detectors ────────────────────────────────────
17
+ function collectContentFiles(dir, extensions = [".md", ".mdx"], maxDepth = 10) {
18
+ const files = [];
19
+ if (!existsSync(dir) || maxDepth <= 0)
20
+ return files;
21
+ try {
22
+ const entries = readdirSync(dir, { withFileTypes: true });
23
+ for (const entry of entries) {
24
+ const fullPath = join(dir, entry.name);
25
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
26
+ files.push(...collectContentFiles(fullPath, extensions, maxDepth - 1));
27
+ }
28
+ else if (entry.isFile() &&
29
+ extensions.some((ext) => entry.name.endsWith(ext))) {
30
+ files.push(fullPath);
31
+ }
32
+ }
33
+ }
34
+ catch {
35
+ // Permission errors, etc. — skip silently
36
+ }
37
+ return files;
38
+ }
39
+ function detectMintlify(projectRoot) {
40
+ const mintJsonPath = join(projectRoot, "mint.json");
41
+ if (!existsSync(mintJsonPath)) {
42
+ return {
43
+ success: false,
44
+ failure: {
45
+ expectedFiles: ["mint.json"],
46
+ hint: "Mintlify projects have a mint.json configuration file in the project root.",
47
+ },
48
+ };
49
+ }
50
+ // Read mint.json to find the content directory
51
+ let docsDir = projectRoot;
52
+ try {
53
+ const mintConfig = JSON.parse(readFileSync(mintJsonPath, "utf-8"));
54
+ // Mintlify's navigation references page paths — content is usually in root or a subdirectory
55
+ if (mintConfig.pages) {
56
+ // pages array or navigation structure — content relative to root
57
+ docsDir = projectRoot;
58
+ }
59
+ }
60
+ catch {
61
+ // Malformed mint.json — still proceed with root as content dir
62
+ }
63
+ const contentFiles = collectContentFiles(docsDir);
64
+ return {
65
+ success: true,
66
+ detected: {
67
+ platform: "mintlify",
68
+ sourceDir: docsDir,
69
+ configPath: mintJsonPath,
70
+ navConfigPath: mintJsonPath, // Navigation is embedded in mint.json
71
+ contentFiles,
72
+ },
73
+ };
74
+ }
75
+ function detectDocusaurus(projectRoot) {
76
+ const configJsPath = join(projectRoot, "docusaurus.config.js");
77
+ const configTsPath = join(projectRoot, "docusaurus.config.ts");
78
+ const configPath = existsSync(configTsPath)
79
+ ? configTsPath
80
+ : existsSync(configJsPath)
81
+ ? configJsPath
82
+ : null;
83
+ if (!configPath) {
84
+ return {
85
+ success: false,
86
+ failure: {
87
+ expectedFiles: ["docusaurus.config.js", "docusaurus.config.ts"],
88
+ hint: "Docusaurus projects have a docusaurus.config.js or docusaurus.config.ts in the project root.",
89
+ },
90
+ };
91
+ }
92
+ const docsDir = join(projectRoot, "docs");
93
+ const sidebarsJs = join(projectRoot, "sidebars.js");
94
+ const sidebarsTs = join(projectRoot, "sidebars.ts");
95
+ const navConfigPath = existsSync(sidebarsTs)
96
+ ? sidebarsTs
97
+ : existsSync(sidebarsJs)
98
+ ? sidebarsJs
99
+ : null;
100
+ const contentFiles = collectContentFiles(docsDir);
101
+ return {
102
+ success: true,
103
+ detected: {
104
+ platform: "docusaurus",
105
+ sourceDir: docsDir,
106
+ configPath,
107
+ navConfigPath,
108
+ contentFiles,
109
+ },
110
+ };
111
+ }
112
+ function detectReadme(projectRoot) {
113
+ const readmeYmlPath = join(projectRoot, ".readme.yml");
114
+ const readmeYamlPath = join(projectRoot, ".readme.yaml");
115
+ const orderYamlPath = join(projectRoot, "_order.yaml");
116
+ // ReadMe projects can be identified by:
117
+ // 1. .readme.yml / .readme.yaml (ReadMe config file)
118
+ // 2. _order.yaml (rdme-cli docs-as-code sync format)
119
+ const configPath = existsSync(readmeYmlPath)
120
+ ? readmeYmlPath
121
+ : existsSync(readmeYamlPath)
122
+ ? readmeYamlPath
123
+ : existsSync(orderYamlPath)
124
+ ? orderYamlPath
125
+ : null;
126
+ if (!configPath) {
127
+ return {
128
+ success: false,
129
+ failure: {
130
+ expectedFiles: [".readme.yml", ".readme.yaml", "_order.yaml"],
131
+ hint: "ReadMe projects have a .readme.yml/.readme.yaml config file or an _order.yaml navigation file in the project root.",
132
+ },
133
+ };
134
+ }
135
+ // ReadMe content is typically in a docs/ directory or root
136
+ const docsDir = existsSync(join(projectRoot, "docs"))
137
+ ? join(projectRoot, "docs")
138
+ : projectRoot;
139
+ const contentFiles = collectContentFiles(docsDir);
140
+ // _order.yaml serves as both config and nav when it's the identifier
141
+ const navConfigPath = existsSync(orderYamlPath) ? orderYamlPath : null;
142
+ return {
143
+ success: true,
144
+ detected: {
145
+ platform: "readme",
146
+ sourceDir: docsDir,
147
+ configPath,
148
+ navConfigPath,
149
+ contentFiles,
150
+ },
151
+ };
152
+ }
153
+ function detectGitbook(projectRoot) {
154
+ const summaryPath = join(projectRoot, "SUMMARY.md");
155
+ const gitbookYaml = join(projectRoot, ".gitbook.yaml");
156
+ // GitBook requires SUMMARY.md (defines navigation), .gitbook.yaml is optional
157
+ if (!existsSync(summaryPath)) {
158
+ return {
159
+ success: false,
160
+ failure: {
161
+ expectedFiles: ["SUMMARY.md", ".gitbook.yaml"],
162
+ hint: "GitBook projects have a SUMMARY.md navigation file (required) and optionally a .gitbook.yaml config file.",
163
+ },
164
+ };
165
+ }
166
+ const configPath = existsSync(gitbookYaml) ? gitbookYaml : null;
167
+ const contentFiles = collectContentFiles(projectRoot);
168
+ return {
169
+ success: true,
170
+ detected: {
171
+ platform: "gitbook",
172
+ sourceDir: projectRoot,
173
+ configPath,
174
+ navConfigPath: summaryPath,
175
+ contentFiles,
176
+ },
177
+ };
178
+ }
179
+ // ─── Public API ─────────────────────────────────────────────────────
180
+ const DETECTORS = {
181
+ mintlify: detectMintlify,
182
+ docusaurus: detectDocusaurus,
183
+ readme: detectReadme,
184
+ gitbook: detectGitbook,
185
+ };
186
+ /**
187
+ * Detect a source platform's content in the given project root.
188
+ *
189
+ * @param platform - Which platform to detect
190
+ * @param projectRoot - Absolute path to the project root (defaults to cwd)
191
+ * @returns Detection result — success with detected platform info, or failure with hints
192
+ */
193
+ export function detectPlatform(platform, projectRoot = process.cwd()) {
194
+ const root = resolve(projectRoot);
195
+ return DETECTORS[platform](root);
196
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Migration planner — builds a plan for migrating content from a source platform
3
+ * to the specglass/ndocs format without executing the actual conversion.
4
+ *
5
+ * Used by both dry-run mode and the actual migration to preview planned changes.
6
+ */
7
+ import type { DetectedPlatform, SupportedPlatform } from "./detector.js";
8
+ /** A single file that will be migrated. */
9
+ export interface PlannedFile {
10
+ /** Absolute path to the source file. */
11
+ sourcePath: string;
12
+ /** Relative target path within src/content/docs/ */
13
+ targetRelativePath: string;
14
+ /** Whether this file needs AI conversion (true) or is a simple copy (false). */
15
+ needsConversion: boolean;
16
+ }
17
+ /** A component mapping from source to ndocs equivalent. */
18
+ export interface ComponentMapping {
19
+ /** Source platform component (e.g., "<Tip>") */
20
+ source: string;
21
+ /** ndocs equivalent component (e.g., "<Callout type=\"tip\">") */
22
+ target: string;
23
+ }
24
+ /** A navigation mapping from source config to ndocs _meta.json entries. */
25
+ export interface NavigationMapping {
26
+ /** Source navigation label or path */
27
+ sourceLabel: string;
28
+ /** Target directory in ndocs content structure */
29
+ targetDir: string;
30
+ }
31
+ /** Complete migration plan. */
32
+ export interface MigrationPlan {
33
+ /** Source platform. */
34
+ platform: SupportedPlatform;
35
+ /** Files to migrate. */
36
+ files: PlannedFile[];
37
+ /** Component conversions that will be applied. */
38
+ componentMappings: ComponentMapping[];
39
+ /** Navigation structure mapping. */
40
+ navigationMappings: NavigationMapping[];
41
+ /** Target content directory (absolute path). */
42
+ targetContentDir: string;
43
+ /** Number of files that need AI conversion. */
44
+ aiConversionCount: number;
45
+ /** Number of files that are simple copies (images, assets). */
46
+ simpleCopyCount: number;
47
+ }
48
+ /**
49
+ * Build a complete migration plan from a detected platform.
50
+ *
51
+ * @param detected - The detected platform information
52
+ * @param targetContentDir - Absolute path to the target content directory (default: src/content/docs)
53
+ * @returns Complete migration plan
54
+ */
55
+ export declare function buildMigrationPlan(detected: DetectedPlatform, targetContentDir: string): MigrationPlan;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Migration planner — builds a plan for migrating content from a source platform
3
+ * to the specglass/ndocs format without executing the actual conversion.
4
+ *
5
+ * Used by both dry-run mode and the actual migration to preview planned changes.
6
+ */
7
+ import { dirname, extname, join, relative } from "path";
8
+ // ─── Component mappings per platform ────────────────────────────────
9
+ const PLATFORM_COMPONENT_MAPPINGS = {
10
+ mintlify: [
11
+ { source: "<Tip>", target: '<Callout type="tip">' },
12
+ { source: "<Warning>", target: '<Callout type="warning">' },
13
+ { source: "<Note>", target: '<Callout>' },
14
+ { source: "<Info>", target: '<Callout>' },
15
+ { source: "<Check>", target: '<Callout type="tip">' },
16
+ { source: "<AccordionGroup>", target: "<Tabs>" },
17
+ { source: "<Accordion>", target: "<TabItem>" },
18
+ { source: "<CodeGroup>", target: "<Tabs>" },
19
+ { source: "<Card>", target: "<Card>" },
20
+ { source: "<CardGroup>", target: "div (grid container)" },
21
+ ],
22
+ docusaurus: [
23
+ { source: ":::note", target: "<Callout>" },
24
+ { source: ":::tip", target: '<Callout type="tip">' },
25
+ { source: ":::info", target: "<Callout>" },
26
+ { source: ":::caution", target: '<Callout type="warning">' },
27
+ { source: ":::danger", target: '<Callout type="danger">' },
28
+ { source: ":::warning", target: '<Callout type="warning">' },
29
+ { source: "<Tabs>", target: "<Tabs>" },
30
+ { source: "<TabItem>", target: "<TabItem>" },
31
+ {
32
+ source: "import CodeBlock",
33
+ target: "<CodeBlock> (auto-injected, no import needed)",
34
+ },
35
+ ],
36
+ readme: [
37
+ { source: "> 📘", target: "<Callout>" },
38
+ { source: "> ⚠️", target: '<Callout type="warning">' },
39
+ { source: "> 🚧", target: '<Callout type="danger">' },
40
+ { source: "> 👍", target: '<Callout type="tip">' },
41
+ { source: "[block:code]", target: "<CodeBlock>" },
42
+ { source: "[block:callout]", target: "<Callout>" },
43
+ { source: "[block:parameters]", target: "markdown table" },
44
+ ],
45
+ gitbook: [
46
+ { source: "{% hint style=\"info\" %}", target: "<Callout>" },
47
+ { source: "{% hint style=\"warning\" %}", target: "<Callout type=\"warning\">" },
48
+ { source: "{% hint style=\"danger\" %}", target: "<Callout type=\"danger\">" },
49
+ { source: "{% hint style=\"success\" %}", target: "<Callout type=\"tip\">" },
50
+ { source: "{% tabs %}", target: "<Tabs>" },
51
+ { source: "{% tab title=... %}", target: "<TabItem>" },
52
+ { source: "{% code ... %}", target: "<CodeBlock>" },
53
+ { source: "{% embed ... %}", target: "iframe or link" },
54
+ ],
55
+ };
56
+ // ─── Planning logic ─────────────────────────────────────────────────
57
+ /**
58
+ * Determine the target relative path for a source file.
59
+ *
60
+ * Maps the source file path relative to the source content directory
61
+ * into the ndocs content structure, normalizing extensions to .mdx.
62
+ */
63
+ function mapTargetPath(sourcePath, sourceDir) {
64
+ const rel = relative(sourceDir, sourcePath);
65
+ const ext = extname(rel);
66
+ // Convert .md to .mdx, keep .mdx as-is
67
+ if (ext === ".md") {
68
+ return rel.slice(0, -3) + ".mdx";
69
+ }
70
+ return rel;
71
+ }
72
+ /**
73
+ * Generate navigation mappings from directory structure.
74
+ *
75
+ * Each directory in the source content corresponds to a potential
76
+ * _meta.json in the ndocs target — we map these as navigation entries.
77
+ */
78
+ function buildNavigationMappings(detected) {
79
+ const dirs = new Set();
80
+ for (const file of detected.contentFiles) {
81
+ const rel = relative(detected.sourceDir, file);
82
+ const dir = dirname(rel);
83
+ if (dir !== ".") {
84
+ dirs.add(dir);
85
+ }
86
+ }
87
+ // Root always gets a mapping
88
+ const mappings = [
89
+ { sourceLabel: "(root)", targetDir: "src/content/docs" },
90
+ ];
91
+ for (const dir of Array.from(dirs).sort()) {
92
+ mappings.push({
93
+ sourceLabel: dir,
94
+ targetDir: join("src/content/docs", dir),
95
+ });
96
+ }
97
+ return mappings;
98
+ }
99
+ /**
100
+ * Build a complete migration plan from a detected platform.
101
+ *
102
+ * @param detected - The detected platform information
103
+ * @param targetContentDir - Absolute path to the target content directory (default: src/content/docs)
104
+ * @returns Complete migration plan
105
+ */
106
+ export function buildMigrationPlan(detected, targetContentDir) {
107
+ const markdownExts = [".md", ".mdx"];
108
+ const files = detected.contentFiles.map((sourcePath) => {
109
+ const ext = extname(sourcePath);
110
+ const needsConversion = markdownExts.includes(ext);
111
+ const targetRelativePath = mapTargetPath(sourcePath, detected.sourceDir);
112
+ return {
113
+ sourcePath,
114
+ targetRelativePath,
115
+ needsConversion,
116
+ };
117
+ });
118
+ const aiConversionCount = files.filter((f) => f.needsConversion).length;
119
+ const simpleCopyCount = files.filter((f) => !f.needsConversion).length;
120
+ return {
121
+ platform: detected.platform,
122
+ files,
123
+ componentMappings: PLATFORM_COMPONENT_MAPPINGS[detected.platform],
124
+ navigationMappings: buildNavigationMappings(detected),
125
+ targetContentDir,
126
+ aiConversionCount,
127
+ simpleCopyCount,
128
+ };
129
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Migration writer — handles writing converted content, copying assets,
3
+ * and generating navigation config to the target ndocs content directory.
4
+ */
5
+ import type { NavigationMapping } from "./planner.js";
6
+ import type { ConversionResult } from "./converter.js";
7
+ import type { PlannedFile } from "./planner.js";
8
+ /** Result of writing files to the target directory. */
9
+ export interface WriteResult {
10
+ /** Total files written successfully. */
11
+ filesWritten: number;
12
+ /** Total files that failed to write. */
13
+ filesFailed: number;
14
+ /** Total files copied (non-conversion assets). */
15
+ filesCopied: number;
16
+ /** Paths of successfully written files. */
17
+ writtenPaths: string[];
18
+ /** Errors from failed writes. */
19
+ errors: Array<{
20
+ path: string;
21
+ error: string;
22
+ }>;
23
+ }
24
+ /**
25
+ * Write a single converted file to the target content directory.
26
+ * Creates parent directories as needed.
27
+ */
28
+ export declare function writeConvertedFile(targetContentDir: string, relativePath: string, content: string): Promise<void>;
29
+ /**
30
+ * Copy a non-conversion file (e.g. image, asset) to the target content directory.
31
+ * Creates parent directories as needed.
32
+ */
33
+ export declare function copyAssetFile(targetContentDir: string, file: PlannedFile): Promise<void>;
34
+ /**
35
+ * Generate and write `_meta.json` files based on navigation mappings.
36
+ * Creates one `_meta.json` per directory level that has navigation entries.
37
+ */
38
+ export declare function writeMetaJson(targetContentDir: string, navigationMappings: NavigationMapping[]): Promise<string[]>;
39
+ /**
40
+ * Write all successfully converted files, copy assets, and generate navigation config.
41
+ */
42
+ export declare function writeAll(targetContentDir: string, results: ConversionResult[], navigationMappings: NavigationMapping[], allFiles?: PlannedFile[]): Promise<WriteResult>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Migration writer — handles writing converted content, copying assets,
3
+ * and generating navigation config to the target ndocs content directory.
4
+ */
5
+ import { mkdir, writeFile, copyFile } from "fs/promises";
6
+ import { dirname, join, relative } from "path";
7
+ /**
8
+ * Write a single converted file to the target content directory.
9
+ * Creates parent directories as needed.
10
+ */
11
+ export async function writeConvertedFile(targetContentDir, relativePath, content) {
12
+ const fullPath = join(targetContentDir, relativePath);
13
+ const dir = dirname(fullPath);
14
+ // Create parent directories recursively
15
+ await mkdir(dir, { recursive: true });
16
+ // Write the converted content
17
+ await writeFile(fullPath, content, "utf-8");
18
+ }
19
+ /**
20
+ * Copy a non-conversion file (e.g. image, asset) to the target content directory.
21
+ * Creates parent directories as needed.
22
+ */
23
+ export async function copyAssetFile(targetContentDir, file) {
24
+ const fullPath = join(targetContentDir, file.targetRelativePath);
25
+ const dir = dirname(fullPath);
26
+ await mkdir(dir, { recursive: true });
27
+ await copyFile(file.sourcePath, fullPath);
28
+ }
29
+ /**
30
+ * Extract the relative path within the content directory from a NavigationMapping targetDir.
31
+ * Handles both absolute and src/content/docs/-relative paths dynamically.
32
+ */
33
+ function resolveNavRelativePath(targetContentDir, targetDir) {
34
+ // If targetDir is absolute, compute relative to targetContentDir
35
+ if (targetDir.startsWith("/")) {
36
+ return relative(targetContentDir, targetDir);
37
+ }
38
+ // If it starts with the known prefix pattern, strip it
39
+ // Compute the expected prefix from targetContentDir (last 3 segments typically)
40
+ const contentDirSuffix = targetContentDir
41
+ .replace(/\/$/, "")
42
+ .split("/")
43
+ .slice(-3)
44
+ .join("/");
45
+ const regex = new RegExp(`^${contentDirSuffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/?`);
46
+ if (regex.test(targetDir)) {
47
+ return targetDir.replace(regex, "");
48
+ }
49
+ // Fallback: strip common src/content/docs prefix
50
+ return targetDir.replace(/^src\/content\/docs\/?/, "");
51
+ }
52
+ /**
53
+ * Generate and write `_meta.json` files based on navigation mappings.
54
+ * Creates one `_meta.json` per directory level that has navigation entries.
55
+ */
56
+ export async function writeMetaJson(targetContentDir, navigationMappings) {
57
+ const writtenPaths = [];
58
+ // Group entries by their parent directory
59
+ const dirEntries = new Map();
60
+ for (const mapping of navigationMappings) {
61
+ const relativeDir = resolveNavRelativePath(targetContentDir, mapping.targetDir);
62
+ const cleanRel = relativeDir.replace(/\/$/, "");
63
+ if (!cleanRel)
64
+ continue;
65
+ // Determine parent directory and entry name
66
+ const segments = cleanRel.split("/");
67
+ const entryName = segments[segments.length - 1];
68
+ const parentDir = segments.length > 1 ? segments.slice(0, -1).join("/") : ".";
69
+ if (!dirEntries.has(parentDir)) {
70
+ dirEntries.set(parentDir, {});
71
+ }
72
+ dirEntries.get(parentDir)[entryName] = mapping.sourceLabel;
73
+ }
74
+ // Write a _meta.json for each parent directory that has entries
75
+ for (const [dirKey, entries] of dirEntries) {
76
+ if (Object.keys(entries).length === 0)
77
+ continue;
78
+ const metaDir = dirKey === "." ? targetContentDir : join(targetContentDir, dirKey);
79
+ const metaPath = join(metaDir, "_meta.json");
80
+ await mkdir(metaDir, { recursive: true });
81
+ await writeFile(metaPath, JSON.stringify(entries, null, 2) + "\n", "utf-8");
82
+ writtenPaths.push(metaPath);
83
+ }
84
+ return writtenPaths;
85
+ }
86
+ /**
87
+ * Write all successfully converted files, copy assets, and generate navigation config.
88
+ */
89
+ export async function writeAll(targetContentDir, results, navigationMappings, allFiles) {
90
+ const writeResult = {
91
+ filesWritten: 0,
92
+ filesFailed: 0,
93
+ filesCopied: 0,
94
+ writtenPaths: [],
95
+ errors: [],
96
+ };
97
+ // Write each successfully converted file
98
+ for (const result of results) {
99
+ if (!result.success) {
100
+ writeResult.filesFailed++;
101
+ writeResult.errors.push({
102
+ path: result.file.targetRelativePath,
103
+ error: result.error || "Conversion failed",
104
+ });
105
+ continue;
106
+ }
107
+ try {
108
+ await writeConvertedFile(targetContentDir, result.file.targetRelativePath, result.content);
109
+ writeResult.filesWritten++;
110
+ writeResult.writtenPaths.push(result.file.targetRelativePath);
111
+ }
112
+ catch (err) {
113
+ writeResult.filesFailed++;
114
+ writeResult.errors.push({
115
+ path: result.file.targetRelativePath,
116
+ error: err instanceof Error ? err.message : String(err),
117
+ });
118
+ }
119
+ }
120
+ // Copy non-conversion files (images, assets)
121
+ if (allFiles) {
122
+ const filesToCopy = allFiles.filter((f) => !f.needsConversion);
123
+ for (const file of filesToCopy) {
124
+ try {
125
+ await copyAssetFile(targetContentDir, file);
126
+ writeResult.filesCopied++;
127
+ writeResult.writtenPaths.push(file.targetRelativePath);
128
+ }
129
+ catch (err) {
130
+ writeResult.filesFailed++;
131
+ writeResult.errors.push({
132
+ path: file.targetRelativePath,
133
+ error: err instanceof Error ? err.message : String(err),
134
+ });
135
+ }
136
+ }
137
+ }
138
+ // Generate _meta.json navigation files
139
+ try {
140
+ const metaPaths = await writeMetaJson(targetContentDir, navigationMappings);
141
+ writeResult.writtenPaths.push(...metaPaths.map((p) => relative(targetContentDir, p)));
142
+ }
143
+ catch (err) {
144
+ writeResult.errors.push({
145
+ path: "_meta.json",
146
+ error: err instanceof Error ? err.message : String(err),
147
+ });
148
+ }
149
+ return writeResult;
150
+ }
@@ -0,0 +1,6 @@
1
+ import type { ValidationResult } from "@specglass/core";
2
+ interface CheckResultsDisplayProps {
3
+ results: ValidationResult[];
4
+ }
5
+ export declare function CheckResultsDisplay({ results }: CheckResultsDisplayProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,50 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ const CATEGORIES = [
4
+ { label: "Links", icon: "🔗", prefix: "LINK_" },
5
+ { label: "Frontmatter", icon: "📝", prefix: "FRONTMATTER_" },
6
+ { label: "Spec Drift", icon: "📊", prefix: "SPEC_DRIFT_" },
7
+ ];
8
+ /**
9
+ * Group validation results by category (code prefix).
10
+ * Results that don't match any known category are placed in "Other".
11
+ */
12
+ function groupByCategory(results) {
13
+ const grouped = new Map();
14
+ for (const result of results) {
15
+ const category = CATEGORIES.find((c) => result.code.startsWith(c.prefix));
16
+ const key = category?.prefix ?? "OTHER_";
17
+ const def = category ?? { label: "Other", icon: "❓", prefix: "OTHER_" };
18
+ if (!grouped.has(key)) {
19
+ grouped.set(key, { def, items: [] });
20
+ }
21
+ grouped.get(key).items.push(result);
22
+ }
23
+ return grouped;
24
+ }
25
+ function ResultItem({ result }) {
26
+ const icon = result.severity === "error" ? "❌" : "⚠️";
27
+ const color = result.severity === "error" ? "red" : "yellow";
28
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: color, children: [icon, " [", result.code, "]"] }), _jsxs(Text, { children: [" ", result.message] })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [result.filePath, result.line ? `:${result.line}` : ""] }) }), result.hint && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "cyan", children: ["Hint: ", result.hint] }) }))] }));
29
+ }
30
+ function CategorySection({ def, items }) {
31
+ const errorCount = items.filter((i) => i.severity === "error").length;
32
+ const warningCount = items.filter((i) => i.severity === "warning").length;
33
+ const parts = [];
34
+ if (errorCount > 0)
35
+ parts.push(`${errorCount} error${errorCount !== 1 ? "s" : ""}`);
36
+ if (warningCount > 0)
37
+ parts.push(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
38
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [def.icon, " ", def.label] }), _jsxs(Text, { dimColor: true, children: [" (", parts.join(", "), ")"] })] }), items.map((item, i) => (_jsx(ResultItem, { result: item }, `${item.code}-${item.filePath}-${i}`)))] }));
39
+ }
40
+ export function CheckResultsDisplay({ results }) {
41
+ if (results.length === 0) {
42
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2705 All checks passed" }) }));
43
+ }
44
+ const grouped = groupByCategory(results);
45
+ const totalErrors = results.filter((r) => r.severity === "error").length;
46
+ const totalWarnings = results.filter((r) => r.severity === "warning").length;
47
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Specglass Check Results" }), Array.from(grouped.values()).map(({ def, items }) => (_jsx(CategorySection, { def: def, items: items }, def.prefix))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: totalErrors > 0 ? "red" : "yellow", children: totalErrors > 0
48
+ ? `✗ ${totalErrors} error${totalErrors !== 1 ? "s" : ""}${totalWarnings > 0 ? `, ${totalWarnings} warning${totalWarnings !== 1 ? "s" : ""}` : ""}`
49
+ : `⚠ ${totalWarnings} warning${totalWarnings !== 1 ? "s" : ""} (no errors)` }) })] }));
50
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Consent prompt component for the migration command.
3
+ *
4
+ * Warns the user that content will be sent to an external AI API
5
+ * and requires explicit consent before proceeding.
6
+ */
7
+ import type { MigrationPlan } from "../migration/planner.js";
8
+ export interface ConsentPromptProps {
9
+ /** The migration plan to display. */
10
+ plan: MigrationPlan;
11
+ /** Whether consent was already given via --yes flag. */
12
+ autoConsent: boolean;
13
+ /** Called when user makes a consent decision. */
14
+ onDecision: (consented: boolean) => void;
15
+ }
16
+ /**
17
+ * Displays the AI consent warning and waits for user input.
18
+ *
19
+ * Shows:
20
+ * - Warning about external API usage
21
+ * - Summary of what will be sent (file count)
22
+ * - Y/N prompt
23
+ */
24
+ export declare function ConsentPrompt({ plan, autoConsent, onDecision, }: ConsentPromptProps): import("react/jsx-runtime").JSX.Element;