@cadiraca/crowntrack-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.
Files changed (2) hide show
  1. package/dist/index.js +402 -0
  2. package/package.json +45 -0
package/dist/index.js ADDED
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import Table from "cli-table3";
7
+
8
+ // src/api.ts
9
+ var BASE_URL = process.env.CROWNTRACK_URL || "http://localhost:3333";
10
+ async function request(path, options) {
11
+ const url = `${BASE_URL}${path}`;
12
+ const res = await fetch(url, {
13
+ ...options,
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ ...options?.headers
17
+ }
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text();
21
+ throw new Error(`HTTP ${res.status}: ${text}`);
22
+ }
23
+ return res.json();
24
+ }
25
+ async function listProjects(filters) {
26
+ const params = new URLSearchParams();
27
+ if (filters?.status) params.set("status", filters.status);
28
+ if (filters?.category) params.set("category", filters.category);
29
+ if (filters?.priority) params.set("priority", filters.priority);
30
+ if (filters?.search) params.set("search", filters.search);
31
+ if (filters?.tag) params.set("tag", filters.tag);
32
+ const qs = params.toString();
33
+ return request(`/api/projects${qs ? `?${qs}` : ""}`);
34
+ }
35
+ async function getProject(id) {
36
+ return request(`/api/projects/${id}`);
37
+ }
38
+ async function createProject(data) {
39
+ return request("/api/projects", {
40
+ method: "POST",
41
+ body: JSON.stringify(data)
42
+ });
43
+ }
44
+ async function updateProject(id, data) {
45
+ return request(`/api/projects/${id}`, {
46
+ method: "PATCH",
47
+ body: JSON.stringify(data)
48
+ });
49
+ }
50
+ async function addAction(projectId, title, priority) {
51
+ return request(`/api/projects/${projectId}/actions`, {
52
+ method: "POST",
53
+ body: JSON.stringify({ title, priority })
54
+ });
55
+ }
56
+ async function completeAction(projectId, actionId) {
57
+ return request(`/api/projects/${projectId}/actions`, {
58
+ method: "PATCH",
59
+ body: JSON.stringify({ id: actionId, done: true })
60
+ });
61
+ }
62
+ async function addDecision(projectId, decision, context) {
63
+ return request(`/api/projects/${projectId}/decisions`, {
64
+ method: "POST",
65
+ body: JSON.stringify({ decision, context })
66
+ });
67
+ }
68
+ async function addNote(projectId, content) {
69
+ return request(`/api/projects/${projectId}/notes`, {
70
+ method: "POST",
71
+ body: JSON.stringify({ content })
72
+ });
73
+ }
74
+ async function getStats() {
75
+ return request("/api/stats");
76
+ }
77
+
78
+ // src/index.ts
79
+ var program = new Command();
80
+ var amber = chalk.hex("#f59e0b");
81
+ var dim = chalk.gray;
82
+ program.name("ct").description(amber("\u{1F451} CrownTrack CLI") + " \u2014 Rule your projects from the terminal").version("0.1.0");
83
+ program.command("projects").description("List all projects").option("-s, --status <status>", "Filter by status").option("-c, --category <category>", "Filter by category").option("-p, --priority <priority>", "Filter by priority").option("--search <query>", "Search by name").option("--tag <tag>", "Filter by tag").action(async (opts) => {
84
+ try {
85
+ const projects = await listProjects({
86
+ status: opts.status,
87
+ category: opts.category,
88
+ priority: opts.priority,
89
+ search: opts.search,
90
+ tag: opts.tag
91
+ });
92
+ if (projects.length === 0) {
93
+ console.log(dim("No projects found."));
94
+ return;
95
+ }
96
+ const table = new Table({
97
+ head: [
98
+ dim("ID"),
99
+ amber("Name"),
100
+ "Status",
101
+ "Priority",
102
+ "Category",
103
+ "Actions",
104
+ "Updated"
105
+ ],
106
+ style: { head: [], border: ["gray"] }
107
+ });
108
+ for (const p of projects) {
109
+ const open = p.actions.filter((a) => !a.done).length;
110
+ const total = p.actions.length;
111
+ table.push([
112
+ dim(p.id.slice(0, 8)),
113
+ p.name,
114
+ statusColor(p.status),
115
+ priorityColor(p.priority),
116
+ p.category || "-",
117
+ `${total - open}/${total}`,
118
+ timeAgo(p.updatedAt)
119
+ ]);
120
+ }
121
+ console.log(table.toString());
122
+ console.log(dim(`
123
+ ${projects.length} project(s)`));
124
+ } catch (e) {
125
+ handleError(e);
126
+ }
127
+ });
128
+ program.command("project <id>").description("Show project details").action(async (id) => {
129
+ try {
130
+ const p = await resolveProject(id);
131
+ console.log();
132
+ console.log(amber("\u{1F451} " + p.name));
133
+ if (p.description) console.log(dim(p.description));
134
+ console.log();
135
+ console.log(
136
+ ` Status: ${statusColor(p.status)} | Priority: ${priorityColor(p.priority)} | Category: ${p.category || "-"}`
137
+ );
138
+ if (p.tags) console.log(` Tags: ${p.tags.split(",").map((t) => chalk.cyan(`#${t.trim()}`)).join(" ")}`);
139
+ console.log(` Created: ${dim(new Date(p.createdAt).toLocaleDateString())}`);
140
+ console.log(` Updated: ${dim(timeAgo(p.updatedAt))}`);
141
+ console.log(` ID: ${dim(p.id)}`);
142
+ if (p.actions.length > 0) {
143
+ console.log(`
144
+ ${amber("\u2705 Actions")} (${p.actions.filter((a) => !a.done).length} open / ${p.actions.length} total)`);
145
+ for (const a of p.actions) {
146
+ const check = a.done ? chalk.green("\u2713") : chalk.gray("\u25CB");
147
+ const text = a.done ? dim(a.title) : a.title;
148
+ console.log(` ${check} ${text} ${dim(`[${a.id.slice(0, 8)}]`)}`);
149
+ }
150
+ }
151
+ if (p.decisions.length > 0) {
152
+ console.log(`
153
+ ${amber("\u{1F4CB} Decisions")} (${p.decisions.length})`);
154
+ for (const d of p.decisions) {
155
+ console.log(` \u2022 ${d.decision}`);
156
+ if (d.context) console.log(` ${dim(d.context)}`);
157
+ console.log(` ${dim(new Date(d.date).toLocaleDateString())}`);
158
+ }
159
+ }
160
+ if (p.notes.length > 0) {
161
+ console.log(`
162
+ ${amber("\u{1F4DD} Notes")} (${p.notes.length})`);
163
+ for (const n of p.notes) {
164
+ const preview = n.content.length > 100 ? n.content.slice(0, 100) + "..." : n.content;
165
+ console.log(` \u2022 ${preview}`);
166
+ console.log(` ${dim(new Date(n.createdAt).toLocaleDateString())}`);
167
+ }
168
+ }
169
+ if (p.links.length > 0) {
170
+ console.log(`
171
+ ${amber("\u{1F517} Links")} (${p.links.length})`);
172
+ for (const l of p.links) {
173
+ console.log(` \u2022 ${l.label}: ${chalk.cyan(l.url)}`);
174
+ }
175
+ }
176
+ console.log();
177
+ } catch (e) {
178
+ handleError(e);
179
+ }
180
+ });
181
+ var add = program.command("add").description("Add items");
182
+ add.command("project <name>").description("Create a new project").option("-s, --status <status>", "Status (Idea/Planning/Building/Deployed/Paused)", "Idea").option("-p, --priority <priority>", "Priority (High/Medium/Low)", "Medium").option("-c, --category <category>", "Category (Personal/Professional/Learning/Infrastructure)", "Personal").option("-d, --description <desc>", "Description").option("-t, --tags <tags>", "Tags (comma-separated)").action(async (name, opts) => {
183
+ try {
184
+ const project = await createProject({
185
+ name,
186
+ description: opts.description,
187
+ status: opts.status,
188
+ priority: opts.priority,
189
+ category: opts.category,
190
+ tags: opts.tags
191
+ });
192
+ console.log(chalk.green("\u2713") + ` Created project: ${amber(project.name)} ${dim(`[${project.id}]`)}`);
193
+ } catch (e) {
194
+ handleError(e);
195
+ }
196
+ });
197
+ add.command("action <project-id> <title>").description("Add an action to a project").option("-p, --priority <priority>", "Priority", "Medium").action(async (projectId, title, opts) => {
198
+ try {
199
+ const p = await resolveProject(projectId);
200
+ const action = await addAction(p.id, title, opts.priority);
201
+ console.log(chalk.green("\u2713") + ` Added action to ${amber(p.name)}: ${action.title} ${dim(`[${action.id}]`)}`);
202
+ } catch (e) {
203
+ handleError(e);
204
+ }
205
+ });
206
+ add.command("decision <project-id> <decision>").description("Log a decision for a project").option("--context <context>", "Why was this decided?").action(async (projectId, decision, opts) => {
207
+ try {
208
+ const p = await resolveProject(projectId);
209
+ const d = await addDecision(p.id, decision, opts.context);
210
+ console.log(chalk.green("\u2713") + ` Logged decision for ${amber(p.name)}: ${d.decision} ${dim(`[${d.id}]`)}`);
211
+ } catch (e) {
212
+ handleError(e);
213
+ }
214
+ });
215
+ add.command("note <project-id> <content>").description("Add a note to a project").action(async (projectId, content) => {
216
+ try {
217
+ const p = await resolveProject(projectId);
218
+ const n = await addNote(p.id, content);
219
+ console.log(chalk.green("\u2713") + ` Added note to ${amber(p.name)} ${dim(`[${n.id}]`)}`);
220
+ } catch (e) {
221
+ handleError(e);
222
+ }
223
+ });
224
+ var complete = program.command("complete").description("Mark items as done");
225
+ complete.command("action <action-id>").description("Mark an action as complete").action(async (actionId) => {
226
+ try {
227
+ const projects = await listProjects();
228
+ let found = false;
229
+ for (const p of projects) {
230
+ const action = p.actions.find(
231
+ (a) => a.id === actionId || a.id.startsWith(actionId)
232
+ );
233
+ if (action) {
234
+ const result = await completeAction(p.id, action.id);
235
+ console.log(chalk.green("\u2713") + ` Completed: ${dim(result.title)} in ${amber(p.name)}`);
236
+ found = true;
237
+ break;
238
+ }
239
+ }
240
+ if (!found) {
241
+ console.log(chalk.red("\u2715") + ` Action not found: ${actionId}`);
242
+ process.exit(1);
243
+ }
244
+ } catch (e) {
245
+ handleError(e);
246
+ }
247
+ });
248
+ var update = program.command("update").description("Update items");
249
+ update.command("project <id>").description("Update project fields").option("-s, --status <status>", "Status").option("-p, --priority <priority>", "Priority").option("-c, --category <category>", "Category").option("-n, --name <name>", "Name").option("-d, --description <desc>", "Description").option("-t, --tags <tags>", "Tags").action(async (id, opts) => {
250
+ try {
251
+ const p = await resolveProject(id);
252
+ const data = {};
253
+ if (opts.status) data.status = opts.status;
254
+ if (opts.priority) data.priority = opts.priority;
255
+ if (opts.category) data.category = opts.category;
256
+ if (opts.name) data.name = opts.name;
257
+ if (opts.description) data.description = opts.description;
258
+ if (opts.tags) data.tags = opts.tags;
259
+ if (Object.keys(data).length === 0) {
260
+ console.log(dim("Nothing to update. Use --status, --priority, --category, etc."));
261
+ return;
262
+ }
263
+ const updated = await updateProject(p.id, data);
264
+ console.log(chalk.green("\u2713") + ` Updated ${amber(updated.name)}`);
265
+ for (const [k, v] of Object.entries(data)) {
266
+ console.log(` ${dim(k)}: ${v}`);
267
+ }
268
+ } catch (e) {
269
+ handleError(e);
270
+ }
271
+ });
272
+ program.command("stats").description("Show KPI summary").action(async () => {
273
+ try {
274
+ const s = await getStats();
275
+ console.log();
276
+ console.log(amber("\u{1F451} CrownTrack Stats"));
277
+ console.log();
278
+ const projTable = new Table({
279
+ style: { head: [], border: ["gray"] }
280
+ });
281
+ projTable.push(
282
+ [amber("Total Projects"), String(s.totalProjects)],
283
+ [amber("Open Actions"), String(s.openActions)],
284
+ [chalk.green("Completed Actions"), String(s.completedActions)],
285
+ [s.overdueActions > 0 ? chalk.red("Overdue Actions") : dim("Overdue Actions"), String(s.overdueActions)],
286
+ [amber("Decisions (Week)"), String(s.decisionsThisWeek)],
287
+ [amber("Decisions (Month)"), String(s.decisionsThisMonth)]
288
+ );
289
+ console.log(projTable.toString());
290
+ console.log(`
291
+ ${amber("Projects by Status:")}`);
292
+ const statusTable = new Table({
293
+ head: [dim("Status"), dim("Count"), dim("Bar")],
294
+ style: { head: [], border: ["gray"] }
295
+ });
296
+ const maxCount = Math.max(...Object.values(s.byStatus), 1);
297
+ const statusEmoji = {
298
+ Idea: "\u{1F4A1}",
299
+ Planning: "\u{1F4D0}",
300
+ Building: "\u{1F528}",
301
+ Deployed: "\u{1F680}",
302
+ Paused: "\u23F8\uFE0F"
303
+ };
304
+ for (const status of ["Building", "Planning", "Idea", "Deployed", "Paused"]) {
305
+ const count = s.byStatus[status] || 0;
306
+ const bar = amber("\u2588".repeat(Math.ceil(count / maxCount * 20)));
307
+ statusTable.push([
308
+ `${statusEmoji[status] || ""} ${status}`,
309
+ String(count),
310
+ bar
311
+ ]);
312
+ }
313
+ console.log(statusTable.toString());
314
+ if (Object.keys(s.byCategory).length > 0) {
315
+ console.log(`
316
+ ${amber("Projects by Category:")}`);
317
+ const catTable = new Table({
318
+ head: [dim("Category"), dim("Count")],
319
+ style: { head: [], border: ["gray"] }
320
+ });
321
+ for (const [cat, count] of Object.entries(s.byCategory)) {
322
+ catTable.push([cat, String(count)]);
323
+ }
324
+ console.log(catTable.toString());
325
+ }
326
+ if (s.staleProjects.length > 0) {
327
+ console.log(`
328
+ ${chalk.red("\u26A0\uFE0F Stale Projects")} (no updates > 7 days):`);
329
+ for (const sp of s.staleProjects) {
330
+ console.log(` \u2022 ${sp.name} \u2014 ${chalk.red(`${sp.daysSinceUpdate} days`)} since last update`);
331
+ }
332
+ } else {
333
+ console.log(`
334
+ ${chalk.green("\u2713")} All active projects updated recently`);
335
+ }
336
+ console.log();
337
+ } catch (e) {
338
+ handleError(e);
339
+ }
340
+ });
341
+ async function resolveProject(idOrPrefix) {
342
+ try {
343
+ return await getProject(idOrPrefix);
344
+ } catch {
345
+ const projects = await listProjects();
346
+ const matches = projects.filter(
347
+ (p) => p.id.startsWith(idOrPrefix) || p.name.toLowerCase().includes(idOrPrefix.toLowerCase())
348
+ );
349
+ if (matches.length === 1) return matches[0];
350
+ if (matches.length === 0) {
351
+ console.log(chalk.red("\u2715") + ` No project found for: ${idOrPrefix}`);
352
+ process.exit(1);
353
+ }
354
+ console.log(chalk.yellow("?") + ` Multiple matches for "${idOrPrefix}":`);
355
+ for (const m of matches) {
356
+ console.log(` ${dim(m.id.slice(0, 8))} ${m.name}`);
357
+ }
358
+ process.exit(1);
359
+ }
360
+ }
361
+ function statusColor(status) {
362
+ const colors = {
363
+ Idea: chalk.magenta,
364
+ Planning: chalk.blue,
365
+ Building: amber,
366
+ Deployed: chalk.green,
367
+ Paused: chalk.gray
368
+ };
369
+ return (colors[status] || chalk.white)(status);
370
+ }
371
+ function priorityColor(priority) {
372
+ const colors = {
373
+ High: chalk.red,
374
+ Medium: amber,
375
+ Low: chalk.green
376
+ };
377
+ return (colors[priority] || chalk.white)(priority);
378
+ }
379
+ function timeAgo(dateStr) {
380
+ const now = Date.now();
381
+ const then = new Date(dateStr).getTime();
382
+ const diff = now - then;
383
+ const mins = Math.floor(diff / 6e4);
384
+ if (mins < 1) return "just now";
385
+ if (mins < 60) return `${mins}m ago`;
386
+ const hours = Math.floor(mins / 60);
387
+ if (hours < 24) return `${hours}h ago`;
388
+ const days = Math.floor(hours / 24);
389
+ if (days < 7) return `${days}d ago`;
390
+ return `${Math.floor(days / 7)}w ago`;
391
+ }
392
+ function handleError(e) {
393
+ const msg = e instanceof Error ? e.message : String(e);
394
+ if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
395
+ console.log(chalk.red("\u2715") + " Cannot connect to CrownTrack at " + dim(process.env.CROWNTRACK_URL || "http://localhost:3333"));
396
+ console.log(dim(" Is the server running? Try: pnpm dev"));
397
+ } else {
398
+ console.log(chalk.red("\u2715") + ` Error: ${msg}`);
399
+ }
400
+ process.exit(1);
401
+ }
402
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cadiraca/crowntrack-cli",
3
+ "version": "0.1.0",
4
+ "description": "👑 CrownTrack CLI — Rule your projects from the terminal",
5
+ "author": "Carlos Diego Ramírez <cadiraca>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "crowntrack": "./dist/index.js",
10
+ "ct": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "keywords": [
16
+ "project-tracker",
17
+ "cli",
18
+ "crowntrack",
19
+ "productivity"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/cadiraca/crowntrack",
24
+ "directory": "cli"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format esm --target node20 --clean",
31
+ "prepublishOnly": "npm run build",
32
+ "dev": "tsx src/index.ts"
33
+ },
34
+ "dependencies": {
35
+ "chalk": "^5.4.1",
36
+ "cli-table3": "^0.6.5",
37
+ "commander": "^13.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "tsup": "^8.4.0",
42
+ "tsx": "^4.21.0",
43
+ "typescript": "^5.8.0"
44
+ }
45
+ }