draftgo-cli 4.0.1 → 4.0.23
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 +87 -11
- package/package.json +9 -4
- package/resources/custom-service-sdk/ai.go +520 -0
- package/resources/custom-service-sdk/ai_test.go +156 -0
- package/resources/custom-service-sdk/auth_test.go +56 -0
- package/resources/custom-service-sdk/billing.go +596 -0
- package/resources/custom-service-sdk/billing_test.go +150 -0
- package/resources/custom-service-sdk/go.mod +3 -0
- package/resources/custom-service-sdk/manifest.json +77 -0
- package/resources/custom-service-sdk/platform.go +345 -0
- package/resources/custom-service-sdk/platform_logger_test.go +24 -0
- package/resources/custom-service-sdk/registration_test.go +39 -0
- package/resources/custom-service-sdk/resources.go +246 -0
- package/resources/custom-service-sdk/resources_billing_test.go +115 -0
- package/resources/custom-service-sdk/resources_files_test.go +57 -0
- package/resources/custom-service-sdk/resources_scope_test.go +87 -0
- package/resources/custom-service-sdk/sdk.go +208 -0
- package/resources/skill/SKILL.md +36 -88
- package/resources/skill/init/SKILL.md +4 -4
- package/resources/skill/manifest.json +5 -1
- package/resources/skill/references/aihub.md +25 -2
- package/resources/skill/references/app-api.md +56 -6
- package/resources/skill/references/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +4 -2
- package/resources/skill/references/checkout.md +17 -3
- package/resources/skill/references/custom-services.md +112 -47
- package/resources/skill/references/data.md +19 -4
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/diagnostics.md +51 -0
- package/resources/skill/references/frontend.md +34 -46
- package/resources/skill/references/mcp.md +33 -5
- package/resources/skill/references/methods.md +189 -0
- package/resources/skill/references/modules.md +37 -9
- package/resources/skill/references/runtime.md +23 -1
- package/src/cli.js +24 -0
- package/src/commandRegistry.js +9 -1
- package/src/commands/api.js +21 -10
- package/src/commands/apiKey.js +34 -0
- package/src/commands/capabilities.js +93 -0
- package/src/commands/checkout.js +1 -1
- package/src/commands/commit.js +1 -1
- package/src/commands/components.js +550 -0
- package/src/commands/conflict.js +1 -1
- package/src/commands/connect.js +18 -8
- package/src/commands/customService.js +20 -4
- package/src/commands/dataRange.js +33 -0
- package/src/commands/delete.js +12 -1
- package/src/commands/diff.js +18 -2
- package/src/commands/grant.js +29 -0
- package/src/commands/group.js +38 -0
- package/src/commands/help.js +64 -20
- package/src/commands/init.js +3 -3
- package/src/commands/map.js +145 -17
- package/src/commands/mcp.js +2 -2
- package/src/commands/reconcile.js +1 -1
- package/src/commands/role.js +32 -0
- package/src/commands/space.js +41 -0
- package/src/commands/status.js +110 -7
- package/src/commands/update.js +23 -11
- package/src/commands/verify.js +75 -0
- package/src/commands/worklog.js +6 -2
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +57 -0
- package/src/customServices.js +138 -18
- package/src/diffReport.js +106 -0
- package/src/index.js +2 -0
- package/src/localRuntime/compose.js +14 -17
- package/src/localRuntime/index.js +22 -23
- package/src/localRuntime/services.js +27 -36
- package/src/mcp/client.js +11 -2
- package/src/mcp/protocol.js +22 -2
- package/src/mcp/tools.js +14 -1
- package/src/platforms.js +9 -0
- package/src/projectConfig.js +6 -4
- package/src/releaseInstall.js +105 -0
- package/src/updateCheck.js +48 -28
- package/src/worklog.js +2 -1
- package/src/worktree/backend.js +1 -1
- package/src/worktree/index.js +7 -2
package/src/customServices.js
CHANGED
|
@@ -8,7 +8,11 @@ const AdmZip = require('adm-zip');
|
|
|
8
8
|
const { loadProjectConfig } = require('./projectConfig');
|
|
9
9
|
const { WorktreeError } = require('./worktree/errors');
|
|
10
10
|
|
|
11
|
-
const FILES = ['service.go', '
|
|
11
|
+
const FILES = ['service.go', 'service.json'];
|
|
12
|
+
const MANAGED_MODULE_FILES = ['go.mod', 'go.sum', 'draftgo_local_main.go', '.draftgo-managed.json'];
|
|
13
|
+
const MANAGED_SDK_DIRECTORY = '.draftgo-sdk';
|
|
14
|
+
const SDK_BUNDLE_ROOT = path.resolve(__dirname, '..', 'resources', 'custom-service-sdk');
|
|
15
|
+
const dependencyDirectivePattern = /^\s*\/\/\s*draftgo:require\s+([^\s@]+)@([^\s]+)\s*$/gm;
|
|
12
16
|
function root(projectDir) { return path.join(projectDir, '.draftgo', 'worktree', 'custom-services'); }
|
|
13
17
|
function baseRoot(projectDir) { return path.join(projectDir, '.draftgo', 'worktree', '.base', 'custom-services'); }
|
|
14
18
|
function conflictRoot(projectDir, id) { return path.join(projectDir, '.draftgo', 'conflicts', 'custom-services', safe(id)); }
|
|
@@ -18,9 +22,10 @@ function sha256(value) { return crypto.createHash('sha256').update(value).digest
|
|
|
18
22
|
function relative(projectDir, file) { return path.relative(projectDir, file).replace(/\\/g, '/'); }
|
|
19
23
|
function loadManifest(projectDir) {
|
|
20
24
|
const file = manifestPath(projectDir);
|
|
21
|
-
if (!fs.existsSync(file)) return { schema_version:
|
|
25
|
+
if (!fs.existsSync(file)) return { schema_version: 2, entries: {} };
|
|
22
26
|
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
23
|
-
if (
|
|
27
|
+
if (value && value.schema_version === 1) throw new WorktreeError('WORKTREE_SCHEMA_UNSUPPORTED', 'Custom-service worktree schema v1 is obsolete; create a new checkout with this CLI.');
|
|
28
|
+
if (!value || value.schema_version !== 2 || !value.entries || Array.isArray(value.entries)) throw new WorktreeError('INVALID_WORKTREE_MANIFEST', 'Invalid custom-service manifest.');
|
|
24
29
|
return value;
|
|
25
30
|
}
|
|
26
31
|
function atomicWrite(file, value) {
|
|
@@ -84,13 +89,104 @@ function readFiles(directory) {
|
|
|
84
89
|
}
|
|
85
90
|
function createArchive(files) {
|
|
86
91
|
const zip = new AdmZip();
|
|
87
|
-
|
|
92
|
+
// Archive hashes are part of the checkout/commit concurrency contract. Keep
|
|
93
|
+
// ZIP metadata deterministic so the same two user files always produce the
|
|
94
|
+
// same content hash, independent of wall-clock time.
|
|
95
|
+
for (const filename of FILES) {
|
|
96
|
+
zip.addFile(filename, files[filename]);
|
|
97
|
+
const entry = zip.getEntry(filename);
|
|
98
|
+
if (entry?.header) entry.header.time = new Date(0);
|
|
99
|
+
}
|
|
88
100
|
return zip.toBuffer();
|
|
89
101
|
}
|
|
90
102
|
function fileHashes(files) { return Object.fromEntries(FILES.map((filename) => [filename, sha256(files[filename])])); }
|
|
91
103
|
function checkoutHash(files) { return sha256(createArchive(files)); }
|
|
104
|
+
|
|
105
|
+
function sdkFingerprint(files) {
|
|
106
|
+
const hash = crypto.createHash('sha256');
|
|
107
|
+
for (const filename of ['go.mod', 'go.sum']) { hash.update(filename); if (files[filename]) hash.update(files[filename]); }
|
|
108
|
+
for (const filename of Object.keys(files).filter((item) => item.endsWith('.go')).sort()) { hash.update(filename); hash.update(files[filename]); }
|
|
109
|
+
return hash.digest('hex');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadSDKBundle() {
|
|
113
|
+
const manifestFile = path.join(SDK_BUNDLE_ROOT, 'manifest.json');
|
|
114
|
+
if (!fs.existsSync(manifestFile)) throw new WorktreeError('LOCAL_SDK_MISSING', 'draftgo-cli is missing its managed custom-service SDK; reinstall or upgrade the CLI.');
|
|
115
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
116
|
+
if (!manifest || manifest.schema_version !== 1 || !/^[a-f0-9]{64}$/.test(String(manifest.fingerprint || '')) || !Array.isArray(manifest.files)) {
|
|
117
|
+
throw new WorktreeError('LOCAL_SDK_INVALID', 'draftgo-cli contains an invalid managed custom-service SDK manifest.');
|
|
118
|
+
}
|
|
119
|
+
const files = {};
|
|
120
|
+
for (const item of manifest.files) {
|
|
121
|
+
const filename = String(item?.path || '');
|
|
122
|
+
if (!/^(?:(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\.go|go\.mod|go\.sum)$/.test(filename) || filename.includes('..')) {
|
|
123
|
+
throw new WorktreeError('LOCAL_SDK_INVALID', `Managed custom-service SDK contains an invalid path: ${filename || '<empty>'}.`);
|
|
124
|
+
}
|
|
125
|
+
const content = fs.readFileSync(path.join(SDK_BUNDLE_ROOT, filename));
|
|
126
|
+
if (sha256(content) !== item.sha256) throw new WorktreeError('LOCAL_SDK_INVALID', `Managed custom-service SDK hash mismatch: ${filename}.`);
|
|
127
|
+
files[filename] = content;
|
|
128
|
+
}
|
|
129
|
+
if (!files['go.mod'] || !Object.keys(files).some((filename) => filename.endsWith('.go'))) {
|
|
130
|
+
throw new WorktreeError('LOCAL_SDK_INVALID', 'Managed custom-service SDK is incomplete.');
|
|
131
|
+
}
|
|
132
|
+
if (sdkFingerprint(files) !== manifest.fingerprint) throw new WorktreeError('LOCAL_SDK_INVALID', 'Managed custom-service SDK fingerprint mismatch.');
|
|
133
|
+
return { manifest, files };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function dependencyRequirements(source) {
|
|
137
|
+
const byModule = new Map();
|
|
138
|
+
for (const match of String(source).matchAll(dependencyDirectivePattern)) {
|
|
139
|
+
const moduleName = match[1].trim(); const version = match[2].trim();
|
|
140
|
+
if (byModule.has(moduleName) && byModule.get(moduleName) !== version) {
|
|
141
|
+
throw new WorktreeError('INVALID_SERVICE_DEPENDENCY', `Dependency ${moduleName} declares conflicting versions.`);
|
|
142
|
+
}
|
|
143
|
+
byModule.set(moduleName, version);
|
|
144
|
+
}
|
|
145
|
+
return [...byModule.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([module, version]) => ({ module, version }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sdkGoVersion(bundle) {
|
|
149
|
+
const match = bundle.files['go.mod'].toString('utf8').match(/^go\s+([^\s]+)\s*$/m);
|
|
150
|
+
if (!match) throw new WorktreeError('LOCAL_SDK_INVALID', 'Managed custom-service SDK go.mod has no Go version.');
|
|
151
|
+
return match[1];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function managedModule(source, bundle) {
|
|
155
|
+
const requirements = dependencyRequirements(source);
|
|
156
|
+
const lines = [
|
|
157
|
+
'// Code generated by draftgo-cli. DO NOT EDIT.',
|
|
158
|
+
'// Local development only; DraftGo uploads only service.go and service.json.',
|
|
159
|
+
'module draftgo.local/customservice', '', `go ${sdkGoVersion(bundle)}`, '', 'require (', '\tdraftgo/sdk v0.0.0',
|
|
160
|
+
...requirements.map((item) => `\t${item.module} ${item.version}`),
|
|
161
|
+
')', '', `replace draftgo/sdk => ./${MANAGED_SDK_DIRECTORY}`, '',
|
|
162
|
+
];
|
|
163
|
+
return { content: `${lines.join('\n')}`, requirements };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function syncManagedDevelopmentFiles(directory) {
|
|
167
|
+
const bundle = loadSDKBundle();
|
|
168
|
+
const source = fs.readFileSync(path.join(directory, 'service.go'), 'utf8');
|
|
169
|
+
const moduleValue = managedModule(source, bundle); const moduleHash = sha256(moduleValue.content);
|
|
170
|
+
const markerFile = path.join(directory, '.draftgo-managed.json');
|
|
171
|
+
let prior = null;
|
|
172
|
+
try { prior = JSON.parse(fs.readFileSync(markerFile, 'utf8')); } catch { /* generated state is disposable */ }
|
|
173
|
+
const preserveGoSum = prior?.schema_version === 1 && prior.module_hash === moduleHash && fs.existsSync(path.join(directory, 'go.sum'));
|
|
174
|
+
|
|
175
|
+
const sdkDirectory = path.join(directory, MANAGED_SDK_DIRECTORY);
|
|
176
|
+
fs.rmSync(sdkDirectory, { recursive: true, force: true }); fs.mkdirSync(sdkDirectory, { recursive: true });
|
|
177
|
+
for (const [filename, content] of Object.entries(bundle.files)) atomicWrite(path.join(sdkDirectory, filename), content);
|
|
178
|
+
atomicWrite(path.join(directory, 'go.mod'), moduleValue.content);
|
|
179
|
+
if (!preserveGoSum) atomicWrite(path.join(directory, 'go.sum'), '');
|
|
180
|
+
atomicWrite(path.join(directory, 'draftgo_local_main.go'), `// Code generated by draftgo-cli. DO NOT EDIT.\n// Local compile entrypoint; DraftGo never uploads this file.\npackage main\n\nimport "draftgo/sdk"\n\nfunc main() {\n\tapp := sdk.NewApp()\n\tRegister(app)\n}\n`);
|
|
181
|
+
atomicWrite(markerFile, `${JSON.stringify({
|
|
182
|
+
schema_version: 1, owner: 'draftgo-cli', notice: 'Generated local-development files; do not edit or commit.',
|
|
183
|
+
sdk_fingerprint: bundle.manifest.fingerprint, module_hash: moduleHash, requirements: moduleValue.requirements,
|
|
184
|
+
}, null, 2)}\n`);
|
|
185
|
+
return { sdk_fingerprint: bundle.manifest.fingerprint, module_hash: moduleHash, requirements: moduleValue.requirements };
|
|
186
|
+
}
|
|
92
187
|
function remoteFileHashes(remote) {
|
|
93
|
-
|
|
188
|
+
if (remote.checkout_schema_version != null && Number(remote.checkout_schema_version) !== 2) throw new WorktreeError('WORKTREE_SCHEMA_UNSUPPORTED', 'DraftGo custom-service checkout schema is not supported; upgrade DraftGo and create a new checkout.');
|
|
189
|
+
return { 'service.go': remote.source_hash, 'service.json': remote.metadata_hash };
|
|
94
190
|
}
|
|
95
191
|
function filesMatchRemote(files, remote) {
|
|
96
192
|
const local = fileHashes(files); const expected = remoteFileHashes(remote);
|
|
@@ -129,6 +225,7 @@ function localState(entry) {
|
|
|
129
225
|
return { local, base, local_hashes: localHashes, base_hashes: baseHashes, base_valid: baseValid,
|
|
130
226
|
modified: FILES.some((filename) => localHashes[filename] !== baseHashes[filename]) };
|
|
131
227
|
}
|
|
228
|
+
|
|
132
229
|
async function checkout(projectDir, ids, options = {}) {
|
|
133
230
|
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
134
231
|
const targets = uniqueIDs(ids);
|
|
@@ -140,6 +237,7 @@ async function checkout(projectDir, ids, options = {}) {
|
|
|
140
237
|
const archive = await download(config, remote, options); const files = archiveFiles(archive);
|
|
141
238
|
const service = JSON.parse(files['service.json'].toString('utf8')); const localDir = old?.local_dir || serviceDir(projectDir, id, service.slug); const baseDir = serviceBaseDir(projectDir, id);
|
|
142
239
|
writeFiles(localDir, files); writeFiles(baseDir, files);
|
|
240
|
+
syncManagedDevelopmentFiles(localDir);
|
|
143
241
|
const entry = { server: config.server, resource_type: 'custom_services', resource_id: String(id), title: remote.title,
|
|
144
242
|
slug: service.slug, local_dir: localDir, base_dir: baseDir, local_path: relative(projectDir, localDir), base_path: relative(projectDir, baseDir),
|
|
145
243
|
content_type: remote.content_type, base_revision: remote.base_revision, base_etag: remote.etag,
|
|
@@ -171,13 +269,20 @@ function diffFile(baseFile, localFile, filename) {
|
|
|
171
269
|
}
|
|
172
270
|
function diff(projectDir, id) {
|
|
173
271
|
const entry = getEntry(projectDir, id); if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
174
|
-
const state = localState(entry);
|
|
175
|
-
|
|
272
|
+
const state = localState(entry);
|
|
273
|
+
const files = FILES.map((filename) => {
|
|
274
|
+
const output = diffFile(path.join(entry.base_dir, filename), path.join(entry.local_dir, filename), filename);
|
|
275
|
+
return { filename, changed: state.local_hashes[filename] !== state.base_hashes[filename], output };
|
|
276
|
+
});
|
|
277
|
+
const result = { changed: files.some((file) => file.changed), entry,
|
|
278
|
+
files: files.map(({ filename, changed }) => ({ filename, changed })), output: files.map((file) => file.output).filter(Boolean).join('\n') };
|
|
279
|
+
Object.defineProperty(result, 'file_outputs', { value: files, enumerable: false });
|
|
280
|
+
return result;
|
|
176
281
|
}
|
|
177
282
|
async function writeConflict(projectDir, entry, remote, archive) {
|
|
178
283
|
const directory = conflictRoot(projectDir, entry.resource_id); const baseDir = path.join(directory, 'base'); const localDir = path.join(directory, 'local'); const remoteDir = path.join(directory, 'remote');
|
|
179
284
|
writeFiles(baseDir, readFiles(entry.base_dir)); writeFiles(localDir, readFiles(entry.local_dir)); writeFiles(remoteDir, archiveFiles(archive));
|
|
180
|
-
const record = { schema_version:
|
|
285
|
+
const record = { schema_version: 2, status: 'unresolved', code: 'RESOURCE_VERSION_CONFLICT', resource_type: 'custom_services', resource_id: entry.resource_id,
|
|
181
286
|
base_path: relative(projectDir, baseDir), local_path: relative(projectDir, localDir), remote_path: relative(projectDir, remoteDir), worktree_local_path: entry.local_path,
|
|
182
287
|
expected_revision: entry.base_revision, actual_revision: remote.base_revision, expected_hash: entry.base_hash, actual_hash: remote.content_hash,
|
|
183
288
|
actual_etag: remote.etag, actual_updated_at: remote.updated_at, created_at: new Date().toISOString() };
|
|
@@ -209,6 +314,7 @@ async function commit(projectDir, ids, options = {}) {
|
|
|
209
314
|
const preflight = await Promise.allSettled(targets.map(async (id) => {
|
|
210
315
|
const entry = manifest.entries[entryKey(id)]; if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
211
316
|
if (entry.server !== config.server) throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
317
|
+
syncManagedDevelopmentFiles(entry.local_dir);
|
|
212
318
|
const state = localState(entry); if (!state.base_valid) throw new WorktreeError('BASE_HASH_MISMATCH', `Custom service ${id} base does not match its manifest.`);
|
|
213
319
|
if (!state.modified) return { id, entry, state, unchanged: true };
|
|
214
320
|
const current = await metadata(config, id, options);
|
|
@@ -240,7 +346,7 @@ async function commit(projectDir, ids, options = {}) {
|
|
|
240
346
|
if ([409, 412].includes(error.status)) { const latest = await metadata(config, id, options); const remoteArchive = await download(config, latest, options); error.details = await writeConflict(projectDir, entry, latest, remoteArchive); }
|
|
241
347
|
throw error;
|
|
242
348
|
}
|
|
243
|
-
applyRemoteEntry(entry, committed, state.local); manifest.entries[entryKey(id)] = entry;
|
|
349
|
+
applyRemoteEntry(entry, committed, state.local); syncManagedDevelopmentFiles(entry.local_dir); manifest.entries[entryKey(id)] = entry;
|
|
244
350
|
return { resource_type: 'custom_services', resource_id: String(id), status: 'committed', revision: committed.base_revision, hash: committed.content_hash };
|
|
245
351
|
}));
|
|
246
352
|
const results = uploaded.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
@@ -283,7 +389,7 @@ function syncCheckoutEntry(manifest, id, remote) {
|
|
|
283
389
|
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
284
390
|
const state = localState(entry);
|
|
285
391
|
if (state.modified) throw new WorktreeError('LOCAL_CHANGES_PRESENT', `Custom service ${id} changed locally during its remote lifecycle.`);
|
|
286
|
-
applyRemoteEntry(entry, remote, state.local); manifest.entries[entryKey(id)] = entry;
|
|
392
|
+
applyRemoteEntry(entry, remote, state.local); syncManagedDevelopmentFiles(entry.local_dir); manifest.entries[entryKey(id)] = entry;
|
|
287
393
|
return entry;
|
|
288
394
|
}
|
|
289
395
|
async function validateCommitted(projectDir, id, config, manifest, options = {}) {
|
|
@@ -314,15 +420,29 @@ function parseHandler(handler) {
|
|
|
314
420
|
return { kind: 'manual', name: value };
|
|
315
421
|
}
|
|
316
422
|
async function test(projectDir, id, input = {}, options = {}) {
|
|
317
|
-
const config = options.config || loadProjectConfig(projectDir);
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
423
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
424
|
+
const source = String(options.source || 'draft').toLowerCase();
|
|
425
|
+
if (!['auto', 'draft', 'published'].includes(source)) throw new Error('source must be auto, draft, or published.');
|
|
426
|
+
let manifest;
|
|
427
|
+
let entry;
|
|
428
|
+
let revision;
|
|
429
|
+
if (source === 'draft') {
|
|
430
|
+
await commit(projectDir, [id], { ...options, config });
|
|
431
|
+
manifest = loadManifest(projectDir);
|
|
432
|
+
const validation = await validateCommitted(projectDir, id, config, manifest, options);
|
|
433
|
+
entry = manifest.entries[entryKey(id)];
|
|
434
|
+
revision = entry.base_revision;
|
|
435
|
+
if (!validation.cached) saveManifest(projectDir, manifest);
|
|
436
|
+
} else {
|
|
437
|
+
manifest = loadManifest(projectDir);
|
|
438
|
+
entry = manifest.entries[entryKey(id)];
|
|
439
|
+
}
|
|
440
|
+
const result = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/trial`, {
|
|
441
|
+
source,
|
|
322
442
|
input, selector: parseHandler(options.handler), headers: options.headers, user: options.user,
|
|
323
|
-
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny', revision
|
|
443
|
+
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny', revision,
|
|
324
444
|
}, options);
|
|
325
|
-
if (result.checkout) { syncCheckoutEntry(manifest, id, result.checkout); saveManifest(projectDir, manifest); }
|
|
445
|
+
if (result.checkout && entry) { syncCheckoutEntry(manifest, id, result.checkout); saveManifest(projectDir, manifest); }
|
|
326
446
|
return result;
|
|
327
447
|
}
|
|
328
448
|
async function publish(projectDir, ids, options = {}) {
|
|
@@ -360,4 +480,4 @@ async function publish(projectDir, ids, options = {}) {
|
|
|
360
480
|
return results;
|
|
361
481
|
}
|
|
362
482
|
|
|
363
|
-
module.exports = { FILES, checkout, commit, diff, validate, test, publish, inspectRemote, loadManifest, getEntry, metadata, archiveFiles, createArchive, serviceConflicts, showConflict, resolveConflict, parseHandler, validationStale };
|
|
483
|
+
module.exports = { FILES, MANAGED_MODULE_FILES, MANAGED_SDK_DIRECTORY, checkout, commit, diff, validate, test, publish, inspectRemote, loadManifest, getEntry, metadata, archiveFiles, createArchive, serviceConflicts, showConflict, resolveConflict, parseHandler, validationStale, dependencyRequirements, managedModule, syncManagedDevelopmentFiles, loadSDKBundle, sdkFingerprint };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function lineStats(output) {
|
|
6
|
+
let additions = 0;
|
|
7
|
+
let deletions = 0;
|
|
8
|
+
let inHunk = false;
|
|
9
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
10
|
+
if (line.startsWith('@@ ')) {
|
|
11
|
+
inHunk = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (line.startsWith('diff --git ')) {
|
|
15
|
+
inHunk = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!inHunk || line === '\') continue;
|
|
19
|
+
if (line.startsWith('+')) additions += 1;
|
|
20
|
+
else if (line.startsWith('-')) deletions += 1;
|
|
21
|
+
}
|
|
22
|
+
return { additions, deletions, lines_changed: additions + deletions };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function byteSize(file) {
|
|
26
|
+
return fs.statSync(file).size;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileStat(file) {
|
|
30
|
+
const base_bytes = byteSize(file.base_path);
|
|
31
|
+
const local_bytes = byteSize(file.local_path);
|
|
32
|
+
return {
|
|
33
|
+
path: file.path,
|
|
34
|
+
changed: Boolean(file.changed),
|
|
35
|
+
...lineStats(file.output),
|
|
36
|
+
base_bytes,
|
|
37
|
+
local_bytes,
|
|
38
|
+
bytes_delta: local_bytes - base_bytes,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resourceMetadata(entry) {
|
|
43
|
+
return {
|
|
44
|
+
resource_type: entry.resource_type,
|
|
45
|
+
resource_id: entry.resource_id,
|
|
46
|
+
title: entry.title || null,
|
|
47
|
+
route: entry.route || null,
|
|
48
|
+
slug: entry.slug || null,
|
|
49
|
+
local_path: entry.local_path || null,
|
|
50
|
+
base_path: entry.base_path || null,
|
|
51
|
+
base_version: entry.base_version ?? null,
|
|
52
|
+
base_revision: entry.base_revision ?? null,
|
|
53
|
+
base_etag: entry.base_etag ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function report(entry, files) {
|
|
58
|
+
const file_stats = files.map(fileStat);
|
|
59
|
+
const changed_files = file_stats.filter((file) => file.changed);
|
|
60
|
+
const additions = changed_files.reduce((total, file) => total + file.additions, 0);
|
|
61
|
+
const deletions = changed_files.reduce((total, file) => total + file.deletions, 0);
|
|
62
|
+
const base_bytes = file_stats.reduce((total, file) => total + file.base_bytes, 0);
|
|
63
|
+
const local_bytes = file_stats.reduce((total, file) => total + file.local_bytes, 0);
|
|
64
|
+
return {
|
|
65
|
+
resource: resourceMetadata(entry),
|
|
66
|
+
changed: changed_files.length > 0,
|
|
67
|
+
bytes: { base: base_bytes, local: local_bytes, delta: local_bytes - base_bytes },
|
|
68
|
+
stats: {
|
|
69
|
+
files_changed: changed_files.length,
|
|
70
|
+
additions,
|
|
71
|
+
deletions,
|
|
72
|
+
lines_changed: additions + deletions,
|
|
73
|
+
files: file_stats,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resourceLabel(resource) {
|
|
79
|
+
return `${resource.resource_type} ${resource.resource_id}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatSummary(value) {
|
|
83
|
+
const { resource, bytes } = value;
|
|
84
|
+
const fields = [
|
|
85
|
+
`${resourceLabel(resource)}: ${value.changed ? 'changed' : 'no local changes'}`,
|
|
86
|
+
`title: ${resource.title || '-'}`,
|
|
87
|
+
`path: ${resource.local_path || '-'}`,
|
|
88
|
+
`base version: ${resource.base_version ?? '-'}`,
|
|
89
|
+
`base revision: ${resource.base_revision ?? '-'}`,
|
|
90
|
+
`bytes: ${bytes.base} -> ${bytes.local} (${bytes.delta >= 0 ? '+' : ''}${bytes.delta})`,
|
|
91
|
+
];
|
|
92
|
+
if (resource.route) fields.splice(2, 0, `route: ${resource.route}`);
|
|
93
|
+
return `${fields.join('\n')}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatStat(value) {
|
|
97
|
+
const { stats } = value;
|
|
98
|
+
const files = stats.files.filter((file) => file.changed).map((file) => {
|
|
99
|
+
const marker = `${file.additions ? `${'+'.repeat(Math.min(file.additions, 20))}` : ''}${file.deletions ? `${'-'.repeat(Math.min(file.deletions, 20))}` : ''}` || '0';
|
|
100
|
+
return `${file.path} | ${file.lines_changed} ${marker}`;
|
|
101
|
+
});
|
|
102
|
+
const summary = `${stats.files_changed} file${stats.files_changed === 1 ? '' : 's'} changed, ${stats.additions} insertion${stats.additions === 1 ? '' : 's'}(+), ${stats.deletions} deletion${stats.deletions === 1 ? '' : 's'}(-)`;
|
|
103
|
+
return `${formatSummary(value)}${files.length ? `${files.join('\n')}\n` : ''}${summary}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { lineStats, report, formatSummary, formatStat };
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { parse } = require('./cli');
|
|
5
5
|
const { resolveCommand } = require('./commandRegistry');
|
|
6
|
+
const { configureConsoleUtf8 } = require('./consoleEncoding');
|
|
6
7
|
const log = require('./logger');
|
|
7
8
|
|
|
8
9
|
async function run(argv) {
|
|
10
|
+
configureConsoleUtf8();
|
|
9
11
|
const { command, positional, flags, errors } = parse(argv);
|
|
10
12
|
|
|
11
13
|
if (errors.length) {
|
|
@@ -44,7 +44,7 @@ function generate(projectDir, opts) {
|
|
|
44
44
|
const env = readEnv(projectDir);
|
|
45
45
|
const projectName = sanitizeProjectName(opts.projectName, 'draftgo');
|
|
46
46
|
const database = opts.mysql.database;
|
|
47
|
-
const
|
|
47
|
+
const qdrant = opts.qdrant || { host: '127.0.0.1', port: 26333, apiKey: '' };
|
|
48
48
|
|
|
49
49
|
env.APP_PORT = String(opts.appPort || 3000);
|
|
50
50
|
env.SECRET_KEY = env.SECRET_KEY || randomHex(32);
|
|
@@ -53,14 +53,12 @@ function generate(projectDir, opts) {
|
|
|
53
53
|
env.REDIS_PORT = String(opts.redis.port);
|
|
54
54
|
env.REDIS_PASSWORD = opts.redis.password || '';
|
|
55
55
|
env.REDIS_KEY_PREFIX = database;
|
|
56
|
-
env.
|
|
57
|
-
env.
|
|
58
|
-
env.
|
|
59
|
-
env.MILVUS_DATABASE = 'default';
|
|
60
|
-
env.MILVUS_COLLECTION_PREFIX = database;
|
|
56
|
+
env.QDRANT_URL = `http://${dockerHost(qdrant.host)}:${qdrant.port}`;
|
|
57
|
+
env.QDRANT_API_KEY = qdrant.apiKey || '';
|
|
58
|
+
env.QDRANT_COLLECTION_PREFIX = database;
|
|
61
59
|
|
|
62
60
|
const yaml = [
|
|
63
|
-
'# Generated by draftgo-cli. Shared MySQL, Redis, and
|
|
61
|
+
'# Generated by draftgo-cli. Shared MySQL, Redis, and Qdrant stay on the host.',
|
|
64
62
|
`name: ${projectName}`,
|
|
65
63
|
'services:',
|
|
66
64
|
' app:',
|
|
@@ -80,23 +78,22 @@ function generate(projectDir, opts) {
|
|
|
80
78
|
' REDIS_PASSWORD: ${REDIS_PASSWORD}',
|
|
81
79
|
' REDIS_DB: "0"',
|
|
82
80
|
' REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX}',
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
81
|
+
' QDRANT_URL: ${QDRANT_URL}',
|
|
82
|
+
' QDRANT_API_KEY: ${QDRANT_API_KEY}',
|
|
83
|
+
' QDRANT_COLLECTION_PREFIX: ${QDRANT_COLLECTION_PREFIX}',
|
|
84
|
+
' LOCAL_UPLOAD_DIR: /app/data/storage/uploads',
|
|
85
|
+
' KB_STORAGE_DIR: /app/data/storage/kb',
|
|
86
|
+
' DRAFTGO_GO_SERVICE_CACHE_DIR: /app/data/storage/custom-services/cache',
|
|
87
|
+
' MCP_TEMP_DIR: /app/data/storage/mcp/tmp',
|
|
89
88
|
' volumes:',
|
|
90
|
-
' - ./data/
|
|
91
|
-
' - ./data/logs:/app/backend/logs',
|
|
92
|
-
' - ./data/db:/app/backend/db',
|
|
89
|
+
' - ./data/storage:/app/data/storage',
|
|
93
90
|
' extra_hosts:',
|
|
94
91
|
' - "host.docker.internal:host-gateway"',
|
|
95
92
|
'',
|
|
96
93
|
].join('\n');
|
|
97
94
|
writeText(composePath(projectDir), yaml);
|
|
98
95
|
writeEnv(projectDir, env);
|
|
99
|
-
for (const subdir of ['data/uploads', 'data/
|
|
96
|
+
for (const subdir of ['data/storage', 'data/storage/uploads', 'data/storage/kb', 'data/storage/mcp/tmp', 'data/storage/custom-services/cache']) {
|
|
100
97
|
try { fs.mkdirSync(path.join(root, subdir), { recursive: true }); } catch {}
|
|
101
98
|
}
|
|
102
99
|
return { dir: root, composeFile: composePath(projectDir), envFile: envPath(projectDir), projectName, env };
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const log = require('../logger');
|
|
5
|
-
const {
|
|
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,
|
|
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
|
|
91
|
+
async function promptQdrantConnection() {
|
|
92
92
|
while (true) {
|
|
93
|
-
const host = await askRequired('
|
|
94
|
-
const port = Number(await askRequired('
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
104
|
-
log.step('Checking
|
|
105
|
-
let conn = { ...defaults.
|
|
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('
|
|
108
|
-
if (!startService(docker, '
|
|
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
|
|
111
|
-
log.warn(' Default unauthenticated
|
|
112
|
-
conn = await
|
|
109
|
+
if (!await probeQdrant(conn)) {
|
|
110
|
+
log.warn(' Default unauthenticated Qdrant REST health check failed.');
|
|
111
|
+
conn = await promptQdrantConnection();
|
|
113
112
|
}
|
|
114
|
-
log.ok(`
|
|
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
|
|
192
|
+
const qdrant = await planQdrant(docker);
|
|
194
193
|
|
|
195
|
-
const out = compose.generate(projectDir, { projectName, appPort, mysql, redis,
|
|
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
|
|
203
|
-
if (token.length < 10) log.err('
|
|
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);
|
|
@@ -5,6 +5,8 @@
|
|
|
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
|
-
|
|
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', '
|
|
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
|
-
'
|
|
70
|
-
' image:
|
|
71
|
-
' container_name: draftgo-local-
|
|
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:
|
|
76
|
+
' - "127.0.0.1:26333:6333"',
|
|
83
77
|
' volumes:',
|
|
84
|
-
' - ./data/
|
|
78
|
+
' - ./data/qdrant:/qdrant/storage',
|
|
85
79
|
' healthcheck:',
|
|
86
|
-
' test: ["CMD", "
|
|
80
|
+
' test: ["CMD-SHELL", "bash -c \':> /dev/tcp/127.0.0.1/6333\'"]',
|
|
87
81
|
' interval: 10s',
|
|
88
82
|
' timeout: 5s',
|
|
89
|
-
' retries:
|
|
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
|
|
120
|
+
function probeQdrant({ host, port, apiKey = '' }) {
|
|
127
121
|
return new Promise((resolve) => {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
152
|
+
probeQdrant,
|
|
162
153
|
startService,
|
|
163
154
|
};
|