draftgo-cli 3.0.55 → 4.0.1
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 +112 -316
- package/package.json +5 -5
- package/resources/skill/SKILL.md +25 -24
- package/resources/skill/init/SKILL.md +5 -10
- package/resources/skill/manifest.json +2 -2
- package/resources/skill/references/aihub.md +10 -5
- package/resources/skill/references/chat-sdk.md +10 -0
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/custom-services.md +65 -226
- package/resources/skill/references/data.md +3 -2
- package/resources/skill/references/frontend.md +96 -490
- package/resources/skill/references/mcp.md +39 -103
- package/resources/skill/references/runtime.md +3 -2
- package/resources/skill/story/SKILL.md +1 -2
- package/src/apiContractCache.js +112 -0
- package/src/cli.js +1 -21
- package/src/commandRegistry.js +6 -11
- package/src/commands/api.js +28 -8
- package/src/commands/check.js +1 -10
- package/src/commands/customService.js +2 -4
- package/src/commands/delete.js +23 -46
- package/src/commands/deploy.js +1 -1
- package/src/commands/help.js +16 -31
- package/src/commands/init.js +4 -10
- package/src/commands/listTargets.js +1 -1
- package/src/commands/local.js +2 -6
- package/src/commands/map.js +0 -11
- package/src/commands/status.js +1 -1
- package/src/commands/uninstall.js +3 -3
- package/src/commands/update.js +1 -1
- package/src/commands/verify.js +43 -21
- package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
- package/src/commands/worklog.js +86 -0
- package/src/customServices.js +150 -33
- package/src/{localdev → localRuntime}/detect.js +1 -1
- package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
- package/src/{localdev → localRuntime}/services.js +1 -1
- package/src/projectConfig.js +2 -0
- package/src/{installers/index.js → targets.js} +3 -5
- package/src/worklog.js +274 -0
- package/src/workspaceHealth.js +1 -1
- package/src/worktree/index.js +81 -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/{localdev → localRuntime}/compose.js +0 -0
- /package/src/{localdev → localRuntime}/index.js +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const {
|
|
5
|
+
appendItem,
|
|
6
|
+
mutateWorklog,
|
|
7
|
+
normalizeDate,
|
|
8
|
+
readWorklog,
|
|
9
|
+
renderWorklog,
|
|
10
|
+
resolveReference,
|
|
11
|
+
updateItem,
|
|
12
|
+
worklogPath,
|
|
13
|
+
} = require('../worklog');
|
|
14
|
+
|
|
15
|
+
function printUsage() {
|
|
16
|
+
log.err('Usage: draftgo work start|add|start-item|complete|show|list <value>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function output(flags, value) {
|
|
20
|
+
if (flags.output === 'json') console.log(JSON.stringify(value, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function latestReference(blocks) {
|
|
24
|
+
const block = blocks[blocks.length - 1];
|
|
25
|
+
return `${block.date}#${block.entries[block.entries.length - 1].number}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resultFor(blocks, reference) {
|
|
29
|
+
const resolved = resolveReference(blocks, reference);
|
|
30
|
+
return { date: resolved.block.date, ...resolved.entry };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function add(projectDir, positional, flags, status) {
|
|
34
|
+
const title = positional.join(' ').trim();
|
|
35
|
+
if (!title) { printUsage(); return 1; }
|
|
36
|
+
const result = mutateWorklog(projectDir, (blocks) => {
|
|
37
|
+
const date = normalizeDate(flags.date);
|
|
38
|
+
const next = appendItem(blocks, title, status, flags.note ? [String(flags.note).trim()] : [], date);
|
|
39
|
+
return { blocks: next, reference: latestReference(next) };
|
|
40
|
+
});
|
|
41
|
+
output(flags, { path: result.path, item: resultFor(result.blocks, result.reference) });
|
|
42
|
+
if (flags.output !== 'json') log.ok(`Worklog item added: ${title}`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function change(projectDir, positional, flags, status) {
|
|
47
|
+
const reference = positional[0];
|
|
48
|
+
if (!reference) { printUsage(); return 1; }
|
|
49
|
+
const result = mutateWorklog(projectDir, (blocks) => {
|
|
50
|
+
const note = flags.note == null ? '' : String(flags.note).trim();
|
|
51
|
+
const next = updateItem(blocks, reference, status, note);
|
|
52
|
+
return { blocks: next, reference };
|
|
53
|
+
});
|
|
54
|
+
output(flags, { path: result.path, item: resultFor(result.blocks, result.reference) });
|
|
55
|
+
if (flags.output !== 'json') log.ok(`Worklog item ${result.reference} marked ${status}.`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function show(projectDir, flags) {
|
|
60
|
+
const blocks = readWorklog(projectDir);
|
|
61
|
+
output(flags, { path: worklogPath(projectDir), blocks });
|
|
62
|
+
if (flags.output !== 'json') console.log(renderWorklog(blocks) || 'Worklog is empty.');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function list(projectDir, flags) {
|
|
67
|
+
const blocks = readWorklog(projectDir);
|
|
68
|
+
const items = blocks.flatMap((block) => block.entries.map((entry) => ({ date: block.date, ...entry })));
|
|
69
|
+
output(flags, { path: worklogPath(projectDir), items });
|
|
70
|
+
if (flags.output !== 'json') items.forEach((item) => log.info(`${item.date}#${item.number} [${item.status}] ${item.title}`));
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function worklog(projectDir, positional = [], flags = {}) {
|
|
75
|
+
const operation = positional[0] || 'show';
|
|
76
|
+
if (operation === 'start') return add(projectDir, positional.slice(1), flags, 'active');
|
|
77
|
+
if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'pending');
|
|
78
|
+
if (operation === 'start-item' || operation === 'start_item') return change(projectDir, positional.slice(1), flags, 'active');
|
|
79
|
+
if (operation === 'complete' || operation === 'done') return change(projectDir, positional.slice(1), flags, 'completed');
|
|
80
|
+
if (operation === 'show') return show(projectDir, flags);
|
|
81
|
+
if (operation === 'list') return list(projectDir, flags);
|
|
82
|
+
throw new Error(`Unknown worklog operation ${operation}.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
worklog.printUsage = printUsage;
|
|
86
|
+
module.exports = worklog;
|
package/src/customServices.js
CHANGED
|
@@ -25,11 +25,12 @@ function loadManifest(projectDir) {
|
|
|
25
25
|
}
|
|
26
26
|
function atomicWrite(file, value) {
|
|
27
27
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
-
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
28
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
|
|
29
29
|
fs.writeFileSync(temporary, value); fs.renameSync(temporary, file);
|
|
30
30
|
}
|
|
31
31
|
function saveManifest(projectDir, value) { value.updated_at = new Date().toISOString(); atomicWrite(manifestPath(projectDir), `${JSON.stringify(value, null, 2)}\n`); }
|
|
32
32
|
function entryKey(id) { return String(id); }
|
|
33
|
+
function uniqueIDs(ids) { return [...new Set(ids.map(String).map((id) => id.trim()).filter(Boolean))]; }
|
|
33
34
|
function serviceDir(projectDir, id, slug) { return path.join(root(projectDir), `${safe(slug || id)}-${safe(id)}`); }
|
|
34
35
|
function serviceBaseDir(projectDir, id) { return path.join(baseRoot(projectDir), safe(id)); }
|
|
35
36
|
function parseEnvelope(text, response) {
|
|
@@ -95,6 +96,32 @@ function filesMatchRemote(files, remote) {
|
|
|
95
96
|
const local = fileHashes(files); const expected = remoteFileHashes(remote);
|
|
96
97
|
return FILES.every((filename) => expected[filename] && local[filename] === expected[filename]);
|
|
97
98
|
}
|
|
99
|
+
function canonicalFiles(files, remote) {
|
|
100
|
+
if (!remote || typeof remote.service_metadata_json !== 'string') {
|
|
101
|
+
throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo did not return canonical custom-service metadata.');
|
|
102
|
+
}
|
|
103
|
+
const canonical = { ...files, 'service.json': Buffer.from(remote.service_metadata_json, 'utf8') };
|
|
104
|
+
JSON.parse(canonical['service.json'].toString('utf8'));
|
|
105
|
+
const actual = fileHashes(canonical); const expected = remoteFileHashes(remote);
|
|
106
|
+
for (const filename of FILES) {
|
|
107
|
+
if (!/^[a-f0-9]{64}$/i.test(String(expected[filename] || '')) || actual[filename] !== expected[filename]) {
|
|
108
|
+
throw new WorktreeError('HASH_MISMATCH', `Canonical custom-service ${filename} does not match checkout metadata.`, {
|
|
109
|
+
filename, expected_hash: expected[filename] || null, actual_hash: actual[filename],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return canonical;
|
|
114
|
+
}
|
|
115
|
+
function applyRemoteEntry(entry, remote, files) {
|
|
116
|
+
const canonical = canonicalFiles(files, remote);
|
|
117
|
+
writeFiles(entry.local_dir, canonical); writeFiles(entry.base_dir, canonical);
|
|
118
|
+
entry.title = remote.title; entry.slug = JSON.parse(canonical['service.json'].toString('utf8')).slug;
|
|
119
|
+
entry.file_hashes = fileHashes(canonical); entry.base_hash = remote.content_hash;
|
|
120
|
+
entry.base_revision = remote.base_revision; entry.base_etag = remote.etag;
|
|
121
|
+
entry.checkout_source = remote.checkout_source; entry.updated_at = remote.updated_at;
|
|
122
|
+
entry.updated_by = remote.updated_by; entry.committed_at = new Date().toISOString();
|
|
123
|
+
return canonical;
|
|
124
|
+
}
|
|
98
125
|
function localState(entry) {
|
|
99
126
|
const local = readFiles(entry.local_dir); const base = readFiles(entry.base_dir);
|
|
100
127
|
const localHashes = fileHashes(local); const baseHashes = fileHashes(base);
|
|
@@ -103,8 +130,9 @@ function localState(entry) {
|
|
|
103
130
|
modified: FILES.some((filename) => localHashes[filename] !== baseHashes[filename]) };
|
|
104
131
|
}
|
|
105
132
|
async function checkout(projectDir, ids, options = {}) {
|
|
106
|
-
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
107
|
-
|
|
133
|
+
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
134
|
+
const targets = uniqueIDs(ids);
|
|
135
|
+
const settled = await Promise.allSettled(targets.map(async (id) => {
|
|
108
136
|
const remote = await metadata(config, id, options); const key = entryKey(id); const old = manifest.entries[key];
|
|
109
137
|
if (old && fs.existsSync(old.local_dir) && !options.force && localState(old).modified) {
|
|
110
138
|
throw new WorktreeError('LOCAL_CHANGES_PRESENT', `Refusing to replace locally modified custom service ${id}; commit it or use checkout --force.`);
|
|
@@ -117,7 +145,20 @@ async function checkout(projectDir, ids, options = {}) {
|
|
|
117
145
|
content_type: remote.content_type, base_revision: remote.base_revision, base_etag: remote.etag,
|
|
118
146
|
base_hash: remote.content_hash, file_hashes: fileHashes(files), checkout_source: remote.checkout_source,
|
|
119
147
|
checked_out_at: new Date().toISOString(), updated_at: remote.updated_at, updated_by: remote.updated_by };
|
|
120
|
-
|
|
148
|
+
return entry;
|
|
149
|
+
}));
|
|
150
|
+
const results = settled.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
151
|
+
for (const entry of results) manifest.entries[entryKey(entry.resource_id)] = entry;
|
|
152
|
+
if (results.length) saveManifest(projectDir, manifest);
|
|
153
|
+
const failures = settled.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
154
|
+
if (failures.length) {
|
|
155
|
+
const error = failures[0].item.reason;
|
|
156
|
+
error.details = { ...(error.details || {}), batch: {
|
|
157
|
+
completed: results.map((entry) => ({ resource_id: entry.resource_id, status: 'checked_out' })),
|
|
158
|
+
failed: failures.map(({ item, id }) => ({ resource_id: id, status: 'failed', code: item.reason.code || 'CHECKOUT_FAILED', message: item.reason.message })),
|
|
159
|
+
not_started: [],
|
|
160
|
+
} };
|
|
161
|
+
throw error;
|
|
121
162
|
}
|
|
122
163
|
return results;
|
|
123
164
|
}
|
|
@@ -163,23 +204,34 @@ function resolveConflict(projectDir, id) {
|
|
|
163
204
|
atomicWrite(path.resolve(projectDir, record.manifest_path), `${JSON.stringify(resolved, null, 2)}\n`); return { ...resolved, manifest_path: record.manifest_path };
|
|
164
205
|
}
|
|
165
206
|
async function commit(projectDir, ids, options = {}) {
|
|
166
|
-
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
167
|
-
const
|
|
168
|
-
|
|
207
|
+
const config = options.config || loadProjectConfig(projectDir); const manifest = loadManifest(projectDir);
|
|
208
|
+
const targets = uniqueIDs(ids);
|
|
209
|
+
const preflight = await Promise.allSettled(targets.map(async (id) => {
|
|
169
210
|
const entry = manifest.entries[entryKey(id)]; if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
170
211
|
if (entry.server !== config.server) throw new WorktreeError('CHECKOUT_SERVER_MISMATCH', 'Checkout belongs to a different DraftGo server.');
|
|
171
212
|
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)
|
|
213
|
+
if (!state.modified) return { id, entry, state, unchanged: true };
|
|
173
214
|
const current = await metadata(config, id, options);
|
|
174
215
|
if (current.content_hash !== entry.base_hash || String(current.base_revision) !== String(entry.base_revision)) {
|
|
175
216
|
const remoteArchive = await download(config, current, options); const conflict = await writeConflict(projectDir, entry, current, remoteArchive);
|
|
176
217
|
throw new WorktreeError('RESOURCE_VERSION_CONFLICT', `Custom service ${id} changed remotely; conflict materials were preserved.`, conflict);
|
|
177
218
|
}
|
|
178
|
-
|
|
219
|
+
return { id, entry, state, current, archive: createArchive(state.local) };
|
|
220
|
+
}));
|
|
221
|
+
const preflightFailures = preflight.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
222
|
+
if (preflightFailures.length) {
|
|
223
|
+
const error = preflightFailures[0].item.reason;
|
|
224
|
+
error.details = { ...(error.details || {}), batch: {
|
|
225
|
+
completed: [],
|
|
226
|
+
failed: preflightFailures.map(({ item, id }) => ({ resource_id: id, status: 'failed', phase: 'preflight', code: item.reason.code || 'PREFLIGHT_FAILED', message: item.reason.message })),
|
|
227
|
+
not_started: preflight.filter((item) => item.status === 'fulfilled').map((item) => ({ resource_id: item.value.id, status: 'not_started', phase: 'preflight' })),
|
|
228
|
+
} };
|
|
229
|
+
throw error;
|
|
179
230
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
231
|
+
const prepared = preflight.map((item) => item.value);
|
|
232
|
+
const uploaded = await Promise.allSettled(prepared.map(async (item) => {
|
|
233
|
+
const { id, entry, current, archive, state } = item;
|
|
234
|
+
if (item.unchanged) return { resource_type: 'custom_services', resource_id: String(id), status: 'unchanged' };
|
|
183
235
|
const url = new URL(current.commit_url, `${config.server.replace(/\/+$/, '')}/`).toString();
|
|
184
236
|
const response = await (options.fetch || fetch)(url, { method: 'PUT', headers: { Authorization: `Bearer ${config.token || config.sat}`, 'Content-Type': current.content_type,
|
|
185
237
|
'Content-Length': String(archive.length), 'X-Content-SHA256': sha256(archive), 'If-Match': entry.base_etag }, body: archive, signal: options.signal });
|
|
@@ -188,12 +240,20 @@ async function commit(projectDir, ids, options = {}) {
|
|
|
188
240
|
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
241
|
throw error;
|
|
190
242
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
243
|
+
applyRemoteEntry(entry, committed, state.local); manifest.entries[entryKey(id)] = entry;
|
|
244
|
+
return { resource_type: 'custom_services', resource_id: String(id), status: 'committed', revision: committed.base_revision, hash: committed.content_hash };
|
|
245
|
+
}));
|
|
246
|
+
const results = uploaded.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
247
|
+
if (results.some((item) => item.status === 'committed')) saveManifest(projectDir, manifest);
|
|
248
|
+
const uploadFailures = uploaded.map((item, index) => ({ item, id: prepared[index].id })).filter(({ item }) => item.status === 'rejected');
|
|
249
|
+
if (uploadFailures.length) {
|
|
250
|
+
const error = uploadFailures[0].item.reason;
|
|
251
|
+
error.details = { ...(error.details || {}), batch: {
|
|
252
|
+
completed: results,
|
|
253
|
+
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 })),
|
|
254
|
+
not_started: [],
|
|
255
|
+
} };
|
|
256
|
+
throw error;
|
|
197
257
|
}
|
|
198
258
|
return results;
|
|
199
259
|
}
|
|
@@ -202,22 +262,48 @@ async function inspectRemote(projectDir, options = {}) {
|
|
|
202
262
|
const requested = options.ids ? new Set(options.ids.map(String)) : null;
|
|
203
263
|
const entries = Object.values(loadManifest(projectDir).entries)
|
|
204
264
|
.filter((entry) => !requested || requested.has(String(entry.resource_id)));
|
|
205
|
-
|
|
206
|
-
for (const entry of entries) {
|
|
265
|
+
return Promise.all(entries.map(async (entry) => {
|
|
207
266
|
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
267
|
const localArchiveHash = checkoutHash(state.local); let status;
|
|
209
268
|
if (remoteMatches) status = state.modified ? 'local_modified' : 'clean';
|
|
210
269
|
else if (!state.modified) status = 'remote_changed';
|
|
211
270
|
else if (filesMatchRemote(state.local, remote)) status = 'committed_unrecorded';
|
|
212
271
|
else status = 'diverged';
|
|
213
|
-
|
|
214
|
-
}
|
|
272
|
+
return { ...entry, state: status, remote_hash: remote.content_hash, remote_revision: remote.base_revision, remote_etag: remote.etag, local_hash: localArchiveHash };
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
function currentServiceMetadata(entry) { return JSON.parse(readFiles(entry.local_dir)['service.json'].toString('utf8')); }
|
|
276
|
+
function validationCurrent(entry) {
|
|
277
|
+
const value = currentServiceMetadata(entry);
|
|
278
|
+
return value.validation_status === 'passed' && String(value.validated_revision) === String(entry.base_revision)
|
|
279
|
+
&& /^[a-f0-9]{64}$/i.test(String(value.validated_hash || ''));
|
|
280
|
+
}
|
|
281
|
+
function syncCheckoutEntry(manifest, id, remote) {
|
|
282
|
+
const entry = manifest.entries[entryKey(id)];
|
|
283
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
284
|
+
const state = localState(entry);
|
|
285
|
+
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;
|
|
287
|
+
return entry;
|
|
288
|
+
}
|
|
289
|
+
async function validateCommitted(projectDir, id, config, manifest, options = {}) {
|
|
290
|
+
const entry = manifest.entries[entryKey(id)];
|
|
291
|
+
if (!entry) throw new WorktreeError('RESOURCE_NOT_CHECKED_OUT', `Custom service ${id} is not checked out.`);
|
|
292
|
+
if (validationCurrent(entry)) return { validation_status: 'passed', revision: entry.base_revision, checkout: null, cached: true };
|
|
293
|
+
const result = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/validate`, { revision: entry.base_revision }, options);
|
|
294
|
+
if (!result.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo validation response did not include checkout metadata.');
|
|
295
|
+
syncCheckoutEntry(manifest, id, result.checkout);
|
|
215
296
|
return result;
|
|
216
297
|
}
|
|
298
|
+
function validationStale(error) {
|
|
299
|
+
return Boolean(error) && error.status === 409 && error.code === 'VALIDATION_FAILED'
|
|
300
|
+
&& error.details && error.details.reason === 'validation_stale';
|
|
301
|
+
}
|
|
217
302
|
async function validate(projectDir, id, options = {}) {
|
|
218
|
-
const config = options.config || loadProjectConfig(projectDir); await commit(projectDir, [id], options);
|
|
219
|
-
const result = await
|
|
220
|
-
|
|
303
|
+
const config = options.config || loadProjectConfig(projectDir); await commit(projectDir, [id], { ...options, config });
|
|
304
|
+
const manifest = loadManifest(projectDir); const result = await validateCommitted(projectDir, id, config, manifest, options);
|
|
305
|
+
if (!result.cached) saveManifest(projectDir, manifest);
|
|
306
|
+
return result;
|
|
221
307
|
}
|
|
222
308
|
function parseHandler(handler) {
|
|
223
309
|
const value = String(handler || '').trim(); if (!value) return undefined;
|
|
@@ -228,19 +314,50 @@ function parseHandler(handler) {
|
|
|
228
314
|
return { kind: 'manual', name: value };
|
|
229
315
|
}
|
|
230
316
|
async function test(projectDir, id, input = {}, options = {}) {
|
|
231
|
-
const config = options.config || loadProjectConfig(projectDir); await
|
|
232
|
-
|
|
317
|
+
const config = options.config || loadProjectConfig(projectDir); await commit(projectDir, [id], { ...options, config });
|
|
318
|
+
const manifest = loadManifest(projectDir); const validation = await validateCommitted(projectDir, id, config, manifest, options);
|
|
319
|
+
const entry = manifest.entries[entryKey(id)];
|
|
320
|
+
if (!validation.cached) saveManifest(projectDir, manifest);
|
|
321
|
+
const result = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/execute`, {
|
|
233
322
|
input, selector: parseHandler(options.handler), headers: options.headers, user: options.user,
|
|
234
|
-
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny',
|
|
323
|
+
test_write: Boolean(options.testWrite), side_effect_policy: options.sideEffectPolicy || 'deny', revision: entry.base_revision,
|
|
235
324
|
}, options);
|
|
325
|
+
if (result.checkout) { syncCheckoutEntry(manifest, id, result.checkout); saveManifest(projectDir, manifest); }
|
|
326
|
+
return result;
|
|
236
327
|
}
|
|
237
328
|
async function publish(projectDir, ids, options = {}) {
|
|
238
|
-
const config = options.config || loadProjectConfig(projectDir); const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
329
|
+
const config = options.config || loadProjectConfig(projectDir); const targets = uniqueIDs(ids);
|
|
330
|
+
await commit(projectDir, targets, { ...options, config });
|
|
331
|
+
const manifest = loadManifest(projectDir);
|
|
332
|
+
const validations = await Promise.all(targets.map((id) => validateCommitted(projectDir, id, config, manifest, options)));
|
|
333
|
+
if (validations.some((result) => !result.cached)) saveManifest(projectDir, manifest);
|
|
334
|
+
const published = await Promise.allSettled(targets.map(async (id) => {
|
|
335
|
+
let entry = manifest.entries[entryKey(id)];
|
|
336
|
+
const requestPublish = () => jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/publish`, { revision: entry.base_revision }, options);
|
|
337
|
+
let result;
|
|
338
|
+
try {
|
|
339
|
+
result = await requestPublish();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (!validationStale(error)) throw error;
|
|
342
|
+
const validation = await jsonRequest(config, 'POST', `scripts/${encodeURIComponent(id)}/draft/validate`, { revision: entry.base_revision }, options);
|
|
343
|
+
if (!validation.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo validation response did not include checkout metadata.');
|
|
344
|
+
entry = syncCheckoutEntry(manifest, id, validation.checkout);
|
|
345
|
+
result = await requestPublish();
|
|
346
|
+
}
|
|
347
|
+
if (!result.checkout) throw new WorktreeError('INCOMPLETE_CHECKOUT_METADATA', 'DraftGo publish response did not include checkout metadata.');
|
|
348
|
+
syncCheckoutEntry(manifest, id, result.checkout); return result;
|
|
349
|
+
}));
|
|
350
|
+
const results = published.filter((item) => item.status === 'fulfilled').map((item) => item.value);
|
|
351
|
+
if (results.length) saveManifest(projectDir, manifest);
|
|
352
|
+
const failures = published.map((item, index) => ({ item, id: targets[index] })).filter(({ item }) => item.status === 'rejected');
|
|
353
|
+
if (failures.length) {
|
|
354
|
+
const error = failures[0].item.reason;
|
|
355
|
+
error.details = { ...(error.details || {}), batch: {
|
|
356
|
+
completed: results, failed: failures.map(({ item, id }) => ({ resource_id: id, status: 'failed', code: item.reason.code || 'PUBLISH_FAILED', message: item.reason.message })), not_started: [],
|
|
357
|
+
} };
|
|
358
|
+
throw error;
|
|
242
359
|
}
|
|
243
360
|
return results;
|
|
244
361
|
}
|
|
245
362
|
|
|
246
|
-
module.exports = { FILES, checkout, commit, diff, validate, test, publish, inspectRemote, loadManifest, getEntry, metadata, archiveFiles, createArchive, serviceConflicts, showConflict, resolveConflict, parseHandler };
|
|
363
|
+
module.exports = { FILES, checkout, commit, diff, validate, test, publish, inspectRemote, loadManifest, getEntry, metadata, archiveFiles, createArchive, serviceConflicts, showConflict, resolveConflict, parseHandler, validationStale };
|
|
@@ -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`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Lightweight MySQL helper used by the local
|
|
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,6 +1,6 @@
|
|
|
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');
|
package/src/projectConfig.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
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('
|
|
8
|
-
const skill = require('
|
|
5
|
+
const { platforms } = require('./platforms');
|
|
6
|
+
const skill = require('./skill');
|
|
9
7
|
|
|
10
8
|
function makeInstaller(p) {
|
|
11
9
|
return {
|