@goodandready/dsh-cron 0.2.5 → 0.2.6
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 +18 -6
- package/docs/README.ru.md +21 -5
- package/docs/README.zh.md +18 -6
- package/docs/design/DESIGN.md +6 -4
- package/docs/plans/0.2.6-economy-block.md +61 -0
- package/lib/api.js +399 -0
- package/lib/client.js +154 -4
- package/lib/failure-inspector.js +107 -0
- package/lib/http-utils.js +24 -0
- package/lib/index.js +121 -388
- package/lib/llm-ask.js +141 -0
- package/lib/recipes.js +238 -0
- package/lib/runner.js +210 -171
- package/lib/scheduler.js +226 -80
- package/lib/silent-rule.js +100 -0
- package/lib/store.js +11 -0
- package/lib/task-patch.js +81 -0
- package/lib/task-transfer.js +4 -0
- package/lib/templates.js +4 -0
- package/package.json +1 -1
package/lib/api.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST surface of the plugin: the /dsh-cron/tasks and /dsh-cron/action route
|
|
3
|
+
* families. Extracted from index.js and split into per-scope handlers so each
|
|
4
|
+
* piece stays reviewable (#97); route contracts and response bodies are
|
|
5
|
+
* unchanged.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { parseScheduleExpression } from './scheduler.js';
|
|
10
|
+
import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
|
|
11
|
+
import { CHANNEL_IDS } from './channels.js';
|
|
12
|
+
import { applyTaskPatch } from './task-patch.js';
|
|
13
|
+
import { sendJson, rejectCrossOrigin, readBody, pickPatchableFields, SCRIPT_CONFIRM_HEADER } from './http-utils.js';
|
|
14
|
+
import {
|
|
15
|
+
validateTaskType,
|
|
16
|
+
buildDuplicateTask,
|
|
17
|
+
buildTaskExport,
|
|
18
|
+
validateImportDocument,
|
|
19
|
+
planImport,
|
|
20
|
+
hasCodeExecutingTask,
|
|
21
|
+
IMPORT_STRATEGIES,
|
|
22
|
+
RESERVED_TASK_IDS,
|
|
23
|
+
PATCHABLE_TASK_FIELDS,
|
|
24
|
+
} from './task-transfer.js';
|
|
25
|
+
|
|
26
|
+
const NOT_FOUND = { ok: false, error: 'Task not found' };
|
|
27
|
+
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
|
|
31
|
+
* route families — the #66 alias is kept alive by the same code path instead
|
|
32
|
+
* of a duplicated handler.
|
|
33
|
+
*
|
|
34
|
+
* IMPORTANT: prefix routes must be registered WITHOUT a trailing slash —
|
|
35
|
+
* with one, the core degrades matching to exact-path equality and every
|
|
36
|
+
* deeper path unrouted (#96). Exported for unit tests.
|
|
37
|
+
*/
|
|
38
|
+
export function createCronApiHandler(store, scheduler, collection) {
|
|
39
|
+
return async function handleCronApi(req, res) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
42
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
43
|
+
const scope = parts[1]; // 'tasks' | 'action'
|
|
44
|
+
const id = parts[2];
|
|
45
|
+
const action = parts[3];
|
|
46
|
+
|
|
47
|
+
if (!id) {
|
|
48
|
+
await handleTaskCollection({ store, scheduler, collection, req, res, url, scope });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (req.method !== 'GET' && rejectCrossOrigin(req, res)) return;
|
|
53
|
+
|
|
54
|
+
// Export/import are collection operations that live under /tasks/:id/…,
|
|
55
|
+
// so they are matched before the generic id handling below.
|
|
56
|
+
if (id === 'export' && req.method === 'GET') {
|
|
57
|
+
handleTaskExport({ store, res });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (id === 'import' && req.method === 'POST') {
|
|
61
|
+
await handleTaskImport({ store, scheduler, req, res });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await handleTaskItem({ store, scheduler, req, res, url, id, action });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ------------------------------------------------------------- collection
|
|
73
|
+
|
|
74
|
+
async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope }) {
|
|
75
|
+
// The alias family only exists for individual tasks.
|
|
76
|
+
if (scope === 'action' || !collection) {
|
|
77
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (req.method === 'GET') {
|
|
81
|
+
listTasks({ store, scheduler, collection, res, url });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (req.method === 'POST') {
|
|
85
|
+
await createOrUpdateTask({ store, scheduler, req, res });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function listTasks({ store, scheduler, collection, res, url }) {
|
|
92
|
+
const status = url.searchParams.get('status') || 'all';
|
|
93
|
+
const query = url.searchParams.get('query') || '';
|
|
94
|
+
const list = store.list({ status, query }).map((t) => ({
|
|
95
|
+
...t,
|
|
96
|
+
running: scheduler.isRunning(t.id),
|
|
97
|
+
runningSince: scheduler.runningSince(t.id),
|
|
98
|
+
}));
|
|
99
|
+
const stats = store.getAggregatedStats();
|
|
100
|
+
sendJson(res, 200, { ok: true, tasks: list, recommendations: collection.recommendations, stats });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function createOrUpdateTask({ store, scheduler, req, res }) {
|
|
104
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
105
|
+
const { body, error } = await readBody(req, res);
|
|
106
|
+
if (error) return;
|
|
107
|
+
if (!body.title || !body.schedule || !body.prompt) {
|
|
108
|
+
sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const taskType = normalizeTaskType(body.type);
|
|
112
|
+
if (CODE_EXECUTING_TYPES.includes(taskType) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
113
|
+
sendJson(res, 403, { ok: false, error: `Creating ${taskType} tasks over HTTP requires the x-dsh-cron-confirm: script header` });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const typeError = validateTaskType(taskType, body);
|
|
117
|
+
if (typeError) {
|
|
118
|
+
sendJson(res, 400, { ok: false, error: typeError });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const parsed = parseScheduleExpression(body.schedule);
|
|
122
|
+
// A client-supplied id must not shadow the collection routes under
|
|
123
|
+
// /dsh-cron/tasks, otherwise that task could never be fetched or deleted
|
|
124
|
+
// again (#42 review finding).
|
|
125
|
+
if (body.id && RESERVED_TASK_IDS.includes(String(body.id))) {
|
|
126
|
+
sendJson(res, 400, { ok: false, error: `Task id "${body.id}" is reserved` });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const task = store.set(buildTaskRecord(store, body, taskType, parsed));
|
|
130
|
+
if (task.status === 'active') {
|
|
131
|
+
scheduler.scheduleTask(task);
|
|
132
|
+
} else {
|
|
133
|
+
scheduler.pauseTask(task.id);
|
|
134
|
+
}
|
|
135
|
+
sendJson(res, 200, { ok: true, task });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Merge a create/update payload onto the stored task, if any. */
|
|
139
|
+
function buildTaskRecord(store, body, taskType, parsed) {
|
|
140
|
+
const current = body.id ? store.get(body.id) : null;
|
|
141
|
+
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
142
|
+
return {
|
|
143
|
+
id: body.id,
|
|
144
|
+
title: body.title,
|
|
145
|
+
schedule: parsed.cronPattern || body.schedule,
|
|
146
|
+
scheduleText: body.scheduleText || parsed.humanText,
|
|
147
|
+
prompt: body.prompt,
|
|
148
|
+
type: taskType,
|
|
149
|
+
delivery: body.delivery || 'current',
|
|
150
|
+
status: resolvedStatus,
|
|
151
|
+
provider: body.provider || undefined,
|
|
152
|
+
model: body.model || undefined,
|
|
153
|
+
fallbackProvider: body.fallbackProvider || undefined,
|
|
154
|
+
fallbackModel: body.fallbackModel || undefined,
|
|
155
|
+
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : false),
|
|
156
|
+
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : false),
|
|
157
|
+
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
158
|
+
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
159
|
+
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
160
|
+
timezone: body.timezone !== undefined ? String(body.timezone).trim() : (current ? current.timezone : ''),
|
|
161
|
+
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
162
|
+
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
163
|
+
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
164
|
+
permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current ? (current.permissionPreset || 'default') : 'default'),
|
|
165
|
+
...mergeExecutionFields(current, body, parsed),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --------------------------------------------------------- export / import
|
|
170
|
+
|
|
171
|
+
function handleTaskExport({ store, res }) {
|
|
172
|
+
const doc = buildTaskExport(store.list({ status: 'all' }));
|
|
173
|
+
sendJson(res, 200, { ok: true, document: doc, count: doc.tasks.length });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleTaskImport({ store, scheduler, req, res }) {
|
|
177
|
+
const { body, error } = await readBody(req, res);
|
|
178
|
+
if (error) return;
|
|
179
|
+
const strategy = IMPORT_STRATEGIES.includes(body && body.strategy) ? body.strategy : 'skip';
|
|
180
|
+
const checked = validateImportDocument(body && body.document);
|
|
181
|
+
if (!checked.ok) {
|
|
182
|
+
sendJson(res, 400, { ok: false, error: checked.error });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// The import file is external input: importing code-executing tasks needs
|
|
186
|
+
// the same explicit confirmation that creating one over HTTP needs,
|
|
187
|
+
// otherwise a hand-edited file would bypass that gate (#86).
|
|
188
|
+
if (hasCodeExecutingTask(checked.tasks) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
189
|
+
sendJson(res, 403, {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: `Importing code-executing tasks over HTTP requires the ${SCRIPT_CONFIRM_HEADER}: script header`,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const existingIds = store.list({ status: 'all' }).map((x) => x.id);
|
|
196
|
+
const plan = planImport(checked.tasks, existingIds, strategy);
|
|
197
|
+
const summary = { add: plan.add.length, replace: plan.replace.length, skip: plan.skip.length };
|
|
198
|
+
if (body && body.dryRun) {
|
|
199
|
+
sendJson(res, 200, {
|
|
200
|
+
ok: true,
|
|
201
|
+
dryRun: true,
|
|
202
|
+
strategy: plan.strategy,
|
|
203
|
+
summary,
|
|
204
|
+
codeExecuting: hasCodeExecutingTask(plan.add.concat(plan.replace)),
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const applied = applyImportPlan({ store, scheduler, plan });
|
|
209
|
+
if (applied.error) {
|
|
210
|
+
sendJson(res, 500, { ok: false, error: applied.error });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
sendJson(res, 200, { ok: true, imported: applied.imported.length, summary, tasks: applied.imported });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Write an import plan. Every imported task is paused (the file's status is
|
|
218
|
+
* ignored) and every touched id is disarmed explicitly, because writing
|
|
219
|
+
* "paused" does not stop an already-armed croner job. A failed write rolls the
|
|
220
|
+
* store back so a partial import cannot survive.
|
|
221
|
+
*/
|
|
222
|
+
function applyImportPlan({ store, scheduler, plan }) {
|
|
223
|
+
const imported = [];
|
|
224
|
+
const rollback = [];
|
|
225
|
+
const disarm = [];
|
|
226
|
+
try {
|
|
227
|
+
for (const task of plan.add) {
|
|
228
|
+
const before = store.get(task.id);
|
|
229
|
+
const created = store.set({ ...task, id: task.id || randomUUID(), status: 'paused', updatedAt: Date.now() });
|
|
230
|
+
imported.push(created);
|
|
231
|
+
rollback.push({ id: created.id, previous: before || null });
|
|
232
|
+
disarm.push(created.id);
|
|
233
|
+
}
|
|
234
|
+
for (const task of plan.replace) {
|
|
235
|
+
const before = store.get(task.id);
|
|
236
|
+
if (!before) continue;
|
|
237
|
+
const replaced = store.set({ ...task, id: task.id, status: 'paused', updatedAt: Date.now() });
|
|
238
|
+
imported.push(replaced);
|
|
239
|
+
rollback.push({ id: replaced.id, previous: before });
|
|
240
|
+
disarm.push(replaced.id);
|
|
241
|
+
}
|
|
242
|
+
} catch (writeErr) {
|
|
243
|
+
for (const entry of rollback.reverse()) {
|
|
244
|
+
try {
|
|
245
|
+
if (entry.previous) store.set(entry.previous);
|
|
246
|
+
else store.delete(entry.id);
|
|
247
|
+
} catch (_) {}
|
|
248
|
+
}
|
|
249
|
+
return { imported: [], error: `Import failed and was rolled back: ${writeErr.message}` };
|
|
250
|
+
}
|
|
251
|
+
// Disarm outside the write transaction: pauseTask is idempotent and a
|
|
252
|
+
// failure here must not undo a successful import.
|
|
253
|
+
for (const id of disarm) {
|
|
254
|
+
try {
|
|
255
|
+
scheduler.pauseTask(id);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error('[dsh-cron] import: could not pause task', id + ':', err.message);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { imported };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ------------------------------------------------------------ single task
|
|
264
|
+
|
|
265
|
+
async function handleTaskItem({ store, scheduler, req, res, url, id, action }) {
|
|
266
|
+
if (req.method === 'GET' && (action === 'history' || !action)) {
|
|
267
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
268
|
+
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (req.method === 'POST') {
|
|
272
|
+
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action });
|
|
273
|
+
if (handled) return;
|
|
274
|
+
}
|
|
275
|
+
if (req.method === 'PATCH') {
|
|
276
|
+
await patchTask({ store, scheduler, req, res, id });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
280
|
+
deleteTask({ store, scheduler, res, id });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Task actions that are plain POSTs. Returns true when one matched. */
|
|
287
|
+
async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
|
|
288
|
+
if (action === 'run') {
|
|
289
|
+
if (!store.get(id)) {
|
|
290
|
+
sendJson(res, 404, NOT_FOUND);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
await scheduler.triggerManualRun(id);
|
|
294
|
+
sendJson(res, 200, { ok: true, task: store.get(id) });
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (action === 'duplicate') {
|
|
298
|
+
const source = store.get(id);
|
|
299
|
+
if (!source) {
|
|
300
|
+
sendJson(res, 404, NOT_FOUND);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const copy = buildDuplicateTask(source, { id: randomUUID() });
|
|
304
|
+
store.set(copy);
|
|
305
|
+
// A copy starts paused: it must never fire on its own before the user
|
|
306
|
+
// reviews the schedule (a duplicated one-shot may point at a past time).
|
|
307
|
+
scheduler.pauseTask(copy.id);
|
|
308
|
+
sendJson(res, 200, { ok: true, task: copy });
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (action === 'pause' || action === 'resume') {
|
|
312
|
+
const task = action === 'pause' ? scheduler.pauseTask(id) : scheduler.resumeTask(id);
|
|
313
|
+
if (!task) {
|
|
314
|
+
sendJson(res, 404, NOT_FOUND);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
sendJson(res, 200, { ok: true, task });
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle')) {
|
|
321
|
+
if (!store.get(id)) {
|
|
322
|
+
sendJson(res, 404, NOT_FOUND);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
const task = scheduler.toggleTask(id);
|
|
326
|
+
sendJson(res, 200, { ok: true, task });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function patchTask({ store, scheduler, req, res, id }) {
|
|
333
|
+
const current = store.get(id);
|
|
334
|
+
if (!current) {
|
|
335
|
+
sendJson(res, 404, NOT_FOUND);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { body, error } = await readBody(req, res);
|
|
339
|
+
if (error) return;
|
|
340
|
+
const result = applyTaskPatch({
|
|
341
|
+
store,
|
|
342
|
+
scheduler,
|
|
343
|
+
id,
|
|
344
|
+
body,
|
|
345
|
+
allowCodeSwitch: req.headers[SCRIPT_CONFIRM_HEADER] === 'script',
|
|
346
|
+
});
|
|
347
|
+
if (!result.ok) {
|
|
348
|
+
const status = result.notFound ? 404 : (result.needsConfirmation ? 403 : 400);
|
|
349
|
+
const error = result.needsConfirmation
|
|
350
|
+
? `Switching a task to the ${normalizeTaskType(body.type)} type over HTTP requires the ${SCRIPT_CONFIRM_HEADER}: script header`
|
|
351
|
+
: result.error;
|
|
352
|
+
sendJson(res, status, { ok: false, error });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
sendJson(res, 200, { ok: true, task: result.task });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function deleteTask({ store, scheduler, res, id }) {
|
|
359
|
+
const current = store.get(id);
|
|
360
|
+
if (current) {
|
|
361
|
+
scheduler.pauseTask(id);
|
|
362
|
+
store.delete(id);
|
|
363
|
+
}
|
|
364
|
+
sendJson(res, 200, { ok: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Runtime and execution settings of a task, merged onto the stored one. Split
|
|
369
|
+
* out of buildTaskRecord so both stay readable.
|
|
370
|
+
*/
|
|
371
|
+
function mergeExecutionFields(current, body, parsed) {
|
|
372
|
+
return {
|
|
373
|
+
env: body.env && typeof body.env === 'object' ? body.env : (current ? current.env : undefined),
|
|
374
|
+
cwd: body.cwd !== undefined ? String(body.cwd).trim() : (current ? current.cwd : ''),
|
|
375
|
+
channels: Array.isArray(body.channels)
|
|
376
|
+
? body.channels.filter((c) => CHANNEL_IDS.includes(c))
|
|
377
|
+
: (current ? current.channels : undefined),
|
|
378
|
+
template: body.template !== undefined ? String(body.template) : (current ? current.template : ''),
|
|
379
|
+
silentRule: body.silentRule !== undefined ? String(body.silentRule) : (current ? current.silentRule : ''),
|
|
380
|
+
inspectOnFailure: body.inspectOnFailure !== undefined ? Boolean(body.inspectOnFailure) : (current ? Boolean(current.inspectOnFailure) : false),
|
|
381
|
+
workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
|
|
382
|
+
worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
|
|
383
|
+
keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
|
|
384
|
+
httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
|
|
385
|
+
httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
|
|
386
|
+
httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
|
|
387
|
+
httpBody: body.httpBody !== undefined ? String(body.httpBody) : (current ? current.httpBody : ''),
|
|
388
|
+
sshProfileId: body.sshProfileId !== undefined ? String(body.sshProfileId).trim() : (current ? current.sshProfileId : ''),
|
|
389
|
+
sshTarget: body.sshTarget !== undefined ? String(body.sshTarget).trim() : (current ? current.sshTarget : ''),
|
|
390
|
+
sshPort: body.sshPort !== undefined ? Number(body.sshPort) || 0 : (current ? current.sshPort : 0),
|
|
391
|
+
sshKeyPath: body.sshKeyPath !== undefined ? String(body.sshKeyPath).trim() : (current ? current.sshKeyPath : ''),
|
|
392
|
+
dockerImage: body.dockerImage !== undefined ? String(body.dockerImage).trim() : (current ? current.dockerImage : ''),
|
|
393
|
+
pythonPath: body.pythonPath !== undefined ? String(body.pythonPath).trim() : (current ? current.pythonPath : ''),
|
|
394
|
+
nodePath: body.nodePath !== undefined ? String(body.nodePath).trim() : (current ? current.nodePath : ''),
|
|
395
|
+
skillName: body.skillName !== undefined ? String(body.skillName).trim() : (current ? current.skillName : ''),
|
|
396
|
+
workflowName: body.workflowName !== undefined ? String(body.workflowName).trim() : (current ? current.workflowName : ''),
|
|
397
|
+
oneShot: Boolean((parsed && parsed.isOneShot) || (body && body.oneShot)),
|
|
398
|
+
};
|
|
399
|
+
}
|