prjct-cli 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +67 -6
  2. package/CLAUDE.md +442 -36
  3. package/README.md +47 -54
  4. package/bin/prjct +174 -240
  5. package/core/agentic/command-executor.js +113 -0
  6. package/core/agentic/context-builder.js +85 -0
  7. package/core/agentic/prompt-builder.js +86 -0
  8. package/core/agentic/template-loader.js +104 -0
  9. package/core/agentic/tool-registry.js +117 -0
  10. package/core/command-registry.js +109 -65
  11. package/core/commands.js +2213 -2173
  12. package/core/domain/agent-generator.js +118 -0
  13. package/core/domain/analyzer.js +211 -0
  14. package/core/domain/architect-session.js +300 -0
  15. package/core/{agents → infrastructure/agents}/claude-agent.js +16 -13
  16. package/core/{author-detector.js → infrastructure/author-detector.js} +3 -1
  17. package/core/{capability-installer.js → infrastructure/capability-installer.js} +3 -6
  18. package/core/{command-installer.js → infrastructure/command-installer.js} +5 -3
  19. package/core/{config-manager.js → infrastructure/config-manager.js} +4 -4
  20. package/core/{editors-config.js → infrastructure/editors-config.js} +2 -10
  21. package/core/{migrator.js → infrastructure/migrator.js} +34 -19
  22. package/core/{path-manager.js → infrastructure/path-manager.js} +20 -44
  23. package/core/{session-manager.js → infrastructure/session-manager.js} +45 -105
  24. package/core/{update-checker.js → infrastructure/update-checker.js} +67 -67
  25. package/core/{animations-simple.js → utils/animations.js} +3 -23
  26. package/core/utils/date-helper.js +238 -0
  27. package/core/utils/file-helper.js +327 -0
  28. package/core/utils/jsonl-helper.js +206 -0
  29. package/core/{project-capabilities.js → utils/project-capabilities.js} +21 -22
  30. package/core/utils/session-helper.js +277 -0
  31. package/core/{version.js → utils/version.js} +1 -1
  32. package/package.json +4 -12
  33. package/templates/agents/AGENTS.md +101 -27
  34. package/templates/analysis/analyze.md +84 -0
  35. package/templates/commands/analyze.md +9 -2
  36. package/templates/commands/bug.md +79 -0
  37. package/templates/commands/build.md +5 -2
  38. package/templates/commands/cleanup.md +5 -2
  39. package/templates/commands/design.md +5 -2
  40. package/templates/commands/done.md +4 -2
  41. package/templates/commands/feature.md +113 -0
  42. package/templates/commands/fix.md +41 -10
  43. package/templates/commands/git.md +7 -2
  44. package/templates/commands/help.md +2 -2
  45. package/templates/commands/idea.md +14 -5
  46. package/templates/commands/init.md +62 -7
  47. package/templates/commands/next.md +4 -2
  48. package/templates/commands/now.md +4 -2
  49. package/templates/commands/progress.md +27 -5
  50. package/templates/commands/recap.md +39 -10
  51. package/templates/commands/roadmap.md +19 -5
  52. package/templates/commands/ship.md +118 -16
  53. package/templates/commands/status.md +4 -2
  54. package/templates/commands/sync.md +19 -15
  55. package/templates/commands/task.md +4 -2
  56. package/templates/commands/test.md +5 -2
  57. package/templates/commands/workflow.md +4 -2
  58. package/core/agent-generator.js +0 -525
  59. package/core/analyzer.js +0 -600
  60. package/core/animations.js +0 -277
  61. package/core/ascii-graphics.js +0 -433
  62. package/core/git-integration.js +0 -401
  63. package/core/task-schema.js +0 -342
  64. package/core/workflow-engine.js +0 -213
  65. package/core/workflow-prompts.js +0 -192
  66. package/core/workflow-rules.js +0 -147
  67. package/scripts/post-install.js +0 -121
  68. package/scripts/preuninstall.js +0 -94
  69. package/scripts/verify-installation.sh +0 -158
  70. package/templates/agents/be.template.md +0 -27
  71. package/templates/agents/coordinator.template.md +0 -34
  72. package/templates/agents/data.template.md +0 -27
  73. package/templates/agents/devops.template.md +0 -27
  74. package/templates/agents/fe.template.md +0 -27
  75. package/templates/agents/mobile.template.md +0 -27
  76. package/templates/agents/qa.template.md +0 -27
  77. package/templates/agents/scribe.template.md +0 -29
  78. package/templates/agents/security.template.md +0 -27
  79. package/templates/agents/ux.template.md +0 -27
  80. package/templates/commands/context.md +0 -36
  81. package/templates/commands/stuck.md +0 -36
  82. package/templates/examples/natural-language-examples.md +0 -532
  83. /package/core/{agent-detector.js → infrastructure/agent-detector.js} +0 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Prompt Builder
3
+ * Builds prompts for Claude based on templates and context
4
+ * Claude decides what to do - NO if/else logic here
5
+ */
6
+
7
+ class PromptBuilder {
8
+ /**
9
+ * Build execution prompt for Claude
10
+ * @param {Object} template - Template from template-loader
11
+ * @param {Object} context - Context from context-builder
12
+ * @param {Object} state - Current state from context-builder
13
+ * @returns {string} Prompt for Claude
14
+ */
15
+ build(template, context, state) {
16
+ const parts = []
17
+
18
+ // 1. Command instructions from template
19
+ parts.push('# Command Instructions\n')
20
+ parts.push(template.content)
21
+ parts.push('\n')
22
+
23
+ // 2. Allowed tools
24
+ if (template.frontmatter['allowed-tools']) {
25
+ parts.push('## Allowed Tools\n')
26
+ parts.push(`You can use: ${template.frontmatter['allowed-tools'].join(', ')}\n\n`)
27
+ }
28
+
29
+ // 3. Project context
30
+ parts.push('## Project Context\n')
31
+ parts.push(`- Project ID: ${context.projectId}\n`)
32
+ parts.push(`- Timestamp: ${context.timestamp}\n`)
33
+ parts.push('\n')
34
+
35
+ // 4. Current state (only non-null files)
36
+ parts.push('## Current State\n')
37
+ for (const [key, content] of Object.entries(state)) {
38
+ if (content && content.trim()) {
39
+ parts.push(`### ${key}\n`)
40
+ parts.push('```\n')
41
+ parts.push(content)
42
+ parts.push('\n```\n\n')
43
+ }
44
+ }
45
+
46
+ // 5. Command parameters
47
+ if (Object.keys(context.params).length > 0) {
48
+ parts.push('## Parameters\n')
49
+ for (const [key, value] of Object.entries(context.params)) {
50
+ parts.push(`- ${key}: ${value}\n`)
51
+ }
52
+ parts.push('\n')
53
+ }
54
+
55
+ // 6. Final instruction
56
+ parts.push('## Execute\n')
57
+ parts.push('Based on the instructions above, execute the command.\n')
58
+ parts.push('Use ONLY the allowed tools.\n')
59
+ parts.push('Make decisions based on context - do not follow rigid if/else rules.\n')
60
+
61
+ return parts.join('')
62
+ }
63
+
64
+ /**
65
+ * Build analysis prompt
66
+ * Used for tasks that need Claude to analyze before acting
67
+ * @param {string} analysisType - Type of analysis
68
+ * @param {Object} context - Context
69
+ * @returns {string} Analysis prompt
70
+ */
71
+ buildAnalysis(analysisType, context) {
72
+ const parts = []
73
+
74
+ parts.push(`# Analyze: ${analysisType}\n\n`)
75
+ parts.push('Read the project context and provide your analysis.\n')
76
+ parts.push('No predetermined patterns - decide based on what you find.\n\n')
77
+
78
+ parts.push('## Project Context\n')
79
+ parts.push(`- Path: ${context.projectPath}\n`)
80
+ parts.push(`- ID: ${context.projectId}\n\n`)
81
+
82
+ return parts.join('')
83
+ }
84
+ }
85
+
86
+ module.exports = new PromptBuilder()
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Template Loader
3
+ * Loads command templates with frontmatter parsing
4
+ * Templates define what Claude should do - NO if/else logic
5
+ */
6
+
7
+ const fs = require('fs').promises
8
+ const path = require('path')
9
+
10
+ class TemplateLoader {
11
+ constructor() {
12
+ this.templatesDir = path.join(__dirname, '..', '..', 'templates', 'commands')
13
+ this.cache = new Map()
14
+ }
15
+
16
+ /**
17
+ * Load template with frontmatter
18
+ * @param {string} commandName - Command name (e.g., 'now', 'done', 'ship')
19
+ * @returns {Promise<{frontmatter: Object, content: string}>}
20
+ */
21
+ async load(commandName) {
22
+ // Check cache first
23
+ if (this.cache.has(commandName)) {
24
+ return this.cache.get(commandName)
25
+ }
26
+
27
+ const templatePath = path.join(this.templatesDir, `${commandName}.md`)
28
+
29
+ try {
30
+ const rawContent = await fs.readFile(templatePath, 'utf-8')
31
+ const parsed = this.parseFrontmatter(rawContent)
32
+
33
+ // Cache result
34
+ this.cache.set(commandName, parsed)
35
+
36
+ return parsed
37
+ } catch (error) {
38
+ throw new Error(`Template not found: ${commandName}.md`)
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Parse frontmatter from markdown
44
+ * @param {string} content - Raw markdown content
45
+ * @returns {Object} Parsed template with frontmatter and content
46
+ */
47
+ parseFrontmatter(content) {
48
+ const frontmatterRegex = /^---\n([\s\S]+?)\n---\n([\s\S]*)$/
49
+ const match = content.match(frontmatterRegex)
50
+
51
+ if (!match) {
52
+ return {
53
+ frontmatter: {},
54
+ content: content.trim(),
55
+ }
56
+ }
57
+
58
+ const [, frontmatterText, mainContent] = match
59
+ const frontmatter = {}
60
+
61
+ // Parse frontmatter lines
62
+ frontmatterText.split('\n').forEach((line) => {
63
+ const [key, ...valueParts] = line.split(':')
64
+ if (key && valueParts.length > 0) {
65
+ const value = valueParts.join(':').trim()
66
+
67
+ // Parse arrays
68
+ if (value.startsWith('[') && value.endsWith(']')) {
69
+ frontmatter[key.trim()] = value
70
+ .slice(1, -1)
71
+ .split(',')
72
+ .map((v) => v.trim())
73
+ } else {
74
+ // Remove quotes if present
75
+ frontmatter[key.trim()] = value.replace(/^["']|["']$/g, '')
76
+ }
77
+ }
78
+ })
79
+
80
+ return {
81
+ frontmatter,
82
+ content: mainContent.trim(),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get allowed tools for a command
88
+ * @param {string} commandName - Command name
89
+ * @returns {Promise<string[]>}
90
+ */
91
+ async getAllowedTools(commandName) {
92
+ const template = await this.load(commandName)
93
+ return template.frontmatter['allowed-tools'] || []
94
+ }
95
+
96
+ /**
97
+ * Clear cache (useful for testing)
98
+ */
99
+ clearCache() {
100
+ this.cache.clear()
101
+ }
102
+ }
103
+
104
+ module.exports = new TemplateLoader()
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tool Registry
3
+ * Maps allowed-tools from templates to actual functions
4
+ * Simple I/O operations - NO business logic, NO if/else
5
+ */
6
+
7
+ const fs = require('fs').promises
8
+ const path = require('path')
9
+ const { promisify } = require('util')
10
+ const { exec: execCallback } = require('child_process')
11
+ const exec = promisify(execCallback)
12
+
13
+ class ToolRegistry {
14
+ constructor() {
15
+ this.tools = {
16
+ Read: this.read.bind(this),
17
+ Write: this.write.bind(this),
18
+ Bash: this.bash.bind(this),
19
+ Exec: this.bash.bind(this), // Alias
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get tool function by name
25
+ * @param {string} toolName - Tool name (e.g., 'Read', 'Write')
26
+ * @returns {Function}
27
+ */
28
+ get(toolName) {
29
+ const tool = this.tools[toolName]
30
+ if (!tool) {
31
+ throw new Error(`Unknown tool: ${toolName}`)
32
+ }
33
+ return tool
34
+ }
35
+
36
+ /**
37
+ * Check if tool is allowed
38
+ * @param {string} toolName - Tool name
39
+ * @param {string[]} allowedTools - List of allowed tools
40
+ * @returns {boolean}
41
+ */
42
+ isAllowed(toolName, allowedTools) {
43
+ return allowedTools.includes(toolName)
44
+ }
45
+
46
+ /**
47
+ * Read file
48
+ * @param {string} filePath - File path
49
+ * @returns {Promise<string>}
50
+ */
51
+ async read(filePath) {
52
+ try {
53
+ return await fs.readFile(filePath, 'utf-8')
54
+ } catch (error) {
55
+ return null
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Write file
61
+ * @param {string} filePath - File path
62
+ * @param {string} content - Content to write
63
+ * @returns {Promise<void>}
64
+ */
65
+ async write(filePath, content) {
66
+ // Ensure directory exists
67
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
68
+ await fs.writeFile(filePath, content, 'utf-8')
69
+ }
70
+
71
+ /**
72
+ * Execute bash command
73
+ * @param {string} command - Command to execute
74
+ * @returns {Promise<{stdout: string, stderr: string}>}
75
+ */
76
+ async bash(command) {
77
+ try {
78
+ const { stdout, stderr } = await exec(command)
79
+ return { stdout, stderr }
80
+ } catch (error) {
81
+ return {
82
+ stdout: '',
83
+ stderr: error.message,
84
+ error: true,
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Check if file exists
91
+ * @param {string} filePath - File path
92
+ * @returns {Promise<boolean>}
93
+ */
94
+ async exists(filePath) {
95
+ try {
96
+ await fs.access(filePath)
97
+ return true
98
+ } catch {
99
+ return false
100
+ }
101
+ }
102
+
103
+ /**
104
+ * List directory
105
+ * @param {string} dirPath - Directory path
106
+ * @returns {Promise<string[]>}
107
+ */
108
+ async list(dirPath) {
109
+ try {
110
+ return await fs.readdir(dirPath)
111
+ } catch {
112
+ return []
113
+ }
114
+ }
115
+ }
116
+
117
+ module.exports = new ToolRegistry()
@@ -20,48 +20,67 @@ const COMMANDS = [
20
20
  category: 'core',
21
21
  description: 'Deep project analysis and initialization',
22
22
  usage: {
23
- claude: '/p:init',
24
- terminal: 'prjct init',
23
+ claude: '/p:init "[idea]"',
24
+ terminal: 'prjct init "[idea]"',
25
25
  },
26
- params: null,
26
+ params: '[idea]',
27
27
  implemented: true,
28
28
  hasTemplate: true,
29
29
  icon: 'Zap',
30
30
  requiresInit: false,
31
31
  blockingRules: null,
32
32
  features: [
33
- 'Analyzes codebase structure',
34
- 'Reviews docs and GitHub',
35
- 'Generates project summary',
36
- 'Creates initial roadmap',
33
+ 'Architect mode for blank projects',
34
+ 'Auto tech stack recommendation',
35
+ 'Project structure generation',
36
+ 'Initial roadmap creation',
37
+ 'Analyzes existing codebases',
37
38
  ],
38
39
  },
39
40
 
40
- // 2. Agentic Idea Evaluation
41
+ // 2. Feature with Roadmap (NEW - replaces idea)
41
42
  {
42
- name: 'idea',
43
+ name: 'feature',
43
44
  category: 'core',
44
- description: 'AI-powered idea evaluation with risk assessment',
45
+ description: 'Add feature with value analysis, roadmap, and task breakdown',
45
46
  usage: {
46
- claude: '/p:idea "add dark mode"',
47
- terminal: 'prjct idea "add dark mode"',
47
+ claude: '/p:feature "add unit testing"',
48
+ terminal: 'prjct feature "add unit testing"',
48
49
  },
49
- params: '<text>',
50
- implemented: false, // Needs agentic enhancement
50
+ params: '<description>',
51
+ implemented: true,
51
52
  hasTemplate: true,
52
- icon: 'Lightbulb',
53
+ icon: 'Package',
53
54
  requiresInit: true,
54
55
  blockingRules: null,
55
56
  features: [
56
- 'Risk assessment (low/medium/high)',
57
- 'Time estimation',
57
+ 'Value analysis (impact/effort/timing)',
58
+ 'Auto roadmap generation',
58
59
  'Task breakdown',
59
- 'Optimal timing recommendation',
60
- 'ASCII decision tree',
61
- 'Interactive keep/discard',
60
+ 'Auto-start first task',
61
+ 'Timing recommendations',
62
62
  ],
63
63
  },
64
64
 
65
+ // DEPRECATED: Use /p:feature instead
66
+ {
67
+ name: 'idea',
68
+ category: 'deprecated',
69
+ description: '[DEPRECATED] Use /p:feature instead',
70
+ usage: {
71
+ claude: null,
72
+ terminal: null,
73
+ },
74
+ params: '<text>',
75
+ implemented: false,
76
+ hasTemplate: true,
77
+ icon: 'Lightbulb',
78
+ requiresInit: true,
79
+ blockingRules: null,
80
+ deprecated: true,
81
+ replacedBy: 'feature',
82
+ },
83
+
65
84
  // 3. Strategic Roadmap
66
85
  {
67
86
  name: 'roadmap',
@@ -72,7 +91,7 @@ const COMMANDS = [
72
91
  terminal: 'prjct roadmap',
73
92
  },
74
93
  params: null,
75
- implemented: false,
94
+ implemented: true,
76
95
  hasTemplate: true,
77
96
  icon: 'Map',
78
97
  requiresInit: true,
@@ -95,7 +114,7 @@ const COMMANDS = [
95
114
  terminal: 'prjct status',
96
115
  },
97
116
  params: null,
98
- implemented: false,
117
+ implemented: true,
99
118
  hasTemplate: true,
100
119
  icon: 'BarChart3',
101
120
  requiresInit: true,
@@ -136,7 +155,7 @@ const COMMANDS = [
136
155
  terminal: 'prjct build "implement auth"',
137
156
  },
138
157
  params: '<task> | [1-5]',
139
- implemented: false,
158
+ implemented: true,
140
159
  hasTemplate: true,
141
160
  icon: 'Play',
142
161
  requiresInit: true,
@@ -209,36 +228,87 @@ const COMMANDS = [
209
228
  terminal: 'prjct ship "user authentication"',
210
229
  },
211
230
  params: '<feature>',
212
- implemented: false, // Needs Git integration
231
+ implemented: true, // Complete automated workflow
213
232
  hasTemplate: true,
214
233
  icon: 'Rocket',
215
234
  requiresInit: true,
216
235
  blockingRules: null,
217
236
  features: [
218
- 'Auto-generates commit message',
219
- 'Adds "Generated-by: prjct/cli" footer',
220
- 'Interactive push confirmation',
221
- 'Moves to shipped.md',
222
- 'Completion metadata tracking',
237
+ 'Lint checks (non-blocking)',
238
+ 'Run tests (non-blocking)',
239
+ 'Update docs',
240
+ 'Update version',
241
+ 'Update CHANGELOG',
242
+ 'Git commit + push',
243
+ 'Recommend compact',
244
+ ],
245
+ },
246
+
247
+ // 10. Bug Tracking
248
+ {
249
+ name: 'bug',
250
+ category: 'core',
251
+ description: 'Report and track bugs with priority',
252
+ usage: {
253
+ claude: '/p:bug "login button not working"',
254
+ terminal: 'prjct bug "login button not working"',
255
+ },
256
+ params: '<description>',
257
+ implemented: true,
258
+ hasTemplate: true,
259
+ icon: 'Bug',
260
+ requiresInit: true,
261
+ blockingRules: null,
262
+ features: [
263
+ 'Auto-detect severity (critical/high/medium/low)',
264
+ 'Priority placement in next.md',
265
+ 'Bug tracking in memory',
266
+ 'Quick bug resolution workflow',
267
+ ],
268
+ },
269
+
270
+ // 11. Architect Execute
271
+ {
272
+ name: 'architect',
273
+ category: 'core',
274
+ description: 'Execute architect plan and generate code',
275
+ usage: {
276
+ claude: '/p:architect execute',
277
+ terminal: 'prjct architect execute',
278
+ },
279
+ params: 'execute',
280
+ implemented: true,
281
+ hasTemplate: false,
282
+ icon: 'Hammer',
283
+ requiresInit: true,
284
+ blockingRules: null,
285
+ features: [
286
+ 'Reads architect-session.md plan',
287
+ 'Generates code structure',
288
+ 'Uses Context7 for documentation',
289
+ 'Language-agnostic implementation',
223
290
  ],
224
291
  },
225
292
 
226
293
  // ===== OPTIONAL COMMANDS (Advanced features) =====
294
+
295
+ // DEPRECATED: Workflow is now automatic in /p:ship
227
296
  {
228
297
  name: 'workflow',
229
- category: 'optional',
230
- description: 'Cascading agentic workflow for complex tasks',
298
+ category: 'deprecated',
299
+ description: '[DEPRECATED] Workflow is now automatic in /p:ship',
231
300
  usage: {
232
- claude: '/p:workflow',
233
- terminal: 'prjct workflow',
301
+ claude: null,
302
+ terminal: null,
234
303
  },
235
304
  params: null,
236
- implemented: true,
305
+ implemented: false,
237
306
  hasTemplate: true,
238
307
  icon: 'GitBranch',
239
308
  requiresInit: true,
240
309
  blockingRules: null,
241
- isOptional: true,
310
+ deprecated: true,
311
+ replacedBy: 'ship',
242
312
  },
243
313
  {
244
314
  name: 'design',
@@ -459,7 +529,7 @@ const registry = {
459
529
  const notImplemented = COMMANDS.filter((c) => c.hasTemplate && !c.implemented)
460
530
  if (notImplemented.length > 0) {
461
531
  issues.push(
462
- `Commands with templates but not implemented: ${notImplemented.map((c) => c.name).join(', ')}`,
532
+ `Commands with templates but not implemented: ${notImplemented.map((c) => c.name).join(', ')}`
463
533
  )
464
534
  }
465
535
 
@@ -468,7 +538,7 @@ const registry = {
468
538
  const invalidCategories = COMMANDS.filter((c) => !validCategories.includes(c.category))
469
539
  if (invalidCategories.length > 0) {
470
540
  issues.push(
471
- `Invalid categories: ${invalidCategories.map((c) => `${c.name}:${c.category}`).join(', ')}`,
541
+ `Invalid categories: ${invalidCategories.map((c) => `${c.name}:${c.category}`).join(', ')}`
472
542
  )
473
543
  }
474
544
 
@@ -495,36 +565,10 @@ const registry = {
495
565
 
496
566
  /**
497
567
  * Get commands with blocking rules
568
+ * NOTE: Blocking rules are now handled by Claude reading templates, not deterministic code
498
569
  */
499
570
  getWithBlockingRules: () => COMMANDS.filter((c) => c.blockingRules !== null),
500
571
 
501
- /**
502
- * Check if command can execute based on blocking rules
503
- */
504
- canExecute: (commandName, context = {}) => {
505
- const command = COMMANDS.find((c) => c.name === commandName)
506
- if (!command) return { allowed: false, message: 'Command not found' }
507
- if (!command.blockingRules) return { allowed: true }
508
-
509
- // Example context checks - should be implemented per command
510
- const { hasActiveTask, hasContent } = context
511
-
512
- if (commandName === 'build' && hasActiveTask) {
513
- return { allowed: false, message: command.blockingRules.message }
514
- }
515
- if (commandName === 'done' && !hasContent) {
516
- return { allowed: false, message: command.blockingRules.message }
517
- }
518
- if (commandName === 'next' && hasActiveTask) {
519
- return {
520
- allowed: true,
521
- warning: 'You have an active task. Complete it with /p:done first.',
522
- }
523
- }
524
-
525
- return { allowed: true }
526
- },
527
-
528
572
  /**
529
573
  * Get statistics
530
574
  */
@@ -545,7 +589,7 @@ const registry = {
545
589
  ...acc,
546
590
  [cat]: COMMANDS.filter((c) => c.category === cat).length,
547
591
  }),
548
- {},
592
+ {}
549
593
  ),
550
594
  }),
551
595
  }