draftgo-cli 4.0.25 → 4.0.26
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 +21 -37
- package/package.json +3 -5
- package/resources/skill/SKILL.md +9 -5
- package/resources/skill/manifest.json +2 -5
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +2 -50
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/chat-sdk.md +29 -37
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/data.md +0 -46
- package/resources/skill/references/delivery.md +3 -3
- package/resources/skill/references/diagnostics.md +10 -11
- package/resources/skill/references/frontend.md +23 -20
- package/resources/skill/references/mcp.md +4 -14
- package/resources/skill/references/methods.md +15 -68
- package/resources/skill/references/modules.md +23 -44
- package/resources/skill/references/runtime.md +3 -20
- package/resources/skill/story/SKILL.md +2 -2
- package/src/apiContractCache.js +14 -6
- package/src/cli.js +0 -7
- package/src/commandRegistry.js +0 -6
- package/src/commands/api.js +87 -17
- package/src/commands/apiKey.js +2 -6
- package/src/commands/autoPush.js +15 -51
- package/src/commands/capabilities.js +22 -15
- package/src/commands/check.js +19 -53
- package/src/commands/checkout.js +1 -4
- package/src/commands/clean.js +1 -1
- package/src/commands/commit.js +1 -4
- package/src/commands/components.js +12 -8
- package/src/commands/conflict.js +4 -6
- package/src/commands/conflicts.js +1 -2
- package/src/commands/connect.js +0 -8
- package/src/commands/delete.js +15 -11
- package/src/commands/deploy.js +64 -26
- package/src/commands/diff.js +1 -4
- package/src/commands/group.js +2 -3
- package/src/commands/help.js +19 -41
- package/src/commands/init.js +13 -6
- package/src/commands/local.js +4 -1
- package/src/commands/map.js +138 -23
- package/src/commands/reconcile.js +1 -15
- package/src/commands/role.js +1 -2
- package/src/commands/status.js +12 -40
- package/src/commands/verify.js +8 -7
- package/src/commands/worklog.js +11 -5
- package/src/contractCompatibility.js +10 -2
- package/src/localRuntime/compose.js +41 -27
- package/src/localRuntime/detect.js +6 -6
- package/src/localRuntime/index.js +47 -47
- package/src/localRuntime/services.js +2 -39
- package/src/mcp/client.js +99 -134
- package/src/mcp/parallel.js +25 -2
- package/src/mcp/protocol.js +38 -9
- package/src/mcp/tools.js +10 -19
- package/src/projectConfig.js +1 -4
- package/src/{workspaceHealth.js → projectHealth.js} +5 -5
- package/src/projectMap.js +1 -1
- package/src/runtimeFiles.js +2 -1
- package/src/worklog.js +3 -2
- package/src/worktree/backend.js +127 -15
- package/src/worktree/index.js +64 -22
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +18 -4
- package/src/worktree/status.js +4 -2
- package/resources/custom-service-sdk/ai.go +0 -520
- package/resources/custom-service-sdk/ai_test.go +0 -156
- package/resources/custom-service-sdk/auth_test.go +0 -56
- package/resources/custom-service-sdk/billing.go +0 -596
- package/resources/custom-service-sdk/billing_test.go +0 -150
- package/resources/custom-service-sdk/go.mod +0 -3
- package/resources/custom-service-sdk/manifest.json +0 -77
- package/resources/custom-service-sdk/platform.go +0 -352
- package/resources/custom-service-sdk/platform_logger_test.go +0 -24
- package/resources/custom-service-sdk/registration_test.go +0 -39
- package/resources/custom-service-sdk/resources.go +0 -247
- package/resources/custom-service-sdk/resources_billing_test.go +0 -115
- package/resources/custom-service-sdk/resources_files_test.go +0 -57
- package/resources/custom-service-sdk/resources_scope_test.go +0 -92
- package/resources/custom-service-sdk/sdk.go +0 -209
- package/resources/skill/references/aihub.md +0 -116
- package/resources/skill/references/custom-services.md +0 -201
- package/src/commands/customService.js +0 -95
- package/src/commands/dataRange.js +0 -33
- package/src/commands/grant.js +0 -29
- package/src/commands/space.js +0 -41
- package/src/customServices.js +0 -484
package/src/commands/map.js
CHANGED
|
@@ -7,15 +7,18 @@ const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
|
7
7
|
const { allWithAbort } = require('../mcp/parallel');
|
|
8
8
|
const { inspectRemoteCheckouts } = require('../worktree/status');
|
|
9
9
|
const { loadManifest } = require('../worktree/manifest');
|
|
10
|
-
const customServices = require('../customServices');
|
|
11
10
|
|
|
12
11
|
const REMOTE_RESOURCE_TYPES = Object.freeze({
|
|
13
12
|
pages: 'pages',
|
|
14
13
|
navigations: 'navigations',
|
|
15
14
|
docs: 'docs/articles',
|
|
16
|
-
custom_services: 'custom_services',
|
|
17
15
|
});
|
|
18
16
|
const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
|
|
17
|
+
const LIST_OPERATIONS = Object.freeze({
|
|
18
|
+
pages: 'listPage',
|
|
19
|
+
navigations: 'listNavigation',
|
|
20
|
+
'docs/articles': 'listDocAdminArticle',
|
|
21
|
+
});
|
|
19
22
|
|
|
20
23
|
function itemsFrom(value) {
|
|
21
24
|
if (Array.isArray(value)) return value;
|
|
@@ -46,8 +49,79 @@ function reportedTotal(value) {
|
|
|
46
49
|
return Number.isInteger(total) && total >= 0 ? total : null;
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
function registryCursor(operationID, page) {
|
|
53
|
+
return Buffer.from(JSON.stringify({ operation_id: operationID, page }), 'utf8').toString('base64url');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registryPageFromCursor(cursor, operationID) {
|
|
57
|
+
if (cursor == null || cursor === '') return 1;
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
|
60
|
+
if (value && value.operation_id === operationID && Number.isSafeInteger(value.page) && value.page > 1) {
|
|
61
|
+
return value.page;
|
|
62
|
+
}
|
|
63
|
+
} catch { /* Report one stable CLI error below. */ }
|
|
64
|
+
throw new Error(`Map --cursor is not valid for Registry operation ${operationID}.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function queryProperties(operation) {
|
|
68
|
+
const schema = operation && operation.input_schema;
|
|
69
|
+
const query = schema && schema.properties && schema.properties.query;
|
|
70
|
+
return query && query.properties && typeof query.properties === 'object' ? query.properties : {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function registryQuery(operation, operationID, flags) {
|
|
74
|
+
const properties = queryProperties(operation);
|
|
75
|
+
const query = {};
|
|
76
|
+
const limit = positiveLimit(flags.limit);
|
|
77
|
+
const cursor = flags.cursor == null ? '' : String(flags.cursor);
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'cursor')) {
|
|
79
|
+
if (cursor) query.cursor = cursor;
|
|
80
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'limit')) query.limit = limit;
|
|
81
|
+
} else if (Object.prototype.hasOwnProperty.call(properties, 'page')
|
|
82
|
+
&& Object.prototype.hasOwnProperty.call(properties, 'page_size')) {
|
|
83
|
+
query.page = registryPageFromCursor(cursor, operationID);
|
|
84
|
+
query.page_size = limit;
|
|
85
|
+
} else if (cursor) {
|
|
86
|
+
throw new Error(`${operationID} does not expose a paginated Registry contract.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const search = flags.route != null ? normalizeRoute(flags.route)
|
|
90
|
+
: flags.title != null ? String(flags.title) : '';
|
|
91
|
+
if (search) {
|
|
92
|
+
const key = flags.route != null && Object.prototype.hasOwnProperty.call(properties, 'route')
|
|
93
|
+
? 'route'
|
|
94
|
+
: ['search', 'q', 'query'].find((candidate) => Object.prototype.hasOwnProperty.call(properties, candidate));
|
|
95
|
+
if (key) query[key] = search;
|
|
96
|
+
}
|
|
97
|
+
return query;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function registryPagination(payload, operationID, query, returned) {
|
|
101
|
+
const serverCursor = nextCursor(payload);
|
|
102
|
+
if (serverCursor != null && serverCursor !== '') {
|
|
103
|
+
return {
|
|
104
|
+
next_cursor: String(serverCursor),
|
|
105
|
+
has_more: true,
|
|
106
|
+
total: reportedTotal(payload),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (claimsMorePages(payload)) {
|
|
110
|
+
throw new Error(`${operationID} reported more pages without a cursor.`);
|
|
111
|
+
}
|
|
112
|
+
const total = reportedTotal(payload);
|
|
113
|
+
const page = Number(query.page);
|
|
114
|
+
const pageSize = Number(query.page_size);
|
|
115
|
+
const hasMore = Number.isSafeInteger(page) && Number.isSafeInteger(pageSize)
|
|
116
|
+
&& (total != null ? page * pageSize < total : returned === pageSize);
|
|
117
|
+
return {
|
|
118
|
+
next_cursor: hasMore ? registryCursor(operationID, page + 1) : null,
|
|
119
|
+
has_more: hasMore,
|
|
120
|
+
total,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
49
124
|
function normalizeMapResourceType(value) {
|
|
50
|
-
if (require('./customService').isServiceType(value)) return 'custom_services';
|
|
51
125
|
return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
|
|
52
126
|
}
|
|
53
127
|
|
|
@@ -84,7 +158,6 @@ function resourceMatches(resource, flags = {}) {
|
|
|
84
158
|
|
|
85
159
|
function resourceKey(resource) {
|
|
86
160
|
const type = resourceType(resource);
|
|
87
|
-
if (type === 'custom_services') return `custom_services:${resource.resource_id}`;
|
|
88
161
|
return `${canonicalResourceType(type)}:${resource.resource_id}`;
|
|
89
162
|
}
|
|
90
163
|
|
|
@@ -130,6 +203,41 @@ async function listMapResources(session, resourceTypes, flags, options = {}) {
|
|
|
130
203
|
};
|
|
131
204
|
}
|
|
132
205
|
|
|
206
|
+
async function listRegistryResources(projectDir, config, session, resourceTypes, flags = {}) {
|
|
207
|
+
const { registryRevision, descriptionForRevision } = require('./api');
|
|
208
|
+
if (flags.cursor != null && flags.cursor !== '' && resourceTypes.length !== 1) {
|
|
209
|
+
throw new Error('Map --cursor requires exactly one --type.');
|
|
210
|
+
}
|
|
211
|
+
const groups = await allWithAbort(resourceTypes.map((type) => async (options) => {
|
|
212
|
+
const operationID = LIST_OPERATIONS[type];
|
|
213
|
+
const revision = await registryRevision(session, operationID);
|
|
214
|
+
const description = await descriptionForRevision(projectDir, config, session, operationID, revision);
|
|
215
|
+
const operation = description.operation || {};
|
|
216
|
+
const query = registryQuery(operation, operationID, flags);
|
|
217
|
+
const invoke = {
|
|
218
|
+
operation_id: operationID,
|
|
219
|
+
registry_revision: revision,
|
|
220
|
+
};
|
|
221
|
+
if (Object.keys(query).length) invoke.query = query;
|
|
222
|
+
const payload = await callStructured(session, TOOL_NAMES.apiCall, invoke, options);
|
|
223
|
+
const response = payload && payload.response !== undefined ? payload.response
|
|
224
|
+
: payload && payload.data !== undefined ? payload.data : payload;
|
|
225
|
+
const responseItems = itemsFrom(response);
|
|
226
|
+
const resources = responseItems
|
|
227
|
+
.map((item) => ({ ...item, resource_type: type, resource_id: String(item.resource_id ?? item.id) }))
|
|
228
|
+
.filter((item) => item.resource_id && item.resource_id !== 'undefined')
|
|
229
|
+
.filter((item) => resourceMatches(item, flags));
|
|
230
|
+
return {
|
|
231
|
+
resources,
|
|
232
|
+
pagination: registryPagination(response, operationID, query, responseItems.length),
|
|
233
|
+
};
|
|
234
|
+
}));
|
|
235
|
+
return {
|
|
236
|
+
resources: groups.flatMap((group) => group.resources),
|
|
237
|
+
pagination: Object.fromEntries(groups.map((group, index) => [resourceTypes[index], group.pagination])),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
133
241
|
async function listRemoteResourceType(session, resourceType, options = {}) {
|
|
134
242
|
const resources = [];
|
|
135
243
|
const seen = new Set();
|
|
@@ -161,43 +269,40 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
|
|
|
161
269
|
return groups.flat();
|
|
162
270
|
}
|
|
163
271
|
|
|
164
|
-
function checkoutChanged(entry) {
|
|
165
|
-
if (entry.resource_type === 'custom_services') {
|
|
166
|
-
return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
|
|
167
|
-
}
|
|
168
|
-
return entry.local_hash !== null && entry.local_hash !== entry.base_hash;
|
|
169
|
-
}
|
|
272
|
+
function checkoutChanged(entry) { return entry.local_hash !== null && entry.local_hash !== entry.base_hash; }
|
|
170
273
|
|
|
171
274
|
async function mapCommand(projectDir, flags = {}) {
|
|
172
275
|
const resourceTypes = requestedResourceTypes(flags);
|
|
173
276
|
const config = loadProjectConfig(projectDir);
|
|
174
277
|
const searching = flags.route != null || flags.title != null;
|
|
175
|
-
|
|
176
|
-
|
|
278
|
+
let session;
|
|
279
|
+
let remote;
|
|
280
|
+
try {
|
|
281
|
+
session = await openToolSession(config, [searching ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList]);
|
|
282
|
+
remote = await listMapResources(session, resourceTypes, flags);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (!error || error.code !== 'MCP_TOOL_UNAVAILABLE') throw error;
|
|
285
|
+
session = await openToolSession(config, [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
|
|
286
|
+
remote = await listRegistryResources(projectDir, config, session, resourceTypes, flags);
|
|
287
|
+
}
|
|
177
288
|
const manifestEntries = Object.values(loadManifest(projectDir).entries);
|
|
178
289
|
const selected = new Set(remote.resources.map(resourceKey));
|
|
179
290
|
// A filtered map is a focused inspection, so do not make unrelated local
|
|
180
291
|
// worktree state dominate its output. The unfiltered command retains the
|
|
181
292
|
// established full-worktree status behavior.
|
|
182
293
|
const statusEntries = searching
|
|
183
|
-
? manifestEntries.filter((entry) => entry.resource_type
|
|
184
|
-
? selected.has(`custom_services:${entry.resource_id}`)
|
|
185
|
-
: selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
|
|
294
|
+
? manifestEntries.filter((entry) => selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
|
|
186
295
|
: 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
296
|
const checkouts = (await allWithAbort([
|
|
192
297
|
async (options) => {
|
|
193
|
-
const
|
|
298
|
+
const contentStatus = await inspectRemoteCheckouts(projectDir, {
|
|
194
299
|
config,
|
|
195
300
|
client: session.client,
|
|
196
301
|
tools: session.tools,
|
|
197
302
|
signal: options.signal,
|
|
198
|
-
entries:
|
|
199
|
-
})
|
|
200
|
-
return
|
|
303
|
+
entries: statusEntries,
|
|
304
|
+
});
|
|
305
|
+
return contentStatus.map((entry) => ({
|
|
201
306
|
...entry,
|
|
202
307
|
exists: entry.local_hash !== null,
|
|
203
308
|
current_hash: entry.local_hash,
|
|
@@ -248,6 +353,11 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
248
353
|
log.title('draftgo map');
|
|
249
354
|
log.info(`Remote resources returned: ${resources.length}`);
|
|
250
355
|
for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
|
|
356
|
+
if (!flags.summary) {
|
|
357
|
+
for (const resource of resources) {
|
|
358
|
+
log.plain(` ${resourceType(resource)} ${resource.resource_id}: ${resource.title || '-'} route=${resource.route || '-'} code=${resource.code || '-'} slug=${resource.slug || '-'}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
251
361
|
log.info(`Local checkouts: ${checkouts.length}`);
|
|
252
362
|
if (flags.summary) {
|
|
253
363
|
for (const [state, count] of [...checkoutStates.entries()].sort()) log.plain(` ${state}: ${count}`);
|
|
@@ -273,8 +383,13 @@ module.exports.requestedResourceTypes = requestedResourceTypes;
|
|
|
273
383
|
module.exports.listRemoteResources = listRemoteResources;
|
|
274
384
|
module.exports.listRemoteResourcePage = listRemoteResourcePage;
|
|
275
385
|
module.exports.listMapResources = listMapResources;
|
|
386
|
+
module.exports.listRegistryResources = listRegistryResources;
|
|
276
387
|
module.exports.normalizeRoute = normalizeRoute;
|
|
277
388
|
module.exports.resourceMatches = resourceMatches;
|
|
278
389
|
module.exports.positiveLimit = positiveLimit;
|
|
279
390
|
module.exports.reportedTotal = reportedTotal;
|
|
391
|
+
module.exports.registryCursor = registryCursor;
|
|
392
|
+
module.exports.registryPageFromCursor = registryPageFromCursor;
|
|
393
|
+
module.exports.registryQuery = registryQuery;
|
|
394
|
+
module.exports.registryPagination = registryPagination;
|
|
280
395
|
module.exports.checkoutChanged = checkoutChanged;
|
|
@@ -6,23 +6,9 @@ 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
|
|
9
|
+
log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
|
-
if (require('./customService').isServiceType(resourceType)) {
|
|
13
|
-
const services = require('../customServices');
|
|
14
|
-
const states = await services.inspectRemote(projectDir);
|
|
15
|
-
const results = [];
|
|
16
|
-
for (const id of ids) {
|
|
17
|
-
const state = states.find((entry) => String(entry.resource_id) === String(id));
|
|
18
|
-
if (!state || state.state !== 'committed_unrecorded') throw new Error(`custom_services ${id} cannot be reconciled from state ${state?.state || 'missing'}.`);
|
|
19
|
-
await services.checkout(projectDir, [id], { force: true });
|
|
20
|
-
results.push({ resource_type: 'custom_services', resource_id: String(id), base_revision: state.remote_revision });
|
|
21
|
-
}
|
|
22
|
-
if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
|
|
23
|
-
else results.forEach((result) => log.ok(`Reconciled custom_services ${result.resource_id} -> ${result.base_revision}`));
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
12
|
const results = await reconcileResources(projectDir, resourceType, ids);
|
|
27
13
|
if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
|
|
28
14
|
else for (const result of results) {
|
package/src/commands/role.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
const log = require('../logger');
|
|
4
4
|
const { callOperation } = require('./api');
|
|
5
5
|
|
|
6
|
-
// Role is a permission template. Use `draftgo grant` to bind it to a subject.
|
|
7
6
|
const OPERATIONS = Object.freeze({
|
|
8
|
-
permissions: '
|
|
7
|
+
permissions: 'listPermissions',
|
|
9
8
|
list: 'listRoles',
|
|
10
9
|
create: 'createRole',
|
|
11
10
|
get: 'getRole',
|
package/src/commands/status.js
CHANGED
|
@@ -5,30 +5,19 @@ const { all } = require('../targets');
|
|
|
5
5
|
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
6
|
const { loadProjectConfig } = require('../projectConfig');
|
|
7
7
|
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
8
|
-
const { normalizeDescription } = require('../apiContractCache');
|
|
9
8
|
const { parseTimeout } = require('../timeout');
|
|
10
9
|
|
|
11
|
-
const API_KEY_STATUS_OPERATION = 'getCurrentUserAPIKey';
|
|
12
|
-
|
|
13
10
|
function asObject(value) {
|
|
14
11
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
function
|
|
14
|
+
function findField(value, keys) {
|
|
18
15
|
let candidates = [asObject(value)];
|
|
19
16
|
for (let depth = 0; depth < 4; depth += 1) {
|
|
20
17
|
const next = [];
|
|
21
18
|
for (const candidate of candidates) {
|
|
22
|
-
const
|
|
23
|
-
|
|
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']) {
|
|
19
|
+
for (const key of keys) if (candidate[key] != null) return candidate[key];
|
|
20
|
+
for (const key of ['operation_registry', 'registry', 'response', 'data', 'value']) {
|
|
32
21
|
const nested = asObject(candidate[key]);
|
|
33
22
|
if (Object.keys(nested).length) next.push(nested);
|
|
34
23
|
}
|
|
@@ -48,30 +37,15 @@ async function connectionDiagnostic(projectDir, options = {}) {
|
|
|
48
37
|
|
|
49
38
|
const diagnostic = { health: 'unhealthy', connected: false, server: config.server };
|
|
50
39
|
try {
|
|
51
|
-
const session = await openToolSession(config, [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const project = asObject(asObject(overview).project);
|
|
40
|
+
const session = await openToolSession(config, [TOOL_NAMES.apiSearch], { timeoutMs: options.timeoutMs });
|
|
41
|
+
const catalog = await callStructured(session, TOOL_NAMES.apiSearch, {
|
|
42
|
+
query: 'system', limit: 1,
|
|
43
|
+
}, { timeoutMs: options.timeoutMs });
|
|
56
44
|
diagnostic.connected = true;
|
|
57
|
-
diagnostic.server_version =
|
|
45
|
+
diagnostic.server_version = asObject(session.initialized.serverInfo).version || null;
|
|
58
46
|
diagnostic.protocol_version = session.client.protocolVersion || null;
|
|
59
|
-
diagnostic.registry_revision =
|
|
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' });
|
|
47
|
+
diagnostic.registry_revision = findField(catalog, ['registry_revision']) || null;
|
|
48
|
+
diagnostic.health = 'healthy';
|
|
75
49
|
return diagnostic;
|
|
76
50
|
} catch (error) {
|
|
77
51
|
diagnostic.error = error && error.message || String(error);
|
|
@@ -104,9 +78,7 @@ async function status(projectDir, _positional = [], flags = {}) {
|
|
|
104
78
|
log.info(`服务端:${connection.server || '(未连接)'}`);
|
|
105
79
|
if (connection.health === 'healthy') {
|
|
106
80
|
log.ok(`连接健康:healthy${connection.server_version ? `(server ${connection.server_version})` : ''}`);
|
|
107
|
-
|
|
108
|
-
? `,workspace_id=${connection.workspace_id ?? '-'},space_id=${connection.space_id ?? '-'}` : '';
|
|
109
|
-
log.info(`当前上下文:${connection.scope_type}${ids}`);
|
|
81
|
+
log.info(`Registry revision:${connection.registry_revision || '-'}`);
|
|
110
82
|
} else {
|
|
111
83
|
log.warn(`连接健康:${connection.health}`);
|
|
112
84
|
if (connection.error) log.dim(` ${connection.error}`);
|
|
@@ -122,5 +94,5 @@ async function status(projectDir, _positional = [], flags = {}) {
|
|
|
122
94
|
}
|
|
123
95
|
|
|
124
96
|
module.exports = status;
|
|
125
|
-
module.exports.
|
|
97
|
+
module.exports.findField = findField;
|
|
126
98
|
module.exports.connectionDiagnostic = connectionDiagnostic;
|
package/src/commands/verify.js
CHANGED
|
@@ -128,18 +128,23 @@ async function verifyPageComponents(projectDir, selected) {
|
|
|
128
128
|
async function verify(projectDir, positional = [], flags = {}) {
|
|
129
129
|
let selected;
|
|
130
130
|
let remoteMode;
|
|
131
|
+
let visual;
|
|
132
|
+
const url = String(flags.url || '').trim();
|
|
131
133
|
try {
|
|
132
134
|
selected = resources(positional);
|
|
133
135
|
remoteMode = flags.remote ? 'always' : 'never';
|
|
136
|
+
visual = visualRequest(flags, url);
|
|
137
|
+
if (flags.output === 'json' && visual.run) throw new Error('Visual verification does not support --output json; run the explicit visual check separately.');
|
|
134
138
|
} catch (error) {
|
|
135
139
|
log.err(error.message);
|
|
136
140
|
return 1;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
log.title('draftgo verify');
|
|
140
|
-
try { await verifyPageComponents(projectDir, selected); }
|
|
143
|
+
if (flags.output !== 'json') log.title('draftgo verify');
|
|
144
|
+
try { if (flags.remote) await verifyPageComponents(projectDir, selected); }
|
|
141
145
|
catch (error) { log.err(error.message); return 1; }
|
|
142
146
|
const checkCode = await check(projectDir, [], {
|
|
147
|
+
output: flags.output,
|
|
143
148
|
strict: flags.strict,
|
|
144
149
|
remote: remoteMode === 'always',
|
|
145
150
|
resourceKeys: selected.length
|
|
@@ -148,13 +153,9 @@ async function verify(projectDir, positional = [], flags = {}) {
|
|
|
148
153
|
});
|
|
149
154
|
if (checkCode !== 0) return checkCode;
|
|
150
155
|
|
|
151
|
-
const url = String(flags.url || '').trim();
|
|
152
|
-
let visual;
|
|
153
|
-
try { visual = visualRequest(flags, url); }
|
|
154
|
-
catch (error) { log.err(error.message); return 1; }
|
|
155
156
|
if (!visual.run) {
|
|
156
157
|
if (url) log.warn('--url was provided without --screenshot always or --ui always; visual verification was skipped.');
|
|
157
|
-
log.ok('
|
|
158
|
+
if (flags.output !== 'json') log.ok('Local verification passed; browser and business interactions were not tested.');
|
|
158
159
|
return 0;
|
|
159
160
|
}
|
|
160
161
|
let targets;
|
package/src/commands/worklog.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
} = require('../worklog');
|
|
14
14
|
|
|
15
15
|
function printUsage() {
|
|
16
|
-
log.err('Usage: draftgo work start|add|start-item|complete|show|list <value>');
|
|
16
|
+
log.err('Usage: draftgo work start|add|start-item|wait|complete|show|list <value>');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function output(flags, value) {
|
|
@@ -27,7 +27,12 @@ function latestReference(blocks) {
|
|
|
27
27
|
|
|
28
28
|
function resultFor(blocks, reference) {
|
|
29
29
|
const resolved = resolveReference(blocks, reference);
|
|
30
|
-
return { date: resolved.block.date, ...resolved.entry };
|
|
30
|
+
return { date: resolved.block.date, ...resolved.entry, status: visibleStatus(resolved.entry.status) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function visibleStatus(status) { return status === 'pending' ? 'waiting' : status; }
|
|
34
|
+
function visibleBlocks(blocks) {
|
|
35
|
+
return blocks.map(block => ({ ...block, entries: block.entries.map(entry => ({ ...entry, status: visibleStatus(entry.status) })) }));
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
function add(projectDir, positional, flags, status) {
|
|
@@ -61,14 +66,14 @@ function change(projectDir, positional, flags, status) {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
function show(projectDir, flags) {
|
|
64
|
-
const blocks = readWorklog(projectDir);
|
|
69
|
+
const blocks = visibleBlocks(readWorklog(projectDir));
|
|
65
70
|
output(flags, { path: worklogPath(projectDir), blocks });
|
|
66
71
|
if (flags.output !== 'json') console.log(renderWorklog(blocks) || 'Worklog is empty.');
|
|
67
72
|
return 0;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
function list(projectDir, flags) {
|
|
71
|
-
const blocks = readWorklog(projectDir);
|
|
76
|
+
const blocks = visibleBlocks(readWorklog(projectDir));
|
|
72
77
|
const items = blocks.flatMap((block) => block.entries.map((entry) => ({ date: block.date, ...entry })));
|
|
73
78
|
output(flags, { path: worklogPath(projectDir), items });
|
|
74
79
|
if (flags.output !== 'json') items.forEach((item) => log.info(`${item.date}#${item.number} [${item.status}] ${item.title}`));
|
|
@@ -78,7 +83,8 @@ function list(projectDir, flags) {
|
|
|
78
83
|
async function worklog(projectDir, positional = [], flags = {}) {
|
|
79
84
|
const operation = positional[0] || 'show';
|
|
80
85
|
if (operation === 'start') return add(projectDir, positional.slice(1), flags, 'active');
|
|
81
|
-
if (operation === 'add') return add(projectDir, positional.slice(1), flags, '
|
|
86
|
+
if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'waiting');
|
|
87
|
+
if (operation === 'wait') return change(projectDir, positional.slice(1), flags, 'waiting');
|
|
82
88
|
if (operation === 'start-item' || operation === 'start_item') return change(projectDir, positional.slice(1), flags, 'active');
|
|
83
89
|
if (operation === 'complete' || operation === 'done') return change(projectDir, positional.slice(1), flags, 'completed');
|
|
84
90
|
if (operation === 'show') return show(projectDir, flags);
|
|
@@ -21,7 +21,8 @@ function isHardContractIncompatibility(error) {
|
|
|
21
21
|
|
|
22
22
|
function isDangerousOperation(contract) {
|
|
23
23
|
const operation = contract && contract.operation || contract || {};
|
|
24
|
-
|
|
24
|
+
const risk = String(operation.risk || '').toLowerCase();
|
|
25
|
+
return operation.destructive === true || risk === 'high' || risk === 'critical';
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function canonicalValue(value) {
|
|
@@ -36,9 +37,15 @@ function sameOperationContract(left, right) {
|
|
|
36
37
|
return JSON.stringify(canonicalValue(leftOperation)) === JSON.stringify(canonicalValue(rightOperation));
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function canRetryAfterContractChange(left, right) {
|
|
41
|
+
const operation = right && right.operation || right || {};
|
|
42
|
+
const method = String(operation.method || '').toUpperCase();
|
|
43
|
+
return method === 'GET' || method === 'HEAD' || sameOperationContract(left, right);
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
function incompatibleOperationError(operationId, error) {
|
|
40
47
|
const wrapped = new Error(
|
|
41
|
-
`DraftGo CLI and server contracts are incompatible for
|
|
48
|
+
`DraftGo CLI and server contracts are incompatible for operation ${operationId}; `
|
|
42
49
|
+ 'the call was blocked before retry. Upgrade DraftGo CLI, then describe and confirm the operation again.',
|
|
43
50
|
);
|
|
44
51
|
wrapped.code = 'CLI_CONTRACT_INCOMPATIBLE';
|
|
@@ -53,5 +60,6 @@ module.exports = {
|
|
|
53
60
|
isHardContractIncompatibility,
|
|
54
61
|
isDangerousOperation,
|
|
55
62
|
sameOperationContract,
|
|
63
|
+
canRetryAfterContractChange,
|
|
56
64
|
incompatibleOperationError,
|
|
57
65
|
};
|
|
@@ -23,6 +23,7 @@ function readEnv(projectDir) {
|
|
|
23
23
|
|
|
24
24
|
function writeEnv(projectDir, env) {
|
|
25
25
|
writeText(envPath(projectDir), `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join('\n')}\n`);
|
|
26
|
+
fs.chmodSync(envPath(projectDir), 0o600);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function sanitizeProjectName(raw, fallback = 'draftgo') {
|
|
@@ -30,76 +31,89 @@ function sanitizeProjectName(raw, fallback = 'draftgo') {
|
|
|
30
31
|
return value || fallback;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function defaultRedisKeyPrefix(projectName, mysql) {
|
|
34
|
-
return (mysql && mysql.database) || projectName;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function dockerHost(host) {
|
|
38
35
|
return host === 'localhost' || host === '127.0.0.1' ? 'host.docker.internal' : host;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
function generate(projectDir, opts) {
|
|
42
39
|
const root = stackDir(projectDir);
|
|
40
|
+
if (exists(composePath(projectDir)) && readText(composePath(projectDir)).includes('./data/storage:/app/data/storage')) {
|
|
41
|
+
throw new Error('Existing runtime uses legacy storage. Back up and migrate its data to the final DraftGo /app/data layout before regenerating Compose; existing files were preserved.');
|
|
42
|
+
}
|
|
43
43
|
ensureDir(root);
|
|
44
44
|
const env = readEnv(projectDir);
|
|
45
45
|
const projectName = sanitizeProjectName(opts.projectName, 'draftgo');
|
|
46
46
|
const database = opts.mysql.database;
|
|
47
47
|
const qdrant = opts.qdrant || { host: '127.0.0.1', port: 26333, apiKey: '' };
|
|
48
48
|
|
|
49
|
-
env.APP_PORT = String(opts.appPort ||
|
|
49
|
+
env.APP_PORT = String(opts.appPort || 7777);
|
|
50
50
|
env.SECRET_KEY = env.SECRET_KEY || randomHex(32);
|
|
51
|
+
env.SYSTEM_CONFIG_MASTER_KEY = env.SYSTEM_CONFIG_MASTER_KEY || randomHex(32);
|
|
51
52
|
env.DATABASE_URL = `mysql://${encodeURIComponent(opts.mysql.user)}:${encodeURIComponent(opts.mysql.password)}@${dockerHost(opts.mysql.host)}:${opts.mysql.port}/${encodeURIComponent(database)}`;
|
|
52
|
-
env.
|
|
53
|
-
|
|
54
|
-
env.
|
|
55
|
-
env.
|
|
56
|
-
env.QDRANT_URL = `http://${dockerHost(qdrant.host)}:${qdrant.port}`;
|
|
57
|
-
env.QDRANT_API_KEY = qdrant.apiKey || '';
|
|
53
|
+
env.REDIS_URL = 'redis://redis:6379/0';
|
|
54
|
+
for (const key of ['REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD', 'REDIS_KEY_PREFIX']) delete env[key];
|
|
55
|
+
env.QDRANT_URL = qdrant ? `http://${dockerHost(qdrant.host)}:${qdrant.port}` : '';
|
|
56
|
+
env.QDRANT_API_KEY = qdrant ? (qdrant.apiKey || '') : '';
|
|
58
57
|
env.QDRANT_COLLECTION_PREFIX = database;
|
|
59
58
|
|
|
60
59
|
const yaml = [
|
|
61
|
-
'# Generated by draftgo-cli.
|
|
60
|
+
'# Generated by draftgo-cli. MySQL and Qdrant are shared; Redis is project-isolated.',
|
|
62
61
|
`name: ${projectName}`,
|
|
63
62
|
'services:',
|
|
64
63
|
' app:',
|
|
65
|
-
|
|
64
|
+
` image: ${opts.image || 'cabinai/draftgo:latest'}`,
|
|
66
65
|
` container_name: ${projectName}`,
|
|
67
66
|
' restart: unless-stopped',
|
|
68
67
|
' ports:',
|
|
69
68
|
' - "${APP_PORT}:3000"',
|
|
70
69
|
' environment:',
|
|
70
|
+
' APP_ENV: production',
|
|
71
71
|
' APP_HOST: 0.0.0.0',
|
|
72
72
|
' APP_PORT: "3000"',
|
|
73
73
|
' PUBLIC_BASE_URL: http://localhost:${APP_PORT}',
|
|
74
74
|
' DATABASE_URL: ${DATABASE_URL}',
|
|
75
75
|
' SECRET_KEY: ${SECRET_KEY}',
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
' REDIS_PASSWORD: ${REDIS_PASSWORD}',
|
|
79
|
-
' REDIS_DB: "0"',
|
|
80
|
-
' REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX}',
|
|
76
|
+
' SYSTEM_CONFIG_MASTER_KEY: ${SYSTEM_CONFIG_MASTER_KEY}',
|
|
77
|
+
' REDIS_URL: ${REDIS_URL}',
|
|
81
78
|
' QDRANT_URL: ${QDRANT_URL}',
|
|
82
79
|
' QDRANT_API_KEY: ${QDRANT_API_KEY}',
|
|
83
80
|
' QDRANT_COLLECTION_PREFIX: ${QDRANT_COLLECTION_PREFIX}',
|
|
84
|
-
'
|
|
85
|
-
' KB_STORAGE_DIR: /app/data/storage/kb',
|
|
86
|
-
' DRAFTGO_GO_SERVICE_CACHE_DIR: /app/data/storage/custom-services/cache',
|
|
87
|
-
' MCP_TEMP_DIR: /app/data/storage/mcp/tmp',
|
|
81
|
+
' FILE_STORAGE_ROOT: /app/data/files',
|
|
88
82
|
' volumes:',
|
|
89
|
-
' -
|
|
83
|
+
' - app_data:/app/data',
|
|
84
|
+
' healthcheck:',
|
|
85
|
+
' test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:3000/api/system/health"]',
|
|
86
|
+
' interval: 10s',
|
|
87
|
+
' timeout: 5s',
|
|
88
|
+
' start_period: 10s',
|
|
89
|
+
' retries: 6',
|
|
90
|
+
' depends_on:',
|
|
91
|
+
' redis:',
|
|
92
|
+
' condition: service_healthy',
|
|
90
93
|
' extra_hosts:',
|
|
91
94
|
' - "host.docker.internal:host-gateway"',
|
|
95
|
+
' redis:',
|
|
96
|
+
' image: redis:7.4-alpine',
|
|
97
|
+
' restart: unless-stopped',
|
|
98
|
+
' command: ["redis-server", "--appendonly", "yes"]',
|
|
99
|
+
' volumes:',
|
|
100
|
+
' - redis_data:/data',
|
|
101
|
+
' healthcheck:',
|
|
102
|
+
' test: ["CMD", "redis-cli", "ping"]',
|
|
103
|
+
' interval: 5s',
|
|
104
|
+
' timeout: 5s',
|
|
105
|
+
' retries: 20',
|
|
106
|
+
'volumes:',
|
|
107
|
+
' app_data:',
|
|
108
|
+
' redis_data:',
|
|
92
109
|
'',
|
|
93
110
|
].join('\n');
|
|
94
111
|
writeText(composePath(projectDir), yaml);
|
|
95
112
|
writeEnv(projectDir, env);
|
|
96
|
-
for (const subdir of ['data/storage', 'data/storage/uploads', 'data/storage/kb', 'data/storage/mcp/tmp', 'data/storage/custom-services/cache']) {
|
|
97
|
-
try { fs.mkdirSync(path.join(root, subdir), { recursive: true }); } catch {}
|
|
98
|
-
}
|
|
99
113
|
return { dir: root, composeFile: composePath(projectDir), envFile: envPath(projectDir), projectName, env };
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
module.exports = {
|
|
103
117
|
stackDir, composePath, envPath,
|
|
104
|
-
generate, sanitizeProjectName,
|
|
118
|
+
generate, sanitizeProjectName, randomHex,
|
|
105
119
|
};
|
|
@@ -31,9 +31,8 @@ function probeHttpOnce(url, timeoutMs = 2000) {
|
|
|
31
31
|
return new Promise((resolve) => {
|
|
32
32
|
try {
|
|
33
33
|
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
34
|
-
|
|
35
|
-
res.
|
|
36
|
-
resolve(true);
|
|
34
|
+
res.resume();
|
|
35
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
37
36
|
});
|
|
38
37
|
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
39
38
|
req.on('error', () => resolve(false));
|
|
@@ -61,9 +60,10 @@ function tryCmd(cmd, args) {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function detectDocker() {
|
|
65
|
-
const hasDocker = tryCmd('docker', ['version'
|
|
66
|
-
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
63
|
+
function detectDocker() {
|
|
64
|
+
const hasDocker = tryCmd('docker', ['--version']);
|
|
65
|
+
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
66
|
+
if (!tryCmd('docker', ['info', '--format', '{{.ServerVersion}}'])) return { ok: false, reason: 'docker-not-running' };
|
|
67
67
|
// Prefer `docker compose` (plugin); fall back to legacy `docker-compose`.
|
|
68
68
|
if (tryCmd('docker', ['compose', 'version'])) {
|
|
69
69
|
return { ok: true, composeCmd: 'docker', composeArgs: ['compose'] };
|