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,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const { PACKAGE_ROOT } = require('./paths');
|
|
10
|
+
|
|
11
|
+
const MARKER = '.draftgo-install.json';
|
|
12
|
+
const REPO = process.env.DRAFTGO_REPO || 'draftgo/draftgo-cli';
|
|
13
|
+
const BASE_URL = process.env.DRAFTGO_INSTALL_BASE_URL || 'https://draftgo.cn/draftgo-cli';
|
|
14
|
+
const MIRROR = process.env.DRAFTGO_GITHUB_MIRROR || `https://ghproxy.net/https://github.com/${REPO}/releases/download`;
|
|
15
|
+
|
|
16
|
+
function readInstallMarker() {
|
|
17
|
+
try {
|
|
18
|
+
const marker = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, MARKER), 'utf8'));
|
|
19
|
+
if (!marker.installRoot || !marker.binDir) return null;
|
|
20
|
+
return { installRoot: path.resolve(marker.installRoot), binDir: path.resolve(marker.binDir) };
|
|
21
|
+
} catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function download(url, output, timeoutMs = 15000, redirects = 0) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
if (redirects > 5) return reject(new Error('Too many download redirects.'));
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
29
|
+
const req = transport.get(parsed, { timeout: timeoutMs, headers: { 'user-agent': 'draftgo-cli' } }, (res) => {
|
|
30
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
31
|
+
res.resume();
|
|
32
|
+
return resolve(download(new URL(res.headers.location, parsed).toString(), output, timeoutMs, redirects + 1));
|
|
33
|
+
}
|
|
34
|
+
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
|
|
35
|
+
const stream = fs.createWriteStream(output, { flags: 'wx' });
|
|
36
|
+
stream.on('error', reject);
|
|
37
|
+
res.on('error', reject);
|
|
38
|
+
stream.on('finish', () => stream.close(() => resolve(url)));
|
|
39
|
+
res.pipe(stream);
|
|
40
|
+
});
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
req.on('timeout', () => req.destroy(new Error('Download timed out.')));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function downloadFirst(urls, output) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
for (const url of urls) {
|
|
49
|
+
try { return { url: await download(url, output), output }; }
|
|
50
|
+
catch (error) { errors.push(`${url}: ${error.message}`); try { fs.rmSync(output, { force: true }); } catch {} }
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Unable to download DraftGo release.\n${errors.join('\n')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hashFile(file) {
|
|
56
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeLauncher(binDir, packageDir) {
|
|
60
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
const launcher = path.join(binDir, 'draftgo.cmd');
|
|
63
|
+
fs.writeFileSync(launcher, `@echo off\r\nnode "${path.join(packageDir, 'bin', 'draftgo.js')}" %*\r\n`, 'ascii');
|
|
64
|
+
return launcher;
|
|
65
|
+
}
|
|
66
|
+
const launcher = path.join(binDir, 'draftgo');
|
|
67
|
+
fs.writeFileSync(launcher, `#!/usr/bin/env sh\nexec node "${path.join(packageDir, 'bin', 'draftgo.js')}" "$@"\n`, { mode: 0o755 });
|
|
68
|
+
fs.chmodSync(launcher, 0o755);
|
|
69
|
+
return launcher;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function installRelease(version, expectedSha, marker = readInstallMarker()) {
|
|
73
|
+
if (!marker) return null;
|
|
74
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version))) throw new Error('Invalid release version.');
|
|
75
|
+
fs.mkdirSync(marker.installRoot, { recursive: true });
|
|
76
|
+
const tempDir = fs.mkdtempSync(path.join(marker.installRoot, '.update-'));
|
|
77
|
+
const archive = path.join(tempDir, `draftgo-cli-${version}.tgz`);
|
|
78
|
+
const asset = path.basename(archive);
|
|
79
|
+
const urls = [`${BASE_URL}/${asset}`, `${MIRROR}/v${version}/${asset}`, `https://github.com/${REPO}/releases/download/v${version}/${asset}`];
|
|
80
|
+
try {
|
|
81
|
+
const downloaded = await downloadFirst(urls, archive);
|
|
82
|
+
let checksum = expectedSha;
|
|
83
|
+
if (!checksum) {
|
|
84
|
+
const checksumFile = path.join(tempDir, `${asset}.sha256`);
|
|
85
|
+
await download(`${downloaded.url}.sha256`, checksumFile);
|
|
86
|
+
checksum = fs.readFileSync(checksumFile, 'utf8').trim().split(/\s+/)[0];
|
|
87
|
+
}
|
|
88
|
+
if (!/^[0-9a-f]{64}$/i.test(String(checksum)) || hashFile(archive).toLowerCase() !== checksum.toLowerCase()) {
|
|
89
|
+
throw new Error('DraftGo archive checksum mismatch.');
|
|
90
|
+
}
|
|
91
|
+
const staged = path.join(tempDir, 'package');
|
|
92
|
+
fs.mkdirSync(staged);
|
|
93
|
+
const extract = spawnSync('tar', ['-xzf', archive, '-C', staged, '--strip-components=1'], { encoding: 'utf8', shell: false });
|
|
94
|
+
if (extract.error || extract.status !== 0) throw new Error(extract.error ? extract.error.message : (extract.stderr || 'Unable to extract DraftGo release.'));
|
|
95
|
+
const installedVersion = JSON.parse(fs.readFileSync(path.join(staged, 'package.json'), 'utf8')).version;
|
|
96
|
+
if (installedVersion !== version) throw new Error(`Release version mismatch: expected ${version}, received ${installedVersion}.`);
|
|
97
|
+
fs.writeFileSync(path.join(staged, MARKER), JSON.stringify(marker, null, 2) + '\n');
|
|
98
|
+
const destination = path.join(marker.installRoot, version);
|
|
99
|
+
if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
|
|
100
|
+
fs.renameSync(staged, destination);
|
|
101
|
+
return { launcher: writeLauncher(marker.binDir, destination), destination };
|
|
102
|
+
} finally { fs.rmSync(tempDir, { recursive: true, force: true }); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { MARKER, readInstallMarker, installRelease, hashFile, writeLauncher };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const SCHEMA_VERSION = 1;
|
|
7
|
+
const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
8
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
9
|
+
function draftgoRoot(projectDir) { return path.resolve(projectDir, '.draftgo'); }
|
|
10
|
+
function manifestPath(projectDir) { return path.join(draftgoRoot(projectDir), 'runtime-manifest.json'); }
|
|
11
|
+
function inside(root, target) { const relation = path.relative(path.resolve(root), path.resolve(target)); return relation && !relation.startsWith('..') && !path.isAbsolute(relation); }
|
|
12
|
+
function load(projectDir) {
|
|
13
|
+
const file = manifestPath(projectDir);
|
|
14
|
+
if (!fs.existsSync(file)) return { schema_version: SCHEMA_VERSION, entries: [] };
|
|
15
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
16
|
+
if (!value || value.schema_version !== SCHEMA_VERSION || !Array.isArray(value.entries)) throw new Error('Invalid .draftgo/runtime-manifest.json.');
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function replaceFile(temporary, file, rename = fs.renameSync, wait = (ms) => Atomics.wait(sleepBuffer, 0, 0, ms)) {
|
|
20
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
21
|
+
try { rename(temporary, file); return; } catch (error) {
|
|
22
|
+
if (!RETRYABLE_RENAME_CODES.has(error.code) || attempt >= 5) throw error;
|
|
23
|
+
wait(20 * (attempt + 1));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function save(projectDir, value) {
|
|
28
|
+
const file = manifestPath(projectDir); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
30
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
31
|
+
try { replaceFile(temporary, file); } catch (error) {
|
|
32
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* retain the original rename error */ }
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function register(projectDir, file, type, command, options = {}) {
|
|
37
|
+
const root = draftgoRoot(projectDir); const absolute = path.resolve(file);
|
|
38
|
+
if (!inside(root, absolute)) throw new Error('Managed runtime files must stay inside .draftgo.');
|
|
39
|
+
if (fs.existsSync(absolute) && !inside(fs.realpathSync(root), fs.realpathSync(absolute))) throw new Error('Managed runtime file escapes .draftgo.');
|
|
40
|
+
const relative = path.relative(root, absolute).replace(/\\/g, '/'); const value = load(projectDir);
|
|
41
|
+
const entry = { path: relative, type: String(type), command: String(command), created_at: new Date().toISOString(), cleanable: options.cleanable !== false };
|
|
42
|
+
value.entries = value.entries.filter((item) => item.path !== relative); value.entries.push(entry); save(projectDir, value); return entry;
|
|
43
|
+
}
|
|
44
|
+
function managedPath(projectDir, ...parts) { const file = path.join(draftgoRoot(projectDir), ...parts); if (!inside(draftgoRoot(projectDir), file)) throw new Error('Managed path escapes .draftgo.'); return file; }
|
|
45
|
+
module.exports = { SCHEMA_VERSION, draftgoRoot, manifestPath, load, save, register, managedPath, inside, replaceFile };
|
package/src/skill.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Per-platform skill renderer.
|
|
4
|
+
//
|
|
5
|
+
// Every AI-tool target receives the complete Skill and its domain references
|
|
6
|
+
// in that target's own project directory.
|
|
7
|
+
//
|
|
8
|
+
// Templating in .md files:
|
|
9
|
+
// {{SKILL_DIR}} → platform's project-relative skill dir (forward slashes)
|
|
10
|
+
// {{SKILL_SCRIPTS}} → {{SKILL_DIR}}/scripts
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const { RESOURCES_DIR } = require('./paths');
|
|
16
|
+
const paths = require('./paths');
|
|
17
|
+
const {
|
|
18
|
+
exists, ensureDir, readText, writeText, copyFile, removePath,
|
|
19
|
+
appendGitignoreLine,
|
|
20
|
+
} = require('./fsx');
|
|
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-]+)*)?$/;
|
|
25
|
+
|
|
26
|
+
function getPackageVersion() {
|
|
27
|
+
try {
|
|
28
|
+
return require(path.join(paths.PACKAGE_ROOT, 'package.json')).version;
|
|
29
|
+
} catch {
|
|
30
|
+
return '0.0.0';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readInstalledVersion(projectDir) {
|
|
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;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeInstalledVersion(projectDir) {
|
|
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
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderFrontmatter(fm) {
|
|
103
|
+
if (!fm) return '';
|
|
104
|
+
const lines = ['---'];
|
|
105
|
+
for (const [k, v] of Object.entries(fm)) {
|
|
106
|
+
const s = String(v);
|
|
107
|
+
if (/[:#"\n]/.test(s)) {
|
|
108
|
+
lines.push(`${k}: "${s.replace(/"/g, '\\"')}"`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(`${k}: ${s}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
lines.push('---', '');
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function splitFrontmatter(text) {
|
|
118
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
119
|
+
if (!normalized.startsWith('---\n')) return { frontmatter: {}, body: normalized };
|
|
120
|
+
const end = normalized.indexOf('\n---\n', 4);
|
|
121
|
+
if (end < 0) return { frontmatter: {}, body: normalized };
|
|
122
|
+
const frontmatter = {};
|
|
123
|
+
for (const line of normalized.slice(4, end).split('\n')) {
|
|
124
|
+
const colon = line.indexOf(':');
|
|
125
|
+
if (colon <= 0) continue;
|
|
126
|
+
const key = line.slice(0, colon).trim();
|
|
127
|
+
let value = line.slice(colon + 1).trim();
|
|
128
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
129
|
+
value = value.slice(1, -1).replace(/\\"/g, '"');
|
|
130
|
+
}
|
|
131
|
+
frontmatter[key] = value;
|
|
132
|
+
}
|
|
133
|
+
return { frontmatter, body: normalized.slice(end + 5) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function substitute(text, platform) {
|
|
137
|
+
const skillDir = platform.skillDir;
|
|
138
|
+
const scriptsDir = `${skillDir}/scripts`;
|
|
139
|
+
return text
|
|
140
|
+
.replace(/\{\{SKILL_DIR\}\}/g, skillDir)
|
|
141
|
+
.replace(/\{\{SKILL_SCRIPTS\}\}/g, scriptsDir);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function walk(root, onFile) {
|
|
145
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
146
|
+
if (entry.name === '__pycache__') continue;
|
|
147
|
+
const abs = path.join(root, entry.name);
|
|
148
|
+
if (entry.isDirectory()) walk(abs, onFile);
|
|
149
|
+
else if (entry.isFile()) onFile(abs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderInto(destDir, platform) {
|
|
154
|
+
ensureDir(destDir);
|
|
155
|
+
const srcRoot = SKILL_SOURCE_DIR;
|
|
156
|
+
walk(srcRoot, (absSrc) => {
|
|
157
|
+
const rel = path.relative(srcRoot, absSrc);
|
|
158
|
+
const absDst = path.join(destDir, rel);
|
|
159
|
+
ensureDir(path.dirname(absDst));
|
|
160
|
+
if (absSrc.endsWith('.md')) {
|
|
161
|
+
let body = readText(absSrc);
|
|
162
|
+
if (rel === 'SKILL.md') {
|
|
163
|
+
const parsed = splitFrontmatter(body);
|
|
164
|
+
body = renderFrontmatter({ ...(platform.frontmatter || {}), ...parsed.frontmatter }) + parsed.body;
|
|
165
|
+
}
|
|
166
|
+
body = substitute(body, platform);
|
|
167
|
+
writeText(absDst, body);
|
|
168
|
+
} else {
|
|
169
|
+
copyFile(absSrc, absDst);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateRenderedSkill(assetDir) {
|
|
176
|
+
const main = path.join(assetDir, 'SKILL.md');
|
|
177
|
+
if (!exists(main)) throw new Error('渲染结果缺少 SKILL.md');
|
|
178
|
+
const text = readText(main);
|
|
179
|
+
const firstEnd = text.indexOf('\n---\n', 4);
|
|
180
|
+
const body = firstEnd >= 0 ? text.slice(firstEnd + 5) : '';
|
|
181
|
+
if (!text.startsWith('---\n') || firstEnd < 0 || body.startsWith('---\n')) {
|
|
182
|
+
throw new Error('SKILL.md frontmatter 必须且只能有一个');
|
|
183
|
+
}
|
|
184
|
+
if (/\{\{(?:SKILL_DIR|SKILL_SCRIPTS)\}\}/.test(text)) {
|
|
185
|
+
throw new Error('SKILL.md 仍包含未替换占位符');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ensureRuntime(projectDir) {
|
|
190
|
+
appendGitignoreLine(projectDir, '.draftgo/config.json');
|
|
191
|
+
appendGitignoreLine(projectDir, '.draftgo/token');
|
|
192
|
+
appendGitignoreLine(projectDir, '.draftgo/worktree/');
|
|
193
|
+
appendGitignoreLine(projectDir, '.draftgo/conflicts/');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function installPlatform(projectDir, platform, opts = {}) {
|
|
197
|
+
if (!exists(SKILL_SOURCE_DIR)) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Skill source missing: ${SKILL_SOURCE_DIR}. The CLI package may be broken.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const destAsset = path.join(projectDir, platform.assetDir);
|
|
203
|
+
const destMain = path.join(projectDir, platform.mainFile);
|
|
204
|
+
const mainOutsideAsset = path.relative(destAsset, destMain).startsWith('..');
|
|
205
|
+
if ((exists(destAsset) || exists(destMain)) && !opts.force) {
|
|
206
|
+
return { path: platform.mainFile, skipped: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
210
|
+
const stageAsset = `${destAsset}.tmp-${token}`;
|
|
211
|
+
const backupAsset = `${destAsset}.bak-${token}`;
|
|
212
|
+
const stageMain = mainOutsideAsset ? `${destMain}.tmp-${token}` : null;
|
|
213
|
+
const backupMain = mainOutsideAsset ? `${destMain}.bak-${token}` : null;
|
|
214
|
+
let assetBackedUp = false;
|
|
215
|
+
let mainBackedUp = false;
|
|
216
|
+
let assetInstalled = false;
|
|
217
|
+
let mainInstalled = false;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
renderInto(stageAsset, platform);
|
|
221
|
+
validateRenderedSkill(stageAsset);
|
|
222
|
+
if (mainOutsideAsset) copyFile(path.join(stageAsset, 'SKILL.md'), stageMain);
|
|
223
|
+
|
|
224
|
+
ensureDir(path.dirname(destAsset));
|
|
225
|
+
if (exists(destAsset)) {
|
|
226
|
+
fs.renameSync(destAsset, backupAsset);
|
|
227
|
+
assetBackedUp = true;
|
|
228
|
+
}
|
|
229
|
+
if (mainOutsideAsset && exists(destMain)) {
|
|
230
|
+
ensureDir(path.dirname(destMain));
|
|
231
|
+
fs.renameSync(destMain, backupMain);
|
|
232
|
+
mainBackedUp = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fs.renameSync(stageAsset, destAsset);
|
|
236
|
+
assetInstalled = true;
|
|
237
|
+
if (mainOutsideAsset) {
|
|
238
|
+
fs.renameSync(stageMain, destMain);
|
|
239
|
+
mainInstalled = true;
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (mainInstalled && exists(destMain)) removePath(destMain);
|
|
243
|
+
if (assetInstalled && exists(destAsset)) removePath(destAsset);
|
|
244
|
+
if (mainBackedUp && exists(backupMain)) fs.renameSync(backupMain, destMain);
|
|
245
|
+
if (assetBackedUp && exists(backupAsset)) fs.renameSync(backupAsset, destAsset);
|
|
246
|
+
throw err;
|
|
247
|
+
} finally {
|
|
248
|
+
if (exists(stageAsset)) removePath(stageAsset);
|
|
249
|
+
if (stageMain && exists(stageMain)) removePath(stageMain);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (exists(backupAsset)) removePath(backupAsset);
|
|
253
|
+
if (backupMain && exists(backupMain)) removePath(backupMain);
|
|
254
|
+
if (!exists(destMain)) throw new Error(`安装后缺少入口文件:${platform.mainFile}`);
|
|
255
|
+
|
|
256
|
+
return { path: platform.mainFile };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function uninstallPlatform(projectDir, platform) {
|
|
260
|
+
const destAsset = path.join(projectDir, platform.assetDir);
|
|
261
|
+
const destMain = path.join(projectDir, platform.mainFile);
|
|
262
|
+
let removed = false;
|
|
263
|
+
if (exists(destAsset)) { removePath(destAsset); removed = true; }
|
|
264
|
+
if (path.relative(destAsset, destMain).startsWith('..') && exists(destMain)) {
|
|
265
|
+
removePath(destMain); removed = true;
|
|
266
|
+
}
|
|
267
|
+
return removed;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function statusPlatform(projectDir, platform) {
|
|
271
|
+
return { installed: exists(path.join(projectDir, platform.mainFile)) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function installAll(projectDir, platforms, opts = {}) {
|
|
275
|
+
ensureRuntime(projectDir);
|
|
276
|
+
const results = [];
|
|
277
|
+
for (const p of platforms) {
|
|
278
|
+
results.push({ platform: p, result: installPlatform(projectDir, p, opts) });
|
|
279
|
+
}
|
|
280
|
+
if (results.every((item) => !item.result.skipped)) writeInstalledVersion(projectDir);
|
|
281
|
+
return results;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
SKILL_SOURCE_DIR,
|
|
286
|
+
getPackageVersion,
|
|
287
|
+
readInstalledVersion,
|
|
288
|
+
writeInstalledVersion,
|
|
289
|
+
installPlatform,
|
|
290
|
+
uninstallPlatform,
|
|
291
|
+
statusPlatform,
|
|
292
|
+
installAll,
|
|
293
|
+
ensureRuntime,
|
|
294
|
+
splitFrontmatter,
|
|
295
|
+
};
|
package/src/targets.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Public Skill target registry backed by shared platform descriptors.
|
|
4
|
+
|
|
5
|
+
const { platforms } = require('./platforms');
|
|
6
|
+
const skill = require('./skill');
|
|
7
|
+
|
|
8
|
+
function makeInstaller(p) {
|
|
9
|
+
return {
|
|
10
|
+
name: p.name,
|
|
11
|
+
displayName: p.displayName,
|
|
12
|
+
mainFile: p.mainFile,
|
|
13
|
+
assetDir: p.assetDir,
|
|
14
|
+
install(projectDir, opts) {
|
|
15
|
+
return skill.installPlatform(projectDir, p, opts || {});
|
|
16
|
+
},
|
|
17
|
+
uninstall(projectDir) {
|
|
18
|
+
return skill.uninstallPlatform(projectDir, p);
|
|
19
|
+
},
|
|
20
|
+
status(projectDir) {
|
|
21
|
+
return skill.statusPlatform(projectDir, p);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const all = platforms.map(makeInstaller);
|
|
27
|
+
const byName = Object.fromEntries(all.map((i) => [i.name, i]));
|
|
28
|
+
|
|
29
|
+
function resolveTargets(input) {
|
|
30
|
+
if (!input) return { resolved: [], unknown: [] };
|
|
31
|
+
if (input === 'all') return { resolved: all.slice(), unknown: [] };
|
|
32
|
+
const list = Array.isArray(input) ? input : [input];
|
|
33
|
+
if (list.includes('all')) return { resolved: all.slice(), unknown: [] };
|
|
34
|
+
const resolved = [];
|
|
35
|
+
const unknown = [];
|
|
36
|
+
for (const name of list) {
|
|
37
|
+
if (byName[name]) resolved.push(byName[name]);
|
|
38
|
+
else unknown.push(name);
|
|
39
|
+
}
|
|
40
|
+
return { resolved, unknown };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { all, byName, resolveTargets };
|
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 };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Fetch the latest draftgo-cli version from the DraftGo release endpoint.
|
|
4
|
+
// Results are cached for 24 hours so update stays fast and deterministic.
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const REPO = process.env.DRAFTGO_REPO || 'draftgo/draftgo-cli';
|
|
13
|
+
const RELEASE_BASE_URL = process.env.DRAFTGO_INSTALL_BASE_URL || 'https://draftgo.cn/draftgo-cli';
|
|
14
|
+
const RELEASE_URLS = [
|
|
15
|
+
`${RELEASE_BASE_URL}/latest.json`,
|
|
16
|
+
`https://api.github.com/repos/${REPO}/releases/latest`,
|
|
17
|
+
];
|
|
18
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
const CACHE_FILE = path.join(os.homedir(), '.draftgo-cli', 'update-check.json');
|
|
20
|
+
|
|
21
|
+
function readCache() {
|
|
22
|
+
try {
|
|
23
|
+
const cached = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
24
|
+
if (!cached.version || !cached.checkedAt) return null;
|
|
25
|
+
if (Date.now() - Number(cached.checkedAt) > CACHE_TTL_MS) return null;
|
|
26
|
+
return String(cached.version);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeCache(version) {
|
|
33
|
+
try {
|
|
34
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
35
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ version, checkedAt: Date.now() }) + '\n', 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
// Cache failures must never block CLI work.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requestVersion(url, timeoutMs, redirects = 0) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
if (redirects > 5) return resolve(null);
|
|
44
|
+
let parsed;
|
|
45
|
+
try { parsed = new URL(url); } catch { return resolve(null); }
|
|
46
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
47
|
+
let req;
|
|
48
|
+
try {
|
|
49
|
+
req = transport.get(parsed, {
|
|
50
|
+
timeout: timeoutMs,
|
|
51
|
+
headers: { accept: 'application/json', 'user-agent': 'draftgo-cli' },
|
|
52
|
+
}, (res) => {
|
|
53
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
54
|
+
res.resume();
|
|
55
|
+
return resolve(requestVersion(new URL(res.headers.location, parsed).toString(), timeoutMs, redirects + 1));
|
|
56
|
+
}
|
|
57
|
+
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
58
|
+
let body = '';
|
|
59
|
+
res.setEncoding('utf8');
|
|
60
|
+
res.on('data', (chunk) => {
|
|
61
|
+
body += chunk;
|
|
62
|
+
if (body.length > 64 * 1024) req.destroy();
|
|
63
|
+
});
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
const payload = JSON.parse(body);
|
|
67
|
+
resolve(payload.version || String(payload.tag_name || '').replace(/^v/, '') || null);
|
|
68
|
+
} catch { resolve(null); }
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
} catch { return resolve(null); }
|
|
72
|
+
req.on('error', () => resolve(null));
|
|
73
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function fetchLatestVersion(timeoutMs = 3000, opts = {}) {
|
|
78
|
+
if (!opts.force) {
|
|
79
|
+
const cached = readCache();
|
|
80
|
+
if (cached) return Promise.resolve(cached);
|
|
81
|
+
}
|
|
82
|
+
for (const url of RELEASE_URLS) {
|
|
83
|
+
const version = await requestVersion(url, timeoutMs);
|
|
84
|
+
if (version) { writeCache(version); return version; }
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compare 'a.b.c' ignoring prerelease tags. Returns 1 / 0 / -1.
|
|
90
|
+
function cmpSemver(a, b) {
|
|
91
|
+
const pa = String(a).split('-')[0].split('.').map((x) => Number(x) || 0);
|
|
92
|
+
const pb = String(b).split('-')[0].split('.').map((x) => Number(x) || 0);
|
|
93
|
+
for (let i = 0; i < 3; i++) {
|
|
94
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
95
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { fetchLatestVersion, cmpSemver, readCache, requestVersion, CACHE_TTL_MS };
|