@dedesfr/prompter 0.7.8 ā 0.8.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/AGENTS.md +18 -0
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +17 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +164 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +21 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +3 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/configurators/slash/antigravity.d.ts +1 -0
- package/dist/core/configurators/slash/antigravity.d.ts.map +1 -1
- package/dist/core/configurators/slash/antigravity.js +3 -0
- package/dist/core/configurators/slash/antigravity.js.map +1 -1
- package/dist/core/configurators/slash/base.d.ts +18 -0
- package/dist/core/configurators/slash/base.d.ts.map +1 -1
- package/dist/core/configurators/slash/base.js +65 -0
- package/dist/core/configurators/slash/base.js.map +1 -1
- package/dist/core/configurators/slash/claude.d.ts +1 -0
- package/dist/core/configurators/slash/claude.d.ts.map +1 -1
- package/dist/core/configurators/slash/claude.js +3 -0
- package/dist/core/configurators/slash/claude.js.map +1 -1
- package/dist/core/configurators/slash/codex.d.ts +1 -0
- package/dist/core/configurators/slash/codex.d.ts.map +1 -1
- package/dist/core/configurators/slash/codex.js +3 -0
- package/dist/core/configurators/slash/codex.js.map +1 -1
- package/dist/core/configurators/slash/droid.d.ts +10 -0
- package/dist/core/configurators/slash/droid.d.ts.map +1 -0
- package/dist/core/configurators/slash/droid.js +39 -0
- package/dist/core/configurators/slash/droid.js.map +1 -0
- package/dist/core/configurators/slash/forge.d.ts +10 -0
- package/dist/core/configurators/slash/forge.d.ts.map +1 -0
- package/dist/core/configurators/slash/forge.js +39 -0
- package/dist/core/configurators/slash/forge.js.map +1 -0
- package/dist/core/configurators/slash/github-copilot.d.ts +1 -0
- package/dist/core/configurators/slash/github-copilot.d.ts.map +1 -1
- package/dist/core/configurators/slash/github-copilot.js +3 -0
- package/dist/core/configurators/slash/github-copilot.js.map +1 -1
- package/dist/core/configurators/slash/index.d.ts +2 -0
- package/dist/core/configurators/slash/index.d.ts.map +1 -1
- package/dist/core/configurators/slash/index.js +2 -0
- package/dist/core/configurators/slash/index.js.map +1 -1
- package/dist/core/configurators/slash/kilocode.d.ts +1 -0
- package/dist/core/configurators/slash/kilocode.d.ts.map +1 -1
- package/dist/core/configurators/slash/kilocode.js +3 -0
- package/dist/core/configurators/slash/kilocode.js.map +1 -1
- package/dist/core/configurators/slash/opencode.d.ts +1 -0
- package/dist/core/configurators/slash/opencode.d.ts.map +1 -1
- package/dist/core/configurators/slash/opencode.js +3 -0
- package/dist/core/configurators/slash/opencode.js.map +1 -1
- package/dist/core/configurators/slash/registry.d.ts.map +1 -1
- package/dist/core/configurators/slash/registry.js +6 -0
- package/dist/core/configurators/slash/registry.js.map +1 -1
- package/dist/core/skill-discovery.d.ts +12 -0
- package/dist/core/skill-discovery.d.ts.map +1 -0
- package/dist/core/skill-discovery.js +58 -0
- package/dist/core/skill-discovery.js.map +1 -0
- package/package.json +1 -1
- package/skills/design-system-generator/SKILL.md +324 -0
- package/skills/design-system-generator/assets/design-system-template.md +348 -0
- package/skills/design-system-generator/references/extraction-patterns.md +321 -0
- package/skills/laravel-code-review/SKILL.md +383 -0
- package/skills/laravel-code-review/assets/report-template-agent.md +195 -0
- package/skills/laravel-code-review/assets/report-template-compact.md +79 -0
- package/skills/laravel-code-review/assets/report-template-full.md +253 -0
- package/skills/laravel-code-review/assets/report-template-human.md +159 -0
- package/skills/laravel-code-review/references/laravel-patterns.md +571 -0
- package/skills/laravel-code-review/references/php84-features.md +442 -0
- package/src/cli/index.ts +2 -1
- package/src/commands/init.ts +182 -2
- package/src/commands/update.ts +22 -0
- package/src/core/config.ts +3 -1
- package/src/core/configurators/slash/antigravity.ts +4 -0
- package/src/core/configurators/slash/base.ts +95 -0
- package/src/core/configurators/slash/claude.ts +4 -0
- package/src/core/configurators/slash/codex.ts +4 -0
- package/src/core/configurators/slash/droid.ts +44 -0
- package/src/core/configurators/slash/forge.ts +44 -0
- package/src/core/configurators/slash/github-copilot.ts +4 -0
- package/src/core/configurators/slash/index.ts +2 -0
- package/src/core/configurators/slash/kilocode.ts +4 -0
- package/src/core/configurators/slash/opencode.ts +4 -0
- package/src/core/configurators/slash/registry.ts +6 -0
- package/src/core/skill-discovery.ts +68 -0
- package/.claude/settings.local.json +0 -7
- package/.github/prompts/prd-agent-generator.prompt.md +0 -133
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# PHP 8.4 Features Reference
|
|
2
|
+
|
|
3
|
+
Quick reference for PHP 8.4 features to use and deprecations to avoid.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## New Features to Adopt
|
|
8
|
+
|
|
9
|
+
### Property Hooks (PHP 8.4)
|
|
10
|
+
|
|
11
|
+
```php
|
|
12
|
+
// ā
New: Property hooks
|
|
13
|
+
class User
|
|
14
|
+
{
|
|
15
|
+
public string $fullName {
|
|
16
|
+
get => $this->firstName . ' ' . $this->lastName;
|
|
17
|
+
set => [$this->firstName, $this->lastName] = explode(' ', $value, 2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public string $email {
|
|
21
|
+
set => strtolower($value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Asymmetric Visibility (PHP 8.4)
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
// ā
New: Different visibility for get/set
|
|
30
|
+
class User
|
|
31
|
+
{
|
|
32
|
+
public private(set) string $id;
|
|
33
|
+
public protected(set) string $name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Can read $user->id publicly
|
|
37
|
+
// Can only set $user->id privately
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### new Without Parentheses (PHP 8.4)
|
|
41
|
+
|
|
42
|
+
```php
|
|
43
|
+
// ā Old: Required parentheses
|
|
44
|
+
$user = (new User())->setName('John');
|
|
45
|
+
|
|
46
|
+
// ā
New: No parentheses needed
|
|
47
|
+
$user = new User()->setName('John');
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Array Find Functions (PHP 8.4)
|
|
51
|
+
|
|
52
|
+
```php
|
|
53
|
+
// ā
New: array_find()
|
|
54
|
+
$users = [
|
|
55
|
+
['name' => 'John', 'active' => false],
|
|
56
|
+
['name' => 'Jane', 'active' => true],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
$activeUser = array_find($users, fn($u) => $u['active']);
|
|
60
|
+
// Returns: ['name' => 'Jane', 'active' => true]
|
|
61
|
+
|
|
62
|
+
// ā
New: array_find_key()
|
|
63
|
+
$key = array_find_key($users, fn($u) => $u['active']);
|
|
64
|
+
// Returns: 1
|
|
65
|
+
|
|
66
|
+
// ā
New: array_any()
|
|
67
|
+
$hasActive = array_any($users, fn($u) => $u['active']);
|
|
68
|
+
// Returns: true
|
|
69
|
+
|
|
70
|
+
// ā
New: array_all()
|
|
71
|
+
$allActive = array_all($users, fn($u) => $u['active']);
|
|
72
|
+
// Returns: false
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### HTML5 DOM Support (PHP 8.4)
|
|
76
|
+
|
|
77
|
+
```php
|
|
78
|
+
// ā
New: Native HTML5 parsing
|
|
79
|
+
$dom = Dom\HTMLDocument::createFromString($html);
|
|
80
|
+
$dom = Dom\HTMLDocument::createFromFile('page.html');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## PHP 8.3 Features (Ensure Usage)
|
|
86
|
+
|
|
87
|
+
### #[Override] Attribute
|
|
88
|
+
|
|
89
|
+
```php
|
|
90
|
+
// ā
Use Override for overridden methods
|
|
91
|
+
class CustomHandler extends BaseHandler
|
|
92
|
+
{
|
|
93
|
+
#[\Override]
|
|
94
|
+
public function handle(): void
|
|
95
|
+
{
|
|
96
|
+
// Implementation
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Typed Class Constants
|
|
102
|
+
|
|
103
|
+
```php
|
|
104
|
+
// ā
Type constants
|
|
105
|
+
class Status
|
|
106
|
+
{
|
|
107
|
+
public const string PENDING = 'pending';
|
|
108
|
+
public const string ACTIVE = 'active';
|
|
109
|
+
public const int MAX_RETRY = 3;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### json_validate()
|
|
114
|
+
|
|
115
|
+
```php
|
|
116
|
+
// ā Old: Decode to validate
|
|
117
|
+
$valid = json_decode($json) !== null;
|
|
118
|
+
|
|
119
|
+
// ā
New: Direct validation
|
|
120
|
+
$valid = json_validate($json);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Randomizer Additions
|
|
124
|
+
|
|
125
|
+
```php
|
|
126
|
+
// ā
New random methods
|
|
127
|
+
$randomizer = new Random\Randomizer();
|
|
128
|
+
$randomizer->getBytesFromString('abc', 10);
|
|
129
|
+
$randomizer->nextFloat();
|
|
130
|
+
$randomizer->getFloat(0.0, 1.0);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## PHP 8.2 Features (Ensure Usage)
|
|
136
|
+
|
|
137
|
+
### Readonly Classes
|
|
138
|
+
|
|
139
|
+
```php
|
|
140
|
+
// ā
Entire class readonly
|
|
141
|
+
readonly class UserDTO
|
|
142
|
+
{
|
|
143
|
+
public function __construct(
|
|
144
|
+
public string $name,
|
|
145
|
+
public string $email,
|
|
146
|
+
) {}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Null/False/True Types
|
|
151
|
+
|
|
152
|
+
```php
|
|
153
|
+
// ā
Standalone null type
|
|
154
|
+
function alwaysNull(): null
|
|
155
|
+
{
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ā
Standalone false type
|
|
160
|
+
function failed(): false
|
|
161
|
+
{
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Disjunctive Normal Form Types
|
|
167
|
+
|
|
168
|
+
```php
|
|
169
|
+
// ā
Complex union types
|
|
170
|
+
function process((A&B)|null $value): void {}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## PHP 8.1 Features (Ensure Usage)
|
|
176
|
+
|
|
177
|
+
### Enums
|
|
178
|
+
|
|
179
|
+
```php
|
|
180
|
+
// ā
Backed enums
|
|
181
|
+
enum OrderStatus: string
|
|
182
|
+
{
|
|
183
|
+
case Pending = 'pending';
|
|
184
|
+
case Processing = 'processing';
|
|
185
|
+
case Completed = 'completed';
|
|
186
|
+
case Cancelled = 'cancelled';
|
|
187
|
+
|
|
188
|
+
public function label(): string
|
|
189
|
+
{
|
|
190
|
+
return match($this) {
|
|
191
|
+
self::Pending => 'Order Pending',
|
|
192
|
+
self::Processing => 'In Progress',
|
|
193
|
+
self::Completed => 'Completed',
|
|
194
|
+
self::Cancelled => 'Cancelled',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Readonly Properties
|
|
201
|
+
|
|
202
|
+
```php
|
|
203
|
+
// ā
Immutable properties
|
|
204
|
+
class User
|
|
205
|
+
{
|
|
206
|
+
public function __construct(
|
|
207
|
+
public readonly int $id,
|
|
208
|
+
public readonly string $email,
|
|
209
|
+
) {}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### First-Class Callables
|
|
214
|
+
|
|
215
|
+
```php
|
|
216
|
+
// ā Old: Closure::fromCallable
|
|
217
|
+
$fn = Closure::fromCallable([$this, 'method']);
|
|
218
|
+
|
|
219
|
+
// ā
New: First-class callable syntax
|
|
220
|
+
$fn = $this->method(...);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Intersection Types
|
|
224
|
+
|
|
225
|
+
```php
|
|
226
|
+
// ā
Require multiple interfaces
|
|
227
|
+
function process(Countable&Iterator $items): void {}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Fibers
|
|
231
|
+
|
|
232
|
+
```php
|
|
233
|
+
// ā
Fiber for async
|
|
234
|
+
$fiber = new Fiber(function() {
|
|
235
|
+
$value = Fiber::suspend('paused');
|
|
236
|
+
return $value;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
$result = $fiber->start();
|
|
240
|
+
$final = $fiber->resume('resumed');
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## PHP 8.0 Features (Ensure Usage)
|
|
246
|
+
|
|
247
|
+
### Constructor Property Promotion
|
|
248
|
+
|
|
249
|
+
```php
|
|
250
|
+
// ā Old
|
|
251
|
+
class User
|
|
252
|
+
{
|
|
253
|
+
private string $name;
|
|
254
|
+
|
|
255
|
+
public function __construct(string $name)
|
|
256
|
+
{
|
|
257
|
+
$this->name = $name;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ā
New: Property promotion
|
|
262
|
+
class User
|
|
263
|
+
{
|
|
264
|
+
public function __construct(
|
|
265
|
+
private readonly string $name,
|
|
266
|
+
) {}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Named Arguments
|
|
271
|
+
|
|
272
|
+
```php
|
|
273
|
+
// ā
Named arguments for clarity
|
|
274
|
+
$user = new User(
|
|
275
|
+
name: 'John',
|
|
276
|
+
email: 'john@example.com',
|
|
277
|
+
isAdmin: false,
|
|
278
|
+
);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Match Expression
|
|
282
|
+
|
|
283
|
+
```php
|
|
284
|
+
// ā Old: Switch
|
|
285
|
+
switch ($status) {
|
|
286
|
+
case 'pending':
|
|
287
|
+
$color = 'yellow';
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
$color = 'gray';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ā
New: Match
|
|
294
|
+
$color = match($status) {
|
|
295
|
+
'pending' => 'yellow',
|
|
296
|
+
'approved' => 'green',
|
|
297
|
+
default => 'gray',
|
|
298
|
+
};
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Nullsafe Operator
|
|
302
|
+
|
|
303
|
+
```php
|
|
304
|
+
// ā Old: Null checks
|
|
305
|
+
$country = null;
|
|
306
|
+
if ($user !== null && $user->address !== null) {
|
|
307
|
+
$country = $user->address->country;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ā
New: Nullsafe
|
|
311
|
+
$country = $user?->address?->country;
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Union Types
|
|
315
|
+
|
|
316
|
+
```php
|
|
317
|
+
// ā
Union types
|
|
318
|
+
function parse(string|int $value): string|false {}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Attributes
|
|
322
|
+
|
|
323
|
+
```php
|
|
324
|
+
// ā
Native attributes
|
|
325
|
+
#[Route('/users', methods: ['GET'])]
|
|
326
|
+
public function index(): Response {}
|
|
327
|
+
|
|
328
|
+
#[Deprecated('Use newMethod() instead')]
|
|
329
|
+
public function oldMethod(): void {}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Deprecations to Fix
|
|
335
|
+
|
|
336
|
+
### PHP 8.4 Deprecations
|
|
337
|
+
|
|
338
|
+
```php
|
|
339
|
+
// ā Deprecated: Implicit nullable
|
|
340
|
+
function foo(string $value = null) {}
|
|
341
|
+
|
|
342
|
+
// ā
Fixed: Explicit nullable
|
|
343
|
+
function foo(?string $value = null) {}
|
|
344
|
+
|
|
345
|
+
// ā Deprecated: session_register()
|
|
346
|
+
session_register('var');
|
|
347
|
+
|
|
348
|
+
// ā
Fixed: Use $_SESSION
|
|
349
|
+
$_SESSION['var'] = $value;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### PHP 8.2 Deprecations
|
|
353
|
+
|
|
354
|
+
```php
|
|
355
|
+
// ā Deprecated: Dynamic properties
|
|
356
|
+
class User
|
|
357
|
+
{
|
|
358
|
+
public string $name;
|
|
359
|
+
}
|
|
360
|
+
$user = new User();
|
|
361
|
+
$user->undefined = 'value'; // Deprecated!
|
|
362
|
+
|
|
363
|
+
// ā
Fixed: Define property or use #[AllowDynamicProperties]
|
|
364
|
+
#[\AllowDynamicProperties]
|
|
365
|
+
class User
|
|
366
|
+
{
|
|
367
|
+
public string $name;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### PHP 8.1 Deprecations
|
|
372
|
+
|
|
373
|
+
```php
|
|
374
|
+
// ā Deprecated: ${var} in strings
|
|
375
|
+
$name = 'John';
|
|
376
|
+
echo "Hello ${name}"; // Deprecated!
|
|
377
|
+
|
|
378
|
+
// ā
Fixed: Use {$var}
|
|
379
|
+
echo "Hello {$name}";
|
|
380
|
+
|
|
381
|
+
// ā Deprecated: Passing null to non-nullable
|
|
382
|
+
strlen(null); // Deprecated!
|
|
383
|
+
|
|
384
|
+
// ā
Fixed: Check for null
|
|
385
|
+
strlen($value ?? '');
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Type Safety Improvements
|
|
391
|
+
|
|
392
|
+
### Strict Return Types
|
|
393
|
+
|
|
394
|
+
```php
|
|
395
|
+
// ā Poor: No return type
|
|
396
|
+
public function getUsers()
|
|
397
|
+
{
|
|
398
|
+
return User::all();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ā
Good: Explicit return type
|
|
402
|
+
public function getUsers(): Collection
|
|
403
|
+
{
|
|
404
|
+
return User::all();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ā
Good: Never return type
|
|
408
|
+
public function fail(): never
|
|
409
|
+
{
|
|
410
|
+
throw new Exception('Failed');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ā
Good: Void return type
|
|
414
|
+
public function log(string $message): void
|
|
415
|
+
{
|
|
416
|
+
Log::info($message);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Strict Parameter Types
|
|
421
|
+
|
|
422
|
+
```php
|
|
423
|
+
// ā Poor: No parameter types
|
|
424
|
+
public function process($data, $options)
|
|
425
|
+
|
|
426
|
+
// ā
Good: Typed parameters
|
|
427
|
+
public function process(array $data, ProcessOptions $options): Result
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Detection Patterns
|
|
433
|
+
|
|
434
|
+
| Issue | Detection Pattern |
|
|
435
|
+
| ------------------ | ----------------------------------------------------------- |
|
|
436
|
+
| Missing readonly | Class properties without `readonly` that are never modified |
|
|
437
|
+
| Missing match | Switch statements that return values |
|
|
438
|
+
| Missing nullsafe | Nested null checks with `&&` |
|
|
439
|
+
| Missing named args | Constructor calls with 4+ positional parameters |
|
|
440
|
+
| Implicit nullable | Parameters with `= null` but no `?` type |
|
|
441
|
+
| Dynamic properties | Property access on undefined properties |
|
|
442
|
+
| Old string syntax | `${var}` instead of `{$var}` |
|
package/src/cli/index.ts
CHANGED
|
@@ -18,13 +18,14 @@ const program = new Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name('prompter')
|
|
20
20
|
.description('Enhance prompts directly in your AI coding workflow')
|
|
21
|
-
.version('0.
|
|
21
|
+
.version('0.8.0');
|
|
22
22
|
|
|
23
23
|
program
|
|
24
24
|
.command('init')
|
|
25
25
|
.description('Initialize Prompter in your project')
|
|
26
26
|
.option('--tools <tools...>', 'Specify AI tools to configure (antigravity, claude, codex, github-copilot, opencode, kilocode)')
|
|
27
27
|
.option('--prompts <prompts...>', 'Specify prompts to install (ai-humanizer, api-contract-generator, apply, archive, design-system, document-explainer, epic-single, epic-generator, erd-generator, fsd-generator, prd-agent-generator, prd-generator, product-brief, proposal, qa-test-scenario, skill-creator, story-single, story-generator, tdd-generator, tdd-lite-generator, wireframe-generator)')
|
|
28
|
+
.option('--skills <skills...>', 'Specify skills to install by name (e.g. laravel-code-review design-system-generator)')
|
|
28
29
|
.option('--no-interactive', 'Run without interactive prompts')
|
|
29
30
|
.action(async (options) => {
|
|
30
31
|
const initCommand = new InitCommand();
|
package/src/commands/init.ts
CHANGED
|
@@ -7,10 +7,12 @@ import { projectTemplate, agentsTemplate, claudeTemplate } from '../core/templat
|
|
|
7
7
|
import { PROMPT_TEMPLATES } from '../core/prompt-templates.js';
|
|
8
8
|
import { registry } from '../core/configurators/slash/index.js';
|
|
9
9
|
import { SlashCommandId } from '../core/templates/index.js';
|
|
10
|
+
import { discoverSkills, SkillMetadata } from '../core/skill-discovery.js';
|
|
10
11
|
|
|
11
12
|
interface InitOptions {
|
|
12
13
|
tools?: string[];
|
|
13
14
|
prompts?: string[];
|
|
15
|
+
skills?: string[];
|
|
14
16
|
noInteractive?: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -145,7 +147,7 @@ export class InitCommand {
|
|
|
145
147
|
// Detect currently installed prompts (use path.join to get prompter path)
|
|
146
148
|
const prompterPathForDetection = path.join(projectPath, PROMPTER_DIR);
|
|
147
149
|
const currentPrompts = await this.detectInstalledPrompts(prompterPathForDetection);
|
|
148
|
-
|
|
150
|
+
|
|
149
151
|
selectedPrompts = await checkbox({
|
|
150
152
|
message: 'Select prompt templates to install:',
|
|
151
153
|
choices: this.getCategorizedPromptChoices(currentPrompts),
|
|
@@ -158,6 +160,34 @@ export class InitCommand {
|
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// Select skills
|
|
164
|
+
const availableSkills = await discoverSkills(path.join(projectPath, 'skills'));
|
|
165
|
+
let selectedSkills: SkillMetadata[] = [];
|
|
166
|
+
|
|
167
|
+
if (options.skills && options.skills.length > 0) {
|
|
168
|
+
const requestedNames = options.skills.flatMap(s => s.split(',').map(s => s.trim()));
|
|
169
|
+
selectedSkills = availableSkills.filter(s => requestedNames.includes(s.name));
|
|
170
|
+
} else if (!options.noInteractive && availableSkills.length > 0) {
|
|
171
|
+
try {
|
|
172
|
+
const prompterPathForDetection = path.join(projectPath, PROMPTER_DIR);
|
|
173
|
+
const currentSkillNames = await this.detectInstalledSkills(prompterPathForDetection);
|
|
174
|
+
|
|
175
|
+
const selectedSkillNames = await checkbox({
|
|
176
|
+
message: 'Select skills to install:',
|
|
177
|
+
choices: availableSkills.map(skill => ({
|
|
178
|
+
name: ` ${skill.name}`,
|
|
179
|
+
value: skill.name,
|
|
180
|
+
checked: currentSkillNames.includes(skill.name)
|
|
181
|
+
}))
|
|
182
|
+
});
|
|
183
|
+
selectedSkills = availableSkills.filter(s => selectedSkillNames.includes(s.name));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// User cancelled
|
|
186
|
+
console.log(chalk.yellow(isReInitialization ? '\nRe-configuration cancelled.' : '\nInitialization cancelled.'));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
161
191
|
// Create or ensure prompter directory
|
|
162
192
|
const prompterPath = await PrompterConfig.ensurePrompterDir(projectPath);
|
|
163
193
|
if (!isReInitialization) {
|
|
@@ -349,10 +379,13 @@ export class InitCommand {
|
|
|
349
379
|
}
|
|
350
380
|
}
|
|
351
381
|
|
|
382
|
+
// --- Skills setup ---
|
|
383
|
+
const skillChanges = await this.setupSkills(projectPath, prompterPath, selectedTools, selectedSkills);
|
|
384
|
+
|
|
352
385
|
// Success message
|
|
353
386
|
if (isReInitialization) {
|
|
354
387
|
console.log(chalk.green('\nā
Prompter tools updated successfully!\n'));
|
|
355
|
-
if (toolsToAdd.length > 0 || toolsToRemove.length > 0 || promptsToAdd.length > 0 || promptsToRemove.length > 0) {
|
|
388
|
+
if (toolsToAdd.length > 0 || toolsToRemove.length > 0 || promptsToAdd.length > 0 || promptsToRemove.length > 0 || skillChanges.added.length > 0 || skillChanges.removed.length > 0) {
|
|
356
389
|
console.log(chalk.blue('Summary:'));
|
|
357
390
|
if (toolsToAdd.length > 0) {
|
|
358
391
|
console.log(chalk.green(' Tools Added: ') + toolsToAdd.map(t => {
|
|
@@ -378,6 +411,12 @@ export class InitCommand {
|
|
|
378
411
|
return prompt ? prompt.name : p;
|
|
379
412
|
}).join(', '));
|
|
380
413
|
}
|
|
414
|
+
if (skillChanges.added.length > 0) {
|
|
415
|
+
console.log(chalk.green(' Skills Added: ') + skillChanges.added.join(', '));
|
|
416
|
+
}
|
|
417
|
+
if (skillChanges.removed.length > 0) {
|
|
418
|
+
console.log(chalk.yellow(' Skills Removed: ') + skillChanges.removed.join(', '));
|
|
419
|
+
}
|
|
381
420
|
console.log();
|
|
382
421
|
} else {
|
|
383
422
|
console.log(chalk.gray(' No changes made.\n'));
|
|
@@ -387,6 +426,9 @@ export class InitCommand {
|
|
|
387
426
|
if (promptsToAdd.length > 0) {
|
|
388
427
|
console.log(chalk.gray(`Installed ${promptsToAdd.length} prompt template(s).\n`));
|
|
389
428
|
}
|
|
429
|
+
if (skillChanges.added.length > 0) {
|
|
430
|
+
console.log(chalk.gray(`Installed ${skillChanges.added.length} skill(s).\n`));
|
|
431
|
+
}
|
|
390
432
|
console.log(chalk.gray('Run `prompter guide` for next steps.\n'));
|
|
391
433
|
}
|
|
392
434
|
}
|
|
@@ -604,6 +646,144 @@ Use \`@/prompter/CLAUDE.md\` to learn:
|
|
|
604
646
|
}
|
|
605
647
|
}
|
|
606
648
|
|
|
649
|
+
private async setupSkills(
|
|
650
|
+
projectPath: string,
|
|
651
|
+
prompterPath: string,
|
|
652
|
+
selectedTools: string[],
|
|
653
|
+
selectedSkills: SkillMetadata[]
|
|
654
|
+
): Promise<{ added: string[]; removed: string[] }> {
|
|
655
|
+
const result = { added: [] as string[], removed: [] as string[] };
|
|
656
|
+
|
|
657
|
+
const installedSkillNames = await this.detectInstalledSkills(prompterPath);
|
|
658
|
+
const selectedSkillNames = selectedSkills.map(s => s.name);
|
|
659
|
+
|
|
660
|
+
const skillsToAdd = selectedSkills.filter(s => !installedSkillNames.includes(s.name));
|
|
661
|
+
const skillsToRemove = installedSkillNames.filter(n => !selectedSkillNames.includes(n));
|
|
662
|
+
const skillsToKeep = selectedSkills.filter(s => installedSkillNames.includes(s.name));
|
|
663
|
+
|
|
664
|
+
const skillsTargetDir = path.join(prompterPath, 'skills');
|
|
665
|
+
|
|
666
|
+
// Remove deselected skills
|
|
667
|
+
if (skillsToRemove.length > 0) {
|
|
668
|
+
console.log(chalk.blue('\nšļø Removing skills...\n'));
|
|
669
|
+
|
|
670
|
+
for (const skillName of skillsToRemove) {
|
|
671
|
+
const staleDir = path.join(skillsTargetDir, skillName);
|
|
672
|
+
try {
|
|
673
|
+
await fs.rm(staleDir, { recursive: true, force: true });
|
|
674
|
+
console.log(chalk.yellow('ā') + ` Removed skill ${chalk.cyan(skillName)}`);
|
|
675
|
+
result.removed.push(skillName);
|
|
676
|
+
} catch {
|
|
677
|
+
// ignore
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const toolId of selectedTools) {
|
|
681
|
+
const configurator = registry.get(toolId);
|
|
682
|
+
if (!configurator) continue;
|
|
683
|
+
const removed = await configurator.removeSkillFiles(projectPath, [skillName]);
|
|
684
|
+
for (const file of removed) {
|
|
685
|
+
console.log(chalk.yellow('ā') + ` Removed ${chalk.cyan(file)}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Install new skills
|
|
692
|
+
if (skillsToAdd.length > 0) {
|
|
693
|
+
console.log(chalk.blue('\nš§© Installing skills...\n'));
|
|
694
|
+
await fs.mkdir(skillsTargetDir, { recursive: true });
|
|
695
|
+
|
|
696
|
+
for (const skill of skillsToAdd) {
|
|
697
|
+
const targetDir = path.join(skillsTargetDir, skill.name);
|
|
698
|
+
try {
|
|
699
|
+
await this.copyDirectory(skill.sourcePath, targetDir);
|
|
700
|
+
console.log(chalk.green('ā') + ` Installed skill ${chalk.cyan(skill.name)}`);
|
|
701
|
+
result.added.push(skill.name);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.log(chalk.red('ā') + ` Failed to install skill ${skill.name}: ${error}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (selectedTools.length > 0) {
|
|
708
|
+
console.log(chalk.blue('\nš Creating skill workflow files...\n'));
|
|
709
|
+
for (const toolId of selectedTools) {
|
|
710
|
+
const configurator = registry.get(toolId);
|
|
711
|
+
if (!configurator) continue;
|
|
712
|
+
try {
|
|
713
|
+
const files = await configurator.generateSkills(projectPath, skillsToAdd);
|
|
714
|
+
for (const file of files) {
|
|
715
|
+
console.log(chalk.green('ā') + ` Created ${chalk.cyan(file)}`);
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.log(chalk.red('ā') + ` Failed to create skill files for ${toolId}: ${error}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Update kept skills
|
|
725
|
+
if (skillsToKeep.length > 0) {
|
|
726
|
+
await fs.mkdir(skillsTargetDir, { recursive: true });
|
|
727
|
+
|
|
728
|
+
for (const skill of skillsToKeep) {
|
|
729
|
+
const targetDir = path.join(skillsTargetDir, skill.name);
|
|
730
|
+
try {
|
|
731
|
+
await this.copyDirectory(skill.sourcePath, targetDir);
|
|
732
|
+
} catch {
|
|
733
|
+
// ignore update errors
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
for (const toolId of selectedTools) {
|
|
738
|
+
const configurator = registry.get(toolId);
|
|
739
|
+
if (!configurator) continue;
|
|
740
|
+
try {
|
|
741
|
+
await configurator.generateSkills(projectPath, skillsToKeep);
|
|
742
|
+
} catch {
|
|
743
|
+
// ignore
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private async detectInstalledSkills(prompterPath: string): Promise<string[]> {
|
|
752
|
+
const skillsDir = path.join(prompterPath, 'skills');
|
|
753
|
+
const names: string[] = [];
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
757
|
+
for (const entry of entries) {
|
|
758
|
+
if (!entry.isDirectory()) continue;
|
|
759
|
+
const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
760
|
+
if (await this.fileExists(skillMdPath)) {
|
|
761
|
+
names.push(entry.name);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} catch {
|
|
765
|
+
// skills directory doesn't exist yet
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return names;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private async copyDirectory(src: string, dest: string): Promise<void> {
|
|
772
|
+
await fs.mkdir(dest, { recursive: true });
|
|
773
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
774
|
+
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
const srcPath = path.join(src, entry.name);
|
|
777
|
+
const destPath = path.join(dest, entry.name);
|
|
778
|
+
|
|
779
|
+
if (entry.isDirectory()) {
|
|
780
|
+
await this.copyDirectory(srcPath, destPath);
|
|
781
|
+
} else {
|
|
782
|
+
await fs.copyFile(srcPath, destPath);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
607
787
|
private async ensureRootAgentsFile(projectPath: string): Promise<void> {
|
|
608
788
|
const rootAgentsPath = path.join(projectPath, 'AGENTS.md');
|
|
609
789
|
const instructionsBlock = `<!-- PROMPTER:START -->
|