@calo-design/cli 0.2.1 → 0.3.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 +67 -40
- package/bin/cli.js +57 -25
- package/bin/login.js +29 -1
- package/bin/share.js +170 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,54 +1,57 @@
|
|
|
1
|
-
# @calo-design/cli
|
|
1
|
+
# @calo-design/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
design-system
|
|
6
|
-
|
|
3
|
+
One-line setup for Calo design tooling. It's a small **installer**, not a runtime: it
|
|
4
|
+
logs you in with your Calo email, installs the `calo-design` Claude Code skill, and sets
|
|
5
|
+
up the design-system stack (`@calo/design-system` + `@calo/flows` + the React Native
|
|
6
|
+
peers) on your machine. After that it gets out of the way — Claude Code is the runtime,
|
|
7
|
+
the skill is the guidance.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
Access is gated by your `@calo.app` email through the Calo broker, which mints a
|
|
10
|
+
short-lived GitHub token at install time. No GitHub account, PAT, or SSH key needed.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Package: https://www.npmjs.com/package/@calo-design/cli
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
|
-
npx @calo-design/cli login
|
|
15
|
-
npx @calo-design/cli init
|
|
17
|
+
npx @calo-design/cli login # one-time: your Calo email + the emailed code
|
|
18
|
+
npx @calo-design/cli init # skill + the shared ds/flows runtime, and a prototype here
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
`init` does two machine-level things every time: it installs the skill into
|
|
22
|
+
`~/.claude/skills/`, and it ensures the shared runtime at `~/.designchef/runtime/` — one
|
|
23
|
+
copy of `@calo/design-system`, `@calo/flows`, Expo and the peers, shared by every
|
|
24
|
+
prototype. Then, if the current folder is empty or an existing Expo project, it links a
|
|
25
|
+
prototype here that points at that runtime.
|
|
26
|
+
|
|
27
|
+
To start a prototype from inside a workspace folder (where the current directory isn't
|
|
28
|
+
empty), give it a name and `init` creates the subfolder for you:
|
|
20
29
|
|
|
21
30
|
```bash
|
|
22
|
-
npx @calo-design/cli init
|
|
23
|
-
|
|
24
|
-
npx
|
|
31
|
+
npx @calo-design/cli init my-prototype # creates ./my-prototype/ and links it
|
|
32
|
+
cd my-prototype
|
|
33
|
+
npx expo start # press w for web, or scan the QR in Expo Go
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
automatically. Override the broker with `CALO_BROKER_URL` (used for local dev).
|
|
29
|
-
|
|
30
|
-
## Publish
|
|
31
|
-
|
|
32
|
-
Share a prototype with the team via the [Calo Mirror](https://github.com/Calo-Design/calo-design-mirror).
|
|
33
|
-
Run from **inside a standalone prototype folder** (a plain Expo app with a `src/app/` route dir):
|
|
36
|
+
Other commands:
|
|
34
37
|
|
|
35
38
|
```bash
|
|
36
|
-
calo-design
|
|
39
|
+
npx @calo-design/cli init --skip-packages # skill only, no runtime
|
|
40
|
+
npx @calo-design/cli init --isolated # install the stack into THIS folder (no shared runtime)
|
|
41
|
+
npx @calo-design/cli update # refresh the skill + re-pin the shared runtime
|
|
42
|
+
npx @calo-design/cli logout # forget the saved session
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
The login session lives at `~/.designchef/session.json` (chmod 600) and refreshes
|
|
46
|
+
automatically. Set `CALO_BROKER_URL` to point at a local broker for development.
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
channel in the shared project (creating the channel on first push; latest wins on re-push).
|
|
44
|
-
Bundling runs locally; auth uses the broker-vended Expo token.
|
|
45
|
-
- Hands the registry entry (and `screenshot.png` if present) to the broker, which upserts the
|
|
46
|
-
Tigris `index.json` feed so the prototype shows up in the Mirror's browsable list.
|
|
48
|
+
## Building a screen
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
Start a Claude Code session in a prototype folder, load the skill with `/calo-design`,
|
|
51
|
+
and describe the screen — for example, "build a Calo-native meal-swap screen." The skill
|
|
52
|
+
composes it from the design-system primitives.
|
|
50
53
|
|
|
51
|
-
Fonts ship with `@calo/design-system
|
|
54
|
+
Fonts ship with `@calo/design-system`; load them in your root layout:
|
|
52
55
|
|
|
53
56
|
```tsx
|
|
54
57
|
import { useFonts } from "expo-font";
|
|
@@ -56,20 +59,44 @@ import { caloFonts } from "@calo/design-system";
|
|
|
56
59
|
const [fontsLoaded] = useFonts(caloFonts);
|
|
57
60
|
```
|
|
58
61
|
|
|
62
|
+
## Publishing to the Mirror
|
|
63
|
+
|
|
64
|
+
Run from inside a standalone prototype folder (a plain Expo app with a `src/app/` route
|
|
65
|
+
directory):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
calo-design push --slug <slug> --title "<Title>" --owner "<name>" --message "<msg>"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
No EAS or Tigris credentials needed — just `calo-design login`. The broker vends the Expo
|
|
72
|
+
token and writes the registry server-side. Under the hood it:
|
|
73
|
+
|
|
74
|
+
- runs `eas update --branch <slug>` to publish the prototype's JS bundle to its own EAS
|
|
75
|
+
Update channel in the shared project (created on first push; latest wins on re-push).
|
|
76
|
+
Bundling runs locally; auth uses the broker-vended Expo token.
|
|
77
|
+
- hands the registry entry (and `screenshot.png` if present) to the broker, which upserts
|
|
78
|
+
the Tigris `index.json` feed so the prototype appears in the Mirror's browsable list.
|
|
79
|
+
|
|
80
|
+
`--dry-run` stages a managed copy and prints what would happen without publishing.
|
|
81
|
+
`--direct` is the legacy path that publishes from your own local EAS + Tigris credentials
|
|
82
|
+
(maintainer/debug).
|
|
83
|
+
|
|
59
84
|
## How access works
|
|
60
85
|
|
|
61
86
|
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
login → broker verifies @calo.app + emails a code → session JWT in ~/.designchef
|
|
88
|
+
init → broker mints a 1-hour read-only GitHub token → private installs
|
|
89
|
+
push → broker vends the Expo token + writes the Mirror registry server-side
|
|
65
90
|
```
|
|
66
91
|
|
|
67
|
-
The broker
|
|
68
|
-
|
|
92
|
+
The broker is the only place the GitHub / Expo / Tigris secrets live.
|
|
93
|
+
|
|
94
|
+
## Local development
|
|
69
95
|
|
|
70
|
-
|
|
96
|
+
Point the CLI at a local broker and run the client-spine test:
|
|
71
97
|
|
|
72
98
|
```bash
|
|
73
|
-
|
|
99
|
+
cd ../calo-broker && npm run dev # one shell
|
|
100
|
+
CALO_BROKER_URL=http://localhost:8080 npx @calo-design/cli login # another shell
|
|
74
101
|
BASE=http://localhost:8080 bash test/client-spine.sh
|
|
75
102
|
```
|
package/bin/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ const fs = require("node:fs");
|
|
|
16
16
|
const os = require("node:os");
|
|
17
17
|
const path = require("node:path");
|
|
18
18
|
const { cmdPush } = require("./mirror-push");
|
|
19
|
+
const { cmdShare } = require("./share");
|
|
19
20
|
const { cmdLogin, cmdLogout, ensureLoggedIn, githubToken } = require("./login");
|
|
20
21
|
|
|
21
22
|
const ORG = "Calo-Design";
|
|
@@ -369,18 +370,20 @@ function isExistingProject() {
|
|
|
369
370
|
|
|
370
371
|
// ---------------------------------------------------------------- commands
|
|
371
372
|
|
|
372
|
-
function nextSteps({ linked } = {}) {
|
|
373
|
+
function nextSteps({ linked, cd } = {}) {
|
|
373
374
|
log(c.b("\n✨ Done. In Claude Code — no restart needed:"));
|
|
374
|
-
|
|
375
|
+
let n = 1;
|
|
376
|
+
if (cd) log(`\n ${c.b(n++ + ".")} Enter the prototype — run ${c.b(c.g("cd " + cd))}`);
|
|
377
|
+
log(`\n ${c.b(n++ + ".")} Load the skill — type:`);
|
|
375
378
|
log(` ${c.b(c.g("/calo-design"))}`);
|
|
376
|
-
log(`\n ${c.b("
|
|
379
|
+
log(`\n ${c.b(n++ + ".")} Create a prototype — describe the screen, e.g.:`);
|
|
377
380
|
log(` ${c.b(c.g("build a Calo-native meal-swap screen"))}`);
|
|
378
|
-
log(`\n ${c.b("
|
|
381
|
+
log(`\n ${c.b(n++ + ".")} Preview — run ${c.dim("npx expo start")} (press w for web, or scan the QR in Expo Go).`);
|
|
379
382
|
if (linked) {
|
|
380
383
|
log(c.dim(`\n This prototype shares one install at ${tilde(runtimeDir())} — \`calo-design update\` refreshes it for every prototype.`));
|
|
381
384
|
log(c.dim(" (expo-doctor may flag dependency versions on linked projects; that's expected and harmless.)"));
|
|
382
385
|
}
|
|
383
|
-
log(c.dim(`\n Docs: https://
|
|
386
|
+
log(c.dim(`\n Docs: https://www.npmjs.com/package/@calo-design/cli`));
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
async function cmdInit() {
|
|
@@ -389,40 +392,63 @@ async function cmdInit() {
|
|
|
389
392
|
if (has("--skip-packages")) return nextSteps({});
|
|
390
393
|
if (has("--isolated")) { await installHere(); return nextSteps({}); }
|
|
391
394
|
|
|
392
|
-
|
|
393
|
-
|
|
395
|
+
// `init <name>`: create ./<name>/ and set the prototype up there, so you can spin
|
|
396
|
+
// one up from inside a workspace container without cd-ing into an empty folder first.
|
|
397
|
+
const targetName = args.slice(1).find((a) => !a.startsWith("-"));
|
|
398
|
+
if (targetName) {
|
|
399
|
+
const dir = path.resolve(process.cwd(), targetName);
|
|
400
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
401
|
+
process.chdir(dir);
|
|
402
|
+
log(c.dim(`\n Prototype folder: ${tilde(dir)}`));
|
|
403
|
+
}
|
|
404
|
+
const cd = targetName;
|
|
405
|
+
|
|
406
|
+
// Machine setup runs every time, independent of this folder: the skill is installed
|
|
407
|
+
// above; ensure the shared runtime so @calo/design-system + @calo/flows live on this
|
|
408
|
+
// machine (one copy, shared by every prototype).
|
|
409
|
+
let runtimeReady = true;
|
|
410
|
+
try {
|
|
394
411
|
await ensureRuntime({ force: false });
|
|
395
|
-
|
|
396
|
-
|
|
412
|
+
} catch (e) {
|
|
413
|
+
runtimeReady = false;
|
|
414
|
+
warn(`couldn't build the shared runtime (${e.message}).`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Per-folder prototype (a separate, optional step): link or scaffold one here.
|
|
418
|
+
if (isLinkedProject()) {
|
|
419
|
+
// Re-running in an already-linked prototype: refresh the link if the runtime is ok.
|
|
420
|
+
if (runtimeReady) linkProject(process.cwd());
|
|
421
|
+
else warn("shared runtime unavailable — left the existing link untouched.");
|
|
422
|
+
return nextSteps({ linked: true, cd });
|
|
397
423
|
}
|
|
398
424
|
if (isExistingProject()) {
|
|
399
425
|
warn("existing project detected — installing the Calo stack here (not linking), to avoid clobbering your node_modules.");
|
|
400
426
|
await installCaloDeps(process.cwd());
|
|
401
427
|
ok("packages + peers installed in this project");
|
|
402
|
-
return nextSteps({});
|
|
428
|
+
return nextSteps({ cd });
|
|
403
429
|
}
|
|
404
430
|
if (!dirIsScaffoldable()) {
|
|
405
|
-
|
|
406
|
-
|
|
431
|
+
// Machine setup is done; we just won't scaffold a prototype into a non-empty,
|
|
432
|
+
// non-Expo folder (e.g. a workspace container). Say what's ready and how to proceed.
|
|
433
|
+
log("");
|
|
434
|
+
ok(`Machine setup complete — skill installed${runtimeReady ? ", ds + flows runtime ready" : ""}.`);
|
|
435
|
+
if (runtimeReady) log(c.dim(` ds + flows live at ${tilde(path.join(runtimeDir(), "node_modules", "@calo"))}, shared by every prototype.`));
|
|
436
|
+
warn("No prototype scaffolded here — this folder isn't empty and isn't an Expo project.");
|
|
437
|
+
log(` Create one with: ${c.b(c.g("npx @calo-design/cli init <name>"))} ${c.dim("(makes ./<name>/ and links it)")}`);
|
|
438
|
+
log(c.dim(" …or run init in an empty folder or an existing Expo project, or pass --isolated to install into this folder."));
|
|
407
439
|
return;
|
|
408
440
|
}
|
|
409
441
|
|
|
410
|
-
// Fresh prototype folder → shared runtime
|
|
411
|
-
|
|
412
|
-
await ensureRuntime({ force: false });
|
|
413
|
-
} catch (e) {
|
|
414
|
-
warn(`couldn't build the shared runtime (${e.message}); falling back to a per-project install.`);
|
|
415
|
-
await installHere();
|
|
416
|
-
return nextSteps({});
|
|
417
|
-
}
|
|
442
|
+
// Fresh prototype folder → link to the shared runtime, with a per-project fallback.
|
|
443
|
+
if (!runtimeReady) { await installHere(); return nextSteps({ cd }); }
|
|
418
444
|
try {
|
|
419
445
|
linkProject(process.cwd());
|
|
420
446
|
} catch (e) {
|
|
421
447
|
warn(`couldn't link to the shared runtime (${e.message}); falling back to a per-project install.`);
|
|
422
448
|
await installHere();
|
|
423
|
-
return nextSteps({});
|
|
449
|
+
return nextSteps({ cd });
|
|
424
450
|
}
|
|
425
|
-
nextSteps({ linked: true });
|
|
451
|
+
nextSteps({ linked: true, cd });
|
|
426
452
|
}
|
|
427
453
|
|
|
428
454
|
async function cmdUpdate() {
|
|
@@ -441,15 +467,20 @@ function help() {
|
|
|
441
467
|
log(`${c.b("calo-design")} — one-line setup for Calo design tooling
|
|
442
468
|
|
|
443
469
|
${c.b("npx @calo-design/cli login")} log in with your Calo email (one-time; no GitHub account needed)
|
|
444
|
-
${c.b("npx @calo-design/cli init")} skill +
|
|
445
|
-
${c.
|
|
446
|
-
${c.dim("init --
|
|
470
|
+
${c.b("npx @calo-design/cli init")} skill + shared ds/flows runtime, and link a prototype in this folder
|
|
471
|
+
${c.b("npx @calo-design/cli init <name>")} same, but scaffold the prototype in ./<name>/ (handy inside a workspace)
|
|
472
|
+
${c.dim("init --isolated")} install the Calo stack into THIS folder (no shared runtime)
|
|
473
|
+
${c.dim("init --skip-packages")} skill only (no runtime)
|
|
447
474
|
${c.dim("update")} refresh the skill + re-pin the shared runtime (all linked prototypes float to latest)
|
|
448
475
|
${c.dim("logout")} forget the saved Calo session
|
|
449
476
|
${c.dim("push")} publish THIS prototype to the Calo Mirror (login only — no EAS/Tigris creds needed)
|
|
450
477
|
${c.dim(" push --slug x --title \"…\" --owner \"…\" --screenshot path --dry-run --direct")}
|
|
478
|
+
${c.dim("share")} publish THIS web prototype (React/Vite/CRA/Next) to Cloudflare Pages → share link
|
|
479
|
+
${c.dim(" share --slug x --dir dist --build --project calo-prototypes --dry-run")}
|
|
451
480
|
|
|
452
481
|
Login is required once before init; the session refreshes automatically.
|
|
482
|
+
init always installs the skill and ensures the shared ds/flows runtime on your machine;
|
|
483
|
+
scaffolding a prototype is a separate step (needs an empty folder, an Expo project, or a name).
|
|
453
484
|
Prototypes share one install at ${c.dim(tilde(runtimeDir()))} (override with DESIGNCHEF_HOME).
|
|
454
485
|
`);
|
|
455
486
|
}
|
|
@@ -461,6 +492,7 @@ function help() {
|
|
|
461
492
|
else if (cmd === "init") await cmdInit();
|
|
462
493
|
else if (cmd === "update") await cmdUpdate();
|
|
463
494
|
else if (cmd === "push") await cmdPush(args.slice(1));
|
|
495
|
+
else if (cmd === "share") await cmdShare(args.slice(1));
|
|
464
496
|
else help();
|
|
465
497
|
} catch (err) {
|
|
466
498
|
log(`\n\x1b[31m✗\x1b[0m ${err.message}`);
|
package/bin/login.js
CHANGED
|
@@ -155,6 +155,34 @@ async function easToken() {
|
|
|
155
155
|
return r.token;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Hand a built web prototype (gzipped tarball) to the broker, which runs
|
|
159
|
+
// `wrangler pages deploy` server-side — its Cloudflare token never reaches the client —
|
|
160
|
+
// and returns the share URL. slug = Pages branch alias. Raw body (not JSON), so it
|
|
161
|
+
// doesn't use api().
|
|
162
|
+
async function publishWeb({ slug, project, tarball }) {
|
|
163
|
+
const session = await ensureSession();
|
|
164
|
+
let res;
|
|
165
|
+
try {
|
|
166
|
+
res = await fetch(BROKER + "/v1/publish-web", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
authorization: `Bearer ${session}`,
|
|
170
|
+
"content-type": "application/gzip",
|
|
171
|
+
"x-calo-slug": slug,
|
|
172
|
+
...(project ? { "x-calo-project": project } : {}),
|
|
173
|
+
},
|
|
174
|
+
body: tarball,
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
178
|
+
}
|
|
179
|
+
const text = await res.text();
|
|
180
|
+
let json;
|
|
181
|
+
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
|
|
182
|
+
if (!res.ok) throw new Error(json.error || `publish-web → HTTP ${res.status}`);
|
|
183
|
+
return json;
|
|
184
|
+
}
|
|
185
|
+
|
|
158
186
|
// Hand a built registry entry (+ optional base64 screenshot) to the broker, which
|
|
159
187
|
// performs the Tigris writes server-side. Returns the broker's JSON result.
|
|
160
188
|
async function publishToMirror(payload) {
|
|
@@ -162,4 +190,4 @@ async function publishToMirror(payload) {
|
|
|
162
190
|
return api("/v1/publish", payload, session);
|
|
163
191
|
}
|
|
164
192
|
|
|
165
|
-
module.exports = { cmdLogin, cmdLogout, ensureSession, ensureLoggedIn, githubToken, easToken, publishToMirror, BROKER, loadSession };
|
|
193
|
+
module.exports = { cmdLogin, cmdLogout, ensureSession, ensureLoggedIn, githubToken, easToken, publishWeb, publishToMirror, BROKER, loadSession };
|
package/bin/share.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `calo-design share` — publish THIS web prototype (a static React / Vite / CRA /
|
|
5
|
+
* Next-export build) to Cloudflare Pages and print a share link.
|
|
6
|
+
*
|
|
7
|
+
* Default: tar the build → POST it to the broker (/v1/publish-web) → the broker runs
|
|
8
|
+
* `wrangler pages deploy` server-side with its own token and returns the URL. Teammates
|
|
9
|
+
* need only `calo-design login` — no Cloudflare account, no wrangler, nothing to install.
|
|
10
|
+
*
|
|
11
|
+
* `--direct`: deploy from here with your own `wrangler login` (no broker) — for
|
|
12
|
+
* debugging or when the broker isn't configured. Mirrors `push --direct`.
|
|
13
|
+
*
|
|
14
|
+
* Either way each prototype is a Pages branch alias (slug) → a stable
|
|
15
|
+
* https://<slug>.calo-prototypes.pages.dev that updates on re-share. SPA deep links
|
|
16
|
+
* work via an injected `_redirects`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { spawnSync } = require("node:child_process");
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const os = require("node:os");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const { publishWeb } = require("./login");
|
|
24
|
+
|
|
25
|
+
const PROJECT = process.env.CALO_PAGES_PROJECT || "calo-prototypes";
|
|
26
|
+
const aliasUrl = (slug, project) => `https://${slug}.${project}.pages.dev`;
|
|
27
|
+
|
|
28
|
+
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` };
|
|
29
|
+
const log = (s = "") => console.log(s);
|
|
30
|
+
const ok = (s) => log(`${c.g("✓")} ${s}`);
|
|
31
|
+
const warn = (s) => log(`${c.y("!")} ${s}`);
|
|
32
|
+
const tilde = (p) => p.replace(os.homedir(), "~");
|
|
33
|
+
|
|
34
|
+
// Supports both `--slug x` and `--slug=x`.
|
|
35
|
+
const flag = (args, name) => {
|
|
36
|
+
const hit = args.find((a) => a.startsWith(`--${name}=`));
|
|
37
|
+
if (hit) return hit.split("=").slice(1).join("=");
|
|
38
|
+
const i = args.indexOf(`--${name}`);
|
|
39
|
+
if (i >= 0 && args[i + 1] && !args[i + 1].startsWith("--")) return args[i + 1];
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
const has = (args, name) => args.includes(`--${name}`);
|
|
43
|
+
|
|
44
|
+
// Cloudflare Pages branch aliases must be a DNS label: [a-z0-9-], <=63 chars.
|
|
45
|
+
function slugify(s) {
|
|
46
|
+
return String(s || "")
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
49
|
+
.replace(/^-+|-+$/g, "")
|
|
50
|
+
.slice(0, 63);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readPkg(root) {
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(bin, argv, opts = {}) {
|
|
58
|
+
const r = spawnSync(bin, argv, { stdio: "inherit", ...opts });
|
|
59
|
+
if (r.error) throw r.error;
|
|
60
|
+
if (typeof r.status === "number" && r.status !== 0) throw new Error(`${bin} ${argv.join(" ")} exited ${r.status}`);
|
|
61
|
+
return r;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run while capturing combined output (to parse the deploy URL) but still show it.
|
|
65
|
+
function runCapture(bin, argv, opts = {}) {
|
|
66
|
+
const r = spawnSync(bin, argv, { encoding: "utf8", ...opts });
|
|
67
|
+
if (r.error) throw r.error;
|
|
68
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
69
|
+
process.stdout.write(out);
|
|
70
|
+
if (typeof r.status === "number" && r.status !== 0) throw new Error(`${bin} pages deploy exited ${r.status}`);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Prefer an installed wrangler (project-local, then global); else npx. Only used by
|
|
75
|
+
// --direct — the default path needs no wrangler on this machine.
|
|
76
|
+
function wranglerCmd(root) {
|
|
77
|
+
const local = path.join(root, "node_modules", ".bin", "wrangler");
|
|
78
|
+
if (fs.existsSync(local)) return [local, []];
|
|
79
|
+
const probe = spawnSync("wrangler", ["--version"], { stdio: "ignore" });
|
|
80
|
+
if (!probe.error && probe.status === 0) return ["wrangler", []];
|
|
81
|
+
return ["npx", ["--yes", "wrangler@latest"]];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find the static build to deploy: an explicit dir, a framework output (Vite=dist,
|
|
85
|
+
// CRA=build, Next-export=out), building first if needed, or a plain static folder.
|
|
86
|
+
function resolveBuildDir(root, { build }) {
|
|
87
|
+
const candidates = ["dist", "build", "out"];
|
|
88
|
+
const hasIndex = (d) => fs.existsSync(path.join(d, "index.html"));
|
|
89
|
+
const pj = readPkg(root);
|
|
90
|
+
const hasBuildScript = pj && pj.scripts && pj.scripts.build;
|
|
91
|
+
|
|
92
|
+
const existing = candidates.map((d) => path.join(root, d)).find(hasIndex);
|
|
93
|
+
if (existing && !build) return existing;
|
|
94
|
+
|
|
95
|
+
if (hasBuildScript && (build || !existing)) {
|
|
96
|
+
log(c.b("\n[build] npm run build"));
|
|
97
|
+
run("npm", ["run", "build"], { cwd: root });
|
|
98
|
+
}
|
|
99
|
+
const built = candidates.map((d) => path.join(root, d)).find(hasIndex);
|
|
100
|
+
if (built) return built;
|
|
101
|
+
|
|
102
|
+
if (hasIndex(root)) return root; // already-static folder (no build tooling)
|
|
103
|
+
|
|
104
|
+
throw new Error("no build output found — looked for dist/, build/, out/ with an index.html. Build first, or pass --dir <folder>.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SPA fallback so client-side routes (React Router etc.) don't 404 on deep links /
|
|
108
|
+
// refresh. Respect a prototype's own _redirects if it ships one.
|
|
109
|
+
function ensureSpaRedirects(dir) {
|
|
110
|
+
const f = path.join(dir, "_redirects");
|
|
111
|
+
if (fs.existsSync(f)) return false;
|
|
112
|
+
fs.writeFileSync(f, "/* /index.html 200\n");
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Gzipped tarball of the build (relative entries), for the broker to extract + deploy.
|
|
117
|
+
function createTarball(buildDir) {
|
|
118
|
+
const tgz = path.join(os.tmpdir(), `calo-share-${path.basename(buildDir)}-${process.pid}.tgz`);
|
|
119
|
+
run("tar", ["-czf", tgz, "-C", buildDir, "."]);
|
|
120
|
+
return tgz;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function cmdShare(args = []) {
|
|
124
|
+
const root = process.cwd();
|
|
125
|
+
const project = flag(args, "project") || PROJECT;
|
|
126
|
+
const slug = slugify(flag(args, "slug") || (readPkg(root) || {}).name || path.basename(root));
|
|
127
|
+
if (!slug) throw new Error("couldn't determine a slug — pass --slug <name>.");
|
|
128
|
+
|
|
129
|
+
log(c.b(`\nShare “${slug}” → Cloudflare Pages (${project})`));
|
|
130
|
+
|
|
131
|
+
const dirFlag = flag(args, "dir");
|
|
132
|
+
const buildDir = dirFlag ? path.resolve(root, dirFlag) : resolveBuildDir(root, { build: has(args, "build") });
|
|
133
|
+
if (!fs.existsSync(path.join(buildDir, "index.html"))) throw new Error(`no index.html in ${buildDir}`);
|
|
134
|
+
ok(`build dir: ${tilde(buildDir)}`);
|
|
135
|
+
|
|
136
|
+
if (ensureSpaRedirects(buildDir)) ok("added _redirects (SPA fallback: /* → /index.html 200)");
|
|
137
|
+
|
|
138
|
+
if (has(args, "direct")) return shareDirect({ buildDir, slug, project, args });
|
|
139
|
+
|
|
140
|
+
// Default: hand the build to the broker, which deploys server-side with its own token.
|
|
141
|
+
if (has(args, "dry-run")) { log(c.dim("\n--dry-run: stopping before upload.")); return; }
|
|
142
|
+
const tgz = createTarball(buildDir);
|
|
143
|
+
try {
|
|
144
|
+
const sizeMb = (fs.statSync(tgz).size / 1e6).toFixed(1);
|
|
145
|
+
log(c.b(`\n[upload] ${sizeMb} MB → broker → wrangler pages deploy`));
|
|
146
|
+
const r = await publishWeb({ slug, project, tarball: fs.readFileSync(tgz) });
|
|
147
|
+
log("");
|
|
148
|
+
ok(`Live: ${c.g(r.url || aliasUrl(slug, project))}`);
|
|
149
|
+
if (r.version) log(c.dim(` this version: ${r.version}`));
|
|
150
|
+
log(c.dim(` Re-run \`calo-design share\` to update “${slug}” in place. Share the link above.`));
|
|
151
|
+
} finally {
|
|
152
|
+
try { fs.rmSync(tgz); } catch {}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --direct: deploy from this machine with the caller's own `wrangler login`.
|
|
157
|
+
function shareDirect({ buildDir, slug, project, args }) {
|
|
158
|
+
if (has(args, "dry-run")) { log(c.dim("\n--dry-run: stopping before deploy.")); return; }
|
|
159
|
+
warn("--direct: deploying with your local `wrangler login` (not via the broker)");
|
|
160
|
+
const [bin, pre] = wranglerCmd(process.cwd());
|
|
161
|
+
const argv = [...pre, "pages", "deploy", buildDir, "--project-name", project, "--branch", slug, "--commit-dirty=true"];
|
|
162
|
+
log(c.b(`\n[deploy] wrangler pages deploy → branch ${slug}`));
|
|
163
|
+
const out = runCapture(bin, argv);
|
|
164
|
+
const version = (out.match(/https:\/\/[a-z0-9]+\.[a-z0-9-]+\.pages\.dev/gi) || []).find((u) => !u.includes(`${slug}.`));
|
|
165
|
+
log("");
|
|
166
|
+
ok(`Live: ${c.g(aliasUrl(slug, project))}`);
|
|
167
|
+
if (version) log(c.dim(` this version: ${version}`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { cmdShare };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": { "calo-design": "bin/cli.js" },
|
|
6
6
|
"files": ["bin"],
|