offwatch 0.5.12 → 0.5.14

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.
Files changed (95) hide show
  1. package/README.md +132 -178
  2. package/bin/offwatch.js +6 -7
  3. package/lib/downloader.js +112 -0
  4. package/package.json +18 -11
  5. package/postinstall.js +18 -0
  6. package/src/__tests__/agent-jwt-env.test.ts +0 -79
  7. package/src/__tests__/allowed-hostname.test.ts +0 -80
  8. package/src/__tests__/auth-command-registration.test.ts +0 -16
  9. package/src/__tests__/board-auth.test.ts +0 -53
  10. package/src/__tests__/common.test.ts +0 -98
  11. package/src/__tests__/company-delete.test.ts +0 -95
  12. package/src/__tests__/company-import-export-e2e.test.ts +0 -502
  13. package/src/__tests__/company-import-url.test.ts +0 -74
  14. package/src/__tests__/company-import-zip.test.ts +0 -44
  15. package/src/__tests__/company.test.ts +0 -599
  16. package/src/__tests__/context.test.ts +0 -70
  17. package/src/__tests__/data-dir.test.ts +0 -79
  18. package/src/__tests__/doctor.test.ts +0 -102
  19. package/src/__tests__/feedback.test.ts +0 -177
  20. package/src/__tests__/helpers/embedded-postgres.ts +0 -6
  21. package/src/__tests__/helpers/zip.ts +0 -87
  22. package/src/__tests__/home-paths.test.ts +0 -44
  23. package/src/__tests__/http.test.ts +0 -106
  24. package/src/__tests__/network-bind.test.ts +0 -62
  25. package/src/__tests__/onboard.test.ts +0 -166
  26. package/src/__tests__/routines.test.ts +0 -249
  27. package/src/__tests__/telemetry.test.ts +0 -117
  28. package/src/__tests__/worktree-merge-history.test.ts +0 -492
  29. package/src/__tests__/worktree.test.ts +0 -982
  30. package/src/adapters/http/format-event.ts +0 -4
  31. package/src/adapters/http/index.ts +0 -7
  32. package/src/adapters/index.ts +0 -2
  33. package/src/adapters/process/format-event.ts +0 -4
  34. package/src/adapters/process/index.ts +0 -7
  35. package/src/adapters/registry.ts +0 -63
  36. package/src/checks/agent-jwt-secret-check.ts +0 -40
  37. package/src/checks/config-check.ts +0 -33
  38. package/src/checks/database-check.ts +0 -59
  39. package/src/checks/deployment-auth-check.ts +0 -88
  40. package/src/checks/index.ts +0 -18
  41. package/src/checks/llm-check.ts +0 -82
  42. package/src/checks/log-check.ts +0 -30
  43. package/src/checks/path-resolver.ts +0 -1
  44. package/src/checks/port-check.ts +0 -24
  45. package/src/checks/secrets-check.ts +0 -146
  46. package/src/checks/storage-check.ts +0 -51
  47. package/src/client/board-auth.ts +0 -282
  48. package/src/client/command-label.ts +0 -4
  49. package/src/client/context.ts +0 -175
  50. package/src/client/http.ts +0 -255
  51. package/src/commands/allowed-hostname.ts +0 -40
  52. package/src/commands/auth-bootstrap-ceo.ts +0 -138
  53. package/src/commands/client/activity.ts +0 -71
  54. package/src/commands/client/agent.ts +0 -315
  55. package/src/commands/client/approval.ts +0 -259
  56. package/src/commands/client/auth.ts +0 -113
  57. package/src/commands/client/common.ts +0 -221
  58. package/src/commands/client/company.ts +0 -1578
  59. package/src/commands/client/context.ts +0 -125
  60. package/src/commands/client/dashboard.ts +0 -34
  61. package/src/commands/client/feedback.ts +0 -645
  62. package/src/commands/client/issue.ts +0 -411
  63. package/src/commands/client/plugin.ts +0 -374
  64. package/src/commands/client/zip.ts +0 -129
  65. package/src/commands/configure.ts +0 -201
  66. package/src/commands/db-backup.ts +0 -102
  67. package/src/commands/doctor.ts +0 -203
  68. package/src/commands/env.ts +0 -411
  69. package/src/commands/heartbeat-run.ts +0 -344
  70. package/src/commands/onboard.ts +0 -692
  71. package/src/commands/routines.ts +0 -352
  72. package/src/commands/run.ts +0 -216
  73. package/src/commands/worktree-lib.ts +0 -279
  74. package/src/commands/worktree-merge-history-lib.ts +0 -764
  75. package/src/commands/worktree.ts +0 -2876
  76. package/src/config/data-dir.ts +0 -48
  77. package/src/config/env.ts +0 -125
  78. package/src/config/home.ts +0 -80
  79. package/src/config/hostnames.ts +0 -26
  80. package/src/config/schema.ts +0 -30
  81. package/src/config/secrets-key.ts +0 -48
  82. package/src/config/server-bind.ts +0 -183
  83. package/src/config/store.ts +0 -120
  84. package/src/index.ts +0 -182
  85. package/src/prompts/database.ts +0 -157
  86. package/src/prompts/llm.ts +0 -43
  87. package/src/prompts/logging.ts +0 -37
  88. package/src/prompts/secrets.ts +0 -99
  89. package/src/prompts/server.ts +0 -221
  90. package/src/prompts/storage.ts +0 -146
  91. package/src/telemetry.ts +0 -49
  92. package/src/utils/banner.ts +0 -24
  93. package/src/utils/net.ts +0 -18
  94. package/src/utils/path-resolver.ts +0 -25
  95. package/src/version.ts +0 -10
@@ -1,764 +0,0 @@
1
- import {
2
- agents,
3
- assets,
4
- documentRevisions,
5
- goals,
6
- issueAttachments,
7
- issueComments,
8
- issueDocuments,
9
- issues,
10
- projects,
11
- projectWorkspaces,
12
- } from "@paperclipai/db";
13
-
14
- type IssueRow = typeof issues.$inferSelect;
15
- type CommentRow = typeof issueComments.$inferSelect;
16
- type AgentRow = typeof agents.$inferSelect;
17
- type ProjectRow = typeof projects.$inferSelect;
18
- type ProjectWorkspaceRow = typeof projectWorkspaces.$inferSelect;
19
- type GoalRow = typeof goals.$inferSelect;
20
- type IssueDocumentLinkRow = typeof issueDocuments.$inferSelect;
21
- type DocumentRevisionTableRow = typeof documentRevisions.$inferSelect;
22
- type IssueAttachmentTableRow = typeof issueAttachments.$inferSelect;
23
- type AssetRow = typeof assets.$inferSelect;
24
-
25
- export const WORKTREE_MERGE_SCOPES = ["issues", "comments"] as const;
26
- export type WorktreeMergeScope = (typeof WORKTREE_MERGE_SCOPES)[number];
27
-
28
- export type ImportAdjustment =
29
- | "clear_assignee_agent"
30
- | "clear_project"
31
- | "clear_project_workspace"
32
- | "clear_goal"
33
- | "clear_author_agent"
34
- | "coerce_in_progress_to_todo"
35
- | "clear_document_agent"
36
- | "clear_document_revision_agent"
37
- | "clear_attachment_agent";
38
-
39
- export type IssueMergeAction = "skip_existing" | "insert";
40
- export type CommentMergeAction = "skip_existing" | "skip_missing_parent" | "insert";
41
-
42
- export type PlannedIssueInsert = {
43
- source: IssueRow;
44
- action: "insert";
45
- previewIssueNumber: number;
46
- previewIdentifier: string;
47
- targetStatus: string;
48
- targetAssigneeAgentId: string | null;
49
- targetCreatedByAgentId: string | null;
50
- targetProjectId: string | null;
51
- targetProjectWorkspaceId: string | null;
52
- targetGoalId: string | null;
53
- projectResolution: "preserved" | "cleared" | "mapped" | "imported";
54
- mappedProjectName: string | null;
55
- adjustments: ImportAdjustment[];
56
- };
57
-
58
- export type PlannedIssueSkip = {
59
- source: IssueRow;
60
- action: "skip_existing";
61
- driftKeys: string[];
62
- };
63
-
64
- export type PlannedCommentInsert = {
65
- source: CommentRow;
66
- action: "insert";
67
- targetAuthorAgentId: string | null;
68
- adjustments: ImportAdjustment[];
69
- };
70
-
71
- export type PlannedCommentSkip = {
72
- source: CommentRow;
73
- action: "skip_existing" | "skip_missing_parent";
74
- };
75
-
76
- export type IssueDocumentRow = {
77
- id: IssueDocumentLinkRow["id"];
78
- companyId: IssueDocumentLinkRow["companyId"];
79
- issueId: IssueDocumentLinkRow["issueId"];
80
- documentId: IssueDocumentLinkRow["documentId"];
81
- key: IssueDocumentLinkRow["key"];
82
- linkCreatedAt: IssueDocumentLinkRow["createdAt"];
83
- linkUpdatedAt: IssueDocumentLinkRow["updatedAt"];
84
- title: string | null;
85
- format: string;
86
- latestBody: string;
87
- latestRevisionId: string | null;
88
- latestRevisionNumber: number;
89
- createdByAgentId: string | null;
90
- createdByUserId: string | null;
91
- updatedByAgentId: string | null;
92
- updatedByUserId: string | null;
93
- documentCreatedAt: Date;
94
- documentUpdatedAt: Date;
95
- };
96
-
97
- export type DocumentRevisionRow = {
98
- id: DocumentRevisionTableRow["id"];
99
- companyId: DocumentRevisionTableRow["companyId"];
100
- documentId: DocumentRevisionTableRow["documentId"];
101
- revisionNumber: DocumentRevisionTableRow["revisionNumber"];
102
- body: DocumentRevisionTableRow["body"];
103
- changeSummary: DocumentRevisionTableRow["changeSummary"];
104
- createdByAgentId: string | null;
105
- createdByUserId: string | null;
106
- createdAt: Date;
107
- };
108
-
109
- export type IssueAttachmentRow = {
110
- id: IssueAttachmentTableRow["id"];
111
- companyId: IssueAttachmentTableRow["companyId"];
112
- issueId: IssueAttachmentTableRow["issueId"];
113
- issueCommentId: IssueAttachmentTableRow["issueCommentId"];
114
- assetId: IssueAttachmentTableRow["assetId"];
115
- provider: AssetRow["provider"];
116
- objectKey: AssetRow["objectKey"];
117
- contentType: AssetRow["contentType"];
118
- byteSize: AssetRow["byteSize"];
119
- sha256: AssetRow["sha256"];
120
- originalFilename: AssetRow["originalFilename"];
121
- createdByAgentId: string | null;
122
- createdByUserId: string | null;
123
- assetCreatedAt: Date;
124
- assetUpdatedAt: Date;
125
- attachmentCreatedAt: Date;
126
- attachmentUpdatedAt: Date;
127
- };
128
-
129
- export type PlannedDocumentRevisionInsert = {
130
- source: DocumentRevisionRow;
131
- targetRevisionNumber: number;
132
- targetCreatedByAgentId: string | null;
133
- adjustments: ImportAdjustment[];
134
- };
135
-
136
- export type PlannedIssueDocumentInsert = {
137
- source: IssueDocumentRow;
138
- action: "insert";
139
- targetCreatedByAgentId: string | null;
140
- targetUpdatedByAgentId: string | null;
141
- latestRevisionId: string | null;
142
- latestRevisionNumber: number;
143
- revisionsToInsert: PlannedDocumentRevisionInsert[];
144
- adjustments: ImportAdjustment[];
145
- };
146
-
147
- export type PlannedIssueDocumentMerge = {
148
- source: IssueDocumentRow;
149
- action: "merge_existing";
150
- targetCreatedByAgentId: string | null;
151
- targetUpdatedByAgentId: string | null;
152
- latestRevisionId: string | null;
153
- latestRevisionNumber: number;
154
- revisionsToInsert: PlannedDocumentRevisionInsert[];
155
- adjustments: ImportAdjustment[];
156
- };
157
-
158
- export type PlannedIssueDocumentSkip = {
159
- source: IssueDocumentRow;
160
- action: "skip_existing" | "skip_missing_parent" | "skip_conflicting_key";
161
- };
162
-
163
- export type PlannedAttachmentInsert = {
164
- source: IssueAttachmentRow;
165
- action: "insert";
166
- targetIssueCommentId: string | null;
167
- targetCreatedByAgentId: string | null;
168
- adjustments: ImportAdjustment[];
169
- };
170
-
171
- export type PlannedAttachmentSkip = {
172
- source: IssueAttachmentRow;
173
- action: "skip_existing" | "skip_missing_parent";
174
- };
175
-
176
- export type PlannedProjectImport = {
177
- source: ProjectRow;
178
- targetLeadAgentId: string | null;
179
- targetGoalId: string | null;
180
- workspaces: ProjectWorkspaceRow[];
181
- };
182
-
183
- export type WorktreeMergePlan = {
184
- companyId: string;
185
- companyName: string;
186
- issuePrefix: string;
187
- previewIssueCounterStart: number;
188
- scopes: WorktreeMergeScope[];
189
- projectImports: PlannedProjectImport[];
190
- issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip>;
191
- commentPlans: Array<PlannedCommentInsert | PlannedCommentSkip>;
192
- documentPlans: Array<PlannedIssueDocumentInsert | PlannedIssueDocumentMerge | PlannedIssueDocumentSkip>;
193
- attachmentPlans: Array<PlannedAttachmentInsert | PlannedAttachmentSkip>;
194
- counts: {
195
- projectsToImport: number;
196
- issuesToInsert: number;
197
- issuesExisting: number;
198
- issueDrift: number;
199
- commentsToInsert: number;
200
- commentsExisting: number;
201
- commentsMissingParent: number;
202
- documentsToInsert: number;
203
- documentsToMerge: number;
204
- documentsExisting: number;
205
- documentsConflictingKey: number;
206
- documentsMissingParent: number;
207
- documentRevisionsToInsert: number;
208
- attachmentsToInsert: number;
209
- attachmentsExisting: number;
210
- attachmentsMissingParent: number;
211
- };
212
- adjustments: Record<ImportAdjustment, number>;
213
- };
214
-
215
- function compareIssueCoreFields(source: IssueRow, target: IssueRow): string[] {
216
- const driftKeys: string[] = [];
217
- if (source.title !== target.title) driftKeys.push("title");
218
- if ((source.description ?? null) !== (target.description ?? null)) driftKeys.push("description");
219
- if (source.status !== target.status) driftKeys.push("status");
220
- if (source.priority !== target.priority) driftKeys.push("priority");
221
- if ((source.parentId ?? null) !== (target.parentId ?? null)) driftKeys.push("parentId");
222
- if ((source.projectId ?? null) !== (target.projectId ?? null)) driftKeys.push("projectId");
223
- if ((source.projectWorkspaceId ?? null) !== (target.projectWorkspaceId ?? null)) driftKeys.push("projectWorkspaceId");
224
- if ((source.goalId ?? null) !== (target.goalId ?? null)) driftKeys.push("goalId");
225
- if ((source.assigneeAgentId ?? null) !== (target.assigneeAgentId ?? null)) driftKeys.push("assigneeAgentId");
226
- if ((source.assigneeUserId ?? null) !== (target.assigneeUserId ?? null)) driftKeys.push("assigneeUserId");
227
- return driftKeys;
228
- }
229
-
230
- function incrementAdjustment(
231
- counts: Record<ImportAdjustment, number>,
232
- adjustment: ImportAdjustment,
233
- ): void {
234
- counts[adjustment] += 1;
235
- }
236
-
237
- function groupBy<T>(rows: T[], keyFor: (row: T) => string): Map<string, T[]> {
238
- const out = new Map<string, T[]>();
239
- for (const row of rows) {
240
- const key = keyFor(row);
241
- const existing = out.get(key);
242
- if (existing) {
243
- existing.push(row);
244
- } else {
245
- out.set(key, [row]);
246
- }
247
- }
248
- return out;
249
- }
250
-
251
- function sameDate(left: Date, right: Date): boolean {
252
- return left.getTime() === right.getTime();
253
- }
254
-
255
- function sortDocumentRows(rows: IssueDocumentRow[]): IssueDocumentRow[] {
256
- return [...rows].sort((left, right) => {
257
- const createdDelta = left.documentCreatedAt.getTime() - right.documentCreatedAt.getTime();
258
- if (createdDelta !== 0) return createdDelta;
259
- const linkDelta = left.linkCreatedAt.getTime() - right.linkCreatedAt.getTime();
260
- if (linkDelta !== 0) return linkDelta;
261
- return left.documentId.localeCompare(right.documentId);
262
- });
263
- }
264
-
265
- function sortDocumentRevisions(rows: DocumentRevisionRow[]): DocumentRevisionRow[] {
266
- return [...rows].sort((left, right) => {
267
- const revisionDelta = left.revisionNumber - right.revisionNumber;
268
- if (revisionDelta !== 0) return revisionDelta;
269
- const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
270
- if (createdDelta !== 0) return createdDelta;
271
- return left.id.localeCompare(right.id);
272
- });
273
- }
274
-
275
- function sortAttachments(rows: IssueAttachmentRow[]): IssueAttachmentRow[] {
276
- return [...rows].sort((left, right) => {
277
- const createdDelta = left.attachmentCreatedAt.getTime() - right.attachmentCreatedAt.getTime();
278
- if (createdDelta !== 0) return createdDelta;
279
- return left.id.localeCompare(right.id);
280
- });
281
- }
282
-
283
- function sortIssuesForImport(sourceIssues: IssueRow[]): IssueRow[] {
284
- const byId = new Map(sourceIssues.map((issue) => [issue.id, issue]));
285
- const memoDepth = new Map<string, number>();
286
-
287
- const depthFor = (issue: IssueRow, stack = new Set<string>()): number => {
288
- const memoized = memoDepth.get(issue.id);
289
- if (memoized !== undefined) return memoized;
290
- if (!issue.parentId) {
291
- memoDepth.set(issue.id, 0);
292
- return 0;
293
- }
294
- if (stack.has(issue.id)) {
295
- memoDepth.set(issue.id, 0);
296
- return 0;
297
- }
298
- const parent = byId.get(issue.parentId);
299
- if (!parent) {
300
- memoDepth.set(issue.id, 0);
301
- return 0;
302
- }
303
- stack.add(issue.id);
304
- const depth = depthFor(parent, stack) + 1;
305
- stack.delete(issue.id);
306
- memoDepth.set(issue.id, depth);
307
- return depth;
308
- };
309
-
310
- return [...sourceIssues].sort((left, right) => {
311
- const depthDelta = depthFor(left) - depthFor(right);
312
- if (depthDelta !== 0) return depthDelta;
313
- const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
314
- if (createdDelta !== 0) return createdDelta;
315
- return left.id.localeCompare(right.id);
316
- });
317
- }
318
-
319
- export function parseWorktreeMergeScopes(rawValue: string | undefined): WorktreeMergeScope[] {
320
- if (!rawValue || rawValue.trim().length === 0) {
321
- return ["issues", "comments"];
322
- }
323
-
324
- const parsed = rawValue
325
- .split(",")
326
- .map((value) => value.trim().toLowerCase())
327
- .filter((value): value is WorktreeMergeScope =>
328
- (WORKTREE_MERGE_SCOPES as readonly string[]).includes(value),
329
- );
330
-
331
- if (parsed.length === 0) {
332
- throw new Error(
333
- `Invalid scope "${rawValue}". Expected a comma-separated list of: ${WORKTREE_MERGE_SCOPES.join(", ")}.`,
334
- );
335
- }
336
-
337
- return [...new Set(parsed)];
338
- }
339
-
340
- export function buildWorktreeMergePlan(input: {
341
- companyId: string;
342
- companyName: string;
343
- issuePrefix: string;
344
- previewIssueCounterStart: number;
345
- scopes: WorktreeMergeScope[];
346
- sourceIssues: IssueRow[];
347
- targetIssues: IssueRow[];
348
- sourceComments: CommentRow[];
349
- targetComments: CommentRow[];
350
- sourceProjects?: ProjectRow[];
351
- sourceProjectWorkspaces?: ProjectWorkspaceRow[];
352
- sourceDocuments?: IssueDocumentRow[];
353
- targetDocuments?: IssueDocumentRow[];
354
- sourceDocumentRevisions?: DocumentRevisionRow[];
355
- targetDocumentRevisions?: DocumentRevisionRow[];
356
- sourceAttachments?: IssueAttachmentRow[];
357
- targetAttachments?: IssueAttachmentRow[];
358
- targetAgents: AgentRow[];
359
- targetProjects: ProjectRow[];
360
- targetProjectWorkspaces: ProjectWorkspaceRow[];
361
- targetGoals: GoalRow[];
362
- importProjectIds?: Iterable<string>;
363
- projectIdOverrides?: Record<string, string | null | undefined>;
364
- }): WorktreeMergePlan {
365
- const targetIssuesById = new Map(input.targetIssues.map((issue) => [issue.id, issue]));
366
- const targetCommentIds = new Set(input.targetComments.map((comment) => comment.id));
367
- const targetAgentIds = new Set(input.targetAgents.map((agent) => agent.id));
368
- const targetProjectIds = new Set(input.targetProjects.map((project) => project.id));
369
- const targetProjectsById = new Map(input.targetProjects.map((project) => [project.id, project]));
370
- const targetProjectWorkspaceIds = new Set(input.targetProjectWorkspaces.map((workspace) => workspace.id));
371
- const targetGoalIds = new Set(input.targetGoals.map((goal) => goal.id));
372
- const sourceProjectsById = new Map((input.sourceProjects ?? []).map((project) => [project.id, project]));
373
- const sourceProjectWorkspaces = input.sourceProjectWorkspaces ?? [];
374
- const sourceProjectWorkspacesByProjectId = groupBy(sourceProjectWorkspaces, (workspace) => workspace.projectId);
375
- const importProjectIds = new Set(input.importProjectIds ?? []);
376
- const scopes = new Set(input.scopes);
377
-
378
- const adjustmentCounts: Record<ImportAdjustment, number> = {
379
- clear_assignee_agent: 0,
380
- clear_project: 0,
381
- clear_project_workspace: 0,
382
- clear_goal: 0,
383
- clear_author_agent: 0,
384
- coerce_in_progress_to_todo: 0,
385
- clear_document_agent: 0,
386
- clear_document_revision_agent: 0,
387
- clear_attachment_agent: 0,
388
- };
389
-
390
- const projectImports: PlannedProjectImport[] = [];
391
- for (const projectId of importProjectIds) {
392
- if (targetProjectIds.has(projectId)) continue;
393
- const sourceProject = sourceProjectsById.get(projectId);
394
- if (!sourceProject) continue;
395
- projectImports.push({
396
- source: sourceProject,
397
- targetLeadAgentId:
398
- sourceProject.leadAgentId && targetAgentIds.has(sourceProject.leadAgentId)
399
- ? sourceProject.leadAgentId
400
- : null,
401
- targetGoalId:
402
- sourceProject.goalId && targetGoalIds.has(sourceProject.goalId)
403
- ? sourceProject.goalId
404
- : null,
405
- workspaces: [...(sourceProjectWorkspacesByProjectId.get(projectId) ?? [])].sort((left, right) => {
406
- const primaryDelta = Number(right.isPrimary) - Number(left.isPrimary);
407
- if (primaryDelta !== 0) return primaryDelta;
408
- const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
409
- if (createdDelta !== 0) return createdDelta;
410
- return left.id.localeCompare(right.id);
411
- }),
412
- });
413
- }
414
- const importedProjectWorkspaceIds = new Set(
415
- projectImports.flatMap((project) => project.workspaces.map((workspace) => workspace.id)),
416
- );
417
-
418
- const issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip> = [];
419
- let nextPreviewIssueNumber = input.previewIssueCounterStart;
420
- for (const issue of sortIssuesForImport(input.sourceIssues)) {
421
- const existing = targetIssuesById.get(issue.id);
422
- if (existing) {
423
- issuePlans.push({
424
- source: issue,
425
- action: "skip_existing",
426
- driftKeys: compareIssueCoreFields(issue, existing),
427
- });
428
- continue;
429
- }
430
-
431
- nextPreviewIssueNumber += 1;
432
- const adjustments: ImportAdjustment[] = [];
433
- const targetAssigneeAgentId =
434
- issue.assigneeAgentId && targetAgentIds.has(issue.assigneeAgentId) ? issue.assigneeAgentId : null;
435
- if (issue.assigneeAgentId && !targetAssigneeAgentId) {
436
- adjustments.push("clear_assignee_agent");
437
- incrementAdjustment(adjustmentCounts, "clear_assignee_agent");
438
- }
439
-
440
- const targetCreatedByAgentId =
441
- issue.createdByAgentId && targetAgentIds.has(issue.createdByAgentId) ? issue.createdByAgentId : null;
442
-
443
- let targetProjectId =
444
- issue.projectId && targetProjectIds.has(issue.projectId) ? issue.projectId : null;
445
- let projectResolution: PlannedIssueInsert["projectResolution"] = targetProjectId ? "preserved" : "cleared";
446
- let mappedProjectName: string | null = null;
447
- const overrideProjectId =
448
- issue.projectId && input.projectIdOverrides
449
- ? input.projectIdOverrides[issue.projectId] ?? null
450
- : null;
451
- if (!targetProjectId && overrideProjectId && targetProjectIds.has(overrideProjectId)) {
452
- targetProjectId = overrideProjectId;
453
- projectResolution = "mapped";
454
- mappedProjectName = targetProjectsById.get(overrideProjectId)?.name ?? null;
455
- }
456
- if (!targetProjectId && issue.projectId && importProjectIds.has(issue.projectId)) {
457
- const sourceProject = sourceProjectsById.get(issue.projectId);
458
- if (sourceProject) {
459
- targetProjectId = sourceProject.id;
460
- projectResolution = "imported";
461
- mappedProjectName = sourceProject.name;
462
- }
463
- }
464
- if (issue.projectId && !targetProjectId) {
465
- adjustments.push("clear_project");
466
- incrementAdjustment(adjustmentCounts, "clear_project");
467
- }
468
-
469
- const targetProjectWorkspaceId =
470
- targetProjectId
471
- && targetProjectId === issue.projectId
472
- && issue.projectWorkspaceId
473
- && (targetProjectWorkspaceIds.has(issue.projectWorkspaceId)
474
- || importedProjectWorkspaceIds.has(issue.projectWorkspaceId))
475
- ? issue.projectWorkspaceId
476
- : null;
477
- if (issue.projectWorkspaceId && !targetProjectWorkspaceId) {
478
- adjustments.push("clear_project_workspace");
479
- incrementAdjustment(adjustmentCounts, "clear_project_workspace");
480
- }
481
-
482
- const targetGoalId =
483
- issue.goalId && targetGoalIds.has(issue.goalId) ? issue.goalId : null;
484
- if (issue.goalId && !targetGoalId) {
485
- adjustments.push("clear_goal");
486
- incrementAdjustment(adjustmentCounts, "clear_goal");
487
- }
488
-
489
- let targetStatus = issue.status;
490
- if (
491
- targetStatus === "in_progress"
492
- && !targetAssigneeAgentId
493
- && !(issue.assigneeUserId && issue.assigneeUserId.trim().length > 0)
494
- ) {
495
- targetStatus = "todo";
496
- adjustments.push("coerce_in_progress_to_todo");
497
- incrementAdjustment(adjustmentCounts, "coerce_in_progress_to_todo");
498
- }
499
-
500
- issuePlans.push({
501
- source: issue,
502
- action: "insert",
503
- previewIssueNumber: nextPreviewIssueNumber,
504
- previewIdentifier: `${input.issuePrefix}-${nextPreviewIssueNumber}`,
505
- targetStatus,
506
- targetAssigneeAgentId,
507
- targetCreatedByAgentId,
508
- targetProjectId,
509
- targetProjectWorkspaceId,
510
- targetGoalId,
511
- projectResolution,
512
- mappedProjectName,
513
- adjustments,
514
- });
515
- }
516
-
517
- const issueIdsAvailableAfterImport = new Set<string>([
518
- ...input.targetIssues.map((issue) => issue.id),
519
- ...issuePlans.filter((plan): plan is PlannedIssueInsert => plan.action === "insert").map((plan) => plan.source.id),
520
- ]);
521
-
522
- const commentPlans: Array<PlannedCommentInsert | PlannedCommentSkip> = [];
523
- if (scopes.has("comments")) {
524
- const sortedComments = [...input.sourceComments].sort((left, right) => {
525
- const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
526
- if (createdDelta !== 0) return createdDelta;
527
- return left.id.localeCompare(right.id);
528
- });
529
-
530
- for (const comment of sortedComments) {
531
- if (targetCommentIds.has(comment.id)) {
532
- commentPlans.push({ source: comment, action: "skip_existing" });
533
- continue;
534
- }
535
- if (!issueIdsAvailableAfterImport.has(comment.issueId)) {
536
- commentPlans.push({ source: comment, action: "skip_missing_parent" });
537
- continue;
538
- }
539
-
540
- const adjustments: ImportAdjustment[] = [];
541
- const targetAuthorAgentId =
542
- comment.authorAgentId && targetAgentIds.has(comment.authorAgentId) ? comment.authorAgentId : null;
543
- if (comment.authorAgentId && !targetAuthorAgentId) {
544
- adjustments.push("clear_author_agent");
545
- incrementAdjustment(adjustmentCounts, "clear_author_agent");
546
- }
547
-
548
- commentPlans.push({
549
- source: comment,
550
- action: "insert",
551
- targetAuthorAgentId,
552
- adjustments,
553
- });
554
- }
555
- }
556
-
557
- const sourceDocuments = input.sourceDocuments ?? [];
558
- const targetDocuments = input.targetDocuments ?? [];
559
- const sourceDocumentRevisions = input.sourceDocumentRevisions ?? [];
560
- const targetDocumentRevisions = input.targetDocumentRevisions ?? [];
561
-
562
- const targetDocumentsById = new Map(targetDocuments.map((document) => [document.documentId, document]));
563
- const targetDocumentsByIssueKey = new Map(targetDocuments.map((document) => [`${document.issueId}:${document.key}`, document]));
564
- const sourceRevisionsByDocumentId = groupBy(sourceDocumentRevisions, (revision) => revision.documentId);
565
- const targetRevisionsByDocumentId = groupBy(targetDocumentRevisions, (revision) => revision.documentId);
566
- const commentIdsAvailableAfterImport = new Set<string>([
567
- ...input.targetComments.map((comment) => comment.id),
568
- ...commentPlans.filter((plan): plan is PlannedCommentInsert => plan.action === "insert").map((plan) => plan.source.id),
569
- ]);
570
-
571
- const documentPlans: Array<PlannedIssueDocumentInsert | PlannedIssueDocumentMerge | PlannedIssueDocumentSkip> = [];
572
- for (const document of sortDocumentRows(sourceDocuments)) {
573
- if (!issueIdsAvailableAfterImport.has(document.issueId)) {
574
- documentPlans.push({ source: document, action: "skip_missing_parent" });
575
- continue;
576
- }
577
-
578
- const existingDocument = targetDocumentsById.get(document.documentId);
579
- const conflictingIssueKeyDocument = targetDocumentsByIssueKey.get(`${document.issueId}:${document.key}`);
580
- if (!existingDocument && conflictingIssueKeyDocument && conflictingIssueKeyDocument.documentId !== document.documentId) {
581
- documentPlans.push({ source: document, action: "skip_conflicting_key" });
582
- continue;
583
- }
584
-
585
- const adjustments: ImportAdjustment[] = [];
586
- const targetCreatedByAgentId =
587
- document.createdByAgentId && targetAgentIds.has(document.createdByAgentId) ? document.createdByAgentId : null;
588
- const targetUpdatedByAgentId =
589
- document.updatedByAgentId && targetAgentIds.has(document.updatedByAgentId) ? document.updatedByAgentId : null;
590
- if (
591
- (document.createdByAgentId && !targetCreatedByAgentId)
592
- || (document.updatedByAgentId && !targetUpdatedByAgentId)
593
- ) {
594
- adjustments.push("clear_document_agent");
595
- incrementAdjustment(adjustmentCounts, "clear_document_agent");
596
- }
597
-
598
- const sourceRevisions = sortDocumentRevisions(sourceRevisionsByDocumentId.get(document.documentId) ?? []);
599
- const targetRevisions = sortDocumentRevisions(targetRevisionsByDocumentId.get(document.documentId) ?? []);
600
- const existingRevisionIds = new Set(targetRevisions.map((revision) => revision.id));
601
- const usedRevisionNumbers = new Set(targetRevisions.map((revision) => revision.revisionNumber));
602
- let nextRevisionNumber = targetRevisions.reduce(
603
- (maxValue, revision) => Math.max(maxValue, revision.revisionNumber),
604
- 0,
605
- ) + 1;
606
-
607
- const targetRevisionNumberById = new Map<string, number>(
608
- targetRevisions.map((revision) => [revision.id, revision.revisionNumber]),
609
- );
610
- const revisionsToInsert: PlannedDocumentRevisionInsert[] = [];
611
-
612
- for (const revision of sourceRevisions) {
613
- if (existingRevisionIds.has(revision.id)) continue;
614
- let targetRevisionNumber = revision.revisionNumber;
615
- if (usedRevisionNumbers.has(targetRevisionNumber)) {
616
- while (usedRevisionNumbers.has(nextRevisionNumber)) {
617
- nextRevisionNumber += 1;
618
- }
619
- targetRevisionNumber = nextRevisionNumber;
620
- nextRevisionNumber += 1;
621
- }
622
- usedRevisionNumbers.add(targetRevisionNumber);
623
- targetRevisionNumberById.set(revision.id, targetRevisionNumber);
624
-
625
- const revisionAdjustments: ImportAdjustment[] = [];
626
- const targetCreatedByAgentId =
627
- revision.createdByAgentId && targetAgentIds.has(revision.createdByAgentId) ? revision.createdByAgentId : null;
628
- if (revision.createdByAgentId && !targetCreatedByAgentId) {
629
- revisionAdjustments.push("clear_document_revision_agent");
630
- incrementAdjustment(adjustmentCounts, "clear_document_revision_agent");
631
- }
632
-
633
- revisionsToInsert.push({
634
- source: revision,
635
- targetRevisionNumber,
636
- targetCreatedByAgentId,
637
- adjustments: revisionAdjustments,
638
- });
639
- }
640
-
641
- const latestRevisionId = document.latestRevisionId ?? existingDocument?.latestRevisionId ?? null;
642
- const latestRevisionNumber =
643
- (latestRevisionId ? targetRevisionNumberById.get(latestRevisionId) : undefined)
644
- ?? document.latestRevisionNumber
645
- ?? existingDocument?.latestRevisionNumber
646
- ?? 0;
647
-
648
- if (!existingDocument) {
649
- documentPlans.push({
650
- source: document,
651
- action: "insert",
652
- targetCreatedByAgentId,
653
- targetUpdatedByAgentId,
654
- latestRevisionId,
655
- latestRevisionNumber,
656
- revisionsToInsert,
657
- adjustments,
658
- });
659
- continue;
660
- }
661
-
662
- const documentAlreadyMatches =
663
- existingDocument.key === document.key
664
- && existingDocument.title === document.title
665
- && existingDocument.format === document.format
666
- && existingDocument.latestBody === document.latestBody
667
- && (existingDocument.latestRevisionId ?? null) === latestRevisionId
668
- && existingDocument.latestRevisionNumber === latestRevisionNumber
669
- && (existingDocument.updatedByAgentId ?? null) === targetUpdatedByAgentId
670
- && (existingDocument.updatedByUserId ?? null) === (document.updatedByUserId ?? null)
671
- && sameDate(existingDocument.documentUpdatedAt, document.documentUpdatedAt)
672
- && sameDate(existingDocument.linkUpdatedAt, document.linkUpdatedAt)
673
- && revisionsToInsert.length === 0;
674
-
675
- if (documentAlreadyMatches) {
676
- documentPlans.push({ source: document, action: "skip_existing" });
677
- continue;
678
- }
679
-
680
- documentPlans.push({
681
- source: document,
682
- action: "merge_existing",
683
- targetCreatedByAgentId,
684
- targetUpdatedByAgentId,
685
- latestRevisionId,
686
- latestRevisionNumber,
687
- revisionsToInsert,
688
- adjustments,
689
- });
690
- }
691
-
692
- const sourceAttachments = input.sourceAttachments ?? [];
693
- const targetAttachmentIds = new Set((input.targetAttachments ?? []).map((attachment) => attachment.id));
694
- const attachmentPlans: Array<PlannedAttachmentInsert | PlannedAttachmentSkip> = [];
695
- for (const attachment of sortAttachments(sourceAttachments)) {
696
- if (targetAttachmentIds.has(attachment.id)) {
697
- attachmentPlans.push({ source: attachment, action: "skip_existing" });
698
- continue;
699
- }
700
- if (!issueIdsAvailableAfterImport.has(attachment.issueId)) {
701
- attachmentPlans.push({ source: attachment, action: "skip_missing_parent" });
702
- continue;
703
- }
704
-
705
- const adjustments: ImportAdjustment[] = [];
706
- const targetCreatedByAgentId =
707
- attachment.createdByAgentId && targetAgentIds.has(attachment.createdByAgentId)
708
- ? attachment.createdByAgentId
709
- : null;
710
- if (attachment.createdByAgentId && !targetCreatedByAgentId) {
711
- adjustments.push("clear_attachment_agent");
712
- incrementAdjustment(adjustmentCounts, "clear_attachment_agent");
713
- }
714
-
715
- attachmentPlans.push({
716
- source: attachment,
717
- action: "insert",
718
- targetIssueCommentId:
719
- attachment.issueCommentId && commentIdsAvailableAfterImport.has(attachment.issueCommentId)
720
- ? attachment.issueCommentId
721
- : null,
722
- targetCreatedByAgentId,
723
- adjustments,
724
- });
725
- }
726
-
727
- const counts = {
728
- projectsToImport: projectImports.length,
729
- issuesToInsert: issuePlans.filter((plan) => plan.action === "insert").length,
730
- issuesExisting: issuePlans.filter((plan) => plan.action === "skip_existing").length,
731
- issueDrift: issuePlans.filter((plan) => plan.action === "skip_existing" && plan.driftKeys.length > 0).length,
732
- commentsToInsert: commentPlans.filter((plan) => plan.action === "insert").length,
733
- commentsExisting: commentPlans.filter((plan) => plan.action === "skip_existing").length,
734
- commentsMissingParent: commentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
735
- documentsToInsert: documentPlans.filter((plan) => plan.action === "insert").length,
736
- documentsToMerge: documentPlans.filter((plan) => plan.action === "merge_existing").length,
737
- documentsExisting: documentPlans.filter((plan) => plan.action === "skip_existing").length,
738
- documentsConflictingKey: documentPlans.filter((plan) => plan.action === "skip_conflicting_key").length,
739
- documentsMissingParent: documentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
740
- documentRevisionsToInsert: documentPlans.reduce(
741
- (sum, plan) =>
742
- sum + (plan.action === "insert" || plan.action === "merge_existing" ? plan.revisionsToInsert.length : 0),
743
- 0,
744
- ),
745
- attachmentsToInsert: attachmentPlans.filter((plan) => plan.action === "insert").length,
746
- attachmentsExisting: attachmentPlans.filter((plan) => plan.action === "skip_existing").length,
747
- attachmentsMissingParent: attachmentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
748
- };
749
-
750
- return {
751
- companyId: input.companyId,
752
- companyName: input.companyName,
753
- issuePrefix: input.issuePrefix,
754
- previewIssueCounterStart: input.previewIssueCounterStart,
755
- scopes: input.scopes,
756
- projectImports,
757
- issuePlans,
758
- commentPlans,
759
- documentPlans,
760
- attachmentPlans,
761
- counts,
762
- adjustments: adjustmentCounts,
763
- };
764
- }