@squadbase/vantage-template 0.0.1 → 0.0.2-dev.7c29258

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 (46) hide show
  1. package/README.md +14 -6
  2. package/base-template/.squadbase/skills/project-storage/SKILL.md +157 -0
  3. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +15 -6
  4. package/base-template/.squadbase/skills/vantage-app/SKILL.md +12 -7
  5. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +22 -0
  6. package/base-template/AGENTS.md +186 -7
  7. package/base-template/package.json +4 -3
  8. package/base-template/src/_error.tsx +10 -0
  9. package/base-template/{_layout.tsx → src/_layout.tsx} +8 -1
  10. package/base-template/src/components/build-recovery.tsx +219 -0
  11. package/base-template/{styles.css → src/styles.css} +19 -0
  12. package/dist/index.js +149 -61
  13. package/package.json +18 -9
  14. package/ui-templates/CLAUDE.md +32 -21
  15. package/ui-templates/blank/preview-square.png +0 -0
  16. package/ui-templates/blank/preview-wide.png +0 -0
  17. package/ui-templates/blank-ja/preview-square.png +0 -0
  18. package/ui-templates/blank-ja/preview-wide.png +0 -0
  19. package/ui-templates/chart-grid/preview-square.png +0 -0
  20. package/ui-templates/chart-grid/preview-wide.png +0 -0
  21. package/ui-templates/chart-grid-ja/preview-square.png +0 -0
  22. package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
  23. package/ui-templates/funnel/preview-square.png +0 -0
  24. package/ui-templates/funnel/preview-wide.png +0 -0
  25. package/ui-templates/funnel-ja/preview-square.png +0 -0
  26. package/ui-templates/funnel-ja/preview-wide.png +0 -0
  27. package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
  28. package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
  29. package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
  30. package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
  31. package/ui-templates/kpi-chart-simple/manifest.json +1 -1
  32. package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
  33. package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
  34. package/ui-templates/kpi-chart-simple/server/kpi-summary.ts +1 -1
  35. package/ui-templates/kpi-chart-simple-ja/manifest.json +1 -1
  36. package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
  37. package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
  38. package/ui-templates/kpi-chart-simple-ja/server/kpi-summary.ts +1 -1
  39. package/ui-templates/table-focus/preview-square.png +0 -0
  40. package/ui-templates/table-focus/preview-wide.png +0 -0
  41. package/ui-templates/table-focus-ja/preview-square.png +0 -0
  42. package/ui-templates/table-focus-ja/preview-wide.png +0 -0
  43. package/base-template/_error.tsx +0 -14
  44. /package/base-template/{_404.tsx → src/_404.tsx} +0 -0
  45. /package/base-template/{components → src/components}/user-card.tsx +0 -0
  46. /package/base-template/{index.tsx → src/index.tsx} +0 -0
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import { parseArgs } from "util";
5
5
 
6
6
  // src/commands/add.ts
7
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
8
- import { join as join4 } from "path";
7
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
8
+ import { join as join5 } from "path";
9
9
 
10
10
  // src/ai/customize.ts
11
11
  import { readFileSync, writeFileSync } from "fs";
12
- import { join } from "path";
12
+ import { join as join2 } from "path";
13
13
 
14
14
  // src/logger.ts
15
15
  var colors = {
@@ -24,6 +24,35 @@ function log(color, message) {
24
24
  console.log(`${colors[color]}${message}${colors.reset}`);
25
25
  }
26
26
 
27
+ // src/project.ts
28
+ import { existsSync } from "fs";
29
+ import { join } from "path";
30
+ var ROOT_LAYOUT_MARKERS = ["index.tsx", "_layout.tsx", "_404.tsx", "_error.tsx", "styles.css"];
31
+ var AUTHORED_PAGE_PREFIX = "src/";
32
+ function resolvePagePrefix(projectRoot) {
33
+ if (existsSync(join(projectRoot, "src"))) return AUTHORED_PAGE_PREFIX;
34
+ return findRootLayoutFiles(projectRoot).length > 0 ? "" : AUTHORED_PAGE_PREFIX;
35
+ }
36
+ function resolvePageRoot(projectRoot) {
37
+ return join(projectRoot, resolvePagePrefix(projectRoot));
38
+ }
39
+ function resolveStylesPath(projectRoot) {
40
+ return join(resolvePageRoot(projectRoot), "styles.css");
41
+ }
42
+ function resolveDest(entry, pagePrefix) {
43
+ return entry.scope === "root" ? entry.dest : pagePrefix + entry.dest;
44
+ }
45
+ function authoredDest(entry) {
46
+ return resolveDest(entry, AUTHORED_PAGE_PREFIX);
47
+ }
48
+ function findRootLayoutFiles(projectRoot) {
49
+ return ROOT_LAYOUT_MARKERS.filter((name) => existsSync(join(projectRoot, name)));
50
+ }
51
+ function findSplitLayoutFiles(projectRoot) {
52
+ if (!existsSync(join(projectRoot, "src"))) return [];
53
+ return findRootLayoutFiles(projectRoot);
54
+ }
55
+
27
56
  // src/ai/diff.ts
28
57
  var ANSI = {
29
58
  reset: "\x1B[0m",
@@ -300,12 +329,13 @@ function buildUserPrompt(intent, files) {
300
329
  parts.push(`</FILES_TO_CUSTOMIZE>`);
301
330
  return parts.join("\n");
302
331
  }
303
- function readInputFiles(projectRoot, templateDir, entries, fromTemplate) {
332
+ function readInputFiles(projectRoot, templateDir, entries, pagePrefix, fromTemplate) {
304
333
  const files = /* @__PURE__ */ new Map();
305
334
  for (const entry of entries) {
306
- const path = fromTemplate ? join(templateDir, entry.src) : join(projectRoot, entry.dest);
335
+ const dest = resolveDest(entry, pagePrefix);
336
+ const path = fromTemplate ? join2(templateDir, entry.src) : join2(projectRoot, dest);
307
337
  try {
308
- files.set(entry.dest, readFileSync(path, "utf-8"));
338
+ files.set(dest, readFileSync(path, "utf-8"));
309
339
  } catch (err) {
310
340
  throw new Error(
311
341
  `Failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -400,13 +430,14 @@ ${snippet}`);
400
430
  if (e.usage) parts.push(`Usage: ${JSON.stringify(e.usage)}`);
401
431
  return parts.join("\n");
402
432
  }
403
- async function customizeWithAI(projectRoot, manifest, templateDir, options) {
433
+ async function customizeWithAI(projectRoot, manifest, templateDir, options, pagePrefix) {
404
434
  const relabelFiles = manifest.files.filter((f) => f.relabel !== false);
405
- const allowedPaths = relabelFiles.map((f) => f.dest);
435
+ const allowedPaths = relabelFiles.map((f) => resolveDest(f, pagePrefix));
406
436
  const inputFiles = readInputFiles(
407
437
  projectRoot,
408
438
  templateDir,
409
439
  relabelFiles,
440
+ pagePrefix,
410
441
  options.dryRun
411
442
  );
412
443
  if (!options.json) {
@@ -515,7 +546,7 @@ ${edit.path} (+${edit.added} -${edit.removed})`);
515
546
  }
516
547
  } else {
517
548
  for (const edit of edits) {
518
- writeFileSync(join(projectRoot, edit.path), edit.after, "utf-8");
549
+ writeFileSync(join2(projectRoot, edit.path), edit.after, "utf-8");
519
550
  if (!options.json) {
520
551
  log("cyan", ` rewrote: ${edit.path} (+${edit.added} -${edit.removed})`);
521
552
  }
@@ -584,34 +615,34 @@ function parseEditsString(raw) {
584
615
  }
585
616
 
586
617
  // src/apply.ts
587
- import { copyFileSync, existsSync as existsSync2, mkdirSync } from "fs";
588
- import { dirname as dirname2, join as join3 } from "path";
618
+ import { copyFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
619
+ import { dirname as dirname2, join as join4, posix } from "path";
589
620
 
590
621
  // src/manifest.ts
591
- import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
592
- import { dirname, join as join2 } from "path";
622
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
623
+ import { dirname, join as join3 } from "path";
593
624
  import { fileURLToPath } from "url";
594
625
  var __dirname = dirname(fileURLToPath(import.meta.url));
595
626
  function getTemplatesDir(source = "ui-templates") {
596
- return join2(__dirname, "..", source);
627
+ return join3(__dirname, "..", source);
597
628
  }
598
629
  function getTemplateDir(templateName, source = "ui-templates") {
599
- return join2(getTemplatesDir(source), templateName);
630
+ return join3(getTemplatesDir(source), templateName);
600
631
  }
601
632
  function listTemplateNames(source = "ui-templates") {
602
633
  const templatesDir = getTemplatesDir(source);
603
- if (!existsSync(templatesDir)) {
634
+ if (!existsSync2(templatesDir)) {
604
635
  return [];
605
636
  }
606
637
  return readdirSync(templatesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
607
638
  }
608
639
  function loadManifest(templateName, source = "ui-templates") {
609
640
  const templateDir = getTemplateDir(templateName, source);
610
- if (!existsSync(templateDir)) {
641
+ if (!existsSync2(templateDir)) {
611
642
  throw new Error(`Template "${templateName}" not found.`);
612
643
  }
613
- const manifestPath = join2(templateDir, "manifest.json");
614
- if (!existsSync(manifestPath)) {
644
+ const manifestPath = join3(templateDir, "manifest.json");
645
+ if (!existsSync2(manifestPath)) {
615
646
  throw new Error(`Template "${templateName}" is missing manifest.json.`);
616
647
  }
617
648
  const raw = JSON.parse(readFileSync2(manifestPath, "utf-8"));
@@ -624,7 +655,8 @@ function loadManifest(templateName, source = "ui-templates") {
624
655
  // src/apply.ts
625
656
  function applyTemplate(projectRoot, manifest, options) {
626
657
  const templateDir = getTemplateDir(manifest.name, options.source ?? "ui-templates");
627
- const conflicts = checkConflicts(projectRoot, manifest.files, options.force);
658
+ const pagePrefix = resolvePagePrefix(projectRoot);
659
+ const conflicts = checkConflicts(projectRoot, manifest.files, pagePrefix, options.force);
628
660
  if (conflicts.length > 0) {
629
661
  log("red", "File conflicts detected (use --force to overwrite):");
630
662
  for (const c of conflicts) {
@@ -633,36 +665,77 @@ function applyTemplate(projectRoot, manifest, options) {
633
665
  process.exit(1);
634
666
  }
635
667
  for (const file of manifest.files) {
636
- const src = join3(templateDir, file.src);
637
- const dest = join3(projectRoot, file.dest);
668
+ const src = join4(templateDir, file.src);
669
+ const destRel = resolveDest(file, pagePrefix);
670
+ const dest = join4(projectRoot, destRel);
638
671
  if (options.dryRun) {
639
672
  const label2 = file.action === "replace" ? "replace" : "add";
640
- log("dim", ` [dry-run] ${label2}: ${file.dest}`);
673
+ log("dim", ` [dry-run] ${label2}: ${destRel}`);
641
674
  continue;
642
675
  }
643
676
  mkdirSync(dirname2(dest), { recursive: true });
644
- copyFileSync(src, dest);
677
+ const authored = isRewritable(file.src) ? readFileSync3(src, "utf-8") : null;
678
+ const retargeted = authored === null ? null : retargetRelativeImports(authored, file, pagePrefix);
679
+ if (retargeted === null) {
680
+ copyFileSync(src, dest);
681
+ } else {
682
+ writeFileSync2(dest, retargeted, "utf-8");
683
+ }
645
684
  const label = file.action === "replace" ? "replaced" : "added";
646
- log("cyan", ` ${label}: ${file.dest}`);
685
+ const note = retargeted !== null && retargeted !== authored ? " (imports retargeted)" : "";
686
+ log("cyan", ` ${label}: ${destRel}${note}`);
647
687
  }
648
688
  }
649
- function checkConflicts(projectRoot, files, force) {
689
+ function checkConflicts(projectRoot, files, pagePrefix, force) {
650
690
  if (force) return [];
651
- return files.filter((f) => f.action === "add" && existsSync2(join3(projectRoot, f.dest))).map((f) => f.dest);
691
+ return files.filter((f) => f.action === "add").map((f) => resolveDest(f, pagePrefix)).filter((dest) => existsSync3(join4(projectRoot, dest)));
692
+ }
693
+ var REWRITABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs"];
694
+ function isRewritable(srcPath) {
695
+ return REWRITABLE_EXTENSIONS.some((ext) => srcPath.endsWith(ext));
696
+ }
697
+ var RELATIVE_SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\s+)(["'])(\.{1,2}\/[^"']*)\2/g;
698
+ function retargetRelativeImports(source, entry, pagePrefix) {
699
+ if (pagePrefix === AUTHORED_PAGE_PREFIX) return source;
700
+ const fromAuthored = posix.dirname(authoredDest(entry));
701
+ const fromActual = posix.dirname(resolveDest(entry, pagePrefix));
702
+ return source.replace(RELATIVE_SPECIFIER, (match, head, quote, spec) => {
703
+ const authoredTarget = posix.normalize(posix.join(fromAuthored, spec));
704
+ const actualTarget = retargetPath(authoredTarget, pagePrefix);
705
+ if (actualTarget === authoredTarget && fromActual === fromAuthored) return match;
706
+ let next = posix.relative(fromActual, actualTarget);
707
+ if (!next.startsWith(".")) next = `./${next}`;
708
+ return `${head}${quote}${next}${quote}`;
709
+ });
710
+ }
711
+ function retargetPath(authoredPath, pagePrefix) {
712
+ if (!authoredPath.startsWith(AUTHORED_PAGE_PREFIX)) return authoredPath;
713
+ return pagePrefix + authoredPath.slice(AUTHORED_PAGE_PREFIX.length);
652
714
  }
653
715
 
654
716
  // src/commands/add.ts
655
717
  function isVantageProject(projectRoot) {
656
- const pkgPath = join4(projectRoot, "package.json");
657
- if (!existsSync3(pkgPath)) return false;
718
+ const pkgPath = join5(projectRoot, "package.json");
719
+ if (!existsSync4(pkgPath)) return false;
658
720
  try {
659
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
721
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
660
722
  return Boolean(pkg.dependencies?.["@squadbase/vantage"] ?? pkg.devDependencies?.["@squadbase/vantage"]);
661
723
  } catch {
662
724
  return false;
663
725
  }
664
726
  }
665
- function findStaleTemplateFiles(projectRoot, appliedTemplate, source) {
727
+ function refuseSplitLayout(projectRoot) {
728
+ const rootPages = findSplitLayoutFiles(projectRoot);
729
+ if (rootPages.length === 0) return;
730
+ log("red", "This project has pages both in src/ and at the project root.");
731
+ log("yellow", " Vantage scans only src/ once it exists. Move these into src/, then re-run:");
732
+ for (const name of rootPages) log("dim", ` ${name}`);
733
+ log("dim", " Any components/ hooks/ lib/ directories holding page code move with them.");
734
+ log("dim", " server/ and public/ stay at the project root.");
735
+ log("dim", " Leaving both sides in place fails `vantage check` with SRC_DIR_SPLIT.");
736
+ process.exit(1);
737
+ }
738
+ function findStaleTemplateFiles(projectRoot, appliedTemplate, source, pagePrefix) {
666
739
  const stale = [];
667
740
  for (const name of listTemplateNames(source)) {
668
741
  if (name === appliedTemplate) continue;
@@ -675,7 +748,8 @@ function findStaleTemplateFiles(projectRoot, appliedTemplate, source) {
675
748
  }
676
749
  for (const file of manifest.files) {
677
750
  if (file.action !== "add") continue;
678
- if (existsSync3(join4(projectRoot, file.dest))) stale.push(file.dest);
751
+ const dest = resolveDest(file, pagePrefix);
752
+ if (existsSync4(join5(projectRoot, dest))) stale.push(dest);
679
753
  }
680
754
  }
681
755
  return [...new Set(stale)].sort();
@@ -688,6 +762,8 @@ async function addTemplate(templateName, options) {
688
762
  log("dim", "Run 'npx @squadbase/vantage-template init' first.");
689
763
  process.exit(1);
690
764
  }
765
+ refuseSplitLayout(projectRoot);
766
+ const pagePrefix = resolvePagePrefix(projectRoot);
691
767
  const available = listTemplateNames(source);
692
768
  if (!available.includes(templateName)) {
693
769
  log("red", `Template "${templateName}" not found in ${source}/.`);
@@ -704,13 +780,14 @@ async function addTemplate(templateName, options) {
704
780
  log("yellow", "(dry run \u2014 no files will be changed)");
705
781
  }
706
782
  const entry = manifest.files.find((f) => f.action === "replace");
707
- if (entry && existsSync3(join4(projectRoot, entry.dest))) {
708
- log("yellow", ` ${entry.dest} will be overwritten \u2014 commit first if it holds your work.`);
783
+ const entryDest = entry ? resolveDest(entry, pagePrefix) : void 0;
784
+ if (entryDest && existsSync4(join5(projectRoot, entryDest))) {
785
+ log("yellow", ` ${entryDest} will be overwritten \u2014 commit first if it holds your work.`);
709
786
  }
710
787
  }
711
788
  applyTemplate(projectRoot, manifest, options);
712
789
  if (!options.ai?.json) {
713
- const stale = findStaleTemplateFiles(projectRoot, templateName, source);
790
+ const stale = findStaleTemplateFiles(projectRoot, templateName, source, pagePrefix);
714
791
  if (stale.length > 0) {
715
792
  log("yellow", "\nLeftovers from a previously applied template (nothing imports them):");
716
793
  for (const dest of stale) log("dim", ` ${dest}`);
@@ -718,7 +795,13 @@ async function addTemplate(templateName, options) {
718
795
  }
719
796
  }
720
797
  if (options.ai) {
721
- const result = await customizeWithAI(projectRoot, manifest, templateDir, options.ai);
798
+ const result = await customizeWithAI(
799
+ projectRoot,
800
+ manifest,
801
+ templateDir,
802
+ options.ai,
803
+ pagePrefix
804
+ );
722
805
  if (options.ai.json) {
723
806
  console.log(
724
807
  JSON.stringify(
@@ -777,12 +860,12 @@ Run "git diff" to review the AI changes.`);
777
860
  }
778
861
 
779
862
  // src/commands/chart.ts
780
- import { existsSync as existsSync5 } from "fs";
781
- import { join as join6 } from "path";
863
+ import { existsSync as existsSync6 } from "fs";
864
+ import { relative as relative2 } from "path";
782
865
 
783
866
  // src/chart-presets.ts
784
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
785
- import { dirname as dirname3, join as join5 } from "path";
867
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
868
+ import { dirname as dirname3, join as join6, relative } from "path";
786
869
  import { fileURLToPath as fileURLToPath2 } from "url";
787
870
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
788
871
  var PRESET_DESCRIPTIONS = {
@@ -796,34 +879,34 @@ var PRESET_DESCRIPTIONS = {
796
879
  var BEGIN_MARKER = "/* vantage-template:chart-preset \u2014 regenerated by `chart`; edits here are lost */";
797
880
  var END_MARKER = "/* vantage-template:chart-preset:end */";
798
881
  function getChartPresetsDir() {
799
- return join5(__dirname2, "..", "chart-presets");
882
+ return join6(__dirname2, "..", "chart-presets");
800
883
  }
801
884
  function listChartPresetNames() {
802
885
  const dir = getChartPresetsDir();
803
- if (!existsSync4(dir)) return [];
886
+ if (!existsSync5(dir)) return [];
804
887
  return readdirSync2(dir).filter((f) => f.endsWith(".css")).map((f) => f.replace(/\.css$/, "")).sort();
805
888
  }
806
889
  function getChartPresetDescription(name) {
807
890
  return PRESET_DESCRIPTIONS[name] ?? "";
808
891
  }
809
892
  function getChartPresetPath(name) {
810
- return join5(getChartPresetsDir(), `${name}.css`);
893
+ return join6(getChartPresetsDir(), `${name}.css`);
811
894
  }
812
895
  function applyChartPreset(projectRoot, presetName, options = {}) {
813
896
  const presetPath = getChartPresetPath(presetName);
814
- if (!existsSync4(presetPath)) {
897
+ if (!existsSync5(presetPath)) {
815
898
  throw new Error(`Chart preset "${presetName}" not found.`);
816
899
  }
817
- const targetPath = join5(projectRoot, "styles.css");
818
- if (!existsSync4(targetPath)) {
819
- throw new Error("styles.css not found in project.");
900
+ const targetPath = resolveStylesPath(projectRoot);
901
+ if (!existsSync5(targetPath)) {
902
+ throw new Error(`${relative(projectRoot, targetPath)} not found in project.`);
820
903
  }
821
- const preset = readFileSync4(presetPath, "utf-8").trimEnd();
904
+ const preset = readFileSync5(presetPath, "utf-8").trimEnd();
822
905
  const block = `${BEGIN_MARKER}
823
906
  ${preset}
824
907
  ${END_MARKER}
825
908
  `;
826
- const current = readFileSync4(targetPath, "utf-8");
909
+ const current = readFileSync5(targetPath, "utf-8");
827
910
  const begin = current.indexOf(BEGIN_MARKER);
828
911
  const end = current.indexOf(END_MARKER);
829
912
  let content;
@@ -835,7 +918,7 @@ ${END_MARKER}
835
918
  ${block}`;
836
919
  }
837
920
  if (!options.dryRun) {
838
- writeFileSync2(targetPath, content, "utf-8");
921
+ writeFileSync3(targetPath, content, "utf-8");
839
922
  }
840
923
  return { targetPath, content };
841
924
  }
@@ -843,9 +926,10 @@ ${block}`;
843
926
  // src/commands/chart.ts
844
927
  function setChartPreset(presetName, options) {
845
928
  const projectRoot = process.cwd();
846
- const stylesPath = join6(projectRoot, "styles.css");
847
- if (!existsSync5(stylesPath)) {
848
- log("red", "styles.css not found. Are you in a Squadbase Vantage project?");
929
+ const stylesPath = resolveStylesPath(projectRoot);
930
+ const stylesLabel = relative2(projectRoot, stylesPath);
931
+ if (!existsSync6(stylesPath)) {
932
+ log("red", `${stylesLabel} not found. Are you in a Squadbase Vantage project?`);
849
933
  process.exit(1);
850
934
  }
851
935
  const available = listChartPresetNames();
@@ -864,18 +948,19 @@ function setChartPreset(presetName, options) {
864
948
  if (options.dryRun) {
865
949
  log("yellow", "(dry run \u2014 no files will be changed)");
866
950
  const { content } = applyChartPreset(projectRoot, presetName, { dryRun: true });
867
- log("dim", "\n--- styles.css (preview) ---");
951
+ log("dim", `
952
+ --- ${stylesLabel} (preview) ---`);
868
953
  console.log(content);
869
954
  return;
870
955
  }
871
956
  applyChartPreset(projectRoot, presetName);
872
- log("cyan", " updated: styles.css");
957
+ log("cyan", ` updated: ${stylesLabel}`);
873
958
  log("green", "\nChart preset applied successfully!");
874
959
  }
875
960
 
876
961
  // src/commands/init.ts
877
962
  import { spawnSync } from "child_process";
878
- import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3 } from "fs";
963
+ import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3 } from "fs";
879
964
  import { dirname as dirname4, join as join7 } from "path";
880
965
  import { fileURLToPath as fileURLToPath3 } from "url";
881
966
  var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
@@ -888,7 +973,7 @@ function getBaseTemplateDir() {
888
973
  function initProject(options) {
889
974
  const targetDir = process.cwd();
890
975
  const baseTemplateDir = getBaseTemplateDir();
891
- if (!existsSync6(baseTemplateDir)) {
976
+ if (!existsSync7(baseTemplateDir)) {
892
977
  log("red", "Base template not found. The package may be corrupted.");
893
978
  process.exit(1);
894
979
  }
@@ -902,13 +987,16 @@ function initProject(options) {
902
987
  process.exit(1);
903
988
  }
904
989
  }
905
- if (!options.force && existsSync6(join7(targetDir, "squadbase.yml"))) {
990
+ if (!options.force && existsSync7(join7(targetDir, "squadbase.yml"))) {
906
991
  log("red", "This directory already contains a Squadbase project (squadbase.yml exists).");
907
992
  log("yellow", "Use --force to overwrite.");
908
993
  process.exit(1);
909
994
  }
910
- if (!options.force && existsSync6(join7(targetDir, "index.tsx"))) {
911
- log("red", "This directory already contains an index.tsx.");
995
+ const existingEntry = ["src/index.tsx", "index.tsx"].find(
996
+ (rel) => existsSync7(join7(targetDir, rel))
997
+ );
998
+ if (!options.force && existingEntry) {
999
+ log("red", `This directory already contains a ${existingEntry}.`);
912
1000
  log("yellow", "Use --force to overwrite.");
913
1001
  process.exit(1);
914
1002
  }
@@ -922,7 +1010,7 @@ function initProject(options) {
922
1010
  mkdirSync2(dest, { recursive: true });
923
1011
  cpSync(src, dest, { recursive: true, force: options.force });
924
1012
  } else {
925
- if (!options.force && existsSync6(dest)) {
1013
+ if (!options.force && existsSync7(dest)) {
926
1014
  log("yellow", ` skipped (exists): ${destName}`);
927
1015
  continue;
928
1016
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squadbase/vantage-template",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-dev.7c29258",
4
4
  "description": "CLI tool for initializing and customizing Squadbase Vantage projects.",
5
5
  "keywords": [
6
6
  "squadbase",
@@ -12,12 +12,12 @@
12
12
  ],
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/squadbase/squadbase-templates.git",
16
- "directory": "vantage-template"
15
+ "url": "git+https://github.com/squadbase/squadbase-monorepo.git",
16
+ "directory": "packages/vantage-template"
17
17
  },
18
- "homepage": "https://github.com/squadbase/squadbase-templates/tree/main/vantage-template#readme",
18
+ "homepage": "https://github.com/squadbase/squadbase-monorepo/tree/main/packages/vantage-template#readme",
19
19
  "bugs": {
20
- "url": "https://github.com/squadbase/squadbase-templates/issues"
20
+ "url": "https://github.com/squadbase/squadbase-monorepo/issues"
21
21
  },
22
22
  "type": "module",
23
23
  "engines": {
@@ -33,16 +33,25 @@
33
33
  "chart-presets"
34
34
  ],
35
35
  "publishConfig": {
36
- "access": "public"
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org"
37
38
  },
38
39
  "scripts": {
39
- "sync-base": "rsync -a --delete --exclude='node_modules' --exclude='dist' --exclude='.vantage' --exclude='package-lock.json' --exclude='*.tsbuildinfo' --exclude='*.log' ../vantage/ base-template/ && mv base-template/.gitignore base-template/_gitignore",
40
+ "sync-base": "rsync -a --delete --exclude='node_modules' --exclude='dist' --exclude='.vantage' --exclude='package-lock.json' --exclude='*.tsbuildinfo' --exclude='*.log' template/ base-template/ && mv base-template/.gitignore base-template/_gitignore",
40
41
  "build": "npm run sync-base && tsup",
41
- "prepublishOnly": "npm run build",
42
- "release": "npm publish --@squadbase:registry=https://registry.npmjs.org"
42
+ "type-check": "tsc",
43
+ "agents": "node scripts/build-agents.cjs",
44
+ "agents:check": "node scripts/build-agents.cjs --check",
45
+ "template:install": "npm --prefix template ci",
46
+ "template:build": "npm --prefix template run build",
47
+ "prepublishOnly": "npm run agents:check && npm run build",
48
+ "release": "npm publish --@squadbase:registry=https://registry.npmjs.org",
49
+ "dev": "node scripts/dev.mjs",
50
+ "screenshot": "node scripts/screenshot.mjs"
43
51
  },
44
52
  "devDependencies": {
45
53
  "@types/node": "^22.15.0",
54
+ "playwright": "^1.49.0",
46
55
  "tsup": "^8.4.0",
47
56
  "typescript": "~5.7.0"
48
57
  },
@@ -6,7 +6,7 @@ UIパターン別テンプレート(KPI重視・チャート格子・テーブ
6
6
 
7
7
  このファイルは `vantage-template/ui-templates/` 配下のファイル構成ルールを定義する。デザイン要件・AI relabel・EN/JA・CLI の挙動は親 [`../CLAUDE.md`](../CLAUDE.md) に従う。
8
8
 
9
- > **Vite 版との差分**: 適用先が Vantage アプリ(`@squadbase/vantage`)になったため、(1) エントリは `src/pages/home.tsx` ではなく **ルートの `index.tsx`**、(2) テンプレ固有ファイルは `src/templates/<slug>/` ではなく **`components/<slug>/`**、(3) 全ての building block を `@squadbase/vantage/*` から import する(ローカルコピーは持たない)。
9
+ > **Vite 版との差分**: 適用先が Vantage アプリ(`@squadbase/vantage`)になったため、(1) エントリは `src/pages/home.tsx` ではなく **`src/index.tsx`**、(2) テンプレ固有ファイルは `src/templates/<slug>/` ではなく **`src/components/<slug>/`**、(3) 全ての building block を `@squadbase/vantage/*` から import する(ローカルコピーは持たない)。
10
10
 
11
11
  ## 設計目標
12
12
 
@@ -20,21 +20,25 @@ UIパターン別テンプレート(KPI重視・チャート格子・テーブ
20
20
 
21
21
  ### 1. 1テンプレ = 1ディレクトリ(コロケーション)
22
22
 
23
- 適用先では、`index.tsx`(エントリ)以外のテンプレ固有ファイルを **`components/<slug>/` 1ディレクトリに集約** する。
23
+ 適用先では、`src/index.tsx`(エントリ)以外のテンプレ固有ファイルを **`src/components/<slug>/` 1ディレクトリに集約** する。
24
24
 
25
25
  ```
26
26
  # 適用先(dest)レイアウト
27
- index.tsx # エントリ(ルート "/")
28
- components/<slug>/
27
+ src/index.tsx # エントリ(ルート "/")
28
+ src/components/<slug>/
29
29
  mock-data.ts # データ層(relabel:false / 差し替え対象)
30
30
  types.ts # 型(index と data で共有)
31
31
  ...(分割した場合のみ追加ファイル)
32
32
  ```
33
33
 
34
- - **探索**: `components/<slug>/` を見ればテンプレ由来が一覧完結
35
- - **削除**: `rm -rf components/<slug>/` で一掃
34
+ - **探索**: `src/components/<slug>/` を見ればテンプレ由来が一覧完結
35
+ - **削除**: `rm -rf src/components/<slug>/` で一掃
36
36
 
37
- **`components/` を使う理由(重要)**: Vantage は**ファイルベースルーティング**で、`.tsx` ファイルは原則すべてページ扱いになる。`components/` / `hooks/` / `lib/` / `server/` / `public/` だけがルート走査から除外される(`NON_ROUTE_DIRS`)ため、`templates/<slug>/` のようなディレクトリに `.tsx` を置くと **`/templates/<slug>/...` という意図しないルートが生え、`vantage check` が `MISSING_DEFAULT_EXPORT` で落ちる**。データファイル(`.ts`)も同じディレクトリに置いてコロケーションを保つ。
37
+ **`src/` を使う理由(v0.3.0 以降)**: ページ探索ルートは `src/` の有無だけで決まる。ベーステンプレートは `src/` を持つので、ページとそれが import するものは**すべて `src/` の中**に置く。`src/` があるのにルート直下に `.tsx` や `styles.css` を残すと `vantage check` が `SRC_DIR_SPLIT` エラーにする。**例外は `server/` と `public/` で、これらは常にプロジェクトルート直下**(`src/server/` はスキャンされない)。
38
+
39
+ **ただし manifest の `dest` に `src/` は書かない。** `dest` は**ページ探索ルート相対**(`index.tsx` / `components/<slug>/x.tsx`)で、`src/` を付けるかどうかは適用先のレイアウトを見て CLI が決める(ルート直下にページを置くプロジェクトにも適用できる)。`server/` `public/` だけ `"scope": "root"` を付けてプロジェクトルート固定にする。オーサリング側のファイルは**常に `src/` レイアウト前提**で書き、`src/` を跨ぐ相対 import は適用時に CLI が張り替える。
40
+
41
+ **`components/` を使う理由(重要)**: Vantage は**ファイルベースルーティング**で、`.tsx` ファイルは原則すべてページ扱いになる。`components/` / `hooks/` / `lib/` / `server/` / `public/` だけがルート走査から除外される(`NON_ROUTE_DIRS`)ため、`src/templates/<slug>/` のようなディレクトリに `.tsx` を置くと **`/templates/<slug>/...` という意図しないルートが生え、`vantage check` が `MISSING_DEFAULT_EXPORT` で落ちる**(`SUSPICIOUS_ROUTE_DIR` 警告も出る)。データファイル(`.ts`)も同じディレクトリに置いてコロケーションを保つ。
38
42
 
39
43
  ### 2. インライン優先 — 薄いラッパ component を作らない
40
44
 
@@ -55,20 +59,21 @@ function areaOption(data: TimePoint[]): EChartsOption {
55
59
 
56
60
  ### 3. データ層(mock-data / types)は分離する
57
61
 
58
- `mock-data.ts` と `types.ts` は `index.tsx` に inline せず、`components/<slug>/` の別ファイルに保つ。表示ラベル(KPI名・軸名・stage名など)は描画側(`index.tsx`)の literal/const に置き、データ配列には値・id・generic な sample text のみ置く。
62
+ `mock-data.ts` と `types.ts` は `src/index.tsx` に inline せず、`src/components/<slug>/` の別ファイルに保つ。表示ラベル(KPI名・軸名・stage名など)は描画側(`index.tsx`)の literal/const に置き、データ配列には値・id・generic な sample text のみ置く。
59
63
 
60
64
  ### 3-b. API 版テンプレ(`server/api` から配信する場合)
61
65
 
62
- `kpi-chart-simple` はモック配列を直読みせず、**`server/api` から `useApiQuery` で取る**構成になっている。実データ接続までの導線を1本コードで示すためのもので、この構成を採るテンプレは dest の置き場が変わる:
66
+ `kpi-chart-simple` はモック配列を直読みせず、**`server/api` から `useApiQuery` で取る**構成になっている。実データ接続までの導線を1本コードで示すためのもので、この構成を採るテンプレは置き場が変わる(以下は `src/` レイアウトに適用したときの実配置。manifest の `dest` は `src/` を除いた形で書く):
63
67
 
64
68
  ```
65
- index.tsx # useApiQuery + useSearchParam
66
- components/<slug>/*.tsx # 表示コンポーネント
67
- lib/<slug>/types.ts # ★ client と server が共有する型(relabel:false)
68
- server/api/<endpoint>.ts # ★ サンプルデータ + GET ハンドラ(relabel:false)
69
+ src/index.tsx # useApiQuery + useSearchParam
70
+ src/components/<slug>/*.tsx # 表示コンポーネント
71
+ src/lib/<slug>/types.ts # ★ client と server が共有する型(relabel:false)
72
+ server/api/<endpoint>.ts # ★ サンプルデータ + GET ハンドラ(relabel:false)※ルート直下
69
73
  ```
70
74
 
71
- - **共有型は `components/` ではなく `lib/`** に置く。クライアントは `server/` を import できない(`CLIENT_IMPORTS_SERVER`)ので、両者が参照できる中立な場所が要る。`lib/` もルート走査対象外なので安全。
75
+ - **共有型は `components/` ではなく `src/lib/`** に置く。クライアントは `server/` を import できない(`CLIENT_IMPORTS_SERVER`)ので、両者が参照できる中立な場所が要る。`lib/` もルート走査対象外なので安全。
76
+ - **`server/` は `src/` の外**なので、ハンドラからの相対 import は `src/` を跨ぐ: `server/api/<endpoint>.ts` からは `../../src/lib/<slug>/types`。`src/components/<slug>/*.tsx` からは `../../lib/<slug>/types`(どちらも `src/` の中なので `src/` は出てこない)。
72
77
  - **サンプルデータはサーバー側に置く。** 「実装者が差し替える場所」がハンドラ1箇所に集約され、`KpiSummary` のような payload 型さえ保てばページを触らずに実データへ移行できる。
73
78
  - **絞り込みは `useSearchParam` + `useApiQuery` の `search`** で組む。`search` はクエリ文字列とキャッシュキーの両方に入るので、URL を変えれば再取得される。
74
79
  - クライアントに見せたいエラーは `HttpError(status, msg)` を throw する。`useApiQuery` の `error` は `ApiError` なのでキャスト不要で `.message` が読める。
@@ -81,7 +86,7 @@ server/api/<endpoint>.ts # ★ サンプルデータ + GET ハ
81
86
 
82
87
  ### 分割してよい例外
83
88
 
84
- **再利用される** / **内部 state・interaction を持つ** / **`index.tsx` が 400〜500 行を超える** のいずれかに当てはまる UI のみ、`components/<slug>/` 配下に切り出してよい。迷ったら inline。
89
+ **再利用される** / **内部 state・interaction を持つ** / **`index.tsx` が 400〜500 行を超える** のいずれかに当てはまる UI のみ、`src/components/<slug>/` 配下に切り出してよい。迷ったら inline。
85
90
 
86
91
  ## import 規約(Vantage)
87
92
 
@@ -101,11 +106,14 @@ server/api/<endpoint>.ts # ★ サンプルデータ + GET ハ
101
106
  **相対 import は拡張子なしで書く**(`@squadbase/vantage` v0.2.0 で `.js` 指定子の規約は廃止。既存の `.js` 付き import も動くが、新規テンプレでは付けない)。
102
107
 
103
108
  ```tsx
104
- // index.tsx から
109
+ // src/index.tsx から
105
110
  import { MOCK_SERIES } from "./components/<slug>/mock-data"
106
111
 
107
- // components/<slug>/detail-table.tsx から
112
+ // src/components/<slug>/detail-table.tsx から
108
113
  import type { Row } from "./types"
114
+
115
+ // server/api/<endpoint>.ts から(src/ の外なので src/ を跨ぐ)
116
+ import type { Payload } from "../../src/lib/<slug>/types"
109
117
  ```
110
118
 
111
119
  `Placeholder` / `Sparkline` はローカルコピーではなく `@squadbase/vantage/components` から取る(v0.1.1 でフレームワークに入った)。
@@ -123,7 +131,7 @@ Vantage の UI キットは Radix ではなく **Base UI** ベース。移植時
123
131
 
124
132
  ## エントリの必須要件
125
133
 
126
- `index.tsx` は **default export の React component** が必須(無いと `vantage check` が `MISSING_DEFAULT_EXPORT`)。加えてタイトル/説明を `definePage` で宣言する:
134
+ `src/index.tsx` は **default export の React component** が必須(無いと `vantage check` が `MISSING_DEFAULT_EXPORT`)。加えてタイトル/説明を `definePage` で宣言する:
127
135
 
128
136
  ```tsx
129
137
  import { definePage } from "@squadbase/vantage"
@@ -150,9 +158,11 @@ ui-templates/<slug>/
150
158
  lib/mock-data.ts → dest: components/<slug>/mock-data.ts (add, relabel:false)
151
159
  lib/types.ts → dest: components/<slug>/types.ts (add, relabel:false)
152
160
  components/*.tsx → dest: components/<slug>/*.tsx (add) ※分割した場合のみ
153
- server/*.ts → dest: server/api/*.ts (add, relabel:false) ※API 版のみ
161
+ server/*.ts → dest: server/api/*.ts, scope: "root" (add, relabel:false) ※API 版のみ
154
162
  ```
155
163
 
164
+ **オーサリング側のディレクトリ名(`pages/` `lib/` `components/` `server/`)は適用先の dest と一致しない。** dest だけが真実で、`manifest.json` に書く。**dest はページ探索ルート相対なので `src/` は付けない**(`scope: "root"` のものだけプロジェクトルート相対)。
165
+
156
166
  API 版(`kpi-chart-simple`)では `lib/types.ts` の dest が `lib/<slug>/types.ts` になり、`lib/mock-data.ts` は無く、代わりに `server/*.ts` を持つ。上の「3-b」を参照。
157
167
 
158
168
  ## manifest.json
@@ -172,6 +182,7 @@ API 版(`kpi-chart-simple`)では `lib/types.ts` の dest が `lib/<slug>/ty
172
182
 
173
183
  - `files[]` が manifest の全て(`nav[]` は無い)。ベーステンプレートのナビは `useRoutes()` からルート一覧を組むので、ページファイルをコピーすればナビにも載る
174
184
  - データファイルには `"relabel": false` を必ず付ける
185
+ - `dest` に `src/` を書かない。`server/` `public/` にだけ `"scope": "root"` を付ける
175
186
 
176
187
  ## 見本市の例外ルール
177
188
 
@@ -189,7 +200,7 @@ API 版(`kpi-chart-simple`)では `lib/types.ts` の dest が `lib/<slug>/ty
189
200
 
190
201
  ## チェックリスト(テンプレ追加・改修時)
191
202
 
192
- 1. テンプレ固有ファイルの `dest` は `components/<slug>/` 配下か(`templates/` に `.tsx` を置いていないか)
203
+ 1. テンプレ固有ファイルの `dest` は `components/<slug>/` 配下か(`templates/` に `.tsx` を置いていないか)。`dest` に `src/` を書いていないか。`server/*` だけ `dest: server/api/*` + `"scope": "root"` か
193
204
  2. `@/` エイリアスや `echarts` / `@tanstack/*` の直接 import、`Placeholder` / `Sparkline` のローカル import が残っていないか
194
205
  3. 相対 import に `.js` 拡張子が残っていないか(v0.2.0 で規約廃止)
195
206
  4. 薄いチャートラッパ component を作っていないか。チャートに色をハードコードしていないか(`option.color` / `itemStyle.color` / `theme`)
@@ -197,4 +208,4 @@ API 版(`kpi-chart-simple`)では `lib/types.ts` の dest が `lib/<slug>/ty
197
208
  6. 未使用の関数・型・import が残っていないか
198
209
  7. 適用先で `npx vantage check` と `npx tsc --noEmit` が通るか
199
210
  8. EN/JA 両方を同じ構成に揃えたか
200
- 9. レイアウトを変えたなら `preview-wide.png` (1600x900) / `preview-square.png` (1200x1200) を撮り直したか
211
+ 9. レイアウトを変えたなら `preview-wide.png` (1600x900) / `preview-square.png` (1200x1200) を撮り直したか — `cd vantage-template && npm run screenshot <slug> <slug>-ja`(全部撮り直すなら引数なし)