prizmkit 1.1.4 → 1.1.5
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.
- package/bundled/VERSION.json +3 -3
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-implement/references/deploy-guide-protocol.md +69 -0
- package/bundled/skills/prizmkit-init/references/config-schema.md +64 -0
- package/bundled/skills/prizmkit-init/references/tech-stack-catalog.md +13 -0
- package/bundled/skills/prizmkit-init/references/update-supplement.md +9 -0
- package/bundled/skills/prizmkit-plan/assets/spec-template.md +56 -0
- package/bundled/skills/prizmkit-plan/references/clarify-guide.md +67 -0
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +49 -0
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +63 -0
- package/package.json +1 -1
- package/src/config.js +504 -0
- package/src/prompts.js +210 -0
package/bundled/VERSION.json
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Deploy Guide Update Protocol
|
|
2
|
+
|
|
3
|
+
When dependency manifests change during implementation, update `deploy_guide.md` at the project root.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
1. Check if any dependency manifests were modified in this session:
|
|
8
|
+
```bash
|
|
9
|
+
git diff --name-only HEAD -- package.json requirements*.txt Pipfile pyproject.toml go.mod Cargo.toml pom.xml build.gradle Gemfile composer.json docker-compose*.yml Dockerfile .tool-versions 2>/dev/null
|
|
10
|
+
```
|
|
11
|
+
2. If no manifest files changed → skip this step entirely
|
|
12
|
+
3. If manifest files changed, scan for **newly added** dependencies (not version bumps):
|
|
13
|
+
```bash
|
|
14
|
+
git diff -U0 HEAD -- package.json requirements*.txt Pipfile pyproject.toml go.mod Cargo.toml pom.xml build.gradle Gemfile composer.json docker-compose*.yml Dockerfile .tool-versions 2>/dev/null | grep '^\+' | grep -v '^\+\+\+' | head -30
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Recording
|
|
18
|
+
|
|
19
|
+
For each genuinely new framework/tool, record in `deploy_guide.md` at project root:
|
|
20
|
+
|
|
21
|
+
| Field | Description | Source |
|
|
22
|
+
|-------|-------------|--------|
|
|
23
|
+
| **Name** | Framework/tool name | Package name from manifest |
|
|
24
|
+
| **Version** | Installed version or constraint | Version spec from manifest |
|
|
25
|
+
| **Purpose** | Why it was introduced | You just added it — you know why |
|
|
26
|
+
| **Install Command** | How to install locally | Standard install command for the ecosystem |
|
|
27
|
+
| **Key Config** | Config files or env vars needed | Config files you just created/modified |
|
|
28
|
+
| **Notes** | Setup gotchas, required services | Docker services, manual steps, env vars |
|
|
29
|
+
|
|
30
|
+
## Template for `deploy_guide.md`
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
# Deploy Guide
|
|
34
|
+
|
|
35
|
+
> Auto-maintained by PrizmKit. Manual edits are preserved.
|
|
36
|
+
> Last updated: YYYY-MM-DD
|
|
37
|
+
|
|
38
|
+
## Frameworks & Tools
|
|
39
|
+
|
|
40
|
+
### <Framework Name>
|
|
41
|
+
|
|
42
|
+
- **Version**: <version constraint>
|
|
43
|
+
- **Purpose**: <why this framework is used>
|
|
44
|
+
- **Install**:
|
|
45
|
+
```bash
|
|
46
|
+
<install command>
|
|
47
|
+
```
|
|
48
|
+
- **Key Config**:
|
|
49
|
+
- `<config file or env var>`: <description>
|
|
50
|
+
- **Notes**:
|
|
51
|
+
- <any setup gotchas, required external services, manual steps>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Update Rules
|
|
55
|
+
|
|
56
|
+
- Create file if absent; append new sections if file exists
|
|
57
|
+
- Update version if framework already documented
|
|
58
|
+
- Preserve manually added content
|
|
59
|
+
- Keep entries sorted alphabetically
|
|
60
|
+
|
|
61
|
+
## Filter Out
|
|
62
|
+
|
|
63
|
+
- Patch version bumps of existing deps
|
|
64
|
+
- Dev-only tools needing no setup (linters, formatters)
|
|
65
|
+
- Transitive/lock-file-only changes
|
|
66
|
+
|
|
67
|
+
## Final Step
|
|
68
|
+
|
|
69
|
+
Stage the file: `git add deploy_guide.md`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# config.json Schema — Tech Stack Fields
|
|
2
|
+
|
|
3
|
+
## Merge Strategy
|
|
4
|
+
|
|
5
|
+
Handles re-init without losing user edits:
|
|
6
|
+
|
|
7
|
+
- Read existing `config.json` if present
|
|
8
|
+
- If `tech_stack` field exists AND `_auto_detected` is `false` or absent:
|
|
9
|
+
→ **SKIP** — user has manually configured tech stack, preserve their settings
|
|
10
|
+
- If `tech_stack` field exists AND `_auto_detected` is `true`:
|
|
11
|
+
→ **MERGE** — overwrite auto-detected values with new detection results, but preserve any keys the user added manually (keys not in the new detection result)
|
|
12
|
+
- If `tech_stack` field does NOT exist:
|
|
13
|
+
→ **WRITE** full detected tech stack with `"_auto_detected": true`
|
|
14
|
+
- Only include fields that were actually detected (no empty/null values)
|
|
15
|
+
|
|
16
|
+
## Field Definitions
|
|
17
|
+
|
|
18
|
+
| Field | Type | Description |
|
|
19
|
+
|-------|------|-------------|
|
|
20
|
+
| `adoption_mode` | string | `"passive"` \| `"advisory"` \| `"active"` |
|
|
21
|
+
| `platform` | string | `"codebuddy"` \| `"claude"` \| `"both"` |
|
|
22
|
+
| `tech_stack` | object | Detected or user-provided tech stack |
|
|
23
|
+
| `tech_stack._auto_detected` | boolean | `true` if auto-detected, `false` if user-provided |
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
Fullstack project:
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"adoption_mode": "passive",
|
|
31
|
+
"platform": "claude",
|
|
32
|
+
"tech_stack": {
|
|
33
|
+
"language": "TypeScript",
|
|
34
|
+
"runtime": "Node.js 20",
|
|
35
|
+
"frontend_framework": "React",
|
|
36
|
+
"frontend_styling": "Tailwind CSS",
|
|
37
|
+
"backend_framework": "Express.js",
|
|
38
|
+
"database": "PostgreSQL",
|
|
39
|
+
"orm": "Prisma",
|
|
40
|
+
"testing": "Vitest",
|
|
41
|
+
"bundler": "Vite",
|
|
42
|
+
"project_type": "fullstack",
|
|
43
|
+
"_auto_detected": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pure Python backend:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"adoption_mode": "passive",
|
|
52
|
+
"platform": "claude",
|
|
53
|
+
"tech_stack": {
|
|
54
|
+
"language": "Python",
|
|
55
|
+
"runtime": "Python >=3.11",
|
|
56
|
+
"backend_framework": "FastAPI",
|
|
57
|
+
"database": "PostgreSQL",
|
|
58
|
+
"orm": "SQLAlchemy",
|
|
59
|
+
"testing": "pytest",
|
|
60
|
+
"project_type": "backend",
|
|
61
|
+
"_auto_detected": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Tech Stack Detection — Field Catalog
|
|
2
|
+
|
|
3
|
+
Adaptive field list — only include fields that apply to the project being scanned.
|
|
4
|
+
|
|
5
|
+
- **Language & Runtime**: e.g. TypeScript + Node.js 20, Python 3.11, Go 1.22
|
|
6
|
+
- **Frontend framework** (if applicable): React, Vue.js, Angular, Next.js, Svelte, etc.
|
|
7
|
+
- **Frontend styling** (if applicable): Tailwind CSS, SCSS, Styled Components, Material UI, etc.
|
|
8
|
+
- **Backend framework** (if applicable): Express.js, FastAPI, Django, NestJS, Gin, etc.
|
|
9
|
+
- **Database** (if applicable): PostgreSQL, MySQL, MongoDB, Redis, SQLite — detected from deps or `docker-compose.yml`
|
|
10
|
+
- **ORM** (if applicable): Prisma, Drizzle, TypeORM, SQLAlchemy, Mongoose, etc.
|
|
11
|
+
- **Bundler** (if applicable): Vite, Webpack, esbuild, Rollup, Turborepo
|
|
12
|
+
- **Testing**: Vitest, Jest, pytest, Go test, etc.
|
|
13
|
+
- **Project type** (inferred): `frontend` | `backend` | `fullstack` | `library` | `cli` | `monorepo`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Update Supplement — Post-Merge Gap-Fill Procedure
|
|
2
|
+
|
|
3
|
+
Runs after tech stack merge in Update mode:
|
|
4
|
+
|
|
5
|
+
1. **Module scan**: Re-scan project directories using the same TWO-TIER model from Step 1. Compare discovered modules against existing MODULE_INDEX in root.prizm.
|
|
6
|
+
2. **Missing L1 check**: For any discovered module with no corresponding L1 `.prizm` doc → create L1 immediately and add to MODULE_INDEX.
|
|
7
|
+
3. **Missing L2 check**: For any module/sub-module that has source files with meaningful logic but no L2 `.prizm` doc → create L2 using the L2 GENERATION TEMPLATE. Judgment call: skip trivial wrapper directories or single-config modules.
|
|
8
|
+
4. **Stale L1 check**: For existing L1 docs, verify FILES count and KEY_FILES are still accurate. Update if source directory contents have changed significantly.
|
|
9
|
+
5. **Report**: Include in the Update report: modules added, L1 docs created, L2 docs created, stale docs refreshed.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Feature: [FEATURE_TITLE]
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
[One paragraph describing the feature purpose and business value]
|
|
5
|
+
|
|
6
|
+
## User Stories
|
|
7
|
+
|
|
8
|
+
### US1: [Story Title]
|
|
9
|
+
**As a** [role]
|
|
10
|
+
**I want** [capability]
|
|
11
|
+
**So that** [benefit]
|
|
12
|
+
|
|
13
|
+
**Acceptance Criteria:**
|
|
14
|
+
- [ ] Given [context], When [action], Then [expected result]
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
### In Scope
|
|
19
|
+
- [Item 1]
|
|
20
|
+
|
|
21
|
+
### Out of Scope
|
|
22
|
+
- [Item 1]
|
|
23
|
+
|
|
24
|
+
## Dependencies
|
|
25
|
+
- [Dependency 1]: [Why needed]
|
|
26
|
+
|
|
27
|
+
## Data Model (if feature involves database changes)
|
|
28
|
+
|
|
29
|
+
### Existing Schema Reference
|
|
30
|
+
<!-- Read existing schema/model files BEFORE designing new tables. Document observed conventions here. -->
|
|
31
|
+
- Tables reviewed: [list existing tables related to this feature]
|
|
32
|
+
- Naming convention: [snake_case/camelCase — match existing]
|
|
33
|
+
- ID strategy: [UUID/auto-increment — match existing]
|
|
34
|
+
- Timestamp pattern: [created_at/updated_at — match existing]
|
|
35
|
+
- Soft delete: [yes/no, field name — match existing]
|
|
36
|
+
|
|
37
|
+
### New/Modified Entities
|
|
38
|
+
| Entity | Type (new/modify) | Key Fields | Relationships | Constraints |
|
|
39
|
+
|--------|-------------------|------------|---------------|-------------|
|
|
40
|
+
| [entity_name] | new | [field: type] | [FK to existing_table] | [NOT NULL, UNIQUE, etc.] |
|
|
41
|
+
|
|
42
|
+
### Open Data Model Questions
|
|
43
|
+
<!-- All questions here MUST be resolved before /prizmkit-plan generates Tasks -->
|
|
44
|
+
- [NEEDS CLARIFICATION] [Any uncertain data model decisions — field types, nullability, relationships]
|
|
45
|
+
|
|
46
|
+
## Constraints
|
|
47
|
+
- [Constraint 1]
|
|
48
|
+
|
|
49
|
+
## Clarifications
|
|
50
|
+
[NEEDS CLARIFICATION]: [Ambiguous item]
|
|
51
|
+
|
|
52
|
+
## Review Checklist
|
|
53
|
+
- [ ] All user stories have acceptance criteria
|
|
54
|
+
- [ ] Scope boundaries are clearly defined
|
|
55
|
+
- [ ] Dependencies are identified
|
|
56
|
+
- [ ] No implementation details (WHAT not HOW)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Clarification Phase — Execution Guide
|
|
2
|
+
|
|
3
|
+
Invoked automatically during `/prizmkit-plan` Phase 0 (specify) when `[NEEDS CLARIFICATION]` markers exist, or during Phase 1 (design) when data model ambiguities arise.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Question Strategy
|
|
8
|
+
|
|
9
|
+
**Ask one question at a time.** Batching questions produces lower-quality answers.
|
|
10
|
+
|
|
11
|
+
For each question:
|
|
12
|
+
1. Cite the exact location in spec.md (e.g., "§3.2 says...")
|
|
13
|
+
2. State what is ambiguous and why it matters for implementation
|
|
14
|
+
3. Provide a **recommended answer** with rationale — gives the user a concrete starting point to accept, modify, or reject
|
|
15
|
+
|
|
16
|
+
**Prioritize by implementation impact** (highest first):
|
|
17
|
+
1. Data model (entities, relationships, field types, constraints, naming) — **hardest to change after code is written**
|
|
18
|
+
2. Functional scope boundaries
|
|
19
|
+
3. UX flow and error handling
|
|
20
|
+
4. Non-functional requirements (performance, security)
|
|
21
|
+
5. Edge cases and integration points
|
|
22
|
+
|
|
23
|
+
## Update Discipline
|
|
24
|
+
|
|
25
|
+
After **each** user answer:
|
|
26
|
+
- Immediately update `spec.md` (do not batch at the end)
|
|
27
|
+
- Remove the resolved `[NEEDS CLARIFICATION]` marker
|
|
28
|
+
- Re-evaluate for new ambiguities exposed by the answer
|
|
29
|
+
|
|
30
|
+
## No Limits Policy
|
|
31
|
+
|
|
32
|
+
**There is no cap on rounds or questions.** Keep asking until:
|
|
33
|
+
- All `[NEEDS CLARIFICATION]` markers are removed, AND
|
|
34
|
+
- No vague or underspecified areas remain, AND
|
|
35
|
+
- You are fully confident about the requirements
|
|
36
|
+
|
|
37
|
+
If the user's answer raises new questions — ask those too. If a previously-resolved item needs revisiting due to new context — go back.
|
|
38
|
+
|
|
39
|
+
## Early Termination
|
|
40
|
+
|
|
41
|
+
If the user says "done", "stop", or "enough" — end immediately and output a summary of:
|
|
42
|
+
- What was resolved
|
|
43
|
+
- What remains unclear (list any open `[NEEDS CLARIFICATION]` items)
|
|
44
|
+
|
|
45
|
+
## Example Exchange
|
|
46
|
+
|
|
47
|
+
**Question:**
|
|
48
|
+
> §3.2 says "User can upload files" but doesn't specify file types or size limits.
|
|
49
|
+
>
|
|
50
|
+
> **Recommended:** Accept JPEG, PNG, PDF up to 10MB — covers common use cases without straining storage.
|
|
51
|
+
>
|
|
52
|
+
> Do you agree, or different constraints?
|
|
53
|
+
|
|
54
|
+
**User:** "Also allow SVG, make it 25MB"
|
|
55
|
+
|
|
56
|
+
**Action:** Update spec.md §3.2 → `File upload: JPEG, PNG, PDF, SVG. Max 25MB per file.` Remove marker.
|
|
57
|
+
|
|
58
|
+
**Follow-up question triggered by answer:**
|
|
59
|
+
> SVGs can contain embedded scripts (XSS risk). Should uploaded SVGs be sanitized (strip `<script>` + event handlers), or restricted to trusted users only?
|
|
60
|
+
>
|
|
61
|
+
> **Recommended:** Sanitize all SVG uploads — allows all users to upload safely.
|
|
62
|
+
|
|
63
|
+
## Guidelines
|
|
64
|
+
|
|
65
|
+
- Stay at WHAT/WHY level — no implementation details (HOW) in spec
|
|
66
|
+
- If an answer contradicts an existing requirement, surface the conflict and ask which takes precedence
|
|
67
|
+
- If the user seems fatigued: "I have N more questions — continue now or later?" — but never silently stop with unresolved ambiguities
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Knowledge Injection — Detailed Steps (2a–2c)
|
|
2
|
+
|
|
3
|
+
**2a.** Gather context — read the **actual code that was changed** plus any available artifacts:
|
|
4
|
+
|
|
5
|
+
- `git diff HEAD` — the real source of truth for what happened
|
|
6
|
+
- `.prizmkit/specs/###-feature-name/context-snapshot.md` — read the '## Implementation Log' section (Dev's changes, decisions, discoveries) and '## Review Notes' section (Reviewer's findings). These are the **preferred source** for pre-categorized decisions and findings. If these sections exist, prefer them over re-extracting from git diff.
|
|
7
|
+
- `.prizmkit/specs/###-feature-name/plan.md` — if feature work, read planned vs actual
|
|
8
|
+
- `.prizmkit/bugfix/<id>/fix-report.md` — if bugfix, read what was discovered
|
|
9
|
+
- `.prizmkit/specs/###-feature-name/failure-log.md` — if a previous session failed, read DISCOVERED_TRAPS. These are high-value TRAPS because they come from actual failures — prioritize injecting them into `.prizm-docs/`
|
|
10
|
+
- The relevant `.prizm-docs/` L1/L2 docs for affected modules
|
|
11
|
+
|
|
12
|
+
**2b.** Extract knowledge from what was **observed in code**, not invented:
|
|
13
|
+
|
|
14
|
+
**TRAPS** (highest priority) — things that look safe but break:
|
|
15
|
+
- Minimal format: `- [SEVERITY] <description> | FIX: <approach>`
|
|
16
|
+
- Full format: `- [SEVERITY] <description> | FIX: <approach> | REF: <hash> | STALE_IF: <glob>`
|
|
17
|
+
- Source: actual bugs hit, surprising behavior discovered in code, non-obvious coupling
|
|
18
|
+
|
|
19
|
+
**TRAPS severity classification**:
|
|
20
|
+
- `[CRITICAL]`: data loss, security, financial error, system crash
|
|
21
|
+
- `[HIGH]`: functional failure, silent error, interface incompatibility
|
|
22
|
+
- `[LOW]`: misleading naming, non-intuitive API, minor performance issue
|
|
23
|
+
|
|
24
|
+
When writing TRAPS:
|
|
25
|
+
- Severity prefix is MANDATORY (e.g., `[CRITICAL]`, `[HIGH]`, `[LOW]`)
|
|
26
|
+
- OPTIONAL: append `| REF: <7-char-hash>` when you know the relevant commit (for traceability)
|
|
27
|
+
- OPTIONAL: append `| STALE_IF: <glob>` when the TRAP is tightly coupled to specific files (for auto-expiry detection)
|
|
28
|
+
|
|
29
|
+
**Consuming [REVIEW] markers** (from staleness check 1g):
|
|
30
|
+
- If you encounter a TRAP prefixed with `[REVIEW]` (e.g., `[REVIEW][HIGH] ...`), verify whether the trap is still valid by checking the current code. If still valid: remove the `[REVIEW]` prefix, keeping the severity. If no longer relevant: delete the TRAP entry and append CHANGELOG.
|
|
31
|
+
|
|
32
|
+
**RULES** — conventions established or constraints discovered:
|
|
33
|
+
- Format: `- MUST/NEVER/PREFER: <rule>`
|
|
34
|
+
- Source: patterns that proved necessary during implementation
|
|
35
|
+
|
|
36
|
+
**DECISIONS** — key design choices that affect future development:
|
|
37
|
+
- Format: `- <what was decided> — <rationale>`
|
|
38
|
+
- Source: non-obvious design choices, interface conventions, cross-module contracts
|
|
39
|
+
- Only record decisions that a future AI session would benefit from knowing
|
|
40
|
+
- Do NOT record obvious implementation details that can be derived by reading the code
|
|
41
|
+
|
|
42
|
+
**QUALITY GATE**: Every item must answer: "If a new AI session reads only `.prizm-docs/` and this entry, does it gain actionable understanding?" If not, discard. Do not record trivially observable code patterns — the AI can read the code directly.
|
|
43
|
+
|
|
44
|
+
**2c.** Inject into the correct `.prizm-docs/` file:
|
|
45
|
+
- Module-level TRAPS/RULES/DECISIONS → the affected **L2** `.prizm` file. If the target L2 does not exist, create it first using the L2 GENERATION TEMPLATE before injecting knowledge. (TRAPS/DECISIONS/RULES belong in L2, not L1.)
|
|
46
|
+
- Project-level RULES/PATTERNS → `root.prizm` (respect the current format — MODULE_INDEX or MODULE_GROUPS — do not convert between them during injection)
|
|
47
|
+
- Cross-module concerns spanning 2+ modules → `root.prizm` CROSS_CUTTING section
|
|
48
|
+
|
|
49
|
+
**RULE**: Only add genuinely new information. Never duplicate existing entries. Never rewrite entire files.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Structural Sync — Detailed Steps (1a–1g)
|
|
2
|
+
|
|
3
|
+
**1a.** Get changed files:
|
|
4
|
+
```bash
|
|
5
|
+
git diff --cached --name-status
|
|
6
|
+
```
|
|
7
|
+
If nothing staged, fallback:
|
|
8
|
+
```bash
|
|
9
|
+
git diff --name-status
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**1b.** Read `.prizm-docs/root.prizm` to get MODULE_INDEX (or MODULE_GROUPS). Map each changed file to its module.
|
|
13
|
+
|
|
14
|
+
**1c.** Classify changes:
|
|
15
|
+
- `A` (added) → add to KEY_FILES, check for new INTERFACES
|
|
16
|
+
- `D` (deleted) → remove from KEY_FILES, update FILE count
|
|
17
|
+
- `M` (modified) → check if public interfaces or dependencies changed
|
|
18
|
+
- `R` (renamed) → update all path references
|
|
19
|
+
|
|
20
|
+
**1d.** Update affected docs (bottom-up: L2 → L1 → L0):
|
|
21
|
+
|
|
22
|
+
- **L2**: If L2 exists → update KEY_FILES, INTERFACES, DATA_FLOW, DEPENDENCIES, CHANGELOG, TRAPS, DECISIONS. If L2 does NOT exist AND the module has Added or Modified source files in the current diff with meaningful logic (not trivial config) → create L2 using the L2 GENERATION TEMPLATE, then populate from source.
|
|
23
|
+
- **L1**: Update FILES count, KEY_FILES (if major files added/removed), DEPENDENCIES (if module-level deps changed). **L1 does NOT contain INTERFACES, DATA_FLOW, TRAPS, or DECISIONS** — those belong in L2 only.
|
|
24
|
+
- **L0 root.prizm**: Update MODULE_INDEX file counts only if counts changed. Update CROSS_CUTTING if cross-module concerns changed. Update only if structural change (module added/removed).
|
|
25
|
+
|
|
26
|
+
**1d-migrate.** Legacy TRAPS format migration (opportunistic):
|
|
27
|
+
While updating an affected L1/L2 doc, if you encounter TRAPS entries **without** a severity prefix (e.g., `- foo | FIX: bar` instead of `- [LOW] foo | FIX: bar`), prepend `[LOW]` as a conservative default. This clears legacy format debt incrementally — only in files already being touched, never as a bulk operation.
|
|
28
|
+
|
|
29
|
+
**1e.** If new directory qualifies as a module (per MODULE_DISCOVERY_CRITERIA in PRIZM-SPEC Section 9.1 Step 2) and matches no existing module:
|
|
30
|
+
1. Create L1 doc immediately, add to MODULE_INDEX.
|
|
31
|
+
2. If the current diff includes Added or Modified source files with meaningful logic in this module → create L2 immediately using the L2 GENERATION TEMPLATE. Otherwise defer L2 to the next session that touches this module.
|
|
32
|
+
|
|
33
|
+
**1f.** Enforce size limits:
|
|
34
|
+
- L0 > 4KB → if using MODULE_INDEX with > 15 entries, convert to MODULE_GROUPS format (group by functional domain). Otherwise, consolidate MODULE_INDEX descriptions.
|
|
35
|
+
- L1 > 4KB → trim KEY_FILES descriptions, ensure RULES <= 3 entries
|
|
36
|
+
- L2 > 5KB → archive old CHANGELOG entries
|
|
37
|
+
|
|
38
|
+
**SKIP structural sync if**: only internal implementation changed (no interface/dependency impact), only comments/whitespace, only .prizm files. **DO NOT skip** test file changes or bug fixes — they may reveal TRAPS worth capturing in L2.
|
|
39
|
+
|
|
40
|
+
**1g. TRAPS staleness check** (only when an L2 doc's TRAPS section has > 10 entries):
|
|
41
|
+
|
|
42
|
+
(Note: `/prizmkit-prizm-docs` Validate uses a lower threshold of > 8 entries to flag TRAPS needing staleness metadata. This step uses > 10 because it performs actual cleanup, which is more expensive. Validate warns early; this step acts later.)
|
|
43
|
+
|
|
44
|
+
Perform a quick staleness scan on existing TRAPS to prevent unbounded accumulation:
|
|
45
|
+
1. If a TRAP has `STALE_IF:` and the glob-matched files no longer exist (verified via `ls`) → delete the TRAP entry, append CHANGELOG: `remove: archived stale TRAP - <summary>`
|
|
46
|
+
2. If a TRAP has `REF:` → check if the referenced file still exists and the REF commit is less than 180 days old (via `git log --since="180 days ago" <hash> 2>/dev/null`). If the file is deleted OR the REF commit is older than 180 days → prepend `[REVIEW]` to the severity, signaling it needs verification during the next retrospective Job 2
|
|
47
|
+
3. Process at most 5 of the oldest TRAPS per L2 doc per session (to bound context cost)
|
|
48
|
+
|
|
49
|
+
This step is lightweight — it only triggers when TRAPS exceed 10 entries, and processes at most 5 per run.
|
|
50
|
+
|
|
51
|
+
**1h. Periodic full TRAPS audit** (runs approximately once every 10 retrospective sessions):
|
|
52
|
+
|
|
53
|
+
Check `.prizm-docs/changelog.prizm` for the marker `audit: traps-audit full-scan`. If the marker does not exist, OR the most recent `audit: traps-audit full-scan` entry has 10+ other changelog entries after it:
|
|
54
|
+
|
|
55
|
+
1. Scan ALL L2 `.prizm` files (not just ones touched this session)
|
|
56
|
+
2. For each L2 doc with TRAPS entries:
|
|
57
|
+
- Verify referenced file paths still exist (via `ls`)
|
|
58
|
+
- If `STALE_IF:` glob matches zero files → delete the TRAP, append CHANGELOG: `remove: archived stale TRAP - <summary>`
|
|
59
|
+
- If `REF:` commit is older than 180 days AND the referenced file was significantly refactored (>50% lines changed since REF commit) → prepend `[REVIEW]`
|
|
60
|
+
3. Process at most 3 TRAPS per L2 doc to bound context cost (total max ~15 TRAPS per audit)
|
|
61
|
+
4. Append to `.prizm-docs/changelog.prizm`: `- root | audit: traps-audit full-scan — checked N docs, removed M stale, marked K for review`
|
|
62
|
+
|
|
63
|
+
This is a lightweight background pass. If the project has fewer than 5 L2 docs, skip this step (not enough accumulation to warrant auditing).
|
package/package.json
CHANGED
package/src/config.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrizmKit Config Command
|
|
3
|
+
*
|
|
4
|
+
* Reconfigure an existing PrizmKit installation without a full reinstall.
|
|
5
|
+
* Supports changing platform, AI CLI, skills suite, rules, team, and pipeline.
|
|
6
|
+
*
|
|
7
|
+
* Flow: READ manifest → PROMPT user → COMPUTE DIFF → DISPLAY SUMMARY → EXECUTE
|
|
8
|
+
*
|
|
9
|
+
* Preserves runtime files (.prizm-docs/, dev-pipeline/state/, .prizmkit/specs/).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { readFileSync } from 'fs';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { confirm } from '@inquirer/prompts';
|
|
19
|
+
|
|
20
|
+
import { readManifest, writeManifest, buildManifest, diffManifest } from './manifest.js';
|
|
21
|
+
import {
|
|
22
|
+
loadMetadata,
|
|
23
|
+
loadRulesMetadata,
|
|
24
|
+
getAgentsDir,
|
|
25
|
+
} from './metadata.js';
|
|
26
|
+
import {
|
|
27
|
+
installSkills,
|
|
28
|
+
installAgents,
|
|
29
|
+
installSettings,
|
|
30
|
+
installTeamConfig,
|
|
31
|
+
installProjectMemory,
|
|
32
|
+
installPipeline,
|
|
33
|
+
installGitignore,
|
|
34
|
+
resolveSkillList,
|
|
35
|
+
resolvePipelineFileList,
|
|
36
|
+
} from './scaffold.js';
|
|
37
|
+
import {
|
|
38
|
+
removeSkillFiles,
|
|
39
|
+
removeAgentFiles,
|
|
40
|
+
removeRuleFiles,
|
|
41
|
+
removePipelineFiles,
|
|
42
|
+
} from './upgrade.js';
|
|
43
|
+
import { detectPlatform } from './detect-platform.js';
|
|
44
|
+
import {
|
|
45
|
+
selectPlatform,
|
|
46
|
+
selectAiCli,
|
|
47
|
+
selectSkillSuite,
|
|
48
|
+
selectRulesPreset,
|
|
49
|
+
confirmTeamMode,
|
|
50
|
+
confirmPipeline,
|
|
51
|
+
} from './prompts.js';
|
|
52
|
+
|
|
53
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// Helpers
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect which platforms are actually installed on filesystem.
|
|
62
|
+
*/
|
|
63
|
+
async function detectInstalledPlatforms(projectRoot) {
|
|
64
|
+
const hasClaude = await fs.pathExists(path.join(projectRoot, '.claude', 'commands'))
|
|
65
|
+
|| await fs.pathExists(path.join(projectRoot, '.claude', 'agents'));
|
|
66
|
+
const hasCodeBuddy = await fs.pathExists(path.join(projectRoot, '.codebuddy', 'skills'))
|
|
67
|
+
|| await fs.pathExists(path.join(projectRoot, '.codebuddy', 'agents'));
|
|
68
|
+
|
|
69
|
+
if (hasClaude && hasCodeBuddy) return 'both';
|
|
70
|
+
if (hasCodeBuddy) return 'codebuddy';
|
|
71
|
+
if (hasClaude) return 'claude';
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Expand a platform string to an array of individual platforms.
|
|
77
|
+
*/
|
|
78
|
+
function expandPlatforms(platform) {
|
|
79
|
+
return platform === 'both' ? ['codebuddy', 'claude'] : [platform];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const platformLabel = (p) => p === 'both' ? 'CodeBuddy + Claude Code'
|
|
83
|
+
: p === 'codebuddy' ? 'CodeBuddy' : 'Claude Code';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove all PrizmKit-owned platform-specific files for a single platform.
|
|
87
|
+
* Selective removal — only removes known PrizmKit files, leaves user-created files intact.
|
|
88
|
+
*/
|
|
89
|
+
async function removePlatformFiles(platform, projectRoot, manifest, dryRun) {
|
|
90
|
+
const skillNames = manifest?.files?.skills || [];
|
|
91
|
+
const agentFileNames = manifest?.files?.agents || [];
|
|
92
|
+
const ruleFileNames = manifest?.files?.rules || [];
|
|
93
|
+
|
|
94
|
+
if (skillNames.length) {
|
|
95
|
+
console.log(chalk.blue(`\n 删除 ${platformLabel(platform)} skills:`));
|
|
96
|
+
await removeSkillFiles(platform, projectRoot, skillNames, dryRun);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (agentFileNames.length) {
|
|
100
|
+
console.log(chalk.blue(`\n 删除 ${platformLabel(platform)} agents:`));
|
|
101
|
+
await removeAgentFiles(platform, projectRoot, agentFileNames, dryRun);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ruleFileNames.length) {
|
|
105
|
+
console.log(chalk.blue(`\n 删除 ${platformLabel(platform)} rules:`));
|
|
106
|
+
await removeRuleFiles(platform, projectRoot, ruleFileNames, dryRun);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Settings
|
|
110
|
+
const settingsFile = platform === 'claude'
|
|
111
|
+
? path.join(projectRoot, '.claude', 'settings.json')
|
|
112
|
+
: path.join(projectRoot, '.codebuddy', 'settings.json');
|
|
113
|
+
if (await fs.pathExists(settingsFile)) {
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
console.log(chalk.gray(` [dry-run] remove ${path.relative(projectRoot, settingsFile)}`));
|
|
116
|
+
} else {
|
|
117
|
+
await fs.remove(settingsFile);
|
|
118
|
+
console.log(chalk.red(` ✗ removed ${path.relative(projectRoot, settingsFile)}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Team info
|
|
123
|
+
if (platform === 'claude') {
|
|
124
|
+
const teamInfo = path.join(projectRoot, '.claude', 'team-info.json');
|
|
125
|
+
if (await fs.pathExists(teamInfo)) {
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
console.log(chalk.gray(' [dry-run] remove .claude/team-info.json'));
|
|
128
|
+
} else {
|
|
129
|
+
await fs.remove(teamInfo);
|
|
130
|
+
console.log(chalk.red(' ✗ removed .claude/team-info.json'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else if (platform === 'codebuddy') {
|
|
134
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
135
|
+
const teamDir = path.join(homeDir, '.codebuddy', 'teams', 'prizm-dev-team');
|
|
136
|
+
if (await fs.pathExists(teamDir)) {
|
|
137
|
+
if (dryRun) {
|
|
138
|
+
console.log(chalk.gray(' [dry-run] remove ~/.codebuddy/teams/prizm-dev-team/'));
|
|
139
|
+
} else {
|
|
140
|
+
await fs.remove(teamDir);
|
|
141
|
+
console.log(chalk.red(' ✗ removed ~/.codebuddy/teams/prizm-dev-team/'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Project memory file
|
|
147
|
+
const memoryFile = platform === 'claude' ? 'CLAUDE.md' : 'CODEBUDDY.md';
|
|
148
|
+
const memoryPath = path.join(projectRoot, memoryFile);
|
|
149
|
+
if (await fs.pathExists(memoryPath)) {
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
console.log(chalk.gray(` [dry-run] remove ${memoryFile}`));
|
|
152
|
+
} else {
|
|
153
|
+
await fs.remove(memoryPath);
|
|
154
|
+
console.log(chalk.red(` ✗ removed ${memoryFile}`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Command assets (Claude only)
|
|
159
|
+
if (platform === 'claude') {
|
|
160
|
+
const assetsDir = path.join(projectRoot, '.claude', 'command-assets');
|
|
161
|
+
if (await fs.pathExists(assetsDir)) {
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
console.log(chalk.gray(' [dry-run] remove .claude/command-assets/'));
|
|
164
|
+
} else {
|
|
165
|
+
await fs.remove(assetsDir);
|
|
166
|
+
console.log(chalk.red(' ✗ removed .claude/command-assets/'));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================
|
|
173
|
+
// Main
|
|
174
|
+
// ============================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run the config reconfiguration command.
|
|
178
|
+
* @param {string} directory - target project directory
|
|
179
|
+
* @param {Object} options - CLI options from commander
|
|
180
|
+
*/
|
|
181
|
+
export async function runConfig(directory, options = {}) {
|
|
182
|
+
const projectRoot = path.resolve(directory || '.');
|
|
183
|
+
const dryRun = Boolean(options.dryRun);
|
|
184
|
+
const nonInteractive = Boolean(options.nonInteractive);
|
|
185
|
+
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(chalk.bold(' PrizmKit Config'));
|
|
188
|
+
console.log(` Target: ${projectRoot}`);
|
|
189
|
+
if (dryRun) console.log(chalk.yellow(' Mode: dry-run'));
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
// ── Phase 1: READ current state ──────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
const oldManifest = await readManifest(projectRoot);
|
|
195
|
+
if (!oldManifest) {
|
|
196
|
+
console.log(chalk.red(' ✗ 未找到 .prizmkit/manifest.json'));
|
|
197
|
+
console.log(chalk.gray(' 请先运行 `npx prizmkit install .` 安装 PrizmKit。'));
|
|
198
|
+
console.log('');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Read user config
|
|
203
|
+
const configPath = path.join(projectRoot, '.prizmkit', 'config.json');
|
|
204
|
+
let userConfig = {};
|
|
205
|
+
if (await fs.pathExists(configPath)) {
|
|
206
|
+
try { userConfig = await fs.readJSON(configPath); } catch { /* ignore */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Filesystem-based platform detection (overrides manifest)
|
|
210
|
+
const detectedPlatform = await detectInstalledPlatforms(projectRoot) || oldManifest.platform || 'claude';
|
|
211
|
+
|
|
212
|
+
const currentConfig = {
|
|
213
|
+
platform: detectedPlatform,
|
|
214
|
+
suite: oldManifest.suite || 'core',
|
|
215
|
+
rules: oldManifest.options?.rules || 'recommended',
|
|
216
|
+
team: oldManifest.options?.team ?? true,
|
|
217
|
+
pipeline: oldManifest.options?.pipeline ?? true,
|
|
218
|
+
aiCli: userConfig.ai_cli || oldManifest.options?.aiCli || '',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Display current config
|
|
222
|
+
console.log(chalk.bold(' 当前配置:'));
|
|
223
|
+
console.log(` 平台: ${chalk.cyan(platformLabel(currentConfig.platform))}`);
|
|
224
|
+
console.log(` AI CLI: ${currentConfig.aiCli ? chalk.cyan(currentConfig.aiCli) : chalk.gray('(未设置)')}`);
|
|
225
|
+
console.log(` 技能套件: ${chalk.cyan(currentConfig.suite)}`);
|
|
226
|
+
console.log(` 规则: ${chalk.cyan(currentConfig.rules)}`);
|
|
227
|
+
console.log(` 团队模式: ${currentConfig.team ? chalk.green('启用') : chalk.gray('禁用')}`);
|
|
228
|
+
console.log(` 流水线: ${currentConfig.pipeline ? chalk.green('启用') : chalk.gray('禁用')}`);
|
|
229
|
+
console.log('');
|
|
230
|
+
|
|
231
|
+
// ── Phase 2: PROMPT for new config ───────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
let newConfig;
|
|
234
|
+
|
|
235
|
+
if (nonInteractive) {
|
|
236
|
+
// Non-interactive: only change what's explicitly specified via CLI options
|
|
237
|
+
if (!options.platform && !options.skills && !options.rules
|
|
238
|
+
&& options.aiCli === undefined && options.team === undefined && options.pipeline === undefined) {
|
|
239
|
+
console.log(chalk.yellow(' ⚠ 非交互式模式下未指定任何变更。'));
|
|
240
|
+
console.log(chalk.gray(' 使用 --platform, --skills, --rules, --ai-cli, --team/--no-team, --pipeline/--no-pipeline'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
newConfig = {
|
|
245
|
+
platform: options.platform || currentConfig.platform,
|
|
246
|
+
suite: options.skills || currentConfig.suite,
|
|
247
|
+
rules: options.rules || currentConfig.rules,
|
|
248
|
+
team: options.team !== undefined ? options.team : currentConfig.team,
|
|
249
|
+
pipeline: options.pipeline !== undefined ? options.pipeline : currentConfig.pipeline,
|
|
250
|
+
aiCli: options.aiCli !== undefined ? options.aiCli : currentConfig.aiCli,
|
|
251
|
+
};
|
|
252
|
+
} else {
|
|
253
|
+
// Interactive: let user select each setting
|
|
254
|
+
const detected = detectPlatform();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
console.log(chalk.bold(' 选择新配置(按 Enter 保持当前值):\n'));
|
|
258
|
+
|
|
259
|
+
const newPlatform = await selectPlatform(currentConfig.platform, detected);
|
|
260
|
+
const newAiCli = await selectAiCli(currentConfig.aiCli, detected);
|
|
261
|
+
|
|
262
|
+
const metadata = await loadMetadata();
|
|
263
|
+
const newSuite = await selectSkillSuite(currentConfig.suite, metadata);
|
|
264
|
+
const newRules = await selectRulesPreset(currentConfig.rules);
|
|
265
|
+
const newTeam = await confirmTeamMode(currentConfig.team);
|
|
266
|
+
const newPipeline = await confirmPipeline(currentConfig.pipeline);
|
|
267
|
+
|
|
268
|
+
newConfig = {
|
|
269
|
+
platform: newPlatform,
|
|
270
|
+
suite: newSuite,
|
|
271
|
+
rules: newRules,
|
|
272
|
+
team: newTeam,
|
|
273
|
+
pipeline: newPipeline,
|
|
274
|
+
aiCli: newAiCli,
|
|
275
|
+
};
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.message?.includes('User force closed')) {
|
|
278
|
+
console.log(chalk.yellow('\n 配置已取消。'));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Phase 3: COMPUTE DIFF ────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
const changes = [];
|
|
288
|
+
if (newConfig.platform !== currentConfig.platform) {
|
|
289
|
+
changes.push(`平台: ${platformLabel(currentConfig.platform)} → ${platformLabel(newConfig.platform)}`);
|
|
290
|
+
}
|
|
291
|
+
if (newConfig.aiCli !== currentConfig.aiCli) {
|
|
292
|
+
changes.push(`AI CLI: ${currentConfig.aiCli || '(未设置)'} → ${newConfig.aiCli || '(未设置)'}`);
|
|
293
|
+
}
|
|
294
|
+
if (newConfig.suite !== currentConfig.suite) {
|
|
295
|
+
changes.push(`技能套件: ${currentConfig.suite} → ${newConfig.suite}`);
|
|
296
|
+
}
|
|
297
|
+
if (newConfig.rules !== currentConfig.rules) {
|
|
298
|
+
changes.push(`规则: ${currentConfig.rules} → ${newConfig.rules}`);
|
|
299
|
+
}
|
|
300
|
+
if (newConfig.team !== currentConfig.team) {
|
|
301
|
+
changes.push(`团队模式: ${currentConfig.team ? '启用' : '禁用'} → ${newConfig.team ? '启用' : '禁用'}`);
|
|
302
|
+
}
|
|
303
|
+
if (newConfig.pipeline !== currentConfig.pipeline) {
|
|
304
|
+
changes.push(`流水线: ${currentConfig.pipeline ? '启用' : '禁用'} → ${newConfig.pipeline ? '启用' : '禁用'}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Phase 4: DISPLAY SUMMARY + CONFIRM ───────────────────────────────────
|
|
308
|
+
|
|
309
|
+
if (changes.length === 0) {
|
|
310
|
+
console.log(chalk.green('\n ✓ 配置未变更,无需操作。'));
|
|
311
|
+
console.log('');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log(chalk.bold('\n 变更摘要:'));
|
|
316
|
+
for (const change of changes) {
|
|
317
|
+
console.log(` ${chalk.yellow('→')} ${change}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Warn about destructive changes
|
|
321
|
+
if (newConfig.platform !== currentConfig.platform) {
|
|
322
|
+
const oldPlatforms = expandPlatforms(currentConfig.platform);
|
|
323
|
+
const newPlatforms = expandPlatforms(newConfig.platform);
|
|
324
|
+
const dropping = oldPlatforms.filter(p => !newPlatforms.includes(p));
|
|
325
|
+
if (dropping.length) {
|
|
326
|
+
console.log(chalk.yellow(`\n ⚠ 将移除 ${dropping.map(platformLabel).join(', ')} 的 PrizmKit 文件`));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log('');
|
|
330
|
+
|
|
331
|
+
if (dryRun) {
|
|
332
|
+
console.log(chalk.yellow(' [dry-run] 预览完成,未修改任何文件。'));
|
|
333
|
+
console.log('');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!nonInteractive) {
|
|
338
|
+
const proceed = await confirm({
|
|
339
|
+
message: '确认应用配置变更?',
|
|
340
|
+
default: true,
|
|
341
|
+
});
|
|
342
|
+
if (!proceed) {
|
|
343
|
+
console.log(chalk.yellow(' 配置已取消。'));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Phase 5: EXECUTE ─────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
console.log(chalk.bold('\n 应用配置变更...\n'));
|
|
351
|
+
|
|
352
|
+
const oldPlatforms = expandPlatforms(currentConfig.platform);
|
|
353
|
+
const newPlatforms = expandPlatforms(newConfig.platform);
|
|
354
|
+
const platformsToRemove = oldPlatforms.filter(p => !newPlatforms.includes(p));
|
|
355
|
+
const platformsToAdd = newPlatforms.filter(p => !oldPlatforms.includes(p));
|
|
356
|
+
const platformsToUpdate = newPlatforms.filter(p => oldPlatforms.includes(p));
|
|
357
|
+
|
|
358
|
+
// Resolve new file lists
|
|
359
|
+
const newSkillList = await resolveSkillList(newConfig.suite);
|
|
360
|
+
const agentsDir = getAgentsDir();
|
|
361
|
+
const newAgentFiles = (await fs.readdir(agentsDir)).filter(f => f.endsWith('.md'));
|
|
362
|
+
const rulesMeta = await loadRulesMetadata();
|
|
363
|
+
const rulesPresetDef = rulesMeta.presets[newConfig.rules] || rulesMeta.presets.recommended;
|
|
364
|
+
const newRuleFiles = (rulesPresetDef?.rules || []).map(name => `${name}.md`);
|
|
365
|
+
const newPipelineFiles = newConfig.pipeline ? resolvePipelineFileList() : [];
|
|
366
|
+
|
|
367
|
+
// 5a. Remove files for platforms being dropped
|
|
368
|
+
for (const p of platformsToRemove) {
|
|
369
|
+
console.log(chalk.bold(` 移除 ${platformLabel(p)} 平台文件...`));
|
|
370
|
+
await removePlatformFiles(p, projectRoot, oldManifest, false);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 5b. Handle skill/rule diff for existing platforms (suite or rules changed)
|
|
374
|
+
if (newConfig.suite !== currentConfig.suite || newConfig.rules !== currentConfig.rules) {
|
|
375
|
+
const newTempManifest = buildManifest({
|
|
376
|
+
version: pkg.version, platform: newConfig.platform, suite: newConfig.suite,
|
|
377
|
+
skills: newSkillList, agents: newAgentFiles, rules: newRuleFiles,
|
|
378
|
+
pipeline: newPipelineFiles, team: newConfig.team, aiCli: newConfig.aiCli,
|
|
379
|
+
rulesPreset: newConfig.rules, extras: oldManifest?.files?.extras || [],
|
|
380
|
+
});
|
|
381
|
+
const diff = diffManifest(oldManifest, newTempManifest);
|
|
382
|
+
|
|
383
|
+
// Remove orphaned skills/rules from existing platforms
|
|
384
|
+
for (const p of platformsToUpdate) {
|
|
385
|
+
if (diff.skills.removed.length) {
|
|
386
|
+
console.log(chalk.blue(`\n 删除多余 skills (${platformLabel(p)}):`));
|
|
387
|
+
await removeSkillFiles(p, projectRoot, diff.skills.removed, false);
|
|
388
|
+
}
|
|
389
|
+
if (diff.rules.removed.length) {
|
|
390
|
+
console.log(chalk.blue(`\n 删除多余 rules (${platformLabel(p)}):`));
|
|
391
|
+
await removeRuleFiles(p, projectRoot, diff.rules.removed, false);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 5c. Install/update files for new + existing platforms
|
|
397
|
+
const allTargetPlatforms = [...platformsToAdd, ...platformsToUpdate];
|
|
398
|
+
const needsReinstall = newConfig.platform !== currentConfig.platform
|
|
399
|
+
|| newConfig.suite !== currentConfig.suite
|
|
400
|
+
|| newConfig.rules !== currentConfig.rules
|
|
401
|
+
|| newConfig.team !== currentConfig.team;
|
|
402
|
+
|
|
403
|
+
for (const p of allTargetPlatforms) {
|
|
404
|
+
const isNew = platformsToAdd.includes(p);
|
|
405
|
+
|
|
406
|
+
if (isNew || needsReinstall) {
|
|
407
|
+
console.log(chalk.bold(`\n ${isNew ? '安装' : '更新'} ${platformLabel(p)} 环境...\n`));
|
|
408
|
+
|
|
409
|
+
console.log(chalk.blue(' Skills:'));
|
|
410
|
+
await installSkills(p, newSkillList, projectRoot, false);
|
|
411
|
+
|
|
412
|
+
console.log(chalk.blue('\n Agents:'));
|
|
413
|
+
await installAgents(p, projectRoot, false);
|
|
414
|
+
|
|
415
|
+
console.log(chalk.blue('\n Settings & Rules:'));
|
|
416
|
+
await installSettings(p, projectRoot, { pipeline: newConfig.pipeline, rules: newConfig.rules }, false);
|
|
417
|
+
|
|
418
|
+
if (isNew) {
|
|
419
|
+
console.log(chalk.blue('\n Project Memory:'));
|
|
420
|
+
await installProjectMemory(p, projectRoot, false);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (newConfig.team) {
|
|
424
|
+
console.log(chalk.blue('\n Team Config:'));
|
|
425
|
+
await installTeamConfig(p, projectRoot, false);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 5d. Pipeline changes
|
|
431
|
+
if (newConfig.pipeline !== currentConfig.pipeline) {
|
|
432
|
+
if (newConfig.pipeline) {
|
|
433
|
+
console.log(chalk.blue('\n 安装 Pipeline:'));
|
|
434
|
+
await installPipeline(projectRoot, false);
|
|
435
|
+
} else {
|
|
436
|
+
console.log(chalk.blue('\n 移除 Pipeline 框架文件(保留 state/):'));
|
|
437
|
+
const oldPipelineFiles = oldManifest?.files?.pipeline || [];
|
|
438
|
+
if (oldPipelineFiles.length) {
|
|
439
|
+
await removePipelineFiles(projectRoot, oldPipelineFiles, false);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 5e. Update .gitignore
|
|
445
|
+
console.log(chalk.blue('\n Gitignore:'));
|
|
446
|
+
await installGitignore(projectRoot, { pipeline: newConfig.pipeline }, false);
|
|
447
|
+
|
|
448
|
+
// 5f. Update .prizmkit/config.json (ai_cli)
|
|
449
|
+
if (newConfig.aiCli !== currentConfig.aiCli) {
|
|
450
|
+
const prizmkitDir = path.join(projectRoot, '.prizmkit');
|
|
451
|
+
await fs.ensureDir(prizmkitDir);
|
|
452
|
+
let existingConfig = {};
|
|
453
|
+
if (await fs.pathExists(configPath)) {
|
|
454
|
+
try { existingConfig = await fs.readJSON(configPath); } catch { /* ignore */ }
|
|
455
|
+
}
|
|
456
|
+
if (newConfig.aiCli) {
|
|
457
|
+
existingConfig.ai_cli = newConfig.aiCli;
|
|
458
|
+
} else {
|
|
459
|
+
delete existingConfig.ai_cli;
|
|
460
|
+
}
|
|
461
|
+
await fs.writeJSON(configPath, existingConfig, { spaces: 2 });
|
|
462
|
+
console.log(chalk.green(`\n ✓ .prizmkit/config.json (ai_cli: ${newConfig.aiCli || '(cleared)'})`));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 5g. Write new manifest
|
|
466
|
+
const newManifest = buildManifest({
|
|
467
|
+
version: pkg.version,
|
|
468
|
+
platform: newConfig.platform,
|
|
469
|
+
suite: newConfig.suite,
|
|
470
|
+
skills: newSkillList,
|
|
471
|
+
agents: newAgentFiles,
|
|
472
|
+
rules: newRuleFiles,
|
|
473
|
+
pipeline: newPipelineFiles,
|
|
474
|
+
team: newConfig.team,
|
|
475
|
+
aiCli: newConfig.aiCli,
|
|
476
|
+
rulesPreset: newConfig.rules,
|
|
477
|
+
extras: oldManifest?.files?.extras || [],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Preserve original installedAt
|
|
481
|
+
if (oldManifest?.installedAt) {
|
|
482
|
+
newManifest.installedAt = oldManifest.installedAt;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await writeManifest(projectRoot, newManifest);
|
|
486
|
+
console.log(chalk.green(' ✓ .prizmkit/manifest.json'));
|
|
487
|
+
|
|
488
|
+
// ── Done ─────────────────────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
console.log('');
|
|
491
|
+
console.log(chalk.bold(' ════════════════════════════════════════════════'));
|
|
492
|
+
console.log(chalk.green.bold(' ✅ 配置变更完成!'));
|
|
493
|
+
console.log(chalk.bold(' ════════════════════════════════════════════════'));
|
|
494
|
+
console.log('');
|
|
495
|
+
|
|
496
|
+
console.log(chalk.gray(' 新配置:'));
|
|
497
|
+
console.log(chalk.gray(` 平台: ${platformLabel(newConfig.platform)}`));
|
|
498
|
+
console.log(chalk.gray(` AI CLI: ${newConfig.aiCli || '(未设置)'}`));
|
|
499
|
+
console.log(chalk.gray(` 技能套件: ${newConfig.suite}`));
|
|
500
|
+
console.log(chalk.gray(` 规则: ${newConfig.rules}`));
|
|
501
|
+
console.log(chalk.gray(` 团队模式: ${newConfig.team ? '启用' : '禁用'}`));
|
|
502
|
+
console.log(chalk.gray(` 流水线: ${newConfig.pipeline ? '启用' : '禁用'}`));
|
|
503
|
+
console.log('');
|
|
504
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable prompting functions for PrizmKit CLI.
|
|
3
|
+
* These functions encapsulate the interactive UI used by both install and config commands.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { select, confirm, checkbox, input } from '@inquirer/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Select target platform (codebuddy, claude, or both).
|
|
11
|
+
* @param {string} currentPlatform - current platform value
|
|
12
|
+
* @param {Object} detected - detected environment info (e.g., { cbc: true, claude: false })
|
|
13
|
+
* @returns {Promise<string>} - 'codebuddy', 'claude', or 'both'
|
|
14
|
+
*/
|
|
15
|
+
export async function selectPlatform(currentPlatform, detected = {}) {
|
|
16
|
+
const platform = await select({
|
|
17
|
+
message: '选择目标平台 (决定安装的目录结构):',
|
|
18
|
+
choices: [
|
|
19
|
+
{
|
|
20
|
+
name: `CodeBuddy (.codebuddy/)${detected.cbc ? chalk.green(' ← 已检测到 cbc') : ''}`,
|
|
21
|
+
value: 'codebuddy',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: `Claude Code (.claude/)${(detected.claude || detected.claudeInternal) ? chalk.green(' ← 已检测到') : ''}`,
|
|
25
|
+
value: 'claude',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: '同时安装两个平台',
|
|
29
|
+
value: 'both',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
default: currentPlatform || detected.suggested || 'claude',
|
|
33
|
+
});
|
|
34
|
+
return platform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Select AI CLI command (cbc, claude, custom, or skip).
|
|
39
|
+
* @param {string} currentCommand - current AI CLI command
|
|
40
|
+
* @param {Object} detected - detected environment info
|
|
41
|
+
* @returns {Promise<string>} - 'cbc', 'claude', custom string, or ''
|
|
42
|
+
*/
|
|
43
|
+
export async function selectAiCli(currentCommand, detected = {}) {
|
|
44
|
+
const cliChoices = [
|
|
45
|
+
{
|
|
46
|
+
name: `cbc — CodeBuddy CLI${detected.cbc ? chalk.green(' ✓ 已安装') : chalk.gray(' (未检测到)')}`,
|
|
47
|
+
value: 'cbc',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: `claude — Claude Code${detected.claude ? chalk.green(' ✓ 已安装') : chalk.gray(' (未检测到)')}`,
|
|
51
|
+
value: 'claude',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: '自定义 — 输入其他命令',
|
|
55
|
+
value: '__custom__',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: '跳过 — 稍后手动配置',
|
|
59
|
+
value: '',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const knownCliValues = ['cbc', 'claude'];
|
|
64
|
+
const cliDefault = knownCliValues.includes(currentCommand)
|
|
65
|
+
? currentCommand
|
|
66
|
+
: (currentCommand ? '__custom__' : (detected.suggestedCli || ''));
|
|
67
|
+
|
|
68
|
+
const cliSelection = await select({
|
|
69
|
+
message: '选择底层 AI CLI 可执行命令:',
|
|
70
|
+
choices: cliChoices,
|
|
71
|
+
default: cliDefault,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let aiCli = cliSelection;
|
|
75
|
+
if (cliSelection === '__custom__') {
|
|
76
|
+
aiCli = await input({
|
|
77
|
+
message: '输入自定义 AI CLI 命令:',
|
|
78
|
+
default: currentCommand || (detected.suggestedCli || ''),
|
|
79
|
+
validate: (v) => v.trim() ? true : '请输入命令',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return aiCli;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Select skill suite (core, minimal, or recommended:<type>).
|
|
88
|
+
* @param {string} currentSuite - current suite value
|
|
89
|
+
* @param {Object} metadata - skill metadata with suites definitions
|
|
90
|
+
* @returns {Promise<string>} - 'core', 'minimal', or 'recommended:type'
|
|
91
|
+
*/
|
|
92
|
+
export async function selectSkillSuite(currentSuite, metadata) {
|
|
93
|
+
const choices = [
|
|
94
|
+
{
|
|
95
|
+
name: `Core — 核心工作流 (${metadata.suites.core.skills.length} 个技能)`,
|
|
96
|
+
value: 'core',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: `Minimal — 最小可用 (${metadata.suites.minimal.skills.length} 个技能)`,
|
|
100
|
+
value: 'minimal',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Add recommended options if available
|
|
105
|
+
if (metadata.recommendations) {
|
|
106
|
+
for (const [type, rec] of Object.entries(metadata.recommendations)) {
|
|
107
|
+
const baseSkills = rec.base === '*'
|
|
108
|
+
? Object.keys(metadata.skills).length
|
|
109
|
+
: metadata.suites[rec.base]?.skills?.length || 0;
|
|
110
|
+
choices.push({
|
|
111
|
+
name: `Recommended: ${type} (${baseSkills} 个技能)`,
|
|
112
|
+
value: `recommended:${type}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const skillSuite = await select({
|
|
118
|
+
message: '选择技能套件:',
|
|
119
|
+
choices,
|
|
120
|
+
default: currentSuite || 'core',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return skillSuite;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Select external skills via checkbox.
|
|
128
|
+
* @param {Array<string>} currentSkills - currently selected external skills
|
|
129
|
+
* @param {Object} metadata - skill metadata with external_skills definitions
|
|
130
|
+
* @returns {Promise<Array<string>>} - array of selected skill names
|
|
131
|
+
*/
|
|
132
|
+
export async function selectExternalSkills(currentSkills, metadata) {
|
|
133
|
+
const knownExternalSkills = metadata.external_skills?.known || [];
|
|
134
|
+
if (knownExternalSkills.length === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const selectedSkills = await checkbox({
|
|
139
|
+
message: '选择要安装的第三方 Skills(可选,需要网络):',
|
|
140
|
+
choices: knownExternalSkills.map(s => ({
|
|
141
|
+
name: `${s.name} — ${s.description}`,
|
|
142
|
+
value: s.name,
|
|
143
|
+
checked: (currentSkills || []).includes(s.name),
|
|
144
|
+
})),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return selectedSkills;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Confirm team mode setting.
|
|
152
|
+
* @param {boolean} currentValue - current team mode value
|
|
153
|
+
* @returns {Promise<boolean>}
|
|
154
|
+
*/
|
|
155
|
+
export async function confirmTeamMode(currentValue = true) {
|
|
156
|
+
const team = await confirm({
|
|
157
|
+
message: '启用多 Agent 团队模式 (prizm-dev-team)?',
|
|
158
|
+
default: currentValue,
|
|
159
|
+
});
|
|
160
|
+
return team;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Confirm pipeline setting.
|
|
165
|
+
* @param {boolean} currentValue - current pipeline value
|
|
166
|
+
* @returns {Promise<boolean>}
|
|
167
|
+
*/
|
|
168
|
+
export async function confirmPipeline(currentValue = true) {
|
|
169
|
+
const pipeline = await confirm({
|
|
170
|
+
message: '安装自动化流水线 (dev-pipeline)?',
|
|
171
|
+
default: currentValue,
|
|
172
|
+
});
|
|
173
|
+
return pipeline;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Select rules preset (recommended, minimal, or none).
|
|
178
|
+
* @param {string} currentPreset - current rules preset
|
|
179
|
+
* @returns {Promise<string>} - 'recommended', 'minimal', or 'none'
|
|
180
|
+
*/
|
|
181
|
+
export async function selectRulesPreset(currentPreset = 'recommended') {
|
|
182
|
+
const rulesPreset = await select({
|
|
183
|
+
message: '安装 AI 行为规则 (rules):',
|
|
184
|
+
choices: [
|
|
185
|
+
{ name: 'Recommended — 全部规则 (推荐)', value: 'recommended' },
|
|
186
|
+
{ name: 'Minimal — 仅渐进式加载 + 通用规则', value: 'minimal' },
|
|
187
|
+
{ name: 'None — 不安装规则', value: 'none' },
|
|
188
|
+
],
|
|
189
|
+
default: currentPreset,
|
|
190
|
+
});
|
|
191
|
+
return rulesPreset;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Display a summary of changes and confirm with user.
|
|
196
|
+
* @param {Object} summary - summary object with change descriptions
|
|
197
|
+
* @param {boolean} dryRun - whether this is a dry-run
|
|
198
|
+
* @returns {Promise<boolean>} - true if user confirms
|
|
199
|
+
*/
|
|
200
|
+
export async function confirmChanges(summary, dryRun = false) {
|
|
201
|
+
const message = dryRun
|
|
202
|
+
? 'Review the changes above. These are the changes that would be made (dry-run mode).'
|
|
203
|
+
: 'Review the changes above. Proceed with configuration?';
|
|
204
|
+
|
|
205
|
+
const proceed = await confirm({
|
|
206
|
+
message,
|
|
207
|
+
default: true,
|
|
208
|
+
});
|
|
209
|
+
return proceed;
|
|
210
|
+
}
|