draftgo-cli 4.0.25 → 4.0.26

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 (87) hide show
  1. package/README.md +21 -37
  2. package/package.json +3 -5
  3. package/resources/skill/SKILL.md +9 -5
  4. package/resources/skill/manifest.json +2 -5
  5. package/resources/skill/references/ai.md +41 -0
  6. package/resources/skill/references/app-api.md +2 -50
  7. package/resources/skill/references/architecture.md +1 -1
  8. package/resources/skill/references/chat-sdk.md +29 -37
  9. package/resources/skill/references/checkout.md +4 -4
  10. package/resources/skill/references/data.md +0 -46
  11. package/resources/skill/references/delivery.md +3 -3
  12. package/resources/skill/references/diagnostics.md +10 -11
  13. package/resources/skill/references/frontend.md +23 -20
  14. package/resources/skill/references/mcp.md +4 -14
  15. package/resources/skill/references/methods.md +15 -68
  16. package/resources/skill/references/modules.md +23 -44
  17. package/resources/skill/references/runtime.md +3 -20
  18. package/resources/skill/story/SKILL.md +2 -2
  19. package/src/apiContractCache.js +14 -6
  20. package/src/cli.js +0 -7
  21. package/src/commandRegistry.js +0 -6
  22. package/src/commands/api.js +87 -17
  23. package/src/commands/apiKey.js +2 -6
  24. package/src/commands/autoPush.js +15 -51
  25. package/src/commands/capabilities.js +22 -15
  26. package/src/commands/check.js +19 -53
  27. package/src/commands/checkout.js +1 -4
  28. package/src/commands/clean.js +1 -1
  29. package/src/commands/commit.js +1 -4
  30. package/src/commands/components.js +12 -8
  31. package/src/commands/conflict.js +4 -6
  32. package/src/commands/conflicts.js +1 -2
  33. package/src/commands/connect.js +0 -8
  34. package/src/commands/delete.js +15 -11
  35. package/src/commands/deploy.js +64 -26
  36. package/src/commands/diff.js +1 -4
  37. package/src/commands/group.js +2 -3
  38. package/src/commands/help.js +19 -41
  39. package/src/commands/init.js +13 -6
  40. package/src/commands/local.js +4 -1
  41. package/src/commands/map.js +138 -23
  42. package/src/commands/reconcile.js +1 -15
  43. package/src/commands/role.js +1 -2
  44. package/src/commands/status.js +12 -40
  45. package/src/commands/verify.js +8 -7
  46. package/src/commands/worklog.js +11 -5
  47. package/src/contractCompatibility.js +10 -2
  48. package/src/localRuntime/compose.js +41 -27
  49. package/src/localRuntime/detect.js +6 -6
  50. package/src/localRuntime/index.js +47 -47
  51. package/src/localRuntime/services.js +2 -39
  52. package/src/mcp/client.js +99 -134
  53. package/src/mcp/parallel.js +25 -2
  54. package/src/mcp/protocol.js +38 -9
  55. package/src/mcp/tools.js +10 -19
  56. package/src/projectConfig.js +1 -4
  57. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  58. package/src/projectMap.js +1 -1
  59. package/src/runtimeFiles.js +2 -1
  60. package/src/worklog.js +3 -2
  61. package/src/worktree/backend.js +127 -15
  62. package/src/worktree/index.js +64 -22
  63. package/src/worktree/locks.js +52 -0
  64. package/src/worktree/manifest.js +18 -4
  65. package/src/worktree/status.js +4 -2
  66. package/resources/custom-service-sdk/ai.go +0 -520
  67. package/resources/custom-service-sdk/ai_test.go +0 -156
  68. package/resources/custom-service-sdk/auth_test.go +0 -56
  69. package/resources/custom-service-sdk/billing.go +0 -596
  70. package/resources/custom-service-sdk/billing_test.go +0 -150
  71. package/resources/custom-service-sdk/go.mod +0 -3
  72. package/resources/custom-service-sdk/manifest.json +0 -77
  73. package/resources/custom-service-sdk/platform.go +0 -352
  74. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  75. package/resources/custom-service-sdk/registration_test.go +0 -39
  76. package/resources/custom-service-sdk/resources.go +0 -247
  77. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  78. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  79. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  80. package/resources/custom-service-sdk/sdk.go +0 -209
  81. package/resources/skill/references/aihub.md +0 -116
  82. package/resources/skill/references/custom-services.md +0 -201
  83. package/src/commands/customService.js +0 -95
  84. package/src/commands/dataRange.js +0 -33
  85. package/src/commands/grant.js +0 -29
  86. package/src/commands/space.js +0 -41
  87. package/src/customServices.js +0 -484
@@ -33,6 +33,8 @@ const {
33
33
  const { validateContentFile } = require('./validate');
34
34
  const { inspectEntry, openMetadataSession, sameVersion } = require('./status');
35
35
 
36
+ const { withResourceLocks } = require('./locks');
37
+ const { mapBounded } = require('../mcp/parallel');
36
38
  const CONFLICT_SCHEMA_VERSION = 1;
37
39
 
38
40
  function backendFor(options) {
@@ -93,17 +95,18 @@ function assertMetadataIdentity(metadata, resourceType, resourceId) {
93
95
  }
94
96
  }
95
97
 
96
- async function checkoutResources(projectDir, resourceType, resourceIds, options = {}) {
98
+ async function checkoutResourcesUnlocked(projectDir, resourceType, resourceIds, options = {}) {
97
99
  const canonical = canonicalResourceType(resourceType);
98
100
  const ids = ensureIds(resourceIds);
99
101
  const config = options.config || loadProjectConfig(projectDir);
100
102
  const backend = backendFor(options);
101
103
  const session = await createSession(config, options);
102
104
  const manifest = loadManifest(projectDir);
103
- const settled = await Promise.allSettled(ids.map(async (resourceId) => {
105
+ const settled = await mapBounded(ids, async (resourceId) => {
104
106
  const metadata = await backend.resolveMetadata(config, canonical, resourceId, {
105
107
  ...options,
106
108
  ...session,
109
+ projectDir,
107
110
  clientInitialized: true,
108
111
  });
109
112
  assertMetadataIdentity(metadata, canonical, resourceId);
@@ -158,7 +161,7 @@ async function checkoutResources(projectDir, resourceType, resourceIds, options
158
161
  updated_by: metadata.updated_by,
159
162
  };
160
163
  return entry;
161
- }));
164
+ }, { ...options, settled: true });
162
165
  const results = settled.filter((item) => item.status === 'fulfilled').map((item) => item.value);
163
166
  for (const entry of results) manifest.entries[entryKey(canonical, entry.resource_id)] = entry;
164
167
  if (results.length) await saveManifest(projectDir, manifest);
@@ -211,6 +214,7 @@ async function writeConflict(projectDir, entry, error, config, backend, options,
211
214
  const remoteMetadata = await backend.resolveMetadata(config, entry.resource_type, entry.resource_id, {
212
215
  ...options,
213
216
  ...session,
217
+ projectDir,
214
218
  clientInitialized: true,
215
219
  });
216
220
  assertMetadataIdentity(remoteMetadata, entry.resource_type, entry.resource_id);
@@ -270,9 +274,7 @@ function unresolvedConflict(projectDir, resourceType, resourceId) {
270
274
  }
271
275
  }
272
276
 
273
- async function commitResources(projectDir, resourceType, resourceIds, options = {}) {
274
- const canonical = canonicalResourceType(resourceType);
275
- const ids = ensureIds(resourceIds);
277
+ async function commitBatchUnlocked(projectDir, targets, options = {}) {
276
278
  const config = options.config || loadProjectConfig(projectDir);
277
279
  const backend = backendFor(options);
278
280
  const session = await createSession(config, options);
@@ -287,8 +289,8 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
287
289
  };
288
290
 
289
291
  // Resolve and validate every target before the first remote write. Different
290
- // resources are independent, so preflight can run without a client-side cap.
291
- const preflight = await Promise.all(ids.map(async (resourceId) => {
292
+ // resources are independent; bound concurrency to avoid exhausting connections.
293
+ const preflight = await mapBounded(targets, async ({ resource_type: canonical, resource_id: resourceId }) => {
292
294
  try {
293
295
  const entry = getEntry(manifest, canonical, resourceId);
294
296
  if (!entry) {
@@ -320,6 +322,7 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
320
322
  const fresh = await backend.resolveMetadata(config, canonical, resourceId, {
321
323
  ...options,
322
324
  ...session,
325
+ projectDir,
323
326
  clientInitialized: true,
324
327
  });
325
328
  assertMetadataIdentity(fresh, canonical, resourceId);
@@ -359,22 +362,21 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
359
362
  details: error.details || {},
360
363
  } };
361
364
  }
362
- }));
365
+ }, options);
363
366
  for (const item of preflight) {
364
367
  if (item.plan) plans.push(item.plan);
365
368
  else preflightFailures.push(item.failure);
366
369
  }
367
370
 
368
371
  if (preflightFailures.length) {
369
- const failedIds = new Set(preflightFailures.map((item) => String(item.resource_id)));
370
- const notStarted = ids.filter((id) => !failedIds.has(String(id))).map((resourceId) => ({
371
- resource_type: canonical,
372
- resource_id: resourceId,
372
+ const failedIds = new Set(preflightFailures.map((item) => entryKey(item.resource_type, item.resource_id)));
373
+ const notStarted = targets.filter((item) => !failedIds.has(entryKey(item.resource_type, item.resource_id))).map((item) => ({
374
+ ...item,
373
375
  status: 'not_started',
374
376
  phase: 'preflight',
375
377
  }));
376
378
  for (const result of [...preflightFailures, ...notStarted]) report(result);
377
- if (ids.length === 1 && preflightFailures.length === 1) {
379
+ if (targets.length === 1 && preflightFailures.length === 1) {
378
380
  const original = new WorktreeError(
379
381
  preflightFailures[0].code,
380
382
  preflightFailures[0].message,
@@ -391,6 +393,7 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
391
393
 
392
394
  async function commitPlan(plan) {
393
395
  const { resourceId, entry, localPath, current, fresh } = plan;
396
+ const canonical = entry.resource_type;
394
397
  if (current.hash === entry.base_hash) {
395
398
  return { resource_type: canonical, resource_id: resourceId, status: 'unchanged', hash: current.hash };
396
399
  }
@@ -430,6 +433,7 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
430
433
  const confirmed = await backend.resolveMetadata(config, canonical, resourceId, {
431
434
  ...options,
432
435
  ...session,
436
+ projectDir,
433
437
  clientInitialized: true,
434
438
  });
435
439
  assertMetadataIdentity(confirmed, canonical, resourceId);
@@ -476,7 +480,7 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
476
480
  }
477
481
  }
478
482
 
479
- const settled = await Promise.allSettled(plans.map(async (plan) => {
483
+ const settled = await mapBounded(plans, async (plan) => {
480
484
  try {
481
485
  const result = await commitPlan(plan);
482
486
  report(result);
@@ -485,16 +489,16 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
485
489
  report(error.batchResult);
486
490
  throw error;
487
491
  }
488
- }));
492
+ }, { ...options, settled: true });
489
493
  const committed = results.filter((item) => item.status === 'committed');
490
494
  if (committed.length) await saveManifest(projectDir, manifest);
491
495
  const failures = settled.filter((item) => item.status === 'rejected');
492
496
  if (failures.length) {
493
497
  const first = failures[0].reason;
494
- const inputOrder = new Map(ids.map((id, index) => [String(id), index]));
498
+ const inputOrder = new Map(targets.map((item, index) => [entryKey(item.resource_type, item.resource_id), index]));
495
499
  const ordered = (items) => [...items].sort((left, right) => (
496
- (inputOrder.get(String(left.resource_id)) ?? Number.MAX_SAFE_INTEGER)
497
- - (inputOrder.get(String(right.resource_id)) ?? Number.MAX_SAFE_INTEGER)
500
+ (inputOrder.get(entryKey(left.resource_type, left.resource_id)) ?? Number.MAX_SAFE_INTEGER)
501
+ - (inputOrder.get(entryKey(right.resource_type, right.resource_id)) ?? Number.MAX_SAFE_INTEGER)
498
502
  ));
499
503
  first.details = {
500
504
  ...(first.details || {}),
@@ -509,7 +513,7 @@ async function commitResources(projectDir, resourceType, resourceIds, options =
509
513
  return results;
510
514
  }
511
515
 
512
- async function reconcileResources(projectDir, resourceType, resourceIds, options = {}) {
516
+ async function reconcileResourcesUnlocked(projectDir, resourceType, resourceIds, options = {}) {
513
517
  const canonical = canonicalResourceType(resourceType);
514
518
  const ids = ensureIds(resourceIds);
515
519
  const config = options.config || loadProjectConfig(projectDir);
@@ -533,7 +537,7 @@ async function reconcileResources(projectDir, resourceType, resourceIds, options
533
537
  );
534
538
  }
535
539
  const remote = await backend.resolveMetadata(config, canonical, resourceId, {
536
- ...options, ...session, clientInitialized: true,
540
+ ...options, ...session, projectDir, clientInitialized: true,
537
541
  });
538
542
  assertMetadataIdentity(remote, canonical, resourceId);
539
543
  const status = await inspectEntry(projectDir, entry, remote);
@@ -654,7 +658,7 @@ function showConflict(projectDir, resourceType, resourceId) {
654
658
  return match;
655
659
  }
656
660
 
657
- async function resolveConflict(projectDir, resourceType, resourceId) {
661
+ async function resolveConflictUnlocked(projectDir, resourceType, resourceId) {
658
662
  const canonical = canonicalResourceType(resourceType);
659
663
  const manifest = loadManifest(projectDir);
660
664
  const entry = getEntry(manifest, canonical, resourceId);
@@ -695,7 +699,45 @@ async function resolveConflict(projectDir, resourceType, resourceId) {
695
699
  return { ...resolved, manifest_path: relativePath(projectDir, manifestFile) };
696
700
  }
697
701
 
702
+ function normalizedTargets(targets) {
703
+ const unique = new Map();
704
+ for (const target of targets) {
705
+ const resource_type = canonicalResourceType(target.resource_type);
706
+ const resource_id = ensureIds([target.resource_id])[0];
707
+ unique.set(entryKey(resource_type, resource_id), { resource_type, resource_id });
708
+ }
709
+ if (!unique.size) throw new WorktreeError('INVALID_RESOURCE_ID', 'At least one resource is required.');
710
+ return [...unique.values()];
711
+ }
712
+
713
+ function commitBatch(projectDir, targets, options = {}) {
714
+ const normalized = normalizedTargets(targets);
715
+ return withResourceLocks(projectDir, normalized.map((item) => entryKey(item.resource_type, item.resource_id)),
716
+ () => commitBatchUnlocked(projectDir, normalized, options));
717
+ }
718
+ function commitResources(projectDir, resourceType, resourceIds, options = {}) {
719
+ return commitBatch(projectDir, ensureIds(resourceIds).map((resource_id) => ({ resource_type: resourceType, resource_id })), options);
720
+ }
721
+ function checkoutResources(projectDir, resourceType, resourceIds, options = {}) {
722
+ const type = canonicalResourceType(resourceType);
723
+ const ids = ensureIds(resourceIds);
724
+ return withResourceLocks(projectDir, ids.map((id) => entryKey(type, id)),
725
+ () => checkoutResourcesUnlocked(projectDir, type, ids, options));
726
+ }
727
+ function reconcileResources(projectDir, resourceType, resourceIds, options = {}) {
728
+ const type = canonicalResourceType(resourceType);
729
+ const ids = ensureIds(resourceIds);
730
+ return withResourceLocks(projectDir, ids.map((id) => entryKey(type, id)),
731
+ () => reconcileResourcesUnlocked(projectDir, type, ids, options));
732
+ }
733
+ function resolveConflict(projectDir, resourceType, resourceId) {
734
+ const type = canonicalResourceType(resourceType);
735
+ return withResourceLocks(projectDir, [entryKey(type, resourceId)],
736
+ () => resolveConflictUnlocked(projectDir, type, resourceId));
737
+ }
738
+
698
739
  module.exports = {
740
+ commitBatch,
699
741
  CONFLICT_SCHEMA_VERSION,
700
742
  worktreePaths,
701
743
  conflictPaths,
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { acquireLock, releaseLock } = require('../worklog');
7
+ const { WorktreeError } = require('./errors');
8
+
9
+ // Reuse owner-token locks, but never expire an active long-running transfer.
10
+ function lockFile(file) {
11
+ const lockPath = `${file}.lock`;
12
+ try {
13
+ const token = fs.readFileSync(lockPath, 'utf8').trim();
14
+ const pid = Number(token.split(':')[0]);
15
+ if (Number.isSafeInteger(pid) && pid > 0) {
16
+ try { process.kill(pid, 0); } catch (error) {
17
+ if (error.code === 'ESRCH' && fs.readFileSync(lockPath, 'utf8').trim() === token) fs.rmSync(lockPath);
18
+ }
19
+ }
20
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
21
+ try { return acquireLock(file, { lockTimeoutMs: 0, lockStaleMs: Number.MAX_SAFE_INTEGER }); }
22
+ catch (error) {
23
+ if (error.code !== 'WORKLOG_LOCK_TIMEOUT') throw error;
24
+ throw new WorktreeError('WORKTREE_LOCKED', 'Another process owns this worktree resource; retry after it finishes.', { lock_path: lockPath });
25
+ }
26
+ }
27
+
28
+ async function withResourceLocks(projectDir, keys, action) {
29
+ const locks = [];
30
+ try {
31
+ for (const key of [...new Set(keys)].sort()) {
32
+ const name = crypto.createHash('sha256').update(key).digest('hex');
33
+ locks.push(lockFile(path.join(projectDir, '.draftgo', 'worktree', '.locks', name)));
34
+ }
35
+ return await action();
36
+ } finally { for (const lock of locks.reverse()) releaseLock(lock); }
37
+ }
38
+
39
+ async function withManifestLock(projectDir, action) {
40
+ const file = path.join(projectDir, '.draftgo', 'worktree', 'manifest.json');
41
+ const started = Date.now();
42
+ let lock;
43
+ while (!lock) {
44
+ try { lock = lockFile(file); } catch (error) {
45
+ if (error.code !== 'WORKTREE_LOCKED' || Date.now() - started >= 10000) throw error;
46
+ await new Promise((resolve) => setTimeout(resolve, 15));
47
+ }
48
+ }
49
+ try { return await action(); } finally { releaseLock(lock); }
50
+ }
51
+
52
+ module.exports = { withResourceLocks, withManifestLock };
@@ -6,6 +6,8 @@ const { WorktreeError } = require('./errors');
6
6
  const { entryKey } = require('./types');
7
7
  const { writeJsonAtomic } = require('./streams');
8
8
 
9
+ const { withManifestLock } = require('./locks');
10
+ const snapshots = new WeakMap();
9
11
  const MANIFEST_SCHEMA_VERSION = 1;
10
12
 
11
13
  function manifestPath(projectDir) {
@@ -13,7 +15,9 @@ function manifestPath(projectDir) {
13
15
  }
14
16
 
15
17
  function emptyManifest() {
16
- return { schema_version: MANIFEST_SCHEMA_VERSION, entries: {} };
18
+ const value = { schema_version: MANIFEST_SCHEMA_VERSION, entries: {} };
19
+ snapshots.set(value, {});
20
+ return value;
17
21
  }
18
22
 
19
23
  function loadManifest(projectDir) {
@@ -29,6 +33,7 @@ function loadManifest(projectDir) {
29
33
  || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
30
34
  throw new WorktreeError('INVALID_WORKTREE_MANIFEST', 'Unsupported or malformed checkout manifest.');
31
35
  }
36
+ snapshots.set(parsed, JSON.parse(JSON.stringify(parsed.entries)));
32
37
  return parsed;
33
38
  }
34
39
 
@@ -58,9 +63,18 @@ function absolutePath(projectDir, relative) {
58
63
  }
59
64
 
60
65
  async function saveManifest(projectDir, manifest) {
61
- manifest.schema_version = MANIFEST_SCHEMA_VERSION;
62
- manifest.updated_at = new Date().toISOString();
63
- await writeJsonAtomic(manifestPath(projectDir), manifest);
66
+ const before = snapshots.get(manifest) || {};
67
+ await withManifestLock(projectDir, async () => {
68
+ const fresh = loadManifest(projectDir);
69
+ for (const key of new Set([...Object.keys(before), ...Object.keys(manifest.entries)])) {
70
+ if (JSON.stringify(before[key]) === JSON.stringify(manifest.entries[key])) continue;
71
+ if (manifest.entries[key] === undefined) delete fresh.entries[key];
72
+ else fresh.entries[key] = manifest.entries[key];
73
+ }
74
+ fresh.updated_at = new Date().toISOString();
75
+ await writeJsonAtomic(manifestPath(projectDir), fresh);
76
+ snapshots.set(manifest, JSON.parse(JSON.stringify(manifest.entries)));
77
+ });
64
78
  }
65
79
 
66
80
  module.exports = {
@@ -102,14 +102,16 @@ async function inspectRemoteCheckouts(projectDir, options = {}) {
102
102
  ? { client: options.client || {}, tools: options.tools || [] }
103
103
  : await openMetadataSession(config, options);
104
104
  const entries = options.entries || Object.values(manifest.entries);
105
- return Promise.all(entries.map(async (entry) => {
105
+ const { mapBounded } = require('../mcp/parallel');
106
+ return mapBounded(entries, async (entry) => {
106
107
  const remote = await backend.resolveMetadata(config, entry.resource_type, entry.resource_id, {
107
108
  ...options,
108
109
  ...session,
110
+ projectDir,
109
111
  clientInitialized: true,
110
112
  });
111
113
  return inspectEntry(projectDir, entry, remote);
112
- }));
114
+ }, options);
113
115
  }
114
116
 
115
117
  module.exports = {