@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.
- package/LICENSE +202 -0
- package/README.md +345 -2
- package/dist/auth/gotrue-client.js +64 -0
- package/dist/auth/gotrue-client.js.map +1 -0
- package/dist/auth/session.js +147 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/token-store.js +165 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/cli.js +134 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/diaries.js +52 -0
- package/dist/commands/diaries.js.map +1 -0
- package/dist/commands/login.js +292 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/profiles.js +287 -0
- package/dist/commands/profiles.js.map +1 -0
- package/dist/commands/publish.js +81 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/read.js +77 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/shared-diary.js +255 -0
- package/dist/commands/shared-diary.js.map +1 -0
- package/dist/commands/whoami.js +50 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/commands/wiki.js +386 -0
- package/dist/commands/wiki.js.map +1 -0
- package/dist/commands/write.js +106 -0
- package/dist/commands/write.js.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-client.js +102 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/append-entry.js +193 -0
- package/dist/lib/append-entry.js.map +1 -0
- package/dist/lib/diary-resolve.js +50 -0
- package/dist/lib/diary-resolve.js.map +1 -0
- package/dist/lib/entry-date.js +96 -0
- package/dist/lib/entry-date.js.map +1 -0
- package/dist/lib/entry-find.js +39 -0
- package/dist/lib/entry-find.js.map +1 -0
- package/dist/lib/image-upload.js +126 -0
- package/dist/lib/image-upload.js.map +1 -0
- package/dist/lib/jwt.js +25 -0
- package/dist/lib/jwt.js.map +1 -0
- package/dist/lib/output.js +29 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/personal-access-token.js +46 -0
- package/dist/lib/personal-access-token.js.map +1 -0
- package/dist/lib/profile-family.js +29 -0
- package/dist/lib/profile-family.js.map +1 -0
- package/dist/lib/publication.js +55 -0
- package/dist/lib/publication.js.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- 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"}
|