@calo-design/cli 0.4.5 → 0.4.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/cli.js +64 -14
- package/bin/mirror-push.js +46 -81
- package/package.json +4 -1
package/bin/cli.js
CHANGED
|
@@ -32,7 +32,10 @@ const PEERS = [
|
|
|
32
32
|
// The Mirror loads each prototype as an EAS Update at runtime; prototypes need
|
|
33
33
|
// the expo-updates JS API (the "back to Mirror" chrome `push` injects calls it).
|
|
34
34
|
// The shell binary provides the native module; prototypes ship JS only.
|
|
35
|
-
"expo-updates"
|
|
35
|
+
"expo-updates",
|
|
36
|
+
// Video + bundled-asset support (Mirror shell ships the native side from build 7).
|
|
37
|
+
"expo-video",
|
|
38
|
+
"expo-asset"
|
|
36
39
|
];
|
|
37
40
|
// Install via explicit HTTPS git URLs. Auth is a short-lived GitHub token the Calo
|
|
38
41
|
// broker mints after `calo-design login` — injected into git for the install
|
|
@@ -185,6 +188,17 @@ function staleCaloDeps(rt, env) {
|
|
|
185
188
|
return stale;
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
// Peers added to PEERS after a runtime was built (e.g. expo-video) never arrive
|
|
192
|
+
// via the @calo git-dep refresh, so detect them from the runtime's package.json.
|
|
193
|
+
function missingPeers(rt) {
|
|
194
|
+
try {
|
|
195
|
+
const deps = JSON.parse(fs.readFileSync(path.join(rt, "package.json"), "utf8")).dependencies || {};
|
|
196
|
+
return PEERS.filter((p) => !deps[p]);
|
|
197
|
+
} catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
async function ensureRuntime({ force } = {}) {
|
|
189
203
|
const rt = runtimeDir();
|
|
190
204
|
if (!force && runtimeExists()) {
|
|
@@ -195,22 +209,35 @@ async function ensureRuntime({ force } = {}) {
|
|
|
195
209
|
env = gitTokenEnv(await githubToken());
|
|
196
210
|
stale = staleCaloDeps(rt, env);
|
|
197
211
|
} catch {} // offline / broker hiccup — keep the existing runtime, don't block init
|
|
198
|
-
|
|
212
|
+
const missing = missingPeers(rt);
|
|
213
|
+
if (!stale.length && !missing.length) {
|
|
199
214
|
ok(`shared runtime ready (${tilde(rt)})`);
|
|
200
215
|
return;
|
|
201
216
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
if (stale.length) {
|
|
218
|
+
log(c.b(`\n[runtime] ${stale.join(" + ")} behind latest — refreshing the shared runtime`));
|
|
219
|
+
try {
|
|
220
|
+
// Non-destructive: `npm install` of the git specs re-resolves HEAD and updates
|
|
221
|
+
// node_modules in place; a failure aborts cleanly and leaves the runtime working.
|
|
222
|
+
runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd: rt, env });
|
|
223
|
+
let manifest = { pkgSpecs: PKG_SPECS, peers: PEERS };
|
|
224
|
+
try { manifest = { ...JSON.parse(fs.readFileSync(runtimeManifestPath(), "utf8")), ...manifest }; } catch {}
|
|
225
|
+
manifest.refreshedAt = new Date().toISOString();
|
|
226
|
+
fs.writeFileSync(runtimeManifestPath(), JSON.stringify(manifest, null, 2) + "\n");
|
|
227
|
+
ok("shared runtime refreshed — every linked prototype now uses the latest Calo stack");
|
|
228
|
+
} catch (e) {
|
|
229
|
+
warn(`couldn't refresh the runtime (${e.message}) — continuing with the existing install; \`calo-design update\` retries this.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (missing.length) {
|
|
233
|
+
log(c.b(`\n[runtime] adding shared deps: ${missing.join(", ")}`));
|
|
234
|
+
try {
|
|
235
|
+
// `expo install` pins the version matching the runtime's Expo SDK.
|
|
236
|
+
run("npx", ["expo", "install", ...missing], { cwd: rt });
|
|
237
|
+
ok(`shared runtime now provides ${missing.join(", ")}`);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
warn(`couldn't add ${missing.join(", ")} (${e.message}) — \`calo-design update\` retries this.`);
|
|
240
|
+
}
|
|
214
241
|
}
|
|
215
242
|
return;
|
|
216
243
|
}
|
|
@@ -659,6 +686,28 @@ async function cmdFeed() {
|
|
|
659
686
|
for (const e of data.events) log(`${c.dim(e.ts)} ${e.email} ${c.b(e.action)} ${e.target || ""}`);
|
|
660
687
|
}
|
|
661
688
|
|
|
689
|
+
// Per-prototype analytics (Design Kitchen reads this via `--json`). Same session reuse as
|
|
690
|
+
// the feed; the broker holds the Clarity token and does the caching — this is a dumb pipe.
|
|
691
|
+
async function cmdInsights() {
|
|
692
|
+
const session = await ensureSession();
|
|
693
|
+
let res;
|
|
694
|
+
try {
|
|
695
|
+
res = await fetch(`${BROKER}/v1/clarity/insights`, {
|
|
696
|
+
headers: { authorization: `Bearer ${session}` },
|
|
697
|
+
signal: AbortSignal.timeout(20000),
|
|
698
|
+
});
|
|
699
|
+
} catch (e) {
|
|
700
|
+
throw new Error(`can't reach the Calo broker at ${BROKER} (${e.message})`);
|
|
701
|
+
}
|
|
702
|
+
const text = await res.text();
|
|
703
|
+
if (!res.ok) throw new Error(`insights failed (${res.status}): ${text.slice(0, 200)}`);
|
|
704
|
+
if (has("--json")) { process.stdout.write("\n" + text + "\n"); return; }
|
|
705
|
+
const data = JSON.parse(text);
|
|
706
|
+
const protos = Object.values(data.prototypes || {});
|
|
707
|
+
log(`${data.totals?.sessions ?? 0} sessions across ${protos.length} prototype(s) · last ${data.windowDays} days`);
|
|
708
|
+
for (const p of protos) log(` ${c.b(p.slug || p.host)} ${p.sessions} sessions · ${p.activeSeconds}s active`);
|
|
709
|
+
}
|
|
710
|
+
|
|
662
711
|
// Serve the component gallery (the @calo/design-system showroom) locally, rendered LIVE from
|
|
663
712
|
// the shared runtime — so `calo-design update` keeps it current (no rebuild/redeploy). Design
|
|
664
713
|
// Kitchen auto-starts this; it also works standalone in a terminal. Runs Metro in the
|
|
@@ -725,6 +774,7 @@ function help() {
|
|
|
725
774
|
else if (cmd === "push") await cmdPush(args.slice(1));
|
|
726
775
|
else if (cmd === "share") await cmdShare(args.slice(1));
|
|
727
776
|
else if (cmd === "feed") await cmdFeed();
|
|
777
|
+
else if (cmd === "insights") await cmdInsights();
|
|
728
778
|
else if (cmd === "whoami") await cmdWhoami();
|
|
729
779
|
else if (cmd === "gallery") await cmdGallery();
|
|
730
780
|
else help();
|
package/bin/mirror-push.js
CHANGED
|
@@ -134,13 +134,27 @@ function ensureBabelConfig(dir, fallbackFrom) {
|
|
|
134
134
|
fs.writeFileSync(dst, 'module.exports = (api) => {\n api.cache(true);\n return { presets: ["babel-preset-expo"] };\n};\n');
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Never staged: installed/build/native output and editor state. Everything else
|
|
138
|
+
// is the author's source and must be copied verbatim — prototypes keep code in
|
|
139
|
+
// sibling folders (src/components, src/lib, public/, …) that the routes import
|
|
140
|
+
// via the `@/*` tsconfig alias, so staging only src/app breaks the export.
|
|
141
|
+
const STAGE_SKIP_DIRS = new Set([
|
|
142
|
+
"node_modules", ".git", ".expo", ".expo-shared", "dist", "build",
|
|
143
|
+
"ios", "android", ".tamagui", ".vscode", ".idea",
|
|
144
|
+
]);
|
|
145
|
+
|
|
137
146
|
function stageProject({ root, stage, slug, title, ad }) {
|
|
138
|
-
// Copy the author's
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
fs.cpSync(
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
// Copy the author's whole project (minus STAGE_SKIP_DIRS). Managed files
|
|
148
|
+
// (app.json, package.json, tsconfig, babel, metro, node_modules, .gitignore,
|
|
149
|
+
// the root _layout) are overwritten below, exactly as before.
|
|
150
|
+
fs.cpSync(root, stage, {
|
|
151
|
+
recursive: true,
|
|
152
|
+
filter: (src) => {
|
|
153
|
+
const name = path.basename(src);
|
|
154
|
+
if (name === ".DS_Store" || name.endsWith(".log")) return false;
|
|
155
|
+
return !STAGE_SKIP_DIRS.has(name);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
144
158
|
|
|
145
159
|
const usesSrc = path.basename(path.dirname(ad)) === "src";
|
|
146
160
|
const stagedAppDir = path.join(stage, usesSrc ? "src" : "", "app");
|
|
@@ -220,9 +234,11 @@ function linkNodeModules(stage) {
|
|
|
220
234
|
fs.symlinkSync(target, link, process.platform === "win32" ? "junction" : "dir");
|
|
221
235
|
}
|
|
222
236
|
|
|
223
|
-
// Overwrite the root _layout with a managed one
|
|
224
|
-
//
|
|
225
|
-
//
|
|
237
|
+
// Overwrite the root _layout with a managed one. Returning to the launcher is now
|
|
238
|
+
// handled natively by the Mirror shell (shake the device — see withCaloMirrorIos),
|
|
239
|
+
// so no visible "back" control is injected; MirrorChrome is a pass-through kept as
|
|
240
|
+
// a seam for any future prototype-side chrome. NOTE (v1): a custom root _layout
|
|
241
|
+
// (custom providers/Tabs) is not preserved — we warn when it looks non-standard.
|
|
226
242
|
function injectBackChrome({ stagedAppDir, mirrorChromeDir, libRel }) {
|
|
227
243
|
const layout = path.join(stagedAppDir, "_layout.tsx");
|
|
228
244
|
if (fs.existsSync(layout)) {
|
|
@@ -251,80 +267,15 @@ export default function RootLayout() {
|
|
|
251
267
|
fs.writeFileSync(
|
|
252
268
|
path.join(mirrorChromeDir, "mirror-chrome.tsx"),
|
|
253
269
|
`import type { ReactNode } from "react";
|
|
254
|
-
import { useState } from "react";
|
|
255
|
-
import { ActivityIndicator, Platform, Pressable, Text, View } from "react-native";
|
|
256
|
-
import * as Updates from "expo-updates";
|
|
257
|
-
|
|
258
|
-
// One return at a time. A second tap before the reload lands fires a second
|
|
259
|
-
// reloadAsync(), racing two runtime teardowns -> SIGBUS in ExpoModulesJSI. The
|
|
260
|
-
// latch (plus the disabled button) drops extra taps; a failure clears it to retry.
|
|
261
|
-
let returning = false;
|
|
262
|
-
|
|
263
|
-
// Injected by \`calo-design push\`. Flips the binary back to the Mirror launcher.
|
|
264
|
-
async function backToMirror() {
|
|
265
|
-
if (returning || !Updates.isEnabled) return;
|
|
266
|
-
returning = true;
|
|
267
|
-
try {
|
|
268
|
-
await Updates.setUpdateURLAndRequestHeadersOverride({
|
|
269
|
-
updateUrl: ${JSON.stringify(UPDATES_URL)},
|
|
270
|
-
requestHeaders: {
|
|
271
|
-
"expo-channel-name": ${JSON.stringify(LAUNCHER_CHANNEL)},
|
|
272
|
-
"expo-runtime-version": Updates.runtimeVersion ?? ${JSON.stringify(RUNTIME_VERSION)},
|
|
273
|
-
"expo-platform": Platform.OS,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
// EAS requires channel + runtime-version + platform (400 otherwise); the
|
|
277
|
-
// override replaces headers, so send all three, then download.
|
|
278
|
-
await Updates.fetchUpdateAsync();
|
|
279
|
-
} catch {
|
|
280
|
-
returning = false;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// Defer the reload to a fresh macrotask. Reloading inside the awaited chain
|
|
284
|
-
// races the fetch's JSI promise + the expo-updates reaper teardown -> SIGBUS
|
|
285
|
-
// in ExpoModulesJSI. Let the chain unwind + reaper settle, then reload.
|
|
286
|
-
// reloadAsync never resolves (the app reloads), so it is not awaited.
|
|
287
|
-
setTimeout(() => {
|
|
288
|
-
void Updates.reloadAsync().catch(() => {
|
|
289
|
-
returning = false;
|
|
290
|
-
});
|
|
291
|
-
}, 300);
|
|
292
|
-
}
|
|
293
270
|
|
|
271
|
+
// Returning to the Mirror launcher is handled NATIVELY by the shell binary:
|
|
272
|
+
// shake the device and the shell flips its EAS Update channel back to the
|
|
273
|
+
// launcher and relaunches (see calo-design-mirror/plugins/withCaloMirrorIos.js,
|
|
274
|
+
// CaloShakeWindow + CaloMirrorBack). That works for every loaded prototype
|
|
275
|
+
// without any JS here, so this wrapper no longer renders a "back" control — it
|
|
276
|
+
// stays as a pass-through seam for any future prototype-side chrome.
|
|
294
277
|
export function MirrorChrome({ children }: { children: ReactNode }) {
|
|
295
|
-
|
|
296
|
-
return (
|
|
297
|
-
<View style={{ flex: 1 }}>
|
|
298
|
-
{children}
|
|
299
|
-
<Pressable
|
|
300
|
-
accessibilityLabel="Back to Mirror"
|
|
301
|
-
accessibilityRole="button"
|
|
302
|
-
disabled={going}
|
|
303
|
-
onPress={() => {
|
|
304
|
-
setGoing(true);
|
|
305
|
-
void backToMirror();
|
|
306
|
-
}}
|
|
307
|
-
hitSlop={8}
|
|
308
|
-
style={({ pressed }) => ({
|
|
309
|
-
position: "absolute",
|
|
310
|
-
left: 16,
|
|
311
|
-
bottom: 36,
|
|
312
|
-
flexDirection: "row",
|
|
313
|
-
alignItems: "center",
|
|
314
|
-
gap: 8,
|
|
315
|
-
backgroundColor: "#208AEF",
|
|
316
|
-
paddingHorizontal: 16,
|
|
317
|
-
paddingVertical: 10,
|
|
318
|
-
borderRadius: 999,
|
|
319
|
-
opacity: going ? 0.85 : pressed ? 0.7 : 1,
|
|
320
|
-
transform: [{ scale: pressed ? 0.96 : 1 }],
|
|
321
|
-
})}
|
|
322
|
-
>
|
|
323
|
-
{going ? <ActivityIndicator size="small" color="#fff" /> : null}
|
|
324
|
-
<Text style={{ color: "#fff", fontWeight: "600" }}>{going ? "Returning…" : "← Mirror"}</Text>
|
|
325
|
-
</Pressable>
|
|
326
|
-
</View>
|
|
327
|
-
);
|
|
278
|
+
return <>{children}</>;
|
|
328
279
|
}
|
|
329
280
|
`
|
|
330
281
|
);
|
|
@@ -519,6 +470,20 @@ async function cmdPush(args) {
|
|
|
519
470
|
}
|
|
520
471
|
|
|
521
472
|
log(c.b("\n✨ Live in the Mirror.") + " Open Calo Mirror and tap Refresh — your prototype is at the top.");
|
|
473
|
+
|
|
474
|
+
// Share target: the deep link the Mirror scanner (and iOS camera) opens.
|
|
475
|
+
// Print it + a scannable QR so a teammate can jump straight in.
|
|
476
|
+
const deepLink = `designchef://open/${slug}`;
|
|
477
|
+
log(c.b("\n📱 Share this prototype") + c.dim(" (teammate needs Calo Mirror installed)"));
|
|
478
|
+
log(c.dim(" Scan the QR with Calo Mirror — or share the link:"));
|
|
479
|
+
log(" " + c.b(deepLink) + "\n");
|
|
480
|
+
try {
|
|
481
|
+
require("qrcode-terminal").generate(deepLink, { small: true }, (qr) =>
|
|
482
|
+
log(qr.replace(/^/gm, " ")),
|
|
483
|
+
);
|
|
484
|
+
} catch {
|
|
485
|
+
/* qrcode-terminal missing — the link above is enough to share */
|
|
486
|
+
}
|
|
522
487
|
} finally {
|
|
523
488
|
if (!keepStage) fs.rmSync(stage, { recursive: true, force: true });
|
|
524
489
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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"
|
|
@@ -14,5 +14,8 @@
|
|
|
14
14
|
"license": "UNLICENSED",
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"qrcode-terminal": "^0.12.0"
|
|
17
20
|
}
|
|
18
21
|
}
|