draftgo-cli 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/resources/skill/SKILL.md +4 -3
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/ai.md +1 -1
- package/resources/skill/references/app-api.md +29 -27
- package/resources/skill/references/chat-sdk.md +10 -184
- package/resources/skill/references/checkout.md +43 -118
- package/resources/skill/references/development.md +0 -3
- package/resources/skill/references/frontend.md +10 -25
- package/resources/skill/references/mcp.md +15 -68
- package/resources/skill/references/methods.md +8 -11
- package/resources/skill/references/modules.md +24 -35
- package/resources/skill/references/runtime.md +11 -102
- package/resources/skill/references/services.md +89 -17
- package/src/commands/checkout.js +50 -3
- package/src/commands/commit.js +1 -1
- package/src/commands/conflict.js +1 -1
- package/src/commands/diff.js +1 -1
- package/src/commands/help.js +4 -4
- package/src/commands/reconcile.js +1 -1
- package/src/worktree/backend.js +157 -21
- package/src/worktree/index.js +3 -0
- package/src/worktree/types.js +22 -18
package/src/commands/checkout.js
CHANGED
|
@@ -1,15 +1,62 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
5
|
+
const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
|
|
6
|
+
const { canonicalResourceType } = require('../worktree/types');
|
|
4
7
|
const { checkoutResources } = require('../worktree');
|
|
8
|
+
const map = require('./map');
|
|
9
|
+
|
|
10
|
+
const USAGE = 'Usage: draftgo checkout <pages|nav|docs|services> <id...>';
|
|
11
|
+
|
|
12
|
+
async function resolveUniqueHit(projectDir, resourceType, flags) {
|
|
13
|
+
const canonical = canonicalResourceType(resourceType);
|
|
14
|
+
if (canonical === 'services') {
|
|
15
|
+
log.err('Checkout --route and --title are not supported for services.');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const config = loadProjectConfig(projectDir);
|
|
19
|
+
const resourceTypes = [map.normalizeMapResourceType(resourceType)];
|
|
20
|
+
const searching = flags.route != null || flags.title != null;
|
|
21
|
+
let remote;
|
|
22
|
+
try {
|
|
23
|
+
const session = await openToolSession(config, [searching ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList]);
|
|
24
|
+
remote = await map.listMapResources(session, resourceTypes, flags);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!error || error.code !== 'MCP_TOOL_UNAVAILABLE') throw error;
|
|
27
|
+
const session = await openToolSession(config, [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
|
|
28
|
+
remote = await map.listRegistryResources(projectDir, config, session, resourceTypes, flags);
|
|
29
|
+
}
|
|
30
|
+
const matches = (remote && remote.resources || []).filter((resource) => map.resourceMatches(resource, flags));
|
|
31
|
+
if (matches.length === 0) {
|
|
32
|
+
log.err(`No ${canonical} matched the given --route/--title.`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (matches.length !== 1) {
|
|
36
|
+
log.err(`Checkout --route/--title matched ${matches.length} ${canonical}; require exactly one.`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return String(matches[0].resource_id);
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
async function checkout(projectDir, positional, flags = {}) {
|
|
7
43
|
const [resourceType, ...ids] = positional;
|
|
8
|
-
|
|
9
|
-
|
|
44
|
+
const selecting = flags.route != null || flags.title != null;
|
|
45
|
+
if (!resourceType || (!ids.length && !selecting)) {
|
|
46
|
+
log.err(USAGE);
|
|
10
47
|
return 1;
|
|
11
48
|
}
|
|
12
|
-
|
|
49
|
+
let resourceIds = ids;
|
|
50
|
+
if (selecting) {
|
|
51
|
+
if (ids.length) {
|
|
52
|
+
log.err('Checkout --route/--title cannot be combined with resource ids.');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const resourceId = await resolveUniqueHit(projectDir, resourceType, flags);
|
|
56
|
+
if (!resourceId) return 1;
|
|
57
|
+
resourceIds = [resourceId];
|
|
58
|
+
}
|
|
59
|
+
const results = await checkoutResources(projectDir, resourceType, resourceIds, { force: Boolean(flags.force) });
|
|
13
60
|
if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
|
|
14
61
|
else for (const entry of results) log.ok(`Checked out ${entry.resource_type} ${entry.resource_id} -> ${entry.local_path}`);
|
|
15
62
|
return 0;
|
package/src/commands/commit.js
CHANGED
|
@@ -6,7 +6,7 @@ const { commitResources } = require('../worktree');
|
|
|
6
6
|
async function commit(projectDir, positional, flags = {}) {
|
|
7
7
|
const [resourceType, ...ids] = positional;
|
|
8
8
|
if (!resourceType || !ids.length) {
|
|
9
|
-
log.err('Usage: draftgo commit <pages|nav|docs> <id...>');
|
|
9
|
+
log.err('Usage: draftgo commit <pages|nav|docs|services> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
12
|
const streamed = [];
|
package/src/commands/conflict.js
CHANGED
|
@@ -15,7 +15,7 @@ function printRecord(record) {
|
|
|
15
15
|
async function conflict(projectDir, positional, flags = {}) {
|
|
16
16
|
const [action, resourceType, resourceId] = positional;
|
|
17
17
|
if (!['show', 'resolve'].includes(action) || !resourceType || !resourceId) {
|
|
18
|
-
log.err('Usage: draftgo conflict <show|resolve> <pages|nav|docs> <id>');
|
|
18
|
+
log.err('Usage: draftgo conflict <show|resolve> <pages|nav|docs|services> <id>');
|
|
19
19
|
return 1;
|
|
20
20
|
}
|
|
21
21
|
const record = action === 'show'
|
package/src/commands/diff.js
CHANGED
|
@@ -8,7 +8,7 @@ const { absolutePath } = require('../worktree/manifest');
|
|
|
8
8
|
function diff(projectDir, positional, flags = {}) {
|
|
9
9
|
const [resourceType, resourceId] = positional;
|
|
10
10
|
if (!resourceType || !resourceId) {
|
|
11
|
-
log.err('Usage: draftgo diff <pages|nav|docs> <id>');
|
|
11
|
+
log.err('Usage: draftgo diff <pages|nav|docs|services> <id>');
|
|
12
12
|
return 1;
|
|
13
13
|
}
|
|
14
14
|
const result = diffResource(projectDir, resourceType, resourceId);
|
package/src/commands/help.js
CHANGED
|
@@ -18,7 +18,7 @@ Tailwind CSS 4, and the DraftGo built-in component library (draftgo/*).
|
|
|
18
18
|
Provider brands and shared icons are served from the local /assets/providers and
|
|
19
19
|
/assets/icons directories; the Page runtime does not require third-party CDNs.
|
|
20
20
|
System MCP handles live discovery and structured resources; complete page,
|
|
21
|
-
navigation, and
|
|
21
|
+
navigation, document, and Go source bodies use checkout/commit outside MCP context.
|
|
22
22
|
The installed Skill routes agents to task-specific References and MCP tools.
|
|
23
23
|
|
|
24
24
|
Usage:
|
|
@@ -113,7 +113,7 @@ Usage:
|
|
|
113
113
|
draftgo -h | --help Show this help.
|
|
114
114
|
|
|
115
115
|
Resource types:
|
|
116
|
-
pages | nav/navigations | docs/articles
|
|
116
|
+
pages | nav/navigations | docs/articles | services
|
|
117
117
|
|
|
118
118
|
Important flags:
|
|
119
119
|
--project <dir> Operate on <dir> instead of the current directory.
|
|
@@ -134,8 +134,8 @@ Important flags:
|
|
|
134
134
|
--output json Print machine-readable output where supported.
|
|
135
135
|
--type <type> (map) pages | nav/navigations | docs/articles;
|
|
136
136
|
(capabilities) alias for a Registry module filter.
|
|
137
|
-
--route <path> (map) Select resources with this exact route.
|
|
138
|
-
--title <title> (map) Select resources with this exact title.
|
|
137
|
+
--route <path> (map/checkout) Select resources with this exact route.
|
|
138
|
+
--title <title> (map/checkout) Select resources with this exact title.
|
|
139
139
|
--limit <1-100> (map) Maximum resources returned per type/page;
|
|
140
140
|
defaults to 20.
|
|
141
141
|
--cursor <cursor> (map) Continue one typed map result page.
|
|
@@ -6,7 +6,7 @@ const { reconcileResources } = require('../worktree');
|
|
|
6
6
|
async function reconcile(projectDir, positional, flags = {}) {
|
|
7
7
|
const [resourceType, ...ids] = positional;
|
|
8
8
|
if (!resourceType || !ids.length) {
|
|
9
|
-
log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
|
|
9
|
+
log.err('Usage: draftgo reconcile <pages|nav|docs|services> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
12
|
const results = await reconcileResources(projectDir, resourceType, ids);
|
package/src/worktree/backend.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
const { Readable } = require('stream');
|
|
5
7
|
const { DraftGoMcpClient } = require('../mcp/client');
|
|
6
8
|
const { BackendHttpError, WorktreeError } = require('./errors');
|
|
@@ -15,8 +17,21 @@ const FINAL_OPERATIONS = Object.freeze({
|
|
|
15
17
|
pages: Object.freeze({ checkout: 'listPageCheckout', commit: 'updatePageCommit', contentType: 'text/html; charset=utf-8' }),
|
|
16
18
|
navigations: Object.freeze({ checkout: 'listNavigationCheckout', commit: 'updateNavigationCommit', contentType: 'text/html; charset=utf-8' }),
|
|
17
19
|
docs: Object.freeze({ checkout: 'listContentArticleCheckout', commit: 'updateContentArticleCommit', contentType: 'text/html; charset=utf-8', jsonCommit: true }),
|
|
20
|
+
services: Object.freeze({
|
|
21
|
+
checkout: 'listCustomServiceDraft',
|
|
22
|
+
commit: 'updateCustomServiceDraft',
|
|
23
|
+
contentType: 'text/plain; charset=utf-8',
|
|
24
|
+
jsonCommit: true,
|
|
25
|
+
commitField: 'source',
|
|
26
|
+
fileExtension: '.go',
|
|
27
|
+
sdk: 'listCustomServiceSdk',
|
|
28
|
+
}),
|
|
18
29
|
});
|
|
19
30
|
|
|
31
|
+
function sha256Utf8(value) {
|
|
32
|
+
return crypto.createHash('sha256').update(String(value), 'utf8').digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function isObject(value) {
|
|
21
36
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
22
37
|
}
|
|
@@ -184,29 +199,126 @@ async function finalMetadata(config, resourceType, resourceId, session, options
|
|
|
184
199
|
if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
|
|
185
200
|
const raw = await readBounded(response, MAX_CHECKOUT_JSON_BYTES, true);
|
|
186
201
|
const value = parseJsonText(raw, 'DraftGo checkout endpoint');
|
|
202
|
+
if (type === 'services' && typeof value.source !== 'string') {
|
|
203
|
+
throw new WorktreeError('INVALID_BACKEND_RESPONSE', 'DraftGo service draft did not include source text.');
|
|
204
|
+
}
|
|
187
205
|
const version = firstValue(value.content_version, value.revision);
|
|
188
|
-
const
|
|
206
|
+
const sourceContent = type === 'services' ? value.source : null;
|
|
207
|
+
const content = sourceContent != null
|
|
208
|
+
? sourceContent
|
|
209
|
+
: (typeof value.content === 'string' ? value.content : null);
|
|
189
210
|
const contentURL = firstValue(value.content_url, checkoutURL);
|
|
190
|
-
const contentType =
|
|
211
|
+
const contentType = spec.contentType;
|
|
191
212
|
const size = content == null ? null : Buffer.byteLength(content, 'utf8');
|
|
213
|
+
const contentHash = sourceContent != null ? sha256Utf8(sourceContent) : value.content_hash;
|
|
214
|
+
const usesRevision = type === 'docs' || type === 'services';
|
|
192
215
|
return normalizeMetadata(config, type, resourceId, {
|
|
193
216
|
resource_type: type,
|
|
194
217
|
resource_id: String(resourceId),
|
|
195
218
|
title: value.title || '', route: value.route || null, code: value.code || null, slug: value.slug || null,
|
|
196
219
|
content_type: contentType,
|
|
197
|
-
file_extension: '.html',
|
|
220
|
+
file_extension: spec.fileExtension || '.html',
|
|
198
221
|
content_size: size,
|
|
199
|
-
content_hash:
|
|
200
|
-
...(
|
|
201
|
-
etag:
|
|
222
|
+
content_hash: contentHash,
|
|
223
|
+
...(usesRevision ? { base_revision: version } : { base_version: version }),
|
|
224
|
+
etag: contentHash,
|
|
202
225
|
download_url: contentURL,
|
|
203
226
|
commit_url: operationPath(config, commitOperation.path, resourceId),
|
|
204
227
|
commit_method: String(commitOperation.method || 'PUT').toUpperCase(),
|
|
205
228
|
inline_content: content,
|
|
206
229
|
json_commit: spec.jsonCommit === true,
|
|
230
|
+
commit_field: spec.commitField || null,
|
|
207
231
|
});
|
|
208
232
|
}
|
|
209
233
|
|
|
234
|
+
function sdkRelativePath(value) {
|
|
235
|
+
const relative = String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
236
|
+
if (!relative || relative.includes('\0') || relative.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
237
|
+
throw new WorktreeError('UNSAFE_SDK_PATH', 'DraftGo SDK bundle included an unsafe relative path.', { path: value });
|
|
238
|
+
}
|
|
239
|
+
return relative;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function syncSDK(config, session, projectDir, options = {}) {
|
|
243
|
+
const { registryRevision, describeOperation } = require('../commands/api');
|
|
244
|
+
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
245
|
+
const spec = FINAL_OPERATIONS.services;
|
|
246
|
+
const sdkDir = path.join(projectDir, '.draftgo', 'worktree', 'sdk');
|
|
247
|
+
let apiSession;
|
|
248
|
+
try {
|
|
249
|
+
apiSession = session && session.names && session.names[TOOL_NAMES.apiCall]
|
|
250
|
+
? session
|
|
251
|
+
: await openToolSession(config, [
|
|
252
|
+
TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
253
|
+
], {
|
|
254
|
+
...options,
|
|
255
|
+
client: (session && session.client) || options.client,
|
|
256
|
+
tools: (session && session.tools) || options.tools,
|
|
257
|
+
clientInitialized: true,
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new WorktreeError(
|
|
261
|
+
'SDK_CHECKOUT_FAILED',
|
|
262
|
+
`Unable to open an API session for the Go SDK bundle: ${error.message}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
let revision;
|
|
266
|
+
let described;
|
|
267
|
+
try {
|
|
268
|
+
revision = await registryRevision(apiSession, spec.sdk);
|
|
269
|
+
described = await describeOperation(projectDir || process.cwd(), config, apiSession, spec.sdk, revision);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new WorktreeError(
|
|
272
|
+
'SDK_CHECKOUT_FAILED',
|
|
273
|
+
`Unable to describe the Go SDK bundle (${spec.sdk}): ${error.message}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const operation = described && described.operation || {};
|
|
277
|
+
const invoke = await callStructured(apiSession, TOOL_NAMES.apiCall, {
|
|
278
|
+
operation_id: spec.sdk,
|
|
279
|
+
registry_revision: revision,
|
|
280
|
+
}, options);
|
|
281
|
+
const workflow = invoke && invoke.workflow || {};
|
|
282
|
+
if (workflow.required !== true || workflow.workflow !== 'checkout_commit') {
|
|
283
|
+
throw new WorktreeError('INVALID_WORKFLOW_DESCRIPTOR', 'DraftGo did not return a checkout/commit workflow descriptor for the Go SDK bundle.');
|
|
284
|
+
}
|
|
285
|
+
const sdkURL = operationPath(config, operation.path || workflow.path, '');
|
|
286
|
+
const response = await (options.fetch || fetch)(sdkURL, {
|
|
287
|
+
method: String(operation.method || 'GET').toUpperCase(),
|
|
288
|
+
headers: authHeaders(config, { Accept: 'application/json' }),
|
|
289
|
+
signal: options.signal,
|
|
290
|
+
redirect: 'error',
|
|
291
|
+
});
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const httpError = await errorForResponse(response, config.token || config.sat);
|
|
294
|
+
throw new WorktreeError(
|
|
295
|
+
'SDK_CHECKOUT_FAILED',
|
|
296
|
+
`Unable to download the Go SDK bundle: ${httpError.message}`,
|
|
297
|
+
{ status: httpError.status, code: httpError.code },
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const raw = await readBounded(response, MAX_CHECKOUT_JSON_BYTES, true);
|
|
301
|
+
const value = parseJsonText(raw, 'DraftGo SDK endpoint');
|
|
302
|
+
const files = isObject(value.files) ? value.files : null;
|
|
303
|
+
if (!files || !Object.keys(files).length) {
|
|
304
|
+
throw new WorktreeError('SDK_CHECKOUT_FAILED', 'DraftGo SDK bundle did not include any files.');
|
|
305
|
+
}
|
|
306
|
+
await fs.promises.rm(sdkDir, { recursive: true, force: true });
|
|
307
|
+
await fs.promises.mkdir(sdkDir, { recursive: true });
|
|
308
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
309
|
+
const safe = sdkRelativePath(relative);
|
|
310
|
+
const destination = path.join(sdkDir, ...safe.split('/'));
|
|
311
|
+
if (typeof content !== 'string') {
|
|
312
|
+
throw new WorktreeError('SDK_CHECKOUT_FAILED', `DraftGo SDK file ${safe} is not text.`);
|
|
313
|
+
}
|
|
314
|
+
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
|
|
315
|
+
await fs.promises.writeFile(destination, content, 'utf8');
|
|
316
|
+
}
|
|
317
|
+
const contentHash = firstValue(value.content_hash, sha256Utf8(JSON.stringify(files)));
|
|
318
|
+
await fs.promises.writeFile(path.join(sdkDir, '.hash'), `${contentHash}\n`, 'utf8');
|
|
319
|
+
return { directory: sdkDir, content_hash: contentHash };
|
|
320
|
+
}
|
|
321
|
+
|
|
210
322
|
function normalizeUrl(config, value, purpose) {
|
|
211
323
|
if (!value) return null;
|
|
212
324
|
let url;
|
|
@@ -281,8 +393,16 @@ function normalizeMetadata(config, resourceType, resourceId, payload) {
|
|
|
281
393
|
|
|
282
394
|
async function resolveMetadata(config, resourceType, resourceId, options = {}) {
|
|
283
395
|
if (options.metadata) return normalizeMetadata(config, resourceType, resourceId, options.metadata);
|
|
396
|
+
const type = canonicalResourceType(resourceType);
|
|
284
397
|
const client = options.client || new DraftGoMcpClient(config);
|
|
285
398
|
if (!options.clientInitialized) await client.initialize(options);
|
|
399
|
+
if (type === 'services') {
|
|
400
|
+
const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
|
|
401
|
+
const session = await openToolSession(config, [
|
|
402
|
+
TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
403
|
+
], { ...options, client });
|
|
404
|
+
return finalMetadata(config, type, resourceId, session, options);
|
|
405
|
+
}
|
|
286
406
|
let toolName = METADATA_TOOL;
|
|
287
407
|
if (typeof client.listAllTools === 'function') {
|
|
288
408
|
const tools = options.tools || await client.listAllTools(options);
|
|
@@ -382,21 +502,29 @@ async function commit(config, metadata, localPath, current, options = {}) {
|
|
|
382
502
|
'Content-Length': String(current.size),
|
|
383
503
|
'X-Content-SHA256': current.hash,
|
|
384
504
|
});
|
|
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
505
|
let body;
|
|
392
506
|
let duplex;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
507
|
+
const raw = metadata.raw && typeof metadata.raw === 'object' ? metadata.raw : {};
|
|
508
|
+
const commitField = raw.commitField || raw.commit_field || null;
|
|
509
|
+
if (commitField !== 'source') {
|
|
510
|
+
if (metadata.etag) headers['If-Match'] = String(metadata.etag);
|
|
511
|
+
else if (metadata.base_version != null) headers['If-Match'] = String(metadata.base_version);
|
|
512
|
+
else if (metadata.base_revision != null) headers['If-Match'] = String(metadata.base_revision);
|
|
513
|
+
}
|
|
514
|
+
if (metadata.base_version != null) headers['DraftGo-Base-Version'] = String(metadata.base_version);
|
|
515
|
+
if (metadata.base_revision != null) headers['DraftGo-Base-Revision'] = String(metadata.base_revision);
|
|
516
|
+
if (raw.json_commit === true) {
|
|
517
|
+
const fileContents = fs.readFileSync(localPath, 'utf8');
|
|
518
|
+
body = JSON.stringify(commitField === 'source'
|
|
519
|
+
? {
|
|
520
|
+
source: fileContents,
|
|
521
|
+
...(metadata.base_revision != null ? { base_revision: metadata.base_revision } : {}),
|
|
522
|
+
}
|
|
523
|
+
: {
|
|
524
|
+
content: fileContents,
|
|
525
|
+
...(metadata.base_revision != null ? { base_revision: metadata.base_revision } : {}),
|
|
526
|
+
...(metadata.base_revision == null && metadata.content_hash ? { base_hash: metadata.content_hash } : {}),
|
|
527
|
+
});
|
|
400
528
|
headers['Content-Type'] = 'application/json';
|
|
401
529
|
headers['Content-Length'] = String(Buffer.byteLength(body, 'utf8'));
|
|
402
530
|
} else {
|
|
@@ -413,8 +541,14 @@ async function commit(config, metadata, localPath, current, options = {}) {
|
|
|
413
541
|
});
|
|
414
542
|
if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
|
|
415
543
|
const text = await readBounded(response, MAX_JSON_BYTES, true);
|
|
416
|
-
if (!text.trim())
|
|
417
|
-
|
|
544
|
+
if (!text.trim()) {
|
|
545
|
+
return commitField === 'source' ? { content_hash: current.hash } : {};
|
|
546
|
+
}
|
|
547
|
+
const payload = parseJsonText(text, 'DraftGo commit endpoint');
|
|
548
|
+
if (commitField === 'source') {
|
|
549
|
+
return { ...payload, content_hash: current.hash };
|
|
550
|
+
}
|
|
551
|
+
return payload;
|
|
418
552
|
}
|
|
419
553
|
|
|
420
554
|
function nodeReadable(response) {
|
|
@@ -424,6 +558,7 @@ function nodeReadable(response) {
|
|
|
424
558
|
|
|
425
559
|
module.exports = {
|
|
426
560
|
METADATA_TOOL,
|
|
561
|
+
FINAL_OPERATIONS,
|
|
427
562
|
readBounded,
|
|
428
563
|
MAX_CHECKOUT_JSON_BYTES,
|
|
429
564
|
toolNameMatches,
|
|
@@ -433,6 +568,7 @@ module.exports = {
|
|
|
433
568
|
resolveMetadata,
|
|
434
569
|
download,
|
|
435
570
|
commit,
|
|
571
|
+
syncSDK,
|
|
436
572
|
nodeReadable,
|
|
437
573
|
errorForResponse,
|
|
438
574
|
};
|
package/src/worktree/index.js
CHANGED
|
@@ -163,6 +163,9 @@ async function checkoutResourcesUnlocked(projectDir, resourceType, resourceIds,
|
|
|
163
163
|
return entry;
|
|
164
164
|
}, { ...options, settled: true });
|
|
165
165
|
const results = settled.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
166
|
+
if (canonical === 'services' && results.length && typeof backend.syncSDK === 'function') {
|
|
167
|
+
await backend.syncSDK(config, session, projectDir, { ...options, ...session, projectDir });
|
|
168
|
+
}
|
|
166
169
|
for (const entry of results) manifest.entries[entryKey(canonical, entry.resource_id)] = entry;
|
|
167
170
|
if (results.length) await saveManifest(projectDir, manifest);
|
|
168
171
|
const failures = settled.filter((item) => item.status === 'rejected');
|
package/src/worktree/types.js
CHANGED
|
@@ -7,6 +7,7 @@ const RESOURCE_TYPES = Object.freeze({
|
|
|
7
7
|
pages: Object.freeze({ directory: 'pages', prefix: 'page' }),
|
|
8
8
|
navigations: Object.freeze({ directory: 'navigations', prefix: 'nav' }),
|
|
9
9
|
docs: Object.freeze({ directory: 'docs', prefix: 'article' }),
|
|
10
|
+
services: Object.freeze({ directory: 'services', prefix: 'service' }),
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
const TYPE_ALIASES = new Map([
|
|
@@ -20,6 +21,8 @@ const TYPE_ALIASES = new Map([
|
|
|
20
21
|
['article', 'docs'],
|
|
21
22
|
['articles', 'docs'],
|
|
22
23
|
['docs/articles', 'docs'],
|
|
24
|
+
['service', 'services'],
|
|
25
|
+
['services', 'services'],
|
|
23
26
|
]);
|
|
24
27
|
|
|
25
28
|
const CONTENT_EXTENSIONS = new Map([
|
|
@@ -28,6 +31,7 @@ const CONTENT_EXTENSIONS = new Map([
|
|
|
28
31
|
['text/markdown', '.md'],
|
|
29
32
|
['text/x-markdown', '.md'],
|
|
30
33
|
['text/plain', '.txt'],
|
|
34
|
+
['text/x-go', '.go'],
|
|
31
35
|
]);
|
|
32
36
|
|
|
33
37
|
function canonicalResourceType(value) {
|
|
@@ -36,7 +40,7 @@ function canonicalResourceType(value) {
|
|
|
36
40
|
if (!canonical) {
|
|
37
41
|
throw new WorktreeError(
|
|
38
42
|
'UNSUPPORTED_RESOURCE_TYPE',
|
|
39
|
-
`Unsupported checkout resource type: ${value}. Expected pages, nav, or
|
|
43
|
+
`Unsupported checkout resource type: ${value}. Expected pages, nav, docs, or services.`,
|
|
40
44
|
{ resource_type: value }
|
|
41
45
|
);
|
|
42
46
|
}
|
|
@@ -48,25 +52,25 @@ function mediaType(contentType) {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function normalizeExtension(contentType, backendExtension) {
|
|
55
|
+
if (backendExtension != null && backendExtension !== '') {
|
|
56
|
+
let extension = String(backendExtension).trim();
|
|
57
|
+
if (!extension.startsWith('.')) extension = `.${extension}`;
|
|
58
|
+
if (!/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/.test(extension)) {
|
|
59
|
+
throw new WorktreeError(
|
|
60
|
+
'UNSAFE_FILE_EXTENSION',
|
|
61
|
+
'The backend returned an unsafe checkout file extension.',
|
|
62
|
+
{ file_extension: backendExtension }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return extension.toLowerCase();
|
|
66
|
+
}
|
|
51
67
|
const known = CONTENT_EXTENSIONS.get(mediaType(contentType));
|
|
52
68
|
if (known) return known;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
let extension = String(backendExtension).trim();
|
|
61
|
-
if (!extension.startsWith('.')) extension = `.${extension}`;
|
|
62
|
-
if (!/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/.test(extension)) {
|
|
63
|
-
throw new WorktreeError(
|
|
64
|
-
'UNSAFE_FILE_EXTENSION',
|
|
65
|
-
'The backend returned an unsafe checkout file extension.',
|
|
66
|
-
{ file_extension: backendExtension }
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return extension.toLowerCase();
|
|
69
|
+
throw new WorktreeError(
|
|
70
|
+
'MISSING_FILE_EXTENSION',
|
|
71
|
+
'DraftGo must provide a safe file extension for this content type.',
|
|
72
|
+
{ content_type: contentType },
|
|
73
|
+
);
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
function safeIdSegment(value) {
|