basecampjs 0.0.7 → 0.0.9

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.
Files changed (2) hide show
  1. package/index.js +782 -16
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -4,7 +4,7 @@ import { createServer } from "http";
4
4
  import { existsSync } from "fs";
5
5
  import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
6
6
  import { basename, dirname, extname, join, relative, resolve } from "path";
7
- import { pathToFileURL } from "url";
7
+ import { pathToFileURL, fileURLToPath } from "url";
8
8
  import { createHash } from "crypto";
9
9
  import * as kolor from "kolorist";
10
10
  import chokidar from "chokidar";
@@ -17,6 +17,7 @@ import { minify as minifyCss } from "csso";
17
17
  import { minify as minifyHtml } from "html-minifier-terser";
18
18
 
19
19
  const cwd = process.cwd();
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
21
  const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
21
22
 
22
23
  const defaultConfig = {
@@ -44,22 +45,96 @@ async function loadConfig(root) {
44
45
  }
45
46
  }
46
47
 
48
+ async function getVersion() {
49
+ try {
50
+ const pkgPath = join(__dirname, "package.json");
51
+ const raw = await readFile(pkgPath, "utf8");
52
+ const pkg = JSON.parse(raw);
53
+ return pkg.version || "0.0.0";
54
+ } catch {
55
+ return "0.0.0";
56
+ }
57
+ }
58
+
59
+ function showHelp() {
60
+ console.log(kolor.cyan(kolor.bold("\nšŸ•ļø CampsiteJS CLI")));
61
+ console.log(kolor.dim("Build and manage your static campsite.\n"));
62
+
63
+ console.log(kolor.bold("Usage:"));
64
+ console.log(" campsite <command> [arguments] [options]\n");
65
+
66
+ console.log(kolor.bold("Project Commands:"));
67
+ console.log(" " + kolor.cyan("init") + " Initialize a new Campsite project in current directory");
68
+ console.log(" Creates config, folder structure, and starter files\n");
69
+
70
+ console.log(kolor.bold("Development Commands:"));
71
+ console.log(" " + kolor.cyan("dev") + " Start development server with hot reloading");
72
+ console.log(" Watches for file changes and rebuilds automatically");
73
+ console.log(" " + kolor.cyan("build") + " Build your site for production");
74
+ console.log(" Optimizes and outputs to dist/ directory");
75
+ console.log(" " + kolor.cyan("serve") + " Serve the built site locally");
76
+ console.log(" Serves from dist/ folder on http://localhost:4173");
77
+ console.log(" " + kolor.cyan("preview") + " Build and serve in production mode");
78
+ console.log(" Combines build + serve for testing production output\n");
79
+
80
+ console.log(kolor.bold("Utility Commands:"));
81
+ console.log(" " + kolor.cyan("list") + " List all content (pages, layouts, components, etc.)");
82
+ console.log(" Overview of your project structure");
83
+ console.log(" " + kolor.cyan("clean") + " Remove build output directory");
84
+ console.log(" Deletes dist/ folder for a fresh build");
85
+ console.log(" " + kolor.cyan("check") + " Validate config and check for issues");
86
+ console.log(" Diagnoses project structure and dependencies");
87
+ console.log(" " + kolor.cyan("upgrade") + " Update CampsiteJS to the latest version");
88
+ console.log(" Checks and upgrades basecampjs and dependencies\n");
89
+
90
+ console.log(kolor.bold("Make Commands:"));
91
+ console.log(" " + kolor.cyan("make:page") + " " + kolor.dim("<name>") + " Create a new page in src/pages/");
92
+ console.log(" " + kolor.cyan("make:post") + " " + kolor.dim("<name>") + " Create a new blog post in src/pages/blog/");
93
+ console.log(" " + kolor.cyan("make:layout") + " " + kolor.dim("<name>") + " Create a new layout in src/layouts/");
94
+ console.log(" " + kolor.cyan("make:component") + " " + kolor.dim("<name>") + " Create a new component in src/components/");
95
+ console.log(" " + kolor.cyan("make:partial") + " " + kolor.dim("<name>") + " Create a new partial in src/partials/");
96
+ console.log(" " + kolor.cyan("make:collection") + " " + kolor.dim("<name>") + " Create a new JSON collection in src/collections/\n");
97
+
98
+ console.log(kolor.bold("Options:"));
99
+ console.log(" -h, --help Show this help message");
100
+ console.log(" -v, --version Show version number\n");
101
+
102
+ console.log(kolor.bold("Examples:"));
103
+ console.log(" " + kolor.dim("# Initialize a new project"));
104
+ console.log(" campsite init\n");
105
+ console.log(" " + kolor.dim("# Start development"));
106
+ console.log(" campsite dev\n");
107
+ console.log(" " + kolor.dim("# Create new content"));
108
+ console.log(" campsite make:page about");
109
+ console.log(" campsite make:post \"My First Post\"");
110
+ console.log(" campsite make:collection products\n");
111
+ console.log(" " + kolor.dim("# Build and preview"));
112
+ console.log(" campsite preview\n");
113
+ console.log(kolor.dim("For more information, visit: https://campsitejs.dev"));
114
+ console.log();
115
+ }
116
+
47
117
  async function ensureDir(dir) {
48
118
  await mkdir(dir, { recursive: true });
49
119
  }
50
120
 
51
- async function loadData(dataDir) {
121
+ async function loadData(dataDirs) {
52
122
  const collections = {};
53
- if (!existsSync(dataDir)) return collections;
54
- const files = await walkFiles(dataDir);
55
- for (const file of files) {
56
- if (extname(file).toLowerCase() !== ".json") continue;
57
- const name = basename(file, ".json");
58
- try {
59
- const raw = await readFile(file, "utf8");
60
- collections[name] = JSON.parse(raw);
61
- } catch (err) {
62
- console.error(kolor.red(`Failed to load data ${relative(dataDir, file)}: ${err.message}`));
123
+ // Support both string and array input
124
+ const dirs = Array.isArray(dataDirs) ? dataDirs : [dataDirs];
125
+
126
+ for (const dataDir of dirs) {
127
+ if (!existsSync(dataDir)) continue;
128
+ const files = await walkFiles(dataDir);
129
+ for (const file of files) {
130
+ if (extname(file).toLowerCase() !== ".json") continue;
131
+ const name = basename(file, ".json");
132
+ try {
133
+ const raw = await readFile(file, "utf8");
134
+ collections[name] = JSON.parse(raw);
135
+ } catch (err) {
136
+ console.error(kolor.red(`Failed to load data ${relative(dataDir, file)}: ${err.message}`));
137
+ }
63
138
  }
64
139
  }
65
140
  return collections;
@@ -371,6 +446,7 @@ async function build(cwdArg = cwd) {
371
446
  const layoutsDir = join(srcDir, "layouts");
372
447
  const partialsDir = join(srcDir, "partials");
373
448
  const dataDir = join(srcDir, "data");
449
+ const collectionsDir = join(srcDir, "collections");
374
450
  const publicDir = resolve(cwdArg, "public");
375
451
  const outDir = resolve(cwdArg, config.outDir || "dist");
376
452
  const env = createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir);
@@ -391,7 +467,7 @@ async function build(cwdArg = cwd) {
391
467
  console.error(kolor.red(`Failed to apply liquidEnv hook: ${err.message}`));
392
468
  }
393
469
  }
394
- const data = await loadData(dataDir);
470
+ const data = await loadData([dataDir, collectionsDir]);
395
471
 
396
472
  await cleanDir(outDir);
397
473
  await copyPublic(publicDir, outDir);
@@ -514,9 +590,10 @@ async function dev(cwdArg = cwd) {
514
590
  const config = await loadConfig(cwdArg);
515
591
  const srcDir = resolve(cwdArg, config.srcDir || "src");
516
592
  const dataDir = join(srcDir, "data");
593
+ const collectionsDir = join(srcDir, "collections");
517
594
  const publicDir = resolve(cwdArg, "public");
518
595
  const outDir = resolve(cwdArg, config.outDir || "dist");
519
- const watcher = chokidar.watch([srcDir, publicDir, dataDir], { ignoreInitial: true });
596
+ const watcher = chokidar.watch([srcDir, publicDir, dataDir, collectionsDir], { ignoreInitial: true });
520
597
 
521
598
  watcher.on("all", (event, path) => {
522
599
  console.log(kolor.cyan(`↻ ${event}: ${relative(cwdArg, path)}`));
@@ -526,10 +603,683 @@ async function dev(cwdArg = cwd) {
526
603
  serve(outDir);
527
604
  }
528
605
 
606
+ function slugify(text) {
607
+ return text
608
+ .toLowerCase()
609
+ .trim()
610
+ .replace(/[^\w\s-]/g, "")
611
+ .replace(/[\s_-]+/g, "-")
612
+ .replace(/^-+|-+$/g, "");
613
+ }
614
+
615
+ function formatDate(date) {
616
+ return date.toISOString().split("T")[0];
617
+ }
618
+
619
+ async function makeContent(type) {
620
+ // Get all arguments after the command and join them
621
+ const args = argv.slice(3);
622
+
623
+ if (args.length === 0) {
624
+ console.log(kolor.red("āŒ Missing name argument"));
625
+ console.log(kolor.dim(`Usage: campsite make:${type} <name> [name2, name3, ...]`));
626
+ console.log(kolor.dim("\nExamples:"));
627
+ console.log(kolor.dim(" campsite make:page about"));
628
+ console.log(kolor.dim(" campsite make:page home, about, contact"));
629
+ console.log(kolor.dim(" campsite make:collection products, categories\n"));
630
+ exit(1);
631
+ }
632
+
633
+ // Join all args and split by comma to support both formats:
634
+ // campsite make:page home about contact
635
+ // campsite make:page home, about, contact
636
+ const namesString = args.join(" ");
637
+ const names = namesString.split(",").map(n => n.trim()).filter(n => n.length > 0);
638
+
639
+ if (names.length === 0) {
640
+ console.log(kolor.red("āŒ No valid names provided\n"));
641
+ exit(1);
642
+ }
643
+
644
+ console.log(kolor.cyan(`\nšŸ•ļø Creating ${names.length} ${type}(s)...\n`));
645
+
646
+ const config = await loadConfig(cwd);
647
+ const srcDir = resolve(cwd, config.srcDir || "src");
648
+
649
+ // Determine file extension based on template engine
650
+ const engineExtMap = {
651
+ nunjucks: ".njk",
652
+ liquid: ".liquid",
653
+ mustache: ".mustache"
654
+ };
655
+ const defaultExt = engineExtMap[config.templateEngine] || ".njk";
656
+
657
+ let successCount = 0;
658
+ let skipCount = 0;
659
+
660
+ for (const name of names) {
661
+ const result = await createSingleContent(type, name, srcDir, config, defaultExt);
662
+ if (result.success) successCount++;
663
+ if (result.skipped) skipCount++;
664
+ }
665
+
666
+ console.log();
667
+ if (successCount > 0) {
668
+ console.log(kolor.green(`āœ… Created ${successCount} ${type}(s)`));
669
+ }
670
+ if (skipCount > 0) {
671
+ console.log(kolor.yellow(`āš ļø Skipped ${skipCount} existing file(s)`));
672
+ }
673
+ console.log(kolor.dim("\n🌲 Happy camping!\n"));
674
+ }
675
+
676
+ async function createSingleContent(type, name, srcDir, config, defaultExt) {
677
+ // Check if user provided an extension
678
+ const hasExtension = name.includes(".");
679
+ const providedExt = hasExtension ? extname(name) : null;
680
+ const nameWithoutExt = hasExtension ? basename(name, providedExt) : name;
681
+
682
+ const slug = slugify(nameWithoutExt);
683
+ const today = formatDate(new Date());
684
+ const title = nameWithoutExt.charAt(0).toUpperCase() + nameWithoutExt.slice(1);
685
+
686
+ let targetPath;
687
+ let content;
688
+ let fileExt;
689
+
690
+ switch (type.toLowerCase()) {
691
+ case "page": {
692
+ // Priority: provided extension > template engine
693
+ if (providedExt) {
694
+ fileExt = providedExt;
695
+ } else {
696
+ fileExt = defaultExt;
697
+ }
698
+
699
+ targetPath = join(srcDir, "pages", `${slug}${fileExt}`);
700
+
701
+ // Determine if we should use markdown content based on extension
702
+ const useMarkdown = fileExt === ".md";
703
+
704
+ if (useMarkdown) {
705
+ content = `---
706
+ layout: base${defaultExt}
707
+ title: ${title}
708
+ ---
709
+
710
+ # ${title}
711
+
712
+ Your new page content goes here.
713
+ `;
714
+ } else {
715
+ content = `---
716
+ layout: base${defaultExt}
717
+ title: ${title}
718
+ ---
719
+
720
+ <h1>${title}</h1>
721
+ <p>Your new page content goes here.</p>
722
+ `;
723
+ }
724
+ break;
725
+ }
726
+
727
+ case "post": {
728
+ const postsDir = join(srcDir, "pages", "blog");
729
+ await ensureDir(postsDir);
730
+
731
+ if (providedExt) {
732
+ fileExt = providedExt;
733
+ } else {
734
+ fileExt = defaultExt;
735
+ }
736
+
737
+ targetPath = join(postsDir, `${slug}${fileExt}`);
738
+
739
+ const useMarkdown = fileExt === ".md";
740
+
741
+ if (useMarkdown) {
742
+ content = `---
743
+ layout: base${defaultExt}
744
+ title: ${title}
745
+ date: ${today}
746
+ author: Your Name
747
+ ---
748
+
749
+ # ${title}
750
+
751
+ Your blog post content goes here.
752
+ `;
753
+ } else {
754
+ content = `---
755
+ layout: base${defaultExt}
756
+ title: ${title}
757
+ date: ${today}
758
+ author: Your Name
759
+ ---
760
+
761
+ <h1>${title}</h1>
762
+ <p>Your blog post content goes here.</p>
763
+ `;
764
+ }
765
+ break;
766
+ }
767
+
768
+ case "layout": {
769
+ const layoutsDir = join(srcDir, "layouts");
770
+ await ensureDir(layoutsDir);
771
+ targetPath = join(layoutsDir, `${slug}.njk`);
772
+ fileExt = ".njk";
773
+ content = `<!DOCTYPE html>
774
+ <html lang="en">
775
+ <head>
776
+ <meta charset="UTF-8">
777
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
778
+ <title>{{ title or site.name }}</title>
779
+ <link rel="stylesheet" href="/style.css">
780
+ </head>
781
+ <body>
782
+ <main>
783
+ {% block content %}
784
+ {{ content | safe }}
785
+ {% endblock %}
786
+ </main>
787
+ </body>
788
+ </html>
789
+ `;
790
+ break;
791
+ }
792
+
793
+ case "component": {
794
+ const componentsDir = join(srcDir, "components");
795
+ await ensureDir(componentsDir);
796
+ targetPath = join(componentsDir, `${slug}.njk`);
797
+ fileExt = ".njk";
798
+ content = `{# ${title} Component #}
799
+ <div class="${slug}">
800
+ {{ content | safe }}
801
+ </div>
802
+ `;
803
+ break;
804
+ }
805
+
806
+ case "partial": {
807
+ const partialsDir = join(srcDir, "partials");
808
+ await ensureDir(partialsDir);
809
+ targetPath = join(partialsDir, `${slug}.njk`);
810
+ fileExt = ".njk";
811
+ content = `{# ${title} Partial #}
812
+ <div class="${slug}">
813
+ {# Your partial content here #}
814
+ </div>
815
+ `;
816
+ break;
817
+ }
818
+
819
+ case "collection": {
820
+ const collectionsDir = join(srcDir, "collections");
821
+ await ensureDir(collectionsDir);
822
+ targetPath = join(collectionsDir, `${slug}.json`);
823
+ fileExt = ".json";
824
+ content = `[
825
+ {
826
+ "id": 1,
827
+ "title": "Sample ${title} Item",
828
+ "description": "Add your collection items here"
829
+ }
830
+ ]
831
+ `;
832
+ break;
833
+ }
834
+
835
+ default:
836
+ console.log(kolor.red(`āŒ Unknown content type: ${type}`));
837
+ console.log(kolor.dim("\nSupported types: page, post, layout, component, partial, collection\n"));
838
+ return { success: false, skipped: false };
839
+ }
840
+
841
+ if (existsSync(targetPath)) {
842
+ console.log(kolor.dim(` āš ļø Skipped ${relative(cwd, targetPath)} (already exists)`));
843
+ return { success: false, skipped: true };
844
+ }
845
+
846
+ await ensureDir(dirname(targetPath));
847
+ await writeFile(targetPath, content, "utf8");
848
+
849
+ console.log(kolor.dim(` āœ… ${relative(cwd, targetPath)}`));
850
+ return { success: true, skipped: false };
851
+ }
852
+
853
+ async function init() {
854
+ const targetDir = cwd;
855
+ console.log(kolor.cyan(kolor.bold("šŸ•ļø Initializing Campsite in current directory...")));
856
+
857
+ // Check if already initialized
858
+ if (existsSync(join(targetDir, "campsite.config.js"))) {
859
+ console.log(kolor.yellow("āš ļø This directory already has a campsite.config.js file."));
860
+ console.log(kolor.dim("Run 'campsite dev' to start developing.\n"));
861
+ return;
862
+ }
863
+
864
+ // Create basic structure
865
+ const dirs = [
866
+ join(targetDir, "src", "pages"),
867
+ join(targetDir, "src", "layouts"),
868
+ join(targetDir, "public")
869
+ ];
870
+
871
+ for (const dir of dirs) {
872
+ await ensureDir(dir);
873
+ }
874
+
875
+ // Create basic config file
876
+ const configContent = `export default {
877
+ siteName: "My Campsite",
878
+ srcDir: "src",
879
+ outDir: "dist",
880
+ templateEngine: "nunjucks",
881
+ markdown: true,
882
+ integrations: {
883
+ nunjucks: true,
884
+ liquid: false,
885
+ mustache: false,
886
+ vue: false,
887
+ alpine: false
888
+ }
889
+ };
890
+ `;
891
+ await writeFile(join(targetDir, "campsite.config.js"), configContent, "utf8");
892
+
893
+ // Create basic layout
894
+ const layoutContent = `<!DOCTYPE html>
895
+ <html lang="en">
896
+ <head>
897
+ <meta charset="UTF-8">
898
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
899
+ <title>{{ title or site.name }}</title>
900
+ <link rel="stylesheet" href="/style.css">
901
+ </head>
902
+ <body>
903
+ {% block content %}
904
+ {{ content | safe }}
905
+ {% endblock %}
906
+ </body>
907
+ </html>
908
+ `;
909
+ await writeFile(join(targetDir, "src", "layouts", "base.njk"), layoutContent, "utf8");
910
+
911
+ // Create sample page
912
+ const pageContent = `---
913
+ layout: base.njk
914
+ title: Welcome to Campsite
915
+ ---
916
+
917
+ # Welcome to Campsite! šŸ•ļø
918
+
919
+ Your cozy static site is ready to build.
920
+
921
+ ## Get Started
922
+
923
+ - Run \`campsite dev\` to start developing
924
+ - Edit pages in \`src/pages/\`
925
+ - Customize layouts in \`src/layouts/\`
926
+
927
+ Happy camping! 🌲🦊
928
+ `;
929
+ await writeFile(join(targetDir, "src", "pages", "index.md"), pageContent, "utf8");
930
+
931
+ // Create basic CSS
932
+ const cssContent = `* {
933
+ margin: 0;
934
+ padding: 0;
935
+ box-sizing: border-box;
936
+ }
937
+
938
+ body {
939
+ font-family: system-ui, -apple-system, sans-serif;
940
+ line-height: 1.6;
941
+ padding: 2rem;
942
+ max-width: 800px;
943
+ margin: 0 auto;
944
+ }
945
+
946
+ h1 { color: #2d5016; margin-bottom: 1rem; }
947
+ h2 { color: #4a7c2c; margin-top: 1.5rem; }
948
+ `;
949
+ await writeFile(join(targetDir, "public", "style.css"), cssContent, "utf8");
950
+
951
+ // Create .gitignore
952
+ const gitignoreContent = `node_modules/
953
+ dist/
954
+ .DS_Store
955
+ `;
956
+ await writeFile(join(targetDir, ".gitignore"), gitignoreContent, "utf8");
957
+
958
+ // Create package.json
959
+ const packageJson = {
960
+ name: basename(targetDir),
961
+ version: "0.0.1",
962
+ type: "module",
963
+ scripts: {
964
+ dev: "campsite dev",
965
+ build: "campsite build",
966
+ serve: "campsite serve",
967
+ preview: "campsite preview"
968
+ },
969
+ dependencies: {
970
+ basecampjs: "^0.0.8"
971
+ }
972
+ };
973
+ await writeFile(join(targetDir, "package.json"), JSON.stringify(packageJson, null, 2), "utf8");
974
+
975
+ console.log(kolor.green("āœ… Campsite initialized successfully!\n"));
976
+ console.log(kolor.bold("Next steps:"));
977
+ console.log(kolor.dim(" 1. Install dependencies: npm install"));
978
+ console.log(kolor.dim(" 2. Start developing: campsite dev\n"));
979
+ }
980
+
981
+ async function clean() {
982
+ const config = await loadConfig(cwd);
983
+ const outDir = resolve(cwd, config.outDir || "dist");
984
+
985
+ if (!existsSync(outDir)) {
986
+ console.log(kolor.dim(`Nothing to clean. ${outDir} does not exist.`));
987
+ return;
988
+ }
989
+
990
+ console.log(kolor.cyan(`🧹 Cleaning ${relative(cwd, outDir)}...`));
991
+ await rm(outDir, { recursive: true, force: true });
992
+ console.log(kolor.green(`āœ… Cleaned ${relative(cwd, outDir)}\n`));
993
+ }
994
+
995
+ async function check() {
996
+ console.log(kolor.cyan(kolor.bold("šŸ” Checking Campsite project...\n")));
997
+ let hasIssues = false;
998
+
999
+ // Check if campsite.config.js exists
1000
+ const configPath = join(cwd, "campsite.config.js");
1001
+ if (!existsSync(configPath)) {
1002
+ console.log(kolor.red("āŒ campsite.config.js not found"));
1003
+ console.log(kolor.dim(" Run 'campsite init' to initialize a project\n"));
1004
+ hasIssues = true;
1005
+ } else {
1006
+ console.log(kolor.green("āœ… campsite.config.js found"));
1007
+ }
1008
+
1009
+ // Load and validate config
1010
+ const config = await loadConfig(cwd);
1011
+ const srcDir = resolve(cwd, config.srcDir || "src");
1012
+ const pagesDir = join(srcDir, "pages");
1013
+ const layoutsDir = join(srcDir, "layouts");
1014
+ const publicDir = resolve(cwd, "public");
1015
+
1016
+ // Check src directory
1017
+ if (!existsSync(srcDir)) {
1018
+ console.log(kolor.red(`āŒ Source directory not found: ${relative(cwd, srcDir)}`));
1019
+ hasIssues = true;
1020
+ } else {
1021
+ console.log(kolor.green(`āœ… Source directory exists: ${relative(cwd, srcDir)}`));
1022
+ }
1023
+
1024
+ // Check pages directory
1025
+ if (!existsSync(pagesDir)) {
1026
+ console.log(kolor.yellow(`āš ļø Pages directory not found: ${relative(cwd, pagesDir)}`));
1027
+ hasIssues = true;
1028
+ } else {
1029
+ const files = await walkFiles(pagesDir);
1030
+ if (files.length === 0) {
1031
+ console.log(kolor.yellow(`āš ļø No pages found in ${relative(cwd, pagesDir)}`));
1032
+ hasIssues = true;
1033
+ } else {
1034
+ console.log(kolor.green(`āœ… Found ${files.length} page(s) in ${relative(cwd, pagesDir)}`));
1035
+ }
1036
+ }
1037
+
1038
+ // Check layouts directory
1039
+ if (existsSync(layoutsDir)) {
1040
+ const layouts = await readdir(layoutsDir).catch(() => []);
1041
+ console.log(kolor.green(`āœ… Found ${layouts.length} layout(s) in ${relative(cwd, layoutsDir)}`));
1042
+ } else {
1043
+ console.log(kolor.dim(`ā„¹ļø No layouts directory (${relative(cwd, layoutsDir)})`));
1044
+ }
1045
+
1046
+ // Check public directory
1047
+ if (existsSync(publicDir)) {
1048
+ console.log(kolor.green(`āœ… Public directory exists: ${relative(cwd, publicDir)}`));
1049
+ } else {
1050
+ console.log(kolor.dim(`ā„¹ļø No public directory (${relative(cwd, publicDir)})`));
1051
+ }
1052
+
1053
+ // Check for package.json and dependencies
1054
+ const pkgPath = join(cwd, "package.json");
1055
+ if (existsSync(pkgPath)) {
1056
+ try {
1057
+ const pkgRaw = await readFile(pkgPath, "utf8");
1058
+ const pkg = JSON.parse(pkgRaw);
1059
+ if (pkg.dependencies?.basecampjs || pkg.devDependencies?.basecampjs) {
1060
+ console.log(kolor.green("āœ… basecampjs dependency found"));
1061
+ } else {
1062
+ console.log(kolor.yellow("āš ļø basecampjs not listed in dependencies"));
1063
+ console.log(kolor.dim(" Consider adding: npm install basecampjs"));
1064
+ }
1065
+ } catch {
1066
+ console.log(kolor.yellow("āš ļø Could not parse package.json"));
1067
+ }
1068
+ } else {
1069
+ console.log(kolor.dim("ā„¹ļø No package.json found"));
1070
+ }
1071
+
1072
+ console.log();
1073
+ if (hasIssues) {
1074
+ console.log(kolor.yellow("āš ļø Some issues found. Review the messages above."));
1075
+ } else {
1076
+ console.log(kolor.green(kolor.bold("šŸŽ‰ Everything looks good! Ready to build.")));
1077
+ }
1078
+ console.log();
1079
+ }
1080
+
1081
+ async function upgrade() {
1082
+ console.log(kolor.cyan(kolor.bold("ā¬†ļø Checking for CampsiteJS updates...\n")));
1083
+
1084
+ // Check if package.json exists
1085
+ const pkgPath = join(cwd, "package.json");
1086
+ if (!existsSync(pkgPath)) {
1087
+ console.log(kolor.red("āŒ package.json not found"));
1088
+ console.log(kolor.dim("This command should be run in a Campsite project directory.\n"));
1089
+ exit(1);
1090
+ }
1091
+
1092
+ // Read current package.json
1093
+ let pkg;
1094
+ try {
1095
+ const pkgRaw = await readFile(pkgPath, "utf8");
1096
+ pkg = JSON.parse(pkgRaw);
1097
+ } catch {
1098
+ console.log(kolor.red("āŒ Could not read package.json\n"));
1099
+ exit(1);
1100
+ }
1101
+
1102
+ const currentVersion = pkg.dependencies?.basecampjs || pkg.devDependencies?.basecampjs;
1103
+ if (!currentVersion) {
1104
+ console.log(kolor.yellow("āš ļø basecampjs not found in dependencies"));
1105
+ console.log(kolor.dim("Install it with: npm install basecampjs\n"));
1106
+ exit(1);
1107
+ }
1108
+
1109
+ console.log(kolor.dim(`Current version: ${currentVersion}`));
1110
+ console.log(kolor.cyan("\nUpgrading basecampjs to latest version...\n"));
1111
+
1112
+ // Use dynamic import to run npm commands
1113
+ const { spawn } = await import("child_process");
1114
+
1115
+ return new Promise((resolve, reject) => {
1116
+ const child = spawn("npm", ["install", "basecampjs@latest"], {
1117
+ cwd,
1118
+ stdio: "inherit",
1119
+ shell: process.platform === "win32"
1120
+ });
1121
+
1122
+ child.on("close", async (code) => {
1123
+ if (code === 0) {
1124
+ console.log();
1125
+ console.log(kolor.green("āœ… CampsiteJS updated successfully!"));
1126
+
1127
+ // Read updated version
1128
+ try {
1129
+ const updatedPkgRaw = await readFile(pkgPath, "utf8");
1130
+ const updatedPkg = JSON.parse(updatedPkgRaw);
1131
+ const newVersion = updatedPkg.dependencies?.basecampjs || updatedPkg.devDependencies?.basecampjs;
1132
+ console.log(kolor.dim(`New version: ${newVersion}`));
1133
+ } catch {}
1134
+
1135
+ console.log();
1136
+ console.log(kolor.dim("🌲 Tip: Run 'campsite dev' to start developing with the latest version\n"));
1137
+ resolve();
1138
+ } else {
1139
+ console.log();
1140
+ console.log(kolor.red(`āŒ Update failed with code ${code}\n`));
1141
+ reject(new Error(`npm install failed with code ${code}`));
1142
+ }
1143
+ });
1144
+
1145
+ child.on("error", (err) => {
1146
+ console.log(kolor.red(`āŒ Update failed: ${err.message}\n`));
1147
+ reject(err);
1148
+ });
1149
+ });
1150
+ }
1151
+
1152
+ async function list() {
1153
+ console.log(kolor.cyan(kolor.bold("šŸ—ŗļø Listing Campsite content...\n")));
1154
+
1155
+ const config = await loadConfig(cwd);
1156
+ const srcDir = resolve(cwd, config.srcDir || "src");
1157
+ const pagesDir = join(srcDir, "pages");
1158
+ const layoutsDir = join(srcDir, "layouts");
1159
+ const componentsDir = join(srcDir, "components");
1160
+ const partialsDir = join(srcDir, "partials");
1161
+ const collectionsDir = join(srcDir, "collections");
1162
+ const dataDir = join(srcDir, "data");
1163
+
1164
+ // List pages
1165
+ if (existsSync(pagesDir)) {
1166
+ const pages = await walkFiles(pagesDir);
1167
+ if (pages.length > 0) {
1168
+ console.log(kolor.bold("šŸ“„ Pages (") + kolor.cyan(pages.length.toString()) + kolor.bold(")"));
1169
+ pages.forEach(page => {
1170
+ const rel = relative(pagesDir, page);
1171
+ console.log(" " + kolor.dim("• ") + rel);
1172
+ });
1173
+ console.log();
1174
+ }
1175
+ }
1176
+
1177
+ // List layouts
1178
+ if (existsSync(layoutsDir)) {
1179
+ const layouts = await readdir(layoutsDir).catch(() => []);
1180
+ if (layouts.length > 0) {
1181
+ console.log(kolor.bold("šŸ“ Layouts (") + kolor.cyan(layouts.length.toString()) + kolor.bold(")"));
1182
+ layouts.forEach(layout => {
1183
+ console.log(" " + kolor.dim("• ") + layout);
1184
+ });
1185
+ console.log();
1186
+ }
1187
+ }
1188
+
1189
+ // List components
1190
+ if (existsSync(componentsDir)) {
1191
+ const components = await readdir(componentsDir).catch(() => []);
1192
+ if (components.length > 0) {
1193
+ console.log(kolor.bold("🧩 Components (") + kolor.cyan(components.length.toString()) + kolor.bold(")"));
1194
+ components.forEach(component => {
1195
+ console.log(" " + kolor.dim("• ") + component);
1196
+ });
1197
+ console.log();
1198
+ }
1199
+ }
1200
+
1201
+ // List partials
1202
+ if (existsSync(partialsDir)) {
1203
+ const partials = await readdir(partialsDir).catch(() => []);
1204
+ if (partials.length > 0) {
1205
+ console.log(kolor.bold("🧰 Partials (") + kolor.cyan(partials.length.toString()) + kolor.bold(")"));
1206
+ partials.forEach(partial => {
1207
+ console.log(" " + kolor.dim("• ") + partial);
1208
+ });
1209
+ console.log();
1210
+ }
1211
+ }
1212
+
1213
+ // List collections
1214
+ if (existsSync(collectionsDir)) {
1215
+ const collections = await readdir(collectionsDir).catch(() => []);
1216
+ const jsonFiles = collections.filter(f => f.endsWith(".json"));
1217
+ if (jsonFiles.length > 0) {
1218
+ console.log(kolor.bold("šŸ“ Collections (") + kolor.cyan(jsonFiles.length.toString()) + kolor.bold(")"));
1219
+ jsonFiles.forEach(collection => {
1220
+ console.log(" " + kolor.dim("• ") + collection);
1221
+ });
1222
+ console.log();
1223
+ }
1224
+ }
1225
+
1226
+ // List data files
1227
+ if (existsSync(dataDir)) {
1228
+ const dataFiles = await readdir(dataDir).catch(() => []);
1229
+ const jsonFiles = dataFiles.filter(f => f.endsWith(".json"));
1230
+ if (jsonFiles.length > 0) {
1231
+ console.log(kolor.bold("šŸ“Š Data (") + kolor.cyan(jsonFiles.length.toString()) + kolor.bold(")"));
1232
+ jsonFiles.forEach(dataFile => {
1233
+ console.log(" " + kolor.dim("• ") + dataFile);
1234
+ });
1235
+ console.log();
1236
+ }
1237
+ }
1238
+
1239
+ console.log(kolor.dim("🌲 Tip: Use 'campsite make:<type> <name>' to create new content\n"));
1240
+ }
1241
+
1242
+ async function preview() {
1243
+ console.log(kolor.cyan(kolor.bold("šŸ”ļø Building for production preview...\n")));
1244
+ await build();
1245
+ console.log();
1246
+ const config = await loadConfig(cwd);
1247
+ const outDir = resolve(cwd, config.outDir || "dist");
1248
+ console.log(kolor.cyan(kolor.bold("šŸ”„ Starting preview server...\n")));
1249
+ serve(outDir);
1250
+ }
1251
+
529
1252
  async function main() {
530
1253
  const command = argv[2] || "help";
531
1254
 
1255
+ // Handle flags
1256
+ if (command === "-h" || command === "--help" || command === "help") {
1257
+ showHelp();
1258
+ exit(0);
1259
+ }
1260
+
1261
+ if (command === "-v" || command === "--version") {
1262
+ const version = await getVersion();
1263
+ console.log(`v${version}`);
1264
+ exit(0);
1265
+ }
1266
+
1267
+ // Handle make:type commands
1268
+ if (command.startsWith("make:")) {
1269
+ const type = command.substring(5); // Remove 'make:' prefix
1270
+ if (!type) {
1271
+ console.log(kolor.red("āŒ No type specified"));
1272
+ console.log(kolor.dim("Run 'campsite --help' for available make commands.\n"));
1273
+ exit(1);
1274
+ }
1275
+ await makeContent(type);
1276
+ return;
1277
+ }
1278
+
532
1279
  switch (command) {
1280
+ case "init":
1281
+ await init();
1282
+ break;
533
1283
  case "dev":
534
1284
  await dev();
535
1285
  break;
@@ -545,9 +1295,25 @@ async function main() {
545
1295
  serve(outDir);
546
1296
  break;
547
1297
  }
1298
+ case "preview":
1299
+ await preview();
1300
+ break;
1301
+ case "clean":
1302
+ await clean();
1303
+ break;
1304
+ case "check":
1305
+ await check();
1306
+ break;
1307
+ case "list":
1308
+ await list();
1309
+ break;
1310
+ case "upgrade":
1311
+ await upgrade();
1312
+ break;
548
1313
  default:
549
- console.log("campsite commands: dev | build | serve");
550
- exit(0);
1314
+ console.log(kolor.yellow(`Unknown command: ${command}`));
1315
+ console.log(kolor.dim("Run 'campsite --help' for usage information."));
1316
+ exit(1);
551
1317
  }
552
1318
  }
553
1319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basecampjs",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "BasecampJS engine for CampsiteJS static site generator.",
6
6
  "bin": {