draftgo-cli 3.0.35 → 3.0.39

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 (64) hide show
  1. package/README.md +220 -272
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +5 -4
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +1 -1
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +47 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +326 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
package/src/skill.js CHANGED
@@ -2,14 +2,12 @@
2
2
 
3
3
  // Per-platform skill renderer.
4
4
  //
5
- // For every AI-tool target (see src/platforms.js) we render the complete
6
- // instruction body into that tool's own directory. The large OpenAPI snapshot
7
- // is shared once under .draftgo/skill-shared to avoid redundant copies.
5
+ // Every AI-tool target receives the complete Skill and its domain references
6
+ // in that target's own project directory.
8
7
  //
9
8
  // Templating in .md files:
10
9
  // {{SKILL_DIR}} → platform's project-relative skill dir (forward slashes)
11
10
  // {{SKILL_SCRIPTS}} → {{SKILL_DIR}}/scripts
12
- // {{SKILL_SHARED}} → .draftgo/skill-shared
13
11
 
14
12
  const path = require('path');
15
13
  const fs = require('fs');
@@ -21,8 +19,6 @@ const {
21
19
  } = require('./fsx');
22
20
 
23
21
  const SKILL_SOURCE_DIR = path.join(RESOURCES_DIR, 'skill');
24
- const SHARED_RESOURCE_DIR = path.join('.draftgo', 'skill-shared');
25
- const SHARED_FILES = new Set(['references/api.json']);
26
22
 
27
23
  function getPackageVersion() {
28
24
  try {
@@ -80,8 +76,7 @@ function substitute(text, platform) {
80
76
  const scriptsDir = `${skillDir}/scripts`;
81
77
  return text
82
78
  .replace(/\{\{SKILL_DIR\}\}/g, skillDir)
83
- .replace(/\{\{SKILL_SCRIPTS\}\}/g, scriptsDir)
84
- .replace(/\{\{SKILL_SHARED\}\}/g, SHARED_RESOURCE_DIR.replace(/\\/g, '/'));
79
+ .replace(/\{\{SKILL_SCRIPTS\}\}/g, scriptsDir);
85
80
  }
86
81
 
87
82
  function walk(root, onFile) {
@@ -98,7 +93,6 @@ function renderInto(destDir, platform) {
98
93
  const srcRoot = SKILL_SOURCE_DIR;
99
94
  walk(srcRoot, (absSrc) => {
100
95
  const rel = path.relative(srcRoot, absSrc);
101
- if (SHARED_FILES.has(rel.replace(/\\/g, '/'))) return;
102
96
  const absDst = path.join(destDir, rel);
103
97
  ensureDir(path.dirname(absDst));
104
98
  if (absSrc.endsWith('.md')) {
@@ -116,21 +110,6 @@ function renderInto(destDir, platform) {
116
110
 
117
111
  }
118
112
 
119
- function ensureSharedResources(projectDir) {
120
- const sharedRoot = path.join(projectDir, SHARED_RESOURCE_DIR);
121
- for (const rel of SHARED_FILES) {
122
- const src = path.join(SKILL_SOURCE_DIR, rel);
123
- const dest = path.join(sharedRoot, rel);
124
- if (exists(dest)) {
125
- const sourceStat = fs.statSync(src);
126
- const destStat = fs.statSync(dest);
127
- if (sourceStat.size === destStat.size && destStat.mtimeMs >= sourceStat.mtimeMs) continue;
128
- }
129
- copyFile(src, dest);
130
- }
131
- return sharedRoot;
132
- }
133
-
134
113
  function validateRenderedSkill(assetDir) {
135
114
  const main = path.join(assetDir, 'SKILL.md');
136
115
  if (!exists(main)) throw new Error('渲染结果缺少 SKILL.md');
@@ -140,7 +119,7 @@ function validateRenderedSkill(assetDir) {
140
119
  if (!text.startsWith('---\n') || firstEnd < 0 || body.startsWith('---\n')) {
141
120
  throw new Error('SKILL.md frontmatter 必须且只能有一个');
142
121
  }
143
- if (/\{\{(?:SKILL_DIR|SKILL_SCRIPTS|SKILL_SHARED)\}\}/.test(text)) {
122
+ if (/\{\{(?:SKILL_DIR|SKILL_SCRIPTS)\}\}/.test(text)) {
144
123
  throw new Error('SKILL.md 仍包含未替换占位符');
145
124
  }
146
125
  }
@@ -157,7 +136,8 @@ function ensureRuntime(projectDir) {
157
136
  }
158
137
  appendGitignoreLine(projectDir, '.draftgo/config.json');
159
138
  appendGitignoreLine(projectDir, '.draftgo/token');
160
- ensureSharedResources(projectDir);
139
+ appendGitignoreLine(projectDir, '.draftgo/worktree/');
140
+ appendGitignoreLine(projectDir, '.draftgo/conflicts/');
161
141
  }
162
142
 
163
143
  function installPlatform(projectDir, platform, opts = {}) {
@@ -173,7 +153,6 @@ function installPlatform(projectDir, platform, opts = {}) {
173
153
  return { path: platform.mainFile, skipped: true };
174
154
  }
175
155
 
176
- ensureSharedResources(projectDir);
177
156
  const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
178
157
  const stageAsset = `${destAsset}.tmp-${token}`;
179
158
  const backupAsset = `${destAsset}.bak-${token}`;
@@ -259,6 +238,5 @@ module.exports = {
259
238
  statusPlatform,
260
239
  installAll,
261
240
  ensureRuntime,
262
- ensureSharedResources,
263
241
  splitFrontmatter,
264
242
  };
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { Readable } = require('stream');
5
+ const { DraftGoMcpClient } = require('../mcp/client');
6
+ const { BackendHttpError, WorktreeError } = require('./errors');
7
+ const { canonicalResourceType, mediaType, normalizeExtension } = require('./types');
8
+ const { normalizeSha256 } = require('./streams');
9
+
10
+ const METADATA_TOOL = 'draftgo_resource_get_metadata';
11
+ const MAX_ERROR_BYTES = 64 * 1024;
12
+ const MAX_JSON_BYTES = 1024 * 1024;
13
+
14
+ function isObject(value) {
15
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+
18
+ function toolNameMatches(name, expected) {
19
+ return name === expected
20
+ || name.endsWith(`.${expected}`)
21
+ || name.endsWith(`/${expected}`)
22
+ || name.endsWith(`:${expected}`);
23
+ }
24
+
25
+ function parseJsonText(text, label) {
26
+ try { return JSON.parse(text); } catch {
27
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', `${label} did not return structured JSON.`);
28
+ }
29
+ }
30
+
31
+ function hasOwn(value, key) {
32
+ return Object.prototype.hasOwnProperty.call(value, key);
33
+ }
34
+
35
+ function isMcpPayload(value) {
36
+ return isObject(value) && (
37
+ hasOwn(value, 'jsonrpc')
38
+ || hasOwn(value, 'result')
39
+ || hasOwn(value, 'structuredContent')
40
+ || hasOwn(value, 'structured_content')
41
+ || hasOwn(value, 'content')
42
+ || hasOwn(value, 'isError')
43
+ || hasOwn(value, 'ok')
44
+ );
45
+ }
46
+
47
+ function toolError(envelope, fallbackCode, fallbackMessage) {
48
+ const error = isObject(envelope && envelope.error) ? envelope.error : {};
49
+ return new WorktreeError(
50
+ String(firstValue(error.code, fallbackCode)),
51
+ String(firstValue(error.message, fallbackMessage)),
52
+ isObject(error.details) ? error.details : {},
53
+ );
54
+ }
55
+
56
+ function unwrapToolResult(payload) {
57
+ if (!isObject(payload)) {
58
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool returned no structured result.');
59
+ }
60
+
61
+ let result = payload;
62
+ if (hasOwn(payload, 'jsonrpc') || hasOwn(payload, 'result')) {
63
+ if (isObject(payload.error)) {
64
+ throw toolError({ error: payload.error }, 'MCP_RPC_ERROR', 'DraftGo MCP request failed.');
65
+ }
66
+ if (!isObject(payload.result)) {
67
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned no tool result.');
68
+ }
69
+ result = payload.result;
70
+ }
71
+
72
+ if (!isObject(result)) {
73
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned an invalid tool result.');
74
+ }
75
+ if (hasOwn(result, 'isError') && typeof result.isError !== 'boolean') {
76
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned an invalid isError value.');
77
+ }
78
+
79
+ let envelope;
80
+ if (hasOwn(result, 'structuredContent')) {
81
+ if (!isObject(result.structuredContent)) {
82
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned invalid structuredContent.');
83
+ }
84
+ envelope = result.structuredContent;
85
+ } else if (hasOwn(result, 'structured_content')) {
86
+ if (!isObject(result.structured_content)) {
87
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned invalid structured_content.');
88
+ }
89
+ envelope = result.structured_content;
90
+ } else if (Array.isArray(result.content)) {
91
+ const textPart = result.content.find((part) => part && part.type === 'text' && typeof part.text === 'string');
92
+ if (textPart && Buffer.byteLength(textPart.text, 'utf8') <= MAX_JSON_BYTES) {
93
+ envelope = parseJsonText(textPart.text, 'DraftGo MCP tool');
94
+ }
95
+ } else if (hasOwn(result, 'ok')) {
96
+ envelope = result;
97
+ }
98
+
99
+ if (!isObject(envelope)) {
100
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool returned no structured JSON result.');
101
+ }
102
+ if (result.isError === true) {
103
+ throw toolError(envelope, 'MCP_TOOL_ERROR', 'DraftGo MCP tool reported an error.');
104
+ }
105
+ if (envelope.ok !== true) {
106
+ throw toolError(envelope, 'INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool did not report success.');
107
+ }
108
+ return envelope.data;
109
+ }
110
+
111
+ function unwrapProtectedResult(data, label = 'DraftGo resource tool') {
112
+ if (!isObject(data) || !hasOwn(data, 'value') || typeof data.omitted !== 'boolean') {
113
+ throw new WorktreeError(
114
+ 'INVALID_PROTECTED_RESULT',
115
+ `${label} returned an invalid protected result.`,
116
+ );
117
+ }
118
+ if (data.artifacts !== undefined && !Array.isArray(data.artifacts)) {
119
+ throw new WorktreeError(
120
+ 'INVALID_PROTECTED_RESULT',
121
+ `${label} returned invalid artifact descriptors.`,
122
+ );
123
+ }
124
+ if (data.omitted || (Array.isArray(data.artifacts) && data.artifacts.length > 0)) {
125
+ throw new WorktreeError(
126
+ 'PROTECTED_RESULT_OMITTED',
127
+ `${label} omitted fields required by the CLI.`,
128
+ { artifacts: data.artifacts || [] },
129
+ );
130
+ }
131
+ return data.value;
132
+ }
133
+
134
+ function firstObject(...values) {
135
+ return values.find(isObject) || {};
136
+ }
137
+
138
+ function firstValue(...values) {
139
+ return values.find((value) => value !== undefined && value !== null && value !== '');
140
+ }
141
+
142
+ function normalizeUrl(config, value, purpose) {
143
+ if (!value) return null;
144
+ let url;
145
+ try { url = new URL(String(value), `${config.server.replace(/\/+$/, '')}/`); } catch {
146
+ throw new WorktreeError('INVALID_BACKEND_URL', `DraftGo returned an invalid ${purpose} URL.`);
147
+ }
148
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
149
+ throw new WorktreeError('INVALID_BACKEND_URL', `DraftGo returned an unsafe ${purpose} URL.`);
150
+ }
151
+ const serverOrigin = new URL(config.server).origin;
152
+ const allowed = new Set([
153
+ serverOrigin,
154
+ ...((config.checkout && config.checkout.allowed_origins) || []).map((origin) => new URL(origin).origin),
155
+ ]);
156
+ if (!allowed.has(url.origin)) {
157
+ throw new WorktreeError(
158
+ 'UNTRUSTED_BACKEND_ORIGIN',
159
+ `${purpose} URL origin is not allowlisted; refusing to send the SAT.`,
160
+ { origin: url.origin },
161
+ );
162
+ }
163
+ return url.toString();
164
+ }
165
+
166
+ function normalizeMetadata(config, resourceType, resourceId, payload) {
167
+ const data = isMcpPayload(payload)
168
+ ? unwrapProtectedResult(unwrapToolResult(payload), 'DraftGo metadata tool')
169
+ : payload;
170
+ if (!isObject(data)) {
171
+ throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo returned invalid checkout metadata.');
172
+ }
173
+ const checkout = isObject(data.checkout) ? data.checkout : {};
174
+ const checkoutConfig = firstObject(config.checkout);
175
+ const version = firstValue(data.base_version, data.version, data.revision, data.base_revision);
176
+ const hash = data.content_hash;
177
+ const contentType = data.content_type;
178
+ const backendExtension = data.file_extension;
179
+ const downloadRaw = firstValue(checkout.download, data.download_url);
180
+ const commitRaw = firstValue(checkout.commit, data.commit_url);
181
+
182
+ if (!contentType || !hash || version === undefined || !downloadRaw || !commitRaw) {
183
+ throw new WorktreeError(
184
+ 'INCOMPLETE_CHECKOUT_METADATA',
185
+ 'DraftGo checkout metadata must include content_type, SHA-256, base version/revision, download URL, and commit URL.',
186
+ );
187
+ }
188
+ const normalizedHash = normalizeSha256(hash);
189
+ if (!normalizedHash) throw new WorktreeError('INVALID_CONTENT_HASH', 'DraftGo returned an invalid content SHA-256.');
190
+
191
+ return {
192
+ resource_type: canonicalResourceType(firstValue(data.resource_type, resourceType)),
193
+ resource_id: String(firstValue(data.resource_id, resourceId)),
194
+ title: String(firstValue(data.title, '')),
195
+ route: firstValue(data.route, null),
196
+ code: firstValue(data.code, null),
197
+ slug: firstValue(data.slug, null),
198
+ content_type: String(contentType),
199
+ file_extension: normalizeExtension(contentType, backendExtension),
200
+ content_size: data.content_size == null ? null : Number(data.content_size),
201
+ content_hash: normalizedHash,
202
+ base_version: data.base_version !== undefined ? data.base_version : (data.version !== undefined ? data.version : null),
203
+ base_revision: data.base_revision !== undefined ? data.base_revision : (data.revision !== undefined ? data.revision : null),
204
+ etag: firstValue(data.etag, null),
205
+ updated_at: firstValue(data.updated_at, null),
206
+ updated_by: firstValue(data.updated_by, null),
207
+ download_url: normalizeUrl(config, downloadRaw, 'download'),
208
+ commit_url: commitRaw ? normalizeUrl(config, commitRaw, 'commit') : null,
209
+ commit_method: String(firstValue(data.commit_method, checkoutConfig.commit_method, 'PUT')).toUpperCase(),
210
+ raw: data,
211
+ };
212
+ }
213
+
214
+ async function resolveMetadata(config, resourceType, resourceId, options = {}) {
215
+ if (options.metadata) return normalizeMetadata(config, resourceType, resourceId, options.metadata);
216
+ const client = options.client || new DraftGoMcpClient(config);
217
+ if (!options.clientInitialized) await client.initialize(options);
218
+ let toolName = METADATA_TOOL;
219
+ if (typeof client.listAllTools === 'function') {
220
+ const tools = options.tools || await client.listAllTools(options);
221
+ const found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), METADATA_TOOL));
222
+ if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${METADATA_TOOL} is not available.`);
223
+ toolName = found.name;
224
+ }
225
+ const result = await client.toolsCall(toolName, {
226
+ resource_type: canonicalResourceType(resourceType),
227
+ resource_id: String(resourceId),
228
+ }, options);
229
+ return normalizeMetadata(config, resourceType, resourceId, result);
230
+ }
231
+
232
+ function authHeaders(config, extra = {}) {
233
+ return { Authorization: `Bearer ${config.token || config.sat}`, ...extra };
234
+ }
235
+
236
+ async function readBounded(response, maximum = MAX_ERROR_BYTES) {
237
+ if (!response.body) return '';
238
+ const reader = response.body.getReader();
239
+ const chunks = [];
240
+ let size = 0;
241
+ let ended = false;
242
+ while (size < maximum) {
243
+ const { done, value } = await reader.read();
244
+ if (done) { ended = true; break; }
245
+ const chunk = Buffer.from(value);
246
+ const remaining = maximum - size;
247
+ chunks.push(chunk.subarray(0, remaining));
248
+ size += Math.min(chunk.length, remaining);
249
+ }
250
+ if (!ended) await reader.cancel().catch(() => {});
251
+ return Buffer.concat(chunks).toString('utf8');
252
+ }
253
+
254
+ async function errorForResponse(response, token) {
255
+ const text = await readBounded(response);
256
+ let details = {};
257
+ try { details = text ? JSON.parse(text) : {}; } catch { details = { response: text }; }
258
+ const error = firstObject(details.error, details);
259
+ const code = firstValue(error.code, details.code, response.status === 409 || response.status === 412
260
+ ? 'RESOURCE_VERSION_CONFLICT' : 'BACKEND_HTTP_ERROR');
261
+ let message = String(firstValue(error.message, details.message, `DraftGo HTTP ${response.status}`));
262
+ if (token) message = message.split(String(token)).join('[REDACTED]');
263
+ return new BackendHttpError(response.status, code, message, isObject(error.details) ? error.details : details);
264
+ }
265
+
266
+ async function download(config, metadata, options = {}) {
267
+ const response = await (options.fetch || fetch)(metadata.download_url, {
268
+ method: 'GET',
269
+ headers: authHeaders(config, { Accept: '*/*' }),
270
+ signal: options.signal,
271
+ });
272
+ if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
273
+ return response;
274
+ }
275
+
276
+ async function commit(config, metadata, localPath, current, options = {}) {
277
+ if (!metadata.commit_url) {
278
+ throw new WorktreeError(
279
+ 'COMMIT_URL_UNAVAILABLE',
280
+ 'DraftGo metadata did not provide a commit URL.',
281
+ );
282
+ }
283
+ if (!['POST', 'PUT', 'PATCH'].includes(metadata.commit_method)) {
284
+ throw new WorktreeError('INVALID_COMMIT_METHOD', 'DraftGo returned an unsupported commit method.');
285
+ }
286
+ const headers = authHeaders(config, {
287
+ 'Content-Type': metadata.content_type,
288
+ 'Content-Length': String(current.size),
289
+ 'X-Content-SHA256': current.hash,
290
+ });
291
+ if (metadata.etag) headers['If-Match'] = String(metadata.etag);
292
+ else if (metadata.base_version != null) headers['If-Match'] = String(metadata.base_version);
293
+ else if (metadata.base_revision != null) headers['If-Match'] = String(metadata.base_revision);
294
+ if (metadata.base_version != null) headers['DraftGo-Base-Version'] = String(metadata.base_version);
295
+ if (metadata.base_revision != null) headers['DraftGo-Base-Revision'] = String(metadata.base_revision);
296
+
297
+ const response = await (options.fetch || fetch)(metadata.commit_url, {
298
+ method: metadata.commit_method,
299
+ headers,
300
+ body: fs.createReadStream(localPath),
301
+ duplex: 'half',
302
+ signal: options.signal,
303
+ });
304
+ if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
305
+ const text = await readBounded(response, MAX_JSON_BYTES);
306
+ if (!text.trim()) return {};
307
+ return parseJsonText(text, 'DraftGo commit endpoint');
308
+ }
309
+
310
+ function nodeReadable(response) {
311
+ if (!response.body) throw new WorktreeError('DOWNLOAD_INCOMPLETE', 'DraftGo response has no body.');
312
+ return typeof response.body.getReader === 'function' ? Readable.fromWeb(response.body) : response.body;
313
+ }
314
+
315
+ module.exports = {
316
+ METADATA_TOOL,
317
+ toolNameMatches,
318
+ unwrapToolResult,
319
+ unwrapProtectedResult,
320
+ normalizeMetadata,
321
+ resolveMetadata,
322
+ download,
323
+ commit,
324
+ nodeReadable,
325
+ errorForResponse,
326
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ class WorktreeError extends Error {
4
+ constructor(code, message, details = {}) {
5
+ super(message);
6
+ this.name = 'WorktreeError';
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ }
11
+
12
+ class BackendHttpError extends WorktreeError {
13
+ constructor(status, code, message, details = {}) {
14
+ super(code || 'BACKEND_HTTP_ERROR', message || `Backend request failed with HTTP ${status}.`, details);
15
+ this.name = 'BackendHttpError';
16
+ this.status = status;
17
+ }
18
+ }
19
+
20
+ function isVersionConflict(error) {
21
+ return Boolean(error) && (
22
+ error.code === 'RESOURCE_VERSION_CONFLICT'
23
+ || error.status === 409
24
+ || error.status === 412
25
+ );
26
+ }
27
+
28
+ module.exports = { WorktreeError, BackendHttpError, isVersionConflict };