draftgo-cli 3.0.49 → 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.
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # draftgo-cli
2
2
 
3
+ ## Workflow 2.0
4
+
5
+ `draftgo context` outputs schema `2.0` with separate `bundled_references`, `live_project`, `live_resources`, `live_apis`, `live_db_meta`, and `live_session` sections. It writes to stdout by default, uses current cloud data on every run, and does not create a persistent context cache. Frontend and custom-service tasks always retain complete live DB Meta.
6
+
7
+ Pages, navigations, documents, and custom services are long-content resources. Custom services use `draftgo checkout custom-services <id>` and a four-file worktree (`service.go`, `go.mod`, `go.sum`, `service.json`). `commit` saves the shared cloud draft, `test` validates or executes it in the DraftGo Runner, and `publish` explicitly promotes a validated draft without interrupting the current live version. Start later work with `refresh`; it stops when local edits exist. Cloud versions are the only history, while local storage keeps only current content and one base.
8
+
9
+ Small, single-point changes do not need Task overhead. Cross-module, cross-session, or multi-Agent work uses one authoritative `.draftgo/Task/[YYYY-MM-DD]<slug>/Task.md` and `draftgo task create|add|claim|start|complete|block|reopen|accept|finish`. The file keeps the user's original request, the clarified executable requirement, expected effects, scope, checklist, decisions, blockers, and final acceptance together. Legacy `task.yaml + plan.md + items/` tasks remain readable and migrate only through an explicit `draftgo task migrate`. CLI temporary output is registered in `.draftgo/runtime-manifest.json` under `.draftgo/tmp/` or typed `.draftgo/artifacts/`; `draftgo clean` only handles those registered files. UI smoke checks remain single viewport and use `--screenshot never` by default. Supply `on-failure` or `always` only for explicit strong visual verification.
10
+
3
11
  面向 AI 编码工具的 DraftGo 工作台 CLI:分发 DraftGo Skill,通过 MCP 发现和操作实时资源,并为页面、导航和文档提供可校验的 `checkout` / `commit` 工作流。
4
12
 
5
13
  DraftGo Next 壳层使用 React + Vite;数据库页面可按任务使用原生 Web 技术、Tailwind CSS 或平台内置组件库。CLI 仍提供 Docker 本地运行、项目检查和浏览器验证能力。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.49",
3
+ "version": "3.0.51",
4
4
  "description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
5
5
  "bin": {
6
6
  "draftgo": "bin/draftgo.js"
@@ -45,9 +45,10 @@
45
45
  "lint": "node scripts/check-syntax.js",
46
46
  "validate:skill": "node scripts/validate-skill.js",
47
47
  "verify:package": "node scripts/verify-package.js",
48
- "test": "npm run lint && npm run validate:skill && npm run verify:package && npm run test:unit && npm run test:context && npm run test:changelog && npm run test:mcp && npm run test:worktree && npm run test:migration && npm run test:local && npm run test:e2e",
48
+ "test": "npm run lint && npm run validate:skill && npm run verify:package && npm run test:unit && npm run test:context && npm run test:workflow && npm run test:changelog && npm run test:mcp && npm run test:worktree && npm run test:migration && npm run test:local && npm run test:e2e",
49
49
  "test:unit": "node tests/unit.js",
50
50
  "test:context": "node tests/context.test.js",
51
+ "test:workflow": "node tests/workflow-upgrade.test.js",
51
52
  "test:changelog": "node tests/changelog.test.js",
52
53
  "test:mcp": "node tests/mcp.test.js",
53
54
  "test:worktree": "node tests/worktree.test.js",
@@ -57,11 +58,11 @@
57
58
  },
58
59
  "dependencies": {
59
60
  "@grpc/grpc-js": "1.14.1",
61
+ "adm-zip": "0.5.16",
60
62
  "parse5": "6.0.1",
61
63
  "playwright-core": "1.61.1"
62
64
  },
63
65
  "directories": {
64
66
  "test": "tests"
65
- },
66
- "devDependencies": {}
67
+ }
67
68
  }
@@ -3,6 +3,15 @@ name: draftgo
3
3
  description: Use this skill to inspect, develop, debug, or deliver a DraftGo application. Use draftgo context for task-specific references and live MCP discovery, checkout/commit for complete page, navigation, and document bodies, and write the changelog only after all verification and delivery succeed.
4
4
  ---
5
5
 
6
+ ## Workflow 2.0 authority
7
+
8
+ - Treat pages, navigations, docs/articles, and custom services as checkout resources. Never save complete MCP responses, duplicate source snapshots, temporary Go modules, SDK stubs, executables, or build directories by hand.
9
+ - `context` schema 2.0 separates `bundled_references` from `live_project`, `live_resources`, `live_apis`, and complete `live_db_meta`. It is stdout-only by default and has no persistent or delta cache.
10
+ - Custom-service flow: `context --task custom-service` -> `checkout custom-services` -> edit four files -> `commit` cloud draft -> `test` in the server Runner -> `check --remote` -> `publish` -> `refresh`. CLI and admin UI edit the same cloud draft.
11
+ - Do not create Task overhead for a small, single-point change. For cross-module, cross-session, or multi-Agent work, create or resume `.draftgo/Task/[YYYY-MM-DD]<slug>/Task.md`. It is the only task state: preserve both the user's original request and the clarified executable requirement, assign one owner per item/resource, and check an item only with verification evidence. Changelog remains the final delivery record, not a task tracker.
12
+ - Normal UI work uses lightweight single-viewport `verify-ui` with no screenshot. Only explicit visual acceptance or regression diagnosis may pass `--screenshot on-failure|always`.
13
+ - CLI temporary data belongs under registered `.draftgo/tmp/` or `.draftgo/artifacts/`; use `draftgo clean --dry-run` before `draftgo clean --yes`.
14
+
6
15
  # DraftGo 开发助手
7
16
 
8
17
  ## 核心边界
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.49",
5
+ "version": "3.0.51",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 context 聚合编排、Skill/reference 原文、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -4,6 +4,12 @@ read_when: 编辑 pages、navigation 或 docs 正文时 · 查看 checkout manif
4
4
 
5
5
  # Checkout / Commit
6
6
 
7
+ ## Workflow 2.0 scope
8
+
9
+ The checkout set includes `pages`, `navigations`, `docs/articles`, and `custom_services`. A custom-service checkout is a directory containing exactly `service.go`, `go.mod`, `go.sum`, and `service.json`, plus one complete `.base` directory. `draftgo commit custom-services <id>` saves a cloud draft; it does not publish. Use `draftgo test custom-services <id>` for server Runner validation/execution and `draftgo publish custom-services <id>` for explicit publication. DB Meta remains a live MCP/API resource and is never checked out.
10
+
11
+ `draftgo refresh <type> <id...>` is a safe checkout shortcut. It updates only a clean local worktree; local changes stop it. A successful commit keeps current local files and one base only. Cloud version storage is the sole history source.
12
+
7
13
  > 根 `SKILL.md` 在 Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明长正文的传输、版本和冲突规则,不能替代页面、前端、运行时或安全资料。
8
14
 
9
15
  ## 适用范围
@@ -1,4 +1,5 @@
1
1
  ---
2
+
2
3
  read_when: 编写、修改、调试或评审自定义服务时;使用服务 SDK、路由、事件、定时任务或服务依赖时
3
4
  ---
4
5
 
package/src/cli.js CHANGED
@@ -29,6 +29,30 @@ const VALUE_FLAGS = new Set([
29
29
  'input',
30
30
  'type',
31
31
  'task',
32
+ 'title',
33
+ 'description',
34
+ 'item-id',
35
+ 'id',
36
+ 'owner',
37
+ 'original',
38
+ 'original-requirement',
39
+ 'clarified',
40
+ 'clarified-requirement',
41
+ 'expected-effect',
42
+ 'effect',
43
+ 'scope',
44
+ 'out-of-scope',
45
+ 'content',
46
+ 'acceptance',
47
+ 'module-logic',
48
+ 'user-journey',
49
+ 'item',
50
+ 'date',
51
+ 'handler',
52
+ 'headers',
53
+ 'user',
54
+ 'side-effect-policy',
55
+ 'evidence',
32
56
  ]);
33
57
 
34
58
  function parse(argv) {
@@ -36,24 +60,24 @@ function parse(argv) {
36
60
  const out = { command: null, positional: [], flags: {}, errors: [] };
37
61
  while (args.length) {
38
62
  const a = args.shift();
39
- if (a === '--') { out.positional.push(...args); break; }
63
+ if (a === '--') {
64
+ out.positional.push(...args);
65
+ break;
66
+ }
40
67
  if (a.startsWith('--')) {
41
68
  const raw = a.slice(2);
42
69
  const eq = raw.indexOf('=');
43
70
  if (eq >= 0) {
44
71
  const key = raw.slice(0, eq);
45
72
  const value = raw.slice(eq + 1);
46
- if (VALUE_FLAGS.has(key) && !value) {
47
- out.errors.push(`选项 --${key} 需要一个值。`);
48
- } else {
49
- out.flags[key] = value;
50
- }
73
+ if (VALUE_FLAGS.has(key) && !value) out.errors.push(`Option --${key} requires a value.`);
74
+ else out.flags[key] = value;
51
75
  } else {
52
76
  const next = args[0];
53
77
  if (VALUE_FLAGS.has(raw)) {
54
78
  // A declared value flag must never silently become boolean true.
55
79
  if (next === undefined || next === '--' || next.startsWith('--')) {
56
- out.errors.push(`选项 --${raw} 需要一个值。`);
80
+ out.errors.push(`Option --${raw} requires a value.`);
57
81
  } else {
58
82
  out.flags[raw] = next;
59
83
  args.shift();
@@ -63,8 +87,7 @@ function parse(argv) {
63
87
  }
64
88
  }
65
89
  } else if (a.startsWith('-') && a.length > 1) {
66
- const key = a.slice(1);
67
- out.flags[key] = true;
90
+ out.flags[a.slice(1)] = true;
68
91
  } else if (!out.command) {
69
92
  out.command = a;
70
93
  } else {
@@ -9,12 +9,18 @@ const commands = [
9
9
  { name: 'status', run: (dir) => require('./commands/status')(dir) },
10
10
  { name: 'context', run: (dir, args, flags) => require('./commands/context')(dir, args, flags) },
11
11
  { name: 'changelog', run: (dir, args, flags) => require('./commands/changelog')(dir, args, flags) },
12
+ { name: 'task', run: (dir, args, flags) => require('./commands/task')(dir, args, flags) },
13
+ { name: 'clean', run: (dir, args, flags) => require('./commands/clean')(dir, args, flags) },
12
14
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
13
- { name: 'check', run: (dir, _args, flags) => require('./commands/check')(dir, flags) },
15
+ { name: 'check', run: (dir, args, flags) => require('./commands/check')(dir, args, flags) },
14
16
  { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
15
17
  { name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
16
18
  { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
19
+ { name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
17
20
  { name: 'commit', run: (dir, args, flags) => require('./commands/commit')(dir, args, flags) },
21
+ { name: 'validate', run: (dir, args, flags) => require('./commands/customService').validate(dir, args, flags) },
22
+ { name: 'test', run: (dir, args, flags) => require('./commands/customService').test(dir, args, flags) },
23
+ { name: 'publish', run: (dir, args, flags) => require('./commands/customService').publish(dir, args, flags) },
18
24
  { name: 'reconcile', run: (dir, args, flags) => require('./commands/reconcile')(dir, args, flags) },
19
25
  { name: 'diff', run: (dir, args, flags) => require('./commands/diff')(dir, args, flags) },
20
26
  { name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
@@ -5,19 +5,63 @@ const path = require('path');
5
5
  const log = require('../logger');
6
6
  const { analyzeProject } = require('../projectMap');
7
7
  const { inspectRemoteCheckouts } = require('../worktree/status');
8
+ const customServices = require('../customServices');
9
+ const { workspaceHealth } = require('../workspaceHealth');
8
10
 
9
11
  function legacyCacheDetected(projectDir) {
10
12
  return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
11
13
  .some((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
12
14
  }
13
15
 
14
- async function check(projectDir, flags = {}) {
16
+ async function check(projectDir, positional = [], flags = {}) {
17
+ if (!Array.isArray(positional)) {
18
+ flags = positional || {};
19
+ positional = [];
20
+ }
21
+ const [requestedType, ...requestedIds] = positional;
22
+ if (requestedType && !customServicesCommand().isServiceType(requestedType)) {
23
+ log.err('Usage: draftgo check [custom-services <id...>] [--remote]');
24
+ return 1;
25
+ }
26
+ if (requestedType && !requestedIds.length) {
27
+ log.err('Usage: draftgo check custom-services <id...> [--remote]');
28
+ return 1;
29
+ }
15
30
  const result = analyzeProject(projectDir);
31
+ const hygiene = workspaceHealth(projectDir);
32
+ hygiene.warnings.forEach((warning) => result.warningDetails.push({ code: warning.code, confidence: 'high', message: `${warning.path}${warning.size ? ` (${warning.size} bytes)` : ''}` }));
33
+ hygiene.warnings.forEach((warning) => result.warnings.push(`${warning.code}: ${warning.path}`));
34
+ const serviceManifest = customServices.loadManifest(projectDir);
35
+ const customServiceChecks = [];
36
+ const requested = new Set(requestedIds.map(String));
37
+ const serviceEntries = Object.values(serviceManifest.entries)
38
+ .filter((entry) => !requestedType || requested.has(String(entry.resource_id)));
39
+ if (requestedType) {
40
+ for (const id of requested) {
41
+ if (!serviceEntries.some((entry) => String(entry.resource_id) === id)) {
42
+ result.errors.push(`custom_services ${id}: resource is not checked out`);
43
+ }
44
+ }
45
+ }
46
+ for (const entry of serviceEntries) {
47
+ try {
48
+ const state = customServices.diff(projectDir, entry.resource_id);
49
+ customServiceChecks.push({ resource_type: 'custom_services', resource_id: entry.resource_id, state: state.changed ? 'local_modified' : 'clean_local', files: state.files });
50
+ } catch (error) {
51
+ result.errors.push(`custom_services ${entry.resource_id}: ${error.message}`);
52
+ }
53
+ }
16
54
  let remote = null;
17
55
  let remoteError = null;
18
56
  if (flags.remote) {
19
57
  try {
20
- remote = await inspectRemoteCheckouts(projectDir);
58
+ const [contentRemote, serviceRemote] = await Promise.all([
59
+ inspectRemoteCheckouts(projectDir),
60
+ customServices.inspectRemote(projectDir, requestedType ? { ids: [...requested] } : {}),
61
+ ]);
62
+ remote = requestedType
63
+ ? serviceRemote.filter((entry) => requested.has(String(entry.resource_id)))
64
+ : [...contentRemote, ...serviceRemote];
21
65
  for (const entry of remote) {
22
66
  if (!['clean', 'clean_local', 'local_modified'].includes(entry.state)) {
23
67
  const detail = `${entry.resource_type} ${entry.resource_id}: ${entry.state}`
@@ -32,7 +76,11 @@ async function check(projectDir, flags = {}) {
32
76
  result.errors.push(`Remote checkout check failed: ${error.message}`);
33
77
  }
34
78
  }
35
- const output = { ...result, remote, remote_error: remoteError };
79
+ const output = {
80
+ content_validation: { map: result.map, errors: result.errors, warnings: result.warnings, warning_details: result.warningDetails, custom_services: customServiceChecks },
81
+ remote_validation: { resources: remote, error: remoteError },
82
+ workspace_hygiene: hygiene,
83
+ };
36
84
  if (flags.output === 'json') {
37
85
  console.log(JSON.stringify({ ...output, legacy_cache_ignored: legacyCacheDetected(projectDir) }, null, 2));
38
86
  return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
@@ -42,6 +90,9 @@ async function check(projectDir, flags = {}) {
42
90
  log.info(`Checked-out pages: ${result.map.pages.length}`);
43
91
  log.info(`Checked-out navigations: ${result.map.navigations.length}`);
44
92
  log.info(`Checked-out docs: ${result.map.docs.length}`);
93
+ log.info(`Checked-out custom services: ${customServiceChecks.length}`);
94
+ log.info(`Workspace: ${hygiene.managed_files} managed, ${hygiene.unknown_files} unknown, ${hygiene.temporary_files} temporary, ${hygiene.artifact_files} artifact file(s)`);
95
+ log.info(`Workspace size: ${hygiene.total_bytes} bytes; reclaimable: ${hygiene.reclaimable_bytes} bytes`);
45
96
  if (remote) log.info(`Remote checkout comparison: ${remote.length} resource(s)`);
46
97
  for (const message of result.errors) log.err(message);
47
98
  for (const detail of result.warningDetails) log.warn(`[${detail.code}/${detail.confidence}] ${detail.message}`);
@@ -52,4 +103,8 @@ async function check(projectDir, flags = {}) {
52
103
  return 0;
53
104
  }
54
105
 
106
+ function customServicesCommand() {
107
+ return require('./customService');
108
+ }
109
+
55
110
  module.exports = check;
@@ -9,6 +9,9 @@ async function checkout(projectDir, positional, flags = {}) {
9
9
  log.err('Usage: draftgo checkout <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
+ if (require('./customService').isServiceType(resourceType)) {
13
+ return require('./customService').checkout(projectDir, ids, flags);
14
+ }
12
15
  const results = await checkoutResources(projectDir, resourceType, ids, { force: Boolean(flags.force) });
13
16
  if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
14
17
  else for (const entry of results) log.ok(`Checked out ${entry.resource_type} ${entry.resource_id} -> ${entry.local_path}`);
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const log = require('../logger');
6
+ const runtime = require('../runtimeFiles');
7
+
8
+ const MANAGED_PREFIXES = ['tmp/', 'artifacts/ui/', 'artifacts/api/', 'artifacts/custom-services/'];
9
+ function fileSize(file) { try { const stat = fs.statSync(file); return stat.isFile() ? stat.size : 0; } catch { return 0; } }
10
+ function olderThan(value) {
11
+ if (value == null || value === '') return 0;
12
+ const match = String(value).match(/^(\d+)([dhm])$/i); if (!match) throw new Error('--older-than must use <number>d, <number>h, or <number>m.');
13
+ const unit = { d: 86400000, h: 3600000, m: 60000 }[match[2].toLowerCase()]; return Number(match[1]) * unit;
14
+ }
15
+ function plan(projectDir, flags = {}) {
16
+ const root = runtime.draftgoRoot(projectDir); const manifest = runtime.load(projectDir); const age = olderThan(flags['older-than']); const now = Date.now();
17
+ const types = new Set(String(flags.type || '').split(',').map((item) => item.trim()).filter(Boolean)); const candidates = [];
18
+ for (const entry of manifest.entries) {
19
+ if (!entry.cleanable || (types.size && !types.has(entry.type))) continue;
20
+ const relative = String(entry.path).replace(/\\/g, '/');
21
+ if (!MANAGED_PREFIXES.some((prefix) => relative.startsWith(prefix))) continue;
22
+ const absolute = path.resolve(root, relative); if (!runtime.inside(root, absolute) || !fs.existsSync(absolute)) continue;
23
+ const stat = fs.statSync(absolute); if (age && now - stat.mtimeMs < age) continue;
24
+ candidates.push({ ...entry, absolute, size: stat.isFile() ? stat.size : 0, reason: 'runtime_manifest' });
25
+ }
26
+ if (flags['all-artifacts']) {
27
+ for (const prefix of MANAGED_PREFIXES.filter((item) => item.startsWith('artifacts/'))) {
28
+ const directory = path.resolve(root, prefix); if (!runtime.inside(root, directory) || !fs.existsSync(directory)) continue;
29
+ const visit = (current) => { for (const child of fs.readdirSync(current, { withFileTypes: true })) { const absolute = path.join(current, child.name); if (child.isDirectory()) visit(absolute); else if (child.isFile() && !candidates.some((entry) => entry.absolute === absolute)) candidates.push({ path: path.relative(root, absolute).replace(/\\/g, '/'), absolute, type: 'artifact', size: fileSize(absolute), reason: 'all_artifacts' }); } };
30
+ visit(directory);
31
+ }
32
+ }
33
+ return { entries: candidates, count: candidates.length, bytes: candidates.reduce((sum, entry) => sum + entry.size, 0) };
34
+ }
35
+ async function clean(projectDir, _positional, flags = {}) {
36
+ const result = plan(projectDir, flags); const output = { count: result.count, bytes: result.bytes, entries: result.entries.map(({ absolute, ...entry }) => entry) };
37
+ if (flags.output === 'json') console.log(JSON.stringify({ ...output, deleted: Boolean(flags.yes && !flags['dry-run']) }, null, 2));
38
+ else { log.title('draftgo clean'); result.entries.forEach((entry) => log.info(`${entry.path} (${entry.size} bytes)`)); log.info(`Reclaimable: ${result.bytes} bytes in ${result.count} file(s).`); }
39
+ if (!flags.yes || flags['dry-run']) { if (!flags.output) log.dim('Run with --yes to delete this plan.'); return 0; }
40
+ const manifest = runtime.load(projectDir); const removed = new Set();
41
+ for (const entry of result.entries) { if (!runtime.inside(runtime.draftgoRoot(projectDir), entry.absolute)) throw new Error('Clean target escaped .draftgo.'); fs.rmSync(entry.absolute, { force: true }); removed.add(entry.path); }
42
+ manifest.entries = manifest.entries.filter((entry) => !removed.has(entry.path)); runtime.save(projectDir, manifest);
43
+ if (!flags.output) log.ok(`Deleted ${result.count} managed file(s).`); return 0;
44
+ }
45
+ clean.plan = plan;
46
+ module.exports = clean;
@@ -9,6 +9,9 @@ async function commit(projectDir, positional, flags = {}) {
9
9
  log.err('Usage: draftgo commit <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
+ if (require('./customService').isServiceType(resourceType)) {
13
+ return require('./customService').commit(projectDir, ids, flags);
14
+ }
12
15
  const streamed = [];
13
16
  const printStatus = (result) => {
14
17
  streamed.push(result);
@@ -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 };
@@ -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,10 +44,18 @@ 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.
48
49
  --remote also compares remote hash/version.
49
50
  draftgo diff <type> <id> Show checkout base versus local body.
50
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.
51
59
  draftgo reconcile <type> <id...> Repair stale metadata only when local equals remote.
52
60
  draftgo conflicts List unresolved conflicts; --all includes resolved.
53
61
  draftgo conflict show <type> <id>
@@ -56,6 +64,12 @@ Usage:
56
64
  Mark a merged worktree file ready against the
57
65
  preserved remote base; then check/diff/commit.
58
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.
59
73
 
60
74
  draftgo api <query> Search the live API contract through MCP.
61
75
  draftgo api describe <operation_id>
@@ -86,7 +100,7 @@ Usage:
86
100
  draftgo -h | --help Show this help.
87
101
 
88
102
  Resource types:
89
- pages | nav/navigations | docs/articles
103
+ pages | nav/navigations | docs/articles | custom-services
90
104
 
91
105
  Important flags:
92
106
  --project <dir> Operate on <dir> instead of the current directory.
@@ -115,12 +129,18 @@ Important flags:
115
129
  --operation-id <id> (delete) Select an operation explicitly.
116
130
  --params <json> (api call/delete) Pass an API parameter object.
117
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.
118
138
  --delivery <mode> (deploy) local | preview | deploy.
119
139
  --dry-run (push) Show diffs without committing.
120
140
  --mobile-check <mode> (verify-ui) auto | always | never.
121
141
  --token <mode> (verify-ui) auto | never; auto appends the configured
122
142
  SAT to same-origin URLs as the token query parameter.
123
- --screenshot <mode> (verify-ui) on-failure | always | never.
143
+ --screenshot <mode> (verify-ui) on-failure | always | never; default never.
124
144
  --browser <name> (verify-ui) chromium | chrome | msedge.
125
145
  --browser-path <file> (verify-ui) Explicit browser executable; environment
126
146
  fallback: DRAFTGO_BROWSER_PATH.
@@ -149,6 +169,9 @@ Examples:
149
169
  draftgo diff pages 42
150
170
  draftgo commit pages 42
151
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"
152
175
  draftgo deploy docs 7 --delivery preview
153
176
  `);
154
177
  }
@@ -8,11 +8,13 @@ const { canonicalResourceType } = require('../worktree/types');
8
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
9
  const { allWithAbort } = require('../mcp/parallel');
10
10
  const { inspectRemoteCheckouts } = require('../worktree/status');
11
+ const customServices = require('../customServices');
11
12
 
12
13
  const REMOTE_RESOURCE_TYPES = Object.freeze({
13
14
  pages: 'pages',
14
15
  navigations: 'navigations',
15
16
  docs: 'docs/articles',
17
+ custom_services: 'custom_services',
16
18
  });
17
19
  const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
18
20
 
@@ -41,6 +43,7 @@ function claimsMorePages(value) {
41
43
  }
42
44
 
43
45
  function normalizeMapResourceType(value) {
46
+ if (require('./customService').isServiceType(value)) return 'custom_services';
44
47
  return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
45
48
  }
46
49
 
@@ -86,6 +89,13 @@ function legacyCaches(projectDir) {
86
89
  .filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
87
90
  }
88
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
+
89
99
  async function mapCommand(projectDir, flags = {}) {
90
100
  const resourceTypes = requestedResourceTypes(flags);
91
101
  const config = loadProjectConfig(projectDir);
@@ -96,17 +106,17 @@ async function mapCommand(projectDir, flags = {}) {
96
106
  (options) => listRemoteResources(session, resourceTypes, options),
97
107
  ], options),
98
108
  async (options) => {
99
- const remoteStatus = await inspectRemoteCheckouts(projectDir, {
109
+ const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
100
110
  config,
101
111
  client: session.client,
102
112
  tools: session.tools,
103
113
  signal: options.signal,
104
- });
105
- return remoteStatus.map((entry) => ({
114
+ }), customServices.inspectRemote(projectDir, { config, signal: options.signal })]);
115
+ return [...contentStatus, ...serviceStatus].map((entry) => ({
106
116
  ...entry,
107
117
  exists: entry.local_hash !== null,
108
118
  current_hash: entry.local_hash,
109
- changed: entry.local_hash !== null && entry.local_hash !== entry.base_hash,
119
+ changed: checkoutChanged(entry),
110
120
  }));
111
121
  },
112
122
  ]);
@@ -150,3 +160,4 @@ module.exports.itemsFrom = itemsFrom;
150
160
  module.exports.normalizeMapResourceType = normalizeMapResourceType;
151
161
  module.exports.requestedResourceTypes = requestedResourceTypes;
152
162
  module.exports.listRemoteResources = listRemoteResources;
163
+ module.exports.checkoutChanged = checkoutChanged;