@ahmednawaz/crank 0.1.8 → 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 +32 -0
- package/README.md +29 -0
- package/companion/dev-loader.js +2 -2
- package/lib/detect.js +128 -28
- package/lib/init.js +360 -52
- package/lib/loader-url.js +55 -0
- package/next.js +53 -0
- package/next.mjs +17 -0
- package/package.json +46 -4
- package/react.d.ts +35 -0
- package/react.js +73 -0
- package/react.mjs +93 -0
- package/skill/SKILL.md +1 -0
- package/vite.js +7 -41
- package/webpack.js +58 -0
package/AGENTS.md
CHANGED
|
@@ -103,6 +103,38 @@ See **[REPLIT.md](./REPLIT.md)** and `.crank/replit.md` (URLs after first run).
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
+
## Panel injection (Retune pattern)
|
|
107
|
+
|
|
108
|
+
**Preferred for React apps** — install the package and mount once:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm add -D @ahmednawaz/crank
|
|
112
|
+
# or: npm i -D @ahmednawaz/crank
|
|
113
|
+
```
|
|
114
|
+
|
|
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 |
|
|
133
|
+
|
|
134
|
+
Agents: do **not** invent Vite-plugin path workarounds for React apps. See `.crank/react-mount.md` / `.crank/injection.md` after setup.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
106
138
|
## MCP tools (agent pull workflow)
|
|
107
139
|
|
|
108
140
|
When the user clicks **Agent** on a copy variant or says *“Apply the pending Crank change”*:
|
package/README.md
CHANGED
|
@@ -102,6 +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 (Retune-style React)
|
|
106
|
+
|
|
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`.
|
|
126
|
+
|
|
127
|
+
| Stack | Integration |
|
|
128
|
+
|-------|-------------|
|
|
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'` |
|
|
132
|
+
| **Fallback** | Companion bookmarklet |
|
|
133
|
+
|
|
105
134
|
Useful variants:
|
|
106
135
|
|
|
107
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/detect.js
CHANGED
|
@@ -4,6 +4,41 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
const VITE_CONFIG_NAMES = [
|
|
8
|
+
'vite.config.ts',
|
|
9
|
+
'vite.config.js',
|
|
10
|
+
'vite.config.mjs',
|
|
11
|
+
'vite.config.cjs',
|
|
12
|
+
'vite.config.mts',
|
|
13
|
+
'vite.config.cts'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const APP_DIR_CANDIDATES = [
|
|
17
|
+
'.',
|
|
18
|
+
'app',
|
|
19
|
+
'client',
|
|
20
|
+
'web',
|
|
21
|
+
'frontend',
|
|
22
|
+
'ui',
|
|
23
|
+
'src',
|
|
24
|
+
'packages/app',
|
|
25
|
+
'packages/web',
|
|
26
|
+
'apps/web',
|
|
27
|
+
'apps/frontend'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const SKIP_DIRS = new Set([
|
|
31
|
+
'node_modules',
|
|
32
|
+
'.git',
|
|
33
|
+
'dist',
|
|
34
|
+
'build',
|
|
35
|
+
'.next',
|
|
36
|
+
'coverage',
|
|
37
|
+
'.turbo',
|
|
38
|
+
'.cache',
|
|
39
|
+
'.crank'
|
|
40
|
+
]);
|
|
41
|
+
|
|
7
42
|
function exists(p) {
|
|
8
43
|
try {
|
|
9
44
|
return fs.existsSync(p);
|
|
@@ -25,21 +60,97 @@ function writeJson(file, data) {
|
|
|
25
60
|
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
26
61
|
}
|
|
27
62
|
|
|
63
|
+
function findViteConfigInDir(dir) {
|
|
64
|
+
for (const name of VITE_CONFIG_NAMES) {
|
|
65
|
+
const full = path.join(dir, name);
|
|
66
|
+
if (exists(full)) {
|
|
67
|
+
return full;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find vite.config.* at project root, common app subdirs, then shallow BFS.
|
|
75
|
+
*/
|
|
76
|
+
function findViteConfig(projectRoot) {
|
|
77
|
+
const root = path.resolve(projectRoot);
|
|
78
|
+
for (const sub of APP_DIR_CANDIDATES) {
|
|
79
|
+
const dir = sub === '.' ? root : path.join(root, sub);
|
|
80
|
+
const hit = findViteConfigInDir(dir);
|
|
81
|
+
if (hit) {
|
|
82
|
+
return hit;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shallow walk (depth 2) for monorepos / Replit layouts
|
|
87
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
88
|
+
while (queue.length) {
|
|
89
|
+
const { dir, depth } = queue.shift();
|
|
90
|
+
if (depth > 0) {
|
|
91
|
+
const hit = findViteConfigInDir(dir);
|
|
92
|
+
if (hit) {
|
|
93
|
+
return hit;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (depth >= 2) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function packageHasDep(pkg, name) {
|
|
116
|
+
if (!pkg || typeof pkg !== 'object') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return Boolean(
|
|
120
|
+
pkg.dependencies?.[name] ||
|
|
121
|
+
pkg.devDependencies?.[name] ||
|
|
122
|
+
pkg.peerDependencies?.[name]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
28
126
|
function detectEnvironment(projectRoot) {
|
|
29
127
|
const env = process.env;
|
|
30
128
|
const isReplit = Boolean(
|
|
31
129
|
env.REPL_ID || env.REPL_SLUG || env.REPLIT_DEV_DOMAIN || env.REPLIT_DB_URL
|
|
32
130
|
);
|
|
33
|
-
const hasVite =
|
|
34
|
-
exists(path.join(projectRoot, 'vite.config.ts')) ||
|
|
35
|
-
exists(path.join(projectRoot, 'vite.config.js')) ||
|
|
36
|
-
exists(path.join(projectRoot, 'vite.config.mjs')) ||
|
|
37
|
-
exists(path.join(projectRoot, 'vite.config.cjs'));
|
|
38
131
|
const hasPackageJson = exists(path.join(projectRoot, 'package.json'));
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
132
|
+
const packageJson = hasPackageJson
|
|
133
|
+
? readJson(path.join(projectRoot, 'package.json'), {})
|
|
134
|
+
: null;
|
|
135
|
+
const viteConfig = findViteConfig(projectRoot);
|
|
136
|
+
const hasVite = Boolean(viteConfig) || packageHasDep(packageJson, 'vite');
|
|
137
|
+
const hasReact =
|
|
138
|
+
packageHasDep(packageJson, 'react') ||
|
|
139
|
+
exists(path.join(projectRoot, 'next.config.js')) ||
|
|
140
|
+
exists(path.join(projectRoot, 'next.config.mjs')) ||
|
|
141
|
+
exists(path.join(projectRoot, 'next.config.ts'));
|
|
142
|
+
const hasNext =
|
|
143
|
+
packageHasDep(packageJson, 'next') ||
|
|
144
|
+
exists(path.join(projectRoot, 'next.config.js')) ||
|
|
145
|
+
exists(path.join(projectRoot, 'next.config.mjs')) ||
|
|
146
|
+
exists(path.join(projectRoot, 'next.config.ts'));
|
|
147
|
+
const hasWebpack =
|
|
148
|
+
packageHasDep(packageJson, 'webpack') ||
|
|
149
|
+
exists(path.join(projectRoot, 'webpack.config.js')) ||
|
|
150
|
+
exists(path.join(projectRoot, 'webpack.config.ts'));
|
|
151
|
+
|
|
152
|
+
const hasCursor =
|
|
153
|
+
exists(path.join(projectRoot, '.cursor')) || exists(path.join(os.homedir(), '.cursor'));
|
|
43
154
|
const claudeConfig = path.join(
|
|
44
155
|
os.homedir(),
|
|
45
156
|
'Library',
|
|
@@ -52,37 +163,26 @@ function detectEnvironment(projectRoot) {
|
|
|
52
163
|
return {
|
|
53
164
|
isReplit,
|
|
54
165
|
hasVite,
|
|
166
|
+
viteConfig,
|
|
167
|
+
hasReact,
|
|
168
|
+
hasNext,
|
|
169
|
+
hasWebpack,
|
|
55
170
|
hasPackageJson,
|
|
56
171
|
hasCursor,
|
|
57
172
|
hasVsCode: exists(path.join(projectRoot, '.vscode')),
|
|
58
173
|
hasClaudeDesktop,
|
|
59
174
|
claudeConfig,
|
|
60
175
|
replitDomain: env.REPLIT_DEV_DOMAIN || null,
|
|
61
|
-
packageJson
|
|
62
|
-
? readJson(path.join(projectRoot, 'package.json'), {})
|
|
63
|
-
: null
|
|
176
|
+
packageJson
|
|
64
177
|
};
|
|
65
178
|
}
|
|
66
179
|
|
|
67
|
-
function findViteConfig(projectRoot) {
|
|
68
|
-
for (const name of [
|
|
69
|
-
'vite.config.ts',
|
|
70
|
-
'vite.config.js',
|
|
71
|
-
'vite.config.mjs',
|
|
72
|
-
'vite.config.cjs'
|
|
73
|
-
]) {
|
|
74
|
-
const full = path.join(projectRoot, name);
|
|
75
|
-
if (exists(full)) {
|
|
76
|
-
return full;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
180
|
module.exports = {
|
|
83
181
|
exists,
|
|
84
182
|
readJson,
|
|
85
183
|
writeJson,
|
|
86
184
|
detectEnvironment,
|
|
87
|
-
findViteConfig
|
|
185
|
+
findViteConfig,
|
|
186
|
+
VITE_CONFIG_NAMES,
|
|
187
|
+
APP_DIR_CANDIDATES
|
|
88
188
|
};
|
package/lib/init.js
CHANGED
|
@@ -94,59 +94,214 @@ function mergeVsCodeMcp(file, projectRoot) {
|
|
|
94
94
|
return file;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function writeVitePluginStub(projectRoot) {
|
|
98
|
+
const dir = path.join(projectRoot, '.crank');
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
const stubPath = path.join(dir, 'vite-plugin.cjs');
|
|
101
|
+
const stub = `'use strict';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Crank Vite plugin loader — Vizpatch-style graceful resolve.
|
|
105
|
+
* Generated by \`npx @ahmednawaz/crank setup\`. Does not throw if Crank is missing.
|
|
106
|
+
*
|
|
107
|
+
* Resolves from (in order):
|
|
108
|
+
* 1. .crank/config.json packageRoot (npx cache / local checkout)
|
|
109
|
+
* 2. node_modules/@ahmednawaz/crank or node_modules/crank
|
|
110
|
+
* 3. require('@ahmednawaz/crank/vite')
|
|
111
|
+
*/
|
|
112
|
+
const fs = require('fs');
|
|
113
|
+
const path = require('path');
|
|
114
|
+
|
|
115
|
+
function load() {
|
|
116
|
+
const candidates = [];
|
|
117
|
+
try {
|
|
118
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
119
|
+
if (fs.existsSync(configPath)) {
|
|
120
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
121
|
+
if (cfg && cfg.packageRoot) {
|
|
122
|
+
candidates.push(path.join(cfg.packageRoot, 'vite.js'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cwd = process.cwd();
|
|
130
|
+
candidates.push(
|
|
131
|
+
path.join(cwd, 'node_modules', '@ahmednawaz', 'crank', 'vite.js'),
|
|
132
|
+
path.join(cwd, 'node_modules', 'crank', 'vite.js'),
|
|
133
|
+
path.join(__dirname, '..', 'node_modules', '@ahmednawaz', 'crank', 'vite.js'),
|
|
134
|
+
path.join(__dirname, '..', 'node_modules', 'crank', 'vite.js')
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
for (const file of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
if (file && fs.existsSync(file)) {
|
|
140
|
+
return require(file);
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// try next
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
return require('@ahmednawaz/crank/vite');
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return require('@ahmednawaz/crank');
|
|
154
|
+
} catch {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
return require('crank/vite');
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function noopPlugin() {
|
|
166
|
+
return {
|
|
167
|
+
name: 'crank-dev-loader-noop',
|
|
168
|
+
apply: 'serve',
|
|
169
|
+
transformIndexHtml(html) {
|
|
170
|
+
return html;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const mod = load();
|
|
176
|
+
const crankFn =
|
|
177
|
+
mod && typeof mod.crank === 'function' ? mod.crank : noopPlugin;
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
crank: crankFn,
|
|
181
|
+
crankDevLoader: crankFn,
|
|
182
|
+
resolveCompanionLoaderUrl: mod && mod.resolveCompanionLoaderUrl
|
|
183
|
+
};
|
|
184
|
+
`;
|
|
185
|
+
fs.writeFileSync(stubPath, stub);
|
|
186
|
+
return stubPath;
|
|
187
|
+
}
|
|
188
|
+
|
|
97
189
|
function patchViteConfig(projectRoot) {
|
|
98
190
|
const configPath = findViteConfig(projectRoot);
|
|
191
|
+
const stubPath = writeVitePluginStub(projectRoot);
|
|
192
|
+
|
|
99
193
|
if (!configPath) {
|
|
100
|
-
return { patched: false, reason: 'no-vite-config' };
|
|
194
|
+
return { patched: false, reason: 'no-vite-config', plugin: stubPath };
|
|
101
195
|
}
|
|
196
|
+
|
|
102
197
|
let source = fs.readFileSync(configPath, 'utf8');
|
|
103
|
-
if (
|
|
104
|
-
|
|
198
|
+
if (
|
|
199
|
+
/vite-plugin\.cjs/.test(source) ||
|
|
200
|
+
(/(\bcrank)\s*\(/.test(source) && /plugins/.test(source))
|
|
201
|
+
) {
|
|
202
|
+
return { patched: false, reason: 'already-present', file: configPath, plugin: stubPath };
|
|
105
203
|
}
|
|
106
204
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'vite.js'), localPlugin);
|
|
205
|
+
let relStub = path.relative(path.dirname(configPath), stubPath).replace(/\\/g, '/');
|
|
206
|
+
if (!relStub.startsWith('.')) {
|
|
207
|
+
relStub = `./${relStub}`;
|
|
208
|
+
}
|
|
112
209
|
|
|
113
|
-
const rel = './.crank/vite-plugin.cjs';
|
|
114
210
|
const isEsm =
|
|
115
211
|
configPath.endsWith('.mjs') ||
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
212
|
+
configPath.endsWith('.mts') ||
|
|
213
|
+
(/^import\s/m.test(source) && !configPath.endsWith('.cjs') && !configPath.endsWith('.cts'));
|
|
214
|
+
|
|
215
|
+
if (isEsm) {
|
|
216
|
+
const isTs = /\.tsx?$|\.mts$/.test(configPath);
|
|
217
|
+
const crankDecl = isTs
|
|
218
|
+
? 'let crank: null | (() => unknown) = null'
|
|
219
|
+
: 'let crank = null';
|
|
220
|
+
const gracefulBlock = `import { createRequire } from 'node:module'
|
|
221
|
+
import { existsSync } from 'node:fs'
|
|
222
|
+
import { fileURLToPath } from 'node:url'
|
|
223
|
+
|
|
224
|
+
// Crank Vite plugin — graceful load (same pattern as Vizpatch); skip if unavailable
|
|
225
|
+
${crankDecl}
|
|
226
|
+
try {
|
|
227
|
+
const _crankPluginPath = fileURLToPath(new URL('${relStub}', import.meta.url))
|
|
228
|
+
if (existsSync(_crankPluginPath)) {
|
|
229
|
+
const require = createRequire(import.meta.url)
|
|
230
|
+
crank = require(_crankPluginPath).crank
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
|
|
234
|
+
`;
|
|
235
|
+
if (!/_crankPluginPath/.test(source)) {
|
|
236
|
+
source = gracefulBlock + source;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
240
|
+
source = source.replace(
|
|
241
|
+
/plugins\s*:\s*\[\s*\]/,
|
|
242
|
+
'plugins: [...(crank ? [crank()] : [])]'
|
|
243
|
+
);
|
|
244
|
+
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
245
|
+
source = source.replace(
|
|
246
|
+
/plugins\s*:\s*\[/,
|
|
247
|
+
'plugins: [...(crank ? [crank()] : []), '
|
|
248
|
+
);
|
|
249
|
+
} else if (/defineConfig\s*\(\s*\{/.test(source)) {
|
|
250
|
+
source = source.replace(
|
|
251
|
+
/defineConfig\s*\(\s*\{/,
|
|
252
|
+
'defineConfig({\n plugins: [...(crank ? [crank()] : [])],'
|
|
253
|
+
);
|
|
254
|
+
} else if (/export\s+default\s+\{/.test(source)) {
|
|
255
|
+
source = source.replace(
|
|
256
|
+
/export\s+default\s+\{/,
|
|
257
|
+
'export default {\n plugins: [...(crank ? [crank()] : [])],'
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
return {
|
|
261
|
+
patched: false,
|
|
262
|
+
reason: 'unrecognized-vite-shape',
|
|
263
|
+
file: configPath,
|
|
264
|
+
plugin: stubPath
|
|
265
|
+
};
|
|
266
|
+
}
|
|
144
267
|
} else {
|
|
145
|
-
|
|
268
|
+
const gracefulBlock = `// Crank Vite plugin — graceful load (same pattern as Vizpatch)
|
|
269
|
+
let crank = null;
|
|
270
|
+
try {
|
|
271
|
+
crank = require('${relStub}').crank;
|
|
272
|
+
} catch {}
|
|
273
|
+
|
|
274
|
+
`;
|
|
275
|
+
if (!/vite-plugin\.cjs/.test(source)) {
|
|
276
|
+
source = gracefulBlock + source;
|
|
277
|
+
}
|
|
278
|
+
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
279
|
+
source = source.replace(
|
|
280
|
+
/plugins\s*:\s*\[\s*\]/,
|
|
281
|
+
'plugins: [...(crank ? [crank()] : [])]'
|
|
282
|
+
);
|
|
283
|
+
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
284
|
+
source = source.replace(
|
|
285
|
+
/plugins\s*:\s*\[/,
|
|
286
|
+
'plugins: [...(crank ? [crank()] : []), '
|
|
287
|
+
);
|
|
288
|
+
} else if (/module\.exports\s*=\s*\{/.test(source)) {
|
|
289
|
+
source = source.replace(
|
|
290
|
+
/module\.exports\s*=\s*\{/,
|
|
291
|
+
'module.exports = {\n plugins: [...(crank ? [crank()] : [])],'
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
return {
|
|
295
|
+
patched: false,
|
|
296
|
+
reason: 'unrecognized-vite-shape',
|
|
297
|
+
file: configPath,
|
|
298
|
+
plugin: stubPath
|
|
299
|
+
};
|
|
300
|
+
}
|
|
146
301
|
}
|
|
147
302
|
|
|
148
303
|
fs.writeFileSync(configPath, source);
|
|
149
|
-
return { patched: true, file: configPath, plugin:
|
|
304
|
+
return { patched: true, file: configPath, plugin: stubPath };
|
|
150
305
|
}
|
|
151
306
|
|
|
152
307
|
function writeProjectEnvExample(projectRoot) {
|
|
@@ -278,7 +433,7 @@ function mergeUserCursorMcp(projectRoot, results) {
|
|
|
278
433
|
}
|
|
279
434
|
|
|
280
435
|
/**
|
|
281
|
-
* Retune-style: add crank to devDependencies
|
|
436
|
+
* Retune-style: add @ahmednawaz/crank to devDependencies (scoped — works with pnpm).
|
|
282
437
|
*/
|
|
283
438
|
function ensureCrankDependency(projectRoot, results) {
|
|
284
439
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
@@ -293,20 +448,28 @@ function ensureCrankDependency(projectRoot, results) {
|
|
|
293
448
|
const crankPkg = readJson(path.join(PACKAGE_ROOT, 'package.json'), {});
|
|
294
449
|
pkg.devDependencies = pkg.devDependencies || {};
|
|
295
450
|
|
|
296
|
-
|
|
451
|
+
const scoped = '@ahmednawaz/crank';
|
|
452
|
+
let versionSpec;
|
|
297
453
|
if (process.env.CRANK_NPM_SPEC) {
|
|
298
|
-
|
|
454
|
+
versionSpec = process.env.CRANK_NPM_SPEC;
|
|
299
455
|
} else {
|
|
300
|
-
|
|
456
|
+
versionSpec = `^${crankPkg.version || '0.2.0'}`;
|
|
301
457
|
}
|
|
302
458
|
|
|
303
|
-
|
|
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) {
|
|
304
467
|
return;
|
|
305
468
|
}
|
|
306
|
-
|
|
469
|
+
|
|
307
470
|
writeJson(pkgPath, pkg);
|
|
308
|
-
results.wrote.push(
|
|
309
|
-
results.crankDependencyAdded =
|
|
471
|
+
results.wrote.push(`package.json#devDependencies.${scoped}`);
|
|
472
|
+
results.crankDependencyAdded = `${scoped}@${versionSpec}`;
|
|
310
473
|
}
|
|
311
474
|
|
|
312
475
|
function writeConfig(projectRoot, env) {
|
|
@@ -327,7 +490,11 @@ function writeConfig(projectRoot, env) {
|
|
|
327
490
|
createdAt: new Date().toISOString(),
|
|
328
491
|
environment: {
|
|
329
492
|
isReplit: env.isReplit,
|
|
330
|
-
hasVite: env.hasVite
|
|
493
|
+
hasVite: env.hasVite,
|
|
494
|
+
viteConfig: env.viteConfig || null,
|
|
495
|
+
hasReact: env.hasReact,
|
|
496
|
+
hasNext: env.hasNext,
|
|
497
|
+
hasWebpack: env.hasWebpack
|
|
331
498
|
}
|
|
332
499
|
};
|
|
333
500
|
writeJson(configPath, config);
|
|
@@ -370,14 +537,26 @@ function initProject(projectRoot, options = {}) {
|
|
|
370
537
|
installCrankSkill(projectRoot, results);
|
|
371
538
|
}
|
|
372
539
|
|
|
373
|
-
|
|
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) {
|
|
374
544
|
const viteResult = patchViteConfig(projectRoot);
|
|
375
545
|
results.vite = viteResult;
|
|
376
546
|
if (viteResult.patched) {
|
|
377
547
|
results.wrote.push(path.relative(projectRoot, viteResult.file));
|
|
378
548
|
}
|
|
549
|
+
if (viteResult.plugin) {
|
|
550
|
+
results.wrote.push(path.relative(projectRoot, viteResult.plugin));
|
|
551
|
+
}
|
|
552
|
+
} else if (options.vite !== false) {
|
|
553
|
+
const stub = writeVitePluginStub(projectRoot);
|
|
554
|
+
results.vite = { patched: false, reason: 'no-vite-detected', plugin: stub };
|
|
555
|
+
results.wrote.push(path.relative(projectRoot, stub));
|
|
379
556
|
}
|
|
380
557
|
|
|
558
|
+
writeInjectionGuide(projectRoot, env, results);
|
|
559
|
+
|
|
381
560
|
// package.json script convenience
|
|
382
561
|
if (env.hasPackageJson && options.scripts !== false) {
|
|
383
562
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
@@ -530,6 +709,117 @@ function copyAgentsDoc(projectRoot, results) {
|
|
|
530
709
|
}
|
|
531
710
|
}
|
|
532
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
|
+
|
|
762
|
+
function writeInjectionGuide(projectRoot, env, results) {
|
|
763
|
+
const guide = path.join(projectRoot, '.crank', 'injection.md');
|
|
764
|
+
fs.mkdirSync(path.dirname(guide), { recursive: true });
|
|
765
|
+
const lines = [
|
|
766
|
+
'# Crank panel injection',
|
|
767
|
+
'',
|
|
768
|
+
'## Preferred — React component (Retune pattern)',
|
|
769
|
+
'',
|
|
770
|
+
'Crank ships a normal npm React export. Vite bundles it like any other dependency.',
|
|
771
|
+
'**No Vite plugin. No npx-cache paths.**',
|
|
772
|
+
'',
|
|
773
|
+
'```bash',
|
|
774
|
+
'pnpm add -D @ahmednawaz/crank',
|
|
775
|
+
'```',
|
|
776
|
+
'',
|
|
777
|
+
'```tsx',
|
|
778
|
+
"import { Crank } from '@ahmednawaz/crank/react'",
|
|
779
|
+
'',
|
|
780
|
+
'// main.tsx — mount once',
|
|
781
|
+
'<App />',
|
|
782
|
+
'<Crank />',
|
|
783
|
+
'```',
|
|
784
|
+
'',
|
|
785
|
+
'Companion must be running: `npx @ahmednawaz/crank start`',
|
|
786
|
+
'',
|
|
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`',
|
|
792
|
+
'',
|
|
793
|
+
'See `.crank/react-mount.md` when this project has React.',
|
|
794
|
+
'',
|
|
795
|
+
'## Next.js',
|
|
796
|
+
'',
|
|
797
|
+
'```tsx',
|
|
798
|
+
"import { Crank } from '@ahmednawaz/crank/next'",
|
|
799
|
+
'<Crank />',
|
|
800
|
+
'```',
|
|
801
|
+
'',
|
|
802
|
+
'## Non-React Vite (Vue / Svelte / vanilla)',
|
|
803
|
+
'',
|
|
804
|
+
'Optional Vite plugin: `import { crank } from "@ahmednawaz/crank/vite"` — only if you are not using React.',
|
|
805
|
+
'',
|
|
806
|
+
'## Bookmarklet fallback',
|
|
807
|
+
'',
|
|
808
|
+
'Open the companion URL → drag the bookmarklet onto any preview page.',
|
|
809
|
+
'',
|
|
810
|
+
'## Detected in this project',
|
|
811
|
+
'',
|
|
812
|
+
`- hasReact: ${Boolean(env.hasReact)}`,
|
|
813
|
+
`- hasVite: ${Boolean(env.hasVite)}`,
|
|
814
|
+
`- viteConfig: ${env.viteConfig || '(none)'}`,
|
|
815
|
+
`- hasNext: ${Boolean(env.hasNext)}`,
|
|
816
|
+
`- hasWebpack: ${Boolean(env.hasWebpack)}`,
|
|
817
|
+
''
|
|
818
|
+
];
|
|
819
|
+
fs.writeFileSync(guide, lines.join('\n'));
|
|
820
|
+
results.wrote.push('.crank/injection.md');
|
|
821
|
+
}
|
|
822
|
+
|
|
533
823
|
function printSetupNextSteps(env, runtime, results) {
|
|
534
824
|
console.log('');
|
|
535
825
|
console.log(' Setup complete (Retune-style):');
|
|
@@ -539,6 +829,16 @@ function printSetupNextSteps(env, runtime, results) {
|
|
|
539
829
|
if (results?.wrote?.some((w) => String(w).includes('skills'))) {
|
|
540
830
|
console.log(' • Skill crank-apply-changes installed (auto-syncs on start)');
|
|
541
831
|
}
|
|
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)');
|
|
836
|
+
} else if (env.hasVite) {
|
|
837
|
+
console.log(' • Vite detected — see .crank/injection.md');
|
|
838
|
+
} else if (env.hasNext) {
|
|
839
|
+
console.log(' • Next.js — mount <Crank /> from @ahmednawaz/crank/next');
|
|
840
|
+
}
|
|
841
|
+
console.log(' • Injection guide: .crank/injection.md');
|
|
542
842
|
if (env.isReplit) {
|
|
543
843
|
console.log(' • Workflow command: npx @ahmednawaz/crank start');
|
|
544
844
|
console.log(' • Add CRANK_TEAM_TOKEN in Secrets (not in the command)');
|
|
@@ -591,8 +891,10 @@ function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
|
|
|
591
891
|
'## Panel in preview',
|
|
592
892
|
'',
|
|
593
893
|
`- Companion: ${companionUrl}`,
|
|
594
|
-
'-
|
|
595
|
-
'-
|
|
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`.',
|
|
596
898
|
''
|
|
597
899
|
].join('\n')
|
|
598
900
|
);
|
|
@@ -608,5 +910,11 @@ module.exports = {
|
|
|
608
910
|
configureMcpRetuneStyle,
|
|
609
911
|
printSetupNextSteps,
|
|
610
912
|
writeReplitGuide,
|
|
611
|
-
|
|
913
|
+
writeReplitPlatformGuide,
|
|
914
|
+
writeVitePluginStub,
|
|
915
|
+
patchViteConfig,
|
|
916
|
+
writeInjectionGuide,
|
|
917
|
+
writeReactMountHint,
|
|
918
|
+
patchReplitConfig,
|
|
919
|
+
copyAgentsDoc
|
|
612
920
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared companion loader URL for Vite / React / Next / webpack adapters.
|
|
5
|
+
* All paths inject the same /dev-loader.js → bookmarklet panel (not a React UI tree).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { env } = require('./env');
|
|
11
|
+
const { readRuntimeManifest } = require('./urls');
|
|
12
|
+
|
|
13
|
+
function findProjectRoot(startDir) {
|
|
14
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
15
|
+
for (let i = 0; i < 8; i++) {
|
|
16
|
+
if (fs.existsSync(path.join(dir, '.crank', 'runtime.json'))) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) break;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(startDir || process.cwd());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveCompanionLoaderUrl(options = {}) {
|
|
30
|
+
if (options.src) {
|
|
31
|
+
return String(options.src);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectRoot = options.projectRoot || findProjectRoot(process.cwd());
|
|
35
|
+
const runtime = readRuntimeManifest(projectRoot);
|
|
36
|
+
if (runtime?.companion?.devLoader) {
|
|
37
|
+
return runtime.companion.devLoader;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const publicUrl = (
|
|
41
|
+
options.companionUrl ||
|
|
42
|
+
env('PUBLIC_URL', '') ||
|
|
43
|
+
''
|
|
44
|
+
).replace(/\/$/, '');
|
|
45
|
+
|
|
46
|
+
const port = String(options.port || env('PORT', '3344')).replace(/[^0-9]/g, '') || '3344';
|
|
47
|
+
const host = options.host || env('HOST', '127.0.0.1');
|
|
48
|
+
const base = publicUrl || `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
49
|
+
return `${base}/dev-loader.js`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
findProjectRoot,
|
|
54
|
+
resolveCompanionLoaderUrl
|
|
55
|
+
};
|
package/next.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CJS Next adapter — prefer ESM `next.mjs` via package exports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
Crank: CrankEffect,
|
|
9
|
+
resolveCrankLoaderSrc,
|
|
10
|
+
resolveCrankCompanionUrl
|
|
11
|
+
} = require('./react.js');
|
|
12
|
+
|
|
13
|
+
function getReact() {
|
|
14
|
+
try {
|
|
15
|
+
return require('react');
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error('@ahmednawaz/crank/next requires react');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
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;
|
|
28
|
+
try {
|
|
29
|
+
const mod = require('next/script');
|
|
30
|
+
Script = mod.default || mod;
|
|
31
|
+
} catch {
|
|
32
|
+
Script = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Script) {
|
|
36
|
+
return React.createElement(Script, {
|
|
37
|
+
src: src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now(),
|
|
38
|
+
strategy,
|
|
39
|
+
'data-crank-loader': '1'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return React.createElement(CrankEffect, props);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
Crank,
|
|
48
|
+
CrankScript: Crank,
|
|
49
|
+
CrankLoader: Crank,
|
|
50
|
+
resolveCrankLoaderSrc,
|
|
51
|
+
resolveCrankCompanionUrl,
|
|
52
|
+
default: Crank
|
|
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,47 @@
|
|
|
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",
|
|
35
|
+
"./webpack": "./webpack.js",
|
|
17
36
|
"./package.json": "./package.json"
|
|
18
37
|
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"html-webpack-plugin": ">=5",
|
|
40
|
+
"next": ">=13",
|
|
41
|
+
"react": ">=17"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"next": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"html-webpack-plugin": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
19
54
|
"workspaces": [
|
|
20
55
|
"companion",
|
|
21
56
|
"mcp-server"
|
|
@@ -57,6 +92,12 @@
|
|
|
57
92
|
"skill",
|
|
58
93
|
"panel",
|
|
59
94
|
"vite.js",
|
|
95
|
+
"react.js",
|
|
96
|
+
"react.mjs",
|
|
97
|
+
"react.d.ts",
|
|
98
|
+
"next.js",
|
|
99
|
+
"next.mjs",
|
|
100
|
+
"webpack.js",
|
|
60
101
|
".env.example",
|
|
61
102
|
"AGENTS.md",
|
|
62
103
|
"REPLIT.md",
|
|
@@ -71,6 +112,7 @@
|
|
|
71
112
|
"glean",
|
|
72
113
|
"mcp",
|
|
73
114
|
"copy",
|
|
74
|
-
"ux-writing"
|
|
115
|
+
"ux-writing",
|
|
116
|
+
"react"
|
|
75
117
|
]
|
|
76
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
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function getReact() {
|
|
9
|
+
try {
|
|
10
|
+
return require('react');
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'@ahmednawaz/crank/react requires react as a peer dependency (npm i react / pnpm add react).'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
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) {
|
|
41
|
+
const React = getReact();
|
|
42
|
+
props = props || {};
|
|
43
|
+
const src = resolveCrankLoaderSrc(props);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (typeof document === 'undefined') {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (document.querySelector('script[data-crank-loader="1"]')) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (window.__CRANK_LOADER__ && document.getElementById('vp-shell')) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const script = document.createElement('script');
|
|
57
|
+
script.src = src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now();
|
|
58
|
+
script.async = true;
|
|
59
|
+
script.dataset.crankLoader = '1';
|
|
60
|
+
document.documentElement.appendChild(script);
|
|
61
|
+
return undefined;
|
|
62
|
+
}, [src]);
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
Crank,
|
|
69
|
+
CrankLoader: Crank,
|
|
70
|
+
resolveCrankCompanionUrl,
|
|
71
|
+
resolveCrankLoaderSrc,
|
|
72
|
+
default: Crank
|
|
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,6 +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 (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`.
|
|
36
37
|
|
|
37
38
|
## When the user says "start Crank" or pastes `npx crank`
|
|
38
39
|
|
package/vite.js
CHANGED
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { env } = require('./lib/env');
|
|
6
|
-
const { readRuntimeManifest } = require('./lib/urls');
|
|
7
|
-
|
|
8
3
|
/**
|
|
9
4
|
* Vite / Rolldown-vite / Vitest-compatible plugin.
|
|
10
|
-
* Injects Crank into the app during `vite` / `npm run dev` — no
|
|
5
|
+
* Injects Crank into the app during `vite` / `npm run dev` — no React component needed.
|
|
11
6
|
*
|
|
12
7
|
* // vite.config.ts
|
|
13
|
-
* import { crank } from 'crank/vite'
|
|
8
|
+
* import { crank } from '@ahmednawaz/crank/vite'
|
|
9
|
+
* // or: import { crank } from 'crank/vite'
|
|
14
10
|
* export default { plugins: [crank()] }
|
|
11
|
+
*
|
|
12
|
+
* Prefer this for Vite apps (React, Vue, Svelte, vanilla). The panel is a script overlay,
|
|
13
|
+
* not a component in your React tree.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
let dir = path.resolve(startDir || process.cwd());
|
|
19
|
-
for (let i = 0; i < 8; i++) {
|
|
20
|
-
if (fs.existsSync(path.join(dir, '.crank', 'runtime.json'))) {
|
|
21
|
-
return dir;
|
|
22
|
-
}
|
|
23
|
-
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
24
|
-
return dir;
|
|
25
|
-
}
|
|
26
|
-
const parent = path.dirname(dir);
|
|
27
|
-
if (parent === dir) break;
|
|
28
|
-
dir = parent;
|
|
29
|
-
}
|
|
30
|
-
return path.resolve(startDir || process.cwd());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveCompanionLoaderUrl(options = {}) {
|
|
34
|
-
const projectRoot = options.projectRoot || findProjectRoot(process.cwd());
|
|
35
|
-
const runtime = readRuntimeManifest(projectRoot);
|
|
36
|
-
if (runtime?.companion?.devLoader) {
|
|
37
|
-
return runtime.companion.devLoader;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const publicUrl = (
|
|
41
|
-
options.companionUrl ||
|
|
42
|
-
env('PUBLIC_URL', '') ||
|
|
43
|
-
''
|
|
44
|
-
).replace(/\/$/, '');
|
|
45
|
-
|
|
46
|
-
const port = String(options.port || env('PORT', '3344')).replace(/[^0-9]/g, '') || '3344';
|
|
47
|
-
const host = options.host || env('HOST', '127.0.0.1');
|
|
48
|
-
const base = publicUrl || `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
49
|
-
return `${base}/dev-loader.js`;
|
|
50
|
-
}
|
|
16
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
51
17
|
|
|
52
18
|
function crank(options = {}) {
|
|
53
19
|
const src = resolveCompanionLoaderUrl(options);
|
package/webpack.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional webpack adapter — injects /dev-loader.js via html-webpack-plugin.
|
|
5
|
+
*
|
|
6
|
+
* // webpack.config.js
|
|
7
|
+
* const { crankWebpackPlugin } = require('@ahmednawaz/crank/webpack')
|
|
8
|
+
* plugins: [new HtmlWebpackPlugin(...), crankWebpackPlugin()]
|
|
9
|
+
*
|
|
10
|
+
* Prefer @ahmednawaz/crank/vite when the project uses Vite.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
14
|
+
|
|
15
|
+
function crankWebpackPlugin(options = {}) {
|
|
16
|
+
const src = resolveCompanionLoaderUrl(options);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
apply(compiler) {
|
|
20
|
+
compiler.hooks.compilation.tap('CrankWebpackPlugin', (compilation) => {
|
|
21
|
+
let hooks;
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
24
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
25
|
+
hooks = HtmlWebpackPlugin.getHooks(compilation);
|
|
26
|
+
} catch {
|
|
27
|
+
compilation.errors.push(
|
|
28
|
+
new Error(
|
|
29
|
+
'@ahmednawaz/crank/webpack requires html-webpack-plugin. Prefer the Vite plugin when possible.'
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hooks.alterAssetTagGroups.tap('CrankWebpackPlugin', (data) => {
|
|
36
|
+
const already = (data.headTags || []).some(
|
|
37
|
+
(t) =>
|
|
38
|
+
t.tagName === 'script' &&
|
|
39
|
+
String(t.attributes?.src || '').includes('dev-loader.js')
|
|
40
|
+
);
|
|
41
|
+
if (!already) {
|
|
42
|
+
data.headTags.push({
|
|
43
|
+
tagName: 'script',
|
|
44
|
+
voidTag: false,
|
|
45
|
+
attributes: { src, defer: true, 'data-crank-loader': '1' }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
crankWebpackPlugin,
|
|
57
|
+
resolveCompanionLoaderUrl
|
|
58
|
+
};
|