@specglass/cli 0.0.5 → 0.0.7

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/dist/cli.js CHANGED
@@ -5,6 +5,8 @@ import { render } from "ink";
5
5
  import { SpecglassError } from "@specglass/core";
6
6
  import { DevCommand } from "./commands/dev.js";
7
7
  import { BuildCommand } from "./commands/build.js";
8
+ import { CheckCommand } from "./commands/check.js";
9
+ import { MigrateCommand, SUPPORTED_PLATFORMS } from "./commands/migrate.js";
8
10
  import { setupErrorHandlers, setupSigintHandler, renderErrorAndExit, EXIT_CODES } from "./utils/error-handler.js";
9
11
  setupErrorHandlers();
10
12
  setupSigintHandler();
@@ -13,14 +15,22 @@ const cli = meow(`
13
15
  $ specglass <command>
14
16
 
15
17
  Commands
16
- dev Start the development server
17
- build Build static site for production
18
+ dev Start the development server
19
+ build Build static site for production
20
+ check Run validation checks (links, frontmatter, spec drift)
21
+ migrate Migrate docs from another platform (Mintlify, Docusaurus, ReadMe, GitBook)
18
22
 
19
23
  Options
20
- --port, -p Port for the dev server (default: 4321)
21
- --config, -c Path to specglass.config.ts
22
- --help Show this help message
23
- --version Show version number
24
+ --port, -p Port for the dev server (default: 4321)
25
+ --config, -c Path to specglass.config.ts
26
+ --links-only Only run link validation
27
+ --frontmatter-only Only run frontmatter validation
28
+ --spec-drift-only Only run spec drift validation
29
+ --from Source platform to migrate from (mintlify, docusaurus, readme, gitbook)
30
+ --dry-run Preview migration plan without calling AI API
31
+ --yes, -y Skip consent prompt (for CI automation)
32
+ --help Show this help message
33
+ --version Show version number
24
34
 
25
35
  Examples
26
36
  $ specglass dev
@@ -28,6 +38,10 @@ const cli = meow(`
28
38
  $ specglass dev --config ./custom.config.ts
29
39
  $ specglass build
30
40
  $ specglass build --config ./custom.config.ts
41
+ $ specglass check
42
+ $ specglass check --links-only
43
+ $ specglass migrate --from mintlify
44
+ $ specglass migrate --from mintlify --dry-run
31
45
  `, {
32
46
  importMeta: import.meta,
33
47
  flags: {
@@ -40,6 +54,30 @@ const cli = meow(`
40
54
  type: "string",
41
55
  shortFlag: "c",
42
56
  },
57
+ linksOnly: {
58
+ type: "boolean",
59
+ default: false,
60
+ },
61
+ frontmatterOnly: {
62
+ type: "boolean",
63
+ default: false,
64
+ },
65
+ specDriftOnly: {
66
+ type: "boolean",
67
+ default: false,
68
+ },
69
+ from: {
70
+ type: "string",
71
+ },
72
+ dryRun: {
73
+ type: "boolean",
74
+ default: false,
75
+ },
76
+ yes: {
77
+ type: "boolean",
78
+ shortFlag: "y",
79
+ default: false,
80
+ },
43
81
  },
44
82
  });
45
83
  const [command] = cli.input;
@@ -52,6 +90,21 @@ else if (command === "dev") {
52
90
  else if (command === "build") {
53
91
  render(_jsx(BuildCommand, { configPath: cli.flags.config }));
54
92
  }
93
+ else if (command === "check") {
94
+ render(_jsx(CheckCommand, { configPath: cli.flags.config, linksOnly: cli.flags.linksOnly, frontmatterOnly: cli.flags.frontmatterOnly, specDriftOnly: cli.flags.specDriftOnly }));
95
+ }
96
+ else if (command === "migrate") {
97
+ const from = cli.flags.from;
98
+ if (!from) {
99
+ renderErrorAndExit(new SpecglassError("Missing required --from flag", "MISSING_FLAG", undefined, undefined, `Specify the source platform: --from <platform>\nSupported platforms: ${SUPPORTED_PLATFORMS.join(", ")}`), EXIT_CODES.ERROR);
100
+ }
101
+ else if (!SUPPORTED_PLATFORMS.includes(from)) {
102
+ renderErrorAndExit(new SpecglassError(`Unsupported platform: ${from}`, "UNSUPPORTED_PLATFORM", undefined, undefined, `Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}`), EXIT_CODES.ERROR);
103
+ }
104
+ else {
105
+ render(_jsx(MigrateCommand, { from: from, dryRun: cli.flags.dryRun, yes: cli.flags.yes, configPath: cli.flags.config }));
106
+ }
107
+ }
55
108
  else {
56
- renderErrorAndExit(new SpecglassError(`Unknown command: ${command}`, "UNKNOWN_COMMAND", undefined, undefined, "Available commands: dev, build. Run 'specglass --help' for usage."), EXIT_CODES.ERROR);
109
+ renderErrorAndExit(new SpecglassError(`Unknown command: ${command}`, "UNKNOWN_COMMAND", undefined, undefined, "Available commands: dev, build, check, migrate. Run 'specglass --help' for usage."), EXIT_CODES.ERROR);
57
110
  }
@@ -0,0 +1,7 @@
1
+ export interface CheckCommandProps {
2
+ configPath?: string;
3
+ linksOnly?: boolean;
4
+ frontmatterOnly?: boolean;
5
+ specDriftOnly?: boolean;
6
+ }
7
+ export declare function CheckCommand(props: CheckCommandProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box } from "ink";
4
+ import { Spinner } from "@inkjs/ui";
5
+ import { loadConfig, SpecglassError, runValidators, validateLinks, validateFrontmatter, validateSpecDrift, } from "@specglass/core";
6
+ import { buildValidatorContext } from "../utils/context-builder.js";
7
+ import { CheckResultsDisplay } from "../ui/check-results-display.js";
8
+ import { ErrorDisplay } from "../ui/error-display.js";
9
+ import { isConfigError, EXIT_CODES } from "../utils/error-handler.js";
10
+ /**
11
+ * Compose the validator array based on active filter flags.
12
+ * If no filter is active, all validators run.
13
+ */
14
+ function composeValidators(props) {
15
+ const { linksOnly, frontmatterOnly, specDriftOnly } = props;
16
+ const hasFilter = linksOnly || frontmatterOnly || specDriftOnly;
17
+ if (!hasFilter) {
18
+ return [validateLinks, validateFrontmatter, validateSpecDrift];
19
+ }
20
+ const validators = [];
21
+ if (linksOnly)
22
+ validators.push(validateLinks);
23
+ if (frontmatterOnly)
24
+ validators.push(validateFrontmatter);
25
+ if (specDriftOnly)
26
+ validators.push(validateSpecDrift);
27
+ return validators;
28
+ }
29
+ export function CheckCommand(props) {
30
+ const { configPath, linksOnly, frontmatterOnly, specDriftOnly } = props;
31
+ const [status, setStatus] = useState("loading-config");
32
+ const [error, setError] = useState(null);
33
+ const [results, setResults] = useState([]);
34
+ useEffect(() => {
35
+ let aborted = false;
36
+ async function run() {
37
+ try {
38
+ // Phase 1: Load & validate config
39
+ const config = await loadConfig(configPath);
40
+ if (aborted)
41
+ return;
42
+ // Phase 2: Build validator context
43
+ setStatus("checking");
44
+ const context = await buildValidatorContext(config, process.cwd());
45
+ if (aborted)
46
+ return;
47
+ // Phase 3: Run validators
48
+ const validators = composeValidators(props);
49
+ const findings = await runValidators(validators, context);
50
+ if (aborted)
51
+ return;
52
+ // Phase 4: Report results
53
+ setResults(findings);
54
+ setStatus("done");
55
+ // Exit based on severity: errors = exit 1, warnings only = exit 0
56
+ const hasErrors = findings.some((f) => f.severity === "error");
57
+ setTimeout(() => {
58
+ process.exit(hasErrors ? EXIT_CODES.ERROR : EXIT_CODES.SUCCESS);
59
+ }, 100);
60
+ }
61
+ catch (err) {
62
+ if (aborted)
63
+ return;
64
+ const specglassErr = err instanceof SpecglassError
65
+ ? err
66
+ : new SpecglassError(err instanceof Error ? err.message : String(err), "CHECK_UNEXPECTED_ERROR");
67
+ setError(specglassErr);
68
+ setStatus("error");
69
+ const exitCode = isConfigError(specglassErr)
70
+ ? EXIT_CODES.CONFIG_ERROR
71
+ : EXIT_CODES.ERROR;
72
+ setTimeout(() => process.exit(exitCode), 100);
73
+ }
74
+ }
75
+ run();
76
+ return () => {
77
+ aborted = true;
78
+ };
79
+ }, [configPath, linksOnly, frontmatterOnly, specDriftOnly]);
80
+ if (status === "error" && error) {
81
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(ErrorDisplay, { error: error }) }));
82
+ }
83
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [status === "loading-config" && (_jsx(Box, { children: _jsx(Spinner, { label: "Loading specglass config..." }) })), status === "checking" && (_jsx(Box, { children: _jsx(Spinner, { label: "Running validation checks..." }) })), status === "done" && _jsx(CheckResultsDisplay, { results: results })] }));
84
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Migration command — `specglass migrate --from <platform>`
3
+ *
4
+ * Migrates documentation from another platform to specglass/ndocs format.
5
+ * Includes platform detection, consent flow, AI-powered content conversion,
6
+ * and file writing.
7
+ */
8
+ import { SUPPORTED_PLATFORMS, type SupportedPlatform } from "../migration/detector.js";
9
+ export { SUPPORTED_PLATFORMS };
10
+ export interface MigrateCommandProps {
11
+ from: SupportedPlatform;
12
+ dryRun?: boolean;
13
+ yes?: boolean;
14
+ configPath?: string;
15
+ }
16
+ export declare function MigrateCommand(props: MigrateCommandProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Migration command — `specglass migrate --from <platform>`
4
+ *
5
+ * Migrates documentation from another platform to specglass/ndocs format.
6
+ * Includes platform detection, consent flow, AI-powered content conversion,
7
+ * and file writing.
8
+ */
9
+ import { useState, useEffect, useCallback, useRef } from "react";
10
+ import { Box, Text } from "ink";
11
+ import { Spinner } from "@inkjs/ui";
12
+ import { SpecglassError } from "@specglass/core";
13
+ import { detectPlatform, SUPPORTED_PLATFORMS, } from "../migration/detector.js";
14
+ import { buildMigrationPlan } from "../migration/planner.js";
15
+ import { getApiKey, convertAll, } from "../migration/converter.js";
16
+ import { writeAll } from "../migration/writer.js";
17
+ import { ConsentPrompt } from "../ui/consent-prompt.js";
18
+ import { ErrorDisplay } from "../ui/error-display.js";
19
+ import { EXIT_CODES } from "../utils/error-handler.js";
20
+ import { resolve, join } from "path";
21
+ export { SUPPORTED_PLATFORMS };
22
+ export function MigrateCommand(props) {
23
+ const { from, dryRun = false, yes = false } = props;
24
+ const [status, setStatus] = useState("detecting");
25
+ const [error, setError] = useState(null);
26
+ const [plan, setPlan] = useState(null);
27
+ const [progressText, setProgressText] = useState("");
28
+ const [writeResult, setWriteResult] = useState(null);
29
+ const conversionResults = useRef([]);
30
+ // Handle consent decision
31
+ const onConsentDecision = useCallback((consented) => {
32
+ if (consented) {
33
+ setStatus("converting");
34
+ }
35
+ else {
36
+ setStatus("declined");
37
+ setTimeout(() => process.exit(EXIT_CODES.SUCCESS), 100);
38
+ }
39
+ }, []);
40
+ useEffect(() => {
41
+ let aborted = false;
42
+ async function run() {
43
+ try {
44
+ // Phase 1: Detect source platform
45
+ const projectRoot = resolve(process.cwd());
46
+ const result = detectPlatform(from, projectRoot);
47
+ if (aborted)
48
+ return;
49
+ if (!result.success) {
50
+ throw new SpecglassError(`No ${from} project detected in the current directory`, "PLATFORM_NOT_DETECTED", projectRoot, undefined, `Expected files: ${result.failure.expectedFiles.join(", ")}\n${result.failure.hint}`);
51
+ }
52
+ // Phase 2: Build migration plan
53
+ const targetContentDir = join(projectRoot, "src", "content", "docs");
54
+ const migrationPlan = buildMigrationPlan(result.detected, targetContentDir);
55
+ if (aborted)
56
+ return;
57
+ setPlan(migrationPlan);
58
+ if (dryRun) {
59
+ // Dry-run: show plan and exit
60
+ setStatus("done");
61
+ setTimeout(() => process.exit(EXIT_CODES.SUCCESS), 100);
62
+ }
63
+ else {
64
+ // Live migration: show consent prompt
65
+ setStatus("consent");
66
+ }
67
+ }
68
+ catch (err) {
69
+ if (aborted)
70
+ return;
71
+ const specglassErr = err instanceof SpecglassError
72
+ ? err
73
+ : new SpecglassError(err instanceof Error ? err.message : String(err), "MIGRATE_UNEXPECTED_ERROR");
74
+ setError(specglassErr);
75
+ setStatus("error");
76
+ setTimeout(() => process.exit(EXIT_CODES.ERROR), 100);
77
+ }
78
+ }
79
+ run();
80
+ return () => {
81
+ aborted = true;
82
+ };
83
+ }, [from, dryRun]);
84
+ // AI conversion phase — Story 7.2
85
+ useEffect(() => {
86
+ if (status !== "converting")
87
+ return;
88
+ if (!plan)
89
+ return;
90
+ let aborted = false;
91
+ async function runConversion() {
92
+ try {
93
+ const apiKey = getApiKey();
94
+ conversionResults.current = [];
95
+ for await (const result of convertAll({
96
+ apiKey,
97
+ plan: plan,
98
+ onProgress: (file, index, total) => {
99
+ if (!aborted) {
100
+ setProgressText(`Converting ${index + 1}/${total}: ${file.targetRelativePath}`);
101
+ }
102
+ },
103
+ })) {
104
+ if (aborted)
105
+ return;
106
+ conversionResults.current.push(result);
107
+ }
108
+ if (aborted)
109
+ return;
110
+ setStatus("writing");
111
+ }
112
+ catch (err) {
113
+ if (aborted)
114
+ return;
115
+ const specglassErr = err instanceof SpecglassError
116
+ ? err
117
+ : new SpecglassError(err instanceof Error ? err.message : String(err), "MIGRATE_CONVERSION_ERROR");
118
+ setError(specglassErr);
119
+ setStatus("error");
120
+ setTimeout(() => process.exit(EXIT_CODES.ERROR), 100);
121
+ }
122
+ }
123
+ runConversion();
124
+ return () => {
125
+ aborted = true;
126
+ };
127
+ }, [status, plan]);
128
+ // File writing phase
129
+ useEffect(() => {
130
+ if (status !== "writing")
131
+ return;
132
+ if (!plan)
133
+ return;
134
+ let aborted = false;
135
+ async function runWrite() {
136
+ try {
137
+ setProgressText("Writing converted files...");
138
+ const result = await writeAll(plan.targetContentDir, conversionResults.current, plan.navigationMappings, plan.files);
139
+ if (aborted)
140
+ return;
141
+ setWriteResult(result);
142
+ setStatus("done");
143
+ setTimeout(() => process.exit(result.filesFailed > 0 ? EXIT_CODES.ERROR : EXIT_CODES.SUCCESS), 100);
144
+ }
145
+ catch (err) {
146
+ if (aborted)
147
+ return;
148
+ const specglassErr = new SpecglassError(err instanceof Error ? err.message : String(err), "MIGRATE_WRITE_ERROR");
149
+ setError(specglassErr);
150
+ setStatus("error");
151
+ setTimeout(() => process.exit(EXIT_CODES.ERROR), 100);
152
+ }
153
+ }
154
+ runWrite();
155
+ return () => {
156
+ aborted = true;
157
+ };
158
+ }, [status, plan]);
159
+ // ─── Render based on status ─────────────────────────────────────
160
+ if (status === "error" && error) {
161
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(ErrorDisplay, { error: error }) }));
162
+ }
163
+ if (status === "declined") {
164
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "yellow", children: "\u270B Migration cancelled \u2014 no data was sent." }) }));
165
+ }
166
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [status === "detecting" && (_jsx(Spinner, { label: `Detecting ${from} project...` })), status === "consent" && plan && (_jsx(ConsentPrompt, { plan: plan, autoConsent: yes, onDecision: onConsentDecision })), status === "converting" && (_jsx(Spinner, { label: progressText || "Starting AI conversion..." })), status === "writing" && (_jsx(Spinner, { label: progressText || "Writing files..." })), status === "done" && plan && (_jsx(MigrationPlanDisplay, { plan: plan, dryRun: dryRun, writeResult: writeResult }))] }));
167
+ }
168
+ function MigrationPlanDisplay({ plan, dryRun, writeResult }) {
169
+ return (_jsxs(Box, { flexDirection: "column", children: [dryRun && (_jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDCCB Migration Plan (dry-run \u2014 no changes will be made)" })), !dryRun && writeResult && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: writeResult.filesFailed > 0 ? "yellow" : "green", children: writeResult.filesFailed > 0
170
+ ? "⚠️ Migration Complete (with errors)"
171
+ : "✅ Migration Complete" }), _jsxs(Text, { children: ["Files converted: ", _jsx(Text, { bold: true, color: "green", children: writeResult.filesWritten }), writeResult.filesCopied > 0 && (_jsxs(Text, { children: [" | Copied: ", _jsx(Text, { bold: true, color: "cyan", children: writeResult.filesCopied })] })), writeResult.filesFailed > 0 && (_jsxs(Text, { children: [" | Failed: ", _jsx(Text, { bold: true, color: "red", children: writeResult.filesFailed })] }))] }), _jsxs(Text, { children: ["Target: ", _jsx(Text, { dimColor: true, children: plan.targetContentDir })] }), writeResult.errors.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Errors:" }), writeResult.errors.map((e, i) => (_jsxs(Text, { color: "red", children: [" \u2022 ", e.path, ": ", e.error] }, i)))] }))] })), !dryRun && !writeResult && (_jsx(Text, { bold: true, color: "green", children: "\u2705 Migration Complete" })), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: ["Platform: ", _jsx(Text, { color: "yellow", children: plan.platform })] }), _jsxs(Text, { children: ["Total files: ", _jsx(Text, { bold: true, children: plan.files.length })] }), _jsxs(Text, { children: ["AI conversions: ", _jsx(Text, { bold: true, children: plan.aiConversionCount })] }), plan.simpleCopyCount > 0 && (_jsxs(Text, { children: ["Simple copies: ", _jsx(Text, { bold: true, children: plan.simpleCopyCount })] })), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, underline: true, children: "File Mapping:" }), plan.files.slice(0, 20).map((file, i) => (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [" ", file.needsConversion ? "🔄" : "📋", " "] }), _jsx(Text, { dimColor: true, children: file.sourcePath }), _jsx(Text, { dimColor: true, children: " \u2192 " }), _jsx(Text, { children: file.targetRelativePath })] }, i))), plan.files.length > 20 && (_jsxs(Text, { dimColor: true, children: [" ... and ", plan.files.length - 20, " more files"] })), _jsx(Text, { children: " " }), plan.componentMappings.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, underline: true, children: "Component Conversions:" }), plan.componentMappings.map((mapping, i) => (_jsxs(Text, { dimColor: true, children: [" ", mapping.source, " \u2192 ", mapping.target] }, i))), _jsx(Text, { children: " " })] })), plan.navigationMappings.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, underline: true, children: "Navigation Structure:" }), plan.navigationMappings.map((mapping, i) => (_jsxs(Text, { dimColor: true, children: [" ", mapping.sourceLabel, " \u2192 ", mapping.targetDir, "/"] }, i)))] })), dryRun && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Run without --dry-run to execute the migration." })] }))] }));
172
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Migration converter — handles AI-powered content conversion using the Claude API.
3
+ *
4
+ * Sends source documentation files to Claude for semantic conversion to ndocs MDX format.
5
+ * Uses the Anthropic SDK with BYOK (Bring Your Own Key) pattern.
6
+ */
7
+ import Anthropic from "@anthropic-ai/sdk";
8
+ import type { SupportedPlatform } from "./detector.js";
9
+ import type { MigrationPlan, PlannedFile, ComponentMapping } from "./planner.js";
10
+ /** Result of converting a single file. */
11
+ export interface ConversionResult {
12
+ /** The planned file entry. */
13
+ file: PlannedFile;
14
+ /** Converted MDX content (empty string on failure). */
15
+ content: string;
16
+ /** Whether the conversion succeeded. */
17
+ success: boolean;
18
+ /** Error message if conversion failed. */
19
+ error?: string;
20
+ }
21
+ /** Options for the convertAll function. */
22
+ export interface ConversionOptions {
23
+ /** Anthropic API key. */
24
+ apiKey: string;
25
+ /** The migration plan to execute. */
26
+ plan: MigrationPlan;
27
+ /** Progress callback — called for each file before conversion starts. */
28
+ onProgress?: (file: PlannedFile, index: number, total: number) => void;
29
+ }
30
+ /**
31
+ * Build the system prompt for Claude — defines role, rules, and component mappings.
32
+ */
33
+ export declare function buildSystemPrompt(platform: SupportedPlatform, componentMappings: ComponentMapping[]): string;
34
+ /**
35
+ * Build the user message for Claude — contains the source file content.
36
+ */
37
+ export declare function buildUserMessage(sourceContent: string, sourcePath: string, platform: SupportedPlatform): string;
38
+ /**
39
+ * Convert a single source file to ndocs MDX format using Claude.
40
+ */
41
+ export declare function convertFile(client: Anthropic, sourcePath: string, platform: SupportedPlatform, componentMappings: ComponentMapping[]): Promise<string>;
42
+ /**
43
+ * Convert all files in a migration plan using Claude.
44
+ * Yields results as each file is processed (sequential to respect rate limits).
45
+ */
46
+ export declare function convertAll(options: ConversionOptions): AsyncGenerator<ConversionResult>;
47
+ /**
48
+ * Validate that the ANTHROPIC_API_KEY is available.
49
+ * Returns the key or throws a SpecglassError.
50
+ */
51
+ export declare function getApiKey(): string;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Migration converter — handles AI-powered content conversion using the Claude API.
3
+ *
4
+ * Sends source documentation files to Claude for semantic conversion to ndocs MDX format.
5
+ * Uses the Anthropic SDK with BYOK (Bring Your Own Key) pattern.
6
+ */
7
+ import Anthropic from "@anthropic-ai/sdk";
8
+ import { readFile } from "fs/promises";
9
+ import { SpecglassError } from "@specglass/core";
10
+ /** Default Claude model used for conversion. */
11
+ const DEFAULT_MODEL = "claude-sonnet-4-20250514";
12
+ /**
13
+ * Build the system prompt for Claude — defines role, rules, and component mappings.
14
+ */
15
+ export function buildSystemPrompt(platform, componentMappings) {
16
+ const mappingLines = componentMappings
17
+ .map((m) => ` - ${m.source} → ${m.target}`)
18
+ .join("\n");
19
+ return `You are a documentation migration tool. You convert ${platform} documentation files to ndocs MDX format.
20
+
21
+ ## Rules
22
+
23
+ 1. Convert ALL platform-specific syntax to standard MDX
24
+ 2. Apply these component mappings:
25
+ ${mappingLines}
26
+ 3. Generate valid frontmatter with at minimum:
27
+ - title: extracted from the first heading or existing frontmatter
28
+ - description: a brief description of the page content
29
+ 4. Preserve ALL content semantics — do not lose any information
30
+ 5. Convert relative links to work in the new file structure
31
+ 6. Remove any platform-specific imports that are not needed in ndocs
32
+ 7. Output ONLY the converted MDX content — no explanations, no wrapping code fences`;
33
+ }
34
+ /**
35
+ * Build the user message for Claude — contains the source file content.
36
+ */
37
+ export function buildUserMessage(sourceContent, sourcePath, platform) {
38
+ return `Convert this ${platform} file to ndocs MDX format.
39
+
40
+ Source path: ${sourcePath}
41
+
42
+ ${sourceContent}`;
43
+ }
44
+ /**
45
+ * Convert a single source file to ndocs MDX format using Claude.
46
+ */
47
+ export async function convertFile(client, sourcePath, platform, componentMappings) {
48
+ const sourceContent = await readFile(sourcePath, "utf-8");
49
+ const systemPrompt = buildSystemPrompt(platform, componentMappings);
50
+ const userMessage = buildUserMessage(sourceContent, sourcePath, platform);
51
+ const model = process.env.ANTHROPIC_MODEL || DEFAULT_MODEL;
52
+ const message = await client.messages.create({
53
+ model,
54
+ max_tokens: 8192,
55
+ system: systemPrompt,
56
+ messages: [{ role: "user", content: userMessage }],
57
+ });
58
+ // Extract text content from the response
59
+ const textBlock = message.content.find((block) => block.type === "text");
60
+ if (!textBlock || textBlock.type !== "text") {
61
+ throw new Error("No text content in Claude response");
62
+ }
63
+ // Warn if response was truncated due to max_tokens
64
+ if (message.stop_reason === "max_tokens") {
65
+ throw new Error(`Claude response truncated for ${sourcePath} — file may be too large for max_tokens=8192`);
66
+ }
67
+ return textBlock.text;
68
+ }
69
+ /**
70
+ * Convert all files in a migration plan using Claude.
71
+ * Yields results as each file is processed (sequential to respect rate limits).
72
+ */
73
+ export async function* convertAll(options) {
74
+ const { apiKey, plan, onProgress } = options;
75
+ const client = new Anthropic({ apiKey });
76
+ const filesToConvert = plan.files.filter((f) => f.needsConversion);
77
+ for (let i = 0; i < filesToConvert.length; i++) {
78
+ const file = filesToConvert[i];
79
+ if (onProgress) {
80
+ onProgress(file, i, filesToConvert.length);
81
+ }
82
+ try {
83
+ const content = await convertFile(client, file.sourcePath, plan.platform, plan.componentMappings);
84
+ yield {
85
+ file,
86
+ content,
87
+ success: true,
88
+ };
89
+ }
90
+ catch (err) {
91
+ yield {
92
+ file,
93
+ content: "",
94
+ success: false,
95
+ error: err instanceof Error ? err.message : String(err),
96
+ };
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Validate that the ANTHROPIC_API_KEY is available.
102
+ * Returns the key or throws a SpecglassError.
103
+ */
104
+ export function getApiKey() {
105
+ const key = process.env.ANTHROPIC_API_KEY;
106
+ if (!key) {
107
+ throw new SpecglassError("Missing ANTHROPIC_API_KEY environment variable", "MISSING_API_KEY", undefined, undefined, "Set ANTHROPIC_API_KEY environment variable with your Anthropic API key.\nGet one at https://console.anthropic.com/");
108
+ }
109
+ return key;
110
+ }
@@ -0,0 +1,45 @@
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
+ /** Supported source platforms for migration. */
8
+ export declare const SUPPORTED_PLATFORMS: readonly ["mintlify", "docusaurus", "readme", "gitbook"];
9
+ export type SupportedPlatform = (typeof SUPPORTED_PLATFORMS)[number];
10
+ /** Result of platform detection — describes the source project. */
11
+ export interface DetectedPlatform {
12
+ /** Which platform was detected. */
13
+ platform: SupportedPlatform;
14
+ /** Absolute path to the source content directory. */
15
+ sourceDir: string;
16
+ /** Absolute path to the platform config file (if found). */
17
+ configPath: string | null;
18
+ /** Absolute path to the navigation config file (if found). */
19
+ navConfigPath: string | null;
20
+ /** List of content files found (absolute paths). */
21
+ contentFiles: string[];
22
+ }
23
+ /** Detection failure result. */
24
+ export interface DetectionFailure {
25
+ /** What files we looked for and didn't find. */
26
+ expectedFiles: string[];
27
+ /** Hint message for the user. */
28
+ hint: string;
29
+ }
30
+ /** Result type for platform detection. */
31
+ export type DetectionResult = {
32
+ success: true;
33
+ detected: DetectedPlatform;
34
+ } | {
35
+ success: false;
36
+ failure: DetectionFailure;
37
+ };
38
+ /**
39
+ * Detect a source platform's content in the given project root.
40
+ *
41
+ * @param platform - Which platform to detect
42
+ * @param projectRoot - Absolute path to the project root (defaults to cwd)
43
+ * @returns Detection result — success with detected platform info, or failure with hints
44
+ */
45
+ export declare function detectPlatform(platform: SupportedPlatform, projectRoot?: string): DetectionResult;