@shawnstack/quickforge 1.7.7 → 1.7.8
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/dist/assets/AgentProfilesPage-BSM5w3bn.js +1 -0
- package/dist/assets/ChatPanelHost-cd80S4EX.js +48 -0
- package/dist/assets/CloudAccountSettingsPage-BJMrfgy9.js +1 -0
- package/dist/assets/{PluginsPage-CPE5tbbf.js → PluginsPage-mJkTmVVV.js} +1 -1
- package/dist/assets/ScheduledTasksPage-DJj_NLm9.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-BE2T4d3P.js → SettingsWorkspacePage-7lQ0Wl63.js} +389 -389
- package/dist/assets/ShareLinksSettingsPage-DjFq_Lnb.js +1 -0
- package/dist/assets/SharedConversationPage-BbuNpcpP.js +1 -0
- package/dist/assets/TerminalDock-CczcCJJE.js +2 -0
- package/dist/assets/WorkspaceInspector-0i6x-diB.js +36 -0
- package/dist/assets/{icons-2D18YbSI.js → icons-DM_qJ7VJ.js} +1 -1
- package/dist/assets/{index-BiCTgUlY.css → index-D-J_8Smf.css} +1 -1
- package/dist/assets/index-DFASc15l.js +66 -0
- package/dist/assets/local-tools-Dn1Y9aPe.js +270 -0
- package/dist/assets/{model-display-label-C6-0eDxF.js → logger-8REuLPGK.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-GY7sdfIr.js → mcp-servers-dialog-BG4b67ea.js} +2 -2
- package/dist/assets/{model-reference-o41fJ6jQ.js → model-display-label-CURwH0xM.js} +1 -1
- package/dist/assets/{monaco-DvXErxp2.js → monaco-bDr860xx.js} +1 -1
- package/dist/assets/{react-vendor-3UaA-C9W.js → react-vendor-BpWW_Nvp.js} +1 -1
- package/dist/assets/{skills-dialog-C1fJ1Jl7.js → skills-dialog-DIzZvQqW.js} +3 -3
- package/dist/index.html +8 -8
- package/package.json +1 -1
- package/server/acp/server.mjs +17 -1
- package/server/agent-manager.mjs +251 -31
- package/server/ai-http-logger.mjs +162 -19
- package/server/ai-provider-options.mjs +4 -1
- package/server/auto-archive.mjs +42 -29
- package/server/cloud/auto-approval.mjs +155 -0
- package/server/cloud/client.mjs +5 -0
- package/server/cloud/qf-agent-process.mjs +163 -3
- package/server/cloud/service-config.mjs +1 -1
- package/server/index.mjs +69 -13
- package/server/lan-access-backup.mjs +256 -0
- package/server/lan-access-cutover.mjs +306 -0
- package/server/lan-access-json-file.mjs +75 -0
- package/server/lan-access-service.mjs +155 -0
- package/server/lan-access-store.mjs +110 -9
- package/server/maintenance/downgrade-lan-access-v1.mjs +101 -0
- package/server/maintenance/downgrade-session-state-v1.mjs +130 -0
- package/server/maintenance/downgrade-share-v1.mjs +101 -0
- package/server/maintenance/export-lan-access-v1.mjs +80 -0
- package/server/maintenance/export-scheduled-runs-v1.mjs +45 -0
- package/server/maintenance/export-session-state-v1.mjs +67 -0
- package/server/maintenance/export-share-v1.mjs +77 -0
- package/server/routes/agent.mjs +37 -4
- package/server/routes/backup.mjs +221 -39
- package/server/routes/cloud.mjs +28 -1
- package/server/routes/scheduled-tasks.mjs +347 -134
- package/server/routes/shares.mjs +4 -2
- package/server/routes/storage.mjs +138 -8
- package/server/routes/workspace.mjs +265 -4
- package/server/scheduled-runs-backup.mjs +235 -0
- package/server/scheduled-runs-cutover.mjs +497 -0
- package/server/scheduled-task-runs-service.mjs +279 -0
- package/server/session-index-service.mjs +495 -0
- package/server/session-state-backup.mjs +277 -0
- package/server/session-state-cutover.mjs +370 -0
- package/server/session-state-service.mjs +709 -0
- package/server/share-backup.mjs +255 -0
- package/server/share-cutover.mjs +296 -0
- package/server/share-json-file.mjs +60 -0
- package/server/share-service.mjs +163 -0
- package/server/share-store.mjs +196 -19
- package/server/skills.mjs +0 -1
- package/server/sqlite/database.mjs +233 -0
- package/server/sqlite/lan-access-repository.mjs +652 -0
- package/server/sqlite/migrations.mjs +577 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +464 -0
- package/server/sqlite/session-index-repository.mjs +297 -0
- package/server/sqlite/session-state-repository.mjs +786 -0
- package/server/sqlite/share-repository.mjs +854 -0
- package/server/storage.mjs +317 -1
- package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +0 -1
- package/dist/assets/ChatPanelHost-Dul1ahKj.js +0 -307
- package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +0 -1
- package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +0 -2
- package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +0 -1
- package/dist/assets/SharedConversationPage-BK9XJnEe.js +0 -1
- package/dist/assets/TerminalDock-CL7HU9DI.js +0 -2
- package/dist/assets/WorkspaceInspector-MNBLIKMn.js +0 -36
- package/dist/assets/index-BTPzyq7d.js +0 -65
- package/dist/assets/mermaid-renderer-jEbqFaTe.js +0 -2
- /package/dist/assets/{plugin-api-Uu2xAB4d.js → plugin-api-BrdQeM4B.js} +0 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
export const SQLITE_MIGRATIONS = Object.freeze([
|
|
2
|
+
Object.freeze({
|
|
3
|
+
version: 1,
|
|
4
|
+
name: 'create_schema_migrations',
|
|
5
|
+
up(database) {
|
|
6
|
+
database.exec(`
|
|
7
|
+
CREATE TABLE schema_migrations (
|
|
8
|
+
version INTEGER PRIMARY KEY CHECK (version > 0),
|
|
9
|
+
name TEXT NOT NULL UNIQUE,
|
|
10
|
+
applied_at TEXT NOT NULL
|
|
11
|
+
)
|
|
12
|
+
`)
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
Object.freeze({
|
|
16
|
+
version: 2,
|
|
17
|
+
name: 'create_scheduled_task_runs',
|
|
18
|
+
up(database) {
|
|
19
|
+
database.exec(`
|
|
20
|
+
CREATE TABLE scheduled_task_runs (
|
|
21
|
+
id TEXT PRIMARY KEY NOT NULL CHECK (length(trim(id)) > 0),
|
|
22
|
+
task_id TEXT NOT NULL CHECK (length(trim(task_id)) > 0),
|
|
23
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'success', 'failed')),
|
|
24
|
+
trigger TEXT CHECK (trigger IS NULL OR trigger IN ('schedule', 'manual')),
|
|
25
|
+
input_content TEXT,
|
|
26
|
+
ai_result TEXT,
|
|
27
|
+
result TEXT,
|
|
28
|
+
error_message TEXT,
|
|
29
|
+
warning TEXT,
|
|
30
|
+
session_id TEXT,
|
|
31
|
+
scheduled_at TEXT,
|
|
32
|
+
started_at TEXT NOT NULL CHECK (length(trim(started_at)) > 0),
|
|
33
|
+
finished_at TEXT,
|
|
34
|
+
duration_ms INTEGER CHECK (duration_ms IS NULL OR (typeof(duration_ms) = 'integer' AND duration_ms >= 0)),
|
|
35
|
+
agent_id TEXT,
|
|
36
|
+
agent_label TEXT,
|
|
37
|
+
agent_snapshot_json TEXT CHECK (agent_snapshot_json IS NULL OR json_valid(agent_snapshot_json))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX scheduled_task_runs_task_started_idx
|
|
41
|
+
ON scheduled_task_runs (task_id, started_at DESC, id DESC);
|
|
42
|
+
CREATE INDEX scheduled_task_runs_started_idx
|
|
43
|
+
ON scheduled_task_runs (started_at DESC, id DESC);
|
|
44
|
+
CREATE INDEX scheduled_task_runs_status_started_idx
|
|
45
|
+
ON scheduled_task_runs (status, started_at DESC, id DESC);
|
|
46
|
+
CREATE INDEX scheduled_task_runs_trigger_started_idx
|
|
47
|
+
ON scheduled_task_runs (trigger, started_at DESC, id DESC);
|
|
48
|
+
`)
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
version: 3,
|
|
53
|
+
name: 'scheduled_task_runs_authoritative_cutover',
|
|
54
|
+
up(database) {
|
|
55
|
+
database.exec(`
|
|
56
|
+
ALTER TABLE scheduled_task_runs RENAME TO scheduled_task_runs_v2;
|
|
57
|
+
|
|
58
|
+
CREATE TABLE scheduled_task_runs (
|
|
59
|
+
task_id TEXT NOT NULL CHECK (length(trim(task_id)) > 0),
|
|
60
|
+
id TEXT NOT NULL CHECK (length(trim(id)) > 0),
|
|
61
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'success', 'failed')),
|
|
62
|
+
trigger TEXT CHECK (trigger IS NULL OR trigger IN ('schedule', 'manual')),
|
|
63
|
+
input_content TEXT,
|
|
64
|
+
ai_result TEXT,
|
|
65
|
+
result TEXT,
|
|
66
|
+
error_message TEXT,
|
|
67
|
+
warning TEXT,
|
|
68
|
+
session_id TEXT,
|
|
69
|
+
scheduled_at TEXT,
|
|
70
|
+
started_at TEXT NOT NULL CHECK (length(trim(started_at)) > 0),
|
|
71
|
+
finished_at TEXT,
|
|
72
|
+
duration_ms INTEGER CHECK (duration_ms IS NULL OR (typeof(duration_ms) = 'integer' AND duration_ms >= 0)),
|
|
73
|
+
agent_id TEXT,
|
|
74
|
+
agent_label TEXT,
|
|
75
|
+
agent_snapshot_json TEXT CHECK (agent_snapshot_json IS NULL OR json_valid(agent_snapshot_json)),
|
|
76
|
+
extra_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(extra_json) AND json_type(extra_json) = 'object'),
|
|
77
|
+
legacy_json TEXT CHECK (legacy_json IS NULL OR (json_valid(legacy_json) AND json_type(legacy_json) = 'object')),
|
|
78
|
+
source TEXT NOT NULL CHECK (length(trim(source)) > 0),
|
|
79
|
+
updated_at TEXT NOT NULL CHECK (length(trim(updated_at)) > 0),
|
|
80
|
+
PRIMARY KEY (task_id, id)
|
|
81
|
+
) WITHOUT ROWID;
|
|
82
|
+
|
|
83
|
+
INSERT INTO scheduled_task_runs (
|
|
84
|
+
task_id, id, status, trigger, input_content, ai_result, result, error_message,
|
|
85
|
+
warning, session_id, scheduled_at, started_at, finished_at, duration_ms,
|
|
86
|
+
agent_id, agent_label, agent_snapshot_json, extra_json, legacy_json, source, updated_at
|
|
87
|
+
)
|
|
88
|
+
SELECT
|
|
89
|
+
task_id, id, status, trigger, input_content, ai_result, result, error_message,
|
|
90
|
+
warning, session_id, scheduled_at, started_at, finished_at, duration_ms,
|
|
91
|
+
agent_id, agent_label, agent_snapshot_json, '{}', NULL, 'v2_shadow',
|
|
92
|
+
COALESCE(finished_at, started_at)
|
|
93
|
+
FROM scheduled_task_runs_v2;
|
|
94
|
+
|
|
95
|
+
DROP TABLE scheduled_task_runs_v2;
|
|
96
|
+
|
|
97
|
+
CREATE INDEX scheduled_task_runs_task_started_idx
|
|
98
|
+
ON scheduled_task_runs (task_id, started_at DESC, id DESC);
|
|
99
|
+
CREATE INDEX scheduled_task_runs_started_idx
|
|
100
|
+
ON scheduled_task_runs (started_at DESC, id DESC, task_id DESC);
|
|
101
|
+
CREATE INDEX scheduled_task_runs_status_started_idx
|
|
102
|
+
ON scheduled_task_runs (status, started_at DESC, id DESC, task_id DESC);
|
|
103
|
+
CREATE INDEX scheduled_task_runs_trigger_started_idx
|
|
104
|
+
ON scheduled_task_runs (trigger, started_at DESC, id DESC, task_id DESC);
|
|
105
|
+
|
|
106
|
+
CREATE TABLE scheduled_runs_state (
|
|
107
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
108
|
+
phase TEXT NOT NULL CHECK (phase IN ('hybrid', 'cutover_running', 'sqlite_authoritative_json_pending', 'authoritative')),
|
|
109
|
+
run_count INTEGER CHECK (run_count IS NULL OR run_count >= 0),
|
|
110
|
+
digest TEXT,
|
|
111
|
+
backup_file TEXT,
|
|
112
|
+
diagnostic_json TEXT CHECK (diagnostic_json IS NULL OR json_valid(diagnostic_json)),
|
|
113
|
+
updated_at TEXT NOT NULL
|
|
114
|
+
);
|
|
115
|
+
INSERT INTO scheduled_runs_state (singleton, phase, updated_at)
|
|
116
|
+
VALUES (1, 'hybrid', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
117
|
+
|
|
118
|
+
CREATE TABLE scheduled_runs_maintenance_lock (
|
|
119
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
120
|
+
owner TEXT NOT NULL,
|
|
121
|
+
owner_pid INTEGER,
|
|
122
|
+
fencing INTEGER NOT NULL DEFAULT 1,
|
|
123
|
+
operation TEXT NOT NULL,
|
|
124
|
+
acquired_at TEXT NOT NULL,
|
|
125
|
+
heartbeat_at TEXT,
|
|
126
|
+
expires_at TEXT NOT NULL
|
|
127
|
+
);
|
|
128
|
+
`)
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
Object.freeze({
|
|
132
|
+
version: 4,
|
|
133
|
+
name: 'create_session_index',
|
|
134
|
+
up(database) {
|
|
135
|
+
database.exec(`
|
|
136
|
+
CREATE TABLE session_index (
|
|
137
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
138
|
+
project_id TEXT NOT NULL,
|
|
139
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
140
|
+
created_at TEXT,
|
|
141
|
+
last_modified TEXT,
|
|
142
|
+
message_count INTEGER CHECK (message_count IS NULL OR (typeof(message_count) = 'integer' AND message_count >= 0)),
|
|
143
|
+
pinned_at TEXT,
|
|
144
|
+
archived_at TEXT,
|
|
145
|
+
is_pinned INTEGER NOT NULL CHECK (is_pinned IN (0, 1)),
|
|
146
|
+
is_archived INTEGER NOT NULL CHECK (is_archived IN (0, 1)),
|
|
147
|
+
state_version INTEGER CHECK (state_version IS NULL OR (typeof(state_version) = 'integer' AND state_version >= 0)),
|
|
148
|
+
metadata_json TEXT NOT NULL CHECK (json_valid(metadata_json) AND json_type(metadata_json) = 'object'),
|
|
149
|
+
metadata_digest TEXT NOT NULL CHECK (length(metadata_digest) = 64 AND metadata_digest NOT GLOB '*[^0-9a-f]*'),
|
|
150
|
+
indexed_at TEXT NOT NULL CHECK (length(trim(indexed_at)) > 0),
|
|
151
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
152
|
+
CHECK (
|
|
153
|
+
(scope = 'global' AND project_id = '') OR
|
|
154
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
155
|
+
)
|
|
156
|
+
) WITHOUT ROWID;
|
|
157
|
+
|
|
158
|
+
CREATE INDEX session_index_created_idx
|
|
159
|
+
ON session_index (scope, project_id, created_at DESC, session_id DESC);
|
|
160
|
+
CREATE INDEX session_index_modified_idx
|
|
161
|
+
ON session_index (scope, project_id, last_modified DESC, session_id DESC);
|
|
162
|
+
CREATE INDEX session_index_pinned_idx
|
|
163
|
+
ON session_index (scope, project_id, pinned_at DESC, session_id DESC)
|
|
164
|
+
WHERE is_pinned = 1;
|
|
165
|
+
CREATE INDEX session_index_archived_idx
|
|
166
|
+
ON session_index (scope, project_id, archived_at DESC, session_id DESC)
|
|
167
|
+
WHERE is_archived = 1;
|
|
168
|
+
`)
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
Object.freeze({
|
|
172
|
+
version: 5,
|
|
173
|
+
name: 'add_session_index_query_indexes',
|
|
174
|
+
up(database) {
|
|
175
|
+
database.exec(`
|
|
176
|
+
CREATE INDEX session_index_scope_created_query_idx
|
|
177
|
+
ON session_index (scope, project_id, is_archived, is_pinned DESC, pinned_at DESC, created_at DESC);
|
|
178
|
+
CREATE INDEX session_index_scope_modified_query_idx
|
|
179
|
+
ON session_index (scope, project_id, is_archived, is_pinned DESC, pinned_at DESC, last_modified DESC);
|
|
180
|
+
CREATE INDEX session_index_projects_created_query_idx
|
|
181
|
+
ON session_index (scope, is_archived, is_pinned DESC, pinned_at DESC, created_at DESC);
|
|
182
|
+
CREATE INDEX session_index_projects_modified_query_idx
|
|
183
|
+
ON session_index (scope, is_archived, is_pinned DESC, pinned_at DESC, last_modified DESC);
|
|
184
|
+
CREATE INDEX session_index_aggregate_modified_query_idx
|
|
185
|
+
ON session_index (is_archived, is_pinned DESC, pinned_at DESC, last_modified DESC);
|
|
186
|
+
`)
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
Object.freeze({
|
|
190
|
+
version: 6,
|
|
191
|
+
name: 'session_state_transactional_storage',
|
|
192
|
+
up(database) {
|
|
193
|
+
database.exec(`
|
|
194
|
+
CREATE TABLE session_states (
|
|
195
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
196
|
+
project_id TEXT NOT NULL,
|
|
197
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
198
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision > 0),
|
|
199
|
+
state_version INTEGER NOT NULL CHECK (typeof(state_version) = 'integer' AND state_version >= 0),
|
|
200
|
+
state_json TEXT NOT NULL CHECK (json_valid(state_json) AND json_type(state_json) = 'object'),
|
|
201
|
+
state_digest TEXT NOT NULL CHECK (length(state_digest) = 64 AND state_digest NOT GLOB '*[^0-9a-f]*'),
|
|
202
|
+
metadata_json TEXT NOT NULL CHECK (json_valid(metadata_json) AND json_type(metadata_json) = 'object'),
|
|
203
|
+
metadata_digest TEXT NOT NULL CHECK (length(metadata_digest) = 64 AND metadata_digest NOT GLOB '*[^0-9a-f]*'),
|
|
204
|
+
created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
|
|
205
|
+
updated_at TEXT NOT NULL CHECK (length(trim(updated_at)) > 0),
|
|
206
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
207
|
+
CHECK (
|
|
208
|
+
(scope = 'global' AND project_id = '') OR
|
|
209
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
210
|
+
)
|
|
211
|
+
) WITHOUT ROWID;
|
|
212
|
+
|
|
213
|
+
CREATE INDEX session_states_session_id_idx ON session_states (session_id);
|
|
214
|
+
|
|
215
|
+
CREATE TABLE session_state_tombstones (
|
|
216
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
217
|
+
project_id TEXT NOT NULL,
|
|
218
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
219
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision > 0),
|
|
220
|
+
deleted_at TEXT NOT NULL CHECK (length(trim(deleted_at)) > 0),
|
|
221
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
222
|
+
CHECK (
|
|
223
|
+
(scope = 'global' AND project_id = '') OR
|
|
224
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
225
|
+
)
|
|
226
|
+
) WITHOUT ROWID;
|
|
227
|
+
CREATE INDEX session_state_tombstones_session_id_idx ON session_state_tombstones (session_id);
|
|
228
|
+
|
|
229
|
+
CREATE TABLE session_storage_state (
|
|
230
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
231
|
+
phase TEXT NOT NULL CHECK (phase IN ('json_authoritative', 'cutover_running', 'sqlite_authoritative_json_pending', 'authoritative')),
|
|
232
|
+
state_count INTEGER CHECK (state_count IS NULL OR state_count >= 0),
|
|
233
|
+
digest TEXT,
|
|
234
|
+
backup_file TEXT,
|
|
235
|
+
diagnostic_json TEXT CHECK (diagnostic_json IS NULL OR json_valid(diagnostic_json)),
|
|
236
|
+
updated_at TEXT NOT NULL
|
|
237
|
+
);
|
|
238
|
+
INSERT INTO session_storage_state (singleton, phase, updated_at)
|
|
239
|
+
VALUES (1, 'json_authoritative', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
240
|
+
|
|
241
|
+
CREATE TABLE session_json_mirror_queue (
|
|
242
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
243
|
+
project_id TEXT NOT NULL,
|
|
244
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
245
|
+
operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete')),
|
|
246
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision >= 0),
|
|
247
|
+
state_json TEXT CHECK (state_json IS NULL OR (json_valid(state_json) AND json_type(state_json) = 'object')),
|
|
248
|
+
metadata_json TEXT CHECK (metadata_json IS NULL OR (json_valid(metadata_json) AND json_type(metadata_json) = 'object')),
|
|
249
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
250
|
+
last_error TEXT,
|
|
251
|
+
updated_at TEXT NOT NULL,
|
|
252
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
253
|
+
CHECK (
|
|
254
|
+
(scope = 'global' AND project_id = '') OR
|
|
255
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
256
|
+
),
|
|
257
|
+
CHECK (
|
|
258
|
+
(operation = 'upsert' AND state_json IS NOT NULL AND metadata_json IS NOT NULL) OR
|
|
259
|
+
(operation = 'delete' AND state_json IS NULL AND metadata_json IS NULL)
|
|
260
|
+
)
|
|
261
|
+
) WITHOUT ROWID;
|
|
262
|
+
|
|
263
|
+
CREATE TABLE session_state_maintenance_lock (
|
|
264
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
265
|
+
owner TEXT NOT NULL,
|
|
266
|
+
owner_pid INTEGER,
|
|
267
|
+
fencing INTEGER NOT NULL DEFAULT 1,
|
|
268
|
+
operation TEXT NOT NULL,
|
|
269
|
+
acquired_at TEXT NOT NULL,
|
|
270
|
+
heartbeat_at TEXT,
|
|
271
|
+
expires_at TEXT NOT NULL
|
|
272
|
+
);
|
|
273
|
+
`)
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
Object.freeze({
|
|
277
|
+
version: 7,
|
|
278
|
+
name: 'session_messages_incremental_storage',
|
|
279
|
+
up(database) {
|
|
280
|
+
database.exec(`
|
|
281
|
+
CREATE TABLE session_messages (
|
|
282
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
283
|
+
project_id TEXT NOT NULL,
|
|
284
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
285
|
+
seq INTEGER NOT NULL CHECK (typeof(seq) = 'integer' AND seq >= 0),
|
|
286
|
+
message_id TEXT CHECK (message_id IS NULL OR length(trim(message_id)) > 0),
|
|
287
|
+
message_json TEXT NOT NULL CHECK (json_valid(message_json) AND json_type(message_json) = 'object'),
|
|
288
|
+
message_digest TEXT NOT NULL CHECK (length(message_digest) = 64 AND message_digest NOT GLOB '*[^0-9a-f]*'),
|
|
289
|
+
created TEXT NOT NULL CHECK (length(trim(created)) > 0),
|
|
290
|
+
updated TEXT NOT NULL CHECK (length(trim(updated)) > 0),
|
|
291
|
+
PRIMARY KEY (scope, project_id, session_id, seq),
|
|
292
|
+
UNIQUE (scope, project_id, session_id, message_id),
|
|
293
|
+
CHECK (
|
|
294
|
+
(scope = 'global' AND project_id = '') OR
|
|
295
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
296
|
+
)
|
|
297
|
+
) WITHOUT ROWID;
|
|
298
|
+
|
|
299
|
+
CREATE INDEX session_messages_session_id_idx ON session_messages (session_id);
|
|
300
|
+
`)
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
Object.freeze({
|
|
304
|
+
version: 8,
|
|
305
|
+
name: 'share_storage_migration',
|
|
306
|
+
up(database) {
|
|
307
|
+
database.exec(`
|
|
308
|
+
CREATE TABLE share_sessions (
|
|
309
|
+
share_id TEXT PRIMARY KEY NOT NULL CHECK (length(trim(share_id)) > 0),
|
|
310
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
311
|
+
permission TEXT NOT NULL CHECK (permission IN ('read', 'operate')),
|
|
312
|
+
title_snapshot TEXT,
|
|
313
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
314
|
+
project_id TEXT CHECK (project_id IS NULL OR project_id = '' OR length(trim(project_id)) > 0),
|
|
315
|
+
password_hash TEXT,
|
|
316
|
+
password_salt TEXT,
|
|
317
|
+
password_version INTEGER CHECK (password_version IS NULL OR (typeof(password_version) = 'integer' AND password_version > 0)),
|
|
318
|
+
auth_version INTEGER NOT NULL CHECK (typeof(auth_version) = 'integer' AND auth_version >= 0),
|
|
319
|
+
allow_cloud_usage INTEGER NOT NULL CHECK (allow_cloud_usage IN (0, 1)),
|
|
320
|
+
created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
|
|
321
|
+
updated_at TEXT NOT NULL CHECK (length(trim(updated_at)) > 0),
|
|
322
|
+
expires_at TEXT,
|
|
323
|
+
revoked_at TEXT,
|
|
324
|
+
superseded_at TEXT,
|
|
325
|
+
access_count INTEGER NOT NULL CHECK (typeof(access_count) = 'integer' AND access_count >= 0),
|
|
326
|
+
last_accessed_at TEXT,
|
|
327
|
+
created_from_host TEXT,
|
|
328
|
+
last_updated_from_host TEXT,
|
|
329
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision > 0),
|
|
330
|
+
record_digest TEXT NOT NULL CHECK (length(record_digest) = 64 AND record_digest NOT GLOB '*[^0-9a-f]*'),
|
|
331
|
+
deleted_at TEXT,
|
|
332
|
+
extra_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(extra_json) AND json_type(extra_json) = 'object'),
|
|
333
|
+
CHECK (
|
|
334
|
+
(scope = 'global' AND (project_id IS NULL OR project_id = '')) OR
|
|
335
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
336
|
+
)
|
|
337
|
+
) WITHOUT ROWID;
|
|
338
|
+
|
|
339
|
+
CREATE INDEX share_sessions_session_id_idx ON share_sessions (session_id);
|
|
340
|
+
|
|
341
|
+
CREATE TABLE share_tokens (
|
|
342
|
+
share_id TEXT NOT NULL CHECK (length(trim(share_id)) > 0),
|
|
343
|
+
token_hash TEXT NOT NULL CHECK (length(trim(token_hash)) > 0),
|
|
344
|
+
issued_at TEXT NOT NULL CHECK (length(trim(issued_at)) > 0),
|
|
345
|
+
expires_at TEXT,
|
|
346
|
+
auth_version INTEGER NOT NULL CHECK (typeof(auth_version) = 'integer' AND auth_version >= 0),
|
|
347
|
+
PRIMARY KEY (share_id, token_hash),
|
|
348
|
+
FOREIGN KEY (share_id) REFERENCES share_sessions (share_id) ON DELETE CASCADE
|
|
349
|
+
) WITHOUT ROWID;
|
|
350
|
+
|
|
351
|
+
CREATE TABLE share_storage_state (
|
|
352
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
353
|
+
phase TEXT NOT NULL CHECK (phase IN ('json_authoritative', 'cutover_running', 'sqlite_authoritative_json_pending', 'authoritative')),
|
|
354
|
+
share_count INTEGER CHECK (share_count IS NULL OR share_count >= 0),
|
|
355
|
+
digest TEXT,
|
|
356
|
+
backup_file TEXT,
|
|
357
|
+
diagnostic_json TEXT CHECK (diagnostic_json IS NULL OR json_valid(diagnostic_json)),
|
|
358
|
+
updated_at TEXT NOT NULL
|
|
359
|
+
);
|
|
360
|
+
INSERT INTO share_storage_state (singleton, phase, updated_at)
|
|
361
|
+
VALUES (1, 'json_authoritative', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
362
|
+
|
|
363
|
+
CREATE TABLE share_maintenance_lock (
|
|
364
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
365
|
+
owner TEXT NOT NULL,
|
|
366
|
+
owner_pid INTEGER,
|
|
367
|
+
fencing INTEGER NOT NULL DEFAULT 1,
|
|
368
|
+
operation TEXT NOT NULL,
|
|
369
|
+
acquired_at TEXT NOT NULL,
|
|
370
|
+
heartbeat_at TEXT,
|
|
371
|
+
expires_at TEXT NOT NULL
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
CREATE TABLE share_json_mirror_queue (
|
|
375
|
+
share_id TEXT PRIMARY KEY NOT NULL CHECK (length(trim(share_id)) > 0),
|
|
376
|
+
operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete')),
|
|
377
|
+
share_json TEXT CHECK (share_json IS NULL OR (json_valid(share_json) AND json_type(share_json) = 'object')),
|
|
378
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
379
|
+
last_error TEXT,
|
|
380
|
+
updated_at TEXT NOT NULL,
|
|
381
|
+
CHECK (
|
|
382
|
+
(operation = 'upsert' AND share_json IS NOT NULL) OR
|
|
383
|
+
(operation = 'delete' AND share_json IS NULL)
|
|
384
|
+
)
|
|
385
|
+
) WITHOUT ROWID;
|
|
386
|
+
`)
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
Object.freeze({
|
|
390
|
+
version: 9,
|
|
391
|
+
name: 'lan_access_storage_migration',
|
|
392
|
+
up(database) {
|
|
393
|
+
database.exec(`
|
|
394
|
+
CREATE TABLE lan_access_state (
|
|
395
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
396
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
397
|
+
password_hash TEXT,
|
|
398
|
+
password_salt TEXT,
|
|
399
|
+
password_version INTEGER CHECK (password_version IS NULL OR (typeof(password_version) = 'integer' AND password_version > 0)),
|
|
400
|
+
auth_version INTEGER NOT NULL CHECK (typeof(auth_version) = 'integer' AND auth_version >= 1),
|
|
401
|
+
session_ttl_hours INTEGER NOT NULL CHECK (typeof(session_ttl_hours) = 'integer' AND session_ttl_hours >= 1 AND session_ttl_hours <= 168),
|
|
402
|
+
updated_at TEXT NOT NULL CHECK (length(trim(updated_at)) > 0),
|
|
403
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision > 0),
|
|
404
|
+
record_digest TEXT NOT NULL CHECK (length(record_digest) = 64 AND record_digest NOT GLOB '*[^0-9a-f]*'),
|
|
405
|
+
extra_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(extra_json) AND json_type(extra_json) = 'object'),
|
|
406
|
+
CHECK (
|
|
407
|
+
(enabled = 0) OR (password_hash IS NOT NULL AND password_salt IS NOT NULL)
|
|
408
|
+
)
|
|
409
|
+
) WITHOUT ROWID;
|
|
410
|
+
|
|
411
|
+
CREATE TABLE lan_access_tokens (
|
|
412
|
+
token_id TEXT NOT NULL CHECK (length(trim(token_id)) > 0),
|
|
413
|
+
seq INTEGER NOT NULL CHECK (typeof(seq) = 'integer' AND seq >= 0),
|
|
414
|
+
token_hash TEXT NOT NULL CHECK (length(trim(token_hash)) > 0),
|
|
415
|
+
issued_at TEXT NOT NULL CHECK (length(trim(issued_at)) > 0),
|
|
416
|
+
expires_at TEXT,
|
|
417
|
+
auth_version INTEGER NOT NULL CHECK (typeof(auth_version) = 'integer' AND auth_version >= 1),
|
|
418
|
+
remote_address TEXT,
|
|
419
|
+
user_agent TEXT,
|
|
420
|
+
PRIMARY KEY (token_id)
|
|
421
|
+
) WITHOUT ROWID;
|
|
422
|
+
CREATE INDEX lan_access_tokens_issued_idx ON lan_access_tokens (issued_at DESC, token_id DESC);
|
|
423
|
+
CREATE INDEX lan_access_tokens_seq_idx ON lan_access_tokens (seq DESC, token_id DESC);
|
|
424
|
+
|
|
425
|
+
CREATE TABLE lan_access_storage_state (
|
|
426
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
427
|
+
phase TEXT NOT NULL CHECK (phase IN ('json_authoritative', 'cutover_running', 'sqlite_authoritative_json_pending', 'authoritative')),
|
|
428
|
+
lan_token_count INTEGER CHECK (lan_token_count IS NULL OR lan_token_count >= 0),
|
|
429
|
+
digest TEXT,
|
|
430
|
+
backup_file TEXT,
|
|
431
|
+
diagnostic_json TEXT CHECK (diagnostic_json IS NULL OR json_valid(diagnostic_json)),
|
|
432
|
+
updated_at TEXT NOT NULL
|
|
433
|
+
);
|
|
434
|
+
INSERT INTO lan_access_storage_state (singleton, phase, updated_at)
|
|
435
|
+
VALUES (1, 'json_authoritative', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
436
|
+
|
|
437
|
+
CREATE TABLE lan_access_maintenance_lock (
|
|
438
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
439
|
+
owner TEXT NOT NULL,
|
|
440
|
+
owner_pid INTEGER,
|
|
441
|
+
fencing INTEGER NOT NULL DEFAULT 1,
|
|
442
|
+
operation TEXT NOT NULL,
|
|
443
|
+
acquired_at TEXT NOT NULL,
|
|
444
|
+
heartbeat_at TEXT,
|
|
445
|
+
expires_at TEXT NOT NULL
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
CREATE TABLE lan_access_json_mirror_queue (
|
|
449
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
450
|
+
operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete')),
|
|
451
|
+
config_json TEXT CHECK (config_json IS NULL OR (json_valid(config_json) AND json_type(config_json) = 'object')),
|
|
452
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
453
|
+
last_error TEXT,
|
|
454
|
+
updated_at TEXT NOT NULL,
|
|
455
|
+
CHECK (
|
|
456
|
+
(operation = 'upsert' AND config_json IS NOT NULL) OR
|
|
457
|
+
(operation = 'delete' AND config_json IS NULL)
|
|
458
|
+
)
|
|
459
|
+
) WITHOUT ROWID;
|
|
460
|
+
`)
|
|
461
|
+
},
|
|
462
|
+
}),
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
function readUserVersion(database) {
|
|
466
|
+
return Number(database.prepare('PRAGMA user_version').get().user_version)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function hasMigrationTable(database) {
|
|
470
|
+
return Boolean(database.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'schema_migrations'").get())
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function validateMigrationList(migrations) {
|
|
474
|
+
if (!Array.isArray(migrations) || migrations.length === 0) {
|
|
475
|
+
throw new Error('SQLite migration list must contain at least one migration')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const names = new Set()
|
|
479
|
+
for (let index = 0; index < migrations.length; index += 1) {
|
|
480
|
+
const migration = migrations[index]
|
|
481
|
+
const expectedVersion = index + 1
|
|
482
|
+
if (!migration || migration.version !== expectedVersion || typeof migration.name !== 'string' || !migration.name || typeof migration.up !== 'function') {
|
|
483
|
+
throw new Error(`SQLite migrations must be continuous and append-only; expected version ${expectedVersion}`)
|
|
484
|
+
}
|
|
485
|
+
if (names.has(migration.name)) throw new Error(`SQLite migration name must be unique: ${migration.name}`)
|
|
486
|
+
names.add(migration.name)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function readAppliedMigrations(database, tableExists) {
|
|
491
|
+
if (!tableExists) return []
|
|
492
|
+
try {
|
|
493
|
+
return database.prepare('SELECT version, name, applied_at FROM schema_migrations ORDER BY version').all()
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new Error(`SQLite migration metadata is inconsistent: ${error instanceof Error ? error.message : String(error)}`, { cause: error })
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function inspectSqliteMigrationState(database, { migrations = SQLITE_MIGRATIONS } = {}) {
|
|
500
|
+
validateMigrationList(migrations)
|
|
501
|
+
const latestVersion = migrations.length
|
|
502
|
+
const userVersion = readUserVersion(database)
|
|
503
|
+
const tableExists = hasMigrationTable(database)
|
|
504
|
+
const applied = readAppliedMigrations(database, tableExists)
|
|
505
|
+
|
|
506
|
+
if (userVersion > latestVersion) {
|
|
507
|
+
throw new Error(`SQLite schema version ${userVersion} is newer than supported version ${latestVersion}`)
|
|
508
|
+
}
|
|
509
|
+
if (userVersion === 0 && tableExists) {
|
|
510
|
+
throw new Error('SQLite migration metadata is inconsistent: schema_migrations exists while user_version is 0')
|
|
511
|
+
}
|
|
512
|
+
if (userVersion > 0 && !tableExists) {
|
|
513
|
+
throw new Error(`SQLite migration metadata is inconsistent: user_version is ${userVersion} but schema_migrations is missing`)
|
|
514
|
+
}
|
|
515
|
+
if (applied.length !== userVersion) {
|
|
516
|
+
throw new Error(`SQLite migration metadata is inconsistent: user_version is ${userVersion} but ${applied.length} migration rows exist`)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (let index = 0; index < applied.length; index += 1) {
|
|
520
|
+
const expected = migrations[index]
|
|
521
|
+
const actual = applied[index]
|
|
522
|
+
if (Number(actual.version) !== expected.version || actual.name !== expected.name) {
|
|
523
|
+
throw new Error(`SQLite migration metadata is inconsistent at version ${expected.version}`)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
userVersion,
|
|
529
|
+
latestVersion,
|
|
530
|
+
tableExists,
|
|
531
|
+
applied,
|
|
532
|
+
consistent: true,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function applySqliteMigrations(database, { migrations = SQLITE_MIGRATIONS, now = () => new Date().toISOString() } = {}) {
|
|
537
|
+
validateMigrationList(migrations)
|
|
538
|
+
let transactionOpen = false
|
|
539
|
+
let currentMigration = null
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
database.exec('BEGIN IMMEDIATE')
|
|
543
|
+
transactionOpen = true
|
|
544
|
+
|
|
545
|
+
const before = inspectSqliteMigrationState(database, { migrations })
|
|
546
|
+
for (const migration of migrations.slice(before.userVersion)) {
|
|
547
|
+
currentMigration = migration
|
|
548
|
+
try {
|
|
549
|
+
migration.up(database)
|
|
550
|
+
database.prepare('INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)')
|
|
551
|
+
.run(migration.version, migration.name, now())
|
|
552
|
+
database.exec(`PRAGMA user_version = ${migration.version}`)
|
|
553
|
+
} catch (error) {
|
|
554
|
+
throw new Error(`SQLite migration ${migration.version} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error })
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const after = inspectSqliteMigrationState(database, { migrations })
|
|
559
|
+
database.exec('COMMIT')
|
|
560
|
+
transactionOpen = false
|
|
561
|
+
return after
|
|
562
|
+
} catch (error) {
|
|
563
|
+
let rollbackError = null
|
|
564
|
+
if (transactionOpen) {
|
|
565
|
+
try {
|
|
566
|
+
database.exec('ROLLBACK')
|
|
567
|
+
} catch (rollbackFailure) {
|
|
568
|
+
rollbackError = rollbackFailure
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (rollbackError) {
|
|
572
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause: error })
|
|
573
|
+
}
|
|
574
|
+
if (currentMigration || error instanceof Error) throw error
|
|
575
|
+
throw new Error(`SQLite migration failed: ${String(error)}`, { cause: error })
|
|
576
|
+
}
|
|
577
|
+
}
|