prjct-cli 0.12.1 → 0.13.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/bin/serve.js +12 -30
  4. package/core/data/index.ts +19 -5
  5. package/core/data/md-base-manager.ts +203 -0
  6. package/core/data/md-queue-manager.ts +179 -0
  7. package/core/data/md-state-manager.ts +133 -0
  8. package/core/serializers/index.ts +20 -0
  9. package/core/serializers/queue-serializer.ts +210 -0
  10. package/core/serializers/state-serializer.ts +136 -0
  11. package/core/utils/file-helper.ts +12 -0
  12. package/package.json +1 -1
  13. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  14. package/packages/web/app/project/[id]/page.tsx +34 -1
  15. package/packages/web/app/project/[id]/stats/page.tsx +11 -5
  16. package/packages/web/app/settings/page.tsx +2 -221
  17. package/packages/web/components/AppSidebar/AppSidebar.tsx +5 -3
  18. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  19. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  20. package/packages/web/components/BlockersCard/index.ts +2 -0
  21. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  22. package/packages/web/lib/projects.ts +28 -27
  23. package/packages/web/lib/services/projects.server.ts +25 -21
  24. package/packages/web/lib/services/stats.server.ts +355 -57
  25. package/packages/web/package.json +0 -2
  26. package/templates/commands/decision.md +226 -0
  27. package/templates/commands/done.md +100 -68
  28. package/templates/commands/feature.md +102 -103
  29. package/templates/commands/idea.md +41 -38
  30. package/templates/commands/now.md +94 -33
  31. package/templates/commands/pause.md +90 -30
  32. package/templates/commands/ship.md +179 -74
  33. package/templates/commands/sync.md +324 -200
  34. package/packages/web/app/api/migrate/route.ts +0 -46
  35. package/packages/web/app/api/settings/route.ts +0 -97
  36. package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
  37. package/packages/web/lib/json-loader.ts +0 -630
  38. package/packages/web/lib/services/migration.server.ts +0 -600
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2025-12-10
4
+
5
+ ### Deep Sync + Cleanup migrations
6
+
7
+ Major update: Deep git analysis, removed external API dependencies, MD-first architecture.
8
+
9
+ - **Deep Sync (`/p:sync`)**
10
+ - Full git analysis: `git status`, `git log`, `git diff`, `git branch`
11
+ - Auto-detect completed tasks from commit messages
12
+ - Sync ALL MD files: now.md, next.md, shipped.md, ideas.md, roadmap.md
13
+ - Update project.json with real stats (fileCount, commitCount, version)
14
+ - Update CLAUDE.md Quick Reference table with current data
15
+ - Infer current task from uncommitted changes
16
+
17
+ - **Sync Button in Project Detail**
18
+ - Prominent primary-colored button in sidebar
19
+ - Always visible for easy access
20
+ - Mobile support in command grid
21
+
22
+ - **Removed External Dependencies**
23
+ - Removed OpenAI/OpenRouter API key management
24
+ - Removed migration service (no longer needed)
25
+ - Removed settings API route
26
+ - Cleaned up web package dependencies
27
+
28
+ - **Ship Workflow Improvements**
29
+ - Language agnostic versioning (package.json, Cargo.toml, pyproject.toml, VERSION)
30
+ - CHANGELOG.md generation is now MANDATORY
31
+ - Auto-runs deep sync after shipping
32
+ - prjct signature in commits
33
+
34
+ - **Files Removed**:
35
+ - `packages/web/app/api/settings/route.ts`
36
+ - `packages/web/app/api/migrate/route.ts`
37
+ - `packages/web/lib/services/migration.server.ts`
38
+ - `packages/web/components/MigrationGate/`
39
+
40
+ - **Files Modified**:
41
+ - `templates/commands/sync.md` - Complete rewrite for deep analysis
42
+ - `templates/commands/ship.md` - Language agnostic, mandatory changelog
43
+ - `packages/web/app/settings/page.tsx` - Removed API keys section
44
+ - `packages/web/app/project/[id]/page.tsx` - Added Sync button
45
+
3
46
  ## [0.12.0] - 2025-12-10
4
47
 
5
48
  ### Added - JSON-First Architecture (Zero Data Loss)
package/CLAUDE.md CHANGED
@@ -28,19 +28,31 @@ p. done → Mark complete, next task
28
28
  p. ship → Lint/test/commit/push
29
29
  ```
30
30
 
31
- ## Architecture
31
+ ## Architecture: MD-First
32
+
33
+ **MD files are the source of truth.** No JSON data files - all state is stored directly in Markdown.
32
34
 
33
35
  ### Global Storage
34
36
  ```
35
37
  ~/.prjct-cli/projects/{id}/
36
- ├── core/ # now.md, next.md, context.md
37
- ├── progress/ # shipped.md, metrics.md, sessions/
38
- ├── planning/ # ideas.md, roadmap.md, sessions/
38
+ ├── core/ # now.md (current task), next.md (queue)
39
+ ├── progress/ # shipped.md, sessions/
40
+ ├── planning/ # ideas.md, roadmap.md
39
41
  ├── analysis/ # repo-summary.md
40
- ├── memory/ # context.jsonl, sessions/
41
- └── agents/ # Generated specialists
42
+ ├── memory/ # context.jsonl (append-only logs)
43
+ ├── agents/ # Generated specialists
44
+ └── project.json # Metadata only (name, repoPath, techStack)
42
45
  ```
43
46
 
47
+ ### Key Files (Source of Truth)
48
+ | File | Purpose |
49
+ |------|---------|
50
+ | `core/now.md` | Current task state |
51
+ | `core/next.md` | Task queue |
52
+ | `planning/ideas.md` | Ideas backlog |
53
+ | `planning/roadmap.md` | Feature roadmap |
54
+ | `progress/shipped.md` | Shipped features |
55
+
44
56
  ### Local Config
45
57
  ```
46
58
  .prjct/prjct.config.json # Links to global data via projectId
package/bin/serve.js CHANGED
@@ -65,55 +65,39 @@ function writeState(state) {
65
65
  }
66
66
 
67
67
  /**
68
- * Get package.json version and hash of dependencies
68
+ * Get package.json version
69
69
  */
70
- function getPackageInfo(pkgPath) {
70
+ function getPackageVersion(pkgPath) {
71
71
  try {
72
72
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'))
73
- const deps = JSON.stringify(pkg.dependencies || {}) + JSON.stringify(pkg.devDependencies || {})
74
- // Simple hash of dependencies
75
- let hash = 0
76
- for (let i = 0; i < deps.length; i++) {
77
- const char = deps.charCodeAt(i)
78
- hash = ((hash << 5) - hash) + char
79
- hash = hash & hash // Convert to 32bit integer
80
- }
81
- return { version: pkg.version, depsHash: hash.toString(16) }
73
+ return pkg.version || '0.0.0'
82
74
  } catch {
83
- return { version: '0.0.0', depsHash: '0' }
75
+ return '0.0.0'
84
76
  }
85
77
  }
86
78
 
87
79
  /**
88
80
  * Check if dependencies need to be installed
81
+ * Simple logic: install only if node_modules missing OR version changed
89
82
  */
90
83
  function needsInstall(pkgDir, stateKey) {
91
84
  const nodeModules = path.join(pkgDir, 'node_modules')
92
85
 
93
- // If node_modules doesn't exist, definitely need install
86
+ // If node_modules doesn't exist, need install
94
87
  if (!fs.existsSync(nodeModules)) {
95
88
  return { needed: true, reason: 'node_modules not found' }
96
89
  }
97
90
 
98
91
  const state = readState()
99
- const pkgInfo = getPackageInfo(pkgDir)
100
- const savedInfo = state[stateKey]
101
-
102
- // If no saved state, need install to track
103
- if (!savedInfo) {
104
- return { needed: true, reason: 'first time tracking' }
105
- }
92
+ const currentVersion = getPackageVersion(pkgDir)
93
+ const savedVersion = state[stateKey]?.version
106
94
 
107
95
  // If version changed, need install
108
- if (savedInfo.version !== pkgInfo.version) {
109
- return { needed: true, reason: `version changed: ${savedInfo.version} → ${pkgInfo.version}` }
110
- }
111
-
112
- // If dependencies hash changed, need install
113
- if (savedInfo.depsHash !== pkgInfo.depsHash) {
114
- return { needed: true, reason: 'dependencies changed' }
96
+ if (savedVersion && savedVersion !== currentVersion) {
97
+ return { needed: true, reason: `${savedVersion} → ${currentVersion}` }
115
98
  }
116
99
 
100
+ // node_modules exists and version unchanged = skip
117
101
  return { needed: false }
118
102
  }
119
103
 
@@ -122,10 +106,8 @@ function needsInstall(pkgDir, stateKey) {
122
106
  */
123
107
  function markInstalled(pkgDir, stateKey) {
124
108
  const state = readState()
125
- const pkgInfo = getPackageInfo(pkgDir)
126
109
  state[stateKey] = {
127
- version: pkgInfo.version,
128
- depsHash: pkgInfo.depsHash,
110
+ version: getPackageVersion(pkgDir),
129
111
  installedAt: new Date().toISOString()
130
112
  }
131
113
  writeState(state)
@@ -1,13 +1,21 @@
1
1
  /**
2
2
  * Data Module
3
3
  *
4
- * JSON file managers for all project data types.
4
+ * MD-First Architecture: MD files are the source of truth.
5
+ * JSON managers are deprecated in favor of MD managers.
5
6
  */
6
7
 
7
- // Base
8
+ // Base (legacy JSON)
8
9
  export { BaseManager, ArrayManager } from './base-manager'
9
10
 
10
- // Managers
11
+ // MD-First Base
12
+ export { MdBaseManager, MdArrayManager } from './md-base-manager'
13
+
14
+ // MD-First Managers (NEW - use these!)
15
+ export { mdStateManager } from './md-state-manager'
16
+ export { mdQueueManager } from './md-queue-manager'
17
+
18
+ // Legacy JSON Managers (deprecated - for backwards compatibility only)
11
19
  export { stateManager, default as stateManagerDefault } from './state-manager'
12
20
  export { projectManager, default as projectManagerDefault } from './project-manager'
13
21
  export { agentsManager, default as agentsManagerDefault } from './agents-manager'
@@ -17,7 +25,13 @@ export { shippedManager, default as shippedManagerDefault } from './shipped-mana
17
25
  export { analysisManager, default as analysisManagerDefault } from './analysis-manager'
18
26
  export { outcomesManager, default as outcomesManagerDefault } from './outcomes-manager'
19
27
 
20
- // Convenience object with all managers
28
+ // MD-First managers (preferred)
29
+ export const mdManagers = {
30
+ state: require('./md-state-manager').mdStateManager,
31
+ queue: require('./md-queue-manager').mdQueueManager
32
+ }
33
+
34
+ // Legacy JSON managers (deprecated)
21
35
  export const dataManagers = {
22
36
  state: require('./state-manager').stateManager,
23
37
  project: require('./project-manager').projectManager,
@@ -29,4 +43,4 @@ export const dataManagers = {
29
43
  outcomes: require('./outcomes-manager').outcomesManager
30
44
  }
31
45
 
32
- export default dataManagers
46
+ export default mdManagers
@@ -0,0 +1,203 @@
1
+ /**
2
+ * MD Base Manager
3
+ *
4
+ * Abstract base class for MD file managers.
5
+ * MD-First Architecture: MD is the source of truth.
6
+ *
7
+ * Each concrete manager must implement:
8
+ * - parse(content: string): T - Convert MD to schema
9
+ * - serialize(data: T): string - Convert schema to MD
10
+ * - getDefault(projectId: string): T - Default value when file doesn't exist
11
+ */
12
+
13
+ import path from 'path'
14
+ import fs from 'fs/promises'
15
+ import * as fileHelper from '../utils/file-helper'
16
+ import pathManager from '../infrastructure/path-manager'
17
+
18
+ export abstract class MdBaseManager<T> {
19
+ protected filename: string
20
+ protected cache: Map<string, T> = new Map()
21
+ protected cacheTimeout = 5000 // 5 seconds
22
+ protected lastRead: Map<string, number> = new Map()
23
+
24
+ constructor(filename: string) {
25
+ this.filename = filename
26
+ }
27
+
28
+ /**
29
+ * Get file path for a project.
30
+ */
31
+ protected getFilePath(projectId: string): string {
32
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
33
+ return path.join(globalPath, this.filename)
34
+ }
35
+
36
+ /**
37
+ * Get default value for this data type.
38
+ */
39
+ protected abstract getDefault(projectId: string): T
40
+
41
+ /**
42
+ * Parse MD content to schema.
43
+ */
44
+ protected abstract parse(content: string): T
45
+
46
+ /**
47
+ * Serialize schema to MD content.
48
+ */
49
+ protected abstract serialize(data: T): string
50
+
51
+ /**
52
+ * Read data from MD file.
53
+ */
54
+ async read(projectId: string): Promise<T> {
55
+ const now = Date.now()
56
+ const lastReadTime = this.lastRead.get(projectId) || 0
57
+
58
+ // Return cached if fresh
59
+ if (now - lastReadTime < this.cacheTimeout && this.cache.has(projectId)) {
60
+ return this.cache.get(projectId)!
61
+ }
62
+
63
+ const filePath = this.getFilePath(projectId)
64
+ const content = await fileHelper.readFile(filePath, '')
65
+
66
+ const data = content.trim() ? this.parse(content) : this.getDefault(projectId)
67
+
68
+ // Update cache
69
+ this.cache.set(projectId, data)
70
+ this.lastRead.set(projectId, now)
71
+
72
+ return data
73
+ }
74
+
75
+ /**
76
+ * Write data to MD file using atomic write (prevents partial writes).
77
+ */
78
+ async write(projectId: string, data: T): Promise<void> {
79
+ const filePath = this.getFilePath(projectId)
80
+ const content = this.serialize(data)
81
+
82
+ // Ensure directory exists
83
+ await fileHelper.ensureDir(path.dirname(filePath))
84
+
85
+ // Atomic write: write to temp file, then rename
86
+ const tempPath = `${filePath}.${Date.now()}.tmp`
87
+ await fs.writeFile(tempPath, content, 'utf-8')
88
+ await fs.rename(tempPath, filePath)
89
+
90
+ // Update cache
91
+ this.cache.set(projectId, data)
92
+ this.lastRead.set(projectId, Date.now())
93
+ }
94
+
95
+ /**
96
+ * Check if file exists.
97
+ */
98
+ async exists(projectId: string): Promise<boolean> {
99
+ const filePath = this.getFilePath(projectId)
100
+ return fileHelper.fileExists(filePath)
101
+ }
102
+
103
+ /**
104
+ * Initialize with default data.
105
+ */
106
+ async initialize(projectId: string): Promise<T> {
107
+ const data = this.getDefault(projectId)
108
+ await this.write(projectId, data)
109
+ return data
110
+ }
111
+
112
+ /**
113
+ * Clear cache.
114
+ */
115
+ clearCache(projectId?: string): void {
116
+ if (projectId) {
117
+ this.cache.delete(projectId)
118
+ this.lastRead.delete(projectId)
119
+ } else {
120
+ this.cache.clear()
121
+ this.lastRead.clear()
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Update data with an updater function (read-modify-write).
127
+ */
128
+ async update(projectId: string, updater: (data: T) => T): Promise<T> {
129
+ const current = await this.read(projectId)
130
+ const updated = updater(current)
131
+ await this.write(projectId, updated)
132
+ return updated
133
+ }
134
+
135
+ /**
136
+ * Get raw MD content without parsing.
137
+ */
138
+ async readRaw(projectId: string): Promise<string> {
139
+ const filePath = this.getFilePath(projectId)
140
+ return fileHelper.readFile(filePath, '')
141
+ }
142
+
143
+ /**
144
+ * Write raw MD content without serialization.
145
+ */
146
+ async writeRaw(projectId: string, content: string): Promise<void> {
147
+ const filePath = this.getFilePath(projectId)
148
+ await fileHelper.ensureDir(path.dirname(filePath))
149
+ await fileHelper.atomicWrite(filePath, content)
150
+ this.clearCache(projectId)
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Base manager for array-based MD files (like shipped, ideas).
156
+ */
157
+ export abstract class MdArrayManager<T> extends MdBaseManager<T[]> {
158
+ protected getDefault(): T[] {
159
+ return []
160
+ }
161
+
162
+ /**
163
+ * Add item to array.
164
+ */
165
+ async add(projectId: string, item: T): Promise<T[]> {
166
+ return this.update(projectId, (data) => [...data, item])
167
+ }
168
+
169
+ /**
170
+ * Prepend item to array (add at beginning).
171
+ */
172
+ async prepend(projectId: string, item: T): Promise<T[]> {
173
+ return this.update(projectId, (data) => [item, ...data])
174
+ }
175
+
176
+ /**
177
+ * Remove item by predicate.
178
+ */
179
+ async remove(projectId: string, predicate: (item: T) => boolean): Promise<T[]> {
180
+ return this.update(projectId, (data) => data.filter((item) => !predicate(item)))
181
+ }
182
+
183
+ /**
184
+ * Find item by predicate.
185
+ */
186
+ async find(projectId: string, predicate: (item: T) => boolean): Promise<T | undefined> {
187
+ const data = await this.read(projectId)
188
+ return data.find(predicate)
189
+ }
190
+
191
+ /**
192
+ * Update item by predicate.
193
+ */
194
+ async updateItem(
195
+ projectId: string,
196
+ predicate: (item: T) => boolean,
197
+ updater: (item: T) => T
198
+ ): Promise<T[]> {
199
+ return this.update(projectId, (data) =>
200
+ data.map((item) => (predicate(item) ? updater(item) : item))
201
+ )
202
+ }
203
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * MD Queue Manager
3
+ *
4
+ * MD-First Architecture: Manages queue via next.md.
5
+ * Source of truth is the markdown file, not JSON.
6
+ */
7
+
8
+ import { MdBaseManager } from './md-base-manager'
9
+ import { parseQueue, serializeQueue } from '../serializers'
10
+ import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
11
+
12
+ class MdQueueManager extends MdBaseManager<QueueJson> {
13
+ constructor() {
14
+ super('core/next.md')
15
+ }
16
+
17
+ protected getDefault(): QueueJson {
18
+ return {
19
+ tasks: [],
20
+ lastUpdated: new Date().toISOString().split('T')[0]
21
+ }
22
+ }
23
+
24
+ protected parse(content: string): QueueJson {
25
+ return parseQueue(content)
26
+ }
27
+
28
+ protected serialize(data: QueueJson): string {
29
+ return serializeQueue(data)
30
+ }
31
+
32
+ // =========== Queue Operations ===========
33
+
34
+ async getTasks(projectId: string): Promise<QueueTask[]> {
35
+ const queue = await this.read(projectId)
36
+ return queue.tasks
37
+ }
38
+
39
+ async getActiveTasks(projectId: string): Promise<QueueTask[]> {
40
+ const queue = await this.read(projectId)
41
+ return queue.tasks.filter(t => t.section === 'active' && !t.completed)
42
+ }
43
+
44
+ async getBacklog(projectId: string): Promise<QueueTask[]> {
45
+ const queue = await this.read(projectId)
46
+ return queue.tasks.filter(t => t.section === 'backlog' && !t.completed)
47
+ }
48
+
49
+ async addTask(
50
+ projectId: string,
51
+ task: Omit<QueueTask, 'id' | 'createdAt' | 'completed'>
52
+ ): Promise<QueueJson> {
53
+ const newTask: QueueTask = {
54
+ ...task,
55
+ id: `task_${Date.now()}`,
56
+ createdAt: new Date().toISOString(),
57
+ completed: false
58
+ }
59
+
60
+ return this.update(projectId, (queue) => ({
61
+ tasks: this.sortTasks([...queue.tasks, newTask]),
62
+ lastUpdated: new Date().toISOString().split('T')[0]
63
+ }))
64
+ }
65
+
66
+ async removeTask(projectId: string, taskId: string): Promise<QueueJson> {
67
+ return this.update(projectId, (queue) => ({
68
+ tasks: queue.tasks.filter(t => t.id !== taskId),
69
+ lastUpdated: new Date().toISOString().split('T')[0]
70
+ }))
71
+ }
72
+
73
+ async completeTask(projectId: string, taskId: string): Promise<QueueJson> {
74
+ return this.update(projectId, (queue) => ({
75
+ tasks: queue.tasks.map(t =>
76
+ t.id === taskId
77
+ ? { ...t, completed: true, completedAt: new Date().toISOString() }
78
+ : t
79
+ ),
80
+ lastUpdated: new Date().toISOString().split('T')[0]
81
+ }))
82
+ }
83
+
84
+ async getNextTask(projectId: string): Promise<QueueTask | null> {
85
+ const queue = await this.read(projectId)
86
+ return queue.tasks.find(t => t.section === 'active' && !t.completed) || null
87
+ }
88
+
89
+ async moveToSection(
90
+ projectId: string,
91
+ taskId: string,
92
+ section: TaskSection
93
+ ): Promise<QueueJson> {
94
+ return this.update(projectId, (queue) => ({
95
+ tasks: queue.tasks.map(t =>
96
+ t.id === taskId ? { ...t, section } : t
97
+ ),
98
+ lastUpdated: new Date().toISOString().split('T')[0]
99
+ }))
100
+ }
101
+
102
+ async setPriority(
103
+ projectId: string,
104
+ taskId: string,
105
+ priority: Priority
106
+ ): Promise<QueueJson> {
107
+ return this.update(projectId, (queue) => ({
108
+ tasks: this.sortTasks(
109
+ queue.tasks.map(t => t.id === taskId ? { ...t, priority } : t)
110
+ ),
111
+ lastUpdated: new Date().toISOString().split('T')[0]
112
+ }))
113
+ }
114
+
115
+ /**
116
+ * Add multiple tasks at once (e.g., from a feature breakdown)
117
+ */
118
+ async addTasks(
119
+ projectId: string,
120
+ tasks: Array<Omit<QueueTask, 'id' | 'createdAt' | 'completed'>>
121
+ ): Promise<QueueJson> {
122
+ const newTasks: QueueTask[] = tasks.map((task, i) => ({
123
+ ...task,
124
+ id: `task_${Date.now()}_${i}`,
125
+ createdAt: new Date().toISOString(),
126
+ completed: false
127
+ }))
128
+
129
+ return this.update(projectId, (queue) => ({
130
+ tasks: this.sortTasks([...queue.tasks, ...newTasks]),
131
+ lastUpdated: new Date().toISOString().split('T')[0]
132
+ }))
133
+ }
134
+
135
+ /**
136
+ * Clear completed tasks
137
+ */
138
+ async clearCompleted(projectId: string): Promise<QueueJson> {
139
+ return this.update(projectId, (queue) => ({
140
+ tasks: queue.tasks.filter(t => !t.completed),
141
+ lastUpdated: new Date().toISOString().split('T')[0]
142
+ }))
143
+ }
144
+
145
+ /**
146
+ * Sort tasks by priority then by creation date
147
+ */
148
+ private sortTasks(tasks: QueueTask[]): QueueTask[] {
149
+ const priorityOrder: Record<Priority, number> = {
150
+ critical: 0,
151
+ high: 1,
152
+ medium: 2,
153
+ low: 3
154
+ }
155
+
156
+ return tasks.sort((a, b) => {
157
+ // First by section (active first)
158
+ const sectionOrder: Record<TaskSection, number> = {
159
+ active: 0,
160
+ previously_active: 1,
161
+ backlog: 2
162
+ }
163
+ if (sectionOrder[a.section] !== sectionOrder[b.section]) {
164
+ return sectionOrder[a.section] - sectionOrder[b.section]
165
+ }
166
+
167
+ // Then by priority
168
+ if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
169
+ return priorityOrder[a.priority] - priorityOrder[b.priority]
170
+ }
171
+
172
+ // Finally by creation date
173
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
174
+ })
175
+ }
176
+ }
177
+
178
+ export const mdQueueManager = new MdQueueManager()
179
+ export default mdQueueManager