draftgo-cli 3.0.33 → 3.0.38
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 +220 -269
- package/package.json +6 -2
- package/resources/skill/SKILL.md +114 -55
- package/resources/skill/init/SKILL.md +29 -15
- package/resources/skill/manifest.json +13 -5
- package/resources/skill/push/SKILL.md +41 -29
- package/resources/skill/references/aihub.md +8 -5
- package/resources/skill/references/api-endpoints.md +5 -3
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/checkout.md +116 -0
- package/resources/skill/references/custom-services.md +9 -10
- package/resources/skill/references/data.md +4 -2
- package/resources/skill/references/frontend.md +99 -23
- package/resources/skill/references/mcp.md +101 -0
- package/resources/skill/references/modules.md +8 -8
- package/resources/skill/references/parallel.md +6 -3
- package/resources/skill/references/runtime.md +7 -10
- package/resources/skill/scripts/README.md +8 -0
- package/resources/skill/story/SKILL.md +8 -8
- package/src/cli.js +5 -0
- package/src/commandRegistry.js +7 -1
- package/src/commands/api.js +24 -187
- package/src/commands/autoPush.js +48 -17
- package/src/commands/check.js +17 -47
- package/src/commands/checkout.js +18 -0
- package/src/commands/commit.js +21 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +60 -48
- package/src/commands/delete.js +79 -64
- package/src/commands/deploy.js +18 -10
- package/src/commands/diff.js +23 -0
- package/src/commands/help.js +99 -75
- package/src/commands/init.js +4 -10
- package/src/commands/local.js +23 -6
- package/src/commands/map.js +89 -89
- package/src/commands/mcp.js +126 -0
- package/src/commands/sync.js +28 -43
- package/src/commands/verifyUi.js +3 -2
- package/src/localdev/index.js +37 -7
- package/src/localdev/mysqlClient.js +1 -1
- package/src/mcp/client.js +275 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/protocol.js +173 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +37 -0
- package/src/platforms.js +3 -4
- package/src/projectConfig.js +91 -49
- package/src/projectMap.js +123 -460
- package/src/skill.js +6 -28
- package/src/worktree/backend.js +250 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +461 -0
- package/src/worktree/manifest.js +75 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
- package/resources/skill/pull/SKILL.md +0 -33
- package/resources/skill/references/api.json +0 -20248
- package/resources/skill/scripts/draftgo_delete.py +0 -149
- package/resources/skill/scripts/draftgo_init.py +0 -80
- package/resources/skill/scripts/draftgo_pull.py +0 -427
- package/resources/skill/scripts/draftgo_push.py +0 -1022
- package/src/python.js +0 -27
package/src/mcp/stdio.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
4
|
+
const { DraftGoMcpClient, redactValue } = require('./client');
|
|
5
|
+
const { redactText } = require('./protocol');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function indexOfHeaderEnd(buffer, start = 0) {
|
|
10
|
+
const crlf = buffer.indexOf('\r\n\r\n', start, 'ascii');
|
|
11
|
+
const lf = buffer.indexOf('\n\n', start, 'ascii');
|
|
12
|
+
if (crlf < 0) return lf < 0 ? null : { index: lf, length: 2 };
|
|
13
|
+
if (lf < 0 || crlf <= lf) return { index: crlf, length: 4 };
|
|
14
|
+
return { index: lf, length: 2 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function leadingWhitespaceLength(buffer) {
|
|
18
|
+
let offset = 0;
|
|
19
|
+
while (offset < buffer.length) {
|
|
20
|
+
const byte = buffer[offset];
|
|
21
|
+
if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0d && byte !== 0x0a) break;
|
|
22
|
+
offset += 1;
|
|
23
|
+
}
|
|
24
|
+
return offset;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function headerState(buffer, offset) {
|
|
28
|
+
const target = 'content-length:';
|
|
29
|
+
const available = buffer.subarray(offset, Math.min(buffer.length, offset + target.length))
|
|
30
|
+
.toString('ascii')
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
if (target.startsWith(available)) return available.length === target.length ? 'header' : 'partial';
|
|
33
|
+
return available.startsWith(target) ? 'header' : 'json';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class StdioFrameParser {
|
|
37
|
+
constructor(onMessage, onError, options = {}) {
|
|
38
|
+
this.onMessage = onMessage;
|
|
39
|
+
this.onError = onError;
|
|
40
|
+
this.maxFrameBytes = Number(options.maxFrameBytes || DEFAULT_MAX_FRAME_BYTES);
|
|
41
|
+
this.buffer = Buffer.alloc(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
push(chunk) {
|
|
45
|
+
if (chunk == null || chunk.length === 0) return;
|
|
46
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
47
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, value]) : value;
|
|
48
|
+
this._consume(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
end() {
|
|
52
|
+
this._consume(true);
|
|
53
|
+
if (this.buffer.length && this.buffer.toString('utf8').trim()) {
|
|
54
|
+
this._emitError(new Error('Incomplete JSON-RPC frame.'), 'newline');
|
|
55
|
+
}
|
|
56
|
+
this.buffer = Buffer.alloc(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_emitError(error, framing) {
|
|
60
|
+
if (typeof this.onError === 'function') this.onError(error, framing);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_emitBody(body, framing) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(body.toString('utf8'));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this._emitError(error, framing);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.onMessage(parsed, framing);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_consume(flush) {
|
|
75
|
+
while (this.buffer.length) {
|
|
76
|
+
const whitespace = leadingWhitespaceLength(this.buffer);
|
|
77
|
+
if (whitespace === this.buffer.length) {
|
|
78
|
+
if (flush) this.buffer = Buffer.alloc(0);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (whitespace) this.buffer = this.buffer.subarray(whitespace);
|
|
83
|
+
const state = headerState(this.buffer, 0);
|
|
84
|
+
if (state === 'partial' && !flush) return;
|
|
85
|
+
|
|
86
|
+
if (state === 'header') {
|
|
87
|
+
const end = indexOfHeaderEnd(this.buffer);
|
|
88
|
+
if (!end) {
|
|
89
|
+
if (flush) {
|
|
90
|
+
this._emitError(new Error('Incomplete Content-Length header.'), 'content-length');
|
|
91
|
+
this.buffer = Buffer.alloc(0);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const rawHeaders = this.buffer.subarray(0, end.index).toString('ascii');
|
|
96
|
+
const headers = rawHeaders.split(/\r?\n/);
|
|
97
|
+
const lengthHeader = headers.find((line) => /^\s*content-length\s*:/i.test(line));
|
|
98
|
+
const rawLength = lengthHeader && lengthHeader.replace(/^[^:]*:/, '').trim();
|
|
99
|
+
const length = rawLength && /^\d+$/.test(rawLength) ? Number(rawLength) : NaN;
|
|
100
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > this.maxFrameBytes) {
|
|
101
|
+
this._emitError(new Error('Invalid Content-Length header.'), 'content-length');
|
|
102
|
+
this.buffer = this.buffer.subarray(end.index + end.length);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const bodyStart = end.index + end.length;
|
|
106
|
+
if (this.buffer.length - bodyStart < length) {
|
|
107
|
+
if (flush) {
|
|
108
|
+
this._emitError(new Error('Incomplete Content-Length body.'), 'content-length');
|
|
109
|
+
this.buffer = Buffer.alloc(0);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length);
|
|
114
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
115
|
+
this._emitBody(body, 'content-length');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const newline = this.buffer.indexOf(0x0a);
|
|
120
|
+
if (newline < 0) {
|
|
121
|
+
if (!flush) return;
|
|
122
|
+
const body = this.buffer;
|
|
123
|
+
this.buffer = Buffer.alloc(0);
|
|
124
|
+
if (body.toString('utf8').trim()) this._emitBody(body, 'newline');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const body = this.buffer.subarray(0, newline);
|
|
128
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
129
|
+
if (body.toString('utf8').trim()) this._emitBody(body, 'newline');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createFrameWriter(output, options = {}) {
|
|
135
|
+
const secrets = options.secrets || [];
|
|
136
|
+
return function writeFrame(message, framing = 'newline') {
|
|
137
|
+
const body = Buffer.from(JSON.stringify(redactValue(message, secrets)), 'utf8');
|
|
138
|
+
if (framing === 'content-length') {
|
|
139
|
+
output.write(Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii'));
|
|
140
|
+
output.write(body);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
output.write(body);
|
|
144
|
+
output.write('\n');
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcError(id, code, message, data) {
|
|
149
|
+
const error = { code, message };
|
|
150
|
+
if (data !== undefined) error.data = data;
|
|
151
|
+
return { jsonrpc: '2.0', id: id == null ? null : id, error };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function requestIds(message) {
|
|
155
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
156
|
+
return messages
|
|
157
|
+
.filter((item) => item && typeof item === 'object'
|
|
158
|
+
&& typeof item.method === 'string'
|
|
159
|
+
&& Object.prototype.hasOwnProperty.call(item, 'id'))
|
|
160
|
+
.map((item) => item.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function idKey(id) {
|
|
164
|
+
return `${typeof id}:${JSON.stringify(id)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validRpcMessage(message) {
|
|
168
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) return false;
|
|
169
|
+
if (message.jsonrpc !== '2.0') return false;
|
|
170
|
+
if (typeof message.method === 'string') return true;
|
|
171
|
+
return Object.prototype.hasOwnProperty.call(message, 'id')
|
|
172
|
+
&& (Object.prototype.hasOwnProperty.call(message, 'result')
|
|
173
|
+
|| Object.prototype.hasOwnProperty.call(message, 'error'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validRpcPayload(payload) {
|
|
177
|
+
return Array.isArray(payload)
|
|
178
|
+
? payload.length > 0 && payload.every(validRpcMessage)
|
|
179
|
+
: validRpcMessage(payload);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isCancellation(payload) {
|
|
183
|
+
const messages = Array.isArray(payload) ? payload : [payload];
|
|
184
|
+
return messages.some((message) => message
|
|
185
|
+
&& message.method === 'notifications/cancelled');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function serveStdio(projectDir, options = {}) {
|
|
189
|
+
const input = options.input || process.stdin;
|
|
190
|
+
const output = options.output || process.stdout;
|
|
191
|
+
const errorOutput = options.error || process.stderr;
|
|
192
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
193
|
+
const token = String(config.token || config.sat || '');
|
|
194
|
+
const secrets = [token].filter(Boolean);
|
|
195
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
196
|
+
const writeFrame = createFrameWriter(output, { secrets });
|
|
197
|
+
const pending = new Set();
|
|
198
|
+
let initializePending = null;
|
|
199
|
+
let ended = false;
|
|
200
|
+
|
|
201
|
+
const reportStreamError = (error) => {
|
|
202
|
+
const text = redactText(error && error.message ? error.message : error, secrets);
|
|
203
|
+
errorOutput.write(`draftgo mcp serve: ${text}\n`);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const dispatch = async (message, framing) => {
|
|
207
|
+
if (!validRpcPayload(message)) {
|
|
208
|
+
writeFrame(rpcError(null, -32600, 'Invalid Request'), framing);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const responded = new Set();
|
|
212
|
+
try {
|
|
213
|
+
await client.forward(message, {
|
|
214
|
+
onMessage(remoteMessage) {
|
|
215
|
+
if (remoteMessage && typeof remoteMessage === 'object' && !remoteMessage.method
|
|
216
|
+
&& Object.prototype.hasOwnProperty.call(remoteMessage, 'id')) {
|
|
217
|
+
responded.add(idKey(remoteMessage.id));
|
|
218
|
+
}
|
|
219
|
+
writeFrame(remoteMessage, framing);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const missing = requestIds(message).filter((id) => !responded.has(idKey(id)));
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
const errors = missing.map((id) => rpcError(id, -32000, 'DraftGo MCP returned no response.'));
|
|
225
|
+
writeFrame(errors.length === 1 ? errors[0] : errors, framing);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
let missing = requestIds(message).filter((id) => !responded.has(idKey(id)));
|
|
229
|
+
if (!missing.length) {
|
|
230
|
+
reportStreamError(error);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (error && error.rpc) {
|
|
234
|
+
const rpc = redactValue(error.rpc, secrets);
|
|
235
|
+
writeFrame(rpc, framing);
|
|
236
|
+
for (const response of Array.isArray(rpc) ? rpc : [rpc]) {
|
|
237
|
+
if (response && Object.prototype.hasOwnProperty.call(response, 'id')) {
|
|
238
|
+
responded.add(idKey(response.id));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
missing = missing.filter((id) => !responded.has(idKey(id)));
|
|
242
|
+
if (!missing.length) return;
|
|
243
|
+
}
|
|
244
|
+
const code = Number.isInteger(error && error.code) ? error.code : -32000;
|
|
245
|
+
const safeMessage = redactText(error && error.message ? error.message : error, secrets);
|
|
246
|
+
const errors = missing.map((id) => rpcError(id, code, safeMessage || 'DraftGo MCP bridge error.'));
|
|
247
|
+
writeFrame(errors.length === 1 ? errors[0] : errors, framing);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const track = (promise) => {
|
|
252
|
+
pending.add(promise);
|
|
253
|
+
promise.finally(() => pending.delete(promise));
|
|
254
|
+
return promise;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const parser = new StdioFrameParser((message, framing) => {
|
|
258
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
259
|
+
const hasInitialize = messages.some((item) => item && item.method === 'initialize');
|
|
260
|
+
let task;
|
|
261
|
+
if (hasInitialize) {
|
|
262
|
+
task = dispatch(message, framing);
|
|
263
|
+
initializePending = task.finally(() => { initializePending = null; });
|
|
264
|
+
} else if (initializePending && !isCancellation(message)) {
|
|
265
|
+
task = initializePending.then(() => dispatch(message, framing));
|
|
266
|
+
} else {
|
|
267
|
+
task = dispatch(message, framing);
|
|
268
|
+
}
|
|
269
|
+
track(Promise.resolve(task).catch(reportStreamError));
|
|
270
|
+
}, (error, framing) => {
|
|
271
|
+
writeFrame(rpcError(null, -32700, 'Parse error'), framing);
|
|
272
|
+
reportStreamError(error);
|
|
273
|
+
}, options);
|
|
274
|
+
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const finish = async () => {
|
|
277
|
+
if (ended) return;
|
|
278
|
+
ended = true;
|
|
279
|
+
parser.end();
|
|
280
|
+
await Promise.allSettled([...pending]);
|
|
281
|
+
resolve(0);
|
|
282
|
+
};
|
|
283
|
+
input.on('data', (chunk) => parser.push(chunk));
|
|
284
|
+
input.once('end', finish);
|
|
285
|
+
input.once('error', (error) => {
|
|
286
|
+
reportStreamError(error);
|
|
287
|
+
reject(error);
|
|
288
|
+
});
|
|
289
|
+
if (input.readableEnded || input.destroyed) queueMicrotask(finish);
|
|
290
|
+
else if (typeof input.resume === 'function') input.resume();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
DEFAULT_MAX_FRAME_BYTES,
|
|
296
|
+
StdioFrameParser,
|
|
297
|
+
createFrameWriter,
|
|
298
|
+
rpcError,
|
|
299
|
+
serveStdio,
|
|
300
|
+
};
|
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { DraftGoMcpClient } = require('./client');
|
|
4
|
+
const { WorktreeError } = require('../worktree/errors');
|
|
5
|
+
const { unwrapToolResult, toolNameMatches } = require('../worktree/backend');
|
|
6
|
+
|
|
7
|
+
const TOOL_NAMES = Object.freeze({
|
|
8
|
+
projectOverview: 'draftgo_project_overview',
|
|
9
|
+
resourceList: 'draftgo_resource_list',
|
|
10
|
+
resourceSearch: 'draftgo_resource_search',
|
|
11
|
+
resourceMetadata: 'draftgo_resource_get_metadata',
|
|
12
|
+
resourceFragment: 'draftgo_resource_read_fragment',
|
|
13
|
+
apiSearch: 'draftgo_api_search',
|
|
14
|
+
apiDescribe: 'draftgo_api_describe',
|
|
15
|
+
apiCall: 'draftgo_api_call',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function openToolSession(config, expected = [], options = {}) {
|
|
19
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
20
|
+
await client.initialize(options);
|
|
21
|
+
const tools = await client.listAllTools(options);
|
|
22
|
+
const names = {};
|
|
23
|
+
for (const expectedName of expected) {
|
|
24
|
+
const found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), expectedName));
|
|
25
|
+
if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${expectedName} is not available.`);
|
|
26
|
+
names[expectedName] = found.name;
|
|
27
|
+
}
|
|
28
|
+
return { client, tools, names };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callStructured(session, expectedName, args = {}, options = {}) {
|
|
32
|
+
const name = session.names[expectedName] || expectedName;
|
|
33
|
+
const result = await session.client.toolsCall(name, args, options);
|
|
34
|
+
return unwrapToolResult(result);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { TOOL_NAMES, openToolSession, callStructured };
|
package/src/platforms.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Single source of truth describing every supported AI-tool target.
|
|
4
|
-
// Each platform receives the complete instruction body
|
|
5
|
-
//
|
|
6
|
-
// .draftgo/skill-shared.
|
|
4
|
+
// Each platform receives the complete instruction body, subskills, domain
|
|
5
|
+
// references, and the small scripts directory used for bundled documentation.
|
|
7
6
|
//
|
|
8
7
|
// mainFile : the markdown file the AI tool reads first
|
|
9
|
-
// assetDir : where
|
|
8
|
+
// assetDir : where subskills, references, and scripts are placed
|
|
10
9
|
// skillDir : project-relative path used to substitute {{SKILL_DIR}} in
|
|
11
10
|
// template files (typically === assetDir written with forward
|
|
12
11
|
// slashes so it works in markdown)
|
package/src/projectConfig.js
CHANGED
|
@@ -1,62 +1,104 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
// bootstrapping local context via draftgo_init.py.
|
|
5
|
-
|
|
3
|
+
const fs = require('fs');
|
|
6
4
|
const path = require('path');
|
|
7
|
-
const {
|
|
8
|
-
const log = require('./logger');
|
|
9
|
-
const { writeText, exists, appendGitignoreLine } = require('./fsx');
|
|
10
|
-
const { findPython } = require('./python');
|
|
11
|
-
const { platforms } = require('./platforms');
|
|
5
|
+
const { exists, ensureDir, appendGitignoreLine } = require('./fsx');
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const IGNORE_ENTRIES = [
|
|
8
|
+
'.draftgo/config.json',
|
|
9
|
+
'.draftgo/token',
|
|
10
|
+
'.draftgo/worktree/',
|
|
11
|
+
'.draftgo/conflicts/',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function normalizeServer(raw) {
|
|
15
|
+
let value = String(raw || '').trim();
|
|
16
|
+
if (!value) return '';
|
|
17
|
+
if (!/^https?:\/\//i.test(value)) value = `http://${value}`;
|
|
18
|
+
const parsed = new URL(value);
|
|
19
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password
|
|
20
|
+
|| parsed.search || parsed.hash) {
|
|
21
|
+
throw new Error('DraftGo server must be an absolute HTTP(S) URL without credentials, query, or fragment.');
|
|
18
22
|
}
|
|
19
|
-
|
|
20
|
-
...existing,
|
|
21
|
-
server,
|
|
22
|
-
token,
|
|
23
|
-
auto_push: existing.auto_push === true,
|
|
24
|
-
};
|
|
25
|
-
writeText(cfg, JSON.stringify(config, null, 2) + '\n');
|
|
26
|
-
appendGitignoreLine(projectDir, '.draftgo/config.json');
|
|
27
|
-
return cfg;
|
|
23
|
+
return value.replace(/\/+$/, '');
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const p of platforms) {
|
|
33
|
-
const candidate = path.join(projectDir, p.assetDir, 'scripts', 'draftgo_init.py');
|
|
34
|
-
if (exists(candidate)) return candidate;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
26
|
+
function configPath(projectDir) {
|
|
27
|
+
return path.join(projectDir, '.draftgo', 'config.json');
|
|
37
28
|
}
|
|
38
29
|
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
function ensureProjectIgnores(projectDir) {
|
|
31
|
+
for (const entry of IGNORE_ENTRIES) appendGitignoreLine(projectDir, entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readExisting(projectDir) {
|
|
35
|
+
const file = configPath(projectDir);
|
|
36
|
+
if (!exists(file)) return {};
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Invalid .draftgo/config.json: ${error.message}`);
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writePrivateJson(file, value) {
|
|
46
|
+
ensureDir(path.dirname(file));
|
|
47
|
+
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
48
|
+
const temporary = `${file}.tmp-${suffix}`;
|
|
49
|
+
const backup = `${file}.bak-${suffix}`;
|
|
50
|
+
let backedUp = false;
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
53
|
+
try { fs.chmodSync(temporary, 0o600); } catch { /* Windows ACLs are inherited. */ }
|
|
54
|
+
const descriptor = fs.openSync(temporary, 'r+');
|
|
55
|
+
try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); }
|
|
56
|
+
if (exists(file)) { fs.renameSync(file, backup); backedUp = true; }
|
|
57
|
+
fs.renameSync(temporary, file);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (backedUp && !exists(file) && exists(backup)) fs.renameSync(backup, file);
|
|
60
|
+
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
if (exists(temporary)) fs.rmSync(temporary, { force: true });
|
|
50
63
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
if (exists(backup)) fs.rmSync(backup, { force: true });
|
|
65
|
+
try { fs.chmodSync(file, 0o600); } catch { /* Windows ACLs are inherited. */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeProjectConfig(projectDir, server, token) {
|
|
69
|
+
const normalizedServer = normalizeServer(server);
|
|
70
|
+
const sat = String(token || '').trim();
|
|
71
|
+
if (!normalizedServer) throw new Error('DraftGo server is required.');
|
|
72
|
+
if (!sat) throw new Error('DraftGo SAT is required.');
|
|
73
|
+
const existing = readExisting(projectDir);
|
|
74
|
+
const config = {
|
|
75
|
+
...existing,
|
|
76
|
+
server: normalizedServer,
|
|
77
|
+
token: sat,
|
|
78
|
+
auto_push: existing.auto_push === true,
|
|
79
|
+
};
|
|
80
|
+
const file = configPath(projectDir);
|
|
81
|
+
writePrivateJson(file, config);
|
|
82
|
+
ensureProjectIgnores(projectDir);
|
|
83
|
+
return file;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadProjectConfig(projectDir, { requireToken = true } = {}) {
|
|
87
|
+
const file = configPath(projectDir);
|
|
88
|
+
if (!exists(file)) throw new Error('Missing .draftgo/config.json; run `draftgo connect` first.');
|
|
89
|
+
const config = readExisting(projectDir);
|
|
90
|
+
const server = normalizeServer(config.server);
|
|
91
|
+
const token = String(config.token || config.sat || '').trim();
|
|
92
|
+
if (!server) throw new Error('DraftGo project config is missing server.');
|
|
93
|
+
if (requireToken && !token) throw new Error('DraftGo project config is missing SAT.');
|
|
94
|
+
return { ...config, server, token, path: file };
|
|
60
95
|
}
|
|
61
96
|
|
|
62
|
-
module.exports = {
|
|
97
|
+
module.exports = {
|
|
98
|
+
IGNORE_ENTRIES,
|
|
99
|
+
configPath,
|
|
100
|
+
normalizeServer,
|
|
101
|
+
ensureProjectIgnores,
|
|
102
|
+
writeProjectConfig,
|
|
103
|
+
loadProjectConfig,
|
|
104
|
+
};
|