draftgo-cli 1.0.4
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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/draftgo.js +9 -0
- package/package.json +70 -0
- package/resources/project-design/README.md +42 -0
- package/resources/skill/SKILL.md +62 -0
- package/resources/skill/init/SKILL.md +41 -0
- package/resources/skill/manifest.json +35 -0
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +97 -0
- package/resources/skill/references/architecture.md +13 -0
- package/resources/skill/references/chat-sdk.md +205 -0
- package/resources/skill/references/checkout.md +140 -0
- package/resources/skill/references/data.md +49 -0
- package/resources/skill/references/db-relations.md +29 -0
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/development.md +41 -0
- package/resources/skill/references/diagnostics.md +50 -0
- package/resources/skill/references/frontend.md +158 -0
- package/resources/skill/references/mcp.md +110 -0
- package/resources/skill/references/methods.md +143 -0
- package/resources/skill/references/modules.md +75 -0
- package/resources/skill/references/runtime.md +109 -0
- package/resources/skill/references/services.md +32 -0
- package/src/apiContractCache.js +120 -0
- package/src/cli.js +100 -0
- package/src/commandRegistry.js +46 -0
- package/src/commands/api.js +244 -0
- package/src/commands/apiKey.js +30 -0
- package/src/commands/autoPush.js +36 -0
- package/src/commands/capabilities.js +100 -0
- package/src/commands/check.js +82 -0
- package/src/commands/checkout.js +18 -0
- package/src/commands/clean.js +72 -0
- package/src/commands/commit.js +47 -0
- package/src/commands/components.js +554 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +91 -0
- package/src/commands/delete.js +95 -0
- package/src/commands/deploy.js +77 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/group.js +37 -0
- package/src/commands/help.js +190 -0
- package/src/commands/init.js +126 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/local.js +79 -0
- package/src/commands/map.js +395 -0
- package/src/commands/mcp.js +150 -0
- package/src/commands/reconcile.js +20 -0
- package/src/commands/role.js +31 -0
- package/src/commands/status.js +98 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +79 -0
- package/src/commands/verify.js +188 -0
- package/src/commands/visualVerify.js +281 -0
- package/src/commands/worklog.js +117 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +65 -0
- package/src/detect.js +25 -0
- package/src/diffReport.js +106 -0
- package/src/fsx.js +67 -0
- package/src/index.js +46 -0
- package/src/localRuntime/compose.js +119 -0
- package/src/localRuntime/detect.js +77 -0
- package/src/localRuntime/index.js +211 -0
- package/src/localRuntime/mysqlClient.js +155 -0
- package/src/localRuntime/services.js +117 -0
- package/src/logger.js +37 -0
- package/src/mcp/client.js +558 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/parallel.js +54 -0
- package/src/mcp/protocol.js +223 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +51 -0
- package/src/paths.js +32 -0
- package/src/platforms.js +110 -0
- package/src/projectConfig.js +139 -0
- package/src/projectDesign.js +19 -0
- package/src/projectHealth.js +33 -0
- package/src/projectMap.js +220 -0
- package/src/prompt.js +94 -0
- package/src/releaseInstall.js +105 -0
- package/src/runtimeFiles.js +45 -0
- package/src/skill.js +295 -0
- package/src/targets.js +43 -0
- package/src/timeout.js +18 -0
- package/src/updateCheck.js +100 -0
- package/src/worklog.js +276 -0
- package/src/worktree/backend.js +438 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +751 -0
- package/src/worktree/inlineScripts.js +99 -0
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +89 -0
- package/src/worktree/status.js +124 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const runtimeFiles = require('../runtimeFiles');
|
|
6
|
+
const log = require('../logger');
|
|
7
|
+
const { configPath, loadProjectConfig } = require('../projectConfig');
|
|
8
|
+
|
|
9
|
+
function numberFlag(value, fallback, min, max) {
|
|
10
|
+
const n = Number(value);
|
|
11
|
+
return Number.isFinite(n) ? Math.max(min, Math.min(max, Math.round(n))) : fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function configuredUiUrl(projectDir, rawUrl, tokenMode = 'auto') {
|
|
15
|
+
if (tokenMode === 'never') return rawUrl;
|
|
16
|
+
if (!fs.existsSync(configPath(projectDir))) return rawUrl;
|
|
17
|
+
|
|
18
|
+
const config = loadProjectConfig(projectDir, { requireToken: false });
|
|
19
|
+
if (!config.token) return rawUrl;
|
|
20
|
+
|
|
21
|
+
let target;
|
|
22
|
+
try {
|
|
23
|
+
target = new URL(rawUrl);
|
|
24
|
+
} catch {
|
|
25
|
+
return rawUrl;
|
|
26
|
+
}
|
|
27
|
+
const server = new URL(config.server);
|
|
28
|
+
if (target.origin !== server.origin) return rawUrl;
|
|
29
|
+
target.searchParams.set('token', config.token);
|
|
30
|
+
return target.toString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function redactUrlTokens(message) {
|
|
34
|
+
return String(message || '')
|
|
35
|
+
.replace(/([?&]token=)[^&#\s]+/gi, '$1<redacted>')
|
|
36
|
+
.replace(/\bsat_[A-Za-z0-9._~+/-]+/g, '<redacted>');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function executableCandidates() {
|
|
40
|
+
const explicit = process.env.DRAFTGO_BROWSER_PATH ? [process.env.DRAFTGO_BROWSER_PATH] : [];
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
|
|
43
|
+
const rels = [
|
|
44
|
+
['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
45
|
+
['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
46
|
+
['Chromium', 'Application', 'chrome.exe'],
|
|
47
|
+
];
|
|
48
|
+
return [...explicit, ...roots.flatMap((root) => rels.map((parts) => path.join(root, ...parts)))];
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
return [...explicit,
|
|
52
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
53
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
54
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
return [...explicit, '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/microsoft-edge', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function playwrightCacheCandidates() {
|
|
61
|
+
const root = process.env.PLAYWRIGHT_BROWSERS_PATH
|
|
62
|
+
|| (process.platform === 'win32'
|
|
63
|
+
? path.join(process.env.LOCALAPPDATA || '', 'ms-playwright')
|
|
64
|
+
: path.join(process.env.HOME || '', '.cache', 'ms-playwright'));
|
|
65
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
66
|
+
const candidates = [];
|
|
67
|
+
const visit = (directory, depth) => {
|
|
68
|
+
if (depth > 3) return;
|
|
69
|
+
let entries;
|
|
70
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return; }
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const absolute = path.join(directory, entry.name);
|
|
73
|
+
if (entry.isFile() && /^(?:chrome(?:-headless-shell)?|chromium|msedge|headless_shell)(?:\.exe)?$/i.test(entry.name)) candidates.push(absolute);
|
|
74
|
+
else if (entry.isDirectory()) visit(absolute, depth + 1);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
visit(root, 0);
|
|
78
|
+
return candidates;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function launchBrowser(chromium, requested, requestedPath) {
|
|
82
|
+
const attempts = [];
|
|
83
|
+
if (requestedPath) attempts.push({ executablePath: requestedPath, label: `path:${requestedPath}` });
|
|
84
|
+
if (requested && requested !== 'chromium') attempts.push({ channel: requested });
|
|
85
|
+
for (const executablePath of [...executableCandidates(), ...playwrightCacheCandidates()].filter((candidate) => fs.existsSync(candidate))) {
|
|
86
|
+
if (!attempts.some((attempt) => attempt.executablePath === executablePath)) {
|
|
87
|
+
attempts.push({ executablePath, label: `path:${executablePath}` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!requested || requested === 'chromium') attempts.push({});
|
|
91
|
+
for (const channel of ['msedge', 'chrome']) {
|
|
92
|
+
if (!attempts.some((a) => a.channel === channel)) attempts.push({ channel });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let lastError = null;
|
|
96
|
+
const attempted = [];
|
|
97
|
+
for (const options of attempts) {
|
|
98
|
+
try {
|
|
99
|
+
const { label, ...launchOptions } = options;
|
|
100
|
+
const browser = await chromium.launch({ headless: true, ...launchOptions });
|
|
101
|
+
return { browser, selected: label || options.channel || 'playwright-managed', attempted };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
lastError = err;
|
|
104
|
+
attempted.push(`${options.label || options.channel || 'playwright-managed'}: ${String(err.message || err).split('\n')[0]}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`未找到可用的 Chromium/Chrome/Edge。请使用 --browser-path <executable> 或 DRAFTGO_BROWSER_PATH。`
|
|
108
|
+
+ `尝试记录:${attempted.join('; ')}`
|
|
109
|
+
+ (lastError ? `;最后错误:${lastError.message.split('\n')[0]}` : ''));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function selectFrames(page, mode = 'auto') {
|
|
113
|
+
const top = page.mainFrame ? page.mainFrame() : page;
|
|
114
|
+
const selected = [{ frame: top, label: 'top' }];
|
|
115
|
+
if (mode === 'top') return selected;
|
|
116
|
+
if (!['auto', 'all'].includes(mode)) {
|
|
117
|
+
const handle = await page.locator(mode).first().elementHandle().catch(() => null);
|
|
118
|
+
const frame = handle && await handle.contentFrame().catch(() => null);
|
|
119
|
+
if (!frame) throw new Error(`Frame not found: ${mode}`);
|
|
120
|
+
return [{ frame, label: `selector:${mode}` }];
|
|
121
|
+
}
|
|
122
|
+
const frames = page.frames ? page.frames() : [];
|
|
123
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
124
|
+
const frame = frames[index];
|
|
125
|
+
if (frame === top) continue;
|
|
126
|
+
let visible = true;
|
|
127
|
+
if (mode === 'auto') {
|
|
128
|
+
const element = await frame.frameElement().catch(() => null);
|
|
129
|
+
visible = Boolean(element && await element.isVisible().catch(() => false));
|
|
130
|
+
}
|
|
131
|
+
if (visible) selected.push({
|
|
132
|
+
frame,
|
|
133
|
+
label: `frame:${frame.name && frame.name() ? frame.name() : index}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return selected;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function inspectFrame(frame) {
|
|
140
|
+
return frame.evaluate(() => {
|
|
141
|
+
const body = document.body;
|
|
142
|
+
const root = document.documentElement;
|
|
143
|
+
return {
|
|
144
|
+
bodyTextLength: body ? (body.innerText || '').trim().length : 0,
|
|
145
|
+
bodyHeight: body ? body.getBoundingClientRect().height : 0,
|
|
146
|
+
hasVisibleMedia: body ? Array.from(body.querySelectorAll('img,svg,canvas,video,iframe,input,button')).some((element) => {
|
|
147
|
+
const rect = element.getBoundingClientRect();
|
|
148
|
+
return rect.width > 1 && rect.height > 1;
|
|
149
|
+
}) : false,
|
|
150
|
+
horizontalOverflow: root.scrollWidth > window.innerWidth + 1,
|
|
151
|
+
scrollWidth: root.scrollWidth,
|
|
152
|
+
viewportWidth: window.innerWidth,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function captureScreenshot(projectDir, page) {
|
|
158
|
+
const dir = path.join(projectDir, '.draftgo', 'artifacts', 'ui');
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
const screenshotPath = path.join(dir, `ui-check-${process.pid}-${Date.now()}.png`);
|
|
161
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
162
|
+
runtimeFiles.register(projectDir, screenshotPath, 'ui-artifact', 'verify');
|
|
163
|
+
return screenshotPath;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function visualVerify(projectDir, positional, flags = {}) {
|
|
167
|
+
const rawUrl = String(flags.url || positional[0] || '').trim();
|
|
168
|
+
if (!/^https?:\/\//i.test(rawUrl)) {
|
|
169
|
+
log.err('Usage: draftgo verify --url <http://localhost:port/path> --screenshot always|--ui always');
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const tokenMode = String(flags.token || 'auto').toLowerCase();
|
|
174
|
+
if (!['auto', 'never'].includes(tokenMode)) {
|
|
175
|
+
log.err('--token in visual verification accepts auto or never.');
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let url;
|
|
180
|
+
try {
|
|
181
|
+
url = configuredUiUrl(projectDir, rawUrl, tokenMode);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
log.err(`Unable to read visual verification config: ${err.message}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let chromium;
|
|
188
|
+
try {
|
|
189
|
+
({ chromium } = require('playwright-core'));
|
|
190
|
+
} catch {
|
|
191
|
+
log.err('缺少 playwright-core,请重新安装或升级 draftgo-cli。');
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const width = numberFlag(flags.width, 1440, 240, 3840);
|
|
196
|
+
const height = numberFlag(flags.height, 900, 320, 2160);
|
|
197
|
+
const waitMs = numberFlag(flags['wait-ms'], 500, 0, 10000);
|
|
198
|
+
const screenshotMode = String(flags.screenshot || 'never').toLowerCase();
|
|
199
|
+
if (!['always', 'never'].includes(screenshotMode)) {
|
|
200
|
+
log.err('--screenshot accepts always or never.');
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let browser;
|
|
205
|
+
try {
|
|
206
|
+
log.info('UI source: requested URL response.');
|
|
207
|
+
const launch = await launchBrowser(
|
|
208
|
+
chromium,
|
|
209
|
+
flags.browser && String(flags.browser),
|
|
210
|
+
flags['browser-path'] && String(flags['browser-path']),
|
|
211
|
+
);
|
|
212
|
+
browser = launch.browser;
|
|
213
|
+
log.info(`Browser: ${launch.selected}`);
|
|
214
|
+
const page = await browser.newPage({ viewport: { width, height } });
|
|
215
|
+
const consoleErrors = [];
|
|
216
|
+
const pageErrors = [];
|
|
217
|
+
page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(redactUrlTokens(msg.text())); });
|
|
218
|
+
page.on('pageerror', (err) => pageErrors.push(redactUrlTokens(err.message)));
|
|
219
|
+
|
|
220
|
+
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
221
|
+
if (waitMs) await page.waitForTimeout(waitMs);
|
|
222
|
+
let screenshotPath = null;
|
|
223
|
+
if (screenshotMode === 'always' || flags['capture-only']) {
|
|
224
|
+
screenshotPath = await captureScreenshot(projectDir, page);
|
|
225
|
+
}
|
|
226
|
+
if (flags['capture-only']) {
|
|
227
|
+
log.ok(`截图已生成:${width}x${height}`);
|
|
228
|
+
log.info(`截图:${screenshotPath}`);
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
const frames = await selectFrames(page, String(flags.frame || 'auto'));
|
|
232
|
+
const states = [];
|
|
233
|
+
for (const item of frames) states.push({ ...item, state: await inspectFrame(item.frame) });
|
|
234
|
+
|
|
235
|
+
const issues = [];
|
|
236
|
+
if (response && response.status() >= 400) issues.push(`页面返回 HTTP ${response.status()}`);
|
|
237
|
+
if (!states.some(({ state }) => state.bodyHeight && (state.bodyTextLength || state.hasVisibleMedia))) issues.push('页面疑似空白');
|
|
238
|
+
for (const { label, state } of states) {
|
|
239
|
+
if (state.horizontalOverflow) issues.push(`${label} 横向溢出:scrollWidth=${state.scrollWidth}, viewport=${state.viewportWidth}`);
|
|
240
|
+
}
|
|
241
|
+
if (consoleErrors.length) issues.push(`console error ${consoleErrors.length} 条`);
|
|
242
|
+
if (pageErrors.length) issues.push(`page error ${pageErrors.length} 条`);
|
|
243
|
+
if (flags.selector) {
|
|
244
|
+
let match = null;
|
|
245
|
+
for (const item of frames) {
|
|
246
|
+
if (await item.frame.locator(String(flags.selector)).first().isVisible().catch(() => false)) {
|
|
247
|
+
match = item.label;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!match) issues.push(`关键元素在已检查 frame 中不可见:${flags.selector}`);
|
|
252
|
+
else log.info(`Selector ${flags.selector}: visible in ${match}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (issues.length) {
|
|
256
|
+
issues.forEach((issue) => log.err(issue));
|
|
257
|
+
if (consoleErrors.length) consoleErrors.slice(0, 5).forEach((msg) => log.dim(` console: ${msg}`));
|
|
258
|
+
if (pageErrors.length) pageErrors.slice(0, 5).forEach((msg) => log.dim(` page: ${msg}`));
|
|
259
|
+
if (screenshotPath) log.info(`失败截图:${screenshotPath}`);
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log.ok(`UI smoke check passed: ${width}x${height}, ${frames.length} frame(s).`);
|
|
264
|
+
if (screenshotPath) log.info(`截图:${screenshotPath}`);
|
|
265
|
+
return 0;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
log.err(`UI smoke check 失败:${redactUrlTokens(err.message)}`);
|
|
268
|
+
return 1;
|
|
269
|
+
} finally {
|
|
270
|
+
if (browser) await browser.close().catch(() => {});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = visualVerify;
|
|
275
|
+
module.exports.configuredUiUrl = configuredUiUrl;
|
|
276
|
+
module.exports.redactUrlTokens = redactUrlTokens;
|
|
277
|
+
module.exports.executableCandidates = executableCandidates;
|
|
278
|
+
module.exports.playwrightCacheCandidates = playwrightCacheCandidates;
|
|
279
|
+
module.exports.selectFrames = selectFrames;
|
|
280
|
+
module.exports.inspectFrame = inspectFrame;
|
|
281
|
+
module.exports.captureScreenshot = captureScreenshot;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const {
|
|
5
|
+
appendItem,
|
|
6
|
+
mutateWorklog,
|
|
7
|
+
normalizeDate,
|
|
8
|
+
readWorklog,
|
|
9
|
+
renderWorklog,
|
|
10
|
+
resolveReference,
|
|
11
|
+
updateItem,
|
|
12
|
+
worklogPath,
|
|
13
|
+
} = require('../worklog');
|
|
14
|
+
|
|
15
|
+
function printUsage() {
|
|
16
|
+
log.err('Usage: draftgo work start|add|start-item|wait|complete|show|list <value>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function output(flags, value) {
|
|
20
|
+
if (flags.output === 'json') console.log(JSON.stringify(value, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function latestReference(blocks) {
|
|
24
|
+
const block = blocks[blocks.length - 1];
|
|
25
|
+
return `${block.date}#${block.entries[block.entries.length - 1].number}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resultFor(blocks, reference) {
|
|
29
|
+
const resolved = resolveReference(blocks, reference);
|
|
30
|
+
return { date: resolved.block.date, ...resolved.entry, status: visibleStatus(resolved.entry.status) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function visibleStatus(status) { return status === 'pending' ? 'waiting' : status; }
|
|
34
|
+
function visibleBlocks(blocks) {
|
|
35
|
+
return blocks.map(block => ({ ...block, entries: block.entries.map(entry => ({ ...entry, status: visibleStatus(entry.status) })) }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function add(projectDir, positional, flags, status) {
|
|
39
|
+
const title = positional.join(' ').trim();
|
|
40
|
+
if (!title) { printUsage(); return 1; }
|
|
41
|
+
const result = mutateWorklog(projectDir, (blocks) => {
|
|
42
|
+
const date = normalizeDate(flags.date);
|
|
43
|
+
const next = appendItem(blocks, title, status, flags.note ? [String(flags.note).trim()] : [], date);
|
|
44
|
+
return { blocks: next, reference: latestReference(next) };
|
|
45
|
+
});
|
|
46
|
+
const item = resultFor(result.blocks, result.reference);
|
|
47
|
+
output(flags, { path: result.path, item });
|
|
48
|
+
if (flags.output !== 'json') {
|
|
49
|
+
const verb = status === 'active' ? 'Started' : 'Added';
|
|
50
|
+
log.ok(`${verb} work item #${item.number} (${item.date}#${item.number}): ${item.title}`);
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function change(projectDir, positional, flags, status) {
|
|
56
|
+
const reference = positional[0];
|
|
57
|
+
if (!reference) { printUsage(); return 1; }
|
|
58
|
+
const result = mutateWorklog(projectDir, (blocks) => {
|
|
59
|
+
const note = flags.note == null ? '' : String(flags.note).trim();
|
|
60
|
+
const next = updateItem(blocks, reference, status, note);
|
|
61
|
+
return { blocks: next, reference };
|
|
62
|
+
});
|
|
63
|
+
output(flags, { path: result.path, item: resultFor(result.blocks, result.reference) });
|
|
64
|
+
if (flags.output !== 'json') log.ok(`Worklog item ${result.reference} marked ${status}.`);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function show(projectDir, flags, reference) {
|
|
69
|
+
if (reference) {
|
|
70
|
+
const item = resultFor(readWorklog(projectDir), reference);
|
|
71
|
+
item.reference = `${item.date}#${item.number}`;
|
|
72
|
+
output(flags, { path: worklogPath(projectDir), item });
|
|
73
|
+
if (flags.output !== 'json') {
|
|
74
|
+
log.info(`${item.reference} [${item.status}] ${item.title}`);
|
|
75
|
+
item.notes.forEach((note) => log.plain(` ${note}`));
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const blocks = visibleBlocks(readWorklog(projectDir));
|
|
80
|
+
output(flags, { path: worklogPath(projectDir), blocks });
|
|
81
|
+
if (flags.output !== 'json') console.log(renderWorklog(blocks) || 'Worklog is empty.');
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function list(projectDir, flags) {
|
|
86
|
+
const blocks = visibleBlocks(readWorklog(projectDir));
|
|
87
|
+
const statuses = ['active', 'waiting', 'completed'];
|
|
88
|
+
if (flags.status !== undefined && !statuses.includes(flags.status)) throw new Error('status must be active, waiting, or completed.');
|
|
89
|
+
const date = flags.date === undefined ? undefined : normalizeDate(flags.date);
|
|
90
|
+
const limit = flags.limit === undefined ? 20 : Number(flags.limit);
|
|
91
|
+
const offset = flags.offset === undefined ? 0 : Number(flags.offset);
|
|
92
|
+
if (typeof flags.limit === 'boolean' || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error('limit must be between 1 and 100.');
|
|
93
|
+
if (typeof flags.offset === 'boolean' || !Number.isSafeInteger(offset) || offset < 0) throw new Error('offset must be a non-negative integer.');
|
|
94
|
+
const matching = blocks.flatMap((block) => block.entries.map((entry) => ({ date: block.date, ...entry, reference: `${block.date}#${entry.number}` })))
|
|
95
|
+
.filter((item) => (!flags.status || item.status === flags.status) && (!date || item.date === date)).reverse();
|
|
96
|
+
const items = matching.slice(offset, offset + limit);
|
|
97
|
+
const nextOffset = offset + items.length < matching.length ? offset + items.length : null;
|
|
98
|
+
output(flags, { path: worklogPath(projectDir), items, total: matching.length, next_offset: nextOffset });
|
|
99
|
+
if (flags.output !== 'json') items.forEach((item) => log.info(`${item.date}#${item.number} [${item.status}] ${item.title}`));
|
|
100
|
+
if (flags.output !== 'json' && nextOffset !== null) log.dim(`More items: repeat with --offset ${nextOffset}.`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function worklog(projectDir, positional = [], flags = {}) {
|
|
105
|
+
const operation = positional[0] || 'show';
|
|
106
|
+
if (operation === 'start') return add(projectDir, positional.slice(1), flags, 'active');
|
|
107
|
+
if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'waiting');
|
|
108
|
+
if (operation === 'wait') return change(projectDir, positional.slice(1), flags, 'waiting');
|
|
109
|
+
if (operation === 'start-item' || operation === 'start_item') return change(projectDir, positional.slice(1), flags, 'active');
|
|
110
|
+
if (operation === 'complete' || operation === 'done') return change(projectDir, positional.slice(1), flags, 'completed');
|
|
111
|
+
if (operation === 'show') return show(projectDir, flags, positional[1]);
|
|
112
|
+
if (operation === 'list') return list(projectDir, flags);
|
|
113
|
+
throw new Error(`Unknown worklog operation ${operation}.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
worklog.printUsage = printUsage;
|
|
117
|
+
module.exports = worklog;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Windows console hosts still use the active console code page to decode the
|
|
7
|
+
* UTF-8 bytes Node writes. Switch only an interactive console to UTF-8.
|
|
8
|
+
*
|
|
9
|
+
* This deliberately does not run for redirected stdout: JSON consumers and
|
|
10
|
+
* MCP stdio use pipes, where Node's UTF-8 byte stream is already correct and
|
|
11
|
+
* changing a shared console would be both unnecessary and surprising.
|
|
12
|
+
*/
|
|
13
|
+
function configureConsoleUtf8(options = {}) {
|
|
14
|
+
const platform = options.platform || process.platform;
|
|
15
|
+
const stdout = options.stdout || process.stdout;
|
|
16
|
+
const run = options.spawnSync || spawnSync;
|
|
17
|
+
|
|
18
|
+
if (platform !== 'win32' || !stdout || !stdout.isTTY) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// chcp changes the code page for the inherited Windows console. Redirect
|
|
22
|
+
// its own localized status line so stdout remains exclusively CLI output.
|
|
23
|
+
run('cmd.exe', ['/d', '/s', '/c', 'chcp 65001 >nul'], {
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
// Encoding setup is a presentation enhancement; never block the command.
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { configureConsoleUtf8 };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HARD_INCOMPATIBLE_CODES = new Set([
|
|
4
|
+
'CONTRACT_INCOMPATIBLE',
|
|
5
|
+
'CLIENT_VERSION_UNSUPPORTED',
|
|
6
|
+
'CLI_VERSION_UNSUPPORTED',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function errorCode(error) {
|
|
10
|
+
return String(error && error.code || '').trim().toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isContractCompatibilityError(error) {
|
|
14
|
+
const code = errorCode(error);
|
|
15
|
+
return code === 'CONTRACT_CHANGED' || HARD_INCOMPATIBLE_CODES.has(code);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isHardContractIncompatibility(error) {
|
|
19
|
+
return HARD_INCOMPATIBLE_CODES.has(errorCode(error));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDangerousOperation(contract) {
|
|
23
|
+
const operation = contract && contract.operation || contract || {};
|
|
24
|
+
const risk = String(operation.risk || '').toLowerCase();
|
|
25
|
+
return operation.destructive === true || risk === 'high' || risk === 'critical';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function canonicalValue(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
30
|
+
if (!value || typeof value !== 'object') return value;
|
|
31
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sameOperationContract(left, right) {
|
|
35
|
+
const leftOperation = left && left.operation || left || {};
|
|
36
|
+
const rightOperation = right && right.operation || right || {};
|
|
37
|
+
return JSON.stringify(canonicalValue(leftOperation)) === JSON.stringify(canonicalValue(rightOperation));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function canRetryAfterContractChange(left, right) {
|
|
41
|
+
const operation = right && right.operation || right || {};
|
|
42
|
+
const method = String(operation.method || '').toUpperCase();
|
|
43
|
+
return method === 'GET' || method === 'HEAD' || sameOperationContract(left, right);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function incompatibleOperationError(operationId, error) {
|
|
47
|
+
const wrapped = new Error(
|
|
48
|
+
`DraftGo CLI and server contracts are incompatible for operation ${operationId}; `
|
|
49
|
+
+ 'the call was blocked before retry. Upgrade DraftGo CLI, then describe and confirm the operation again.',
|
|
50
|
+
);
|
|
51
|
+
wrapped.code = 'CLI_CONTRACT_INCOMPATIBLE';
|
|
52
|
+
wrapped.details = error && (error.details || error.data);
|
|
53
|
+
wrapped.cause = error;
|
|
54
|
+
return wrapped;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
errorCode,
|
|
59
|
+
isContractCompatibilityError,
|
|
60
|
+
isHardContractIncompatibility,
|
|
61
|
+
isDangerousOperation,
|
|
62
|
+
sameOperationContract,
|
|
63
|
+
canRetryAfterContractChange,
|
|
64
|
+
incompatibleOperationError,
|
|
65
|
+
};
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { exists } = require('./fsx');
|
|
5
|
+
const { platforms } = require('./platforms');
|
|
6
|
+
|
|
7
|
+
// Each target declares one or more signals (files/dirs). If ANY signal matches,
|
|
8
|
+
// the target is considered "in use" in the current project. Signals must be
|
|
9
|
+
// host-owned directories or files, not generic instruction files such as
|
|
10
|
+
// AGENTS.md that several tools may share.
|
|
11
|
+
const SIGNALS = Object.fromEntries(
|
|
12
|
+
platforms.map((p) => [p.name, p.signals || []])
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
function detectTargets(projectDir) {
|
|
16
|
+
const hits = [];
|
|
17
|
+
for (const [name, signals] of Object.entries(SIGNALS)) {
|
|
18
|
+
if (signals.some((s) => exists(path.join(projectDir, s)))) {
|
|
19
|
+
hits.push(name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return hits;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { SIGNALS, detectTargets };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function lineStats(output) {
|
|
6
|
+
let additions = 0;
|
|
7
|
+
let deletions = 0;
|
|
8
|
+
let inHunk = false;
|
|
9
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
10
|
+
if (line.startsWith('@@ ')) {
|
|
11
|
+
inHunk = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (line.startsWith('diff --git ')) {
|
|
15
|
+
inHunk = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!inHunk || line === '\') continue;
|
|
19
|
+
if (line.startsWith('+')) additions += 1;
|
|
20
|
+
else if (line.startsWith('-')) deletions += 1;
|
|
21
|
+
}
|
|
22
|
+
return { additions, deletions, lines_changed: additions + deletions };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function byteSize(file) {
|
|
26
|
+
return fs.statSync(file).size;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileStat(file) {
|
|
30
|
+
const base_bytes = byteSize(file.base_path);
|
|
31
|
+
const local_bytes = byteSize(file.local_path);
|
|
32
|
+
return {
|
|
33
|
+
path: file.path,
|
|
34
|
+
changed: Boolean(file.changed),
|
|
35
|
+
...lineStats(file.output),
|
|
36
|
+
base_bytes,
|
|
37
|
+
local_bytes,
|
|
38
|
+
bytes_delta: local_bytes - base_bytes,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resourceMetadata(entry) {
|
|
43
|
+
return {
|
|
44
|
+
resource_type: entry.resource_type,
|
|
45
|
+
resource_id: entry.resource_id,
|
|
46
|
+
title: entry.title || null,
|
|
47
|
+
route: entry.route || null,
|
|
48
|
+
slug: entry.slug || null,
|
|
49
|
+
local_path: entry.local_path || null,
|
|
50
|
+
base_path: entry.base_path || null,
|
|
51
|
+
base_version: entry.base_version ?? null,
|
|
52
|
+
base_revision: entry.base_revision ?? null,
|
|
53
|
+
base_etag: entry.base_etag ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function report(entry, files) {
|
|
58
|
+
const file_stats = files.map(fileStat);
|
|
59
|
+
const changed_files = file_stats.filter((file) => file.changed);
|
|
60
|
+
const additions = changed_files.reduce((total, file) => total + file.additions, 0);
|
|
61
|
+
const deletions = changed_files.reduce((total, file) => total + file.deletions, 0);
|
|
62
|
+
const base_bytes = file_stats.reduce((total, file) => total + file.base_bytes, 0);
|
|
63
|
+
const local_bytes = file_stats.reduce((total, file) => total + file.local_bytes, 0);
|
|
64
|
+
return {
|
|
65
|
+
resource: resourceMetadata(entry),
|
|
66
|
+
changed: changed_files.length > 0,
|
|
67
|
+
bytes: { base: base_bytes, local: local_bytes, delta: local_bytes - base_bytes },
|
|
68
|
+
stats: {
|
|
69
|
+
files_changed: changed_files.length,
|
|
70
|
+
additions,
|
|
71
|
+
deletions,
|
|
72
|
+
lines_changed: additions + deletions,
|
|
73
|
+
files: file_stats,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resourceLabel(resource) {
|
|
79
|
+
return `${resource.resource_type} ${resource.resource_id}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatSummary(value) {
|
|
83
|
+
const { resource, bytes } = value;
|
|
84
|
+
const fields = [
|
|
85
|
+
`${resourceLabel(resource)}: ${value.changed ? 'changed' : 'no local changes'}`,
|
|
86
|
+
`title: ${resource.title || '-'}`,
|
|
87
|
+
`path: ${resource.local_path || '-'}`,
|
|
88
|
+
`base version: ${resource.base_version ?? '-'}`,
|
|
89
|
+
`base revision: ${resource.base_revision ?? '-'}`,
|
|
90
|
+
`bytes: ${bytes.base} -> ${bytes.local} (${bytes.delta >= 0 ? '+' : ''}${bytes.delta})`,
|
|
91
|
+
];
|
|
92
|
+
if (resource.route) fields.splice(2, 0, `route: ${resource.route}`);
|
|
93
|
+
return `${fields.join('\n')}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatStat(value) {
|
|
97
|
+
const { stats } = value;
|
|
98
|
+
const files = stats.files.filter((file) => file.changed).map((file) => {
|
|
99
|
+
const marker = `${file.additions ? `${'+'.repeat(Math.min(file.additions, 20))}` : ''}${file.deletions ? `${'-'.repeat(Math.min(file.deletions, 20))}` : ''}` || '0';
|
|
100
|
+
return `${file.path} | ${file.lines_changed} ${marker}`;
|
|
101
|
+
});
|
|
102
|
+
const summary = `${stats.files_changed} file${stats.files_changed === 1 ? '' : 's'} changed, ${stats.additions} insertion${stats.additions === 1 ? '' : 's'}(+), ${stats.deletions} deletion${stats.deletions === 1 ? '' : 's'}(-)`;
|
|
103
|
+
return `${formatSummary(value)}${files.length ? `${files.join('\n')}\n` : ''}${summary}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { lineStats, report, formatSummary, formatStat };
|