@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/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 hasCursor = exists(path.join(projectRoot, '.cursor')) ||
40
- exists(path.join(os.homedir(), '.cursor'));
41
- const hasVsCode = exists(path.join(projectRoot, '.vscode')) ||
42
- exists(path.join(projectRoot, '.vscode'.replace('s', 's')));
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: hasPackageJson
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 (/(\bcrank)\s*\(/.test(source) && /plugins/.test(source)) {
104
- return { patched: false, reason: 'already-present', file: configPath };
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
- // Copy plugin into the project so Vite can resolve a relative import everywhere.
108
- const localPluginDir = path.join(projectRoot, '.crank');
109
- fs.mkdirSync(localPluginDir, { recursive: true });
110
- const localPlugin = path.join(localPluginDir, 'vite-plugin.cjs');
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
- (/^import\s/m.test(source) && !configPath.endsWith('.cjs'));
117
- const importLine = isEsm
118
- ? `import { crank } from '${rel}';\n`
119
- : `const { crank } = require('${rel}');\n`;
120
-
121
- if (!/vite-plugin\.cjs|from ['"]crank/.test(source)) {
122
- source = importLine + source;
123
- }
124
-
125
- if (/plugins\s*:\s*\[\s*\]/.test(source)) {
126
- source = source.replace(/plugins\s*:\s*\[\s*\]/, 'plugins: [crank()]');
127
- } else if (/plugins\s*:\s*\[/.test(source)) {
128
- source = source.replace(/plugins\s*:\s*\[/, 'plugins: [crank(), ');
129
- } else if (/defineConfig\s*\(\s*\{/.test(source)) {
130
- source = source.replace(
131
- /defineConfig\s*\(\s*\{/,
132
- 'defineConfig({\n plugins: [crank()],'
133
- );
134
- } else if (/export\s+default\s+\{/.test(source)) {
135
- source = source.replace(
136
- /export\s+default\s+\{/,
137
- 'export default {\n plugins: [crank()],'
138
- );
139
- } else if (/module\.exports\s*=\s*\{/.test(source)) {
140
- source = source.replace(
141
- /module\.exports\s*=\s*\{/,
142
- 'module.exports = {\n plugins: [crank()],'
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
- return { patched: false, reason: 'unrecognized-vite-shape', file: configPath };
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: localPlugin };
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 apps: panel auto-loads in the webview.',
595
- '- Other stacks: open the companion URL and use the bookmarklet on your preview tab.',
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
- patchReplitConfig
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
+ };
@@ -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 Vizpatch text-writer. Set persist=false for DOM-only preview.',
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: {