draftgo-cli 3.0.56 → 4.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -297
- package/package.json +12 -7
- package/resources/custom-service-sdk/ai.go +520 -0
- package/resources/custom-service-sdk/ai_test.go +156 -0
- package/resources/custom-service-sdk/billing.go +596 -0
- package/resources/custom-service-sdk/billing_test.go +150 -0
- package/resources/custom-service-sdk/go.mod +3 -0
- package/resources/custom-service-sdk/manifest.json +72 -0
- package/resources/custom-service-sdk/platform.go +360 -0
- package/resources/custom-service-sdk/platform_logger_test.go +24 -0
- package/resources/custom-service-sdk/registration_test.go +39 -0
- package/resources/custom-service-sdk/resources.go +246 -0
- package/resources/custom-service-sdk/resources_billing_test.go +115 -0
- package/resources/custom-service-sdk/resources_files_test.go +57 -0
- package/resources/custom-service-sdk/resources_scope_test.go +87 -0
- package/resources/custom-service-sdk/sdk.go +208 -0
- package/resources/skill/SKILL.md +36 -87
- package/resources/skill/init/SKILL.md +9 -14
- package/resources/skill/manifest.json +6 -2
- package/resources/skill/references/aihub.md +28 -5
- package/resources/skill/references/app-api.md +56 -6
- package/resources/skill/references/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +4 -2
- package/resources/skill/references/checkout.md +21 -7
- package/resources/skill/references/custom-services.md +124 -222
- package/resources/skill/references/data.md +22 -6
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/diagnostics.md +51 -0
- package/resources/skill/references/frontend.md +93 -499
- package/resources/skill/references/mcp.md +65 -101
- package/resources/skill/references/methods.md +189 -0
- package/resources/skill/references/modules.md +36 -8
- package/resources/skill/references/runtime.md +26 -3
- package/resources/skill/story/SKILL.md +1 -2
- package/src/apiContractCache.js +112 -0
- package/src/cli.js +24 -20
- package/src/commandRegistry.js +15 -12
- package/src/commands/api.js +41 -10
- package/src/commands/apiKey.js +34 -0
- package/src/commands/capabilities.js +93 -0
- package/src/commands/check.js +1 -10
- package/src/commands/checkout.js +1 -1
- package/src/commands/commit.js +1 -1
- package/src/commands/components.js +550 -0
- package/src/commands/conflict.js +1 -1
- package/src/commands/connect.js +18 -8
- package/src/commands/customService.js +22 -8
- package/src/commands/dataRange.js +33 -0
- package/src/commands/delete.js +34 -46
- package/src/commands/deploy.js +1 -1
- package/src/commands/diff.js +18 -2
- package/src/commands/grant.js +29 -0
- package/src/commands/group.js +38 -0
- package/src/commands/help.js +80 -51
- package/src/commands/init.js +6 -12
- package/src/commands/listTargets.js +1 -1
- package/src/commands/local.js +2 -6
- package/src/commands/map.js +145 -28
- package/src/commands/mcp.js +2 -2
- package/src/commands/reconcile.js +1 -1
- package/src/commands/role.js +32 -0
- package/src/commands/space.js +41 -0
- package/src/commands/status.js +111 -8
- package/src/commands/uninstall.js +3 -3
- package/src/commands/update.js +24 -12
- package/src/commands/verify.js +118 -21
- package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
- package/src/commands/worklog.js +90 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +57 -0
- package/src/customServices.js +278 -41
- package/src/diffReport.js +106 -0
- package/src/index.js +2 -0
- package/src/{localdev → localRuntime}/compose.js +14 -17
- package/src/{localdev → localRuntime}/detect.js +1 -1
- package/src/{localdev → localRuntime}/index.js +22 -23
- package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
- package/src/{localdev → localRuntime}/services.js +28 -37
- package/src/mcp/client.js +11 -2
- package/src/mcp/protocol.js +2 -2
- package/src/mcp/tools.js +14 -1
- package/src/platforms.js +9 -0
- package/src/projectConfig.js +8 -4
- package/src/releaseInstall.js +105 -0
- package/src/{installers/index.js → targets.js} +3 -5
- package/src/updateCheck.js +48 -28
- package/src/worklog.js +275 -0
- package/src/workspaceHealth.js +1 -1
- package/src/worktree/backend.js +1 -1
- package/src/worktree/index.js +86 -51
- package/src/changelog.js +0 -276
- package/src/commands/changelog.js +0 -24
- package/src/commands/localDev.js +0 -9
- package/src/commands/sync.js +0 -46
- package/src/commands/task.js +0 -408
- package/src/commands/verifyUiCompat.js +0 -16
package/src/commands/local.js
CHANGED
|
@@ -4,8 +4,8 @@ const path = require('path');
|
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
5
|
const log = require('../logger');
|
|
6
6
|
const { exists } = require('../fsx');
|
|
7
|
-
const { detectDocker } = require('../
|
|
8
|
-
const { runWizard } = require('../
|
|
7
|
+
const { detectDocker } = require('../localRuntime/detect');
|
|
8
|
+
const { runWizard } = require('../localRuntime');
|
|
9
9
|
|
|
10
10
|
function composeFile(projectDir) {
|
|
11
11
|
return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
|
|
@@ -58,21 +58,17 @@ function local(projectDir, positional, flags = {}) {
|
|
|
58
58
|
switch (action) {
|
|
59
59
|
case 'setup':
|
|
60
60
|
return runWizard(projectDir, { yes: !!(flags.yes || flags.y) });
|
|
61
|
-
case 'up':
|
|
62
61
|
case 'start':
|
|
63
62
|
return runCompose(projectDir, ['up', '-d']);
|
|
64
|
-
case 'down':
|
|
65
63
|
case 'stop':
|
|
66
64
|
return runCompose(projectDir, ['down']);
|
|
67
65
|
case 'logs':
|
|
68
66
|
return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
|
|
69
67
|
case 'status':
|
|
70
|
-
case 'ps':
|
|
71
68
|
return runCompose(projectDir, ['ps']);
|
|
72
69
|
default:
|
|
73
70
|
log.err(`未知 local 子命令:${action}`);
|
|
74
71
|
log.dim(' 可用:draftgo local setup | start | stop | logs | status');
|
|
75
|
-
log.dim(' 兼容别名:up | down');
|
|
76
72
|
return 1;
|
|
77
73
|
}
|
|
78
74
|
}
|
package/src/commands/map.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
3
|
const log = require('../logger');
|
|
6
4
|
const { loadProjectConfig } = require('../projectConfig');
|
|
7
5
|
const { canonicalResourceType } = require('../worktree/types');
|
|
8
6
|
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
9
7
|
const { allWithAbort } = require('../mcp/parallel');
|
|
10
8
|
const { inspectRemoteCheckouts } = require('../worktree/status');
|
|
9
|
+
const { loadManifest } = require('../worktree/manifest');
|
|
11
10
|
const customServices = require('../customServices');
|
|
12
11
|
|
|
13
12
|
const REMOTE_RESOURCE_TYPES = Object.freeze({
|
|
@@ -42,6 +41,11 @@ function claimsMorePages(value) {
|
|
|
42
41
|
&& (value.has_more === true || value.hasMore === true);
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
function reportedTotal(value) {
|
|
45
|
+
const total = value && typeof value === 'object' ? value.total : null;
|
|
46
|
+
return Number.isInteger(total) && total >= 0 ? total : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
function normalizeMapResourceType(value) {
|
|
46
50
|
if (require('./customService').isServiceType(value)) return 'custom_services';
|
|
47
51
|
return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
|
|
@@ -53,6 +57,79 @@ function requestedResourceTypes(flags = {}) {
|
|
|
53
57
|
: [normalizeMapResourceType(flags.type)];
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
function normalizeRoute(value) {
|
|
61
|
+
// DraftGo stores page routes without surrounding slashes. Accept either the
|
|
62
|
+
// stored form or the browser form users naturally paste into the CLI.
|
|
63
|
+
return String(value == null ? '' : value).trim().replace(/^\/+|\/+$/g, '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function positiveLimit(value, fallback = 20) {
|
|
67
|
+
if (value == null) return fallback;
|
|
68
|
+
if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > 100) {
|
|
69
|
+
throw new Error('Map --limit must be an integer between 1 and 100.');
|
|
70
|
+
}
|
|
71
|
+
return Number(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resourceType(resource) {
|
|
75
|
+
return resource.resource_type || resource.type || 'unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resourceMatches(resource, flags = {}) {
|
|
79
|
+
const route = flags.route == null ? null : normalizeRoute(flags.route);
|
|
80
|
+
const title = flags.title == null ? null : String(flags.title);
|
|
81
|
+
return (route == null || normalizeRoute(resource.route) === route)
|
|
82
|
+
&& (title == null || String(resource.title == null ? '' : resource.title) === title);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resourceKey(resource) {
|
|
86
|
+
const type = resourceType(resource);
|
|
87
|
+
if (type === 'custom_services') return `custom_services:${resource.resource_id}`;
|
|
88
|
+
return `${canonicalResourceType(type)}:${resource.resource_id}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function paginationFrom(payload, resourceTypeName) {
|
|
92
|
+
const cursor = nextCursor(payload);
|
|
93
|
+
if ((cursor == null || cursor === '') && claimsMorePages(payload)) {
|
|
94
|
+
throw new Error(`DraftGo resource_list (${resourceTypeName}) reported more pages without a cursor.`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
next_cursor: cursor == null || cursor === '' ? null : String(cursor),
|
|
98
|
+
has_more: claimsMorePages(payload) || (cursor != null && cursor !== ''),
|
|
99
|
+
total: reportedTotal(payload),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function listRemoteResourcePage(session, resourceTypeName, flags = {}, options = {}) {
|
|
104
|
+
const filtered = flags.route != null || flags.title != null;
|
|
105
|
+
const args = { resource_type: resourceTypeName, limit: positiveLimit(flags.limit) };
|
|
106
|
+
if (flags.cursor != null && flags.cursor !== '') args.cursor = String(flags.cursor);
|
|
107
|
+
if (filtered) {
|
|
108
|
+
// resource_search narrows the remote candidate set; the strict equality
|
|
109
|
+
// checks below keep --route/--title deterministic even when search is fuzzy.
|
|
110
|
+
args.query = flags.route != null ? normalizeRoute(flags.route) : String(flags.title);
|
|
111
|
+
}
|
|
112
|
+
const payload = await callStructured(
|
|
113
|
+
session,
|
|
114
|
+
filtered ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList,
|
|
115
|
+
args,
|
|
116
|
+
options,
|
|
117
|
+
);
|
|
118
|
+
return { resources: itemsFrom(payload).filter((resource) => resourceMatches(resource, flags)), pagination: paginationFrom(payload, resourceTypeName) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function listMapResources(session, resourceTypes, flags, options = {}) {
|
|
122
|
+
if (flags.cursor != null && flags.cursor !== '' && resourceTypes.length !== 1) {
|
|
123
|
+
throw new Error('Map --cursor requires exactly one --type.');
|
|
124
|
+
}
|
|
125
|
+
const groups = await allWithAbort(resourceTypes.map((resourceTypeName) =>
|
|
126
|
+
(queryOptions) => listRemoteResourcePage(session, resourceTypeName, flags, queryOptions)), options);
|
|
127
|
+
return {
|
|
128
|
+
resources: groups.flatMap((group) => group.resources),
|
|
129
|
+
pagination: Object.fromEntries(groups.map((group, index) => [resourceTypes[index], group.pagination])),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
56
133
|
async function listRemoteResourceType(session, resourceType, options = {}) {
|
|
57
134
|
const resources = [];
|
|
58
135
|
const seen = new Set();
|
|
@@ -84,11 +161,6 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
|
|
|
84
161
|
return groups.flat();
|
|
85
162
|
}
|
|
86
163
|
|
|
87
|
-
function legacyCaches(projectDir) {
|
|
88
|
-
return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
|
|
89
|
-
.filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
164
|
function checkoutChanged(entry) {
|
|
93
165
|
if (entry.resource_type === 'custom_services') {
|
|
94
166
|
return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
|
|
@@ -99,19 +171,32 @@ function checkoutChanged(entry) {
|
|
|
99
171
|
async function mapCommand(projectDir, flags = {}) {
|
|
100
172
|
const resourceTypes = requestedResourceTypes(flags);
|
|
101
173
|
const config = loadProjectConfig(projectDir);
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
174
|
+
const searching = flags.route != null || flags.title != null;
|
|
175
|
+
const session = await openToolSession(config, [searching ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList]);
|
|
176
|
+
const remote = await listMapResources(session, resourceTypes, flags);
|
|
177
|
+
const manifestEntries = Object.values(loadManifest(projectDir).entries);
|
|
178
|
+
const selected = new Set(remote.resources.map(resourceKey));
|
|
179
|
+
// A filtered map is a focused inspection, so do not make unrelated local
|
|
180
|
+
// worktree state dominate its output. The unfiltered command retains the
|
|
181
|
+
// established full-worktree status behavior.
|
|
182
|
+
const statusEntries = searching
|
|
183
|
+
? manifestEntries.filter((entry) => entry.resource_type === 'custom_services'
|
|
184
|
+
? selected.has(`custom_services:${entry.resource_id}`)
|
|
185
|
+
: selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
|
|
186
|
+
: manifestEntries;
|
|
187
|
+
const contentEntries = statusEntries.filter((entry) => entry.resource_type !== 'custom_services');
|
|
188
|
+
const serviceIds = statusEntries
|
|
189
|
+
.filter((entry) => entry.resource_type === 'custom_services')
|
|
190
|
+
.map((entry) => entry.resource_id);
|
|
191
|
+
const checkouts = (await allWithAbort([
|
|
108
192
|
async (options) => {
|
|
109
193
|
const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
|
|
110
194
|
config,
|
|
111
195
|
client: session.client,
|
|
112
196
|
tools: session.tools,
|
|
113
197
|
signal: options.signal,
|
|
114
|
-
|
|
198
|
+
entries: contentEntries,
|
|
199
|
+
}), customServices.inspectRemote(projectDir, { config, signal: options.signal, ids: serviceIds })]);
|
|
115
200
|
return [...contentStatus, ...serviceStatus].map((entry) => ({
|
|
116
201
|
...entry,
|
|
117
202
|
exists: entry.local_hash !== null,
|
|
@@ -119,29 +204,58 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
119
204
|
changed: checkoutChanged(entry),
|
|
120
205
|
}));
|
|
121
206
|
},
|
|
122
|
-
]);
|
|
123
|
-
const
|
|
207
|
+
]))[0];
|
|
208
|
+
const resources = remote.resources;
|
|
209
|
+
const counts = new Map();
|
|
210
|
+
for (const resource of resources) {
|
|
211
|
+
const type = resourceType(resource);
|
|
212
|
+
counts.set(type, (counts.get(type) || 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
const checkoutStates = new Map();
|
|
215
|
+
for (const entry of checkouts) {
|
|
216
|
+
const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
|
|
217
|
+
checkoutStates.set(state, (checkoutStates.get(state) || 0) + 1);
|
|
218
|
+
}
|
|
124
219
|
const result = {
|
|
125
220
|
server: config.server,
|
|
126
|
-
overview,
|
|
127
221
|
resources,
|
|
128
222
|
checkouts,
|
|
129
|
-
|
|
223
|
+
pagination: remote.pagination,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const summary = {
|
|
227
|
+
server: config.server,
|
|
228
|
+
summary: {
|
|
229
|
+
resources: {
|
|
230
|
+
returned: resources.length,
|
|
231
|
+
matched: resources.length,
|
|
232
|
+
returned_by_type: Object.fromEntries([...counts.entries()].sort()),
|
|
233
|
+
// This is the server's fuzzy-search/list candidate count before the
|
|
234
|
+
// client-side exact route/title check. It is null when not reported.
|
|
235
|
+
candidate_total_by_type: Object.fromEntries(Object.entries(remote.pagination)
|
|
236
|
+
.map(([type, page]) => [type, page.total])),
|
|
237
|
+
},
|
|
238
|
+
checkouts: { total: checkouts.length, by_state: Object.fromEntries([...checkoutStates.entries()].sort()) },
|
|
239
|
+
truncated: Object.values(remote.pagination).some((page) => page.has_more),
|
|
240
|
+
pagination: remote.pagination,
|
|
241
|
+
},
|
|
130
242
|
};
|
|
131
243
|
|
|
132
244
|
if (flags.output === 'json') {
|
|
133
|
-
console.log(JSON.stringify(result, null, 2));
|
|
245
|
+
console.log(JSON.stringify(flags.summary ? summary : result, null, 2));
|
|
134
246
|
return 0;
|
|
135
247
|
}
|
|
136
248
|
log.title('draftgo map');
|
|
137
|
-
log.info(`Remote resources: ${resources.length}`);
|
|
138
|
-
const counts = new Map();
|
|
139
|
-
for (const resource of resources) {
|
|
140
|
-
const type = resource.resource_type || resource.type || 'unknown';
|
|
141
|
-
counts.set(type, (counts.get(type) || 0) + 1);
|
|
142
|
-
}
|
|
249
|
+
log.info(`Remote resources returned: ${resources.length}`);
|
|
143
250
|
for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
|
|
144
251
|
log.info(`Local checkouts: ${checkouts.length}`);
|
|
252
|
+
if (flags.summary) {
|
|
253
|
+
for (const [state, count] of [...checkoutStates.entries()].sort()) log.plain(` ${state}: ${count}`);
|
|
254
|
+
for (const [type, page] of Object.entries(remote.pagination)) {
|
|
255
|
+
if (page.has_more) log.plain(` ${type}: more results; use --type ${type} --cursor ${page.next_cursor}`);
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
145
259
|
for (const entry of checkouts) {
|
|
146
260
|
const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
|
|
147
261
|
log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
|
|
@@ -149,9 +263,6 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
149
263
|
+ `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
|
|
150
264
|
+ `(${entry.local_path})`);
|
|
151
265
|
}
|
|
152
|
-
if (result.legacy_cache.detected.length) {
|
|
153
|
-
log.warn(`Ignored legacy cache indexes: ${result.legacy_cache.detected.join(', ')}`);
|
|
154
|
-
}
|
|
155
266
|
return 0;
|
|
156
267
|
}
|
|
157
268
|
|
|
@@ -160,4 +271,10 @@ module.exports.itemsFrom = itemsFrom;
|
|
|
160
271
|
module.exports.normalizeMapResourceType = normalizeMapResourceType;
|
|
161
272
|
module.exports.requestedResourceTypes = requestedResourceTypes;
|
|
162
273
|
module.exports.listRemoteResources = listRemoteResources;
|
|
274
|
+
module.exports.listRemoteResourcePage = listRemoteResourcePage;
|
|
275
|
+
module.exports.listMapResources = listMapResources;
|
|
276
|
+
module.exports.normalizeRoute = normalizeRoute;
|
|
277
|
+
module.exports.resourceMatches = resourceMatches;
|
|
278
|
+
module.exports.positiveLimit = positiveLimit;
|
|
279
|
+
module.exports.reportedTotal = reportedTotal;
|
|
163
280
|
module.exports.checkoutChanged = checkoutChanged;
|
package/src/commands/mcp.js
CHANGED
|
@@ -17,7 +17,7 @@ function classifyMcpFailure(error) {
|
|
|
17
17
|
const message = String(error && error.message || error || '');
|
|
18
18
|
const code = error && error.code;
|
|
19
19
|
const status = error && error.status;
|
|
20
|
-
if (status === 401 || status === 403) return '
|
|
20
|
+
if (status === 401 || status === 403) return 'API Key authentication or authorization failed.';
|
|
21
21
|
if (/session|uninitialized|initialize first|not initialized/i.test(message)
|
|
22
22
|
|| ['SESSION_EXPIRED', 'MCP_SESSION_EXPIRED', -32002].includes(code)) {
|
|
23
23
|
return 'The MCP session was lost or the server rejected the initialized state; check sticky sessions and service restarts.';
|
|
@@ -79,7 +79,7 @@ function status(projectDir, positional = [], flags = {}) {
|
|
|
79
79
|
let exitCode = 0;
|
|
80
80
|
try {
|
|
81
81
|
const config = loadProjectConfig(projectDir);
|
|
82
|
-
log.plain(` project configured (${config.server};
|
|
82
|
+
log.plain(` project configured (${config.server}; API Key present)`);
|
|
83
83
|
} catch (error) {
|
|
84
84
|
log.plain(` project not ready (${error.message})`);
|
|
85
85
|
exitCode = 1;
|
|
@@ -6,7 +6,7 @@ const { reconcileResources } = require('../worktree');
|
|
|
6
6
|
async function reconcile(projectDir, positional, flags = {}) {
|
|
7
7
|
const [resourceType, ...ids] = positional;
|
|
8
8
|
if (!resourceType || !ids.length) {
|
|
9
|
-
log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
|
|
9
|
+
log.err('Usage: draftgo reconcile <pages|nav|docs|custom-services> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
12
|
if (require('./customService').isServiceType(resourceType)) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { callOperation } = require('./api');
|
|
5
|
+
|
|
6
|
+
// Role is a permission template. Use `draftgo grant` to bind it to a subject.
|
|
7
|
+
const OPERATIONS = Object.freeze({
|
|
8
|
+
permissions: 'listPermissionCatalog',
|
|
9
|
+
list: 'listRoles',
|
|
10
|
+
create: 'createRole',
|
|
11
|
+
get: 'getRole',
|
|
12
|
+
update: 'updateRole',
|
|
13
|
+
delete: 'deleteRole',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function operationKey(value) {
|
|
17
|
+
const action = String(value || 'list').trim().toLowerCase();
|
|
18
|
+
return OPERATIONS[action] ? action : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function roleCommand(projectDir, positional, flags = {}) {
|
|
22
|
+
const action = operationKey(positional[0]);
|
|
23
|
+
if (!action) {
|
|
24
|
+
log.err('Usage: draftgo role permissions|list|create|get|update|delete [--input <json-file>]');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
return callOperation(projectDir, OPERATIONS[action], flags);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = roleCommand;
|
|
31
|
+
module.exports.OPERATIONS = OPERATIONS;
|
|
32
|
+
module.exports.operationKey = operationKey;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { callOperation } = require('./api');
|
|
5
|
+
|
|
6
|
+
const OPERATIONS = Object.freeze({
|
|
7
|
+
list: 'listSpaces',
|
|
8
|
+
create: 'createSpace',
|
|
9
|
+
get: 'getSpace',
|
|
10
|
+
update: 'updateSpace',
|
|
11
|
+
replace: 'replaceSpace',
|
|
12
|
+
disable: 'deleteSpace',
|
|
13
|
+
'members.list': 'listWorkspaceMembers',
|
|
14
|
+
'members.add': 'addWorkspaceMember',
|
|
15
|
+
'members.get': 'getWorkspaceMember',
|
|
16
|
+
'members.update': 'updateWorkspaceMember',
|
|
17
|
+
'members.replace': 'replaceWorkspaceMember',
|
|
18
|
+
'members.remove': 'removeWorkspaceMember',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function operationKey(parts) {
|
|
22
|
+
const values = parts.map((part) => String(part || '').trim().toLowerCase()).filter(Boolean);
|
|
23
|
+
if (values.length === 1) return values[0] === 'delete' ? 'disable' : values[0];
|
|
24
|
+
if (values.length === 2 && ['member', 'members'].includes(values[0])) {
|
|
25
|
+
return `members.${values[1] === 'delete' ? 'remove' : values[1]}`;
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function spaceCommand(projectDir, positional, flags = {}) {
|
|
31
|
+
const key = operationKey(positional);
|
|
32
|
+
if (!OPERATIONS[key]) {
|
|
33
|
+
log.err('Usage: draftgo space list|create|get|update|replace|disable | members list|add|get|update|replace|remove [--input <json-file>]');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
return callOperation(projectDir, OPERATIONS[key], flags);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = spaceCommand;
|
|
40
|
+
module.exports.OPERATIONS = OPERATIONS;
|
|
41
|
+
module.exports.operationKey = operationKey;
|
package/src/commands/status.js
CHANGED
|
@@ -1,23 +1,126 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
|
-
const { all } = require('../
|
|
4
|
+
const { all } = require('../targets');
|
|
5
5
|
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
7
|
+
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
8
|
+
const { normalizeDescription } = require('../apiContractCache');
|
|
9
|
+
const { parseTimeout } = require('../timeout');
|
|
10
|
+
|
|
11
|
+
const API_KEY_STATUS_OPERATION = 'getCurrentUserAPIKey';
|
|
12
|
+
|
|
13
|
+
function asObject(value) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scopeFrom(value) {
|
|
18
|
+
let candidates = [asObject(value)];
|
|
19
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
20
|
+
const next = [];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
const type = String(candidate.scope_type || candidate.type || '').trim().toLowerCase();
|
|
23
|
+
if (type === 'platform') return { scope_type: 'platform', workspace_id: null, space_id: null };
|
|
24
|
+
if (type === 'space') {
|
|
25
|
+
return {
|
|
26
|
+
scope_type: 'space',
|
|
27
|
+
workspace_id: candidate.workspace_id ?? null,
|
|
28
|
+
space_id: candidate.space_id ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
for (const key of ['context', 'scope', 'principal', 'credential', 'authentication', 'response', 'data', 'value']) {
|
|
32
|
+
const nested = asObject(candidate[key]);
|
|
33
|
+
if (Object.keys(nested).length) next.push(nested);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
candidates = next;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function connectionDiagnostic(projectDir, options = {}) {
|
|
42
|
+
let config;
|
|
43
|
+
try {
|
|
44
|
+
config = loadProjectConfig(projectDir);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { health: 'not_configured', connected: false, error: error.message };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const diagnostic = { health: 'unhealthy', connected: false, server: config.server };
|
|
50
|
+
try {
|
|
51
|
+
const session = await openToolSession(config, [
|
|
52
|
+
TOOL_NAMES.projectOverview, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
53
|
+
], { timeoutMs: options.timeoutMs });
|
|
54
|
+
const overview = await callStructured(session, TOOL_NAMES.projectOverview, {}, { timeoutMs: options.timeoutMs });
|
|
55
|
+
const project = asObject(asObject(overview).project);
|
|
56
|
+
diagnostic.connected = true;
|
|
57
|
+
diagnostic.server_version = project.version || null;
|
|
58
|
+
diagnostic.protocol_version = session.client.protocolVersion || null;
|
|
59
|
+
diagnostic.registry_revision = asObject(asObject(overview).operation_registry).registry_revision || null;
|
|
60
|
+
|
|
61
|
+
let scope = scopeFrom(overview);
|
|
62
|
+
if (!scope) {
|
|
63
|
+
const described = normalizeDescription(await callStructured(
|
|
64
|
+
session, TOOL_NAMES.apiDescribe, { operation_id: API_KEY_STATUS_OPERATION }, { timeoutMs: options.timeoutMs },
|
|
65
|
+
));
|
|
66
|
+
if (!described) throw new Error('DraftGo API Key status contract is missing revision metadata.');
|
|
67
|
+
const result = await callStructured(session, TOOL_NAMES.apiCall, {
|
|
68
|
+
operation_id: API_KEY_STATUS_OPERATION,
|
|
69
|
+
registry_revision: described.registry_revision,
|
|
70
|
+
}, { timeoutMs: options.timeoutMs });
|
|
71
|
+
scope = scopeFrom(asObject(result).response || result);
|
|
72
|
+
}
|
|
73
|
+
if (!scope) throw new Error('DraftGo server did not report the current API Key scope.');
|
|
74
|
+
Object.assign(diagnostic, scope, { health: 'healthy' });
|
|
75
|
+
return diagnostic;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
diagnostic.error = error && error.message || String(error);
|
|
78
|
+
return diagnostic;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function status(projectDir, _positional = [], flags = {}) {
|
|
83
|
+
const targets = all.map((target) => {
|
|
84
|
+
const value = target.status(projectDir);
|
|
85
|
+
return { name: target.name, display_name: target.displayName, installed: value.installed === true };
|
|
86
|
+
});
|
|
87
|
+
const connection = await connectionDiagnostic(projectDir, { timeoutMs: parseTimeout(flags.timeout) });
|
|
88
|
+
const result = {
|
|
89
|
+
project_dir: projectDir,
|
|
90
|
+
cli_version: getPackageVersion(),
|
|
91
|
+
skill_version: readInstalledVersion(projectDir) || null,
|
|
92
|
+
connection,
|
|
93
|
+
targets,
|
|
94
|
+
};
|
|
95
|
+
if (flags.output === 'json') {
|
|
96
|
+
console.log(JSON.stringify(result, null, 2));
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
6
99
|
|
|
7
|
-
function status(projectDir) {
|
|
8
100
|
log.title('draftgo status');
|
|
9
101
|
log.info(`项目目录:${projectDir}`);
|
|
10
|
-
log.info(`CLI 版本:${
|
|
11
|
-
log.info(`已装 skill 版本:${
|
|
102
|
+
log.info(`CLI 版本:${result.cli_version}`);
|
|
103
|
+
log.info(`已装 skill 版本:${result.skill_version || '(未安装)'}`);
|
|
104
|
+
log.info(`服务端:${connection.server || '(未连接)'}`);
|
|
105
|
+
if (connection.health === 'healthy') {
|
|
106
|
+
log.ok(`连接健康:healthy${connection.server_version ? `(server ${connection.server_version})` : ''}`);
|
|
107
|
+
const ids = connection.scope_type === 'space'
|
|
108
|
+
? `,workspace_id=${connection.workspace_id ?? '-'},space_id=${connection.space_id ?? '-'}` : '';
|
|
109
|
+
log.info(`当前上下文:${connection.scope_type}${ids}`);
|
|
110
|
+
} else {
|
|
111
|
+
log.warn(`连接健康:${connection.health}`);
|
|
112
|
+
if (connection.error) log.dim(` ${connection.error}`);
|
|
113
|
+
}
|
|
12
114
|
|
|
13
115
|
console.log('');
|
|
14
116
|
console.log('AI 工具入口:');
|
|
15
|
-
for (const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
console.log(` ${mark} ${t.name.padEnd(12)} ${t.displayName}`);
|
|
117
|
+
for (const target of targets) {
|
|
118
|
+
const mark = target.installed ? log.c.green('✓') : log.c.gray('·');
|
|
119
|
+
console.log(` ${mark} ${target.name.padEnd(12)} ${target.display_name}`);
|
|
19
120
|
}
|
|
20
121
|
return 0;
|
|
21
122
|
}
|
|
22
123
|
|
|
23
124
|
module.exports = status;
|
|
125
|
+
module.exports.scopeFrom = scopeFrom;
|
|
126
|
+
module.exports.connectionDiagnostic = connectionDiagnostic;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
|
-
const { all, resolveTargets } = require('../
|
|
4
|
+
const { all, resolveTargets } = require('../targets');
|
|
5
5
|
const { removePath } = require('../fsx');
|
|
6
6
|
const { dgDir } = require('../paths');
|
|
7
7
|
|
|
@@ -38,10 +38,10 @@ async function uninstall(projectDir, positional, flags) {
|
|
|
38
38
|
|
|
39
39
|
// Each AI-tool skill directory is self-contained, so per-target uninstall
|
|
40
40
|
// already wiped its content above. Only --purge nukes runtime data under
|
|
41
|
-
// .draftgo/ (config/
|
|
41
|
+
// .draftgo/ (config/worklog/lessons/cache etc.).
|
|
42
42
|
if (flags.purge) {
|
|
43
43
|
if (removePath(dgDir(projectDir))) {
|
|
44
|
-
log.warn('已使用 --purge:.draftgo/(含 config/
|
|
44
|
+
log.warn('已使用 --purge:.draftgo/(含 config/worklog/lessons 等运行时数据)已删除。');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
package/src/commands/update.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const log = require('../logger');
|
|
5
|
-
const { all, byName, resolveTargets } = require('../
|
|
5
|
+
const { all, byName, resolveTargets } = require('../targets');
|
|
6
6
|
const { ensureRuntime, writeInstalledVersion, readInstalledVersion, getPackageVersion } = require('../skill');
|
|
7
7
|
const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
|
|
8
8
|
const { detectTargets } = require('../detect');
|
|
9
|
+
const { readInstallMarker, installRelease } = require('../releaseInstall');
|
|
9
10
|
|
|
10
11
|
const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
|
|
11
12
|
|
|
@@ -30,19 +31,30 @@ async function updateCliIfNeeded(projectDir, positional, flags) {
|
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const marker = readInstallMarker();
|
|
35
|
+
let command = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
|
|
34
36
|
log.step(`更新 draftgo-cli:${current} → ${latest}`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
if (marker) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await installRelease(latest, null, marker);
|
|
40
|
+
command = result.launcher;
|
|
41
|
+
log.ok(`CLI Release updated to ${latest}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
log.err(`CLI Release update failed: ${error.message}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
48
|
+
const install = spawnSync(npm, ['install', '-g', `draftgo-cli@${latest}`], { stdio: 'inherit', shell: false });
|
|
49
|
+
if (install.error) {
|
|
50
|
+
log.err(`CLI 更新启动失败:${install.error.message}`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (install.status !== 0) {
|
|
54
|
+
log.err(`npm install 返回码 ${install.status}`);
|
|
55
|
+
return Number.isInteger(install.status) ? install.status : 1;
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
const command = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
|
|
46
58
|
const rerun = spawnSync(command, ['update', ...positional, '--project', projectDir, '--skip-update-check'], {
|
|
47
59
|
stdio: 'inherit',
|
|
48
60
|
shell: false,
|