@calo-design/cli 0.3.0 → 0.4.1

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 CHANGED
@@ -18,6 +18,16 @@ npx @calo-design/cli login # one-time: your Calo email + the emailed code
18
18
  npx @calo-design/cli init # skill + the shared ds/flows runtime, and a prototype here
19
19
  ```
20
20
 
21
+ > **`@calo-design/cli` is a public npm package.** If npx reports it can't be found — "not in
22
+ > this registry", "404", or "not a public package" — your npm is pointed at a private or
23
+ > corporate registry that doesn't carry it (common on managed laptops). Check with
24
+ > `npm config get registry` (it should be `https://registry.npmjs.org/`). To run regardless
25
+ > of your `.npmrc`, pin just this scope to public npm:
26
+ >
27
+ > ```bash
28
+ > npx --@calo-design:registry=https://registry.npmjs.org/ @calo-design/cli login
29
+ > ```
30
+
21
31
  `init` does two machine-level things every time: it installs the skill into
22
32
  `~/.claude/skills/`, and it ensures the shared runtime at `~/.designchef/runtime/` — one
23
33
  copy of `@calo/design-system`, `@calo/flows`, Expo and the peers, shared by every
package/bin/cli.js CHANGED
@@ -131,12 +131,31 @@ function runtimeExists() {
131
131
  );
132
132
  }
133
133
 
134
+ // expo-router needs babel-preset-expo (its Babel plugin injects the route require.context);
135
+ // without a babel.config.js, `expo export` produces an empty, routeless app. The shared
136
+ // runtime isn't always scaffolded with one, so guarantee it. Linked prototypes inherit it
137
+ // via writeThinConfigs; push staging backstops it again (see mirror-push.js).
138
+ function ensureBabelConfig(dir) {
139
+ const p = path.join(dir, "babel.config.js");
140
+ if (fs.existsSync(p)) return;
141
+ fs.writeFileSync(p, 'module.exports = (api) => {\n api.cache(true);\n return { presets: ["babel-preset-expo"] };\n};\n');
142
+ }
143
+
134
144
  async function ensureRuntime({ force } = {}) {
135
145
  const rt = runtimeDir();
136
- if (!force && runtimeExists()) { ok(`shared runtime ready (${tilde(rt)})`); return; }
137
- log(c.b("\n[runtime] Building the shared Calo runtime (one-time, ~a couple of minutes)"));
146
+ if (!force && runtimeExists()) {
147
+ ensureBabelConfig(rt); // backfill: runtimes built before this shipped no babel.config.js
148
+ ok(`shared runtime ready (${tilde(rt)})`);
149
+ return;
150
+ }
151
+ const scaffolded = fs.existsSync(path.join(rt, "package.json"));
152
+ const repin = force && scaffolded && !has("--clean");
153
+ log(c.b(`\n[runtime] ${repin ? "Re-pinning" : "Building"} the shared Calo runtime${repin ? "" : " (one-time, ~a couple of minutes)"}`));
138
154
  fs.mkdirSync(rt, { recursive: true });
139
- if (force) {
155
+ // Re-pin is NON-destructive: `npm install` updates node_modules in place and aborts
156
+ // cleanly on failure (e.g. a peer conflict), so a working shared runtime is never left
157
+ // wiped. `--clean` opts into a from-scratch rebuild for a genuinely corrupted tree.
158
+ if (force && has("--clean")) {
140
159
  fs.rmSync(path.join(rt, "node_modules"), { recursive: true, force: true });
141
160
  try { fs.rmSync(runtimeManifestPath()); } catch {}
142
161
  }
@@ -144,11 +163,12 @@ async function ensureRuntime({ force } = {}) {
144
163
  if (!scaffoldExpoApp(rt)) throw new Error("runtime scaffold failed");
145
164
  }
146
165
  await installCaloDeps(rt);
166
+ ensureBabelConfig(rt); // a freshly scaffolded runtime may also lack one
147
167
  fs.writeFileSync(
148
168
  runtimeManifestPath(),
149
169
  JSON.stringify({ builtAt: new Date().toISOString(), pkgSpecs: PKG_SPECS, peers: PEERS }, null, 2) + "\n"
150
170
  );
151
- ok(`shared runtime built -> ${tilde(rt)}`);
171
+ ok(`shared runtime ${repin ? "re-pinned" : "built"} -> ${tilde(rt)}`);
152
172
  }
153
173
 
154
174
  // ---------------------------------------------------------------- shared helpers
@@ -199,12 +219,14 @@ function scaffoldExpoApp(destDir = process.cwd()) {
199
219
  async function installCaloDeps(cwd) {
200
220
  // Private @calo/* installs authenticate with a short-lived broker-minted token,
201
221
  // injected into git for the npm subprocess only (no gh / PAT / SSH key).
222
+ // --legacy-peer-deps: the Expo/RN dep graph routinely has benign peerOptional conflicts
223
+ // (e.g. overlapping typescript ranges); without it a single conflict ERESOLVEs the whole install.
202
224
  const env = gitTokenEnv(await githubToken());
203
- runWithRetry("npm", ["install", ...PKG_SPECS], 3, { cwd, env });
225
+ runWithRetry("npm", ["install", ...PKG_SPECS, "--legacy-peer-deps"], 3, { cwd, env });
204
226
  if (isExpoProject(cwd)) run("npx", ["expo", "install", ...PEERS, "expo-font"], { cwd });
205
227
  else {
206
228
  warn("non-Expo project — installing latest peers; pin them to match your React Native version if needed.");
207
- run("npm", ["install", ...PEERS], { cwd });
229
+ run("npm", ["install", ...PEERS, "--legacy-peer-deps"], { cwd });
208
230
  }
209
231
  }
210
232
 
@@ -455,7 +477,10 @@ async function cmdUpdate() {
455
477
  await ensureLoggedIn();
456
478
  await installSkill();
457
479
  ok("skill updated to latest");
458
- if (runtimeExists() || has("--build")) {
480
+ // Re-pin when the runtime is fully present OR merely scaffolded (package.json on disk),
481
+ // so a half-installed runtime self-heals via plain `update` instead of being stranded.
482
+ const scaffolded = fs.existsSync(path.join(runtimeDir(), "package.json"));
483
+ if (runtimeExists() || scaffolded || has("--build")) {
459
484
  await ensureRuntime({ force: true });
460
485
  ok("shared runtime re-pinned — every linked prototype now uses the latest Calo stack");
461
486
  } else {
@@ -472,6 +497,7 @@ function help() {
472
497
  ${c.dim("init --isolated")} install the Calo stack into THIS folder (no shared runtime)
473
498
  ${c.dim("init --skip-packages")} skill only (no runtime)
474
499
  ${c.dim("update")} refresh the skill + re-pin the shared runtime (all linked prototypes float to latest)
500
+ ${c.dim(" update --clean")} re-pin with a from-scratch runtime rebuild (only if the tree is corrupted)
475
501
  ${c.dim("logout")} forget the saved Calo session
476
502
  ${c.dim("push")} publish THIS prototype to the Calo Mirror (login only — no EAS/Tigris creds needed)
477
503
  ${c.dim(" push --slug x --title \"…\" --owner \"…\" --screenshot path --dry-run --direct")}
package/bin/login.js CHANGED
@@ -179,7 +179,7 @@ async function publishWeb({ slug, project, tarball }) {
179
179
  const text = await res.text();
180
180
  let json;
181
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}`);
182
+ if (!res.ok) throw new Error([json.error || `publish-web → HTTP ${res.status}`, json.detail].filter(Boolean).join("\n "));
183
183
  return json;
184
184
  }
185
185
 
@@ -122,6 +122,18 @@ function writeJSON(p, obj) {
122
122
  fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
123
123
  }
124
124
 
125
+ // A babel.config.js with babel-preset-expo MUST exist wherever `expo export` runs. Without
126
+ // it the expo-router Babel plugin never injects the route require.context, so the export is
127
+ // a routeless, empty shell (no screens, no design system, no assets). The shared runtime
128
+ // doesn't always ship one, so guarantee it: prefer the prototype's own config, else default.
129
+ function ensureBabelConfig(dir, fallbackFrom) {
130
+ const dst = path.join(dir, "babel.config.js");
131
+ if (fs.existsSync(dst)) return;
132
+ const fb = fallbackFrom && path.join(fallbackFrom, "babel.config.js");
133
+ if (fb && fs.existsSync(fb)) { fs.cpSync(fb, dst); return; }
134
+ fs.writeFileSync(dst, 'module.exports = (api) => {\n api.cache(true);\n return { presets: ["babel-preset-expo"] };\n};\n');
135
+ }
136
+
125
137
  function stageProject({ root, stage, slug, title, ad }) {
126
138
  // Copy the author's routes + assets verbatim.
127
139
  const stageSrcParent = path.join(stage, path.relative(root, path.dirname(ad)));
@@ -164,6 +176,9 @@ function stageProject({ root, stage, slug, title, ad }) {
164
176
  const src = path.join(runtimeDir(), f);
165
177
  if (fs.existsSync(src)) fs.cpSync(src, path.join(stage, f));
166
178
  }
179
+ // The runtime may not have a babel.config.js; without one the staged export is an empty,
180
+ // routeless app. Backstop it from the prototype's own config, else a default.
181
+ ensureBabelConfig(stage, root);
167
182
 
168
183
  writeMetroConfig(stage);
169
184
  linkNodeModules(stage);
@@ -213,7 +228,7 @@ function injectBackChrome({ stagedAppDir, mirrorChromeDir, libRel }) {
213
228
  if (fs.existsSync(layout)) {
214
229
  const cur = fs.readFileSync(layout, "utf8");
215
230
  const standard = /useFonts\(caloFonts\)/.test(cur) && /<Stack/.test(cur);
216
- if (!standard) warn("custom root _layout detected — Mirror wraps it with a Stack; custom providers won't carry over.");
231
+ if (!standard) warn("custom root _layout detected — Mirror wraps it with a Stack; providers in the ROOT layout won't carry over. Move them to a nested layout (e.g. src/app/(group)/_layout.tsx), which the Mirror preserves.");
217
232
  }
218
233
  fs.writeFileSync(
219
234
  layout,
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@calo-design/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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
- "bin": { "calo-design": "bin/cli.js" },
6
- "files": ["bin"],
7
- "publishConfig": { "access": "public" },
5
+ "bin": {
6
+ "calo-design": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
8
14
  "license": "UNLICENSED",
9
- "engines": { "node": ">=18" }
15
+ "engines": {
16
+ "node": ">=18"
17
+ }
10
18
  }