doccupine 0.0.114 ā 0.0.115
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/index.js +102 -67
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.js +56 -0
- package/dist/templates/components/layout/Callout.d.ts +1 -1
- package/dist/templates/components/layout/Callout.js +1 -0
- package/dist/templates/components/layout/Code.d.ts +1 -1
- package/dist/templates/components/layout/Code.js +1 -0
- package/dist/templates/package.js +5 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import chalk from "chalk";
|
|
|
9
9
|
import { appStructure, obsoleteFiles, startingDocsStructure, } from "./lib/structures.js";
|
|
10
10
|
import { rootLayoutTemplate, siteLayoutTemplate } from "./lib/layout.js";
|
|
11
11
|
import { ConfigManager } from "./lib/config-manager.js";
|
|
12
|
-
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
|
|
12
|
+
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, resolvePackageManager, } from "./lib/utils.js";
|
|
13
13
|
import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, generateJsonLdScript, } from "./lib/metadata.js";
|
|
14
14
|
import { nextConfigTemplate } from "./templates/next.config.js";
|
|
15
15
|
import { pnpmWorkspaceTemplate } from "./templates/pnpmWorkspace.js";
|
|
@@ -60,9 +60,6 @@ class MDXToNextJSGenerator {
|
|
|
60
60
|
await fs.ensureDir(this.outputDir);
|
|
61
61
|
this.sectionsConfig = await this.resolveSections();
|
|
62
62
|
this.analyticsConfig = await this.loadAnalyticsConfig();
|
|
63
|
-
if (this.sectionsConfig) {
|
|
64
|
-
console.log(chalk.blue(`š Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
|
|
65
|
-
}
|
|
66
63
|
if (this.analyticsConfig) {
|
|
67
64
|
console.log(chalk.blue(`š Analytics enabled: ${this.analyticsConfig.provider}`));
|
|
68
65
|
}
|
|
@@ -72,8 +69,16 @@ class MDXToNextJSGenerator {
|
|
|
72
69
|
await this.copyFontConfig();
|
|
73
70
|
await this.copyAnalyticsConfig();
|
|
74
71
|
await this.copyPublicFiles();
|
|
72
|
+
// createStartingDocs() may have written the sample docs - which carry
|
|
73
|
+
// section frontmatter - after the initial resolveSections() ran against an
|
|
74
|
+
// empty watch dir. Re-resolve now that every MDX file is on disk so the
|
|
75
|
+
// build applies the correct sections in a single O(n) pass, instead of
|
|
76
|
+
// rediscovering them per file (the old O(n²) behavior).
|
|
77
|
+
this.sectionsConfig = await this.resolveSections();
|
|
78
|
+
if (this.sectionsConfig) {
|
|
79
|
+
console.log(chalk.blue(`š Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
|
|
80
|
+
}
|
|
75
81
|
await this.processAllMDXFiles();
|
|
76
|
-
await this.generateSectionIndexPages();
|
|
77
82
|
console.log(chalk.green("ā
Initial setup complete!"));
|
|
78
83
|
console.log(chalk.cyan("š” To start the Next.js dev server:"));
|
|
79
84
|
console.log(chalk.white(` cd ${path.relative(process.cwd(), this.outputDir)}`));
|
|
@@ -260,7 +265,6 @@ class MDXToNextJSGenerator {
|
|
|
260
265
|
console.log(chalk.cyan("š Sections configuration changed"));
|
|
261
266
|
this.sectionsConfig = await this.resolveSections();
|
|
262
267
|
await this.processAllMDXFiles();
|
|
263
|
-
await this.generateSectionIndexPages();
|
|
264
268
|
}
|
|
265
269
|
async maybeUpdateSections() {
|
|
266
270
|
if (this.isReprocessing)
|
|
@@ -278,8 +282,9 @@ class MDXToNextJSGenerator {
|
|
|
278
282
|
this.sectionsConfig = newSections;
|
|
279
283
|
this.isReprocessing = true;
|
|
280
284
|
try {
|
|
285
|
+
// processAllMDXFiles() already refreshes section index pages via its
|
|
286
|
+
// aggregate pass, so no separate generateSectionIndexPages() here.
|
|
281
287
|
await this.processAllMDXFiles();
|
|
282
|
-
await this.generateSectionIndexPages();
|
|
283
288
|
}
|
|
284
289
|
finally {
|
|
285
290
|
this.isReprocessing = false;
|
|
@@ -681,36 +686,58 @@ class MDXToNextJSGenerator {
|
|
|
681
686
|
const files = await this.getAllMDXFiles();
|
|
682
687
|
return Promise.all(files.map((file) => this.parseMDXFile(file)));
|
|
683
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Writes the generated page(s) for a single MDX file: the doc page and, for a
|
|
691
|
+
* section-index file, the section landing page. Deliberately does NOT run the
|
|
692
|
+
* site-wide aggregations (pages index, layout, sitemap, llms, section
|
|
693
|
+
* redirects) - the caller batches those so a bulk build runs them once at the
|
|
694
|
+
* end instead of once per file (which is what made large builds O(n²)).
|
|
695
|
+
*/
|
|
696
|
+
async writePageForFile(filePath) {
|
|
697
|
+
const fullPath = path.join(this.watchDir, filePath);
|
|
698
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
699
|
+
const { data: frontmatter, content: mdxContent } = matter(content);
|
|
700
|
+
const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, frontmatter);
|
|
701
|
+
const fullSlug = getFullSlug(pageSlug, sectionSlug);
|
|
702
|
+
const isIndex = filePath === "index.mdx" || filePath === "./index.mdx";
|
|
703
|
+
const isSectionIndex = this.sectionsConfig && pageSlug === "" && sectionSlug !== "";
|
|
704
|
+
if (isIndex) {
|
|
705
|
+
// The homepage is emitted by updatePagesIndex() in the aggregate pass, so
|
|
706
|
+
// there is no per-file page to write here.
|
|
707
|
+
console.log(chalk.blue("š Updating homepage with index.mdx content"));
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const mdxFile = {
|
|
711
|
+
path: filePath,
|
|
712
|
+
content: mdxContent,
|
|
713
|
+
frontmatter,
|
|
714
|
+
slug: fullSlug,
|
|
715
|
+
};
|
|
716
|
+
await this.generatePageFromMDX(mdxFile);
|
|
717
|
+
}
|
|
718
|
+
if (isSectionIndex) {
|
|
719
|
+
await this.updateSectionIndex(sectionSlug, frontmatter, mdxContent);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Regenerates every file that depends on the full set of pages (pages index,
|
|
724
|
+
* root/site layout, sitemap, llms files, section redirects). Parses all MDX
|
|
725
|
+
* exactly once and threads the result through each generator, so one refresh
|
|
726
|
+
* is a single scan rather than one scan per generator.
|
|
727
|
+
*/
|
|
728
|
+
async refreshSiteAggregates() {
|
|
729
|
+
const pages = await this.buildAllPagesMeta();
|
|
730
|
+
await this.updatePagesIndex();
|
|
731
|
+
await this.updateRootLayout(pages);
|
|
732
|
+
await this.updateSitemap(pages);
|
|
733
|
+
await this.updateLlmsFiles(pages);
|
|
734
|
+
await this.generateSectionIndexPages(pages);
|
|
735
|
+
}
|
|
684
736
|
async handleFileChange(action, filePath) {
|
|
685
737
|
console.log(chalk.cyan(`š File ${action}: ${filePath}`));
|
|
686
|
-
const fullPath = path.join(this.watchDir, filePath);
|
|
687
738
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, frontmatter);
|
|
691
|
-
const fullSlug = getFullSlug(pageSlug, sectionSlug);
|
|
692
|
-
const isIndex = filePath === "index.mdx" || filePath === "./index.mdx";
|
|
693
|
-
const isSectionIndex = this.sectionsConfig && pageSlug === "" && sectionSlug !== "";
|
|
694
|
-
if (isIndex) {
|
|
695
|
-
console.log(chalk.blue("š Updating homepage with index.mdx content"));
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
const mdxFile = {
|
|
699
|
-
path: filePath,
|
|
700
|
-
content: mdxContent,
|
|
701
|
-
frontmatter,
|
|
702
|
-
slug: fullSlug,
|
|
703
|
-
};
|
|
704
|
-
await this.generatePageFromMDX(mdxFile);
|
|
705
|
-
}
|
|
706
|
-
if (isSectionIndex) {
|
|
707
|
-
await this.updateSectionIndex(sectionSlug, frontmatter, mdxContent);
|
|
708
|
-
}
|
|
709
|
-
await this.updatePagesIndex();
|
|
710
|
-
await this.updateRootLayout();
|
|
711
|
-
await this.updateSitemap();
|
|
712
|
-
await this.updateLlmsFiles();
|
|
713
|
-
await this.generateSectionIndexPages();
|
|
739
|
+
await this.writePageForFile(filePath);
|
|
740
|
+
await this.refreshSiteAggregates();
|
|
714
741
|
console.log(chalk.green(`ā
Generated page for: ${filePath}`));
|
|
715
742
|
await this.maybeUpdateSections();
|
|
716
743
|
}
|
|
@@ -744,9 +771,21 @@ class MDXToNextJSGenerator {
|
|
|
744
771
|
}
|
|
745
772
|
async processAllMDXFiles() {
|
|
746
773
|
const files = await this.getAllMDXFiles();
|
|
774
|
+
// Write each page first (the only genuinely per-file work), then run the
|
|
775
|
+
// site-wide aggregations a single time. Doing the aggregations per file
|
|
776
|
+
// re-scanned and re-parsed every MDX file on each iteration, which made a
|
|
777
|
+
// full build O(n²); batching them makes it O(n). A single bad file is
|
|
778
|
+
// logged and skipped so it never aborts the whole build.
|
|
747
779
|
for (const file of files) {
|
|
748
|
-
|
|
780
|
+
console.log(chalk.cyan(`š Processing: ${file}`));
|
|
781
|
+
try {
|
|
782
|
+
await this.writePageForFile(file);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
console.error(chalk.red(`ā Error processing ${file}:`), error);
|
|
786
|
+
}
|
|
749
787
|
}
|
|
788
|
+
await this.refreshSiteAggregates();
|
|
750
789
|
}
|
|
751
790
|
async getAllMDXFiles() {
|
|
752
791
|
const files = [];
|
|
@@ -771,23 +810,23 @@ class MDXToNextJSGenerator {
|
|
|
771
810
|
const analyticsEnabled = this.analyticsConfig !== null;
|
|
772
811
|
return rootLayoutTemplate(fontConfig, analyticsEnabled);
|
|
773
812
|
}
|
|
774
|
-
async generateSiteLayout() {
|
|
775
|
-
const
|
|
776
|
-
return siteLayoutTemplate(
|
|
813
|
+
async generateSiteLayout(pages) {
|
|
814
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
815
|
+
return siteLayoutTemplate(resolvedPages, this.sectionsConfig);
|
|
777
816
|
}
|
|
778
|
-
async generateSectionIndexPages() {
|
|
817
|
+
async generateSectionIndexPages(pages) {
|
|
779
818
|
if (!this.sectionsConfig || this.sectionsConfig.length === 0)
|
|
780
819
|
return;
|
|
781
|
-
const
|
|
820
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
782
821
|
for (const section of this.sectionsConfig) {
|
|
783
822
|
if (section.slug === "")
|
|
784
823
|
continue;
|
|
785
824
|
// Check if a page already exists at the section root
|
|
786
|
-
const hasIndex =
|
|
825
|
+
const hasIndex = resolvedPages.some((p) => p.slug === section.slug);
|
|
787
826
|
if (hasIndex)
|
|
788
827
|
continue;
|
|
789
828
|
// Find the first page in this section
|
|
790
|
-
const sectionPages =
|
|
829
|
+
const sectionPages = resolvedPages
|
|
791
830
|
.filter((p) => p.section === section.slug)
|
|
792
831
|
.sort((a, b) => {
|
|
793
832
|
if (a.categoryOrder !== b.categoryOrder)
|
|
@@ -983,11 +1022,11 @@ export default function Page() {
|
|
|
983
1022
|
await fs.ensureDir(path.dirname(pagePath));
|
|
984
1023
|
await fs.writeFile(pagePath, indexContent, "utf8");
|
|
985
1024
|
}
|
|
986
|
-
async updateRootLayout() {
|
|
1025
|
+
async updateRootLayout(pages) {
|
|
987
1026
|
await fs.writeFile(path.join(this.outputDir, "app", "layout.tsx"), await this.generateRootLayout(), "utf8");
|
|
988
1027
|
const siteLayoutPath = path.join(this.outputDir, "app", "(site)", "layout.tsx");
|
|
989
1028
|
await fs.ensureDir(path.dirname(siteLayoutPath));
|
|
990
|
-
await fs.writeFile(siteLayoutPath, await this.generateSiteLayout(), "utf8");
|
|
1029
|
+
await fs.writeFile(siteLayoutPath, await this.generateSiteLayout(pages), "utf8");
|
|
991
1030
|
}
|
|
992
1031
|
async loadSiteUrl() {
|
|
993
1032
|
const configPath = path.join(this.rootDir, "config.json");
|
|
@@ -1033,7 +1072,7 @@ export default function Page() {
|
|
|
1033
1072
|
}
|
|
1034
1073
|
return entries;
|
|
1035
1074
|
}
|
|
1036
|
-
async updateSitemap() {
|
|
1075
|
+
async updateSitemap(pages) {
|
|
1037
1076
|
const sitemapPath = path.join(this.outputDir, "app", "sitemap.ts");
|
|
1038
1077
|
const siteUrl = await this.loadSiteUrl();
|
|
1039
1078
|
if (!siteUrl) {
|
|
@@ -1043,8 +1082,8 @@ export default function Page() {
|
|
|
1043
1082
|
}
|
|
1044
1083
|
return;
|
|
1045
1084
|
}
|
|
1046
|
-
const
|
|
1047
|
-
const entries = this.buildSitemapEntries(
|
|
1085
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
1086
|
+
const entries = this.buildSitemapEntries(resolvedPages);
|
|
1048
1087
|
await fs.writeFile(sitemapPath, sitemapTemplate(entries), "utf8");
|
|
1049
1088
|
console.log(chalk.green(`šŗļø Generated sitemap.ts with ${entries.length} page(s) using ${siteUrl}`));
|
|
1050
1089
|
}
|
|
@@ -1116,17 +1155,17 @@ export default function Page() {
|
|
|
1116
1155
|
const json = JSON.stringify(payload, null, 2) + "\n";
|
|
1117
1156
|
await fs.writeFile(manifestPath, json, "utf8");
|
|
1118
1157
|
}
|
|
1119
|
-
async updateLlmsFiles() {
|
|
1158
|
+
async updateLlmsFiles(pages) {
|
|
1120
1159
|
const publicDir = path.join(this.outputDir, "public");
|
|
1121
1160
|
await fs.ensureDir(publicDir);
|
|
1122
1161
|
const { url: baseUrl, name, description } = await this.loadSiteMetadata();
|
|
1123
|
-
const
|
|
1124
|
-
const pagesWithBodies = await Promise.all(
|
|
1162
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
1163
|
+
const pagesWithBodies = await Promise.all(resolvedPages.map((page) => this.readPageWithBody(page)));
|
|
1125
1164
|
const indexContent = llmsIndexTemplate({
|
|
1126
1165
|
siteName: name,
|
|
1127
1166
|
siteDescription: description,
|
|
1128
1167
|
baseUrl,
|
|
1129
|
-
pages,
|
|
1168
|
+
pages: resolvedPages,
|
|
1130
1169
|
sectionsConfig: this.sectionsConfig,
|
|
1131
1170
|
});
|
|
1132
1171
|
const fullContent = llmsFullTemplate({
|
|
@@ -1167,7 +1206,7 @@ export default function Page() {
|
|
|
1167
1206
|
}
|
|
1168
1207
|
this.generatedLlmsPagePaths = nextRelativePaths;
|
|
1169
1208
|
await this.writeLlmsManifest(nextRelativePaths);
|
|
1170
|
-
console.log(chalk.green(`š¤ Generated llms.txt and llms-full.txt with ${
|
|
1209
|
+
console.log(chalk.green(`š¤ Generated llms.txt and llms-full.txt with ${resolvedPages.length} page(s)${baseUrl ? ` using ${baseUrl}` : " (relative URLs)"}`));
|
|
1171
1210
|
}
|
|
1172
1211
|
async stop() {
|
|
1173
1212
|
if (this.watcher) {
|
|
@@ -1205,6 +1244,7 @@ program
|
|
|
1205
1244
|
.option("--port <port>", "Port for Next.js dev server", "3000")
|
|
1206
1245
|
.option("--verbose", "Show verbose output")
|
|
1207
1246
|
.option("--reset", "Reset configuration and prompt for new directories")
|
|
1247
|
+
.option("--package-manager <name>", "Package manager for the generated app: pnpm or npm (default: auto-detect)")
|
|
1208
1248
|
.action(async (options) => {
|
|
1209
1249
|
const configManager = new ConfigManager();
|
|
1210
1250
|
const config = await configManager.getConfig({
|
|
@@ -1215,18 +1255,13 @@ program
|
|
|
1215
1255
|
await generator.init();
|
|
1216
1256
|
let devServer = null;
|
|
1217
1257
|
console.log(chalk.blue("š¦ Installing dependencies..."));
|
|
1218
|
-
const { spawn
|
|
1219
|
-
//
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
catch {
|
|
1226
|
-
// pnpm not available, use npm
|
|
1227
|
-
}
|
|
1228
|
-
console.log(chalk.blue(`š¦ Using ${packageManager}...`));
|
|
1229
|
-
const install = spawn(packageManager, ["install"], {
|
|
1258
|
+
const { spawn } = await import("child_process");
|
|
1259
|
+
// Prefer pnpm (the generated app ships a pnpm workspace) and fall back to
|
|
1260
|
+
// npm. A --package-manager flag or a "packageManager" field in
|
|
1261
|
+
// doccupine.json overrides detection; the flag wins.
|
|
1262
|
+
const packageManager = resolvePackageManager(options.packageManager ?? config.packageManager);
|
|
1263
|
+
console.log(chalk.blue(`š¦ Using ${packageManager.name}...`));
|
|
1264
|
+
const install = spawn(packageManager.bin, ["install"], {
|
|
1230
1265
|
cwd: config.outputDir,
|
|
1231
1266
|
stdio: "inherit",
|
|
1232
1267
|
});
|
|
@@ -1237,7 +1272,7 @@ program
|
|
|
1237
1272
|
resolve(void 0);
|
|
1238
1273
|
}
|
|
1239
1274
|
else {
|
|
1240
|
-
reject(new Error(`${packageManager} install failed with code ${code}`));
|
|
1275
|
+
reject(new Error(`${packageManager.name} install failed with code ${code}`));
|
|
1241
1276
|
}
|
|
1242
1277
|
});
|
|
1243
1278
|
install.on("error", reject);
|
|
@@ -1248,10 +1283,10 @@ program
|
|
|
1248
1283
|
}
|
|
1249
1284
|
console.log(chalk.blue(`š Starting Next.js dev server on port ${port}...`));
|
|
1250
1285
|
const portStr = String(port);
|
|
1251
|
-
const devArgs = packageManager === "npm"
|
|
1286
|
+
const devArgs = packageManager.name === "npm"
|
|
1252
1287
|
? ["run", "dev", "--", "--port", portStr]
|
|
1253
1288
|
: ["run", "dev", "--port", portStr];
|
|
1254
|
-
devServer = spawn(packageManager, devArgs, {
|
|
1289
|
+
devServer = spawn(packageManager.bin, devArgs, {
|
|
1255
1290
|
cwd: config.outputDir,
|
|
1256
1291
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1257
1292
|
});
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface DoccupineConfig {
|
|
|
27
27
|
watchDir: string;
|
|
28
28
|
outputDir: string;
|
|
29
29
|
port: string;
|
|
30
|
+
/** Force the package manager used to install and run the generated app
|
|
31
|
+
* ("pnpm" or "npm"), overriding auto-detection. Also settable per run via
|
|
32
|
+
* the `--package-manager` flag, which takes precedence over this field. */
|
|
33
|
+
packageManager?: string;
|
|
30
34
|
}
|
|
31
35
|
export interface FontConfig {
|
|
32
36
|
[key: string]: any;
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export type PackageManagerName = "pnpm" | "npm";
|
|
2
|
+
export interface ResolvedPackageManager {
|
|
3
|
+
name: PackageManagerName;
|
|
4
|
+
/** Absolute path when we can resolve one, otherwise the bare command name.
|
|
5
|
+
* Spawning the resolved path (not the bare name) is what lets pnpm work
|
|
6
|
+
* even when its install dir is missing from the process PATH. */
|
|
7
|
+
bin: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Chooses the package manager for the generated app. The app ships a pnpm
|
|
11
|
+
* workspace, so pnpm is preferred whenever it can be found; npm is the fallback.
|
|
12
|
+
* An explicit `override` (from `--package-manager` or doccupine.json) always
|
|
13
|
+
* wins. Auto-detection also trusts `npm_config_user_agent` when pnpm launched
|
|
14
|
+
* the CLI.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolvePackageManager(override?: string): ResolvedPackageManager;
|
|
1
17
|
export declare function findAvailablePort(startPort: number): Promise<number>;
|
|
2
18
|
export declare function generateSlug(filePath: string): string;
|
|
3
19
|
export declare function getFullSlug(pageSlug: string, sectionSlug: string): string;
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
/**
|
|
6
|
+
* Locates a usable pnpm binary without relying solely on the caller's PATH.
|
|
7
|
+
* Standalone pnpm installs (e.g. ~/Library/pnpm) expose PNPM_HOME but only add
|
|
8
|
+
* their bin dir to PATH via an interactive shell rc, so a CLI launched from an
|
|
9
|
+
* IDE terminal or GUI often can't see `pnpm` on PATH. Returns null if pnpm
|
|
10
|
+
* genuinely isn't available.
|
|
11
|
+
*/
|
|
12
|
+
function findPnpmBin() {
|
|
13
|
+
const exe = process.platform === "win32" ? "pnpm.exe" : "pnpm";
|
|
14
|
+
const home = process.env.PNPM_HOME;
|
|
15
|
+
if (home) {
|
|
16
|
+
const candidate = path.join(home, exe);
|
|
17
|
+
if (fs.existsSync(candidate))
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
22
|
+
return "pnpm";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Chooses the package manager for the generated app. The app ships a pnpm
|
|
30
|
+
* workspace, so pnpm is preferred whenever it can be found; npm is the fallback.
|
|
31
|
+
* An explicit `override` (from `--package-manager` or doccupine.json) always
|
|
32
|
+
* wins. Auto-detection also trusts `npm_config_user_agent` when pnpm launched
|
|
33
|
+
* the CLI.
|
|
34
|
+
*/
|
|
35
|
+
export function resolvePackageManager(override) {
|
|
36
|
+
const requested = override?.trim().toLowerCase();
|
|
37
|
+
if (requested === "npm")
|
|
38
|
+
return { name: "npm", bin: "npm" };
|
|
39
|
+
if (requested === "pnpm") {
|
|
40
|
+
const bin = findPnpmBin();
|
|
41
|
+
if (bin)
|
|
42
|
+
return { name: "pnpm", bin };
|
|
43
|
+
console.warn(chalk.yellow("ā ļø packageManager is set to pnpm but pnpm could not be found; using npm."));
|
|
44
|
+
return { name: "npm", bin: "npm" };
|
|
45
|
+
}
|
|
46
|
+
if (requested) {
|
|
47
|
+
console.warn(chalk.yellow(`ā ļø Unknown packageManager "${override}" (expected "pnpm" or "npm"); auto-detecting instead.`));
|
|
48
|
+
}
|
|
49
|
+
const launchedByPnpm = (process.env.npm_config_user_agent || "").startsWith("pnpm");
|
|
50
|
+
if (launchedByPnpm)
|
|
51
|
+
return { name: "pnpm", bin: "pnpm" };
|
|
52
|
+
const bin = findPnpmBin();
|
|
53
|
+
if (bin)
|
|
54
|
+
return { name: "pnpm", bin };
|
|
55
|
+
return { name: "npm", bin: "npm" };
|
|
56
|
+
}
|
|
1
57
|
export async function findAvailablePort(startPort) {
|
|
2
58
|
const net = await import("net");
|
|
3
59
|
return new Promise((resolve) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const calloutTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport styled, { css } from \"styled-components\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\ntype CalloutType = \"note\" | \"info\" | \"warning\" | \"danger\" | \"success\";\n\nconst StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledSmall(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n\n & svg {\n vertical-align: middle;\n min-width: min-content;\n margin: 3px 10px 0 0;\n }\n\n /* Callout tints are alert-semantic (info-blue, warning-amber, danger-red,\n etc.) and intentionally independent of theme.json. Light-mode values are\n defined by default; dark-mode overrides live in :root.dark & blocks so\n the swap happens via the active <html> class with no re-render. */\n ${({ $type }) =>\n $type === \"note\" &&\n css`\n border-color: #0ea5e933;\n background: #f0f9ff80;\n\n & svg.lucide,\n & p {\n color: #0c4a6e;\n }\n\n :root.dark & {\n border-color: #0ea5e94d;\n background: #0ea5e91a;\n\n & svg.lucide,\n & p {\n color: #bae6fd;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"info\" &&\n css`\n border-color: #71717a33;\n background: #fafafa80;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #18181b;\n }\n\n :root.dark & {\n border-color: #71717a4d;\n background: #71717a1a;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #e4e4e7;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"warning\" &&\n css`\n border-color: #f59e0b33;\n background: #fffbeb80;\n\n & svg.lucide,\n & p {\n color: #78350f;\n }\n\n :root.dark & {\n border-color: #f59e0b4d;\n background: #f59e0b1a;\n\n & svg.lucide,\n & p {\n color: #fde68a;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"danger\" &&\n css`\n border-color: #ef444433;\n background: #fef2f280;\n\n & svg.lucide,\n & p {\n color: #7f1d1d;\n }\n\n :root.dark & {\n border-color: #ef44444d;\n background: #ef44441a;\n\n & svg.lucide,\n & p {\n color: #fecaca;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"success\" &&\n css`\n border-color: #10b98133;\n background: #ecfdf580;\n\n & svg.lucide,\n & p {\n color: #064e3b;\n }\n\n :root.dark & {\n border-color: #10b9814d;\n background: #10b9811a;\n\n & svg.lucide,\n & p {\n color: #a7f3d0;\n }\n }\n `}\n`;\n\nconst StyledChildren = styled.span`\n display: flex;\n flex-direction: column;\n gap: 10px;\n`;\n\ninterface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n icon?: IconProps;\n type?: CalloutType;\n}\n\nfunction Callout({ children, type, icon }: CalloutProps) {\n const iconType =\n type === \"note\"\n ? \"CircleAlert\"\n : type === \"info\"\n ? \"Info\"\n : type === \"warning\"\n ? \"TriangleAlert\"\n : type === \"danger\"\n ? \"OctagonAlert\"\n : type === \"success\"\n ? \"Check\"\n : (icon as IconProps);\n return (\n <StyledCallout $type={type}>\n {iconType ? <Icon name={iconType} size={16} /> : null}\n <StyledChildren>{children}</StyledChildren>\n </StyledCallout>\n );\n}\n\nexport { Callout };\n";
|
|
1
|
+
export declare const calloutTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport styled, { css } from \"styled-components\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\ntype CalloutType = \"note\" | \"info\" | \"warning\" | \"danger\" | \"success\";\n\nconst StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n max-width: 100%;\n ${({ theme }) => styledSmall(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n\n & svg {\n vertical-align: middle;\n min-width: min-content;\n margin: 3px 10px 0 0;\n }\n\n /* Callout tints are alert-semantic (info-blue, warning-amber, danger-red,\n etc.) and intentionally independent of theme.json. Light-mode values are\n defined by default; dark-mode overrides live in :root.dark & blocks so\n the swap happens via the active <html> class with no re-render. */\n ${({ $type }) =>\n $type === \"note\" &&\n css`\n border-color: #0ea5e933;\n background: #f0f9ff80;\n\n & svg.lucide,\n & p {\n color: #0c4a6e;\n }\n\n :root.dark & {\n border-color: #0ea5e94d;\n background: #0ea5e91a;\n\n & svg.lucide,\n & p {\n color: #bae6fd;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"info\" &&\n css`\n border-color: #71717a33;\n background: #fafafa80;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #18181b;\n }\n\n :root.dark & {\n border-color: #71717a4d;\n background: #71717a1a;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #e4e4e7;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"warning\" &&\n css`\n border-color: #f59e0b33;\n background: #fffbeb80;\n\n & svg.lucide,\n & p {\n color: #78350f;\n }\n\n :root.dark & {\n border-color: #f59e0b4d;\n background: #f59e0b1a;\n\n & svg.lucide,\n & p {\n color: #fde68a;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"danger\" &&\n css`\n border-color: #ef444433;\n background: #fef2f280;\n\n & svg.lucide,\n & p {\n color: #7f1d1d;\n }\n\n :root.dark & {\n border-color: #ef44444d;\n background: #ef44441a;\n\n & svg.lucide,\n & p {\n color: #fecaca;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"success\" &&\n css`\n border-color: #10b98133;\n background: #ecfdf580;\n\n & svg.lucide,\n & p {\n color: #064e3b;\n }\n\n :root.dark & {\n border-color: #10b9814d;\n background: #10b9811a;\n\n & svg.lucide,\n & p {\n color: #a7f3d0;\n }\n }\n `}\n`;\n\nconst StyledChildren = styled.span`\n display: flex;\n flex-direction: column;\n gap: 10px;\n`;\n\ninterface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n icon?: IconProps;\n type?: CalloutType;\n}\n\nfunction Callout({ children, type, icon }: CalloutProps) {\n const iconType =\n type === \"note\"\n ? \"CircleAlert\"\n : type === \"info\"\n ? \"Info\"\n : type === \"warning\"\n ? \"TriangleAlert\"\n : type === \"danger\"\n ? \"OctagonAlert\"\n : type === \"success\"\n ? \"Check\"\n : (icon as IconProps);\n return (\n <StyledCallout $type={type}>\n {iconType ? <Icon name={iconType} size={16} /> : null}\n <StyledChildren>{children}</StyledChildren>\n </StyledCallout>\n );\n}\n\nexport { Callout };\n";
|
|
@@ -12,6 +12,7 @@ const StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>\`
|
|
|
12
12
|
border-radius: \${({ theme }) => theme.spacing.radius.lg};
|
|
13
13
|
padding: 20px;
|
|
14
14
|
margin: 0;
|
|
15
|
+
max-width: 100%;
|
|
15
16
|
\${({ theme }) => styledSmall(theme)}
|
|
16
17
|
color: \${({ theme }) => theme.colors.grayDark};
|
|
17
18
|
display: flex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo, useId } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n interactiveStyles,\n resetButton,\n styledCode,\n} from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n title?: string;\n theme?: Theme;\n}\n\ninterface CodeTab {\n label: string;\n code: string;\n language?: string;\n}\n\ninterface CodeTabsProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n tabs: CodeTab[];\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n border-color: rgba(255, 255, 255, 0.2);\n }\n`;\n\n/* Code block uses a fixed GitHub-style palette in both modes. Independent of\n theme.json so syntax highlighting stays legible regardless of brand colors.\n Dark variants live in :root.dark & blocks so the swap happens via the\n active <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n position: relative;\n background: #f6f8fa;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px rgba(0, 0, 0, 0.1);\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n\n :root.dark & {\n background: #0d1117;\n border-bottom-color: rgba(255, 255, 255, 0.1);\n }\n`;\n\nconst DotsContainer = styled.div`\n display: flex;\n gap: 5px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n background: rgba(255, 255, 255, 0.1);\n }\n`;\n\n/* Icon-only copy button. interactiveStyles supplies the border highlight on\n hover plus the focus/active rings (no scale effect); the GitHub-style\n copied/base colors stay fixed like the rest of the code block. The dark\n block re-declares the hover border because its higher-specificity\n :root.dark & border-color would otherwise override the mixin's hover. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ $copied }) =>\n $copied ? \"rgba(45, 164, 78, 0.1)\" : \"transparent\"};\n border-color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"rgba(0, 0, 0, 0.1)\")};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: -6px;\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"#57606a\")};\n }\n\n :root.dark & {\n background: ${({ $copied }) =>\n $copied ? \"rgba(126, 231, 135, 0.2)\" : \"transparent\"};\n border-color: ${({ $copied }) =>\n $copied ? \"#7ee787\" : \"rgba(255, 255, 255, 0.1)\"};\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#7ee787\" : \"#c9d1d9\")};\n }\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\n/* Centered file name in the TopBar. Sits absolutely over the flex row so the\n dots and copy button keep their edge alignment. Monospace + muted GitHub\n grays, swapped via :root.dark like the rest of the block. pointer-events off\n so clicks fall through to nothing and never steal focus from the buttons. */\nconst TopBarTitle = styled.span<{ theme: Theme }>`\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n max-width: 60%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 2;\n color: #57606a;\n\n :root.dark & {\n color: #8b949e;\n }\n`;\n\n/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally\n with the thin scrollbar when the labels overflow the 33px bar. */\nconst TabList = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n overflow-x: auto;\n ${thinScrollbar};\n margin-left: -6px;\n`;\n\n/* Individual tab button. Active tab reads as part of the window: a light body\n fill (#ffffff) that echoes the code area, with a soft border. Inactive tabs\n are muted and transparent. resetButton strips native styling; focus-visible\n draws an inset brand primary ring on a pseudo-element so the scrolling\n TabList can't clip it. All colors stay fixed and swap purely via :root.dark,\n matching the no-re-render palette approach. */\nconst CodeTab = styled.button<{ theme: Theme; $active: boolean }>`\n ${resetButton}\n flex: 0 0 auto;\n position: relative;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 1;\n padding: 5px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.15s ease,\n background 0.15s ease,\n border-color 0.15s ease;\n border: solid 1px\n ${({ $active }) => ($active ? \"rgba(0, 0, 0, 0.1)\" : \"transparent\")};\n background: ${({ $active }) => ($active ? \"#ffffff\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#24292f\" : \"#57606a\")};\n\n &:hover {\n color: #24292f;\n background: ${({ $active }) =>\n $active ? \"#ffffff\" : \"rgba(0, 0, 0, 0.04)\"};\n }\n\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::after {\n content: \"\";\n position: absolute;\n inset: 2px;\n border: solid 2px ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n pointer-events: none;\n }\n\n :root.dark & {\n border-color: ${({ $active }) =>\n $active ? \"rgba(255, 255, 255, 0.15)\" : \"transparent\"};\n background: ${({ $active }) => ($active ? \"#161b22\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#e6edf3\" : \"#8b949e\")};\n\n &:hover {\n color: #e6edf3;\n background: ${({ $active }) =>\n $active ? \"#161b22\" : \"rgba(255, 255, 255, 0.06)\"};\n }\n }\n`;\n\n/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.\n Browser resolves which rule wins based on the active <html> class with no\n JS or React re-render involved. */\nconst lightSyntaxHighlight = css`\n & .hljs {\n color: #24292f;\n background: #ffffff;\n min-width: min-content;\n width: 100%;\n display: block;\n padding-right: 20px;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #cf222e;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #8250df;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #0550ae;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #0a3069;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #953800;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #6e7781;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #116329;\n }\n & .hljs-subst {\n color: #24292f;\n }\n & .hljs-section {\n color: #0550ae;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #953800;\n }\n & .hljs-emphasis {\n color: #24292f;\n font-style: italic;\n }\n & .hljs-strong {\n color: #24292f;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #116329;\n background-color: #dafbe1;\n }\n & .hljs-deletion {\n color: #82071e;\n background-color: #ffebe9;\n }\n`;\n\nconst darkSyntaxHighlight = css`\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n & .hljs-subst {\n color: #c9d1d9;\n }\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #f2cc60;\n }\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #ffffff;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #24292f;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100dvh - 400px);\n ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n /* Diff lines: highlight.js wraps each +/- line in a single span, so\n stretching it to the full row reads like a GitHub diff. inline-block keeps\n the trailing newline as the line break instead of adding an empty row. */\n & .hljs-addition,\n & .hljs-deletion {\n display: inline-table;\n width: 100%;\n }\n\n :root.dark & {\n background: #0d1117;\n color: #ffffff;\n ${darkSyntaxHighlight};\n }\n`;\n\nconst escapeHtml = (unsafe: string): string => {\n return unsafe\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n};\n\nconst sanitizeLanguage = (lang: string): string =>\n lang.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\nconst highlightCode = (code: string, language: string): string => {\n const escapedCode = escapeHtml(code);\n const safeLang = sanitizeLanguage(language);\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${safeLang}\">${escapedCode}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({\n code,\n language = \"javascript\",\n title,\n theme,\n className,\n}: CodeProps) {\n const [copied, setCopied] = useState(false);\n const highlightedCode = useMemo(\n () => highlightCode(code, language),\n [code, language],\n );\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [code]);\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <DotsContainer>\n <Dot theme={theme} />\n <Dot theme={theme} />\n <Dot theme={theme} />\n </DotsContainer>\n {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\n/* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS\n dots in the TopBar as a keyboard-accessible tablist; the copy button copies\n whichever tab is active and resets its copied state when the tab changes. */\nfunction CodeTabs({ tabs, theme, className }: CodeTabsProps) {\n const baseId = useId();\n const [active, setActive] = useState(0);\n const [copied, setCopied] = useState(false);\n\n const safeActive = active < tabs.length ? active : 0;\n const activeTab = tabs[safeActive];\n\n const highlightedCode = useMemo(\n () =>\n activeTab\n ? highlightCode(activeTab.code, activeTab.language ?? \"bash\")\n : \"\",\n [activeTab],\n );\n\n const handleSelect = useCallback((index: number) => {\n setActive(index);\n setCopied(false);\n }, []);\n\n const handleCopy = useCallback(async () => {\n if (!activeTab) return;\n try {\n await navigator.clipboard.writeText(activeTab.code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [activeTab]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLButtonElement>) => {\n let next = safeActive;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (safeActive + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (safeActive - 1 + tabs.length) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n handleSelect(next);\n const target = document.getElementById(`${baseId}-tab-${next}`);\n if (target) target.focus();\n },\n [baseId, handleSelect, safeActive, tabs.length],\n );\n\n if (!tabs || tabs.length === 0) return null;\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <TabList role=\"tablist\" aria-label=\"Code variants\">\n {tabs.map((tab, index) => (\n <CodeTab\n key={`${tab.label}-${index}`}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${index}`}\n aria-selected={index === safeActive}\n aria-controls={`${baseId}-panel`}\n tabIndex={index === safeActive ? 0 : -1}\n onClick={() => handleSelect(index)}\n onKeyDown={handleKeyDown}\n $active={index === safeActive}\n theme={theme}\n >\n {tab.label}\n </CodeTab>\n ))}\n </TabList>\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n id={`${baseId}-panel`}\n role=\"tabpanel\"\n aria-labelledby={`${baseId}-tab-${safeActive}`}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code, CodeTabs };\n";
|
|
1
|
+
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo, useId } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n interactiveStyles,\n resetButton,\n styledCode,\n} from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n title?: string;\n theme?: Theme;\n}\n\ninterface CodeTab {\n label: string;\n code: string;\n language?: string;\n}\n\ninterface CodeTabsProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n tabs: CodeTab[];\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n border-color: rgba(255, 255, 255, 0.2);\n }\n`;\n\n/* Code block uses a fixed GitHub-style palette in both modes. Independent of\n theme.json so syntax highlighting stays legible regardless of brand colors.\n Dark variants live in :root.dark & blocks so the swap happens via the\n active <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n position: relative;\n background: #f6f8fa;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px rgba(0, 0, 0, 0.1);\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n\n :root.dark & {\n background: #0d1117;\n border-bottom-color: rgba(255, 255, 255, 0.1);\n }\n`;\n\nconst DotsContainer = styled.div`\n display: flex;\n gap: 5px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n background: rgba(255, 255, 255, 0.1);\n }\n`;\n\n/* Icon-only copy button. interactiveStyles supplies the border highlight on\n hover plus the focus/active rings (no scale effect); the GitHub-style\n copied/base colors stay fixed like the rest of the code block. The dark\n block re-declares the hover border because its higher-specificity\n :root.dark & border-color would otherwise override the mixin's hover. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ $copied }) =>\n $copied ? \"rgba(45, 164, 78, 0.1)\" : \"transparent\"};\n border-color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"rgba(0, 0, 0, 0.1)\")};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: -6px;\n\n & svg.lucide {\n margin: 0;\n color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"#57606a\")};\n }\n\n :root.dark & {\n background: ${({ $copied }) =>\n $copied ? \"rgba(126, 231, 135, 0.2)\" : \"transparent\"};\n border-color: ${({ $copied }) =>\n $copied ? \"#7ee787\" : \"rgba(255, 255, 255, 0.1)\"};\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#7ee787\" : \"#c9d1d9\")};\n }\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\n/* Centered file name in the TopBar. Sits absolutely over the flex row so the\n dots and copy button keep their edge alignment. Monospace + muted GitHub\n grays, swapped via :root.dark like the rest of the block. pointer-events off\n so clicks fall through to nothing and never steal focus from the buttons. */\nconst TopBarTitle = styled.span<{ theme: Theme }>`\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n max-width: 60%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 2;\n color: #57606a;\n\n :root.dark & {\n color: #8b949e;\n }\n`;\n\n/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally\n with the thin scrollbar when the labels overflow the 33px bar. */\nconst TabList = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n overflow-x: auto;\n ${thinScrollbar};\n margin-left: -6px;\n`;\n\n/* Individual tab button. Active tab reads as part of the window: a light body\n fill (#ffffff) that echoes the code area, with a soft border. Inactive tabs\n are muted and transparent. resetButton strips native styling; focus-visible\n draws an inset brand primary ring on a pseudo-element so the scrolling\n TabList can't clip it. All colors stay fixed and swap purely via :root.dark,\n matching the no-re-render palette approach. */\nconst CodeTab = styled.button<{ theme: Theme; $active: boolean }>`\n ${resetButton}\n flex: 0 0 auto;\n position: relative;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 1;\n padding: 5px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.15s ease,\n background 0.15s ease,\n border-color 0.15s ease;\n border: solid 1px\n ${({ $active }) => ($active ? \"rgba(0, 0, 0, 0.1)\" : \"transparent\")};\n background: ${({ $active }) => ($active ? \"#ffffff\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#24292f\" : \"#57606a\")};\n\n &:hover {\n color: #24292f;\n background: ${({ $active }) =>\n $active ? \"#ffffff\" : \"rgba(0, 0, 0, 0.04)\"};\n }\n\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::after {\n content: \"\";\n position: absolute;\n inset: 2px;\n border: solid 2px ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n pointer-events: none;\n }\n\n :root.dark & {\n border-color: ${({ $active }) =>\n $active ? \"rgba(255, 255, 255, 0.15)\" : \"transparent\"};\n background: ${({ $active }) => ($active ? \"#161b22\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#e6edf3\" : \"#8b949e\")};\n\n &:hover {\n color: #e6edf3;\n background: ${({ $active }) =>\n $active ? \"#161b22\" : \"rgba(255, 255, 255, 0.06)\"};\n }\n }\n`;\n\n/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.\n Browser resolves which rule wins based on the active <html> class with no\n JS or React re-render involved. */\nconst lightSyntaxHighlight = css`\n & .hljs {\n color: #24292f;\n background: #ffffff;\n min-width: min-content;\n width: 100%;\n display: block;\n padding-right: 20px;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #cf222e;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #8250df;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #0550ae;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #0a3069;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #953800;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #6e7781;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #116329;\n }\n & .hljs-subst {\n color: #24292f;\n }\n & .hljs-section {\n color: #0550ae;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #953800;\n }\n & .hljs-emphasis {\n color: #24292f;\n font-style: italic;\n }\n & .hljs-strong {\n color: #24292f;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #116329;\n background-color: #dafbe1;\n }\n & .hljs-deletion {\n color: #82071e;\n background-color: #ffebe9;\n }\n`;\n\nconst darkSyntaxHighlight = css`\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n & .hljs-subst {\n color: #c9d1d9;\n }\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #f2cc60;\n }\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #ffffff;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #24292f;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100dvh - 400px);\n ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n /* Diff lines: highlight.js wraps each +/- line in a single span, so\n stretching it to the full row reads like a GitHub diff. inline-block keeps\n the trailing newline as the line break instead of adding an empty row. */\n & .hljs-addition,\n & .hljs-deletion {\n display: inline-table;\n width: 100%;\n }\n\n :root.dark & {\n background: #0d1117;\n color: #ffffff;\n ${darkSyntaxHighlight};\n }\n`;\n\nconst escapeHtml = (unsafe: string): string => {\n return unsafe\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n};\n\nconst sanitizeLanguage = (lang: string): string =>\n lang.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\nconst highlightCode = (code: string, language: string): string => {\n const escapedCode = escapeHtml(code);\n const safeLang = sanitizeLanguage(language);\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${safeLang}\">${escapedCode}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({\n code,\n language = \"javascript\",\n title,\n theme,\n className,\n}: CodeProps) {\n const [copied, setCopied] = useState(false);\n const highlightedCode = useMemo(\n () => highlightCode(code, language),\n [code, language],\n );\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [code]);\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <DotsContainer>\n <Dot theme={theme} />\n <Dot theme={theme} />\n <Dot theme={theme} />\n </DotsContainer>\n {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\n/* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS\n dots in the TopBar as a keyboard-accessible tablist; the copy button copies\n whichever tab is active and resets its copied state when the tab changes. */\nfunction CodeTabs({ tabs, theme, className }: CodeTabsProps) {\n const baseId = useId();\n const [active, setActive] = useState(0);\n const [copied, setCopied] = useState(false);\n\n const safeActive = active < tabs.length ? active : 0;\n const activeTab = tabs[safeActive];\n\n const highlightedCode = useMemo(\n () =>\n activeTab\n ? highlightCode(activeTab.code, activeTab.language ?? \"bash\")\n : \"\",\n [activeTab],\n );\n\n const handleSelect = useCallback((index: number) => {\n setActive(index);\n setCopied(false);\n }, []);\n\n const handleCopy = useCallback(async () => {\n if (!activeTab) return;\n try {\n await navigator.clipboard.writeText(activeTab.code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [activeTab]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLButtonElement>) => {\n let next = safeActive;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (safeActive + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (safeActive - 1 + tabs.length) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n handleSelect(next);\n const target = document.getElementById(`${baseId}-tab-${next}`);\n if (target) target.focus();\n },\n [baseId, handleSelect, safeActive, tabs.length],\n );\n\n if (!tabs || tabs.length === 0) return null;\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <TabList role=\"tablist\" aria-label=\"Code variants\">\n {tabs.map((tab, index) => (\n <CodeTab\n key={`${tab.label}-${index}`}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${index}`}\n aria-selected={index === safeActive}\n aria-controls={`${baseId}-panel`}\n tabIndex={index === safeActive ? 0 : -1}\n onClick={() => handleSelect(index)}\n onKeyDown={handleKeyDown}\n $active={index === safeActive}\n theme={theme}\n >\n {tab.label}\n </CodeTab>\n ))}\n </TabList>\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n id={`${baseId}-panel`}\n role=\"tabpanel\"\n aria-labelledby={`${baseId}-tab-${safeActive}`}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code, CodeTabs };\n";
|
|
@@ -12,19 +12,19 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
12
12
|
},
|
|
13
13
|
dependencies: {
|
|
14
14
|
"@langchain/anthropic": "^1.5.1",
|
|
15
|
-
"@langchain/core": "^1.2.
|
|
15
|
+
"@langchain/core": "^1.2.2",
|
|
16
16
|
"@langchain/google-genai": "^2.2.0",
|
|
17
|
-
"@langchain/openai": "^1.5.
|
|
17
|
+
"@langchain/openai": "^1.5.5",
|
|
18
18
|
"@mdx-js/react": "^3.1.1",
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
20
|
"@posthog/react": "^1.10.3",
|
|
21
21
|
"cherry-styled-components": "^0.2.10",
|
|
22
22
|
langchain: "^1.5.3",
|
|
23
|
-
"lucide-react": "^1.
|
|
23
|
+
"lucide-react": "^1.24.0",
|
|
24
24
|
minisearch: "^7.2.0",
|
|
25
25
|
next: "16.2.10",
|
|
26
26
|
"next-mdx-remote": "^6.0.0",
|
|
27
|
-
"posthog-js": "^1.399.
|
|
27
|
+
"posthog-js": "^1.399.1",
|
|
28
28
|
"posthog-node": "^5.40.0",
|
|
29
29
|
react: "19.2.7",
|
|
30
30
|
"react-dom": "19.2.7",
|
|
@@ -50,7 +50,7 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
50
50
|
"eslint-plugin-react": "^7.37.5",
|
|
51
51
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
52
52
|
globals: "^17.7.0",
|
|
53
|
-
prettier: "^3.9.
|
|
53
|
+
prettier: "^3.9.5",
|
|
54
54
|
typescript: "npm:@typescript/typescript6@^6.0.2",
|
|
55
55
|
},
|
|
56
56
|
}, null, 2) + "\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doccupine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.115",
|
|
4
4
|
"description": "Free and open-source documentation platform. Write MDX, get a production-ready site with AI chat, built-in components, and an MCP server - in one command.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/fs-extra": "^11.0.4",
|
|
49
49
|
"@types/node": "^26.1.1",
|
|
50
50
|
"@types/prompts": "^2.4.9",
|
|
51
|
-
"prettier": "^3.9.
|
|
51
|
+
"prettier": "^3.9.5",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.10"
|
|
54
54
|
},
|