draftgo-cli 3.0.56 → 4.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +169 -297
  2. package/package.json +12 -7
  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 -87
  18. package/resources/skill/init/SKILL.md +9 -14
  19. package/resources/skill/manifest.json +6 -2
  20. package/resources/skill/references/aihub.md +28 -5
  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 +21 -7
  25. package/resources/skill/references/custom-services.md +124 -222
  26. package/resources/skill/references/data.md +22 -6
  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 +93 -499
  30. package/resources/skill/references/mcp.md +65 -101
  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 +26 -3
  34. package/resources/skill/story/SKILL.md +1 -2
  35. package/src/apiContractCache.js +112 -0
  36. package/src/cli.js +24 -20
  37. package/src/commandRegistry.js +15 -12
  38. package/src/commands/api.js +41 -10
  39. package/src/commands/apiKey.js +34 -0
  40. package/src/commands/capabilities.js +93 -0
  41. package/src/commands/check.js +1 -10
  42. package/src/commands/checkout.js +1 -1
  43. package/src/commands/commit.js +1 -1
  44. package/src/commands/components.js +550 -0
  45. package/src/commands/conflict.js +1 -1
  46. package/src/commands/connect.js +18 -8
  47. package/src/commands/customService.js +22 -8
  48. package/src/commands/dataRange.js +33 -0
  49. package/src/commands/delete.js +34 -46
  50. package/src/commands/deploy.js +1 -1
  51. package/src/commands/diff.js +18 -2
  52. package/src/commands/grant.js +29 -0
  53. package/src/commands/group.js +38 -0
  54. package/src/commands/help.js +80 -51
  55. package/src/commands/init.js +6 -12
  56. package/src/commands/listTargets.js +1 -1
  57. package/src/commands/local.js +2 -6
  58. package/src/commands/map.js +145 -28
  59. package/src/commands/mcp.js +2 -2
  60. package/src/commands/reconcile.js +1 -1
  61. package/src/commands/role.js +32 -0
  62. package/src/commands/space.js +41 -0
  63. package/src/commands/status.js +111 -8
  64. package/src/commands/uninstall.js +3 -3
  65. package/src/commands/update.js +24 -12
  66. package/src/commands/verify.js +118 -21
  67. package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
  68. package/src/commands/worklog.js +90 -0
  69. package/src/consoleEncoding.js +34 -0
  70. package/src/contractCompatibility.js +57 -0
  71. package/src/customServices.js +278 -41
  72. package/src/diffReport.js +106 -0
  73. package/src/index.js +2 -0
  74. package/src/{localdev → localRuntime}/compose.js +14 -17
  75. package/src/{localdev → localRuntime}/detect.js +1 -1
  76. package/src/{localdev → localRuntime}/index.js +22 -23
  77. package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
  78. package/src/{localdev → localRuntime}/services.js +28 -37
  79. package/src/mcp/client.js +11 -2
  80. package/src/mcp/protocol.js +2 -2
  81. package/src/mcp/tools.js +14 -1
  82. package/src/platforms.js +9 -0
  83. package/src/projectConfig.js +8 -4
  84. package/src/releaseInstall.js +105 -0
  85. package/src/{installers/index.js → targets.js} +3 -5
  86. package/src/updateCheck.js +48 -28
  87. package/src/worklog.js +275 -0
  88. package/src/workspaceHealth.js +1 -1
  89. package/src/worktree/backend.js +1 -1
  90. package/src/worktree/index.js +86 -51
  91. package/src/changelog.js +0 -276
  92. package/src/commands/changelog.js +0 -24
  93. package/src/commands/localDev.js +0 -9
  94. package/src/commands/sync.js +0 -46
  95. package/src/commands/task.js +0 -408
  96. package/src/commands/verifyUiCompat.js +0 -16
@@ -2,11 +2,11 @@
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
4
  const log = require('../logger');
5
- const { ask, askRequired, askPassword, confirm } = require('../prompt');
5
+ const { askRequired, askPassword, confirm } = require('../prompt');
6
6
  const { probePort, probeHttp, detectDocker } = require('./detect');
7
7
  const compose = require('./compose');
8
8
  const { ensureDatabase, testConnection, describeClient } = require('./mysqlClient');
9
- const { defaults, portOpen, probeRedis, probeMilvus, startService } = require('./services');
9
+ const { defaults, portOpen, probeRedis, probeQdrant, startService } = require('./services');
10
10
  const { writeProjectConfig } = require('../projectConfig');
11
11
  const { appendGitignoreLine } = require('../fsx');
12
12
 
@@ -88,30 +88,29 @@ async function planRedis(docker) {
88
88
  return conn;
89
89
  }
90
90
 
91
- async function promptMilvusConnection() {
91
+ async function promptQdrantConnection() {
92
92
  while (true) {
93
- const host = await askRequired(' Milvus host', { default: '127.0.0.1' });
94
- const port = Number(await askRequired(' Milvus port', { default: '19530' }));
95
- const username = await ask(' Milvus username (empty for none)', { default: '' });
96
- const password = await askPassword(' Milvus password (empty for none)', { default: '' });
97
- const conn = { host, port, username, password };
98
- if (await probeMilvus(conn)) return conn;
99
- log.err(' Milvus health check failed. Check the address and credentials.');
93
+ const host = await askRequired(' Qdrant host', { default: '127.0.0.1' });
94
+ const port = Number(await askRequired(' Qdrant port', { default: '26333' }));
95
+ const apiKey = await askPassword(' Qdrant API key (empty for none)', { default: '' });
96
+ const conn = { host, port, apiKey };
97
+ if (await probeQdrant(conn)) return conn;
98
+ log.err(' Qdrant REST health check failed. Check the address and API key.');
100
99
  }
101
100
  }
102
101
 
103
- async function planMilvus(docker) {
104
- log.step('Checking Milvus');
105
- let conn = { ...defaults.milvus };
102
+ async function planQdrant(docker) {
103
+ log.step('Checking Qdrant');
104
+ let conn = { ...defaults.qdrant };
106
105
  if (!await portOpen(conn.host, conn.port)) {
107
- log.dim(' Milvus is not running locally; starting the shared local service.');
108
- if (!startService(docker, 'milvus')) throw new Error('Unable to start shared Milvus.');
106
+ log.dim(' Qdrant is not running locally; starting the shared local service.');
107
+ if (!startService(docker, 'qdrant')) throw new Error('Unable to start shared Qdrant.');
109
108
  }
110
- if (!await probeMilvus(conn)) {
111
- log.warn(' Default unauthenticated Milvus health check failed.');
112
- conn = await promptMilvusConnection();
109
+ if (!await probeQdrant(conn)) {
110
+ log.warn(' Default unauthenticated Qdrant REST health check failed.');
111
+ conn = await promptQdrantConnection();
113
112
  }
114
- log.ok(` Milvus ready: ${conn.host}:${conn.port}`);
113
+ log.ok(` Qdrant ready: ${conn.host}:${conn.port}`);
115
114
  return conn;
116
115
  }
117
116
 
@@ -190,17 +189,17 @@ async function runWizard(projectDir, { yes = false, mcpCommands } = {}) {
190
189
  const projectName = await pickProjectName();
191
190
  const mysql = await planMysql(docker, projectName);
192
191
  const redis = await planRedis(docker);
193
- const milvus = await planMilvus(docker);
192
+ const qdrant = await planQdrant(docker);
194
193
 
195
- const out = compose.generate(projectDir, { projectName, appPort, mysql, redis, milvus });
194
+ const out = compose.generate(projectDir, { projectName, appPort, mysql, redis, qdrant });
196
195
  appendGitignoreLine(projectDir, '.draftgo/docker/');
197
196
  if (!composeUp(docker, out.dir) || !await waitForApp(appPort)) return 1;
198
197
 
199
198
  const url = `http://localhost:${appPort}`;
200
199
  let token = '';
201
200
  while (token.length < 10) {
202
- token = String(await askPassword('Paste a DraftGo system access token (SAT)')).trim();
203
- if (token.length < 10) log.err(' Token appears too short. Please try again.');
201
+ token = String(await askPassword('Paste a DraftGo user API Key')).trim();
202
+ if (token.length < 10) log.err(' API Key appears too short. Please try again.');
204
203
  }
205
204
  const cfgPath = writeProjectConfig(projectDir, url, token);
206
205
  await finishMcpSetup(projectDir, mcpCommands);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Lightweight MySQL helper used by the local-dev wizard to validate
3
+ // Lightweight MySQL helper used by the local stack wizard to validate
4
4
  // credentials and auto-create the project database when missing.
5
5
  //
6
6
  // We avoid taking a JS MySQL driver as a dependency. Instead we shell out
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
- // Shared local dependencies live outside individual projects. Project Compose
3
+ // Shared local runtime dependencies live outside individual projects. Project Compose
4
4
  // files only start DraftGo itself and connect back to these loopback services.
5
5
  const net = require('net');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
+ const http = require('http');
9
+ const https = require('https');
8
10
  const { spawnSync } = require('child_process');
9
11
  const { ensureDir, writeText } = require('../fsx');
10
12
 
@@ -15,14 +17,14 @@ const MYSQL_INIT_FILE = path.join(SERVICES_DIR, 'mysql-init.sql');
15
17
  const defaults = {
16
18
  mysql: { host: '127.0.0.1', port: 3306, user: 'draftgo', password: 'draftgo' },
17
19
  redis: { host: '127.0.0.1', port: 6379, password: '' },
18
- milvus: { host: '127.0.0.1', port: 19530, username: '', password: '' },
20
+ qdrant: { host: '127.0.0.1', port: 26333, apiKey: '' },
19
21
  };
20
22
 
21
23
  function writeServiceFiles() {
22
24
  ensureDir(SERVICES_DIR);
23
25
  ensureDir(path.join(SERVICES_DIR, 'data', 'mysql'));
24
26
  ensureDir(path.join(SERVICES_DIR, 'data', 'redis'));
25
- ensureDir(path.join(SERVICES_DIR, 'data', 'milvus'));
27
+ ensureDir(path.join(SERVICES_DIR, 'data', 'qdrant'));
26
28
  writeText(MYSQL_INIT_FILE, [
27
29
  "GRANT ALL PRIVILEGES ON *.* TO 'draftgo'@'%' WITH GRANT OPTION;",
28
30
  'FLUSH PRIVILEGES;',
@@ -66,27 +68,19 @@ function writeServiceFiles() {
66
68
  ' interval: 5s',
67
69
  ' timeout: 5s',
68
70
  ' retries: 20',
69
- ' milvus:',
70
- ' image: milvusdb/milvus:v2.6.0',
71
- ' container_name: draftgo-local-milvus',
71
+ ' qdrant:',
72
+ ' image: qdrant/qdrant:v1.19.0',
73
+ ' container_name: draftgo-local-qdrant',
72
74
  ' restart: unless-stopped',
73
- ' command: ["milvus", "run", "standalone"]',
74
- ' security_opt:',
75
- ' - seccomp:unconfined',
76
- ' environment:',
77
- ' DEPLOY_MODE: STANDALONE',
78
- ' ETCD_USE_EMBED: "true"',
79
- ' ETCD_DATA_DIR: /var/lib/milvus/etcd',
80
- ' COMMON_STORAGETYPE: local',
81
75
  ' ports:',
82
- ' - "127.0.0.1:19530:19530"',
76
+ ' - "127.0.0.1:26333:6333"',
83
77
  ' volumes:',
84
- ' - ./data/milvus:/var/lib/milvus',
78
+ ' - ./data/qdrant:/qdrant/storage',
85
79
  ' healthcheck:',
86
- ' test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]',
80
+ ' test: ["CMD-SHELL", "bash -c \':> /dev/tcp/127.0.0.1/6333\'"]',
87
81
  ' interval: 10s',
88
82
  ' timeout: 5s',
89
- ' retries: 18',
83
+ ' retries: 12',
90
84
  ' start_period: 30s',
91
85
  '',
92
86
  ].join('\n'));
@@ -123,25 +117,22 @@ function probeRedis({ host, port, password = '' }) {
123
117
  });
124
118
  }
125
119
 
126
- function probeMilvus({ host, port, username = '', password = '' }) {
120
+ function probeQdrant({ host, port, apiKey = '' }) {
127
121
  return new Promise((resolve) => {
128
- const grpc = require('@grpc/grpc-js');
129
- const client = new grpc.Client(`${host}:${port}`, grpc.credentials.createInsecure());
130
- const metadata = new grpc.Metadata();
131
- if (username || password) metadata.add('authorization', `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`);
132
- client.makeUnaryRequest(
133
- '/milvus.proto.milvus.MilvusService/CheckHealth',
134
- (value) => value,
135
- (value) => value,
136
- Buffer.alloc(0),
137
- metadata,
138
- { deadline: Date.now() + 4000 },
139
- (err, response) => {
140
- client.close();
141
- // CheckHealthResponse starts with field 1 (is_healthy) when the server is healthy.
142
- resolve(!err && Buffer.isBuffer(response) && response.includes(0x01));
143
- },
144
- );
122
+ const client = String(host || '').startsWith('https://') ? https : http;
123
+ const hostname = String(host || '').replace(/^https?:\/\//, '');
124
+ const request = client.get({
125
+ hostname,
126
+ port,
127
+ path: '/healthz',
128
+ headers: apiKey ? { 'api-key': apiKey } : undefined,
129
+ timeout: 4000,
130
+ }, (response) => {
131
+ response.resume();
132
+ resolve(response.statusCode >= 200 && response.statusCode < 300);
133
+ });
134
+ request.once('timeout', () => { request.destroy(); resolve(false); });
135
+ request.once('error', () => resolve(false));
145
136
  });
146
137
  }
147
138
 
@@ -158,6 +149,6 @@ module.exports = {
158
149
  defaults,
159
150
  portOpen,
160
151
  probeRedis,
161
- probeMilvus,
152
+ probeQdrant,
162
153
  startService,
163
154
  };
package/src/mcp/client.js CHANGED
@@ -184,7 +184,7 @@ class DraftGoMcpClient {
184
184
  constructor(config, options = {}) {
185
185
  if (!config || typeof config !== 'object') throw new TypeError('DraftGo MCP config is required.');
186
186
  const token = String(config.token || config.sat || '').trim();
187
- if (!token) throw new Error('DraftGo MCP config is missing SAT.');
187
+ if (!token) throw new Error('DraftGo MCP config is missing API Key.');
188
188
  if (!config.server && !config.mcp_url) throw new Error('DraftGo MCP config is missing server.');
189
189
 
190
190
  this.config = { ...config, token };
@@ -453,7 +453,7 @@ class DraftGoMcpClient {
453
453
  })).filter((entry) => entry.tool);
454
454
  const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
455
455
  const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
456
- const defaultPlan = hasDiagnosticOverrides ? [{
456
+ let defaultPlan = hasDiagnosticOverrides ? [{
457
457
  label: overrideCanonical,
458
458
  canonical: overrideCanonical,
459
459
  tool: firstSafeTool,
@@ -473,6 +473,15 @@ class DraftGoMcpClient {
473
473
  args: { resource_type: 'db_meta', limit: 100 },
474
474
  },
475
475
  ];
476
+ const spaceScope = options.scopeType === 'space' && Number(options.spaceId) > 0
477
+ ? { scope_type: 'space', space_id: Number(options.spaceId) }
478
+ : {};
479
+ if (!hasDiagnosticOverrides && !spaceScope.scope_type) {
480
+ defaultPlan = defaultPlan.filter(entry => entry.label !== 'resource:docs/articles');
481
+ }
482
+ for (const entry of defaultPlan) {
483
+ if (entry.label === 'resource:docs/articles') entry.args = { ...entry.args, ...spaceScope };
484
+ }
476
485
  const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
477
486
  async (queryOptions) => {
478
487
  if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'started');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
3
+ const DEFAULT_PROTOCOL_VERSION = '2026-07-28';
4
4
 
5
5
  class McpHttpError extends Error {
6
6
  constructor(message, { status = 0, rpc = null, code = null } = {}) {
@@ -34,7 +34,7 @@ function endpointFor(config) {
34
34
  ...((config.mcp_allowed_origins || []).map((value) => new URL(value).origin)),
35
35
  ]);
36
36
  if (!allowedOrigins.has(endpoint.origin)) {
37
- throw new McpHttpError('DraftGo MCP endpoint origin is not allowlisted; refusing to send the SAT.');
37
+ throw new McpHttpError('DraftGo MCP endpoint origin is not allowlisted; refusing to send the API Key.');
38
38
  }
39
39
  return endpoint.toString();
40
40
  }
package/src/mcp/tools.js CHANGED
@@ -35,6 +35,19 @@ async function openToolSession(config, expected = [], options = {}) {
35
35
  return { client, tools, names };
36
36
  }
37
37
 
38
+ function configuredScope(config, description = null) {
39
+ const operation = description && (description.operation || description) || {};
40
+ if (String(operation.ownership_mode || '').toLowerCase() === 'platform_only') return {};
41
+ const supported = Array.isArray(operation.supported_scopes)
42
+ ? operation.supported_scopes.map(value => String(value).toLowerCase()) : [];
43
+ const needsSpace = String(operation.ownership_mode || '').toLowerCase() === 'workspace_required'
44
+ && supported.length > 0 && !supported.includes('platform');
45
+ if (config && config.scope_type === 'space' && Number(config.space_id) > 0 && (needsSpace || supported.includes('space'))) {
46
+ return { scope_type: 'space', space_id: Number(config.space_id) };
47
+ }
48
+ return {};
49
+ }
50
+
38
51
  async function callStructured(session, expectedName, args = {}, options = {}) {
39
52
  const name = session.names[expectedName] || expectedName;
40
53
  const result = await session.client.toolsCall(name, args, options);
@@ -44,4 +57,4 @@ async function callStructured(session, expectedName, args = {}, options = {}) {
44
57
  : data;
45
58
  }
46
59
 
47
- module.exports = { TOOL_NAMES, openToolSession, callStructured };
60
+ module.exports = { TOOL_NAMES, openToolSession, callStructured, configuredScope };
package/src/platforms.js CHANGED
@@ -94,6 +94,15 @@ const platforms = [
94
94
  signals: ['.gemini', 'GEMINI.md'],
95
95
  frontmatter: { name: SKILL_NAME, description: DESCRIPTION },
96
96
  },
97
+ {
98
+ name: 'pi',
99
+ displayName: 'Pi',
100
+ mainFile: '.agents/skills/draftgo/SKILL.md',
101
+ assetDir: '.agents/skills/draftgo',
102
+ skillDir: '.agents/skills/draftgo',
103
+ signals: ['.agents/skills', '.pi'],
104
+ frontmatter: { name: SKILL_NAME, description: DESCRIPTION },
105
+ },
97
106
  ];
98
107
 
99
108
  const byName = Object.fromEntries(platforms.map((p) => [p.name, p]));
@@ -9,6 +9,8 @@ const IGNORE_ENTRIES = [
9
9
  '.draftgo/token',
10
10
  '.draftgo/worktree/',
11
11
  '.draftgo/conflicts/',
12
+ '.draftgo/api-contract-cache.json',
13
+ '.draftgo/api-contract-cache.json.lock',
12
14
  ];
13
15
 
14
16
  function parseHttpUrl(raw) {
@@ -93,16 +95,18 @@ function writePrivateJson(file, value) {
93
95
 
94
96
  function writeProjectConfig(projectDir, server, token, options = {}) {
95
97
  const normalizedServer = normalizeServer(server);
96
- const sat = String(token || '').trim();
98
+ const apiKey = String(token || '').trim();
97
99
  if (!normalizedServer) throw new Error('DraftGo server is required.');
98
- if (!sat) throw new Error('DraftGo SAT is required.');
100
+ if (!apiKey) throw new Error('DraftGo API Key is required.');
99
101
  const existing = readExisting(projectDir);
100
102
  const config = {
101
103
  ...existing,
102
104
  server: normalizedServer,
103
- token: sat,
105
+ token: apiKey,
104
106
  auto_push: existing.auto_push === true,
105
107
  };
108
+ if (options.scope_type) config.scope_type = String(options.scope_type).trim().toLowerCase();
109
+ if (options.space_id) config.space_id = Number(options.space_id);
106
110
  if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
107
111
  else delete config.mcp_url;
108
112
  const file = configPath(projectDir);
@@ -119,7 +123,7 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
119
123
  const mcpUrl = config.mcp_url ? normalizeMcpEndpoint(config.mcp_url) : '';
120
124
  const token = String(config.token || config.sat || '').trim();
121
125
  if (!server) throw new Error('DraftGo project config is missing server.');
122
- if (requireToken && !token) throw new Error('DraftGo project config is missing SAT.');
126
+ if (requireToken && !token) throw new Error('DraftGo project config is missing API Key.');
123
127
  const result = { ...config, server, token, path: file };
124
128
  if (mcpUrl) result.mcp_url = mcpUrl;
125
129
  else delete result.mcp_url;
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+ const { PACKAGE_ROOT } = require('./paths');
10
+
11
+ const MARKER = '.draftgo-install.json';
12
+ const REPO = process.env.DRAFTGO_REPO || 'draftgo/draftgo-cli';
13
+ const BASE_URL = process.env.DRAFTGO_INSTALL_BASE_URL || 'https://draftgo.cn/draftgo-cli';
14
+ const MIRROR = process.env.DRAFTGO_GITHUB_MIRROR || `https://ghproxy.net/https://github.com/${REPO}/releases/download`;
15
+
16
+ function readInstallMarker() {
17
+ try {
18
+ const marker = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, MARKER), 'utf8'));
19
+ if (!marker.installRoot || !marker.binDir) return null;
20
+ return { installRoot: path.resolve(marker.installRoot), binDir: path.resolve(marker.binDir) };
21
+ } catch { return null; }
22
+ }
23
+
24
+ function download(url, output, timeoutMs = 15000, redirects = 0) {
25
+ return new Promise((resolve, reject) => {
26
+ if (redirects > 5) return reject(new Error('Too many download redirects.'));
27
+ const parsed = new URL(url);
28
+ const transport = parsed.protocol === 'http:' ? http : https;
29
+ const req = transport.get(parsed, { timeout: timeoutMs, headers: { 'user-agent': 'draftgo-cli' } }, (res) => {
30
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
31
+ res.resume();
32
+ return resolve(download(new URL(res.headers.location, parsed).toString(), output, timeoutMs, redirects + 1));
33
+ }
34
+ if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
35
+ const stream = fs.createWriteStream(output, { flags: 'wx' });
36
+ stream.on('error', reject);
37
+ res.on('error', reject);
38
+ stream.on('finish', () => stream.close(() => resolve(url)));
39
+ res.pipe(stream);
40
+ });
41
+ req.on('error', reject);
42
+ req.on('timeout', () => req.destroy(new Error('Download timed out.')));
43
+ });
44
+ }
45
+
46
+ async function downloadFirst(urls, output) {
47
+ const errors = [];
48
+ for (const url of urls) {
49
+ try { return { url: await download(url, output), output }; }
50
+ catch (error) { errors.push(`${url}: ${error.message}`); try { fs.rmSync(output, { force: true }); } catch {} }
51
+ }
52
+ throw new Error(`Unable to download DraftGo release.\n${errors.join('\n')}`);
53
+ }
54
+
55
+ function hashFile(file) {
56
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
57
+ }
58
+
59
+ function writeLauncher(binDir, packageDir) {
60
+ fs.mkdirSync(binDir, { recursive: true });
61
+ if (process.platform === 'win32') {
62
+ const launcher = path.join(binDir, 'draftgo.cmd');
63
+ fs.writeFileSync(launcher, `@echo off\r\nnode "${path.join(packageDir, 'bin', 'draftgo.js')}" %*\r\n`, 'ascii');
64
+ return launcher;
65
+ }
66
+ const launcher = path.join(binDir, 'draftgo');
67
+ fs.writeFileSync(launcher, `#!/usr/bin/env sh\nexec node "${path.join(packageDir, 'bin', 'draftgo.js')}" "$@"\n`, { mode: 0o755 });
68
+ fs.chmodSync(launcher, 0o755);
69
+ return launcher;
70
+ }
71
+
72
+ async function installRelease(version, expectedSha, marker = readInstallMarker()) {
73
+ if (!marker) return null;
74
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version))) throw new Error('Invalid release version.');
75
+ fs.mkdirSync(marker.installRoot, { recursive: true });
76
+ const tempDir = fs.mkdtempSync(path.join(marker.installRoot, '.update-'));
77
+ const archive = path.join(tempDir, `draftgo-cli-${version}.tgz`);
78
+ const asset = path.basename(archive);
79
+ const urls = [`${BASE_URL}/${asset}`, `${MIRROR}/v${version}/${asset}`, `https://github.com/${REPO}/releases/download/v${version}/${asset}`];
80
+ try {
81
+ const downloaded = await downloadFirst(urls, archive);
82
+ let checksum = expectedSha;
83
+ if (!checksum) {
84
+ const checksumFile = path.join(tempDir, `${asset}.sha256`);
85
+ await download(`${downloaded.url}.sha256`, checksumFile);
86
+ checksum = fs.readFileSync(checksumFile, 'utf8').trim().split(/\s+/)[0];
87
+ }
88
+ if (!/^[0-9a-f]{64}$/i.test(String(checksum)) || hashFile(archive).toLowerCase() !== checksum.toLowerCase()) {
89
+ throw new Error('DraftGo archive checksum mismatch.');
90
+ }
91
+ const staged = path.join(tempDir, 'package');
92
+ fs.mkdirSync(staged);
93
+ const extract = spawnSync('tar', ['-xzf', archive, '-C', staged, '--strip-components=1'], { encoding: 'utf8', shell: false });
94
+ if (extract.error || extract.status !== 0) throw new Error(extract.error ? extract.error.message : (extract.stderr || 'Unable to extract DraftGo release.'));
95
+ const installedVersion = JSON.parse(fs.readFileSync(path.join(staged, 'package.json'), 'utf8')).version;
96
+ if (installedVersion !== version) throw new Error(`Release version mismatch: expected ${version}, received ${installedVersion}.`);
97
+ fs.writeFileSync(path.join(staged, MARKER), JSON.stringify(marker, null, 2) + '\n');
98
+ const destination = path.join(marker.installRoot, version);
99
+ if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
100
+ fs.renameSync(staged, destination);
101
+ return { launcher: writeLauncher(marker.binDir, destination), destination };
102
+ } finally { fs.rmSync(tempDir, { recursive: true, force: true }); }
103
+ }
104
+
105
+ module.exports = { MARKER, readInstallMarker, installRelease, hashFile, writeLauncher };
@@ -1,11 +1,9 @@
1
1
  'use strict';
2
2
 
3
- // Thin compatibility layer over src/platforms.js + src/skill.js.
4
- // Each "installer" is just a platform descriptor exposing install/uninstall/
5
- // status methods that delegate to the generic renderer.
3
+ // Public Skill target registry backed by shared platform descriptors.
6
4
 
7
- const { platforms } = require('../platforms');
8
- const skill = require('../skill');
5
+ const { platforms } = require('./platforms');
6
+ const skill = require('./skill');
9
7
 
10
8
  function makeInstaller(p) {
11
9
  return {
@@ -1,14 +1,20 @@
1
1
  'use strict';
2
2
 
3
- // Fetch the latest draftgo-cli version from the npm registry.
3
+ // Fetch the latest draftgo-cli version from the DraftGo release endpoint.
4
4
  // Results are cached for 24 hours so update stays fast and deterministic.
5
5
 
6
+ const http = require('http');
6
7
  const https = require('https');
7
8
  const fs = require('fs');
8
9
  const os = require('os');
9
10
  const path = require('path');
10
11
 
11
- const REGISTRY_URL = 'https://registry.npmjs.org/draftgo-cli/latest';
12
+ const REPO = process.env.DRAFTGO_REPO || 'draftgo/draftgo-cli';
13
+ const RELEASE_BASE_URL = process.env.DRAFTGO_INSTALL_BASE_URL || 'https://draftgo.cn/draftgo-cli';
14
+ const RELEASE_URLS = [
15
+ `${RELEASE_BASE_URL}/latest.json`,
16
+ `https://api.github.com/repos/${REPO}/releases/latest`,
17
+ ];
12
18
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
13
19
  const CACHE_FILE = path.join(os.homedir(), '.draftgo-cli', 'update-check.json');
14
20
 
@@ -32,40 +38,54 @@ function writeCache(version) {
32
38
  }
33
39
  }
34
40
 
35
- function fetchLatestVersion(timeoutMs = 3000, opts = {}) {
36
- if (!opts.force) {
37
- const cached = readCache();
38
- if (cached) return Promise.resolve(cached);
39
- }
41
+ function requestVersion(url, timeoutMs, redirects = 0) {
40
42
  return new Promise((resolve) => {
43
+ if (redirects > 5) return resolve(null);
44
+ let parsed;
45
+ try { parsed = new URL(url); } catch { return resolve(null); }
46
+ const transport = parsed.protocol === 'http:' ? http : https;
41
47
  let req;
42
48
  try {
43
- req = https.get(
44
- REGISTRY_URL,
45
- { timeout: timeoutMs, headers: { accept: 'application/json' } },
46
- (res) => {
47
- if (res.statusCode !== 200) { res.resume(); return resolve(null); }
48
- let body = '';
49
- res.setEncoding('utf8');
50
- res.on('data', (c) => { body += c; });
51
- res.on('end', () => {
52
- try {
53
- const version = JSON.parse(body).version || null;
54
- if (version) writeCache(version);
55
- resolve(version);
56
- }
57
- catch { resolve(null); }
58
- });
49
+ req = transport.get(parsed, {
50
+ timeout: timeoutMs,
51
+ headers: { accept: 'application/json', 'user-agent': 'draftgo-cli' },
52
+ }, (res) => {
53
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
54
+ res.resume();
55
+ return resolve(requestVersion(new URL(res.headers.location, parsed).toString(), timeoutMs, redirects + 1));
59
56
  }
60
- );
61
- } catch {
62
- return resolve(null);
63
- }
57
+ if (res.statusCode !== 200) { res.resume(); return resolve(null); }
58
+ let body = '';
59
+ res.setEncoding('utf8');
60
+ res.on('data', (chunk) => {
61
+ body += chunk;
62
+ if (body.length > 64 * 1024) req.destroy();
63
+ });
64
+ res.on('end', () => {
65
+ try {
66
+ const payload = JSON.parse(body);
67
+ resolve(payload.version || String(payload.tag_name || '').replace(/^v/, '') || null);
68
+ } catch { resolve(null); }
69
+ });
70
+ });
71
+ } catch { return resolve(null); }
64
72
  req.on('error', () => resolve(null));
65
73
  req.on('timeout', () => { req.destroy(); resolve(null); });
66
74
  });
67
75
  }
68
76
 
77
+ async function fetchLatestVersion(timeoutMs = 3000, opts = {}) {
78
+ if (!opts.force) {
79
+ const cached = readCache();
80
+ if (cached) return Promise.resolve(cached);
81
+ }
82
+ for (const url of RELEASE_URLS) {
83
+ const version = await requestVersion(url, timeoutMs);
84
+ if (version) { writeCache(version); return version; }
85
+ }
86
+ return null;
87
+ }
88
+
69
89
  // Compare 'a.b.c' ignoring prerelease tags. Returns 1 / 0 / -1.
70
90
  function cmpSemver(a, b) {
71
91
  const pa = String(a).split('-')[0].split('.').map((x) => Number(x) || 0);
@@ -77,4 +97,4 @@ function cmpSemver(a, b) {
77
97
  return 0;
78
98
  }
79
99
 
80
- module.exports = { fetchLatestVersion, cmpSemver, readCache, CACHE_TTL_MS };
100
+ module.exports = { fetchLatestVersion, cmpSemver, readCache, requestVersion, CACHE_TTL_MS };