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,438 @@
|
|
|
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
|
+
const MAX_CHECKOUT_JSON_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
const FINAL_OPERATIONS = Object.freeze({
|
|
15
|
+
pages: Object.freeze({ checkout: 'listPageCheckout', commit: 'updatePageCommit', contentType: 'text/html; charset=utf-8' }),
|
|
16
|
+
navigations: Object.freeze({ checkout: 'listNavigationCheckout', commit: 'updateNavigationCommit', contentType: 'text/html; charset=utf-8' }),
|
|
17
|
+
docs: Object.freeze({ checkout: 'listContentArticleCheckout', commit: 'updateContentArticleCommit', contentType: 'text/html; charset=utf-8', jsonCommit: true }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function isObject(value) {
|
|
21
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toolNameMatches(name, expected) {
|
|
25
|
+
return name === expected
|
|
26
|
+
|| name.endsWith(`.${expected}`)
|
|
27
|
+
|| name.endsWith(`/${expected}`)
|
|
28
|
+
|| name.endsWith(`:${expected}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseJsonText(text, label) {
|
|
32
|
+
try { return JSON.parse(text); } catch {
|
|
33
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', `${label} did not return structured JSON.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasOwn(value, key) {
|
|
38
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isMcpPayload(value) {
|
|
42
|
+
return isObject(value) && (
|
|
43
|
+
hasOwn(value, 'jsonrpc')
|
|
44
|
+
|| hasOwn(value, 'result')
|
|
45
|
+
|| hasOwn(value, 'structuredContent')
|
|
46
|
+
|| hasOwn(value, 'structured_content')
|
|
47
|
+
|| hasOwn(value, 'content')
|
|
48
|
+
|| hasOwn(value, 'isError')
|
|
49
|
+
|| hasOwn(value, 'ok')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toolError(envelope, fallbackCode, fallbackMessage) {
|
|
54
|
+
const error = isObject(envelope && envelope.error) ? envelope.error : {};
|
|
55
|
+
return new WorktreeError(
|
|
56
|
+
String(firstValue(error.code, fallbackCode)),
|
|
57
|
+
String(firstValue(error.message, fallbackMessage)),
|
|
58
|
+
isObject(error.details) ? error.details : {},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function unwrapToolResult(payload) {
|
|
63
|
+
if (!isObject(payload)) {
|
|
64
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool returned no structured result.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let result = payload;
|
|
68
|
+
if (hasOwn(payload, 'jsonrpc') || hasOwn(payload, 'result')) {
|
|
69
|
+
if (isObject(payload.error)) {
|
|
70
|
+
throw toolError({ error: payload.error }, 'MCP_RPC_ERROR', 'DraftGo MCP request failed.');
|
|
71
|
+
}
|
|
72
|
+
if (!isObject(payload.result)) {
|
|
73
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned no tool result.');
|
|
74
|
+
}
|
|
75
|
+
result = payload.result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isObject(result)) {
|
|
79
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned an invalid tool result.');
|
|
80
|
+
}
|
|
81
|
+
if (hasOwn(result, 'isError') && typeof result.isError !== 'boolean') {
|
|
82
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned an invalid isError value.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let envelope;
|
|
86
|
+
if (hasOwn(result, 'structuredContent')) {
|
|
87
|
+
if (!isObject(result.structuredContent)) {
|
|
88
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned invalid structuredContent.');
|
|
89
|
+
}
|
|
90
|
+
envelope = result.structuredContent;
|
|
91
|
+
} else if (hasOwn(result, 'structured_content')) {
|
|
92
|
+
if (!isObject(result.structured_content)) {
|
|
93
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP returned invalid structured_content.');
|
|
94
|
+
}
|
|
95
|
+
envelope = result.structured_content;
|
|
96
|
+
} else if (Array.isArray(result.content)) {
|
|
97
|
+
const textPart = result.content.find((part) => part && part.type === 'text' && typeof part.text === 'string');
|
|
98
|
+
if (textPart && Buffer.byteLength(textPart.text, 'utf8') <= MAX_JSON_BYTES) {
|
|
99
|
+
envelope = parseJsonText(textPart.text, 'DraftGo MCP tool');
|
|
100
|
+
}
|
|
101
|
+
} else if (hasOwn(result, 'ok')) {
|
|
102
|
+
envelope = result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isObject(envelope)) {
|
|
106
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool returned no structured JSON result.');
|
|
107
|
+
}
|
|
108
|
+
if (result.isError === true) {
|
|
109
|
+
throw toolError(envelope, 'MCP_TOOL_ERROR', 'DraftGo MCP tool reported an error.');
|
|
110
|
+
}
|
|
111
|
+
if (envelope.ok !== true) {
|
|
112
|
+
throw toolError(envelope, 'INVALID_BACKEND_RESPONSE', 'DraftGo MCP tool did not report success.');
|
|
113
|
+
}
|
|
114
|
+
return envelope.data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unwrapProtectedResult(data, label = 'DraftGo resource tool') {
|
|
118
|
+
if (!isObject(data) || !hasOwn(data, 'value') || typeof data.omitted !== 'boolean') {
|
|
119
|
+
throw new WorktreeError(
|
|
120
|
+
'INVALID_PROTECTED_RESULT',
|
|
121
|
+
`${label} returned an invalid protected result.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (data.artifacts !== undefined && !Array.isArray(data.artifacts)) {
|
|
125
|
+
throw new WorktreeError(
|
|
126
|
+
'INVALID_PROTECTED_RESULT',
|
|
127
|
+
`${label} returned invalid artifact descriptors.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (data.omitted || (Array.isArray(data.artifacts) && data.artifacts.length > 0)) {
|
|
131
|
+
throw new WorktreeError(
|
|
132
|
+
'PROTECTED_RESULT_OMITTED',
|
|
133
|
+
`${label} omitted fields required by the CLI.`,
|
|
134
|
+
{ artifacts: data.artifacts || [] },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return data.value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function firstObject(...values) {
|
|
141
|
+
return values.find(isObject) || {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function firstValue(...values) {
|
|
145
|
+
return values.find((value) => value !== undefined && value !== null && value !== '');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function operationPath(config, template, resourceId) {
|
|
149
|
+
const encoded = encodeURIComponent(String(resourceId));
|
|
150
|
+
const relative = String(template || '').replace(/\{[^}]+\}/, encoded);
|
|
151
|
+
if (!relative.startsWith('/api/')) {
|
|
152
|
+
throw new WorktreeError('INVALID_WORKFLOW_PATH', 'DraftGo checkout workflow returned an invalid API path.');
|
|
153
|
+
}
|
|
154
|
+
return new URL(relative, `${config.server.replace(/\/+$/, '')}/`).toString();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function finalMetadata(config, resourceType, resourceId, session, options = {}) {
|
|
158
|
+
// Loaded lazily to avoid the API command's dependency on this backend module.
|
|
159
|
+
const { registryRevision, describeOperation } = require('../commands/api');
|
|
160
|
+
const { TOOL_NAMES, callStructured } = require('../mcp/tools');
|
|
161
|
+
const type = canonicalResourceType(resourceType);
|
|
162
|
+
const spec = FINAL_OPERATIONS[type];
|
|
163
|
+
const revision = await registryRevision(session, spec.checkout);
|
|
164
|
+
const checkout = await describeOperation(options.projectDir || process.cwd(), config, session, spec.checkout, revision);
|
|
165
|
+
const commit = await describeOperation(options.projectDir || process.cwd(), config, session, spec.commit, revision);
|
|
166
|
+
const checkoutOperation = checkout && checkout.operation || {};
|
|
167
|
+
const commitOperation = commit && commit.operation || {};
|
|
168
|
+
const checkoutResult = await callStructured(session, TOOL_NAMES.apiCall, {
|
|
169
|
+
operation_id: spec.checkout,
|
|
170
|
+
registry_revision: revision,
|
|
171
|
+
path: { id: Number(resourceId) },
|
|
172
|
+
}, options);
|
|
173
|
+
const workflow = checkoutResult && checkoutResult.workflow || {};
|
|
174
|
+
if (workflow.required !== true || workflow.workflow !== 'checkout_commit') {
|
|
175
|
+
throw new WorktreeError('INVALID_WORKFLOW_DESCRIPTOR', 'DraftGo did not return a checkout/commit workflow descriptor.');
|
|
176
|
+
}
|
|
177
|
+
const checkoutURL = operationPath(config, checkoutOperation.path || workflow.path, resourceId);
|
|
178
|
+
const response = await (options.fetch || fetch)(checkoutURL, {
|
|
179
|
+
method: String(checkoutOperation.method || 'GET').toUpperCase(),
|
|
180
|
+
headers: authHeaders(config, { Accept: 'application/json' }),
|
|
181
|
+
signal: options.signal,
|
|
182
|
+
redirect: 'error',
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
|
|
185
|
+
const raw = await readBounded(response, MAX_CHECKOUT_JSON_BYTES, true);
|
|
186
|
+
const value = parseJsonText(raw, 'DraftGo checkout endpoint');
|
|
187
|
+
const version = firstValue(value.content_version, value.revision);
|
|
188
|
+
const content = typeof value.content === 'string' ? value.content : null;
|
|
189
|
+
const contentURL = firstValue(value.content_url, checkoutURL);
|
|
190
|
+
const contentType = type === 'docs' ? spec.contentType : spec.contentType;
|
|
191
|
+
const size = content == null ? null : Buffer.byteLength(content, 'utf8');
|
|
192
|
+
return normalizeMetadata(config, type, resourceId, {
|
|
193
|
+
resource_type: type,
|
|
194
|
+
resource_id: String(resourceId),
|
|
195
|
+
title: value.title || '', route: value.route || null, code: value.code || null, slug: value.slug || null,
|
|
196
|
+
content_type: contentType,
|
|
197
|
+
file_extension: '.html',
|
|
198
|
+
content_size: size,
|
|
199
|
+
content_hash: value.content_hash,
|
|
200
|
+
...(type === 'docs' ? { base_revision: version } : { base_version: version }),
|
|
201
|
+
etag: value.content_hash,
|
|
202
|
+
download_url: contentURL,
|
|
203
|
+
commit_url: operationPath(config, commitOperation.path, resourceId),
|
|
204
|
+
commit_method: String(commitOperation.method || 'PUT').toUpperCase(),
|
|
205
|
+
inline_content: content,
|
|
206
|
+
json_commit: spec.jsonCommit === true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeUrl(config, value, purpose) {
|
|
211
|
+
if (!value) return null;
|
|
212
|
+
let url;
|
|
213
|
+
try { url = new URL(String(value), `${config.server.replace(/\/+$/, '')}/`); } catch {
|
|
214
|
+
throw new WorktreeError('INVALID_BACKEND_URL', `DraftGo returned an invalid ${purpose} URL.`);
|
|
215
|
+
}
|
|
216
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
217
|
+
throw new WorktreeError('INVALID_BACKEND_URL', `DraftGo returned an unsafe ${purpose} URL.`);
|
|
218
|
+
}
|
|
219
|
+
const serverOrigin = new URL(config.server).origin;
|
|
220
|
+
const allowed = new Set([
|
|
221
|
+
serverOrigin,
|
|
222
|
+
...((config.checkout && config.checkout.allowed_origins) || []).map((origin) => new URL(origin).origin),
|
|
223
|
+
]);
|
|
224
|
+
if (!allowed.has(url.origin)) {
|
|
225
|
+
throw new WorktreeError(
|
|
226
|
+
'UNTRUSTED_BACKEND_ORIGIN',
|
|
227
|
+
`${purpose} URL origin is not allowlisted; refusing to send the API Key.`,
|
|
228
|
+
{ origin: url.origin },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return url.toString();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeMetadata(config, resourceType, resourceId, payload) {
|
|
235
|
+
const data = isMcpPayload(payload)
|
|
236
|
+
? unwrapProtectedResult(unwrapToolResult(payload), 'DraftGo metadata tool')
|
|
237
|
+
: payload;
|
|
238
|
+
if (!isObject(data)) {
|
|
239
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo returned invalid checkout metadata.');
|
|
240
|
+
}
|
|
241
|
+
const checkout = isObject(data.checkout) ? data.checkout : {};
|
|
242
|
+
const checkoutConfig = firstObject(config.checkout);
|
|
243
|
+
const version = firstValue(data.base_version, data.version, data.revision, data.base_revision);
|
|
244
|
+
const hash = data.content_hash;
|
|
245
|
+
const contentType = data.content_type;
|
|
246
|
+
const backendExtension = data.file_extension;
|
|
247
|
+
const downloadRaw = firstValue(checkout.download, data.download_url);
|
|
248
|
+
const commitRaw = firstValue(checkout.commit, data.commit_url);
|
|
249
|
+
|
|
250
|
+
if (!contentType || !hash || version === undefined || !downloadRaw || !commitRaw) {
|
|
251
|
+
throw new WorktreeError(
|
|
252
|
+
'INCOMPLETE_CHECKOUT_METADATA',
|
|
253
|
+
'DraftGo checkout metadata must include content_type, SHA-256, base version/revision, download URL, and commit URL.',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const normalizedHash = normalizeSha256(hash);
|
|
257
|
+
if (!normalizedHash) throw new WorktreeError('INVALID_CONTENT_HASH', 'DraftGo returned an invalid content SHA-256.');
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
resource_type: canonicalResourceType(firstValue(data.resource_type, resourceType)),
|
|
261
|
+
resource_id: String(firstValue(data.resource_id, resourceId)),
|
|
262
|
+
title: String(firstValue(data.title, '')),
|
|
263
|
+
route: firstValue(data.route, null),
|
|
264
|
+
code: firstValue(data.code, null),
|
|
265
|
+
slug: firstValue(data.slug, null),
|
|
266
|
+
content_type: String(contentType),
|
|
267
|
+
file_extension: normalizeExtension(contentType, backendExtension),
|
|
268
|
+
content_size: data.content_size == null ? null : Number(data.content_size),
|
|
269
|
+
content_hash: normalizedHash,
|
|
270
|
+
base_version: data.base_version !== undefined ? data.base_version : (data.version !== undefined ? data.version : null),
|
|
271
|
+
base_revision: data.base_revision !== undefined ? data.base_revision : (data.revision !== undefined ? data.revision : null),
|
|
272
|
+
etag: firstValue(data.etag, null),
|
|
273
|
+
updated_at: firstValue(data.updated_at, null),
|
|
274
|
+
updated_by: firstValue(data.updated_by, null),
|
|
275
|
+
download_url: normalizeUrl(config, downloadRaw, 'download'),
|
|
276
|
+
commit_url: commitRaw ? normalizeUrl(config, commitRaw, 'commit') : null,
|
|
277
|
+
commit_method: String(firstValue(data.commit_method, checkoutConfig.commit_method, 'PUT')).toUpperCase(),
|
|
278
|
+
raw: data,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function resolveMetadata(config, resourceType, resourceId, options = {}) {
|
|
283
|
+
if (options.metadata) return normalizeMetadata(config, resourceType, resourceId, options.metadata);
|
|
284
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
285
|
+
if (!options.clientInitialized) await client.initialize(options);
|
|
286
|
+
let toolName = METADATA_TOOL;
|
|
287
|
+
if (typeof client.listAllTools === 'function') {
|
|
288
|
+
const tools = options.tools || await client.listAllTools(options);
|
|
289
|
+
const found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), METADATA_TOOL));
|
|
290
|
+
if (!found) {
|
|
291
|
+
const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
|
|
292
|
+
const session = await openToolSession(config, [
|
|
293
|
+
TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
294
|
+
], { ...options, client });
|
|
295
|
+
return finalMetadata(config, resourceType, resourceId, session, options);
|
|
296
|
+
}
|
|
297
|
+
toolName = found.name;
|
|
298
|
+
} else if (Array.isArray(options.tools)) {
|
|
299
|
+
const found = options.tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), METADATA_TOOL));
|
|
300
|
+
if (!found) {
|
|
301
|
+
const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
|
|
302
|
+
const session = await openToolSession(config, [
|
|
303
|
+
TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
304
|
+
], { ...options, client });
|
|
305
|
+
return finalMetadata(config, resourceType, resourceId, session, options);
|
|
306
|
+
}
|
|
307
|
+
toolName = found.name;
|
|
308
|
+
}
|
|
309
|
+
const result = await client.toolsCall(toolName, {
|
|
310
|
+
resource_type: canonicalResourceType(resourceType),
|
|
311
|
+
resource_id: String(resourceId),
|
|
312
|
+
}, options);
|
|
313
|
+
return normalizeMetadata(config, resourceType, resourceId, result);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function authHeaders(config, extra = {}) {
|
|
317
|
+
return { Authorization: `Bearer ${config.token || config.sat}`, ...extra };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function readBounded(response, maximum = MAX_ERROR_BYTES, rejectOverflow = false) {
|
|
321
|
+
if (!response.body) return '';
|
|
322
|
+
const reader = response.body.getReader();
|
|
323
|
+
const chunks = [];
|
|
324
|
+
let size = 0;
|
|
325
|
+
try {
|
|
326
|
+
while (true) {
|
|
327
|
+
const { done, value } = await reader.read();
|
|
328
|
+
if (done) break;
|
|
329
|
+
const chunk = Buffer.from(value);
|
|
330
|
+
if (size + chunk.length > maximum) {
|
|
331
|
+
if (rejectOverflow) throw new WorktreeError('RESPONSE_TOO_LARGE',
|
|
332
|
+
'DraftGo response exceeds the explicit ' + maximum + '-byte limit; local content was preserved.', { max_bytes: maximum });
|
|
333
|
+
chunks.push(chunk.subarray(0, maximum - size));
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
chunks.push(chunk);
|
|
337
|
+
size += chunk.length;
|
|
338
|
+
}
|
|
339
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
340
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function errorForResponse(response, token) {
|
|
344
|
+
const text = await readBounded(response);
|
|
345
|
+
let details = {};
|
|
346
|
+
try { details = text ? JSON.parse(text) : {}; } catch { details = { response: text }; }
|
|
347
|
+
const error = firstObject(details.error, details);
|
|
348
|
+
const code = firstValue(error.code, details.code, response.status === 409 || response.status === 412
|
|
349
|
+
? 'RESOURCE_VERSION_CONFLICT' : 'BACKEND_HTTP_ERROR');
|
|
350
|
+
let message = String(firstValue(error.message, details.message, `DraftGo HTTP ${response.status}`));
|
|
351
|
+
if (token) message = message.split(String(token)).join('[REDACTED]');
|
|
352
|
+
return new BackendHttpError(response.status, code, message, isObject(error.details) ? error.details : details);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function download(config, metadata, options = {}) {
|
|
356
|
+
const raw = metadata.raw && typeof metadata.raw === 'object' ? metadata.raw : {};
|
|
357
|
+
if (typeof raw.inline_content === 'string') {
|
|
358
|
+
return new Response(raw.inline_content, { headers: { 'content-type': metadata.content_type } });
|
|
359
|
+
}
|
|
360
|
+
const response = await (options.fetch || fetch)(metadata.download_url, {
|
|
361
|
+
method: 'GET',
|
|
362
|
+
headers: authHeaders(config, { Accept: '*/*' }),
|
|
363
|
+
signal: options.signal,
|
|
364
|
+
redirect: 'error',
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
|
|
367
|
+
return response;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function commit(config, metadata, localPath, current, options = {}) {
|
|
371
|
+
if (!metadata.commit_url) {
|
|
372
|
+
throw new WorktreeError(
|
|
373
|
+
'COMMIT_URL_UNAVAILABLE',
|
|
374
|
+
'DraftGo metadata did not provide a commit URL.',
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (!['POST', 'PUT', 'PATCH'].includes(metadata.commit_method)) {
|
|
378
|
+
throw new WorktreeError('INVALID_COMMIT_METHOD', 'DraftGo returned an unsupported commit method.');
|
|
379
|
+
}
|
|
380
|
+
const headers = authHeaders(config, {
|
|
381
|
+
'Content-Type': metadata.content_type,
|
|
382
|
+
'Content-Length': String(current.size),
|
|
383
|
+
'X-Content-SHA256': current.hash,
|
|
384
|
+
});
|
|
385
|
+
if (metadata.etag) headers['If-Match'] = String(metadata.etag);
|
|
386
|
+
else if (metadata.base_version != null) headers['If-Match'] = String(metadata.base_version);
|
|
387
|
+
else if (metadata.base_revision != null) headers['If-Match'] = String(metadata.base_revision);
|
|
388
|
+
if (metadata.base_version != null) headers['DraftGo-Base-Version'] = String(metadata.base_version);
|
|
389
|
+
if (metadata.base_revision != null) headers['DraftGo-Base-Revision'] = String(metadata.base_revision);
|
|
390
|
+
|
|
391
|
+
let body;
|
|
392
|
+
let duplex;
|
|
393
|
+
if (metadata.raw && metadata.raw.json_commit === true) {
|
|
394
|
+
const content = fs.readFileSync(localPath, 'utf8');
|
|
395
|
+
body = JSON.stringify({
|
|
396
|
+
content,
|
|
397
|
+
...(metadata.base_revision != null ? { base_revision: metadata.base_revision } : {}),
|
|
398
|
+
...(metadata.base_revision == null && metadata.content_hash ? { base_hash: metadata.content_hash } : {}),
|
|
399
|
+
});
|
|
400
|
+
headers['Content-Type'] = 'application/json';
|
|
401
|
+
headers['Content-Length'] = String(Buffer.byteLength(body, 'utf8'));
|
|
402
|
+
} else {
|
|
403
|
+
body = fs.createReadStream(localPath);
|
|
404
|
+
duplex = 'half';
|
|
405
|
+
}
|
|
406
|
+
const response = await (options.fetch || fetch)(metadata.commit_url, {
|
|
407
|
+
method: metadata.commit_method,
|
|
408
|
+
headers,
|
|
409
|
+
body,
|
|
410
|
+
...(duplex ? { duplex } : {}),
|
|
411
|
+
signal: options.signal,
|
|
412
|
+
redirect: 'error',
|
|
413
|
+
});
|
|
414
|
+
if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
|
|
415
|
+
const text = await readBounded(response, MAX_JSON_BYTES, true);
|
|
416
|
+
if (!text.trim()) return {};
|
|
417
|
+
return parseJsonText(text, 'DraftGo commit endpoint');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function nodeReadable(response) {
|
|
421
|
+
if (!response.body) throw new WorktreeError('DOWNLOAD_INCOMPLETE', 'DraftGo response has no body.');
|
|
422
|
+
return typeof response.body.getReader === 'function' ? Readable.fromWeb(response.body) : response.body;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
METADATA_TOOL,
|
|
427
|
+
readBounded,
|
|
428
|
+
MAX_CHECKOUT_JSON_BYTES,
|
|
429
|
+
toolNameMatches,
|
|
430
|
+
unwrapToolResult,
|
|
431
|
+
unwrapProtectedResult,
|
|
432
|
+
normalizeMetadata,
|
|
433
|
+
resolveMetadata,
|
|
434
|
+
download,
|
|
435
|
+
commit,
|
|
436
|
+
nodeReadable,
|
|
437
|
+
errorForResponse,
|
|
438
|
+
};
|
|
@@ -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 };
|