@specglass/cli 0.0.5 → 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.
- package/dist/cli.js +60 -7
- package/dist/commands/check.d.ts +7 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/migrate.d.ts +16 -0
- package/dist/commands/migrate.js +172 -0
- package/dist/migration/converter.d.ts +51 -0
- package/dist/migration/converter.js +110 -0
- package/dist/migration/detector.d.ts +45 -0
- package/dist/migration/detector.js +196 -0
- package/dist/migration/planner.d.ts +55 -0
- package/dist/migration/planner.js +129 -0
- package/dist/migration/writer.d.ts +42 -0
- package/dist/migration/writer.js +150 -0
- package/dist/ui/check-results-display.d.ts +6 -0
- package/dist/ui/check-results-display.js +50 -0
- package/dist/ui/consent-prompt.d.ts +24 -0
- package/dist/ui/consent-prompt.js +40 -0
- package/dist/utils/context-builder.d.ts +9 -0
- package/dist/utils/context-builder.js +63 -0
- package/package.json +2 -1
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
|
|
17
|
-
build
|
|
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
|
|
21
|
-
--config, -c
|
|
22
|
-
--
|
|
23
|
-
--
|
|
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,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;
|