@calo-design/cli 0.13.6 → 0.13.7
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/backend.js +56 -6
- package/bin/cli.js +112 -12
- package/package.json +1 -1
package/bin/backend.js
CHANGED
|
@@ -105,9 +105,10 @@ function recipesDir(root) {
|
|
|
105
105
|
const listRecipes = (dir) => fs.readdirSync(dir).filter((r) => fs.existsSync(path.join(dir, r, "recipe.json")));
|
|
106
106
|
|
|
107
107
|
// Which resources a worker/ dir needs, inferred from the bindings its code references
|
|
108
|
-
// (env.DB → d1, env.KV → kv, env.BUCKET → r2). Used on the
|
|
109
|
-
// there's no recipe.json to read — a fork of ANY recipe
|
|
110
|
-
// not just d1
|
|
108
|
+
// (env.DB → d1, env.KV → kv, env.BUCKET → r2, env.OPENROUTER_API_KEY → ai). Used on the
|
|
109
|
+
// reconnect/fork path where there's no recipe.json to read — a fork of ANY recipe
|
|
110
|
+
// provisions the right resources, not just d1 (an ai fork mints its OWN capped key,
|
|
111
|
+
// never the parent's). Scans worker source (skips migrations/); migrations imply d1.
|
|
111
112
|
function inferResources(workerDir) {
|
|
112
113
|
const kinds = new Set();
|
|
113
114
|
const scan = (d) => {
|
|
@@ -121,6 +122,7 @@ function inferResources(workerDir) {
|
|
|
121
122
|
if (/\benv\.DB\b/.test(src)) kinds.add("d1");
|
|
122
123
|
if (/\benv\.KV\b/.test(src)) kinds.add("kv");
|
|
123
124
|
if (/\benv\.BUCKET\b/.test(src)) kinds.add("r2");
|
|
125
|
+
if (/\benv\.OPENROUTER_API_KEY\b/.test(src)) kinds.add("ai");
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
};
|
|
@@ -215,10 +217,18 @@ async function cmdInit(args) {
|
|
|
215
217
|
|
|
216
218
|
async function cmdAdd(args) {
|
|
217
219
|
const kind = (args[0] || "").toLowerCase();
|
|
218
|
-
if (!["d1", "kv", "r2"].includes(kind)) throw new Error("usage: calo-design backend add <d1|kv|r2>");
|
|
220
|
+
if (!["d1", "kv", "r2", "ai"].includes(kind)) throw new Error("usage: calo-design backend add <d1|kv|r2|ai>");
|
|
219
221
|
const root = process.cwd();
|
|
220
222
|
const slug = resolveSlug(args.slice(1), root);
|
|
221
223
|
const r = await call("POST", "/v1/backend/provision", { body: { slug, kind } });
|
|
224
|
+
if (kind === "ai") {
|
|
225
|
+
// The prototype's OWN capped OpenRouter key — minted by the broker, enforced by
|
|
226
|
+
// OpenRouter (monthly reset). Nothing to sign up for, nothing to paste.
|
|
227
|
+
ok(`ai ready — this prototype has its own $${(r.ai && r.ai.limitUsd) || 50}/month AI budget`);
|
|
228
|
+
log(c.dim(" key lands as a worker secret on the next `calo-design backend deploy`"));
|
|
229
|
+
log(c.dim(" spend is viewer-time: anyone with a share link can generate against the cap"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
222
232
|
ok(`${kind} ready ${c.dim(`(${(r.resources.find((x) => x.kind === kind) || {}).name || ""})`)}`);
|
|
223
233
|
log(c.dim(" binding lands on the next `calo-design backend deploy`"));
|
|
224
234
|
}
|
|
@@ -283,7 +293,18 @@ async function cmdStatus(args) {
|
|
|
283
293
|
log(` worker ${s.workerUrl}${s.deployHash ? "" : c.dim(" (never deployed)")}`);
|
|
284
294
|
if (s.deployHash) log(` deploy ${s.deployHash.slice(0, 12)} ${c.dim(new Date(s.deployedAt).toISOString())}`);
|
|
285
295
|
for (const r of s.resources) log(` ${r.kind.padEnd(8)} ${r.name}`);
|
|
286
|
-
if (
|
|
296
|
+
if (s.ai) {
|
|
297
|
+
// AI budget line: cap from the registry, month-to-date spend read live from
|
|
298
|
+
// OpenRouter (best-effort — the budget line still prints if the rollup fails).
|
|
299
|
+
let spend = "";
|
|
300
|
+
try {
|
|
301
|
+
const u = JSON.parse(await call("GET", "/v1/backend/ai-usage", { raw: true }));
|
|
302
|
+
const mine = (u.keys || []).find((k) => k.slug === s.slug);
|
|
303
|
+
if (mine && Number.isFinite(mine.usageMonthUsd)) spend = `$${mine.usageMonthUsd.toFixed(2)} used of `;
|
|
304
|
+
} catch {}
|
|
305
|
+
log(` ai ${spend}$${s.ai.limitUsd}/month AI budget${s.ai.disabled ? c.dim(" (disabled — `backend add ai` re-mints)") : ""}`);
|
|
306
|
+
}
|
|
307
|
+
if (!s.resources.length && !s.ai) log(c.dim(" no resources — `calo-design backend add <d1|kv|r2|ai>`"));
|
|
287
308
|
}
|
|
288
309
|
|
|
289
310
|
const parseSince = (v) => {
|
|
@@ -425,6 +446,33 @@ async function cmdAll(args) {
|
|
|
425
446
|
}
|
|
426
447
|
}
|
|
427
448
|
|
|
449
|
+
// AI spend across YOUR AI-enabled prototypes (broker reads each key's usage live
|
|
450
|
+
// from OpenRouter). Feeds Design Kitchen's Usage tab (`--json`) and the terminal.
|
|
451
|
+
async function cmdAiUsage(args) {
|
|
452
|
+
const text = await call("GET", "/v1/backend/ai-usage", { raw: true });
|
|
453
|
+
if (has(args, "json")) return emitJson(text);
|
|
454
|
+
const u = JSON.parse(text);
|
|
455
|
+
if (!u.keys.length) return log(c.dim("no AI-enabled prototypes — `calo-design backend add ai` gives one a budget"));
|
|
456
|
+
log(c.b(`AI budgets · ${u.totals.activeKeys}/${u.totals.maxKeys} slots used`));
|
|
457
|
+
for (const k of u.keys) {
|
|
458
|
+
const spend = Number.isFinite(k.usageMonthUsd) ? `$${k.usageMonthUsd.toFixed(2)}` : c.dim("(usage unavailable)");
|
|
459
|
+
log(` ${k.slug.padEnd(28)} ${spend} of $${k.limitUsd}/month${k.disabled ? c.dim(" (disabled)") : ""}`);
|
|
460
|
+
}
|
|
461
|
+
log(c.dim(` total this month: $${u.totals.usageMonthUsd.toFixed(2)} of $${u.totals.limitUsd} across active budgets`));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Kill switch for the prototype's AI key (leak, abuse, or freeing a quota slot).
|
|
465
|
+
// Disables upstream at OpenRouter — spend stops immediately, even though the secret
|
|
466
|
+
// is still on the worker. Re-mint = `backend add ai`, then `backend deploy`.
|
|
467
|
+
async function cmdAiDisable(args) {
|
|
468
|
+
const root = process.cwd();
|
|
469
|
+
const slug = resolveSlug(args, root);
|
|
470
|
+
const r = await call("POST", "/v1/backend/ai-disable", { body: { slug } });
|
|
471
|
+
if (r.alreadyDisabled) return ok("AI key already disabled");
|
|
472
|
+
ok("AI key disabled — spend stopped at OpenRouter");
|
|
473
|
+
log(c.dim(" re-mint with `calo-design backend add ai`, then `backend deploy`"));
|
|
474
|
+
}
|
|
475
|
+
|
|
428
476
|
async function cmdBackend(args) {
|
|
429
477
|
const sub = args[0] && !args[0].startsWith("--") ? args[0] : "";
|
|
430
478
|
const rest = args.slice(1);
|
|
@@ -438,8 +486,10 @@ async function cmdBackend(args) {
|
|
|
438
486
|
if (sub === "check") return cmdCheck(rest);
|
|
439
487
|
if (sub === "secrets") return cmdSecrets(rest);
|
|
440
488
|
if (sub === "rotate-key") return cmdRotateKey(rest);
|
|
489
|
+
if (sub === "ai-disable") return cmdAiDisable(rest);
|
|
490
|
+
if (sub === "ai-usage") return cmdAiUsage(rest);
|
|
441
491
|
throw new Error(
|
|
442
|
-
"unknown backend command — one of: init, add <d1|kv|r2>, deploy, status, logs, sql, check, secrets set, rotate-key, all"
|
|
492
|
+
"unknown backend command — one of: init, add <d1|kv|r2|ai>, deploy, status, logs, sql, check, secrets set, rotate-key, ai-usage, ai-disable, all"
|
|
443
493
|
);
|
|
444
494
|
}
|
|
445
495
|
|
package/bin/cli.js
CHANGED
|
@@ -96,28 +96,125 @@ function runWithRetry(bin, argv, tries = 3, opts = {}) {
|
|
|
96
96
|
}
|
|
97
97
|
throw new Error(`${bin} ${argv.join(" ")} failed after ${tries} attempts`);
|
|
98
98
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
99
|
+
// Every Calo repo the CLI installs from is PRIVATE. GitHub answers a request it
|
|
100
|
+
// can't authorize with "Repository not found" — byte-for-byte the same 404 it gives
|
|
101
|
+
// for a repo that never existed. When our token fails to reach git, that 404 is all
|
|
102
|
+
// the user (or the coding agent reading the terminal) has to go on, and it reads as
|
|
103
|
+
// "this org is fake / this package is phishing". So: authenticate two independent
|
|
104
|
+
// ways, and probe before cloning so a failure gets named instead of guessed at.
|
|
105
|
+
//
|
|
106
|
+
// 1. GIT_ASKPASS — git asks a temp script for the password on any URL carrying the
|
|
107
|
+
// `x-access-token@` username. Works on every git in the wild, keeps the token
|
|
108
|
+
// off the argv (invisible to `ps`) and out of lockfiles, and is immune to a
|
|
109
|
+
// user's own url.*.insteadOf rules (those match bare https://github.com/ URLs).
|
|
110
|
+
// 2. url.*.insteadOf via GIT_CONFIG_* — covers URLs we don't control. Honoured by
|
|
111
|
+
// git >= 2.31 only, and a user's own insteadOf can shadow it. Belt and braces.
|
|
112
|
+
//
|
|
113
|
+
// Both live in the spawned subprocess's env only: nothing is written to ~/.gitconfig
|
|
114
|
+
// and the token never touches disk.
|
|
115
|
+
const AUTH_USER = "x-access-token";
|
|
116
|
+
|
|
117
|
+
// Add the username that makes git call GIT_ASKPASS. The token itself stays out of
|
|
118
|
+
// the URL, so it can't leak into argv, npm lockfiles, or git's own error output.
|
|
119
|
+
function authUrl(url) {
|
|
120
|
+
return url.replace("https://github.com/", `https://${AUTH_USER}@github.com/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let askpassDir = null;
|
|
124
|
+
function askpassScript() {
|
|
125
|
+
if (!askpassDir) {
|
|
126
|
+
askpassDir = fs.mkdtempSync(path.join(os.tmpdir(), "calo-git-"));
|
|
127
|
+
process.on("exit", () => { try { fs.rmSync(askpassDir, { recursive: true, force: true }); } catch {} });
|
|
128
|
+
}
|
|
129
|
+
const win = process.platform === "win32";
|
|
130
|
+
const p = path.join(askpassDir, win ? "askpass.bat" : "askpass.sh");
|
|
131
|
+
if (!fs.existsSync(p)) {
|
|
132
|
+
fs.writeFileSync(p, win ? "@echo off\r\necho %CALO_GIT_TOKEN%\r\n" : '#!/bin/sh\nprintf "%s" "$CALO_GIT_TOKEN"\n');
|
|
133
|
+
try { fs.chmodSync(p, 0o700); } catch {}
|
|
134
|
+
}
|
|
135
|
+
return p;
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
function gitTokenEnv(token, base = process.env) {
|
|
103
139
|
// Append at the next free GIT_CONFIG index so we don't clobber any the caller set.
|
|
104
140
|
const n = parseInt(base.GIT_CONFIG_COUNT || "0", 10) || 0;
|
|
105
141
|
return {
|
|
106
142
|
...base,
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
// (1) askpass — what the URLs we build actually authenticate with.
|
|
144
|
+
GIT_ASKPASS: askpassScript(),
|
|
145
|
+
CALO_GIT_TOKEN: token,
|
|
146
|
+
// (2) insteadOf for bare github.com URLs, plus an empty credential.helper so a
|
|
147
|
+
// stale github.com entry in the user's keychain can't answer ahead of askpass.
|
|
148
|
+
GIT_CONFIG_COUNT: String(n + 2),
|
|
149
|
+
[`GIT_CONFIG_KEY_${n}`]: `url.https://${AUTH_USER}:${token}@github.com/.insteadOf`,
|
|
109
150
|
[`GIT_CONFIG_VALUE_${n}`]: "https://github.com/",
|
|
151
|
+
[`GIT_CONFIG_KEY_${n + 1}`]: "credential.helper",
|
|
152
|
+
[`GIT_CONFIG_VALUE_${n + 1}`]: "",
|
|
110
153
|
GIT_TERMINAL_PROMPT: "0"
|
|
111
154
|
};
|
|
112
155
|
}
|
|
113
156
|
|
|
157
|
+
function gitVersion() {
|
|
158
|
+
const r = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
159
|
+
const m = r.stdout && r.stdout.match(/(\d+)\.(\d+)/);
|
|
160
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Quiet reachability check against a repo we're about to install from, so the failure
|
|
164
|
+
// lands somewhere we can explain it rather than as a bare 404 in the middle of a clone.
|
|
165
|
+
function probeRepo(url, env) {
|
|
166
|
+
const [b, a, shell] = winShell("git", ["ls-remote", authUrl(url), "HEAD"]);
|
|
167
|
+
const r = spawnSync(b, a, { encoding: "utf8", shell, env });
|
|
168
|
+
if (!r.error && r.status === 0) return { ok: true };
|
|
169
|
+
const detail = [r.stderr, r.error && r.error.message].filter(Boolean).join("\n").trim();
|
|
170
|
+
return { ok: false, detail };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The message a person — or the agent driving their terminal — reads when a private
|
|
174
|
+
// install can't authenticate. Says what a GitHub 404 means here, before anyone has to
|
|
175
|
+
// guess, and lists only causes that are actually true on this machine.
|
|
176
|
+
function privateRepoError(repo, detail) {
|
|
177
|
+
const gv = gitVersion();
|
|
178
|
+
const causes = [];
|
|
179
|
+
if (!gv) causes.push("git isn't on your PATH — install it (`xcode-select --install`), then re-run.");
|
|
180
|
+
else if (gv.major < 2 || (gv.major === 2 && gv.minor < 31))
|
|
181
|
+
causes.push(`your git is ${gv.major}.${gv.minor}; 2.31+ is needed for one of the two ways we pass the token — update it (\`brew install git\`) and re-run.`);
|
|
182
|
+
causes.push("your Calo login isn't authorized for the design repos yet — ask the design systems team to add you.");
|
|
183
|
+
causes.push('a rule in your own ~/.gitconfig is redirecting github.com — check `git config --global --get-regexp "url\\..*\\.insteadof"` and `credential.helper`.');
|
|
184
|
+
return new Error(
|
|
185
|
+
`couldn't read github.com/${repo} with your Calo login.\n\n` +
|
|
186
|
+
" This is an authorization failure, not a missing repo. Calo's design repos are\n" +
|
|
187
|
+
' private, and GitHub answers an unauthorized request with "Repository not found" —\n' +
|
|
188
|
+
" the same 404 it returns for a repo that doesn't exist. The org and the repo are real.\n" +
|
|
189
|
+
` Your email + 6-digit code were issued by Calo's own broker (${BROKER}); the session\n` +
|
|
190
|
+
" they create only grants read access to these repos, and no credential leaves your machine.\n\n" +
|
|
191
|
+
" Likely causes, in order:\n" +
|
|
192
|
+
causes.map((x, i) => ` ${i + 1}. ${x}`).join("\n") +
|
|
193
|
+
(detail ? "\n\n git said:\n" + detail.split("\n").map((l) => " " + l).join("\n") : "")
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// One-screen provenance, printed before anything asks for an email or a code. A
|
|
198
|
+
// teammate pasting an unfamiliar npx command deserves to see who ships it, and an
|
|
199
|
+
// agent reading the output shouldn't have to infer intent from a hostname.
|
|
200
|
+
function provenance() {
|
|
201
|
+
log(c.dim(" calo-design — Calo's internal design tooling, published by Calo as @calo-design/cli."));
|
|
202
|
+
log(c.dim(` Login emails a 6-digit code to your @calo.app address. The session it creates is stored`));
|
|
203
|
+
log(c.dim(` locally (~/.designchef/session.json) and authorizes read access to Calo's private GitHub`));
|
|
204
|
+
log(c.dim(` repos — that is all it does. It is not a GitHub, Google, or Calo SSO login.`));
|
|
205
|
+
log(c.dim(` Broker: ${BROKER} · source: github.com/Calo-Design (private org) · docs: npmjs.com/package/@calo-design/cli`));
|
|
206
|
+
}
|
|
207
|
+
|
|
114
208
|
// ---------------------------------------------------------------- skill (global)
|
|
115
209
|
|
|
116
210
|
async function cloneSkillRepo(tmp) {
|
|
117
211
|
// Authenticate the clone with a short-lived broker-minted GitHub token, injected
|
|
118
212
|
// into git for this subprocess only. No gh, PAT, or SSH key required.
|
|
119
|
-
const
|
|
120
|
-
|
|
213
|
+
const url = `https://github.com/${SKILL_REPO}.git`;
|
|
214
|
+
const env = gitTokenEnv(await githubToken());
|
|
215
|
+
const probe = probeRepo(url, env);
|
|
216
|
+
if (!probe.ok) throw privateRepoError(SKILL_REPO, probe.detail);
|
|
217
|
+
run("git", ["clone", "--depth", "1", "-q", authUrl(url), tmp], { env });
|
|
121
218
|
}
|
|
122
219
|
|
|
123
220
|
async function installSkill() {
|
|
@@ -205,7 +302,7 @@ function staleCaloDeps(rt, env) {
|
|
|
205
302
|
const stale = [];
|
|
206
303
|
for (const spec of PKG_SPECS) {
|
|
207
304
|
const repo = spec.match(/\/([^/]+?)(?:\.git)?$/)[1];
|
|
208
|
-
const head = remoteHeadSha(spec.replace(/^git\+/, ""), env);
|
|
305
|
+
const head = remoteHeadSha(authUrl(spec.replace(/^git\+/, "")), env);
|
|
209
306
|
// Unknown local sha with a reachable remote also counts, so old runtimes self-heal.
|
|
210
307
|
if (head && head !== installed[repo]) stale.push(repo);
|
|
211
308
|
}
|
|
@@ -243,7 +340,7 @@ async function ensureRuntime({ force } = {}) {
|
|
|
243
340
|
try {
|
|
244
341
|
// Non-destructive: `npm install` of the git specs re-resolves HEAD and updates
|
|
245
342
|
// node_modules in place; a failure aborts cleanly and leaves the runtime working.
|
|
246
|
-
runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd: rt, env });
|
|
343
|
+
runWithRetry("npm", ["install", ...PKG_SPECS.map(authUrl), "--legacy-peer-deps"], 3, { cwd: rt, env });
|
|
247
344
|
let manifest = { pkgSpecs: PKG_SPECS, peers: PEERS };
|
|
248
345
|
try { manifest = { ...JSON.parse(fs.readFileSync(runtimeManifestPath(), "utf8")), ...manifest }; } catch {}
|
|
249
346
|
manifest.refreshedAt = new Date().toISOString();
|
|
@@ -339,7 +436,7 @@ async function installCaloDeps(cwd) {
|
|
|
339
436
|
// --legacy-peer-deps: the Expo/RN dep graph routinely has benign peerOptional conflicts
|
|
340
437
|
// (e.g. overlapping typescript ranges); without it a single conflict ERESOLVEs the whole install.
|
|
341
438
|
const env = gitTokenEnv(await githubToken());
|
|
342
|
-
runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd, env });
|
|
439
|
+
runWithRetry("npm", ["install", ...PKG_SPECS.map(authUrl), "--legacy-peer-deps"], 3, { cwd, env });
|
|
343
440
|
if (isExpoProject(cwd)) run("npx", ["expo", "install", ...PEERS, "expo-font"], { cwd });
|
|
344
441
|
else {
|
|
345
442
|
warn("non-Expo project — installing latest peers; pin them to match your React Native version if needed.");
|
|
@@ -564,6 +661,9 @@ function nextSteps({ linked, cd } = {}) {
|
|
|
564
661
|
}
|
|
565
662
|
|
|
566
663
|
async function cmdInit() {
|
|
664
|
+
// Only on a first run: the people who need the provenance are the ones who haven't
|
|
665
|
+
// logged in yet. Returning users have already made up their minds about us.
|
|
666
|
+
if (!loadSession()) provenance();
|
|
567
667
|
await ensureLoggedIn();
|
|
568
668
|
await installSkill();
|
|
569
669
|
if (has("--skip-packages")) return nextSteps({});
|
|
@@ -791,7 +891,7 @@ function help() {
|
|
|
791
891
|
${c.dim("open <owner>/<slug>[@v] [folder]")} unpack a teammate's checkpoint; your first save becomes your own fork
|
|
792
892
|
${c.dim("projects")} all shared prototypes (--mine, --json)
|
|
793
893
|
${c.dim("backend init --recipe <name>")} provision THIS prototype a real backend (Cloudflare worker + data; login only)
|
|
794
|
-
${c.dim(" backend add <d1|kv|r2> deploy status logs --since 10m --errors sql \"…\" check secrets set K --value v rotate-key")}
|
|
894
|
+
${c.dim(" backend add <d1|kv|r2|ai> deploy status logs --since 10m --errors sql \"…\" check secrets set K --value v rotate-key ai-disable")}
|
|
795
895
|
${c.dim("feedback [slug]")} feedback inbox for shared prototypes (open threads; --all, --json)
|
|
796
896
|
${c.dim(" feedback new <slug> <text> comment|resolve|reopen <threadId> shot <slug> <png>")}
|
|
797
897
|
${c.dim("tasks [slug]")} triaged work items (--status todo|in_progress|done|wontfix, --json)
|
|
@@ -807,7 +907,7 @@ function help() {
|
|
|
807
907
|
(async () => {
|
|
808
908
|
try {
|
|
809
909
|
if (cmd === "--version" || cmd === "-v" || cmd === "version") { console.log(require("../package.json").version); return; }
|
|
810
|
-
if (cmd === "login") await cmdLogin(args.slice(1));
|
|
910
|
+
if (cmd === "login") { provenance(); await cmdLogin(args.slice(1)); }
|
|
811
911
|
else if (cmd === "logout") cmdLogout();
|
|
812
912
|
else if (cmd === "init") await cmdInit();
|
|
813
913
|
else if (cmd === "update") await cmdUpdate();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
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"
|