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.
- package/README.md +169 -297
- package/package.json +12 -7
- package/resources/custom-service-sdk/ai.go +520 -0
- package/resources/custom-service-sdk/ai_test.go +156 -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 +72 -0
- package/resources/custom-service-sdk/platform.go +360 -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 -87
- package/resources/skill/init/SKILL.md +9 -14
- package/resources/skill/manifest.json +6 -2
- package/resources/skill/references/aihub.md +28 -5
- 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 +21 -7
- package/resources/skill/references/custom-services.md +124 -222
- package/resources/skill/references/data.md +22 -6
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/diagnostics.md +51 -0
- package/resources/skill/references/frontend.md +93 -499
- package/resources/skill/references/mcp.md +65 -101
- package/resources/skill/references/methods.md +189 -0
- package/resources/skill/references/modules.md +36 -8
- package/resources/skill/references/runtime.md +26 -3
- package/resources/skill/story/SKILL.md +1 -2
- package/src/apiContractCache.js +112 -0
- package/src/cli.js +24 -20
- package/src/commandRegistry.js +15 -12
- package/src/commands/api.js +41 -10
- package/src/commands/apiKey.js +34 -0
- package/src/commands/capabilities.js +93 -0
- package/src/commands/check.js +1 -10
- 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 +22 -8
- package/src/commands/dataRange.js +33 -0
- package/src/commands/delete.js +34 -46
- package/src/commands/deploy.js +1 -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 +80 -51
- package/src/commands/init.js +6 -12
- package/src/commands/listTargets.js +1 -1
- package/src/commands/local.js +2 -6
- package/src/commands/map.js +145 -28
- 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 +111 -8
- package/src/commands/uninstall.js +3 -3
- package/src/commands/update.js +24 -12
- package/src/commands/verify.js +118 -21
- package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
- package/src/commands/worklog.js +90 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +57 -0
- package/src/customServices.js +278 -41
- package/src/diffReport.js +106 -0
- package/src/index.js +2 -0
- package/src/{localdev → localRuntime}/compose.js +14 -17
- package/src/{localdev → localRuntime}/detect.js +1 -1
- package/src/{localdev → localRuntime}/index.js +22 -23
- package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
- package/src/{localdev → localRuntime}/services.js +28 -37
- package/src/mcp/client.js +11 -2
- package/src/mcp/protocol.js +2 -2
- package/src/mcp/tools.js +14 -1
- package/src/platforms.js +9 -0
- package/src/projectConfig.js +8 -4
- package/src/releaseInstall.js +105 -0
- package/src/{installers/index.js → targets.js} +3 -5
- package/src/updateCheck.js +48 -28
- package/src/worklog.js +275 -0
- package/src/workspaceHealth.js +1 -1
- package/src/worktree/backend.js +1 -1
- package/src/worktree/index.js +86 -51
- package/src/changelog.js +0 -276
- package/src/commands/changelog.js +0 -24
- package/src/commands/localDev.js +0 -9
- package/src/commands/sync.js +0 -46
- package/src/commands/task.js +0 -408
- package/src/commands/verifyUiCompat.js +0 -16
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,18 +22,20 @@ 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) {
|
|
27
32
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
-
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
33
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
|
|
29
34
|
fs.writeFileSync(temporary, value); fs.renameSync(temporary, file);
|
|
30
35
|
}
|
|
31
36
|
function saveManifest(projectDir, value) { value.updated_at = new Date().toISOString(); atomicWrite(manifestPath(projectDir), `${JSON.stringify(value, null, 2)}\n`); }
|
|
32
37
|
function entryKey(id) { return String(id); }
|
|
38
|
+
function uniqueIDs(ids) { return [...new Set(ids.map(String).map((id) => id.trim()).filter(Boolean))]; }
|
|
33
39
|
function serviceDir(projectDir, id, slug) { return path.join(root(projectDir), `${safe(slug || id)}-${safe(id)}`); }
|
|
34
40
|
function serviceBaseDir(projectDir, id) { return path.join(baseRoot(projectDir), safe(id)); }
|
|
35
41
|
function parseEnvelope(text, response) {
|
|
@@ -83,18 +89,135 @@ function readFiles(directory) {
|
|
|
83
89
|
}
|
|
84
90
|
function createArchive(files) {
|
|
85
91
|
const zip = new AdmZip();
|
|
86
|
-
|
|
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
|
+
}
|
|
87
100
|
return zip.toBuffer();
|
|
88
101
|
}
|
|
89
102
|
function fileHashes(files) { return Object.fromEntries(FILES.map((filename) => [filename, sha256(files[filename])])); }
|
|
90
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
|
+
}
|
|
91
187
|
function remoteFileHashes(remote) {
|
|
92
|
-
|
|
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 };
|
|
93
190
|
}
|
|
94
191
|
function filesMatchRemote(files, remote) {
|
|
95
192
|
const local = fileHashes(files); const expected = remoteFileHashes(remote);
|
|
96
193
|
return FILES.every((filename) => expected[filename] && local[filename] === expected[filename]);
|
|
97
194
|
}
|
|
195
|
+
function canonicalFiles(files, remote) {
|
|
196
|
+
if (!remote || typeof remote.service_metadata_json !== 'string') {
|
|
197
|
+
throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo did not return canonical custom-service metadata.');
|
|
198
|
+
}
|
|
199
|
+
const canonical = { ...files, 'service.json': Buffer.from(remote.service_metadata_json, 'utf8') };
|
|
200
|
+
JSON.parse(canonical['service.json'].toString('utf8'));
|
|
201
|
+
const actual = fileHashes(canonical); const expected = remoteFileHashes(remote);
|
|
202
|
+
for (const filename of FILES) {
|
|
203
|
+
if (!/^[a-f0-9]{64}$/i.test(String(expected[filename] || '')) || actual[filename] !== expected[filename]) {
|
|
204
|
+
throw new WorktreeError('HASH_MISMATCH', `Canonical custom-service ${filename} does not match checkout metadata.`, {
|
|
205
|
+
filename, expected_hash: expected[filename] || null, actual_hash: actual[filename],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return canonical;
|
|
210
|
+
}
|
|
211
|
+
function applyRemoteEntry(entry, remote, files) {
|
|
212
|
+
const canonical = canonicalFiles(files, remote);
|
|
213
|
+
writeFiles(entry.local_dir, canonical); writeFiles(entry.base_dir, canonical);
|
|
214
|
+
entry.title = remote.title; entry.slug = JSON.parse(canonical['service.json'].toString('utf8')).slug;
|
|
215
|
+
entry.file_hashes = fileHashes(canonical); entry.base_hash = remote.content_hash;
|
|
216
|
+
entry.base_revision = remote.base_revision; entry.base_etag = remote.etag;
|
|
217
|
+
entry.checkout_source = remote.checkout_source; entry.updated_at = remote.updated_at;
|
|
218
|
+
entry.updated_by = remote.updated_by; entry.committed_at = new Date().toISOString();
|
|
219
|
+
return canonical;
|
|
220
|
+
}
|
|
98
221
|
function localState(entry) {
|
|
99
222
|
const local = readFiles(entry.local_dir); const base = readFiles(entry.base_dir);
|
|
100
223
|
const localHashes = fileHashes(local); const baseHashes = fileHashes(base);
|
|
@@ -102,9 +225,11 @@ function localState(entry) {
|
|
|
102
225
|
return { local, base, local_hashes: localHashes, base_hashes: baseHashes, base_valid: baseValid,
|
|
103
226
|
modified: FILES.some((filename) => localHashes[filename] !== baseHashes[filename]) };
|
|
104
227
|
}
|
|
228
|
+
|
|
105
229
|
async function checkout(projectDir, ids, options = {}) {
|
|
106
|
-
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
107
|
-
|
|
230
|
+
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
231
|
+
const targets = uniqueIDs(ids);
|
|
232
|
+
const settled = await Promise.allSettled(targets.map(async (id) => {
|
|
108
233
|
const remote = await metadata(config, id, options); const key = entryKey(id); const old = manifest.entries[key];
|
|
109
234
|
if (old && fs.existsSync(old.local_dir) && !options.force && localState(old).modified) {
|
|
110
235
|
throw new WorktreeError('LOCAL_CHANGES_PRESENT', `Refusing to replace locally modified custom service ${id}; commit it or use checkout --force.`);
|
|
@@ -112,12 +237,26 @@ async function checkout(projectDir, ids, options = {}) {
|
|
|
112
237
|
const archive = await download(config, remote, options); const files = archiveFiles(archive);
|
|
113
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);
|
|
114
239
|
writeFiles(localDir, files); writeFiles(baseDir, files);
|
|
240
|
+
syncManagedDevelopmentFiles(localDir);
|
|
115
241
|
const entry = { server: config.server, resource_type: 'custom_services', resource_id: String(id), title: remote.title,
|
|
116
242
|
slug: service.slug, local_dir: localDir, base_dir: baseDir, local_path: relative(projectDir, localDir), base_path: relative(projectDir, baseDir),
|
|
117
243
|
content_type: remote.content_type, base_revision: remote.base_revision, base_etag: remote.etag,
|
|
118
244
|
base_hash: remote.content_hash, file_hashes: fileHashes(files), checkout_source: remote.checkout_source,
|
|
119
245
|
checked_out_at: new Date().toISOString(), updated_at: remote.updated_at, updated_by: remote.updated_by };
|
|
120
|
-
|
|
246
|
+
return entry;
|
|
247
|
+
}));
|
|
248
|
+
const results = settled.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
249
|
+
for (const entry of results) manifest.entries[entryKey(entry.resource_id)] = entry;
|
|
250
|
+
if (results.length) saveManifest(projectDir, manifest);
|
|
251
|
+
const failures = settled.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
252
|
+
if (failures.length) {
|
|
253
|
+
const error = failures[0].item.reason;
|
|
254
|
+
error.details = { ...(error.details || {}), batch: {
|
|
255
|
+
completed: results.map((entry) => ({ resource_id: entry.resource_id, status: 'checked_out' })),
|
|
256
|
+
failed: failures.map(({ item, id }) => ({ resource_id: id, status: 'failed', code: item.reason.code || 'CHECKOUT_FAILED', message: item.reason.message })),
|
|
257
|
+
not_started: [],
|
|
258
|
+
} };
|
|
259
|
+
throw error;
|
|
121
260
|
}
|
|
122
261
|
return results;
|
|
123
262
|
}
|
|
@@ -130,13 +269,20 @@ function diffFile(baseFile, localFile, filename) {
|
|
|
130
269
|
}
|
|
131
270
|
function diff(projectDir, id) {
|
|
132
271
|
const entry = getEntry(projectDir, id); if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
133
|
-
const state = localState(entry);
|
|
134
|
-
|
|
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;
|
|
135
281
|
}
|
|
136
282
|
async function writeConflict(projectDir, entry, remote, archive) {
|
|
137
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');
|
|
138
284
|
writeFiles(baseDir, readFiles(entry.base_dir)); writeFiles(localDir, readFiles(entry.local_dir)); writeFiles(remoteDir, archiveFiles(archive));
|
|
139
|
-
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,
|
|
140
286
|
base_path: relative(projectDir, baseDir), local_path: relative(projectDir, localDir), remote_path: relative(projectDir, remoteDir), worktree_local_path: entry.local_path,
|
|
141
287
|
expected_revision: entry.base_revision, actual_revision: remote.base_revision, expected_hash: entry.base_hash, actual_hash: remote.content_hash,
|
|
142
288
|
actual_etag: remote.etag, actual_updated_at: remote.updated_at, created_at: new Date().toISOString() };
|
|
@@ -163,23 +309,35 @@ function resolveConflict(projectDir, id) {
|
|
|
163
309
|
atomicWrite(path.resolve(projectDir, record.manifest_path), `${JSON.stringify(resolved, null, 2)}\n`); return { ...resolved, manifest_path: record.manifest_path };
|
|
164
310
|
}
|
|
165
311
|
async function commit(projectDir, ids, options = {}) {
|
|
166
|
-
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
167
|
-
const
|
|
168
|
-
|
|
312
|
+
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
313
|
+
const targets = uniqueIDs(ids);
|
|
314
|
+
const preflight = await Promise.allSettled(targets.map(async (id) => {
|
|
169
315
|
const entry = manifest.entries[entryKey(id)]; if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
170
316
|
if (entry.server !== config.server) throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
317
|
+
syncManagedDevelopmentFiles(entry.local_dir);
|
|
171
318
|
const state = localState(entry); if (!state.base_valid) throw new WorktreeError('BASE_HASH_MISMATCH', `Custom service ${id} base does not match its manifest.`);
|
|
172
|
-
if (!state.modified)
|
|
319
|
+
if (!state.modified) return { id, entry, state, unchanged: true };
|
|
173
320
|
const current = await metadata(config, id, options);
|
|
174
321
|
if (current.content_hash !== entry.base_hash || String(current.base_revision) !== String(entry.base_revision)) {
|
|
175
322
|
const remoteArchive = await download(config, current, options); const conflict = await writeConflict(projectDir, entry, current, remoteArchive);
|
|
176
323
|
throw new WorktreeError('RESOURCE_VERSION_CONFLICT', `Custom service ${id} changed remotely; conflict materials were preserved.`, conflict);
|
|
177
324
|
}
|
|
178
|
-
|
|
325
|
+
return { id, entry, state, current, archive: createArchive(state.local) };
|
|
326
|
+
}));
|
|
327
|
+
const preflightFailures = preflight.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
328
|
+
if (preflightFailures.length) {
|
|
329
|
+
const error = preflightFailures[0].item.reason;
|
|
330
|
+
error.details = { ...(error.details || {}), batch: {
|
|
331
|
+
completed: [],
|
|
332
|
+
failed: preflightFailures.map(({ item, id }) => ({ resource_id: id, status: 'failed', phase: 'preflight', code: item.reason.code || 'PREFLIGHT_FAILED', message: item.reason.message })),
|
|
333
|
+
not_started: preflight.filter((item) => item.status === 'fulfilled').map((item) => ({ resource_id: item.value.id, status: 'not_started', phase: 'preflight' })),
|
|
334
|
+
} };
|
|
335
|
+
throw error;
|
|
179
336
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
337
|
+
const prepared = preflight.map((item) => item.value);
|
|
338
|
+
const uploaded = await Promise.allSettled(prepared.map(async (item) => {
|
|
339
|
+
const { id, entry, current, archive, state } = item;
|
|
340
|
+
if (item.unchanged) return { resource_type: 'custom_services', resource_id: String(id), status: 'unchanged' };
|
|
183
341
|
const url = new URL(current.commit_url, `${config.server.replace(/\/+$/, '')}/`).toString();
|
|
184
342
|
const response = await (options.fetch || fetch)(url, { method: 'PUT', headers: { Authorization: `Bearer ${config.token || config.sat}`, 'Content-Type': current.content_type,
|
|
185
343
|
'Content-Length': String(archive.length), 'X-Content-SHA256': sha256(archive), 'If-Match': entry.base_etag }, body: archive, signal: options.signal });
|
|
@@ -188,12 +346,20 @@ async function commit(projectDir, ids, options = {}) {
|
|
|
188
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); }
|
|
189
347
|
throw error;
|
|
190
348
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
349
|
+
applyRemoteEntry(entry, committed, state.local); syncManagedDevelopmentFiles(entry.local_dir); manifest.entries[entryKey(id)] = entry;
|
|
350
|
+
return { resource_type: 'custom_services', resource_id: String(id), status: 'committed', revision: committed.base_revision, hash: committed.content_hash };
|
|
351
|
+
}));
|
|
352
|
+
const results = uploaded.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
353
|
+
if (results.some((item) => item.status === 'committed')) saveManifest(projectDir, manifest);
|
|
354
|
+
const uploadFailures = uploaded.map((item, index) => ({ item, id: prepared[index].id })).filter(({ item }) => item.status === 'rejected');
|
|
355
|
+
if (uploadFailures.length) {
|
|
356
|
+
const error = uploadFailures[0].item.reason;
|
|
357
|
+
error.details = { ...(error.details || {}), batch: {
|
|
358
|
+
completed: results,
|
|
359
|
+
failed: uploadFailures.map(({ item, id }) => ({ resource_id: id, status: 'failed', phase: 'upload', code: item.reason.code || 'COMMIT_FAILED', message: item.reason.message, remote_change_possible: true })),
|
|
360
|
+
not_started: [],
|
|
361
|
+
} };
|
|
362
|
+
throw error;
|
|
197
363
|
}
|
|
198
364
|
return results;
|
|
199
365
|
}
|
|
@@ -202,22 +368,48 @@ async function inspectRemote(projectDir, options = {}) {
|
|
|
202
368
|
const requested = options.ids ? new Set(options.ids.map(String)) : null;
|
|
203
369
|
const entries = Object.values(loadManifest(projectDir).entries)
|
|
204
370
|
.filter((entry) => !requested || requested.has(String(entry.resource_id)));
|
|
205
|
-
|
|
206
|
-
for (const entry of entries) {
|
|
371
|
+
return Promise.all(entries.map(async (entry) => {
|
|
207
372
|
const state = localState(entry); const remote = await metadata(config, entry.resource_id, options); const remoteMatches = remote.content_hash === entry.base_hash && String(remote.base_revision) === String(entry.base_revision);
|
|
208
373
|
const localArchiveHash = checkoutHash(state.local); let status;
|
|
209
374
|
if (remoteMatches) status = state.modified ? 'local_modified' : 'clean';
|
|
210
375
|
else if (!state.modified) status = 'remote_changed';
|
|
211
376
|
else if (filesMatchRemote(state.local, remote)) status = 'committed_unrecorded';
|
|
212
377
|
else status = 'diverged';
|
|
213
|
-
|
|
214
|
-
}
|
|
378
|
+
return { ...entry, state: status, remote_hash: remote.content_hash, remote_revision: remote.base_revision, remote_etag: remote.etag, local_hash: localArchiveHash };
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
function currentServiceMetadata(entry) { return JSON.parse(readFiles(entry.local_dir)['service.json'].toString('utf8')); }
|
|
382
|
+
function validationCurrent(entry) {
|
|
383
|
+
const value = currentServiceMetadata(entry);
|
|
384
|
+
return value.validation_status === 'passed' && String(value.validated_revision) === String(entry.base_revision)
|
|
385
|
+
&& /^[a-f0-9]{64}$/i.test(String(value.validated_hash || ''));
|
|
386
|
+
}
|
|
387
|
+
function syncCheckoutEntry(manifest, id, remote) {
|
|
388
|
+
const entry = manifest.entries[entryKey(id)];
|
|
389
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
390
|
+
const state = localState(entry);
|
|
391
|
+
if (state.modified) throw new WorktreeError('LOCAL_CHANGES_PRESENT', `Custom service ${id} changed locally during its remote lifecycle.`);
|
|
392
|
+
applyRemoteEntry(entry, remote, state.local); syncManagedDevelopmentFiles(entry.local_dir); manifest.entries[entryKey(id)] = entry;
|
|
393
|
+
return entry;
|
|
394
|
+
}
|
|
395
|
+
async function validateCommitted(projectDir, id, config, manifest, options = {}) {
|
|
396
|
+
const entry = manifest.entries[entryKey(id)];
|
|
397
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
398
|
+
if (validationCurrent(entry)) return { validation_status: 'passed', revision: entry.base_revision, checkout: null, cached: true };
|
|
399
|
+
const result = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/validate`, { revision: entry.base_revision }, options);
|
|
400
|
+
if (!result.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo validation response did not include checkout metadata.');
|
|
401
|
+
syncCheckoutEntry(manifest, id, result.checkout);
|
|
215
402
|
return result;
|
|
216
403
|
}
|
|
404
|
+
function validationStale(error) {
|
|
405
|
+
return Boolean(error) && error.status === 409 && error.code === 'VALIDATION_FAILED'
|
|
406
|
+
&& error.details && error.details.reason === 'validation_stale';
|
|
407
|
+
}
|
|
217
408
|
async function validate(projectDir, id, options = {}) {
|
|
218
|
-
const config = options.config || loadProjectConfig(projectDir); await commit(projectDir, [id], options);
|
|
219
|
-
const result = await
|
|
220
|
-
|
|
409
|
+
const config = options.config || loadProjectConfig(projectDir); await commit(projectDir, [id], { ...options, config });
|
|
410
|
+
const manifest = loadManifest(projectDir); const result = await validateCommitted(projectDir, id, config, manifest, options);
|
|
411
|
+
if (!result.cached) saveManifest(projectDir, manifest);
|
|
412
|
+
return result;
|
|
221
413
|
}
|
|
222
414
|
function parseHandler(handler) {
|
|
223
415
|
const value = String(handler || '').trim(); if (!value) return undefined;
|
|
@@ -228,19 +420,64 @@ function parseHandler(handler) {
|
|
|
228
420
|
return { kind: 'manual', name: value };
|
|
229
421
|
}
|
|
230
422
|
async function test(projectDir, id, input = {}, options = {}) {
|
|
231
|
-
const config = options.config || loadProjectConfig(projectDir);
|
|
232
|
-
|
|
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,
|
|
233
442
|
input, selector: parseHandler(options.handler), headers: options.headers, user: options.user,
|
|
234
|
-
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny',
|
|
443
|
+
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny', revision,
|
|
235
444
|
}, options);
|
|
445
|
+
if (result.checkout && entry) { syncCheckoutEntry(manifest, id, result.checkout); saveManifest(projectDir, manifest); }
|
|
446
|
+
return result;
|
|
236
447
|
}
|
|
237
448
|
async function publish(projectDir, ids, options = {}) {
|
|
238
|
-
const config = options.config || loadProjectConfig(projectDir); const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
449
|
+
const config = options.config || loadProjectConfig(projectDir); const targets = uniqueIDs(ids);
|
|
450
|
+
await commit(projectDir, targets, { ...options, config });
|
|
451
|
+
const manifest = loadManifest(projectDir);
|
|
452
|
+
const validations = await Promise.all(targets.map((id) => validateCommitted(projectDir, id, config, manifest, options)));
|
|
453
|
+
if (validations.some((result) => !result.cached)) saveManifest(projectDir, manifest);
|
|
454
|
+
const published = await Promise.allSettled(targets.map(async (id) => {
|
|
455
|
+
let entry = manifest.entries[entryKey(id)];
|
|
456
|
+
const requestPublish = () => jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/publish`, { revision: entry.base_revision }, options);
|
|
457
|
+
let result;
|
|
458
|
+
try {
|
|
459
|
+
result = await requestPublish();
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (!validationStale(error)) throw error;
|
|
462
|
+
const validation = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/validate`, { revision: entry.base_revision }, options);
|
|
463
|
+
if (!validation.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo validation response did not include checkout metadata.');
|
|
464
|
+
entry = syncCheckoutEntry(manifest, id, validation.checkout);
|
|
465
|
+
result = await requestPublish();
|
|
466
|
+
}
|
|
467
|
+
if (!result.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo publish response did not include checkout metadata.');
|
|
468
|
+
syncCheckoutEntry(manifest, id, result.checkout); return result;
|
|
469
|
+
}));
|
|
470
|
+
const results = published.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
471
|
+
if (results.length) saveManifest(projectDir, manifest);
|
|
472
|
+
const failures = published.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
473
|
+
if (failures.length) {
|
|
474
|
+
const error = failures[0].item.reason;
|
|
475
|
+
error.details = { ...(error.details || {}), batch: {
|
|
476
|
+
completed: results, failed: failures.map(({ item, id }) => ({ resource_id: id, status: 'failed', code: item.reason.code || 'PUBLISH_FAILED', message: item.reason.message })), not_started: [],
|
|
477
|
+
} };
|
|
478
|
+
throw error;
|
|
242
479
|
}
|
|
243
480
|
return results;
|
|
244
481
|
}
|
|
245
482
|
|
|
246
|
-
module.exports = { FILES, checkout, commit, diff, validate, test, publish, inspectRemote, loadManifest, getEntry, metadata, archiveFiles, createArchive, serviceConflicts, showConflict, resolveConflict, parseHandler };
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Environment detection helpers for the local
|
|
3
|
+
// Environment detection helpers for the local stack setup wizard.
|
|
4
4
|
// - probePort: TCP probe (used to find existing MySQL/Redis)
|
|
5
5
|
// - probeHttp: HTTP GET probe with retry (used to wait for the app)
|
|
6
6
|
// - detectDocker: Find a working `docker` + `docker compose` (or `docker-compose`)
|