@ahmednawaz/crank 0.1.9 → 0.2.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/AGENTS.md CHANGED
@@ -103,20 +103,35 @@ See **[REPLIT.md](./REPLIT.md)** and `.crank/replit.md` (URLs after first run).
103
103
 
104
104
  ---
105
105
 
106
- ## Panel injection (Vite vs React)
106
+ ## Panel injection (Retune pattern)
107
107
 
108
- Crank’s panel is a **script overlay** (`/dev-loader.js` → bookmarklet), **not** a Retune-style React component in the app tree.
108
+ **Preferred for React apps** install the package and mount once:
109
109
 
110
- | Stack | What to do |
111
- |-------|------------|
112
- | **Vite** (React/Vue/Svelte/vanilla) | **Prefer** Vite plugin. `setup` writes `.crank/vite-plugin.cjs` and patches `vite.config` with Vizpatch-style graceful load (works when `pnpm add` fails — uses `packageRoot` from `.crank/config.json`). |
113
- | **Next.js** | Optional: `import { CrankScript } from '@ahmednawaz/crank/next'` |
114
- | **React without Vite** | Optional: `import { CrankLoader } from '@ahmednawaz/crank/react'` — injects the same script only |
115
- | **Any** | Bookmarklet from companion URL |
110
+ ```bash
111
+ pnpm add -D @ahmednawaz/crank
112
+ # or: npm i -D @ahmednawaz/crank
113
+ ```
116
114
 
117
- **Agents must not** invent a fake in-tree `<Crank />` panel UI. If `hasVite: false` but a `vite.config.*` exists nearby, re-run setup from the app root or check `environment.viteConfig` from `doctor`.
115
+ ```tsx
116
+ import { Crank } from '@ahmednawaz/crank/react'
117
+
118
+ // main.tsx
119
+ <App />
120
+ <Crank />
121
+ ```
122
+
123
+ Keep companion running: `npx @ahmednawaz/crank start`
124
+
125
+ On Replit set public companion URL if needed: `window.__CRANK_URL__`, `<Crank companionUrl="…" />`, or `VITE_CRANK_URL`.
126
+
127
+ | Stack | Integration |
128
+ |-------|-------------|
129
+ | **React + Vite** | `<Crank />` from `@ahmednawaz/crank/react` (bundled like Retune — **no Vite plugin**) |
130
+ | **Next.js** | `<Crank />` from `@ahmednawaz/crank/next` |
131
+ | **Vue / Svelte / vanilla Vite** | Optional `@ahmednawaz/crank/vite` plugin |
132
+ | **Any** | Bookmarklet from companion URL |
118
133
 
119
- Keep companion up via Workflow: `npx @ahmednawaz/crank start`. See `.crank/injection.md` after setup.
134
+ Agents: do **not** invent Vite-plugin path workarounds for React apps. See `.crank/react-mount.md` / `.crank/injection.md` after setup.
120
135
 
121
136
  ---
122
137
 
package/README.md CHANGED
@@ -102,20 +102,35 @@ No external sibling tool required. Crank ships:
102
102
 
103
103
  Optional: `CRANK_ASSET_ROOT` overrides where packaged UI assets are loaded from (defaults to this install).
104
104
 
105
- ### Panel injection (Vite first)
105
+ ### Panel injection (Retune-style React)
106
106
 
107
- The panel is a **script overlay**, not a Retune-style React component.
107
+ **Preferred** install and mount like Retune:
108
+
109
+ ```bash
110
+ pnpm add -D @ahmednawaz/crank
111
+ ```
112
+
113
+ ```tsx
114
+ import { Crank } from '@ahmednawaz/crank/react'
115
+
116
+ createRoot(document.getElementById('root')!).render(
117
+ <>
118
+ <App />
119
+ <Crank />
120
+ </>
121
+ )
122
+ ```
123
+
124
+ Companion: `npx @ahmednawaz/crank start`
125
+ Replit public URL: `window.__CRANK_URL__` or `<Crank companionUrl="https://3344-….replit.dev" />` or `VITE_CRANK_URL`.
108
126
 
109
127
  | Stack | Integration |
110
128
  |-------|-------------|
111
- | **Vite** | `setup` patches `vite.config` with Vizpatch-style graceful load via `.crank/vite-plugin.cjs` (resolves from `packageRoot` / npx cache when `pnpm add` fails) |
112
- | **Next.js** | `import { CrankScript } from '@ahmednawaz/crank/next'` |
113
- | **React (no Vite)** | `import { CrankLoader } from '@ahmednawaz/crank/react'` — script injector only |
114
- | **Webpack** | `crankWebpackPlugin()` from `@ahmednawaz/crank/webpack` |
129
+ | **React** | `<Crank />` bundled by Vite, **no plugin** |
130
+ | **Next.js** | `import { Crank } from '@ahmednawaz/crank/next'` |
131
+ | **Non-React Vite** | Optional `import { crank } from '@ahmednawaz/crank/vite'` |
115
132
  | **Fallback** | Companion bookmarklet |
116
133
 
117
- Keep companion running (`npx @ahmednawaz/crank start`). See `.crank/injection.md` after setup.
118
-
119
134
  Useful variants:
120
135
 
121
136
  ```bash
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Injected by Vite plugin (or bookmarklet fallback).
5
- * Loads the Crank panel from the local companion.
4
+ * Injected by <Crank /> (React) or the optional Vite plugin / bookmarklet.
5
+ * Loads the Crank panel from the companion that served this script.
6
6
  */
7
7
  (function () {
8
8
  if (window.__CRANK_LOADER__) {
package/lib/init.js CHANGED
@@ -433,7 +433,7 @@ function mergeUserCursorMcp(projectRoot, results) {
433
433
  }
434
434
 
435
435
  /**
436
- * Retune-style: add crank to devDependencies so `npx crank` works with no path.
436
+ * Retune-style: add @ahmednawaz/crank to devDependencies (scoped works with pnpm).
437
437
  */
438
438
  function ensureCrankDependency(projectRoot, results) {
439
439
  const pkgPath = path.join(projectRoot, 'package.json');
@@ -448,20 +448,28 @@ function ensureCrankDependency(projectRoot, results) {
448
448
  const crankPkg = readJson(path.join(PACKAGE_ROOT, 'package.json'), {});
449
449
  pkg.devDependencies = pkg.devDependencies || {};
450
450
 
451
- let spec;
451
+ const scoped = '@ahmednawaz/crank';
452
+ let versionSpec;
452
453
  if (process.env.CRANK_NPM_SPEC) {
453
- spec = process.env.CRANK_NPM_SPEC;
454
+ versionSpec = process.env.CRANK_NPM_SPEC;
454
455
  } else {
455
- spec = `@ahmednawaz/crank@^${crankPkg.version || '0.1.0'}`;
456
+ versionSpec = `^${crankPkg.version || '0.2.0'}`;
456
457
  }
457
458
 
458
- if (pkg.devDependencies.crank === spec) {
459
+ const prev = pkg.devDependencies[scoped];
460
+ const hadLegacyAlias = Boolean(pkg.devDependencies.crank);
461
+ pkg.devDependencies[scoped] = versionSpec;
462
+ if (hadLegacyAlias) {
463
+ delete pkg.devDependencies.crank;
464
+ }
465
+
466
+ if (prev === versionSpec && !hadLegacyAlias) {
459
467
  return;
460
468
  }
461
- pkg.devDependencies.crank = spec;
469
+
462
470
  writeJson(pkgPath, pkg);
463
- results.wrote.push('package.json#devDependencies.crank');
464
- results.crankDependencyAdded = spec;
471
+ results.wrote.push(`package.json#devDependencies.${scoped}`);
472
+ results.crankDependencyAdded = `${scoped}@${versionSpec}`;
465
473
  }
466
474
 
467
475
  function writeConfig(projectRoot, env) {
@@ -529,7 +537,10 @@ function initProject(projectRoot, options = {}) {
529
537
  installCrankSkill(projectRoot, results);
530
538
  }
531
539
 
532
- if (env.hasVite && options.vite !== false) {
540
+ // Retune-style: React apps mount <Crank /> — do not rely on Vite plugin path hacks.
541
+ if (env.hasReact && options.react !== false) {
542
+ writeReactMountHint(projectRoot, env, results);
543
+ } else if (env.hasVite && options.vite !== false) {
533
544
  const viteResult = patchViteConfig(projectRoot);
534
545
  results.vite = viteResult;
535
546
  if (viteResult.patched) {
@@ -539,7 +550,6 @@ function initProject(projectRoot, options = {}) {
539
550
  results.wrote.push(path.relative(projectRoot, viteResult.plugin));
540
551
  }
541
552
  } else if (options.vite !== false) {
542
- // Still write stub so agents can wire the graceful load manually.
543
553
  const stub = writeVitePluginStub(projectRoot);
544
554
  results.vite = { patched: false, reason: 'no-vite-detected', plugin: stub };
545
555
  results.wrote.push(path.relative(projectRoot, stub));
@@ -699,76 +709,99 @@ function copyAgentsDoc(projectRoot, results) {
699
709
  }
700
710
  }
701
711
 
712
+ function writeReactMountHint(projectRoot, env, results) {
713
+ const hint = path.join(projectRoot, '.crank', 'react-mount.md');
714
+ fs.mkdirSync(path.dirname(hint), { recursive: true });
715
+ fs.writeFileSync(
716
+ hint,
717
+ [
718
+ '# Mount Crank (Retune-style)',
719
+ '',
720
+ '1. Install (from the app package in a monorepo):',
721
+ '',
722
+ '```bash',
723
+ 'pnpm add -D @ahmednawaz/crank',
724
+ '# or: npm i -D @ahmednawaz/crank',
725
+ '```',
726
+ '',
727
+ '2. Mount once near your app root (e.g. `main.tsx` / `App.tsx`):',
728
+ '',
729
+ '```tsx',
730
+ "import { Crank } from '@ahmednawaz/crank/react'",
731
+ '',
732
+ 'createRoot(document.getElementById("root")!).render(',
733
+ ' <>',
734
+ ' <App />',
735
+ ' <Crank />',
736
+ ' </>',
737
+ ')',
738
+ '```',
739
+ '',
740
+ '3. Keep the companion running:',
741
+ '',
742
+ '```bash',
743
+ 'npx @ahmednawaz/crank start',
744
+ '```',
745
+ '',
746
+ 'On Replit, set the public companion URL if the preview is not on localhost:',
747
+ '',
748
+ '```ts',
749
+ "window.__CRANK_URL__ = 'https://3344-<your-repl>.replit.dev'",
750
+ '// or: <Crank companionUrl="https://3344-….replit.dev" />',
751
+ '// or: VITE_CRANK_URL=https://3344-….replit.dev',
752
+ '```',
753
+ '',
754
+ 'No Vite plugin required.',
755
+ ''
756
+ ].join('\n')
757
+ );
758
+ results.wrote.push('.crank/react-mount.md');
759
+ results.reactMount = true;
760
+ }
761
+
702
762
  function writeInjectionGuide(projectRoot, env, results) {
703
763
  const guide = path.join(projectRoot, '.crank', 'injection.md');
704
764
  fs.mkdirSync(path.dirname(guide), { recursive: true });
705
765
  const lines = [
706
766
  '# Crank panel injection',
707
767
  '',
708
- 'Crank’s panel is a **script overlay** (companion `/dev-loader.js` → bookmarklet).',
709
- 'It is **not** a Retune-style React UI component living in your component tree.',
710
- '',
711
- '## Prefer Vite plugin (default)',
712
- '',
713
- 'For Vite apps (React, Vue, Svelte, vanilla):',
768
+ '## Preferred React component (Retune pattern)',
714
769
  '',
715
- '1. Run `npx @ahmednawaz/crank setup` (writes `.crank/vite-plugin.cjs` + patches `vite.config`).',
716
- '2. Keep a **Workflow** running: `npx @ahmednawaz/crank start` (companion on **3344**).',
717
- '3. Dev server loads the panel automatically via the Vite plugin.',
770
+ 'Crank ships a normal npm React export. Vite bundles it like any other dependency.',
771
+ '**No Vite plugin. No npx-cache paths.**',
718
772
  '',
719
- 'Graceful load (Vizpatch-style) — works even when `pnpm add @ahmednawaz/crank` fails:',
720
- '',
721
- '```ts',
722
- "import { createRequire } from 'node:module'",
723
- "import { existsSync } from 'node:fs'",
724
- "import { fileURLToPath } from 'node:url'",
725
- '',
726
- 'let crank = null',
727
- 'try {',
728
- " const pluginPath = fileURLToPath(new URL('./.crank/vite-plugin.cjs', import.meta.url))",
729
- ' if (existsSync(pluginPath)) {',
730
- ' const require = createRequire(import.meta.url)',
731
- ' crank = require(pluginPath).crank',
732
- ' }',
733
- '} catch {}',
734
- '',
735
- 'export default defineConfig({',
736
- ' plugins: [',
737
- ' ...(crank ? [crank()] : []),',
738
- ' // ...your plugins',
739
- ' ],',
740
- '})',
773
+ '```bash',
774
+ 'pnpm add -D @ahmednawaz/crank',
741
775
  '```',
742
776
  '',
743
- '`.crank/vite-plugin.cjs` resolves Crank from `config.json` → `packageRoot` (npx cache),',
744
- 'then `node_modules`, and noops if missing.',
777
+ '```tsx',
778
+ "import { Crank } from '@ahmednawaz/crank/react'",
745
779
  '',
746
- '## React adapter (non-Vite React only)',
780
+ '// main.tsx mount once',
781
+ '<App />',
782
+ '<Crank />',
783
+ '```',
747
784
  '',
748
- 'Use only when there is **no** Vite config (CRA / custom webpack React):',
785
+ 'Companion must be running: `npx @ahmednawaz/crank start`',
749
786
  '',
750
- '```tsx',
751
- "import { CrankLoader } from '@ahmednawaz/crank/react'",
752
- '// client root only injects the same /dev-loader.js script',
753
- '<CrankLoader />',
754
- '```',
787
+ 'URL resolution (first match):',
788
+ '1. `window.__CRANK_URL__`',
789
+ '2. `<Crank companionUrl="…" />` / `url` prop',
790
+ '3. `VITE_CRANK_URL` / `VITE_CRANK_COMPANION_URL`',
791
+ '4. `http://localhost:3344`',
755
792
  '',
756
- 'Do **not** invent a fake in-tree `<Crank />` panel UI. `CrankLoader` / `Crank` only inject a script.',
793
+ 'See `.crank/react-mount.md` when this project has React.',
757
794
  '',
758
795
  '## Next.js',
759
796
  '',
760
797
  '```tsx',
761
- "import { CrankScript } from '@ahmednawaz/crank/next'",
762
- '// app/layout.tsx or pages/_app',
763
- '<CrankScript />',
798
+ "import { Crank } from '@ahmednawaz/crank/next'",
799
+ '<Crank />',
764
800
  '```',
765
801
  '',
766
- '## Webpack',
802
+ '## Non-React Vite (Vue / Svelte / vanilla)',
767
803
  '',
768
- '```js',
769
- "const { crankWebpackPlugin } = require('@ahmednawaz/crank/webpack')",
770
- 'plugins: [new HtmlWebpackPlugin(...), crankWebpackPlugin()]',
771
- '```',
804
+ 'Optional Vite plugin: `import { crank } from "@ahmednawaz/crank/vite"` — only if you are not using React.',
772
805
  '',
773
806
  '## Bookmarklet fallback',
774
807
  '',
@@ -776,9 +809,9 @@ function writeInjectionGuide(projectRoot, env, results) {
776
809
  '',
777
810
  '## Detected in this project',
778
811
  '',
812
+ `- hasReact: ${Boolean(env.hasReact)}`,
779
813
  `- hasVite: ${Boolean(env.hasVite)}`,
780
814
  `- viteConfig: ${env.viteConfig || '(none)'}`,
781
- `- hasReact: ${Boolean(env.hasReact)}`,
782
815
  `- hasNext: ${Boolean(env.hasNext)}`,
783
816
  `- hasWebpack: ${Boolean(env.hasWebpack)}`,
784
817
  ''
@@ -796,14 +829,14 @@ function printSetupNextSteps(env, runtime, results) {
796
829
  if (results?.wrote?.some((w) => String(w).includes('skills'))) {
797
830
  console.log(' • Skill crank-apply-changes installed (auto-syncs on start)');
798
831
  }
799
- if (results?.vite?.patched) {
800
- console.log(' • Vite plugin patched (graceful load via .crank/vite-plugin.cjs)');
832
+ if (results?.reactMount || env.hasReact) {
833
+ console.log(' • React: mount <Crank /> from @ahmednawaz/crank/react (see .crank/react-mount.md)');
834
+ } else if (results?.vite?.patched) {
835
+ console.log(' • Vite plugin patched (non-React stack)');
801
836
  } else if (env.hasVite) {
802
- console.log(' • Vite detected — see .crank/injection.md if the panel does not auto-load');
837
+ console.log(' • Vite detected — see .crank/injection.md');
803
838
  } else if (env.hasNext) {
804
- console.log(' • Next.js — add <CrankScript /> from @ahmednawaz/crank/next (see .crank/injection.md)');
805
- } else if (env.hasReact) {
806
- console.log(' • React without Vite — add <CrankLoader /> from @ahmednawaz/crank/react (see .crank/injection.md)');
839
+ console.log(' • Next.js — mount <Crank /> from @ahmednawaz/crank/next');
807
840
  }
808
841
  console.log(' • Injection guide: .crank/injection.md');
809
842
  if (env.isReplit) {
@@ -858,10 +891,10 @@ function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
858
891
  '## Panel in preview',
859
892
  '',
860
893
  `- Companion: ${companionUrl}`,
861
- '- **Vite:** panel auto-loads via `.crank/vite-plugin.cjs` (graceful load). Keep companion Workflow running.',
862
- '- **React/Next without Vite:** see `.crank/injection.md` (`CrankLoader` / `CrankScript`).',
863
- '- **Fallback:** open the companion URL and use the bookmarklet on your preview tab.',
864
- '- Full guide: `.crank/injection.md`',
894
+ '- **React (preferred):** `pnpm add -D @ahmednawaz/crank` then `<Crank />` from `@ahmednawaz/crank/react` (Retune pattern).',
895
+ '- Set `window.__CRANK_URL__` or `VITE_CRANK_URL` to the public companion URL on Replit.',
896
+ '- Keep companion Workflow running: `npx @ahmednawaz/crank start`.',
897
+ '- See `.crank/react-mount.md` and `.crank/injection.md`.',
865
898
  ''
866
899
  ].join('\n')
867
900
  );
@@ -881,6 +914,7 @@ module.exports = {
881
914
  writeVitePluginStub,
882
915
  patchViteConfig,
883
916
  writeInjectionGuide,
917
+ writeReactMountHint,
884
918
  patchReplitConfig,
885
919
  copyAgentsDoc
886
920
  };
package/next.js CHANGED
@@ -1,63 +1,53 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Optional Next.js adapter — injects Crank via next/script (App Router or Pages).
5
- *
6
- * Prefer Vite plugin for Vite apps. Use this for Next.js when there is no vite.config.
7
- *
8
- * // app/layout.tsx (or a client layout section)
9
- * import { CrankScript } from '@ahmednawaz/crank/next'
10
- * <CrankScript />
11
- *
12
- * peerDependencies: react, next
4
+ * CJS Next adapter — prefer ESM `next.mjs` via package exports.
13
5
  */
14
6
 
15
- const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
16
- const { CrankLoader } = require('./react');
7
+ const {
8
+ Crank: CrankEffect,
9
+ resolveCrankLoaderSrc,
10
+ resolveCrankCompanionUrl
11
+ } = require('./react.js');
17
12
 
18
13
  function getReact() {
19
14
  try {
20
15
  return require('react');
21
16
  } catch {
22
- throw new Error(
23
- '@ahmednawaz/crank/next requires react. Prefer @ahmednawaz/crank/vite for Vite apps.'
24
- );
17
+ throw new Error('@ahmednawaz/crank/next requires react');
25
18
  }
26
19
  }
27
20
 
28
- function getNextScript() {
21
+ function Crank(props) {
22
+ const React = getReact();
23
+ props = props || {};
24
+ const src = resolveCrankLoaderSrc(props);
25
+ const strategy = props.strategy || 'afterInteractive';
26
+
27
+ let Script = null;
29
28
  try {
30
- return require('next/script');
29
+ const mod = require('next/script');
30
+ Script = mod.default || mod;
31
31
  } catch {
32
- return null;
32
+ Script = null;
33
33
  }
34
- }
35
-
36
- /**
37
- * @param {{ src?: string, companionUrl?: string, strategy?: string }} props
38
- */
39
- function CrankScript(props) {
40
- const React = getReact();
41
- const NextScript = getNextScript();
42
- const src = resolveCompanionLoaderUrl(props || {});
43
- const strategy = (props && props.strategy) || 'afterInteractive';
44
34
 
45
- if (NextScript) {
46
- const Script = NextScript.default || NextScript;
35
+ if (Script) {
47
36
  return React.createElement(Script, {
48
- src,
37
+ src: src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now(),
49
38
  strategy,
50
39
  'data-crank-loader': '1'
51
40
  });
52
41
  }
53
42
 
54
- // Fallback when next/script is unavailable (tests / non-Next bundlers).
55
- return React.createElement(CrankLoader, props || {});
43
+ return React.createElement(CrankEffect, props);
56
44
  }
57
45
 
58
46
  module.exports = {
59
- CrankScript,
60
- CrankLoader,
61
- Crank: CrankScript,
62
- resolveCompanionLoaderUrl
47
+ Crank,
48
+ CrankScript: Crank,
49
+ CrankLoader: Crank,
50
+ resolveCrankLoaderSrc,
51
+ resolveCrankCompanionUrl,
52
+ default: Crank
63
53
  };
package/next.mjs ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Next.js entry — same Retune-style `<Crank />` (client component).
3
+ *
4
+ * import { Crank } from '@ahmednawaz/crank/next'
5
+ * <Crank />
6
+ */
7
+
8
+ 'use client';
9
+
10
+ export {
11
+ Crank,
12
+ CrankLoader,
13
+ resolveCrankCompanionUrl,
14
+ resolveCrankLoaderSrc
15
+ } from './react.mjs';
16
+
17
+ export { Crank as CrankScript, Crank as default } from './react.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmednawaz/crank",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
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",
@@ -10,12 +10,28 @@
10
10
  "bin": {
11
11
  "crank": "bin/crank.js"
12
12
  },
13
- "main": "vite.js",
13
+ "main": "./react.js",
14
+ "module": "./react.mjs",
15
+ "types": "./react.d.ts",
14
16
  "exports": {
15
- ".": "./vite.js",
17
+ ".": {
18
+ "types": "./react.d.ts",
19
+ "import": "./react.mjs",
20
+ "require": "./react.js",
21
+ "default": "./react.mjs"
22
+ },
23
+ "./react": {
24
+ "types": "./react.d.ts",
25
+ "import": "./react.mjs",
26
+ "require": "./react.js",
27
+ "default": "./react.mjs"
28
+ },
29
+ "./next": {
30
+ "import": "./next.mjs",
31
+ "require": "./next.js",
32
+ "default": "./next.mjs"
33
+ },
16
34
  "./vite": "./vite.js",
17
- "./react": "./react.js",
18
- "./next": "./next.js",
19
35
  "./webpack": "./webpack.js",
20
36
  "./package.json": "./package.json"
21
37
  },
@@ -25,9 +41,15 @@
25
41
  "react": ">=17"
26
42
  },
27
43
  "peerDependenciesMeta": {
28
- "react": { "optional": true },
29
- "next": { "optional": true },
30
- "html-webpack-plugin": { "optional": true }
44
+ "react": {
45
+ "optional": true
46
+ },
47
+ "next": {
48
+ "optional": true
49
+ },
50
+ "html-webpack-plugin": {
51
+ "optional": true
52
+ }
31
53
  },
32
54
  "workspaces": [
33
55
  "companion",
@@ -71,7 +93,10 @@
71
93
  "panel",
72
94
  "vite.js",
73
95
  "react.js",
96
+ "react.mjs",
97
+ "react.d.ts",
74
98
  "next.js",
99
+ "next.mjs",
75
100
  "webpack.js",
76
101
  ".env.example",
77
102
  "AGENTS.md",
@@ -87,6 +112,7 @@
87
112
  "glean",
88
113
  "mcp",
89
114
  "copy",
90
- "ux-writing"
115
+ "ux-writing",
116
+ "react"
91
117
  ]
92
118
  }
package/react.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ export interface CrankProps {
4
+ /** Full loader script URL (overrides companionUrl). */
5
+ src?: string;
6
+ /** Companion base URL, e.g. https://3344-….replit.dev */
7
+ companionUrl?: string;
8
+ /** Alias for companionUrl. */
9
+ url?: string;
10
+ }
11
+
12
+ export declare function resolveCrankCompanionUrl(props?: CrankProps): string;
13
+ export declare function resolveCrankLoaderSrc(props?: CrankProps): string;
14
+
15
+ /**
16
+ * Retune-style mount — injects companion `/dev-loader.js` once. Renders null.
17
+ *
18
+ * import { Crank } from '@ahmednawaz/crank/react'
19
+ * <Crank />
20
+ *
21
+ * Optional: `window.__CRANK_URL__` or `VITE_CRANK_URL` for Replit public companion host.
22
+ * Keep companion running: `npx @ahmednawaz/crank start`
23
+ */
24
+ export declare function Crank(props?: CrankProps): ReactElement | null;
25
+ export declare const CrankLoader: typeof Crank;
26
+ export default Crank;
27
+
28
+ declare global {
29
+ interface Window {
30
+ __CRANK_URL__?: string;
31
+ __CRANK_LOADER__?: boolean;
32
+ }
33
+ }
34
+
35
+ export {};
package/react.js CHANGED
@@ -1,56 +1,61 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Optional React adapter for non-Vite React apps (CRA, custom webpack, etc.).
5
- *
6
- * This is NOT a Retune-style UI component. It only injects the same /dev-loader.js
7
- * script the Vite plugin uses. Prefer `@ahmednawaz/crank/vite` when the app uses Vite.
8
- *
9
- * import { CrankLoader } from '@ahmednawaz/crank/react'
10
- * // in App / root layout (client-only):
11
- * <CrankLoader />
12
- *
13
- * peerDependency: react >= 17
4
+ * CJS build of @ahmednawaz/crank/react (browser-safe, no Node/fs).
5
+ * Vite / modern apps should import the ESM build via package exports "import" condition.
14
6
  */
15
7
 
16
- const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
17
-
18
8
  function getReact() {
19
9
  try {
20
- // eslint-disable-next-line import/no-extraneous-dependencies
21
10
  return require('react');
22
11
  } catch {
23
12
  throw new Error(
24
- '@ahmednawaz/crank/react requires react as a peer dependency. Prefer the Vite plugin when possible: import { crank } from "@ahmednawaz/crank/vite".'
13
+ '@ahmednawaz/crank/react requires react as a peer dependency (npm i react / pnpm add react).'
25
14
  );
26
15
  }
27
16
  }
28
17
 
29
- /**
30
- * Injects Crank's companion loader once on the client. Renders null.
31
- * @param {{ src?: string, companionUrl?: string, port?: string|number, host?: string }} props
32
- */
33
- function CrankLoader(props) {
18
+ function resolveCrankCompanionUrl(props) {
19
+ props = props || {};
20
+ if (typeof window !== 'undefined' && window.__CRANK_URL__) {
21
+ return String(window.__CRANK_URL__).replace(/\/$/, '');
22
+ }
23
+ if (props.companionUrl) {
24
+ return String(props.companionUrl).replace(/\/$/, '');
25
+ }
26
+ if (props.url) {
27
+ return String(props.url).replace(/\/$/, '');
28
+ }
29
+ return 'http://localhost:3344';
30
+ }
31
+
32
+ function resolveCrankLoaderSrc(props) {
33
+ props = props || {};
34
+ if (props.src) {
35
+ return String(props.src);
36
+ }
37
+ return `${resolveCrankCompanionUrl(props)}/dev-loader.js`;
38
+ }
39
+
40
+ function Crank(props) {
34
41
  const React = getReact();
35
- const src = resolveCompanionLoaderUrl(props || {});
42
+ props = props || {};
43
+ const src = resolveCrankLoaderSrc(props);
36
44
 
37
45
  React.useEffect(() => {
38
46
  if (typeof document === 'undefined') {
39
47
  return undefined;
40
48
  }
41
- if (window.__CRANK_LOADER__) {
49
+ if (document.querySelector('script[data-crank-loader="1"]')) {
42
50
  return undefined;
43
51
  }
44
- window.__CRANK_LOADER__ = true;
45
-
46
- const existing = document.querySelector('script[data-crank-loader="1"]');
47
- if (existing) {
52
+ if (window.__CRANK_LOADER__ && document.getElementById('vp-shell')) {
48
53
  return undefined;
49
54
  }
50
55
 
51
56
  const script = document.createElement('script');
52
- script.src = src;
53
- script.defer = true;
57
+ script.src = src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now();
58
+ script.async = true;
54
59
  script.dataset.crankLoader = '1';
55
60
  document.documentElement.appendChild(script);
56
61
  return undefined;
@@ -59,11 +64,10 @@ function CrankLoader(props) {
59
64
  return null;
60
65
  }
61
66
 
62
- /** Alias — still only a script injector, not an in-tree panel UI. */
63
- const Crank = CrankLoader;
64
-
65
67
  module.exports = {
66
- CrankLoader,
67
68
  Crank,
68
- resolveCompanionLoaderUrl
69
+ CrankLoader: Crank,
70
+ resolveCrankCompanionUrl,
71
+ resolveCrankLoaderSrc,
72
+ default: Crank
69
73
  };
package/react.mjs ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Retune-style React entry — browser-only, Vite-bundlable (no Node/fs).
3
+ *
4
+ * // main.tsx
5
+ * import { Crank } from '@ahmednawaz/crank/react'
6
+ * // or: import { Crank } from '@ahmednawaz/crank'
7
+ *
8
+ * <Crank />
9
+ *
10
+ * Mount near your app root. Injects companion `/dev-loader.js` once (same panel as the
11
+ * optional Vite plugin). Requires the companion process: `npx @ahmednawaz/crank start`
12
+ *
13
+ * peerDependency: react >= 17
14
+ */
15
+
16
+ import { useEffect } from 'react';
17
+
18
+ /**
19
+ * @param {{ companionUrl?: string, url?: string, src?: string }} [props]
20
+ * @returns {string}
21
+ */
22
+ export function resolveCrankCompanionUrl(props = {}) {
23
+ if (typeof window !== 'undefined' && window.__CRANK_URL__) {
24
+ return String(window.__CRANK_URL__).replace(/\/$/, '');
25
+ }
26
+ if (props.companionUrl) {
27
+ return String(props.companionUrl).replace(/\/$/, '');
28
+ }
29
+ if (props.url) {
30
+ return String(props.url).replace(/\/$/, '');
31
+ }
32
+ try {
33
+ const envUrl =
34
+ typeof import.meta !== 'undefined' &&
35
+ import.meta.env &&
36
+ (import.meta.env.VITE_CRANK_URL || import.meta.env.VITE_CRANK_COMPANION_URL);
37
+ if (envUrl) {
38
+ return String(envUrl).replace(/\/$/, '');
39
+ }
40
+ } catch {
41
+ // ignore
42
+ }
43
+ return 'http://localhost:3344';
44
+ }
45
+
46
+ /**
47
+ * @param {{ companionUrl?: string, url?: string, src?: string }} [props]
48
+ * @returns {string}
49
+ */
50
+ export function resolveCrankLoaderSrc(props = {}) {
51
+ if (props.src) {
52
+ return String(props.src);
53
+ }
54
+ return `${resolveCrankCompanionUrl(props)}/dev-loader.js`;
55
+ }
56
+
57
+ /**
58
+ * Retune-style one-liner. Renders null; injects the companion loader script on the client.
59
+ *
60
+ * Do not set window.__CRANK_LOADER__ here — `/dev-loader.js` owns that flag and loads the panel.
61
+ *
62
+ * @param {{ companionUrl?: string, url?: string, src?: string }} [props]
63
+ */
64
+ export function Crank(props = {}) {
65
+ const src = resolveCrankLoaderSrc(props);
66
+
67
+ useEffect(() => {
68
+ if (typeof document === 'undefined') {
69
+ return undefined;
70
+ }
71
+ if (document.querySelector('script[data-crank-loader="1"]')) {
72
+ return undefined;
73
+ }
74
+ // Panel already live (bookmarklet / prior mount)
75
+ if (window.__CRANK_LOADER__ && document.getElementById('vp-shell')) {
76
+ return undefined;
77
+ }
78
+
79
+ const script = document.createElement('script');
80
+ script.src = src.includes('?') ? src : `${src}?t=${Date.now()}`;
81
+ script.async = true;
82
+ script.dataset.crankLoader = '1';
83
+ document.documentElement.appendChild(script);
84
+ return undefined;
85
+ }, [src]);
86
+
87
+ return null;
88
+ }
89
+
90
+ /** Explicit alias. */
91
+ export const CrankLoader = Crank;
92
+
93
+ export default Crank;
package/skill/SKILL.md CHANGED
@@ -33,7 +33,7 @@ When the user asks to start Crank:
33
33
  ```
34
34
  `start` skips setup/init — companion on **3344** (+ HTTP MCP on **3345** on Replit). Add `CRANK_TEAM_TOKEN` in Secrets first.
35
35
  4. Call **`crank_get_environment`** after the companion is up.
36
- 5. **Panel injection:** Vite apps use the Vite plugin (graceful `.crank/vite-plugin.cjs`)not a React component. See **AGENTS.md** / `.crank/injection.md`. Keep companion running in a Workflow.
36
+ 5. **Panel (React):** `pnpm add -D @ahmednawaz/crank` then mount `<Crank />` from `@ahmednawaz/crank/react` in `main.tsx` (Retune pattern no Vite plugin). Keep companion Workflow running. See `.crank/react-mount.md`.
37
37
 
38
38
  ## When the user says "start Crank" or pastes `npx crank`
39
39