@whatcha/cli 0.0.0-stage → 0.1.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 (43) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +184 -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 +97 -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 +103 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/diaries.js +41 -0
  12. package/dist/commands/diaries.js.map +1 -0
  13. package/dist/commands/login.js +217 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/profiles.js +176 -0
  16. package/dist/commands/profiles.js.map +1 -0
  17. package/dist/commands/read.js +77 -0
  18. package/dist/commands/read.js.map +1 -0
  19. package/dist/commands/write.js +75 -0
  20. package/dist/commands/write.js.map +1 -0
  21. package/dist/config.js +47 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/errors.js +46 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/http-client.js +86 -0
  26. package/dist/http-client.js.map +1 -0
  27. package/dist/index.js +16 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/lib/append-entry.js +141 -0
  30. package/dist/lib/append-entry.js.map +1 -0
  31. package/dist/lib/diary-resolve.js +50 -0
  32. package/dist/lib/diary-resolve.js.map +1 -0
  33. package/dist/lib/entry-date.js +76 -0
  34. package/dist/lib/entry-date.js.map +1 -0
  35. package/dist/lib/entry-find.js +39 -0
  36. package/dist/lib/entry-find.js.map +1 -0
  37. package/dist/lib/jwt.js +25 -0
  38. package/dist/lib/jwt.js.map +1 -0
  39. package/dist/lib/output.js +29 -0
  40. package/dist/lib/output.js.map +1 -0
  41. package/dist/types.js +6 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +46 -4
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeEntry = writeEntry;
4
+ const errors_1 = require("../errors");
5
+ function existingEntryIdFrom(err) {
6
+ if (err.status !== 409 || err.code !== "VERSION_CONFLICT")
7
+ return null;
8
+ const details = err.details;
9
+ return typeof details?.existingEntryId === "string" ? details.existingEntryId : null;
10
+ }
11
+ function headerOf(entry) {
12
+ return {
13
+ title: entry.title,
14
+ mood: entry.mood,
15
+ location: entry.location,
16
+ tags: entry.tags,
17
+ coverMediaAssetId: entry.coverMediaAssetId,
18
+ };
19
+ }
20
+ /**
21
+ * What the stored body should read as after appending `text`, mirroring the
22
+ * API's own `appendedBody` (apps/api/src/modules/diaries/entries/
23
+ * entries-content.ts, merged to master in #1424): existing text, trimmed,
24
+ * then a blank line, then the new text -- or just the new text if there was
25
+ * none yet.
26
+ */
27
+ function expectedAppendedBody(existingBody, text) {
28
+ const trimmed = existingBody.trimEnd();
29
+ return trimmed ? `${trimmed}\n\n${text}` : text;
30
+ }
31
+ /**
32
+ * The one thing this whole command exists to get right: add `text` to
33
+ * `diaryId`'s entry for `occurredAt`'s day without ever touching what
34
+ * somebody else already wrote there.
35
+ *
36
+ * Three paths, in order:
37
+ *
38
+ * 1. No entry exists yet for that day -- `POST /entries` creates it with
39
+ * exactly `text` as its body. Nothing to append to, nothing at risk.
40
+ * 2. An entry exists (`POST` answers 409 with `details.existingEntryId`).
41
+ * Read it (`GET /entries/{id}`) to get its current header fields --
42
+ * title/mood/location/tags/cover are full-replace-on-save same as the
43
+ * body (entries-content.ts's `contentToItems`), so sending `content`
44
+ * without them would silently wipe whatever the other person set, even
45
+ * though `appendBody` protects the body itself. Then `PATCH` with
46
+ * `appendBody: true` and only the *new* text as `content.body` -- the
47
+ * server appends it to whatever is currently stored, in the same
48
+ * transaction that checks `expectedHeadCommitId`, so this is safe even
49
+ * against a write that lands between our GET and our PATCH.
50
+ * 3. The PATCH in (2) answers 422 VALIDATION_ERROR citing `appendBody` as
51
+ * an unrecognized key (ApiError#isUnrecognizedAppendBodyFlag) -- the
52
+ * server does not have the #1422 fix. Build the appended body ourselves
53
+ * from the text we just read in (2) and PATCH again without the flag,
54
+ * still carrying `expectedHeadCommitId` so a concurrent write is caught
55
+ * as a conflict rather than silently overwritten. This is the read
56
+ * that "fall back to read-then-append" refers to; note it is the *same*
57
+ * read as (2), not a second one -- appendBody is tried first precisely
58
+ * so the common case does not pay for a read it does not need.
59
+ *
60
+ * Either way, nothing is ever PATCHed as a plain replacement of a body this
61
+ * function did not just read.
62
+ */
63
+ async function writeEntry(client, diaryId, occurredAt, text) {
64
+ try {
65
+ const created = await client.post("/entries", {
66
+ diaryId,
67
+ occurredAt: occurredAt.toISOString(),
68
+ content: { body: text },
69
+ });
70
+ return { entryId: created.id, headCommitId: created.headCommitId, mode: "created", verified: null };
71
+ }
72
+ catch (err) {
73
+ if (!(err instanceof errors_1.ApiError))
74
+ throw err;
75
+ const existingEntryId = existingEntryIdFrom(err);
76
+ if (!existingEntryId)
77
+ throw err;
78
+ return appendToExisting(client, existingEntryId, text);
79
+ }
80
+ }
81
+ async function appendToExisting(client, entryId, text) {
82
+ const existing = await client.get(`/entries/${entryId}`);
83
+ const header = headerOf(existing);
84
+ try {
85
+ const saved = await client.patch(`/entries/${entryId}`, {
86
+ content: { body: text, ...header },
87
+ appendBody: true,
88
+ expectedHeadCommitId: existing.headCommitId,
89
+ });
90
+ const verified = await verifyAppend(client, entryId, existing.body, text);
91
+ return { entryId, headCommitId: saved.headCommitId, mode: "appended", verified };
92
+ }
93
+ catch (err) {
94
+ if (!(err instanceof errors_1.ApiError))
95
+ throw err;
96
+ if (err.status === 409)
97
+ throw concurrentWriteError(err);
98
+ if (!err.isUnrecognizedAppendBodyFlag())
99
+ throw err;
100
+ // Fallback: this server does not know appendBody. Merge client-side
101
+ // from the body we already read, and send the merged body as a plain
102
+ // replacement -- which is safe precisely because we just read what it
103
+ // is replacing.
104
+ const mergedBody = expectedAppendedBody(existing.body, text);
105
+ const saved = await client
106
+ .patch(`/entries/${entryId}`, {
107
+ content: { body: mergedBody, ...header },
108
+ expectedHeadCommitId: existing.headCommitId,
109
+ })
110
+ .catch((fallbackErr) => {
111
+ if (fallbackErr instanceof errors_1.ApiError && fallbackErr.status === 409)
112
+ throw concurrentWriteError(fallbackErr);
113
+ throw fallbackErr;
114
+ });
115
+ const verified = await verifyAppend(client, entryId, existing.body, text);
116
+ return { entryId, headCommitId: saved.headCommitId, mode: "appended-fallback", verified };
117
+ }
118
+ }
119
+ function concurrentWriteError(err) {
120
+ return new errors_1.CliUsageError("Someone else saved to this entry while whatcha was writing to it. Nothing was overwritten -- " +
121
+ `run \`whatcha write\` again to retry. (${err.message})`);
122
+ }
123
+ /**
124
+ * Defense in depth against the exact failure #1422 was about, for the one
125
+ * case none of the above can rule out: a server old enough that it neither
126
+ * knows `appendBody` NOR rejects the unrecognized key (STRICT_REQUEST_BODIES
127
+ * off, which is the API's own default -- apps/api/src/config/index.ts). Such
128
+ * a server answers 200 having silently replaced the body instead of
129
+ * appending, and there is no error to catch.
130
+ *
131
+ * So: read the entry back and check the stored body actually starts with
132
+ * what was there before. This cannot repair a loss it finds -- the PATCH
133
+ * already committed -- but it turns a silent data loss into a loud one, and
134
+ * `whatcha write --json` surfaces `verified: false` so an agent calling
135
+ * this does not mistake a corrupted write for a clean one.
136
+ */
137
+ async function verifyAppend(client, entryId, bodyBeforeWrite, text) {
138
+ const after = await client.get(`/entries/${entryId}`);
139
+ return after.body === expectedAppendedBody(bodyBeforeWrite, text);
140
+ }
141
+ //# 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,76 @@
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 readInZone = new Date(asIfUtc.toLocaleString("en-US", { timeZone }));
57
+ const diff = asIfUtc.getTime() - readInZone.getTime();
58
+ return new Date(asIfUtc.getTime() + diff);
59
+ }
60
+ /**
61
+ * Resolves `--date`/`--time` into the `occurredAt` instant to send the API,
62
+ * defaulting both to "now" in the diary's own timezone -- not the machine's,
63
+ * since §11.4's one-entry-per-day check is computed in the diary's timezone
64
+ * (entries-create-service.ts) and a mismatch is exactly the kind of thing
65
+ * that would land a `--date 2026-09-25` entry on the 24th or 26th instead.
66
+ */
67
+ function resolveOccurredAt(opts, timeZone, now = new Date()) {
68
+ if (opts.date)
69
+ assertValidDate(opts.date);
70
+ if (opts.time)
71
+ assertValidTime(opts.time);
72
+ const date = opts.date ?? dateKeyInTimeZone(now, timeZone);
73
+ const time = opts.time ?? timeKeyInTimeZone(now, timeZone);
74
+ return zonedTimeToUtc(date, time, timeZone);
75
+ }
76
+ //# 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"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeJwtPayloadUnsafe = decodeJwtPayloadUnsafe;
4
+ /**
5
+ * Reads the payload of a JWT without verifying its signature -- fine here
6
+ * because we never trust this for authorization (the server does that on
7
+ * every request), only to show a human "signed in as ...". Used for the
8
+ * `--token`/`WHATCHA_TOKEN` escape hatch (issue #1428): `POST /auth/refresh`
9
+ * returns only `accessToken`/`expiresIn`/`refreshToken`, no user object, so
10
+ * this is the only way to get an email/id to display after that path.
11
+ */
12
+ function decodeJwtPayloadUnsafe(token) {
13
+ const parts = token.split(".");
14
+ if (parts.length !== 3)
15
+ return null;
16
+ try {
17
+ const json = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
18
+ const parsed = JSON.parse(json);
19
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../src/lib/jwt.ts"],"names":[],"mappings":";;AAQA,wDAUC;AAlBD;;;;;;;GAOG;AACH,SAAgB,sBAAsB,CAAC,KAAa;IAClD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpG,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAE,MAAkC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printJson = printJson;
4
+ exports.printLine = printLine;
5
+ exports.printErrorLine = printErrorLine;
6
+ /**
7
+ * The `--json` contract (see README.md): every command prints exactly one
8
+ * JSON object to stdout on success, `{"ok": true, ...command-specific
9
+ * fields}`, and `{"ok": false, "error": {"code", "message"}}` on failure --
10
+ * always to stdout, even for an error, so a script doing `whatcha ... --json
11
+ * | jq` gets parseable JSON either way instead of having to also capture
12
+ * stderr. Human-mode output goes to stdout on success and stderr on
13
+ * failure, and is not meant to be parsed by anything.
14
+ *
15
+ * Never pass an access/refresh token or a password through this -- see
16
+ * README.md's "--json contract" section. Nothing in src/commands builds a
17
+ * JSON payload from a StoredSession or a LoginResult directly; they pick
18
+ * the fields by hand.
19
+ */
20
+ function printJson(value) {
21
+ process.stdout.write(JSON.stringify(value) + "\n");
22
+ }
23
+ function printLine(text) {
24
+ process.stdout.write(text + "\n");
25
+ }
26
+ function printErrorLine(text) {
27
+ process.stderr.write(text + "\n");
28
+ }
29
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":";;AAcA,8BAEC;AAED,8BAEC;AAED,wCAEC;AAxBD;;;;;;;;;;;;;GAaG;AACH,SAAgB,SAAS,CAAC,KAA8B;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,SAAgB,SAAS,CAAC,IAAY;IACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,SAAgB,cAAc,CAAC,IAAY;IACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACpC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ // Wire shapes this CLI actually touches, trimmed from apps/api/openapi.yaml.
3
+ // Not the whole API -- just the fields the four commands read or send. Keep
4
+ // these in sync with openapi.yaml by hand; there is no codegen step here.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,0EAA0E"}
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@whatcha/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Official command-line client for what-are-you-do-ing (whatareyoudo.ing) -- login, list diaries, write and read entries.",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://whatareyoudo.ing/cli",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitea.lovekapibarasan.org/LoveKapibarasan/what-are-you-do-ing.git",
10
+ "directory": "apps/cli"
11
+ },
12
+ "bugs": {
13
+ "url": "https://gitea.lovekapibarasan.org/LoveKapibarasan/what-are-you-do-ing/issues"
14
+ },
15
+ "keywords": [
16
+ "whatcha",
17
+ "diary",
18
+ "journal",
19
+ "cli",
20
+ "whatareyoudo"
21
+ ],
22
+ "bin": {
23
+ "whatcha": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "node node_modules/typescript/bin/tsc",
38
+ "typecheck": "node node_modules/typescript/bin/tsc --noEmit",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "typescript": "^5.5.2",
46
+ "vitest": "^4.1.10"
47
+ }
48
+ }