draftgo-cli 3.0.48 → 3.0.51
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/README.md +16 -2
- package/package.json +5 -4
- package/resources/skill/SKILL.md +11 -2
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/checkout.md +8 -1
- package/resources/skill/references/custom-services.md +1 -0
- package/src/cli.js +34 -9
- package/src/commandRegistry.js +8 -1
- package/src/commands/api.js +102 -0
- package/src/commands/autoPush.js +1 -1
- package/src/commands/check.js +79 -2
- package/src/commands/checkout.js +3 -0
- package/src/commands/clean.js +46 -0
- package/src/commands/commit.js +34 -5
- package/src/commands/conflict.js +5 -3
- package/src/commands/conflicts.js +2 -1
- package/src/commands/context.js +2 -2
- package/src/commands/customService.js +81 -0
- package/src/commands/deploy.js +1 -1
- package/src/commands/diff.js +3 -0
- package/src/commands/help.js +37 -6
- package/src/commands/map.js +31 -22
- package/src/commands/mcp.js +26 -3
- package/src/commands/reconcile.js +34 -0
- package/src/commands/task.js +413 -0
- package/src/commands/verifyUi.js +96 -12
- package/src/context/index.js +28 -12
- package/src/customServices.js +246 -0
- package/src/mcp/client.js +52 -19
- package/src/projectMap.js +7 -2
- package/src/runtimeFiles.js +44 -0
- package/src/workspaceHealth.js +33 -0
- package/src/worktree/index.js +272 -59
- package/src/worktree/status.js +122 -0
package/src/worktree/index.js
CHANGED
|
@@ -15,7 +15,14 @@ const {
|
|
|
15
15
|
resourceFileName,
|
|
16
16
|
safeIdSegment,
|
|
17
17
|
} = require('./types');
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
streamToFiles,
|
|
20
|
+
hashFile,
|
|
21
|
+
copyFileAtomic,
|
|
22
|
+
writeJsonAtomic,
|
|
23
|
+
normalizeSha256,
|
|
24
|
+
tempPathFor,
|
|
25
|
+
} = require('./streams');
|
|
19
26
|
const {
|
|
20
27
|
loadManifest,
|
|
21
28
|
getEntry,
|
|
@@ -24,6 +31,7 @@ const {
|
|
|
24
31
|
saveManifest,
|
|
25
32
|
} = require('./manifest');
|
|
26
33
|
const { validateContentFile } = require('./validate');
|
|
34
|
+
const { inspectEntry, openMetadataSession, sameVersion } = require('./status');
|
|
27
35
|
|
|
28
36
|
const CONFLICT_SCHEMA_VERSION = 1;
|
|
29
37
|
|
|
@@ -248,36 +256,148 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
|
|
|
248
256
|
const session = await createSession(config, options);
|
|
249
257
|
const manifest = loadManifest(projectDir);
|
|
250
258
|
const results = [];
|
|
259
|
+
const plans = [];
|
|
260
|
+
const preflightFailures = [];
|
|
261
|
+
|
|
262
|
+
const report = (result) => {
|
|
263
|
+
results.push(result);
|
|
264
|
+
if (typeof options.onStatus === 'function') options.onStatus(result);
|
|
265
|
+
};
|
|
251
266
|
|
|
267
|
+
// Resolve and validate every target before the first remote write. This prevents
|
|
268
|
+
// a late local or stale-version error from causing an avoidable partial batch.
|
|
252
269
|
for (const resourceId of ids) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
try {
|
|
271
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
272
|
+
if (!entry) {
|
|
273
|
+
throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${canonical} ${resourceId} is not checked out.`);
|
|
274
|
+
}
|
|
275
|
+
if (entry.server !== config.server) {
|
|
276
|
+
throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
277
|
+
}
|
|
278
|
+
if (unresolvedConflict(projectDir, canonical, resourceId)) {
|
|
279
|
+
throw new WorktreeError(
|
|
280
|
+
'UNRESOLVED_CONFLICT',
|
|
281
|
+
`${canonical} ${resourceId} has an unresolved conflict; resolve it before committing.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const localPath = absolutePath(projectDir, entry.local_path);
|
|
286
|
+
const current = await hashFile(localPath);
|
|
287
|
+
const basePath = absolutePath(projectDir, entry.base_path);
|
|
288
|
+
const base = await hashFile(basePath);
|
|
289
|
+
if (base.hash !== entry.base_hash) {
|
|
290
|
+
throw new WorktreeError(
|
|
291
|
+
'BASE_HASH_MISMATCH',
|
|
292
|
+
`${canonical} ${resourceId} base hash differs from its manifest; run draftgo check --remote.`,
|
|
293
|
+
{ manifest_hash: entry.base_hash, base_hash: base.hash },
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (current.hash !== entry.base_hash) validateContentFile(localPath, entry.content_type);
|
|
297
|
+
|
|
298
|
+
const fresh = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
299
|
+
...options,
|
|
300
|
+
...session,
|
|
301
|
+
clientInitialized: true,
|
|
302
|
+
});
|
|
303
|
+
assertMetadataIdentity(fresh, canonical, resourceId);
|
|
304
|
+
const remoteMatchesBase = fresh.content_hash === entry.base_hash && sameVersion(entry, fresh);
|
|
305
|
+
if (!remoteMatchesBase) {
|
|
306
|
+
const state = await inspectEntry(projectDir, entry, fresh);
|
|
307
|
+
throw new WorktreeError(
|
|
308
|
+
state.local_matches_remote ? 'COMMITTED_UNRECORDED' : 'REMOTE_VERSION_CHANGED',
|
|
309
|
+
state.local_matches_remote
|
|
310
|
+
? `${canonical} ${resourceId} already equals remote; run ${state.recommendation}.`
|
|
311
|
+
: `${canonical} ${resourceId} changed remotely; inspect with draftgo check --remote before committing.`,
|
|
312
|
+
state,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
plans.push({ resourceId, entry, localPath, current, fresh });
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error.code === 'REMOTE_VERSION_CHANGED') {
|
|
318
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
319
|
+
try {
|
|
320
|
+
const conflict = await writeConflict(projectDir, entry, error, config, backend, options, session);
|
|
321
|
+
error = new WorktreeError(
|
|
322
|
+
'RESOURCE_VERSION_CONFLICT',
|
|
323
|
+
`${canonical} ${resourceId} changed remotely; conflict materials were preserved during preflight.`,
|
|
324
|
+
conflict,
|
|
325
|
+
);
|
|
326
|
+
} catch (conflictError) {
|
|
327
|
+
error = conflictError;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
preflightFailures.push({
|
|
331
|
+
resource_type: canonical,
|
|
332
|
+
resource_id: resourceId,
|
|
333
|
+
status: 'failed',
|
|
334
|
+
phase: 'preflight',
|
|
335
|
+
code: error.code || 'PREFLIGHT_FAILED',
|
|
336
|
+
message: error.message,
|
|
337
|
+
details: error.details || {},
|
|
338
|
+
});
|
|
259
339
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (preflightFailures.length) {
|
|
343
|
+
const failedIds = new Set(preflightFailures.map((item) => String(item.resource_id)));
|
|
344
|
+
const notStarted = ids.filter((id) => !failedIds.has(String(id))).map((resourceId) => ({
|
|
345
|
+
resource_type: canonical,
|
|
346
|
+
resource_id: resourceId,
|
|
347
|
+
status: 'not_started',
|
|
348
|
+
phase: 'preflight',
|
|
349
|
+
}));
|
|
350
|
+
for (const result of [...preflightFailures, ...notStarted]) report(result);
|
|
351
|
+
if (ids.length === 1 && preflightFailures.length === 1) {
|
|
352
|
+
const original = new WorktreeError(
|
|
353
|
+
preflightFailures[0].code,
|
|
354
|
+
preflightFailures[0].message,
|
|
355
|
+
preflightFailures[0].details,
|
|
264
356
|
);
|
|
357
|
+
throw original;
|
|
265
358
|
}
|
|
359
|
+
throw new WorktreeError(
|
|
360
|
+
'BATCH_PREFLIGHT_FAILED',
|
|
361
|
+
`Commit preflight failed for ${preflightFailures.length} resource(s); no remote content was changed.`,
|
|
362
|
+
{ completed: [], failed: preflightFailures, not_started: notStarted },
|
|
363
|
+
);
|
|
364
|
+
}
|
|
266
365
|
|
|
267
|
-
|
|
268
|
-
const
|
|
366
|
+
function throwBatchFailure(error, index, resourceId, remoteChangePossible = false) {
|
|
367
|
+
const failed = {
|
|
368
|
+
resource_type: canonical,
|
|
369
|
+
resource_id: resourceId,
|
|
370
|
+
status: 'failed',
|
|
371
|
+
phase: 'upload',
|
|
372
|
+
code: error.code || 'COMMIT_FAILED',
|
|
373
|
+
message: error.message,
|
|
374
|
+
remote_change_possible: remoteChangePossible,
|
|
375
|
+
};
|
|
376
|
+
const notStarted = plans.slice(index + 1).map((plan) => ({
|
|
377
|
+
resource_type: canonical,
|
|
378
|
+
resource_id: plan.resourceId,
|
|
379
|
+
status: 'not_started',
|
|
380
|
+
phase: 'upload',
|
|
381
|
+
}));
|
|
382
|
+
report(failed);
|
|
383
|
+
for (const item of notStarted) report(item);
|
|
384
|
+
error.details = {
|
|
385
|
+
...(error.details || {}),
|
|
386
|
+
batch: {
|
|
387
|
+
completed: results.filter((item) => ['committed', 'unchanged'].includes(item.status)),
|
|
388
|
+
failed: [failed],
|
|
389
|
+
not_started: notStarted,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (let index = 0; index < plans.length; index += 1) {
|
|
396
|
+
const { resourceId, entry, localPath, current, fresh } = plans[index];
|
|
269
397
|
if (current.hash === entry.base_hash) {
|
|
270
|
-
|
|
398
|
+
report({ resource_type: canonical, resource_id: resourceId, status: 'unchanged', hash: current.hash });
|
|
271
399
|
continue;
|
|
272
400
|
}
|
|
273
|
-
validateContentFile(localPath, entry.content_type);
|
|
274
|
-
|
|
275
|
-
const fresh = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
276
|
-
...options,
|
|
277
|
-
...session,
|
|
278
|
-
clientInitialized: true,
|
|
279
|
-
});
|
|
280
|
-
assertMetadataIdentity(fresh, canonical, resourceId);
|
|
281
401
|
const commitMetadata = {
|
|
282
402
|
...fresh,
|
|
283
403
|
content_type: entry.content_type,
|
|
@@ -290,55 +410,147 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
|
|
|
290
410
|
try {
|
|
291
411
|
responsePayload = await backend.commit(config, commitMetadata, localPath, current, options);
|
|
292
412
|
} catch (error) {
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
413
|
+
let failure = error;
|
|
414
|
+
const versionConflict = isVersionConflict(error);
|
|
415
|
+
if (versionConflict) {
|
|
416
|
+
const conflict = await writeConflict(projectDir, entry, error, config, backend, options, session);
|
|
417
|
+
failure = new WorktreeError(
|
|
418
|
+
'RESOURCE_VERSION_CONFLICT',
|
|
419
|
+
`${canonical} ${resourceId} changed remotely; conflict materials were preserved.`,
|
|
420
|
+
conflict,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
throwBatchFailure(failure, index, resourceId, !versionConflict);
|
|
300
424
|
}
|
|
301
425
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
426
|
+
try {
|
|
427
|
+
let committed = commitVersion(normalizeCommitPayload(responsePayload));
|
|
428
|
+
if (!committed.hash || (committed.version == null && committed.revision == null)) {
|
|
429
|
+
const confirmed = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
430
|
+
...options,
|
|
431
|
+
...session,
|
|
432
|
+
clientInitialized: true,
|
|
433
|
+
});
|
|
434
|
+
assertMetadataIdentity(confirmed, canonical, resourceId);
|
|
435
|
+
committed = {
|
|
436
|
+
version: confirmed.base_version,
|
|
437
|
+
revision: confirmed.base_revision,
|
|
438
|
+
etag: confirmed.etag,
|
|
439
|
+
hash: confirmed.content_hash,
|
|
440
|
+
updated_at: confirmed.updated_at,
|
|
441
|
+
updated_by: confirmed.updated_by,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (committed.hash && committed.hash !== current.hash) {
|
|
445
|
+
throw new WorktreeError(
|
|
446
|
+
'HASH_MISMATCH',
|
|
447
|
+
'DraftGo commit response hash differs from the uploaded raw bytes; manifest was not advanced.',
|
|
448
|
+
{ local_hash: current.hash, remote_hash: committed.hash },
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
await copyFileAtomic(localPath, absolutePath(projectDir, entry.base_path), { expectedHash: current.hash });
|
|
453
|
+
entry.base_hash = current.hash;
|
|
454
|
+
entry.content_size = current.size;
|
|
455
|
+
entry.base_version = committed.version;
|
|
456
|
+
entry.base_revision = committed.revision;
|
|
457
|
+
entry.base_etag = committed.etag;
|
|
458
|
+
entry.updated_at = committed.updated_at;
|
|
459
|
+
entry.updated_by = committed.updated_by;
|
|
460
|
+
entry.committed_at = new Date().toISOString();
|
|
461
|
+
await saveManifest(projectDir, manifest);
|
|
462
|
+
report({
|
|
463
|
+
resource_type: canonical,
|
|
464
|
+
resource_id: resourceId,
|
|
465
|
+
status: 'committed',
|
|
466
|
+
hash: current.hash,
|
|
467
|
+
base_version: entry.base_version,
|
|
468
|
+
base_revision: entry.base_revision,
|
|
308
469
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throwBatchFailure(error, index, resourceId, true);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return results;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function reconcileResources(projectDir, resourceType, resourceIds, options = {}) {
|
|
478
|
+
const canonical = canonicalResourceType(resourceType);
|
|
479
|
+
const ids = ensureIds(resourceIds);
|
|
480
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
481
|
+
const backend = backendFor(options);
|
|
482
|
+
const manifest = loadManifest(projectDir);
|
|
483
|
+
const session = options.backend && typeof options.backend.resolveMetadata === 'function'
|
|
484
|
+
? { client: options.client || {}, tools: options.tools || [] }
|
|
485
|
+
: await openMetadataSession(config, options);
|
|
486
|
+
const results = [];
|
|
487
|
+
|
|
488
|
+
for (const resourceId of ids) {
|
|
489
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
490
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${canonical} ${resourceId} is not checked out.`);
|
|
491
|
+
if (entry.server !== config.server) {
|
|
492
|
+
throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
318
493
|
}
|
|
319
|
-
if (
|
|
494
|
+
if (unresolvedConflict(projectDir, canonical, resourceId)) {
|
|
320
495
|
throw new WorktreeError(
|
|
321
|
-
'
|
|
322
|
-
|
|
323
|
-
|
|
496
|
+
'UNRESOLVED_CONFLICT',
|
|
497
|
+
`${canonical} ${resourceId} has an unresolved conflict; use conflict resolve instead of reconcile.`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const remote = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
501
|
+
...options, ...session, clientInitialized: true,
|
|
502
|
+
});
|
|
503
|
+
assertMetadataIdentity(remote, canonical, resourceId);
|
|
504
|
+
const status = await inspectEntry(projectDir, entry, remote);
|
|
505
|
+
if (!status.local_matches_remote) {
|
|
506
|
+
throw new WorktreeError(
|
|
507
|
+
'RECONCILE_UNSAFE',
|
|
508
|
+
`${canonical} ${resourceId} local content does not equal remote; refusing to advance metadata.`,
|
|
509
|
+
status,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (entry.content_type !== remote.content_type || entry.file_extension !== remote.file_extension) {
|
|
513
|
+
throw new WorktreeError(
|
|
514
|
+
'RECONCILE_FORMAT_CHANGED',
|
|
515
|
+
`${canonical} ${resourceId} remote content format changed; use checkout after preserving local work.`,
|
|
324
516
|
);
|
|
325
517
|
}
|
|
326
518
|
|
|
327
|
-
await
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
519
|
+
const response = await backend.download(config, remote, options);
|
|
520
|
+
const basePath = absolutePath(projectDir, entry.base_path);
|
|
521
|
+
const verifiedRemote = tempPathFor(basePath);
|
|
522
|
+
let verified;
|
|
523
|
+
try {
|
|
524
|
+
verified = await streamToFiles(response.body, [verifiedRemote], {
|
|
525
|
+
expectedHash: remote.content_hash,
|
|
526
|
+
expectedSize: remote.content_size,
|
|
527
|
+
});
|
|
528
|
+
const localAfterDownload = await hashFile(absolutePath(projectDir, entry.local_path));
|
|
529
|
+
if (localAfterDownload.hash !== verified.hash) {
|
|
530
|
+
throw new WorktreeError(
|
|
531
|
+
'RECONCILE_LOCAL_CHANGED',
|
|
532
|
+
`${canonical} ${resourceId} local content changed during reconcile; manifest was not advanced.`,
|
|
533
|
+
{ local_hash: localAfterDownload.hash, remote_hash: verified.hash },
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
await copyFileAtomic(verifiedRemote, basePath, { expectedHash: verified.hash });
|
|
537
|
+
} finally {
|
|
538
|
+
await fs.promises.rm(verifiedRemote, { force: true }).catch(() => {});
|
|
539
|
+
}
|
|
540
|
+
entry.base_hash = verified.hash;
|
|
541
|
+
entry.content_size = verified.size;
|
|
542
|
+
entry.base_version = remote.base_version;
|
|
543
|
+
entry.base_revision = remote.base_revision;
|
|
544
|
+
entry.base_etag = remote.etag;
|
|
545
|
+
entry.updated_at = remote.updated_at;
|
|
546
|
+
entry.updated_by = remote.updated_by;
|
|
547
|
+
entry.reconciled_at = new Date().toISOString();
|
|
336
548
|
await saveManifest(projectDir, manifest);
|
|
337
549
|
results.push({
|
|
338
550
|
resource_type: canonical,
|
|
339
551
|
resource_id: resourceId,
|
|
340
|
-
status: '
|
|
341
|
-
hash:
|
|
552
|
+
status: 'reconciled',
|
|
553
|
+
hash: verified.hash,
|
|
342
554
|
base_version: entry.base_version,
|
|
343
555
|
base_revision: entry.base_revision,
|
|
344
556
|
});
|
|
@@ -454,6 +666,7 @@ module.exports = {
|
|
|
454
666
|
conflictPaths,
|
|
455
667
|
checkoutResources,
|
|
456
668
|
commitResources,
|
|
669
|
+
reconcileResources,
|
|
457
670
|
diffResource,
|
|
458
671
|
listConflicts,
|
|
459
672
|
showConflict,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
5
|
+
const { DraftGoMcpClient } = require('../mcp/client');
|
|
6
|
+
const backendDefaults = require('./backend');
|
|
7
|
+
const { loadManifest, absolutePath } = require('./manifest');
|
|
8
|
+
const { hashFile } = require('./streams');
|
|
9
|
+
|
|
10
|
+
function versionValue(source) {
|
|
11
|
+
if (!source) return null;
|
|
12
|
+
if (source.base_version != null) return source.base_version;
|
|
13
|
+
if (source.base_revision != null) return source.base_revision;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sameVersion(entry, remote) {
|
|
18
|
+
const local = versionValue(entry);
|
|
19
|
+
const current = versionValue(remote);
|
|
20
|
+
if (local == null || current == null) return true;
|
|
21
|
+
return String(local) === String(current);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function optionalHash(projectDir, relative) {
|
|
25
|
+
if (!relative) return null;
|
|
26
|
+
const file = absolutePath(projectDir, relative);
|
|
27
|
+
return fs.existsSync(file) ? (await hashFile(file)).hash : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function classify(entry, localHash, baseHash, remote) {
|
|
31
|
+
const manifestHash = entry.base_hash;
|
|
32
|
+
const remoteHash = remote && remote.content_hash || null;
|
|
33
|
+
const baseMatchesManifest = baseHash === manifestHash;
|
|
34
|
+
const localMatchesManifest = localHash === manifestHash;
|
|
35
|
+
const localMatchesRemote = Boolean(localHash && remoteHash && localHash === remoteHash);
|
|
36
|
+
const remoteMatchesManifest = Boolean(remoteHash && remoteHash === manifestHash);
|
|
37
|
+
const versionMatches = remote ? sameVersion(entry, remote) : null;
|
|
38
|
+
|
|
39
|
+
let state;
|
|
40
|
+
let recommendation = null;
|
|
41
|
+
if (!localHash) {
|
|
42
|
+
state = 'local_missing';
|
|
43
|
+
recommendation = `draftgo checkout ${entry.resource_type} ${entry.resource_id} --force`;
|
|
44
|
+
} else if (!baseHash) {
|
|
45
|
+
state = 'base_missing';
|
|
46
|
+
recommendation = `draftgo checkout ${entry.resource_type} ${entry.resource_id} --force`;
|
|
47
|
+
} else if (!remote) {
|
|
48
|
+
state = baseMatchesManifest
|
|
49
|
+
? (localMatchesManifest ? 'clean_local' : 'local_modified')
|
|
50
|
+
: 'local_metadata_corrupt';
|
|
51
|
+
} else if (localMatchesRemote && (!remoteMatchesManifest || !versionMatches || !baseMatchesManifest)) {
|
|
52
|
+
state = remoteMatchesManifest ? 'metadata_stale' : 'committed_unrecorded';
|
|
53
|
+
recommendation = `draftgo reconcile ${entry.resource_type} ${entry.resource_id}`;
|
|
54
|
+
} else if (!baseMatchesManifest) {
|
|
55
|
+
state = 'local_metadata_corrupt';
|
|
56
|
+
recommendation = `draftgo checkout ${entry.resource_type} ${entry.resource_id} --force`;
|
|
57
|
+
} else if (remoteMatchesManifest && versionMatches) {
|
|
58
|
+
state = localMatchesManifest ? 'clean' : 'local_modified';
|
|
59
|
+
} else if (localMatchesManifest) {
|
|
60
|
+
state = 'remote_changed';
|
|
61
|
+
recommendation = `draftgo checkout ${entry.resource_type} ${entry.resource_id}`;
|
|
62
|
+
} else {
|
|
63
|
+
state = 'diverged';
|
|
64
|
+
recommendation = `draftgo commit ${entry.resource_type} ${entry.resource_id}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
state,
|
|
69
|
+
local_hash: localHash,
|
|
70
|
+
base_file_hash: baseHash,
|
|
71
|
+
manifest_hash: manifestHash,
|
|
72
|
+
remote_hash: remoteHash,
|
|
73
|
+
manifest_version: versionValue(entry),
|
|
74
|
+
remote_version: versionValue(remote),
|
|
75
|
+
version_matches: versionMatches,
|
|
76
|
+
local_matches_remote: localMatchesRemote,
|
|
77
|
+
recommendation,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function inspectEntry(projectDir, entry, remote = null) {
|
|
82
|
+
const [localHash, baseHash] = await Promise.all([
|
|
83
|
+
optionalHash(projectDir, entry.local_path),
|
|
84
|
+
optionalHash(projectDir, entry.base_path),
|
|
85
|
+
]);
|
|
86
|
+
return { ...entry, ...classify(entry, localHash, baseHash, remote) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function openMetadataSession(config, options = {}) {
|
|
90
|
+
if (options.client && options.tools) return { client: options.client, tools: options.tools };
|
|
91
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
92
|
+
await client.initialize(options);
|
|
93
|
+
const tools = options.tools || await client.listAllTools(options);
|
|
94
|
+
return { client, tools };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function inspectRemoteCheckouts(projectDir, options = {}) {
|
|
98
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
99
|
+
const manifest = options.manifest || loadManifest(projectDir);
|
|
100
|
+
const backend = { ...backendDefaults, ...(options.backend || {}) };
|
|
101
|
+
const session = options.backend && typeof options.backend.resolveMetadata === 'function'
|
|
102
|
+
? { client: options.client || {}, tools: options.tools || [] }
|
|
103
|
+
: await openMetadataSession(config, options);
|
|
104
|
+
const entries = Object.values(manifest.entries);
|
|
105
|
+
return Promise.all(entries.map(async (entry) => {
|
|
106
|
+
const remote = await backend.resolveMetadata(config, entry.resource_type, entry.resource_id, {
|
|
107
|
+
...options,
|
|
108
|
+
...session,
|
|
109
|
+
clientInitialized: true,
|
|
110
|
+
});
|
|
111
|
+
return inspectEntry(projectDir, entry, remote);
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
versionValue,
|
|
117
|
+
sameVersion,
|
|
118
|
+
classify,
|
|
119
|
+
inspectEntry,
|
|
120
|
+
openMetadataSession,
|
|
121
|
+
inspectRemoteCheckouts,
|
|
122
|
+
};
|