scc-universal 1.1.0

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 (271) hide show
  1. package/.claude-plugin/plugin.json +44 -0
  2. package/.cursor/agents/deep-researcher.md +142 -0
  3. package/.cursor/agents/doc-updater.md +219 -0
  4. package/.cursor/agents/eval-runner.md +335 -0
  5. package/.cursor/agents/learning-engine.md +210 -0
  6. package/.cursor/agents/loop-operator.md +245 -0
  7. package/.cursor/agents/refactor-cleaner.md +119 -0
  8. package/.cursor/agents/sf-admin-agent.md +127 -0
  9. package/.cursor/agents/sf-agentforce-agent.md +126 -0
  10. package/.cursor/agents/sf-apex-agent.md +117 -0
  11. package/.cursor/agents/sf-architect.md +426 -0
  12. package/.cursor/agents/sf-aura-reviewer.md +369 -0
  13. package/.cursor/agents/sf-bugfix-agent.md +101 -0
  14. package/.cursor/agents/sf-flow-agent.md +155 -0
  15. package/.cursor/agents/sf-integration-agent.md +141 -0
  16. package/.cursor/agents/sf-lwc-agent.md +123 -0
  17. package/.cursor/agents/sf-review-agent.md +357 -0
  18. package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
  19. package/.cursor/hooks/adapter.js +81 -0
  20. package/.cursor/hooks/after-file-edit.js +26 -0
  21. package/.cursor/hooks/after-mcp-execution.js +12 -0
  22. package/.cursor/hooks/after-shell-execution.js +30 -0
  23. package/.cursor/hooks/after-tab-file-edit.js +12 -0
  24. package/.cursor/hooks/before-mcp-execution.js +11 -0
  25. package/.cursor/hooks/before-read-file.js +13 -0
  26. package/.cursor/hooks/before-shell-execution.js +29 -0
  27. package/.cursor/hooks/before-submit-prompt.js +23 -0
  28. package/.cursor/hooks/pre-compact.js +7 -0
  29. package/.cursor/hooks/session-end.js +10 -0
  30. package/.cursor/hooks/session-start.js +10 -0
  31. package/.cursor/hooks/stop.js +18 -0
  32. package/.cursor/hooks/subagent-start.js +10 -0
  33. package/.cursor/hooks/subagent-stop.js +10 -0
  34. package/.cursor/hooks.json +107 -0
  35. package/.cursor/skills/aside/SKILL.md +115 -0
  36. package/.cursor/skills/checkpoint/SKILL.md +50 -0
  37. package/.cursor/skills/configure-scc/SKILL.md +160 -0
  38. package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
  39. package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
  40. package/.cursor/skills/model-route/SKILL.md +81 -0
  41. package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
  42. package/.cursor/skills/refactor-clean/SKILL.md +133 -0
  43. package/.cursor/skills/resume-session/SKILL.md +111 -0
  44. package/.cursor/skills/save-session/SKILL.md +183 -0
  45. package/.cursor/skills/search-first/SKILL.md +140 -0
  46. package/.cursor/skills/security-scan/SKILL.md +142 -0
  47. package/.cursor/skills/sessions/SKILL.md +124 -0
  48. package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
  49. package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
  50. package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
  51. package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
  52. package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
  53. package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
  54. package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
  55. package/.cursor/skills/sf-api-design/SKILL.md +237 -0
  56. package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
  57. package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
  58. package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
  59. package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
  60. package/.cursor/skills/sf-debugging/SKILL.md +362 -0
  61. package/.cursor/skills/sf-deployment/SKILL.md +291 -0
  62. package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
  63. package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
  64. package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
  65. package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
  66. package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
  67. package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
  68. package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
  69. package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
  70. package/.cursor/skills/sf-help/SKILL.md +156 -0
  71. package/.cursor/skills/sf-integration/SKILL.md +479 -0
  72. package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
  73. package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
  74. package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
  75. package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
  76. package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
  77. package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
  78. package/.cursor/skills/sf-security/SKILL.md +330 -0
  79. package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
  80. package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
  81. package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
  82. package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
  83. package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
  84. package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
  85. package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
  86. package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
  87. package/.cursor/skills/strategic-compact/SKILL.md +205 -0
  88. package/.cursor/skills/update-docs/SKILL.md +162 -0
  89. package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
  90. package/.cursor-plugin/plugin.json +26 -0
  91. package/LICENSE +21 -0
  92. package/README.md +522 -0
  93. package/agents/deep-researcher.md +145 -0
  94. package/agents/doc-updater.md +222 -0
  95. package/agents/eval-runner.md +340 -0
  96. package/agents/learning-engine.md +211 -0
  97. package/agents/loop-operator.md +247 -0
  98. package/agents/refactor-cleaner.md +122 -0
  99. package/agents/sf-admin-agent.md +131 -0
  100. package/agents/sf-agentforce-agent.md +132 -0
  101. package/agents/sf-apex-agent.md +124 -0
  102. package/agents/sf-architect.md +435 -0
  103. package/agents/sf-aura-reviewer.md +372 -0
  104. package/agents/sf-bugfix-agent.md +105 -0
  105. package/agents/sf-flow-agent.md +159 -0
  106. package/agents/sf-integration-agent.md +146 -0
  107. package/agents/sf-lwc-agent.md +127 -0
  108. package/agents/sf-review-agent.md +366 -0
  109. package/agents/sf-visualforce-reviewer.md +468 -0
  110. package/assets/logo.svg +18 -0
  111. package/docs/ARCHITECTURE.md +133 -0
  112. package/docs/authoring-guide.md +373 -0
  113. package/docs/hook-development.md +578 -0
  114. package/docs/token-optimization.md +139 -0
  115. package/docs/workflow-examples.md +645 -0
  116. package/examples/agentforce-action/README.md +227 -0
  117. package/examples/apex-trigger-handler/README.md +114 -0
  118. package/examples/devops-pipeline/README.md +325 -0
  119. package/examples/flow-automation/README.md +188 -0
  120. package/examples/integration-pattern/README.md +416 -0
  121. package/examples/lwc-component/README.md +180 -0
  122. package/examples/platform-events/README.md +492 -0
  123. package/examples/scratch-org-setup/README.md +138 -0
  124. package/examples/security-audit/README.md +244 -0
  125. package/examples/visualforce-migration/README.md +314 -0
  126. package/hooks/hooks.json +338 -0
  127. package/hooks/memory-persistence/README.md +73 -0
  128. package/manifests/install-modules.json +217 -0
  129. package/manifests/install-profiles.json +17 -0
  130. package/mcp-configs/mcp-servers.json +19 -0
  131. package/package.json +89 -0
  132. package/schemas/hooks.schema.json +123 -0
  133. package/schemas/install-modules.schema.json +76 -0
  134. package/schemas/install-profiles.schema.json +28 -0
  135. package/schemas/install-state.schema.json +73 -0
  136. package/schemas/package-manager.schema.json +18 -0
  137. package/schemas/plugin.schema.json +112 -0
  138. package/schemas/scc-install-config.schema.json +29 -0
  139. package/schemas/state-store.schema.json +111 -0
  140. package/scripts/cli/install-apply.js +170 -0
  141. package/scripts/cli/uninstall.js +193 -0
  142. package/scripts/hooks/check-console-log.js +101 -0
  143. package/scripts/hooks/check-hook-enabled.js +17 -0
  144. package/scripts/hooks/check-platform-docs-age.js +48 -0
  145. package/scripts/hooks/cost-tracker.js +78 -0
  146. package/scripts/hooks/doc-file-warning.js +63 -0
  147. package/scripts/hooks/evaluate-session.js +98 -0
  148. package/scripts/hooks/governor-check.js +220 -0
  149. package/scripts/hooks/learning-observe.sh +206 -0
  150. package/scripts/hooks/mcp-health-check.js +588 -0
  151. package/scripts/hooks/post-bash-build-complete.js +34 -0
  152. package/scripts/hooks/post-bash-pr-created.js +43 -0
  153. package/scripts/hooks/post-edit-console-warn.js +61 -0
  154. package/scripts/hooks/post-edit-format.js +79 -0
  155. package/scripts/hooks/post-edit-typecheck.js +98 -0
  156. package/scripts/hooks/post-write.js +168 -0
  157. package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
  158. package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
  159. package/scripts/hooks/pre-compact.js +51 -0
  160. package/scripts/hooks/pre-tool-use.js +163 -0
  161. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  162. package/scripts/hooks/quality-gate.js +251 -0
  163. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  164. package/scripts/hooks/run-with-flags.js +135 -0
  165. package/scripts/hooks/session-end-marker.js +29 -0
  166. package/scripts/hooks/session-end.js +311 -0
  167. package/scripts/hooks/session-start.js +202 -0
  168. package/scripts/hooks/sfdx-scanner-check.js +142 -0
  169. package/scripts/hooks/sfdx-validate.js +119 -0
  170. package/scripts/hooks/stop-hook.js +170 -0
  171. package/scripts/hooks/suggest-compact.js +67 -0
  172. package/scripts/lib/agent-adapter.js +82 -0
  173. package/scripts/lib/apex-analysis.js +194 -0
  174. package/scripts/lib/hook-flags.js +74 -0
  175. package/scripts/lib/install-config.js +73 -0
  176. package/scripts/lib/install-executor.js +363 -0
  177. package/scripts/lib/install-state.js +121 -0
  178. package/scripts/lib/orchestration-session.js +299 -0
  179. package/scripts/lib/package-manager.js +124 -0
  180. package/scripts/lib/project-detect.js +228 -0
  181. package/scripts/lib/schema-validator.js +190 -0
  182. package/scripts/lib/skill-adapter.js +100 -0
  183. package/scripts/lib/state-store.js +376 -0
  184. package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
  185. package/scripts/lib/utils.js +313 -0
  186. package/scripts/scc.js +164 -0
  187. package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
  188. package/skills/_reference/APEX_CURSOR.md +159 -0
  189. package/skills/_reference/API_VERSIONS.md +78 -0
  190. package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
  191. package/skills/_reference/ASYNC_PATTERNS.md +163 -0
  192. package/skills/_reference/AURA_COMPONENTS.md +146 -0
  193. package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
  194. package/skills/_reference/DATA_MODELING.md +124 -0
  195. package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
  196. package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
  197. package/skills/_reference/DEPRECATIONS.md +79 -0
  198. package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
  199. package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
  200. package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
  201. package/skills/_reference/FLOW_PATTERNS.md +113 -0
  202. package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
  203. package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
  204. package/skills/_reference/LWC_PATTERNS.md +79 -0
  205. package/skills/_reference/METADATA_TYPES.md +115 -0
  206. package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
  207. package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
  208. package/skills/_reference/PLATFORM_EVENTS.md +121 -0
  209. package/skills/_reference/REPORTING_API.md +143 -0
  210. package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
  211. package/skills/_reference/SECURITY_PATTERNS.md +127 -0
  212. package/skills/_reference/SHARING_MODEL.md +120 -0
  213. package/skills/_reference/SOQL_PATTERNS.md +119 -0
  214. package/skills/_reference/TESTING_STANDARDS.md +96 -0
  215. package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
  216. package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
  217. package/skills/aside/SKILL.md +118 -0
  218. package/skills/checkpoint/SKILL.md +53 -0
  219. package/skills/configure-scc/SKILL.md +163 -0
  220. package/skills/continuous-agent-loop/SKILL.md +264 -0
  221. package/skills/mcp-server-patterns/SKILL.md +146 -0
  222. package/skills/model-route/SKILL.md +84 -0
  223. package/skills/prompt-optimizer/SKILL.md +369 -0
  224. package/skills/refactor-clean/SKILL.md +136 -0
  225. package/skills/resume-session/SKILL.md +114 -0
  226. package/skills/save-session/SKILL.md +186 -0
  227. package/skills/search-first/SKILL.md +144 -0
  228. package/skills/security-scan/SKILL.md +146 -0
  229. package/skills/sessions/SKILL.md +127 -0
  230. package/skills/sf-agentforce-development/SKILL.md +450 -0
  231. package/skills/sf-apex-async-patterns/SKILL.md +326 -0
  232. package/skills/sf-apex-best-practices/SKILL.md +425 -0
  233. package/skills/sf-apex-constraints/SKILL.md +81 -0
  234. package/skills/sf-apex-cursor/SKILL.md +338 -0
  235. package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
  236. package/skills/sf-apex-testing/SKILL.md +409 -0
  237. package/skills/sf-api-design/SKILL.md +238 -0
  238. package/skills/sf-approval-processes/SKILL.md +315 -0
  239. package/skills/sf-aura-development/SKILL.md +263 -0
  240. package/skills/sf-build-fix/SKILL.md +121 -0
  241. package/skills/sf-data-modeling/SKILL.md +278 -0
  242. package/skills/sf-debugging/SKILL.md +363 -0
  243. package/skills/sf-deployment/SKILL.md +295 -0
  244. package/skills/sf-deployment-constraints/SKILL.md +155 -0
  245. package/skills/sf-devops-ci-cd/SKILL.md +325 -0
  246. package/skills/sf-docs-lookup/SKILL.md +103 -0
  247. package/skills/sf-e2e-testing/SKILL.md +324 -0
  248. package/skills/sf-experience-cloud/SKILL.md +249 -0
  249. package/skills/sf-flow-development/SKILL.md +377 -0
  250. package/skills/sf-governor-limits/SKILL.md +323 -0
  251. package/skills/sf-harness-audit/SKILL.md +142 -0
  252. package/skills/sf-help/SKILL.md +159 -0
  253. package/skills/sf-integration/SKILL.md +483 -0
  254. package/skills/sf-lwc-constraints/SKILL.md +130 -0
  255. package/skills/sf-lwc-development/SKILL.md +303 -0
  256. package/skills/sf-lwc-testing/SKILL.md +388 -0
  257. package/skills/sf-metadata-management/SKILL.md +288 -0
  258. package/skills/sf-platform-events-cdc/SKILL.md +375 -0
  259. package/skills/sf-quickstart/SKILL.md +173 -0
  260. package/skills/sf-security/SKILL.md +334 -0
  261. package/skills/sf-security-constraints/SKILL.md +127 -0
  262. package/skills/sf-soql-constraints/SKILL.md +131 -0
  263. package/skills/sf-soql-optimization/SKILL.md +354 -0
  264. package/skills/sf-tdd-workflow/SKILL.md +336 -0
  265. package/skills/sf-testing-constraints/SKILL.md +200 -0
  266. package/skills/sf-trigger-constraints/SKILL.md +90 -0
  267. package/skills/sf-trigger-frameworks/SKILL.md +347 -0
  268. package/skills/sf-visualforce-development/SKILL.md +260 -0
  269. package/skills/strategic-compact/SKILL.md +208 -0
  270. package/skills/update-docs/SKILL.md +165 -0
  271. package/skills/update-platform-docs/SKILL.md +90 -0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared hook enable/disable controls.
4
+ *
5
+ * Controls:
6
+ * - SCC_HOOK_PROFILE=minimal|standard|strict (default: standard)
7
+ * - SCC_DISABLED_HOOKS=comma,separated,hook,ids
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const VALID_PROFILES = new Set(['minimal', 'standard', 'strict']);
13
+
14
+ function normalizeId(value) {
15
+ return String(value || '').trim().toLowerCase();
16
+ }
17
+
18
+ function getHookProfile() {
19
+ const raw = String(process.env.SCC_HOOK_PROFILE || 'standard').trim().toLowerCase();
20
+ return VALID_PROFILES.has(raw) ? raw : 'standard';
21
+ }
22
+
23
+ function getDisabledHookIds() {
24
+ const raw = String(process.env.SCC_DISABLED_HOOKS || '');
25
+ if (!raw.trim()) return new Set();
26
+
27
+ return new Set(
28
+ raw
29
+ .split(',')
30
+ .map(v => normalizeId(v))
31
+ .filter(Boolean)
32
+ );
33
+ }
34
+
35
+ function parseProfiles(rawProfiles, fallback = ['standard', 'strict']) {
36
+ if (!rawProfiles) return [...fallback];
37
+
38
+ if (Array.isArray(rawProfiles)) {
39
+ const parsed = rawProfiles
40
+ .map(v => String(v || '').trim().toLowerCase())
41
+ .filter(v => VALID_PROFILES.has(v));
42
+ return parsed.length > 0 ? parsed : [...fallback];
43
+ }
44
+
45
+ const parsed = String(rawProfiles)
46
+ .split(',')
47
+ .map(v => v.trim().toLowerCase())
48
+ .filter(v => VALID_PROFILES.has(v));
49
+
50
+ return parsed.length > 0 ? parsed : [...fallback];
51
+ }
52
+
53
+ function isHookEnabled(hookId, options = {}) {
54
+ const id = normalizeId(hookId);
55
+ if (!id) return true;
56
+
57
+ const disabled = getDisabledHookIds();
58
+ if (disabled.has(id)) {
59
+ return false;
60
+ }
61
+
62
+ const profile = getHookProfile();
63
+ const allowedProfiles = parseProfiles(options.profiles);
64
+ return allowedProfiles.includes(profile);
65
+ }
66
+
67
+ module.exports = {
68
+ VALID_PROFILES,
69
+ normalizeId,
70
+ getHookProfile,
71
+ getDisabledHookIds,
72
+ parseProfiles,
73
+ isHookEnabled,
74
+ };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * install-config.js — Loads and validates SCC install config files.
5
+ *
6
+ * Config files (scc-install.json) let teams commit install preferences to their repo.
7
+ * Read-only — SCC never writes config files. CLI args override config values.
8
+ *
9
+ * Validates against schemas/scc-install-config.schema.json via AJV.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { assertAgainstSchema, formatErrors } = require('./schema-validator');
15
+
16
+ const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schemas', 'scc-install-config.schema.json');
17
+
18
+ /**
19
+ * Deduplicate an array of strings while preserving order.
20
+ */
21
+ function dedupeStrings(arr) {
22
+ return [...new Set(arr)];
23
+ }
24
+
25
+ /**
26
+ * Resolve a config file path. Relative paths resolve against cwd.
27
+ *
28
+ * @param {string} configPath - path to config file
29
+ * @returns {string} resolved absolute path
30
+ */
31
+ function resolveConfigPath(configPath) {
32
+ if (!configPath) throw new Error('Config path is required');
33
+ return path.resolve(configPath);
34
+ }
35
+
36
+ /**
37
+ * Load and validate an SCC install config file.
38
+ *
39
+ * @param {string} configPath - path to scc-install.json
40
+ * @returns {Object} normalized config: { path, version, target, profile, modules, include, exclude, options }
41
+ * @throws {Error} if file not found, invalid JSON, or schema validation fails
42
+ */
43
+ function loadInstallConfig(configPath) {
44
+ const resolved = resolveConfigPath(configPath);
45
+
46
+ if (!fs.existsSync(resolved)) {
47
+ throw new Error(`Install config not found: ${resolved}`);
48
+ }
49
+
50
+ let raw;
51
+ try {
52
+ raw = JSON.parse(fs.readFileSync(resolved, 'utf8'));
53
+ } catch (err) {
54
+ throw new Error(`Invalid JSON in config ${resolved}: ${err.message}`, { cause: err });
55
+ }
56
+
57
+ // Validate against schema
58
+ assertAgainstSchema(SCHEMA_PATH, raw, `install config ${path.basename(resolved)}`);
59
+
60
+ // Normalize and deduplicate
61
+ return {
62
+ path: resolved,
63
+ version: raw.version,
64
+ target: raw.target || null,
65
+ profile: raw.profile || null,
66
+ modules: dedupeStrings(raw.modules || []),
67
+ include: dedupeStrings(raw.include || []),
68
+ exclude: dedupeStrings(raw.exclude || []),
69
+ options: raw.options || {},
70
+ };
71
+ }
72
+
73
+ module.exports = { loadInstallConfig, resolveConfigPath };
@@ -0,0 +1,363 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * install-executor.js — Executes SCC content installation.
5
+ *
6
+ * Handles file copying for different targets:
7
+ * - claude → .claude/ subdirs (agents, skills, commands)
8
+ * - cursor → .cursor/ subdirs (agents, skills)
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { copyFile, readJson, fileExists, simpleHash } = require('./utils');
14
+ const { saveState } = require('./state-store');
15
+ const { transformSkillDir } = require('./skill-adapter');
16
+ const { transformAgentFile } = require('./agent-adapter');
17
+
18
+ const VALID_TARGETS = ['claude', 'cursor'];
19
+ const VALID_PROFILES = ['apex', 'lwc', 'full'];
20
+
21
+ /**
22
+ * Resolve target directory mappings.
23
+ * @param {string} target
24
+ * @param {string} projectRoot - directory where content is being installed
25
+ */
26
+ function getTargetDirs(target, projectRoot) {
27
+ switch (target) {
28
+ case 'claude':
29
+ return {
30
+ agents: path.join(projectRoot, '.claude', 'agents'),
31
+ skills: path.join(projectRoot, '.claude', 'skills'),
32
+ commands: path.join(projectRoot, '.claude', 'commands'),
33
+ hooks: path.join(projectRoot, '.claude', 'hooks'),
34
+ };
35
+ case 'cursor':
36
+ return {
37
+ agents: path.join(projectRoot, '.cursor', 'agents'),
38
+ skills: path.join(projectRoot, '.cursor', 'skills'),
39
+ commands: path.join(projectRoot, '.cursor', 'commands'),
40
+ hooks: null,
41
+ };
42
+ default:
43
+ throw new Error(`Unknown target: ${target}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load install manifests from the plugin root.
49
+ */
50
+ function loadManifests(pluginRoot) {
51
+ const profilesPath = path.join(pluginRoot, 'manifests', 'install-profiles.json');
52
+ const modulesPath = path.join(pluginRoot, 'manifests', 'install-modules.json');
53
+
54
+ const profilesData = readJson(profilesPath);
55
+ const modulesData = readJson(modulesPath);
56
+
57
+ if (!profilesData) throw new Error(`Cannot read install-profiles.json at ${profilesPath}`);
58
+ if (!modulesData) throw new Error(`Cannot read install-modules.json at ${modulesPath}`);
59
+
60
+ // Extract nested profiles object from { version, profiles } wrapper
61
+ const profiles = profilesData.profiles || profilesData;
62
+
63
+ // Convert modules array to object keyed by id for lookup
64
+ const modulesArray = modulesData.modules || modulesData;
65
+ const modules = {};
66
+ if (Array.isArray(modulesArray)) {
67
+ for (const mod of modulesArray) {
68
+ if (mod.id) modules[mod.id] = mod;
69
+ }
70
+ } else {
71
+ Object.assign(modules, modulesArray);
72
+ }
73
+
74
+ return { profiles, modules, targets: {} };
75
+ }
76
+
77
+ /**
78
+ * Resolve which modules belong to a profile.
79
+ * @param {Object} profiles
80
+ * @param {string} profileName
81
+ * @returns {string[]} list of module names
82
+ */
83
+ function resolveProfileModules(profiles, profileName, visited = new Set()) {
84
+ if (visited.has(profileName)) {
85
+ throw new Error(`Circular profile dependency detected: ${[...visited, profileName].join(' → ')}`);
86
+ }
87
+ const profile = profiles[profileName];
88
+ if (!profile) throw new Error(`Unknown profile: ${profileName}. Valid profiles: ${Object.keys(profiles).join(', ')}`);
89
+
90
+ visited.add(profileName);
91
+
92
+ // Profile may extend others
93
+ const moduleList = [];
94
+ if (profile.extends) {
95
+ for (const parent of (Array.isArray(profile.extends) ? profile.extends : [profile.extends])) {
96
+ moduleList.push(...resolveProfileModules(profiles, parent, new Set(visited)));
97
+ }
98
+ }
99
+ moduleList.push(...(profile.modules || []));
100
+
101
+ // Deduplicate while preserving order
102
+ return [...new Set(moduleList)];
103
+ }
104
+
105
+ /**
106
+ * Copy a single content file to the target directory.
107
+ * @returns {{ destPath: string, srcPath: string, module: string, hash: string } | null}
108
+ */
109
+ function installFile(srcPath, destDir, relativeName, moduleName, dryRun) {
110
+ if (!fileExists(srcPath)) {
111
+ console.warn(` [WARN] Source file not found: ${srcPath}`);
112
+ return null;
113
+ }
114
+
115
+ const destPath = path.join(destDir, relativeName);
116
+
117
+ if (dryRun) {
118
+ console.log(` [dry-run] Would copy: ${srcPath} → ${destPath}`);
119
+ return { destPath, srcPath, module: moduleName, hash: simpleHash(srcPath) };
120
+ }
121
+
122
+ copyFile(srcPath, destPath);
123
+ return { destPath, srcPath, module: moduleName, hash: simpleHash(srcPath) };
124
+ }
125
+
126
+ /**
127
+ * Check if a source path is a skill directory (contains SKILL.md).
128
+ */
129
+ function isSkillDir(srcPath) {
130
+ return fs.existsSync(path.join(srcPath, 'SKILL.md'));
131
+ }
132
+
133
+ /**
134
+ * Check if a source path is an agent file (agents/*.md).
135
+ */
136
+ function isAgentFile(srcRelative) {
137
+ return srcRelative.startsWith('agents/') && srcRelative.endsWith('.md');
138
+ }
139
+
140
+ /**
141
+ * Install paths for a given source list to a target directory.
142
+ * @param {string} targetName - install target ('claude' or 'cursor')
143
+ * @returns {Array} installed file records
144
+ */
145
+ function installPaths(pathsList, destDir, pluginRoot, moduleName, dryRun, targetName) {
146
+ const installed = [];
147
+
148
+ for (const srcRelative of pathsList) {
149
+ const srcPath = path.join(pluginRoot, srcRelative);
150
+
151
+ if (srcRelative.endsWith('/')) {
152
+ // Directory path — copy all files, preserving subdirectory name
153
+ if (!fs.existsSync(srcPath)) {
154
+ console.warn(` [WARN] Source dir not found: ${srcPath}`);
155
+ continue;
156
+ }
157
+
158
+ const dirName = path.basename(srcRelative.slice(0, -1));
159
+ const destSubDir = path.join(destDir, dirName);
160
+
161
+ // Use skill adapter for Cursor target when source is a skill directory
162
+ if (targetName === 'cursor' && isSkillDir(srcPath)) {
163
+ if (dryRun) {
164
+ console.log(` [dry-run] Would transform skill: ${srcRelative} → ${path.relative(pluginRoot, destSubDir)}/`);
165
+ } else {
166
+ transformSkillDir(srcPath, destSubDir);
167
+ }
168
+ installed.push({ destPath: destSubDir, srcPath, module: moduleName, hash: null });
169
+ continue;
170
+ }
171
+
172
+ const entries = fs.readdirSync(srcPath);
173
+ for (const entry of entries) {
174
+ const fullSrc = path.join(srcPath, entry);
175
+ if (fs.statSync(fullSrc).isFile()) {
176
+ const record = installFile(fullSrc, destSubDir, entry, moduleName, dryRun);
177
+ if (record) installed.push(record);
178
+ }
179
+ }
180
+ } else {
181
+ // Specific file path — use agent adapter for Cursor target
182
+ if (targetName === 'cursor' && isAgentFile(srcRelative)) {
183
+ const destPath = path.join(destDir, path.basename(srcRelative));
184
+ if (dryRun) {
185
+ console.log(` [dry-run] Would transform agent: ${srcRelative} → ${path.relative(pluginRoot, destPath)}`);
186
+ } else {
187
+ transformAgentFile(srcPath, destPath);
188
+ }
189
+ installed.push({ destPath, srcPath, module: moduleName, hash: null });
190
+ } else {
191
+ const record = installFile(srcPath, destDir, path.basename(srcRelative), moduleName, dryRun);
192
+ if (record) installed.push(record);
193
+ }
194
+ }
195
+ }
196
+
197
+ return installed;
198
+ }
199
+
200
+ /**
201
+ * Install files for a single module definition.
202
+ * Supports two formats:
203
+ * - Legacy: `paths` + `targets` (single target directory for all paths)
204
+ * - Bundle: `pathGroups` array of { paths, targets } (different targets per content type)
205
+ *
206
+ * @param {Object} moduleDef - from install-modules.json
207
+ * @param {string} moduleName
208
+ * @param {string} pluginRoot
209
+ * @param {string} targetName - e.g. 'claude', 'cursor'
210
+ * @param {string} projectRoot
211
+ * @param {boolean} dryRun
212
+ * @returns {Array} installed file records
213
+ */
214
+ function installModule(moduleDef, moduleName, pluginRoot, targetName, projectRoot, dryRun) {
215
+ // Bundle format: pathGroups array with per-group targets
216
+ if (Array.isArray(moduleDef.pathGroups)) {
217
+ const installed = [];
218
+ let hasAnyTarget = false;
219
+
220
+ for (const group of moduleDef.pathGroups) {
221
+ const destRelative = (group.targets || {})[targetName];
222
+ if (!destRelative) continue;
223
+ hasAnyTarget = true;
224
+ const destDir = path.join(projectRoot, destRelative);
225
+ installed.push(...installPaths(group.paths || [], destDir, pluginRoot, moduleName, dryRun, targetName));
226
+ }
227
+
228
+ if (!hasAnyTarget) {
229
+ console.log(` [SKIP] Module ${moduleName} doesn't support target: ${targetName}`);
230
+ }
231
+ return installed;
232
+ }
233
+
234
+ // Legacy format: single paths + targets
235
+ const installed = [];
236
+ const destRelative = (moduleDef.targets || {})[targetName];
237
+ if (!destRelative) {
238
+ console.log(` [SKIP] Module ${moduleName} doesn't support target: ${targetName}`);
239
+ return installed;
240
+ }
241
+
242
+ const destDir = path.join(projectRoot, destRelative);
243
+ return installPaths(moduleDef.paths || [], destDir, pluginRoot, moduleName, dryRun, targetName);
244
+ }
245
+
246
+ /**
247
+ * Execute a full installation.
248
+ *
249
+ * @param {string} profileName - e.g. 'full', 'apex', 'core'
250
+ * @param {string} targetName - e.g. 'claude', 'cursor'
251
+ * @param {Object} options
252
+ * @param {boolean} [options.dryRun=false]
253
+ * @param {string} [options.projectRoot=process.cwd()]
254
+ * @param {string} [options.pluginRoot]
255
+ * @returns {{ installedFiles: Array, moduleCount: number, fileCount: number }}
256
+ */
257
+ function executeInstall(profileName, targetName, options = {}) {
258
+ const dryRun = options.dryRun || false;
259
+ const projectRoot = options.projectRoot || process.cwd();
260
+ const pluginRoot = options.pluginRoot || process.env.CLAUDE_PLUGIN_ROOT || process.env.SCC_PLUGIN_ROOT || path.join(__dirname, '..', '..');
261
+
262
+ if (!VALID_TARGETS.includes(targetName)) {
263
+ throw new Error(`Invalid target: ${targetName}. Valid targets: ${VALID_TARGETS.join(', ')}`);
264
+ }
265
+ if (!VALID_PROFILES.includes(profileName)) {
266
+ throw new Error(`Invalid profile: ${profileName}. Valid profiles: ${VALID_PROFILES.join(', ')}`);
267
+ }
268
+
269
+ const { profiles, modules } = loadManifests(pluginRoot);
270
+ const moduleNames = resolveProfileModules(profiles, profileName);
271
+
272
+ console.log(`\nInstalling SCC — profile: ${profileName}, target: ${targetName}${dryRun ? ' [DRY RUN]' : ''}`);
273
+ console.log(`Project root: ${projectRoot}`);
274
+ console.log(`Modules to install: ${moduleNames.join(', ')}\n`);
275
+
276
+ const allInstalled = [];
277
+
278
+ for (const moduleName of moduleNames) {
279
+ const moduleDef = modules[moduleName];
280
+ if (!moduleDef) {
281
+ console.warn(`[WARN] Module definition not found: ${moduleName}`);
282
+ continue;
283
+ }
284
+ console.log(`Installing module: ${moduleName}`);
285
+ const records = installModule(moduleDef, moduleName, pluginRoot, targetName, projectRoot, dryRun);
286
+ allInstalled.push(...records);
287
+ console.log(` ${records.length} file(s) installed`);
288
+ }
289
+
290
+ // Install MCP config (renamed file — claude: .mcp.json at root, cursor: .cursor/mcp.json)
291
+ const mcpSrc = path.join(pluginRoot, 'mcp-configs', 'mcp-servers.json');
292
+ const mcpDestMap = { claude: '.mcp.json', cursor: path.join('.cursor', 'mcp.json') };
293
+ const mcpDest = mcpDestMap[targetName];
294
+ if (mcpDest && fileExists(mcpSrc)) {
295
+ const destPath = path.join(projectRoot, mcpDest);
296
+ if (dryRun) {
297
+ console.log(`\nMCP config:`);
298
+ console.log(` [dry-run] Would copy: mcp-configs/mcp-servers.json → ${mcpDest}`);
299
+ } else {
300
+ copyFile(mcpSrc, destPath);
301
+ console.log(`\nMCP config:`);
302
+ console.log(` [OK] mcp-configs/mcp-servers.json → ${mcpDest}`);
303
+ }
304
+ allInstalled.push({ destPath, srcPath: mcpSrc, module: 'core', hash: simpleHash(mcpSrc) });
305
+ }
306
+
307
+ if (!dryRun) {
308
+ saveState({
309
+ profile: profileName,
310
+ target: targetName,
311
+ installedFiles: allInstalled,
312
+ });
313
+ }
314
+
315
+ console.log(`\n${dryRun ? '[DRY RUN] Would install' : 'Installed'} ${allInstalled.length} file(s) across ${moduleNames.length} module(s).`);
316
+
317
+ return {
318
+ installedFiles: allInstalled,
319
+ moduleCount: moduleNames.length,
320
+ fileCount: allInstalled.length,
321
+ };
322
+ }
323
+
324
+ /**
325
+ * List available install targets.
326
+ * @returns {string[]}
327
+ */
328
+ function listAvailableTargets() {
329
+ return [...VALID_TARGETS];
330
+ }
331
+
332
+ /**
333
+ * List available install profiles.
334
+ * @param {string} [pluginRoot]
335
+ * @returns {Object} profile definitions
336
+ */
337
+ function listAvailableProfiles(pluginRoot) {
338
+ const root = pluginRoot || process.env.CLAUDE_PLUGIN_ROOT || process.env.SCC_PLUGIN_ROOT || path.join(__dirname, '..', '..');
339
+ try {
340
+ const { profiles } = loadManifests(root);
341
+ return profiles;
342
+ } catch {
343
+ // Return defaults if manifests not yet created
344
+ return {
345
+ core: { description: 'Minimal baseline', modules: [] },
346
+ apex: { description: 'Apex development suite', modules: [] },
347
+ lwc: { description: 'LWC development suite', modules: [] },
348
+ devops: { description: 'DevOps and deployment', modules: [] },
349
+ security: { description: 'Security-focused', modules: [] },
350
+ full: { description: 'Complete suite', extends: ['apex', 'lwc', 'devops', 'security'], modules: [] },
351
+ };
352
+ }
353
+ }
354
+
355
+ module.exports = {
356
+ executeInstall,
357
+ listAvailableTargets,
358
+ listAvailableProfiles,
359
+ getTargetDirs,
360
+ resolveProfileModules,
361
+ VALID_TARGETS,
362
+ VALID_PROFILES,
363
+ };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * install-state.js — Schema-validated install state tracking for SCC.
5
+ *
6
+ * Validates against schemas/install-state.schema.json on every read/write.
7
+ * Tracks rich installation provenance: target, request, resolution, source, operations.
8
+ *
9
+ * State is written to ~/.scc/install-state.json (separate from state.json).
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { validateAgainstSchema, assertAgainstSchema, formatErrors } = require('./schema-validator');
16
+
17
+ const STATE_DIR = path.join(os.homedir(), '.scc');
18
+ const INSTALL_STATE_PATH = path.join(STATE_DIR, 'install-state.json');
19
+ const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schemas', 'install-state.schema.json');
20
+
21
+ function ensureStateDir() {
22
+ fs.mkdirSync(STATE_DIR, { recursive: true });
23
+ }
24
+
25
+ /**
26
+ * Create a new install state object with all required fields.
27
+ *
28
+ * @param {Object} params
29
+ * @param {Object} params.target - { id, root, installStatePath, target?, kind? }
30
+ * @param {Object} params.request - { profile, modules, includeComponents, excludeComponents, legacyLanguages, legacyMode }
31
+ * @param {Object} params.resolution - { selectedModules, skippedModules }
32
+ * @param {Object} params.source - { repoVersion, repoCommit, manifestVersion }
33
+ * @param {Array} params.operations - array of { kind, moduleId, sourceRelativePath, destinationPath, strategy, ownership, scaffoldOnly }
34
+ * @returns {Object} validated install state
35
+ */
36
+ function createInstallState({ target, request, resolution, source, operations }) {
37
+ const state = {
38
+ schemaVersion: 'scc.install.v1',
39
+ installedAt: new Date().toISOString(),
40
+ target: {
41
+ id: target.id || 'default',
42
+ root: target.root || process.cwd(),
43
+ installStatePath: target.installStatePath || INSTALL_STATE_PATH,
44
+ ...(target.target ? { target: target.target } : {}),
45
+ ...(target.kind ? { kind: target.kind } : {}),
46
+ },
47
+ request: {
48
+ profile: request.profile || null,
49
+ modules: request.modules || [],
50
+ includeComponents: request.includeComponents || [],
51
+ excludeComponents: request.excludeComponents || [],
52
+ legacyLanguages: request.legacyLanguages || [],
53
+ legacyMode: request.legacyMode || false,
54
+ },
55
+ resolution: {
56
+ selectedModules: resolution.selectedModules || [],
57
+ skippedModules: resolution.skippedModules || [],
58
+ },
59
+ source: {
60
+ repoVersion: source.repoVersion || null,
61
+ repoCommit: source.repoCommit || null,
62
+ manifestVersion: source.manifestVersion || 2,
63
+ },
64
+ operations: (operations || []).map(op => ({
65
+ kind: op.kind || 'copy',
66
+ moduleId: op.moduleId || '',
67
+ sourceRelativePath: op.sourceRelativePath || '',
68
+ destinationPath: op.destinationPath || '',
69
+ strategy: op.strategy || 'overwrite',
70
+ ownership: op.ownership || 'scc',
71
+ scaffoldOnly: op.scaffoldOnly || false,
72
+ })),
73
+ };
74
+
75
+ assertAgainstSchema(SCHEMA_PATH, state, 'install-state');
76
+ return state;
77
+ }
78
+
79
+ /**
80
+ * Read install state from disk. Returns validated state or null if not found.
81
+ *
82
+ * @param {string} [statePath] - path to install state file (defaults to ~/.scc/install-state.json)
83
+ * @returns {Object|null} validated install state or null
84
+ */
85
+ function readInstallState(statePath) {
86
+ const filePath = statePath || INSTALL_STATE_PATH;
87
+ try {
88
+ const raw = fs.readFileSync(filePath, 'utf8');
89
+ const state = JSON.parse(raw);
90
+
91
+ const result = validateAgainstSchema(SCHEMA_PATH, state);
92
+ if (!result.valid) {
93
+ process.stderr.write(`[SCC] install-state validation warning: ${formatErrors(result.errors)}\n`);
94
+ return null;
95
+ }
96
+
97
+ return state;
98
+ } catch (err) {
99
+ if (err.code !== 'ENOENT') {
100
+ process.stderr.write(`[SCC] Failed to read install-state: ${err.message}\n`);
101
+ }
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Write install state to disk after validation.
108
+ *
109
+ * @param {string} [statePath] - path to write (defaults to ~/.scc/install-state.json)
110
+ * @param {Object} state - install state object (will be validated)
111
+ */
112
+ function writeInstallState(state, statePath) {
113
+ assertAgainstSchema(SCHEMA_PATH, state, 'install-state');
114
+
115
+ const filePath = statePath || INSTALL_STATE_PATH;
116
+ const dir = path.dirname(filePath);
117
+ fs.mkdirSync(dir, { recursive: true });
118
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf8');
119
+ }
120
+
121
+ module.exports = { createInstallState, readInstallState, writeInstallState, INSTALL_STATE_PATH };