@calo-design/cli 0.4.6 → 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 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
- if (!stale.length) {
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
- log(c.b(`\n[runtime] ${stale.join(" + ")} behind latest — refreshing the shared runtime`));
203
- try {
204
- // Non-destructive: `npm install` of the git specs re-resolves HEAD and updates
205
- // node_modules in place; a failure aborts cleanly and leaves the runtime working.
206
- runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd: rt, env });
207
- let manifest = { pkgSpecs: PKG_SPECS, peers: PEERS };
208
- try { manifest = { ...JSON.parse(fs.readFileSync(runtimeManifestPath(), "utf8")), ...manifest }; } catch {}
209
- manifest.refreshedAt = new Date().toISOString();
210
- fs.writeFileSync(runtimeManifestPath(), JSON.stringify(manifest, null, 2) + "\n");
211
- ok("shared runtime refreshed every linked prototype now uses the latest Calo stack");
212
- } catch (e) {
213
- warn(`couldn't refresh the runtime (${e.message}) — continuing with the existing install; \`calo-design update\` retries this.`);
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
  }
@@ -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 routes + assets verbatim.
139
- const stageSrcParent = path.join(stage, path.relative(root, path.dirname(ad)));
140
- fs.mkdirSync(stageSrcParent, { recursive: true });
141
- fs.cpSync(ad, path.join(stageSrcParent, path.basename(ad)), { recursive: true });
142
- const assets = path.join(root, "assets");
143
- if (fs.existsSync(assets)) fs.cpSync(assets, path.join(stage, "assets"), { recursive: true });
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 that wraps the app in a floating
224
- // "← Mirror" control. NOTE (v1): a custom root _layout (custom providers/Tabs) is
225
- // not preserved we warn when the layout looks non-standard.
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
- const [going, setGoing] = useState(false);
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.6",
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
  }