circuschief 0.7.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/agents/adapters/CodexAdapter.js +5 -4
- package/packages/server/src/api/canvas.js +22 -57
- package/packages/server/src/api/commandButtons.js +16 -15
- package/packages/server/src/api/index.js +2 -0
- package/packages/server/src/api/kanban.js +4 -2
- package/packages/server/src/api/projects-commandButtons.js +6 -6
- package/packages/server/src/api/projects-helpers.js +20 -3
- 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 +57 -5
- package/packages/server/src/api/projects-templates.js +38 -0
- package/packages/server/src/api/projects.js +28 -170
- package/packages/server/src/api/providers.js +11 -1
- package/packages/server/src/api/sessions-commands.js +46 -25
- package/packages/server/src/api/sessions-lifecycle.js +10 -10
- package/packages/server/src/api/sessions-patch.js +45 -1
- package/packages/server/src/api/sessions.js +6 -5
- package/packages/server/src/database.js +0 -2
- package/packages/server/src/db/DatabaseManager.js +5 -1
- package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
- package/packages/server/src/db/ProviderRepository.js +87 -32
- package/packages/server/src/db/SessionRepository.js +2 -1
- package/packages/server/src/db/SessionTemplateRepository.js +23 -2
- package/packages/server/src/db/index.js +0 -3
- package/packages/server/src/db/migrations/index.js +60 -7
- package/packages/server/src/db/migrations/kanbanMigrations.js +1 -1
- package/packages/server/src/db/migrations/miscMigrations.js +59 -184
- package/packages/server/src/db/migrations/projectsMigrations.js +31 -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 +18 -5
- package/packages/server/src/db/seedBaselineData.js +137 -0
- package/packages/server/src/db/session-helpers.js +32 -4
- package/packages/server/src/middleware/sessionLookup.js +81 -8
- package/packages/server/src/schema.sql +153 -132
- package/packages/server/src/scripts/backupDatabase.js +21 -0
- package/packages/server/src/scripts/dbUtils.js +81 -0
- package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
- package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
- package/packages/server/src/services/codexSpawnHelper.js +9 -0
- package/packages/server/src/services/commandButtonPrompts.js +14 -12
- package/packages/server/src/services/commandRunner.js +7 -1
- package/packages/server/src/services/e2eSpawnCapture.js +147 -0
- package/packages/server/src/services/gitCommitAttribution.js +150 -0
- 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 +48 -311
- package/packages/server/src/services/gitSessionSetup.js +11 -1
- package/packages/server/src/services/gitWorktree.js +127 -0
- package/packages/server/src/services/kanbanTriggers.js +6 -3
- package/packages/server/src/services/nodeSpawnHelper.js +9 -0
- package/packages/server/src/services/prUrlService.js +3 -3
- package/packages/server/src/services/queryParamBuilder.js +90 -0
- package/packages/server/src/services/sessionDuplicator.js +1 -5
- package/packages/server/src/services/sessionExecution.js +56 -108
- package/packages/server/src/services/sessionPrompts.js +12 -47
- package/packages/server/src/services/sessionProvider.js +10 -0
- package/packages/server/src/services/summaryService.js +5 -3
- package/packages/server/src/services/summaryStaleCheck.js +23 -4
- package/packages/server/src/services/templateTriggerService.js +3 -1
- package/packages/shared/src/constants.js +3 -0
- package/packages/shared/src/contracts/commandButtons.js +16 -2
- package/packages/shared/src/contracts/projects.js +2 -2
- package/packages/shared/src/contracts/providers.js +60 -0
- package/packages/shared/src/contracts/sessions.js +29 -2
- package/packages/shared/src/contracts/templates.js +12 -2
- package/packages/shared/src/types.js +1 -9
- package/packages/shared/src/utils.js +2 -2
- package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
- package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.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/ArchiveConfirmModal-DeoCVGXt.css +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-DsHChEhv.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
- package/packages/web/dist/assets/InputWithButton-B-o0DgMH.js +1 -0
- package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.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-D8hbTRIt.css → ModelSelector-BNYKujL-.css} +1 -1
- package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
- package/packages/web/dist/assets/NewSessionView-DBl7T2Xp.css +1 -0
- 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-B9FuWESY.js → ProjectListView-CYmmAcBD.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-DEhqw3Jv.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
- package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +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-DYUISplS.js → SessionFormOptions-hqijxc0S.js} +1 -1
- package/packages/web/dist/assets/SessionListView-3-xx6EVs.css +1 -0
- package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-5NfVr9pF.js} +6 -6
- package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-DI8ncOAV.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.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-D4RPpLiu.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-BGwH4Cfn.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-Bn5xdGFM.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-Cs2nxhrT.css → index-uySCcnA_.css} +1 -1
- package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
- package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-C-8PSxKi.js} +1 -1
- package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-oXifvvqN.js} +1 -1
- package/packages/web/dist/assets/sessions-Nq5VafSf.js +1 -0
- package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-DtpuiyT6.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/server/src/api/sessions-notes.js +0 -51
- package/packages/server/src/db/SessionNoteRepository.js +0 -60
- package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-D8S258uP.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
- package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
- package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +0 -1
- package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
- package/packages/web/dist/assets/NewSessionView-CUUdHkfv.css +0 -1
- package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +0 -1
- package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
- package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
- package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-DZ_Lre_l.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-DiIOEGjN.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
- package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +0 -1
- package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
- package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
- package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
- package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
- package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +0 -1
- package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
- package/packages/web/dist/assets/index-4rhEeO0B.js +0 -1
- package/packages/web/dist/assets/index-9vb2KaAd.js +0 -1
- package/packages/web/dist/assets/index-B0CvZXuN.js +0 -7
- package/packages/web/dist/assets/index-B6G18FqB.js +0 -82
- package/packages/web/dist/assets/index-BUhvkAdF.js +0 -1
- package/packages/web/dist/assets/index-BcnkUk2o.js +0 -1
- package/packages/web/dist/assets/index-CNwkdB0T.js +0 -1
- package/packages/web/dist/assets/index-CfL84oGW.js +0 -1
- package/packages/web/dist/assets/index-CkmxO8Mm.js +0 -1
- package/packages/web/dist/assets/index-Cpy4-yv3.js +0 -1
- package/packages/web/dist/assets/index-CrAQJmoZ.js +0 -1
- package/packages/web/dist/assets/index-D6Ky9vJe.js +0 -3
- package/packages/web/dist/assets/index-DfrE0gAC.js +0 -1
- package/packages/web/dist/assets/index-KwEyz0F3.js +0 -1
- package/packages/web/dist/assets/index-OfCywayk.js +0 -1
- package/packages/web/dist/assets/index-PDesaJc6.js +0 -1
- package/packages/web/dist/assets/index-uB6nhSvz.js +0 -1
- package/packages/web/dist/assets/sessions-DH1R-NhV.js +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
createFreshBaselineDb,
|
|
6
|
+
getActiveDbPath,
|
|
7
|
+
getIndexColumns,
|
|
8
|
+
getSchemaObjects,
|
|
9
|
+
getTableColumns,
|
|
10
|
+
normalizeSql,
|
|
11
|
+
} from './dbUtils.js';
|
|
12
|
+
import {
|
|
13
|
+
BUILT_IN_ANTHROPIC_MODELS,
|
|
14
|
+
BUILT_IN_OPENAI_MODELS,
|
|
15
|
+
BUILT_IN_ANTHROPIC_PROVIDER,
|
|
16
|
+
BUILT_IN_OPENAI_PROVIDER,
|
|
17
|
+
} from '../db/seedBaselineData.js';
|
|
18
|
+
|
|
19
|
+
export const DRIFT_SENSITIVE_TABLES = [
|
|
20
|
+
'sessions',
|
|
21
|
+
'project_session_defaults',
|
|
22
|
+
'session_templates',
|
|
23
|
+
'canvas_items',
|
|
24
|
+
'providers',
|
|
25
|
+
'provider_models',
|
|
26
|
+
'agent_call_logs',
|
|
27
|
+
'kanban_lanes',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const REQUIRED_INDEXES = {
|
|
31
|
+
idx_sessions_project: ['project_id'],
|
|
32
|
+
idx_sessions_status: ['status'],
|
|
33
|
+
idx_sessions_archived: ['archived'],
|
|
34
|
+
idx_sessions_starred: ['archived', 'starred'],
|
|
35
|
+
idx_sessions_next_template: ['next_template_id'],
|
|
36
|
+
idx_sessions_parent: ['parent_session_id'],
|
|
37
|
+
idx_sessions_scheduled: ['scheduled_at'],
|
|
38
|
+
idx_messages_conversation: ['conversation_id'],
|
|
39
|
+
idx_canvas_deleted: ['deleted_at'],
|
|
40
|
+
idx_todos_conversation: ['conversation_id'],
|
|
41
|
+
idx_project_defaults_projectId: ['project_id'],
|
|
42
|
+
idx_conversations_parent: ['parent_conversation_id'],
|
|
43
|
+
idx_agent_call_logs_agent_type: ['agent_type'],
|
|
44
|
+
idx_agent_call_logs_call_type: ['call_type'],
|
|
45
|
+
idx_agent_call_logs_status: ['status'],
|
|
46
|
+
idx_agent_call_logs_model: ['model'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function byName(rows) {
|
|
50
|
+
return new Map(rows.map((row) => [row.name, row]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compareTables(actualDb, expectedDb, errors) {
|
|
54
|
+
const actualObjects = byName(getSchemaObjects(actualDb).filter((row) => row.type === 'table'));
|
|
55
|
+
const expectedObjects = getSchemaObjects(expectedDb).filter((row) => row.type === 'table');
|
|
56
|
+
|
|
57
|
+
for (const expected of expectedObjects) {
|
|
58
|
+
const actual = actualObjects.get(expected.name);
|
|
59
|
+
if (!actual) {
|
|
60
|
+
errors.push(`Missing table: ${expected.name}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const actualColumns = new Map(
|
|
65
|
+
getTableColumns(actualDb, expected.name).map((col) => [col.name, col])
|
|
66
|
+
);
|
|
67
|
+
const expectedColumns = getTableColumns(expectedDb, expected.name);
|
|
68
|
+
|
|
69
|
+
for (const expectedColumn of expectedColumns) {
|
|
70
|
+
const actualColumn = actualColumns.get(expectedColumn.name);
|
|
71
|
+
if (!actualColumn) {
|
|
72
|
+
errors.push(`Missing column: ${expected.name}.${expectedColumn.name}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const actualSummary = {
|
|
77
|
+
type: actualColumn.type,
|
|
78
|
+
notnull: actualColumn.notnull,
|
|
79
|
+
dflt_value: actualColumn.dflt_value,
|
|
80
|
+
pk: actualColumn.pk,
|
|
81
|
+
};
|
|
82
|
+
const expectedSummary = {
|
|
83
|
+
type: expectedColumn.type,
|
|
84
|
+
notnull: expectedColumn.notnull,
|
|
85
|
+
dflt_value: expectedColumn.dflt_value,
|
|
86
|
+
pk: expectedColumn.pk,
|
|
87
|
+
};
|
|
88
|
+
if (JSON.stringify(actualSummary) !== JSON.stringify(expectedSummary)) {
|
|
89
|
+
errors.push(`Column mismatch for ${expected.name}.${expectedColumn.name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compareIndexes(actualDb, expectedDb, errors) {
|
|
96
|
+
const actualObjects = byName(getSchemaObjects(actualDb).filter((row) => row.type === 'index'));
|
|
97
|
+
const expectedObjects = byName(getSchemaObjects(expectedDb).filter((row) => row.type === 'index'));
|
|
98
|
+
|
|
99
|
+
for (const [name, expectedColumns] of Object.entries(REQUIRED_INDEXES)) {
|
|
100
|
+
const actual = actualObjects.get(name);
|
|
101
|
+
if (!actual) {
|
|
102
|
+
errors.push(`Missing index: ${name}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const actualColumns = getIndexColumns(actualDb, name);
|
|
106
|
+
if (JSON.stringify(actualColumns) !== JSON.stringify(expectedColumns)) {
|
|
107
|
+
errors.push(`Index column mismatch for ${name}: ${actualColumns.join(',')} !== ${expectedColumns.join(',')}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const expectedScheduledSql = normalizeSql(expectedObjects.get('idx_sessions_scheduled')?.sql);
|
|
112
|
+
const actualScheduledSql = normalizeSql(actualObjects.get('idx_sessions_scheduled')?.sql);
|
|
113
|
+
if (expectedScheduledSql !== actualScheduledSql) {
|
|
114
|
+
errors.push('Partial index SQL mismatch for idx_sessions_scheduled');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateProviders(actualDb, errors) {
|
|
119
|
+
for (const provider of [BUILT_IN_ANTHROPIC_PROVIDER, BUILT_IN_OPENAI_PROVIDER]) {
|
|
120
|
+
const row = actualDb.prepare(
|
|
121
|
+
'SELECT id, name, kind, base_url, auth_token, commit_attribution_override FROM providers WHERE id = ?'
|
|
122
|
+
).get(provider.id);
|
|
123
|
+
if (!row) {
|
|
124
|
+
errors.push(`Missing provider seed row: ${provider.id}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (row.name !== provider.name || row.kind !== provider.kind) {
|
|
128
|
+
errors.push(`Provider seed mismatch: ${provider.id}`);
|
|
129
|
+
}
|
|
130
|
+
if (row.base_url !== null || row.auth_token !== null) {
|
|
131
|
+
errors.push(`Provider nullable seed columns mismatch: ${provider.id}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateProviderModels(actualDb, errors) {
|
|
137
|
+
for (const model of [...BUILT_IN_ANTHROPIC_MODELS, ...BUILT_IN_OPENAI_MODELS]) {
|
|
138
|
+
const row = actualDb.prepare(
|
|
139
|
+
'SELECT provider_id, model_id, display_name, description, tier FROM provider_models WHERE id = ?'
|
|
140
|
+
).get(model.id);
|
|
141
|
+
if (!row) {
|
|
142
|
+
errors.push(`Missing provider model seed row: ${model.id}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const expected = {
|
|
146
|
+
provider_id: model.providerId,
|
|
147
|
+
model_id: model.modelId,
|
|
148
|
+
display_name: model.displayName,
|
|
149
|
+
description: model.description,
|
|
150
|
+
tier: model.tier,
|
|
151
|
+
};
|
|
152
|
+
if (JSON.stringify(row) !== JSON.stringify(expected)) {
|
|
153
|
+
errors.push(`Provider model seed mismatch: ${model.id}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function compareSeedRows(actualDb, errors) {
|
|
159
|
+
const tables = new Set(actualDb.prepare(
|
|
160
|
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
161
|
+
).all().map((row) => row.name));
|
|
162
|
+
if (!tables.has('providers') || !tables.has('provider_models') || !tables.has('quick_responses') || !tables.has('session_templates')) {
|
|
163
|
+
errors.push('Required seed tables are missing');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
validateProviders(actualDb, errors);
|
|
167
|
+
validateProviderModels(actualDb, errors);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function validateDatabaseBaseline(actualDb) {
|
|
171
|
+
const fresh = createFreshBaselineDb();
|
|
172
|
+
const errors = [];
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
compareTables(actualDb, fresh.db, errors);
|
|
176
|
+
compareIndexes(actualDb, fresh.db, errors);
|
|
177
|
+
compareSeedRows(actualDb, errors);
|
|
178
|
+
} finally {
|
|
179
|
+
fresh.close();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function main() {
|
|
186
|
+
const dbPath = getActiveDbPath();
|
|
187
|
+
if (!existsSync(dbPath)) {
|
|
188
|
+
console.log(`No database found at ${dbPath}; nothing to validate.`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
193
|
+
try {
|
|
194
|
+
const errors = validateDatabaseBaseline(db);
|
|
195
|
+
if (errors.length === 0) {
|
|
196
|
+
console.log(`Database baseline validation passed: ${dbPath}`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.error(`Database baseline validation failed: ${dbPath}`);
|
|
201
|
+
for (const error of errors) {
|
|
202
|
+
console.error(`- ${error}`);
|
|
203
|
+
}
|
|
204
|
+
return 1;
|
|
205
|
+
} finally {
|
|
206
|
+
db.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
211
|
+
process.exit(main());
|
|
212
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { createRobustEnv } from './nodeSpawnHelper.js';
|
|
3
|
+
import {
|
|
4
|
+
captureSpawnAttempt,
|
|
5
|
+
createCapturedSpawnProcess,
|
|
6
|
+
isE2ESpawnCaptureEnabled,
|
|
7
|
+
} from './e2eSpawnCapture.js';
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Create a custom spawn function for the Codex CLI.
|
|
@@ -19,6 +24,10 @@ import { createRobustEnv } from './nodeSpawnHelper.js';
|
|
|
19
24
|
export function createCodexSpawner() {
|
|
20
25
|
return (options) => {
|
|
21
26
|
const { command, args, cwd, env, signal } = options;
|
|
27
|
+
if (isE2ESpawnCaptureEnabled()) {
|
|
28
|
+
captureSpawnAttempt('codex', options);
|
|
29
|
+
return createCapturedSpawnProcess('codex');
|
|
30
|
+
}
|
|
22
31
|
|
|
23
32
|
// Replace 'node' with the absolute path to the current Node executable
|
|
24
33
|
const actualCommand = command === 'node' ? process.execPath : command;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { commandButtons } from '../database.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Build Command
|
|
4
|
+
* Build Command API instructions for system prompt if the project has commands.
|
|
5
5
|
* @param {string} apiUrl - Base API URL
|
|
6
6
|
* @param {string} sessionId - Current session ID
|
|
7
7
|
* @param {string} projectId - Current project ID
|
|
8
|
-
* @returns {string} Command
|
|
8
|
+
* @returns {string} Command instructions or empty string if no commands configured
|
|
9
9
|
*/
|
|
10
10
|
export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
|
|
11
11
|
const buttons = commandButtons.getByProjectId(projectId);
|
|
@@ -13,36 +13,38 @@ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId)
|
|
|
13
13
|
return '';
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
return `##
|
|
16
|
+
return `## Circus Commands
|
|
17
17
|
|
|
18
|
-
This project has
|
|
18
|
+
This project has Circus Commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
> When the user asks to "run a command", "what commands are available", "list circus commands", or similar, use the Commands API below to discover and execute them.
|
|
21
|
+
|
|
22
|
+
### List Available Commands
|
|
21
23
|
\`\`\`bash
|
|
22
|
-
curl ${apiUrl}/api/
|
|
24
|
+
curl ${apiUrl}/api/sessions/${sessionId}/circus-commands
|
|
23
25
|
\`\`\`
|
|
24
26
|
|
|
25
|
-
### Run a
|
|
27
|
+
### Run a Command
|
|
26
28
|
\`\`\`bash
|
|
27
|
-
curl -X POST ${apiUrl}/api/sessions/${sessionId}/
|
|
29
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/<button_id>/run
|
|
28
30
|
\`\`\`
|
|
29
31
|
|
|
30
32
|
Response: { runId, buttonId, status: "running", output: "" }
|
|
31
33
|
|
|
32
34
|
### Check Run Status & Output
|
|
33
35
|
\`\`\`bash
|
|
34
|
-
curl ${apiUrl}/api/sessions/${sessionId}/
|
|
36
|
+
curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>
|
|
35
37
|
\`\`\`
|
|
36
38
|
|
|
37
39
|
Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
|
|
38
40
|
|
|
39
|
-
### List
|
|
41
|
+
### List Command Runs
|
|
40
42
|
\`\`\`bash
|
|
41
|
-
curl ${apiUrl}/api/sessions/${sessionId}/
|
|
43
|
+
curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs
|
|
42
44
|
\`\`\`
|
|
43
45
|
|
|
44
46
|
### Kill a Running Command
|
|
45
47
|
\`\`\`bash
|
|
46
|
-
curl -X POST ${apiUrl}/api/sessions/${sessionId}/
|
|
48
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>/kill
|
|
47
49
|
\`\`\``;
|
|
48
50
|
}
|
|
@@ -7,6 +7,12 @@ import { TerminalOutputProcessor } from './terminalOutput.js';
|
|
|
7
7
|
// Re-export for backward compatibility
|
|
8
8
|
export { stripAnsiCodes, TerminalOutputProcessor } from './terminalOutput.js';
|
|
9
9
|
|
|
10
|
+
export function createCommandRunnerEnv(baseEnv = process.env) {
|
|
11
|
+
const env = createRobustEnv(baseEnv);
|
|
12
|
+
delete env.CIRCUSCHIEF_COMMIT_ATTRIBUTION;
|
|
13
|
+
return env;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Service for running commands and managing their execution
|
|
12
18
|
*/
|
|
@@ -141,7 +147,7 @@ export class CommandRunner {
|
|
|
141
147
|
cwd: workingDirectory,
|
|
142
148
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
149
|
detached: true,
|
|
144
|
-
env:
|
|
150
|
+
env: createCommandRunnerEnv(),
|
|
145
151
|
});
|
|
146
152
|
|
|
147
153
|
const entry = this.#createProcessEntry(child, sessionId, buttonId);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { appendFileSync } from 'fs';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { PassThrough } from 'stream';
|
|
4
|
+
|
|
5
|
+
export function isE2ESpawnCaptureEnabled() {
|
|
6
|
+
return Boolean(process.env.E2E_AGENT_SPAWN_CAPTURE_FILE);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function captureSpawnAttempt(agentType, spawnOptions) {
|
|
10
|
+
const filePath = process.env.E2E_AGENT_SPAWN_CAPTURE_FILE;
|
|
11
|
+
if (!filePath) return;
|
|
12
|
+
|
|
13
|
+
const record = {
|
|
14
|
+
agentType,
|
|
15
|
+
command: spawnOptions.command,
|
|
16
|
+
args: spawnOptions.args || [],
|
|
17
|
+
cwd: spawnOptions.cwd || null,
|
|
18
|
+
env: summarizeSpawnEnv(spawnOptions.env),
|
|
19
|
+
options: summarizeSpawnOptions(agentType, spawnOptions),
|
|
20
|
+
capturedAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
appendFileSync(filePath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createCapturedSpawnProcess(agentType) {
|
|
27
|
+
const processStub = new EventEmitter();
|
|
28
|
+
const stdin = new PassThrough();
|
|
29
|
+
const stdout = new PassThrough();
|
|
30
|
+
const stderr = new PassThrough();
|
|
31
|
+
|
|
32
|
+
processStub.stdin = stdin;
|
|
33
|
+
processStub.stdout = stdout;
|
|
34
|
+
processStub.stderr = stderr;
|
|
35
|
+
processStub.killed = false;
|
|
36
|
+
processStub.exitCode = null;
|
|
37
|
+
processStub.kill = (signal = 'SIGTERM') => {
|
|
38
|
+
if (processStub.killed || processStub.exitCode !== null) return true;
|
|
39
|
+
processStub.killed = true;
|
|
40
|
+
finishProcess({ processStub, stdout, stderr, code: null, signal });
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const complete = () => {
|
|
45
|
+
setImmediate(() => {
|
|
46
|
+
if (processStub.killed || processStub.exitCode !== null) return;
|
|
47
|
+
writeCapturedAgentEvents(agentType, stdout);
|
|
48
|
+
finishProcess({ processStub, stdout, stderr, code: 0, signal: null });
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (agentType === 'claude-code') {
|
|
53
|
+
setTimeout(complete, 10);
|
|
54
|
+
} else {
|
|
55
|
+
stdin.once('finish', complete);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return processStub;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function summarizeSpawnEnv(env = {}) {
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(env, 'CIRCUSCHIEF_COMMIT_ATTRIBUTION')) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
CIRCUSCHIEF_COMMIT_ATTRIBUTION: env.CIRCUSCHIEF_COMMIT_ATTRIBUTION,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function summarizeSpawnOptions(agentType, spawnOptions) {
|
|
71
|
+
const args = spawnOptions.args || [];
|
|
72
|
+
if (agentType === 'claude-code') {
|
|
73
|
+
return {
|
|
74
|
+
model: valueAfter(args, '--model'),
|
|
75
|
+
settings: valueAfter(args, '--settings'),
|
|
76
|
+
permissionMode: valueAfter(args, '--permission-mode'),
|
|
77
|
+
settingSources: valueAfter(args, '--setting-sources'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
model: valueAfter(args, '-m'),
|
|
83
|
+
sandbox: valueAfter(args, '--sandbox'),
|
|
84
|
+
config: valuesAfter(args, '-c'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function valueAfter(args, flag) {
|
|
89
|
+
const index = args.indexOf(flag);
|
|
90
|
+
if (index === -1) return null;
|
|
91
|
+
return args[index + 1] ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function valuesAfter(args, flag) {
|
|
95
|
+
const values = [];
|
|
96
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
97
|
+
if (args[index] === flag && args[index + 1] !== undefined) {
|
|
98
|
+
values.push(args[index + 1]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeCapturedAgentEvents(agentType, stdout) {
|
|
105
|
+
if (agentType === 'codex') {
|
|
106
|
+
writeJsonLine(stdout, { type: 'thread.started', thread_id: `e2e-codex-${Date.now()}` });
|
|
107
|
+
writeJsonLine(stdout, { type: 'turn.started' });
|
|
108
|
+
writeJsonLine(stdout, {
|
|
109
|
+
type: 'item.completed',
|
|
110
|
+
item: { type: 'agent_message', text: 'E2E spawn capture response.' },
|
|
111
|
+
});
|
|
112
|
+
writeJsonLine(stdout, {
|
|
113
|
+
type: 'turn.completed',
|
|
114
|
+
usage: { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 },
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeJsonLine(stdout, {
|
|
120
|
+
type: 'system',
|
|
121
|
+
subtype: 'init',
|
|
122
|
+
session_id: `e2e-claude-${Date.now()}`,
|
|
123
|
+
});
|
|
124
|
+
writeJsonLine(stdout, {
|
|
125
|
+
type: 'assistant',
|
|
126
|
+
message: { content: [{ type: 'text', text: 'E2E spawn capture response.' }] },
|
|
127
|
+
});
|
|
128
|
+
writeJsonLine(stdout, {
|
|
129
|
+
type: 'result',
|
|
130
|
+
subtype: 'success',
|
|
131
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeJsonLine(stream, value) {
|
|
136
|
+
stream.write(`${JSON.stringify(value)}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function finishProcess({ processStub, stdout, stderr, code, signal }) {
|
|
140
|
+
Object.assign(processStub, { exitCode: code });
|
|
141
|
+
stdout.end();
|
|
142
|
+
stderr.end();
|
|
143
|
+
setImmediate(() => {
|
|
144
|
+
processStub.emit('exit', code, signal);
|
|
145
|
+
processStub.emit('close', code, signal);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { chmod, mkdir, writeFile } from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MANAGED_HOOKS_PATH = path.join(os.homedir(), '.circuschief', 'hooks');
|
|
10
|
+
let _managedHooksPath = DEFAULT_MANAGED_HOOKS_PATH;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the managed hooks directory path.
|
|
14
|
+
* Production code uses the real home directory; tests can override via _setManagedHooksPath().
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function getManagedHooksPath() {
|
|
18
|
+
return _managedHooksPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Override the managed hooks path (for testing only).
|
|
23
|
+
* Restores the default when called with no arguments.
|
|
24
|
+
* @param {string} [overridePath]
|
|
25
|
+
*/
|
|
26
|
+
export function _setManagedHooksPath(overridePath) {
|
|
27
|
+
_managedHooksPath = overridePath ?? DEFAULT_MANAGED_HOOKS_PATH;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Legacy managed hooks path (pre-migration). Used by ensureWorktreeCommitAttributionHook()
|
|
32
|
+
* to recognize and auto-upgrade worktrees that still have the old relative path set.
|
|
33
|
+
*/
|
|
34
|
+
const LEGACY_MANAGED_HOOKS_PATH = '.circuschief-hooks';
|
|
35
|
+
|
|
36
|
+
const ATTRIBUTION_CONFIG_KEY = 'circuschief.commitAttribution';
|
|
37
|
+
const ATTRIBUTION_ENV_KEY = 'CIRCUSCHIEF_COMMIT_ATTRIBUTION';
|
|
38
|
+
|
|
39
|
+
async function git(directory, command) {
|
|
40
|
+
const { stdout } = await execAsync(`git ${command}`, { cwd: directory });
|
|
41
|
+
return stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function gitConfigValue(directory, key) {
|
|
45
|
+
try {
|
|
46
|
+
return await git(directory, `config --get ${key}`);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shellQuote(value) {
|
|
53
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildCommitMsgHook() {
|
|
57
|
+
return `#!/bin/sh
|
|
58
|
+
set -eu
|
|
59
|
+
|
|
60
|
+
msg_file="$1"
|
|
61
|
+
trailer="\${${ATTRIBUTION_ENV_KEY}:-}"
|
|
62
|
+
|
|
63
|
+
[ -n "$trailer" ] || exit 0
|
|
64
|
+
[ -n "$(printf '%s' "$trailer" | tr -d '[:space:]')" ] || exit 0
|
|
65
|
+
|
|
66
|
+
if printf '%s' "$trailer" | grep '[[:cntrl:]]' >/dev/null 2>&1; then
|
|
67
|
+
echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if ! printf '%s\\n' "$trailer" | grep -E '^Co-authored-by: [^<>[:space:]][^<>]* <[^[:space:]<>@]+@[^[:space:]<>@]+\\.[^[:space:]<>@]+>$' >/dev/null 2>&1; then
|
|
72
|
+
echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if grep -F -x -i -- "$trailer" "$msg_file" >/dev/null 2>&1; then
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
git interpret-trailers --trailer "$trailer" --in-place "$msg_file"
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function clearWorktreeCommitAttribution(worktreePath) {
|
|
85
|
+
const managedHooksPath = getManagedHooksPath();
|
|
86
|
+
const currentAttribution = await gitConfigValue(worktreePath, ATTRIBUTION_CONFIG_KEY);
|
|
87
|
+
const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
|
|
88
|
+
if (!currentAttribution && currentHooksPath !== managedHooksPath) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await git(worktreePath, 'config extensions.worktreeConfig true');
|
|
93
|
+
|
|
94
|
+
if (currentAttribution) {
|
|
95
|
+
try {
|
|
96
|
+
await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
|
|
97
|
+
} catch {
|
|
98
|
+
// Unset is idempotent for callers that are clearing a value that was never set.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (currentHooksPath === managedHooksPath) {
|
|
103
|
+
try {
|
|
104
|
+
await git(worktreePath, 'config --worktree --unset core.hooksPath');
|
|
105
|
+
} catch {
|
|
106
|
+
// Unset is idempotent for callers that are clearing stale managed hook config.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Install/update managed commit attribution enforcement for a worktree.
|
|
114
|
+
*
|
|
115
|
+
* Runtime attribution is process-scoped: the hook reads
|
|
116
|
+
* CIRCUSCHIEF_COMMIT_ATTRIBUTION from the `git commit` process environment.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} worktreePath - The worktree directory
|
|
119
|
+
* @returns {Promise<boolean>} True when a hook is installed or updated
|
|
120
|
+
*/
|
|
121
|
+
export async function ensureWorktreeCommitAttributionHook(worktreePath) {
|
|
122
|
+
const managedHooksPath = getManagedHooksPath();
|
|
123
|
+
|
|
124
|
+
await git(worktreePath, 'config extensions.worktreeConfig true');
|
|
125
|
+
|
|
126
|
+
const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
|
|
127
|
+
if (currentHooksPath && currentHooksPath !== managedHooksPath && currentHooksPath !== LEGACY_MANAGED_HOOKS_PATH) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Cannot install managed commit attribution hook: worktree already has core.hooksPath set to "${currentHooksPath}"`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
|
|
135
|
+
} catch {
|
|
136
|
+
// Unset is idempotent for stale worktrees that never stored attribution.
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(managedHooksPath)}`);
|
|
140
|
+
|
|
141
|
+
const hookPath = path.join(managedHooksPath, 'commit-msg');
|
|
142
|
+
await mkdir(managedHooksPath, { recursive: true });
|
|
143
|
+
await writeFile(hookPath, buildCommitMsgHook(), 'utf8');
|
|
144
|
+
await chmod(hookPath, 0o755);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function configureWorktreeCommitAttribution(worktreePath, _commitAttribution) {
|
|
149
|
+
return ensureWorktreeCommitAttributionHook(worktreePath);
|
|
150
|
+
}
|