circuschief 0.8.0 → 1.0.0
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/package.json +1 -1
- package/packages/server/src/api/commandButtons.js +16 -15
- package/packages/server/src/api/projects-commandButtons.js +6 -6
- package/packages/server/src/api/projects-session-create.js +109 -0
- package/packages/server/src/api/projects-session-defaults.js +51 -0
- package/packages/server/src/api/projects-session-helpers.js +47 -1
- package/packages/server/src/api/projects-templates.js +38 -0
- package/packages/server/src/api/projects.js +28 -180
- package/packages/server/src/api/sessions-commands.js +21 -18
- package/packages/server/src/api/sessions-patch.js +41 -1
- package/packages/server/src/db/SessionRepository.js +1 -1
- package/packages/server/src/db/SessionTemplateRepository.js +23 -2
- package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
- package/packages/server/src/db/migrations/index.js +225 -6
- package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
- package/packages/server/src/db/migrations/miscMigrations.js +244 -0
- package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
- package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
- package/packages/server/src/db/migrations/providerMigrations.js +165 -0
- package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
- package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
- package/packages/server/src/db/session-helpers.js +26 -1
- package/packages/server/src/schema.sql +4 -0
- package/packages/server/src/services/commandButtonPrompts.js +9 -7
- package/packages/server/src/services/gitCommitAttribution.js +38 -8
- package/packages/server/src/services/gitDiff.js +132 -0
- package/packages/server/src/services/gitRepoUrl.js +174 -0
- package/packages/server/src/services/gitService.js +37 -309
- package/packages/server/src/services/gitWorktree.js +127 -0
- package/packages/server/src/services/sessionPrompts.js +1 -1
- package/packages/shared/src/contracts/sessions.js +27 -1
- package/packages/shared/src/contracts/templates.js +10 -0
- package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
- package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-xdfI2bR6.js} +2 -2
- package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-B-o0DgMH.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-Dxn1li4l.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
- package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
- package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-BNYKujL-.css} +1 -1
- package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
- package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
- package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
- package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
- package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CYmmAcBD.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-DEhqw3Jv.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
- package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
- package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
- package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
- package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
- package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-hqijxc0S.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
- package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-5NfVr9pF.js} +6 -6
- package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DI8ncOAV.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-Cy04d7-o.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-C2Qs35mm.js} +1 -1
- package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
- package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
- package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-CoU3G4zK.js} +1 -1
- package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
- package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
- package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
- package/packages/web/dist/assets/{index-DgkC10TW.js → index-BjuRttEY.js} +3 -3
- package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
- package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
- package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
- package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
- package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
- package/packages/web/dist/assets/{index-DtfUt785.js → index-CP-SxOlV.js} +1 -1
- package/packages/web/dist/assets/index-CslU0psO.js +1 -0
- package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
- package/packages/web/dist/assets/index-DOzONENy.js +1 -0
- package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
- package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
- package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
- package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
- package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
- package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
- package/packages/web/dist/assets/{index-BY174HVJ.css → index-uySCcnA_.css} +1 -1
- package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
- package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-C-8PSxKi.js} +1 -1
- package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-oXifvvqN.js} +1 -1
- package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-Nq5VafSf.js} +1 -1
- package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-DtpuiyT6.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
- package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
- package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
- package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
- package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
- package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
- package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
- package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
- package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
- package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
- package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
- package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
- package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
- package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
- package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
- package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
- package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
- package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
- package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
- package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
- package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
- package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
- package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
- package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
- package/packages/web/dist/assets/index-fK8FIZgP.js +0 -83
- package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
- package/packages/web/dist/assets/index-irD539ZM.js +0 -3
- package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { projects, sessions,
|
|
2
|
+
import { projects, sessions, commandRuns } from '../database.js';
|
|
3
3
|
import { commandRunner } from '../services/commandRunner.js';
|
|
4
|
-
import { CreateProjectRequest, UpdateProjectRequest
|
|
5
|
-
import { ProjectDefaultsRepository } from '../db/ProjectDefaultsRepository.js';
|
|
6
|
-
import { CreateSessionTemplateRequest } from '../../../shared/src/contracts/templates.js';
|
|
7
|
-
import { broadcastToProject } from '../websocket.js';
|
|
4
|
+
import { CreateProjectRequest, UpdateProjectRequest } from '../../../shared/src/contracts/projects.js';
|
|
8
5
|
import projectCommandButtonsRouter from './projects-commandButtons.js';
|
|
9
|
-
import
|
|
6
|
+
import projectSessionDefaultsRouter from './projects-session-defaults.js';
|
|
7
|
+
import projectTemplatesRouter from './projects-templates.js';
|
|
10
8
|
import { handleUploadError, uploadMiddleware } from '../middleware/upload.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
prepareSessionConfig,
|
|
14
|
-
applyTemplateOverrides,
|
|
15
|
-
resolveNextTemplateId,
|
|
16
|
-
determineInitialStatus,
|
|
17
|
-
buildSchedulingUpdate,
|
|
18
|
-
setupAndStartSession,
|
|
19
|
-
} from './projects-session-helpers.js';
|
|
20
|
-
import { validateGitSettings, buildRunsBySession } from './projects-helpers.js';
|
|
9
|
+
import { determineInitialStatus } from './projects-session-helpers.js';
|
|
10
|
+
import { buildRunsBySession } from './projects-helpers.js';
|
|
21
11
|
import { resolveAgentTypeFromModel } from '../services/sessionProvider.js';
|
|
22
12
|
import { access, constants } from 'fs/promises';
|
|
23
13
|
import { dirname, isAbsolute } from 'path';
|
|
14
|
+
import { getRepositoryUrl } from '../services/gitService.js';
|
|
15
|
+
import { validateAndPrepareSessionConfig, createSessionRow, startSessionOrFail } from './projects-session-create.js';
|
|
24
16
|
|
|
25
17
|
// Error message constants
|
|
26
18
|
const ERR_PROJECT_NOT_FOUND = 'Project not found';
|
|
@@ -65,17 +57,31 @@ router.post('/', async (req, res) => {
|
|
|
65
57
|
return res.status(400).json({ error: result.error.issues[0].message });
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted, worktreePath, kanbanEnabled } = result.data;
|
|
60
|
+
const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted, worktreePath, kanbanEnabled, repoUrl } = result.data;
|
|
69
61
|
|
|
70
62
|
const pathError = await validateWorktreePath(worktreePath);
|
|
71
63
|
if (pathError) {
|
|
72
64
|
return res.status(400).json({ error: pathError });
|
|
73
65
|
}
|
|
74
66
|
|
|
67
|
+
// Resolve repoUrl:
|
|
68
|
+
// - string: use as-is (explicitly provided)
|
|
69
|
+
// - null: suppress detection (explicitly sent as null)
|
|
70
|
+
// - undefined: auto-detect from git remote
|
|
71
|
+
let resolvedRepoUrl = repoUrl;
|
|
72
|
+
if (resolvedRepoUrl === undefined) {
|
|
73
|
+
try {
|
|
74
|
+
resolvedRepoUrl = await getRepositoryUrl(workingDirectory);
|
|
75
|
+
} catch {
|
|
76
|
+
resolvedRepoUrl = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
const createOptions = {
|
|
76
81
|
onSessionCreated: onSessionCreated || null,
|
|
77
82
|
onSessionDeleted: onSessionDeleted || null,
|
|
78
83
|
worktreePath: worktreePath || null,
|
|
84
|
+
repoUrl: resolvedRepoUrl,
|
|
79
85
|
};
|
|
80
86
|
if (kanbanEnabled !== undefined) {
|
|
81
87
|
createOptions.kanbanEnabled = kanbanEnabled;
|
|
@@ -198,98 +204,6 @@ router.get('/:id/sessions', (req, res) => {
|
|
|
198
204
|
}
|
|
199
205
|
});
|
|
200
206
|
|
|
201
|
-
/**
|
|
202
|
-
* Validate and prepare the session configuration from the request body.
|
|
203
|
-
* Returns { config, nextTemplateId } on success, or { error, status } on failure.
|
|
204
|
-
*/
|
|
205
|
-
async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, project) {
|
|
206
|
-
const projectDefs = projectDefaults.getByProjectId(projectId);
|
|
207
|
-
const systemDefaults = ProjectDefaultsRepository.getSystemDefaults();
|
|
208
|
-
const config = prepareSessionConfig(reqBody, projectDefs, systemDefaults);
|
|
209
|
-
config.files = reqFiles || [];
|
|
210
|
-
|
|
211
|
-
if (!config.prompt) {
|
|
212
|
-
return { error: 'Prompt is required', status: 400 };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (config.parentSessionId) {
|
|
216
|
-
const parentSession = sessions.getById(config.parentSessionId);
|
|
217
|
-
if (!parentSession) {
|
|
218
|
-
return { error: 'Parent session not found', status: 404 };
|
|
219
|
-
}
|
|
220
|
-
if (parentSession.projectId !== projectId) {
|
|
221
|
-
return { error: 'Parent session does not belong to this project', status: 400 };
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Apply template overrides and resolve nextTemplateId
|
|
226
|
-
applyTemplateOverrides(config);
|
|
227
|
-
const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(reqBody, config.nextTemplateId || null);
|
|
228
|
-
if (nextTemplateError) {
|
|
229
|
-
return { error: nextTemplateError, status: 400 };
|
|
230
|
-
}
|
|
231
|
-
config.nextTemplateId = nextTemplateId;
|
|
232
|
-
|
|
233
|
-
// Validate git settings for git repos
|
|
234
|
-
const { config: updatedConfig, error: gitError } = await validateGitSettings(config, project);
|
|
235
|
-
if (gitError) {
|
|
236
|
-
return { error: gitError, status: 400 };
|
|
237
|
-
}
|
|
238
|
-
Object.assign(config, updatedConfig);
|
|
239
|
-
|
|
240
|
-
return { config, nextTemplateId };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Create the session row and apply any post-create updates.
|
|
245
|
-
* Returns the created session (already persisted in DB).
|
|
246
|
-
*/
|
|
247
|
-
function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
|
|
248
|
-
const sessionName = config.name || generateInitialName(config.prompt);
|
|
249
|
-
const session = sessions.create(projectId, sessionName, config.prompt, {
|
|
250
|
-
mode: config.mode,
|
|
251
|
-
thinkingEnabled: config.thinkingEnabled,
|
|
252
|
-
gitBranch: config.gitBranch,
|
|
253
|
-
parentSessionId: config.parentSessionId,
|
|
254
|
-
status: initialStatus,
|
|
255
|
-
model: config.model,
|
|
256
|
-
providerId: config.providerId,
|
|
257
|
-
effortLevel: config.effortLevel,
|
|
258
|
-
agentType: config.agentType,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const postCreateUpdate = {
|
|
262
|
-
...(nextTemplateId ? { nextTemplateId } : {}),
|
|
263
|
-
...buildSchedulingUpdate(config, initialStatus),
|
|
264
|
-
};
|
|
265
|
-
if (Object.keys(postCreateUpdate).length > 0) {
|
|
266
|
-
sessions.update(session.id, postCreateUpdate);
|
|
267
|
-
}
|
|
268
|
-
return session;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Run setupAndStartSession and translate any failure into an error response,
|
|
273
|
-
* marking the session as errored and broadcasting the update.
|
|
274
|
-
*/
|
|
275
|
-
async function startSessionOrFail(req, res, { session, config, project }) {
|
|
276
|
-
try {
|
|
277
|
-
const { updatedSession } = await setupAndStartSession({
|
|
278
|
-
session, config, project, projectId: req.params.id, files: config.files,
|
|
279
|
-
});
|
|
280
|
-
return res.status(201).json(updatedSession);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
console.error('Git setup error:', error);
|
|
283
|
-
const updatedSession = sessions.update(session.id, { status: 'error', error: error.message });
|
|
284
|
-
broadcastToProject(req.params.id, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
285
|
-
projectId: req.params.id,
|
|
286
|
-
sessionId: session.id,
|
|
287
|
-
session: updatedSession,
|
|
288
|
-
});
|
|
289
|
-
return res.status(500).json({ error: `Git setup failed: ${error.message}` });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
207
|
// POST /api/projects/:id/sessions - Create session
|
|
294
208
|
// Supports both JSON and multipart/form-data (for file attachments)
|
|
295
209
|
router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, async (req, res) => {
|
|
@@ -335,79 +249,13 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
|
|
|
335
249
|
}
|
|
336
250
|
});
|
|
337
251
|
|
|
338
|
-
//
|
|
339
|
-
router.
|
|
340
|
-
const project = projects.getById(req.params.id);
|
|
341
|
-
if (!project) {
|
|
342
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const available = sessionTemplates.getAvailableForProject(req.params.id);
|
|
346
|
-
res.json(available);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// POST /api/projects/:id/templates - Create project template
|
|
350
|
-
router.post('/:id/templates', (req, res) => {
|
|
351
|
-
const project = projects.getById(req.params.id);
|
|
352
|
-
if (!project) {
|
|
353
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const result = CreateSessionTemplateRequest.safeParse(req.body);
|
|
357
|
-
if (!result.success) {
|
|
358
|
-
return res.status(400).json({ error: result.error.issues[0].message });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const template = sessionTemplates.create({
|
|
362
|
-
projectId: req.params.id,
|
|
363
|
-
...result.data,
|
|
364
|
-
});
|
|
365
|
-
res.status(201).json(template);
|
|
366
|
-
});
|
|
252
|
+
// Template routes are mounted as a sub-router
|
|
253
|
+
router.use('/:id/templates', projectTemplatesRouter);
|
|
367
254
|
|
|
368
255
|
// Command button routes are mounted as a sub-router
|
|
369
|
-
router.use('/:id/
|
|
370
|
-
|
|
371
|
-
// GET /api/projects/:id/session-defaults - Get session defaults for project
|
|
372
|
-
router.get('/:id/session-defaults', (req, res) => {
|
|
373
|
-
const project = projects.getById(req.params.id);
|
|
374
|
-
if (!project) {
|
|
375
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const defaults = projectDefaults.getByProjectId(req.params.id);
|
|
379
|
-
if (!defaults) {
|
|
380
|
-
return res.json(null);
|
|
381
|
-
}
|
|
256
|
+
router.use('/:id/circus-commands', projectCommandButtonsRouter);
|
|
382
257
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// POST /api/projects/:id/session-defaults - Update/create session defaults for project
|
|
387
|
-
router.post('/:id/session-defaults', (req, res) => {
|
|
388
|
-
const project = projects.getById(req.params.id);
|
|
389
|
-
if (!project) {
|
|
390
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const result = ProjectSessionDefaultsRequest.safeParse(req.body);
|
|
394
|
-
if (!result.success) {
|
|
395
|
-
return res.status(400).json({ error: result.error.issues[0].message });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const updated = projectDefaults.upsert(req.params.id, result.data);
|
|
399
|
-
res.status(200).json(updated);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// DELETE /api/projects/:id/session-defaults - Reset session defaults for project
|
|
403
|
-
router.delete('/:id/session-defaults', (req, res) => {
|
|
404
|
-
const project = projects.getById(req.params.id);
|
|
405
|
-
if (!project) {
|
|
406
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
projectDefaults.resetToDefaults(req.params.id);
|
|
410
|
-
res.json({ message: 'Session defaults reset to system defaults' });
|
|
411
|
-
});
|
|
258
|
+
// Session defaults routes are mounted as a sub-router
|
|
259
|
+
router.use('/:id/session-defaults', projectSessionDefaultsRouter);
|
|
412
260
|
|
|
413
261
|
export default router;
|
|
@@ -6,6 +6,9 @@ import { requireRootSessionAndProject } from '../middleware/sessionLookup.js';
|
|
|
6
6
|
import { commandRunner } from '../services/commandRunner.js';
|
|
7
7
|
import { databaseManager } from '../db/DatabaseManager.js';
|
|
8
8
|
|
|
9
|
+
// Error message constants
|
|
10
|
+
const ERR_BUTTON_NOT_FOUND = 'Circus Command not found';
|
|
11
|
+
|
|
9
12
|
const router = Router();
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -46,13 +49,13 @@ function broadcastCommandError(ctx, errorMessage) {
|
|
|
46
49
|
broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, { projectId, sessionId, runId, buttonId, error: errorMessage });
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
// GET /api/sessions/:id/
|
|
50
|
-
router.get('/:id/
|
|
52
|
+
// GET /api/sessions/:id/circus-commands - List command buttons for the workflow project
|
|
53
|
+
router.get('/:id/circus-commands', requireRootSessionAndProject, (req, res) => {
|
|
51
54
|
res.json(commandButtons.getByProjectId(req.rootSession_.projectId));
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
// POST /api/sessions/:id/
|
|
55
|
-
router.post('/:id/
|
|
57
|
+
// POST /api/sessions/:id/circus-commands/:buttonId/run - Execute button command
|
|
58
|
+
router.post('/:id/circus-commands/:buttonId/run', requireRootSessionAndProject, (req, res) => {
|
|
56
59
|
const sessionId = req.rootSessionId;
|
|
57
60
|
const buttonId = req.params.buttonId;
|
|
58
61
|
|
|
@@ -60,10 +63,10 @@ router.post('/:id/command-buttons/:buttonId/run', requireRootSessionAndProject,
|
|
|
60
63
|
|
|
61
64
|
const button = commandButtons.getById(buttonId);
|
|
62
65
|
if (!button) {
|
|
63
|
-
return res.status(404).json({ error:
|
|
66
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
64
67
|
}
|
|
65
68
|
if (button.projectId !== req.rootSession_.projectId) {
|
|
66
|
-
return res.status(404).json({ error:
|
|
69
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
// Generate run ID
|
|
@@ -105,16 +108,16 @@ router.post('/:id/command-buttons/:buttonId/run', requireRootSessionAndProject,
|
|
|
105
108
|
})();
|
|
106
109
|
});
|
|
107
110
|
|
|
108
|
-
// GET /api/sessions/:id/
|
|
109
|
-
router.get('/:id/
|
|
111
|
+
// GET /api/sessions/:id/circus-commands/runs - Get active runs for session
|
|
112
|
+
router.get('/:id/circus-commands/runs', requireRootSessionAndProject, (req, res) => {
|
|
110
113
|
const sessionId = req.rootSessionId;
|
|
111
114
|
|
|
112
115
|
const activeRuns = commandRunner.getRunsBySession(sessionId);
|
|
113
116
|
res.json(activeRuns);
|
|
114
117
|
});
|
|
115
118
|
|
|
116
|
-
// GET /api/sessions/:id/
|
|
117
|
-
router.get('/:id/
|
|
119
|
+
// GET /api/sessions/:id/circus-commands/runs/:runId - Get single run by ID
|
|
120
|
+
router.get('/:id/circus-commands/runs/:runId', requireRootSessionAndProject, (req, res) => {
|
|
118
121
|
const { runId } = req.params;
|
|
119
122
|
const sessionId = req.rootSessionId;
|
|
120
123
|
|
|
@@ -144,8 +147,8 @@ router.get('/:id/command-buttons/runs/:runId', requireRootSessionAndProject, (re
|
|
|
144
147
|
});
|
|
145
148
|
});
|
|
146
149
|
|
|
147
|
-
// DELETE /api/sessions/:id/
|
|
148
|
-
router.delete('/:id/
|
|
150
|
+
// DELETE /api/sessions/:id/circus-commands/runs/:runId - Delete a command run record
|
|
151
|
+
router.delete('/:id/circus-commands/runs/:runId', requireRootSessionAndProject, (req, res) => {
|
|
149
152
|
const sessionId = req.rootSessionId;
|
|
150
153
|
const { runId } = req.params;
|
|
151
154
|
|
|
@@ -178,17 +181,17 @@ router.delete('/:id/command-buttons/runs/:runId', requireRootSessionAndProject,
|
|
|
178
181
|
res.status(204).send();
|
|
179
182
|
});
|
|
180
183
|
|
|
181
|
-
// DELETE /api/sessions/:id/
|
|
182
|
-
router.delete('/:id/
|
|
184
|
+
// DELETE /api/sessions/:id/circus-commands/:buttonId/runs/all - Delete all runs for a button in a session
|
|
185
|
+
router.delete('/:id/circus-commands/:buttonId/runs/all', requireRootSessionAndProject, (req, res) => {
|
|
183
186
|
const sessionId = req.rootSessionId;
|
|
184
187
|
const { buttonId } = req.params;
|
|
185
188
|
|
|
186
189
|
const button = commandButtons.getById(buttonId);
|
|
187
190
|
if (!button) {
|
|
188
|
-
return res.status(404).json({ error:
|
|
191
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
189
192
|
}
|
|
190
193
|
if (button.projectId !== req.rootSession_.projectId) {
|
|
191
|
-
return res.status(404).json({ error:
|
|
194
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
const { deletedRuns } = commandRuns.deleteByButtonAndSession(buttonId, sessionId);
|
|
@@ -213,8 +216,8 @@ router.delete('/:id/command-buttons/:buttonId/runs/all', requireRootSessionAndPr
|
|
|
213
216
|
res.status(204).send();
|
|
214
217
|
});
|
|
215
218
|
|
|
216
|
-
// POST /api/sessions/:id/
|
|
217
|
-
router.post('/:id/
|
|
219
|
+
// POST /api/sessions/:id/circus-commands/runs/:runId/kill - Kill running command
|
|
220
|
+
router.post('/:id/circus-commands/runs/:runId/kill', requireRootSessionAndProject, (req, res) => {
|
|
218
221
|
const sessionId = req.rootSessionId;
|
|
219
222
|
const runId = req.params.runId;
|
|
220
223
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { sessions, sessionTemplates, modelProviders } from '../database.js';
|
|
2
|
+
import { sessions, sessionTemplates, modelProviders, sessionSummaries } from '../database.js';
|
|
3
3
|
import { broadcastToSession, broadcastToProject } from '../websocket.js';
|
|
4
4
|
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
5
5
|
import * as summaryService from '../services/summaryService.js';
|
|
6
6
|
import { setSessionNameFromPr } from '../services/prUrlService.js';
|
|
7
|
+
import { checkSessionCiStatusNow } from '../services/prStatusService.js';
|
|
8
|
+
import { broadcastSummaryUpdate } from '../services/summaryBroadcast.js';
|
|
7
9
|
import { requireSession } from '../middleware/sessionLookup.js';
|
|
8
10
|
|
|
9
11
|
const router = Router();
|
|
@@ -195,6 +197,30 @@ function broadcastSessionUpdate(sessionId, projectId, updated, updateData) {
|
|
|
195
197
|
});
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Reset all PR state fields in the session summary when the PR URL changes or is cleared.
|
|
202
|
+
* This ensures stale PR state (e.g., "merged") doesn't persist for a different PR
|
|
203
|
+
* and doesn't block summary regeneration.
|
|
204
|
+
* @param {string} sessionId
|
|
205
|
+
* @param {string|null} projectId - For broadcasting to project subscribers
|
|
206
|
+
*/
|
|
207
|
+
function resetPrStateForSession(sessionId, projectId) {
|
|
208
|
+
const existingSummary = sessionSummaries.getBySessionId(sessionId);
|
|
209
|
+
if (!existingSummary) return;
|
|
210
|
+
|
|
211
|
+
sessionSummaries.upsert(sessionId, {
|
|
212
|
+
prState: null,
|
|
213
|
+
prMerged: false,
|
|
214
|
+
hasMergeConflicts: false,
|
|
215
|
+
ciStatus: null,
|
|
216
|
+
ciFailures: [],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Broadcast the reset to both session and project subscribers
|
|
220
|
+
const updatedSummary = sessionSummaries.getBySessionId(sessionId);
|
|
221
|
+
broadcastSummaryUpdate(sessionId, projectId, updatedSummary);
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
// PATCH /api/sessions/:id - Update session settings
|
|
199
225
|
router.patch('/:id', requireSession, (req, res) => {
|
|
200
226
|
const { updateData, error } = buildUpdateData(req.body);
|
|
@@ -209,6 +235,15 @@ router.patch('/:id', requireSession, (req, res) => {
|
|
|
209
235
|
|
|
210
236
|
const updated = sessions.update(req.params.id, updateData);
|
|
211
237
|
|
|
238
|
+
// Reset PR state when URL changes to a different PR or is cleared
|
|
239
|
+
const previousPrUrl = req.session_.prUrl;
|
|
240
|
+
const prUrlProvided = Object.prototype.hasOwnProperty.call(updateData, 'prUrl');
|
|
241
|
+
const prUrlChanged = prUrlProvided && previousPrUrl && previousPrUrl !== updateData.prUrl;
|
|
242
|
+
|
|
243
|
+
if (prUrlChanged) {
|
|
244
|
+
resetPrStateForSession(req.params.id, req.session_.projectId);
|
|
245
|
+
}
|
|
246
|
+
|
|
212
247
|
// Propagate PR URL to parent session if set (not when clearing)
|
|
213
248
|
if (updateData.prUrl) {
|
|
214
249
|
summaryService.propagatePrUrlToParent(req.params.id, updateData.prUrl);
|
|
@@ -218,6 +253,11 @@ router.patch('/:id', requireSession, (req, res) => {
|
|
|
218
253
|
setSessionNameFromPr(req.params.id, updateData.prUrl).catch(err => {
|
|
219
254
|
console.error(`[Sessions API] Failed to set session name from PR:`, err);
|
|
220
255
|
});
|
|
256
|
+
|
|
257
|
+
// Trigger immediate PR status check for the new/changed URL
|
|
258
|
+
checkSessionCiStatusNow(req.params.id).catch(err => {
|
|
259
|
+
console.error(`[Sessions API] Failed to check PR status after URL change:`, err);
|
|
260
|
+
});
|
|
221
261
|
}
|
|
222
262
|
|
|
223
263
|
broadcastSessionUpdate(req.params.id, req.session_.projectId, updated, updateData);
|
|
@@ -74,6 +74,7 @@ export class SessionRepository extends BaseRepository {
|
|
|
74
74
|
createdAt: row.created_at,
|
|
75
75
|
updatedAt: row.updated_at,
|
|
76
76
|
lastActivityAt: row.last_activity_at ?? null,
|
|
77
|
+
lastMessageAt: row.last_message_at ?? null,
|
|
77
78
|
activeTimeMs: row.active_time_ms || 0,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
@@ -147,7 +148,6 @@ export class SessionRepository extends BaseRepository {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
sql += ` ORDER BY
|
|
150
|
-
starred DESC,
|
|
151
151
|
COALESCE(last_activity_at, updated_at, created_at) DESC,
|
|
152
152
|
updated_at DESC,
|
|
153
153
|
created_at DESC,
|
|
@@ -23,6 +23,10 @@ export class SessionTemplateRepository extends BaseRepository {
|
|
|
23
23
|
mode: row.mode || null,
|
|
24
24
|
effortLevel: row.effort_level ?? null,
|
|
25
25
|
targetLaneId: row.target_lane_id || null,
|
|
26
|
+
showInQuickResponses: Boolean(row.show_in_quick_responses),
|
|
27
|
+
quickResponseAutoSubmit: Boolean(row.quick_response_auto_submit),
|
|
28
|
+
quickResponseSortOrder: row.quick_response_sort_order ?? 0,
|
|
29
|
+
legacyQuickResponseId: row.legacy_quick_response_id || null,
|
|
26
30
|
createdAt: row.created_at,
|
|
27
31
|
updatedAt: row.updated_at,
|
|
28
32
|
};
|
|
@@ -51,13 +55,22 @@ export class SessionTemplateRepository extends BaseRepository {
|
|
|
51
55
|
return value ? 1 : 0;
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
static #normalizeBoolean(value) {
|
|
59
|
+
return value ? 1 : 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
create(data) {
|
|
55
63
|
const id = databaseManager.generateId();
|
|
56
64
|
const now = Date.now();
|
|
57
65
|
this.db
|
|
58
66
|
.prepare(
|
|
59
|
-
`INSERT INTO session_templates (
|
|
60
|
-
|
|
67
|
+
`INSERT INTO session_templates (
|
|
68
|
+
id, project_id, name, prompt, next_template_id, thinking_enabled,
|
|
69
|
+
git_branch, git_mode, model, mode, effort_level, target_lane_id,
|
|
70
|
+
show_in_quick_responses, quick_response_auto_submit,
|
|
71
|
+
quick_response_sort_order, legacy_quick_response_id,
|
|
72
|
+
created_at, updated_at
|
|
73
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
61
74
|
)
|
|
62
75
|
.run(
|
|
63
76
|
id,
|
|
@@ -72,6 +85,10 @@ export class SessionTemplateRepository extends BaseRepository {
|
|
|
72
85
|
data.mode !== undefined && data.mode !== null ? data.mode : null,
|
|
73
86
|
data.effortLevel ?? null,
|
|
74
87
|
data.targetLaneId || null,
|
|
88
|
+
SessionTemplateRepository.#normalizeBoolean(data.showInQuickResponses),
|
|
89
|
+
SessionTemplateRepository.#normalizeBoolean(data.quickResponseAutoSubmit),
|
|
90
|
+
data.quickResponseSortOrder ?? 0,
|
|
91
|
+
data.legacyQuickResponseId || null,
|
|
75
92
|
now,
|
|
76
93
|
now
|
|
77
94
|
);
|
|
@@ -93,6 +110,10 @@ export class SessionTemplateRepository extends BaseRepository {
|
|
|
93
110
|
mode: { column: 'mode', transform: (v) => v },
|
|
94
111
|
effortLevel: { column: 'effort_level', transform: (v) => v },
|
|
95
112
|
targetLaneId: { column: 'target_lane_id', transform: (v) => v },
|
|
113
|
+
showInQuickResponses: { column: 'show_in_quick_responses', transform: (v) => v ? 1 : 0 },
|
|
114
|
+
quickResponseAutoSubmit: { column: 'quick_response_auto_submit', transform: (v) => v ? 1 : 0 },
|
|
115
|
+
quickResponseSortOrder: { column: 'quick_response_sort_order', transform: (v) => v },
|
|
116
|
+
legacyQuickResponseId: { column: 'legacy_quick_response_id', transform: (v) => v || null },
|
|
96
117
|
};
|
|
97
118
|
|
|
98
119
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrations for the canvas_items table.
|
|
3
|
+
* Each export is an array of { name, up(db) } migration objects.
|
|
4
|
+
*/
|
|
5
|
+
import { addColumnIfMissing, getColumns, getTableSql } from './migrationUtils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Migrate canvas_items table to include 'code' in type CHECK constraint.
|
|
9
|
+
* SQLite doesn't support ALTER TABLE to modify constraints, so we recreate the table.
|
|
10
|
+
*/
|
|
11
|
+
function migrateCanvasItemsTypeConstraint(db) {
|
|
12
|
+
const tableSql = getTableSql(db, 'canvas_items');
|
|
13
|
+
|
|
14
|
+
// If schema already includes 'code', no migration needed
|
|
15
|
+
if (tableSql?.includes("'code'")) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE canvas_items_new (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
|
|
23
|
+
type TEXT NOT NULL CHECK (type IN ('image', 'markdown', 'text', 'json', 'pdf', 'code')),
|
|
24
|
+
content TEXT,
|
|
25
|
+
data TEXT,
|
|
26
|
+
mime_type TEXT,
|
|
27
|
+
filename TEXT,
|
|
28
|
+
label TEXT,
|
|
29
|
+
width INTEGER,
|
|
30
|
+
height INTEGER,
|
|
31
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
INSERT INTO canvas_items_new SELECT * FROM canvas_items;
|
|
35
|
+
|
|
36
|
+
DROP TABLE canvas_items;
|
|
37
|
+
|
|
38
|
+
ALTER TABLE canvas_items_new RENAME TO canvas_items;
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_canvas_session ON canvas_items(session_id);
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Migrate canvas_items table to drop label column.
|
|
46
|
+
*/
|
|
47
|
+
function migrateCanvasItemsDropLabel(db) {
|
|
48
|
+
const columns = getColumns(db, 'canvas_items');
|
|
49
|
+
|
|
50
|
+
// If label column doesn't exist, migration already done
|
|
51
|
+
if (!columns.includes('label')) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
db.exec(`
|
|
56
|
+
CREATE TABLE canvas_items_new (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
|
|
59
|
+
type TEXT NOT NULL CHECK (type IN ('image', 'markdown', 'text', 'json', 'pdf', 'code')),
|
|
60
|
+
content TEXT,
|
|
61
|
+
data TEXT,
|
|
62
|
+
mime_type TEXT,
|
|
63
|
+
filename TEXT,
|
|
64
|
+
width INTEGER,
|
|
65
|
+
height INTEGER,
|
|
66
|
+
deleted_at INTEGER,
|
|
67
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
INSERT INTO canvas_items_new (id, session_id, type, content, data, mime_type, filename, width, height, deleted_at, created_at)
|
|
71
|
+
SELECT id, session_id, type, content, data, mime_type, filename, width, height, deleted_at, created_at FROM canvas_items;
|
|
72
|
+
|
|
73
|
+
DROP TABLE canvas_items;
|
|
74
|
+
|
|
75
|
+
ALTER TABLE canvas_items_new RENAME TO canvas_items;
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_canvas_session ON canvas_items(session_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_canvas_deleted ON canvas_items(deleted_at);
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @type {Array<{name: string, up: (db: import('better-sqlite3').Database) => void}>} */
|
|
83
|
+
export const canvasItemsMigrations = [
|
|
84
|
+
{
|
|
85
|
+
name: 'canvas_items-migrate-type-constraint',
|
|
86
|
+
up(db) { migrateCanvasItemsTypeConstraint(db); },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'canvas_items-add-deleted_at',
|
|
90
|
+
up(db) {
|
|
91
|
+
addColumnIfMissing(db, 'canvas_items', 'deleted_at', 'INTEGER');
|
|
92
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_canvas_deleted ON canvas_items(deleted_at)');
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'canvas_items-drop-label',
|
|
97
|
+
up(db) { migrateCanvasItemsDropLabel(db); },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'canvas_items-add-updated_at',
|
|
101
|
+
up(db) {
|
|
102
|
+
const columns = getColumns(db, 'canvas_items');
|
|
103
|
+
if (!columns.includes('updated_at')) {
|
|
104
|
+
db.exec('ALTER TABLE canvas_items ADD COLUMN updated_at INTEGER');
|
|
105
|
+
db.exec('UPDATE canvas_items SET updated_at = created_at WHERE updated_at IS NULL');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|