@agent-native/core 0.164.12 → 0.164.15
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/corpus/README.md +1 -1
- package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
- package/corpus/templates/content/actions/_database-utils.ts +2 -0
- package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
- package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
- package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
- package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
- package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
- package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
- package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
- package/corpus/templates/content/app/i18n-data.ts +79 -0
- package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
- package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
- package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
- package/corpus/templates/content/app/root.tsx +2 -1
- package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
- package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
- package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
- package/corpus/templates/content/docs/product/roadmap.md +2 -2
- package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
- package/corpus/templates/content/shared/api.ts +25 -0
- package/dist/app-config/auth.d.ts +4 -0
- package/dist/app-config/auth.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/cli/code-agent-executor.js +6 -1
- package/dist/cli/code-agent-mcp-config.d.ts +2 -2
- package/dist/cli/code-agent-mcp-config.js +34 -9
- package/dist/client/org/OrgSwitcher.js +8 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp-client/config.d.ts +11 -0
- package/dist/mcp-client/config.js +33 -26
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/auth.js +15 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -60,6 +60,10 @@ import {
|
|
|
60
60
|
chunks,
|
|
61
61
|
processWithConcurrency,
|
|
62
62
|
} from "./_batch-utils.js";
|
|
63
|
+
import {
|
|
64
|
+
LOCAL_FOLDER_SOURCE_TYPE,
|
|
65
|
+
localFolderSourceIdentityFromMetadata,
|
|
66
|
+
} from "./_local-folder-source.js";
|
|
63
67
|
export { bulkChunkSizeForColumnCount } from "./_batch-utils.js";
|
|
64
68
|
import {
|
|
65
69
|
readBuilderCmsContentEntry,
|
|
@@ -176,6 +180,9 @@ type SourceMetadataRecord = {
|
|
|
176
180
|
connectionId?: string | null;
|
|
177
181
|
connectionLabel?: string | null;
|
|
178
182
|
truthPolicy?: ContentDatabaseSource["metadata"]["truthPolicy"];
|
|
183
|
+
syncPolicy?: "manual" | "keep_in_sync";
|
|
184
|
+
liveBridgeEnabled?: boolean;
|
|
185
|
+
localIdentity?: unknown;
|
|
179
186
|
liveReadConfigured?: boolean;
|
|
180
187
|
lastReadEntryCount?: number;
|
|
181
188
|
lastReadMatchedRowCount?: number;
|
|
@@ -4572,14 +4579,19 @@ async function loadSourceSnapshot(
|
|
|
4572
4579
|
? metadata.writeMode
|
|
4573
4580
|
: undefined;
|
|
4574
4581
|
const capabilities = normalizeCapabilities(source.capabilitiesJson);
|
|
4575
|
-
|
|
4582
|
+
const sourceType = normalizeSourceType(source.sourceType);
|
|
4583
|
+
if (sourceType === LOCAL_FOLDER_SOURCE_TYPE) {
|
|
4584
|
+
capabilities.liveWritesEnabled =
|
|
4585
|
+
metadata.liveBridgeEnabled === true &&
|
|
4586
|
+
metadata.syncPolicy === "keep_in_sync";
|
|
4587
|
+
} else if (normalizedWriteMode) {
|
|
4576
4588
|
capabilities.liveWritesEnabled = normalizedWriteMode !== "read_only";
|
|
4577
4589
|
}
|
|
4578
4590
|
|
|
4579
4591
|
// A local-table source shows the target database's *live* title, so renaming
|
|
4580
4592
|
// the underlying table is reflected here instead of the name frozen at attach.
|
|
4581
4593
|
let displaySourceName = source.sourceName;
|
|
4582
|
-
if (
|
|
4594
|
+
if (sourceType === "local-table") {
|
|
4583
4595
|
const [target] = await db
|
|
4584
4596
|
.select({ title: schema.contentDatabases.title })
|
|
4585
4597
|
.from(schema.contentDatabases)
|
|
@@ -4626,6 +4638,17 @@ async function loadSourceSnapshot(
|
|
|
4626
4638
|
metadata.truthPolicy === "reviewed_bidirectional"
|
|
4627
4639
|
? metadata.truthPolicy
|
|
4628
4640
|
: undefined,
|
|
4641
|
+
syncPolicy:
|
|
4642
|
+
metadata.syncPolicy === "manual" ||
|
|
4643
|
+
metadata.syncPolicy === "keep_in_sync"
|
|
4644
|
+
? metadata.syncPolicy
|
|
4645
|
+
: undefined,
|
|
4646
|
+
liveBridgeEnabled:
|
|
4647
|
+
metadata.liveBridgeEnabled === true &&
|
|
4648
|
+
metadata.syncPolicy === "keep_in_sync",
|
|
4649
|
+
localIdentity: localFolderSourceIdentityFromMetadata(
|
|
4650
|
+
metadata.localIdentity,
|
|
4651
|
+
),
|
|
4629
4652
|
liveReadConfigured: metadata.liveReadConfigured === true,
|
|
4630
4653
|
lastReadEntryCount:
|
|
4631
4654
|
typeof metadata.lastReadEntryCount === "number"
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getDatabaseMutationContract,
|
|
51
51
|
} from "./_database-row-mutation.js";
|
|
52
52
|
import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
|
|
53
|
+
import { serializeDocumentSource } from "./_document-source.js";
|
|
53
54
|
import {
|
|
54
55
|
applyFederatedOverlayValues,
|
|
55
56
|
federateSources,
|
|
@@ -443,6 +444,7 @@ function serializeDocument(
|
|
|
443
444
|
databaseMembership: membership
|
|
444
445
|
? serializeDatabaseMembership(membership)
|
|
445
446
|
: undefined,
|
|
447
|
+
source: serializeDocumentSource(doc),
|
|
446
448
|
createdAt: doc.createdAt,
|
|
447
449
|
updatedAt: doc.updatedAt,
|
|
448
450
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
ContentDatabaseSourceCapabilities,
|
|
5
7
|
ContentDatabaseSourceTruthPolicy,
|
|
@@ -7,6 +9,171 @@ import type {
|
|
|
7
9
|
|
|
8
10
|
export const LOCAL_FOLDER_SOURCE_TYPE = "local-folder" as const;
|
|
9
11
|
|
|
12
|
+
export const localFolderOpaqueIdentitySchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.trim()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(300)
|
|
17
|
+
.refine(
|
|
18
|
+
(value) =>
|
|
19
|
+
!value.includes("/") &&
|
|
20
|
+
!value.includes("\\") &&
|
|
21
|
+
!value.includes("..") &&
|
|
22
|
+
!value.includes("\0"),
|
|
23
|
+
"Local source identities must be opaque IDs, not paths.",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const labelSchema = z.string().trim().min(1).max(200);
|
|
27
|
+
|
|
28
|
+
export const localFolderSourceConnectionMetadataSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
repository: z
|
|
31
|
+
.object({
|
|
32
|
+
localId: localFolderOpaqueIdentitySchema,
|
|
33
|
+
providerBinding: z
|
|
34
|
+
.object({
|
|
35
|
+
provider: z.literal("github"),
|
|
36
|
+
repositoryId: localFolderOpaqueIdentitySchema,
|
|
37
|
+
})
|
|
38
|
+
.strict()
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.optional(),
|
|
43
|
+
workingCopy: z
|
|
44
|
+
.object({
|
|
45
|
+
id: localFolderOpaqueIdentitySchema,
|
|
46
|
+
repositoryId: localFolderOpaqueIdentitySchema.optional(),
|
|
47
|
+
kind: z.enum(["persistent", "temporary"]),
|
|
48
|
+
name: labelSchema,
|
|
49
|
+
branch: z.string().trim().min(1).max(300).optional(),
|
|
50
|
+
commit: z.string().trim().min(1).max(300).optional(),
|
|
51
|
+
deviceId: localFolderOpaqueIdentitySchema,
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.optional(),
|
|
55
|
+
liveBridgeEnabled: z.boolean().optional().default(false),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
export type LocalFolderSourceConnectionMetadata = z.infer<
|
|
60
|
+
typeof localFolderSourceConnectionMetadataSchema
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
export type LocalFolderSourceIdentity = {
|
|
64
|
+
repository?: {
|
|
65
|
+
localId: string;
|
|
66
|
+
providerBinding?: { provider: "github"; repositoryId: string };
|
|
67
|
+
};
|
|
68
|
+
workingCopy: {
|
|
69
|
+
id: string;
|
|
70
|
+
repositoryId?: string;
|
|
71
|
+
kind: "persistent" | "temporary";
|
|
72
|
+
name: string;
|
|
73
|
+
branch?: string;
|
|
74
|
+
commit?: string;
|
|
75
|
+
deviceId: string;
|
|
76
|
+
localOnly: boolean;
|
|
77
|
+
shareable: boolean;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function localFolderSourceIdentityFromMetadata(
|
|
82
|
+
value: unknown,
|
|
83
|
+
): LocalFolderSourceIdentity | undefined {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const stored = value as {
|
|
88
|
+
repository?: unknown;
|
|
89
|
+
workingCopy?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
const connectionMetadata =
|
|
92
|
+
localFolderSourceConnectionMetadataSchema.safeParse({
|
|
93
|
+
repository: stored.repository,
|
|
94
|
+
workingCopy: stored.workingCopy
|
|
95
|
+
? Object.fromEntries(
|
|
96
|
+
Object.entries(stored.workingCopy).filter(
|
|
97
|
+
([key]) => key !== "localOnly" && key !== "shareable",
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
: undefined,
|
|
101
|
+
});
|
|
102
|
+
if (!connectionMetadata.success || !connectionMetadata.data.workingCopy) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return normalizeLocalFolderSourceIdentity({
|
|
107
|
+
connectionId: "stored-local-source",
|
|
108
|
+
label: connectionMetadata.data.workingCopy.name,
|
|
109
|
+
connectionMetadata: connectionMetadata.data,
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
// coercion-ok: invalid legacy metadata is represented as absent optional identity
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizeLocalFolderSourceIdentity(input: {
|
|
118
|
+
connectionId: string;
|
|
119
|
+
label: string;
|
|
120
|
+
connectionMetadata?: LocalFolderSourceConnectionMetadata;
|
|
121
|
+
}): LocalFolderSourceIdentity {
|
|
122
|
+
const metadata = localFolderSourceConnectionMetadataSchema.parse(
|
|
123
|
+
input.connectionMetadata ?? {},
|
|
124
|
+
);
|
|
125
|
+
const workingCopy = metadata.workingCopy ?? {
|
|
126
|
+
id: input.connectionId,
|
|
127
|
+
kind: "persistent" as const,
|
|
128
|
+
name: input.label,
|
|
129
|
+
deviceId: "unknown-device",
|
|
130
|
+
};
|
|
131
|
+
if (
|
|
132
|
+
workingCopy.repositoryId &&
|
|
133
|
+
(!metadata.repository ||
|
|
134
|
+
workingCopy.repositoryId !== metadata.repository.localId)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Working-copy repositoryId must match the supplied repository identity.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...(metadata.repository ? { repository: metadata.repository } : {}),
|
|
142
|
+
workingCopy: {
|
|
143
|
+
...workingCopy,
|
|
144
|
+
localOnly: workingCopy.kind === "temporary",
|
|
145
|
+
shareable: workingCopy.kind !== "temporary",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function localFolderObservedRevision(input: {
|
|
151
|
+
contentHash: string;
|
|
152
|
+
metadataHash: string;
|
|
153
|
+
}) {
|
|
154
|
+
return `sha256:${createHash("sha256")
|
|
155
|
+
.update(`${input.contentHash}:${input.metadataHash}`)
|
|
156
|
+
.digest("hex")}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function localFolderSourceFileIdentity(input: {
|
|
160
|
+
workingCopyId: string;
|
|
161
|
+
relativePath: string;
|
|
162
|
+
observedRevision: string;
|
|
163
|
+
}) {
|
|
164
|
+
const relativePath = input.relativePath.replace(/\\/g, "/");
|
|
165
|
+
if (
|
|
166
|
+
!relativePath ||
|
|
167
|
+
relativePath.startsWith("/") ||
|
|
168
|
+
relativePath.split("/").some((part) => part === "." || part === "..")
|
|
169
|
+
) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Local source file identity requires a safe relative path.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return { ...input, relativePath };
|
|
175
|
+
}
|
|
176
|
+
|
|
10
177
|
export function localFolderSourceId(databaseId: string, connectionId: string) {
|
|
11
178
|
return `content_database_source_local_folder_${createHash("sha256")
|
|
12
179
|
.update(`${databaseId}:${connectionId}`)
|
|
@@ -14,7 +181,9 @@ export function localFolderSourceId(databaseId: string, connectionId: string) {
|
|
|
14
181
|
.slice(0, 32)}`;
|
|
15
182
|
}
|
|
16
183
|
|
|
17
|
-
export function localFolderSourceCapabilities(
|
|
184
|
+
export function localFolderSourceCapabilities(input?: {
|
|
185
|
+
liveBridgeEnabled?: boolean;
|
|
186
|
+
}): ContentDatabaseSourceCapabilities {
|
|
18
187
|
return {
|
|
19
188
|
canRefresh: true,
|
|
20
189
|
canCreateChangeSets: true,
|
|
@@ -25,7 +194,7 @@ export function localFolderSourceCapabilities(): ContentDatabaseSourceCapabiliti
|
|
|
25
194
|
canPublish: false,
|
|
26
195
|
canDelete: true,
|
|
27
196
|
canStageLocalRevision: true,
|
|
28
|
-
liveWritesEnabled:
|
|
197
|
+
liveWritesEnabled: input?.liveBridgeEnabled === true,
|
|
29
198
|
readOnlyRefresh: false,
|
|
30
199
|
canRename: true,
|
|
31
200
|
canReveal: true,
|
|
@@ -37,7 +206,16 @@ export function localFolderSourceMetadata(input: {
|
|
|
37
206
|
connectionId: string;
|
|
38
207
|
label: string;
|
|
39
208
|
truthPolicy: ContentDatabaseSourceTruthPolicy;
|
|
209
|
+
connectionMetadata?: LocalFolderSourceConnectionMetadata;
|
|
40
210
|
}) {
|
|
211
|
+
const connectionMetadata = localFolderSourceConnectionMetadataSchema.parse(
|
|
212
|
+
input.connectionMetadata ?? {},
|
|
213
|
+
);
|
|
214
|
+
const identity = normalizeLocalFolderSourceIdentity({
|
|
215
|
+
connectionId: input.connectionId,
|
|
216
|
+
label: input.label,
|
|
217
|
+
connectionMetadata,
|
|
218
|
+
});
|
|
41
219
|
return {
|
|
42
220
|
primaryKey: "relative_path",
|
|
43
221
|
naturalKeyField: "relative_path",
|
|
@@ -47,6 +225,12 @@ export function localFolderSourceMetadata(input: {
|
|
|
47
225
|
connectionLabel: input.label,
|
|
48
226
|
truthPolicy: input.truthPolicy,
|
|
49
227
|
writeMode: "stage_only" as const,
|
|
228
|
+
syncPolicy:
|
|
229
|
+
connectionMetadata.liveBridgeEnabled === true
|
|
230
|
+
? ("keep_in_sync" as const)
|
|
231
|
+
: ("manual" as const),
|
|
232
|
+
liveBridgeEnabled: connectionMetadata.liveBridgeEnabled === true,
|
|
233
|
+
localIdentity: identity,
|
|
50
234
|
notes:
|
|
51
235
|
"The trusted local bridge owns the folder handle or path; SQL stores only this opaque connection identity.",
|
|
52
236
|
};
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "./_content-spaces.js";
|
|
18
18
|
import {
|
|
19
19
|
LOCAL_FOLDER_SOURCE_TYPE,
|
|
20
|
+
localFolderSourceConnectionMetadataSchema,
|
|
21
|
+
localFolderOpaqueIdentitySchema,
|
|
20
22
|
localFolderSourceCapabilities,
|
|
21
23
|
localFolderSourceId,
|
|
22
24
|
localFolderSourceMetadata,
|
|
@@ -30,12 +32,15 @@ export default defineAction({
|
|
|
30
32
|
description:
|
|
31
33
|
"Connect an opaque trusted local-folder handle to a canonical Files database. The browser or Desktop bridge keeps the real handle/path; Content stores only safe connection metadata.",
|
|
32
34
|
schema: z.object({
|
|
33
|
-
connectionId:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.max(300)
|
|
37
|
-
.describe("Stable opaque ID from the trusted browser/Desktop registry"),
|
|
35
|
+
connectionId: localFolderOpaqueIdentitySchema.describe(
|
|
36
|
+
"Stable opaque ID from the trusted browser/Desktop registry",
|
|
37
|
+
),
|
|
38
38
|
label: z.string().min(1).max(200).describe("User-visible folder label"),
|
|
39
|
+
connectionMetadata: localFolderSourceConnectionMetadataSchema
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(
|
|
42
|
+
"Safe repository/working-copy identity from the trusted local bridge; raw paths and handles are not accepted.",
|
|
43
|
+
),
|
|
39
44
|
spaceId: z
|
|
40
45
|
.string()
|
|
41
46
|
.optional()
|
|
@@ -70,9 +75,10 @@ export default defineAction({
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
const db = getDb();
|
|
73
|
-
const connectionId = args.connectionId
|
|
78
|
+
const connectionId = args.connectionId;
|
|
74
79
|
const label = args.label.trim();
|
|
75
80
|
const truthPolicy = args.truthPolicy as ContentDatabaseSourceTruthPolicy;
|
|
81
|
+
const connectionMetadata = args.connectionMetadata;
|
|
76
82
|
if (args.dryRun) {
|
|
77
83
|
const [existing] = await db
|
|
78
84
|
.select({
|
|
@@ -185,9 +191,18 @@ export default defineAction({
|
|
|
185
191
|
sourceTable: connectionId,
|
|
186
192
|
syncState: "linked",
|
|
187
193
|
freshness: "unknown",
|
|
188
|
-
capabilitiesJson: JSON.stringify(
|
|
194
|
+
capabilitiesJson: JSON.stringify(
|
|
195
|
+
localFolderSourceCapabilities({
|
|
196
|
+
liveBridgeEnabled: connectionMetadata?.liveBridgeEnabled,
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
189
199
|
metadataJson: JSON.stringify(
|
|
190
|
-
localFolderSourceMetadata({
|
|
200
|
+
localFolderSourceMetadata({
|
|
201
|
+
connectionId,
|
|
202
|
+
label,
|
|
203
|
+
truthPolicy,
|
|
204
|
+
connectionMetadata,
|
|
205
|
+
}),
|
|
191
206
|
),
|
|
192
207
|
lastError: null,
|
|
193
208
|
createdAt: now,
|
|
@@ -80,7 +80,7 @@ export default defineAction({
|
|
|
80
80
|
documentId: string;
|
|
81
81
|
sourceDisplayKey: string | null;
|
|
82
82
|
sourceValuesJson: string | null;
|
|
83
|
-
|
|
83
|
+
sourceTable: string;
|
|
84
84
|
};
|
|
85
85
|
const remainingLocalRows: RemainingLocalRow[] = documentIds.length
|
|
86
86
|
? await tx
|
|
@@ -90,7 +90,7 @@ export default defineAction({
|
|
|
90
90
|
schema.contentDatabaseSourceRows.sourceDisplayKey,
|
|
91
91
|
sourceValuesJson:
|
|
92
92
|
schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
93
|
-
|
|
93
|
+
sourceTable: schema.contentDatabaseSources.sourceTable,
|
|
94
94
|
})
|
|
95
95
|
.from(schema.contentDatabaseSourceRows)
|
|
96
96
|
.innerJoin(
|
|
@@ -152,7 +152,7 @@ export default defineAction({
|
|
|
152
152
|
sourceMode: "local-files",
|
|
153
153
|
sourceKind: "file",
|
|
154
154
|
sourcePath: relativePath,
|
|
155
|
-
sourceRootPath: remaining.
|
|
155
|
+
sourceRootPath: remaining.sourceTable,
|
|
156
156
|
sourceUpdatedAt: now,
|
|
157
157
|
updatedAt: now,
|
|
158
158
|
})
|
|
@@ -227,7 +227,7 @@ export default defineAction({
|
|
|
227
227
|
.select({
|
|
228
228
|
sourceDisplayKey: schema.contentDatabaseSourceRows.sourceDisplayKey,
|
|
229
229
|
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
230
|
-
|
|
230
|
+
sourceTable: schema.contentDatabaseSources.sourceTable,
|
|
231
231
|
})
|
|
232
232
|
.from(schema.contentDatabaseSourceRows)
|
|
233
233
|
.innerJoin(
|
|
@@ -267,7 +267,7 @@ export default defineAction({
|
|
|
267
267
|
sourceMode: "local-files",
|
|
268
268
|
sourceKind: "file",
|
|
269
269
|
sourcePath: remainingPath ?? null,
|
|
270
|
-
sourceRootPath: remainingLocalRow.
|
|
270
|
+
sourceRootPath: remainingLocalRow.sourceTable,
|
|
271
271
|
sourceUpdatedAt: now,
|
|
272
272
|
updatedAt: now,
|
|
273
273
|
}
|
|
@@ -19,7 +19,13 @@ import { ensureDocumentsFilesMembership } from "./_content-files.js";
|
|
|
19
19
|
import { resolveContentSpaceAccess } from "./_content-space-access.js";
|
|
20
20
|
import { provisionContentSpaces } from "./_content-spaces.js";
|
|
21
21
|
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
LOCAL_FOLDER_SOURCE_TYPE,
|
|
24
|
+
localFolderObservedRevision,
|
|
25
|
+
localFolderSourceFileIdentity,
|
|
26
|
+
localFolderSourceIdentityFromMetadata,
|
|
27
|
+
normalizeLocalFolderSourceIdentity,
|
|
28
|
+
} from "./_local-folder-source.js";
|
|
23
29
|
|
|
24
30
|
const MAX_SOURCE_FILES = 500;
|
|
25
31
|
const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
|
|
@@ -75,13 +81,28 @@ function sourceValues(args: {
|
|
|
75
81
|
title: string;
|
|
76
82
|
hash: string;
|
|
77
83
|
metadataHash: string;
|
|
84
|
+
workingCopyId: string;
|
|
85
|
+
bridgeFileIdentity?: string;
|
|
78
86
|
}) {
|
|
87
|
+
const observedRevision = localFolderObservedRevision({
|
|
88
|
+
contentHash: args.hash,
|
|
89
|
+
metadataHash: args.metadataHash,
|
|
90
|
+
});
|
|
79
91
|
return JSON.stringify({
|
|
80
92
|
relativePath: args.path,
|
|
81
93
|
extension: args.path.toLowerCase().endsWith(".mdx") ? ".mdx" : ".md",
|
|
82
94
|
title: args.title,
|
|
83
95
|
contentHash: args.hash,
|
|
84
96
|
metadataHash: args.metadataHash,
|
|
97
|
+
observedRevision,
|
|
98
|
+
sourceFileIdentity: localFolderSourceFileIdentity({
|
|
99
|
+
workingCopyId: args.workingCopyId,
|
|
100
|
+
relativePath: args.path,
|
|
101
|
+
observedRevision,
|
|
102
|
+
}),
|
|
103
|
+
...(args.bridgeFileIdentity
|
|
104
|
+
? { bridgeFileIdentity: args.bridgeFileIdentity }
|
|
105
|
+
: {}),
|
|
85
106
|
});
|
|
86
107
|
}
|
|
87
108
|
|
|
@@ -142,9 +163,19 @@ export default defineAction({
|
|
|
142
163
|
.refine((value) => Object.keys(value).length <= MAX_SOURCE_FILES, {
|
|
143
164
|
message: `Sync is limited to ${MAX_SOURCE_FILES} files.`,
|
|
144
165
|
}),
|
|
166
|
+
fileIdentities: z.record(z.string(), z.string().min(1).max(256)).optional(),
|
|
167
|
+
observedRevisions: z
|
|
168
|
+
.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/i))
|
|
169
|
+
.optional(),
|
|
145
170
|
dryRun: z.boolean().optional().default(false),
|
|
146
171
|
}),
|
|
147
|
-
run: async ({
|
|
172
|
+
run: async ({
|
|
173
|
+
sourceId,
|
|
174
|
+
files,
|
|
175
|
+
fileIdentities = {},
|
|
176
|
+
observedRevisions,
|
|
177
|
+
dryRun,
|
|
178
|
+
}) => {
|
|
148
179
|
const userEmail = getRequestUserEmail();
|
|
149
180
|
if (!userEmail) throw new Error("no authenticated user");
|
|
150
181
|
const builderPaths = Object.keys(files).filter(isBuilderMdxSourcePath);
|
|
@@ -154,6 +185,15 @@ export default defineAction({
|
|
|
154
185
|
);
|
|
155
186
|
}
|
|
156
187
|
const entries = normalizedEntries(files);
|
|
188
|
+
if (observedRevisions) {
|
|
189
|
+
for (const [filePath, content] of entries) {
|
|
190
|
+
if (observedRevisions[filePath] !== contentHash(content)) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Local file revision changed while reading "${filePath}"`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
157
197
|
const db = getDb();
|
|
158
198
|
const [target] = await db
|
|
159
199
|
.select({
|
|
@@ -263,6 +303,12 @@ export default defineAction({
|
|
|
263
303
|
|
|
264
304
|
const metadata = parseJson(target.source.metadataJson);
|
|
265
305
|
const policy = truthPolicy(metadata.truthPolicy);
|
|
306
|
+
const localIdentity =
|
|
307
|
+
localFolderSourceIdentityFromMetadata(metadata.localIdentity) ??
|
|
308
|
+
normalizeLocalFolderSourceIdentity({
|
|
309
|
+
connectionId: target.source.sourceTable,
|
|
310
|
+
label: target.source.sourceName,
|
|
311
|
+
});
|
|
266
312
|
const now = new Date().toISOString();
|
|
267
313
|
const created: Array<{ id: string; path: string; title: string }> = [];
|
|
268
314
|
const updated: Array<{ id: string; path: string; title: string }> = [];
|
|
@@ -271,16 +317,42 @@ export default defineAction({
|
|
|
271
317
|
const conflicts: Array<{ id: string; path: string; title: string }> = [];
|
|
272
318
|
const outbound: Array<{ id: string; path: string; title: string }> = [];
|
|
273
319
|
|
|
274
|
-
const buildPlans = (snapshot: Awaited<ReturnType<typeof loadSnapshot>>) =>
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
const buildPlans = (snapshot: Awaited<ReturnType<typeof loadSnapshot>>) => {
|
|
321
|
+
const rowByBridgeIdentity = new Map<
|
|
322
|
+
string,
|
|
323
|
+
(typeof snapshot.storedRows)[number]
|
|
324
|
+
>();
|
|
325
|
+
for (const row of snapshot.storedRows) {
|
|
326
|
+
const values = parseJson(row.sourceValuesJson);
|
|
327
|
+
if (typeof values.bridgeFileIdentity === "string") {
|
|
328
|
+
rowByBridgeIdentity.set(values.bridgeFileIdentity, row);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return valid.map((file, index) => {
|
|
333
|
+
const directPathRow = snapshot.rowByPath.get(file.path);
|
|
334
|
+
const incomingBridgeFileIdentity = fileIdentities[file.path];
|
|
335
|
+
const identityRow = incomingBridgeFileIdentity
|
|
336
|
+
? rowByBridgeIdentity.get(incomingBridgeFileIdentity)
|
|
337
|
+
: undefined;
|
|
338
|
+
const incomingHash = contentHash(file.content);
|
|
339
|
+
const pathRow = directPathRow ?? identityRow;
|
|
340
|
+
const explicitId =
|
|
341
|
+
file.id && localIdentity.workingCopy.kind === "temporary"
|
|
342
|
+
? opaqueId("content_local_file", `${sourceId}:${file.id}`)
|
|
343
|
+
: file.id;
|
|
277
344
|
const id =
|
|
278
|
-
|
|
345
|
+
explicitId ??
|
|
279
346
|
pathRow?.documentId ??
|
|
280
347
|
opaqueId("content_local_file", `${sourceId}:${file.path}`);
|
|
281
348
|
const existing = snapshot.documentById.get(id);
|
|
282
349
|
const previousRow = snapshot.rowByDocumentId.get(id) ?? pathRow;
|
|
283
350
|
const previousValues = parseJson(previousRow?.sourceValuesJson);
|
|
351
|
+
const bridgeFileIdentity =
|
|
352
|
+
incomingBridgeFileIdentity ??
|
|
353
|
+
(typeof previousValues.bridgeFileIdentity === "string"
|
|
354
|
+
? previousValues.bridgeFileIdentity
|
|
355
|
+
: undefined);
|
|
284
356
|
const previousHash =
|
|
285
357
|
typeof previousValues.contentHash === "string"
|
|
286
358
|
? previousValues.contentHash
|
|
@@ -289,7 +361,6 @@ export default defineAction({
|
|
|
289
361
|
typeof previousValues.metadataHash === "string"
|
|
290
362
|
? previousValues.metadataHash
|
|
291
363
|
: null;
|
|
292
|
-
const incomingHash = contentHash(file.content);
|
|
293
364
|
const incomingMetadataHash = metadataHash(file);
|
|
294
365
|
const localHash = existing ? contentHash(existing.content) : null;
|
|
295
366
|
const localMetadataHash = existing ? metadataHash(existing) : null;
|
|
@@ -332,8 +403,10 @@ export default defineAction({
|
|
|
332
403
|
conflict,
|
|
333
404
|
keepContent,
|
|
334
405
|
applyIncoming,
|
|
406
|
+
bridgeFileIdentity,
|
|
335
407
|
};
|
|
336
408
|
});
|
|
409
|
+
};
|
|
337
410
|
|
|
338
411
|
let plans = buildPlans(initialSnapshot);
|
|
339
412
|
const initialDocumentIds = new Set(plans.map((plan) => plan.id));
|
|
@@ -348,7 +421,7 @@ export default defineAction({
|
|
|
348
421
|
unchanged.length = 0;
|
|
349
422
|
conflicts.length = 0;
|
|
350
423
|
outbound.length = 0;
|
|
351
|
-
for (const row of missingRows) {
|
|
424
|
+
for (const row of policy === "source_primary" ? [] : missingRows) {
|
|
352
425
|
const values = parseJson(row.sourceValuesJson);
|
|
353
426
|
const path = String(values.relativePath ?? row.sourceDisplayKey ?? "");
|
|
354
427
|
const document = snapshot.documentById.get(row.documentId);
|
|
@@ -375,7 +448,8 @@ export default defineAction({
|
|
|
375
448
|
} else if (
|
|
376
449
|
plan.applyIncoming ||
|
|
377
450
|
plan.existing.title !== plan.file.title ||
|
|
378
|
-
plan.existing.sourcePath !== plan.file.path
|
|
451
|
+
plan.existing.sourcePath !== plan.file.path ||
|
|
452
|
+
plan.existing.sourceRootPath !== target.source.sourceTable
|
|
379
453
|
) {
|
|
380
454
|
updated.push({
|
|
381
455
|
id: plan.id,
|
|
@@ -518,7 +592,7 @@ export default defineAction({
|
|
|
518
592
|
sourceMode: "local-files",
|
|
519
593
|
sourceKind: "file",
|
|
520
594
|
sourcePath: plan.file.path,
|
|
521
|
-
sourceRootPath: target.source.
|
|
595
|
+
sourceRootPath: target.source.sourceTable,
|
|
522
596
|
sourceUpdatedAt: now,
|
|
523
597
|
visibility: target.database.orgId ? "org" : "private",
|
|
524
598
|
createdAt: now,
|
|
@@ -526,7 +600,8 @@ export default defineAction({
|
|
|
526
600
|
});
|
|
527
601
|
} else if (
|
|
528
602
|
plan.applyIncoming ||
|
|
529
|
-
plan.existing.sourcePath !== plan.file.path
|
|
603
|
+
plan.existing.sourcePath !== plan.file.path ||
|
|
604
|
+
plan.existing.sourceRootPath !== target.source.sourceTable
|
|
530
605
|
) {
|
|
531
606
|
await tx
|
|
532
607
|
.insert(schema.documentVersions)
|
|
@@ -557,7 +632,7 @@ export default defineAction({
|
|
|
557
632
|
sourceMode: "local-files",
|
|
558
633
|
sourceKind: "file",
|
|
559
634
|
sourcePath: plan.file.path,
|
|
560
|
-
sourceRootPath: target.source.
|
|
635
|
+
sourceRootPath: target.source.sourceTable,
|
|
561
636
|
sourceUpdatedAt: now,
|
|
562
637
|
updatedAt: now,
|
|
563
638
|
})
|
|
@@ -647,6 +722,8 @@ export default defineAction({
|
|
|
647
722
|
title: plan.file.title,
|
|
648
723
|
hash: plan.incomingHash,
|
|
649
724
|
metadataHash: plan.incomingMetadataHash,
|
|
725
|
+
workingCopyId: localIdentity.workingCopy.id,
|
|
726
|
+
bridgeFileIdentity: plan.bridgeFileIdentity,
|
|
650
727
|
}),
|
|
651
728
|
provenance: "trusted local-folder bridge",
|
|
652
729
|
syncState: "linked",
|
|
@@ -670,6 +747,47 @@ export default defineAction({
|
|
|
670
747
|
.where(eq(schema.contentDatabaseSourceRows.id, rowId));
|
|
671
748
|
}
|
|
672
749
|
for (const row of missingRows) {
|
|
750
|
+
if (policy === "source_primary") {
|
|
751
|
+
await tx
|
|
752
|
+
.delete(schema.contentDatabaseSourceRows)
|
|
753
|
+
.where(eq(schema.contentDatabaseSourceRows.id, row.id));
|
|
754
|
+
const [remainingSourceRow] = await tx
|
|
755
|
+
.select({ id: schema.contentDatabaseSourceRows.id })
|
|
756
|
+
.from(schema.contentDatabaseSourceRows)
|
|
757
|
+
.where(
|
|
758
|
+
eq(
|
|
759
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
760
|
+
row.databaseItemId,
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
.limit(1);
|
|
764
|
+
if (!remainingSourceRow) {
|
|
765
|
+
await tx
|
|
766
|
+
.delete(schema.contentDatabaseItems)
|
|
767
|
+
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
768
|
+
await tx
|
|
769
|
+
.update(schema.documents)
|
|
770
|
+
.set({
|
|
771
|
+
sourceMode: null,
|
|
772
|
+
sourceKind: null,
|
|
773
|
+
sourcePath: null,
|
|
774
|
+
sourceRootPath: null,
|
|
775
|
+
sourceUpdatedAt: now,
|
|
776
|
+
updatedAt: now,
|
|
777
|
+
})
|
|
778
|
+
.where(
|
|
779
|
+
and(
|
|
780
|
+
eq(schema.documents.id, row.documentId),
|
|
781
|
+
eq(schema.documents.spaceId, targetSpaceId),
|
|
782
|
+
eq(
|
|
783
|
+
schema.documents.sourceRootPath,
|
|
784
|
+
target.source.sourceTable,
|
|
785
|
+
),
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
673
791
|
const values = parseJson(row.sourceValuesJson);
|
|
674
792
|
const path = String(
|
|
675
793
|
values.relativePath ?? row.sourceDisplayKey ?? row.sourceRowId,
|