draftgo-cli 1.0.4
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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/draftgo.js +9 -0
- package/package.json +70 -0
- package/resources/project-design/README.md +42 -0
- package/resources/skill/SKILL.md +62 -0
- package/resources/skill/init/SKILL.md +41 -0
- package/resources/skill/manifest.json +35 -0
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +97 -0
- package/resources/skill/references/architecture.md +13 -0
- package/resources/skill/references/chat-sdk.md +205 -0
- package/resources/skill/references/checkout.md +140 -0
- package/resources/skill/references/data.md +49 -0
- package/resources/skill/references/db-relations.md +29 -0
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/development.md +41 -0
- package/resources/skill/references/diagnostics.md +50 -0
- package/resources/skill/references/frontend.md +158 -0
- package/resources/skill/references/mcp.md +110 -0
- package/resources/skill/references/methods.md +143 -0
- package/resources/skill/references/modules.md +75 -0
- package/resources/skill/references/runtime.md +109 -0
- package/resources/skill/references/services.md +32 -0
- package/src/apiContractCache.js +120 -0
- package/src/cli.js +100 -0
- package/src/commandRegistry.js +46 -0
- package/src/commands/api.js +244 -0
- package/src/commands/apiKey.js +30 -0
- package/src/commands/autoPush.js +36 -0
- package/src/commands/capabilities.js +100 -0
- package/src/commands/check.js +82 -0
- package/src/commands/checkout.js +18 -0
- package/src/commands/clean.js +72 -0
- package/src/commands/commit.js +47 -0
- package/src/commands/components.js +554 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +91 -0
- package/src/commands/delete.js +95 -0
- package/src/commands/deploy.js +77 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/group.js +37 -0
- package/src/commands/help.js +190 -0
- package/src/commands/init.js +126 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/local.js +79 -0
- package/src/commands/map.js +395 -0
- package/src/commands/mcp.js +150 -0
- package/src/commands/reconcile.js +20 -0
- package/src/commands/role.js +31 -0
- package/src/commands/status.js +98 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +79 -0
- package/src/commands/verify.js +188 -0
- package/src/commands/visualVerify.js +281 -0
- package/src/commands/worklog.js +117 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +65 -0
- package/src/detect.js +25 -0
- package/src/diffReport.js +106 -0
- package/src/fsx.js +67 -0
- package/src/index.js +46 -0
- package/src/localRuntime/compose.js +119 -0
- package/src/localRuntime/detect.js +77 -0
- package/src/localRuntime/index.js +211 -0
- package/src/localRuntime/mysqlClient.js +155 -0
- package/src/localRuntime/services.js +117 -0
- package/src/logger.js +37 -0
- package/src/mcp/client.js +558 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/parallel.js +54 -0
- package/src/mcp/protocol.js +223 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +51 -0
- package/src/paths.js +32 -0
- package/src/platforms.js +110 -0
- package/src/projectConfig.js +139 -0
- package/src/projectDesign.js +19 -0
- package/src/projectHealth.js +33 -0
- package/src/projectMap.js +220 -0
- package/src/prompt.js +94 -0
- package/src/releaseInstall.js +105 -0
- package/src/runtimeFiles.js +45 -0
- package/src/skill.js +295 -0
- package/src/targets.js +43 -0
- package/src/timeout.js +18 -0
- package/src/updateCheck.js +100 -0
- package/src/worklog.js +276 -0
- package/src/worktree/backend.js +438 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +751 -0
- package/src/worktree/inlineScripts.js +99 -0
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +89 -0
- package/src/worktree/status.js +124 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
7
|
+
const { DraftGoMcpClient } = require('../mcp/client');
|
|
8
|
+
const backendDefaults = require('./backend');
|
|
9
|
+
const { WorktreeError, isVersionConflict } = require('./errors');
|
|
10
|
+
const {
|
|
11
|
+
RESOURCE_TYPES,
|
|
12
|
+
canonicalResourceType,
|
|
13
|
+
entryKey,
|
|
14
|
+
mediaType,
|
|
15
|
+
resourceFileName,
|
|
16
|
+
safeIdSegment,
|
|
17
|
+
} = require('./types');
|
|
18
|
+
const {
|
|
19
|
+
streamToFiles,
|
|
20
|
+
hashFile,
|
|
21
|
+
copyFileAtomic,
|
|
22
|
+
writeJsonAtomic,
|
|
23
|
+
normalizeSha256,
|
|
24
|
+
tempPathFor,
|
|
25
|
+
} = require('./streams');
|
|
26
|
+
const {
|
|
27
|
+
loadManifest,
|
|
28
|
+
getEntry,
|
|
29
|
+
relativePath,
|
|
30
|
+
absolutePath,
|
|
31
|
+
saveManifest,
|
|
32
|
+
} = require('./manifest');
|
|
33
|
+
const { validateContentFile } = require('./validate');
|
|
34
|
+
const { inspectEntry, openMetadataSession, sameVersion } = require('./status');
|
|
35
|
+
|
|
36
|
+
const { withResourceLocks } = require('./locks');
|
|
37
|
+
const { mapBounded } = require('../mcp/parallel');
|
|
38
|
+
const CONFLICT_SCHEMA_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
function backendFor(options) {
|
|
41
|
+
return { ...backendDefaults, ...(options.backend || {}) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function worktreePaths(projectDir, resourceType, resourceId, extension) {
|
|
45
|
+
const canonical = canonicalResourceType(resourceType);
|
|
46
|
+
const directory = RESOURCE_TYPES[canonical].directory;
|
|
47
|
+
const fileName = resourceFileName(canonical, resourceId, extension);
|
|
48
|
+
return {
|
|
49
|
+
local: path.join(projectDir, '.draftgo', 'worktree', directory, fileName),
|
|
50
|
+
base: path.join(projectDir, '.draftgo', 'worktree', '.base', directory, fileName),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function conflictPaths(projectDir, resourceType, resourceId, extension) {
|
|
55
|
+
const canonical = canonicalResourceType(resourceType);
|
|
56
|
+
const directory = RESOURCE_TYPES[canonical].directory;
|
|
57
|
+
const root = path.join(projectDir, '.draftgo', 'conflicts', directory, safeIdSegment(resourceId));
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
manifest: path.join(root, 'conflict.json'),
|
|
61
|
+
base: path.join(root, `base${extension}`),
|
|
62
|
+
local: path.join(root, `local${extension}`),
|
|
63
|
+
remote: path.join(root, `remote${extension}`),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createSession(config, options) {
|
|
68
|
+
if (options.session) return options.session;
|
|
69
|
+
if (options.backend && typeof options.backend.resolveMetadata === 'function') return {};
|
|
70
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
71
|
+
await client.initialize(options);
|
|
72
|
+
const tools = await client.listAllTools(options);
|
|
73
|
+
return { client, tools };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureIds(resourceIds) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const ids = resourceIds.map((value) => String(value).trim()).filter((value) => {
|
|
79
|
+
if (!value || seen.has(value)) return false;
|
|
80
|
+
seen.add(value);
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
if (!ids.length) throw new WorktreeError('INVALID_RESOURCE_ID', 'At least one resource id is required.');
|
|
84
|
+
return ids;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function responseContentType(response) {
|
|
88
|
+
return mediaType(response && response.headers && response.headers.get('content-type'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertMetadataIdentity(metadata, resourceType, resourceId) {
|
|
92
|
+
if (metadata.resource_type !== resourceType || String(metadata.resource_id) !== String(resourceId)) {
|
|
93
|
+
throw new WorktreeError('METADATA_RESOURCE_MISMATCH',
|
|
94
|
+
'DraftGo returned metadata for a different resource.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function checkoutResourcesUnlocked(projectDir, resourceType, resourceIds, options = {}) {
|
|
99
|
+
const canonical = canonicalResourceType(resourceType);
|
|
100
|
+
const ids = ensureIds(resourceIds);
|
|
101
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
102
|
+
const backend = backendFor(options);
|
|
103
|
+
const session = await createSession(config, options);
|
|
104
|
+
const manifest = loadManifest(projectDir);
|
|
105
|
+
const settled = await mapBounded(ids, async (resourceId) => {
|
|
106
|
+
const metadata = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
107
|
+
...options,
|
|
108
|
+
...session,
|
|
109
|
+
projectDir,
|
|
110
|
+
clientInitialized: true,
|
|
111
|
+
});
|
|
112
|
+
assertMetadataIdentity(metadata, canonical, resourceId);
|
|
113
|
+
|
|
114
|
+
const existing = getEntry(manifest, canonical, resourceId);
|
|
115
|
+
if (existing) {
|
|
116
|
+
const existingLocal = absolutePath(projectDir, existing.local_path);
|
|
117
|
+
if (fs.existsSync(existingLocal)) {
|
|
118
|
+
const current = await hashFile(existingLocal);
|
|
119
|
+
if (current.hash !== existing.base_hash && !options.force) {
|
|
120
|
+
throw new WorktreeError(
|
|
121
|
+
'LOCAL_CHANGES_PRESENT',
|
|
122
|
+
`Refusing to replace locally modified ${canonical} ${resourceId}; commit it or use checkout --force explicitly.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const paths = worktreePaths(projectDir, canonical, resourceId, metadata.file_extension);
|
|
129
|
+
const response = await backend.download(config, metadata, options);
|
|
130
|
+
const actualType = responseContentType(response);
|
|
131
|
+
if (actualType && actualType !== mediaType(metadata.content_type)) {
|
|
132
|
+
throw new WorktreeError('CONTENT_TYPE_MISMATCH', 'Downloaded content type differs from checkout metadata.', {
|
|
133
|
+
expected: metadata.content_type,
|
|
134
|
+
actual: response.headers.get('content-type'),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const downloaded = await streamToFiles(response.body, [paths.local, paths.base], {
|
|
138
|
+
expectedHash: metadata.content_hash,
|
|
139
|
+
expectedSize: metadata.content_size,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const entry = {
|
|
143
|
+
server: config.server,
|
|
144
|
+
resource_type: canonical,
|
|
145
|
+
resource_id: resourceId,
|
|
146
|
+
title: metadata.title,
|
|
147
|
+
route: metadata.route,
|
|
148
|
+
code: metadata.code,
|
|
149
|
+
slug: metadata.slug,
|
|
150
|
+
local_path: relativePath(projectDir, paths.local),
|
|
151
|
+
content_type: metadata.content_type,
|
|
152
|
+
file_extension: metadata.file_extension,
|
|
153
|
+
content_size: downloaded.size,
|
|
154
|
+
base_path: relativePath(projectDir, paths.base),
|
|
155
|
+
base_version: metadata.base_version,
|
|
156
|
+
base_revision: metadata.base_revision,
|
|
157
|
+
base_etag: metadata.etag,
|
|
158
|
+
base_hash: downloaded.hash,
|
|
159
|
+
checked_out_at: new Date().toISOString(),
|
|
160
|
+
updated_at: metadata.updated_at,
|
|
161
|
+
updated_by: metadata.updated_by,
|
|
162
|
+
};
|
|
163
|
+
return entry;
|
|
164
|
+
}, { ...options, settled: true });
|
|
165
|
+
const results = settled.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
166
|
+
for (const entry of results) manifest.entries[entryKey(canonical, entry.resource_id)] = entry;
|
|
167
|
+
if (results.length) await saveManifest(projectDir, manifest);
|
|
168
|
+
const failures = settled.filter((item) => item.status === 'rejected');
|
|
169
|
+
if (failures.length) {
|
|
170
|
+
const first = failures[0].reason;
|
|
171
|
+
first.details = {
|
|
172
|
+
...(first.details || {}),
|
|
173
|
+
batch: {
|
|
174
|
+
completed: results.map((entry) => ({
|
|
175
|
+
resource_type: entry.resource_type, resource_id: entry.resource_id,
|
|
176
|
+
status: 'checked_out', local_path: entry.local_path,
|
|
177
|
+
})),
|
|
178
|
+
failed: failures.map((item) => ({
|
|
179
|
+
status: 'failed', code: item.reason.code || 'CHECKOUT_FAILED', message: item.reason.message,
|
|
180
|
+
})),
|
|
181
|
+
not_started: [],
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
throw first;
|
|
185
|
+
}
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeCommitPayload(value) {
|
|
190
|
+
if (!value || typeof value !== 'object') return {};
|
|
191
|
+
if (value.data && typeof value.data === 'object') return normalizeCommitPayload(value.data);
|
|
192
|
+
if (value.metadata && typeof value.metadata === 'object') return normalizeCommitPayload(value.metadata);
|
|
193
|
+
if (value.resource && typeof value.resource === 'object') return normalizeCommitPayload(value.resource);
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function commitVersion(payload) {
|
|
198
|
+
return {
|
|
199
|
+
version: payload.base_version !== undefined ? payload.base_version
|
|
200
|
+
: (payload.version !== undefined ? payload.version : null),
|
|
201
|
+
revision: payload.base_revision !== undefined ? payload.base_revision
|
|
202
|
+
: (payload.revision !== undefined ? payload.revision : null),
|
|
203
|
+
etag: payload.etag || payload.e_tag || null,
|
|
204
|
+
hash: normalizeSha256(payload.content_hash || payload.hash || payload.sha256),
|
|
205
|
+
updated_at: payload.updated_at || null,
|
|
206
|
+
updated_by: payload.updated_by || null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function writeConflict(projectDir, entry, error, config, backend, options, session) {
|
|
211
|
+
const localPath = absolutePath(projectDir, entry.local_path);
|
|
212
|
+
const basePath = absolutePath(projectDir, entry.base_path);
|
|
213
|
+
const paths = conflictPaths(projectDir, entry.resource_type, entry.resource_id, entry.file_extension);
|
|
214
|
+
const remoteMetadata = await backend.resolveMetadata(config, entry.resource_type, entry.resource_id, {
|
|
215
|
+
...options,
|
|
216
|
+
...session,
|
|
217
|
+
projectDir,
|
|
218
|
+
clientInitialized: true,
|
|
219
|
+
});
|
|
220
|
+
assertMetadataIdentity(remoteMetadata, entry.resource_type, entry.resource_id);
|
|
221
|
+
const response = await backend.download(config, remoteMetadata, options);
|
|
222
|
+
|
|
223
|
+
await copyFileAtomic(basePath, paths.base, { expectedHash: entry.base_hash });
|
|
224
|
+
const local = await copyFileAtomic(localPath, paths.local);
|
|
225
|
+
const remote = await streamToFiles(response.body, [paths.remote], {
|
|
226
|
+
expectedHash: remoteMetadata.content_hash,
|
|
227
|
+
expectedSize: remoteMetadata.content_size,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const record = {
|
|
231
|
+
schema_version: CONFLICT_SCHEMA_VERSION,
|
|
232
|
+
status: 'unresolved',
|
|
233
|
+
code: 'RESOURCE_VERSION_CONFLICT',
|
|
234
|
+
server: config.server,
|
|
235
|
+
resource_type: entry.resource_type,
|
|
236
|
+
resource_id: entry.resource_id,
|
|
237
|
+
title: entry.title,
|
|
238
|
+
content_type: entry.content_type,
|
|
239
|
+
base_path: relativePath(projectDir, paths.base),
|
|
240
|
+
local_path: relativePath(projectDir, paths.local),
|
|
241
|
+
remote_path: relativePath(projectDir, paths.remote),
|
|
242
|
+
worktree_local_path: entry.local_path,
|
|
243
|
+
expected_version: entry.base_version != null ? entry.base_version : entry.base_revision,
|
|
244
|
+
actual_version: remoteMetadata.base_version != null
|
|
245
|
+
? remoteMetadata.base_version : remoteMetadata.base_revision,
|
|
246
|
+
expected_hash: entry.base_hash,
|
|
247
|
+
local_hash: local.hash,
|
|
248
|
+
actual_hash: remote.hash,
|
|
249
|
+
actual_base_version: remoteMetadata.base_version,
|
|
250
|
+
actual_base_revision: remoteMetadata.base_revision,
|
|
251
|
+
actual_etag: remoteMetadata.etag,
|
|
252
|
+
updated_at: remoteMetadata.updated_at,
|
|
253
|
+
updated_by: remoteMetadata.updated_by,
|
|
254
|
+
backend_error: {
|
|
255
|
+
status: error.status || null,
|
|
256
|
+
code: error.code || 'RESOURCE_VERSION_CONFLICT',
|
|
257
|
+
},
|
|
258
|
+
created_at: new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
await writeJsonAtomic(paths.manifest, record);
|
|
261
|
+
return { ...record, manifest_path: relativePath(projectDir, paths.manifest) };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function unresolvedConflict(projectDir, resourceType, resourceId) {
|
|
265
|
+
const entry = loadManifest(projectDir).entries[entryKey(resourceType, resourceId)];
|
|
266
|
+
const extension = entry && entry.file_extension || '.bin';
|
|
267
|
+
const file = conflictPaths(projectDir, resourceType, resourceId, extension).manifest;
|
|
268
|
+
if (!fs.existsSync(file)) return null;
|
|
269
|
+
try {
|
|
270
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
271
|
+
return record.status === 'unresolved' ? record : null;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new WorktreeError('INVALID_CONFLICT_MANIFEST', `Invalid conflict manifest: ${error.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function commitBatchUnlocked(projectDir, targets, options = {}) {
|
|
278
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
279
|
+
const backend = backendFor(options);
|
|
280
|
+
const session = await createSession(config, options);
|
|
281
|
+
const manifest = loadManifest(projectDir);
|
|
282
|
+
const results = [];
|
|
283
|
+
const plans = [];
|
|
284
|
+
const preflightFailures = [];
|
|
285
|
+
|
|
286
|
+
const report = (result) => {
|
|
287
|
+
results.push(result);
|
|
288
|
+
if (typeof options.onStatus === 'function') options.onStatus(result);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Resolve and validate every target before the first remote write. Different
|
|
292
|
+
// resources are independent; bound concurrency to avoid exhausting connections.
|
|
293
|
+
const preflight = await mapBounded(targets, async ({ resource_type: canonical, resource_id: resourceId }) => {
|
|
294
|
+
try {
|
|
295
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
296
|
+
if (!entry) {
|
|
297
|
+
throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${canonical} ${resourceId} is not checked out.`);
|
|
298
|
+
}
|
|
299
|
+
if (entry.server !== config.server) {
|
|
300
|
+
throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
301
|
+
}
|
|
302
|
+
if (unresolvedConflict(projectDir, canonical, resourceId)) {
|
|
303
|
+
throw new WorktreeError(
|
|
304
|
+
'UNRESOLVED_CONFLICT',
|
|
305
|
+
`${canonical} ${resourceId} has an unresolved conflict; resolve it before committing.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const localPath = absolutePath(projectDir, entry.local_path);
|
|
310
|
+
const current = await hashFile(localPath);
|
|
311
|
+
const basePath = absolutePath(projectDir, entry.base_path);
|
|
312
|
+
const base = await hashFile(basePath);
|
|
313
|
+
if (base.hash !== entry.base_hash) {
|
|
314
|
+
throw new WorktreeError(
|
|
315
|
+
'BASE_HASH_MISMATCH',
|
|
316
|
+
`${canonical} ${resourceId} base hash differs from its manifest; run draftgo check --remote.`,
|
|
317
|
+
{ manifest_hash: entry.base_hash, base_hash: base.hash },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (current.hash !== entry.base_hash) validateContentFile(localPath, entry.content_type);
|
|
321
|
+
|
|
322
|
+
const fresh = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
323
|
+
...options,
|
|
324
|
+
...session,
|
|
325
|
+
projectDir,
|
|
326
|
+
clientInitialized: true,
|
|
327
|
+
});
|
|
328
|
+
assertMetadataIdentity(fresh, canonical, resourceId);
|
|
329
|
+
const remoteMatchesBase = fresh.content_hash === entry.base_hash && sameVersion(entry, fresh);
|
|
330
|
+
if (!remoteMatchesBase) {
|
|
331
|
+
const state = await inspectEntry(projectDir, entry, fresh);
|
|
332
|
+
throw new WorktreeError(
|
|
333
|
+
state.local_matches_remote ? 'COMMITTED_UNRECORDED' : 'REMOTE_VERSION_CHANGED',
|
|
334
|
+
state.local_matches_remote
|
|
335
|
+
? `${canonical} ${resourceId} already equals remote; run ${state.recommendation}.`
|
|
336
|
+
: `${canonical} ${resourceId} changed remotely; inspect with draftgo check --remote before committing.`,
|
|
337
|
+
state,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return { plan: { resourceId, entry, localPath, current, fresh } };
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error.code === 'REMOTE_VERSION_CHANGED') {
|
|
343
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
344
|
+
try {
|
|
345
|
+
const conflict = await writeConflict(projectDir, entry, error, config, backend, options, session);
|
|
346
|
+
error = new WorktreeError(
|
|
347
|
+
'RESOURCE_VERSION_CONFLICT',
|
|
348
|
+
`${canonical} ${resourceId} changed remotely; conflict materials were preserved during preflight.`,
|
|
349
|
+
conflict,
|
|
350
|
+
);
|
|
351
|
+
} catch (conflictError) {
|
|
352
|
+
error = conflictError;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { failure: {
|
|
356
|
+
resource_type: canonical,
|
|
357
|
+
resource_id: resourceId,
|
|
358
|
+
status: 'failed',
|
|
359
|
+
phase: 'preflight',
|
|
360
|
+
code: error.code || 'PREFLIGHT_FAILED',
|
|
361
|
+
message: error.message,
|
|
362
|
+
details: error.details || {},
|
|
363
|
+
} };
|
|
364
|
+
}
|
|
365
|
+
}, options);
|
|
366
|
+
for (const item of preflight) {
|
|
367
|
+
if (item.plan) plans.push(item.plan);
|
|
368
|
+
else preflightFailures.push(item.failure);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (preflightFailures.length) {
|
|
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,
|
|
375
|
+
status: 'not_started',
|
|
376
|
+
phase: 'preflight',
|
|
377
|
+
}));
|
|
378
|
+
for (const result of [...preflightFailures, ...notStarted]) report(result);
|
|
379
|
+
if (targets.length === 1 && preflightFailures.length === 1) {
|
|
380
|
+
const original = new WorktreeError(
|
|
381
|
+
preflightFailures[0].code,
|
|
382
|
+
preflightFailures[0].message,
|
|
383
|
+
preflightFailures[0].details,
|
|
384
|
+
);
|
|
385
|
+
throw original;
|
|
386
|
+
}
|
|
387
|
+
throw new WorktreeError(
|
|
388
|
+
'BATCH_PREFLIGHT_FAILED',
|
|
389
|
+
`Commit preflight failed for ${preflightFailures.length} resource(s); no remote content was changed.`,
|
|
390
|
+
{ completed: [], failed: preflightFailures, not_started: notStarted },
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function commitPlan(plan) {
|
|
395
|
+
const { resourceId, entry, localPath, current, fresh } = plan;
|
|
396
|
+
const canonical = entry.resource_type;
|
|
397
|
+
if (current.hash === entry.base_hash) {
|
|
398
|
+
return { resource_type: canonical, resource_id: resourceId, status: 'unchanged', hash: current.hash };
|
|
399
|
+
}
|
|
400
|
+
const commitMetadata = {
|
|
401
|
+
...fresh,
|
|
402
|
+
content_type: entry.content_type,
|
|
403
|
+
base_version: entry.base_version,
|
|
404
|
+
base_revision: entry.base_revision,
|
|
405
|
+
etag: entry.base_etag,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
let responsePayload;
|
|
409
|
+
try {
|
|
410
|
+
responsePayload = await backend.commit(config, commitMetadata, localPath, current, options);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
let failure = error;
|
|
413
|
+
const versionConflict = isVersionConflict(error);
|
|
414
|
+
if (versionConflict) {
|
|
415
|
+
const conflict = await writeConflict(projectDir, entry, error, config, backend, options, session);
|
|
416
|
+
failure = new WorktreeError(
|
|
417
|
+
'RESOURCE_VERSION_CONFLICT',
|
|
418
|
+
`${canonical} ${resourceId} changed remotely; conflict materials were preserved.`,
|
|
419
|
+
conflict,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
failure.batchResult = {
|
|
423
|
+
resource_type: canonical, resource_id: resourceId, status: 'failed', phase: 'upload',
|
|
424
|
+
code: failure.code || 'COMMIT_FAILED', message: failure.message,
|
|
425
|
+
remote_change_possible: !versionConflict,
|
|
426
|
+
};
|
|
427
|
+
throw failure;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
let committed = commitVersion(normalizeCommitPayload(responsePayload));
|
|
432
|
+
if (!committed.hash || (committed.version == null && committed.revision == null)) {
|
|
433
|
+
const confirmed = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
434
|
+
...options,
|
|
435
|
+
...session,
|
|
436
|
+
projectDir,
|
|
437
|
+
clientInitialized: true,
|
|
438
|
+
});
|
|
439
|
+
assertMetadataIdentity(confirmed, canonical, resourceId);
|
|
440
|
+
committed = {
|
|
441
|
+
version: confirmed.base_version,
|
|
442
|
+
revision: confirmed.base_revision,
|
|
443
|
+
etag: confirmed.etag,
|
|
444
|
+
hash: confirmed.content_hash,
|
|
445
|
+
updated_at: confirmed.updated_at,
|
|
446
|
+
updated_by: confirmed.updated_by,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (committed.hash && committed.hash !== current.hash) {
|
|
450
|
+
throw new WorktreeError(
|
|
451
|
+
'HASH_MISMATCH',
|
|
452
|
+
'DraftGo commit response hash differs from the uploaded raw bytes; manifest was not advanced.',
|
|
453
|
+
{ local_hash: current.hash, remote_hash: committed.hash },
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await copyFileAtomic(localPath, absolutePath(projectDir, entry.base_path), { expectedHash: current.hash });
|
|
458
|
+
entry.base_hash = current.hash;
|
|
459
|
+
entry.content_size = current.size;
|
|
460
|
+
entry.base_version = committed.version;
|
|
461
|
+
entry.base_revision = committed.revision;
|
|
462
|
+
entry.base_etag = committed.etag;
|
|
463
|
+
entry.updated_at = committed.updated_at;
|
|
464
|
+
entry.updated_by = committed.updated_by;
|
|
465
|
+
entry.committed_at = new Date().toISOString();
|
|
466
|
+
return {
|
|
467
|
+
resource_type: canonical,
|
|
468
|
+
resource_id: resourceId,
|
|
469
|
+
status: 'committed',
|
|
470
|
+
hash: current.hash,
|
|
471
|
+
base_version: entry.base_version,
|
|
472
|
+
base_revision: entry.base_revision,
|
|
473
|
+
};
|
|
474
|
+
} catch (error) {
|
|
475
|
+
error.batchResult = {
|
|
476
|
+
resource_type: canonical, resource_id: resourceId, status: 'failed', phase: 'upload',
|
|
477
|
+
code: error.code || 'COMMIT_FAILED', message: error.message, remote_change_possible: true,
|
|
478
|
+
};
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const settled = await mapBounded(plans, async (plan) => {
|
|
484
|
+
try {
|
|
485
|
+
const result = await commitPlan(plan);
|
|
486
|
+
report(result);
|
|
487
|
+
return result;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
report(error.batchResult);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
}, { ...options, settled: true });
|
|
493
|
+
const committed = results.filter((item) => item.status === 'committed');
|
|
494
|
+
if (committed.length) await saveManifest(projectDir, manifest);
|
|
495
|
+
const failures = settled.filter((item) => item.status === 'rejected');
|
|
496
|
+
if (failures.length) {
|
|
497
|
+
const first = failures[0].reason;
|
|
498
|
+
const inputOrder = new Map(targets.map((item, index) => [entryKey(item.resource_type, item.resource_id), index]));
|
|
499
|
+
const ordered = (items) => [...items].sort((left, right) => (
|
|
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)
|
|
502
|
+
));
|
|
503
|
+
first.details = {
|
|
504
|
+
...(first.details || {}),
|
|
505
|
+
batch: {
|
|
506
|
+
completed: ordered(results.filter((item) => ['committed', 'unchanged'].includes(item.status))),
|
|
507
|
+
failed: ordered(results.filter((item) => item.status === 'failed')),
|
|
508
|
+
not_started: [],
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
throw first;
|
|
512
|
+
}
|
|
513
|
+
return results;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function reconcileResourcesUnlocked(projectDir, resourceType, resourceIds, options = {}) {
|
|
517
|
+
const canonical = canonicalResourceType(resourceType);
|
|
518
|
+
const ids = ensureIds(resourceIds);
|
|
519
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
520
|
+
const backend = backendFor(options);
|
|
521
|
+
const manifest = loadManifest(projectDir);
|
|
522
|
+
const session = options.backend && typeof options.backend.resolveMetadata === 'function'
|
|
523
|
+
? { client: options.client || {}, tools: options.tools || [] }
|
|
524
|
+
: await openMetadataSession(config, options);
|
|
525
|
+
const results = [];
|
|
526
|
+
|
|
527
|
+
for (const resourceId of ids) {
|
|
528
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
529
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${canonical} ${resourceId} is not checked out.`);
|
|
530
|
+
if (entry.server !== config.server) {
|
|
531
|
+
throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
532
|
+
}
|
|
533
|
+
if (unresolvedConflict(projectDir, canonical, resourceId)) {
|
|
534
|
+
throw new WorktreeError(
|
|
535
|
+
'UNRESOLVED_CONFLICT',
|
|
536
|
+
`${canonical} ${resourceId} has an unresolved conflict; use conflict resolve instead of reconcile.`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const remote = await backend.resolveMetadata(config, canonical, resourceId, {
|
|
540
|
+
...options, ...session, projectDir, clientInitialized: true,
|
|
541
|
+
});
|
|
542
|
+
assertMetadataIdentity(remote, canonical, resourceId);
|
|
543
|
+
const status = await inspectEntry(projectDir, entry, remote);
|
|
544
|
+
if (!status.local_matches_remote) {
|
|
545
|
+
throw new WorktreeError(
|
|
546
|
+
'RECONCILE_UNSAFE',
|
|
547
|
+
`${canonical} ${resourceId} local content does not equal remote; refusing to advance metadata.`,
|
|
548
|
+
status,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (entry.content_type !== remote.content_type || entry.file_extension !== remote.file_extension) {
|
|
552
|
+
throw new WorktreeError(
|
|
553
|
+
'RECONCILE_FORMAT_CHANGED',
|
|
554
|
+
`${canonical} ${resourceId} remote content format changed; use checkout after preserving local work.`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const response = await backend.download(config, remote, options);
|
|
559
|
+
const basePath = absolutePath(projectDir, entry.base_path);
|
|
560
|
+
const verifiedRemote = tempPathFor(basePath);
|
|
561
|
+
let verified;
|
|
562
|
+
try {
|
|
563
|
+
verified = await streamToFiles(response.body, [verifiedRemote], {
|
|
564
|
+
expectedHash: remote.content_hash,
|
|
565
|
+
expectedSize: remote.content_size,
|
|
566
|
+
});
|
|
567
|
+
const localAfterDownload = await hashFile(absolutePath(projectDir, entry.local_path));
|
|
568
|
+
if (localAfterDownload.hash !== verified.hash) {
|
|
569
|
+
throw new WorktreeError(
|
|
570
|
+
'RECONCILE_LOCAL_CHANGED',
|
|
571
|
+
`${canonical} ${resourceId} local content changed during reconcile; manifest was not advanced.`,
|
|
572
|
+
{ local_hash: localAfterDownload.hash, remote_hash: verified.hash },
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
await copyFileAtomic(verifiedRemote, basePath, { expectedHash: verified.hash });
|
|
576
|
+
} finally {
|
|
577
|
+
await fs.promises.rm(verifiedRemote, { force: true }).catch(() => {});
|
|
578
|
+
}
|
|
579
|
+
entry.base_hash = verified.hash;
|
|
580
|
+
entry.content_size = verified.size;
|
|
581
|
+
entry.base_version = remote.base_version;
|
|
582
|
+
entry.base_revision = remote.base_revision;
|
|
583
|
+
entry.base_etag = remote.etag;
|
|
584
|
+
entry.updated_at = remote.updated_at;
|
|
585
|
+
entry.updated_by = remote.updated_by;
|
|
586
|
+
entry.reconciled_at = new Date().toISOString();
|
|
587
|
+
await saveManifest(projectDir, manifest);
|
|
588
|
+
results.push({
|
|
589
|
+
resource_type: canonical,
|
|
590
|
+
resource_id: resourceId,
|
|
591
|
+
status: 'reconciled',
|
|
592
|
+
hash: verified.hash,
|
|
593
|
+
base_version: entry.base_version,
|
|
594
|
+
base_revision: entry.base_revision,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return results;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function diffResource(projectDir, resourceType, resourceId, options = {}) {
|
|
601
|
+
const entry = getEntry(loadManifest(projectDir), resourceType, resourceId);
|
|
602
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${resourceType} ${resourceId} is not checked out.`);
|
|
603
|
+
const base = absolutePath(projectDir, entry.base_path);
|
|
604
|
+
const local = absolutePath(projectDir, entry.local_path);
|
|
605
|
+
const result = spawnSync('git', [
|
|
606
|
+
'diff', '--no-index', '--no-color', '--', base, local,
|
|
607
|
+
], {
|
|
608
|
+
cwd: projectDir,
|
|
609
|
+
encoding: 'utf8',
|
|
610
|
+
shell: false,
|
|
611
|
+
maxBuffer: options.maxBuffer || 64 * 1024 * 1024,
|
|
612
|
+
});
|
|
613
|
+
if (result.error) throw new WorktreeError('DIFF_UNAVAILABLE', `Unable to run git diff: ${result.error.message}`);
|
|
614
|
+
if (![0, 1].includes(result.status)) {
|
|
615
|
+
throw new WorktreeError('DIFF_FAILED', result.stderr || `git diff exited with ${result.status}.`);
|
|
616
|
+
}
|
|
617
|
+
return { entry, changed: result.status === 1, output: result.stdout || '' };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function readConflictFile(file) {
|
|
621
|
+
try {
|
|
622
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
623
|
+
if (!record || record.schema_version !== CONFLICT_SCHEMA_VERSION) {
|
|
624
|
+
throw new Error('unsupported schema');
|
|
625
|
+
}
|
|
626
|
+
return record;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw new WorktreeError('INVALID_CONFLICT_MANIFEST', `Invalid conflict manifest ${file}: ${error.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function listConflicts(projectDir, options = {}) {
|
|
633
|
+
const root = path.join(projectDir, '.draftgo', 'conflicts');
|
|
634
|
+
if (!fs.existsSync(root)) return [];
|
|
635
|
+
const records = [];
|
|
636
|
+
const visit = (directory) => {
|
|
637
|
+
for (const item of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
638
|
+
const absolute = path.join(directory, item.name);
|
|
639
|
+
if (item.isDirectory()) visit(absolute);
|
|
640
|
+
else if (item.isFile() && item.name === 'conflict.json') {
|
|
641
|
+
const record = readConflictFile(absolute);
|
|
642
|
+
if (options.all || record.status === 'unresolved') {
|
|
643
|
+
records.push({ ...record, manifest_path: relativePath(projectDir, absolute) });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
visit(root);
|
|
649
|
+
return records.sort((left, right) => `${left.resource_type}:${left.resource_id}`
|
|
650
|
+
.localeCompare(`${right.resource_type}:${right.resource_id}`));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function showConflict(projectDir, resourceType, resourceId) {
|
|
654
|
+
const canonical = canonicalResourceType(resourceType);
|
|
655
|
+
const match = listConflicts(projectDir, { all: true }).find((record) =>
|
|
656
|
+
record.resource_type === canonical && String(record.resource_id) === String(resourceId));
|
|
657
|
+
if (!match) throw new WorktreeError('CONFLICT_NOT_FOUND', `No conflict found for ${canonical} ${resourceId}.`);
|
|
658
|
+
return match;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function resolveConflictUnlocked(projectDir, resourceType, resourceId) {
|
|
662
|
+
const canonical = canonicalResourceType(resourceType);
|
|
663
|
+
const manifest = loadManifest(projectDir);
|
|
664
|
+
const entry = getEntry(manifest, canonical, resourceId);
|
|
665
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `${canonical} ${resourceId} is not checked out.`);
|
|
666
|
+
const conflict = showConflict(projectDir, canonical, resourceId);
|
|
667
|
+
if (conflict.status !== 'unresolved') {
|
|
668
|
+
throw new WorktreeError('CONFLICT_ALREADY_RESOLVED', `${canonical} ${resourceId} is already resolved.`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const localPath = absolutePath(projectDir, entry.local_path);
|
|
672
|
+
validateContentFile(localPath, entry.content_type);
|
|
673
|
+
const local = await hashFile(localPath);
|
|
674
|
+
const remotePath = absolutePath(projectDir, conflict.remote_path);
|
|
675
|
+
const remote = await hashFile(remotePath);
|
|
676
|
+
if (remote.hash !== conflict.actual_hash) {
|
|
677
|
+
throw new WorktreeError('HASH_MISMATCH', 'Preserved remote conflict content no longer matches its manifest.');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
await copyFileAtomic(remotePath, absolutePath(projectDir, entry.base_path), { expectedHash: remote.hash });
|
|
681
|
+
entry.base_hash = remote.hash;
|
|
682
|
+
entry.base_version = conflict.actual_base_version;
|
|
683
|
+
entry.base_revision = conflict.actual_base_revision;
|
|
684
|
+
entry.base_etag = conflict.actual_etag;
|
|
685
|
+
entry.updated_at = conflict.updated_at;
|
|
686
|
+
entry.updated_by = conflict.updated_by;
|
|
687
|
+
entry.conflict_resolved_at = new Date().toISOString();
|
|
688
|
+
await saveManifest(projectDir, manifest);
|
|
689
|
+
|
|
690
|
+
const manifestFile = absolutePath(projectDir, conflict.manifest_path);
|
|
691
|
+
const resolved = {
|
|
692
|
+
...conflict,
|
|
693
|
+
status: 'resolved',
|
|
694
|
+
resolution_local_hash: local.hash,
|
|
695
|
+
resolved_at: new Date().toISOString(),
|
|
696
|
+
};
|
|
697
|
+
delete resolved.manifest_path;
|
|
698
|
+
await writeJsonAtomic(manifestFile, resolved);
|
|
699
|
+
return { ...resolved, manifest_path: relativePath(projectDir, manifestFile) };
|
|
700
|
+
}
|
|
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
|
+
|
|
739
|
+
module.exports = {
|
|
740
|
+
commitBatch,
|
|
741
|
+
CONFLICT_SCHEMA_VERSION,
|
|
742
|
+
worktreePaths,
|
|
743
|
+
conflictPaths,
|
|
744
|
+
checkoutResources,
|
|
745
|
+
commitResources,
|
|
746
|
+
reconcileResources,
|
|
747
|
+
diffResource,
|
|
748
|
+
listConflicts,
|
|
749
|
+
showConflict,
|
|
750
|
+
resolveConflict,
|
|
751
|
+
};
|