innernote 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.
- package/README.md +241 -0
- package/dist/index.js +1969 -0
- package/package.json +25 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1969 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
var DEFAULT_API_URL = "https://www.innernote.space";
|
|
9
|
+
var CONFIG_DIR = join(homedir(), ".innernote");
|
|
10
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
11
|
+
async function readStored() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!parsed || typeof parsed !== "object")
|
|
16
|
+
return {};
|
|
17
|
+
return parsed;
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function loadConfig() {
|
|
23
|
+
const stored = await readStored();
|
|
24
|
+
const envToken = process.env.INNERNOTE_TOKEN?.trim();
|
|
25
|
+
return {
|
|
26
|
+
token: envToken || stored.token || null,
|
|
27
|
+
apiUrl: (process.env.INNERNOTE_API_URL || stored.apiUrl || DEFAULT_API_URL).replace(/\/+$/, ""),
|
|
28
|
+
fromEnv: Boolean(envToken)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function saveToken(token, apiUrl) {
|
|
32
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
33
|
+
const body = { token, apiUrl };
|
|
34
|
+
await writeFile(CONFIG_FILE, JSON.stringify(body, null, 2) + `
|
|
35
|
+
`, {
|
|
36
|
+
mode: 384
|
|
37
|
+
});
|
|
38
|
+
await chmod(CONFIG_DIR, 448);
|
|
39
|
+
await chmod(CONFIG_FILE, 384);
|
|
40
|
+
return CONFIG_FILE;
|
|
41
|
+
}
|
|
42
|
+
async function clearToken() {
|
|
43
|
+
const stored = await readStored();
|
|
44
|
+
if (!stored.token)
|
|
45
|
+
return false;
|
|
46
|
+
await rm(CONFIG_FILE, { force: true });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
var configPath = CONFIG_FILE;
|
|
50
|
+
|
|
51
|
+
// src/client.ts
|
|
52
|
+
function isNotLoggedIn(err) {
|
|
53
|
+
return err instanceof Error && err.name === "NotLoggedIn";
|
|
54
|
+
}
|
|
55
|
+
function isApiError(err) {
|
|
56
|
+
return err instanceof Error && err.name === "ApiError";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/commands/auth.ts
|
|
60
|
+
import { hostname } from "node:os";
|
|
61
|
+
|
|
62
|
+
// src/client.ts
|
|
63
|
+
class ApiError extends Error {
|
|
64
|
+
status;
|
|
65
|
+
body;
|
|
66
|
+
constructor(message, status, body) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "ApiError";
|
|
69
|
+
this.status = status;
|
|
70
|
+
this.body = body;
|
|
71
|
+
}
|
|
72
|
+
get isRefusal() {
|
|
73
|
+
return this.status === 402 || this.status === 429;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class NotLoggedIn extends Error {
|
|
78
|
+
constructor() {
|
|
79
|
+
super("Not logged in.");
|
|
80
|
+
this.name = "NotLoggedIn";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isNotLoggedIn2(err) {
|
|
84
|
+
return err instanceof Error && err.name === "NotLoggedIn";
|
|
85
|
+
}
|
|
86
|
+
function isApiError2(err) {
|
|
87
|
+
return err instanceof Error && err.name === "ApiError";
|
|
88
|
+
}
|
|
89
|
+
async function request(path, options = {}) {
|
|
90
|
+
const config = await loadConfig();
|
|
91
|
+
return requestWith(config, path, options);
|
|
92
|
+
}
|
|
93
|
+
async function requestWith(config, path, options = {}) {
|
|
94
|
+
const { method = "GET", body, timeoutMs = 30000, anonymous = false } = options;
|
|
95
|
+
if (!anonymous && !config.token)
|
|
96
|
+
throw new NotLoggedIn;
|
|
97
|
+
const headers = { Accept: "application/json" };
|
|
98
|
+
if (!anonymous && config.token) {
|
|
99
|
+
headers.Authorization = `Bearer ${config.token}`;
|
|
100
|
+
}
|
|
101
|
+
if (body !== undefined)
|
|
102
|
+
headers["Content-Type"] = "application/json";
|
|
103
|
+
const abort = new AbortController;
|
|
104
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
105
|
+
let response;
|
|
106
|
+
try {
|
|
107
|
+
response = await fetch(`${config.apiUrl}${path}`, {
|
|
108
|
+
method,
|
|
109
|
+
headers,
|
|
110
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
111
|
+
signal: abort.signal
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
if (abort.signal.aborted) {
|
|
115
|
+
throw new Error(`That took longer than ${Math.round(timeoutMs / 1000)}s and was given up on. Try again.`);
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Could not reach ${config.apiUrl}. Check your connection.`);
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
let parsed = null;
|
|
123
|
+
try {
|
|
124
|
+
parsed = text ? JSON.parse(text) : null;
|
|
125
|
+
} catch {
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new ApiError(`The server returned an unexpected response (${response.status}).`, response.status, {});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const payload = parsed ?? {};
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
if (response.status === 401) {
|
|
133
|
+
throw new ApiError("That token is not valid any more. Run `innernote login` to reconnect.", 401, payload);
|
|
134
|
+
}
|
|
135
|
+
const message = typeof payload.message === "string" && payload.message || typeof payload.error === "string" && payload.error || `Request failed (${response.status}).`;
|
|
136
|
+
throw new ApiError(message, response.status, payload);
|
|
137
|
+
}
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/ui.ts
|
|
142
|
+
var useColor = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
143
|
+
var wrap = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
144
|
+
var dim = wrap("2");
|
|
145
|
+
var bold = wrap("1");
|
|
146
|
+
var green = wrap("32");
|
|
147
|
+
var yellow = wrap("33");
|
|
148
|
+
var red = wrap("31");
|
|
149
|
+
function out(line = "") {
|
|
150
|
+
process.stdout.write(line + `
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
function note(line = "") {
|
|
154
|
+
process.stderr.write(line + `
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
function fail(line) {
|
|
158
|
+
process.stderr.write(`${red("×")} ${line}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
function ok(line) {
|
|
162
|
+
process.stderr.write(`${green("✓")} ${line}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
function ago(ms) {
|
|
166
|
+
const seconds = Math.max(0, Math.round((Date.now() - ms) / 1000));
|
|
167
|
+
if (seconds < 60)
|
|
168
|
+
return "now";
|
|
169
|
+
const minutes = Math.round(seconds / 60);
|
|
170
|
+
if (minutes < 60)
|
|
171
|
+
return `${minutes}m`;
|
|
172
|
+
const hours = Math.round(minutes / 60);
|
|
173
|
+
if (hours < 24)
|
|
174
|
+
return `${hours}h`;
|
|
175
|
+
const days = Math.round(hours / 24);
|
|
176
|
+
if (days < 30)
|
|
177
|
+
return `${days}d`;
|
|
178
|
+
return `${Math.round(days / 30)}mo`;
|
|
179
|
+
}
|
|
180
|
+
function oneLine(text, max = 68) {
|
|
181
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
182
|
+
if (flat.length <= max)
|
|
183
|
+
return flat;
|
|
184
|
+
const cut = flat.slice(0, max);
|
|
185
|
+
const space = cut.lastIndexOf(" ");
|
|
186
|
+
return (space > max * 0.6 ? cut.slice(0, space) : cut) + "…";
|
|
187
|
+
}
|
|
188
|
+
async function readStdin() {
|
|
189
|
+
if (process.stdin.isTTY)
|
|
190
|
+
return null;
|
|
191
|
+
const chunks = [];
|
|
192
|
+
for await (const chunk of process.stdin)
|
|
193
|
+
chunks.push(chunk);
|
|
194
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
195
|
+
return text.length > 0 ? text : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/brand.ts
|
|
199
|
+
var isTTY = process.stdout.isTTY === true;
|
|
200
|
+
var wantsColor = isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
201
|
+
var truecolor = process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit";
|
|
202
|
+
function paint(r, g, b, s) {
|
|
203
|
+
if (!wantsColor)
|
|
204
|
+
return s;
|
|
205
|
+
if (truecolor)
|
|
206
|
+
return `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
|
|
207
|
+
const q = (v) => Math.round(v / 255 * 5);
|
|
208
|
+
return `\x1B[38;5;${16 + 36 * q(r) + 6 * q(g) + q(b)}m${s}\x1B[0m`;
|
|
209
|
+
}
|
|
210
|
+
function rgb(r, g, b) {
|
|
211
|
+
if (!wantsColor)
|
|
212
|
+
return (s) => s;
|
|
213
|
+
if (truecolor)
|
|
214
|
+
return (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
|
|
215
|
+
const q = (v) => Math.round(v / 255 * 5);
|
|
216
|
+
const code = 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
217
|
+
return (s) => `\x1B[38;5;${code}m${s}\x1B[0m`;
|
|
218
|
+
}
|
|
219
|
+
var plain = (s) => s;
|
|
220
|
+
var sgr = (code) => wantsColor ? (s) => `\x1B[${code}m${s}\x1B[0m` : plain;
|
|
221
|
+
var caramel = rgb(196, 149, 106);
|
|
222
|
+
var caramelDark = rgb(160, 120, 80);
|
|
223
|
+
var bold2 = sgr("1");
|
|
224
|
+
var dim2 = sgr("2");
|
|
225
|
+
var green2 = sgr("32");
|
|
226
|
+
var yellow2 = sgr("33");
|
|
227
|
+
var red2 = sgr("31");
|
|
228
|
+
function width(s) {
|
|
229
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
230
|
+
}
|
|
231
|
+
function pad(s, n) {
|
|
232
|
+
return s + " ".repeat(Math.max(0, n - width(s)));
|
|
233
|
+
}
|
|
234
|
+
var canAnimate = isTTY && process.env.TERM !== "dumb" && !process.env.CI;
|
|
235
|
+
var MARK_GRID = [
|
|
236
|
+
"....#....",
|
|
237
|
+
"..#...#..",
|
|
238
|
+
"...#.#...",
|
|
239
|
+
".#.....#.",
|
|
240
|
+
"..#...#..",
|
|
241
|
+
".#.....#.",
|
|
242
|
+
"...#.#...",
|
|
243
|
+
"..#...#..",
|
|
244
|
+
"....#...."
|
|
245
|
+
];
|
|
246
|
+
function logoGrid() {
|
|
247
|
+
return MARK_GRID;
|
|
248
|
+
}
|
|
249
|
+
var MARK_BLOCK_COUNT = MARK_GRID.reduce((n, row) => n + row.split("").filter((c) => c === "#").length, 0);
|
|
250
|
+
function paintRGB(r, g, b, s) {
|
|
251
|
+
return paint(r, g, b, s);
|
|
252
|
+
}
|
|
253
|
+
function logo(cells = 2) {
|
|
254
|
+
return MARK_GRID.map((row) => row.split("").map((c) => c === "#" ? "█".repeat(cells) : " ".repeat(cells)).join(""));
|
|
255
|
+
}
|
|
256
|
+
var GLYPHS = {
|
|
257
|
+
i: ["##", "##", " ", "##", "##", "##", "##", "##", "##"],
|
|
258
|
+
n: [" ", " ", " ", "#####", "# #", "# #", "# #", "# #", "# #"],
|
|
259
|
+
e: [" ", " ", " ", " ### ", "# #", "#####", "# ", "# #", " ### "],
|
|
260
|
+
r: [" ", " ", " ", "# ##", "## ", "# ", "# ", "# ", "# "],
|
|
261
|
+
o: [" ", " ", " ", " ### ", "# #", "# #", "# #", "# #", " ### "],
|
|
262
|
+
t: [" ", " # ", " # ", "####", " # ", " # ", " # ", " # ", " ###"]
|
|
263
|
+
};
|
|
264
|
+
function wordmark(name, gap = 1) {
|
|
265
|
+
const letters = [...name];
|
|
266
|
+
if (letters.some((c) => !GLYPHS[c]))
|
|
267
|
+
return null;
|
|
268
|
+
const rows = Array(9).fill("");
|
|
269
|
+
letters.forEach((c, i) => {
|
|
270
|
+
for (let r = 0;r < 9; r++)
|
|
271
|
+
rows[r] += (i ? " ".repeat(gap) : "") + GLYPHS[c][r];
|
|
272
|
+
});
|
|
273
|
+
return rows.map((r) => r.replace(/#/g, "█").trimEnd());
|
|
274
|
+
}
|
|
275
|
+
function widest(rows) {
|
|
276
|
+
return Math.max(...rows.map((r) => r.length));
|
|
277
|
+
}
|
|
278
|
+
function lockup(name = "innernote", tagline) {
|
|
279
|
+
const room = (process.stdout.columns ?? 80) - 5;
|
|
280
|
+
const mark = logo(2);
|
|
281
|
+
const markWidth = widest(mark);
|
|
282
|
+
const word = wordmark(name);
|
|
283
|
+
if (word) {
|
|
284
|
+
const gap2 = 3;
|
|
285
|
+
if (markWidth + gap2 + widest(word) <= room) {
|
|
286
|
+
const rows2 = mark.map((r, i) => {
|
|
287
|
+
const right = word[i] ?? "";
|
|
288
|
+
return right ? `${caramel(r.padEnd(markWidth))}${" ".repeat(gap2)}${bold2(caramel(right))}` : caramel(r.trimEnd());
|
|
289
|
+
});
|
|
290
|
+
if (tagline)
|
|
291
|
+
rows2.push("", ` ${dim2(tagline)}`);
|
|
292
|
+
return rows2;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const gap = 3;
|
|
296
|
+
const beside = Math.max(name.length, tagline?.length ?? 0);
|
|
297
|
+
if (markWidth + gap + beside <= room) {
|
|
298
|
+
const mid = Math.floor(mark.length / 2);
|
|
299
|
+
return mark.map((r, i) => {
|
|
300
|
+
const left = caramel(r.padEnd(markWidth)) + " ".repeat(gap);
|
|
301
|
+
if (i === mid)
|
|
302
|
+
return left + bold2(caramel(name));
|
|
303
|
+
if (i === mid + 1 && tagline)
|
|
304
|
+
return left + dim2(tagline);
|
|
305
|
+
return caramel(r.trimEnd());
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const rows = mark.map((r) => caramel(r.trimEnd()));
|
|
309
|
+
rows.push("", bold2(caramel(name)));
|
|
310
|
+
if (tagline)
|
|
311
|
+
rows.push(dim2(tagline));
|
|
312
|
+
return rows;
|
|
313
|
+
}
|
|
314
|
+
function banner(tagline = "your voice, from the terminal") {
|
|
315
|
+
return lockup("innernote", tagline).map((r) => " " + r).join(`
|
|
316
|
+
`);
|
|
317
|
+
}
|
|
318
|
+
function bar(score, width2 = 10) {
|
|
319
|
+
const filled = Math.max(0, Math.min(width2, Math.round(score / 100 * width2)));
|
|
320
|
+
const paint2 = score >= 45 ? green2 : score >= 30 ? caramel : yellow2;
|
|
321
|
+
return paint2("█".repeat(filled)) + dim2("░".repeat(width2 - filled));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/commands/auth.ts
|
|
325
|
+
var PAIR_PAGE = "/dashboard/settings";
|
|
326
|
+
async function login(args) {
|
|
327
|
+
const config = await loadConfig();
|
|
328
|
+
if (config.fromEnv) {
|
|
329
|
+
fail("INNERNOTE_TOKEN is set, so login would have no effect.");
|
|
330
|
+
note(dim2("Unset it to log in normally, or leave it as is."));
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
let code = args.find((a) => !a.startsWith("-"))?.trim();
|
|
334
|
+
if (!code) {
|
|
335
|
+
out(`Open ${bold2(config.apiUrl + PAIR_PAGE)} and copy your pairing code.`);
|
|
336
|
+
out();
|
|
337
|
+
if (!process.stdin.isTTY) {
|
|
338
|
+
fail("No pairing code given, and there is no terminal to ask on.");
|
|
339
|
+
note(dim2("Pass it directly: innernote login ABCD-2345"));
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
code = (prompt("Pairing code:") ?? "").trim();
|
|
343
|
+
}
|
|
344
|
+
if (!code) {
|
|
345
|
+
fail("No pairing code given.");
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const { token } = await requestWith(config, "/api/devices/token", {
|
|
350
|
+
method: "POST",
|
|
351
|
+
anonymous: true,
|
|
352
|
+
body: {
|
|
353
|
+
code,
|
|
354
|
+
client: "cli",
|
|
355
|
+
label: `cli on ${hostname()}`
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
const path = await saveToken(token, config.apiUrl);
|
|
359
|
+
out();
|
|
360
|
+
out(banner("connected"));
|
|
361
|
+
out();
|
|
362
|
+
note(dim2(`Key saved to ${path}. Revoke it any time in Settings.`));
|
|
363
|
+
note(dim2("Try: innernote week"));
|
|
364
|
+
return 0;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
fail(err instanceof Error ? err.message : "Pairing failed.");
|
|
367
|
+
if (isApiError2(err) && err.status === 400) {
|
|
368
|
+
note(dim2("Codes last ten minutes and work once. Generate a fresh one."));
|
|
369
|
+
}
|
|
370
|
+
return 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function logout() {
|
|
374
|
+
const config = await loadConfig();
|
|
375
|
+
const had = await clearToken();
|
|
376
|
+
if (config.fromEnv) {
|
|
377
|
+
note("INNERNOTE_TOKEN is set in this environment, so commands will keep using it.");
|
|
378
|
+
note(dim2("Unset that variable to fully disconnect."));
|
|
379
|
+
}
|
|
380
|
+
if (!had) {
|
|
381
|
+
note(`Nothing stored at ${configPath}.`);
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
ok("Disconnected. The token is gone from this machine.");
|
|
385
|
+
note(dim2("It still exists on the account until you revoke it in Settings."));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/week.ts
|
|
390
|
+
var DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
391
|
+
function weekStart(from) {
|
|
392
|
+
const d = new Date(from);
|
|
393
|
+
d.setHours(0, 0, 0, 0);
|
|
394
|
+
d.setDate(d.getDate() - (d.getDay() + 6) % 7);
|
|
395
|
+
return d;
|
|
396
|
+
}
|
|
397
|
+
function sameDay(a, b) {
|
|
398
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
399
|
+
}
|
|
400
|
+
function renderWeek(posts, now = new Date) {
|
|
401
|
+
const start = weekStart(now);
|
|
402
|
+
const columns = [];
|
|
403
|
+
for (let i = 0;i < 7; i++) {
|
|
404
|
+
const date = new Date(start);
|
|
405
|
+
date.setDate(start.getDate() + i);
|
|
406
|
+
columns.push({
|
|
407
|
+
label: DAYS[i],
|
|
408
|
+
date,
|
|
409
|
+
today: sameDay(date, now),
|
|
410
|
+
posts: posts.filter((p) => {
|
|
411
|
+
const when = p.scheduledFor ?? p.publishedAt;
|
|
412
|
+
return when != null && sameDay(new Date(when), date);
|
|
413
|
+
})
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const W = 10;
|
|
417
|
+
const pad2 = (s, n = W) => s + " ".repeat(Math.max(0, n - width(s)));
|
|
418
|
+
const head = columns.map((c) => {
|
|
419
|
+
const label = `${c.label} ${c.date.getDate()}`;
|
|
420
|
+
return pad2(c.today ? caramel(bold2(label)) : dim2(label));
|
|
421
|
+
}).join("");
|
|
422
|
+
const cells = columns.map((c) => {
|
|
423
|
+
if (c.posts.length === 0)
|
|
424
|
+
return pad2(dim2("·"));
|
|
425
|
+
const marks = c.posts.map((p) => p.publishedAt ? green2("█") : caramelDark("▓")).join(" ");
|
|
426
|
+
return pad2(marks);
|
|
427
|
+
}).join("");
|
|
428
|
+
const lines = [" " + head.trimEnd(), " " + cells.trimEnd()];
|
|
429
|
+
const listed = posts.slice().sort((a, b) => (a.scheduledFor ?? a.publishedAt ?? 0) - (b.scheduledFor ?? b.publishedAt ?? 0));
|
|
430
|
+
if (listed.length) {
|
|
431
|
+
lines.push("");
|
|
432
|
+
for (const p of listed) {
|
|
433
|
+
const when = new Date(p.scheduledFor ?? p.publishedAt ?? 0);
|
|
434
|
+
const day = DAYS[(when.getDay() + 6) % 7];
|
|
435
|
+
const time = when.toLocaleTimeString(undefined, {
|
|
436
|
+
hour: "numeric",
|
|
437
|
+
minute: "2-digit"
|
|
438
|
+
});
|
|
439
|
+
const state = p.publishedAt ? green2("published") : caramelDark("scheduled");
|
|
440
|
+
const text = (p.title || p.content).replace(/\s+/g, " ").slice(0, 44);
|
|
441
|
+
lines.push(` ${dim2(`${day} ${time}`.padEnd(12))} ${state} ${text}`.trimEnd());
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return lines.join(`
|
|
445
|
+
`);
|
|
446
|
+
}
|
|
447
|
+
function emptyWeekNote() {
|
|
448
|
+
return "Nothing scheduled. The whole week is open.";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/draft.ts
|
|
452
|
+
var current = null;
|
|
453
|
+
function setDraft(d) {
|
|
454
|
+
current = d;
|
|
455
|
+
}
|
|
456
|
+
function getDraft() {
|
|
457
|
+
return current;
|
|
458
|
+
}
|
|
459
|
+
function updateDraft(patch) {
|
|
460
|
+
if (current)
|
|
461
|
+
current = { ...current, ...patch };
|
|
462
|
+
}
|
|
463
|
+
function clearDraft() {
|
|
464
|
+
current = null;
|
|
465
|
+
}
|
|
466
|
+
var listed = [];
|
|
467
|
+
function rememberList(items) {
|
|
468
|
+
listed = items;
|
|
469
|
+
}
|
|
470
|
+
function resolveRef(ref) {
|
|
471
|
+
const n = Number(ref);
|
|
472
|
+
if (Number.isInteger(n) && n >= 1 && n <= listed.length)
|
|
473
|
+
return listed[n - 1];
|
|
474
|
+
const byId = listed.find((i) => i.id === ref);
|
|
475
|
+
if (byId)
|
|
476
|
+
return byId;
|
|
477
|
+
return /^[a-z0-9]{20,}$/.test(ref) ? { id: ref, title: ref } : null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/commands/read.ts
|
|
481
|
+
function parseFlags(args) {
|
|
482
|
+
return {
|
|
483
|
+
json: args.includes("--json"),
|
|
484
|
+
rest: args.filter((a) => !a.startsWith("--"))
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function powers(scopes) {
|
|
488
|
+
if (!scopes || scopes.length === 0)
|
|
489
|
+
return "full access";
|
|
490
|
+
if (scopes.length === 1)
|
|
491
|
+
return scopes[0];
|
|
492
|
+
return `${scopes.slice(0, -1).join(", ")} and ${scopes[scopes.length - 1]}`;
|
|
493
|
+
}
|
|
494
|
+
async function whoami(args) {
|
|
495
|
+
const { json } = parseFlags(args);
|
|
496
|
+
const [data, conn] = await Promise.all([
|
|
497
|
+
request("/api/users"),
|
|
498
|
+
request("/api/devices/me").catch(() => null)
|
|
499
|
+
]);
|
|
500
|
+
if (json) {
|
|
501
|
+
out(JSON.stringify({ ...data, connection: conn }, null, 2));
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
if (!data.user) {
|
|
505
|
+
note("Connected, but this account has no profile yet. Finish onboarding in the app.");
|
|
506
|
+
return 1;
|
|
507
|
+
}
|
|
508
|
+
const { name, email, plan } = data.user;
|
|
509
|
+
out(bold2(name || email || "your account"));
|
|
510
|
+
if (email && name)
|
|
511
|
+
out(dim2(email));
|
|
512
|
+
if (plan)
|
|
513
|
+
out(dim2(`plan: ${plan}`));
|
|
514
|
+
if (conn) {
|
|
515
|
+
out();
|
|
516
|
+
out(dim2(`this connection: ${conn.label ?? "unnamed"}, can ${powers(conn.scopes)}`));
|
|
517
|
+
const days = conn.expiresInDays;
|
|
518
|
+
if (days == null) {
|
|
519
|
+
out(dim2("does not expire"));
|
|
520
|
+
} else if (days <= 0) {
|
|
521
|
+
out(yellow2("expired. Run `innernote login` to reconnect."));
|
|
522
|
+
} else if (days <= 14) {
|
|
523
|
+
out(yellow2(`expires in ${days} ${days === 1 ? "day" : "days"}. Reconnect with \`innernote login\`.`));
|
|
524
|
+
} else {
|
|
525
|
+
out(dim2(`expires in ${days} days`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
async function ideas(args) {
|
|
531
|
+
const { json, rest } = parseFlags(args);
|
|
532
|
+
const status = rest[0];
|
|
533
|
+
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
534
|
+
const data = await request(`/api/ideas${query}`);
|
|
535
|
+
if (json) {
|
|
536
|
+
out(JSON.stringify(data.ideas, null, 2));
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
if (data.ideas.length === 0) {
|
|
540
|
+
note('No ideas yet. Capture one with: innernote capture "..."');
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
for (const idea of data.ideas) {
|
|
544
|
+
out(`${dim2(ago(idea.createdAt).padStart(4))} ${oneLine(idea.content)}`);
|
|
545
|
+
}
|
|
546
|
+
note(dim2(`
|
|
547
|
+
${data.ideas.length} ${data.ideas.length === 1 ? "idea" : "ideas"}`));
|
|
548
|
+
note(dim2(`write "one of these" to turn it into a post`));
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
async function drafts(args) {
|
|
552
|
+
const { json, rest } = parseFlags(args);
|
|
553
|
+
const status = rest[0];
|
|
554
|
+
const data = await request(`/api/posts${status ? `?status=${encodeURIComponent(status)}` : ""}`);
|
|
555
|
+
if (json) {
|
|
556
|
+
out(JSON.stringify(data.posts, null, 2));
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
if (data.posts.length === 0) {
|
|
560
|
+
note("No posts yet. Write one with: write");
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
const shown = data.posts.slice(0, 30);
|
|
564
|
+
rememberList(shown.map((p) => ({ id: p._id, title: (p.title || p.content).replace(/\s+/g, " ") })));
|
|
565
|
+
for (const [i, post] of shown.entries()) {
|
|
566
|
+
const n = String(i + 1).padStart(2);
|
|
567
|
+
out(`${caramel(n)} ${dim2(post.status.padEnd(9))} ${oneLine(post.title || post.content, 52)}`);
|
|
568
|
+
}
|
|
569
|
+
if (data.posts.length > shown.length) {
|
|
570
|
+
note(dim2(`
|
|
571
|
+
...and ${data.posts.length - shown.length} more`));
|
|
572
|
+
}
|
|
573
|
+
note(dim2(`
|
|
574
|
+
open <number> to work on one`));
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
async function week(args) {
|
|
578
|
+
const { json } = parseFlags(args);
|
|
579
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
580
|
+
const now = new Date;
|
|
581
|
+
const monday = new Date(now);
|
|
582
|
+
monday.setHours(0, 0, 0, 0);
|
|
583
|
+
monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
|
|
584
|
+
const data = await request(`/api/posts/calendar?start=${monday.getTime()}&end=${monday.getTime() + 7 * DAY}`);
|
|
585
|
+
if (json) {
|
|
586
|
+
out(JSON.stringify(data.posts, null, 2));
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
out();
|
|
590
|
+
out(renderWeek(data.posts, now));
|
|
591
|
+
out();
|
|
592
|
+
if (data.posts.length === 0) {
|
|
593
|
+
note(dim2(emptyWeekNote()));
|
|
594
|
+
note(dim2("`write` to start something"));
|
|
595
|
+
} else {
|
|
596
|
+
note(dim2("`drafts` to see everything, `write` to add one"));
|
|
597
|
+
}
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/commands/read.ts
|
|
602
|
+
function parseFlags2(args) {
|
|
603
|
+
return {
|
|
604
|
+
json: args.includes("--json"),
|
|
605
|
+
rest: args.filter((a) => !a.startsWith("--"))
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/progress.ts
|
|
610
|
+
var DRAFTING = [
|
|
611
|
+
{ at: 0, text: "reading your voice..." },
|
|
612
|
+
{ at: 3, text: "writing..." },
|
|
613
|
+
{ at: 11, text: "scoring the draft..." },
|
|
614
|
+
{ at: 15, text: "checking it against what you have already published..." },
|
|
615
|
+
{ at: 24, text: "still going, this one is taking a while..." }
|
|
616
|
+
];
|
|
617
|
+
var RESHAPING = [
|
|
618
|
+
{ at: 0, text: "reading your voice..." },
|
|
619
|
+
{ at: 3, text: "reshaping..." },
|
|
620
|
+
{ at: 12, text: "scoring it..." },
|
|
621
|
+
{ at: 20, text: "still going..." }
|
|
622
|
+
];
|
|
623
|
+
async function reveal(text, write) {
|
|
624
|
+
if (!canAnimate) {
|
|
625
|
+
write(text + `
|
|
626
|
+
`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const paragraphs = text.split(/\n\s*\n/);
|
|
630
|
+
for (let i = 0;i < paragraphs.length; i++) {
|
|
631
|
+
write(paragraphs[i] + (i < paragraphs.length - 1 ? `
|
|
632
|
+
|
|
633
|
+
` : `
|
|
634
|
+
`));
|
|
635
|
+
if (i < paragraphs.length - 1)
|
|
636
|
+
await sleep(70);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function sleep(ms) {
|
|
640
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/thinking.ts
|
|
644
|
+
function hash(x, y, salt) {
|
|
645
|
+
const n = Math.sin(x * 12.9898 + y * 78.233 + salt * 37.719) * 43758.5453;
|
|
646
|
+
return n - Math.floor(n);
|
|
647
|
+
}
|
|
648
|
+
var CYCLE = 0.8;
|
|
649
|
+
var SPARKS = (() => {
|
|
650
|
+
const out2 = [];
|
|
651
|
+
logoGrid().forEach((line, row) => {
|
|
652
|
+
line.split("").forEach((ch, col) => {
|
|
653
|
+
if (ch !== "#")
|
|
654
|
+
return;
|
|
655
|
+
out2.push({
|
|
656
|
+
row,
|
|
657
|
+
col,
|
|
658
|
+
delay: hash(col, row, 1) * CYCLE,
|
|
659
|
+
duration: CYCLE * (0.75 + hash(col, row, 2) * 0.6)
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
return out2;
|
|
664
|
+
})();
|
|
665
|
+
function level(s, t) {
|
|
666
|
+
const phase = ((t - s.delay) % s.duration + s.duration) % s.duration;
|
|
667
|
+
const up = phase / (s.duration / 2);
|
|
668
|
+
const tri = up <= 1 ? up : 2 - up;
|
|
669
|
+
return 0.18 + tri * 0.82;
|
|
670
|
+
}
|
|
671
|
+
function paintAt(level2, s) {
|
|
672
|
+
const from = [74, 58, 44];
|
|
673
|
+
const to = [232, 201, 176];
|
|
674
|
+
const c = (i) => Math.round(from[i] + (to[i] - from[i]) * level2);
|
|
675
|
+
return paintRGB(c(0), c(1), c(2), s);
|
|
676
|
+
}
|
|
677
|
+
function frame(t) {
|
|
678
|
+
const grid = logoGrid();
|
|
679
|
+
const lit = new Map;
|
|
680
|
+
for (const s of SPARKS)
|
|
681
|
+
lit.set(`${s.row},${s.col}`, level(s, t));
|
|
682
|
+
return grid.map((line, row) => line.split("").map((ch, col) => ch === "#" ? paintAt(lit.get(`${row},${col}`) ?? 0.18, "██") : " ").join(""));
|
|
683
|
+
}
|
|
684
|
+
var HEIGHT = 9;
|
|
685
|
+
|
|
686
|
+
class Thinking {
|
|
687
|
+
phases;
|
|
688
|
+
timer = null;
|
|
689
|
+
started = 0;
|
|
690
|
+
drawn = false;
|
|
691
|
+
label = "";
|
|
692
|
+
constructor(phases) {
|
|
693
|
+
this.phases = phases;
|
|
694
|
+
this.phases = [...phases].sort((a, b) => a.at - b.at);
|
|
695
|
+
}
|
|
696
|
+
start() {
|
|
697
|
+
if (!canAnimate) {
|
|
698
|
+
if (this.phases[0])
|
|
699
|
+
process.stderr.write(this.phases[0].text + `
|
|
700
|
+
`);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
this.started = Date.now();
|
|
704
|
+
process.stderr.write("\x1B[?25l");
|
|
705
|
+
this.timer = setInterval(() => this.draw(), 83);
|
|
706
|
+
this.draw();
|
|
707
|
+
}
|
|
708
|
+
draw() {
|
|
709
|
+
const t = (Date.now() - this.started) / 1000;
|
|
710
|
+
for (const p of this.phases)
|
|
711
|
+
if (t >= p.at)
|
|
712
|
+
this.label = p.text;
|
|
713
|
+
const rows = frame(t);
|
|
714
|
+
const mid = Math.floor(rows.length / 2);
|
|
715
|
+
const out2 = rows.map((r, i) => ` ${r}${i === mid ? ` \x1B[2m${this.label}\x1B[0m` : ""}`).join(`
|
|
716
|
+
`);
|
|
717
|
+
if (this.drawn)
|
|
718
|
+
process.stderr.write(`\x1B[${HEIGHT}A`);
|
|
719
|
+
process.stderr.write(out2.split(`
|
|
720
|
+
`).map((l) => `\x1B[2K${l}`).join(`
|
|
721
|
+
`) + `
|
|
722
|
+
`);
|
|
723
|
+
this.drawn = true;
|
|
724
|
+
}
|
|
725
|
+
stop() {
|
|
726
|
+
if (!this.timer)
|
|
727
|
+
return;
|
|
728
|
+
clearInterval(this.timer);
|
|
729
|
+
this.timer = null;
|
|
730
|
+
if (this.drawn) {
|
|
731
|
+
process.stderr.write(`\x1B[${HEIGHT}A`);
|
|
732
|
+
process.stderr.write(`\x1B[2K
|
|
733
|
+
`.repeat(HEIGHT));
|
|
734
|
+
process.stderr.write(`\x1B[${HEIGHT}A`);
|
|
735
|
+
}
|
|
736
|
+
process.stderr.write("\x1B[?25h");
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function whileThinking(phases, run) {
|
|
740
|
+
const t = new Thinking(phases);
|
|
741
|
+
t.start();
|
|
742
|
+
try {
|
|
743
|
+
return await run();
|
|
744
|
+
} finally {
|
|
745
|
+
t.stop();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/commands/write.ts
|
|
750
|
+
async function capture(args) {
|
|
751
|
+
const { json, rest } = parseFlags2(args);
|
|
752
|
+
const piped = await readStdin();
|
|
753
|
+
const content = (rest.join(" ").trim() || piped || "").trim();
|
|
754
|
+
if (!content) {
|
|
755
|
+
fail("Nothing to capture.");
|
|
756
|
+
note(dim2('Try: innernote capture "the thing you just thought of"'));
|
|
757
|
+
note(dim2("Or pipe it in: pbpaste | innernote capture"));
|
|
758
|
+
return 1;
|
|
759
|
+
}
|
|
760
|
+
const data = await request("/api/ideas", {
|
|
761
|
+
method: "POST",
|
|
762
|
+
body: { content, source: "manual", topics: [] }
|
|
763
|
+
});
|
|
764
|
+
if (json) {
|
|
765
|
+
out(JSON.stringify(data, null, 2));
|
|
766
|
+
return 0;
|
|
767
|
+
}
|
|
768
|
+
ok("Captured.");
|
|
769
|
+
note(dim2("`ideas` to see them, `write` to turn one into a post"));
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
async function write(args) {
|
|
773
|
+
const { json, rest } = parseFlags2(args);
|
|
774
|
+
const flagValue = (name) => {
|
|
775
|
+
const exact = args.indexOf(`--${name}`);
|
|
776
|
+
if (exact !== -1 && args[exact + 1] && !args[exact + 1].startsWith("--")) {
|
|
777
|
+
return args[exact + 1];
|
|
778
|
+
}
|
|
779
|
+
const inline = args.find((a) => a.startsWith(`--${name}=`));
|
|
780
|
+
return inline?.slice(name.length + 3);
|
|
781
|
+
};
|
|
782
|
+
const piped = await readStdin();
|
|
783
|
+
const thought = flagValue("thought") ?? (piped ?? undefined);
|
|
784
|
+
const topic = rest.filter((a) => a !== thought).join(" ").trim();
|
|
785
|
+
const format = flagValue("format");
|
|
786
|
+
const series = flagValue("series");
|
|
787
|
+
const save = args.includes("--save");
|
|
788
|
+
const call = () => request("/api/content/generate", {
|
|
789
|
+
method: "POST",
|
|
790
|
+
body: {
|
|
791
|
+
...topic ? { topic } : {},
|
|
792
|
+
...thought ? { rawThought: thought } : {},
|
|
793
|
+
...format ? { format } : {},
|
|
794
|
+
...series ? { series } : {}
|
|
795
|
+
},
|
|
796
|
+
timeoutMs: 120000
|
|
797
|
+
});
|
|
798
|
+
const post = json ? await call() : await whileThinking(DRAFTING, call);
|
|
799
|
+
setDraft({
|
|
800
|
+
content: post.content,
|
|
801
|
+
series,
|
|
802
|
+
seriesSummary: post.seriesSummary ?? undefined,
|
|
803
|
+
quality: post.qualityScore
|
|
804
|
+
});
|
|
805
|
+
if (json) {
|
|
806
|
+
out(JSON.stringify(post, null, 2));
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
out();
|
|
810
|
+
await reveal(post.content, (chunk) => process.stdout.write(chunk));
|
|
811
|
+
out();
|
|
812
|
+
if (!topic && !thought && !series) {
|
|
813
|
+
note(dim2("No topic given, so innernote picked the subject you have covered least."));
|
|
814
|
+
}
|
|
815
|
+
const notes = [`${bar(post.qualityScore)} ${post.qualityScore}`];
|
|
816
|
+
if (post.revised)
|
|
817
|
+
notes.push("revised once");
|
|
818
|
+
if (post.similarity?.regenerated)
|
|
819
|
+
notes.push("rewritten to avoid repeating an earlier post");
|
|
820
|
+
else if (post.similarity?.band === "adjacent")
|
|
821
|
+
notes.push("close to something you already published");
|
|
822
|
+
note(dim2(notes.join(", ")));
|
|
823
|
+
if (save) {
|
|
824
|
+
const saved = await request("/api/posts", {
|
|
825
|
+
method: "POST",
|
|
826
|
+
body: {
|
|
827
|
+
content: post.content,
|
|
828
|
+
source: "cli",
|
|
829
|
+
...series ? { series, seriesSummary: post.seriesSummary } : {}
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
updateDraft({ id: saved.id });
|
|
833
|
+
ok(series ? `Saved into "${series}". ${dim2(saved.id)}` : `Saved as a draft. ${dim2(saved.id)}`);
|
|
834
|
+
} else {
|
|
835
|
+
note(dim2("Not saved yet. `save` to keep it, `shape shorter` to change it."));
|
|
836
|
+
}
|
|
837
|
+
return 0;
|
|
838
|
+
}
|
|
839
|
+
var SHAPES = [
|
|
840
|
+
"hook",
|
|
841
|
+
"example",
|
|
842
|
+
"specific",
|
|
843
|
+
"punchier",
|
|
844
|
+
"shorter",
|
|
845
|
+
"ending",
|
|
846
|
+
"warmer",
|
|
847
|
+
"deslop",
|
|
848
|
+
"mobile",
|
|
849
|
+
"question",
|
|
850
|
+
"takeaway",
|
|
851
|
+
"jargon"
|
|
852
|
+
];
|
|
853
|
+
async function shape(args) {
|
|
854
|
+
const { json, rest } = parseFlags2(args);
|
|
855
|
+
const piped = await readStdin();
|
|
856
|
+
const chosen = rest.filter((a) => SHAPES.includes(a));
|
|
857
|
+
const freeText = rest.filter((a) => !SHAPES.includes(a)).join(" ").trim();
|
|
858
|
+
const held = getDraft();
|
|
859
|
+
const content = (piped ?? held?.content ?? "").trim();
|
|
860
|
+
if (!content) {
|
|
861
|
+
fail("Nothing to reshape.");
|
|
862
|
+
note(dim2("Write one first, or pipe a post in: pbpaste | innernote shape shorter"));
|
|
863
|
+
return 1;
|
|
864
|
+
}
|
|
865
|
+
if (!chosen.length && !freeText) {
|
|
866
|
+
fail("Say how to reshape it.");
|
|
867
|
+
note(dim2(`Named moves: ${SHAPES.join(", ")}`));
|
|
868
|
+
note(dim2(`Or say it yourself: shape make this about the customer`));
|
|
869
|
+
return 1;
|
|
870
|
+
}
|
|
871
|
+
const asked = chosen.length ? chosen.join(", ") : freeText;
|
|
872
|
+
const call = () => request("/api/content/revise", {
|
|
873
|
+
method: "POST",
|
|
874
|
+
body: chosen.length ? { content, shapes: chosen } : { content, feedback: [freeText] },
|
|
875
|
+
timeoutMs: 120000
|
|
876
|
+
});
|
|
877
|
+
const result = json ? await call() : await whileThinking([{ at: 0, text: `reshaping: ${asked}...` }, ...RESHAPING.slice(1)], call);
|
|
878
|
+
if (json) {
|
|
879
|
+
out(JSON.stringify(result, null, 2));
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
setDraft({
|
|
883
|
+
...held ?? {},
|
|
884
|
+
content: result.content,
|
|
885
|
+
quality: result.qualityScore
|
|
886
|
+
});
|
|
887
|
+
out();
|
|
888
|
+
await reveal(result.content, (chunk) => process.stdout.write(chunk));
|
|
889
|
+
out();
|
|
890
|
+
const delta = content.length - result.content.length;
|
|
891
|
+
const shift = delta > 0 ? `${delta} chars shorter` : delta < 0 ? `${-delta} chars longer` : "same length";
|
|
892
|
+
note(dim2(`${bar(result.qualityScore)} ${result.qualityScore}, ${shift}`));
|
|
893
|
+
note(dim2(held?.id ? "`save` to update it" : "`save` to keep it"));
|
|
894
|
+
return 0;
|
|
895
|
+
}
|
|
896
|
+
async function queue(args) {
|
|
897
|
+
const { json, rest } = parseFlags2(args);
|
|
898
|
+
const id = rest[0];
|
|
899
|
+
if (!id) {
|
|
900
|
+
fail("Which post?");
|
|
901
|
+
note(dim2("innernote queue <post-id> (ids come from `innernote drafts --json`)"));
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
const result = await request(`/api/posts/${encodeURIComponent(id)}/queue`, { method: "POST", timeoutMs: 60000 });
|
|
905
|
+
if (json) {
|
|
906
|
+
out(JSON.stringify(result, null, 2));
|
|
907
|
+
return 0;
|
|
908
|
+
}
|
|
909
|
+
const when = new Date(result.scheduledFor).toLocaleString(undefined, {
|
|
910
|
+
weekday: "long",
|
|
911
|
+
hour: "numeric",
|
|
912
|
+
minute: "2-digit"
|
|
913
|
+
});
|
|
914
|
+
ok(`Queued for ${when}. It will go live on its own.`);
|
|
915
|
+
note(dim2("`week` to see where it landed"));
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
async function save(args) {
|
|
919
|
+
const { json } = parseFlags2(args);
|
|
920
|
+
const held = getDraft();
|
|
921
|
+
if (!held?.content) {
|
|
922
|
+
fail("Nothing to save.");
|
|
923
|
+
note(dim2('Write one first: write "your topic"'));
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
const saved = await request("/api/posts", {
|
|
927
|
+
method: "POST",
|
|
928
|
+
body: {
|
|
929
|
+
...held.id ? { id: held.id } : {},
|
|
930
|
+
content: held.content,
|
|
931
|
+
source: "cli",
|
|
932
|
+
...held.series ? { series: held.series, seriesSummary: held.seriesSummary } : {}
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
updateDraft({ id: saved.id });
|
|
936
|
+
if (json) {
|
|
937
|
+
out(JSON.stringify(saved, null, 2));
|
|
938
|
+
return 0;
|
|
939
|
+
}
|
|
940
|
+
const id = saved.id ?? held.id;
|
|
941
|
+
ok(held.id ? `Updated. ${dim2(String(id))}` : held.series ? `Saved into "${held.series}". ${dim2(String(id))}` : `Saved. ${dim2(String(id))}`);
|
|
942
|
+
note(dim2(`queue ${id} to schedule it`));
|
|
943
|
+
return 0;
|
|
944
|
+
}
|
|
945
|
+
async function open(args) {
|
|
946
|
+
const { json, rest } = parseFlags2(args);
|
|
947
|
+
const ref = rest[0];
|
|
948
|
+
if (!ref) {
|
|
949
|
+
fail("Open which one?");
|
|
950
|
+
note(dim2("drafts then open 3"));
|
|
951
|
+
return 1;
|
|
952
|
+
}
|
|
953
|
+
const target = resolveRef(ref);
|
|
954
|
+
if (!target) {
|
|
955
|
+
fail(`No post ${ref} in the last list.`);
|
|
956
|
+
note(dim2("Run `drafts` to see them numbered."));
|
|
957
|
+
return 1;
|
|
958
|
+
}
|
|
959
|
+
const data = await request("/api/posts");
|
|
960
|
+
const post = data.posts.find((p) => p._id === target.id);
|
|
961
|
+
if (!post) {
|
|
962
|
+
fail("That post is gone.");
|
|
963
|
+
return 1;
|
|
964
|
+
}
|
|
965
|
+
setDraft({ content: post.content, id: post._id });
|
|
966
|
+
if (json) {
|
|
967
|
+
out(JSON.stringify(post, null, 2));
|
|
968
|
+
return 0;
|
|
969
|
+
}
|
|
970
|
+
out();
|
|
971
|
+
out(post.content);
|
|
972
|
+
out();
|
|
973
|
+
note(dim2(`${post.status} ${post._id}`));
|
|
974
|
+
note(dim2("`shape shorter`, or say it yourself: `shape cut the second half`"));
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
async function ask(args) {
|
|
978
|
+
const { json, rest } = parseFlags2(args);
|
|
979
|
+
const message = rest.join(" ").trim();
|
|
980
|
+
const held = getDraft();
|
|
981
|
+
if (!message) {
|
|
982
|
+
fail("Say what you want changed.");
|
|
983
|
+
note(dim2("ask cut the second half and land it harder"));
|
|
984
|
+
return 1;
|
|
985
|
+
}
|
|
986
|
+
if (!held?.content) {
|
|
987
|
+
fail("Nothing to talk about.");
|
|
988
|
+
note(dim2("write one first, or `drafts` then `open 3`"));
|
|
989
|
+
return 1;
|
|
990
|
+
}
|
|
991
|
+
let id = held.id;
|
|
992
|
+
if (!id) {
|
|
993
|
+
if (!json)
|
|
994
|
+
note(dim2("Saving this first, so the assistant can work on it..."));
|
|
995
|
+
const saved = await request("/api/posts", {
|
|
996
|
+
method: "POST",
|
|
997
|
+
body: {
|
|
998
|
+
content: held.content,
|
|
999
|
+
source: "cli",
|
|
1000
|
+
...held.series ? { series: held.series, seriesSummary: held.seriesSummary } : {}
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
id = saved.id;
|
|
1004
|
+
updateDraft({ id });
|
|
1005
|
+
}
|
|
1006
|
+
const call = () => request(`/api/posts/${encodeURIComponent(id)}/edit`, { method: "POST", body: { text: message, scope: "whole" }, timeoutMs: 150000 });
|
|
1007
|
+
const result = json ? await call() : await whileThinking([
|
|
1008
|
+
{ at: 0, text: "reading the post..." },
|
|
1009
|
+
{ at: 3, text: "working on it..." },
|
|
1010
|
+
{ at: 14, text: "still going..." }
|
|
1011
|
+
], call);
|
|
1012
|
+
if (json) {
|
|
1013
|
+
out(JSON.stringify(result, null, 2));
|
|
1014
|
+
return 0;
|
|
1015
|
+
}
|
|
1016
|
+
if (result.changed && result.post) {
|
|
1017
|
+
updateDraft({ content: result.post });
|
|
1018
|
+
out();
|
|
1019
|
+
await reveal(result.post, (chunk) => process.stdout.write(chunk));
|
|
1020
|
+
out();
|
|
1021
|
+
if (result.reply)
|
|
1022
|
+
note(dim2(result.reply));
|
|
1023
|
+
note(dim2("`save` to keep this version, or keep asking"));
|
|
1024
|
+
} else {
|
|
1025
|
+
out();
|
|
1026
|
+
out(result.reply ?? "No change.");
|
|
1027
|
+
out();
|
|
1028
|
+
}
|
|
1029
|
+
return 0;
|
|
1030
|
+
}
|
|
1031
|
+
async function show(args) {
|
|
1032
|
+
const { json } = parseFlags2(args);
|
|
1033
|
+
const held = getDraft();
|
|
1034
|
+
if (!held?.content) {
|
|
1035
|
+
note("Nothing in hand.");
|
|
1036
|
+
note(dim2("`write` to start one, or `drafts` then `open 3`"));
|
|
1037
|
+
return 0;
|
|
1038
|
+
}
|
|
1039
|
+
if (json) {
|
|
1040
|
+
out(JSON.stringify(held, null, 2));
|
|
1041
|
+
return 0;
|
|
1042
|
+
}
|
|
1043
|
+
out();
|
|
1044
|
+
out(held.content);
|
|
1045
|
+
out();
|
|
1046
|
+
const facts = [
|
|
1047
|
+
held.id ? `saved ${held.id}` : "not saved",
|
|
1048
|
+
...held.quality ? [`quality ${held.quality}`] : [],
|
|
1049
|
+
...held.series ? [`in "${held.series}"`] : []
|
|
1050
|
+
];
|
|
1051
|
+
note(dim2(facts.join(" ")));
|
|
1052
|
+
note(dim2(held.id ? "`ask` to change it, `queue` to schedule it" : "`ask` to change it, `save` to keep it"));
|
|
1053
|
+
return 0;
|
|
1054
|
+
}
|
|
1055
|
+
async function drop() {
|
|
1056
|
+
const held = getDraft();
|
|
1057
|
+
if (!held) {
|
|
1058
|
+
note("Nothing in hand.");
|
|
1059
|
+
return 0;
|
|
1060
|
+
}
|
|
1061
|
+
if (!held.id) {
|
|
1062
|
+
note(dim2("That draft was never saved. It is gone."));
|
|
1063
|
+
}
|
|
1064
|
+
clearDraft();
|
|
1065
|
+
ok("Put it down.");
|
|
1066
|
+
note(dim2("`write` to start something, or `drafts` to pick one up"));
|
|
1067
|
+
return 0;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/ui.ts
|
|
1071
|
+
var useColor2 = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
1072
|
+
var wrap2 = (code) => (s) => useColor2 ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
1073
|
+
var dim3 = wrap2("2");
|
|
1074
|
+
var bold3 = wrap2("1");
|
|
1075
|
+
var green3 = wrap2("32");
|
|
1076
|
+
var yellow3 = wrap2("33");
|
|
1077
|
+
var red3 = wrap2("31");
|
|
1078
|
+
function out2(line = "") {
|
|
1079
|
+
process.stdout.write(line + `
|
|
1080
|
+
`);
|
|
1081
|
+
}
|
|
1082
|
+
function note2(line = "") {
|
|
1083
|
+
process.stderr.write(line + `
|
|
1084
|
+
`);
|
|
1085
|
+
}
|
|
1086
|
+
function fail2(line) {
|
|
1087
|
+
process.stderr.write(`${red3("×")} ${line}
|
|
1088
|
+
`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/brand.ts
|
|
1092
|
+
var isTTY2 = process.stdout.isTTY === true;
|
|
1093
|
+
var wantsColor2 = isTTY2 && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
1094
|
+
var truecolor2 = process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit";
|
|
1095
|
+
function rgb2(r, g, b) {
|
|
1096
|
+
if (!wantsColor2)
|
|
1097
|
+
return (s) => s;
|
|
1098
|
+
if (truecolor2)
|
|
1099
|
+
return (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
|
|
1100
|
+
const q = (v) => Math.round(v / 255 * 5);
|
|
1101
|
+
const code = 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
1102
|
+
return (s) => `\x1B[38;5;${code}m${s}\x1B[0m`;
|
|
1103
|
+
}
|
|
1104
|
+
var plain2 = (s) => s;
|
|
1105
|
+
var sgr2 = (code) => wantsColor2 ? (s) => `\x1B[${code}m${s}\x1B[0m` : plain2;
|
|
1106
|
+
var caramel2 = rgb2(196, 149, 106);
|
|
1107
|
+
var caramelDark2 = rgb2(160, 120, 80);
|
|
1108
|
+
var bold4 = sgr2("1");
|
|
1109
|
+
var dim4 = sgr2("2");
|
|
1110
|
+
var green4 = sgr2("32");
|
|
1111
|
+
var yellow4 = sgr2("33");
|
|
1112
|
+
var red4 = sgr2("31");
|
|
1113
|
+
var canAnimate2 = isTTY2 && process.env.TERM !== "dumb" && !process.env.CI;
|
|
1114
|
+
var MARK_GRID2 = [
|
|
1115
|
+
"....#....",
|
|
1116
|
+
"..#...#..",
|
|
1117
|
+
"...#.#...",
|
|
1118
|
+
".#.....#.",
|
|
1119
|
+
"..#...#..",
|
|
1120
|
+
".#.....#.",
|
|
1121
|
+
"...#.#...",
|
|
1122
|
+
"..#...#..",
|
|
1123
|
+
"....#...."
|
|
1124
|
+
];
|
|
1125
|
+
var MARK_BLOCK_COUNT2 = MARK_GRID2.reduce((n, row) => n + row.split("").filter((c) => c === "#").length, 0);
|
|
1126
|
+
function logo2(cells = 2) {
|
|
1127
|
+
return MARK_GRID2.map((row) => row.split("").map((c) => c === "#" ? "█".repeat(cells) : " ".repeat(cells)).join(""));
|
|
1128
|
+
}
|
|
1129
|
+
var GLYPHS2 = {
|
|
1130
|
+
i: ["##", "##", " ", "##", "##", "##", "##", "##", "##"],
|
|
1131
|
+
n: [" ", " ", " ", "#####", "# #", "# #", "# #", "# #", "# #"],
|
|
1132
|
+
e: [" ", " ", " ", " ### ", "# #", "#####", "# ", "# #", " ### "],
|
|
1133
|
+
r: [" ", " ", " ", "# ##", "## ", "# ", "# ", "# ", "# "],
|
|
1134
|
+
o: [" ", " ", " ", " ### ", "# #", "# #", "# #", "# #", " ### "],
|
|
1135
|
+
t: [" ", " # ", " # ", "####", " # ", " # ", " # ", " # ", " ###"]
|
|
1136
|
+
};
|
|
1137
|
+
function wordmark2(name, gap = 1) {
|
|
1138
|
+
const letters = [...name];
|
|
1139
|
+
if (letters.some((c) => !GLYPHS2[c]))
|
|
1140
|
+
return null;
|
|
1141
|
+
const rows = Array(9).fill("");
|
|
1142
|
+
letters.forEach((c, i) => {
|
|
1143
|
+
for (let r = 0;r < 9; r++)
|
|
1144
|
+
rows[r] += (i ? " ".repeat(gap) : "") + GLYPHS2[c][r];
|
|
1145
|
+
});
|
|
1146
|
+
return rows.map((r) => r.replace(/#/g, "█").trimEnd());
|
|
1147
|
+
}
|
|
1148
|
+
function widest2(rows) {
|
|
1149
|
+
return Math.max(...rows.map((r) => r.length));
|
|
1150
|
+
}
|
|
1151
|
+
function lockup2(name = "innernote", tagline) {
|
|
1152
|
+
const room = (process.stdout.columns ?? 80) - 5;
|
|
1153
|
+
const mark = logo2(2);
|
|
1154
|
+
const markWidth = widest2(mark);
|
|
1155
|
+
const word = wordmark2(name);
|
|
1156
|
+
if (word) {
|
|
1157
|
+
const gap2 = 3;
|
|
1158
|
+
if (markWidth + gap2 + widest2(word) <= room) {
|
|
1159
|
+
const rows2 = mark.map((r, i) => {
|
|
1160
|
+
const right = word[i] ?? "";
|
|
1161
|
+
return right ? `${caramel2(r.padEnd(markWidth))}${" ".repeat(gap2)}${bold4(caramel2(right))}` : caramel2(r.trimEnd());
|
|
1162
|
+
});
|
|
1163
|
+
if (tagline)
|
|
1164
|
+
rows2.push("", ` ${dim4(tagline)}`);
|
|
1165
|
+
return rows2;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const gap = 3;
|
|
1169
|
+
const beside = Math.max(name.length, tagline?.length ?? 0);
|
|
1170
|
+
if (markWidth + gap + beside <= room) {
|
|
1171
|
+
const mid = Math.floor(mark.length / 2);
|
|
1172
|
+
return mark.map((r, i) => {
|
|
1173
|
+
const left = caramel2(r.padEnd(markWidth)) + " ".repeat(gap);
|
|
1174
|
+
if (i === mid)
|
|
1175
|
+
return left + bold4(caramel2(name));
|
|
1176
|
+
if (i === mid + 1 && tagline)
|
|
1177
|
+
return left + dim4(tagline);
|
|
1178
|
+
return caramel2(r.trimEnd());
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
const rows = mark.map((r) => caramel2(r.trimEnd()));
|
|
1182
|
+
rows.push("", bold4(caramel2(name)));
|
|
1183
|
+
if (tagline)
|
|
1184
|
+
rows.push(dim4(tagline));
|
|
1185
|
+
return rows;
|
|
1186
|
+
}
|
|
1187
|
+
function banner2(tagline = "your voice, from the terminal") {
|
|
1188
|
+
return lockup2("innernote", tagline).map((r) => " " + r).join(`
|
|
1189
|
+
`);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/help.ts
|
|
1193
|
+
var COMMANDS = {
|
|
1194
|
+
login: {
|
|
1195
|
+
summary: "connect this machine to your account",
|
|
1196
|
+
usage: "innernote login [code]",
|
|
1197
|
+
detail: "Opens nothing and asks for no password. Generate a code in Settings, paste it here, and this machine gets its own key. The code works once and expires in ten minutes.",
|
|
1198
|
+
examples: ["innernote login", "innernote login AW4E-7QKN"]
|
|
1199
|
+
},
|
|
1200
|
+
logout: {
|
|
1201
|
+
summary: "forget the key stored on this machine",
|
|
1202
|
+
usage: "innernote logout",
|
|
1203
|
+
detail: "Removes the key from this machine only. It still exists on your account until you revoke it in Settings, which is what you want if the laptop is the thing you lost."
|
|
1204
|
+
},
|
|
1205
|
+
whoami: {
|
|
1206
|
+
summary: "which account is connected, and what this key can do",
|
|
1207
|
+
usage: "innernote whoami",
|
|
1208
|
+
detail: "Shows the account, then what this particular connection is allowed to do and when it stops working. Keys expire, so this is where you find out before it happens."
|
|
1209
|
+
},
|
|
1210
|
+
capture: {
|
|
1211
|
+
summary: "save an idea to your inbox",
|
|
1212
|
+
usage: "innernote capture <text>",
|
|
1213
|
+
detail: "For the thought you do not want to lose and are not ready to write. Reads piped input too, so anything on your clipboard or in a file can go straight in.",
|
|
1214
|
+
examples: [
|
|
1215
|
+
'innernote capture "the onboarding question nobody asks"',
|
|
1216
|
+
"pbpaste | innernote capture"
|
|
1217
|
+
]
|
|
1218
|
+
},
|
|
1219
|
+
write: {
|
|
1220
|
+
summary: "draft a post in your voice",
|
|
1221
|
+
usage: "innernote write [topic] [options]",
|
|
1222
|
+
detail: "Not a prompt box. innernote already holds your voice, your pillars and what you have published, so give it the subject and let it write. Run it bare and it picks the pillar you have covered least.",
|
|
1223
|
+
options: [
|
|
1224
|
+
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1225
|
+
["--series <name>", "write into one of your series, so it follows on"],
|
|
1226
|
+
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1227
|
+
["--save", "keep it as a draft in the app"]
|
|
1228
|
+
],
|
|
1229
|
+
examples: [
|
|
1230
|
+
"innernote write",
|
|
1231
|
+
'innernote write "hiring your first engineer" --save',
|
|
1232
|
+
'innernote write --series "Build in Public" --save',
|
|
1233
|
+
"cat notes/monday.md | innernote write"
|
|
1234
|
+
]
|
|
1235
|
+
},
|
|
1236
|
+
shape: {
|
|
1237
|
+
summary: "push a post shorter, punchier, warmer",
|
|
1238
|
+
usage: "innernote shape <shape...>",
|
|
1239
|
+
detail: "The same twelve moves the app's editor has. In a session it works on the post you are looking at, so shapes compose: shorter, then punchier, each acting on the last result. From a shell it reads the post from stdin instead.",
|
|
1240
|
+
options: [
|
|
1241
|
+
[
|
|
1242
|
+
"named moves",
|
|
1243
|
+
"hook, example, specific, punchier, shorter, ending, warmer, deslop, mobile, question, takeaway, jargon"
|
|
1244
|
+
],
|
|
1245
|
+
["anything else", "taken as your own instruction, in your words"]
|
|
1246
|
+
],
|
|
1247
|
+
examples: [
|
|
1248
|
+
"shape shorter punchier",
|
|
1249
|
+
"shape make this about the customer, not the product",
|
|
1250
|
+
"shape cut the second half and land it harder",
|
|
1251
|
+
"pbpaste | innernote shape shorter | pbcopy"
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
ask: {
|
|
1255
|
+
summary: "talk to the post in your own words",
|
|
1256
|
+
usage: "innernote ask <what you want>",
|
|
1257
|
+
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not. `shape` covers the moves that have names; this covers everything else.",
|
|
1258
|
+
examples: [
|
|
1259
|
+
"ask cut the second half and land it harder",
|
|
1260
|
+
"ask this ending is soft, what would you do",
|
|
1261
|
+
"ask say what happened instead of what I learned"
|
|
1262
|
+
]
|
|
1263
|
+
},
|
|
1264
|
+
save: {
|
|
1265
|
+
summary: "keep the draft you are looking at",
|
|
1266
|
+
usage: "innernote save",
|
|
1267
|
+
detail: "Saves the post currently in hand, the one write or shape just produced. Rerunning write with --save would regenerate it instead, which gives you a different post: the one you liked is gone.",
|
|
1268
|
+
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1269
|
+
},
|
|
1270
|
+
show: {
|
|
1271
|
+
summary: "see the post you are holding",
|
|
1272
|
+
usage: "innernote show",
|
|
1273
|
+
detail: "A session scrolls, and the post you are working on goes off the top while you look at your week. This brings it back, and says whether it is saved.",
|
|
1274
|
+
examples: ["show"]
|
|
1275
|
+
},
|
|
1276
|
+
drop: {
|
|
1277
|
+
summary: "put down whatever you are holding",
|
|
1278
|
+
usage: "innernote drop",
|
|
1279
|
+
detail: "Starts clean. An unsaved draft is gone, and it says so before it goes.",
|
|
1280
|
+
examples: ["drop"]
|
|
1281
|
+
},
|
|
1282
|
+
ideas: {
|
|
1283
|
+
summary: "what you have captured",
|
|
1284
|
+
usage: "innernote ideas [status]",
|
|
1285
|
+
examples: ["innernote ideas", "innernote ideas new", "innernote ideas --json | jq -r '.[].content'"]
|
|
1286
|
+
},
|
|
1287
|
+
drafts: {
|
|
1288
|
+
summary: "your posts, numbered",
|
|
1289
|
+
usage: "innernote drafts [status]",
|
|
1290
|
+
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one.",
|
|
1291
|
+
examples: ["drafts", "drafts ready", "drafts --json | jq -r '.[]._id'"]
|
|
1292
|
+
},
|
|
1293
|
+
open: {
|
|
1294
|
+
summary: "pull a post out of the list and work on it",
|
|
1295
|
+
usage: "innernote open <number>",
|
|
1296
|
+
detail: "Prints the post and holds it, so `shape` and `save` mean that one. Saving after opening UPDATES it rather than making a second copy.",
|
|
1297
|
+
examples: ["drafts", "open 3", "shape shorter", "save"]
|
|
1298
|
+
},
|
|
1299
|
+
week: {
|
|
1300
|
+
summary: "what your week looks like",
|
|
1301
|
+
usage: "innernote week",
|
|
1302
|
+
detail: "Seven days across, so the gaps are visible before you read a word. Published is filled, scheduled is open."
|
|
1303
|
+
},
|
|
1304
|
+
queue: {
|
|
1305
|
+
summary: "schedule a post into your next open slot",
|
|
1306
|
+
usage: "innernote queue <post-id>",
|
|
1307
|
+
detail: "A queued post GOES LIVE on LinkedIn on its own at your next cadence slot. Nobody presses anything again. Needs a connection you allowed to publish: tick that box in Settings when you generate the code.",
|
|
1308
|
+
examples: ["innernote drafts", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
function commandHelp(name) {
|
|
1312
|
+
const c = COMMANDS[name];
|
|
1313
|
+
if (!c)
|
|
1314
|
+
return null;
|
|
1315
|
+
const lines = ["", ` ${bold2(c.usage)}`, ` ${dim2(c.summary)}`];
|
|
1316
|
+
if (c.detail)
|
|
1317
|
+
lines.push("", wrap3(c.detail, 74, " "));
|
|
1318
|
+
if (c.options?.length) {
|
|
1319
|
+
lines.push("");
|
|
1320
|
+
for (const [flag, what] of c.options) {
|
|
1321
|
+
lines.push(` ${caramel(flag.padEnd(18))} ${dim2(what)}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (c.examples?.length) {
|
|
1325
|
+
lines.push("");
|
|
1326
|
+
for (const ex of c.examples)
|
|
1327
|
+
lines.push(` ${dim2("$")} ${ex}`);
|
|
1328
|
+
}
|
|
1329
|
+
lines.push("");
|
|
1330
|
+
return lines.join(`
|
|
1331
|
+
`);
|
|
1332
|
+
}
|
|
1333
|
+
function wrap3(text, width2, indent) {
|
|
1334
|
+
const words = text.split(/\s+/);
|
|
1335
|
+
const lines = [];
|
|
1336
|
+
let line = "";
|
|
1337
|
+
for (const w of words) {
|
|
1338
|
+
if ((line + " " + w).trim().length > width2) {
|
|
1339
|
+
lines.push(indent + line.trim());
|
|
1340
|
+
line = w;
|
|
1341
|
+
} else {
|
|
1342
|
+
line += " " + w;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (line.trim())
|
|
1346
|
+
lines.push(indent + line.trim());
|
|
1347
|
+
return lines.join(`
|
|
1348
|
+
`);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/panel.ts
|
|
1352
|
+
function line(content = "") {
|
|
1353
|
+
return ` ${caramelDark("▍")} ${content}`.trimEnd();
|
|
1354
|
+
}
|
|
1355
|
+
function margin(rows) {
|
|
1356
|
+
return rows.map(line).join(`
|
|
1357
|
+
`);
|
|
1358
|
+
}
|
|
1359
|
+
function landing(state) {
|
|
1360
|
+
const rows = ["", ...lockup("innernote"), ""];
|
|
1361
|
+
if (state.name) {
|
|
1362
|
+
rows.push(state.name + (state.plan ? dim2(` ${state.plan}`) : ""));
|
|
1363
|
+
}
|
|
1364
|
+
const sub = [];
|
|
1365
|
+
if (state.powers)
|
|
1366
|
+
sub.push(state.powers);
|
|
1367
|
+
if (typeof state.expiresInDays === "number" && state.expiresInDays <= 14) {
|
|
1368
|
+
sub.push(`key expires in ${state.expiresInDays} ${state.expiresInDays === 1 ? "day" : "days"}`);
|
|
1369
|
+
}
|
|
1370
|
+
if (state.version)
|
|
1371
|
+
sub.push(`v${state.version}`);
|
|
1372
|
+
if (sub.length)
|
|
1373
|
+
rows.push(dim2(sub.join(" ")));
|
|
1374
|
+
if (state.week?.length) {
|
|
1375
|
+
rows.push("", ...state.week);
|
|
1376
|
+
}
|
|
1377
|
+
const counts = [];
|
|
1378
|
+
if (state.ideas)
|
|
1379
|
+
counts.push(`${state.ideas} ${state.ideas === 1 ? "idea" : "ideas"} waiting`);
|
|
1380
|
+
if (state.drafts)
|
|
1381
|
+
counts.push(`${state.drafts} drafts`);
|
|
1382
|
+
if (counts.length)
|
|
1383
|
+
rows.push("", dim2(counts.join(" ")));
|
|
1384
|
+
if (state.next) {
|
|
1385
|
+
rows.push("", `${caramel(state.next.command)}${pad("", Math.max(1, 20 - width(state.next.command)))}${dim2(state.next.why)}`);
|
|
1386
|
+
}
|
|
1387
|
+
rows.push("");
|
|
1388
|
+
return margin(rows);
|
|
1389
|
+
}
|
|
1390
|
+
function landingDisconnected(apiUrl, version) {
|
|
1391
|
+
return margin([
|
|
1392
|
+
"",
|
|
1393
|
+
...lockup("innernote", "your voice, from the terminal"),
|
|
1394
|
+
...version ? [` ${dim2(`v${version}`)}`] : [],
|
|
1395
|
+
"",
|
|
1396
|
+
dim2("Write LinkedIn posts in your voice, without leaving here."),
|
|
1397
|
+
"",
|
|
1398
|
+
`${caramel("innernote login")}${pad("", 5)}${dim2("connect this machine")}`,
|
|
1399
|
+
dim2(`Get a code at ${apiUrl.replace(/^https?:\/\//, "")}/dashboard/settings`),
|
|
1400
|
+
""
|
|
1401
|
+
]);
|
|
1402
|
+
}
|
|
1403
|
+
function suggest(state) {
|
|
1404
|
+
if (state.scheduled === 0 && state.ideas > 0) {
|
|
1405
|
+
return { command: "innernote ideas", why: "nothing scheduled, and you have things saved" };
|
|
1406
|
+
}
|
|
1407
|
+
if (state.scheduled === 0) {
|
|
1408
|
+
return { command: "innernote write", why: "the week is empty" };
|
|
1409
|
+
}
|
|
1410
|
+
if (state.ideas >= 5) {
|
|
1411
|
+
return { command: "innernote ideas", why: "your inbox is filling up" };
|
|
1412
|
+
}
|
|
1413
|
+
return { command: "innernote write", why: "start something" };
|
|
1414
|
+
}
|
|
1415
|
+
// package.json
|
|
1416
|
+
var package_default = {
|
|
1417
|
+
name: "innernote",
|
|
1418
|
+
version: "0.1.0",
|
|
1419
|
+
description: "Write LinkedIn posts in your voice, from the terminal.",
|
|
1420
|
+
type: "module",
|
|
1421
|
+
bin: {
|
|
1422
|
+
innernote: "dist/index.js"
|
|
1423
|
+
},
|
|
1424
|
+
files: [
|
|
1425
|
+
"dist",
|
|
1426
|
+
"README.md"
|
|
1427
|
+
],
|
|
1428
|
+
scripts: {
|
|
1429
|
+
build: "bun run build.ts",
|
|
1430
|
+
start: "bun run src/index.ts",
|
|
1431
|
+
typecheck: "tsc --noEmit",
|
|
1432
|
+
prepublishOnly: "bun run build.ts"
|
|
1433
|
+
},
|
|
1434
|
+
engines: {
|
|
1435
|
+
node: ">=20"
|
|
1436
|
+
},
|
|
1437
|
+
devDependencies: {
|
|
1438
|
+
"@types/bun": "^1.3.14"
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
// src/version.ts
|
|
1443
|
+
var VERSION = package_default.version;
|
|
1444
|
+
|
|
1445
|
+
// src/commands/home.ts
|
|
1446
|
+
function powers2(scopes) {
|
|
1447
|
+
if (scopes === undefined)
|
|
1448
|
+
return;
|
|
1449
|
+
if (!scopes || scopes.length === 0)
|
|
1450
|
+
return "full access";
|
|
1451
|
+
if (scopes.length === 1)
|
|
1452
|
+
return scopes[0];
|
|
1453
|
+
return `${scopes.slice(0, -1).join(", ")} and ${scopes[scopes.length - 1]}`;
|
|
1454
|
+
}
|
|
1455
|
+
async function home() {
|
|
1456
|
+
const config = await loadConfig();
|
|
1457
|
+
if (!config.token) {
|
|
1458
|
+
out();
|
|
1459
|
+
out(landingDisconnected(config.apiUrl, VERSION));
|
|
1460
|
+
out();
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
1464
|
+
const monday = new Date;
|
|
1465
|
+
monday.setHours(0, 0, 0, 0);
|
|
1466
|
+
monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
|
|
1467
|
+
const [me, conn, week2, ideas2, drafts2] = await Promise.all([
|
|
1468
|
+
request("/api/users").catch(() => null),
|
|
1469
|
+
request("/api/devices/me").catch(() => null),
|
|
1470
|
+
request(`/api/posts/calendar?start=${monday.getTime()}&end=${monday.getTime() + 7 * DAY}`).catch(() => null),
|
|
1471
|
+
request("/api/ideas").catch(() => null),
|
|
1472
|
+
request("/api/posts?status=draft").catch(() => null)
|
|
1473
|
+
]);
|
|
1474
|
+
const posts = week2?.posts ?? [];
|
|
1475
|
+
const scheduled = posts.filter((p) => !p.publishedAt).length;
|
|
1476
|
+
out();
|
|
1477
|
+
out(landing({
|
|
1478
|
+
name: me?.user?.name ?? me?.user?.email ?? undefined,
|
|
1479
|
+
plan: me?.user?.plan,
|
|
1480
|
+
powers: powers2(conn?.scopes),
|
|
1481
|
+
expiresInDays: conn?.expiresInDays ?? null,
|
|
1482
|
+
version: VERSION,
|
|
1483
|
+
week: week2 ? renderWeek(posts).split(`
|
|
1484
|
+
`).map((l) => l.replace(/^ {2}/, "")) : null,
|
|
1485
|
+
ideas: ideas2?.ideas.length,
|
|
1486
|
+
drafts: drafts2?.posts.length,
|
|
1487
|
+
next: suggest({
|
|
1488
|
+
scheduled,
|
|
1489
|
+
ideas: ideas2?.ideas.length ?? 0,
|
|
1490
|
+
drafts: drafts2?.posts.length ?? 0
|
|
1491
|
+
})
|
|
1492
|
+
}));
|
|
1493
|
+
out();
|
|
1494
|
+
return 0;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/session.ts
|
|
1498
|
+
import { createInterface } from "node:readline";
|
|
1499
|
+
|
|
1500
|
+
// src/help.ts
|
|
1501
|
+
var COMMANDS2 = {
|
|
1502
|
+
login: {
|
|
1503
|
+
summary: "connect this machine to your account",
|
|
1504
|
+
usage: "innernote login [code]",
|
|
1505
|
+
detail: "Opens nothing and asks for no password. Generate a code in Settings, paste it here, and this machine gets its own key. The code works once and expires in ten minutes.",
|
|
1506
|
+
examples: ["innernote login", "innernote login AW4E-7QKN"]
|
|
1507
|
+
},
|
|
1508
|
+
logout: {
|
|
1509
|
+
summary: "forget the key stored on this machine",
|
|
1510
|
+
usage: "innernote logout",
|
|
1511
|
+
detail: "Removes the key from this machine only. It still exists on your account until you revoke it in Settings, which is what you want if the laptop is the thing you lost."
|
|
1512
|
+
},
|
|
1513
|
+
whoami: {
|
|
1514
|
+
summary: "which account is connected, and what this key can do",
|
|
1515
|
+
usage: "innernote whoami",
|
|
1516
|
+
detail: "Shows the account, then what this particular connection is allowed to do and when it stops working. Keys expire, so this is where you find out before it happens."
|
|
1517
|
+
},
|
|
1518
|
+
capture: {
|
|
1519
|
+
summary: "save an idea to your inbox",
|
|
1520
|
+
usage: "innernote capture <text>",
|
|
1521
|
+
detail: "For the thought you do not want to lose and are not ready to write. Reads piped input too, so anything on your clipboard or in a file can go straight in.",
|
|
1522
|
+
examples: [
|
|
1523
|
+
'innernote capture "the onboarding question nobody asks"',
|
|
1524
|
+
"pbpaste | innernote capture"
|
|
1525
|
+
]
|
|
1526
|
+
},
|
|
1527
|
+
write: {
|
|
1528
|
+
summary: "draft a post in your voice",
|
|
1529
|
+
usage: "innernote write [topic] [options]",
|
|
1530
|
+
detail: "Not a prompt box. innernote already holds your voice, your pillars and what you have published, so give it the subject and let it write. Run it bare and it picks the pillar you have covered least.",
|
|
1531
|
+
options: [
|
|
1532
|
+
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1533
|
+
["--series <name>", "write into one of your series, so it follows on"],
|
|
1534
|
+
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1535
|
+
["--save", "keep it as a draft in the app"]
|
|
1536
|
+
],
|
|
1537
|
+
examples: [
|
|
1538
|
+
"innernote write",
|
|
1539
|
+
'innernote write "hiring your first engineer" --save',
|
|
1540
|
+
'innernote write --series "Build in Public" --save',
|
|
1541
|
+
"cat notes/monday.md | innernote write"
|
|
1542
|
+
]
|
|
1543
|
+
},
|
|
1544
|
+
shape: {
|
|
1545
|
+
summary: "push a post shorter, punchier, warmer",
|
|
1546
|
+
usage: "innernote shape <shape...>",
|
|
1547
|
+
detail: "The same twelve moves the app's editor has. In a session it works on the post you are looking at, so shapes compose: shorter, then punchier, each acting on the last result. From a shell it reads the post from stdin instead.",
|
|
1548
|
+
options: [
|
|
1549
|
+
[
|
|
1550
|
+
"named moves",
|
|
1551
|
+
"hook, example, specific, punchier, shorter, ending, warmer, deslop, mobile, question, takeaway, jargon"
|
|
1552
|
+
],
|
|
1553
|
+
["anything else", "taken as your own instruction, in your words"]
|
|
1554
|
+
],
|
|
1555
|
+
examples: [
|
|
1556
|
+
"shape shorter punchier",
|
|
1557
|
+
"shape make this about the customer, not the product",
|
|
1558
|
+
"shape cut the second half and land it harder",
|
|
1559
|
+
"pbpaste | innernote shape shorter | pbcopy"
|
|
1560
|
+
]
|
|
1561
|
+
},
|
|
1562
|
+
ask: {
|
|
1563
|
+
summary: "talk to the post in your own words",
|
|
1564
|
+
usage: "innernote ask <what you want>",
|
|
1565
|
+
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not. `shape` covers the moves that have names; this covers everything else.",
|
|
1566
|
+
examples: [
|
|
1567
|
+
"ask cut the second half and land it harder",
|
|
1568
|
+
"ask this ending is soft, what would you do",
|
|
1569
|
+
"ask say what happened instead of what I learned"
|
|
1570
|
+
]
|
|
1571
|
+
},
|
|
1572
|
+
save: {
|
|
1573
|
+
summary: "keep the draft you are looking at",
|
|
1574
|
+
usage: "innernote save",
|
|
1575
|
+
detail: "Saves the post currently in hand, the one write or shape just produced. Rerunning write with --save would regenerate it instead, which gives you a different post: the one you liked is gone.",
|
|
1576
|
+
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1577
|
+
},
|
|
1578
|
+
show: {
|
|
1579
|
+
summary: "see the post you are holding",
|
|
1580
|
+
usage: "innernote show",
|
|
1581
|
+
detail: "A session scrolls, and the post you are working on goes off the top while you look at your week. This brings it back, and says whether it is saved.",
|
|
1582
|
+
examples: ["show"]
|
|
1583
|
+
},
|
|
1584
|
+
drop: {
|
|
1585
|
+
summary: "put down whatever you are holding",
|
|
1586
|
+
usage: "innernote drop",
|
|
1587
|
+
detail: "Starts clean. An unsaved draft is gone, and it says so before it goes.",
|
|
1588
|
+
examples: ["drop"]
|
|
1589
|
+
},
|
|
1590
|
+
ideas: {
|
|
1591
|
+
summary: "what you have captured",
|
|
1592
|
+
usage: "innernote ideas [status]",
|
|
1593
|
+
examples: ["innernote ideas", "innernote ideas new", "innernote ideas --json | jq -r '.[].content'"]
|
|
1594
|
+
},
|
|
1595
|
+
drafts: {
|
|
1596
|
+
summary: "your posts, numbered",
|
|
1597
|
+
usage: "innernote drafts [status]",
|
|
1598
|
+
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one.",
|
|
1599
|
+
examples: ["drafts", "drafts ready", "drafts --json | jq -r '.[]._id'"]
|
|
1600
|
+
},
|
|
1601
|
+
open: {
|
|
1602
|
+
summary: "pull a post out of the list and work on it",
|
|
1603
|
+
usage: "innernote open <number>",
|
|
1604
|
+
detail: "Prints the post and holds it, so `shape` and `save` mean that one. Saving after opening UPDATES it rather than making a second copy.",
|
|
1605
|
+
examples: ["drafts", "open 3", "shape shorter", "save"]
|
|
1606
|
+
},
|
|
1607
|
+
week: {
|
|
1608
|
+
summary: "what your week looks like",
|
|
1609
|
+
usage: "innernote week",
|
|
1610
|
+
detail: "Seven days across, so the gaps are visible before you read a word. Published is filled, scheduled is open."
|
|
1611
|
+
},
|
|
1612
|
+
queue: {
|
|
1613
|
+
summary: "schedule a post into your next open slot",
|
|
1614
|
+
usage: "innernote queue <post-id>",
|
|
1615
|
+
detail: "A queued post GOES LIVE on LinkedIn on its own at your next cadence slot. Nobody presses anything again. Needs a connection you allowed to publish: tick that box in Settings when you generate the code.",
|
|
1616
|
+
examples: ["innernote drafts", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
function commandHelp2(name) {
|
|
1620
|
+
const c = COMMANDS2[name];
|
|
1621
|
+
if (!c)
|
|
1622
|
+
return null;
|
|
1623
|
+
const lines = ["", ` ${bold2(c.usage)}`, ` ${dim2(c.summary)}`];
|
|
1624
|
+
if (c.detail)
|
|
1625
|
+
lines.push("", wrap4(c.detail, 74, " "));
|
|
1626
|
+
if (c.options?.length) {
|
|
1627
|
+
lines.push("");
|
|
1628
|
+
for (const [flag, what] of c.options) {
|
|
1629
|
+
lines.push(` ${caramel(flag.padEnd(18))} ${dim2(what)}`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (c.examples?.length) {
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
for (const ex of c.examples)
|
|
1635
|
+
lines.push(` ${dim2("$")} ${ex}`);
|
|
1636
|
+
}
|
|
1637
|
+
lines.push("");
|
|
1638
|
+
return lines.join(`
|
|
1639
|
+
`);
|
|
1640
|
+
}
|
|
1641
|
+
function wrap4(text, width2, indent) {
|
|
1642
|
+
const words = text.split(/\s+/);
|
|
1643
|
+
const lines = [];
|
|
1644
|
+
let line2 = "";
|
|
1645
|
+
for (const w of words) {
|
|
1646
|
+
if ((line2 + " " + w).trim().length > width2) {
|
|
1647
|
+
lines.push(indent + line2.trim());
|
|
1648
|
+
line2 = w;
|
|
1649
|
+
} else {
|
|
1650
|
+
line2 += " " + w;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (line2.trim())
|
|
1654
|
+
lines.push(indent + line2.trim());
|
|
1655
|
+
return lines.join(`
|
|
1656
|
+
`);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/session.ts
|
|
1660
|
+
var BUILTINS = ["help", "exit", "quit", "clear"];
|
|
1661
|
+
var INDENT = " ";
|
|
1662
|
+
function tokenize(line2) {
|
|
1663
|
+
const out3 = [];
|
|
1664
|
+
let cur = "";
|
|
1665
|
+
let quote = null;
|
|
1666
|
+
for (const ch of line2.trim()) {
|
|
1667
|
+
if (quote) {
|
|
1668
|
+
if (ch === quote)
|
|
1669
|
+
quote = null;
|
|
1670
|
+
else
|
|
1671
|
+
cur += ch;
|
|
1672
|
+
} else if (ch === '"' || ch === "'") {
|
|
1673
|
+
quote = ch;
|
|
1674
|
+
} else if (/\s/.test(ch)) {
|
|
1675
|
+
if (cur)
|
|
1676
|
+
out3.push(cur);
|
|
1677
|
+
cur = "";
|
|
1678
|
+
} else {
|
|
1679
|
+
cur += ch;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (cur)
|
|
1683
|
+
out3.push(cur);
|
|
1684
|
+
return out3;
|
|
1685
|
+
}
|
|
1686
|
+
function indented(run) {
|
|
1687
|
+
const streams = [process.stdout, process.stderr];
|
|
1688
|
+
const originals = streams.map((s) => s.write.bind(s));
|
|
1689
|
+
const pending = ["", ""];
|
|
1690
|
+
streams.forEach((stream, i) => {
|
|
1691
|
+
const write2 = originals[i];
|
|
1692
|
+
stream.write = (chunk, ...rest) => {
|
|
1693
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
|
1694
|
+
if (/\x1b\[(\d*[ABCD]|2K|\?25[lh])/.test(text)) {
|
|
1695
|
+
return write2(chunk, ...rest);
|
|
1696
|
+
}
|
|
1697
|
+
const buffered = pending[i] + text;
|
|
1698
|
+
const lines = buffered.split(`
|
|
1699
|
+
`);
|
|
1700
|
+
pending[i] = lines.pop() ?? "";
|
|
1701
|
+
if (lines.length === 0)
|
|
1702
|
+
return true;
|
|
1703
|
+
const out3 = lines.map((l) => l.length ? INDENT + l : l).join(`
|
|
1704
|
+
`) + `
|
|
1705
|
+
`;
|
|
1706
|
+
return write2(out3, ...rest);
|
|
1707
|
+
};
|
|
1708
|
+
});
|
|
1709
|
+
const restore = () => {
|
|
1710
|
+
streams.forEach((stream, i) => {
|
|
1711
|
+
if (pending[i])
|
|
1712
|
+
originals[i](INDENT + pending[i]);
|
|
1713
|
+
stream.write = originals[i];
|
|
1714
|
+
});
|
|
1715
|
+
};
|
|
1716
|
+
return run().finally(restore);
|
|
1717
|
+
}
|
|
1718
|
+
async function session(opts) {
|
|
1719
|
+
await opts.intro();
|
|
1720
|
+
const names = [...Object.keys(COMMANDS2), ...BUILTINS];
|
|
1721
|
+
const promptFor = () => {
|
|
1722
|
+
const tag = opts.status?.();
|
|
1723
|
+
return tag ? ` ${dim2(tag)} ${caramelDark("▍")} ` : ` ${caramelDark("▍")} `;
|
|
1724
|
+
};
|
|
1725
|
+
const rl = createInterface({
|
|
1726
|
+
input: process.stdin,
|
|
1727
|
+
output: process.stdout,
|
|
1728
|
+
prompt: promptFor(),
|
|
1729
|
+
historySize: 200,
|
|
1730
|
+
completer(line2) {
|
|
1731
|
+
const hits = names.filter((n) => n.startsWith(line2));
|
|
1732
|
+
return [hits.length ? hits : names, line2];
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
console.log(` ${dim2("Type a command, or")} ${caramel("help")}${dim2(". Ctrl-C twice to leave.")}
|
|
1736
|
+
`);
|
|
1737
|
+
rl.prompt();
|
|
1738
|
+
return new Promise((resolve) => {
|
|
1739
|
+
const queue2 = [];
|
|
1740
|
+
let running = false;
|
|
1741
|
+
let leaving = false;
|
|
1742
|
+
async function drain() {
|
|
1743
|
+
if (running)
|
|
1744
|
+
return;
|
|
1745
|
+
running = true;
|
|
1746
|
+
while (queue2.length) {
|
|
1747
|
+
const [command, ...args] = queue2.shift();
|
|
1748
|
+
if (command === "exit" || command === "quit") {
|
|
1749
|
+
leaving = true;
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
if (command === "clear") {
|
|
1753
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (command === "help") {
|
|
1757
|
+
console.log(args[0] ? commandHelp2(args[0]) ?? whatThereIs() : whatThereIs());
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (!COMMANDS2[command]) {
|
|
1761
|
+
console.log(`${INDENT}${dim2(`No command "${command}". Try`)} ${caramel("help")}${dim2(".")}`);
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
rl.pause();
|
|
1765
|
+
try {
|
|
1766
|
+
await indented(() => opts.run(command, args));
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
const text = isNotLoggedIn2(err) ? `${dim2("Not connected. Run")} ${caramel("login")}${dim2(".")}` : err instanceof Error ? err.message : String(err);
|
|
1769
|
+
console.log(`${INDENT}${text}`);
|
|
1770
|
+
}
|
|
1771
|
+
rl.resume();
|
|
1772
|
+
}
|
|
1773
|
+
running = false;
|
|
1774
|
+
if (leaving)
|
|
1775
|
+
rl.close();
|
|
1776
|
+
else {
|
|
1777
|
+
rl.setPrompt(promptFor());
|
|
1778
|
+
rl.prompt();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
rl.on("line", (raw) => {
|
|
1782
|
+
const parts = tokenize(raw);
|
|
1783
|
+
if (parts.length === 0) {
|
|
1784
|
+
if (!running) {
|
|
1785
|
+
rl.setPrompt(promptFor());
|
|
1786
|
+
rl.prompt();
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
queue2.push(parts);
|
|
1791
|
+
drain();
|
|
1792
|
+
});
|
|
1793
|
+
rl.on("close", () => {
|
|
1794
|
+
console.log(`
|
|
1795
|
+
${dim2("See you.")}
|
|
1796
|
+
`);
|
|
1797
|
+
resolve(0);
|
|
1798
|
+
});
|
|
1799
|
+
let armed = false;
|
|
1800
|
+
let disarm = null;
|
|
1801
|
+
rl.on("SIGINT", () => {
|
|
1802
|
+
if (armed) {
|
|
1803
|
+
rl.close();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
armed = true;
|
|
1807
|
+
if (disarm)
|
|
1808
|
+
clearTimeout(disarm);
|
|
1809
|
+
disarm = setTimeout(() => {
|
|
1810
|
+
armed = false;
|
|
1811
|
+
}, 2000);
|
|
1812
|
+
process.stdout.write(`
|
|
1813
|
+
`);
|
|
1814
|
+
console.log(` ${dim2("Press Ctrl-C again to leave.")}`);
|
|
1815
|
+
rl.prompt();
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
function whatThereIs() {
|
|
1820
|
+
const lines = [
|
|
1821
|
+
"",
|
|
1822
|
+
` ${bold2("The loop")}`,
|
|
1823
|
+
...[
|
|
1824
|
+
[`${caramel("write")} ${dim2("something")}`, "draft it in your voice"],
|
|
1825
|
+
[caramel("shape shorter"), "or say it yourself: shape cut the ending"],
|
|
1826
|
+
[`${caramel("ask")} ${dim2("what you want")}`, "talk to it, like the app's writer"],
|
|
1827
|
+
[caramel("save"), "keep it"],
|
|
1828
|
+
[`${caramel("queue")} ${dim2("<id>")}`, "schedule it to go live"]
|
|
1829
|
+
].map(([cmd, why]) => ` ${pad(cmd, 26)}${dim2(why)}`),
|
|
1830
|
+
"",
|
|
1831
|
+
` ${dim2("Already have one?")} ${caramel("drafts")}${dim2(", then")} ${caramel("open 3")}`,
|
|
1832
|
+
` ${dim2("Lost your place?")} ${caramel("show")}${dim2(" reprints what you are holding")}`,
|
|
1833
|
+
""
|
|
1834
|
+
];
|
|
1835
|
+
const groups = [
|
|
1836
|
+
["Writing", ["write", "shape", "ask", "save", "capture"]],
|
|
1837
|
+
["Looking", ["show", "ideas", "drafts", "open", "week", "whoami"]],
|
|
1838
|
+
["Shipping", ["queue"]],
|
|
1839
|
+
["Connection", ["login", "logout"]]
|
|
1840
|
+
];
|
|
1841
|
+
for (const [title, cmds] of groups) {
|
|
1842
|
+
lines.push(` ${bold2(title)}`);
|
|
1843
|
+
for (const c of cmds) {
|
|
1844
|
+
lines.push(` ${caramel(c.padEnd(9))} ${dim2(COMMANDS2[c]?.summary ?? "")}`);
|
|
1845
|
+
}
|
|
1846
|
+
lines.push("");
|
|
1847
|
+
}
|
|
1848
|
+
lines.push(` ${dim2("help <command> for detail. drop to put a draft down.")}`, ` ${dim2("exit, or Ctrl-C twice, to leave.")}`, "");
|
|
1849
|
+
return lines.join(`
|
|
1850
|
+
`);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/draft.ts
|
|
1854
|
+
var current2 = null;
|
|
1855
|
+
function getDraft2() {
|
|
1856
|
+
return current2;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/version.ts
|
|
1860
|
+
var VERSION2 = package_default.version;
|
|
1861
|
+
|
|
1862
|
+
// src/index.ts
|
|
1863
|
+
function commandList() {
|
|
1864
|
+
const groups = [
|
|
1865
|
+
["Getting connected", ["login", "logout", "whoami"]],
|
|
1866
|
+
["Writing", ["write", "shape", "ask", "save", "capture"]],
|
|
1867
|
+
["Looking", ["show", "ideas", "drafts", "open", "week"]],
|
|
1868
|
+
["Shipping", ["queue"]]
|
|
1869
|
+
];
|
|
1870
|
+
const lines = [];
|
|
1871
|
+
for (const [title, names] of groups) {
|
|
1872
|
+
lines.push(bold4(title));
|
|
1873
|
+
for (const n of names) {
|
|
1874
|
+
const h = COMMANDS[n];
|
|
1875
|
+
lines.push(` ${caramel2(n.padEnd(9))} ${h ? dim4(h.summary) : dim4("(no help written)")}`);
|
|
1876
|
+
}
|
|
1877
|
+
lines.push("");
|
|
1878
|
+
}
|
|
1879
|
+
return lines.join(`
|
|
1880
|
+
`);
|
|
1881
|
+
}
|
|
1882
|
+
var HELP = () => [
|
|
1883
|
+
commandList(),
|
|
1884
|
+
`${bold4("Anywhere")}`,
|
|
1885
|
+
` ${caramel2("--json".padEnd(9))} ${dim4("print raw JSON instead of the human view")}`,
|
|
1886
|
+
` ${caramel2("--help".padEnd(9))} ${dim4("this, or details for one command")}`,
|
|
1887
|
+
"",
|
|
1888
|
+
dim4(" innernote <command> --help for what a command actually does"),
|
|
1889
|
+
dim4(" INNERNOTE_API_URL point at another host, for development")
|
|
1890
|
+
].join(`
|
|
1891
|
+
`);
|
|
1892
|
+
var COMMANDS3 = {
|
|
1893
|
+
login,
|
|
1894
|
+
logout: () => logout(),
|
|
1895
|
+
whoami,
|
|
1896
|
+
capture,
|
|
1897
|
+
write,
|
|
1898
|
+
shape,
|
|
1899
|
+
ask,
|
|
1900
|
+
save,
|
|
1901
|
+
show,
|
|
1902
|
+
drop,
|
|
1903
|
+
ideas,
|
|
1904
|
+
drafts,
|
|
1905
|
+
open,
|
|
1906
|
+
week,
|
|
1907
|
+
queue
|
|
1908
|
+
};
|
|
1909
|
+
async function main() {
|
|
1910
|
+
const [, , command, ...args] = process.argv;
|
|
1911
|
+
if (!command) {
|
|
1912
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1913
|
+
return home();
|
|
1914
|
+
return session({
|
|
1915
|
+
intro: async () => {
|
|
1916
|
+
await home();
|
|
1917
|
+
},
|
|
1918
|
+
status: () => {
|
|
1919
|
+
const d = getDraft2();
|
|
1920
|
+
if (!d)
|
|
1921
|
+
return null;
|
|
1922
|
+
return d.id ? "saved" : "unsaved";
|
|
1923
|
+
},
|
|
1924
|
+
run: async (name, rest) => {
|
|
1925
|
+
const h = COMMANDS3[name];
|
|
1926
|
+
return h ? h(rest) : 1;
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
1931
|
+
out2();
|
|
1932
|
+
out2(banner2());
|
|
1933
|
+
out2();
|
|
1934
|
+
out2(HELP());
|
|
1935
|
+
return 0;
|
|
1936
|
+
}
|
|
1937
|
+
if (command === "--version" || command === "-v") {
|
|
1938
|
+
out2(VERSION2);
|
|
1939
|
+
return 0;
|
|
1940
|
+
}
|
|
1941
|
+
const handler = COMMANDS3[command];
|
|
1942
|
+
if (!handler) {
|
|
1943
|
+
fail2(`Unknown command: ${command}`);
|
|
1944
|
+
note2(dim4("Run `innernote --help` to see what there is."));
|
|
1945
|
+
return 1;
|
|
1946
|
+
}
|
|
1947
|
+
if (args.includes("--help")) {
|
|
1948
|
+
out2(commandHelp(command) ?? HELP());
|
|
1949
|
+
return 0;
|
|
1950
|
+
}
|
|
1951
|
+
return handler(args);
|
|
1952
|
+
}
|
|
1953
|
+
main().then((code) => {
|
|
1954
|
+
process.exitCode = code;
|
|
1955
|
+
}).catch((err) => {
|
|
1956
|
+
if (isNotLoggedIn(err)) {
|
|
1957
|
+
fail2("Not connected to an account.");
|
|
1958
|
+
note2(dim4("Run `innernote login` to connect this machine."));
|
|
1959
|
+
process.exitCode = 2;
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (isApiError(err)) {
|
|
1963
|
+
fail2(err.message);
|
|
1964
|
+
process.exitCode = err.isRefusal ? 3 : 1;
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1968
|
+
process.exitCode = 1;
|
|
1969
|
+
});
|