@whatcha/cli 0.0.0-stage → 0.3.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.
Files changed (59) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +345 -2
  3. package/dist/auth/gotrue-client.js +64 -0
  4. package/dist/auth/gotrue-client.js.map +1 -0
  5. package/dist/auth/session.js +147 -0
  6. package/dist/auth/session.js.map +1 -0
  7. package/dist/auth/token-store.js +165 -0
  8. package/dist/auth/token-store.js.map +1 -0
  9. package/dist/cli.js +134 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/diaries.js +52 -0
  12. package/dist/commands/diaries.js.map +1 -0
  13. package/dist/commands/login.js +292 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/profiles.js +287 -0
  16. package/dist/commands/profiles.js.map +1 -0
  17. package/dist/commands/publish.js +81 -0
  18. package/dist/commands/publish.js.map +1 -0
  19. package/dist/commands/read.js +77 -0
  20. package/dist/commands/read.js.map +1 -0
  21. package/dist/commands/shared-diary.js +255 -0
  22. package/dist/commands/shared-diary.js.map +1 -0
  23. package/dist/commands/whoami.js +50 -0
  24. package/dist/commands/whoami.js.map +1 -0
  25. package/dist/commands/wiki.js +386 -0
  26. package/dist/commands/wiki.js.map +1 -0
  27. package/dist/commands/write.js +106 -0
  28. package/dist/commands/write.js.map +1 -0
  29. package/dist/config.js +47 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/errors.js +46 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/http-client.js +102 -0
  34. package/dist/http-client.js.map +1 -0
  35. package/dist/index.js +16 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib/append-entry.js +193 -0
  38. package/dist/lib/append-entry.js.map +1 -0
  39. package/dist/lib/diary-resolve.js +50 -0
  40. package/dist/lib/diary-resolve.js.map +1 -0
  41. package/dist/lib/entry-date.js +96 -0
  42. package/dist/lib/entry-date.js.map +1 -0
  43. package/dist/lib/entry-find.js +39 -0
  44. package/dist/lib/entry-find.js.map +1 -0
  45. package/dist/lib/image-upload.js +126 -0
  46. package/dist/lib/image-upload.js.map +1 -0
  47. package/dist/lib/jwt.js +25 -0
  48. package/dist/lib/jwt.js.map +1 -0
  49. package/dist/lib/output.js +29 -0
  50. package/dist/lib/output.js.map +1 -0
  51. package/dist/lib/personal-access-token.js +46 -0
  52. package/dist/lib/personal-access-token.js.map +1 -0
  53. package/dist/lib/profile-family.js +29 -0
  54. package/dist/lib/profile-family.js.map +1 -0
  55. package/dist/lib/publication.js +55 -0
  56. package/dist/lib/publication.js.map +1 -0
  57. package/dist/types.js +6 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +46 -4
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchWhatchaClient = void 0;
4
+ const errors_1 = require("./errors");
5
+ function isErrorEnvelope(json) {
6
+ return typeof json === "object" && json !== null && "error" in json;
7
+ }
8
+ /**
9
+ * Real implementation, over Node 22's built-in fetch. `authHeader` is asked
10
+ * fresh on every call rather than captured once, so a token refreshed
11
+ * mid-command (see auth/session.ts) is picked up without rebuilding the
12
+ * client.
13
+ */
14
+ class FetchWhatchaClient {
15
+ baseUrl;
16
+ authHeader;
17
+ constructor(baseUrl, authHeader) {
18
+ this.baseUrl = baseUrl;
19
+ this.authHeader = authHeader;
20
+ }
21
+ get(path, query) {
22
+ return this.send("GET", path + this.queryString(query), undefined);
23
+ }
24
+ post(path, body) {
25
+ return this.send("POST", path, body);
26
+ }
27
+ put(path, body) {
28
+ return this.send("PUT", path, body);
29
+ }
30
+ patch(path, body) {
31
+ return this.send("PATCH", path, body);
32
+ }
33
+ delete(path) {
34
+ return this.send("DELETE", path, undefined);
35
+ }
36
+ async getPage(path, query) {
37
+ const qs = this.queryString(query);
38
+ const env = await this.sendRaw("GET", path + qs, undefined);
39
+ return { items: env.data, nextCursor: env.pagination.nextCursor, hasNext: env.pagination.hasNext };
40
+ }
41
+ async putBytes(url, headers, bytes) {
42
+ let res;
43
+ try {
44
+ res = await fetch(url, { method: "PUT", headers, body: bytes });
45
+ }
46
+ catch (err) {
47
+ throw new errors_1.ApiError(0, "NETWORK_ERROR", `Could not upload to ${new URL(url).host}: ${err.message}`);
48
+ }
49
+ if (!res.ok) {
50
+ await res.text().catch(() => undefined);
51
+ throw new errors_1.ApiError(res.status, "UPLOAD_FAILED", `Uploading the file failed with status ${res.status}.`);
52
+ }
53
+ }
54
+ queryString(query) {
55
+ if (!query)
56
+ return "";
57
+ const entries = Object.entries(query).filter((e) => e[1] !== undefined);
58
+ if (entries.length === 0)
59
+ return "";
60
+ return "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
61
+ }
62
+ async send(method, path, body) {
63
+ const env = await this.sendRaw(method, path, body);
64
+ return env === undefined ? undefined : env.data;
65
+ }
66
+ /** Parses the response and returns the whole JSON body, unwrapped from neither `data` nor `pagination` -- callers decide the shape. */
67
+ async sendRaw(method, path, body) {
68
+ const headers = { Accept: "application/json" };
69
+ if (body !== undefined)
70
+ headers["Content-Type"] = "application/json";
71
+ const auth = await this.authHeader();
72
+ if (auth)
73
+ headers.Authorization = auth;
74
+ let res;
75
+ try {
76
+ res = await fetch(this.baseUrl + path, {
77
+ method,
78
+ headers,
79
+ body: body !== undefined ? JSON.stringify(body) : undefined,
80
+ });
81
+ }
82
+ catch (err) {
83
+ // DNS failure, connection refused, TLS error -- never seen a status
84
+ // code at all. Wrapped so commands can print one clear line instead
85
+ // of a raw TypeError("fetch failed").
86
+ throw new errors_1.ApiError(0, "NETWORK_ERROR", `Could not reach ${this.baseUrl}: ${err.message}`);
87
+ }
88
+ if (res.status === 204)
89
+ return undefined;
90
+ const text = await res.text();
91
+ const json = text ? JSON.parse(text) : undefined;
92
+ if (!res.ok) {
93
+ if (isErrorEnvelope(json)) {
94
+ throw new errors_1.ApiError(res.status, json.error.code, json.error.message, json.error.details);
95
+ }
96
+ throw new errors_1.ApiError(res.status, "HTTP_ERROR", `${method} ${path} failed with status ${res.status}`);
97
+ }
98
+ return json;
99
+ }
100
+ }
101
+ exports.FetchWhatchaClient = FetchWhatchaClient;
102
+ //# sourceMappingURL=http-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const cli_1 = require("./cli");
5
+ (0, cli_1.run)(process.argv.slice(2))
6
+ .then((code) => {
7
+ process.exitCode = code;
8
+ })
9
+ .catch((err) => {
10
+ // run() catches everything it knows how to report -- reaching here
11
+ // means something threw before or outside that try/catch (a bug), so
12
+ // this is the last resort, not the normal error path.
13
+ process.stderr.write(`whatcha: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
14
+ process.exitCode = 1;
15
+ });
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,+BAA4B;AAE5B,IAAA,SAAG,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACvB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,mEAAmE;IACnE,qEAAqE;IACrE,sDAAsD;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtH,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_TAGS = exports.MAX_TAG_LENGTH = exports.MAX_TITLE_LENGTH = void 0;
4
+ exports.normalizeTags = normalizeTags;
5
+ exports.mergeHeader = mergeHeader;
6
+ exports.writeEntry = writeEntry;
7
+ const errors_1 = require("../errors");
8
+ /** The API's own limits (entries-content.ts `contentSchema`). */
9
+ exports.MAX_TITLE_LENGTH = 200;
10
+ exports.MAX_TAG_LENGTH = 64;
11
+ exports.MAX_TAGS = 32;
12
+ function existingEntryIdFrom(err) {
13
+ if (err.status !== 409 || err.code !== "VERSION_CONFLICT")
14
+ return null;
15
+ const details = err.details;
16
+ return typeof details?.existingEntryId === "string" ? details.existingEntryId : null;
17
+ }
18
+ function headerOf(entry) {
19
+ return {
20
+ title: entry.title,
21
+ mood: entry.mood,
22
+ location: entry.location,
23
+ // Only sent back when the API sent them: an older server that does not
24
+ // know the fields must not be handed keys it would drop or refuse.
25
+ ...(entry.locationLat !== undefined ? { locationLat: entry.locationLat } : {}),
26
+ ...(entry.locationLng !== undefined ? { locationLng: entry.locationLng } : {}),
27
+ tags: entry.tags,
28
+ coverMediaAssetId: entry.coverMediaAssetId,
29
+ };
30
+ }
31
+ /** Tags trimmed, empties dropped, exact duplicates removed -- the app's own rule (entry editor's _addTag). */
32
+ function normalizeTags(tags) {
33
+ const out = [];
34
+ for (const raw of tags) {
35
+ const tag = raw.trim();
36
+ if (tag && !out.includes(tag))
37
+ out.push(tag);
38
+ }
39
+ return out;
40
+ }
41
+ /**
42
+ * The header an append saves: the entry's own, with `meta` merged in under
43
+ * the append rule. Exported for the tests.
44
+ */
45
+ function mergeHeader(header, meta) {
46
+ const wanted = meta.title?.trim();
47
+ const hasTitle = Boolean(header.title && header.title.trim());
48
+ const title = hasTitle ? header.title : wanted ? wanted : header.title;
49
+ const tags = normalizeTags([...(header.tags ?? []), ...(meta.tags ?? [])]);
50
+ // Kept as the entry had them when nothing new was asked for -- a merge
51
+ // that only re-normalised somebody else's tags would still be a change.
52
+ const finalTags = meta.tags && meta.tags.length > 0 ? tags : header.tags;
53
+ if (finalTags.length > exports.MAX_TAGS) {
54
+ throw new errors_1.CliUsageError(`This entry would end up with ${finalTags.length} tags; the limit is ${exports.MAX_TAGS}. Nothing was written.`);
55
+ }
56
+ return {
57
+ header: { ...header, title, tags: finalTags },
58
+ titleKept: Boolean(wanted && hasTitle && header.title.trim() !== wanted),
59
+ };
60
+ }
61
+ /**
62
+ * What the stored body should read as after appending `text`, mirroring the
63
+ * API's own `appendedBody` (apps/api/src/modules/diaries/entries/
64
+ * entries-content.ts, merged to master in #1424): existing text, trimmed,
65
+ * then a blank line, then the new text -- or just the new text if there was
66
+ * none yet.
67
+ */
68
+ function expectedAppendedBody(existingBody, text) {
69
+ const trimmed = existingBody.trimEnd();
70
+ return trimmed ? `${trimmed}\n\n${text}` : text;
71
+ }
72
+ /**
73
+ * The one thing this whole command exists to get right: add `text` to
74
+ * `diaryId`'s entry for `occurredAt`'s day without ever touching what
75
+ * somebody else already wrote there.
76
+ *
77
+ * Three paths, in order:
78
+ *
79
+ * 1. No entry exists yet for that day -- `POST /entries` creates it with
80
+ * exactly `text` as its body. Nothing to append to, nothing at risk.
81
+ * 2. An entry exists (`POST` answers 409 with `details.existingEntryId`).
82
+ * Read it (`GET /entries/{id}`) to get its current header fields --
83
+ * title/mood/location/tags/cover are full-replace-on-save same as the
84
+ * body (entries-content.ts's `contentToItems`), so sending `content`
85
+ * without them would silently wipe whatever the other person set, even
86
+ * though `appendBody` protects the body itself. Then `PATCH` with
87
+ * `appendBody: true` and only the *new* text as `content.body` -- the
88
+ * server appends it to whatever is currently stored, in the same
89
+ * transaction that checks `expectedHeadCommitId`, so this is safe even
90
+ * against a write that lands between our GET and our PATCH.
91
+ * 3. The PATCH in (2) answers 422 VALIDATION_ERROR citing `appendBody` as
92
+ * an unrecognized key (ApiError#isUnrecognizedAppendBodyFlag) -- the
93
+ * server does not have the #1422 fix. Build the appended body ourselves
94
+ * from the text we just read in (2) and PATCH again without the flag,
95
+ * still carrying `expectedHeadCommitId` so a concurrent write is caught
96
+ * as a conflict rather than silently overwritten. This is the read
97
+ * that "fall back to read-then-append" refers to; note it is the *same*
98
+ * read as (2), not a second one -- appendBody is tried first precisely
99
+ * so the common case does not pay for a read it does not need.
100
+ *
101
+ * Either way, nothing is ever PATCHed as a plain replacement of a body this
102
+ * function did not just read.
103
+ */
104
+ async function writeEntry(client, diaryId, occurredAt, text, meta = {}) {
105
+ const title = meta.title?.trim() || undefined;
106
+ const tags = normalizeTags(meta.tags ?? []);
107
+ try {
108
+ const created = await client.post("/entries", {
109
+ diaryId,
110
+ occurredAt: occurredAt.toISOString(),
111
+ content: { body: text, ...(title ? { title } : {}), ...(tags.length > 0 ? { tags } : {}) },
112
+ });
113
+ return {
114
+ entryId: created.id,
115
+ headCommitId: created.headCommitId,
116
+ mode: "created",
117
+ verified: null,
118
+ title: title ?? null,
119
+ tags,
120
+ titleKept: false,
121
+ };
122
+ }
123
+ catch (err) {
124
+ if (!(err instanceof errors_1.ApiError))
125
+ throw err;
126
+ const existingEntryId = existingEntryIdFrom(err);
127
+ if (!existingEntryId)
128
+ throw err;
129
+ return appendToExisting(client, existingEntryId, text, { title, tags });
130
+ }
131
+ }
132
+ async function appendToExisting(client, entryId, text, meta) {
133
+ const existing = await client.get(`/entries/${entryId}`);
134
+ const { header, titleKept } = mergeHeader(headerOf(existing), meta);
135
+ const after = { title: header.title, tags: header.tags, titleKept };
136
+ try {
137
+ const saved = await client.patch(`/entries/${entryId}`, {
138
+ content: { body: text, ...header },
139
+ appendBody: true,
140
+ expectedHeadCommitId: existing.headCommitId,
141
+ });
142
+ const verified = await verifyAppend(client, entryId, existing.body, text);
143
+ return { entryId, headCommitId: saved.headCommitId, mode: "appended", verified, ...after };
144
+ }
145
+ catch (err) {
146
+ if (!(err instanceof errors_1.ApiError))
147
+ throw err;
148
+ if (err.status === 409)
149
+ throw concurrentWriteError(err);
150
+ if (!err.isUnrecognizedAppendBodyFlag())
151
+ throw err;
152
+ // Fallback: this server does not know appendBody. Merge client-side
153
+ // from the body we already read, and send the merged body as a plain
154
+ // replacement -- which is safe precisely because we just read what it
155
+ // is replacing.
156
+ const mergedBody = expectedAppendedBody(existing.body, text);
157
+ const saved = await client
158
+ .patch(`/entries/${entryId}`, {
159
+ content: { body: mergedBody, ...header },
160
+ expectedHeadCommitId: existing.headCommitId,
161
+ })
162
+ .catch((fallbackErr) => {
163
+ if (fallbackErr instanceof errors_1.ApiError && fallbackErr.status === 409)
164
+ throw concurrentWriteError(fallbackErr);
165
+ throw fallbackErr;
166
+ });
167
+ const verified = await verifyAppend(client, entryId, existing.body, text);
168
+ return { entryId, headCommitId: saved.headCommitId, mode: "appended-fallback", verified, ...after };
169
+ }
170
+ }
171
+ function concurrentWriteError(err) {
172
+ return new errors_1.CliUsageError("Someone else saved to this entry while whatcha was writing to it. Nothing was overwritten -- " +
173
+ `run \`whatcha write\` again to retry. (${err.message})`);
174
+ }
175
+ /**
176
+ * Defense in depth against the exact failure #1422 was about, for the one
177
+ * case none of the above can rule out: a server old enough that it neither
178
+ * knows `appendBody` NOR rejects the unrecognized key (STRICT_REQUEST_BODIES
179
+ * off, which is the API's own default -- apps/api/src/config/index.ts). Such
180
+ * a server answers 200 having silently replaced the body instead of
181
+ * appending, and there is no error to catch.
182
+ *
183
+ * So: read the entry back and check the stored body actually starts with
184
+ * what was there before. This cannot repair a loss it finds -- the PATCH
185
+ * already committed -- but it turns a silent data loss into a loud one, and
186
+ * `whatcha write --json` surfaces `verified: false` so an agent calling
187
+ * this does not mistake a corrupted write for a clean one.
188
+ */
189
+ async function verifyAppend(client, entryId, bodyBeforeWrite, text) {
190
+ const after = await client.get(`/entries/${entryId}`);
191
+ return after.body === expectedAppendedBody(bodyBeforeWrite, text);
192
+ }
193
+ //# sourceMappingURL=append-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append-entry.js","sourceRoot":"","sources":["../../src/lib/append-entry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDiary = resolveDiary;
4
+ exports.listAllDiaries = listAllDiaries;
5
+ const errors_1 = require("../errors");
6
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ /**
8
+ * `<diary>` on the command line -- a diary id, or a name to look up against
9
+ * `GET /diaries` (list, not search: the API also offers `?q=` substring
10
+ * search, but that is for the app's search box, not for turning a name a
11
+ * person typed back into exactly the diary they meant, so this matches by
12
+ * exact name, case-insensitively, and refuses to guess when more than one
13
+ * diary shares a name).
14
+ */
15
+ async function resolveDiary(client, nameOrId) {
16
+ if (UUID_RE.test(nameOrId)) {
17
+ try {
18
+ return await client.get(`/diaries/${nameOrId}`);
19
+ }
20
+ catch (err) {
21
+ if (err instanceof errors_1.ApiError && err.status === 404) {
22
+ throw new errors_1.CliUsageError(`No diary with id ${nameOrId} (or it is not visible to you).`);
23
+ }
24
+ throw err;
25
+ }
26
+ }
27
+ const all = await listAllDiaries(client);
28
+ const matches = all.filter((d) => d.name.toLowerCase() === nameOrId.toLowerCase());
29
+ if (matches.length === 1)
30
+ return matches[0];
31
+ if (matches.length > 1) {
32
+ const ids = matches.map((d) => ` ${d.id} ${d.name} (${d.groupName})`).join("\n");
33
+ throw new errors_1.CliUsageError(`"${nameOrId}" matches more than one diary -- use the id instead:\n${ids}`);
34
+ }
35
+ const known = all.map((d) => d.name).join(", ") || "(none)";
36
+ throw new errors_1.CliUsageError(`No diary named "${nameOrId}". Diaries you can see: ${known}`);
37
+ }
38
+ async function listAllDiaries(client) {
39
+ const all = [];
40
+ let cursor;
41
+ for (;;) {
42
+ const page = await client.getPage("/diaries", { limit: "50", cursor });
43
+ all.push(...page.items);
44
+ if (!page.hasNext || !page.nextCursor)
45
+ break;
46
+ cursor = page.nextCursor;
47
+ }
48
+ return all;
49
+ }
50
+ //# sourceMappingURL=diary-resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diary-resolve.js","sourceRoot":"","sources":["../../src/lib/diary-resolve.ts"],"names":[],"mappings":";;AAcA,oCAqBC;AAED,wCAUC;AA/CD,sCAAoD;AAIpD,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF;;;;;;;GAOG;AACI,KAAK,UAAU,YAAY,CAAC,MAAqB,EAAE,QAAgB;IACxE,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,MAAM,MAAM,CAAC,GAAG,CAAQ,YAAY,QAAQ,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,iBAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAClD,MAAM,IAAI,sBAAa,CAAC,oBAAoB,QAAQ,iCAAiC,CAAC,CAAC;YACzF,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IACnF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnF,MAAM,IAAI,sBAAa,CAAC,IAAI,QAAQ,yDAAyD,GAAG,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;IAC5D,MAAM,IAAI,sBAAa,CAAC,mBAAmB,QAAQ,2BAA2B,KAAK,EAAE,CAAC,CAAC;AACzF,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,MAAqB;IACxD,MAAM,GAAG,GAAY,EAAE,CAAC;IACxB,IAAI,MAA0B,CAAC;IAC/B,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAQ,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9E,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,MAAM;QAC7C,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertValidDate = assertValidDate;
4
+ exports.assertValidTime = assertValidTime;
5
+ exports.dateKeyInTimeZone = dateKeyInTimeZone;
6
+ exports.timeKeyInTimeZone = timeKeyInTimeZone;
7
+ exports.zonedTimeToUtc = zonedTimeToUtc;
8
+ exports.resolveOccurredAt = resolveOccurredAt;
9
+ const errors_1 = require("../errors");
10
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
11
+ const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
12
+ function assertValidDate(date) {
13
+ if (!DATE_RE.test(date))
14
+ throw new errors_1.CliUsageError(`--date must be YYYY-MM-DD, got "${date}".`);
15
+ // DATE_RE only checks the shape; Date.UTC quietly rolls "2026-02-30" into
16
+ // March, so check the round-trip too rather than writing to the wrong day.
17
+ const [y, m, d] = date.split("-").map(Number);
18
+ const roundTrip = new Date(Date.UTC(y, m - 1, d));
19
+ if (roundTrip.getUTCFullYear() !== y || roundTrip.getUTCMonth() !== m - 1 || roundTrip.getUTCDate() !== d) {
20
+ throw new errors_1.CliUsageError(`--date "${date}" is not a real calendar date.`);
21
+ }
22
+ }
23
+ function assertValidTime(time) {
24
+ if (!TIME_RE.test(time))
25
+ throw new errors_1.CliUsageError(`--time must be HH:MM (24h), got "${time}".`);
26
+ }
27
+ /** "YYYY-MM-DD" for `date`, as read in `timeZone` -- what day the diary's own §11.4 uniqueness check would see. */
28
+ function dateKeyInTimeZone(date, timeZone) {
29
+ // en-CA formats as YYYY-MM-DD, which is also the wire format --
30
+ // avoids hand-assembling the string from getters that don't accept a tz.
31
+ return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
32
+ }
33
+ /** "HH:MM" for `date`, as read in `timeZone`. Used to default --time to "now" without a second copy of the format string. */
34
+ function timeKeyInTimeZone(date, timeZone) {
35
+ return new Intl.DateTimeFormat("en-GB", {
36
+ timeZone,
37
+ hour: "2-digit",
38
+ minute: "2-digit",
39
+ hourCycle: "h23",
40
+ }).format(date);
41
+ }
42
+ /**
43
+ * The inverse of the two functions above: the UTC instant that reads as
44
+ * `dateStr` `timeStr` when displayed in `timeZone`.
45
+ *
46
+ * There is no built-in for this (Intl only goes UTC -> zoned, never back),
47
+ * so this uses the standard trick: read `dateStr timeStr` as if it were
48
+ * already UTC, ask what that UTC instant looks like in `timeZone`, and shift
49
+ * by the difference. Good to the minute, which is all `--time` offers;
50
+ * degrades right at a DST transition (an hour that either repeats or does
51
+ * not exist), which is an acceptable rough edge for a diary entry's
52
+ * timestamp and not worth a timezone-database dependency for.
53
+ */
54
+ function zonedTimeToUtc(dateStr, timeStr, timeZone) {
55
+ const asIfUtc = new Date(`${dateStr}T${timeStr}:00.000Z`);
56
+ const diff = asIfUtc.getTime() - wallClockAsUtcMs(asIfUtc, timeZone);
57
+ return new Date(asIfUtc.getTime() + diff);
58
+ }
59
+ /**
60
+ * The wall-clock reading of `instant` in `timeZone`, re-read as if it were
61
+ * UTC. Assembled from formatToParts and Date.UTC rather than by parsing a
62
+ * toLocaleString() back through `new Date()` -- that parse happens in the
63
+ * MACHINE's timezone, which shifted every --date/--time by the machine's
64
+ * own UTC offset on anything not set to UTC.
65
+ */
66
+ function wallClockAsUtcMs(instant, timeZone) {
67
+ const parts = new Intl.DateTimeFormat("en-US", {
68
+ timeZone,
69
+ year: "numeric",
70
+ month: "2-digit",
71
+ day: "2-digit",
72
+ hour: "2-digit",
73
+ minute: "2-digit",
74
+ second: "2-digit",
75
+ hourCycle: "h23",
76
+ }).formatToParts(instant);
77
+ const get = (type) => Number(parts.find((p) => p.type === type)?.value);
78
+ return Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second"));
79
+ }
80
+ /**
81
+ * Resolves `--date`/`--time` into the `occurredAt` instant to send the API,
82
+ * defaulting both to "now" in the diary's own timezone -- not the machine's,
83
+ * since §11.4's one-entry-per-day check is computed in the diary's timezone
84
+ * (entries-create-service.ts) and a mismatch is exactly the kind of thing
85
+ * that would land a `--date 2026-09-25` entry on the 24th or 26th instead.
86
+ */
87
+ function resolveOccurredAt(opts, timeZone, now = new Date()) {
88
+ if (opts.date)
89
+ assertValidDate(opts.date);
90
+ if (opts.time)
91
+ assertValidTime(opts.time);
92
+ const date = opts.date ?? dateKeyInTimeZone(now, timeZone);
93
+ const time = opts.time ?? timeKeyInTimeZone(now, timeZone);
94
+ return zonedTimeToUtc(date, time, timeZone);
95
+ }
96
+ //# sourceMappingURL=entry-date.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-date.js","sourceRoot":"","sources":["../../src/lib/entry-date.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findEntryOnDate = findEntryOnDate;
4
+ const entry_date_1 = require("./entry-date");
5
+ const PAGE_LIMIT = 50;
6
+ /** Ten pages is 500 entries -- generous for "find the one entry on this diary-day" without paging forever through years of history for a typo'd date. */
7
+ const MAX_PAGES = 10;
8
+ /**
9
+ * `GET /entries` has no `?date=`, only cursor pagination newest-first
10
+ * (entries-query-service.ts's `listEntriesPage`, `orderBy: occurred_at
11
+ * desc`). Since there is at most one entry per diary-day (§11.4), walking
12
+ * that list until an entry's day (in the diary's own timezone) matches or
13
+ * we pass it is exact, not a heuristic -- it just is not O(1). Returns null
14
+ * if the diary has no entry on that day, or if it is farther back than
15
+ * MAX_PAGES cover.
16
+ */
17
+ async function findEntryOnDate(client, diaryId, timeZone, dateKey) {
18
+ let cursor;
19
+ for (let page = 0; page < MAX_PAGES; page++) {
20
+ const result = await client.getPage("/entries", {
21
+ diaryId,
22
+ limit: String(PAGE_LIMIT),
23
+ cursor,
24
+ });
25
+ for (const entry of result.items) {
26
+ const entryDateKey = (0, entry_date_1.dateKeyInTimeZone)(new Date(entry.occurredAt), timeZone);
27
+ if (entryDateKey === dateKey)
28
+ return entry;
29
+ // Newest-first: once we're past the target day, it is not coming back.
30
+ if (entryDateKey < dateKey)
31
+ return null;
32
+ }
33
+ if (!result.hasNext || !result.nextCursor)
34
+ return null;
35
+ cursor = result.nextCursor;
36
+ }
37
+ return null;
38
+ }
39
+ //# sourceMappingURL=entry-find.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-find.js","sourceRoot":"","sources":["../../src/lib/entry-find.ts"],"names":[],"mappings":";;AAiBA,0CAuBC;AAtCD,6CAAiD;AAEjD,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,yJAAyJ;AACzJ,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB;;;;;;;;GAQG;AACI,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,OAAe,EACf,QAAgB,EAChB,OAAe;IAEf,IAAI,MAA0B,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAgB,UAAU,EAAE;YAC7D,OAAO;YACP,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC;YACzB,MAAM;SACP,CAAC,CAAC;QACH,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjC,MAAM,YAAY,GAAG,IAAA,8BAAiB,EAAC,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC7E,IAAI,YAAY,KAAK,OAAO;gBAAE,OAAO,KAAK,CAAC;YAC3C,uEAAuE;YACvE,IAAI,YAAY,GAAG,OAAO;gBAAE,OAAO,IAAI,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC;QACvD,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}