prizmkit 1.0.15 → 1.0.17

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.0.15",
3
- "bundledAt": "2026-03-15T12:24:22.555Z",
4
- "bundledFrom": "8b809a8"
2
+ "frameworkVersion": "1.0.17",
3
+ "bundledAt": "2026-03-15T14:53:53.793Z",
4
+ "bundledFrom": "527811d"
5
5
  }
@@ -32,9 +32,10 @@ log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"
32
32
  # PLATFORM
33
33
  # PRIZMKIT_PLATFORM
34
34
  prizm_detect_cli_and_platform() {
35
+ local _raw_cli=""
36
+
35
37
  if [[ -n "${AI_CLI:-}" ]]; then
36
- CLI_CMD="$AI_CLI"
37
- # Read from .prizmkit/config.json if present
38
+ _raw_cli="$AI_CLI"
38
39
  elif [[ -f ".prizmkit/config.json" ]]; then
39
40
  _config_ai_cli=$(python3 -c "
40
41
  import json, sys
@@ -46,31 +47,33 @@ try:
46
47
  except: pass
47
48
  " 2>/dev/null || true)
48
49
  if [[ -n "$_config_ai_cli" ]]; then
49
- CLI_CMD="$_config_ai_cli"
50
+ _raw_cli="$_config_ai_cli"
50
51
  elif [[ -n "${CODEBUDDY_CLI:-}" ]]; then
51
- CLI_CMD="$CODEBUDDY_CLI"
52
+ _raw_cli="$CODEBUDDY_CLI"
52
53
  elif command -v cbc &>/dev/null; then
53
- CLI_CMD="cbc"
54
+ _raw_cli="cbc"
54
55
  elif command -v claude &>/dev/null; then
55
- CLI_CMD="claude"
56
+ _raw_cli="claude"
56
57
  else
57
58
  echo "ERROR: No AI CLI found. Install CodeBuddy (cbc) or Claude Code (claude)." >&2
58
59
  exit 1
59
60
  fi
60
61
  elif [[ -n "${CODEBUDDY_CLI:-}" ]]; then
61
- CLI_CMD="$CODEBUDDY_CLI"
62
+ _raw_cli="$CODEBUDDY_CLI"
62
63
  elif command -v cbc &>/dev/null; then
63
- CLI_CMD="cbc"
64
+ _raw_cli="cbc"
64
65
  elif command -v claude &>/dev/null; then
65
- CLI_CMD="claude"
66
+ _raw_cli="claude"
66
67
  else
67
68
  echo "ERROR: No AI CLI found. Install CodeBuddy (cbc) or Claude Code (claude)." >&2
68
69
  exit 1
69
70
  fi
70
71
 
72
+ CLI_CMD="$_raw_cli"
73
+
71
74
  if [[ -n "${PRIZMKIT_PLATFORM:-}" ]]; then
72
75
  PLATFORM="$PRIZMKIT_PLATFORM"
73
- elif [[ "$CLI_CMD" == *"claude"* ]]; then
76
+ elif [[ "$_raw_cli" == *"claude"* ]]; then
74
77
  PLATFORM="claude"
75
78
  else
76
79
  PLATFORM="codebuddy"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.15",
2
+ "version": "1.0.17",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
@@ -335,43 +335,24 @@
335
335
  }
336
336
  },
337
337
  "external_skills": {
338
- "registries": [
339
- {
340
- "name": "skills.sh",
341
- "url": "https://skills.sh/registry.json",
342
- "description": "Community skill registry"
343
- }
344
- ],
345
338
  "known": [
346
339
  {
347
- "name": "find-skill",
348
- "description": "Intelligent file and code search skill",
349
- "source": "https://skills.sh/skills/find-skill/SKILL.md",
350
- "registry": "skills.sh",
340
+ "name": "find-skills",
341
+ "description": "Helps discover and install agent skills by capability",
342
+ "repo": "https://github.com/vercel-labs/skills",
351
343
  "tags": [
352
344
  "search",
353
345
  "utility"
354
346
  ]
355
347
  },
356
348
  {
357
- "name": "uiuxpromax",
349
+ "name": "ui-ux-pro-max",
358
350
  "description": "UI/UX design review and suggestions",
359
- "source": "https://skills.sh/skills/uiuxpromax/SKILL.md",
360
- "registry": "skills.sh",
351
+ "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill",
361
352
  "tags": [
362
353
  "design",
363
354
  "frontend"
364
355
  ]
365
- },
366
- {
367
- "name": "thinkthought",
368
- "description": "Structured reasoning and problem decomposition",
369
- "source": "https://skills.sh/skills/thinkthought/SKILL.md",
370
- "registry": "skills.sh",
371
- "tags": [
372
- "reasoning",
373
- "universal"
374
- ]
375
356
  }
376
357
  ]
377
358
  }
@@ -39,44 +39,32 @@ BROWNFIELD WORKFLOW (existing project):
39
39
  4. Catalog dependencies (external packages)
40
40
  5. Count source files per directory
41
41
 
42
- **Step 2: State Assessment**
43
- 2a. Run dependency analysis:
44
- - Count outdated dependencies (if lockfile exists)
45
- - Note any known vulnerability patterns
46
-
47
- 2b. Scan for technical debt indicators:
48
- - Count TODO/FIXME/HACK/XXX comments
49
- - Identify large files (>500 lines)
50
- - Check for test directories and coverage config
51
-
52
- 2c. Generate `ASSESSMENT.md` in project root with findings
53
-
54
- **Step 3: Prizm Documentation Generation**
55
- 3a. Invoke prizmkit-prizm-docs `prizmkit.doc.init` algorithm:
42
+ **Step 2: Prizm Documentation Generation**
43
+ 2a. Invoke prizmkit-prizm-docs `prizmkit.doc.init` algorithm:
56
44
  - Create `.prizm-docs/` directory structure
57
45
  - Generate `root.prizm` (L0) with project meta and module index
58
46
  - Generate L1 docs for all discovered modules
59
47
  - Create `changelog.prizm`
60
48
  - Skip L2 (lazy generation)
61
49
 
62
- 3b. If project has existing `docs/AI_CONTEXT/`: suggest running `prizmkit.doc.migrate`
50
+ 2b. If project has existing `docs/AI_CONTEXT/`: suggest running `prizmkit.doc.migrate`
63
51
 
64
- **Step 4: PrizmKit Workspace Initialization**
65
- 4a. Create `.prizmkit/` directory:
52
+ **Step 3: PrizmKit Workspace Initialization**
53
+ 3a. Create `.prizmkit/` directory:
66
54
  - `.prizmkit/config.json` (adoption_mode, speckit_hooks_enabled, platform)
67
55
  - `.prizmkit/specs/` (empty)
68
56
 
69
- **Step 5: Hook & Settings Configuration (Platform-Specific)**
57
+ **Step 4: Hook & Settings Configuration (Platform-Specific)**
70
58
 
71
59
  **If platform is CodeBuddy (or both):**
72
- 5a-cb. Read or create `.codebuddy/settings.json`
73
- 5b-cb. Add UserPromptSubmit hook from `${SKILL_DIR}/../../../assets/hooks/prizm-commit-hook.json`
74
- 5c-cb. Preserve any existing hooks
60
+ 4a-cb. Read or create `.codebuddy/settings.json`
61
+ 4b-cb. Add UserPromptSubmit hook from `${SKILL_DIR}/../../../assets/hooks/prizm-commit-hook.json`
62
+ 4c-cb. Preserve any existing hooks
75
63
 
76
64
  **If platform is Claude Code (or both):**
77
- 5a-cl. Read or create `.claude/settings.json`
78
- 5b-cl. Add `permissions` and `allowedTools` entries if needed
79
- 5c-cl. Create `.claude/rules/prizm-documentation.md` with glob-scoped rules:
65
+ 4a-cl. Read or create `.claude/settings.json`
66
+ 4b-cl. Add `permissions` and `allowedTools` entries if needed
67
+ 4c-cl. Create `.claude/rules/prizm-documentation.md` with glob-scoped rules:
80
68
  ```yaml
81
69
  ---
82
70
  description: PrizmKit documentation rules
@@ -91,7 +79,7 @@ BROWNFIELD WORKFLOW (existing project):
91
79
  2. After changes, update affected `.prizm-docs/` files
92
80
  3. Follow Prizm doc format (KEY: value, not prose)
93
81
  ```
94
- 5d-cl. Create `.claude/rules/prizm-commit-workflow.md` with commit-scoped rules:
82
+ 4d-cl. Create `.claude/rules/prizm-commit-workflow.md` with commit-scoped rules:
95
83
  ```yaml
96
84
  ---
97
85
  description: PrizmKit commit workflow enforcement
@@ -105,33 +93,33 @@ BROWNFIELD WORKFLOW (existing project):
105
93
  4. Stage .prizm-docs/ changes
106
94
  5. Use /prizmkit-committer for the complete workflow
107
95
  ```
108
- 5e-cl. Preserve any existing Claude settings and rules
96
+ 4e-cl. Preserve any existing Claude settings and rules
109
97
 
110
- **Step 6: Project Memory Update (Platform-Specific)**
98
+ **Step 5: Project Memory Update (Platform-Specific)**
111
99
 
112
100
  **If platform is CodeBuddy (or both):**
113
- 6a-cb. Read existing `CODEBUDDY.md` (or create if missing)
114
- 6b-cb. Append PrizmKit section from `${SKILL_DIR}/../../../assets/codebuddy-md-template.md`
115
- 6c-cb. Do not duplicate if already present
101
+ 5a-cb. Read existing `CODEBUDDY.md` (or create if missing)
102
+ 5b-cb. Append PrizmKit section from `${SKILL_DIR}/../../../assets/codebuddy-md-template.md`
103
+ 5c-cb. Do not duplicate if already present
116
104
 
117
105
  **If platform is Claude Code (or both):**
118
- 6a-cl. Read existing `CLAUDE.md` (or create if missing)
119
- 6b-cl. Append PrizmKit section from `${SKILL_DIR}/../../../assets/claude-md-template.md`
120
- 6c-cl. Adjust command references to use `/command-name` format (not `prizmkit.xxx`)
121
- 6d-cl. Do not duplicate if already present
106
+ 5a-cl. Read existing `CLAUDE.md` (or create if missing)
107
+ 5b-cl. Append PrizmKit section from `${SKILL_DIR}/../../../assets/claude-md-template.md`
108
+ 5c-cl. Adjust command references to use `/command-name` format (not `prizmkit.xxx`)
109
+ 5d-cl. Do not duplicate if already present
122
110
 
123
- **Step 7: Report**
124
- Output summary: platform detected, tech stack detected, modules discovered, L1 docs generated, assessment highlights, platform-specific configuration applied, next recommended steps.
111
+ **Step 6: Report**
112
+ Output summary: platform detected, tech stack detected, modules discovered, L1 docs generated, platform-specific configuration applied, next recommended steps.
125
113
 
126
114
  Include platform-specific guidance:
127
115
  - CodeBuddy: "Use `prizmkit.specify` to start your first feature"
128
116
  - Claude Code: "Use `/prizmkit-specify` to start your first feature"
129
117
 
130
118
  GREENFIELD WORKFLOW (new project):
131
- - Skip Steps 1-2 (no code to scan)
132
- - Step 3: Create minimal `.prizm-docs/` with just `root.prizm` skeleton
133
- - Steps 4-6: Same as brownfield
134
- - Step 7: Recommend starting with specify for first feature (platform-appropriate command format)
119
+ - Skip Step 1 (no code to scan)
120
+ - Step 2: Create minimal `.prizm-docs/` with just `root.prizm` skeleton
121
+ - Steps 3-5: Same as brownfield
122
+ - Step 6: Recommend starting with specify for first feature (platform-appropriate command format)
135
123
 
136
124
  ### Gradual Adoption Path
137
125
  After init, PrizmKit operates in phases:
@@ -10,6 +10,17 @@
10
10
  }
11
11
  ]
12
12
  }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Bash",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "if echo \"$CLAUDE_TOOL_INPUT\" 2>/dev/null | grep -qE 'git (commit|merge|rebase)' 2>/dev/null; then DRIFT=$(sh .prizmkit/scripts/diff-prizm-docs.sh 2>/dev/null); if [ -n \"$DRIFT\" ]; then echo \"PRIZMKIT_DRIFT_DETECTED: prizm-docs structural differences found after git operation:\"; echo \"$DRIFT\"; echo \"Please update affected .prizm-docs/ files using /prizmkit-prizm-docs.\"; fi; fi"
21
+ }
22
+ ]
23
+ }
13
24
  ]
14
25
  }
15
26
  }
@@ -10,6 +10,17 @@
10
10
  }
11
11
  ]
12
12
  }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Bash",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "TOOL_INPUT=\"${CLAUDE_TOOL_INPUT:-${CODEBUDDY_TOOL_INPUT:-}}\"; if echo \"$TOOL_INPUT\" | grep -qE 'git (commit|merge|rebase)' 2>/dev/null; then DRIFT=$(sh .prizmkit/scripts/diff-prizm-docs.sh 2>/dev/null); if [ -n \"$DRIFT\" ]; then echo \"PRIZMKIT_DRIFT_DETECTED: prizm-docs structural differences found:\"; echo \"$DRIFT\"; echo \"Please update affected .prizm-docs/ files.\"; fi; fi"
21
+ }
22
+ ]
23
+ }
13
24
  ]
14
25
  }
15
26
  }
@@ -0,0 +1,158 @@
1
+ #!/bin/sh
2
+ # PrizmKit: diff-prizm-docs.sh
3
+ # Outputs structural differences between .prizm-docs/ and source code.
4
+ # Silent (exit 0) if no differences or if .prizm-docs doesn't exist.
5
+ # POSIX sh only — no arrays, no bashisms.
6
+
7
+ set -e
8
+
9
+ ROOT_PRIZM=".prizm-docs/root.prizm"
10
+
11
+ # 1. Exit silently if no root.prizm
12
+ [ -f "$ROOT_PRIZM" ] || exit 0
13
+
14
+ # 2. Detect language from LANG: field
15
+ LANG_VAL=$(grep '^LANG:' "$ROOT_PRIZM" | head -1 | sed 's/^LANG:[[:space:]]*//' | tr '[:upper:]' '[:lower:]')
16
+
17
+ case "$LANG_VAL" in
18
+ go)
19
+ FILE_PATTERN="*.go"
20
+ ;;
21
+ javascript|typescript|javascript/typescript|typescript/javascript)
22
+ FILE_PATTERN="*.ts *.tsx *.js *.jsx"
23
+ ;;
24
+ python)
25
+ FILE_PATTERN="*.py"
26
+ ;;
27
+ rust)
28
+ FILE_PATTERN="*.rs"
29
+ ;;
30
+ *)
31
+ FILE_PATTERN=""
32
+ ;;
33
+ esac
34
+
35
+ # Helper: count source files in a directory (maxdepth 1) by language
36
+ count_source_files() {
37
+ _dir="$1"
38
+ [ -d "$_dir" ] || { echo 0; return; }
39
+ if [ -z "$FILE_PATTERN" ]; then
40
+ # All non-hidden files
41
+ find "$_dir" -maxdepth 1 -type f ! -name '.*' 2>/dev/null | wc -l | tr -d ' '
42
+ else
43
+ _count=0
44
+ for _pat in $FILE_PATTERN; do
45
+ _c=$(find "$_dir" -maxdepth 1 -type f -name "$_pat" 2>/dev/null | wc -l | tr -d ' ')
46
+ _count=$((_count + _c))
47
+ done
48
+ echo "$_count"
49
+ fi
50
+ }
51
+
52
+ # 3. Parse MODULE_INDEX from root.prizm
53
+ # Extract lines between MODULE_INDEX: and next ALL-CAPS header or EOF
54
+ # Each module line: "- src/auth: ..." -> source path is text before first ":" after "- "
55
+ REGISTERED_PATHS=""
56
+ _in_index=0
57
+ while IFS= read -r line; do
58
+ case "$line" in
59
+ MODULE_INDEX:*)
60
+ _in_index=1
61
+ continue
62
+ ;;
63
+ esac
64
+ if [ "$_in_index" -eq 1 ]; then
65
+ # Check for next ALL-CAPS section header (word followed by colon, all uppercase letters)
66
+ _header=$(echo "$line" | grep '^[A-Z_][A-Z_]*:' || true)
67
+ if [ -n "$_header" ]; then
68
+ break
69
+ fi
70
+ # Extract source path from "- src/auth: ..."
71
+ case "$line" in
72
+ "- "*)
73
+ _path=$(echo "$line" | sed 's/^- //' | sed 's/:.*//')
74
+ if [ -n "$_path" ]; then
75
+ REGISTERED_PATHS="${REGISTERED_PATHS}${_path}
76
+ "
77
+ fi
78
+ ;;
79
+ esac
80
+ fi
81
+ done < "$ROOT_PRIZM"
82
+
83
+ # 4. Check each registered module for drift
84
+ echo "$REGISTERED_PATHS" | while IFS= read -r source_path; do
85
+ [ -z "$source_path" ] && continue
86
+
87
+ # Derive L1 prizm file path
88
+ l1_file=".prizm-docs/${source_path}.prizm"
89
+
90
+ if [ ! -f "$l1_file" ]; then
91
+ continue
92
+ fi
93
+
94
+ # Read declared FILES: count
95
+ declared=$(grep '^FILES:' "$l1_file" | head -1 | awk '{print $2}')
96
+ [ -z "$declared" ] && continue
97
+
98
+ # Count actual source files
99
+ actual=$(count_source_files "$source_path")
100
+
101
+ if [ "$actual" -gt 0 ] && [ "$declared" != "$actual" ]; then
102
+ echo "MODULE_DRIFT: $source_path | declared FILES: $declared | actual: $actual"
103
+ fi
104
+ done
105
+
106
+ # 5. Find orphan docs
107
+ find .prizm-docs -name '*.prizm' 2>/dev/null | while IFS= read -r prizm_file; do
108
+ _basename=$(basename "$prizm_file")
109
+ case "$_basename" in
110
+ root.prizm|changelog.prizm|changelog-archive.prizm)
111
+ continue
112
+ ;;
113
+ esac
114
+
115
+ # Derive source path: remove .prizm-docs/ prefix and .prizm suffix
116
+ source_path=$(echo "$prizm_file" | sed 's|^\.prizm-docs/||' | sed 's|\.prizm$||')
117
+
118
+ if [ ! -d "$source_path" ]; then
119
+ echo "ORPHAN_DOC: $prizm_file | source dir missing"
120
+ fi
121
+ done
122
+
123
+ # 6. Find unregistered directories
124
+ find . -mindepth 1 -maxdepth 3 -type d \
125
+ ! -path '*/.*' \
126
+ ! -path '*/node_modules/*' ! -path './node_modules' \
127
+ ! -path '*/vendor/*' ! -path './vendor' \
128
+ ! -path '*/dist/*' ! -path './dist' \
129
+ ! -path '*/build/*' ! -path './build' \
130
+ ! -path '*/__pycache__/*' ! -path './__pycache__' \
131
+ ! -path '*/target/*' ! -path './target' \
132
+ ! -path '*/tests/*' ! -path './tests' \
133
+ ! -path '*/__tests__/*' ! -path './__tests__' \
134
+ ! -path '*/.prizm-docs/*' ! -path './.prizm-docs' \
135
+ ! -path '*/.claude/*' ! -path './.claude' \
136
+ ! -path '*/.codebuddy/*' ! -path './.codebuddy' \
137
+ ! -path '*/dev-pipeline/*' ! -path './dev-pipeline' \
138
+ ! -path '*/coverage/*' ! -path './coverage' \
139
+ ! -path '*/.nyc_output/*' ! -path './.nyc_output' \
140
+ ! -path '*/out/*' ! -path './out' \
141
+ ! -path '*/tmp/*' ! -path './tmp' \
142
+ 2>/dev/null | while IFS= read -r candidate; do
143
+ # Normalize: remove leading ./
144
+ candidate_clean=$(echo "$candidate" | sed 's|^\./||')
145
+
146
+ # Count source files
147
+ file_count=$(count_source_files "$candidate_clean")
148
+
149
+ if [ "$file_count" -ge 3 ]; then
150
+ # Check if registered in MODULE_INDEX
151
+ _match=$(echo "$REGISTERED_PATHS" | grep -Fx "$candidate_clean" || true)
152
+ if [ -z "$_match" ]; then
153
+ echo "UNREGISTERED: $candidate_clean | $file_count source files | not in MODULE_INDEX"
154
+ fi
155
+ fi
156
+ done
157
+
158
+ exit 0
@@ -6,7 +6,8 @@ PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
6
6
  # Only run in prizm projects
7
7
  [ -f "$PROJECT_ROOT/.prizm-docs/root.prizm" ] || exit 0
8
8
 
9
- VALIDATE_SCRIPT="$PROJECT_ROOT/dev-pipeline/scripts/validate-prizm-docs.sh"
9
+ VALIDATE_SCRIPT="$PROJECT_ROOT/.prizmkit/scripts/validate-prizm-docs.sh"
10
+ [ -f "$VALIDATE_SCRIPT" ] || VALIDATE_SCRIPT="$PROJECT_ROOT/dev-pipeline/scripts/validate-prizm-docs.sh"
10
11
 
11
12
  if [ -f "$VALIDATE_SCRIPT" ]; then
12
13
  sh "$VALIDATE_SCRIPT" --staged
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/clean.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import path from 'path';
3
3
  import fs from 'fs-extra';
4
+ import { loadMetadata, loadRulesMetadata } from './metadata.js';
4
5
 
5
6
  async function removePath(targetPath, dryRun) {
6
7
  if (!await fs.pathExists(targetPath)) {
@@ -13,6 +14,38 @@ async function removePath(targetPath, dryRun) {
13
14
  return { path: targetPath, removed: true };
14
15
  }
15
16
 
17
+ /**
18
+ * Remove known PrizmKit files from a shared directory (skills, agents, rules, commands).
19
+ * Leaves user-created files untouched. Removes the parent dir if it becomes empty.
20
+ */
21
+ async function removeKnownEntries(parentDir, entries, dryRun) {
22
+ const results = [];
23
+
24
+ for (const entry of entries) {
25
+ const abs = path.join(parentDir, entry);
26
+ if (!await fs.pathExists(abs)) continue;
27
+
28
+ if (dryRun) {
29
+ results.push({ path: abs, removed: false, reason: 'dry_run' });
30
+ continue;
31
+ }
32
+
33
+ await fs.remove(abs);
34
+ results.push({ path: abs, removed: true });
35
+ }
36
+
37
+ // Remove parent dir only if it now exists and is empty
38
+ if (!dryRun && await fs.pathExists(parentDir)) {
39
+ const remaining = await fs.readdir(parentDir);
40
+ if (remaining.length === 0) {
41
+ await fs.remove(parentDir);
42
+ results.push({ path: parentDir, removed: true, note: '(empty dir)' });
43
+ }
44
+ }
45
+
46
+ return results;
47
+ }
48
+
16
49
  async function cleanGitignore(projectRoot, dryRun) {
17
50
  const gitignorePath = path.join(projectRoot, '.gitignore');
18
51
  if (!await fs.pathExists(gitignorePath)) {
@@ -48,30 +81,78 @@ export async function runClean(directory, options = {}) {
48
81
  console.log(` 预览模式: ${dryRun ? chalk.yellow('是') : chalk.green('否')}`);
49
82
  console.log('');
50
83
 
51
- const targets = [
84
+ // Load metadata to know exactly which files PrizmKit owns
85
+ const [metadata, rulesMeta] = await Promise.all([
86
+ loadMetadata().catch(() => ({ skills: {}, external_skills: { known: [] } })),
87
+ loadRulesMetadata().catch(() => ({ rules: {} })),
88
+ ]);
89
+
90
+ const skillNames = Object.keys(metadata.skills);
91
+ const externalSkillNames = (metadata.external_skills?.known || []).map(s => s.name);
92
+ const allSkillNames = [...new Set([...skillNames, ...externalSkillNames])];
93
+
94
+ // Agent file names installed by PrizmKit
95
+ const agentFiles = [
96
+ 'prizm-dev-team-coordinator.md',
97
+ 'prizm-dev-team-dev.md',
98
+ 'prizm-dev-team-pm.md',
99
+ 'prizm-dev-team-reviewer.md',
100
+ ];
101
+
102
+ // Rule file names installed by PrizmKit (basename without category prefix)
103
+ const ruleFileNames = Object.keys(metadata_rules_to_filenames(rulesMeta));
104
+
105
+ const results = [];
106
+
107
+ // ── Full removes (100% PrizmKit-owned paths) ──────────────────────────────
108
+ const fullRemoveTargets = [
52
109
  '.prizmkit',
53
110
  '.dev-team',
54
111
  'dev-pipeline',
112
+ '.prizm-docs', // AI-generated project context docs
55
113
  'CODEBUDDY.md',
56
114
  'CLAUDE.md',
57
- path.join('.codebuddy', 'skills'),
58
- path.join('.codebuddy', 'agents'),
59
115
  path.join('.codebuddy', 'settings.json'),
60
- path.join('.claude', 'commands'),
61
- path.join('.claude', 'agents'),
62
- path.join('.claude', 'rules'),
63
116
  path.join('.claude', 'settings.json'),
64
117
  path.join('.claude', 'team-info.json'),
65
118
  ];
66
119
 
67
- const results = [];
68
- for (const rel of targets) {
120
+ for (const rel of fullRemoveTargets) {
69
121
  const abs = path.join(projectRoot, rel);
70
122
  results.push(await removePath(abs, dryRun));
71
123
  }
72
124
 
125
+ // ── Selective removes (shared dirs — only remove PrizmKit-known entries) ──
126
+
127
+ // .claude/commands/ — skills installed as .md files or subdirs
128
+ const claudeCommandsDir = path.join(projectRoot, '.claude', 'commands');
129
+ const claudeCommandEntries = allSkillNames.flatMap(name => [name, `${name}.md`]);
130
+ results.push(...await removeKnownEntries(claudeCommandsDir, claudeCommandEntries, dryRun));
131
+
132
+ // .codebuddy/skills/ — skills installed as subdirectories
133
+ const cbSkillsDir = path.join(projectRoot, '.codebuddy', 'skills');
134
+ results.push(...await removeKnownEntries(cbSkillsDir, allSkillNames, dryRun));
135
+
136
+ // .claude/agents/ — PrizmKit agent files
137
+ const claudeAgentsDir = path.join(projectRoot, '.claude', 'agents');
138
+ results.push(...await removeKnownEntries(claudeAgentsDir, agentFiles, dryRun));
139
+
140
+ // .codebuddy/agents/ — same agent files
141
+ const cbAgentsDir = path.join(projectRoot, '.codebuddy', 'agents');
142
+ results.push(...await removeKnownEntries(cbAgentsDir, agentFiles, dryRun));
143
+
144
+ // .claude/rules/ — PrizmKit rule .md files
145
+ const claudeRulesDir = path.join(projectRoot, '.claude', 'rules');
146
+ results.push(...await removeKnownEntries(claudeRulesDir, ruleFileNames.map(n => `${n}.md`), dryRun));
147
+
148
+ // .codebuddy/rules/ — PrizmKit rule .mdc files
149
+ const cbRulesDir = path.join(projectRoot, '.codebuddy', 'rules');
150
+ results.push(...await removeKnownEntries(cbRulesDir, ruleFileNames.map(n => `${n}.mdc`), dryRun));
151
+
152
+ // ── .gitignore cleanup ────────────────────────────────────────────────────
73
153
  const gitignoreResult = await cleanGitignore(projectRoot, dryRun);
74
154
 
155
+ // ── Global team (optional) ────────────────────────────────────────────────
75
156
  if (cleanGlobalTeam) {
76
157
  const homeDir = process.env.HOME || process.env.USERPROFILE;
77
158
  if (homeDir) {
@@ -80,13 +161,15 @@ export async function runClean(directory, options = {}) {
80
161
  }
81
162
  }
82
163
 
164
+ // ── Output ────────────────────────────────────────────────────────────────
83
165
  const removed = results.filter(r => r.removed).length;
84
166
  const found = results.filter(r => r.reason !== 'not_found').length;
85
167
 
86
168
  console.log(chalk.bold(' 清理结果:'));
87
169
  for (const item of results) {
88
170
  if (item.removed) {
89
- console.log(chalk.green(` 已删除: ${item.path}`));
171
+ const note = item.note ? chalk.gray(` ${item.note}`) : '';
172
+ console.log(chalk.green(` ✓ 已删除: ${item.path}`) + note);
90
173
  } else if (item.reason === 'dry_run') {
91
174
  console.log(chalk.gray(` [dry-run] ${item.path}`));
92
175
  }
@@ -107,4 +190,20 @@ export async function runClean(directory, options = {}) {
107
190
  console.log(chalk.green(` 清理完成:已删除 ${removed} 项。`));
108
191
  }
109
192
  console.log('');
193
+ console.log(chalk.gray(' 注意:.prizm-docs/ 为 AI 生成的项目上下文文档,已一并清理。'));
194
+ console.log(chalk.gray(' 用户自定义的 agent/skill/rule 文件未受影响。'));
195
+ console.log('');
196
+ }
197
+
198
+ /**
199
+ * Extract base file names from rules metadata (strip category prefix).
200
+ * e.g. "prizm/prizm-documentation" -> "prizm-documentation"
201
+ */
202
+ function metadata_rules_to_filenames(rulesMeta) {
203
+ const names = {};
204
+ for (const key of Object.keys(rulesMeta.rules || {})) {
205
+ const basename = key.split('/').pop();
206
+ names[basename] = true;
207
+ }
208
+ return names;
110
209
  }
@@ -1,71 +1,80 @@
1
1
  /**
2
2
  * External Skills Installer
3
- * Fetches and installs third-party skills from remote URLs.
3
+ *
4
+ * Delegates to `npx skills add <repo> --skill <name> --yes` for installation,
5
+ * then removes symlinks for platforms not selected by the user.
6
+ *
7
+ * npx skills creates symlinks for ALL platforms (.claude/skills/, .codebuddy/skills/,
8
+ * .trae/skills/, skills/, .agent/skills/). We keep only what the user asked for.
4
9
  */
5
10
 
6
- import https from 'node:https';
7
- import http from 'node:http';
11
+ import { execSync } from 'node:child_process';
8
12
  import path from 'path';
9
13
  import fs from 'fs-extra';
10
14
 
11
- /**
12
- * Fetch content from a remote URL (supports http and https)
13
- * @param {string} url - Remote URL to fetch
14
- * @returns {Promise<string>} - Content string
15
- */
16
- export function fetchRemoteSkill(skill) {
17
- return new Promise((resolve, reject) => {
18
- const url = skill.source;
19
- const protocol = url.startsWith('https://') ? https : http;
20
-
21
- const request = protocol.get(url, (res) => {
22
- if (res.statusCode === 301 || res.statusCode === 302) {
23
- // Follow redirect
24
- const redirectSkill = { ...skill, source: res.headers.location };
25
- resolve(fetchRemoteSkill(redirectSkill));
26
- return;
27
- }
15
+ // All platform symlink dirs that `npx skills` creates (besides .agents/ canonical store)
16
+ const ALL_PLATFORM_DIRS = [
17
+ '.claude/skills',
18
+ '.codebuddy/skills',
19
+ '.trae/skills',
20
+ '.agent/skills',
21
+ 'skills',
22
+ ];
28
23
 
29
- if (res.statusCode !== 200) {
30
- reject(new Error(`HTTP ${res.statusCode}: ${url}`));
31
- return;
32
- }
33
-
34
- const chunks = [];
35
- res.on('data', chunk => chunks.push(chunk));
36
- res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
37
- res.on('error', reject);
38
- });
39
-
40
- request.on('error', reject);
41
- request.setTimeout(15000, () => {
42
- request.destroy();
43
- reject(new Error(`Timeout fetching ${url}`));
44
- });
45
- });
46
- }
24
+ // Which dirs to KEEP per platform selection
25
+ const KEEP_DIRS = {
26
+ claude: ['.claude/skills'],
27
+ codebuddy: ['.codebuddy/skills'],
28
+ both: ['.claude/skills', '.codebuddy/skills'],
29
+ };
47
30
 
48
31
  /**
49
- * Install an external skill to the target platform directory.
50
- * @param {Object} skill - Skill definition from external_skills.known
51
- * @param {string} platform - 'codebuddy' | 'claude'
52
- * @param {string} projectRoot - Target project root
53
- * @param {boolean} dryRun - If true, don't write files
32
+ * Install an external skill via `npx skills add`, then prune unwanted platform symlinks.
33
+ *
34
+ * @param {Object} skill - Skill definition from external_skills.known
35
+ * skill.repo {string} - GitHub URL
36
+ * skill.name {string} - Skill name as listed in the repo
37
+ * @param {string} platform - 'claude' | 'codebuddy' | 'both'
38
+ * @param {string} projectRoot - Target project root
39
+ * @param {boolean} dryRun
54
40
  */
55
41
  export async function installExternalSkill(skill, platform, projectRoot, dryRun) {
56
- const content = await fetchRemoteSkill(skill);
42
+ if (!skill.repo) {
43
+ throw new Error(`Skill "${skill.name}" has no repo URL defined`);
44
+ }
45
+
46
+ const cmd = `npx skills add ${skill.repo} --skill ${skill.name} --yes`;
57
47
 
58
48
  if (dryRun) {
49
+ const keepDirs = KEEP_DIRS[platform] || KEEP_DIRS.claude;
50
+ console.log(` [dry-run] ${cmd}`);
51
+ console.log(` [dry-run] keep symlinks: ${keepDirs.map(d => path.join(d, skill.name)).join(', ')}`);
59
52
  return;
60
53
  }
61
54
 
62
- if (platform === 'codebuddy') {
63
- const targetDir = path.join(projectRoot, '.codebuddy', 'skills', skill.name);
64
- await fs.ensureDir(targetDir);
65
- await fs.writeFile(path.join(targetDir, 'SKILL.md'), content);
66
- } else if (platform === 'claude') {
67
- const targetDir = path.join(projectRoot, '.claude', 'commands');
68
- await fs.ensureDir(targetDir);
69
- await fs.writeFile(path.join(targetDir, `${skill.name}.md`), content);
55
+ execSync(cmd, {
56
+ cwd: projectRoot,
57
+ stdio: 'pipe',
58
+ timeout: 60000,
59
+ });
60
+
61
+ // Prune symlinks for platforms not selected
62
+ const keepDirs = new Set(KEEP_DIRS[platform] || KEEP_DIRS.claude);
63
+
64
+ for (const dir of ALL_PLATFORM_DIRS) {
65
+ const linkPath = path.join(projectRoot, dir, skill.name);
66
+ if (keepDirs.has(dir)) continue;
67
+ try {
68
+ const stat = await fs.lstat(linkPath);
69
+ if (stat.isSymbolicLink()) {
70
+ await fs.remove(linkPath);
71
+ // Remove parent dir if now empty
72
+ const parent = path.join(projectRoot, dir);
73
+ const remaining = await fs.readdir(parent).catch(() => ['_']);
74
+ if (remaining.length === 0) await fs.remove(parent);
75
+ }
76
+ } catch {
77
+ // not present, skip
78
+ }
70
79
  }
71
80
  }
package/src/scaffold.js CHANGED
@@ -472,25 +472,34 @@ async function installGitHook(projectRoot, dryRun) {
472
472
  }
473
473
 
474
474
  /**
475
- * 安装 validate-prizm-docs.sh dev-pipeline/scripts/
475
+ * 安装 PrizmKit 脚本到 .prizmkit/scripts/(始终安装)
476
476
  */
477
- async function installValidateScript(projectRoot, dryRun) {
478
- const sourcePath = path.join(getTemplatesDir(), 'hooks', 'validate-prizm-docs.sh');
479
- if (!fs.existsSync(sourcePath)) return;
477
+ async function installPrizmkitScripts(projectRoot, dryRun) {
478
+ const scriptsDir = path.join(projectRoot, '.prizmkit', 'scripts');
479
+ const templateHooksDir = path.join(getTemplatesDir(), 'hooks');
480
480
 
481
- const targetDir = path.join(projectRoot, 'dev-pipeline', 'scripts');
482
- const targetPath = path.join(targetDir, 'validate-prizm-docs.sh');
481
+ const scripts = [
482
+ 'validate-prizm-docs.sh',
483
+ 'diff-prizm-docs.sh',
484
+ ];
483
485
 
484
486
  if (dryRun) {
485
- console.log(chalk.gray(' [dry-run] dev-pipeline/scripts/validate-prizm-docs.sh'));
487
+ for (const s of scripts) {
488
+ console.log(chalk.gray(` [dry-run] .prizmkit/scripts/${s}`));
489
+ }
486
490
  return;
487
491
  }
488
492
 
489
- await fs.ensureDir(targetDir);
490
- await fs.copy(sourcePath, targetPath);
491
- fs.chmodSync(targetPath, 0o755);
493
+ await fs.ensureDir(scriptsDir);
492
494
 
493
- console.log(chalk.green(' ✓ dev-pipeline/scripts/validate-prizm-docs.sh'));
495
+ for (const s of scripts) {
496
+ const src = path.join(templateHooksDir, s);
497
+ if (!fs.existsSync(src)) continue;
498
+ const tgt = path.join(scriptsDir, s);
499
+ await fs.copy(src, tgt);
500
+ fs.chmodSync(tgt, 0o755);
501
+ console.log(chalk.green(` ✓ .prizmkit/scripts/${s}`));
502
+ }
494
503
  }
495
504
 
496
505
  /**
@@ -695,13 +704,12 @@ export async function scaffold(config) {
695
704
  for (const name of externalSkills) {
696
705
  const skillDef = knownSkills.find(s => s.name === name);
697
706
  if (skillDef) {
698
- for (const p of platforms) {
699
- try {
700
- await installExternalSkill(skillDef, p, projectRoot, dryRun);
701
- console.log(chalk.green(` ✓ ${name} (external)`));
702
- } catch (e) {
703
- console.log(chalk.yellow(` ⚠ ${name} 拉取失败,跳过: ${e.message}`));
704
- }
707
+ try {
708
+ // npx skills add handles installation; we pass platform to prune unwanted symlinks
709
+ await installExternalSkill(skillDef, platform, projectRoot, dryRun);
710
+ console.log(chalk.green(` ✓ ${name} (external)`));
711
+ } catch (e) {
712
+ console.log(chalk.yellow(` ⚠ ${name} 安装失败,跳过: ${e.message}`));
705
713
  }
706
714
  }
707
715
  }
@@ -711,11 +719,9 @@ export async function scaffold(config) {
711
719
  console.log(chalk.blue(' Git Hook:'));
712
720
  await installGitHook(projectRoot, dryRun);
713
721
 
714
- // 11. Validate script (only if pipeline enabled)
715
- if (pipeline) {
716
- console.log(chalk.blue(' 验证脚本:'));
717
- await installValidateScript(projectRoot, dryRun);
718
- }
722
+ // 11. PrizmKit scripts (always installed)
723
+ console.log(chalk.blue(' PrizmKit 脚本:'));
724
+ await installPrizmkitScripts(projectRoot, dryRun);
719
725
 
720
726
  // === 完成 ===
721
727
  console.log('');