draftgo-cli 3.0.48 → 3.0.51

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.
@@ -18,9 +18,11 @@ async function conflict(projectDir, positional, flags = {}) {
18
18
  log.err('Usage: draftgo conflict <show|resolve> <pages|nav|docs> <id>');
19
19
  return 1;
20
20
  }
21
- const record = action === 'show'
22
- ? showConflict(projectDir, resourceType, resourceId)
23
- : await resolveConflict(projectDir, resourceType, resourceId);
21
+ const custom = require('./customService').isServiceType(resourceType);
22
+ const services = custom ? require('../customServices') : null;
23
+ const record = custom
24
+ ? (action === 'show' ? services.showConflict(projectDir, resourceId) : services.resolveConflict(projectDir, resourceId))
25
+ : (action === 'show' ? showConflict(projectDir, resourceType, resourceId) : await resolveConflict(projectDir, resourceType, resourceId));
24
26
  if (flags.output === 'json') console.log(JSON.stringify(record, null, 2));
25
27
  else printRecord(record);
26
28
  if (action === 'resolve') log.info('The remote revision is now the base; run check, diff, and commit the merged worktree file.');
@@ -2,9 +2,10 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const { listConflicts } = require('../worktree');
5
+ const { serviceConflicts } = require('../customServices');
5
6
 
6
7
  function conflicts(projectDir, _positional, flags = {}) {
7
- const records = listConflicts(projectDir, { all: Boolean(flags.all) });
8
+ const records = [...listConflicts(projectDir, { all: Boolean(flags.all) }), ...serviceConflicts(projectDir, { all: Boolean(flags.all) })];
8
9
  if (flags.output === 'json') console.log(JSON.stringify(records, null, 2));
9
10
  else if (!records.length) log.ok('No unresolved DraftGo conflicts.');
10
11
  else for (const record of records) {
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const log = require('../logger');
4
- const { collectContext, TASK_PROFILES } = require('../context');
4
+ const { collectContext, compactContext, TASK_PROFILES } = require('../context');
5
5
  const { parseTimeout } = require('../timeout');
6
6
 
7
7
  function printUsage() {
@@ -17,7 +17,7 @@ async function contextCommand(projectDir, positional = [], flags = {}) {
17
17
  const result = await collectContext(projectDir, flags.task, {
18
18
  timeoutMs: parseTimeout(flags.timeout),
19
19
  });
20
- console.log(JSON.stringify(result, null, 2));
20
+ console.log(JSON.stringify(flags.compact ? compactContext(result) : result, null, 2));
21
21
  return 0;
22
22
  }
23
23
 
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const log = require('../logger');
5
+ const services = require('../customServices');
6
+
7
+ function ids(positional) { return positional.map(String).filter(Boolean); }
8
+ const TYPE_ALIASES = new Set(['custom-services', 'custom-service', 'custom_services', 'custom_service', 'services', 'scripts', 'custom_scripts']);
9
+ function isServiceType(value) {
10
+ return TYPE_ALIASES.has(String(value || '').toLowerCase());
11
+ }
12
+ function serviceIds(positional) {
13
+ const values = ids(positional);
14
+ if (isServiceType(values[0])) values.shift();
15
+ return values;
16
+ }
17
+ function output(flags, value) {
18
+ if (flags.output === 'json') console.log(JSON.stringify(value, null, 2));
19
+ }
20
+
21
+ async function checkout(projectDir, positional, flags = {}) {
22
+ const values = ids(positional);
23
+ if (!values.length) { log.err('Usage: draftgo checkout custom-services <id...>'); return 1; }
24
+ const result = await services.checkout(projectDir, values, { force: Boolean(flags.force) });
25
+ if (flags.output === 'json') output(flags, result);
26
+ else result.forEach((item) => log.ok(`custom service ${item.resource_id} -> ${item.local_path}`));
27
+ return 0;
28
+ }
29
+
30
+ async function commit(projectDir, positional, flags = {}) {
31
+ const values = ids(positional);
32
+ if (!values.length) { log.err('Usage: draftgo commit custom-services <id...>'); return 1; }
33
+ const result = await services.commit(projectDir, values);
34
+ if (flags.output === 'json') output(flags, result);
35
+ else result.forEach((item) => log.ok(`custom service ${item.resource_id}: ${item.status}`));
36
+ return 0;
37
+ }
38
+
39
+ async function diff(projectDir, positional, flags = {}) {
40
+ const id = positional[0];
41
+ if (!id) { log.err('Usage: draftgo diff custom-services <id>'); return 1; }
42
+ const result = services.diff(projectDir, id);
43
+ if (flags.output === 'json') output(flags, result);
44
+ else if (result.changed) process.stdout.write(result.output);
45
+ else log.dim(`custom service ${id}: no local changes`);
46
+ return 0;
47
+ }
48
+
49
+ async function validate(projectDir, positional, flags = {}) {
50
+ const id = serviceIds(positional)[0];
51
+ if (!id) { log.err('Usage: draftgo validate custom-services <id>'); return 1; }
52
+ const result = await services.validate(projectDir, id);
53
+ if (flags.output === 'json') output(flags, result); else log.ok(`custom service ${id}: ${result.validation_status}`);
54
+ return result.validation_status === 'passed' ? 0 : 1;
55
+ }
56
+
57
+ async function test(projectDir, positional, flags = {}) {
58
+ const id = serviceIds(positional)[0];
59
+ if (!id) { log.err('Usage: draftgo test custom-services <id> [--input <json-file>]'); return 1; }
60
+ let input = {};
61
+ if (flags.input) input = JSON.parse(fs.readFileSync(String(flags.input), 'utf8'));
62
+ const readJSON = (value) => value ? JSON.parse(fs.readFileSync(String(value), 'utf8')) : undefined;
63
+ const policy = String(flags['side-effect-policy'] || 'deny').toLowerCase();
64
+ if (!['deny', 'mock', 'live'].includes(policy)) throw new Error('--side-effect-policy must be deny, mock, or live.');
65
+ const result = await services.test(projectDir, id, input, { handler: flags.handler, headers: readJSON(flags.headers), user: readJSON(flags.user),
66
+ testWrite: Boolean(flags['test-write']), sideEffectPolicy: policy });
67
+ if (flags.output === 'json') output(flags, result);
68
+ else log[result.status === 'success' ? 'ok' : 'err'](`custom service ${id}: ${result.status}`);
69
+ return result.status === 'success' ? 0 : 1;
70
+ }
71
+
72
+ async function publish(projectDir, positional, flags = {}) {
73
+ const values = serviceIds(positional);
74
+ if (!values.length) { log.err('Usage: draftgo publish custom-services <id...>'); return 1; }
75
+ const result = await services.publish(projectDir, values);
76
+ if (flags.output === 'json') output(flags, result);
77
+ else values.forEach((id) => log.ok(`custom service ${id}: published`));
78
+ return 0;
79
+ }
80
+
81
+ module.exports = { checkout, commit, diff, validate, test, publish, serviceIds, isServiceType };
@@ -11,7 +11,7 @@ async function deploy(projectDir, positional, flags = {}) {
11
11
  }
12
12
 
13
13
  log.step('deploy: validating checked-out content...');
14
- const checkCode = check(projectDir, { strict: flags.strict });
14
+ const checkCode = await check(projectDir, { strict: flags.strict });
15
15
  if (checkCode !== 0) {
16
16
  log.err('Deploy stopped because local validation failed.');
17
17
  return checkCode;
@@ -9,6 +9,9 @@ function diff(projectDir, positional, flags = {}) {
9
9
  log.err('Usage: draftgo diff <pages|nav|docs> <id>');
10
10
  return 1;
11
11
  }
12
+ if (require('./customService').isServiceType(resourceType)) {
13
+ return require('./customService').diff(projectDir, [resourceId], flags);
14
+ }
12
15
  const result = diffResource(projectDir, resourceType, resourceId);
13
16
  if (flags.output === 'json') {
14
17
  console.log(JSON.stringify({ changed: result.changed, entry: result.entry, diff: result.output }, null, 2));
@@ -16,7 +16,7 @@ function help() {
16
16
  DraftGo Next frontend baseline: React + Vite.
17
17
  Database pages may use native web technologies or bundled component libraries.
18
18
  MCP handles live discovery and structured resources; complete page, navigation,
19
- and document bodies use checkout/commit outside MCP context.
19
+ document, and custom-service bodies use checkout/commit outside MCP context.
20
20
  The context command combines exact local Reference sections with live MCP data;
21
21
  it is a CLI orchestration command, not an MCP tool.
22
22
 
@@ -44,9 +44,19 @@ Usage:
44
44
  overlay local checkout state.
45
45
  draftgo checkout <type> <id...> Download pages/nav/docs body + verified base.
46
46
  --force explicitly replaces local changes.
47
- draftgo check Validate checked-out worktree/base files only.
47
+ draftgo check [custom-services <id...>]
48
+ Validate all checkouts or selected custom services.
49
+ --remote also compares remote hash/version.
48
50
  draftgo diff <type> <id> Show checkout base versus local body.
49
51
  draftgo commit <type> <id...> Validate and upload complete checked-out bodies.
52
+ draftgo refresh <type> <id...> Safely refresh a clean checkout from the cloud.
53
+ draftgo validate custom-services <id>
54
+ Build and inspect the current cloud draft.
55
+ draftgo test custom-services <id> [--handler <selector>]
56
+ Run the cloud draft in the DraftGo Runner.
57
+ draftgo publish custom-services <id...>
58
+ Publish validated drafts atomically.
59
+ draftgo reconcile <type> <id...> Repair stale metadata only when local equals remote.
50
60
  draftgo conflicts List unresolved conflicts; --all includes resolved.
51
61
  draftgo conflict show <type> <id>
52
62
  Show preserved base/local/remote paths.
@@ -54,10 +64,18 @@ Usage:
54
64
  Mark a merged worktree file ready against the
55
65
  preserved remote base; then check/diff/commit.
56
66
  draftgo verify-ui <url> Run a deterministic browser smoke check.
67
+ Screenshots default to never.
68
+ draftgo clean [--dry-run|--yes] Plan or remove registered tmp/artifacts only.
69
+ draftgo task <operation> [...] Track sustained work in one authoritative Task.md.
70
+ Operations: create, list, show, add, claim,
71
+ start, complete, block, reopen, accept, finish,
72
+ and explicit legacy migrate.
57
73
 
58
74
  draftgo api <query> Search the live API contract through MCP.
59
75
  draftgo api describe <operation_id>
60
76
  Describe one live API operation through MCP.
77
+ draftgo api call <operation_id> --input <json-file>
78
+ Call one described operation with UTF-8 JSON.
61
79
  draftgo delete <operation_id> [id]
62
80
  Confirm and call a live delete operation.
63
81
  Legacy <type> <id> works only when unambiguous.
@@ -82,7 +100,7 @@ Usage:
82
100
  draftgo -h | --help Show this help.
83
101
 
84
102
  Resource types:
85
- pages | nav/navigations | docs/articles
103
+ pages | nav/navigations | docs/articles | custom-services
86
104
 
87
105
  Important flags:
88
106
  --project <dir> Operate on <dir> instead of the current directory.
@@ -106,17 +124,27 @@ Important flags:
106
124
  content | project.
107
125
  --type <type> (map) pages | nav/navigations | docs/articles.
108
126
  --strict Treat check warnings as failures.
127
+ --remote (check) Compare checkout hashes/versions with remote.
109
128
  --yes Skip supported confirmation prompts.
110
129
  --operation-id <id> (delete) Select an operation explicitly.
111
- --params <json> (delete) Pass an API parameter object.
112
- --input <file> (delete) Read the parameter object from JSON.
130
+ --params <json> (api call/delete) Pass an API parameter object.
131
+ --input <file> (api call/delete) Read a UTF-8 JSON parameter object.
132
+ --handler <selector> (custom-service test) route:METHOD:/path, event:name,
133
+ scheduled:name, or a handler name.
134
+ --headers/--user <file> (custom-service test) Read JSON test identity data.
135
+ --test-write (custom-service test) Permit declared write testing.
136
+ --side-effect-policy <mode>
137
+ (custom-service test) deny | mock | live; default deny.
113
138
  --delivery <mode> (deploy) local | preview | deploy.
114
139
  --dry-run (push) Show diffs without committing.
115
140
  --mobile-check <mode> (verify-ui) auto | always | never.
116
141
  --token <mode> (verify-ui) auto | never; auto appends the configured
117
142
  SAT to same-origin URLs as the token query parameter.
118
- --screenshot <mode> (verify-ui) on-failure | always | never.
143
+ --screenshot <mode> (verify-ui) on-failure | always | never; default never.
119
144
  --browser <name> (verify-ui) chromium | chrome | msedge.
145
+ --browser-path <file> (verify-ui) Explicit browser executable; environment
146
+ fallback: DRAFTGO_BROWSER_PATH.
147
+ --resource <type:id> (verify-ui) Compare local and remote content first.
120
148
  --selector <css> (verify-ui) Require a visible key element.
121
149
  --width/--height <px> (verify-ui) Override the default 390x844 viewport.
122
150
 
@@ -141,6 +169,9 @@ Examples:
141
169
  draftgo diff pages 42
142
170
  draftgo commit pages 42
143
171
  draftgo changelog add "Complete document management and role permissions"
172
+ draftgo task create "Personal growth" --original "Build a Notion-like system" \
173
+ --clarified "Build goals, habits, notes, and reviews" \
174
+ --expected-effect "Users manage personal growth in one workspace"
144
175
  draftgo deploy docs 7 --delivery preview
145
176
  `);
146
177
  }
@@ -4,16 +4,17 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const log = require('../logger');
6
6
  const { loadProjectConfig } = require('../projectConfig');
7
- const { loadManifest, absolutePath } = require('../worktree/manifest');
8
- const { hashFile } = require('../worktree/streams');
9
7
  const { canonicalResourceType } = require('../worktree/types');
10
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
11
9
  const { allWithAbort } = require('../mcp/parallel');
10
+ const { inspectRemoteCheckouts } = require('../worktree/status');
11
+ const customServices = require('../customServices');
12
12
 
13
13
  const REMOTE_RESOURCE_TYPES = Object.freeze({
14
14
  pages: 'pages',
15
15
  navigations: 'navigations',
16
16
  docs: 'docs/articles',
17
+ custom_services: 'custom_services',
17
18
  });
18
19
  const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
19
20
 
@@ -42,6 +43,7 @@ function claimsMorePages(value) {
42
43
  }
43
44
 
44
45
  function normalizeMapResourceType(value) {
46
+ if (require('./customService').isServiceType(value)) return 'custom_services';
45
47
  return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
46
48
  }
47
49
 
@@ -82,28 +84,18 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
82
84
  return groups.flat();
83
85
  }
84
86
 
85
- async function localCheckouts(projectDir) {
86
- const manifest = loadManifest(projectDir);
87
- const entries = [];
88
- for (const entry of Object.values(manifest.entries)) {
89
- const local = absolutePath(projectDir, entry.local_path);
90
- let currentHash = null;
91
- if (fs.existsSync(local)) currentHash = (await hashFile(local)).hash;
92
- entries.push({
93
- ...entry,
94
- exists: currentHash !== null,
95
- current_hash: currentHash,
96
- changed: currentHash !== null && currentHash !== entry.base_hash,
97
- });
98
- }
99
- return entries;
100
- }
101
-
102
87
  function legacyCaches(projectDir) {
103
88
  return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
104
89
  .filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
105
90
  }
106
91
 
92
+ function checkoutChanged(entry) {
93
+ if (entry.resource_type === 'custom_services') {
94
+ return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
95
+ }
96
+ return entry.local_hash !== null && entry.local_hash !== entry.base_hash;
97
+ }
98
+
107
99
  async function mapCommand(projectDir, flags = {}) {
108
100
  const resourceTypes = requestedResourceTypes(flags);
109
101
  const config = loadProjectConfig(projectDir);
@@ -113,7 +105,20 @@ async function mapCommand(projectDir, flags = {}) {
113
105
  (options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
114
106
  (options) => listRemoteResources(session, resourceTypes, options),
115
107
  ], options),
116
- () => localCheckouts(projectDir),
108
+ async (options) => {
109
+ const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
110
+ config,
111
+ client: session.client,
112
+ tools: session.tools,
113
+ signal: options.signal,
114
+ }), customServices.inspectRemote(projectDir, { config, signal: options.signal })]);
115
+ return [...contentStatus, ...serviceStatus].map((entry) => ({
116
+ ...entry,
117
+ exists: entry.local_hash !== null,
118
+ current_hash: entry.local_hash,
119
+ changed: checkoutChanged(entry),
120
+ }));
121
+ },
117
122
  ]);
118
123
  const [overview, resources] = remote;
119
124
  const result = {
@@ -138,8 +143,11 @@ async function mapCommand(projectDir, flags = {}) {
138
143
  for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
139
144
  log.info(`Local checkouts: ${checkouts.length}`);
140
145
  for (const entry of checkouts) {
141
- const state = !entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean';
142
- log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state} (${entry.local_path})`);
146
+ const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
147
+ log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
148
+ + ` local=${entry.local_hash || entry.current_hash || '-'} base=${entry.manifest_hash || entry.base_hash || '-'} remote=${entry.remote_hash || '-'} `
149
+ + `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
150
+ + `(${entry.local_path})`);
143
151
  }
144
152
  if (result.legacy_cache.detected.length) {
145
153
  log.warn(`Ignored legacy cache indexes: ${result.legacy_cache.detected.join(', ')}`);
@@ -152,3 +160,4 @@ module.exports.itemsFrom = itemsFrom;
152
160
  module.exports.normalizeMapResourceType = normalizeMapResourceType;
153
161
  module.exports.requestedResourceTypes = requestedResourceTypes;
154
162
  module.exports.listRemoteResources = listRemoteResources;
163
+ module.exports.checkoutChanged = checkoutChanged;
@@ -13,6 +13,20 @@ const {
13
13
  const { serveStdio } = require('../mcp/stdio');
14
14
  const { parseTimeout } = require('../timeout');
15
15
 
16
+ function classifyMcpFailure(error) {
17
+ const message = String(error && error.message || error || '');
18
+ const code = error && error.code;
19
+ const status = error && error.status;
20
+ if (status === 401 || status === 403) return 'SAT authentication or authorization failed.';
21
+ if (/session|uninitialized|initialize first|not initialized/i.test(message)
22
+ || ['SESSION_EXPIRED', 'MCP_SESSION_EXPIRED', -32002].includes(code)) {
23
+ return 'The MCP session was lost or the server rejected the initialized state; check sticky sessions and service restarts.';
24
+ }
25
+ if (status >= 500) return 'The MCP service returned a server error; inspect the service request ID and logs.';
26
+ if (/tools\/call|tool/i.test(message)) return 'Tool invocation failed after protocol setup; inspect the tool contract and server handler.';
27
+ return 'Connection or MCP protocol negotiation failed.';
28
+ }
29
+
16
30
  function targetArgs(positional, flags) {
17
31
  const values = positional.slice();
18
32
  if (flags.target) values.push(...String(flags.target).split(','));
@@ -87,19 +101,27 @@ function status(projectDir, positional = [], flags = {}) {
87
101
 
88
102
  async function test(projectDir, _positional = [], flags = {}) {
89
103
  let config;
104
+ const stages = [];
90
105
  try {
91
106
  config = loadProjectConfig(projectDir);
92
107
  const result = await testConnection(config, {
93
108
  timeoutMs: parseTimeout(flags.timeout),
109
+ onStage(stage, status) {
110
+ stages.push({ stage, status });
111
+ if (status === 'succeeded') log.ok(`MCP ${stage} succeeded.`);
112
+ else if (status === 'failed') log.err(`MCP ${stage} failed.`);
113
+ else log.step(`MCP ${stage}...`);
114
+ },
94
115
  });
95
- log.ok('DraftGo MCP initialize succeeded.');
96
- log.ok(`tools/list returned ${result.tools.length} tools.`);
97
- log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
116
+ log.ok(`MCP diagnostic completed: ${result.testedCalls.length} tools/call checks passed.`);
98
117
  log.dim(` protocol: ${result.protocolVersion}`);
99
118
  return 0;
100
119
  } catch (error) {
101
120
  const token = config && String(config.token || config.sat || '');
102
121
  log.err(redactText(error && error.message ? error.message : error, [token]));
122
+ const last = stages[stages.length - 1];
123
+ if (last) log.info(`MCP diagnostic stopped at ${last.stage} (${last.status}).`);
124
+ log.info(classifyMcpFailure(error));
103
125
  return 1;
104
126
  }
105
127
  }
@@ -125,3 +147,4 @@ mcp.serve = serve;
125
147
  mcp.printUsage = printUsage;
126
148
 
127
149
  module.exports = mcp;
150
+ module.exports.classifyMcpFailure = classifyMcpFailure;
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { reconcileResources } = require('../worktree');
5
+
6
+ async function reconcile(projectDir, positional, flags = {}) {
7
+ const [resourceType, ...ids] = positional;
8
+ if (!resourceType || !ids.length) {
9
+ log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
10
+ return 1;
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
+ const results = await reconcileResources(projectDir, resourceType, ids);
27
+ if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
28
+ else for (const result of results) {
29
+ log.ok(`Reconciled ${result.resource_type} ${result.resource_id} -> ${result.base_version ?? result.base_revision}`);
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ module.exports = reconcile;