tlc-claude-code 2.4.10 → 2.6.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/.claude/commands/tlc/autofix.md +34 -1
- package/.claude/commands/tlc/build.md +203 -27
- package/.claude/commands/tlc/ci.md +178 -414
- package/.claude/commands/tlc/coverage.md +34 -0
- package/.claude/commands/tlc/deploy.md +19 -6
- package/.claude/commands/tlc/discuss.md +34 -0
- package/.claude/commands/tlc/docs.md +35 -1
- package/.claude/commands/tlc/e2e.md +300 -0
- package/.claude/commands/tlc/edge-cases.md +35 -1
- package/.claude/commands/tlc/init.md +38 -8
- package/.claude/commands/tlc/issues.md +46 -0
- package/.claude/commands/tlc/new-project.md +46 -4
- package/.claude/commands/tlc/plan.md +76 -0
- package/.claude/commands/tlc/quick.md +33 -0
- package/.claude/commands/tlc/release.md +85 -135
- package/.claude/commands/tlc/restore.md +14 -0
- package/.claude/commands/tlc/review.md +80 -1
- package/.claude/commands/tlc/tlc.md +134 -0
- package/.claude/commands/tlc/verify.md +64 -65
- package/.claude/commands/tlc/watchci.md +10 -0
- package/.claude/hooks/tlc-block-tools.sh +13 -0
- package/.claude/hooks/tlc-session-init.sh +9 -0
- package/CODING-STANDARDS.md +35 -10
- package/package.json +1 -1
- package/server/lib/block-tools-hook.js +23 -0
- package/server/lib/e2e/acceptance-parser.js +132 -0
- package/server/lib/e2e/acceptance-parser.test.js +110 -0
- package/server/lib/e2e/framework-detector.js +47 -0
- package/server/lib/e2e/framework-detector.test.js +94 -0
- package/server/lib/e2e/log-assertions.js +107 -0
- package/server/lib/e2e/log-assertions.test.js +68 -0
- package/server/lib/e2e/test-generator.js +159 -0
- package/server/lib/e2e/test-generator.test.js +121 -0
- package/server/lib/e2e/verify-runner.js +191 -0
- package/server/lib/e2e/verify-runner.test.js +167 -0
- package/server/lib/github/config.js +458 -0
- package/server/lib/github/config.test.js +385 -0
- package/server/lib/github/gh-client.js +303 -0
- package/server/lib/github/gh-client.test.js +499 -0
- package/server/lib/github/gh-projects.js +594 -0
- package/server/lib/github/gh-projects.test.js +583 -0
- package/server/lib/github/index.js +19 -0
- package/server/lib/github/plan-sync.js +456 -0
- package/server/lib/github/plan-sync.test.js +805 -0
- package/server/lib/hooks/block-tools-hook.test.js +54 -0
- package/server/lib/orchestration/cli-dispatch.js +16 -1
- package/server/lib/orchestration/cli-dispatch.test.js +94 -8
- package/server/lib/orchestration/completion-checker.js +101 -0
- package/server/lib/orchestration/completion-checker.test.js +177 -0
- package/server/lib/orchestration/result-verifier.js +143 -0
- package/server/lib/orchestration/result-verifier.test.js +291 -0
- package/server/lib/orchestration/session-dispatcher.js +99 -0
- package/server/lib/orchestration/session-dispatcher.test.js +215 -0
- package/server/lib/orchestration/session-status.js +147 -0
- package/server/lib/orchestration/session-status.test.js +130 -0
- package/server/lib/release/agent-runner-updates.js +24 -0
- package/server/lib/release/agent-runner-updates.test.js +22 -0
- package/server/lib/release/changelog-generator.js +142 -0
- package/server/lib/release/changelog-generator.test.js +113 -0
- package/server/lib/release/ci-watcher.js +83 -0
- package/server/lib/release/ci-watcher.test.js +81 -0
- package/server/lib/release/health-checker.js +111 -0
- package/server/lib/release/health-checker.test.js +121 -0
- package/server/lib/release/release-pipeline.js +187 -0
- package/server/lib/release/release-pipeline.test.js +262 -0
- package/server/lib/release/version-bumper.js +183 -0
- package/server/lib/release/version-bumper.test.js +142 -0
- package/server/lib/routing-preamble.integration.test.js +12 -0
- package/server/lib/routing-preamble.js +13 -2
- package/server/lib/routing-preamble.test.js +49 -0
- package/server/lib/scaffolding/ci-detector.js +139 -0
- package/server/lib/scaffolding/ci-detector.test.js +198 -0
- package/server/lib/scaffolding/ci-scaffolder.js +347 -0
- package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
- package/server/lib/scaffolding/deploy-detector.js +135 -0
- package/server/lib/scaffolding/deploy-detector.test.js +106 -0
- package/server/lib/scaffolding/health-scaffold.js +374 -0
- package/server/lib/scaffolding/health-scaffold.test.js +99 -0
- package/server/lib/scaffolding/logger-scaffold.js +196 -0
- package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
- package/server/lib/scaffolding/migration-detector.js +78 -0
- package/server/lib/scaffolding/migration-detector.test.js +127 -0
- package/server/lib/scaffolding/snapshot-manager.js +142 -0
- package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
- package/server/lib/task-router-config.js +50 -20
- package/server/lib/task-router-config.test.js +29 -15
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Config — configuration management, setup detection, and offline queue
|
|
3
|
+
* Phase 97 Task 5
|
|
4
|
+
*
|
|
5
|
+
* All functions accept `{ fs }` for dependency injection,
|
|
6
|
+
* defaulting to the Node.js `fs` module.
|
|
7
|
+
*
|
|
8
|
+
* Error handling: no silent failures. Every catch logs with context or rethrows.
|
|
9
|
+
*
|
|
10
|
+
* @module github/config
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const nodeFs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Paths
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const TLC_JSON = '.tlc.json';
|
|
21
|
+
const QUEUE_DIR = '.tlc';
|
|
22
|
+
const QUEUE_FILE = '.github-sync-queue.json';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the .tlc.json path for a project directory.
|
|
26
|
+
* @param {string} projectDir
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function tlcJsonPath(projectDir) {
|
|
30
|
+
return path.join(projectDir, TLC_JSON);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the sync queue file path.
|
|
35
|
+
* @param {string} projectDir
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
function queueFilePath(projectDir) {
|
|
39
|
+
return path.join(projectDir, QUEUE_DIR, QUEUE_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// getDefaultConfig
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the default GitHub integration config object.
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
function getDefaultConfig() {
|
|
51
|
+
return {
|
|
52
|
+
autoSync: true,
|
|
53
|
+
phasePrefix: 'Phase',
|
|
54
|
+
sprintField: 'Sprint',
|
|
55
|
+
statusField: 'Status',
|
|
56
|
+
statusMapping: {
|
|
57
|
+
todo: 'Backlog',
|
|
58
|
+
in_progress: 'In progress',
|
|
59
|
+
in_review: 'In review',
|
|
60
|
+
done: 'Done',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// loadGitHubConfig
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read `.tlc.json` from projectDir and return the `github` section.
|
|
71
|
+
* Validates required fields and collects warnings.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
* @param {object} [options.fs] - Injected fs module
|
|
76
|
+
* @returns {{ config: object|null, warnings: string[] }}
|
|
77
|
+
*/
|
|
78
|
+
function loadGitHubConfig(projectDir, { fs = nodeFs } = {}) {
|
|
79
|
+
const filePath = tlcJsonPath(projectDir);
|
|
80
|
+
const warnings = [];
|
|
81
|
+
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.code === 'ENOENT') {
|
|
87
|
+
return { config: null, warnings: [] };
|
|
88
|
+
}
|
|
89
|
+
console.error(`[TLC] loadGitHubConfig: failed to read ${filePath}: ${err.message}`);
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[TLC] loadGitHubConfig: invalid JSON in ${filePath}: ${err.message}`);
|
|
98
|
+
return { config: null, warnings: [`Invalid JSON in ${TLC_JSON}: ${err.message}`] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!parsed.github) {
|
|
102
|
+
return { config: null, warnings: [] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const config = parsed.github;
|
|
106
|
+
|
|
107
|
+
// Validate expected fields
|
|
108
|
+
if (!config.project) {
|
|
109
|
+
warnings.push('Missing "project" field in github config — GitHub Projects integration will not work');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { config, warnings };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// isGitHubEnabled
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Quick synchronous check: does `.tlc.json` have `github.autoSync` set to true?
|
|
121
|
+
* Designed to be fast (<1ms). Returns false on any error.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
124
|
+
* @param {object} [options]
|
|
125
|
+
* @param {object} [options.fs] - Injected fs module
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
function isGitHubEnabled(projectDir, { fs = nodeFs } = {}) {
|
|
129
|
+
const filePath = tlcJsonPath(projectDir);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (!fs.existsSync(filePath)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
return parsed.github?.autoSync === true;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(`[TLC] isGitHubEnabled: error reading ${filePath}: ${err.message}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// detectAndSuggestConfig
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Auto-detection flow for `/tlc:issues setup`.
|
|
150
|
+
*
|
|
151
|
+
* 1. Calls ghClient.detectRepo() to get owner/repo
|
|
152
|
+
* 2. Calls ghClient.checkAuth() to verify auth and scopes
|
|
153
|
+
* 3. If missing `project` scope, returns `{ needsScope, command }`
|
|
154
|
+
* 4. Tries to find a matching GitHub Project (by repo name or org)
|
|
155
|
+
* 5. Returns `{ owner, repo, project, fields, suggestedConfig }`
|
|
156
|
+
*
|
|
157
|
+
* @param {object} params
|
|
158
|
+
* @param {object} params.ghClient - GitHub client (detectRepo, checkAuth)
|
|
159
|
+
* @param {object} params.ghProjects - GitHub Projects client (discoverProject)
|
|
160
|
+
* @param {string} params.projectDir - Absolute path to the project root
|
|
161
|
+
* @param {object} [params.fs] - Injected fs module
|
|
162
|
+
* @returns {object} Detection result
|
|
163
|
+
*/
|
|
164
|
+
function detectAndSuggestConfig({ ghClient, ghProjects, projectDir, fs = nodeFs }) {
|
|
165
|
+
// Step 1: Detect repo
|
|
166
|
+
const repoInfo = ghClient.detectRepo();
|
|
167
|
+
if (!repoInfo) {
|
|
168
|
+
return { error: 'Could not detect a GitHub repo in this directory. Run from a git repository with a GitHub remote.' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { owner, repo } = repoInfo;
|
|
172
|
+
|
|
173
|
+
// Step 2: Check auth + scopes
|
|
174
|
+
const authResult = ghClient.checkAuth();
|
|
175
|
+
if (!authResult.authenticated) {
|
|
176
|
+
return { error: `GitHub CLI not authenticated: ${authResult.error || 'unknown'}`, code: authResult.code };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const scopes = authResult.scopes || [];
|
|
180
|
+
if (!scopes.includes('project')) {
|
|
181
|
+
return {
|
|
182
|
+
needsScope: true,
|
|
183
|
+
command: 'gh auth refresh -s project',
|
|
184
|
+
owner,
|
|
185
|
+
repo,
|
|
186
|
+
message: 'Missing "project" scope. Run the command below to add it.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 3: Try to discover a matching GitHub Project
|
|
191
|
+
let project = null;
|
|
192
|
+
let fields = [];
|
|
193
|
+
|
|
194
|
+
// Try by org first, then by user
|
|
195
|
+
const discovered = ghProjects.discoverProject({ org: owner, projectTitle: repo });
|
|
196
|
+
if (discovered && !discovered.error) {
|
|
197
|
+
project = discovered;
|
|
198
|
+
fields = discovered.fields || [];
|
|
199
|
+
} else {
|
|
200
|
+
// Try as user
|
|
201
|
+
const discoveredUser = ghProjects.discoverProject({ user: owner, projectTitle: repo });
|
|
202
|
+
if (discoveredUser && !discoveredUser.error) {
|
|
203
|
+
project = discoveredUser;
|
|
204
|
+
fields = discoveredUser.fields || [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Step 4: Build suggested config
|
|
209
|
+
const defaults = getDefaultConfig();
|
|
210
|
+
const suggestedConfig = {
|
|
211
|
+
...defaults,
|
|
212
|
+
owner,
|
|
213
|
+
repo,
|
|
214
|
+
project: project ? project.title : null,
|
|
215
|
+
projectId: project ? project.projectId : null,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
owner,
|
|
220
|
+
repo,
|
|
221
|
+
project,
|
|
222
|
+
fields,
|
|
223
|
+
suggestedConfig,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// writeGitHubConfig
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Read existing `.tlc.json`, add/update the `github` key, write back.
|
|
233
|
+
* Preserves all other configuration.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
236
|
+
* @param {object} githubConfig - The github config object to write
|
|
237
|
+
* @param {object} [options]
|
|
238
|
+
* @param {object} [options.fs] - Injected fs module
|
|
239
|
+
* @returns {{ written: boolean }}
|
|
240
|
+
*/
|
|
241
|
+
function writeGitHubConfig(projectDir, githubConfig, { fs = nodeFs } = {}) {
|
|
242
|
+
const filePath = tlcJsonPath(projectDir);
|
|
243
|
+
|
|
244
|
+
let existing = {};
|
|
245
|
+
try {
|
|
246
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
247
|
+
existing = JSON.parse(raw);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err.code !== 'ENOENT') {
|
|
250
|
+
console.error(`[TLC] writeGitHubConfig: error reading ${filePath}: ${err.message}`);
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
// File doesn't exist yet, start from empty object
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
existing.github = githubConfig;
|
|
257
|
+
|
|
258
|
+
fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
|
259
|
+
return { written: true };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// queueSyncAction
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Append an action to the offline sync queue.
|
|
268
|
+
* Creates the queue file and directory if they don't exist.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
271
|
+
* @param {object} action - `{ type, payload, timestamp }`
|
|
272
|
+
* @param {object} [options]
|
|
273
|
+
* @param {object} [options.fs] - Injected fs module
|
|
274
|
+
*/
|
|
275
|
+
function queueSyncAction(projectDir, action, { fs = nodeFs } = {}) {
|
|
276
|
+
const dirPath = path.join(projectDir, QUEUE_DIR);
|
|
277
|
+
const filePath = queueFilePath(projectDir);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// Ensure directory exists
|
|
281
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(`[TLC] queueSyncAction: cannot create queue dir ${dirPath}: ${err.message}`);
|
|
284
|
+
return { error: `Cannot create queue directory: ${err.message}`, code: 'QUEUE_WRITE_ERROR' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Load existing queue
|
|
288
|
+
let queue = [];
|
|
289
|
+
try {
|
|
290
|
+
if (fs.existsSync(filePath)) {
|
|
291
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
292
|
+
queue = JSON.parse(raw);
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (err.code === 'ENOENT') {
|
|
296
|
+
queue = [];
|
|
297
|
+
} else {
|
|
298
|
+
console.error(`[TLC] queueSyncAction: error reading queue at ${filePath}: ${err.message}. Refusing to overwrite.`);
|
|
299
|
+
return { error: 'Queue file corrupted', code: 'QUEUE_CORRUPT' };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
queue.push(action);
|
|
304
|
+
try {
|
|
305
|
+
fs.writeFileSync(filePath, JSON.stringify(queue, null, 2), 'utf-8');
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(`[TLC] queueSyncAction: cannot write queue at ${filePath}: ${err.message}`);
|
|
308
|
+
return { error: `Cannot write queue file: ${err.message}`, code: 'QUEUE_WRITE_ERROR' };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// loadSyncQueue
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Read and return the sync queue array, or empty array if no file exists.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
320
|
+
* @param {object} [options]
|
|
321
|
+
* @param {object} [options.fs] - Injected fs module
|
|
322
|
+
* @returns {Array | { queue: Array, error: string }}
|
|
323
|
+
*/
|
|
324
|
+
function loadSyncQueue(projectDir, { fs = nodeFs } = {}) {
|
|
325
|
+
const filePath = queueFilePath(projectDir);
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
if (!fs.existsSync(filePath)) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
332
|
+
return JSON.parse(raw);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.error(`[TLC] loadSyncQueue: error reading queue at ${filePath}: ${err.message}`);
|
|
335
|
+
return { queue: [], error: 'Queue file corrupted or unreadable' };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// flushSyncQueue
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Replay action dispatcher — maps queue action types to client calls.
|
|
345
|
+
* @param {object} action - Queue action `{ type, payload }`
|
|
346
|
+
* @param {object} ghClient - GitHub client
|
|
347
|
+
* @param {object} ghProjects - GitHub Projects client
|
|
348
|
+
* @returns {{ success: boolean, result?: object }}
|
|
349
|
+
*/
|
|
350
|
+
function replayAction(action, ghClient, ghProjects) {
|
|
351
|
+
const { type, payload } = action;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
let result;
|
|
355
|
+
switch (type) {
|
|
356
|
+
case 'create_issue':
|
|
357
|
+
result = ghClient.createIssue(payload);
|
|
358
|
+
break;
|
|
359
|
+
case 'close_issue':
|
|
360
|
+
result = ghClient.closeIssue(payload);
|
|
361
|
+
break;
|
|
362
|
+
case 'assign_issue':
|
|
363
|
+
result = ghClient.assignIssue(payload);
|
|
364
|
+
break;
|
|
365
|
+
case 'add_labels':
|
|
366
|
+
result = ghClient.addLabels(payload);
|
|
367
|
+
break;
|
|
368
|
+
case 'add_to_project':
|
|
369
|
+
result = ghProjects.addItemToProject(payload);
|
|
370
|
+
break;
|
|
371
|
+
case 'set_field':
|
|
372
|
+
result = ghProjects.setFieldValue(payload);
|
|
373
|
+
break;
|
|
374
|
+
default:
|
|
375
|
+
console.error(`[TLC] flushSyncQueue: unknown action type "${type}"`);
|
|
376
|
+
return { success: false };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check for structured error responses
|
|
380
|
+
if (result && result.error) {
|
|
381
|
+
console.error(`[TLC] flushSyncQueue: action "${type}" failed: ${result.error} (code: ${result.code})`);
|
|
382
|
+
return { success: false, result };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { success: true, result };
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`[TLC] flushSyncQueue: action "${type}" threw: ${err.message}`);
|
|
388
|
+
return { success: false };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Read queue, replay each action, remove successful ones, keep failed ones.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
396
|
+
* @param {object} params
|
|
397
|
+
* @param {object} params.ghClient - GitHub client
|
|
398
|
+
* @param {object} params.ghProjects - GitHub Projects client
|
|
399
|
+
* @param {object} [params.fs] - Injected fs module
|
|
400
|
+
* @returns {{ flushed: number, failed: number, remaining: number }}
|
|
401
|
+
*/
|
|
402
|
+
function flushSyncQueue(projectDir, { ghClient, ghProjects, fs = nodeFs }) {
|
|
403
|
+
const loaded = loadSyncQueue(projectDir, { fs });
|
|
404
|
+
|
|
405
|
+
// loadSyncQueue returns an array on success, or { queue, error } on corruption
|
|
406
|
+
if (loaded && loaded.error) {
|
|
407
|
+
console.error(`[TLC] flushSyncQueue: ${loaded.error}`);
|
|
408
|
+
return { flushed: 0, failed: 0, remaining: 0, error: loaded.error };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const queue = loaded;
|
|
412
|
+
|
|
413
|
+
if (queue.length === 0) {
|
|
414
|
+
return { flushed: 0, failed: 0, remaining: 0 };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let flushed = 0;
|
|
418
|
+
let failed = 0;
|
|
419
|
+
const remaining = [];
|
|
420
|
+
|
|
421
|
+
for (const action of queue) {
|
|
422
|
+
const { success } = replayAction(action, ghClient, ghProjects);
|
|
423
|
+
if (success) {
|
|
424
|
+
flushed++;
|
|
425
|
+
} else {
|
|
426
|
+
failed++;
|
|
427
|
+
remaining.push(action);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Write back remaining (failed) actions
|
|
432
|
+
const filePath = queueFilePath(projectDir);
|
|
433
|
+
const dirPath = path.join(projectDir, QUEUE_DIR);
|
|
434
|
+
try {
|
|
435
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
436
|
+
fs.writeFileSync(filePath, JSON.stringify(remaining, null, 2), 'utf-8');
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error(`[TLC] flushSyncQueue: cannot write remaining queue at ${filePath}: ${err.message}`);
|
|
439
|
+
return { flushed, failed, remaining: remaining.length, error: `Cannot write queue file: ${err.message}` };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { flushed, failed, remaining: remaining.length };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Exports
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
loadGitHubConfig,
|
|
451
|
+
isGitHubEnabled,
|
|
452
|
+
detectAndSuggestConfig,
|
|
453
|
+
writeGitHubConfig,
|
|
454
|
+
getDefaultConfig,
|
|
455
|
+
queueSyncAction,
|
|
456
|
+
flushSyncQueue,
|
|
457
|
+
loadSyncQueue,
|
|
458
|
+
};
|