@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 +25 -10
- package/README.md +23 -8
- package/companion/dev-loader.js +2 -2
- package/lib/init.js +104 -70
- package/next.js +26 -36
- package/next.mjs +17 -0
- package/package.json +35 -9
- package/react.d.ts +35 -0
- package/react.js +36 -32
- package/react.mjs +93 -0
- package/skill/SKILL.md +1 -1
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 (
|
|
106
|
+
## Panel injection (Retune pattern)
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
**Preferred for React apps** — install the package and mount once:
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
105
|
+
### Panel injection (Retune-style React)
|
|
106
106
|
|
|
107
|
-
|
|
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
|
-
| **
|
|
112
|
-
| **Next.js** | `import {
|
|
113
|
-
| **React
|
|
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
|
package/companion/dev-loader.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Injected by
|
|
5
|
-
* Loads the Crank panel from the
|
|
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
|
|
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
|
-
|
|
451
|
+
const scoped = '@ahmednawaz/crank';
|
|
452
|
+
let versionSpec;
|
|
452
453
|
if (process.env.CRANK_NPM_SPEC) {
|
|
453
|
-
|
|
454
|
+
versionSpec = process.env.CRANK_NPM_SPEC;
|
|
454
455
|
} else {
|
|
455
|
-
|
|
456
|
+
versionSpec = `^${crankPkg.version || '0.2.0'}`;
|
|
456
457
|
}
|
|
457
458
|
|
|
458
|
-
|
|
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
|
-
|
|
469
|
+
|
|
462
470
|
writeJson(pkgPath, pkg);
|
|
463
|
-
results.wrote.push(
|
|
464
|
-
results.crankDependencyAdded =
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
716
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
744
|
-
|
|
777
|
+
'```tsx',
|
|
778
|
+
"import { Crank } from '@ahmednawaz/crank/react'",
|
|
745
779
|
'',
|
|
746
|
-
'
|
|
780
|
+
'// main.tsx — mount once',
|
|
781
|
+
'<App />',
|
|
782
|
+
'<Crank />',
|
|
783
|
+
'```',
|
|
747
784
|
'',
|
|
748
|
-
'
|
|
785
|
+
'Companion must be running: `npx @ahmednawaz/crank start`',
|
|
749
786
|
'',
|
|
750
|
-
'
|
|
751
|
-
|
|
752
|
-
'
|
|
753
|
-
'
|
|
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
|
-
'
|
|
793
|
+
'See `.crank/react-mount.md` when this project has React.',
|
|
757
794
|
'',
|
|
758
795
|
'## Next.js',
|
|
759
796
|
'',
|
|
760
797
|
'```tsx',
|
|
761
|
-
"import {
|
|
762
|
-
'
|
|
763
|
-
'<CrankScript />',
|
|
798
|
+
"import { Crank } from '@ahmednawaz/crank/next'",
|
|
799
|
+
'<Crank />',
|
|
764
800
|
'```',
|
|
765
801
|
'',
|
|
766
|
-
'##
|
|
802
|
+
'## Non-React Vite (Vue / Svelte / vanilla)',
|
|
767
803
|
'',
|
|
768
|
-
'
|
|
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?.
|
|
800
|
-
console.log(' •
|
|
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
|
|
837
|
+
console.log(' • Vite detected — see .crank/injection.md');
|
|
803
838
|
} else if (env.hasNext) {
|
|
804
|
-
console.log(' • Next.js —
|
|
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
|
-
'- **
|
|
862
|
-
'-
|
|
863
|
-
'-
|
|
864
|
-
'-
|
|
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
|
-
*
|
|
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 {
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
29
|
+
const mod = require('next/script');
|
|
30
|
+
Script = mod.default || mod;
|
|
31
31
|
} catch {
|
|
32
|
-
|
|
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 (
|
|
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
|
-
|
|
55
|
-
return React.createElement(CrankLoader, props || {});
|
|
43
|
+
return React.createElement(CrankEffect, props);
|
|
56
44
|
}
|
|
57
45
|
|
|
58
46
|
module.exports = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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": "
|
|
13
|
+
"main": "./react.js",
|
|
14
|
+
"module": "./react.mjs",
|
|
15
|
+
"types": "./react.d.ts",
|
|
14
16
|
"exports": {
|
|
15
|
-
".":
|
|
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": {
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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 (
|
|
49
|
+
if (document.querySelector('script[data-crank-loader="1"]')) {
|
|
42
50
|
return undefined;
|
|
43
51
|
}
|
|
44
|
-
window.__CRANK_LOADER__
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|