draftgo-cli 1.0.4 → 1.0.6
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 +4 -4
- package/package.json +4 -3
- 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 +125 -125
- package/src/commands/map.js +60 -17
- package/src/commands/reconcile.js +1 -1
- package/src/commands/uninstall.js +16 -9
- package/src/mcp/hosts.js +68 -23
- package/src/platforms.js +16 -4
- package/src/skill.js +26 -6
- package/src/targets.js +2 -2
- package/src/worktree/backend.js +157 -21
- package/src/worktree/index.js +3 -0
- package/src/worktree/types.js +22 -18
package/src/mcp/hosts.js
CHANGED
|
@@ -22,6 +22,7 @@ const HOSTS = Object.freeze([
|
|
|
22
22
|
{ name: 'gemini', displayName: 'Gemini CLI', path: '.gemini/settings.json', format: 'jsonc', registry: 'mcpServers', signals: ['.gemini'] },
|
|
23
23
|
{ name: 'kiro', displayName: 'Kiro', path: '.kiro/settings/mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.kiro'] },
|
|
24
24
|
{ name: 'copilot', aliases: ['github-copilot'], displayName: 'GitHub Copilot', path: '.vscode/mcp.json', format: 'jsonc', registry: 'servers', vscode: true, signals: ['.vscode', '.github/prompts', '.github/copilot-instructions.md'] },
|
|
25
|
+
{ name: 'zcode', displayName: 'ZCode', path: '.zcode/config.json', format: 'jsonc', registry: 'mcp.servers', vscode: true, signals: ['.zcode'] },
|
|
25
26
|
{ name: 'windsurf', displayName: 'Windsurf', supported: false, reason: 'Windsurf has no supported project-level MCP configuration adapter.', signals: ['.windsurf'] },
|
|
26
27
|
{ name: 'antigravity', displayName: 'Antigravity', supported: false, reason: 'Antigravity has no supported project-level MCP configuration adapter.', signals: ['.agent'] },
|
|
27
28
|
]);
|
|
@@ -249,41 +250,85 @@ function insertObjectProperty(source, open, close, key, value, newline) {
|
|
|
249
250
|
);
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
function registryPath(registry) {
|
|
254
|
+
return String(registry || '').split('.').map((part) => part.trim()).filter(Boolean);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function nestedValue(value, keys) {
|
|
258
|
+
let current = value;
|
|
259
|
+
for (const key of keys) {
|
|
260
|
+
if (!current || typeof current !== 'object' || Array.isArray(current) || !Object.prototype.hasOwnProperty.call(current, key)) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
current = current[key];
|
|
264
|
+
}
|
|
265
|
+
return current;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function assertRegistryObjects(parsed, keys, file) {
|
|
269
|
+
let current = parsed;
|
|
270
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
271
|
+
const key = keys[index];
|
|
272
|
+
if (current[key] === undefined) return;
|
|
273
|
+
if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
|
|
274
|
+
throw new Error(`Invalid ${file}: ${keys.slice(0, index + 1).join('.')} must be an object.`);
|
|
275
|
+
}
|
|
276
|
+
current = current[key];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function findNestedObject(source, open, close, keys) {
|
|
281
|
+
let currentOpen = open;
|
|
282
|
+
let currentClose = close;
|
|
283
|
+
let matched = 0;
|
|
284
|
+
for (const key of keys) {
|
|
285
|
+
const property = objectProperties(source, currentOpen, currentClose).find((item) => item.key === key);
|
|
286
|
+
if (!property) break;
|
|
287
|
+
currentOpen = property.valueStart;
|
|
288
|
+
currentClose = scanComposite(source, currentOpen) - 1;
|
|
289
|
+
matched += 1;
|
|
290
|
+
}
|
|
291
|
+
return { open: currentOpen, close: currentClose, matched };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function upsertDraftGoEntry(source, open, close, entry, newline) {
|
|
295
|
+
const serverProperties = objectProperties(source, open, close);
|
|
296
|
+
const draftgo = serverProperties.find((item) => item.key === 'draftgo');
|
|
297
|
+
if (draftgo) {
|
|
298
|
+
const indent = lineIndent(source, draftgo.propertyStart);
|
|
299
|
+
return replaceRange(
|
|
300
|
+
source,
|
|
301
|
+
draftgo.valueStart,
|
|
302
|
+
draftgo.valueEnd,
|
|
303
|
+
formatJsonValue(entry, indent, newline),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return insertObjectProperty(source, open, close, 'draftgo', entry, newline);
|
|
307
|
+
}
|
|
308
|
+
|
|
252
309
|
function mergeJsoncConfig(source, registry, entry, file = 'MCP config') {
|
|
310
|
+
const keys = registryPath(registry);
|
|
311
|
+
if (!keys.length) throw new Error(`Invalid ${file}: MCP registry path is missing.`);
|
|
253
312
|
const initial = source.trim() ? source : '{\n}\n';
|
|
254
313
|
const parsed = parseJsonc(initial, file);
|
|
255
|
-
|
|
256
|
-
&& (!parsed[registry] || typeof parsed[registry] !== 'object' || Array.isArray(parsed[registry]))) {
|
|
257
|
-
throw new Error(`Invalid ${file}: ${registry} must be an object.`);
|
|
258
|
-
}
|
|
314
|
+
assertRegistryObjects(parsed, keys, file);
|
|
259
315
|
|
|
260
316
|
const newline = initial.includes('\r\n') ? '\r\n' : '\n';
|
|
261
317
|
const rootOffset = initial.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
262
318
|
const rootOpen = skipTrivia(initial, rootOffset);
|
|
263
319
|
const rootEnd = scanComposite(initial, rootOpen);
|
|
264
320
|
const rootClose = rootEnd - 1;
|
|
265
|
-
const
|
|
266
|
-
const registryProperty = rootProperties.find((item) => item.key === registry);
|
|
321
|
+
const nested = findNestedObject(initial, rootOpen, rootClose, keys);
|
|
267
322
|
let result;
|
|
268
323
|
|
|
269
|
-
if (
|
|
270
|
-
result =
|
|
324
|
+
if (nested.matched === keys.length) {
|
|
325
|
+
result = upsertDraftGoEntry(initial, nested.open, nested.close, entry, newline);
|
|
271
326
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const draftgo = serverProperties.find((item) => item.key === 'draftgo');
|
|
276
|
-
if (draftgo) {
|
|
277
|
-
const indent = lineIndent(initial, draftgo.propertyStart);
|
|
278
|
-
result = replaceRange(
|
|
279
|
-
initial,
|
|
280
|
-
draftgo.valueStart,
|
|
281
|
-
draftgo.valueEnd,
|
|
282
|
-
formatJsonValue(entry, indent, newline),
|
|
283
|
-
);
|
|
284
|
-
} else {
|
|
285
|
-
result = insertObjectProperty(initial, registryOpen, registryClose, 'draftgo', entry, newline);
|
|
327
|
+
let value = { draftgo: entry };
|
|
328
|
+
for (let index = keys.length - 1; index > nested.matched; index -= 1) {
|
|
329
|
+
value = { [keys[index]]: value };
|
|
286
330
|
}
|
|
331
|
+
result = insertObjectProperty(initial, nested.open, nested.close, keys[nested.matched], value, newline);
|
|
287
332
|
}
|
|
288
333
|
|
|
289
334
|
parseJsonc(result, file);
|
|
@@ -482,7 +527,7 @@ function statusHost(projectDir, target) {
|
|
|
482
527
|
if (host.format === 'toml') return { host, supported: true, path: file, ...codexStatus(source) };
|
|
483
528
|
try {
|
|
484
529
|
const value = parseJsonc(source, host.path);
|
|
485
|
-
const registry = value
|
|
530
|
+
const registry = nestedValue(value, registryPath(host.registry));
|
|
486
531
|
const entry = registry && typeof registry === 'object' && !Array.isArray(registry)
|
|
487
532
|
? registry.draftgo
|
|
488
533
|
: undefined;
|
package/src/platforms.js
CHANGED
|
@@ -79,10 +79,13 @@ const platforms = [
|
|
|
79
79
|
{
|
|
80
80
|
name: 'codex',
|
|
81
81
|
displayName: 'Codex CLI',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// Repo-level Codex skills now follow the Agent Skills path.
|
|
83
|
+
// MCP config still lives in .codex/config.toml, so detection stays on .codex.
|
|
84
|
+
mainFile: '.agents/skills/draftgo/SKILL.md',
|
|
85
|
+
assetDir: '.agents/skills/draftgo',
|
|
86
|
+
skillDir: '.agents/skills/draftgo',
|
|
85
87
|
signals: ['.codex'],
|
|
88
|
+
legacyAssetDirs: ['.codex/skills/draftgo'],
|
|
86
89
|
frontmatter: { name: SKILL_NAME, description: DESCRIPTION },
|
|
87
90
|
},
|
|
88
91
|
{
|
|
@@ -100,7 +103,16 @@ const platforms = [
|
|
|
100
103
|
mainFile: '.agents/skills/draftgo/SKILL.md',
|
|
101
104
|
assetDir: '.agents/skills/draftgo',
|
|
102
105
|
skillDir: '.agents/skills/draftgo',
|
|
103
|
-
signals: ['.
|
|
106
|
+
signals: ['.pi'],
|
|
107
|
+
frontmatter: { name: SKILL_NAME, description: DESCRIPTION },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'zcode',
|
|
111
|
+
displayName: 'ZCode',
|
|
112
|
+
mainFile: '.zcode/skills/draftgo/SKILL.md',
|
|
113
|
+
assetDir: '.zcode/skills/draftgo',
|
|
114
|
+
skillDir: '.zcode/skills/draftgo',
|
|
115
|
+
signals: ['.zcode'],
|
|
104
116
|
frontmatter: { name: SKILL_NAME, description: DESCRIPTION },
|
|
105
117
|
},
|
|
106
118
|
];
|
package/src/skill.js
CHANGED
|
@@ -193,6 +193,19 @@ function ensureRuntime(projectDir) {
|
|
|
193
193
|
appendGitignoreLine(projectDir, '.draftgo/conflicts/');
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
function legacySkillDirs(projectDir, platform) {
|
|
197
|
+
const destAsset = path.resolve(path.join(projectDir, platform.assetDir));
|
|
198
|
+
return (platform.legacyAssetDirs || [])
|
|
199
|
+
.map((relative) => path.join(projectDir, relative))
|
|
200
|
+
.filter((absolute) => path.resolve(absolute) !== destAsset && exists(absolute));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function skillAlreadyInstalled(projectDir, platform) {
|
|
204
|
+
const destAsset = path.join(projectDir, platform.assetDir);
|
|
205
|
+
const destMain = path.join(projectDir, platform.mainFile);
|
|
206
|
+
return exists(destAsset) || exists(destMain) || legacySkillDirs(projectDir, platform).length > 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
196
209
|
function installPlatform(projectDir, platform, opts = {}) {
|
|
197
210
|
if (!exists(SKILL_SOURCE_DIR)) {
|
|
198
211
|
throw new Error(
|
|
@@ -202,7 +215,7 @@ function installPlatform(projectDir, platform, opts = {}) {
|
|
|
202
215
|
const destAsset = path.join(projectDir, platform.assetDir);
|
|
203
216
|
const destMain = path.join(projectDir, platform.mainFile);
|
|
204
217
|
const mainOutsideAsset = path.relative(destAsset, destMain).startsWith('..');
|
|
205
|
-
if ((
|
|
218
|
+
if (skillAlreadyInstalled(projectDir, platform) && !opts.force) {
|
|
206
219
|
return { path: platform.mainFile, skipped: true };
|
|
207
220
|
}
|
|
208
221
|
|
|
@@ -251,24 +264,31 @@ function installPlatform(projectDir, platform, opts = {}) {
|
|
|
251
264
|
|
|
252
265
|
if (exists(backupAsset)) removePath(backupAsset);
|
|
253
266
|
if (backupMain && exists(backupMain)) removePath(backupMain);
|
|
267
|
+
for (const legacyDir of legacySkillDirs(projectDir, platform)) removePath(legacyDir);
|
|
254
268
|
if (!exists(destMain)) throw new Error(`安装后缺少入口文件:${platform.mainFile}`);
|
|
255
269
|
|
|
256
270
|
return { path: platform.mainFile };
|
|
257
271
|
}
|
|
258
272
|
|
|
259
|
-
function uninstallPlatform(projectDir, platform) {
|
|
273
|
+
function uninstallPlatform(projectDir, platform, opts = {}) {
|
|
260
274
|
const destAsset = path.join(projectDir, platform.assetDir);
|
|
261
275
|
const destMain = path.join(projectDir, platform.mainFile);
|
|
262
276
|
let removed = false;
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
if (!opts.keepSharedSkill) {
|
|
278
|
+
if (exists(destAsset)) { removePath(destAsset); removed = true; }
|
|
279
|
+
if (path.relative(destAsset, destMain).startsWith('..') && exists(destMain)) {
|
|
280
|
+
removePath(destMain); removed = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const legacyDir of legacySkillDirs(projectDir, platform)) {
|
|
284
|
+
removePath(legacyDir);
|
|
285
|
+
removed = true;
|
|
266
286
|
}
|
|
267
287
|
return removed;
|
|
268
288
|
}
|
|
269
289
|
|
|
270
290
|
function statusPlatform(projectDir, platform) {
|
|
271
|
-
return { installed:
|
|
291
|
+
return { installed: skillAlreadyInstalled(projectDir, platform) };
|
|
272
292
|
}
|
|
273
293
|
|
|
274
294
|
function installAll(projectDir, platforms, opts = {}) {
|
package/src/targets.js
CHANGED
|
@@ -14,8 +14,8 @@ function makeInstaller(p) {
|
|
|
14
14
|
install(projectDir, opts) {
|
|
15
15
|
return skill.installPlatform(projectDir, p, opts || {});
|
|
16
16
|
},
|
|
17
|
-
uninstall(projectDir) {
|
|
18
|
-
return skill.uninstallPlatform(projectDir, p);
|
|
17
|
+
uninstall(projectDir, opts) {
|
|
18
|
+
return skill.uninstallPlatform(projectDir, p, opts || {});
|
|
19
19
|
},
|
|
20
20
|
status(projectDir) {
|
|
21
21
|
return skill.statusPlatform(projectDir, p);
|
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) {
|