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.
Files changed (64) hide show
  1. package/README.md +220 -269
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +13 -5
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +99 -23
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +37 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +250 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
@@ -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
+ };
@@ -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 (SKILL.md + sub-skills
5
- // + rules + scripts). Only the large OpenAPI snapshot is shared once under
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 sub-skills (init/sync/bug), rules/, scripts/ are placed
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)
@@ -1,62 +1,104 @@
1
1
  'use strict';
2
2
 
3
- // Shared helpers for writing <project>/.draftgo/config.json and (optionally)
4
- // bootstrapping local context via draftgo_init.py.
5
-
3
+ const fs = require('fs');
6
4
  const path = require('path');
7
- const { spawnSync } = require('child_process');
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
- function writeProjectConfig(projectDir, server, token) {
14
- const cfg = path.join(projectDir, '.draftgo', 'config.json');
15
- let existing = {};
16
- if (exists(cfg)) {
17
- try { existing = JSON.parse(require('fs').readFileSync(cfg, 'utf8')); } catch { /* reset malformed config */ }
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
- const config = {
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
- // Find a draftgo_init.py shipped with any installed platform.
31
- function findInitScript(projectDir) {
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 maybeRunInit(projectDir, server, token) {
40
- const py = findPython();
41
- if (!py) {
42
- log.dim(' 未检测到 Python,跳过自动初始化项目上下文。');
43
- log.dim(' 安装 Python 3.9+ 后运行 `draftgo pull` 获取项目资源。');
44
- return false;
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
- const script = findInitScript(projectDir);
47
- if (!script) {
48
- log.dim(' 未找到 init 脚本(先运行 `draftgo init` 安装到至少一个 AI 工具目录)。');
49
- return false;
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
- log.step('运行 draftgo_init.py 拉取初始上下文…');
52
- const r = spawnSync(py.bin, [script, '--server', server, projectDir], {
53
- stdio: 'inherit',
54
- shell: false,
55
- env: { ...process.env, DRAFTGO_TOKEN: token },
56
- });
57
- if (r.status === 0) { log.ok(' 初始化完成'); return true; }
58
- log.warn(' init 脚本返回非零状态,可稍后手动重跑。');
59
- return false;
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 = { writeProjectConfig, maybeRunInit, findInitScript };
97
+ module.exports = {
98
+ IGNORE_ENTRIES,
99
+ configPath,
100
+ normalizeServer,
101
+ ensureProjectIgnores,
102
+ writeProjectConfig,
103
+ loadProjectConfig,
104
+ };