draftgo-cli 3.0.39 → 3.0.43
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 +66 -15
- package/package.json +4 -2
- package/resources/skill/SKILL.md +53 -15
- package/resources/skill/init/SKILL.md +5 -3
- package/resources/skill/manifest.json +4 -2
- package/resources/skill/push/SKILL.md +2 -0
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/checkout.md +3 -2
- package/resources/skill/references/frontend.md +18 -287
- package/resources/skill/references/mcp.md +6 -2
- package/resources/skill/references/parallel.md +11 -6
- package/resources/skill/references/runtime.md +1 -1
- package/resources/skill/references/ui-protocol.md +1 -1
- package/resources/skill/scripts/README.md +2 -0
- package/src/changelog.js +276 -0
- package/src/cli.js +2 -0
- package/src/commandRegistry.js +2 -0
- package/src/commands/changelog.js +24 -0
- package/src/commands/connect.js +21 -6
- package/src/commands/context.js +27 -0
- package/src/commands/help.js +16 -2
- package/src/commands/map.js +64 -15
- package/src/commands/mcp.js +3 -2
- package/src/commands/update.js +16 -1
- package/src/context/index.js +576 -0
- package/src/mcp/client.js +170 -14
- package/src/mcp/parallel.js +31 -0
- package/src/mcp/protocol.js +2 -1
- package/src/projectConfig.js +39 -5
- package/src/skill.js +65 -3
- package/src/timeout.js +18 -0
package/src/mcp/client.js
CHANGED
|
@@ -7,11 +7,14 @@ const {
|
|
|
7
7
|
postJsonRpc,
|
|
8
8
|
redactText,
|
|
9
9
|
} = require('./protocol');
|
|
10
|
+
const { allWithAbort } = require('./parallel');
|
|
10
11
|
|
|
11
12
|
const SAFE_TEST_TOOLS = [
|
|
12
13
|
'draftgo_project_overview',
|
|
13
14
|
'draftgo_resource_list',
|
|
14
15
|
'draftgo_api_search',
|
|
16
|
+
'draftgo_api_describe',
|
|
17
|
+
'draftgo_api_call',
|
|
15
18
|
];
|
|
16
19
|
|
|
17
20
|
const REQUIRED_DRAFTGO_TOOLS = [
|
|
@@ -25,6 +28,33 @@ const REQUIRED_DRAFTGO_TOOLS = [
|
|
|
25
28
|
'draftgo_api_call',
|
|
26
29
|
];
|
|
27
30
|
|
|
31
|
+
function diagnosticArguments(canonicalName) {
|
|
32
|
+
if (canonicalName === 'draftgo_resource_list') {
|
|
33
|
+
return { resource_type: 'pages', limit: 1 };
|
|
34
|
+
}
|
|
35
|
+
if (canonicalName === 'draftgo_api_search') return { query: 'project', limit: 1 };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function diagnosticData(result) {
|
|
40
|
+
let value = result;
|
|
41
|
+
if (value && value.structuredContent) value = value.structuredContent;
|
|
42
|
+
else if (value && value.structured_content) value = value.structured_content;
|
|
43
|
+
if (value && value.data) value = value.data;
|
|
44
|
+
if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findDBMetaListOperation(result) {
|
|
49
|
+
const value = diagnosticData(result);
|
|
50
|
+
const items = value && Array.isArray(value.items) ? value.items : [];
|
|
51
|
+
return items.find((operation) => operation
|
|
52
|
+
&& String(operation.method || '').toUpperCase() === 'GET'
|
|
53
|
+
&& String(operation.resource_type || '').toLowerCase() === 'db_meta'
|
|
54
|
+
&& String(operation.path || operation.path_template || '') === '/api/db-meta'
|
|
55
|
+
&& operation.destructive !== true) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
class McpRpcError extends Error {
|
|
29
59
|
constructor(message, { code = -32000, data, id = null } = {}) {
|
|
30
60
|
super(message);
|
|
@@ -35,6 +65,40 @@ class McpRpcError extends Error {
|
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
67
|
|
|
68
|
+
function diagnosticNextCursor(result) {
|
|
69
|
+
const value = diagnosticData(result);
|
|
70
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
71
|
+
const hasMore = value.has_more === true || value.hasMore === true;
|
|
72
|
+
|
|
73
|
+
let cursor;
|
|
74
|
+
let present = false;
|
|
75
|
+
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
76
|
+
cursor = value.next_cursor;
|
|
77
|
+
present = true;
|
|
78
|
+
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
79
|
+
cursor = value.nextCursor;
|
|
80
|
+
present = true;
|
|
81
|
+
} else if (hasMore) {
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
83
|
+
throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
|
|
84
|
+
}
|
|
85
|
+
cursor = value.cursor;
|
|
86
|
+
present = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!present) return null;
|
|
90
|
+
if (cursor == null || cursor === '') {
|
|
91
|
+
if (hasMore) {
|
|
92
|
+
throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (typeof cursor !== 'string' || !cursor.trim()) {
|
|
97
|
+
throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
|
|
98
|
+
}
|
|
99
|
+
return cursor;
|
|
100
|
+
}
|
|
101
|
+
|
|
38
102
|
function redactValue(value, secrets = [], seen = new WeakMap()) {
|
|
39
103
|
if (typeof value === 'string') return redactText(value, secrets);
|
|
40
104
|
if (!value || typeof value !== 'object') return value;
|
|
@@ -222,30 +286,118 @@ class DraftGoMcpClient {
|
|
|
222
286
|
if (missing.length) {
|
|
223
287
|
throw new McpRpcError(`DraftGo MCP is missing required tools: ${missing.join(', ')}.`, { code: -32601 });
|
|
224
288
|
}
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
289
|
+
const safeTools = options.safeTools || SAFE_TEST_TOOLS;
|
|
290
|
+
const hasDiagnosticOverrides = Boolean(options.requiredTools || options.safeTools);
|
|
291
|
+
const firstSafeTool = tools.find((tool) => tool && typeof tool.name === 'string'
|
|
292
|
+
&& safeTools.some((canonical) => toolMatches(tool.name, canonical)));
|
|
293
|
+
if (!firstSafeTool) {
|
|
229
294
|
throw new McpRpcError(
|
|
230
|
-
`DraftGo MCP exposes no safe diagnostic tool (${
|
|
295
|
+
`DraftGo MCP exposes no safe diagnostic tool (${safeTools.join(', ')}).`,
|
|
231
296
|
{ code: -32601 },
|
|
232
297
|
);
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
300
|
+
const selected = safeTools.map((canonical) => ({
|
|
301
|
+
canonical,
|
|
302
|
+
tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
|
|
303
|
+
&& toolMatches(candidate.name, canonical)),
|
|
304
|
+
})).filter((entry) => entry.tool);
|
|
305
|
+
const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
|
|
306
|
+
const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
|
|
307
|
+
const defaultPlan = hasDiagnosticOverrides ? [{
|
|
308
|
+
label: overrideCanonical,
|
|
309
|
+
canonical: overrideCanonical,
|
|
310
|
+
tool: firstSafeTool,
|
|
311
|
+
args: diagnosticArguments(overrideCanonical),
|
|
312
|
+
}] : [
|
|
313
|
+
{ label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
|
|
314
|
+
...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
|
|
315
|
+
label: `resource:${resourceType}`,
|
|
316
|
+
canonical: 'draftgo_resource_list',
|
|
317
|
+
tool: byCanonical.draftgo_resource_list,
|
|
318
|
+
args: { resource_type: resourceType, limit: 1 },
|
|
319
|
+
})),
|
|
320
|
+
{
|
|
321
|
+
label: 'api:db_meta',
|
|
322
|
+
canonical: 'draftgo_api_search',
|
|
323
|
+
tool: byCanonical.draftgo_api_search,
|
|
324
|
+
args: { resource_type: 'db_meta', limit: 100 },
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
|
|
328
|
+
async (queryOptions) => ({
|
|
329
|
+
label: entry.label,
|
|
330
|
+
canonical: entry.canonical,
|
|
331
|
+
name: entry.tool.name,
|
|
332
|
+
arguments: options.toolArguments && startIndex + offset === 0
|
|
333
|
+
? options.toolArguments
|
|
334
|
+
: entry.args,
|
|
335
|
+
result: await this.toolsCall(
|
|
336
|
+
entry.tool.name,
|
|
337
|
+
options.toolArguments && startIndex + offset === 0
|
|
338
|
+
? options.toolArguments
|
|
339
|
+
: entry.args,
|
|
340
|
+
queryOptions,
|
|
341
|
+
),
|
|
342
|
+
})), options);
|
|
343
|
+
const tested = await runPlan(defaultPlan);
|
|
344
|
+
if (!hasDiagnosticOverrides) {
|
|
345
|
+
let discovery = tested.find((entry) => entry.label === 'api:db_meta');
|
|
346
|
+
let operation = discovery && findDBMetaListOperation(discovery.result);
|
|
347
|
+
const seenCursors = new Set();
|
|
348
|
+
const maxPages = Number(options.maxPages || 100);
|
|
349
|
+
let page = 1;
|
|
350
|
+
while (!operation) {
|
|
351
|
+
const cursor = diagnosticNextCursor(discovery && discovery.result);
|
|
352
|
+
if (cursor == null) break;
|
|
353
|
+
if (seenCursors.has(cursor)) {
|
|
354
|
+
throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
|
|
355
|
+
}
|
|
356
|
+
seenCursors.add(cursor);
|
|
357
|
+
if (page >= maxPages) {
|
|
358
|
+
throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
|
|
359
|
+
}
|
|
360
|
+
page += 1;
|
|
361
|
+
[discovery] = await runPlan([{
|
|
362
|
+
label: `api:db_meta:page-${page}`,
|
|
363
|
+
canonical: 'draftgo_api_search',
|
|
364
|
+
tool: byCanonical.draftgo_api_search,
|
|
365
|
+
args: { resource_type: 'db_meta', limit: 100, cursor },
|
|
366
|
+
}], tested.length);
|
|
367
|
+
tested.push(discovery);
|
|
368
|
+
operation = findDBMetaListOperation(discovery.result);
|
|
369
|
+
}
|
|
370
|
+
if (!operation || !operation.operation_id) {
|
|
371
|
+
throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
|
|
372
|
+
code: -32601,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const operationID = String(operation.operation_id);
|
|
376
|
+
tested.push(...await runPlan([
|
|
377
|
+
{
|
|
378
|
+
label: 'api:describe-db_meta',
|
|
379
|
+
canonical: 'draftgo_api_describe',
|
|
380
|
+
tool: byCanonical.draftgo_api_describe,
|
|
381
|
+
args: { operation_id: operationID },
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: 'api:call-db_meta',
|
|
385
|
+
canonical: 'draftgo_api_call',
|
|
386
|
+
tool: byCanonical.draftgo_api_call,
|
|
387
|
+
args: { operation_id: operationID, query: { page: 1, page_size: 1 } },
|
|
388
|
+
},
|
|
389
|
+
], tested.length));
|
|
390
|
+
}
|
|
242
391
|
return {
|
|
243
392
|
initialized,
|
|
244
393
|
protocolVersion: this.protocolVersion,
|
|
245
394
|
sessionId: this.sessionId,
|
|
246
395
|
tools,
|
|
247
|
-
testedTool:
|
|
248
|
-
toolResult,
|
|
396
|
+
testedTool: tested[0].name,
|
|
397
|
+
toolResult: tested[0].result,
|
|
398
|
+
testedTools: [...new Set(tested.map((entry) => entry.name))],
|
|
399
|
+
testedCalls: tested.map((entry) => entry.label),
|
|
400
|
+
diagnosticResults: tested,
|
|
249
401
|
};
|
|
250
402
|
}
|
|
251
403
|
}
|
|
@@ -268,6 +420,10 @@ module.exports = {
|
|
|
268
420
|
McpHttpError,
|
|
269
421
|
SAFE_TEST_TOOLS,
|
|
270
422
|
REQUIRED_DRAFTGO_TOOLS,
|
|
423
|
+
diagnosticArguments,
|
|
424
|
+
diagnosticData,
|
|
425
|
+
diagnosticNextCursor,
|
|
426
|
+
findDBMetaListOperation,
|
|
271
427
|
redactValue,
|
|
272
428
|
testConnection,
|
|
273
429
|
callTool,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function allWithAbort(taskFactories, options = {}) {
|
|
4
|
+
if (!Array.isArray(taskFactories) || taskFactories.some((task) => typeof task !== 'function')) {
|
|
5
|
+
throw new TypeError('Parallel MCP tasks must be functions.');
|
|
6
|
+
}
|
|
7
|
+
const externalSignal = options.signal;
|
|
8
|
+
if (externalSignal && externalSignal.aborted) {
|
|
9
|
+
if (typeof externalSignal.throwIfAborted === 'function') externalSignal.throwIfAborted();
|
|
10
|
+
const error = new Error('Parallel MCP tasks were aborted before they started.');
|
|
11
|
+
error.name = 'AbortError';
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const forwardAbort = () => controller.abort(externalSignal.reason);
|
|
17
|
+
if (externalSignal) {
|
|
18
|
+
externalSignal.addEventListener('abort', forwardAbort, { once: true });
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const taskOptions = { ...options, signal: controller.signal };
|
|
22
|
+
return await Promise.all(taskFactories.map((task) => task(taskOptions)));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!controller.signal.aborted) controller.abort(error);
|
|
25
|
+
throw error;
|
|
26
|
+
} finally {
|
|
27
|
+
if (externalSignal) externalSignal.removeEventListener('abort', forwardAbort);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { allWithAbort };
|
package/src/mcp/protocol.js
CHANGED
|
@@ -22,9 +22,10 @@ function redactText(value, secrets = []) {
|
|
|
22
22
|
|
|
23
23
|
function endpointFor(config) {
|
|
24
24
|
const server = new URL(String(config.server || config.mcp_url));
|
|
25
|
+
const serverPath = server.pathname.replace(/\/+$/, '');
|
|
25
26
|
const endpoint = config.mcp_url
|
|
26
27
|
? new URL(String(config.mcp_url), server)
|
|
27
|
-
: new URL(`${
|
|
28
|
+
: new URL(`${serverPath}/mcp`, server);
|
|
28
29
|
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
|
|
29
30
|
throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
|
|
30
31
|
}
|
package/src/projectConfig.js
CHANGED
|
@@ -11,16 +11,42 @@ const IGNORE_ENTRIES = [
|
|
|
11
11
|
'.draftgo/conflicts/',
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
function parseHttpUrl(raw) {
|
|
15
15
|
let value = String(raw || '').trim();
|
|
16
|
-
if (!value) return
|
|
16
|
+
if (!value) return null;
|
|
17
17
|
if (!/^https?:\/\//i.test(value)) value = `http://${value}`;
|
|
18
18
|
const parsed = new URL(value);
|
|
19
19
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password
|
|
20
20
|
|| parsed.search || parsed.hash) {
|
|
21
21
|
throw new Error('DraftGo server must be an absolute HTTP(S) URL without credentials, query, or fragment.');
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeServer(raw) {
|
|
27
|
+
const parsed = parseHttpUrl(raw);
|
|
28
|
+
if (!parsed) return '';
|
|
29
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeMcpEndpoint(raw) {
|
|
33
|
+
const parsed = parseHttpUrl(raw);
|
|
34
|
+
return parsed ? parsed.toString() : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeConnection(raw) {
|
|
38
|
+
const value = normalizeMcpEndpoint(raw);
|
|
39
|
+
if (!value) return { server: '', mcp_url: '' };
|
|
40
|
+
const parsed = new URL(value);
|
|
41
|
+
const pathname = parsed.pathname.replace(/\/+$/, '');
|
|
42
|
+
if (!pathname.toLowerCase().endsWith('/mcp')) {
|
|
43
|
+
return { server: '', mcp_url: value };
|
|
44
|
+
}
|
|
45
|
+
parsed.pathname = pathname.slice(0, -4) || '/';
|
|
46
|
+
return {
|
|
47
|
+
server: parsed.toString().replace(/\/+$/, ''),
|
|
48
|
+
mcp_url: value,
|
|
49
|
+
};
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
function configPath(projectDir) {
|
|
@@ -65,7 +91,7 @@ function writePrivateJson(file, value) {
|
|
|
65
91
|
try { fs.chmodSync(file, 0o600); } catch { /* Windows ACLs are inherited. */ }
|
|
66
92
|
}
|
|
67
93
|
|
|
68
|
-
function writeProjectConfig(projectDir, server, token) {
|
|
94
|
+
function writeProjectConfig(projectDir, server, token, options = {}) {
|
|
69
95
|
const normalizedServer = normalizeServer(server);
|
|
70
96
|
const sat = String(token || '').trim();
|
|
71
97
|
if (!normalizedServer) throw new Error('DraftGo server is required.');
|
|
@@ -77,6 +103,8 @@ function writeProjectConfig(projectDir, server, token) {
|
|
|
77
103
|
token: sat,
|
|
78
104
|
auto_push: existing.auto_push === true,
|
|
79
105
|
};
|
|
106
|
+
if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
|
|
107
|
+
else delete config.mcp_url;
|
|
80
108
|
const file = configPath(projectDir);
|
|
81
109
|
writePrivateJson(file, config);
|
|
82
110
|
ensureProjectIgnores(projectDir);
|
|
@@ -88,16 +116,22 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
|
|
|
88
116
|
if (!exists(file)) throw new Error('Missing .draftgo/config.json; run `draftgo connect` first.');
|
|
89
117
|
const config = readExisting(projectDir);
|
|
90
118
|
const server = normalizeServer(config.server);
|
|
119
|
+
const mcpUrl = config.mcp_url ? normalizeMcpEndpoint(config.mcp_url) : '';
|
|
91
120
|
const token = String(config.token || config.sat || '').trim();
|
|
92
121
|
if (!server) throw new Error('DraftGo project config is missing server.');
|
|
93
122
|
if (requireToken && !token) throw new Error('DraftGo project config is missing SAT.');
|
|
94
|
-
|
|
123
|
+
const result = { ...config, server, token, path: file };
|
|
124
|
+
if (mcpUrl) result.mcp_url = mcpUrl;
|
|
125
|
+
else delete result.mcp_url;
|
|
126
|
+
return result;
|
|
95
127
|
}
|
|
96
128
|
|
|
97
129
|
module.exports = {
|
|
98
130
|
IGNORE_ENTRIES,
|
|
99
131
|
configPath,
|
|
100
132
|
normalizeServer,
|
|
133
|
+
normalizeMcpEndpoint,
|
|
134
|
+
normalizeConnection,
|
|
101
135
|
ensureProjectIgnores,
|
|
102
136
|
writeProjectConfig,
|
|
103
137
|
loadProjectConfig,
|
package/src/skill.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// {{SKILL_DIR}} → platform's project-relative skill dir (forward slashes)
|
|
10
10
|
// {{SKILL_SCRIPTS}} → {{SKILL_DIR}}/scripts
|
|
11
11
|
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const fs = require('fs');
|
|
14
15
|
const { RESOURCES_DIR } = require('./paths');
|
|
@@ -19,6 +20,8 @@ const {
|
|
|
19
20
|
} = require('./fsx');
|
|
20
21
|
|
|
21
22
|
const SKILL_SOURCE_DIR = path.join(RESOURCES_DIR, 'skill');
|
|
23
|
+
const MAX_VERSION_FILE_BYTES = 128;
|
|
24
|
+
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
22
25
|
|
|
23
26
|
function getPackageVersion() {
|
|
24
27
|
try {
|
|
@@ -29,12 +32,71 @@ function getPackageVersion() {
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function readInstalledVersion(projectDir) {
|
|
32
|
-
const
|
|
33
|
-
|
|
35
|
+
const file = paths.versionFile(projectDir);
|
|
36
|
+
let linkStat;
|
|
37
|
+
try {
|
|
38
|
+
linkStat = fs.lstatSync(file);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error && error.code === 'ENOENT') return null;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (linkStat.isSymbolicLink() || !linkStat.isFile()) {
|
|
44
|
+
throw new Error('Invalid .draftgo/.version: expected a regular file.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let descriptor;
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
const noFollow = Number.isInteger(fs.constants.O_NOFOLLOW) ? fs.constants.O_NOFOLLOW : 0;
|
|
51
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
52
|
+
const stat = fs.fstatSync(descriptor);
|
|
53
|
+
if (!stat.isFile() || stat.size > MAX_VERSION_FILE_BYTES) {
|
|
54
|
+
throw new Error('Invalid .draftgo/.version: file is too large or not regular.');
|
|
55
|
+
}
|
|
56
|
+
const buffer = Buffer.alloc(MAX_VERSION_FILE_BYTES + 1);
|
|
57
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
58
|
+
if (bytes > MAX_VERSION_FILE_BYTES) {
|
|
59
|
+
throw new Error('Invalid .draftgo/.version: file exceeds 128 bytes.');
|
|
60
|
+
}
|
|
61
|
+
raw = buffer.subarray(0, bytes).toString('utf8');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error && error.code === 'ELOOP') {
|
|
64
|
+
throw new Error('Invalid .draftgo/.version: symbolic links are not allowed.');
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
} finally {
|
|
68
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const version = raw.trim();
|
|
72
|
+
const validLineEnding = raw === version || raw === `${version}\n` || raw === `${version}\r\n`;
|
|
73
|
+
if (!validLineEnding || !VERSION_PATTERN.test(version)) {
|
|
74
|
+
throw new Error('Invalid .draftgo/.version: expected one semantic version.');
|
|
75
|
+
}
|
|
76
|
+
return version;
|
|
34
77
|
}
|
|
35
78
|
|
|
36
79
|
function writeInstalledVersion(projectDir) {
|
|
37
|
-
|
|
80
|
+
const file = paths.versionFile(projectDir);
|
|
81
|
+
ensureDir(path.dirname(file));
|
|
82
|
+
const temporary = path.join(
|
|
83
|
+
path.dirname(file),
|
|
84
|
+
`.${path.basename(file)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
|
|
85
|
+
);
|
|
86
|
+
let descriptor;
|
|
87
|
+
try {
|
|
88
|
+
descriptor = fs.openSync(temporary, 'wx', 0o600);
|
|
89
|
+
fs.writeFileSync(descriptor, getPackageVersion() + '\n', 'utf8');
|
|
90
|
+
fs.fsyncSync(descriptor);
|
|
91
|
+
fs.closeSync(descriptor);
|
|
92
|
+
descriptor = undefined;
|
|
93
|
+
fs.renameSync(temporary, file);
|
|
94
|
+
} finally {
|
|
95
|
+
if (descriptor !== undefined) {
|
|
96
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the original failure. */ }
|
|
97
|
+
}
|
|
98
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* Best-effort cleanup. */ }
|
|
99
|
+
}
|
|
38
100
|
}
|
|
39
101
|
|
|
40
102
|
function renderFrontmatter(fm) {
|
package/src/timeout.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
4
|
+
|
|
5
|
+
function parseTimeout(value) {
|
|
6
|
+
if (value == null) return undefined;
|
|
7
|
+
const text = String(value).trim();
|
|
8
|
+
if (!/^\d+$/.test(text)) {
|
|
9
|
+
throw new TypeError('--timeout must be a positive integer in milliseconds.');
|
|
10
|
+
}
|
|
11
|
+
const timeoutMs = Number(text);
|
|
12
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
13
|
+
throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
14
|
+
}
|
|
15
|
+
return timeoutMs;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { MAX_TIMEOUT_MS, parseTimeout };
|