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,32 @@
|
|
|
1
|
+
# 自定义 Go 服务
|
|
2
|
+
|
|
3
|
+
底座现有操作无法表达所需业务逻辑时读取。通用发现与调用规则见 `mcp.md`;服务管理使用 `draftgo api search "custom service"` 发现能力。
|
|
4
|
+
|
|
5
|
+
## 当前 CLI 能力边界
|
|
6
|
+
|
|
7
|
+
CLI 能发现服务接口并执行 inline operation。底座将源码和 SDK 接口标为 checkout_commit workflow,但当前 CLI checkout 仅支持 pages/nav/docs,api call 对 workflow 返回 WORKFLOW_REQUIRED,不会执行传输。因此当前 CLI 尚不能独立完成源码/SDK 获取与保存闭环;不要反复调用或声称已实现服务。需要源码传输时明确报告此缺口,保留本地工作,其他 inline 配置和查询可继续。
|
|
8
|
+
|
|
9
|
+
## 底座开发链
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
1. 搜索服务相关能力,确认 SDK、草稿、验证、试运行、策略与发布入口。已有服务先唯一定位,新服务通过创建 operation 获取 ID。
|
|
13
|
+
2. 获取当前实例 SDK 和草稿,完整内容落到本地文件编辑,避免大响应进入对话。源码使用单文件 main.go,通过 Register 注册入口;签名、支持方法与依赖方式以当前 SDK 为准。
|
|
14
|
+
3. 保存源码仅提交 source 和读取的 base_revision;策略独立版本管理,不能随源码携带空 config 重置权限。保存后回读草稿 revision。
|
|
15
|
+
4. 验证准确草稿版本,再通过草稿执行检查业务结果和日志。发布使用已验证的 revision,回读发布版本;仅要求草稿时不扩大为上线。
|
|
16
|
+
5. 按任务范围验证实际入口与权限,记录执行 ID、版本和结果。
|
|
17
|
+
|
|
18
|
+
源码和 SDK 使用专用 API,不传给只支持 pages/nav/docs 的 checkout。MCP 对长响应返回 workflow 指引时,沿其允许流程获取完整文件;缺少可执行传输入口时报告具体 operation 和响应,不用摘要代替源码或声称完成。
|
|
19
|
+
|
|
20
|
+
## 策略与运行
|
|
21
|
+
|
|
22
|
+
服务默认要求登录,公开访问依据明确产品需求。先读取策略与版本,使用 base_version 保存;平台共享配置走独立接口。访问范围、角色顺序、个人限制和服务运行限制按实时 schema 配置,以后端预览核对匿名、目标用户和多角色结果。
|
|
23
|
+
|
|
24
|
+
入口允许调用不代表业务数据全部可访问。SDK 使用调用者身份;受信任服务有明确业务需要并验证条件后才显式使用 ctx.Admin.*。CLI 不注入系统身份。
|
|
25
|
+
|
|
26
|
+
每次执行独立子进程。goroutine、channel 与请求内并行在 handler 返回前收束;持久任务使用底座可靠执行机制。依赖通过平台声明、验证和锁定流程获取,不替换托管 SDK。
|
|
27
|
+
|
|
28
|
+
## 失败与完成证据
|
|
29
|
+
|
|
30
|
+
401/403 检查身份、操作权限和策略;409 检查版本或幂等冲突并保留源码;429 检查调用限制;503 检查服务和容量;504 检查超时。执行结果不明先查记录,不重复有副作用调用。
|
|
31
|
+
|
|
32
|
+
完成证据:草稿 revision、验证结果、试运行执行 ID;涉及发布时补充发布版本和真实调用结果。HTTP 路由存在而 MCP 搜不到时检查 OpenAPI/Registry 登记,不据此断言模块不存在。
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SCHEMA_VERSION = 1;
|
|
8
|
+
const WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
9
|
+
|
|
10
|
+
function cachePath(projectDir) {
|
|
11
|
+
return path.join(projectDir, '.draftgo', 'api-contract-cache.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function emptyCache(server) {
|
|
15
|
+
return { schema_version: SCHEMA_VERSION, server, operations: {} };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readCache(projectDir, server) {
|
|
19
|
+
const file = cachePath(projectDir);
|
|
20
|
+
if (!fs.existsSync(file)) return emptyCache(server);
|
|
21
|
+
try {
|
|
22
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
23
|
+
if (!value || value.schema_version !== SCHEMA_VERSION || value.server !== server
|
|
24
|
+
|| !value.operations || Array.isArray(value.operations)) return emptyCache(server);
|
|
25
|
+
return value;
|
|
26
|
+
} catch {
|
|
27
|
+
return emptyCache(server);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeDescription(value, fallbackRevision = '') {
|
|
32
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
33
|
+
const operation = value.operation && typeof value.operation === 'object' ? value.operation : value;
|
|
34
|
+
const registryRevision = String(value.registry_revision || operation.registry_revision || fallbackRevision).trim();
|
|
35
|
+
const contractHash = String(value.contract_hash || operation.contract_hash || '').trim();
|
|
36
|
+
const operationId = String(operation.operation_id || '').trim();
|
|
37
|
+
if (!operationId || !registryRevision) return null;
|
|
38
|
+
const normalized = { ...operation };
|
|
39
|
+
delete normalized.registry_revision;
|
|
40
|
+
delete normalized.contract_hash;
|
|
41
|
+
const resolvedHash = contractHash || crypto.createHash('sha256')
|
|
42
|
+
.update(JSON.stringify(canonicalValue(normalized))).digest('hex');
|
|
43
|
+
return { operation: normalized, registry_revision: registryRevision, contract_hash: resolvedHash };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canonicalValue(value) {
|
|
47
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
48
|
+
if (!value || typeof value !== 'object') return value;
|
|
49
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cachedDescription(projectDir, server, operationId) {
|
|
53
|
+
const cached = readCache(projectDir, server).operations[String(operationId)];
|
|
54
|
+
return normalizeDescription(cached);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function acquire(file) {
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
const lock = `${file}.lock`;
|
|
60
|
+
const started = Date.now();
|
|
61
|
+
while (true) {
|
|
62
|
+
try {
|
|
63
|
+
const descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
64
|
+
fs.closeSync(descriptor);
|
|
65
|
+
return lock;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 30_000) {
|
|
71
|
+
fs.rmSync(lock, { force: true });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (Date.now() - started >= 2000) return null;
|
|
79
|
+
Atomics.wait(WAIT, 0, 0, 10);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeAtomic(file, value) {
|
|
84
|
+
const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
87
|
+
fs.renameSync(temporary, file);
|
|
88
|
+
} finally {
|
|
89
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* Best-effort cleanup. */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function storeDescription(projectDir, server, value, fallbackRevision = '') {
|
|
94
|
+
const normalized = normalizeDescription(value, fallbackRevision);
|
|
95
|
+
if (!normalized) return null;
|
|
96
|
+
const file = cachePath(projectDir);
|
|
97
|
+
const lock = acquire(file);
|
|
98
|
+
if (!lock) return normalized;
|
|
99
|
+
try {
|
|
100
|
+
const cache = readCache(projectDir, server);
|
|
101
|
+
cache.operations[normalized.operation.operation_id] = {
|
|
102
|
+
...normalized,
|
|
103
|
+
described_at: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
cache.updated_at = new Date().toISOString();
|
|
106
|
+
writeAtomic(file, cache);
|
|
107
|
+
} finally {
|
|
108
|
+
try { fs.rmSync(lock, { force: true }); } catch { /* Cache writes are best effort. */ }
|
|
109
|
+
}
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
SCHEMA_VERSION,
|
|
115
|
+
cachePath,
|
|
116
|
+
readCache,
|
|
117
|
+
normalizeDescription,
|
|
118
|
+
cachedDescription,
|
|
119
|
+
storeDescription,
|
|
120
|
+
};
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tiny, dependency-free argv parser tailored for draftgo-cli.
|
|
4
|
+
// Supports: subcommand, positional args, --flag, --key value, --key=value.
|
|
5
|
+
|
|
6
|
+
const VALUE_FLAGS = new Set([
|
|
7
|
+
'project',
|
|
8
|
+
'target',
|
|
9
|
+
'server',
|
|
10
|
+
'mcp-url',
|
|
11
|
+
'api-key',
|
|
12
|
+
'token',
|
|
13
|
+
'output',
|
|
14
|
+
'url',
|
|
15
|
+
'browser',
|
|
16
|
+
'browser-path',
|
|
17
|
+
'ui',
|
|
18
|
+
'frame',
|
|
19
|
+
'viewport',
|
|
20
|
+
'screenshot',
|
|
21
|
+
'selector',
|
|
22
|
+
'width',
|
|
23
|
+
'height',
|
|
24
|
+
'wait-ms',
|
|
25
|
+
'delivery',
|
|
26
|
+
'assignee',
|
|
27
|
+
'assignee-id',
|
|
28
|
+
'timeout',
|
|
29
|
+
'operation-id',
|
|
30
|
+
'params',
|
|
31
|
+
'input',
|
|
32
|
+
'type',
|
|
33
|
+
'route',
|
|
34
|
+
'title',
|
|
35
|
+
'limit',
|
|
36
|
+
'offset',
|
|
37
|
+
'cursor',
|
|
38
|
+
'date',
|
|
39
|
+
'page',
|
|
40
|
+
'instance',
|
|
41
|
+
'note',
|
|
42
|
+
'file',
|
|
43
|
+
'library',
|
|
44
|
+
'name',
|
|
45
|
+
'slug',
|
|
46
|
+
'category',
|
|
47
|
+
'description',
|
|
48
|
+
'tags',
|
|
49
|
+
'source-type',
|
|
50
|
+
'status',
|
|
51
|
+
'module',
|
|
52
|
+
'resource-type',
|
|
53
|
+
'method',
|
|
54
|
+
'risk',
|
|
55
|
+
'permission',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function parse(argv) {
|
|
59
|
+
const args = argv.slice();
|
|
60
|
+
const out = { command: null, positional: [], flags: {}, errors: [] };
|
|
61
|
+
while (args.length) {
|
|
62
|
+
const a = args.shift();
|
|
63
|
+
if (a === '--') {
|
|
64
|
+
out.positional.push(...args);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (a.startsWith('--')) {
|
|
68
|
+
const raw = a.slice(2);
|
|
69
|
+
const eq = raw.indexOf('=');
|
|
70
|
+
if (eq >= 0) {
|
|
71
|
+
const key = raw.slice(0, eq);
|
|
72
|
+
const value = raw.slice(eq + 1);
|
|
73
|
+
if (VALUE_FLAGS.has(key) && !value) out.errors.push(`Option --${key} requires a value.`);
|
|
74
|
+
else out.flags[key] = value;
|
|
75
|
+
} else {
|
|
76
|
+
const next = args[0];
|
|
77
|
+
if (VALUE_FLAGS.has(raw)) {
|
|
78
|
+
// A declared value flag must never silently become boolean true.
|
|
79
|
+
if (next === undefined || next === '--' || next.startsWith('--')) {
|
|
80
|
+
out.errors.push(`Option --${raw} requires a value.`);
|
|
81
|
+
} else {
|
|
82
|
+
out.flags[raw] = next;
|
|
83
|
+
args.shift();
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
out.flags[raw] = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else if (a.startsWith('-') && a.length > 1) {
|
|
90
|
+
out.flags[a.slice(1)] = true;
|
|
91
|
+
} else if (!out.command) {
|
|
92
|
+
out.command = a;
|
|
93
|
+
} else {
|
|
94
|
+
out.positional.push(a);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { parse, VALUE_FLAGS };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Keep command names, aliases, and dispatch behavior in one place. This avoids
|
|
4
|
+
// aliases silently drifting away from the implementation as the CLI grows.
|
|
5
|
+
const commands = [
|
|
6
|
+
{ name: 'init', run: (dir, args, flags) => require('./commands/init')(dir, args, flags) },
|
|
7
|
+
{ name: 'update', run: (dir, args, flags) => require('./commands/update')(dir, args, flags) },
|
|
8
|
+
{ name: 'uninstall', run: (dir, args, flags) => require('./commands/uninstall')(dir, args, flags) },
|
|
9
|
+
{ name: 'status', run: (dir, args, flags) => require('./commands/status')(dir, args, flags) },
|
|
10
|
+
{ name: 'work', run: (dir, args, flags) => require('./commands/worklog')(dir, args, flags) },
|
|
11
|
+
{ name: 'clean', run: (dir, args, flags) => require('./commands/clean')(dir, args, flags) },
|
|
12
|
+
{ name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
|
|
13
|
+
{ name: 'check', run: (dir, args, flags) => require('./commands/check')(dir, args, flags) },
|
|
14
|
+
{ name: 'verify', run: (dir, args, flags) => require('./commands/verify')(dir, args, flags) },
|
|
15
|
+
{ name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
|
|
16
|
+
{ name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
|
|
17
|
+
{ name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
|
|
18
|
+
{ name: 'commit', run: (dir, args, flags) => require('./commands/commit')(dir, args, flags) },
|
|
19
|
+
{ name: 'reconcile', run: (dir, args, flags) => require('./commands/reconcile')(dir, args, flags) },
|
|
20
|
+
{ name: 'diff', run: (dir, args, flags) => require('./commands/diff')(dir, args, flags) },
|
|
21
|
+
{ name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
|
|
22
|
+
{ name: 'conflict', run: (dir, args, flags) => require('./commands/conflict')(dir, args, flags) },
|
|
23
|
+
{ name: 'api', run: (dir, args, flags) => require('./commands/api')(dir, args, flags) },
|
|
24
|
+
{ name: 'group', run: (dir, args, flags) => require('./commands/group')(dir, args, flags) },
|
|
25
|
+
{ name: 'api-key', run: (dir, args, flags) => require('./commands/apiKey')(dir, args, flags) },
|
|
26
|
+
{ name: 'role', run: (dir, args, flags) => require('./commands/role')(dir, args, flags) },
|
|
27
|
+
{ name: 'delete', run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
|
|
28
|
+
{ name: 'deploy', run: (dir, args, flags) => require('./commands/deploy')(dir, args, flags) },
|
|
29
|
+
{ name: 'auto-push', run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
|
|
30
|
+
{ name: 'local', run: (dir, args, flags) => require('./commands/local')(dir, args, flags) },
|
|
31
|
+
{ name: 'list-targets', run: () => require('./commands/listTargets')() },
|
|
32
|
+
{ name: 'connect', run: (dir, args, flags) => require('./commands/connect')(dir, args, flags) },
|
|
33
|
+
{ name: 'components', run: (dir, args, flags) => require('./commands/components')(dir, args, flags) },
|
|
34
|
+
{ name: 'capabilities', run: (dir, args, flags) => require('./commands/capabilities')(dir, args, flags) },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const aliases = new Map();
|
|
38
|
+
for (const command of commands) {
|
|
39
|
+
for (const alias of [command.name, ...(command.aliases || [])]) aliases.set(alias, command);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveCommand(name) {
|
|
43
|
+
return aliases.get(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { commands, resolveCommand };
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const log = require('../logger');
|
|
6
|
+
const { confirm } = require('../prompt');
|
|
7
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
8
|
+
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
9
|
+
const { cachedDescription, normalizeDescription, storeDescription } = require('../apiContractCache');
|
|
10
|
+
const {
|
|
11
|
+
isContractCompatibilityError,
|
|
12
|
+
isHardContractIncompatibility,
|
|
13
|
+
isDangerousOperation,
|
|
14
|
+
sameOperationContract,
|
|
15
|
+
canRetryAfterContractChange,
|
|
16
|
+
incompatibleOperationError,
|
|
17
|
+
} = require('../contractCompatibility');
|
|
18
|
+
|
|
19
|
+
async function registryRevision(session, operationId) {
|
|
20
|
+
const result = await callStructured(session, TOOL_NAMES.apiSearch, {
|
|
21
|
+
query: operationId,
|
|
22
|
+
limit: 100,
|
|
23
|
+
});
|
|
24
|
+
const items = result && Array.isArray(result.items) ? result.items : [];
|
|
25
|
+
const operation = items.find((item) => item && item.operation_id === operationId);
|
|
26
|
+
const revision = String(result && result.registry_revision || '').trim();
|
|
27
|
+
if (!operation || !revision) {
|
|
28
|
+
throw new Error(`DraftGo Registry could not resolve operation ${operationId} with a revision.`);
|
|
29
|
+
}
|
|
30
|
+
return revision;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function describeOperation(projectDir, config, session, operationId, knownRevision = '') {
|
|
34
|
+
let revision = String(knownRevision || '').trim();
|
|
35
|
+
if (!revision) revision = await registryRevision(session, operationId);
|
|
36
|
+
const described = await callStructured(session, TOOL_NAMES.apiDescribe, {
|
|
37
|
+
operation_id: operationId,
|
|
38
|
+
registry_revision: revision,
|
|
39
|
+
});
|
|
40
|
+
return storeDescription(projectDir, config.server, described, revision)
|
|
41
|
+
|| normalizeDescription(described, revision);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function descriptionForRevision(projectDir, config, session, operationId, knownRevision = '') {
|
|
45
|
+
const revision = String(knownRevision || '').trim() || await registryRevision(session, operationId);
|
|
46
|
+
const cached = cachedDescription(projectDir, config.server, operationId);
|
|
47
|
+
if (cached && cached.registry_revision === revision) return cached;
|
|
48
|
+
return describeOperation(projectDir, config, session, operationId, revision);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readCallInput(projectDir, flags) {
|
|
52
|
+
if (flags.input && flags.params) throw new Error('Use either --input or --params, not both.');
|
|
53
|
+
const raw = flags.input
|
|
54
|
+
? fs.readFileSync(path.resolve(projectDir, String(flags.input)))
|
|
55
|
+
: flags.params ? Buffer.from(String(flags.params), 'utf8') : Buffer.from('{}', 'utf8');
|
|
56
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
|
|
57
|
+
const value = JSON.parse(text);
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
59
|
+
throw new Error('API call input must be a UTF-8 JSON object.');
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function diagnosticResult(operationId, result) {
|
|
65
|
+
const source = result && typeof result === 'object' ? result : { data: result };
|
|
66
|
+
const statusCode = source.status_code ?? source.statusCode ?? source.status ?? null;
|
|
67
|
+
const headerRequestId = source.headers
|
|
68
|
+
&& (source.headers['x-request-id'] || source.headers['X-Request-Id']);
|
|
69
|
+
const rawRequestId = source.request_id ?? source.requestId ?? headerRequestId ?? null;
|
|
70
|
+
const requestId = Array.isArray(rawRequestId) ? rawRequestId[0] || null : rawRequestId;
|
|
71
|
+
const data = source.data && typeof source.data === 'object' ? source.data : {};
|
|
72
|
+
const error = source.error || data.error || {};
|
|
73
|
+
return {
|
|
74
|
+
operation_id: operationId,
|
|
75
|
+
status_code: statusCode,
|
|
76
|
+
server_code: error.code || data.code || source.code || null,
|
|
77
|
+
request_id: requestId,
|
|
78
|
+
result,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertInvocationResult(operationId, result) {
|
|
83
|
+
const diagnostic = diagnosticResult(operationId, result);
|
|
84
|
+
if (result && result.workflow && result.workflow.required) {
|
|
85
|
+
const error = new Error(`Operation ${operationId} requires the ${result.workflow.workflow || result.workflow.mode} workflow; it has not executed.`);
|
|
86
|
+
error.code = 'WORKFLOW_REQUIRED';
|
|
87
|
+
error.details = { ...diagnostic, workflow: result.workflow };
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (Number(diagnostic.status_code) >= 400) {
|
|
91
|
+
const data = result.data || {};
|
|
92
|
+
const error = new Error(data.error && data.error.message || data.message || `DraftGo returned HTTP ${diagnostic.status_code}.`);
|
|
93
|
+
error.code = diagnostic.server_code || 'HTTP_ERROR';
|
|
94
|
+
error.status = diagnostic.status_code;
|
|
95
|
+
error.details = { ...data, request_id: diagnostic.request_id };
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function diagnosticError(operationId, error) {
|
|
102
|
+
const source = error && (error.details || error.data) || {};
|
|
103
|
+
const nested = source.error && typeof source.error === 'object' ? source.error : {};
|
|
104
|
+
return {
|
|
105
|
+
operation_id: operationId,
|
|
106
|
+
status_code: (error && error.status) ?? source.status_code ?? source.status ?? null,
|
|
107
|
+
server_code: nested.code || source.code || error && error.code || null,
|
|
108
|
+
request_id: source.request_id || source.requestId || nested.request_id || null,
|
|
109
|
+
error: { message: error && error.message || String(error) },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printCallError(operationId, error, flags) {
|
|
114
|
+
const diagnostic = diagnosticError(operationId, error);
|
|
115
|
+
if (flags.output === 'json') console.log(JSON.stringify(diagnostic, null, 2));
|
|
116
|
+
else {
|
|
117
|
+
log.err(`DraftGo operation failed: ${operationId}`);
|
|
118
|
+
log.info(`status=${diagnostic.status_code ?? '-'} code=${diagnostic.server_code ?? '-'} request_id=${diagnostic.request_id ?? '-'}`);
|
|
119
|
+
log.err(diagnostic.error.message);
|
|
120
|
+
}
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function callOperation(projectDir, operationId, flags = {}) {
|
|
125
|
+
if (!operationId) {
|
|
126
|
+
log.err('Usage: draftgo api call <operation_id> --input <json-file>');
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
let session;
|
|
130
|
+
let description;
|
|
131
|
+
let input;
|
|
132
|
+
let config;
|
|
133
|
+
try {
|
|
134
|
+
config = loadProjectConfig(projectDir);
|
|
135
|
+
session = await openToolSession(config, [
|
|
136
|
+
TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
137
|
+
]);
|
|
138
|
+
description = cachedDescription(projectDir, config.server, operationId);
|
|
139
|
+
if (!description) {
|
|
140
|
+
description = await describeOperation(projectDir, config, session, operationId);
|
|
141
|
+
}
|
|
142
|
+
if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
|
|
143
|
+
input = readCallInput(projectDir, flags);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return printCallError(operationId, error, flags);
|
|
146
|
+
}
|
|
147
|
+
const dangerous = isDangerousOperation(description);
|
|
148
|
+
if (dangerous && !flags.yes && !flags.y) {
|
|
149
|
+
if (flags.output === 'json' || !process.stdin.isTTY) {
|
|
150
|
+
console.log(JSON.stringify({
|
|
151
|
+
operation_id: operationId,
|
|
152
|
+
error: { code: 'CONFIRMATION_REQUIRED', message: 'Dangerous operations require --yes in JSON output mode.' },
|
|
153
|
+
}, null, 2));
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
const approved = await confirm(`Call destructive DraftGo operation ${operationId}?`, { default: false });
|
|
157
|
+
if (!approved) return 1;
|
|
158
|
+
}
|
|
159
|
+
let result;
|
|
160
|
+
try {
|
|
161
|
+
const invoke = (contract, confirmed) => callStructured(session, TOOL_NAMES.apiCall, {
|
|
162
|
+
...input, operation_id: operationId, registry_revision: contract.registry_revision,
|
|
163
|
+
...(confirmed ? { confirm: true } : {}),
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
result = await invoke(description, dangerous);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (!isContractCompatibilityError(error)) throw error;
|
|
169
|
+
if (isHardContractIncompatibility(error)) {
|
|
170
|
+
throw incompatibleOperationError(operationId, error);
|
|
171
|
+
}
|
|
172
|
+
const previousDescription = description;
|
|
173
|
+
const revision = await registryRevision(session, operationId);
|
|
174
|
+
description = await describeOperation(projectDir, config, session, operationId, revision);
|
|
175
|
+
if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
|
|
176
|
+
const changedDangerous = isDangerousOperation(description);
|
|
177
|
+
if ((dangerous || changedDangerous) && !sameOperationContract(previousDescription, description)) {
|
|
178
|
+
throw incompatibleOperationError(operationId, error);
|
|
179
|
+
}
|
|
180
|
+
if (!canRetryAfterContractChange(previousDescription, description)) {
|
|
181
|
+
throw incompatibleOperationError(operationId, error);
|
|
182
|
+
}
|
|
183
|
+
result = await invoke(description, changedDangerous);
|
|
184
|
+
}
|
|
185
|
+
assertInvocationResult(operationId, result);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return printCallError(operationId, error, flags);
|
|
188
|
+
}
|
|
189
|
+
const diagnostic = diagnosticResult(operationId, result);
|
|
190
|
+
if (flags.output === 'json') console.log(JSON.stringify(diagnostic, null, 2));
|
|
191
|
+
else {
|
|
192
|
+
log.ok(`DraftGo operation completed: ${operationId}`);
|
|
193
|
+
log.info(`status=${diagnostic.status_code ?? '-'} code=${diagnostic.server_code ?? '-'} request_id=${diagnostic.request_id ?? '-'}`);
|
|
194
|
+
console.log(JSON.stringify(result, null, 2));
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function printResult(title, result) {
|
|
200
|
+
log.title(title);
|
|
201
|
+
if (Array.isArray(result)) {
|
|
202
|
+
for (const item of result) log.plain(` ${JSON.stringify(item)}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result && Array.isArray(result.items)) {
|
|
206
|
+
for (const item of result.items) log.plain(` ${JSON.stringify(item)}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(JSON.stringify(result, null, 2));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function apiCommand(projectDir, positional, flags = {}) {
|
|
213
|
+
const first = String(positional[0] || '').toLowerCase();
|
|
214
|
+
if (first === 'call') return callOperation(projectDir, String(positional[1] || ''), flags);
|
|
215
|
+
const describe = first === 'describe';
|
|
216
|
+
const queryParts = first === 'search' || describe ? positional.slice(1) : positional;
|
|
217
|
+
const query = queryParts.join(' ').trim();
|
|
218
|
+
if (!query) {
|
|
219
|
+
log.err('Usage: draftgo api <query> | draftgo api search <query> | draftgo api describe <operation_id>');
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const config = loadProjectConfig(projectDir);
|
|
224
|
+
const expected = describe ? [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe] : [TOOL_NAMES.apiSearch];
|
|
225
|
+
const session = await openToolSession(config, expected);
|
|
226
|
+
const result = describe
|
|
227
|
+
? await describeOperation(projectDir, config, session, query)
|
|
228
|
+
: await callStructured(session, TOOL_NAMES.apiSearch,
|
|
229
|
+
require('./capabilities').searchArguments(query, flags, flags.cursor));
|
|
230
|
+
if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
|
|
231
|
+
else printResult(describe ? `DraftGo API ${query}` : `DraftGo API search: ${query}`, result);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = apiCommand;
|
|
236
|
+
module.exports.readCallInput = readCallInput;
|
|
237
|
+
module.exports.diagnosticResult = diagnosticResult;
|
|
238
|
+
module.exports.diagnosticError = diagnosticError;
|
|
239
|
+
module.exports.printCallError = printCallError;
|
|
240
|
+
module.exports.callOperation = callOperation;
|
|
241
|
+
module.exports.registryRevision = registryRevision;
|
|
242
|
+
module.exports.describeOperation = describeOperation;
|
|
243
|
+
module.exports.descriptionForRevision = descriptionForRevision;
|
|
244
|
+
module.exports.assertInvocationResult = assertInvocationResult;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { callOperation } = require('./api');
|
|
5
|
+
|
|
6
|
+
const OPERATIONS = Object.freeze({
|
|
7
|
+
status: 'getCurrentUserAPIKey',
|
|
8
|
+
create: 'createCurrentUserAPIKey',
|
|
9
|
+
update: 'setCurrentUserAPIKeyActive',
|
|
10
|
+
delete: 'deleteCurrentUserAPIKey',
|
|
11
|
+
rotate: 'rotateCurrentUserAPIKey',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function operationKey(value) {
|
|
15
|
+
const action = String(value || 'status').trim().toLowerCase();
|
|
16
|
+
return OPERATIONS[action] ? action : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function apiKeyCommand(projectDir, positional, flags = {}) {
|
|
20
|
+
const action = operationKey(positional[0]);
|
|
21
|
+
if (!action) {
|
|
22
|
+
log.err('Usage: draftgo api-key status|create|update|delete|rotate [--input <json-file>]');
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
return callOperation(projectDir, OPERATIONS[action], flags);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = apiKeyCommand;
|
|
29
|
+
module.exports.OPERATIONS = OPERATIONS;
|
|
30
|
+
module.exports.operationKey = operationKey;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
5
|
+
const { loadManifest, absolutePath } = require('../worktree/manifest');
|
|
6
|
+
const { hashFile } = require('../worktree/streams');
|
|
7
|
+
const { targetsFrom, deliverTargets, outputResult } = require('./deploy');
|
|
8
|
+
|
|
9
|
+
async function changedEntries(projectDir, manifest) {
|
|
10
|
+
const changed = [];
|
|
11
|
+
for (const entry of Object.values(manifest.entries)) {
|
|
12
|
+
const local = absolutePath(projectDir, entry.local_path);
|
|
13
|
+
// Missing files must be validated, not silently dropped from delivery.
|
|
14
|
+
if (!fs.existsSync(local) || (await hashFile(local)).hash !== entry.base_hash) changed.push(entry);
|
|
15
|
+
}
|
|
16
|
+
return changed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function autoPush(projectDir, positional, flags = {}) {
|
|
20
|
+
try {
|
|
21
|
+
let config;
|
|
22
|
+
try { config = loadProjectConfig(projectDir); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (fs.existsSync(require('path').join(projectDir, '.draftgo', 'config.json'))) throw error;
|
|
25
|
+
return outputResult({ skipped: 'auto-push skipped: this project is not connected to DraftGo.' }, flags);
|
|
26
|
+
}
|
|
27
|
+
if (config.auto_push !== true) return outputResult({ skipped: 'auto-push skipped: auto_push is not enabled.' }, flags);
|
|
28
|
+
const targets = positional.length ? targetsFrom(positional)
|
|
29
|
+
: (await changedEntries(projectDir, loadManifest(projectDir))).map(({ resource_type, resource_id }) => ({ resource_type, resource_id }));
|
|
30
|
+
if (!targets.length) return outputResult({ skipped: 'auto-push: no checked-out content changed.' }, flags);
|
|
31
|
+
const value = await deliverTargets(projectDir, targets, { ...flags, delivery: 'deploy' }, { config });
|
|
32
|
+
return outputResult({ ...value, delivery: 'auto-push' }, flags);
|
|
33
|
+
} catch (error) { return outputResult({ error: { code: error.code || 'AUTO_PUSH_FAILED', message: error.message } }, flags); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = autoPush;
|