@cubis/foundry 0.3.14 → 0.3.15

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/bin/cubis.js +396 -40
  2. package/package.json +2 -1
package/bin/cubis.js CHANGED
@@ -172,6 +172,54 @@ const TECH_LANGUAGE_BY_EXTENSION = new Map([
172
172
  [".sh", "Shell"],
173
173
  [".ps1", "PowerShell"]
174
174
  ]);
175
+ const TECH_PACKAGE_PREVIEW_LIMIT = 40;
176
+ const TECH_JS_FRAMEWORK_SIGNALS = [
177
+ ["next", "Next.js"],
178
+ ["react", "React"],
179
+ ["vue", "Vue"],
180
+ ["nuxt", "Nuxt"],
181
+ ["svelte", "Svelte"],
182
+ ["@nestjs/core", "NestJS"],
183
+ ["express", "Express"],
184
+ ["fastify", "Fastify"],
185
+ ["hono", "Hono"],
186
+ ["tailwindcss", "Tailwind CSS"],
187
+ ["prisma", "Prisma"],
188
+ ["drizzle-orm", "Drizzle ORM"],
189
+ ["mongoose", "Mongoose"],
190
+ ["typeorm", "TypeORM"],
191
+ ["@playwright/test", "Playwright"],
192
+ ["vitest", "Vitest"],
193
+ ["jest", "Jest"],
194
+ ["cypress", "Cypress"]
195
+ ];
196
+ const TECH_DART_FRAMEWORK_SIGNALS = [
197
+ ["flutter_riverpod", "Riverpod"],
198
+ ["riverpod", "Riverpod"],
199
+ ["go_router", "go_router"],
200
+ ["dio", "Dio"],
201
+ ["freezed", "Freezed"],
202
+ ["bloc", "BLoC"]
203
+ ];
204
+ const TECH_GO_FRAMEWORK_SIGNALS = [
205
+ ["github.com/gofiber/fiber/v2", "Go Fiber"],
206
+ ["github.com/gin-gonic/gin", "Gin"],
207
+ ["github.com/labstack/echo/v4", "Echo"],
208
+ ["github.com/go-chi/chi/v5", "Chi"]
209
+ ];
210
+ const TECH_PYTHON_FRAMEWORK_SIGNALS = [
211
+ ["fastapi", "FastAPI"],
212
+ ["django", "Django"],
213
+ ["flask", "Flask"],
214
+ ["pydantic", "Pydantic"],
215
+ ["sqlalchemy", "SQLAlchemy"]
216
+ ];
217
+ const TECH_RUST_FRAMEWORK_SIGNALS = [
218
+ ["axum", "Axum"],
219
+ ["actix-web", "Actix Web"],
220
+ ["rocket", "Rocket"],
221
+ ["tokio", "Tokio"]
222
+ ];
175
223
 
176
224
  function platformInstallsCustomAgents(platformId) {
177
225
  const profile = WORKFLOW_PROFILES[platformId];
@@ -585,6 +633,198 @@ async function upsertEngineeringRulesBlock({
585
633
  };
586
634
  }
587
635
 
636
+ function normalizeTechPackageName(value) {
637
+ if (value === undefined || value === null) return null;
638
+ const normalized = String(value).trim().replace(/^['"]|['"]$/g, "").toLowerCase();
639
+ return normalized || null;
640
+ }
641
+
642
+ function parseTomlSections(content) {
643
+ const sections = new Map();
644
+ let currentSection = "";
645
+ sections.set(currentSection, []);
646
+
647
+ for (const line of content.split(/\r?\n/)) {
648
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
649
+ if (sectionMatch) {
650
+ currentSection = sectionMatch[1].trim();
651
+ if (!sections.has(currentSection)) sections.set(currentSection, []);
652
+ continue;
653
+ }
654
+ sections.get(currentSection).push(line);
655
+ }
656
+
657
+ return sections;
658
+ }
659
+
660
+ function parsePubspecDependencyNames(content) {
661
+ const packages = new Set();
662
+ let currentSection = null;
663
+
664
+ for (const rawLine of content.split(/\r?\n/)) {
665
+ const line = rawLine.replace(/\t/g, " ");
666
+ const trimmed = line.trim();
667
+ if (!trimmed || trimmed.startsWith("#")) continue;
668
+
669
+ if (!line.startsWith(" ")) {
670
+ const sectionMatch = trimmed.match(/^([a-zA-Z0-9_]+):\s*$/);
671
+ if (!sectionMatch) {
672
+ currentSection = null;
673
+ continue;
674
+ }
675
+ currentSection = sectionMatch[1];
676
+ continue;
677
+ }
678
+
679
+ if (currentSection !== "dependencies" && currentSection !== "dev_dependencies") continue;
680
+ const depMatch = trimmed.match(/^([a-zA-Z0-9_]+):/);
681
+ if (!depMatch) continue;
682
+ const packageName = normalizeTechPackageName(depMatch[1]);
683
+ if (!packageName || packageName === "flutter" || packageName === "sdk") continue;
684
+ packages.add(packageName);
685
+ }
686
+
687
+ return packages;
688
+ }
689
+
690
+ function parseGoModuleNames(content) {
691
+ const modules = new Set();
692
+ let inRequireBlock = false;
693
+
694
+ for (const rawLine of content.split(/\r?\n/)) {
695
+ const trimmed = rawLine.trim();
696
+ if (!trimmed || trimmed.startsWith("//")) continue;
697
+
698
+ if (trimmed.startsWith("require (")) {
699
+ inRequireBlock = true;
700
+ continue;
701
+ }
702
+ if (inRequireBlock && trimmed === ")") {
703
+ inRequireBlock = false;
704
+ continue;
705
+ }
706
+
707
+ if (inRequireBlock) {
708
+ const moduleName = normalizeTechPackageName(trimmed.split(/\s+/)[0]);
709
+ if (moduleName) modules.add(moduleName);
710
+ continue;
711
+ }
712
+
713
+ if (trimmed.startsWith("require ")) {
714
+ const moduleName = normalizeTechPackageName(trimmed.slice("require ".length).trim().split(/\s+/)[0]);
715
+ if (moduleName) modules.add(moduleName);
716
+ }
717
+ }
718
+
719
+ return modules;
720
+ }
721
+
722
+ function parseRequirementsPackageNames(content) {
723
+ const packages = new Set();
724
+ for (const rawLine of content.split(/\r?\n/)) {
725
+ const withoutComment = rawLine.split("#")[0].trim();
726
+ if (!withoutComment) continue;
727
+ if (withoutComment.startsWith("-")) continue;
728
+
729
+ const cleaned = withoutComment.split(";")[0].trim();
730
+ const match = cleaned.match(/^([A-Za-z0-9_.-]+)/);
731
+ if (!match) continue;
732
+ const packageName = normalizeTechPackageName(match[1]);
733
+ if (packageName) packages.add(packageName);
734
+ }
735
+ return packages;
736
+ }
737
+
738
+ function parsePyprojectPackageNames(content) {
739
+ const packages = new Set();
740
+ const sections = parseTomlSections(content);
741
+
742
+ const projectSection = sections.get("project");
743
+ if (projectSection) {
744
+ const projectBody = projectSection.join("\n");
745
+ const dependenciesArrayMatch = projectBody.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
746
+ if (dependenciesArrayMatch) {
747
+ const entries = dependenciesArrayMatch[1].match(/"([^"]+)"|'([^']+)'/g) || [];
748
+ for (const entry of entries) {
749
+ const normalizedEntry = normalizeTechPackageName(entry);
750
+ if (!normalizedEntry) continue;
751
+ const packageName = normalizeTechPackageName(normalizedEntry.split(/[<>=!~\s\[]/)[0]);
752
+ if (packageName) packages.add(packageName);
753
+ }
754
+ }
755
+
756
+ for (const line of projectSection) {
757
+ const trimmed = line.trim();
758
+ const optionalDepsMatch = trimmed.match(
759
+ /^([A-Za-z0-9_.-]+)\s*=\s*\[\s*("([^"]+)"|'([^']+)')/
760
+ );
761
+ if (!optionalDepsMatch) continue;
762
+ const entry = optionalDepsMatch[3] || optionalDepsMatch[4];
763
+ const packageName = normalizeTechPackageName(String(entry).split(/[<>=!~\s\[]/)[0]);
764
+ if (packageName) packages.add(packageName);
765
+ }
766
+ }
767
+
768
+ for (const [sectionName, lines] of sections.entries()) {
769
+ const isPoetryDependencySection =
770
+ sectionName === "tool.poetry.dependencies" ||
771
+ /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(sectionName);
772
+ if (!isPoetryDependencySection) continue;
773
+
774
+ for (const line of lines) {
775
+ const trimmed = line.trim();
776
+ if (!trimmed || trimmed.startsWith("#")) continue;
777
+ const depMatch = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=/);
778
+ if (!depMatch) continue;
779
+ const packageName = normalizeTechPackageName(depMatch[1]);
780
+ if (!packageName || packageName === "python") continue;
781
+ packages.add(packageName);
782
+ }
783
+ }
784
+
785
+ return packages;
786
+ }
787
+
788
+ function parseCargoCrateNames(content) {
789
+ const crates = new Set();
790
+ const sections = parseTomlSections(content);
791
+
792
+ for (const [sectionName, lines] of sections.entries()) {
793
+ const isDependencySection =
794
+ sectionName === "dependencies" ||
795
+ sectionName === "dev-dependencies" ||
796
+ sectionName === "build-dependencies" ||
797
+ sectionName === "workspace.dependencies" ||
798
+ /\.dependencies$/.test(sectionName) ||
799
+ /\.dev-dependencies$/.test(sectionName) ||
800
+ /\.build-dependencies$/.test(sectionName);
801
+ if (!isDependencySection) continue;
802
+
803
+ for (const line of lines) {
804
+ const trimmed = line.trim();
805
+ if (!trimmed || trimmed.startsWith("#")) continue;
806
+ const depMatch = trimmed.match(/^([A-Za-z0-9_-]+)\s*=/);
807
+ if (!depMatch) continue;
808
+ const crateName = normalizeTechPackageName(depMatch[1]);
809
+ if (crateName) crates.add(crateName);
810
+ }
811
+ }
812
+
813
+ return crates;
814
+ }
815
+
816
+ function addFrameworkSignalsFromPackages({ packages, frameworks, signals }) {
817
+ for (const [signal, frameworkLabel] of signals) {
818
+ if (packages.has(signal.toLowerCase())) {
819
+ frameworks.add(frameworkLabel);
820
+ }
821
+ }
822
+ }
823
+
824
+ function toSortedArray(values) {
825
+ return [...values].sort((a, b) => a.localeCompare(b));
826
+ }
827
+
588
828
  async function collectTechSnapshot(rootDir) {
589
829
  const discoveredFiles = [];
590
830
  const queue = [rootDir];
@@ -614,6 +854,12 @@ async function collectTechSnapshot(rootDir) {
614
854
 
615
855
  const languageCounts = new Map();
616
856
  const topDirs = new Set();
857
+ const packageJsonFiles = [];
858
+ const pubspecFiles = [];
859
+ const goModFiles = [];
860
+ const pyprojectFiles = [];
861
+ const requirementsFiles = [];
862
+ const cargoTomlFiles = [];
617
863
 
618
864
  for (const fullPath of discoveredFiles) {
619
865
  const rel = toPosixPath(path.relative(rootDir, fullPath));
@@ -622,15 +868,31 @@ async function collectTechSnapshot(rootDir) {
622
868
 
623
869
  const extension = path.extname(fullPath).toLowerCase();
624
870
  const language = TECH_LANGUAGE_BY_EXTENSION.get(extension);
625
- if (!language) continue;
626
- languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
871
+ if (language) {
872
+ languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
873
+ }
874
+
875
+ const baseName = path.basename(fullPath).toLowerCase();
876
+ if (baseName === "package.json") packageJsonFiles.push(fullPath);
877
+ if (baseName === "pubspec.yaml") pubspecFiles.push(fullPath);
878
+ if (baseName === "go.mod") goModFiles.push(fullPath);
879
+ if (baseName === "pyproject.toml") pyprojectFiles.push(fullPath);
880
+ if (baseName === "cargo.toml") cargoTomlFiles.push(fullPath);
881
+ if (baseName === "requirements.txt" || /^requirements(?:[-_.].+)?\.txt$/.test(baseName)) {
882
+ requirementsFiles.push(fullPath);
883
+ }
627
884
  }
628
885
 
629
886
  const fileExists = (name) => existsSync(path.join(rootDir, name));
630
- const packageJsonPath = path.join(rootDir, "package.json");
887
+ const rootPackageJsonPath = path.join(rootDir, "package.json");
631
888
  const packageScripts = new Map();
632
889
  const frameworks = new Set();
633
890
  const lockfiles = [];
891
+ const javascriptPackages = new Set();
892
+ const dartPackages = new Set();
893
+ const goModules = new Set();
894
+ const pythonPackages = new Set();
895
+ const rustCrates = new Set();
634
896
 
635
897
  if (fileExists("bun.lock") || fileExists("bun.lockb")) lockfiles.push("bun");
636
898
  if (fileExists("pnpm-lock.yaml")) lockfiles.push("pnpm");
@@ -641,54 +903,118 @@ async function collectTechSnapshot(rootDir) {
641
903
  if (fileExists("go.sum")) lockfiles.push("go");
642
904
  if (fileExists("pubspec.lock")) lockfiles.push("pub");
643
905
 
644
- if (fileExists("pubspec.yaml")) frameworks.add("Flutter");
645
- if (fileExists("go.mod")) frameworks.add("Go Modules");
646
- if (fileExists("Cargo.toml")) frameworks.add("Rust Cargo");
647
- if (fileExists("requirements.txt") || fileExists("pyproject.toml")) frameworks.add("Python");
906
+ if (pubspecFiles.length > 0) frameworks.add("Flutter");
907
+ if (goModFiles.length > 0) frameworks.add("Go Modules");
908
+ if (cargoTomlFiles.length > 0) frameworks.add("Rust Cargo");
909
+ if (requirementsFiles.length > 0 || pyprojectFiles.length > 0) frameworks.add("Python");
648
910
 
649
- if (existsSync(packageJsonPath)) {
911
+ for (const packageJsonFile of packageJsonFiles) {
650
912
  try {
651
- const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
652
- const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
653
- for (const [name, command] of Object.entries(scripts)) {
654
- if (typeof command !== "string") continue;
655
- packageScripts.set(name, command);
656
- }
657
-
913
+ const parsed = JSON.parse(await readFile(packageJsonFile, "utf8"));
658
914
  const deps = {
659
915
  ...(parsed.dependencies || {}),
660
916
  ...(parsed.devDependencies || {}),
661
- ...(parsed.peerDependencies || {})
917
+ ...(parsed.peerDependencies || {}),
918
+ ...(parsed.optionalDependencies || {})
662
919
  };
663
- const depNames = new Set(Object.keys(deps));
664
- const frameworkSignals = [
665
- ["next", "Next.js"],
666
- ["react", "React"],
667
- ["vue", "Vue"],
668
- ["nuxt", "Nuxt"],
669
- ["svelte", "Svelte"],
670
- ["@nestjs/core", "NestJS"],
671
- ["express", "Express"],
672
- ["fastify", "Fastify"],
673
- ["hono", "Hono"],
674
- ["tailwindcss", "Tailwind CSS"],
675
- ["prisma", "Prisma"],
676
- ["drizzle-orm", "Drizzle ORM"],
677
- ["mongoose", "Mongoose"],
678
- ["typeorm", "TypeORM"],
679
- ["@playwright/test", "Playwright"],
680
- ["vitest", "Vitest"],
681
- ["jest", "Jest"],
682
- ["cypress", "Cypress"]
683
- ];
684
- for (const [signal, label] of frameworkSignals) {
685
- if (depNames.has(signal)) frameworks.add(label);
920
+ for (const depName of Object.keys(deps)) {
921
+ const normalized = normalizeTechPackageName(depName);
922
+ if (normalized) javascriptPackages.add(normalized);
923
+ }
924
+
925
+ if (path.resolve(packageJsonFile) === path.resolve(rootPackageJsonPath)) {
926
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
927
+ for (const [name, command] of Object.entries(scripts)) {
928
+ if (typeof command !== "string") continue;
929
+ packageScripts.set(name, command);
930
+ }
686
931
  }
687
932
  } catch {
688
933
  // ignore malformed package.json
689
934
  }
690
935
  }
691
936
 
937
+ for (const pubspecFile of pubspecFiles) {
938
+ try {
939
+ const content = await readFile(pubspecFile, "utf8");
940
+ for (const packageName of parsePubspecDependencyNames(content)) {
941
+ dartPackages.add(packageName);
942
+ }
943
+ } catch {
944
+ // ignore malformed pubspec.yaml
945
+ }
946
+ }
947
+
948
+ for (const goModFile of goModFiles) {
949
+ try {
950
+ const content = await readFile(goModFile, "utf8");
951
+ for (const moduleName of parseGoModuleNames(content)) {
952
+ goModules.add(moduleName);
953
+ }
954
+ } catch {
955
+ // ignore unreadable go.mod
956
+ }
957
+ }
958
+
959
+ for (const pyprojectFile of pyprojectFiles) {
960
+ try {
961
+ const content = await readFile(pyprojectFile, "utf8");
962
+ for (const packageName of parsePyprojectPackageNames(content)) {
963
+ pythonPackages.add(packageName);
964
+ }
965
+ } catch {
966
+ // ignore malformed pyproject.toml
967
+ }
968
+ }
969
+
970
+ for (const requirementsFile of requirementsFiles) {
971
+ try {
972
+ const content = await readFile(requirementsFile, "utf8");
973
+ for (const packageName of parseRequirementsPackageNames(content)) {
974
+ pythonPackages.add(packageName);
975
+ }
976
+ } catch {
977
+ // ignore unreadable requirements file
978
+ }
979
+ }
980
+
981
+ for (const cargoTomlFile of cargoTomlFiles) {
982
+ try {
983
+ const content = await readFile(cargoTomlFile, "utf8");
984
+ for (const crateName of parseCargoCrateNames(content)) {
985
+ rustCrates.add(crateName);
986
+ }
987
+ } catch {
988
+ // ignore malformed Cargo.toml
989
+ }
990
+ }
991
+
992
+ addFrameworkSignalsFromPackages({
993
+ packages: javascriptPackages,
994
+ frameworks,
995
+ signals: TECH_JS_FRAMEWORK_SIGNALS
996
+ });
997
+ addFrameworkSignalsFromPackages({
998
+ packages: dartPackages,
999
+ frameworks,
1000
+ signals: TECH_DART_FRAMEWORK_SIGNALS
1001
+ });
1002
+ addFrameworkSignalsFromPackages({
1003
+ packages: goModules,
1004
+ frameworks,
1005
+ signals: TECH_GO_FRAMEWORK_SIGNALS
1006
+ });
1007
+ addFrameworkSignalsFromPackages({
1008
+ packages: pythonPackages,
1009
+ frameworks,
1010
+ signals: TECH_PYTHON_FRAMEWORK_SIGNALS
1011
+ });
1012
+ addFrameworkSignalsFromPackages({
1013
+ packages: rustCrates,
1014
+ frameworks,
1015
+ signals: TECH_RUST_FRAMEWORK_SIGNALS
1016
+ });
1017
+
692
1018
  const sortedLanguages = [...languageCounts.entries()].sort((a, b) => b[1] - a[1]);
693
1019
  const sortedFrameworks = [...frameworks].sort((a, b) => a.localeCompare(b));
694
1020
  const sortedTopDirs = [...topDirs].sort((a, b) => a.localeCompare(b)).slice(0, 12);
@@ -717,10 +1043,33 @@ async function collectTechSnapshot(rootDir) {
717
1043
  frameworks: sortedFrameworks,
718
1044
  lockfiles: sortedLockfiles,
719
1045
  topDirs: sortedTopDirs,
720
- keyScripts
1046
+ keyScripts,
1047
+ packageSignals: {
1048
+ javascript: toSortedArray(javascriptPackages),
1049
+ dart: toSortedArray(dartPackages),
1050
+ go: toSortedArray(goModules),
1051
+ python: toSortedArray(pythonPackages),
1052
+ rust: toSortedArray(rustCrates)
1053
+ }
721
1054
  };
722
1055
  }
723
1056
 
1057
+ function appendTechPackageSection(lines, heading, packages) {
1058
+ lines.push(`### ${heading}`);
1059
+ if (!packages || packages.length === 0) {
1060
+ lines.push("- None detected.");
1061
+ } else {
1062
+ const preview = packages.slice(0, TECH_PACKAGE_PREVIEW_LIMIT);
1063
+ for (const packageName of preview) {
1064
+ lines.push(`- \`${packageName}\``);
1065
+ }
1066
+ if (packages.length > TECH_PACKAGE_PREVIEW_LIMIT) {
1067
+ lines.push(`- ... (+${packages.length - TECH_PACKAGE_PREVIEW_LIMIT} more)`);
1068
+ }
1069
+ }
1070
+ lines.push("");
1071
+ }
1072
+
724
1073
  function buildTechMd(snapshot) {
725
1074
  const lines = [];
726
1075
  lines.push("# TECH.md");
@@ -750,6 +1099,13 @@ function buildTechMd(snapshot) {
750
1099
  }
751
1100
  lines.push("");
752
1101
 
1102
+ lines.push("## Package Signals");
1103
+ appendTechPackageSection(lines, "JavaScript / TypeScript (package.json)", snapshot.packageSignals.javascript);
1104
+ appendTechPackageSection(lines, "Dart / Flutter (pubspec.yaml)", snapshot.packageSignals.dart);
1105
+ appendTechPackageSection(lines, "Go Modules (go.mod)", snapshot.packageSignals.go);
1106
+ appendTechPackageSection(lines, "Python Packages (requirements / pyproject)", snapshot.packageSignals.python);
1107
+ appendTechPackageSection(lines, "Rust Crates (Cargo.toml)", snapshot.packageSignals.rust);
1108
+
753
1109
  lines.push("## Tooling and Lockfiles");
754
1110
  if (snapshot.lockfiles.length === 0) {
755
1111
  lines.push("- No lockfiles detected.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "check": "node bin/cubis.js --help",
19
19
  "test:attributes": "node scripts/validate-platform-attributes.mjs",
20
20
  "test:attributes:strict": "node scripts/validate-platform-attributes.mjs --strict",
21
+ "test:tech-md": "node scripts/test-tech-md-scanner.mjs",
21
22
  "test:smoke": "bash scripts/smoke-workflows.sh",
22
23
  "test:all": "npm run test:attributes && npm run test:smoke",
23
24
  "test:all:strict": "npm run test:attributes:strict && npm run test:smoke",