@clankmates/cli 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +4 -3
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +3 -196
- package/src/commands/channel/render.ts +224 -0
- package/src/commands/channel/tokens.ts +145 -0
- package/src/commands/channel/validation.ts +11 -0
- package/src/commands/channel.ts +11 -340
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +12 -371
- package/src/commands/feed.ts +19 -178
- package/src/commands/inbox/content.ts +31 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +69 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +59 -0
- package/src/commands/inbox/thread-output.ts +344 -0
- package/src/commands/inbox/watch.ts +203 -0
- package/src/commands/inbox.ts +58 -1220
- package/src/commands/post.ts +24 -116
- package/src/lib/args.ts +8 -0
- package/src/lib/cache/scopes.ts +216 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +18 -382
- package/src/lib/client/auth.ts +122 -0
- package/src/lib/client/channel-keys.ts +57 -0
- package/src/lib/client/channels.ts +364 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +76 -0
- package/src/lib/client/inbox.ts +361 -0
- package/src/lib/client/posts.ts +213 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +197 -894
- package/src/lib/help.ts +66 -9
- package/src/lib/json_api.ts +74 -9
- package/src/lib/pagination.ts +5 -0
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +55 -0
- package/src/types/api.ts +1 -0
package/src/commands/post.ts
CHANGED
|
@@ -4,12 +4,8 @@ import {
|
|
|
4
4
|
cacheFlags,
|
|
5
5
|
cacheResult,
|
|
6
6
|
ownedPostsScope,
|
|
7
|
-
prepareCachePlan,
|
|
8
7
|
publicPostsScope,
|
|
9
|
-
saveCacheTimestamp,
|
|
10
8
|
sharedPostsScope,
|
|
11
|
-
type CachePlan,
|
|
12
|
-
type CacheResult,
|
|
13
9
|
type CacheScope,
|
|
14
10
|
} from "../lib/cache";
|
|
15
11
|
import {
|
|
@@ -28,11 +24,16 @@ import {
|
|
|
28
24
|
renderFields,
|
|
29
25
|
formatTimestamp,
|
|
30
26
|
joinBlocks,
|
|
31
|
-
renderPagination,
|
|
32
27
|
} from "../lib/human";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
28
|
+
import { printValue, type Io } from "../lib/output";
|
|
29
|
+
import {
|
|
30
|
+
maybePrepareCachePlan,
|
|
31
|
+
maybeSaveCacheTimestamp,
|
|
32
|
+
parseLatestFirstOrder,
|
|
33
|
+
resolvedSince,
|
|
34
|
+
} from "../lib/polling";
|
|
35
|
+
import { printPostCollection } from "../lib/post-output";
|
|
36
|
+
import type { PostAttributes, ShareTokenResponse } from "../types/api";
|
|
36
37
|
|
|
37
38
|
export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
38
39
|
const subcommand = args.positionals[0];
|
|
@@ -74,6 +75,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
74
75
|
channelId,
|
|
75
76
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
76
77
|
cursor: stringFlag(args.flags, "cursor"),
|
|
78
|
+
before: stringFlag(args.flags, "before"),
|
|
77
79
|
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
78
80
|
since: resolvedSince(args, cachePlan),
|
|
79
81
|
});
|
|
@@ -119,6 +121,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
119
121
|
channelName,
|
|
120
122
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
121
123
|
cursor: stringFlag(args.flags, "cursor"),
|
|
124
|
+
before: stringFlag(args.flags, "before"),
|
|
122
125
|
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
123
126
|
since: resolvedSince(args, cachePlan),
|
|
124
127
|
});
|
|
@@ -149,6 +152,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
149
152
|
token,
|
|
150
153
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
151
154
|
cursor: stringFlag(args.flags, "cursor"),
|
|
155
|
+
before: stringFlag(args.flags, "before"),
|
|
152
156
|
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
153
157
|
since: resolvedSince(args, cachePlan),
|
|
154
158
|
});
|
|
@@ -296,88 +300,6 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
302
|
|
|
299
|
-
function printPostCollection(
|
|
300
|
-
args: ParsedArgs,
|
|
301
|
-
outputMode: "json" | "table",
|
|
302
|
-
io: Io,
|
|
303
|
-
response: {
|
|
304
|
-
items: Array<{ id: string; attributes: PostAttributes }>;
|
|
305
|
-
nextCursor?: string;
|
|
306
|
-
meta?: Record<string, unknown>;
|
|
307
|
-
},
|
|
308
|
-
cache?: CacheResult,
|
|
309
|
-
): void {
|
|
310
|
-
if (outputMode === "json") {
|
|
311
|
-
printJson(
|
|
312
|
-
io,
|
|
313
|
-
paginatedJson(args, {
|
|
314
|
-
items: response.items,
|
|
315
|
-
nextCursor: response.nextCursor,
|
|
316
|
-
meta: response.meta,
|
|
317
|
-
...(cache ? { cache } : {}),
|
|
318
|
-
}),
|
|
319
|
-
);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
printValue(
|
|
324
|
-
io,
|
|
325
|
-
outputMode,
|
|
326
|
-
response.items.map((item) => ({
|
|
327
|
-
id: item.id,
|
|
328
|
-
source: item.attributes.source,
|
|
329
|
-
date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
|
|
330
|
-
body: item.attributes.body,
|
|
331
|
-
})),
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
const pagination = paginationInfo(args, response.nextCursor);
|
|
335
|
-
const message = renderPagination(
|
|
336
|
-
pagination?.nextCursor,
|
|
337
|
-
pagination?.nextCommand,
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
if (message) {
|
|
341
|
-
io.stdout(message);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
printCacheNote(io, cache);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
|
|
348
|
-
if (!value) {
|
|
349
|
-
return undefined;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (value === "latest" || value === "oldest") {
|
|
353
|
-
return value;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function maybePrepareCachePlan(
|
|
360
|
-
args: ParsedArgs,
|
|
361
|
-
context: Awaited<ReturnType<typeof createCommandContext>>,
|
|
362
|
-
scope: CacheScope | undefined,
|
|
363
|
-
): Promise<CachePlan | undefined> {
|
|
364
|
-
return cacheFlags(args).sinceCache && scope
|
|
365
|
-
? prepareCachePlan(context, scope)
|
|
366
|
-
: undefined;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function maybeSaveCacheTimestamp(
|
|
370
|
-
args: ParsedArgs,
|
|
371
|
-
context: Awaited<ReturnType<typeof createCommandContext>>,
|
|
372
|
-
scope: CacheScope | undefined,
|
|
373
|
-
meta: Record<string, unknown> | undefined,
|
|
374
|
-
shouldSave: boolean,
|
|
375
|
-
): Promise<string | undefined> {
|
|
376
|
-
return cacheFlags(args).saveCache && scope && shouldSave
|
|
377
|
-
? saveCacheTimestamp(context, scope, meta)
|
|
378
|
-
: undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
303
|
async function maybeOwnedPostsScope(
|
|
382
304
|
args: ParsedArgs,
|
|
383
305
|
context: Awaited<ReturnType<typeof createCommandContext>>,
|
|
@@ -391,6 +313,7 @@ async function maybeOwnedPostsScope(
|
|
|
391
313
|
context,
|
|
392
314
|
actorKey: await authenticatedActorKey(context),
|
|
393
315
|
channelId,
|
|
316
|
+
before: stringFlag(args.flags, "before"),
|
|
394
317
|
});
|
|
395
318
|
}
|
|
396
319
|
|
|
@@ -404,7 +327,12 @@ function maybePublicPostsScope(
|
|
|
404
327
|
return undefined;
|
|
405
328
|
}
|
|
406
329
|
|
|
407
|
-
return publicPostsScope({
|
|
330
|
+
return publicPostsScope({
|
|
331
|
+
context,
|
|
332
|
+
publicHandle,
|
|
333
|
+
channelName,
|
|
334
|
+
before: stringFlag(args.flags, "before"),
|
|
335
|
+
});
|
|
408
336
|
}
|
|
409
337
|
|
|
410
338
|
function maybeSharedPostsScope(
|
|
@@ -416,31 +344,11 @@ function maybeSharedPostsScope(
|
|
|
416
344
|
return undefined;
|
|
417
345
|
}
|
|
418
346
|
|
|
419
|
-
return sharedPostsScope({
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
cachePlan: CachePlan | undefined,
|
|
425
|
-
): string | undefined {
|
|
426
|
-
return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function printCacheNote(io: Io, cache: CacheResult | undefined): void {
|
|
430
|
-
if (!cache) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const details = [
|
|
435
|
-
cache.previousServerTimestamp
|
|
436
|
-
? `used ${cache.previousServerTimestamp}`
|
|
437
|
-
: cache.hit
|
|
438
|
-
? "used cache"
|
|
439
|
-
: "no cached timestamp",
|
|
440
|
-
cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
|
|
441
|
-
].filter(Boolean);
|
|
442
|
-
|
|
443
|
-
io.stdout(`Cache: ${details.join("; ")}.`);
|
|
347
|
+
return sharedPostsScope({
|
|
348
|
+
context,
|
|
349
|
+
shareToken,
|
|
350
|
+
before: stringFlag(args.flags, "before"),
|
|
351
|
+
});
|
|
444
352
|
}
|
|
445
353
|
|
|
446
354
|
function renderPostDetail(
|
package/src/lib/args.ts
CHANGED
|
@@ -49,14 +49,22 @@ const CLI_OPTIONS = {
|
|
|
49
49
|
"schema-stdin": { type: "boolean" },
|
|
50
50
|
limit: { type: "string" },
|
|
51
51
|
cursor: { type: "string" },
|
|
52
|
+
before: { type: "string" },
|
|
52
53
|
order: { type: "string" },
|
|
53
54
|
since: { type: "string" },
|
|
54
55
|
sinceCache: { type: "boolean" },
|
|
55
56
|
"since-cache": { type: "boolean" },
|
|
56
57
|
saveCache: { type: "boolean" },
|
|
57
58
|
"save-cache": { type: "boolean" },
|
|
59
|
+
once: { type: "boolean" },
|
|
58
60
|
status: { type: "string" },
|
|
59
61
|
mailbox: { type: "string" },
|
|
62
|
+
participant: { type: "string" },
|
|
63
|
+
participantScope: { type: "string" },
|
|
64
|
+
"participant-scope": { type: "string" },
|
|
65
|
+
query: { type: "string" },
|
|
66
|
+
hasAttachment: { type: "boolean" },
|
|
67
|
+
"has-attachment": { type: "boolean" },
|
|
60
68
|
} as const;
|
|
61
69
|
|
|
62
70
|
type ParsedValue = string | boolean;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { CommandContext } from "../context";
|
|
4
|
+
import type { WhoamiActor } from "../../types/api";
|
|
5
|
+
import type { CacheScope } from "./types";
|
|
6
|
+
|
|
7
|
+
const PUBLIC_ACTOR_KEY = "public";
|
|
8
|
+
const SHARED_ACTOR_KEY = "shared";
|
|
9
|
+
|
|
10
|
+
export function actorKey(actor: WhoamiActor): string {
|
|
11
|
+
if (actor.type === "user") {
|
|
12
|
+
return `user:${actor.id}:${actor.scope ?? "master"}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `channel:${actor.id}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function publicActorKey(): string {
|
|
19
|
+
return PUBLIC_ACTOR_KEY;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sharedActorKey(): string {
|
|
23
|
+
return SHARED_ACTOR_KEY;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function feedMyScope(input: {
|
|
27
|
+
context: CommandContext;
|
|
28
|
+
actorKey: string;
|
|
29
|
+
channelId?: string;
|
|
30
|
+
before?: string;
|
|
31
|
+
}): CacheScope {
|
|
32
|
+
const channel = input.channelId ?? "all";
|
|
33
|
+
const before = input.before ?? "default";
|
|
34
|
+
return scoped({
|
|
35
|
+
context: input.context,
|
|
36
|
+
actorKey: input.actorKey,
|
|
37
|
+
resource: "feed:my",
|
|
38
|
+
parts: ["feed", "my", channel, before],
|
|
39
|
+
params: { channel, before },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function feedSearchScope(input: {
|
|
44
|
+
context: CommandContext;
|
|
45
|
+
actorKey: string;
|
|
46
|
+
query: string;
|
|
47
|
+
channelId?: string;
|
|
48
|
+
before?: string;
|
|
49
|
+
}): CacheScope {
|
|
50
|
+
const channel = input.channelId ?? "all";
|
|
51
|
+
const before = input.before ?? "default";
|
|
52
|
+
return scoped({
|
|
53
|
+
context: input.context,
|
|
54
|
+
actorKey: input.actorKey,
|
|
55
|
+
resource: "feed:search",
|
|
56
|
+
parts: ["feed", "search", input.query, channel, before],
|
|
57
|
+
params: { query: input.query, channel, before },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function inboxThreadsScope(input: {
|
|
62
|
+
context: CommandContext;
|
|
63
|
+
actorKey: string;
|
|
64
|
+
status?: string;
|
|
65
|
+
mailbox?: string;
|
|
66
|
+
participant?: string;
|
|
67
|
+
participantScope?: string;
|
|
68
|
+
query?: string;
|
|
69
|
+
hasAttachment?: boolean;
|
|
70
|
+
before?: string;
|
|
71
|
+
}): CacheScope {
|
|
72
|
+
const status = input.status ?? "default";
|
|
73
|
+
const mailbox = input.mailbox ?? "default";
|
|
74
|
+
const participant = input.participant ?? "default";
|
|
75
|
+
const participantScope = input.participantScope ?? "default";
|
|
76
|
+
const query = input.query ?? "default";
|
|
77
|
+
const hasAttachment = input.hasAttachment ?? false;
|
|
78
|
+
const before = input.before ?? "default";
|
|
79
|
+
return scoped({
|
|
80
|
+
context: input.context,
|
|
81
|
+
actorKey: input.actorKey,
|
|
82
|
+
resource: "inbox:threads",
|
|
83
|
+
parts: [
|
|
84
|
+
"inbox",
|
|
85
|
+
"threads",
|
|
86
|
+
status,
|
|
87
|
+
mailbox,
|
|
88
|
+
participant,
|
|
89
|
+
participantScope,
|
|
90
|
+
query,
|
|
91
|
+
String(hasAttachment),
|
|
92
|
+
before,
|
|
93
|
+
input.actorKey,
|
|
94
|
+
],
|
|
95
|
+
params: {
|
|
96
|
+
status,
|
|
97
|
+
mailbox,
|
|
98
|
+
participant,
|
|
99
|
+
participantScope,
|
|
100
|
+
query,
|
|
101
|
+
hasAttachment,
|
|
102
|
+
before,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function inboxMessagesScope(input: {
|
|
108
|
+
context: CommandContext;
|
|
109
|
+
actorKey: string;
|
|
110
|
+
threadId: string;
|
|
111
|
+
query?: string;
|
|
112
|
+
hasAttachment?: boolean;
|
|
113
|
+
before?: string;
|
|
114
|
+
}): CacheScope {
|
|
115
|
+
const query = input.query ?? "default";
|
|
116
|
+
const hasAttachment = input.hasAttachment ?? false;
|
|
117
|
+
const before = input.before ?? "default";
|
|
118
|
+
return scoped({
|
|
119
|
+
context: input.context,
|
|
120
|
+
actorKey: input.actorKey,
|
|
121
|
+
resource: "inbox:messages",
|
|
122
|
+
parts: [
|
|
123
|
+
"inbox",
|
|
124
|
+
"messages",
|
|
125
|
+
input.threadId,
|
|
126
|
+
query,
|
|
127
|
+
String(hasAttachment),
|
|
128
|
+
before,
|
|
129
|
+
input.actorKey,
|
|
130
|
+
],
|
|
131
|
+
params: { threadId: input.threadId, query, hasAttachment, before },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ownedPostsScope(input: {
|
|
136
|
+
context: CommandContext;
|
|
137
|
+
actorKey: string;
|
|
138
|
+
channelId: string;
|
|
139
|
+
before?: string;
|
|
140
|
+
}): CacheScope {
|
|
141
|
+
const before = input.before ?? "default";
|
|
142
|
+
return scoped({
|
|
143
|
+
context: input.context,
|
|
144
|
+
actorKey: input.actorKey,
|
|
145
|
+
resource: "posts:owned",
|
|
146
|
+
parts: ["posts", "owned", input.channelId, before],
|
|
147
|
+
params: { channelId: input.channelId, before },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function publicPostsScope(input: {
|
|
152
|
+
context: CommandContext;
|
|
153
|
+
publicHandle: string;
|
|
154
|
+
channelName: string;
|
|
155
|
+
before?: string;
|
|
156
|
+
}): CacheScope {
|
|
157
|
+
const before = input.before ?? "default";
|
|
158
|
+
return scoped({
|
|
159
|
+
context: input.context,
|
|
160
|
+
actorKey: PUBLIC_ACTOR_KEY,
|
|
161
|
+
resource: "posts:public",
|
|
162
|
+
parts: ["posts", "public", input.publicHandle, input.channelName, before],
|
|
163
|
+
params: {
|
|
164
|
+
publicHandle: input.publicHandle,
|
|
165
|
+
channelName: input.channelName,
|
|
166
|
+
before,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function sharedPostsScope(input: {
|
|
172
|
+
context: CommandContext;
|
|
173
|
+
shareToken: string;
|
|
174
|
+
before?: string;
|
|
175
|
+
}): CacheScope {
|
|
176
|
+
const shareTokenHash = hashValue(input.shareToken);
|
|
177
|
+
const before = input.before ?? "default";
|
|
178
|
+
return scoped({
|
|
179
|
+
context: input.context,
|
|
180
|
+
actorKey: SHARED_ACTOR_KEY,
|
|
181
|
+
resource: "posts:shared",
|
|
182
|
+
parts: ["posts", "shared", shareTokenHash, before],
|
|
183
|
+
params: { shareTokenHash, before },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function hashValue(value: string): string {
|
|
188
|
+
return createHash("sha256").update(value).digest("hex");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function scoped(input: {
|
|
192
|
+
context: CommandContext;
|
|
193
|
+
actorKey: string;
|
|
194
|
+
resource: string;
|
|
195
|
+
parts: string[];
|
|
196
|
+
params: Record<string, unknown>;
|
|
197
|
+
}): CacheScope {
|
|
198
|
+
const prefix = [
|
|
199
|
+
"v1",
|
|
200
|
+
normalizePart(input.context.profile.baseUrl),
|
|
201
|
+
normalizePart(input.context.profileName),
|
|
202
|
+
normalizePart(input.actorKey),
|
|
203
|
+
];
|
|
204
|
+
const scopeKey = [...prefix, ...input.parts.map(normalizePart)].join(":");
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
scopeKey,
|
|
208
|
+
resource: input.resource,
|
|
209
|
+
params: input.params,
|
|
210
|
+
actorKey: input.actorKey,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizePart(value: string): string {
|
|
215
|
+
return encodeURIComponent(value);
|
|
216
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getCachePath } from "../paths";
|
|
6
|
+
import { CLI_VERSION } from "../version";
|
|
7
|
+
import type { CacheScope, SyncScopeRow } from "./types";
|
|
8
|
+
|
|
9
|
+
const MIGRATION_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
export class SyncCache {
|
|
12
|
+
private readonly db: Database;
|
|
13
|
+
|
|
14
|
+
constructor(private readonly dbPath = getCachePath()) {
|
|
15
|
+
this.db = new Database(dbPath, { create: true });
|
|
16
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
17
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
18
|
+
this.migrate();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
path(): string {
|
|
22
|
+
return this.dbPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get(scopeKey: string): SyncScopeRow | undefined {
|
|
26
|
+
return this.db
|
|
27
|
+
.query<SyncScopeRow, [string]>(
|
|
28
|
+
`select scope_key, base_url, profile, actor_key, resource, params_json,
|
|
29
|
+
server_timestamp, cached_at, cli_version
|
|
30
|
+
from sync_scopes
|
|
31
|
+
where scope_key = ?`,
|
|
32
|
+
)
|
|
33
|
+
.get(scopeKey) ?? undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
list(input: { baseUrl?: string; profile?: string } = {}): SyncScopeRow[] {
|
|
37
|
+
if (input.baseUrl && input.profile) {
|
|
38
|
+
return this.db
|
|
39
|
+
.query<SyncScopeRow, [string, string]>(
|
|
40
|
+
`select scope_key, base_url, profile, actor_key, resource, params_json,
|
|
41
|
+
server_timestamp, cached_at, cli_version
|
|
42
|
+
from sync_scopes
|
|
43
|
+
where base_url = ? and profile = ?
|
|
44
|
+
order by resource, scope_key`,
|
|
45
|
+
)
|
|
46
|
+
.all(input.baseUrl, input.profile);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.db
|
|
50
|
+
.query<SyncScopeRow, []>(
|
|
51
|
+
`select scope_key, base_url, profile, actor_key, resource, params_json,
|
|
52
|
+
server_timestamp, cached_at, cli_version
|
|
53
|
+
from sync_scopes
|
|
54
|
+
order by base_url, profile, resource, scope_key`,
|
|
55
|
+
)
|
|
56
|
+
.all();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
upsert(input: {
|
|
60
|
+
scope: CacheScope;
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
profile: string;
|
|
63
|
+
serverTimestamp: string;
|
|
64
|
+
}): void {
|
|
65
|
+
this.db
|
|
66
|
+
.query<
|
|
67
|
+
unknown,
|
|
68
|
+
[string, string, string, string, string, string, string, string, string]
|
|
69
|
+
>(
|
|
70
|
+
`insert into sync_scopes (
|
|
71
|
+
scope_key, base_url, profile, actor_key, resource, params_json,
|
|
72
|
+
server_timestamp, cached_at, cli_version
|
|
73
|
+
)
|
|
74
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
on conflict(scope_key) do update set
|
|
76
|
+
base_url = excluded.base_url,
|
|
77
|
+
profile = excluded.profile,
|
|
78
|
+
actor_key = excluded.actor_key,
|
|
79
|
+
resource = excluded.resource,
|
|
80
|
+
params_json = excluded.params_json,
|
|
81
|
+
server_timestamp = excluded.server_timestamp,
|
|
82
|
+
cached_at = excluded.cached_at,
|
|
83
|
+
cli_version = excluded.cli_version`,
|
|
84
|
+
)
|
|
85
|
+
.run(
|
|
86
|
+
input.scope.scopeKey,
|
|
87
|
+
input.baseUrl,
|
|
88
|
+
input.profile,
|
|
89
|
+
input.scope.actorKey,
|
|
90
|
+
input.scope.resource,
|
|
91
|
+
stableJson(input.scope.params),
|
|
92
|
+
input.serverTimestamp,
|
|
93
|
+
new Date().toISOString(),
|
|
94
|
+
CLI_VERSION,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clear(input: { scopeKey?: string; baseUrl?: string; profile?: string } = {}): number {
|
|
99
|
+
if (input.scopeKey) {
|
|
100
|
+
const result = this.db
|
|
101
|
+
.query<unknown, [string]>("delete from sync_scopes where scope_key = ?")
|
|
102
|
+
.run(input.scopeKey);
|
|
103
|
+
return result.changes;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (input.baseUrl && input.profile) {
|
|
107
|
+
const result = this.db
|
|
108
|
+
.query<unknown, [string, string]>(
|
|
109
|
+
"delete from sync_scopes where base_url = ? and profile = ?",
|
|
110
|
+
)
|
|
111
|
+
.run(input.baseUrl, input.profile);
|
|
112
|
+
return result.changes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = this.db.query<unknown, []>("delete from sync_scopes").run();
|
|
116
|
+
return result.changes;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
close(): void {
|
|
120
|
+
this.db.close();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private migrate(): void {
|
|
124
|
+
this.db.exec(`
|
|
125
|
+
create table if not exists schema_migrations (
|
|
126
|
+
version integer primary key,
|
|
127
|
+
applied_at text not null
|
|
128
|
+
);
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
const applied = this.db
|
|
132
|
+
.query<{ version: number }, [number]>(
|
|
133
|
+
"select version from schema_migrations where version = ?",
|
|
134
|
+
)
|
|
135
|
+
.get(MIGRATION_VERSION);
|
|
136
|
+
|
|
137
|
+
if (applied) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.db.exec(`
|
|
142
|
+
create table if not exists sync_scopes (
|
|
143
|
+
scope_key text primary key,
|
|
144
|
+
base_url text not null,
|
|
145
|
+
profile text not null,
|
|
146
|
+
actor_key text not null,
|
|
147
|
+
resource text not null,
|
|
148
|
+
params_json text not null,
|
|
149
|
+
server_timestamp text,
|
|
150
|
+
cached_at text not null,
|
|
151
|
+
cli_version text not null
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
create index if not exists sync_scopes_profile_idx
|
|
155
|
+
on sync_scopes(base_url, profile);
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
this.db
|
|
159
|
+
.query<unknown, [number, string]>(
|
|
160
|
+
"insert into schema_migrations(version, applied_at) values (?, ?)",
|
|
161
|
+
)
|
|
162
|
+
.run(MIGRATION_VERSION, new Date().toISOString());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function ensureCacheParentDirectory(
|
|
167
|
+
cachePath = getCachePath(),
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function openSyncCache(cachePath = getCachePath()): Promise<SyncCache> {
|
|
173
|
+
await ensureCacheParentDirectory(cachePath);
|
|
174
|
+
return new SyncCache(cachePath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stableJson(value: Record<string, unknown>): string {
|
|
178
|
+
return JSON.stringify(sortObject(value));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sortObject(value: unknown): unknown {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
return value.map(sortObject);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof value !== "object" || value === null) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return Object.fromEntries(
|
|
191
|
+
Object.entries(value as Record<string, unknown>)
|
|
192
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
193
|
+
.map(([key, entry]) => [key, sortObject(entry)]),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SyncScopeRow {
|
|
2
|
+
scope_key: string;
|
|
3
|
+
base_url: string;
|
|
4
|
+
profile: string;
|
|
5
|
+
actor_key: string;
|
|
6
|
+
resource: string;
|
|
7
|
+
params_json: string;
|
|
8
|
+
server_timestamp?: string | null;
|
|
9
|
+
cached_at: string;
|
|
10
|
+
cli_version: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CacheScope {
|
|
14
|
+
scopeKey: string;
|
|
15
|
+
resource: string;
|
|
16
|
+
params: Record<string, unknown>;
|
|
17
|
+
actorKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CachePlan {
|
|
21
|
+
scope: CacheScope;
|
|
22
|
+
previousServerTimestamp?: string;
|
|
23
|
+
hit: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CacheResult {
|
|
27
|
+
scopeKey: string;
|
|
28
|
+
hit: boolean;
|
|
29
|
+
previousServerTimestamp?: string;
|
|
30
|
+
savedServerTimestamp?: string;
|
|
31
|
+
}
|