@cubis/foundry 0.3.13 → 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.
@@ -12,7 +12,7 @@ Before proceeding, validate that the user has completed the following steps befo
12
12
 
13
13
  ## Step 1
14
14
 
15
- Prompt the user to configure their own Postman API key for authentication. They can either set it as an environment variable named POSTMAN_API_KEY on their system, or hardcode it directly into the user level MCP configuration file (usually at ~/.kiro/settings/mcp.json) in the power section. To obtain an API key, they log into their Postman account, navigate to Settings API Keys, and generate a new key with appropriate permissions for workspace, collection, and environment management. The key will be automatically used by the MCP server to authenticate all API requests to Postman's services.
15
+ Generate and maintain `postman_setting.json` as the primary Postman configuration. Use env-first authentication by default: keep `apiKey` as `null`, set `apiKeyEnvVar` to `POSTMAN_API_KEY`, and read the key from environment variables. Only store `apiKey` inline when the user explicitly requests file-based key storage. Keep `defaultWorkspaceId` nullable (`null` when unknown) so workflows can run without a preselected workspace.
16
16
 
17
17
  ## Step 2
18
18
 
@@ -68,7 +68,7 @@ Create a hook that runs anytime the source code or configuration file has been c
68
68
 
69
69
  Automate API testing and collection management with Postman. Create workspaces, collections, environments, and run tests programmatically.
70
70
 
71
- **Authentication**: Requires Postman API key (Settings API Keys at postman.com)
71
+ **Authentication**: Env-first via `postman_setting.json` + `POSTMAN_API_KEY`; inline `apiKey` is optional.
72
72
 
73
73
  ## Available MCP Servers
74
74
 
@@ -10,7 +10,7 @@ Before proceeding, validate that the user has completed the following steps befo
10
10
 
11
11
  ## Step 1
12
12
 
13
- Prompt the user to configure their own Postman API key for authentication. They can either set it as an environment variable named POSTMAN_API_KEY on their system, or hardcode it directly into the user level MCP configuration file (usually at ~/.kiro/settings/mcp.json) in the power section. To obtain an API key, they log into their Postman account, navigate to Settings API Keys, and generate a new key with appropriate permissions for workspace, collection, and environment management. The key will be automatically used by the MCP server to authenticate all API requests to Postman's services.
13
+ Generate and maintain `postman_setting.json` as the primary Postman configuration. Use env-first authentication by default: keep `apiKey` as `null`, set `apiKeyEnvVar` to `POSTMAN_API_KEY`, and read the key from environment variables. Only store `apiKey` inline when the user explicitly requests file-based key storage. Keep `defaultWorkspaceId` nullable (`null` when unknown) so workflows can run without a preselected workspace.
14
14
 
15
15
  ## Step 2
16
16
 
@@ -66,7 +66,7 @@ Create a hook that runs anytime the source code or configuration file has been c
66
66
 
67
67
  Automate API testing and collection management with Postman. Create workspaces, collections, environments, and run tests programmatically.
68
68
 
69
- **Authentication**: Requires Postman API key (Settings API Keys at postman.com)
69
+ **Authentication**: Env-first via `postman_setting.json` + `POSTMAN_API_KEY`; inline `apiKey` is optional.
70
70
 
71
71
  ## Available MCP Servers
72
72
 
@@ -12,7 +12,7 @@ Before proceeding, validate that the user has completed the following steps befo
12
12
 
13
13
  ## Step 1
14
14
 
15
- Prompt the user to configure their own Postman API key for authentication. They can either set it as an environment variable named POSTMAN_API_KEY on their system, or hardcode it directly into the user level MCP configuration file (usually at ~/.kiro/settings/mcp.json) in the power section. To obtain an API key, they log into their Postman account, navigate to Settings API Keys, and generate a new key with appropriate permissions for workspace, collection, and environment management. The key will be automatically used by the MCP server to authenticate all API requests to Postman's services.
15
+ Generate and maintain `postman_setting.json` as the primary Postman configuration. Use env-first authentication by default: keep `apiKey` as `null`, set `apiKeyEnvVar` to `POSTMAN_API_KEY`, and read the key from environment variables. Only store `apiKey` inline when the user explicitly requests file-based key storage. Keep `defaultWorkspaceId` nullable (`null` when unknown) so workflows can run without a preselected workspace.
16
16
 
17
17
  ## Step 2
18
18
 
@@ -68,7 +68,7 @@ Create a hook that runs anytime the source code or configuration file has been c
68
68
 
69
69
  Automate API testing and collection management with Postman. Create workspaces, collections, environments, and run tests programmatically.
70
70
 
71
- **Authentication**: Requires Postman API key (Settings API Keys at postman.com)
71
+ **Authentication**: Env-first via `postman_setting.json` + `POSTMAN_API_KEY`; inline `apiKey` is optional.
72
72
 
73
73
  ## Available MCP Servers
74
74
 
@@ -239,4 +239,4 @@ for (const collection of collections) {
239
239
 
240
240
  **Full mode (112 tools):** Change URL to `https://mcp.postman.com/full`
241
241
 
242
- **API Key Permissions:** Workspace management, collection read/write, environment read/write, collection runs
242
+ **API Key Permissions:** Workspace management, collection read/write, environment read/write, collection runs
@@ -10,7 +10,7 @@ Before proceeding, validate that the user has completed the following steps befo
10
10
 
11
11
  ## Step 1
12
12
 
13
- Prompt the user to configure their own Postman API key for authentication. They can either set it as an environment variable named POSTMAN_API_KEY on their system, or hardcode it directly into the user level MCP configuration file (usually at ~/.kiro/settings/mcp.json) in the power section. To obtain an API key, they log into their Postman account, navigate to Settings API Keys, and generate a new key with appropriate permissions for workspace, collection, and environment management. The key will be automatically used by the MCP server to authenticate all API requests to Postman's services.
13
+ Generate and maintain `postman_setting.json` as the primary Postman configuration. Use env-first authentication by default: keep `apiKey` as `null`, set `apiKeyEnvVar` to `POSTMAN_API_KEY`, and read the key from environment variables. Only store `apiKey` inline when the user explicitly requests file-based key storage. Keep `defaultWorkspaceId` nullable (`null` when unknown) so workflows can run without a preselected workspace.
14
14
 
15
15
  ## Step 2
16
16
 
@@ -66,7 +66,7 @@ Create a hook that runs anytime the source code or configuration file has been c
66
66
 
67
67
  Automate API testing and collection management with Postman. Create workspaces, collections, environments, and run tests programmatically.
68
68
 
69
- **Authentication**: Requires Postman API key (Settings API Keys at postman.com)
69
+ **Authentication**: Env-first via `postman_setting.json` + `POSTMAN_API_KEY`; inline `apiKey` is optional.
70
70
 
71
71
  ## Available MCP Servers
72
72
 
@@ -3,7 +3,7 @@ name: backend-specialist
3
3
  description: Expert backend architect for Node.js, Python, and modern serverless/edge systems. Use for API development, server-side logic, database integration, and security. Triggers on backend, server, api, endpoint, database, auth.
4
4
  tools: Read, Grep, Glob, Bash, Edit, Write
5
5
  model: inherit
6
- skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
6
+ skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, openapi-docs, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
7
7
  ---
8
8
 
9
9
  # Backend Development Architect
@@ -14,4 +14,14 @@ Use this workflow when backend architecture or implementation is primary.
14
14
  1. Ask `@backend-specialist` for solution outline.
15
15
  2. Validate API contracts, data model, and failure handling.
16
16
  3. Implement backend changes with tests and observability.
17
- 4. Return summary including migration and rollout notes.
17
+ 4. Always update API docs: OpenAPI spec, Swagger UI route, and Stoplight Elements route/component.
18
+ 5. Return summary including migration and rollout notes.
19
+
20
+ ## Output Contract
21
+ - API/contract changes
22
+ - OpenAPI spec path
23
+ - Swagger UI route
24
+ - Stoplight route/component status
25
+ - Migration impact
26
+ - Reliability/security notes
27
+ - Verification evidence
@@ -3,7 +3,7 @@ name: backend-specialist
3
3
  description: Expert backend architect for Node.js, Python, and modern serverless/edge systems. Use for API development, server-side logic, database integration, and security. Triggers on backend, server, api, endpoint, database, auth.
4
4
  tools: Read, Grep, Glob, Bash, Edit, Write
5
5
  model: inherit
6
- skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
6
+ skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, openapi-docs, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
7
7
  ---
8
8
 
9
9
  # Backend Development Architect
@@ -16,10 +16,14 @@ Use this when backend architecture or service logic is primary.
16
16
  1. Ask specialist(s) for design and risk assessment.
17
17
  2. Validate contracts, data model, and failure handling.
18
18
  3. Implement backend changes with observability.
19
- 4. Run targeted tests and summarize rollout notes.
19
+ 4. Always update API docs: OpenAPI spec, Swagger UI route, and Stoplight Elements route/component.
20
+ 5. Run targeted tests and summarize rollout notes.
20
21
 
21
22
  ## Output Contract
22
23
  - API/contract changes
24
+ - OpenAPI spec path
25
+ - Swagger UI route
26
+ - Stoplight route/component status
23
27
  - Migration impact
24
28
  - Reliability/security notes
25
29
  - Verification evidence
@@ -2,7 +2,7 @@
2
2
  name: backend-specialist
3
3
  description: Expert backend architect for Node.js, Python, and modern serverless/edge systems. Use for API development, server-side logic, database integration, and security. Triggers on backend, server, api, endpoint, database, auth.
4
4
  tools: Read, Grep, Glob, Bash, Edit, Write
5
- skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
5
+ skills: clean-code, nodejs-best-practices, python-patterns, api-patterns, openapi-docs, database-design, database-optimizer, database-skills, mcp-builder, lint-and-validate, powershell-windows, bash-linux, rust-pro, api-designer, architecture-designer, typescript-pro, nestjs-expert, fastapi-expert, secure-code-guardian, test-master
6
6
  ---
7
7
 
8
8
  # Backend Development Architect
@@ -16,10 +16,14 @@ Use this when backend architecture or service logic is primary.
16
16
  1. Ask specialist(s) for design and risk assessment.
17
17
  2. Validate contracts, data model, and failure handling.
18
18
  3. Implement backend changes with observability.
19
- 4. Run targeted tests and summarize rollout notes.
19
+ 4. Always update API docs: OpenAPI spec, Swagger UI route, and Stoplight Elements route/component.
20
+ 5. Run targeted tests and summarize rollout notes.
20
21
 
21
22
  ## Output Contract
22
23
  - API/contract changes
24
+ - OpenAPI spec path
25
+ - Swagger UI route
26
+ - Stoplight route/component status
23
27
  - Migration impact
24
28
  - Reliability/security notes
25
29
  - Verification evidence
package/README.md CHANGED
@@ -34,10 +34,13 @@ cbx workflows doctor codex
34
34
  cbx workflows platforms
35
35
  cbx workflows install --platform antigravity --dry-run
36
36
  cbx workflows install --platform antigravity --terminal-integration --terminal-verifier codex
37
+ cbx workflows install --platform codex --postman
38
+ cbx workflows install --platform codex --postman --postman-workspace-id null
37
39
  ```
38
40
 
39
41
  Install bootstrap behavior:
40
42
  - `cbx workflows install` now also bootstraps `ENGINEERING_RULES.md` and `TECH.md` (creates when missing; keeps existing files unless explicitly regenerated).
43
+ - Optional `--postman` bootstrap creates `postman_setting.json` and installs/configures the Postman skill.
41
44
  - Use `cbx rules init --platform <platform> --overwrite` to force-regenerate both files.
42
45
 
43
46
  `rules` manages strict engineering policy and a generated codebase tech map:
@@ -112,6 +115,7 @@ Routing behavior:
112
115
  - Antigravity/Copilot: workflow + agent markdown can be routed by platform conventions.
113
116
  - Codex: use generated callable wrapper skills (`$workflow-*`, `$agent-*`).
114
117
  - Example for backend intent in Codex: `$workflow-backend` or `$agent-backend-specialist`.
118
+ - Backend workflow policy: always include OpenAPI updates plus Swagger UI and Stoplight Elements status in output.
115
119
 
116
120
  ### Codex Runtime Mode
117
121
 
package/bin/cubis.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { confirm, select } from "@inquirer/prompts";
3
+ import { confirm, input, select } from "@inquirer/prompts";
4
4
  import { Command } from "commander";
5
5
  import { existsSync } from "node:fs";
6
6
  import {
@@ -122,6 +122,12 @@ const CODEX_WORKFLOW_SKILL_PREFIX = "workflow-";
122
122
  const CODEX_AGENT_SKILL_PREFIX = "agent-";
123
123
  const TERMINAL_VERIFIER_PROVIDERS = ["codex", "gemini"];
124
124
  const DEFAULT_TERMINAL_VERIFIER = "codex";
125
+ const POSTMAN_API_KEY_ENV_VAR = "POSTMAN_API_KEY";
126
+ const POSTMAN_MCP_URL = "https://mcp.postman.com/minimal";
127
+ const POSTMAN_SKILL_ID = "postman";
128
+ const POSTMAN_SETTINGS_FILENAME = "postman_setting.json";
129
+ const POSTMAN_API_KEY_MISSING_WARNING =
130
+ `Postman API key is not configured. Set ${POSTMAN_API_KEY_ENV_VAR} or update ${POSTMAN_SETTINGS_FILENAME}.`;
125
131
  const TECH_SCAN_MAX_FILES = 5000;
126
132
  const TECH_SCAN_IGNORED_DIRS = new Set([
127
133
  ".git",
@@ -166,6 +172,54 @@ const TECH_LANGUAGE_BY_EXTENSION = new Map([
166
172
  [".sh", "Shell"],
167
173
  [".ps1", "PowerShell"]
168
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
+ ];
169
223
 
170
224
  function platformInstallsCustomAgents(platformId) {
171
225
  const profile = WORKFLOW_PROFILES[platformId];
@@ -231,6 +285,13 @@ function normalizeTerminalVerifier(value) {
231
285
  return TERMINAL_VERIFIER_ALIASES[normalized] || null;
232
286
  }
233
287
 
288
+ function normalizePostmanWorkspaceId(value) {
289
+ if (value === undefined || value === null) return null;
290
+ const normalized = String(value).trim();
291
+ if (!normalized || normalized.toLowerCase() === "null") return null;
292
+ return normalized;
293
+ }
294
+
234
295
  function defaultState() {
235
296
  return {
236
297
  schemaVersion: 1,
@@ -572,6 +633,198 @@ async function upsertEngineeringRulesBlock({
572
633
  };
573
634
  }
574
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
+
575
828
  async function collectTechSnapshot(rootDir) {
576
829
  const discoveredFiles = [];
577
830
  const queue = [rootDir];
@@ -601,6 +854,12 @@ async function collectTechSnapshot(rootDir) {
601
854
 
602
855
  const languageCounts = new Map();
603
856
  const topDirs = new Set();
857
+ const packageJsonFiles = [];
858
+ const pubspecFiles = [];
859
+ const goModFiles = [];
860
+ const pyprojectFiles = [];
861
+ const requirementsFiles = [];
862
+ const cargoTomlFiles = [];
604
863
 
605
864
  for (const fullPath of discoveredFiles) {
606
865
  const rel = toPosixPath(path.relative(rootDir, fullPath));
@@ -609,15 +868,31 @@ async function collectTechSnapshot(rootDir) {
609
868
 
610
869
  const extension = path.extname(fullPath).toLowerCase();
611
870
  const language = TECH_LANGUAGE_BY_EXTENSION.get(extension);
612
- if (!language) continue;
613
- 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
+ }
614
884
  }
615
885
 
616
886
  const fileExists = (name) => existsSync(path.join(rootDir, name));
617
- const packageJsonPath = path.join(rootDir, "package.json");
887
+ const rootPackageJsonPath = path.join(rootDir, "package.json");
618
888
  const packageScripts = new Map();
619
889
  const frameworks = new Set();
620
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();
621
896
 
622
897
  if (fileExists("bun.lock") || fileExists("bun.lockb")) lockfiles.push("bun");
623
898
  if (fileExists("pnpm-lock.yaml")) lockfiles.push("pnpm");
@@ -628,54 +903,118 @@ async function collectTechSnapshot(rootDir) {
628
903
  if (fileExists("go.sum")) lockfiles.push("go");
629
904
  if (fileExists("pubspec.lock")) lockfiles.push("pub");
630
905
 
631
- if (fileExists("pubspec.yaml")) frameworks.add("Flutter");
632
- if (fileExists("go.mod")) frameworks.add("Go Modules");
633
- if (fileExists("Cargo.toml")) frameworks.add("Rust Cargo");
634
- 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");
635
910
 
636
- if (existsSync(packageJsonPath)) {
911
+ for (const packageJsonFile of packageJsonFiles) {
637
912
  try {
638
- const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
639
- const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
640
- for (const [name, command] of Object.entries(scripts)) {
641
- if (typeof command !== "string") continue;
642
- packageScripts.set(name, command);
643
- }
644
-
913
+ const parsed = JSON.parse(await readFile(packageJsonFile, "utf8"));
645
914
  const deps = {
646
915
  ...(parsed.dependencies || {}),
647
916
  ...(parsed.devDependencies || {}),
648
- ...(parsed.peerDependencies || {})
917
+ ...(parsed.peerDependencies || {}),
918
+ ...(parsed.optionalDependencies || {})
649
919
  };
650
- const depNames = new Set(Object.keys(deps));
651
- const frameworkSignals = [
652
- ["next", "Next.js"],
653
- ["react", "React"],
654
- ["vue", "Vue"],
655
- ["nuxt", "Nuxt"],
656
- ["svelte", "Svelte"],
657
- ["@nestjs/core", "NestJS"],
658
- ["express", "Express"],
659
- ["fastify", "Fastify"],
660
- ["hono", "Hono"],
661
- ["tailwindcss", "Tailwind CSS"],
662
- ["prisma", "Prisma"],
663
- ["drizzle-orm", "Drizzle ORM"],
664
- ["mongoose", "Mongoose"],
665
- ["typeorm", "TypeORM"],
666
- ["@playwright/test", "Playwright"],
667
- ["vitest", "Vitest"],
668
- ["jest", "Jest"],
669
- ["cypress", "Cypress"]
670
- ];
671
- for (const [signal, label] of frameworkSignals) {
672
- 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
+ }
673
931
  }
674
932
  } catch {
675
933
  // ignore malformed package.json
676
934
  }
677
935
  }
678
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
+
679
1018
  const sortedLanguages = [...languageCounts.entries()].sort((a, b) => b[1] - a[1]);
680
1019
  const sortedFrameworks = [...frameworks].sort((a, b) => a.localeCompare(b));
681
1020
  const sortedTopDirs = [...topDirs].sort((a, b) => a.localeCompare(b)).slice(0, 12);
@@ -704,10 +1043,33 @@ async function collectTechSnapshot(rootDir) {
704
1043
  frameworks: sortedFrameworks,
705
1044
  lockfiles: sortedLockfiles,
706
1045
  topDirs: sortedTopDirs,
707
- 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
+ }
708
1054
  };
709
1055
  }
710
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
+
711
1073
  function buildTechMd(snapshot) {
712
1074
  const lines = [];
713
1075
  lines.push("# TECH.md");
@@ -737,6 +1099,13 @@ function buildTechMd(snapshot) {
737
1099
  }
738
1100
  lines.push("");
739
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
+
740
1109
  lines.push("## Tooling and Lockfiles");
741
1110
  if (snapshot.lockfiles.length === 0) {
742
1111
  lines.push("- No lockfiles detected.");
@@ -1872,6 +2241,237 @@ async function writeGeneratedArtifact({ destination, content, dryRun = false })
1872
2241
  return { action: exists ? "replaced" : "installed", path: destination };
1873
2242
  }
1874
2243
 
2244
+ function resolvePostmanSettingsPath({ scope, cwd = process.cwd() }) {
2245
+ if (scope === "global") {
2246
+ return path.join(os.homedir(), ".cbx", POSTMAN_SETTINGS_FILENAME);
2247
+ }
2248
+ const workspaceRoot = findWorkspaceRoot(cwd);
2249
+ return path.join(workspaceRoot, POSTMAN_SETTINGS_FILENAME);
2250
+ }
2251
+
2252
+ function buildPostmanMcpConfig({ apiKey = null, mcpUrl = POSTMAN_MCP_URL }) {
2253
+ const authHeader = apiKey
2254
+ ? `Bearer ${apiKey}`
2255
+ : `Bearer \${${POSTMAN_API_KEY_ENV_VAR}}`;
2256
+ return {
2257
+ mcpServers: {
2258
+ postman: {
2259
+ url: mcpUrl,
2260
+ headers: {
2261
+ Authorization: authHeader
2262
+ }
2263
+ }
2264
+ },
2265
+ disabled: false
2266
+ };
2267
+ }
2268
+
2269
+ function getPostmanApiKeySource({ apiKey, envApiKey }) {
2270
+ if (apiKey) return "inline";
2271
+ if (envApiKey) return "env";
2272
+ return "unset";
2273
+ }
2274
+
2275
+ function normalizePostmanApiKey(value) {
2276
+ if (value === undefined || value === null) return null;
2277
+ const normalized = String(value).trim();
2278
+ return normalized || null;
2279
+ }
2280
+
2281
+ async function ensureGitIgnoreEntry({
2282
+ filePath,
2283
+ entry,
2284
+ dryRun = false
2285
+ }) {
2286
+ const exists = await pathExists(filePath);
2287
+ const original = exists ? await readFile(filePath, "utf8") : "";
2288
+ const lines = original.split(/\r?\n/).map((line) => line.trim());
2289
+ const alreadyPresent = lines.includes(entry);
2290
+
2291
+ if (alreadyPresent) {
2292
+ return { action: "unchanged", filePath };
2293
+ }
2294
+
2295
+ if (dryRun) {
2296
+ return {
2297
+ action: exists ? "would-patch" : "would-create",
2298
+ filePath
2299
+ };
2300
+ }
2301
+
2302
+ const suffix = original.endsWith("\n") || original.length === 0 ? "" : "\n";
2303
+ const nextContent = `${original}${suffix}${entry}\n`;
2304
+ await mkdir(path.dirname(filePath), { recursive: true });
2305
+ await writeFile(filePath, nextContent, "utf8");
2306
+ return {
2307
+ action: exists ? "patched" : "created",
2308
+ filePath
2309
+ };
2310
+ }
2311
+
2312
+ async function resolvePostmanInstallSelection({
2313
+ scope,
2314
+ options,
2315
+ cwd = process.cwd()
2316
+ }) {
2317
+ const hasApiKeyOption = options.postmanApiKey !== undefined;
2318
+ const hasWorkspaceOption = options.postmanWorkspaceId !== undefined;
2319
+ const enabled = Boolean(options.postman) || hasApiKeyOption || hasWorkspaceOption;
2320
+ if (!enabled) return { enabled: false };
2321
+
2322
+ const explicitApiKey = hasApiKeyOption ? String(options.postmanApiKey || "").trim() : "";
2323
+ let apiKey = explicitApiKey || null;
2324
+ const envApiKey = String(process.env[POSTMAN_API_KEY_ENV_VAR] || "").trim();
2325
+ let defaultWorkspaceId = hasWorkspaceOption
2326
+ ? normalizePostmanWorkspaceId(options.postmanWorkspaceId)
2327
+ : null;
2328
+ const warnings = [];
2329
+
2330
+ const canPrompt = !options.yes && process.stdin.isTTY;
2331
+ if (canPrompt && !hasApiKeyOption && !apiKey && !envApiKey) {
2332
+ const promptedApiKey = String(
2333
+ await input({
2334
+ message: `Postman API key (optional, leave blank to keep ${POSTMAN_API_KEY_ENV_VAR} env mode):`,
2335
+ default: ""
2336
+ })
2337
+ ).trim();
2338
+ if (promptedApiKey) {
2339
+ apiKey = promptedApiKey;
2340
+ }
2341
+ }
2342
+
2343
+ if (canPrompt && !hasWorkspaceOption) {
2344
+ const promptedWorkspaceId = await input({
2345
+ message: "Default Postman workspace ID (optional, leave blank for null):",
2346
+ default: ""
2347
+ });
2348
+ defaultWorkspaceId = normalizePostmanWorkspaceId(promptedWorkspaceId);
2349
+ }
2350
+
2351
+ const apiKeySource = getPostmanApiKeySource({ apiKey, envApiKey });
2352
+ if (apiKeySource === "unset") {
2353
+ warnings.push(POSTMAN_API_KEY_MISSING_WARNING);
2354
+ }
2355
+
2356
+ const settingsPath = resolvePostmanSettingsPath({ scope, cwd });
2357
+ const settings = {
2358
+ apiKey: apiKey || null,
2359
+ apiKeyEnvVar: POSTMAN_API_KEY_ENV_VAR,
2360
+ apiKeySource,
2361
+ defaultWorkspaceId: defaultWorkspaceId ?? null,
2362
+ mcpUrl: POSTMAN_MCP_URL,
2363
+ generatedBy: "cbx workflows install --postman",
2364
+ generatedAt: new Date().toISOString()
2365
+ };
2366
+
2367
+ return {
2368
+ enabled: true,
2369
+ apiKey,
2370
+ apiKeySource,
2371
+ defaultWorkspaceId: defaultWorkspaceId ?? null,
2372
+ warnings,
2373
+ settings,
2374
+ settingsPath
2375
+ };
2376
+ }
2377
+
2378
+ async function configurePostmanInstallArtifacts({
2379
+ scope,
2380
+ profilePaths,
2381
+ postmanSelection,
2382
+ overwrite = false,
2383
+ dryRun = false,
2384
+ cwd = process.cwd()
2385
+ }) {
2386
+ if (!postmanSelection?.enabled) return null;
2387
+
2388
+ let warnings = postmanSelection.warnings.filter((warning) => warning !== POSTMAN_API_KEY_MISSING_WARNING);
2389
+ const settingsContent = `${JSON.stringify(postmanSelection.settings, null, 2)}\n`;
2390
+ const settingsResult = await writeTextFile({
2391
+ targetPath: postmanSelection.settingsPath,
2392
+ content: settingsContent,
2393
+ overwrite,
2394
+ dryRun
2395
+ });
2396
+
2397
+ let effectiveApiKey = normalizePostmanApiKey(postmanSelection.settings.apiKey);
2398
+ let effectiveDefaultWorkspaceId = postmanSelection.defaultWorkspaceId ?? null;
2399
+ let effectiveMcpUrl = postmanSelection.settings.mcpUrl || POSTMAN_MCP_URL;
2400
+
2401
+ if (settingsResult.action === "skipped" || settingsResult.action === "would-skip") {
2402
+ try {
2403
+ const existingSettingsRaw = await readFile(postmanSelection.settingsPath, "utf8");
2404
+ const existingSettings = JSON.parse(existingSettingsRaw);
2405
+ effectiveApiKey = normalizePostmanApiKey(existingSettings?.apiKey);
2406
+ effectiveDefaultWorkspaceId = normalizePostmanWorkspaceId(existingSettings?.defaultWorkspaceId);
2407
+ const existingMcpUrl = String(existingSettings?.mcpUrl || "").trim();
2408
+ if (existingMcpUrl) {
2409
+ effectiveMcpUrl = existingMcpUrl;
2410
+ }
2411
+ } catch {
2412
+ warnings.push(
2413
+ `Existing ${POSTMAN_SETTINGS_FILENAME} could not be parsed. Using install-time Postman values for MCP config.`
2414
+ );
2415
+ }
2416
+ }
2417
+
2418
+ const envApiKey = normalizePostmanApiKey(process.env[POSTMAN_API_KEY_ENV_VAR]);
2419
+ const effectiveApiKeySource = getPostmanApiKeySource({
2420
+ apiKey: effectiveApiKey,
2421
+ envApiKey
2422
+ });
2423
+ if (effectiveApiKeySource === "unset") {
2424
+ warnings.push(POSTMAN_API_KEY_MISSING_WARNING);
2425
+ }
2426
+
2427
+ let gitIgnoreResult = null;
2428
+ if (scope === "project") {
2429
+ const workspaceRoot = findWorkspaceRoot(cwd);
2430
+ const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
2431
+ gitIgnoreResult = await ensureGitIgnoreEntry({
2432
+ filePath: gitIgnorePath,
2433
+ entry: POSTMAN_SETTINGS_FILENAME,
2434
+ dryRun
2435
+ });
2436
+ }
2437
+
2438
+ const postmanSkillDir = path.join(profilePaths.skillsDir, POSTMAN_SKILL_ID);
2439
+ const postmanMcpPath = path.join(postmanSkillDir, "mcp.json");
2440
+ let mcpResult = null;
2441
+ const postmanSkillExists = await pathExists(postmanSkillDir);
2442
+ if (!dryRun && !postmanSkillExists) {
2443
+ mcpResult = {
2444
+ action: "missing-postman-skill",
2445
+ path: postmanMcpPath
2446
+ };
2447
+ } else {
2448
+ const mcpConfigContent = `${JSON.stringify(
2449
+ buildPostmanMcpConfig({
2450
+ apiKey: effectiveApiKey,
2451
+ mcpUrl: effectiveMcpUrl
2452
+ }),
2453
+ null,
2454
+ 2
2455
+ )}\n`;
2456
+ mcpResult = await writeGeneratedArtifact({
2457
+ destination: postmanMcpPath,
2458
+ content: mcpConfigContent,
2459
+ dryRun
2460
+ });
2461
+ }
2462
+
2463
+ return {
2464
+ enabled: true,
2465
+ apiKeySource: effectiveApiKeySource,
2466
+ defaultWorkspaceId: effectiveDefaultWorkspaceId,
2467
+ warnings,
2468
+ settingsPath: postmanSelection.settingsPath,
2469
+ settingsResult,
2470
+ gitIgnoreResult,
2471
+ mcpResult
2472
+ };
2473
+ }
2474
+
1875
2475
  async function installAntigravityTerminalIntegrationArtifacts({
1876
2476
  profilePaths,
1877
2477
  provider,
@@ -2032,6 +2632,7 @@ async function installBundleArtifacts({
2032
2632
  platform,
2033
2633
  scope,
2034
2634
  overwrite,
2635
+ extraSkillIds = [],
2035
2636
  terminalVerifierSelection = null,
2036
2637
  dryRun = false,
2037
2638
  cwd = process.cwd()
@@ -2092,7 +2693,8 @@ async function installBundleArtifacts({
2092
2693
  else installed.push(destination);
2093
2694
  }
2094
2695
 
2095
- const skillIds = Array.isArray(platformSpec.skills) ? platformSpec.skills : [];
2696
+ const manifestSkillIds = Array.isArray(platformSpec.skills) ? platformSpec.skills : [];
2697
+ const skillIds = unique([...manifestSkillIds, ...extraSkillIds.filter(Boolean)]);
2096
2698
  for (const skillId of skillIds) {
2097
2699
  const source = path.join(agentAssetsRoot(), "skills", skillId);
2098
2700
  const destination = path.join(profilePaths.skillsDir, skillId);
@@ -2389,6 +2991,32 @@ function printInstallSummary({
2389
2991
  }
2390
2992
  }
2391
2993
 
2994
+ function printPostmanSetupSummary({ postmanSetup }) {
2995
+ if (!postmanSetup?.enabled) return;
2996
+
2997
+ console.log("\nPostman setup:");
2998
+ console.log(`- Settings file: ${postmanSetup.settingsResult.action} (${postmanSetup.settingsPath})`);
2999
+ console.log(`- API key source: ${postmanSetup.apiKeySource}`);
3000
+ console.log(
3001
+ `- Default workspace ID: ${postmanSetup.defaultWorkspaceId === null ? "null" : postmanSetup.defaultWorkspaceId}`
3002
+ );
3003
+ if (postmanSetup.gitIgnoreResult) {
3004
+ console.log(
3005
+ `- .gitignore (${postmanSetup.gitIgnoreResult.filePath}): ${postmanSetup.gitIgnoreResult.action}`
3006
+ );
3007
+ }
3008
+ if (postmanSetup.mcpResult) {
3009
+ console.log(`- Postman MCP config (${postmanSetup.mcpResult.path}): ${postmanSetup.mcpResult.action}`);
3010
+ }
3011
+
3012
+ if (postmanSetup.warnings.length > 0) {
3013
+ console.log("- Warnings:");
3014
+ for (const warning of postmanSetup.warnings) {
3015
+ console.log(` - ${warning}`);
3016
+ }
3017
+ }
3018
+ }
3019
+
2392
3020
  function printRemoveSummary({
2393
3021
  platform,
2394
3022
  scope,
@@ -2694,6 +3322,18 @@ function withInstallOptions(command) {
2694
3322
  return withWorkflowBaseOptions(command)
2695
3323
  .option("-b, --bundle <bundle>", "bundle id (default: agent-environment-setup)")
2696
3324
  .option("--overwrite", "overwrite existing files")
3325
+ .option(
3326
+ "--postman",
3327
+ "optional: install Postman skill and generate postman_setting.json"
3328
+ )
3329
+ .option(
3330
+ "--postman-api-key <key>",
3331
+ "optional: set Postman API key inline for generated postman_setting.json and installed Postman MCP config"
3332
+ )
3333
+ .option(
3334
+ "--postman-workspace-id <id|null>",
3335
+ "optional: set default Postman workspace ID (use 'null' for no default)"
3336
+ )
2697
3337
  .option(
2698
3338
  "--terminal-integration",
2699
3339
  "Antigravity only: enable terminal verification integration (prompts for verifier when interactive)"
@@ -2854,6 +3494,11 @@ async function runWorkflowInstall(options) {
2854
3494
  platform,
2855
3495
  options
2856
3496
  });
3497
+ const postmanSelection = await resolvePostmanInstallSelection({
3498
+ scope,
3499
+ options,
3500
+ cwd: process.cwd()
3501
+ });
2857
3502
 
2858
3503
  const installResult = await installBundleArtifacts({
2859
3504
  bundleId,
@@ -2861,6 +3506,7 @@ async function runWorkflowInstall(options) {
2861
3506
  platform,
2862
3507
  scope,
2863
3508
  overwrite: Boolean(options.overwrite),
3509
+ extraSkillIds: postmanSelection.enabled ? [POSTMAN_SKILL_ID] : [],
2864
3510
  terminalVerifierSelection,
2865
3511
  dryRun,
2866
3512
  cwd: process.cwd()
@@ -2890,6 +3536,14 @@ async function runWorkflowInstall(options) {
2890
3536
  skipTech: false,
2891
3537
  cwd: process.cwd()
2892
3538
  });
3539
+ const postmanSetupResult = await configurePostmanInstallArtifacts({
3540
+ scope,
3541
+ profilePaths: installResult.profilePaths,
3542
+ postmanSelection,
3543
+ overwrite: Boolean(options.overwrite),
3544
+ dryRun,
3545
+ cwd: process.cwd()
3546
+ });
2893
3547
 
2894
3548
  const terminalVerificationRuleResult =
2895
3549
  platform === "antigravity" && installResult.terminalIntegration
@@ -2930,6 +3584,9 @@ async function runWorkflowInstall(options) {
2930
3584
  engineeringResults: engineeringArtifactsResult.engineeringResults,
2931
3585
  techResult: engineeringArtifactsResult.techResult
2932
3586
  });
3587
+ printPostmanSetupSummary({
3588
+ postmanSetup: postmanSetupResult
3589
+ });
2933
3590
  if (dryRun) {
2934
3591
  console.log("\nDry-run complete. Re-run without `--dry-run` to apply changes.");
2935
3592
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.13",
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",