ente-cli 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/dist/api.d.ts +221 -0
- package/dist/api.js +182 -0
- package/dist/commands/activity.d.ts +16 -0
- package/dist/commands/activity.js +345 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +86 -0
- package/dist/commands/entity.d.ts +13 -0
- package/dist/commands/entity.js +205 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +13 -0
- package/dist/commands/task.d.ts +3 -0
- package/dist/commands/task.js +305 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +104 -0
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.js +171 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +287 -0
- package/dist/guide/index.d.ts +2 -0
- package/dist/guide/index.js +27 -0
- package/dist/guide/topics.d.ts +5 -0
- package/dist/guide/topics.js +208 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/resolve.d.ts +11 -0
- package/dist/resolve.js +66 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +18 -0
- package/package.json +32 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { resolveEntity, resolveEntities, resolveSpaceByName } from "../resolve.js";
|
|
2
|
+
import { parseDeadline, defaultDeadline } from "../deadline.js";
|
|
3
|
+
import { formatTaskList, formatTaskDetail, formatSpacesFooter, jsonOutputWithSpaces, } from "../format.js";
|
|
4
|
+
import { parseAttrs } from "../utils.js";
|
|
5
|
+
const TZ = "Europe/Berlin";
|
|
6
|
+
function endOfTodayBerlin() {
|
|
7
|
+
const berlinStr = new Date().toLocaleDateString("en-CA", { timeZone: TZ });
|
|
8
|
+
const [y, m, d] = berlinStr.split("-").map(Number);
|
|
9
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
10
|
+
const naive = `${y}-${pad(m)}-${pad(d)}T23:59:59`;
|
|
11
|
+
const utcGuess = new Date(`${naive}Z`);
|
|
12
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
13
|
+
timeZone: TZ,
|
|
14
|
+
hour: "2-digit",
|
|
15
|
+
minute: "2-digit",
|
|
16
|
+
hour12: false,
|
|
17
|
+
});
|
|
18
|
+
const parts = formatter.formatToParts(utcGuess);
|
|
19
|
+
const berlinHour = Number(parts.find((p) => p.type === "hour")?.value ?? 0);
|
|
20
|
+
const berlinMinute = Number(parts.find((p) => p.type === "minute")?.value ?? 0);
|
|
21
|
+
let offsetMinutes = (berlinHour * 60 + berlinMinute) - (utcGuess.getUTCHours() * 60 + utcGuess.getUTCMinutes());
|
|
22
|
+
if (offsetMinutes > 720)
|
|
23
|
+
offsetMinutes -= 1440;
|
|
24
|
+
if (offsetMinutes < -720)
|
|
25
|
+
offsetMinutes += 1440;
|
|
26
|
+
return utcGuess.getTime() - offsetMinutes * 60000;
|
|
27
|
+
}
|
|
28
|
+
export function registerTaskCommands(program, getClient) {
|
|
29
|
+
const task = program.command("task").description("Manage tasks");
|
|
30
|
+
// -- list -----------------------------------------------------------------
|
|
31
|
+
task
|
|
32
|
+
.command("list")
|
|
33
|
+
.description("List open tasks")
|
|
34
|
+
.option("--due <filter>", "Filter: today, overdue, all")
|
|
35
|
+
.option("--entity <name>", "Filter by entity (name or ID)")
|
|
36
|
+
.option("--json", "Output raw JSON")
|
|
37
|
+
.action(async (opts) => {
|
|
38
|
+
const client = getClient();
|
|
39
|
+
const query = {
|
|
40
|
+
onlyOpen: true,
|
|
41
|
+
};
|
|
42
|
+
if (opts.entity) {
|
|
43
|
+
query.entityId = await resolveEntity(client, opts.entity);
|
|
44
|
+
}
|
|
45
|
+
if (opts.due === "all") {
|
|
46
|
+
query.onlyOpen = false;
|
|
47
|
+
}
|
|
48
|
+
else if (opts.due === "today") {
|
|
49
|
+
query.dueBy = endOfTodayBerlin();
|
|
50
|
+
}
|
|
51
|
+
else if (opts.due === "overdue") {
|
|
52
|
+
query.dueBy = Date.now();
|
|
53
|
+
}
|
|
54
|
+
else if (opts.due) {
|
|
55
|
+
console.error(`Unknown --due value: '${opts.due}'. Use: today, overdue, or all.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const [res, spaces] = await Promise.all([
|
|
59
|
+
client.listTasks(query),
|
|
60
|
+
client.listSpaces(),
|
|
61
|
+
]);
|
|
62
|
+
// Sort by deadline ascending (earliest first)
|
|
63
|
+
res.data.sort((a, b) => {
|
|
64
|
+
const da = a.activity.deadline ?? Infinity;
|
|
65
|
+
const db = b.activity.deadline ?? Infinity;
|
|
66
|
+
return da - db;
|
|
67
|
+
});
|
|
68
|
+
if (opts.json) {
|
|
69
|
+
console.log(jsonOutputWithSpaces(res, spaces));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(formatTaskList(res.data, res.users));
|
|
73
|
+
console.log(formatSpacesFooter(spaces));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// -- get ------------------------------------------------------------------
|
|
77
|
+
task
|
|
78
|
+
.command("get <id>")
|
|
79
|
+
.description("Get task by shortId (#N) or activity ID")
|
|
80
|
+
.option("--space <name-or-id>", "Filter by space (name or ID) to disambiguate shortIds")
|
|
81
|
+
.option("--json", "Output raw JSON")
|
|
82
|
+
.action(async (id, opts) => {
|
|
83
|
+
const client = getClient();
|
|
84
|
+
let spaceId;
|
|
85
|
+
if (opts.space) {
|
|
86
|
+
spaceId = await resolveSpaceByName(client, opts.space);
|
|
87
|
+
}
|
|
88
|
+
const [res, spaces] = await Promise.all([
|
|
89
|
+
getTask(client, id, spaceId),
|
|
90
|
+
client.listSpaces(),
|
|
91
|
+
]);
|
|
92
|
+
if (opts.json) {
|
|
93
|
+
console.log(jsonOutputWithSpaces(res, spaces));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(formatTaskDetail(res.data, res.users));
|
|
97
|
+
console.log(formatSpacesFooter(spaces));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// -- create ---------------------------------------------------------------
|
|
101
|
+
task
|
|
102
|
+
.command("create <title>")
|
|
103
|
+
.description("Create a task")
|
|
104
|
+
.requiredOption("--entity <names...>", "Linked entities (resolved by name)")
|
|
105
|
+
.requiredOption("--space <name-or-id>", "Space to create the task in (name or ID). Determines who can see this task.")
|
|
106
|
+
.option("--deadline <when>", "Deadline (human-friendly)")
|
|
107
|
+
.option("--attr <pairs...>", "Attributes as key=value pairs")
|
|
108
|
+
.option("--content <text>", "Task content/description")
|
|
109
|
+
.option("--assignee <userIds...>", "Assignee user IDs")
|
|
110
|
+
.option("--auto-link", "Auto-link entities not yet in the target space (only works for personal space)")
|
|
111
|
+
.option("--json", "Output raw JSON")
|
|
112
|
+
.addHelpText("after", `
|
|
113
|
+
Examples:
|
|
114
|
+
ente task create "Reply to Ashley" --entity "Ashley" --entity "TiVA" --space "Personal" --auto-link --deadline "tomorrow"
|
|
115
|
+
ente task create "Fix bug" --entity "TiVA" --space "TiVA" --deadline "+3 days" --attr github-issue=https://...
|
|
116
|
+
ente task create "Call Erik" --entity "Erik Stahl" --entity "Preclinics" --space "Preclinics" --deadline "today 14:00"
|
|
117
|
+
|
|
118
|
+
--space determines who can see the activity (required):
|
|
119
|
+
- Use your personal space for private notes, reflections, personal tasks.
|
|
120
|
+
- Use a shared space (e.g. "Preclinics", "TiVA") for business activities visible to colleagues.
|
|
121
|
+
All linked entities must belong to the target space. If an entity doesn't belong yet,
|
|
122
|
+
use --auto-link to add it (only works when --space is your personal space).
|
|
123
|
+
|
|
124
|
+
Deadlines: "tomorrow", "today", "+3 days", "+1 week", "2026-03-15", "2026-03-15 14:00"
|
|
125
|
+
Default deadline: tomorrow 22:00 Berlin. Default time when only date given: 22:00.
|
|
126
|
+
Always link ALL relevant entities (person + their org + project if applicable).`)
|
|
127
|
+
.action(async (title, opts) => {
|
|
128
|
+
const client = getClient();
|
|
129
|
+
const entityIds = await resolveEntities(client, opts.entity);
|
|
130
|
+
const spaceId = await resolveSpaceByName(client, opts.space);
|
|
131
|
+
const deadline = opts.deadline
|
|
132
|
+
? parseDeadline(opts.deadline)
|
|
133
|
+
: defaultDeadline();
|
|
134
|
+
const attributes = parseAttrs(opts.attr);
|
|
135
|
+
const res = await client.createActivity({
|
|
136
|
+
entityIds,
|
|
137
|
+
type: "task",
|
|
138
|
+
title,
|
|
139
|
+
content: opts.content,
|
|
140
|
+
deadline,
|
|
141
|
+
spaceId,
|
|
142
|
+
attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
|
|
143
|
+
assigneeIds: opts.assignee,
|
|
144
|
+
autoLinkPersonalSpace: opts.autoLink ?? false,
|
|
145
|
+
});
|
|
146
|
+
const spaces = await client.listSpaces();
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(jsonOutputWithSpaces(res, spaces));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(`Created task '${title}' (${res.data.activityId})`);
|
|
152
|
+
console.log(formatSpacesFooter(spaces));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// -- update ---------------------------------------------------------------
|
|
156
|
+
task
|
|
157
|
+
.command("update <id>")
|
|
158
|
+
.description("Update a task")
|
|
159
|
+
.option("--title <text>", "Update title")
|
|
160
|
+
.option("--content <text>", "Update content")
|
|
161
|
+
.option("--attr <pairs...>", "Set attributes as key=value pairs")
|
|
162
|
+
.option("--deadline <when>", "Update deadline (human-friendly)")
|
|
163
|
+
.option("--json", "Output raw JSON")
|
|
164
|
+
.action(async (id, opts) => {
|
|
165
|
+
const client = getClient();
|
|
166
|
+
const activityId = await resolveTaskId(client, id);
|
|
167
|
+
const body = {};
|
|
168
|
+
if (opts.title !== undefined)
|
|
169
|
+
body.title = opts.title;
|
|
170
|
+
if (opts.content !== undefined)
|
|
171
|
+
body.content = opts.content;
|
|
172
|
+
if (opts.deadline)
|
|
173
|
+
body.deadline = parseDeadline(opts.deadline);
|
|
174
|
+
const attributes = parseAttrs(opts.attr);
|
|
175
|
+
if (Object.keys(attributes).length > 0)
|
|
176
|
+
body.attributes = attributes;
|
|
177
|
+
if (Object.keys(body).length === 0) {
|
|
178
|
+
console.error("Nothing to update. Specify --title, --content, --attr, or --deadline.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const [res, spaces] = await Promise.all([
|
|
182
|
+
client.updateActivity(activityId, body),
|
|
183
|
+
client.listSpaces(),
|
|
184
|
+
]);
|
|
185
|
+
if (opts.json) {
|
|
186
|
+
console.log(jsonOutputWithSpaces(res, spaces));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(`Updated task ${id}`);
|
|
190
|
+
console.log(formatSpacesFooter(spaces));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// -- done -----------------------------------------------------------------
|
|
194
|
+
task
|
|
195
|
+
.command("done <id>")
|
|
196
|
+
.description("Mark task as completed")
|
|
197
|
+
.option("--json", "Output JSON")
|
|
198
|
+
.action(async (id, opts) => {
|
|
199
|
+
const client = getClient();
|
|
200
|
+
const activityId = await resolveTaskId(client, id);
|
|
201
|
+
const [, spaces] = await Promise.all([
|
|
202
|
+
client.updateActivity(activityId, { completed: true }),
|
|
203
|
+
client.listSpaces(),
|
|
204
|
+
]);
|
|
205
|
+
if (opts.json) {
|
|
206
|
+
console.log(jsonOutputWithSpaces({ data: { success: true, activityId } }, spaces));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(`Task ${id} marked as done.`);
|
|
210
|
+
console.log(formatSpacesFooter(spaces));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// -- assign / unassign ----------------------------------------------------
|
|
214
|
+
task
|
|
215
|
+
.command("assign <id> <userId>")
|
|
216
|
+
.description("Add assignee to task")
|
|
217
|
+
.option("--json", "Output JSON")
|
|
218
|
+
.action(async (id, userId, opts) => {
|
|
219
|
+
const client = getClient();
|
|
220
|
+
const taskRes = await getTask(client, id);
|
|
221
|
+
const activityId = taskRes.data.activity._id;
|
|
222
|
+
const current = taskRes.data.activity.assigneeIds || [];
|
|
223
|
+
if (current.includes(userId)) {
|
|
224
|
+
console.log("User is already assigned.");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const [, spaces] = await Promise.all([
|
|
228
|
+
client.updateActivity(activityId, { assigneeIds: [...current, userId] }),
|
|
229
|
+
client.listSpaces(),
|
|
230
|
+
]);
|
|
231
|
+
if (opts.json) {
|
|
232
|
+
console.log(jsonOutputWithSpaces({ data: { success: true, activityId, assigneeIds: [...current, userId] } }, spaces));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.log(`Assigned ${userId} to task ${id}.`);
|
|
236
|
+
console.log(formatSpacesFooter(spaces));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
task
|
|
240
|
+
.command("unassign <id> <userId>")
|
|
241
|
+
.description("Remove assignee from task")
|
|
242
|
+
.option("--json", "Output JSON")
|
|
243
|
+
.action(async (id, userId, opts) => {
|
|
244
|
+
const client = getClient();
|
|
245
|
+
const taskRes = await getTask(client, id);
|
|
246
|
+
const activityId = taskRes.data.activity._id;
|
|
247
|
+
const current = taskRes.data.activity.assigneeIds || [];
|
|
248
|
+
if (!current.includes(userId)) {
|
|
249
|
+
console.log("User is not assigned to this task.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const updated = current.filter((u) => u !== userId);
|
|
253
|
+
const [, spaces] = await Promise.all([
|
|
254
|
+
client.updateActivity(activityId, { assigneeIds: updated }),
|
|
255
|
+
client.listSpaces(),
|
|
256
|
+
]);
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
console.log(jsonOutputWithSpaces({ data: { success: true, activityId, assigneeIds: updated } }, spaces));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.log(`Unassigned ${userId} from task ${id}.`);
|
|
262
|
+
console.log(formatSpacesFooter(spaces));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Helpers
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
async function getTask(client, id, spaceId) {
|
|
270
|
+
const num = parseShortId(id);
|
|
271
|
+
if (num !== null) {
|
|
272
|
+
return client.getTaskByNumber(num, spaceId ? { spaceId } : undefined);
|
|
273
|
+
}
|
|
274
|
+
// Fallback: treat as activity ID — search open tasks first, then all
|
|
275
|
+
const openTasks = await client.listTasks({ onlyOpen: true });
|
|
276
|
+
let match = openTasks.data.find((t) => t.activity._id === id);
|
|
277
|
+
let users = openTasks.users ?? {};
|
|
278
|
+
if (!match) {
|
|
279
|
+
// Also check completed tasks
|
|
280
|
+
const allTasks = await client.listTasks({ onlyOpen: false });
|
|
281
|
+
match = allTasks.data.find((t) => t.activity._id === id);
|
|
282
|
+
users = allTasks.users ?? {};
|
|
283
|
+
}
|
|
284
|
+
if (!match) {
|
|
285
|
+
throw new Error(`No task found with ID "${id}". Use short ID (#N) for easier lookup.`);
|
|
286
|
+
}
|
|
287
|
+
return { data: match, users };
|
|
288
|
+
}
|
|
289
|
+
async function resolveTaskId(client, id) {
|
|
290
|
+
const num = parseShortId(id);
|
|
291
|
+
if (num !== null) {
|
|
292
|
+
const res = await client.getTaskByNumber(num);
|
|
293
|
+
return res.data.activity._id;
|
|
294
|
+
}
|
|
295
|
+
// Assume it's a raw activity ID
|
|
296
|
+
return id;
|
|
297
|
+
}
|
|
298
|
+
function parseShortId(id) {
|
|
299
|
+
const cleaned = id.startsWith("#") ? id.slice(1) : id;
|
|
300
|
+
const n = parseInt(cleaned, 10);
|
|
301
|
+
if (!isNaN(n) && String(n) === cleaned)
|
|
302
|
+
return n;
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=task.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Check for updates (non-blocking, cached for 24h).
|
|
4
|
+
* Call at CLI startup — prints a hint if outdated.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkForUpdates(): Promise<void>;
|
|
7
|
+
export declare function registerUpdate(program: Command): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
const PACKAGE_NAME = "ente-cli";
|
|
6
|
+
const CACHE_DIR = path.join(os.homedir(), ".config", "ente");
|
|
7
|
+
const VERSION_CACHE = path.join(CACHE_DIR, ".version-check.json");
|
|
8
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
function readCache() {
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(VERSION_CACHE, "utf-8");
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeCache(cache) {
|
|
19
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
20
|
+
fs.writeFileSync(VERSION_CACHE, JSON.stringify(cache));
|
|
21
|
+
}
|
|
22
|
+
async function fetchLatestVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
25
|
+
signal: AbortSignal.timeout(3000), // 3s timeout — don't block CLI
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
return null;
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
return data.version || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function getCurrentVersion() {
|
|
37
|
+
// Read from package.json at build time (embedded in dist)
|
|
38
|
+
try {
|
|
39
|
+
const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "package.json");
|
|
40
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
41
|
+
return pkg.version;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return "0.0.0";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function compareVersions(a, b) {
|
|
48
|
+
const pa = a.split(".").map(Number);
|
|
49
|
+
const pb = b.split(".").map(Number);
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
if ((pa[i] || 0) < (pb[i] || 0))
|
|
52
|
+
return -1;
|
|
53
|
+
if ((pa[i] || 0) > (pb[i] || 0))
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check for updates (non-blocking, cached for 24h).
|
|
60
|
+
* Call at CLI startup — prints a hint if outdated.
|
|
61
|
+
*/
|
|
62
|
+
export async function checkForUpdates() {
|
|
63
|
+
const current = getCurrentVersion();
|
|
64
|
+
const cache = readCache();
|
|
65
|
+
let latest;
|
|
66
|
+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
67
|
+
latest = cache.latest;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const fetched = await fetchLatestVersion();
|
|
71
|
+
if (!fetched)
|
|
72
|
+
return; // network issue, skip silently
|
|
73
|
+
latest = fetched;
|
|
74
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
75
|
+
}
|
|
76
|
+
if (compareVersions(current, latest) < 0) {
|
|
77
|
+
console.error(`\n⚠ ente-cli v${current} → v${latest} available. Run: ente update\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function registerUpdate(program) {
|
|
81
|
+
program
|
|
82
|
+
.command("update")
|
|
83
|
+
.description("Update ente-cli to the latest version")
|
|
84
|
+
.action(() => {
|
|
85
|
+
const current = getCurrentVersion();
|
|
86
|
+
console.log(`Current version: ${current}`);
|
|
87
|
+
console.log(`Updating ${PACKAGE_NAME}...`);
|
|
88
|
+
try {
|
|
89
|
+
// Detect how the CLI was installed
|
|
90
|
+
const npmGlobalPrefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
|
|
91
|
+
execSync(`npm install -g ${PACKAGE_NAME}@latest`, {
|
|
92
|
+
stdio: "inherit",
|
|
93
|
+
});
|
|
94
|
+
console.log("✓ Updated successfully");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Fallback: try npx
|
|
98
|
+
console.error("npm global install failed. Try manually:");
|
|
99
|
+
console.error(` npm install -g ${PACKAGE_NAME}@latest`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=update.js.map
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-friendly deadline parsing. All times in Europe/Berlin.
|
|
3
|
+
*/
|
|
4
|
+
const TZ = "Europe/Berlin";
|
|
5
|
+
const DEFAULT_HOUR = 22;
|
|
6
|
+
const DEFAULT_MINUTE = 0;
|
|
7
|
+
/** Get current date components in Berlin timezone */
|
|
8
|
+
function berlinNowParts() {
|
|
9
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
10
|
+
timeZone: TZ,
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "2-digit",
|
|
13
|
+
day: "2-digit",
|
|
14
|
+
});
|
|
15
|
+
const s = formatter.format(new Date()); // "YYYY-MM-DD"
|
|
16
|
+
const [y, m, d] = s.split("-").map(Number);
|
|
17
|
+
return { year: y, month: m, day: d };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convert a Berlin date+time to epoch milliseconds.
|
|
21
|
+
* Uses a two-pass approach to handle DST correctly.
|
|
22
|
+
*/
|
|
23
|
+
function berlinDate(year, month, day, hour, minute) {
|
|
24
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
25
|
+
const naive = `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:00`;
|
|
26
|
+
// Start with the naive time interpreted as UTC
|
|
27
|
+
const utcGuess = new Date(`${naive}Z`);
|
|
28
|
+
// Find what Berlin hour this UTC time corresponds to
|
|
29
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
30
|
+
timeZone: TZ,
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
hour12: false,
|
|
34
|
+
});
|
|
35
|
+
const parts = formatter.formatToParts(utcGuess);
|
|
36
|
+
const berlinHour = Number(parts.find((p) => p.type === "hour")?.value ?? 0);
|
|
37
|
+
const berlinMinute = Number(parts.find((p) => p.type === "minute")?.value ?? 0);
|
|
38
|
+
// Compute offset in minutes (handles midnight crossing correctly)
|
|
39
|
+
let offsetMinutes = (berlinHour * 60 + berlinMinute) - (utcGuess.getUTCHours() * 60 + utcGuess.getUTCMinutes());
|
|
40
|
+
// Normalize to [-720, 720] (±12 hours)
|
|
41
|
+
if (offsetMinutes > 720)
|
|
42
|
+
offsetMinutes -= 1440;
|
|
43
|
+
if (offsetMinutes < -720)
|
|
44
|
+
offsetMinutes += 1440;
|
|
45
|
+
// Target UTC = naive time minus offset
|
|
46
|
+
const targetUtc = new Date(utcGuess.getTime() - offsetMinutes * 60000);
|
|
47
|
+
// Verify with a second pass (handles DST transitions)
|
|
48
|
+
const checkParts = formatter.formatToParts(targetUtc);
|
|
49
|
+
const checkHour = Number(checkParts.find((p) => p.type === "hour")?.value ?? 0);
|
|
50
|
+
if (checkHour !== hour) {
|
|
51
|
+
const correction = (hour - checkHour + 24) % 24;
|
|
52
|
+
// Only correct by ±1 hour for DST
|
|
53
|
+
if (correction === 1 || correction === 23) {
|
|
54
|
+
targetUtc.setTime(targetUtc.getTime() + (correction === 1 ? 3600000 : -3600000));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return targetUtc.getTime();
|
|
58
|
+
}
|
|
59
|
+
function parseTime(s) {
|
|
60
|
+
const m = s.match(/^(\d{1,2}):(\d{2})$/);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
const hour = parseInt(m[1], 10);
|
|
64
|
+
const minute = parseInt(m[2], 10);
|
|
65
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
66
|
+
return null;
|
|
67
|
+
return { hour, minute };
|
|
68
|
+
}
|
|
69
|
+
export function parseDeadline(input) {
|
|
70
|
+
const trimmed = input.trim().toLowerCase();
|
|
71
|
+
const now = berlinNowParts();
|
|
72
|
+
// "tomorrow" or "tomorrow 14:00"
|
|
73
|
+
if (trimmed.startsWith("tomorrow")) {
|
|
74
|
+
const rest = trimmed.slice("tomorrow".length).trim();
|
|
75
|
+
let hour = DEFAULT_HOUR;
|
|
76
|
+
let minute = DEFAULT_MINUTE;
|
|
77
|
+
if (rest) {
|
|
78
|
+
const timeParts = parseTime(rest);
|
|
79
|
+
if (!timeParts) {
|
|
80
|
+
throw new Error(`Invalid time format: '${rest}'. Use HH:MM (e.g. 14:00).`);
|
|
81
|
+
}
|
|
82
|
+
hour = timeParts.hour;
|
|
83
|
+
minute = timeParts.minute;
|
|
84
|
+
}
|
|
85
|
+
const d = new Date(now.year, now.month - 1, now.day + 1);
|
|
86
|
+
return berlinDate(d.getFullYear(), d.getMonth() + 1, d.getDate(), hour, minute);
|
|
87
|
+
}
|
|
88
|
+
// "today" or "today 14:00"
|
|
89
|
+
if (trimmed.startsWith("today")) {
|
|
90
|
+
const rest = trimmed.slice("today".length).trim();
|
|
91
|
+
let hour = DEFAULT_HOUR;
|
|
92
|
+
let minute = DEFAULT_MINUTE;
|
|
93
|
+
if (rest) {
|
|
94
|
+
const timeParts = parseTime(rest);
|
|
95
|
+
if (!timeParts) {
|
|
96
|
+
throw new Error(`Invalid time format: '${rest}'. Use HH:MM (e.g. 14:00).`);
|
|
97
|
+
}
|
|
98
|
+
hour = timeParts.hour;
|
|
99
|
+
minute = timeParts.minute;
|
|
100
|
+
}
|
|
101
|
+
return berlinDate(now.year, now.month, now.day, hour, minute);
|
|
102
|
+
}
|
|
103
|
+
// "+3 days", "+1 week", "+2 weeks"
|
|
104
|
+
const relMatch = trimmed.match(/^\+(\d+)\s*(day|days|week|weeks)$/);
|
|
105
|
+
if (relMatch) {
|
|
106
|
+
const n = parseInt(relMatch[1], 10);
|
|
107
|
+
const unit = relMatch[2];
|
|
108
|
+
const days = unit.startsWith("week") ? n * 7 : n;
|
|
109
|
+
const d = new Date(now.year, now.month - 1, now.day + days);
|
|
110
|
+
return berlinDate(d.getFullYear(), d.getMonth() + 1, d.getDate(), DEFAULT_HOUR, DEFAULT_MINUTE);
|
|
111
|
+
}
|
|
112
|
+
// ISO 8601 with timezone offset: "2026-03-28T22:00:00+01:00", "2026-03-28T22:00:00Z"
|
|
113
|
+
const isoMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})t(\d{2}):(\d{2})(?::(\d{2}))?(?:z|([+-])(\d{2}):(\d{2}))$/);
|
|
114
|
+
if (isoMatch) {
|
|
115
|
+
const year = parseInt(isoMatch[1], 10);
|
|
116
|
+
const month = parseInt(isoMatch[2], 10);
|
|
117
|
+
const day = parseInt(isoMatch[3], 10);
|
|
118
|
+
const hour = parseInt(isoMatch[4], 10);
|
|
119
|
+
const minute = parseInt(isoMatch[5], 10);
|
|
120
|
+
const second = isoMatch[6] ? parseInt(isoMatch[6], 10) : 0;
|
|
121
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
122
|
+
throw new Error(`Invalid date: '${input}'.`);
|
|
123
|
+
}
|
|
124
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
125
|
+
throw new Error(`Invalid time in deadline: '${input}'.`);
|
|
126
|
+
}
|
|
127
|
+
// Validate date exists
|
|
128
|
+
const testDate = new Date(year, month - 1, day);
|
|
129
|
+
if (testDate.getFullYear() !== year || testDate.getMonth() !== month - 1 || testDate.getDate() !== day) {
|
|
130
|
+
throw new Error(`Invalid date: '${input}' — day ${day} does not exist in month ${month}.`);
|
|
131
|
+
}
|
|
132
|
+
// If 'Z' matched, offset is 0; otherwise parse sign/hours/minutes
|
|
133
|
+
const offsetSign = isoMatch[7] === "-" ? -1 : 1;
|
|
134
|
+
const offsetHours = isoMatch[8] ? parseInt(isoMatch[8], 10) : 0;
|
|
135
|
+
const offsetMinutes = isoMatch[9] ? parseInt(isoMatch[9], 10) : 0;
|
|
136
|
+
const totalOffsetMs = offsetSign * (offsetHours * 60 + offsetMinutes) * 60000;
|
|
137
|
+
// Compute UTC directly from the given offset
|
|
138
|
+
const utcMs = Date.UTC(year, month - 1, day, hour, minute, second) - totalOffsetMs;
|
|
139
|
+
return utcMs;
|
|
140
|
+
}
|
|
141
|
+
// "2026-03-15" or "2026-03-15 14:00"
|
|
142
|
+
const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{1,2}):(\d{2}))?$/);
|
|
143
|
+
if (dateMatch) {
|
|
144
|
+
const year = parseInt(dateMatch[1], 10);
|
|
145
|
+
const month = parseInt(dateMatch[2], 10);
|
|
146
|
+
const day = parseInt(dateMatch[3], 10);
|
|
147
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
148
|
+
throw new Error(`Invalid date: '${input}'.`);
|
|
149
|
+
}
|
|
150
|
+
// Validate the date actually exists (e.g. reject Feb 30)
|
|
151
|
+
const testDate = new Date(year, month - 1, day);
|
|
152
|
+
if (testDate.getFullYear() !== year || testDate.getMonth() !== month - 1 || testDate.getDate() !== day) {
|
|
153
|
+
throw new Error(`Invalid date: '${input}' — day ${day} does not exist in month ${month}.`);
|
|
154
|
+
}
|
|
155
|
+
const hour = dateMatch[4] ? parseInt(dateMatch[4], 10) : DEFAULT_HOUR;
|
|
156
|
+
const minute = dateMatch[5] ? parseInt(dateMatch[5], 10) : DEFAULT_MINUTE;
|
|
157
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
158
|
+
throw new Error(`Invalid time in deadline: '${input}'.`);
|
|
159
|
+
}
|
|
160
|
+
return berlinDate(year, month, day, hour, minute);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Cannot parse deadline '${input}'. ` +
|
|
163
|
+
`Accepted: "tomorrow", "today", "+3 days", "+1 week", "2026-03-15", "2026-03-15 14:00", "2026-03-15T14:00:00+01:00"`);
|
|
164
|
+
}
|
|
165
|
+
/** Default deadline: tomorrow 22:00 Berlin */
|
|
166
|
+
export function defaultDeadline() {
|
|
167
|
+
const now = berlinNowParts();
|
|
168
|
+
const d = new Date(now.year, now.month - 1, now.day + 1);
|
|
169
|
+
return berlinDate(d.getFullYear(), d.getMonth() + 1, d.getDate(), DEFAULT_HOUR, DEFAULT_MINUTE);
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=deadline.js.map
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output formatting.
|
|
3
|
+
*/
|
|
4
|
+
import type { Activity, EntityResult, OpenTaskSummary, SpaceInfo, Pin, TaskResult, UserInfo } from "./api.js";
|
|
5
|
+
export declare function formatDate(ts: number | undefined): string;
|
|
6
|
+
export declare function formatDateTime(ts: number | undefined): string;
|
|
7
|
+
export declare function formatEntityList(entities: EntityResult[], spaces?: Record<string, SpaceInfo>): string;
|
|
8
|
+
export declare function formatEntityDetail(data: {
|
|
9
|
+
entity: EntityResult["entity"];
|
|
10
|
+
pins: Pin[];
|
|
11
|
+
activities: Activity[];
|
|
12
|
+
openTasks?: OpenTaskSummary[];
|
|
13
|
+
descriptionPlainText?: string;
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function formatTaskList(tasks: TaskResult[], users?: Record<string, UserInfo>): string;
|
|
16
|
+
export declare function formatTaskDetail(task: TaskResult, users?: Record<string, UserInfo>): string;
|
|
17
|
+
export declare function formatActivityList(activities: Activity[]): string;
|
|
18
|
+
export declare function formatActivityDetail(activity: Activity): string;
|
|
19
|
+
export declare function formatSpacesFooter(spaces: Record<string, SpaceInfo>): string;
|
|
20
|
+
export declare function jsonOutputWithSpaces(data: unknown, spaces: Record<string, SpaceInfo>): string;
|
|
21
|
+
export declare function jsonOutput(data: unknown): string;
|