@ahmednawaz/crank 0.1.7 → 0.1.9
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/.env.example +2 -1
- package/AGENTS.md +20 -2
- package/README.md +30 -14
- package/bookmarklet/crank-bookmarklet.js +3 -3
- package/bookmarklet/crank-ui-helpers.js +5 -5
- package/bookmarklet/crank-ui.css +1991 -0
- package/bookmarklet/text-source.js +1 -1
- package/companion/agent-queue.js +1 -1
- package/companion/asset-bridge.js +28 -0
- package/companion/package.json +3 -0
- package/companion/persist-apply.js +1 -1
- package/companion/server.js +12 -10
- package/companion/text-dispatch.js +3 -5
- package/companion/text-writer.js +580 -0
- package/lib/detect.js +128 -28
- package/lib/init.js +317 -43
- package/lib/loader-url.js +55 -0
- package/mcp-server/tools.js +1 -1
- package/next.js +63 -0
- package/package.json +20 -1
- package/react.js +69 -0
- package/skill/SKILL.md +1 -0
- package/vite.js +7 -41
- package/webpack.js +58 -0
- package/companion/vizpatch-bridge.js +0 -49
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) {
|
|
@@ -327,7 +482,11 @@ function writeConfig(projectRoot, env) {
|
|
|
327
482
|
createdAt: new Date().toISOString(),
|
|
328
483
|
environment: {
|
|
329
484
|
isReplit: env.isReplit,
|
|
330
|
-
hasVite: env.hasVite
|
|
485
|
+
hasVite: env.hasVite,
|
|
486
|
+
viteConfig: env.viteConfig || null,
|
|
487
|
+
hasReact: env.hasReact,
|
|
488
|
+
hasNext: env.hasNext,
|
|
489
|
+
hasWebpack: env.hasWebpack
|
|
331
490
|
}
|
|
332
491
|
};
|
|
333
492
|
writeJson(configPath, config);
|
|
@@ -376,8 +535,18 @@ function initProject(projectRoot, options = {}) {
|
|
|
376
535
|
if (viteResult.patched) {
|
|
377
536
|
results.wrote.push(path.relative(projectRoot, viteResult.file));
|
|
378
537
|
}
|
|
538
|
+
if (viteResult.plugin) {
|
|
539
|
+
results.wrote.push(path.relative(projectRoot, viteResult.plugin));
|
|
540
|
+
}
|
|
541
|
+
} else if (options.vite !== false) {
|
|
542
|
+
// Still write stub so agents can wire the graceful load manually.
|
|
543
|
+
const stub = writeVitePluginStub(projectRoot);
|
|
544
|
+
results.vite = { patched: false, reason: 'no-vite-detected', plugin: stub };
|
|
545
|
+
results.wrote.push(path.relative(projectRoot, stub));
|
|
379
546
|
}
|
|
380
547
|
|
|
548
|
+
writeInjectionGuide(projectRoot, env, results);
|
|
549
|
+
|
|
381
550
|
// package.json script convenience
|
|
382
551
|
if (env.hasPackageJson && options.scripts !== false) {
|
|
383
552
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
@@ -530,6 +699,94 @@ function copyAgentsDoc(projectRoot, results) {
|
|
|
530
699
|
}
|
|
531
700
|
}
|
|
532
701
|
|
|
702
|
+
function writeInjectionGuide(projectRoot, env, results) {
|
|
703
|
+
const guide = path.join(projectRoot, '.crank', 'injection.md');
|
|
704
|
+
fs.mkdirSync(path.dirname(guide), { recursive: true });
|
|
705
|
+
const lines = [
|
|
706
|
+
'# Crank panel injection',
|
|
707
|
+
'',
|
|
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):',
|
|
714
|
+
'',
|
|
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.',
|
|
718
|
+
'',
|
|
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
|
+
'})',
|
|
741
|
+
'```',
|
|
742
|
+
'',
|
|
743
|
+
'`.crank/vite-plugin.cjs` resolves Crank from `config.json` → `packageRoot` (npx cache),',
|
|
744
|
+
'then `node_modules`, and noops if missing.',
|
|
745
|
+
'',
|
|
746
|
+
'## React adapter (non-Vite React only)',
|
|
747
|
+
'',
|
|
748
|
+
'Use only when there is **no** Vite config (CRA / custom webpack React):',
|
|
749
|
+
'',
|
|
750
|
+
'```tsx',
|
|
751
|
+
"import { CrankLoader } from '@ahmednawaz/crank/react'",
|
|
752
|
+
'// client root only — injects the same /dev-loader.js script',
|
|
753
|
+
'<CrankLoader />',
|
|
754
|
+
'```',
|
|
755
|
+
'',
|
|
756
|
+
'Do **not** invent a fake in-tree `<Crank />` panel UI. `CrankLoader` / `Crank` only inject a script.',
|
|
757
|
+
'',
|
|
758
|
+
'## Next.js',
|
|
759
|
+
'',
|
|
760
|
+
'```tsx',
|
|
761
|
+
"import { CrankScript } from '@ahmednawaz/crank/next'",
|
|
762
|
+
'// app/layout.tsx or pages/_app',
|
|
763
|
+
'<CrankScript />',
|
|
764
|
+
'```',
|
|
765
|
+
'',
|
|
766
|
+
'## Webpack',
|
|
767
|
+
'',
|
|
768
|
+
'```js',
|
|
769
|
+
"const { crankWebpackPlugin } = require('@ahmednawaz/crank/webpack')",
|
|
770
|
+
'plugins: [new HtmlWebpackPlugin(...), crankWebpackPlugin()]',
|
|
771
|
+
'```',
|
|
772
|
+
'',
|
|
773
|
+
'## Bookmarklet fallback',
|
|
774
|
+
'',
|
|
775
|
+
'Open the companion URL → drag the bookmarklet onto any preview page.',
|
|
776
|
+
'',
|
|
777
|
+
'## Detected in this project',
|
|
778
|
+
'',
|
|
779
|
+
`- hasVite: ${Boolean(env.hasVite)}`,
|
|
780
|
+
`- viteConfig: ${env.viteConfig || '(none)'}`,
|
|
781
|
+
`- hasReact: ${Boolean(env.hasReact)}`,
|
|
782
|
+
`- hasNext: ${Boolean(env.hasNext)}`,
|
|
783
|
+
`- hasWebpack: ${Boolean(env.hasWebpack)}`,
|
|
784
|
+
''
|
|
785
|
+
];
|
|
786
|
+
fs.writeFileSync(guide, lines.join('\n'));
|
|
787
|
+
results.wrote.push('.crank/injection.md');
|
|
788
|
+
}
|
|
789
|
+
|
|
533
790
|
function printSetupNextSteps(env, runtime, results) {
|
|
534
791
|
console.log('');
|
|
535
792
|
console.log(' Setup complete (Retune-style):');
|
|
@@ -539,6 +796,16 @@ function printSetupNextSteps(env, runtime, results) {
|
|
|
539
796
|
if (results?.wrote?.some((w) => String(w).includes('skills'))) {
|
|
540
797
|
console.log(' • Skill crank-apply-changes installed (auto-syncs on start)');
|
|
541
798
|
}
|
|
799
|
+
if (results?.vite?.patched) {
|
|
800
|
+
console.log(' • Vite plugin patched (graceful load via .crank/vite-plugin.cjs)');
|
|
801
|
+
} else if (env.hasVite) {
|
|
802
|
+
console.log(' • Vite detected — see .crank/injection.md if the panel does not auto-load');
|
|
803
|
+
} 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)');
|
|
807
|
+
}
|
|
808
|
+
console.log(' • Injection guide: .crank/injection.md');
|
|
542
809
|
if (env.isReplit) {
|
|
543
810
|
console.log(' • Workflow command: npx @ahmednawaz/crank start');
|
|
544
811
|
console.log(' • Add CRANK_TEAM_TOKEN in Secrets (not in the command)');
|
|
@@ -591,8 +858,10 @@ function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
|
|
|
591
858
|
'## Panel in preview',
|
|
592
859
|
'',
|
|
593
860
|
`- Companion: ${companionUrl}`,
|
|
594
|
-
'- Vite
|
|
595
|
-
'-
|
|
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`',
|
|
596
865
|
''
|
|
597
866
|
].join('\n')
|
|
598
867
|
);
|
|
@@ -608,5 +877,10 @@ module.exports = {
|
|
|
608
877
|
configureMcpRetuneStyle,
|
|
609
878
|
printSetupNextSteps,
|
|
610
879
|
writeReplitGuide,
|
|
611
|
-
|
|
880
|
+
writeReplitPlatformGuide,
|
|
881
|
+
writeVitePluginStub,
|
|
882
|
+
patchViteConfig,
|
|
883
|
+
writeInjectionGuide,
|
|
884
|
+
patchReplitConfig,
|
|
885
|
+
copyAgentsDoc
|
|
612
886
|
};
|
|
@@ -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/mcp-server/tools.js
CHANGED
|
@@ -105,7 +105,7 @@ export const toolDefinitions = [
|
|
|
105
105
|
{
|
|
106
106
|
name: 'apply_copy_variant',
|
|
107
107
|
description:
|
|
108
|
-
'Apply a chosen Glean copy variant: update live DOM in connected browsers AND persist editable hardcoded strings to source via
|
|
108
|
+
'Apply a chosen Glean copy variant: update live DOM in connected browsers AND persist editable hardcoded strings to source via Crank text-writer. Set persist=false for DOM-only preview.',
|
|
109
109
|
inputSchema: {
|
|
110
110
|
type: 'object',
|
|
111
111
|
properties: {
|