@ahmednawaz/crank 0.2.2 → 0.2.3

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/crank.js CHANGED
@@ -635,7 +635,11 @@ async function main() {
635
635
  console.log(`[Crank] Installed package (${result.installCommand})`);
636
636
  } else if (result.installError) {
637
637
  console.warn(`[Crank] Package install warning: ${result.installError}`);
638
- console.warn('[Crank] Try: pnpm add -D @ahmednawaz/crank');
638
+ if (result.installHint) {
639
+ console.warn(`[Crank] Run manually:\n ${result.installHint}`);
640
+ } else {
641
+ console.warn('[Crank] Try: pnpm add -D @ahmednawaz/crank');
642
+ }
639
643
  }
640
644
  if (result.reactWire?.wired) {
641
645
  console.log(`[Crank] Patched ${result.reactWire.file}`);
package/lib/wire-react.js CHANGED
@@ -226,44 +226,153 @@ function wireReactEntry(projectRoot) {
226
226
  return { wired: true, file: entryPath };
227
227
  }
228
228
 
229
- function detectInstallCommand(projectRoot) {
230
- if (
231
- exists(path.join(projectRoot, 'pnpm-lock.yaml')) ||
232
- exists(path.join(projectRoot, 'pnpm-workspace.yaml'))
233
- ) {
234
- return { cmd: 'pnpm', args: ['add', '-D', '@ahmednawaz/crank'] };
229
+ function findPnpmWorkspaceRoot(startDir) {
230
+ let dir = path.resolve(startDir);
231
+ for (let i = 0; i < 12; i++) {
232
+ if (
233
+ exists(path.join(dir, 'pnpm-workspace.yaml')) ||
234
+ exists(path.join(dir, 'pnpm-workspace.yml'))
235
+ ) {
236
+ return dir;
237
+ }
238
+ // lockfile alone can mean pnpm without workspaces
239
+ if (exists(path.join(dir, 'pnpm-lock.yaml')) && i === 0) {
240
+ return dir;
241
+ }
242
+ const parent = path.dirname(dir);
243
+ if (parent === dir) break;
244
+ dir = parent;
235
245
  }
236
- if (exists(path.join(projectRoot, 'yarn.lock'))) {
237
- return { cmd: 'yarn', args: ['add', '-D', '@ahmednawaz/crank'] };
246
+ // Prefer ancestor with lockfile if no workspace file
247
+ dir = path.resolve(startDir);
248
+ for (let i = 0; i < 12; i++) {
249
+ if (exists(path.join(dir, 'pnpm-lock.yaml'))) {
250
+ return dir;
251
+ }
252
+ const parent = path.dirname(dir);
253
+ if (parent === dir) break;
254
+ dir = parent;
238
255
  }
239
- return { cmd: 'npm', args: ['install', '-D', '@ahmednawaz/crank'] };
256
+ return null;
240
257
  }
241
258
 
242
- function installCrankPackage(projectRoot) {
243
- const { execFileSync } = require('child_process');
244
- const { cmd, args } = detectInstallCommand(projectRoot);
245
- const result = { installed: false, installCommand: `${cmd} ${args.join(' ')}` };
259
+ function readPackageName(projectRoot) {
246
260
  try {
247
- execFileSync(cmd, args, { cwd: projectRoot, stdio: 'inherit', env: process.env });
248
- result.installed = true;
249
- return result;
250
- } catch (err) {
251
- result.installError = err.message;
261
+ const pkg = JSON.parse(
262
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
263
+ );
264
+ return pkg.name || null;
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Build install attempts for this project (pnpm monorepo-aware).
272
+ * Never uses npm as a fallback inside a pnpm workspace.
273
+ */
274
+ function detectInstallAttempts(projectRoot) {
275
+ const root = path.resolve(projectRoot);
276
+ const pnpmRoot = findPnpmWorkspaceRoot(root);
277
+ const attempts = [];
278
+
279
+ if (pnpmRoot) {
280
+ const isWorkspace = exists(path.join(pnpmRoot, 'pnpm-workspace.yaml')) ||
281
+ exists(path.join(pnpmRoot, 'pnpm-workspace.yml'));
282
+ const pkgName = readPackageName(root);
283
+ const relFilter = path.relative(pnpmRoot, root).replace(/\\/g, '/') || '.';
284
+
285
+ // 1) From the package directory (works when cwd is the workspace package)
286
+ attempts.push({
287
+ cmd: 'pnpm',
288
+ args: ['add', '-D', '@ahmednawaz/crank'],
289
+ cwd: root,
290
+ label: `pnpm add -D @ahmednawaz/crank (in ${relFilter})`
291
+ });
292
+
293
+ if (isWorkspace && pnpmRoot !== root) {
294
+ // 2) From monorepo root with --filter by package name
295
+ if (pkgName) {
296
+ attempts.push({
297
+ cmd: 'pnpm',
298
+ args: ['add', '-D', '@ahmednawaz/crank', '--filter', pkgName],
299
+ cwd: pnpmRoot,
300
+ label: `pnpm add -D @ahmednawaz/crank --filter ${pkgName}`
301
+ });
302
+ }
303
+ // 3) From monorepo root with --filter by path
304
+ attempts.push({
305
+ cmd: 'pnpm',
306
+ args: ['add', '-D', '@ahmednawaz/crank', '--filter', `./${relFilter}`],
307
+ cwd: pnpmRoot,
308
+ label: `pnpm add -D @ahmednawaz/crank --filter ./${relFilter}`
309
+ });
310
+ }
311
+
312
+ return attempts;
252
313
  }
253
- if (cmd !== 'npm') {
314
+
315
+ if (exists(path.join(root, 'yarn.lock'))) {
316
+ return [
317
+ {
318
+ cmd: 'yarn',
319
+ args: ['add', '-D', '@ahmednawaz/crank'],
320
+ cwd: root,
321
+ label: 'yarn add -D @ahmednawaz/crank'
322
+ }
323
+ ];
324
+ }
325
+
326
+ return [
327
+ {
328
+ cmd: 'npm',
329
+ args: ['install', '-D', '@ahmednawaz/crank'],
330
+ cwd: root,
331
+ label: 'npm install -D @ahmednawaz/crank'
332
+ }
333
+ ];
334
+ }
335
+
336
+ function detectInstallCommand(projectRoot) {
337
+ const attempts = detectInstallAttempts(projectRoot);
338
+ const first = attempts[0];
339
+ return { cmd: first.cmd, args: first.args, cwd: first.cwd };
340
+ }
341
+
342
+ function installCrankPackage(projectRoot) {
343
+ const { execFileSync } = require('child_process');
344
+ const attempts = detectInstallAttempts(projectRoot);
345
+ const result = {
346
+ installed: false,
347
+ installCommand: attempts[0]?.label || '',
348
+ installAttempts: []
349
+ };
350
+
351
+ for (const attempt of attempts) {
352
+ result.installAttempts.push(attempt.label);
254
353
  try {
255
- execFileSync('npm', ['install', '-D', '@ahmednawaz/crank'], {
256
- cwd: projectRoot,
354
+ execFileSync(attempt.cmd, attempt.args, {
355
+ cwd: attempt.cwd,
257
356
  stdio: 'inherit',
258
357
  env: process.env
259
358
  });
260
359
  result.installed = true;
261
- result.installCommand = 'npm install -D @ahmednawaz/crank';
360
+ result.installCommand = attempt.label;
262
361
  delete result.installError;
263
- } catch (err2) {
264
- result.installError = err2.message;
362
+ return result;
363
+ } catch (err) {
364
+ result.installError = err.message;
265
365
  }
266
366
  }
367
+
368
+ // Hint for humans / agents — do not npm-install into a pnpm monorepo
369
+ const pkgName = readPackageName(projectRoot);
370
+ if (findPnpmWorkspaceRoot(projectRoot)) {
371
+ result.installHint = pkgName
372
+ ? `pnpm add -D @ahmednawaz/crank --filter ${pkgName}`
373
+ : `pnpm add -D @ahmednawaz/crank --filter ${path.basename(projectRoot)}`;
374
+ }
375
+
267
376
  return result;
268
377
  }
269
378
 
@@ -293,5 +402,7 @@ module.exports = {
293
402
  installCrankPackage,
294
403
  injectCrank,
295
404
  findEntry,
296
- detectInstallCommand
405
+ detectInstallCommand,
406
+ detectInstallAttempts,
407
+ findPnpmWorkspaceRoot
297
408
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmednawaz/crank",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Internal Motive UX copy iteration: select UI text → Glean variants → MCP apply. Cursor, Replit, Claude.",
5
5
  "repository": {
6
6
  "type": "git",