draftgo-cli 4.0.1 → 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.
Files changed (78) hide show
  1. package/README.md +87 -11
  2. package/package.json +9 -4
  3. package/resources/custom-service-sdk/ai.go +520 -0
  4. package/resources/custom-service-sdk/ai_test.go +156 -0
  5. package/resources/custom-service-sdk/billing.go +596 -0
  6. package/resources/custom-service-sdk/billing_test.go +150 -0
  7. package/resources/custom-service-sdk/go.mod +3 -0
  8. package/resources/custom-service-sdk/manifest.json +72 -0
  9. package/resources/custom-service-sdk/platform.go +360 -0
  10. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  11. package/resources/custom-service-sdk/registration_test.go +39 -0
  12. package/resources/custom-service-sdk/resources.go +246 -0
  13. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  14. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  15. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  16. package/resources/custom-service-sdk/sdk.go +208 -0
  17. package/resources/skill/SKILL.md +36 -88
  18. package/resources/skill/init/SKILL.md +4 -4
  19. package/resources/skill/manifest.json +5 -1
  20. package/resources/skill/references/aihub.md +25 -2
  21. package/resources/skill/references/app-api.md +56 -6
  22. package/resources/skill/references/architecture.md +2 -2
  23. package/resources/skill/references/chat-sdk.md +4 -2
  24. package/resources/skill/references/checkout.md +17 -3
  25. package/resources/skill/references/custom-services.md +111 -46
  26. package/resources/skill/references/data.md +19 -4
  27. package/resources/skill/references/delivery.md +33 -0
  28. package/resources/skill/references/diagnostics.md +51 -0
  29. package/resources/skill/references/frontend.md +34 -46
  30. package/resources/skill/references/mcp.md +33 -5
  31. package/resources/skill/references/methods.md +189 -0
  32. package/resources/skill/references/modules.md +36 -8
  33. package/resources/skill/references/runtime.md +23 -1
  34. package/src/cli.js +24 -0
  35. package/src/commandRegistry.js +9 -1
  36. package/src/commands/api.js +21 -10
  37. package/src/commands/apiKey.js +34 -0
  38. package/src/commands/capabilities.js +93 -0
  39. package/src/commands/checkout.js +1 -1
  40. package/src/commands/commit.js +1 -1
  41. package/src/commands/components.js +550 -0
  42. package/src/commands/conflict.js +1 -1
  43. package/src/commands/connect.js +18 -8
  44. package/src/commands/customService.js +20 -4
  45. package/src/commands/dataRange.js +33 -0
  46. package/src/commands/delete.js +12 -1
  47. package/src/commands/diff.js +18 -2
  48. package/src/commands/grant.js +29 -0
  49. package/src/commands/group.js +38 -0
  50. package/src/commands/help.js +64 -20
  51. package/src/commands/init.js +3 -3
  52. package/src/commands/map.js +145 -17
  53. package/src/commands/mcp.js +2 -2
  54. package/src/commands/reconcile.js +1 -1
  55. package/src/commands/role.js +32 -0
  56. package/src/commands/space.js +41 -0
  57. package/src/commands/status.js +110 -7
  58. package/src/commands/update.js +23 -11
  59. package/src/commands/verify.js +75 -0
  60. package/src/commands/worklog.js +6 -2
  61. package/src/consoleEncoding.js +34 -0
  62. package/src/contractCompatibility.js +57 -0
  63. package/src/customServices.js +138 -18
  64. package/src/diffReport.js +106 -0
  65. package/src/index.js +2 -0
  66. package/src/localRuntime/compose.js +14 -17
  67. package/src/localRuntime/index.js +22 -23
  68. package/src/localRuntime/services.js +27 -36
  69. package/src/mcp/client.js +11 -2
  70. package/src/mcp/protocol.js +2 -2
  71. package/src/mcp/tools.js +14 -1
  72. package/src/platforms.js +9 -0
  73. package/src/projectConfig.js +6 -4
  74. package/src/releaseInstall.js +105 -0
  75. package/src/updateCheck.js +48 -28
  76. package/src/worklog.js +2 -1
  77. package/src/worktree/backend.js +1 -1
  78. package/src/worktree/index.js +7 -2
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const path = require('path');
4
5
  const log = require('../logger');
5
6
  const services = require('../customServices');
7
+ const { report, formatSummary, formatStat } = require('../diffReport');
6
8
 
7
9
  function ids(positional) { return positional.map(String).filter(Boolean); }
8
10
  function isServiceType(value) {
@@ -28,9 +30,10 @@ async function checkout(projectDir, positional, flags = {}) {
28
30
  async function commit(projectDir, positional, flags = {}) {
29
31
  const values = ids(positional);
30
32
  if (!values.length) { log.err('Usage: draftgo commit custom-services <id...>'); return 1; }
31
- const result = await services.commit(projectDir, values);
33
+ // The user-facing custom-service commit is a complete delivery operation.
34
+ const result = await services.publish(projectDir, values);
32
35
  if (flags.output === 'json') output(flags, result);
33
- else result.forEach((item) => log.ok(`custom service ${item.resource_id}: ${item.status}`));
36
+ else result.forEach((item) => log.ok(`custom service ${item.id || item.resource_id}: published (commit + validate + publish)`));
34
37
  return 0;
35
38
  }
36
39
 
@@ -38,7 +41,18 @@ async function diff(projectDir, positional, flags = {}) {
38
41
  const id = positional[0];
39
42
  if (!id) { log.err('Usage: draftgo diff custom-services <id>'); return 1; }
40
43
  const result = services.diff(projectDir, id);
41
- if (flags.output === 'json') output(flags, result);
44
+ const concise = Boolean(flags.stat || flags.summary);
45
+ const value = concise ? report(result.entry, result.file_outputs.map((file) => ({
46
+ path: file.filename,
47
+ changed: file.changed,
48
+ output: file.output,
49
+ base_path: path.join(result.entry.base_dir, file.filename),
50
+ local_path: path.join(result.entry.local_dir, file.filename),
51
+ }))) : null;
52
+ if (flags.output === 'json' && concise) output(flags, value);
53
+ else if (flags.output === 'json') output(flags, result);
54
+ else if (flags.stat) process.stdout.write(formatStat(value));
55
+ else if (flags.summary) process.stdout.write(formatSummary(value));
42
56
  else if (result.changed) process.stdout.write(result.output);
43
57
  else log.dim(`custom service ${id}: no local changes`);
44
58
  return 0;
@@ -60,8 +74,10 @@ async function test(projectDir, positional, flags = {}) {
60
74
  const readJSON = (value) => value ? JSON.parse(fs.readFileSync(String(value), 'utf8')) : undefined;
61
75
  const policy = String(flags['side-effect-policy'] || 'deny').toLowerCase();
62
76
  if (!['deny', 'mock', 'live'].includes(policy)) throw new Error('--side-effect-policy must be deny, mock, or live.');
77
+ const source = String(flags.source || 'draft').toLowerCase();
78
+ if (!['auto', 'draft', 'published'].includes(source)) throw new Error('--source must be auto, draft, or published.');
63
79
  const result = await services.test(projectDir, id, input, { handler: flags.handler, headers: readJSON(flags.headers), user: readJSON(flags.user),
64
- testWrite: Boolean(flags['test-write']), sideEffectPolicy: policy });
80
+ testWrite: Boolean(flags['test-write']), sideEffectPolicy: policy, source });
65
81
  if (flags.output === 'json') output(flags, result);
66
82
  else log[result.status === 'success' ? 'ok' : 'err'](`custom service ${id}: ${result.status}`);
67
83
  return result.status === 'success' ? 0 : 1;
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { callOperation } = require('./api');
5
+
6
+ // DataRange is represented by db_meta record policies; it is not a second
7
+ // authorization scope or a free-form client-side filter.
8
+ const OPERATIONS = Object.freeze({
9
+ list: 'list_meta_api_db_meta_get',
10
+ create: 'create_meta_api_db_meta_post',
11
+ get: 'get_meta_api_db_meta__type__get',
12
+ update: 'update_meta_api_db_meta__id__put',
13
+ batch: 'update_meta_batch_api_db_meta_batch_patch',
14
+ delete: 'delete_meta_api_db_meta__id__delete',
15
+ });
16
+
17
+ function operationKey(value) {
18
+ const action = String(value || 'list').trim().toLowerCase();
19
+ return OPERATIONS[action] ? action : '';
20
+ }
21
+
22
+ async function dataRangeCommand(projectDir, positional, flags = {}) {
23
+ const action = operationKey(positional[0]);
24
+ if (!action) {
25
+ log.err('Usage: draftgo data-range list|create|get|update|batch|delete --input <json-file>');
26
+ return 1;
27
+ }
28
+ return callOperation(projectDir, OPERATIONS[action], flags);
29
+ }
30
+
31
+ module.exports = dataRangeCommand;
32
+ module.exports.OPERATIONS = OPERATIONS;
33
+ module.exports.operationKey = operationKey;
@@ -7,6 +7,12 @@ const { confirm } = require('../prompt');
7
7
  const { loadProjectConfig } = require('../projectConfig');
8
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
9
  const { cachedDescription, normalizeDescription, storeDescription } = require('../apiContractCache');
10
+ const {
11
+ isContractCompatibilityError,
12
+ isHardContractIncompatibility,
13
+ sameOperationContract,
14
+ incompatibleOperationError,
15
+ } = require('../contractCompatibility');
10
16
 
11
17
  function readParams(projectDir, flags) {
12
18
  const raw = flags.params ? String(flags.params)
@@ -65,10 +71,15 @@ async function deleteResource(projectDir, positional, flags = {}) {
65
71
  try {
66
72
  result = await invoke();
67
73
  } catch (error) {
68
- if (error.code !== 'CONTRACT_CHANGED') throw error;
74
+ if (!isContractCompatibilityError(error)) throw error;
75
+ if (isHardContractIncompatibility(error)) throw incompatibleOperationError(selectedOperation, error);
76
+ const previousContract = contract;
69
77
  const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: selectedOperation });
70
78
  contract = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
71
79
  if (!contract) throw new Error('DraftGo API description is missing contract revision metadata.');
80
+ if (!sameOperationContract(previousContract, contract)) {
81
+ throw incompatibleOperationError(selectedOperation, error);
82
+ }
72
83
  result = await invoke();
73
84
  }
74
85
  if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
@@ -2,19 +2,35 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const { diffResource } = require('../worktree');
5
+ const { report, formatSummary, formatStat } = require('../diffReport');
6
+ const { absolutePath } = require('../worktree/manifest');
5
7
 
6
8
  function diff(projectDir, positional, flags = {}) {
7
9
  const [resourceType, resourceId] = positional;
8
10
  if (!resourceType || !resourceId) {
9
- log.err('Usage: draftgo diff <pages|nav|docs> <id>');
11
+ log.err('Usage: draftgo diff <pages|nav|docs|custom-services> <id>');
10
12
  return 1;
11
13
  }
12
14
  if (require('./customService').isServiceType(resourceType)) {
13
15
  return require('./customService').diff(projectDir, [resourceId], flags);
14
16
  }
15
17
  const result = diffResource(projectDir, resourceType, resourceId);
16
- if (flags.output === 'json') {
18
+ const concise = Boolean(flags.stat || flags.summary);
19
+ const value = concise ? report(result.entry, [{
20
+ path: result.entry.local_path,
21
+ changed: result.changed,
22
+ output: result.output,
23
+ base_path: absolutePath(projectDir, result.entry.base_path),
24
+ local_path: absolutePath(projectDir, result.entry.local_path),
25
+ }]) : null;
26
+ if (flags.output === 'json' && concise) {
27
+ console.log(JSON.stringify(value, null, 2));
28
+ } else if (flags.output === 'json') {
17
29
  console.log(JSON.stringify({ changed: result.changed, entry: result.entry, diff: result.output }, null, 2));
30
+ } else if (flags.stat) {
31
+ process.stdout.write(formatStat(value));
32
+ } else if (flags.summary) {
33
+ process.stdout.write(formatSummary(value));
18
34
  } else if (result.changed) {
19
35
  process.stdout.write(result.output);
20
36
  } else {
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { callOperation } = require('./api');
5
+
6
+ const OPERATIONS = Object.freeze({
7
+ list: 'listAccessGrants',
8
+ create: 'createAccessGrant',
9
+ get: 'getAccessGrant',
10
+ revoke: 'revokeAccessGrant',
11
+ });
12
+
13
+ function operationKey(value) {
14
+ const action = String(value || 'list').trim().toLowerCase();
15
+ return OPERATIONS[action] ? action : '';
16
+ }
17
+
18
+ async function grantCommand(projectDir, positional, flags = {}) {
19
+ const action = operationKey(positional[0]);
20
+ if (!action) {
21
+ log.err('Usage: draftgo grant list|create|get|revoke [--input <json-file>]');
22
+ return 1;
23
+ }
24
+ return callOperation(projectDir, OPERATIONS[action], flags);
25
+ }
26
+
27
+ module.exports = grantCommand;
28
+ module.exports.OPERATIONS = OPERATIONS;
29
+ module.exports.operationKey = operationKey;
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { callOperation } = require('./api');
5
+
6
+ const OPERATIONS = Object.freeze({
7
+ list: 'listUserGroups',
8
+ create: 'createUserGroup',
9
+ get: 'getUserGroup',
10
+ update: 'updateUserGroup',
11
+ replace: 'replaceUserGroup',
12
+ remove: 'removeUserGroup',
13
+ 'members.list': 'listUserGroupMembers',
14
+ 'members.add': 'addUserGroupMember',
15
+ 'members.remove': 'removeUserGroupMember',
16
+ });
17
+
18
+ function operationKey(parts) {
19
+ const values = parts.map((part) => String(part || '').trim().toLowerCase()).filter(Boolean);
20
+ if (values.length === 1) return values[0] === 'delete' ? 'remove' : values[0];
21
+ if (values.length === 2 && ['member', 'members'].includes(values[0])) {
22
+ return `members.${values[1] === 'delete' ? 'remove' : values[1]}`;
23
+ }
24
+ return '';
25
+ }
26
+
27
+ async function groupCommand(projectDir, positional, flags = {}) {
28
+ const key = operationKey(positional);
29
+ if (!OPERATIONS[key]) {
30
+ log.err('Usage: draftgo group list|create|get|update|replace|remove | members list|add|remove [--input <json-file>]');
31
+ return 1;
32
+ }
33
+ return callOperation(projectDir, OPERATIONS[key], flags);
34
+ }
35
+
36
+ module.exports = groupCommand;
37
+ module.exports.OPERATIONS = OPERATIONS;
38
+ module.exports.operationKey = operationKey;
@@ -13,8 +13,8 @@ function help() {
13
13
 
14
14
  console.log(`draftgo v${getPackageVersion()} - DraftGo workbench CLI for AI coding agents
15
15
 
16
- DraftGo Next frontend baseline: React + Vite.
17
- Database pages may use native web technologies or bundled component libraries.
16
+ DraftGo shell baseline: React + Vite. Database Pages default to native HTML,
17
+ Tailwind CSS 4, and Basecoat UI; Oat UI remains an optional Web Components layer.
18
18
  MCP handles live discovery and structured resources; complete page, navigation,
19
19
  document, and custom-service bodies use checkout/commit outside MCP context.
20
20
  The installed Skill routes agents to task-specific References and MCP tools.
@@ -26,9 +26,9 @@ Usage:
26
26
  refresh installed or detected Skills.
27
27
  draftgo uninstall <target|all> Remove Skill files. --purge also removes
28
28
  the entire .draftgo/ runtime directory.
29
- draftgo status Show installed targets and Skill version.
29
+ draftgo status Show connection health, current scope, targets, and Skill version.
30
30
 
31
- draftgo connect [<target>...] Verify and save server/SAT, then configure
31
+ draftgo connect [<target>...] Verify and save server/API Key, then configure
32
32
  a detected or explicit MCP host. This never
33
33
  downloads DraftGo business resources.
34
34
  draftgo mcp setup [<target>...] Merge project-level stdio MCP configuration.
@@ -36,24 +36,42 @@ Usage:
36
36
  draftgo mcp test Test project, resource, and API MCP calls.
37
37
  draftgo mcp serve Bridge local stdio to the remote /mcp endpoint.
38
38
 
39
- draftgo map Read remote overview/resources through MCP and
40
- overlay local checkout state.
41
- draftgo checkout <type> <id...> Download pages/nav/docs body + verified base.
39
+ draftgo map [--type <type>] Locate bounded remote resource metadata and
40
+ overlay matching local checkout state.
41
+ Use --route/--title for exact selection.
42
+ draftgo checkout <type> <id...> Download a long-form body + verified base,
43
+ including custom-service worktrees.
42
44
  --force explicitly replaces local changes.
43
45
  draftgo check [custom-services <id...>]
44
46
  Validate all checkouts or selected custom services.
45
47
  --remote also compares remote hash/version.
46
48
  draftgo verify [<type> <id...>] Run the default delivery verification.
47
49
  Browser options are explicit and optional.
48
- draftgo diff <type> <id> Show checkout base versus local body.
49
- draftgo commit <type> <id...> Validate and upload complete checked-out bodies.
50
+ draftgo diff <type> <id> Show checkout base versus local body/files.
51
+ --stat/--summary omit the full patch.
52
+ draftgo commit <type> <id...> Validate and upload complete checked-out bodies/files.
53
+ draftgo components search <query> Search the live component catalog.
54
+ draftgo components show <library/component>
55
+ Show HTML, props, slots, CSS variables and revision.
56
+ draftgo components expand --page <id> --instance <data-dg-instance>
57
+ Expand one component in a checked-out Page worktree.
58
+ draftgo components libraries list|show|create|update|delete
59
+ Manage component libraries.
60
+ draftgo components list|create|copy|delete
61
+ Manage component catalog entries.
62
+ draftgo components checkout|diff|verify|commit|publish <library/component>
63
+ Develop a component draft locally, then publish explicitly.
64
+ draftgo components import <zip> [--dry-run]
65
+ draftgo components export <library> [--file <zip>]
66
+ Transfer standard DraftGo component archives.
50
67
  draftgo refresh <type> <id...> Safely refresh a clean checkout from the cloud.
51
68
  draftgo validate custom-services <id>
52
69
  Build and inspect the current cloud draft.
53
- draftgo test custom-services <id> [--handler <selector>]
70
+ draftgo test custom-services <id> [--source <source>] [--input <file>] [--handler <selector>]
54
71
  Run the cloud draft in the DraftGo Runner.
55
72
  draftgo publish custom-services <id...>
56
- Publish validated drafts atomically.
73
+ Publish current validated drafts; batch errors
74
+ report services already completed.
57
75
  draftgo reconcile <type> <id...> Repair stale metadata only when local equals remote.
58
76
  draftgo conflicts List unresolved conflicts; --all includes resolved.
59
77
  draftgo conflict show <type> <id>
@@ -73,6 +91,18 @@ Usage:
73
91
  Describe one live API operation through MCP.
74
92
  draftgo api call <operation_id> --input <json-file>
75
93
  Call one described operation with UTF-8 JSON.
94
+ draftgo capabilities list|search|show|audit
95
+ Discover and audit the current server's live operation catalog.
96
+ draftgo space <action> Manage spaces and workspace members.
97
+ draftgo group <action> Manage workspace groups and group members.
98
+ draftgo grant list|create|get|revoke
99
+ Manage platform/space AccessGrants.
100
+ draftgo api-key status|create|update|delete|rotate|reset|enable|disable
101
+ Manage the current user's API key through MCP.
102
+ draftgo role permissions|list|create|get|update|delete
103
+ Manage structured permission templates.
104
+ draftgo data-range list|create|get|update|batch|delete
105
+ Manage db_meta record policies (none|owner|all).
76
106
  draftgo delete <operation_id> [id]
77
107
  Confirm and call a live delete operation.
78
108
  draftgo deploy [<type> <id...>] Check, then diff or commit checked-out content.
@@ -96,7 +126,9 @@ Important flags:
96
126
  --server <url> (connect) DraftGo base URL.
97
127
  --mcp-url <url> (connect) Explicit endpoint; standard /mcp derives the
98
128
  base URL; custom paths also need --server.
99
- --token <sat> (connect) SAT for non-interactive use.
129
+ --api-key <key> (connect) DraftGo user API Key for non-interactive use.
130
+ --scope-type space --space-id <id>
131
+ (connect) Save a target space for workspace resources.
100
132
  --timeout <ms> (connect/mcp test) Network timeout.
101
133
  --allow-offline (connect) Save only after explicitly accepting a
102
134
  failed MCP validation.
@@ -107,15 +139,25 @@ Important flags:
107
139
  --connect (init) Continue into DraftGo server connection.
108
140
  --no-setup (init) Install the Skill without either setup flow.
109
141
  --output json Print machine-readable output where supported.
110
- --type <type> (map) pages | nav/navigations | docs/articles.
142
+ --type <type> (map) pages | nav/navigations | docs/articles |
143
+ custom-services.
144
+ --route <path> (map) Select resources with this exact route.
145
+ --title <title> (map) Select resources with this exact title.
146
+ --limit <1-100> (map) Maximum resources returned per type/page;
147
+ defaults to 20.
148
+ --cursor <cursor> (map) Continue one typed map result page.
149
+ --summary (map/diff) Print bounded resource or change summary.
150
+ --stat (diff) Print per-file and total change statistics.
111
151
  --strict Treat check warnings as failures.
112
152
  --remote (check) Compare checkout hashes/versions with remote.
113
153
  --yes Skip supported confirmation prompts.
114
154
  --operation-id <id> (delete) Select an operation explicitly.
115
155
  --params <json> (api call/delete) Pass an API parameter object.
116
- --input <file> (api call/delete) Read a UTF-8 JSON parameter object.
156
+ --input <file> (api call/delete/custom-service test) Read a UTF-8
157
+ JSON object.
117
158
  --handler <selector> (custom-service test) route:METHOD:/path, event:name,
118
159
  scheduled:name, or a handler name.
160
+ --source <source> (custom-service test) draft (default), auto, or published.
119
161
  --headers/--user <file> (custom-service test) Read JSON test identity data.
120
162
  --test-write (custom-service test) Permit declared write testing.
121
163
  --side-effect-policy <mode>
@@ -127,7 +169,7 @@ Important flags:
127
169
  --viewport <mode> (verify) mobile | desktop | both.
128
170
  --frame <mode> (verify) auto | top | all | <iframe-selector>.
129
171
  --token <mode> (verify) auto | never; auto appends the configured
130
- SAT to same-origin URLs as the token query parameter.
172
+ API Key to same-origin URLs as the token query parameter.
131
173
  --screenshot <mode> (verify) always | never; always alone captures a screenshot.
132
174
  --browser <name> (verify) chromium | chrome | msedge.
133
175
  --browser-path <file> (verify) Explicit browser executable; environment
@@ -136,8 +178,8 @@ Important flags:
136
178
  --width/--height <px> (verify) Override the default 1440x900 desktop viewport.
137
179
 
138
180
  Security:
139
- .draftgo/config.json stores the server and SAT and is gitignored. Host MCP
140
- files contain only "draftgo mcp serve"; they never contain the SAT.
181
+ .draftgo/config.json stores the server and API Key and is gitignored. Host MCP
182
+ files contain only "draftgo mcp serve"; they never contain the API Key.
141
183
 
142
184
  MCP targets (project config or status):
143
185
  ${mcpTargets}
@@ -147,13 +189,15 @@ ${targets}
147
189
 
148
190
  Examples:
149
191
  draftgo init codex
150
- draftgo connect codex --server https://draftgo.example --token <sat>
192
+ draftgo connect codex --server https://draftgo.example --api-key <key>
151
193
  draftgo mcp test
152
- draftgo map --output json
194
+ draftgo map --type pages --route /admin/channel-ops --summary --output json
153
195
  draftgo checkout pages 42
154
196
  draftgo verify pages 42
155
- draftgo diff pages 42
197
+ draftgo diff pages 42 --stat
156
198
  draftgo commit pages 42
199
+ draftgo checkout custom-services 12
200
+ draftgo test custom-services 12 --handler route:POST:/health --input request.json
157
201
  draftgo work start "Build document management and role permissions"
158
202
  draftgo work complete 1 --note "Verification and delivery passed"
159
203
  draftgo deploy docs 7 --delivery preview
@@ -80,8 +80,8 @@ async function init(projectDir, positional, flags) {
80
80
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
81
81
 
82
82
  if (!hasConfig && !flagSkip) {
83
- // 非交互场景下,如果同时给了 --server --token,直接走 connect,不再阻塞在 TTY 提示上。
84
- if (flags.server && flags.token && !flagConnect) {
83
+ // Non-interactive setup can connect immediately with a server and API Key.
84
+ if (flags.server && (flags['api-key'] || flags.token) && !flagConnect) {
85
85
  return await require('./connect')(projectDir, [], flags);
86
86
  }
87
87
  if (flagConnect) {
@@ -92,7 +92,7 @@ async function init(projectDir, positional, flags) {
92
92
  log.plain(` ${log.c.cyan('[A]')} 连接基座服务`);
93
93
  log.dim(' 使用已有 DraftGo 服务,接下来输入 BaseURL 和系统访问令牌。');
94
94
  log.plain(` ${log.c.cyan('[B]')} 本地启动基座`);
95
- log.dim(' 使用 Docker 启动 DraftGo;按需准备共享的 MySQL、Redis 和 Milvus。');
95
+ log.dim(' 使用 Docker 启动 DraftGo;按需准备共享的 MySQL、Redis 和 Qdrant。');
96
96
  log.plain(` ${log.c.cyan('[C]')} 稍后自行处理`);
97
97
  log.dim(' 仅安装 Skill,不写入服务配置,也不创建容器。');
98
98
  const choice = (await ask('请选择 [A/B/C]', { default: 'C' })).trim().toUpperCase();
@@ -6,6 +6,7 @@ const { canonicalResourceType } = require('../worktree/types');
6
6
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
7
7
  const { allWithAbort } = require('../mcp/parallel');
8
8
  const { inspectRemoteCheckouts } = require('../worktree/status');
9
+ const { loadManifest } = require('../worktree/manifest');
9
10
  const customServices = require('../customServices');
10
11
 
11
12
  const REMOTE_RESOURCE_TYPES = Object.freeze({
@@ -40,6 +41,11 @@ function claimsMorePages(value) {
40
41
  && (value.has_more === true || value.hasMore === true);
41
42
  }
42
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
+
43
49
  function normalizeMapResourceType(value) {
44
50
  if (require('./customService').isServiceType(value)) return 'custom_services';
45
51
  return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
@@ -51,6 +57,79 @@ function requestedResourceTypes(flags = {}) {
51
57
  : [normalizeMapResourceType(flags.type)];
52
58
  }
53
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
+
54
133
  async function listRemoteResourceType(session, resourceType, options = {}) {
55
134
  const resources = [];
56
135
  const seen = new Set();
@@ -92,19 +171,32 @@ function checkoutChanged(entry) {
92
171
  async function mapCommand(projectDir, flags = {}) {
93
172
  const resourceTypes = requestedResourceTypes(flags);
94
173
  const config = loadProjectConfig(projectDir);
95
- const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList]);
96
- const [remote, checkouts] = await allWithAbort([
97
- (options) => allWithAbort([
98
- (options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
99
- (options) => listRemoteResources(session, resourceTypes, options),
100
- ], options),
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([
101
192
  async (options) => {
102
193
  const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
103
194
  config,
104
195
  client: session.client,
105
196
  tools: session.tools,
106
197
  signal: options.signal,
107
- }), customServices.inspectRemote(projectDir, { config, signal: options.signal })]);
198
+ entries: contentEntries,
199
+ }), customServices.inspectRemote(projectDir, { config, signal: options.signal, ids: serviceIds })]);
108
200
  return [...contentStatus, ...serviceStatus].map((entry) => ({
109
201
  ...entry,
110
202
  exists: entry.local_hash !== null,
@@ -112,28 +204,58 @@ async function mapCommand(projectDir, flags = {}) {
112
204
  changed: checkoutChanged(entry),
113
205
  }));
114
206
  },
115
- ]);
116
- const [overview, resources] = remote;
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
+ }
117
219
  const result = {
118
220
  server: config.server,
119
- overview,
120
221
  resources,
121
222
  checkouts,
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
+ },
122
242
  };
123
243
 
124
244
  if (flags.output === 'json') {
125
- console.log(JSON.stringify(result, null, 2));
245
+ console.log(JSON.stringify(flags.summary ? summary : result, null, 2));
126
246
  return 0;
127
247
  }
128
248
  log.title('draftgo map');
129
- log.info(`Remote resources: ${resources.length}`);
130
- const counts = new Map();
131
- for (const resource of resources) {
132
- const type = resource.resource_type || resource.type || 'unknown';
133
- counts.set(type, (counts.get(type) || 0) + 1);
134
- }
249
+ log.info(`Remote resources returned: ${resources.length}`);
135
250
  for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
136
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
+ }
137
259
  for (const entry of checkouts) {
138
260
  const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
139
261
  log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
@@ -149,4 +271,10 @@ module.exports.itemsFrom = itemsFrom;
149
271
  module.exports.normalizeMapResourceType = normalizeMapResourceType;
150
272
  module.exports.requestedResourceTypes = requestedResourceTypes;
151
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;
152
280
  module.exports.checkoutChanged = checkoutChanged;
@@ -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 'SAT authentication or authorization failed.';
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}; SAT present)`);
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;