@cregis-dev/cckit 0.6.5 → 0.6.7

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 (52) hide show
  1. package/README.md +221 -221
  2. package/package.json +1 -1
  3. package/registry.json +145 -128
  4. package/src/cli.js +79 -79
  5. package/src/commands/init.js +174 -161
  6. package/src/commands/status.js +125 -85
  7. package/src/commands/update.js +192 -151
  8. package/src/core/config.js +82 -74
  9. package/src/core/orchestrator.js +79 -79
  10. package/src/core/registry.js +60 -60
  11. package/src/steps/add-plugin.js +148 -0
  12. package/src/steps/configure-user.js +181 -181
  13. package/src/steps/enable-plugins.js +97 -97
  14. package/src/steps/install-bmad.js +85 -85
  15. package/src/steps/install-mcp.js +70 -70
  16. package/src/steps/install-rules.js +69 -69
  17. package/src/steps/install-skills.js +56 -56
  18. package/src/utils/compare-versions.js +106 -0
  19. package/src/utils/fs.js +33 -33
  20. package/src/utils/manifest.js +101 -99
  21. package/src/utils/prompt.js +41 -41
  22. package/templates/mcp/claude-code/.mcp.json +40 -40
  23. package/templates/rules/README.md +103 -103
  24. package/templates/rules/common/agents.md +49 -49
  25. package/templates/rules/common/coding-style.md +48 -48
  26. package/templates/rules/common/development-workflow.md +37 -37
  27. package/templates/rules/common/git-workflow.md +24 -24
  28. package/templates/rules/common/hooks.md +30 -30
  29. package/templates/rules/common/patterns.md +31 -31
  30. package/templates/rules/common/performance.md +55 -55
  31. package/templates/rules/common/security.md +29 -29
  32. package/templates/rules/common/testing.md +29 -29
  33. package/templates/rules/golang/coding-style.md +32 -32
  34. package/templates/rules/golang/hooks.md +17 -17
  35. package/templates/rules/golang/patterns.md +45 -45
  36. package/templates/rules/golang/security.md +34 -34
  37. package/templates/rules/golang/testing.md +31 -31
  38. package/templates/rules/python/coding-style.md +42 -42
  39. package/templates/rules/python/hooks.md +19 -19
  40. package/templates/rules/python/patterns.md +39 -39
  41. package/templates/rules/python/security.md +30 -30
  42. package/templates/rules/python/testing.md +38 -38
  43. package/templates/rules/swift/coding-style.md +47 -47
  44. package/templates/rules/swift/hooks.md +20 -20
  45. package/templates/rules/swift/patterns.md +66 -66
  46. package/templates/rules/swift/security.md +33 -33
  47. package/templates/rules/swift/testing.md +45 -45
  48. package/templates/rules/typescript/coding-style.md +65 -65
  49. package/templates/rules/typescript/hooks.md +22 -22
  50. package/templates/rules/typescript/patterns.md +52 -52
  51. package/templates/rules/typescript/security.md +28 -28
  52. package/templates/rules/typescript/testing.md +18 -18
@@ -1,85 +1,85 @@
1
- /**
2
- * Step: Install BMAD methodology via `npx bmad-method install`.
3
- *
4
- * Spawns the BMAD CLI as a child process with stdio inherited
5
- * so the user sees real-time output.
6
- */
7
-
8
- import { spawn } from 'node:child_process'
9
-
10
- /**
11
- * @param {object} opts
12
- * @param {string} opts.targetDir - Project root
13
- * @param {object} opts.bmadConfig - { command, args, outputDirs } from registry
14
- * @param {object} [opts.config] - User config (communication_language, document_output_language)
15
- * @param {boolean} opts.skip - Skip this step
16
- * @param {object} logger
17
- * @param {object} [_deps] - Injectable dependencies for testing
18
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
19
- */
20
- export async function installBmad(opts, logger, _deps = {}) {
21
- if (opts.skip) {
22
- return { stepId: 'install-bmad', name: 'Install BMAD', success: true, skipped: true, details: {} }
23
- }
24
-
25
- const { spawnFn = spawn } = _deps
26
- const { command, args: baseArgs } = opts.bmadConfig
27
-
28
- // Override language args from user config if provided
29
- const args = [...baseArgs]
30
- if (opts.config) {
31
- const langMap = {
32
- '--communication-language': opts.config.communication_language,
33
- '--document-output-language': opts.config.document_output_language,
34
- }
35
- for (const [flag, value] of Object.entries(langMap)) {
36
- if (!value) continue
37
- const idx = args.indexOf(flag)
38
- if (idx !== -1) {
39
- args[idx + 1] = value
40
- }
41
- }
42
- }
43
-
44
- logger.info(` Running: ${command} ${args.join(' ')}`)
45
-
46
- return new Promise((resolve) => {
47
- const child = spawnFn(command, args, {
48
- cwd: opts.targetDir,
49
- stdio: 'inherit',
50
- shell: true,
51
- })
52
-
53
- child.on('close', (code) => {
54
- if (code === 0) {
55
- resolve({
56
- stepId: 'install-bmad',
57
- name: 'Install BMAD',
58
- success: true,
59
- skipped: false,
60
- details: { outputDirs: opts.bmadConfig.outputDirs },
61
- })
62
- } else {
63
- resolve({
64
- stepId: 'install-bmad',
65
- name: 'Install BMAD',
66
- success: false,
67
- skipped: false,
68
- details: {},
69
- error: `npx bmad-method exited with code ${code}`,
70
- })
71
- }
72
- })
73
-
74
- child.on('error', (err) => {
75
- resolve({
76
- stepId: 'install-bmad',
77
- name: 'Install BMAD',
78
- success: false,
79
- skipped: false,
80
- details: {},
81
- error: `Failed to spawn: ${err.message}`,
82
- })
83
- })
84
- })
85
- }
1
+ /**
2
+ * Step: Install BMAD methodology via `npx bmad-method install`.
3
+ *
4
+ * Spawns the BMAD CLI as a child process with stdio inherited
5
+ * so the user sees real-time output.
6
+ */
7
+
8
+ import { spawn } from 'node:child_process'
9
+
10
+ /**
11
+ * @param {object} opts
12
+ * @param {string} opts.targetDir - Project root
13
+ * @param {object} opts.bmadConfig - { command, args, outputDirs } from registry
14
+ * @param {object} [opts.config] - User config (communication_language, document_output_language)
15
+ * @param {boolean} opts.skip - Skip this step
16
+ * @param {object} logger
17
+ * @param {object} [_deps] - Injectable dependencies for testing
18
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
19
+ */
20
+ export async function installBmad(opts, logger, _deps = {}) {
21
+ if (opts.skip) {
22
+ return { stepId: 'install-bmad', name: 'Install BMAD', success: true, skipped: true, details: {} }
23
+ }
24
+
25
+ const { spawnFn = spawn } = _deps
26
+ const { command, args: baseArgs } = opts.bmadConfig
27
+
28
+ // Override language args from user config if provided
29
+ const args = [...baseArgs]
30
+ if (opts.config) {
31
+ const langMap = {
32
+ '--communication-language': opts.config.communication_language,
33
+ '--document-output-language': opts.config.document_output_language,
34
+ }
35
+ for (const [flag, value] of Object.entries(langMap)) {
36
+ if (!value) continue
37
+ const idx = args.indexOf(flag)
38
+ if (idx !== -1) {
39
+ args[idx + 1] = value
40
+ }
41
+ }
42
+ }
43
+
44
+ logger.info(` Running: ${command} ${args.join(' ')}`)
45
+
46
+ return new Promise((resolve) => {
47
+ const child = spawnFn(command, args, {
48
+ cwd: opts.targetDir,
49
+ stdio: 'inherit',
50
+ shell: true,
51
+ })
52
+
53
+ child.on('close', (code) => {
54
+ if (code === 0) {
55
+ resolve({
56
+ stepId: 'install-bmad',
57
+ name: 'Install BMAD',
58
+ success: true,
59
+ skipped: false,
60
+ details: { outputDirs: opts.bmadConfig.outputDirs },
61
+ })
62
+ } else {
63
+ resolve({
64
+ stepId: 'install-bmad',
65
+ name: 'Install BMAD',
66
+ success: false,
67
+ skipped: false,
68
+ details: {},
69
+ error: `npx bmad-method exited with code ${code}`,
70
+ })
71
+ }
72
+ })
73
+
74
+ child.on('error', (err) => {
75
+ resolve({
76
+ stepId: 'install-bmad',
77
+ name: 'Install BMAD',
78
+ success: false,
79
+ skipped: false,
80
+ details: {},
81
+ error: `Failed to spawn: ${err.message}`,
82
+ })
83
+ })
84
+ })
85
+ }
@@ -1,70 +1,70 @@
1
- /**
2
- * Step: Install MCP configuration.
3
- *
4
- * Copies the bundled `.mcp.json` template to the project root.
5
- * Uses `overwrite: false` to preserve user modifications.
6
- */
7
-
8
- import path from 'node:path'
9
- import { fileURLToPath } from 'node:url'
10
- import fse from 'fs-extra'
11
- import { computeFileHash } from '../utils/fs.js'
12
-
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
- const CCKIT_ROOT = path.resolve(__dirname, '../..')
15
-
16
- /**
17
- * @param {object} opts
18
- * @param {string} opts.targetDir - Project root
19
- * @param {object} opts.mcpConfig - { template, target } from registry
20
- * @param {boolean} opts.skip - Skip this step
21
- * @param {boolean} [opts.forceOverwrite] - Overwrite existing file
22
- * @param {object} logger
23
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
24
- */
25
- export async function installMcp(opts, logger) {
26
- if (opts.skip) {
27
- return { stepId: 'install-mcp', name: 'Install MCP Config', success: true, skipped: true, details: {} }
28
- }
29
-
30
- const srcPath = path.resolve(CCKIT_ROOT, opts.mcpConfig.template)
31
- const destPath = path.join(opts.targetDir, opts.mcpConfig.target)
32
-
33
- if (!await fse.pathExists(srcPath)) {
34
- return {
35
- stepId: 'install-mcp',
36
- name: 'Install MCP Config',
37
- success: false,
38
- skipped: false,
39
- details: {},
40
- error: `MCP template not found at ${opts.mcpConfig.template}`,
41
- }
42
- }
43
-
44
- // Don't overwrite user-modified file unless forced
45
- if (!opts.forceOverwrite && await fse.pathExists(destPath)) {
46
- const hash = await computeFileHash(destPath)
47
- logger.info(` ${opts.mcpConfig.target} already exists, skipping (use --force to overwrite)`)
48
- return {
49
- stepId: 'install-mcp',
50
- name: 'Install MCP Config',
51
- success: true,
52
- skipped: false,
53
- details: { target: opts.mcpConfig.target, hash, preserved: true },
54
- }
55
- }
56
-
57
- await fse.ensureDir(path.dirname(destPath))
58
- await fse.copy(srcPath, destPath)
59
-
60
- const hash = await computeFileHash(destPath)
61
- logger.debug(`Installed MCP config to ${opts.mcpConfig.target}`)
62
-
63
- return {
64
- stepId: 'install-mcp',
65
- name: 'Install MCP Config',
66
- success: true,
67
- skipped: false,
68
- details: { target: opts.mcpConfig.target, hash },
69
- }
70
- }
1
+ /**
2
+ * Step: Install MCP configuration.
3
+ *
4
+ * Copies the bundled `.mcp.json` template to the project root.
5
+ * Uses `overwrite: false` to preserve user modifications.
6
+ */
7
+
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+ import fse from 'fs-extra'
11
+ import { computeFileHash } from '../utils/fs.js'
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
+ const CCKIT_ROOT = path.resolve(__dirname, '../..')
15
+
16
+ /**
17
+ * @param {object} opts
18
+ * @param {string} opts.targetDir - Project root
19
+ * @param {object} opts.mcpConfig - { template, target } from registry
20
+ * @param {boolean} opts.skip - Skip this step
21
+ * @param {boolean} [opts.forceOverwrite] - Overwrite existing file
22
+ * @param {object} logger
23
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
24
+ */
25
+ export async function installMcp(opts, logger) {
26
+ if (opts.skip) {
27
+ return { stepId: 'install-mcp', name: 'Install MCP Config', success: true, skipped: true, details: {} }
28
+ }
29
+
30
+ const srcPath = path.resolve(CCKIT_ROOT, opts.mcpConfig.template)
31
+ const destPath = path.join(opts.targetDir, opts.mcpConfig.target)
32
+
33
+ if (!await fse.pathExists(srcPath)) {
34
+ return {
35
+ stepId: 'install-mcp',
36
+ name: 'Install MCP Config',
37
+ success: false,
38
+ skipped: false,
39
+ details: {},
40
+ error: `MCP template not found at ${opts.mcpConfig.template}`,
41
+ }
42
+ }
43
+
44
+ // Don't overwrite user-modified file unless forced
45
+ if (!opts.forceOverwrite && await fse.pathExists(destPath)) {
46
+ const hash = await computeFileHash(destPath)
47
+ logger.info(` ${opts.mcpConfig.target} already exists, skipping (use --force to overwrite)`)
48
+ return {
49
+ stepId: 'install-mcp',
50
+ name: 'Install MCP Config',
51
+ success: true,
52
+ skipped: false,
53
+ details: { target: opts.mcpConfig.target, hash, preserved: true },
54
+ }
55
+ }
56
+
57
+ await fse.ensureDir(path.dirname(destPath))
58
+ await fse.copy(srcPath, destPath)
59
+
60
+ const hash = await computeFileHash(destPath)
61
+ logger.debug(`Installed MCP config to ${opts.mcpConfig.target}`)
62
+
63
+ return {
64
+ stepId: 'install-mcp',
65
+ name: 'Install MCP Config',
66
+ success: true,
67
+ skipped: false,
68
+ details: { target: opts.mcpConfig.target, hash },
69
+ }
70
+ }
@@ -1,69 +1,69 @@
1
- /**
2
- * Step: Install ECC rules via template copy.
3
- *
4
- * Copies the bundled rules template to `.claude/rules/`.
5
- */
6
-
7
- import path from 'node:path'
8
- import fse from 'fs-extra'
9
- import { listFiles } from '../utils/fs.js'
10
- import { fileURLToPath } from 'node:url'
11
-
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
-
14
- /**
15
- * @param {object} opts
16
- * @param {string} opts.targetDir - Project root
17
- * @param {object} opts.rulesConfig - { template, target }
18
- * @param {boolean} opts.skip - Skip this step
19
- * @param {object} logger
20
- * @param {object} [_deps] - Injectable dependencies
21
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
22
- */
23
- export async function installRules(opts, logger, _deps = {}) {
24
- if (opts.skip) {
25
- return { stepId: 'install-rules', name: 'Install ECC Rules', success: true, skipped: true, details: {} }
26
- }
27
-
28
- const { template, target } = opts.rulesConfig
29
-
30
- try {
31
- // Resolve template path - use absolute path if provided, otherwise relative to cckit package
32
- const templatePath = path.resolve(__dirname, '../../', template)
33
- const destDir = path.join(opts.targetDir, target)
34
-
35
- if (!await fse.pathExists(templatePath)) {
36
- return {
37
- stepId: 'install-rules',
38
- name: 'Install ECC Rules',
39
- success: false,
40
- skipped: false,
41
- details: {},
42
- error: `Rules template not found: ${templatePath}`,
43
- }
44
- }
45
-
46
- await fse.ensureDir(destDir)
47
- await fse.copy(templatePath, destDir, { overwrite: true })
48
-
49
- const files = await listFiles(destDir)
50
- logger.debug(`Copied ${files.length} rule files to ${target}`)
51
-
52
- return {
53
- stepId: 'install-rules',
54
- name: 'Install ECC Rules',
55
- success: true,
56
- skipped: false,
57
- details: { files: files.length, target },
58
- }
59
- } catch (err) {
60
- return {
61
- stepId: 'install-rules',
62
- name: 'Install ECC Rules',
63
- success: false,
64
- skipped: false,
65
- details: {},
66
- error: err.message,
67
- }
68
- }
69
- }
1
+ /**
2
+ * Step: Install ECC rules via template copy.
3
+ *
4
+ * Copies the bundled rules template to `.claude/rules/`.
5
+ */
6
+
7
+ import path from 'node:path'
8
+ import fse from 'fs-extra'
9
+ import { listFiles } from '../utils/fs.js'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+
14
+ /**
15
+ * @param {object} opts
16
+ * @param {string} opts.targetDir - Project root
17
+ * @param {object} opts.rulesConfig - { template, target }
18
+ * @param {boolean} opts.skip - Skip this step
19
+ * @param {object} logger
20
+ * @param {object} [_deps] - Injectable dependencies
21
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
22
+ */
23
+ export async function installRules(opts, logger, _deps = {}) {
24
+ if (opts.skip) {
25
+ return { stepId: 'install-rules', name: 'Install ECC Rules', success: true, skipped: true, details: {} }
26
+ }
27
+
28
+ const { template, target } = opts.rulesConfig
29
+
30
+ try {
31
+ // Resolve template path - use absolute path if provided, otherwise relative to cckit package
32
+ const templatePath = path.resolve(__dirname, '../../', template)
33
+ const destDir = path.join(opts.targetDir, target)
34
+
35
+ if (!await fse.pathExists(templatePath)) {
36
+ return {
37
+ stepId: 'install-rules',
38
+ name: 'Install ECC Rules',
39
+ success: false,
40
+ skipped: false,
41
+ details: {},
42
+ error: `Rules template not found: ${templatePath}`,
43
+ }
44
+ }
45
+
46
+ await fse.ensureDir(destDir)
47
+ await fse.copy(templatePath, destDir, { overwrite: true })
48
+
49
+ const files = await listFiles(destDir)
50
+ logger.debug(`Copied ${files.length} rule files to ${target}`)
51
+
52
+ return {
53
+ stepId: 'install-rules',
54
+ name: 'Install ECC Rules',
55
+ success: true,
56
+ skipped: false,
57
+ details: { files: files.length, target },
58
+ }
59
+ } catch (err) {
60
+ return {
61
+ stepId: 'install-rules',
62
+ name: 'Install ECC Rules',
63
+ success: false,
64
+ skipped: false,
65
+ details: {},
66
+ error: err.message,
67
+ }
68
+ }
69
+ }
@@ -1,56 +1,56 @@
1
- /**
2
- * Step: Install skills via `npx skills add`.
3
- *
4
- * Iterates over skill entries from the registry and runs
5
- * `npx skills add <repo> --skill <id>` for each.
6
- * Individual failures do not abort the overall step.
7
- */
8
-
9
- import { execFile } from 'node:child_process'
10
- import { promisify } from 'node:util'
11
-
12
- const execFileAsync = promisify(execFile)
13
-
14
- /**
15
- * @param {object} opts
16
- * @param {string} opts.targetDir - Project root
17
- * @param {Array<{id: string, repo: string, skill: string}>} opts.skills - Skill definitions
18
- * @param {boolean} opts.skip - Skip this step
19
- * @param {object} logger
20
- * @param {object} [_deps] - Injectable dependencies
21
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
22
- */
23
- export async function installSkills(opts, logger, _deps = {}) {
24
- if (opts.skip) {
25
- return { stepId: 'install-skills', name: 'Install Skills', success: true, skipped: true, details: {} }
26
- }
27
-
28
- const { execFileFn = execFileAsync } = _deps
29
- const results = []
30
-
31
- for (const skillDef of opts.skills) {
32
- logger.info(` Installing skill: ${skillDef.id}`)
33
- try {
34
- // Use installCommand from registry if available, otherwise fallback to basic command
35
- const installCmd = skillDef.installCommand || `npx skills add ${skillDef.repo} --skill ${skillDef.skill} -y -a claude-code`
36
- await execFileFn(installCmd, [], {
37
- cwd: opts.targetDir,
38
- shell: true,
39
- })
40
- results.push({ id: skillDef.id, success: true })
41
- logger.debug(` Skill "${skillDef.id}" installed successfully`)
42
- } catch (err) {
43
- results.push({ id: skillDef.id, success: false, error: err.message })
44
- logger.warn(`Skill "${skillDef.id}" failed: ${err.message}`)
45
- }
46
- }
47
-
48
- const allSuccess = results.every(r => r.success)
49
- return {
50
- stepId: 'install-skills',
51
- name: 'Install Skills',
52
- success: allSuccess,
53
- skipped: false,
54
- details: { results },
55
- }
56
- }
1
+ /**
2
+ * Step: Install skills via `npx skills add`.
3
+ *
4
+ * Iterates over skill entries from the registry and runs
5
+ * `npx skills add <repo> --skill <id>` for each.
6
+ * Individual failures do not abort the overall step.
7
+ */
8
+
9
+ import { execFile } from 'node:child_process'
10
+ import { promisify } from 'node:util'
11
+
12
+ const execFileAsync = promisify(execFile)
13
+
14
+ /**
15
+ * @param {object} opts
16
+ * @param {string} opts.targetDir - Project root
17
+ * @param {Array<{id: string, repo: string, skill: string}>} opts.skills - Skill definitions
18
+ * @param {boolean} opts.skip - Skip this step
19
+ * @param {object} logger
20
+ * @param {object} [_deps] - Injectable dependencies
21
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
22
+ */
23
+ export async function installSkills(opts, logger, _deps = {}) {
24
+ if (opts.skip) {
25
+ return { stepId: 'install-skills', name: 'Install Skills', success: true, skipped: true, details: {} }
26
+ }
27
+
28
+ const { execFileFn = execFileAsync } = _deps
29
+ const results = []
30
+
31
+ for (const skillDef of opts.skills) {
32
+ logger.info(` Installing skill: ${skillDef.id}`)
33
+ try {
34
+ // Use installCommand from registry if available, otherwise fallback to basic command
35
+ const installCmd = skillDef.installCommand || `npx skills add ${skillDef.repo} --skill ${skillDef.skill} -y -a claude-code`
36
+ await execFileFn(installCmd, [], {
37
+ cwd: opts.targetDir,
38
+ shell: true,
39
+ })
40
+ results.push({ id: skillDef.id, success: true })
41
+ logger.debug(` Skill "${skillDef.id}" installed successfully`)
42
+ } catch (err) {
43
+ results.push({ id: skillDef.id, success: false, error: err.message })
44
+ logger.warn(`Skill "${skillDef.id}" failed: ${err.message}`)
45
+ }
46
+ }
47
+
48
+ const allSuccess = results.every(r => r.success)
49
+ return {
50
+ stepId: 'install-skills',
51
+ name: 'Install Skills',
52
+ success: allSuccess,
53
+ skipped: false,
54
+ details: { results },
55
+ }
56
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Version comparison utilities for detecting configuration changes.
3
+ *
4
+ * Compares installed versions (from manifest) with registry versions
5
+ * to determine which components need updating.
6
+ */
7
+
8
+ import { loadRegistry } from '../core/registry.js'
9
+
10
+ /**
11
+ * Component map: stepId -> version key in registry.versions
12
+ */
13
+ const COMPONENT_MAP = {
14
+ 'configure-user': 'userSettings',
15
+ 'enable-plugins': 'plugins',
16
+ 'install-rules': 'rules',
17
+ 'install-skills': 'skills',
18
+ 'install-bmad': 'bmad',
19
+ 'install-mcp': 'mcp',
20
+ }
21
+
22
+ /**
23
+ * Compare installed versions against registry versions.
24
+ *
25
+ * @param {object} manifest - Installed manifest with config.versions
26
+ * @param {object} registry - Current registry with versions
27
+ * @returns {object} - { needsUpdate: boolean, components: { [stepId]: { current, latest, needsUpdate } } }
28
+ */
29
+ export function compareVersions(manifest, registry) {
30
+ const installedVersions = manifest.cckit?.config?.versions || {}
31
+ const latestVersions = registry.versions || {}
32
+
33
+ const components = {}
34
+ let needsUpdate = false
35
+
36
+ // Compare each component
37
+ for (const [stepId, versionKey] of Object.entries(COMPONENT_MAP)) {
38
+ const current = installedVersions[versionKey]
39
+ const latest = latestVersions[versionKey]
40
+
41
+ // If either version is missing, consider it needs update
42
+ const componentNeedsUpdate = !current || !latest || current !== latest
43
+
44
+ components[stepId] = {
45
+ versionKey,
46
+ current: current || 'none',
47
+ latest: latest || 'none',
48
+ needsUpdate: componentNeedsUpdate,
49
+ }
50
+
51
+ if (componentNeedsUpdate) {
52
+ needsUpdate = true
53
+ }
54
+ }
55
+
56
+ return { needsUpdate, components }
57
+ }
58
+
59
+ /**
60
+ * Check if specific component needs update.
61
+ *
62
+ * @param {object} manifest - Installed manifest
63
+ * @param {object} registry - Current registry
64
+ * @param {string} stepId - Step ID to check
65
+ * @returns {boolean}
66
+ */
67
+ export function needsComponentUpdate(manifest, registry, stepId) {
68
+ const versionKey = COMPONENT_MAP[stepId]
69
+ if (!versionKey) return false
70
+
71
+ const current = manifest.cckit?.config?.versions?.[versionKey]
72
+ const latest = registry.versions?.[versionKey]
73
+
74
+ return !current || !latest || current !== latest
75
+ }
76
+
77
+ /**
78
+ * Get version info for a specific component.
79
+ *
80
+ * @param {object} registry - Current registry
81
+ * @returns {object} - { plugins, rules, skills, bmad, mcp, userSettings }
82
+ */
83
+ export function getRegistryVersions(registry) {
84
+ return registry.versions || {}
85
+ }
86
+
87
+ /**
88
+ * Load registry and compare versions (async).
89
+ *
90
+ * @param {string} targetDir - Project directory
91
+ * @returns {Promise<{ needsUpdate: boolean, components: object }>}
92
+ */
93
+ export async function checkForUpdates(targetDir) {
94
+ const { readManifest } = await import('../utils/manifest.js')
95
+ const registry = await loadRegistry()
96
+
97
+ let manifest
98
+ try {
99
+ manifest = await readManifest(targetDir)
100
+ } catch {
101
+ // Not installed yet
102
+ return { needsUpdate: true, components: {}, notInstalled: true }
103
+ }
104
+
105
+ return compareVersions(manifest, registry)
106
+ }