@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/next.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional Next.js adapter — injects Crank via next/script (App Router or Pages).
|
|
5
|
+
*
|
|
6
|
+
* Prefer Vite plugin for Vite apps. Use this for Next.js when there is no vite.config.
|
|
7
|
+
*
|
|
8
|
+
* // app/layout.tsx (or a client layout section)
|
|
9
|
+
* import { CrankScript } from '@ahmednawaz/crank/next'
|
|
10
|
+
* <CrankScript />
|
|
11
|
+
*
|
|
12
|
+
* peerDependencies: react, next
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
16
|
+
const { CrankLoader } = require('./react');
|
|
17
|
+
|
|
18
|
+
function getReact() {
|
|
19
|
+
try {
|
|
20
|
+
return require('react');
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'@ahmednawaz/crank/next requires react. Prefer @ahmednawaz/crank/vite for Vite apps.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNextScript() {
|
|
29
|
+
try {
|
|
30
|
+
return require('next/script');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ src?: string, companionUrl?: string, strategy?: string }} props
|
|
38
|
+
*/
|
|
39
|
+
function CrankScript(props) {
|
|
40
|
+
const React = getReact();
|
|
41
|
+
const NextScript = getNextScript();
|
|
42
|
+
const src = resolveCompanionLoaderUrl(props || {});
|
|
43
|
+
const strategy = (props && props.strategy) || 'afterInteractive';
|
|
44
|
+
|
|
45
|
+
if (NextScript) {
|
|
46
|
+
const Script = NextScript.default || NextScript;
|
|
47
|
+
return React.createElement(Script, {
|
|
48
|
+
src,
|
|
49
|
+
strategy,
|
|
50
|
+
'data-crank-loader': '1'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback when next/script is unavailable (tests / non-Next bundlers).
|
|
55
|
+
return React.createElement(CrankLoader, props || {});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
CrankScript,
|
|
60
|
+
CrankLoader,
|
|
61
|
+
Crank: CrankScript,
|
|
62
|
+
resolveCompanionLoaderUrl
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmednawaz/crank",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -14,8 +14,21 @@
|
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./vite.js",
|
|
16
16
|
"./vite": "./vite.js",
|
|
17
|
+
"./react": "./react.js",
|
|
18
|
+
"./next": "./next.js",
|
|
19
|
+
"./webpack": "./webpack.js",
|
|
17
20
|
"./package.json": "./package.json"
|
|
18
21
|
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"html-webpack-plugin": ">=5",
|
|
24
|
+
"next": ">=13",
|
|
25
|
+
"react": ">=17"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"react": { "optional": true },
|
|
29
|
+
"next": { "optional": true },
|
|
30
|
+
"html-webpack-plugin": { "optional": true }
|
|
31
|
+
},
|
|
19
32
|
"workspaces": [
|
|
20
33
|
"companion",
|
|
21
34
|
"mcp-server"
|
|
@@ -37,8 +50,11 @@
|
|
|
37
50
|
"node": ">=18"
|
|
38
51
|
},
|
|
39
52
|
"dependencies": {
|
|
53
|
+
"@babel/parser": "^7.28.0",
|
|
54
|
+
"@babel/traverse": "^7.28.0",
|
|
40
55
|
"@cursor/sdk": "^1.0.23",
|
|
41
56
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
57
|
+
"cheerio": "^1.0.0",
|
|
42
58
|
"cors": "^2.8.5",
|
|
43
59
|
"express": "^4.21.2",
|
|
44
60
|
"ws": "^8.18.0",
|
|
@@ -54,6 +70,9 @@
|
|
|
54
70
|
"skill",
|
|
55
71
|
"panel",
|
|
56
72
|
"vite.js",
|
|
73
|
+
"react.js",
|
|
74
|
+
"next.js",
|
|
75
|
+
"webpack.js",
|
|
57
76
|
".env.example",
|
|
58
77
|
"AGENTS.md",
|
|
59
78
|
"REPLIT.md",
|
package/react.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional React adapter for non-Vite React apps (CRA, custom webpack, etc.).
|
|
5
|
+
*
|
|
6
|
+
* This is NOT a Retune-style UI component. It only injects the same /dev-loader.js
|
|
7
|
+
* script the Vite plugin uses. Prefer `@ahmednawaz/crank/vite` when the app uses Vite.
|
|
8
|
+
*
|
|
9
|
+
* import { CrankLoader } from '@ahmednawaz/crank/react'
|
|
10
|
+
* // in App / root layout (client-only):
|
|
11
|
+
* <CrankLoader />
|
|
12
|
+
*
|
|
13
|
+
* peerDependency: react >= 17
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
17
|
+
|
|
18
|
+
function getReact() {
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
21
|
+
return require('react');
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'@ahmednawaz/crank/react requires react as a peer dependency. Prefer the Vite plugin when possible: import { crank } from "@ahmednawaz/crank/vite".'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Injects Crank's companion loader once on the client. Renders null.
|
|
31
|
+
* @param {{ src?: string, companionUrl?: string, port?: string|number, host?: string }} props
|
|
32
|
+
*/
|
|
33
|
+
function CrankLoader(props) {
|
|
34
|
+
const React = getReact();
|
|
35
|
+
const src = resolveCompanionLoaderUrl(props || {});
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (typeof document === 'undefined') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (window.__CRANK_LOADER__) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
window.__CRANK_LOADER__ = true;
|
|
45
|
+
|
|
46
|
+
const existing = document.querySelector('script[data-crank-loader="1"]');
|
|
47
|
+
if (existing) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const script = document.createElement('script');
|
|
52
|
+
script.src = src;
|
|
53
|
+
script.defer = true;
|
|
54
|
+
script.dataset.crankLoader = '1';
|
|
55
|
+
document.documentElement.appendChild(script);
|
|
56
|
+
return undefined;
|
|
57
|
+
}, [src]);
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Alias — still only a script injector, not an in-tree panel UI. */
|
|
63
|
+
const Crank = CrankLoader;
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
CrankLoader,
|
|
67
|
+
Crank,
|
|
68
|
+
resolveCompanionLoaderUrl
|
|
69
|
+
};
|
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 injection:** Vite apps use the Vite plugin (graceful `.crank/vite-plugin.cjs`) — not a React component. See **AGENTS.md** / `.crank/injection.md`. Keep companion running in a Workflow.
|
|
36
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
|
+
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolve sibling Vizpatch install so Crank can reuse text-writer, UI CSS, etc.
|
|
5
|
-
*/
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
function resolveVizpatchRoot() {
|
|
10
|
-
if (process.env.VIZPATCH_ROOT) {
|
|
11
|
-
return path.resolve(process.env.VIZPATCH_ROOT);
|
|
12
|
-
}
|
|
13
|
-
const candidates = [
|
|
14
|
-
path.resolve(__dirname, '..', '..', 'vizpatch'),
|
|
15
|
-
path.resolve(__dirname, '..', '..', 'Downloads', 'vizpatch'),
|
|
16
|
-
path.resolve(process.cwd(), '..', 'vizpatch'),
|
|
17
|
-
'/Users/ahmed.nawaz/Downloads/vizpatch'
|
|
18
|
-
];
|
|
19
|
-
for (const candidate of candidates) {
|
|
20
|
-
if (fs.existsSync(path.join(candidate, 'companion', 'text-writer.js'))) {
|
|
21
|
-
return candidate;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function requireVizpatch(relPath) {
|
|
28
|
-
const root = resolveVizpatchRoot();
|
|
29
|
-
if (!root) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
'Vizpatch not found. Set VIZPATCH_ROOT to the vizpatch checkout path.'
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
return require(path.join(root, relPath));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function vizpatchAsset(...parts) {
|
|
38
|
-
const root = resolveVizpatchRoot();
|
|
39
|
-
if (!root) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return path.join(root, ...parts);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = {
|
|
46
|
-
resolveVizpatchRoot,
|
|
47
|
-
requireVizpatch,
|
|
48
|
-
vizpatchAsset
|
|
49
|
-
};
|