@masslessai/push-todo 3.8.3 → 3.9.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/lib/api.js +3 -0
- package/lib/connect.js +29 -4
- package/lib/daemon.js +60 -7
- package/lib/project-registry.js +180 -34
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -52,6 +52,9 @@ export async function fetchTasks(gitRemote, options = {}) {
|
|
|
52
52
|
if (gitRemote) {
|
|
53
53
|
params.set('git_remote', gitRemote);
|
|
54
54
|
}
|
|
55
|
+
if (options.actionType) {
|
|
56
|
+
params.set('action_type', options.actionType);
|
|
57
|
+
}
|
|
55
58
|
if (options.backlogOnly) {
|
|
56
59
|
params.set('later_only', 'true');
|
|
57
60
|
}
|
package/lib/connect.js
CHANGED
|
@@ -937,6 +937,8 @@ async function registerProjectWithBackend(apiKey, clientType = 'claude-code', ke
|
|
|
937
937
|
if (data.success) {
|
|
938
938
|
return {
|
|
939
939
|
status: 'success',
|
|
940
|
+
action_id: data.action_id || null,
|
|
941
|
+
action_type: data.action_type || null,
|
|
940
942
|
action_name: data.normalized_name || data.action_name || 'Unknown',
|
|
941
943
|
created: data.created !== false,
|
|
942
944
|
message: data.message || ''
|
|
@@ -946,17 +948,31 @@ async function registerProjectWithBackend(apiKey, clientType = 'claude-code', ke
|
|
|
946
948
|
return { status: 'error', message: 'Unknown error' };
|
|
947
949
|
}
|
|
948
950
|
|
|
951
|
+
// Mapping from CLI client_type to canonical DB action_type
|
|
952
|
+
// Must match CLIENT_TO_ACTION_TYPE in register-project edge function
|
|
953
|
+
const CLIENT_TO_ACTION_TYPE = {
|
|
954
|
+
'claude-code': 'claude-code',
|
|
955
|
+
'openai-codex': 'openai-codex',
|
|
956
|
+
'openclaw': 'clawdbot',
|
|
957
|
+
'clawdbot': 'clawdbot',
|
|
958
|
+
};
|
|
959
|
+
|
|
949
960
|
/**
|
|
950
961
|
* Register project in local registry for daemon routing.
|
|
962
|
+
*
|
|
963
|
+
* @param {string} gitRemoteRaw - Raw git remote URL
|
|
964
|
+
* @param {string} localPath - Absolute local path
|
|
965
|
+
* @param {Object} [actionMeta] - Action metadata from register-project response
|
|
966
|
+
* @returns {boolean}
|
|
951
967
|
*/
|
|
952
|
-
function registerProjectLocally(gitRemoteRaw, localPath) {
|
|
968
|
+
function registerProjectLocally(gitRemoteRaw, localPath, actionMeta = {}) {
|
|
953
969
|
if (!gitRemoteRaw) return false;
|
|
954
970
|
|
|
955
971
|
const gitRemote = normalizeGitRemote(gitRemoteRaw);
|
|
956
972
|
if (!gitRemote) return false;
|
|
957
973
|
|
|
958
974
|
const registry = getRegistry();
|
|
959
|
-
return registry.register(gitRemote, localPath);
|
|
975
|
+
return registry.register(gitRemote, localPath, actionMeta);
|
|
960
976
|
}
|
|
961
977
|
|
|
962
978
|
/**
|
|
@@ -1128,7 +1144,12 @@ export async function runConnect(options = {}) {
|
|
|
1128
1144
|
// Register in local project registry for global daemon routing
|
|
1129
1145
|
const gitRemoteRaw = getGitRemote();
|
|
1130
1146
|
const localPath = process.cwd();
|
|
1131
|
-
const
|
|
1147
|
+
const actionType = result.action_type || CLIENT_TO_ACTION_TYPE[clientType] || clientType;
|
|
1148
|
+
const isNewLocal = registerProjectLocally(gitRemoteRaw, localPath, {
|
|
1149
|
+
actionType,
|
|
1150
|
+
actionId: result.action_id,
|
|
1151
|
+
actionName: result.action_name,
|
|
1152
|
+
});
|
|
1132
1153
|
|
|
1133
1154
|
console.log('');
|
|
1134
1155
|
console.log(' ' + '='.repeat(40));
|
|
@@ -1204,7 +1225,11 @@ export async function runConnect(options = {}) {
|
|
|
1204
1225
|
// Register in local project registry for global daemon routing
|
|
1205
1226
|
const gitRemoteRaw = getGitRemote();
|
|
1206
1227
|
const localPath = process.cwd();
|
|
1207
|
-
const
|
|
1228
|
+
const actionType = CLIENT_TO_ACTION_TYPE[clientType] || clientType;
|
|
1229
|
+
const isNewLocal = registerProjectLocally(gitRemoteRaw, localPath, {
|
|
1230
|
+
actionType,
|
|
1231
|
+
actionName: authResult.action_name,
|
|
1232
|
+
});
|
|
1208
1233
|
|
|
1209
1234
|
// Show success
|
|
1210
1235
|
console.log('');
|
package/lib/daemon.js
CHANGED
|
@@ -372,13 +372,19 @@ async function fetchQueuedTasks() {
|
|
|
372
372
|
const projects = getListedProjects();
|
|
373
373
|
const gitRemotes = Object.keys(projects);
|
|
374
374
|
|
|
375
|
+
// Get projects with action type for structured heartbeat (multi-agent support)
|
|
376
|
+
const projectsWithType = getListedProjectsWithActionType();
|
|
377
|
+
|
|
375
378
|
// Add machine registry headers for daemon status tracking
|
|
376
379
|
// See: /docs/20260204_daemon_heartbeat_status_indicator_implementation_plan.md (machine_registry table)
|
|
377
380
|
const heartbeatHeaders = {};
|
|
378
381
|
if (machineId && gitRemotes.length > 0) {
|
|
379
382
|
heartbeatHeaders['X-Machine-Id'] = machineId;
|
|
380
383
|
heartbeatHeaders['X-Machine-Name'] = machineName || 'Unknown Mac';
|
|
381
|
-
|
|
384
|
+
// Structured format: "remote::type,remote::type" (backward compat: old parsers split on , and ignore ::)
|
|
385
|
+
heartbeatHeaders['X-Git-Remotes'] = projectsWithType
|
|
386
|
+
.map(p => `${p.gitRemote}::${p.actionType}`)
|
|
387
|
+
.join(',');
|
|
382
388
|
heartbeatHeaders['X-Daemon-Version'] = getVersion();
|
|
383
389
|
heartbeatHeaders['X-Capabilities'] = JSON.stringify(getCapabilities());
|
|
384
390
|
}
|
|
@@ -503,19 +509,38 @@ async function claimTask(displayNumber) {
|
|
|
503
509
|
|
|
504
510
|
// ==================== Project Registry ====================
|
|
505
511
|
|
|
506
|
-
function getProjectPath(gitRemote) {
|
|
512
|
+
function getProjectPath(gitRemote, actionType) {
|
|
507
513
|
if (!existsSync(REGISTRY_FILE)) {
|
|
508
514
|
return null;
|
|
509
515
|
}
|
|
510
516
|
|
|
511
517
|
try {
|
|
512
518
|
const data = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
513
|
-
|
|
519
|
+
const projects = data.projects || {};
|
|
520
|
+
|
|
521
|
+
// Try exact composite key first (V2 format)
|
|
522
|
+
if (actionType) {
|
|
523
|
+
const key = `${gitRemote}::${actionType}`;
|
|
524
|
+
if (projects[key]) {
|
|
525
|
+
return projects[key].localPath || projects[key].local_path || null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Fall back to scanning for matching gitRemote (V2 or V1 format)
|
|
530
|
+
for (const [key, info] of Object.entries(projects)) {
|
|
531
|
+
if ((info.gitRemote || key) === gitRemote) {
|
|
532
|
+
return info.localPath || info.local_path || null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
514
536
|
} catch {
|
|
515
537
|
return null;
|
|
516
538
|
}
|
|
517
539
|
}
|
|
518
540
|
|
|
541
|
+
/**
|
|
542
|
+
* List all registered projects (backward-compatible: gitRemote -> localPath).
|
|
543
|
+
*/
|
|
519
544
|
function getListedProjects() {
|
|
520
545
|
if (!existsSync(REGISTRY_FILE)) {
|
|
521
546
|
return {};
|
|
@@ -524,8 +549,11 @@ function getListedProjects() {
|
|
|
524
549
|
try {
|
|
525
550
|
const data = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
526
551
|
const result = {};
|
|
527
|
-
for (const [
|
|
528
|
-
|
|
552
|
+
for (const [key, info] of Object.entries(data.projects || {})) {
|
|
553
|
+
const remote = info.gitRemote || key;
|
|
554
|
+
if (!(remote in result)) {
|
|
555
|
+
result[remote] = info.localPath || info.local_path;
|
|
556
|
+
}
|
|
529
557
|
}
|
|
530
558
|
return result;
|
|
531
559
|
} catch {
|
|
@@ -533,6 +561,30 @@ function getListedProjects() {
|
|
|
533
561
|
}
|
|
534
562
|
}
|
|
535
563
|
|
|
564
|
+
/**
|
|
565
|
+
* List all registered projects with action type info.
|
|
566
|
+
* Returns array of {gitRemote, actionType} for structured heartbeat.
|
|
567
|
+
*/
|
|
568
|
+
function getListedProjectsWithActionType() {
|
|
569
|
+
if (!existsSync(REGISTRY_FILE)) {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const data = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
575
|
+
const result = [];
|
|
576
|
+
for (const [key, info] of Object.entries(data.projects || {})) {
|
|
577
|
+
result.push({
|
|
578
|
+
gitRemote: info.gitRemote || key,
|
|
579
|
+
actionType: info.actionType || 'claude-code',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
} catch {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
536
588
|
// ==================== Git Worktree Management ====================
|
|
537
589
|
|
|
538
590
|
function getWorktreeSuffix() {
|
|
@@ -1200,6 +1252,7 @@ async function executeTask(task) {
|
|
|
1200
1252
|
|
|
1201
1253
|
const displayNumber = task.displayNumber || task.display_number;
|
|
1202
1254
|
const gitRemote = task.gitRemote || task.git_remote;
|
|
1255
|
+
const taskActionType = task.actionType || task.action_type || null;
|
|
1203
1256
|
const summary = task.summary || 'No summary';
|
|
1204
1257
|
const content = task.normalizedContent || task.normalized_content ||
|
|
1205
1258
|
task.content || task.summary || 'Work on this task';
|
|
@@ -1219,10 +1272,10 @@ async function executeTask(task) {
|
|
|
1219
1272
|
return null;
|
|
1220
1273
|
}
|
|
1221
1274
|
|
|
1222
|
-
// Get project path
|
|
1275
|
+
// Get project path (use action_type for multi-agent routing)
|
|
1223
1276
|
let projectPath = null;
|
|
1224
1277
|
if (gitRemote) {
|
|
1225
|
-
projectPath = getProjectPath(gitRemote);
|
|
1278
|
+
projectPath = getProjectPath(gitRemote, taskActionType);
|
|
1226
1279
|
if (!projectPath) {
|
|
1227
1280
|
log(`Task #${displayNumber}: Project not registered: ${gitRemote}`);
|
|
1228
1281
|
log("Run '/push-todo connect' in the project directory to register");
|
package/lib/project-registry.js
CHANGED
|
@@ -12,7 +12,7 @@ import { homedir } from 'os';
|
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
|
|
14
14
|
const REGISTRY_FILE = join(homedir(), '.config', 'push', 'projects.json');
|
|
15
|
-
const REGISTRY_VERSION =
|
|
15
|
+
const REGISTRY_VERSION = 2;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Project Registry class.
|
|
@@ -62,36 +62,91 @@ class ProjectRegistry {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
_migrate(data) {
|
|
65
|
-
|
|
65
|
+
const oldVersion = data.version || 0;
|
|
66
|
+
|
|
67
|
+
// V1 → V2: Add actionType to project keys
|
|
68
|
+
// V1 keys: "github.com/user/repo" → V2 keys: "github.com/user/repo::claude-code"
|
|
69
|
+
// All V1 projects were Claude Code (the only agent type that existed)
|
|
70
|
+
if (oldVersion < 2 && data.projects) {
|
|
71
|
+
const newProjects = {};
|
|
72
|
+
for (const [key, info] of Object.entries(data.projects)) {
|
|
73
|
+
// Skip if already in V2 format (contains ::)
|
|
74
|
+
if (key.includes('::')) {
|
|
75
|
+
newProjects[key] = { ...info, gitRemote: info.gitRemote || key.split('::')[0] };
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const v2Key = `${key}::claude-code`;
|
|
79
|
+
newProjects[v2Key] = {
|
|
80
|
+
...info,
|
|
81
|
+
gitRemote: key,
|
|
82
|
+
actionType: 'claude-code',
|
|
83
|
+
actionId: null,
|
|
84
|
+
actionName: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
data.projects = newProjects;
|
|
88
|
+
|
|
89
|
+
// Migrate defaultProject key too
|
|
90
|
+
if (data.defaultProject && !data.defaultProject.includes('::')) {
|
|
91
|
+
data.defaultProject = `${data.defaultProject}::claude-code`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
data.version = REGISTRY_VERSION;
|
|
96
|
+
// Persist the migrated data immediately
|
|
97
|
+
writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2));
|
|
67
98
|
return data;
|
|
68
99
|
}
|
|
69
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Build the composite key for a project entry.
|
|
103
|
+
* Format: "gitRemote::actionType" (e.g., "github.com/user/repo::claude-code")
|
|
104
|
+
*
|
|
105
|
+
* @param {string} gitRemote - Normalized git remote
|
|
106
|
+
* @param {string} actionType - Action type (e.g., "claude-code", "clawdbot")
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
_makeKey(gitRemote, actionType) {
|
|
110
|
+
return `${gitRemote}::${actionType || 'claude-code'}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
70
113
|
/**
|
|
71
114
|
* Register a project.
|
|
72
115
|
*
|
|
73
116
|
* @param {string} gitRemote - Normalized git remote (e.g., "github.com/user/repo")
|
|
74
117
|
* @param {string} localPath - Absolute local path
|
|
118
|
+
* @param {Object} [actionMeta] - Action metadata from register-project response
|
|
119
|
+
* @param {string} [actionMeta.actionType] - Action type (e.g., "claude-code", "clawdbot")
|
|
120
|
+
* @param {string} [actionMeta.actionId] - Action UUID
|
|
121
|
+
* @param {string} [actionMeta.actionName] - Action display name
|
|
75
122
|
* @returns {boolean} True if newly registered, false if updated existing
|
|
76
123
|
*/
|
|
77
|
-
register(gitRemote, localPath) {
|
|
78
|
-
const
|
|
124
|
+
register(gitRemote, localPath, actionMeta = {}) {
|
|
125
|
+
const actionType = actionMeta.actionType || 'claude-code';
|
|
126
|
+
const key = this._makeKey(gitRemote, actionType);
|
|
127
|
+
const isNew = !(key in this._data.projects);
|
|
79
128
|
const now = new Date().toISOString();
|
|
80
129
|
|
|
81
130
|
if (isNew) {
|
|
82
|
-
this._data.projects[
|
|
131
|
+
this._data.projects[key] = {
|
|
132
|
+
gitRemote,
|
|
83
133
|
localPath,
|
|
134
|
+
actionType,
|
|
135
|
+
actionId: actionMeta.actionId || null,
|
|
136
|
+
actionName: actionMeta.actionName || null,
|
|
84
137
|
registeredAt: now,
|
|
85
138
|
lastUsed: now
|
|
86
139
|
};
|
|
87
140
|
} else {
|
|
88
|
-
this._data.projects[
|
|
89
|
-
this._data.projects[
|
|
141
|
+
this._data.projects[key].localPath = localPath;
|
|
142
|
+
this._data.projects[key].lastUsed = now;
|
|
143
|
+
if (actionMeta.actionId) this._data.projects[key].actionId = actionMeta.actionId;
|
|
144
|
+
if (actionMeta.actionName) this._data.projects[key].actionName = actionMeta.actionName;
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
// Set as default if first project
|
|
93
148
|
if (this._data.defaultProject === null) {
|
|
94
|
-
this._data.defaultProject =
|
|
149
|
+
this._data.defaultProject = key;
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
this._save();
|
|
@@ -99,16 +154,20 @@ class ProjectRegistry {
|
|
|
99
154
|
}
|
|
100
155
|
|
|
101
156
|
/**
|
|
102
|
-
* Get local path for a git remote.
|
|
157
|
+
* Get local path for a git remote (and optional action type).
|
|
103
158
|
* Updates lastUsed timestamp.
|
|
104
159
|
*
|
|
160
|
+
* Supports both:
|
|
161
|
+
* - getPath("github.com/user/repo", "claude-code") → exact match
|
|
162
|
+
* - getPath("github.com/user/repo") → first match for that repo (backward compat)
|
|
163
|
+
*
|
|
105
164
|
* @param {string} gitRemote - Normalized git remote
|
|
165
|
+
* @param {string} [actionType] - Optional action type for exact match
|
|
106
166
|
* @returns {string|null} Local path or null if not registered
|
|
107
167
|
*/
|
|
108
|
-
getPath(gitRemote) {
|
|
109
|
-
const project = this.
|
|
168
|
+
getPath(gitRemote, actionType) {
|
|
169
|
+
const project = this._findProject(gitRemote, actionType);
|
|
110
170
|
if (project) {
|
|
111
|
-
// Update last_used
|
|
112
171
|
project.lastUsed = new Date().toISOString();
|
|
113
172
|
this._save();
|
|
114
173
|
return project.localPath;
|
|
@@ -121,55 +180,126 @@ class ProjectRegistry {
|
|
|
121
180
|
* Useful for status checks and listing operations.
|
|
122
181
|
*
|
|
123
182
|
* @param {string} gitRemote - Normalized git remote
|
|
183
|
+
* @param {string} [actionType] - Optional action type
|
|
124
184
|
* @returns {string|null} Local path or null if not registered
|
|
125
185
|
*/
|
|
126
|
-
getPathWithoutUpdate(gitRemote) {
|
|
127
|
-
const project = this.
|
|
186
|
+
getPathWithoutUpdate(gitRemote, actionType) {
|
|
187
|
+
const project = this._findProject(gitRemote, actionType);
|
|
128
188
|
return project ? project.localPath : null;
|
|
129
189
|
}
|
|
130
190
|
|
|
131
191
|
/**
|
|
132
|
-
*
|
|
192
|
+
* Find a project entry by gitRemote and optional actionType.
|
|
193
|
+
* If actionType provided, tries exact composite key first.
|
|
194
|
+
* Falls back to scanning all entries for matching gitRemote.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} gitRemote
|
|
197
|
+
* @param {string} [actionType]
|
|
198
|
+
* @returns {Object|null}
|
|
199
|
+
*/
|
|
200
|
+
_findProject(gitRemote, actionType) {
|
|
201
|
+
// Try exact composite key first
|
|
202
|
+
if (actionType) {
|
|
203
|
+
const key = this._makeKey(gitRemote, actionType);
|
|
204
|
+
if (key in this._data.projects) {
|
|
205
|
+
return this._data.projects[key];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fall back to scanning for matching gitRemote (backward compat + no actionType case)
|
|
210
|
+
for (const [key, info] of Object.entries(this._data.projects)) {
|
|
211
|
+
// V2 format: check the stored gitRemote field
|
|
212
|
+
if (info.gitRemote === gitRemote) {
|
|
213
|
+
return info;
|
|
214
|
+
}
|
|
215
|
+
// V1 format fallback: key IS the gitRemote (pre-migration)
|
|
216
|
+
if (key === gitRemote) {
|
|
217
|
+
return info;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* List all registered projects (backward-compatible format).
|
|
225
|
+
* Returns unique gitRemote -> localPath mapping (first entry per remote wins).
|
|
133
226
|
*
|
|
134
227
|
* @returns {Object} Dict of gitRemote -> localPath
|
|
135
228
|
*/
|
|
136
229
|
listProjects() {
|
|
137
230
|
const result = {};
|
|
138
|
-
for (const [
|
|
139
|
-
|
|
231
|
+
for (const [_key, info] of Object.entries(this._data.projects)) {
|
|
232
|
+
const remote = info.gitRemote || _key;
|
|
233
|
+
// First entry per gitRemote wins (backward compat)
|
|
234
|
+
if (!(remote in result)) {
|
|
235
|
+
result[remote] = info.localPath;
|
|
236
|
+
}
|
|
140
237
|
}
|
|
141
238
|
return result;
|
|
142
239
|
}
|
|
143
240
|
|
|
144
241
|
/**
|
|
145
242
|
* List all registered projects with full metadata.
|
|
243
|
+
* V2 format: includes actionType, actionId, gitRemote per entry.
|
|
146
244
|
*
|
|
147
|
-
* @returns {Object} Dict of
|
|
245
|
+
* @returns {Object} Dict of compositeKey -> {gitRemote, localPath, actionType, ...}
|
|
148
246
|
*/
|
|
149
247
|
listProjectsWithMetadata() {
|
|
150
248
|
return { ...this._data.projects };
|
|
151
249
|
}
|
|
152
250
|
|
|
251
|
+
/**
|
|
252
|
+
* List all registered projects with action type info.
|
|
253
|
+
* Returns array of {gitRemote, localPath, actionType} for daemon routing.
|
|
254
|
+
*
|
|
255
|
+
* @returns {Array<{gitRemote: string, localPath: string, actionType: string}>}
|
|
256
|
+
*/
|
|
257
|
+
listProjectsWithActionType() {
|
|
258
|
+
const result = [];
|
|
259
|
+
for (const [_key, info] of Object.entries(this._data.projects)) {
|
|
260
|
+
result.push({
|
|
261
|
+
gitRemote: info.gitRemote || _key,
|
|
262
|
+
localPath: info.localPath,
|
|
263
|
+
actionType: info.actionType || 'claude-code',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
153
269
|
/**
|
|
154
270
|
* Unregister a project.
|
|
271
|
+
* Accepts either composite key ("remote::type") or plain gitRemote (removes all entries for that remote).
|
|
155
272
|
*
|
|
156
|
-
* @param {string}
|
|
273
|
+
* @param {string} keyOrRemote - Composite key or plain git remote
|
|
157
274
|
* @returns {boolean} True if was registered, false if not found
|
|
158
275
|
*/
|
|
159
|
-
unregister(
|
|
160
|
-
|
|
161
|
-
delete this._data.projects[gitRemote];
|
|
276
|
+
unregister(keyOrRemote) {
|
|
277
|
+
let found = false;
|
|
162
278
|
|
|
163
|
-
|
|
164
|
-
|
|
279
|
+
// Try as exact key first
|
|
280
|
+
if (keyOrRemote in this._data.projects) {
|
|
281
|
+
delete this._data.projects[keyOrRemote];
|
|
282
|
+
found = true;
|
|
283
|
+
} else {
|
|
284
|
+
// Remove all entries matching this gitRemote
|
|
285
|
+
for (const [key, info] of Object.entries(this._data.projects)) {
|
|
286
|
+
if ((info.gitRemote || key) === keyOrRemote) {
|
|
287
|
+
delete this._data.projects[key];
|
|
288
|
+
found = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (found) {
|
|
294
|
+
// Fix default if needed
|
|
295
|
+
if (this._data.defaultProject && !(this._data.defaultProject in this._data.projects)) {
|
|
165
296
|
const remaining = Object.keys(this._data.projects);
|
|
166
297
|
this._data.defaultProject = remaining.length > 0 ? remaining[0] : null;
|
|
167
298
|
}
|
|
168
|
-
|
|
169
299
|
this._save();
|
|
170
|
-
return true;
|
|
171
300
|
}
|
|
172
|
-
|
|
301
|
+
|
|
302
|
+
return found;
|
|
173
303
|
}
|
|
174
304
|
|
|
175
305
|
/**
|
|
@@ -183,16 +313,26 @@ class ProjectRegistry {
|
|
|
183
313
|
|
|
184
314
|
/**
|
|
185
315
|
* Set a project as the default.
|
|
316
|
+
* Accepts composite key or plain gitRemote (finds first match).
|
|
186
317
|
*
|
|
187
|
-
* @param {string}
|
|
318
|
+
* @param {string} keyOrRemote
|
|
188
319
|
* @returns {boolean} True if successful
|
|
189
320
|
*/
|
|
190
|
-
setDefaultProject(
|
|
191
|
-
|
|
192
|
-
|
|
321
|
+
setDefaultProject(keyOrRemote) {
|
|
322
|
+
// Exact key match
|
|
323
|
+
if (keyOrRemote in this._data.projects) {
|
|
324
|
+
this._data.defaultProject = keyOrRemote;
|
|
193
325
|
this._save();
|
|
194
326
|
return true;
|
|
195
327
|
}
|
|
328
|
+
// Find by gitRemote
|
|
329
|
+
for (const [key, info] of Object.entries(this._data.projects)) {
|
|
330
|
+
if ((info.gitRemote || key) === keyOrRemote) {
|
|
331
|
+
this._data.defaultProject = key;
|
|
332
|
+
this._save();
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
196
336
|
return false;
|
|
197
337
|
}
|
|
198
338
|
|
|
@@ -207,12 +347,14 @@ class ProjectRegistry {
|
|
|
207
347
|
|
|
208
348
|
/**
|
|
209
349
|
* Check if a project is registered.
|
|
350
|
+
* Checks both composite keys and plain gitRemote.
|
|
210
351
|
*
|
|
211
352
|
* @param {string} gitRemote
|
|
353
|
+
* @param {string} [actionType] - Optional action type for exact match
|
|
212
354
|
* @returns {boolean}
|
|
213
355
|
*/
|
|
214
|
-
isRegistered(gitRemote) {
|
|
215
|
-
return gitRemote
|
|
356
|
+
isRegistered(gitRemote, actionType) {
|
|
357
|
+
return this._findProject(gitRemote, actionType) !== null;
|
|
216
358
|
}
|
|
217
359
|
|
|
218
360
|
/**
|
|
@@ -223,19 +365,22 @@ class ProjectRegistry {
|
|
|
223
365
|
validatePaths() {
|
|
224
366
|
const invalid = [];
|
|
225
367
|
|
|
226
|
-
for (const [
|
|
368
|
+
for (const [key, info] of Object.entries(this._data.projects)) {
|
|
227
369
|
const path = info.localPath;
|
|
370
|
+
const gitRemote = info.gitRemote || key;
|
|
228
371
|
|
|
229
372
|
try {
|
|
230
373
|
const stats = statSync(path);
|
|
231
374
|
if (!stats.isDirectory()) {
|
|
232
375
|
invalid.push({
|
|
376
|
+
key,
|
|
233
377
|
gitRemote,
|
|
234
378
|
localPath: path,
|
|
235
379
|
reason: 'not_a_directory'
|
|
236
380
|
});
|
|
237
381
|
} else if (!existsSync(join(path, '.git'))) {
|
|
238
382
|
invalid.push({
|
|
383
|
+
key,
|
|
239
384
|
gitRemote,
|
|
240
385
|
localPath: path,
|
|
241
386
|
reason: 'not_a_git_repo'
|
|
@@ -243,6 +388,7 @@ class ProjectRegistry {
|
|
|
243
388
|
}
|
|
244
389
|
} catch {
|
|
245
390
|
invalid.push({
|
|
391
|
+
key,
|
|
246
392
|
gitRemote,
|
|
247
393
|
localPath: path,
|
|
248
394
|
reason: 'path_not_found'
|