mvc-kit 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,25 @@ Zero-
15
15
  npm install mvc-kit
16
16
  ```
17
17
 
18
+ ## AI Agent Plugin
19
+
20
+ mvc-kit ships with built-in context for AI coding assistants — stays in sync with your installed version automatically.
21
+
22
+ ```bash
23
+ npx mvc-kit-setup # Set up all agents
24
+ npx mvc-kit-setup cursor # Cursor only
25
+ npx mvc-kit-setup copilot # GitHub Copilot only
26
+ ```
27
+
28
+ **Claude Code** — load directly as a plugin (no file copying, auto-updates):
29
+ ```bash
30
+ claude --plugin-dir node_modules/mvc-kit/agent-config/claude-code
31
+ ```
32
+
33
+ Skills: `/mvc-kit:guide` (framework reference), `/mvc-kit:scaffold <type> <Name>` (code generation), `/mvc-kit:review <path>` (pattern review).
34
+
35
+ **Cursor** — writes `.cursorrules`. **Copilot** — writes `.github/copilot-instructions.md`. Both use idempotent markers, safe to re-run.
36
+
18
37
  ## Quick Start
19
38
 
20
39
  ```typescript
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
4
+ import { join, dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createInterface } from 'node:readline';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const AGENT_CONFIG_DIR = resolve(__dirname, '..');
11
+ const PROJECT_DIR = process.cwd();
12
+
13
+ const MARKER_BEGIN = '<!-- BEGIN mvc-kit -->';
14
+ const MARKER_END = '<!-- END mvc-kit -->';
15
+
16
+ const VALID_TARGETS = ['claude', 'cursor', 'copilot', 'all'];
17
+
18
+ // ─── Helpers ──────────────────────────────────────────────
19
+
20
+ function printUsage() {
21
+ console.log(`
22
+ Usage: npx mvc-kit-setup [targets...] [options]
23
+
24
+ Targets:
25
+ claude Set up Claude Code plugin (prints usage instructions)
26
+ cursor Copy mvc-kit rules to .cursorrules
27
+ copilot Copy mvc-kit instructions to .github/copilot-instructions.md
28
+ all Set up all targets (default)
29
+
30
+ Options:
31
+ --help Show this help message
32
+ --force Overwrite existing files without prompting
33
+
34
+ Examples:
35
+ npx mvc-kit-setup # Set up all targets
36
+ npx mvc-kit-setup cursor # Set up Cursor only
37
+ npx mvc-kit-setup cursor copilot # Set up Cursor and Copilot
38
+ npx mvc-kit-setup --force # Overwrite without prompting
39
+ `);
40
+ }
41
+
42
+ function readSourceFile(relativePath) {
43
+ const fullPath = join(AGENT_CONFIG_DIR, relativePath);
44
+ return readFileSync(fullPath, 'utf-8');
45
+ }
46
+
47
+ function wrapWithMarkers(content) {
48
+ return `${MARKER_BEGIN}\n${content.trim()}\n${MARKER_END}`;
49
+ }
50
+
51
+ function replaceMarkerSection(existingContent, newContent) {
52
+ const beginIdx = existingContent.indexOf(MARKER_BEGIN);
53
+ const endIdx = existingContent.indexOf(MARKER_END);
54
+
55
+ if (beginIdx !== -1 && endIdx !== -1) {
56
+ // Replace existing section
57
+ const before = existingContent.slice(0, beginIdx);
58
+ const after = existingContent.slice(endIdx + MARKER_END.length);
59
+ return before + wrapWithMarkers(newContent) + after;
60
+ }
61
+
62
+ // Append new section
63
+ const separator = existingContent.trim() ? '\n\n' : '';
64
+ return existingContent.trim() + separator + wrapWithMarkers(newContent) + '\n';
65
+ }
66
+
67
+ async function confirm(message) {
68
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
69
+ return new Promise((resolve) => {
70
+ rl.question(`${message} (y/N) `, (answer) => {
71
+ rl.close();
72
+ resolve(answer.toLowerCase() === 'y');
73
+ });
74
+ });
75
+ }
76
+
77
+ // ─── Target Handlers ──────────────────────────────────────
78
+
79
+ function setupClaude() {
80
+ const pluginDir = join(AGENT_CONFIG_DIR, 'claude-code');
81
+
82
+ console.log('\n Claude Code');
83
+ console.log(' ───────────');
84
+ console.log(' The mvc-kit plugin is loaded directly from node_modules.');
85
+ console.log(' No file copying needed — it updates automatically with npm update.\n');
86
+ console.log(' To use, start Claude Code with the --plugin-dir flag:\n');
87
+ console.log(` claude --plugin-dir ${pluginDir}\n`);
88
+ console.log(' Skills available:');
89
+ console.log(' /mvc-kit:guide — Framework reference (auto-loaded)');
90
+ console.log(' /mvc-kit:scaffold — Scaffold classes and components');
91
+ console.log(' /mvc-kit:review — Review code for pattern adherence\n');
92
+ }
93
+
94
+ async function setupCursor(force) {
95
+ const targetPath = join(PROJECT_DIR, '.cursorrules');
96
+ const sourceContent = readSourceFile('cursor/cursorrules');
97
+
98
+ console.log('\n Cursor');
99
+ console.log(' ──────');
100
+
101
+ if (existsSync(targetPath)) {
102
+ const existing = readFileSync(targetPath, 'utf-8');
103
+
104
+ if (existing.includes(MARKER_BEGIN)) {
105
+ // Update existing section
106
+ const updated = replaceMarkerSection(existing, sourceContent);
107
+ writeFileSync(targetPath, updated, 'utf-8');
108
+ console.log(' Updated mvc-kit section in .cursorrules\n');
109
+ return;
110
+ }
111
+
112
+ if (!force) {
113
+ const ok = await confirm(' .cursorrules already exists. Append mvc-kit rules?');
114
+ if (!ok) {
115
+ console.log(' Skipped.\n');
116
+ return;
117
+ }
118
+ }
119
+
120
+ // Append with markers
121
+ const updated = replaceMarkerSection(existing, sourceContent);
122
+ writeFileSync(targetPath, updated, 'utf-8');
123
+ console.log(' Appended mvc-kit rules to .cursorrules\n');
124
+ } else {
125
+ writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
126
+ console.log(' Created .cursorrules\n');
127
+ }
128
+ }
129
+
130
+ async function setupCopilot(force) {
131
+ const githubDir = join(PROJECT_DIR, '.github');
132
+ const targetPath = join(githubDir, 'copilot-instructions.md');
133
+ const sourceContent = readSourceFile('copilot/copilot-instructions.md');
134
+
135
+ console.log('\n GitHub Copilot');
136
+ console.log(' ──────────────');
137
+
138
+ if (!existsSync(githubDir)) {
139
+ mkdirSync(githubDir, { recursive: true });
140
+ }
141
+
142
+ if (existsSync(targetPath)) {
143
+ const existing = readFileSync(targetPath, 'utf-8');
144
+
145
+ if (existing.includes(MARKER_BEGIN)) {
146
+ // Update existing section
147
+ const updated = replaceMarkerSection(existing, sourceContent);
148
+ writeFileSync(targetPath, updated, 'utf-8');
149
+ console.log(' Updated mvc-kit section in .github/copilot-instructions.md\n');
150
+ return;
151
+ }
152
+
153
+ if (!force) {
154
+ const ok = await confirm(' .github/copilot-instructions.md already exists. Append mvc-kit instructions?');
155
+ if (!ok) {
156
+ console.log(' Skipped.\n');
157
+ return;
158
+ }
159
+ }
160
+
161
+ const updated = replaceMarkerSection(existing, sourceContent);
162
+ writeFileSync(targetPath, updated, 'utf-8');
163
+ console.log(' Appended mvc-kit instructions to .github/copilot-instructions.md\n');
164
+ } else {
165
+ writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
166
+ console.log(' Created .github/copilot-instructions.md\n');
167
+ }
168
+ }
169
+
170
+ // ─── Main ─────────────────────────────────────────────────
171
+
172
+ async function main() {
173
+ const args = process.argv.slice(2);
174
+ const force = args.includes('--force');
175
+ const help = args.includes('--help') || args.includes('-h');
176
+ const targets = args.filter(a => !a.startsWith('--') && !a.startsWith('-'));
177
+
178
+ if (help) {
179
+ printUsage();
180
+ process.exit(0);
181
+ }
182
+
183
+ // Validate targets
184
+ for (const t of targets) {
185
+ if (!VALID_TARGETS.includes(t)) {
186
+ console.error(`Unknown target: "${t}". Valid targets: ${VALID_TARGETS.join(', ')}`);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ const selectedTargets = targets.length === 0 || targets.includes('all')
192
+ ? ['claude', 'cursor', 'copilot']
193
+ : targets;
194
+
195
+ console.log('\nmvc-kit AI Agent Setup');
196
+ console.log('═════════════════════');
197
+
198
+ if (selectedTargets.includes('claude')) {
199
+ setupClaude();
200
+ }
201
+
202
+ if (selectedTargets.includes('cursor')) {
203
+ await setupCursor(force);
204
+ }
205
+
206
+ if (selectedTargets.includes('copilot')) {
207
+ await setupCopilot(force);
208
+ }
209
+
210
+ console.log('Done! To keep rules in sync after npm update, re-run:');
211
+ console.log(' npx mvc-kit-setup\n');
212
+ }
213
+
214
+ main().catch((err) => {
215
+ console.error('Error:', err.message);
216
+ process.exit(1);
217
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "mvc-kit",
3
+ "version": "2.0.0",
4
+ "description": "AI coding assistant plugin for mvc-kit — a zero-dependency, TypeScript-first reactive state management library. Provides framework guidance, code scaffolding, and pattern review."
5
+ }
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: mvc-kit-architect
3
+ description: "Architecture planning agent for mvc-kit applications. Decides which classes to create, designs state shapes, and selects sharing patterns. Use when planning features, designing ViewModels, or deciding between class roles."
4
+ model: sonnet
5
+ ---
6
+
7
+ You are an architecture planning agent for applications built with **mvc-kit**, a TypeScript-first reactive state management library for React. You help developers plan features by deciding which classes to create, designing state shapes, and selecting sharing patterns.
8
+
9
+ ## Core Classes
10
+
11
+ | Class | Role | Scope |
12
+ |-------|------|-------|
13
+ | `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
14
+ | `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
15
+ | `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
16
+ | `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
17
+ | `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
18
+ | `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
19
+ | `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
20
+
21
+ ## Decision Framework
22
+
23
+ ### Which class?
24
+ 1. Holds UI state for a component? → **ViewModel**
25
+ 2. Single entity with validation? → **Model**
26
+ 3. List of entities with CRUD? → **Collection**
27
+ 4. Fetches external data? → **Service**
28
+ 5. Broadcasts cross-cutting events? → **EventBus**
29
+ 6. Persistent connection? → **Channel**
30
+ 7. Coordinates multiple ViewModels? → **Controller** (rare)
31
+ 8. None of the above → plain utility function
32
+
33
+ ### Which sharing pattern?
34
+ - "Can the parent own one ViewModel and pass props?" → **Pattern A** (default)
35
+ - "Should state survive route changes?" → **Pattern B** (singleton ViewModel via `useSingleton`)
36
+ - "Different concerns, shared data?" → **Pattern C** (separate ViewModels, shared Collection)
37
+
38
+ ## Your Process
39
+
40
+ When asked to plan a feature:
41
+
42
+ 1. **Understand the requirement** — ask clarifying questions if ambiguous
43
+ 2. **Identify class roles** — list all classes needed with their role from the table
44
+ 3. **Design state shapes** — source-of-truth only. No derived values. No loading/error flags.
45
+ 4. **Plan getters** — what derived values will be computed from state
46
+ 5. **Design async flow** — which methods are async, how they use `disposeSignal`
47
+ 6. **Select sharing pattern** — A (props), B (singleton), or C (shared collection)
48
+ 7. **Map component hierarchy** — connected vs presentational
49
+ 8. **Outline error handling** — which layer for each method (automatic, events, classification)
50
+
51
+ ## Architecture Rules
52
+
53
+ - State holds only source-of-truth values. Derived values are `get` accessors. Async status comes from `vm.async.methodName`.
54
+ - One ViewModel per component via `useLocal`. No `useEffect` for data loading.
55
+ - Collections are encapsulated by ViewModels. Components never import Collections.
56
+ - Services are stateless, accept `AbortSignal`, throw `HttpError`.
57
+ - `onInit()` handles data loading and subscription setup.
58
+ - Use `subscribeTo()` for Collection subscriptions (auto-cleanup).
59
+ - ViewModel section order: Private fields → Computed getters → Lifecycle → Actions → Setters.
60
+ - Pass `this.disposeSignal` to every async call.
61
+
62
+ ## Output Format
63
+
64
+ ```
65
+ ## Feature: [name]
66
+
67
+ ### Classes
68
+ - `FooViewModel` — [role]
69
+ - `FooService` — [role]
70
+ - `FoosCollection` — [role]
71
+
72
+ ### State Design
73
+ interface FooState {
74
+ // source-of-truth only
75
+ }
76
+
77
+ ### Getters
78
+ - `filtered` — filters items by search/type
79
+ - `total` — count of all items
80
+
81
+ ### Async Methods
82
+ - `load()` — fetches from service, resets collection
83
+ - `save()` — persists draft, emits 'saved' event
84
+
85
+ ### Sharing Pattern
86
+ Pattern A/B/C — [justification]
87
+
88
+ ### Component Hierarchy
89
+ - FooPage (connected, owns FooViewModel)
90
+ - FooFilters (presentational)
91
+ - FooTable (presentational)
92
+ - FooForm (connected if using Model, otherwise presentational)
93
+
94
+ ### Error Handling
95
+ - load(): automatic (async tracking)
96
+ - save(): explicit (emit event on success, re-throw)
97
+ ```
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: guide
3
+ description: "mvc-kit framework reference — class roles, architecture rules, React hooks, and decision framework. Auto-loaded when mvc-kit imports are detected."
4
+ invocable_by:
5
+ - model
6
+ ---
7
+
8
+ # mvc-kit Framework Reference
9
+
10
+ You are assisting a developer using **mvc-kit**, a zero-dependency TypeScript-first reactive state management library for React applications. Always follow these rules when writing or reviewing code.
11
+
12
+ ## Core Classes
13
+
14
+ | Class | Role | Scope |
15
+ |-------|------|-------|
16
+ | `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
17
+ | `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
18
+ | `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
19
+ | `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
20
+ | `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
21
+ | `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
22
+ | `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
23
+
24
+ ## Architecture Rules
25
+
26
+ 1. **State = source of truth only.** No derived values, no loading/error flags. Derived values are `get` accessors. Async status comes from `vm.async.methodName`.
27
+
28
+ 2. **One ViewModel per component** via `useLocal`. No `useEffect` for data loading — use `onInit()`. No `useState`/`useMemo`/`useCallback` — the ViewModel is the hook.
29
+
30
+ 3. **Components are declarative.** Read `state.x` for raw values, `vm.x` for computed, `vm.async.x` for loading/error. Call `vm.method()` for actions.
31
+
32
+ 4. **Collections are encapsulated.** ViewModels subscribe via `subscribeTo()` in `onInit()` and mirror data into state. Components never import Collections.
33
+
34
+ 5. **Services are stateless.** Accept `AbortSignal`, throw `HttpError`, no knowledge of ViewModels or Collections.
35
+
36
+ 6. **Lifecycle**: `construct → init() → use → dispose()`. React hooks auto-call `init()`. Use `onInit()` for data loading.
37
+
38
+ 7. **Async tracking is automatic.** After `init()`, all async methods are tracked. `vm.async.methodName` returns `{ loading, error, errorCode }`. AbortErrors are silently swallowed. Other errors are captured AND re-thrown.
39
+
40
+ 8. **Pass `this.disposeSignal`** to every async call for automatic cancellation on unmount.
41
+
42
+ ## ViewModel Section Order
43
+
44
+ ```
45
+ Private fields → Computed getters → Lifecycle → Actions → Setters
46
+ ```
47
+
48
+ ## React Hooks
49
+
50
+ | Hook | Usage |
51
+ |------|-------|
52
+ | `useLocal(Class, ...args)` | Component-scoped instance, auto-init/dispose |
53
+ | `useSingleton(Class, ...args)` | Singleton instance, shared state |
54
+ | `useInstance(subscribable)` | Subscribe to existing instance |
55
+ | `useModel(factory)` | Model with validation/dirty state |
56
+ | `useField(model, key)` | Single field subscription |
57
+ | `useEvent(source, event, handler)` | Subscribe to EventBus or ViewModel event |
58
+ | `useEmit(bus)` | Stable emit function |
59
+ | `useResolve(Class, ...args)` | Resolve from Provider or singleton |
60
+ | `useTeardown(...Classes)` | Teardown singletons on unmount |
61
+
62
+ Import core from `mvc-kit`, hooks from `mvc-kit/react`.
63
+
64
+ ## Decision Framework
65
+
66
+ **Which class?**
67
+ - Holds UI state for a component → **ViewModel**
68
+ - Single entity with validation → **Model**
69
+ - List of entities with CRUD → **Collection**
70
+ - Fetches external data → **Service**
71
+ - Broadcasts cross-cutting events → **EventBus**
72
+ - Persistent connection → **Channel**
73
+ - Coordinates multiple ViewModels → **Controller** (rare)
74
+ - None of the above → plain utility function
75
+
76
+ **Which sharing pattern?**
77
+ 1. Parent ViewModel passes props → **Pattern A** (default)
78
+ 2. State survives route changes → **Pattern B** (singleton ViewModel via `useSingleton`)
79
+ 3. Different concerns, shared data → **Pattern C** (separate ViewModels, shared Collection)
80
+
81
+ ## Supporting Files
82
+
83
+ For complete API details, see `api-reference.md` in this skill directory.
84
+ For prescribed patterns with code examples, see `patterns.md`.
85
+ For anti-pattern rejection list with fixes, see `anti-patterns.md`.