@calo-design/cli 0.6.0 → 0.7.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/bin/cli.js +7 -0
- package/bin/feedback.js +193 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ const { cmdArtPush } = require("./art-push");
|
|
|
20
20
|
const { cmdShare } = require("./share");
|
|
21
21
|
const { cmdImage } = require("./imagegen");
|
|
22
22
|
const { cmdLogin, cmdLogout, ensureLoggedIn, githubToken, reportEvent, ensureSession, loadSession, BROKER } = require("./login");
|
|
23
|
+
const { cmdFeedback, cmdTasks } = require("./feedback");
|
|
23
24
|
|
|
24
25
|
const ORG = "Calo-Design";
|
|
25
26
|
const SKILL_REPO = `${ORG}/calo-design`;
|
|
@@ -779,6 +780,10 @@ function help() {
|
|
|
779
780
|
${c.dim(" share --slug x --dir dist --build --project calo-prototypes --dry-run")}
|
|
780
781
|
${c.dim("gallery")} serve the @calo/design-system component gallery locally (live from the runtime)
|
|
781
782
|
${c.dim(" gallery --port 8091")}
|
|
783
|
+
${c.dim("feedback [slug]")} feedback inbox for shared prototypes (open threads; --all, --json)
|
|
784
|
+
${c.dim(" feedback new <slug> <text> comment|resolve|reopen <threadId> shot <slug> <png>")}
|
|
785
|
+
${c.dim("tasks [slug]")} triaged work items (--status todo|in_progress|done|wontfix, --json)
|
|
786
|
+
${c.dim(" tasks new <slug> <title> --from <threadId> start|done|wontfix <id> done <id> --version v9")}
|
|
782
787
|
|
|
783
788
|
Login is required once before init; the session refreshes automatically.
|
|
784
789
|
init always installs the skill and ensures the shared ds/flows runtime on your machine;
|
|
@@ -803,6 +808,8 @@ function help() {
|
|
|
803
808
|
else if (cmd === "insights") await cmdInsights();
|
|
804
809
|
else if (cmd === "whoami") await cmdWhoami();
|
|
805
810
|
else if (cmd === "gallery") await cmdGallery();
|
|
811
|
+
else if (cmd === "feedback") await cmdFeedback(args.slice(1));
|
|
812
|
+
else if (cmd === "tasks") await cmdTasks(args.slice(1));
|
|
806
813
|
else help();
|
|
807
814
|
} catch (err) {
|
|
808
815
|
// Diagnostics go to stderr so stdout stays pure for machine callers (The Pass parses
|
package/bin/feedback.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `calo-design feedback …` / `calo-design tasks …` — prototype feedback threads and
|
|
5
|
+
* triaged tasks, stored broker-side (calo-broker docs/feedback-plan.md).
|
|
6
|
+
*
|
|
7
|
+
* Triple duty: humans in the terminal, Claude working the task queue, and Design
|
|
8
|
+
* Kitchen shelling out with --json (same bridge pattern as `feed --json`). All calls
|
|
9
|
+
* ride the existing broker session (ensureSession → Bearer).
|
|
10
|
+
*
|
|
11
|
+
* feedback [slug] inbox (open threads; --all for resolved too)
|
|
12
|
+
* feedback new <slug> <text…> plain thread (no pin)
|
|
13
|
+
* feedback comment <threadId> <text…>
|
|
14
|
+
* feedback resolve <threadId> | reopen <threadId>
|
|
15
|
+
* feedback shot <slug> <file.png> upload a pin screenshot → prints URL
|
|
16
|
+
* tasks [slug] list (--status todo|in_progress|done|wontfix)
|
|
17
|
+
* tasks new <slug> <title…> (--from <threadId> converts a thread, --body <text>)
|
|
18
|
+
* tasks start|done|wontfix <id> (done: --version <v> stamps closed_in_version)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
const { ensureSession, BROKER } = require("./login");
|
|
23
|
+
|
|
24
|
+
const c = { dim: (s) => `\x1b[2m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m`, g: (s) => `\x1b[32m${s}\x1b[0m`, y: (s) => `\x1b[33m${s}\x1b[0m` };
|
|
25
|
+
const log = (s = "") => console.log(s);
|
|
26
|
+
|
|
27
|
+
// Bounded fetch that speaks the broker's JSON envelope. GET+POST (login.js api() is
|
|
28
|
+
// POST-only); --json callers get the raw response text so the bridge never re-encodes.
|
|
29
|
+
async function call(method, pathname, { body, raw } = {}) {
|
|
30
|
+
const session = await ensureSession();
|
|
31
|
+
let res;
|
|
32
|
+
try {
|
|
33
|
+
res = await fetch(BROKER + pathname, {
|
|
34
|
+
method,
|
|
35
|
+
headers: {
|
|
36
|
+
authorization: `Bearer ${session}`,
|
|
37
|
+
...(body !== undefined && !Buffer.isBuffer(body) ? { "content-type": "application/json" } : {}),
|
|
38
|
+
...(Buffer.isBuffer(body) ? { "content-type": "image/png" } : {}),
|
|
39
|
+
},
|
|
40
|
+
body: body === undefined ? undefined : Buffer.isBuffer(body) ? body : JSON.stringify(body),
|
|
41
|
+
signal: AbortSignal.timeout(30000),
|
|
42
|
+
});
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
45
|
+
}
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
let json;
|
|
48
|
+
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
|
|
49
|
+
if (!res.ok) throw new Error(json.error || `${pathname} → HTTP ${res.status}`);
|
|
50
|
+
return raw ? text : json;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const flagVal = (args, name) => {
|
|
54
|
+
const i = args.indexOf(`--${name}`);
|
|
55
|
+
return i >= 0 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : undefined;
|
|
56
|
+
};
|
|
57
|
+
const stripFlags = (args) => args.filter((a, i, all) => !a.startsWith("--") && !(all[i - 1] || "").match(/^--(status|version|from|body|route|anchor|shot)$/));
|
|
58
|
+
|
|
59
|
+
const ago = (ts) => {
|
|
60
|
+
const s = Math.max(1, Math.floor(Date.now() / 1000) - ts);
|
|
61
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
62
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
63
|
+
return `${Math.floor(s / 86400)}d`;
|
|
64
|
+
};
|
|
65
|
+
const short = (id) => id.slice(0, 8);
|
|
66
|
+
// A thread's version is the deploy it was left on; slugs.last_version is current.
|
|
67
|
+
const stale = (t) => t.version && t.current_version && t.version !== t.current_version;
|
|
68
|
+
|
|
69
|
+
function printThread(t, { comments = false } = {}) {
|
|
70
|
+
const state = t.status === "resolved" ? c.g("✓ resolved") : c.y("● open");
|
|
71
|
+
const pin = t.anchor_json ? " 📌" : "";
|
|
72
|
+
const staleTag = stale(t) ? c.y(" [stale]") : "";
|
|
73
|
+
const task = t.task ? c.dim(` → task ${short(t.task.id)} (${t.task.status})`) : "";
|
|
74
|
+
log(`${state} ${c.b(t.slug)}${t.route ? c.dim(" " + t.route) : ""}${pin}${staleTag} ${c.dim(short(t.id))} ${c.dim(ago(t.created_at) + " ago")}${task}`);
|
|
75
|
+
const list = comments ? t.comments : t.comments.slice(0, 1);
|
|
76
|
+
for (const cm of list) {
|
|
77
|
+
const who = cm.kind === "system" ? c.dim("system") : cm.author.split("@")[0];
|
|
78
|
+
log(` ${who}: ${cm.kind === "system" ? c.dim(cm.body) : cm.body}`);
|
|
79
|
+
}
|
|
80
|
+
if (!comments && t.comments.length > 1) log(c.dim(` … ${t.comments.length - 1} more — calo-design feedback ${t.slug}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function cmdFeedback(args = []) {
|
|
84
|
+
const json = args.includes("--json");
|
|
85
|
+
const sub = args[0] && !args[0].startsWith("--") ? args[0] : "";
|
|
86
|
+
|
|
87
|
+
if (sub === "new") {
|
|
88
|
+
const [slug, ...words] = stripFlags(args.slice(1));
|
|
89
|
+
const body = words.join(" ").trim();
|
|
90
|
+
if (!slug || !body) throw new Error("usage: calo-design feedback new <slug> <text…> [--shot <png>] [--anchor <json>] [--route <path>] [--version <v>]");
|
|
91
|
+
// Pin threads (Kitchen Review view): upload the frozen frame first, then attach
|
|
92
|
+
// its URL — /v1/feedback/thread only accepts screenshot URLs minted by /shot.
|
|
93
|
+
let screenshotUrl;
|
|
94
|
+
const shotFile = flagVal(args, "shot");
|
|
95
|
+
if (shotFile) {
|
|
96
|
+
const png = fs.readFileSync(shotFile);
|
|
97
|
+
const up = await call("POST", `/v1/feedback/shot?slug=${encodeURIComponent(slug)}`, { body: png });
|
|
98
|
+
screenshotUrl = up.url;
|
|
99
|
+
}
|
|
100
|
+
let anchor;
|
|
101
|
+
const anchorRaw = flagVal(args, "anchor");
|
|
102
|
+
if (anchorRaw) {
|
|
103
|
+
try { anchor = JSON.parse(anchorRaw); } catch { throw new Error("--anchor must be a JSON object"); }
|
|
104
|
+
}
|
|
105
|
+
const r = await call("POST", "/v1/feedback/thread", {
|
|
106
|
+
body: { slug, body, screenshotUrl, anchor, route: flagVal(args, "route"), version: flagVal(args, "version") },
|
|
107
|
+
raw: json,
|
|
108
|
+
});
|
|
109
|
+
if (json) return process.stdout.write(r + "\n");
|
|
110
|
+
log(`${c.g("✓")} thread ${short(r.thread.id)} opened on ${c.b(slug)}${screenshotUrl ? c.dim(" (with screenshot)") : ""}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (sub === "comment" || sub === "resolve" || sub === "reopen") {
|
|
115
|
+
const [threadId, ...words] = stripFlags(args.slice(1));
|
|
116
|
+
if (!threadId) throw new Error(`usage: calo-design feedback ${sub} <threadId>${sub === "comment" ? " <text…>" : ""}`);
|
|
117
|
+
const path = { comment: "/v1/feedback/comment", resolve: "/v1/feedback/resolve", reopen: "/v1/feedback/reopen" }[sub];
|
|
118
|
+
const body = sub === "comment" ? { threadId, body: words.join(" ").trim() } : { threadId };
|
|
119
|
+
if (sub === "comment" && !body.body) throw new Error("comment text required");
|
|
120
|
+
const r = await call("POST", path, { body, raw: json });
|
|
121
|
+
if (json) return process.stdout.write(r + "\n");
|
|
122
|
+
log(`${c.g("✓")} ${sub === "comment" ? "commented" : sub + "ed"}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (sub === "shot") {
|
|
127
|
+
const [slug, file] = stripFlags(args.slice(1));
|
|
128
|
+
if (!slug || !file) throw new Error("usage: calo-design feedback shot <slug> <file.png>");
|
|
129
|
+
const png = fs.readFileSync(file);
|
|
130
|
+
const r = await call("POST", `/v1/feedback/shot?slug=${encodeURIComponent(slug)}`, { body: png, raw: json });
|
|
131
|
+
if (json) return process.stdout.write(r + "\n");
|
|
132
|
+
log(`${c.g("✓")} ${r.url}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// list: `feedback` (inbox: open everywhere) or `feedback <slug>` (--all incl. resolved)
|
|
137
|
+
const slug = sub || undefined;
|
|
138
|
+
const all = args.includes("--all");
|
|
139
|
+
const qs = new URLSearchParams();
|
|
140
|
+
if (slug) qs.set("slug", slug);
|
|
141
|
+
if (!all) qs.set("status", "open");
|
|
142
|
+
const r = await call("GET", `/v1/feedback?${qs}`, { raw: json });
|
|
143
|
+
if (json) return process.stdout.write(r + "\n");
|
|
144
|
+
const threads = r.threads;
|
|
145
|
+
if (!threads.length) return log(c.dim(slug ? `no ${all ? "" : "open "}feedback on ${slug}` : "inbox zero 🎉"));
|
|
146
|
+
log(c.b(`\n${threads.length} thread(s)${slug ? ` on ${slug}` : ""}${all ? "" : " (open)"}\n`));
|
|
147
|
+
for (const t of threads) printThread(t, { comments: !!slug });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function cmdTasks(args = []) {
|
|
151
|
+
const json = args.includes("--json");
|
|
152
|
+
const sub = args[0] && !args[0].startsWith("--") ? args[0] : "";
|
|
153
|
+
|
|
154
|
+
if (sub === "new") {
|
|
155
|
+
const [slug, ...words] = stripFlags(args.slice(1));
|
|
156
|
+
const title = words.join(" ").trim();
|
|
157
|
+
if (!slug || !title) throw new Error("usage: calo-design tasks new <slug> <title…> [--from <threadId>] [--body <text>]");
|
|
158
|
+
const body = { slug, title, sourceThread: flagVal(args, "from"), body: flagVal(args, "body") };
|
|
159
|
+
const r = await call("POST", "/v1/tasks", { body, raw: json });
|
|
160
|
+
if (json) return process.stdout.write(r + "\n");
|
|
161
|
+
log(`${c.g("✓")} task ${short(r.task.id)}: ${r.task.title}${r.task.source_thread ? c.dim(` (from thread ${short(r.task.source_thread)})`) : ""}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (["start", "done", "wontfix"].includes(sub)) {
|
|
166
|
+
const [id] = stripFlags(args.slice(1));
|
|
167
|
+
if (!id) throw new Error(`usage: calo-design tasks ${sub} <id>`);
|
|
168
|
+
const status = sub === "start" ? "in_progress" : sub;
|
|
169
|
+
const body = { id, status, ...(sub === "done" && flagVal(args, "version") ? { closedInVersion: flagVal(args, "version") } : {}) };
|
|
170
|
+
const r = await call("POST", "/v1/tasks/update", { body, raw: json });
|
|
171
|
+
if (json) return process.stdout.write(r + "\n");
|
|
172
|
+
log(`${c.g("✓")} ${r.task.title} → ${c.b(r.task.status)}${r.task.closed_in_version ? c.dim(` (${r.task.closed_in_version})`) : ""}`);
|
|
173
|
+
if (sub === "done") log(c.dim(" source thread (if any) auto-resolved"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const slug = sub || undefined;
|
|
178
|
+
const qs = new URLSearchParams();
|
|
179
|
+
if (slug) qs.set("slug", slug);
|
|
180
|
+
const status = flagVal(args, "status");
|
|
181
|
+
if (status) qs.set("status", status);
|
|
182
|
+
const r = await call("GET", `/v1/tasks?${qs}`, { raw: json });
|
|
183
|
+
if (json) return process.stdout.write(r + "\n");
|
|
184
|
+
const list = r.tasks;
|
|
185
|
+
if (!list.length) return log(c.dim("no tasks" + (slug ? ` on ${slug}` : "")));
|
|
186
|
+
log(c.b(`\n${list.length} task(s)${slug ? ` on ${slug}` : ""}\n`));
|
|
187
|
+
const badge = { todo: c.y("○ todo"), in_progress: c.b("◐ in progress"), done: c.g("● done"), wontfix: c.dim("✕ wontfix") };
|
|
188
|
+
for (const t of list) {
|
|
189
|
+
log(`${badge[t.status] || t.status} ${c.b(t.slug)} ${t.title} ${c.dim(short(t.id))}${t.source_thread ? c.dim(" ← " + short(t.source_thread)) : ""}${t.closed_in_version ? c.dim(" " + t.closed_in_version) : ""}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { cmdFeedback, cmdTasks };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "One-line setup for Calo design tooling: logs in with your Calo email and installs the calo-design skill + design-system packages. No GitHub account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"calo-design": "bin/cli.js"
|