@ahmednawaz/crank 0.1.2
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 +53 -0
- package/README.md +266 -0
- package/bin/crank.js +737 -0
- package/bookmarklet/crank-bookmarklet.js +2295 -0
- package/bookmarklet/crank-ui-helpers.js +355 -0
- package/bookmarklet/install-snippet.txt +5 -0
- package/bookmarklet/text-source.js +239 -0
- package/companion/agent-queue.js +485 -0
- package/companion/agent-runner.js +334 -0
- package/companion/bin/crank.js +69 -0
- package/companion/dev-loader.js +39 -0
- package/companion/glean-client.js +991 -0
- package/companion/package.json +18 -0
- package/companion/persist-apply.js +189 -0
- package/companion/selection-store.js +175 -0
- package/companion/server.js +1147 -0
- package/companion/text-dispatch.js +419 -0
- package/companion/vizpatch-bridge.js +49 -0
- package/lib/copy-labels.js +86 -0
- package/lib/detect.js +88 -0
- package/lib/env.js +23 -0
- package/lib/init.js +435 -0
- package/lib/load-team-env.js +43 -0
- package/lib/resolve-project.js +203 -0
- package/lib/team-auth.js +82 -0
- package/lib/urls.js +164 -0
- package/mcp-server/index.js +174 -0
- package/mcp-server/package.json +16 -0
- package/mcp-server/tools.js +480 -0
- package/package.json +57 -0
- package/panel/demo.html +62 -0
- package/skill/SKILL.md +60 -0
- package/skills/README.md +16 -0
- package/skills/copy-guidance.md +25 -0
- package/team.env +4 -0
- package/vite.js +75 -0
package/lib/init.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
exists,
|
|
7
|
+
readJson,
|
|
8
|
+
writeJson,
|
|
9
|
+
detectEnvironment,
|
|
10
|
+
findViteConfig
|
|
11
|
+
} = require('./detect');
|
|
12
|
+
|
|
13
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
const { resolveUrls } = require('./urls');
|
|
15
|
+
|
|
16
|
+
function ensureGitignore(projectRoot) {
|
|
17
|
+
const file = path.join(projectRoot, '.gitignore');
|
|
18
|
+
const lines = [
|
|
19
|
+
'.env',
|
|
20
|
+
'.env.local',
|
|
21
|
+
'.crank/secrets.json',
|
|
22
|
+
'.crank/runtime.json',
|
|
23
|
+
'node_modules/'
|
|
24
|
+
];
|
|
25
|
+
let current = exists(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
26
|
+
let changed = false;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (!current.split(/\r?\n/).includes(line)) {
|
|
29
|
+
current = `${current.trimEnd()}\n${line}\n`;
|
|
30
|
+
changed = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (changed || !exists(file)) {
|
|
34
|
+
fs.writeFileSync(file, current.endsWith('\n') ? current : `${current}\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mcpServerEntry(projectRoot) {
|
|
39
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
40
|
+
const companionUrl = `http://127.0.0.1:${process.env.CRANK_PORT || '3344'}`;
|
|
41
|
+
const envBlock = {
|
|
42
|
+
CRANK_COMPANION_URL: companionUrl,
|
|
43
|
+
CRANK_MCP_MODE: 'stdio',
|
|
44
|
+
CRANK_PROJECT_ROOT: root
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const localBin = path.join(root, 'node_modules', 'crank', 'bin', 'crank.js');
|
|
48
|
+
if (exists(localBin)) {
|
|
49
|
+
return {
|
|
50
|
+
command: 'node',
|
|
51
|
+
args: [localBin, 'mcp'],
|
|
52
|
+
env: envBlock
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Retune-style — npx resolves @ahmednawaz/crank from npm (no git clone).
|
|
57
|
+
return {
|
|
58
|
+
command: 'npx',
|
|
59
|
+
args: ['-y', '@ahmednawaz/crank', 'mcp'],
|
|
60
|
+
env: envBlock
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mergeMcpServers(file, serverKey = 'crank', projectRoot) {
|
|
65
|
+
const existing = readJson(file, { mcpServers: {} }) || { mcpServers: {} };
|
|
66
|
+
if (!existing.mcpServers) {
|
|
67
|
+
existing.mcpServers = {};
|
|
68
|
+
}
|
|
69
|
+
existing.mcpServers[serverKey] = mcpServerEntry(projectRoot);
|
|
70
|
+
writeJson(file, existing);
|
|
71
|
+
return file;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** VS Code / Copilot style uses `servers` in some versions; write both shapes safely. */
|
|
75
|
+
function mergeVsCodeMcp(file, projectRoot) {
|
|
76
|
+
const existing = readJson(file, {}) || {};
|
|
77
|
+
if (!existing.servers && !existing.mcpServers) {
|
|
78
|
+
existing.servers = {};
|
|
79
|
+
}
|
|
80
|
+
const entryBase = mcpServerEntry(projectRoot);
|
|
81
|
+
const entry = {
|
|
82
|
+
type: 'stdio',
|
|
83
|
+
command: entryBase.command,
|
|
84
|
+
args: entryBase.args,
|
|
85
|
+
env: entryBase.env
|
|
86
|
+
};
|
|
87
|
+
if (existing.servers) {
|
|
88
|
+
existing.servers.crank = entry;
|
|
89
|
+
} else {
|
|
90
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
91
|
+
existing.mcpServers.crank = entryBase;
|
|
92
|
+
}
|
|
93
|
+
writeJson(file, existing);
|
|
94
|
+
return file;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function patchViteConfig(projectRoot) {
|
|
98
|
+
const configPath = findViteConfig(projectRoot);
|
|
99
|
+
if (!configPath) {
|
|
100
|
+
return { patched: false, reason: 'no-vite-config' };
|
|
101
|
+
}
|
|
102
|
+
let source = fs.readFileSync(configPath, 'utf8');
|
|
103
|
+
if (/(\bcrank)\s*\(/.test(source) && /plugins/.test(source)) {
|
|
104
|
+
return { patched: false, reason: 'already-present', file: configPath };
|
|
105
|
+
}
|
|
106
|
+
|
|
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);
|
|
112
|
+
|
|
113
|
+
const rel = './.crank/vite-plugin.cjs';
|
|
114
|
+
const isEsm =
|
|
115
|
+
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
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
return { patched: false, reason: 'unrecognized-vite-shape', file: configPath };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(configPath, source);
|
|
149
|
+
return { patched: true, file: configPath, plugin: localPlugin };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeProjectEnvExample(projectRoot) {
|
|
153
|
+
const dest = path.join(projectRoot, '.env.crank.example');
|
|
154
|
+
const src = path.join(PACKAGE_ROOT, '.env.example');
|
|
155
|
+
if (exists(src) && !exists(dest)) {
|
|
156
|
+
fs.copyFileSync(src, dest);
|
|
157
|
+
}
|
|
158
|
+
// If package root has a real .env (dev machine), do NOT copy secrets into target repos.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Install crank-apply-changes skill like `npx retune setup`:
|
|
163
|
+
* user-level ~/.cursor/skills + project .cursor/skills when present.
|
|
164
|
+
*/
|
|
165
|
+
function installCrankSkill(projectRoot, results) {
|
|
166
|
+
const skillSrc = path.join(PACKAGE_ROOT, 'skill', 'SKILL.md');
|
|
167
|
+
if (!exists(skillSrc)) {
|
|
168
|
+
results.skillError = 'skill/SKILL.md missing from crank package';
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const targets = [];
|
|
172
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
173
|
+
if (home) {
|
|
174
|
+
targets.push(
|
|
175
|
+
path.join(home, '.cursor', 'skills', 'crank-apply-changes', 'SKILL.md')
|
|
176
|
+
);
|
|
177
|
+
targets.push(
|
|
178
|
+
path.join(home, '.claude', 'skills', 'crank-apply-changes', 'SKILL.md')
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (projectRoot) {
|
|
182
|
+
targets.push(
|
|
183
|
+
path.join(
|
|
184
|
+
projectRoot,
|
|
185
|
+
'.cursor',
|
|
186
|
+
'skills',
|
|
187
|
+
'crank-apply-changes',
|
|
188
|
+
'SKILL.md'
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
for (const dest of targets) {
|
|
193
|
+
try {
|
|
194
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
195
|
+
fs.copyFileSync(skillSrc, dest);
|
|
196
|
+
results.wrote.push(dest);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
results.skillError = (results.skillError ? results.skillError + '; ' : '') +
|
|
199
|
+
`${dest}: ${err.message}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Also merge crank into user ~/.cursor/mcp.json (Retune setup parity).
|
|
206
|
+
*/
|
|
207
|
+
function mergeUserCursorMcp(projectRoot, results) {
|
|
208
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
209
|
+
if (!home) return;
|
|
210
|
+
const userMcp = path.join(home, '.cursor', 'mcp.json');
|
|
211
|
+
try {
|
|
212
|
+
if (!exists(path.dirname(userMcp))) {
|
|
213
|
+
fs.mkdirSync(path.dirname(userMcp), { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
mergeMcpServers(userMcp, 'crank', projectRoot);
|
|
216
|
+
results.wrote.push(userMcp);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
results.userMcpError = err.message;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Retune-style: add crank to devDependencies so `npx crank` works with no path.
|
|
224
|
+
*/
|
|
225
|
+
function ensureCrankDependency(projectRoot, results) {
|
|
226
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
227
|
+
if (!exists(pkgPath)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (path.resolve(projectRoot) === path.resolve(PACKAGE_ROOT)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const pkg = readJson(pkgPath, {});
|
|
235
|
+
const crankPkg = readJson(path.join(PACKAGE_ROOT, 'package.json'), {});
|
|
236
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
237
|
+
|
|
238
|
+
let spec;
|
|
239
|
+
if (process.env.CRANK_NPM_SPEC) {
|
|
240
|
+
spec = process.env.CRANK_NPM_SPEC;
|
|
241
|
+
} else {
|
|
242
|
+
spec = `@ahmednawaz/crank@^${crankPkg.version || '0.1.0'}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (pkg.devDependencies.crank === spec) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
pkg.devDependencies.crank = spec;
|
|
249
|
+
writeJson(pkgPath, pkg);
|
|
250
|
+
results.wrote.push('package.json#devDependencies.crank');
|
|
251
|
+
results.crankDependencyAdded = spec;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function writeConfig(projectRoot, env) {
|
|
255
|
+
const dir = path.join(projectRoot, '.crank');
|
|
256
|
+
const configPath = path.join(dir, 'config.json');
|
|
257
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
|
+
if (exists(configPath)) {
|
|
259
|
+
const existing = readJson(configPath, {}) || {};
|
|
260
|
+
existing.projectRoot = projectRoot;
|
|
261
|
+
existing.packageRoot = PACKAGE_ROOT;
|
|
262
|
+
writeJson(configPath, existing);
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
const config = {
|
|
266
|
+
version: 1,
|
|
267
|
+
packageRoot: PACKAGE_ROOT,
|
|
268
|
+
projectRoot,
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
environment: {
|
|
271
|
+
isReplit: env.isReplit,
|
|
272
|
+
hasVite: env.hasVite
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
writeJson(configPath, config);
|
|
276
|
+
return config;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* One-shot init into the target repo. Idempotent.
|
|
281
|
+
*/
|
|
282
|
+
function initProject(projectRoot, options = {}) {
|
|
283
|
+
const env = detectEnvironment(projectRoot);
|
|
284
|
+
const results = {
|
|
285
|
+
projectRoot,
|
|
286
|
+
environment: env,
|
|
287
|
+
wrote: []
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
ensureGitignore(projectRoot);
|
|
291
|
+
results.wrote.push('.gitignore');
|
|
292
|
+
|
|
293
|
+
writeConfig(projectRoot, env);
|
|
294
|
+
results.wrote.push('.crank/config.json');
|
|
295
|
+
|
|
296
|
+
writeProjectEnvExample(projectRoot);
|
|
297
|
+
if (exists(path.join(projectRoot, '.env.crank.example'))) {
|
|
298
|
+
results.wrote.push('.env.crank.example');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Cursor project MCP
|
|
302
|
+
if (options.mcp !== false) {
|
|
303
|
+
ensureCrankDependency(projectRoot, results);
|
|
304
|
+
|
|
305
|
+
const cursorMcp = path.join(projectRoot, '.cursor', 'mcp.json');
|
|
306
|
+
mergeMcpServers(cursorMcp, 'crank', projectRoot);
|
|
307
|
+
results.wrote.push('.cursor/mcp.json');
|
|
308
|
+
|
|
309
|
+
// VS Code / Copilot MCP
|
|
310
|
+
const vsMcp = path.join(projectRoot, '.vscode', 'mcp.json');
|
|
311
|
+
mergeVsCodeMcp(vsMcp, projectRoot);
|
|
312
|
+
results.wrote.push('.vscode/mcp.json');
|
|
313
|
+
|
|
314
|
+
if (options.userMcp !== false) {
|
|
315
|
+
mergeUserCursorMcp(projectRoot, results);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (env.hasClaudeDesktop && options.claude !== false) {
|
|
319
|
+
try {
|
|
320
|
+
mergeMcpServers(env.claudeConfig, 'crank', projectRoot);
|
|
321
|
+
results.wrote.push(env.claudeConfig);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
results.claudeError = err.message;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.skill !== false) {
|
|
329
|
+
installCrankSkill(projectRoot, results);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (env.hasVite && options.vite !== false) {
|
|
333
|
+
const viteResult = patchViteConfig(projectRoot);
|
|
334
|
+
results.vite = viteResult;
|
|
335
|
+
if (viteResult.patched) {
|
|
336
|
+
results.wrote.push(path.relative(projectRoot, viteResult.file));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// package.json script convenience
|
|
341
|
+
if (env.hasPackageJson && options.scripts !== false) {
|
|
342
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
343
|
+
const pkg = readJson(pkgPath, {});
|
|
344
|
+
pkg.scripts = pkg.scripts || {};
|
|
345
|
+
if (!pkg.scripts.crank) {
|
|
346
|
+
pkg.scripts.crank = `node ${JSON.stringify(
|
|
347
|
+
path.join(PACKAGE_ROOT, 'bin', 'crank.js')
|
|
348
|
+
)}`;
|
|
349
|
+
writeJson(pkgPath, pkg);
|
|
350
|
+
results.wrote.push('package.json#scripts.crank');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (env.isReplit) {
|
|
355
|
+
patchReplitConfig(projectRoot, results);
|
|
356
|
+
writeReplitGuide(projectRoot, env, results);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (results.crankDependencyAdded && options.install !== false) {
|
|
360
|
+
try {
|
|
361
|
+
const { execSync } = require('child_process');
|
|
362
|
+
execSync('npm install', { cwd: projectRoot, stdio: 'inherit' });
|
|
363
|
+
results.installed = true;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
results.installError = err.message;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function patchReplitConfig(projectRoot, results) {
|
|
373
|
+
const replitFile = path.join(projectRoot, '.replit');
|
|
374
|
+
const companionPort = Number(process.env.CRANK_PORT || 3344);
|
|
375
|
+
const mcpPort = Number(process.env.CRANK_MCP_HTTP_PORT || 3345);
|
|
376
|
+
let source = exists(replitFile) ? fs.readFileSync(replitFile, 'utf8') : '';
|
|
377
|
+
|
|
378
|
+
if (!/localPort\s*=\s*3344/.test(source)) {
|
|
379
|
+
source += `\n[[ports]]\nlocalPort = ${companionPort}\nexternalPort = 80\n`;
|
|
380
|
+
}
|
|
381
|
+
if (!/localPort\s*=\s*3345/.test(source)) {
|
|
382
|
+
source += `\n[[ports]]\nlocalPort = ${mcpPort}\nexternalPort = 80\n`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!/^run\s*=/m.test(source)) {
|
|
386
|
+
source = `run = "npx crank"\n\n${source}`;
|
|
387
|
+
results.replitRun = 'npx crank';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fs.writeFileSync(replitFile, source.trimEnd() + '\n');
|
|
391
|
+
results.wrote.push('.replit');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
|
|
395
|
+
const urls = runtime ? null : resolveUrls(detectEnv);
|
|
396
|
+
const mcpEndpoint =
|
|
397
|
+
runtime?.mcp?.endpoint || urls?.mcpEndpoint || 'https://3345-<repl-host>/mcp';
|
|
398
|
+
const companionUrl =
|
|
399
|
+
runtime?.companion?.publicUrl || urls?.companionPublic || 'https://3344-<repl-host>';
|
|
400
|
+
const hint = path.join(projectRoot, '.crank', 'replit.md');
|
|
401
|
+
const apiKeyLine = process.env.CRANK_MCP_API_KEY
|
|
402
|
+
? '3. Header: `X-API-Key` = your `CRANK_MCP_API_KEY` secret.'
|
|
403
|
+
: '3. Optional: set `CRANK_MCP_API_KEY` in Secrets and pass `X-API-Key` in MCP headers.';
|
|
404
|
+
fs.writeFileSync(
|
|
405
|
+
hint,
|
|
406
|
+
[
|
|
407
|
+
'# Crank on Replit',
|
|
408
|
+
'',
|
|
409
|
+
'One command: `npx crank setup` then `npx crank` — no paths required.',
|
|
410
|
+
'',
|
|
411
|
+
'## Replit Agent MCP',
|
|
412
|
+
'',
|
|
413
|
+
'Integrations → MCP Servers → Add custom server:',
|
|
414
|
+
`- URL: \`${mcpEndpoint}\``,
|
|
415
|
+
apiKeyLine,
|
|
416
|
+
'',
|
|
417
|
+
'## Panel in preview',
|
|
418
|
+
'',
|
|
419
|
+
`- Companion: ${companionUrl}`,
|
|
420
|
+
'- Vite apps: panel auto-loads in the webview.',
|
|
421
|
+
'- Other stacks: open the companion URL and use the bookmarklet on your preview tab.',
|
|
422
|
+
''
|
|
423
|
+
].join('\n')
|
|
424
|
+
);
|
|
425
|
+
results.wrote.push('.crank/replit.md');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
initProject,
|
|
430
|
+
PACKAGE_ROOT,
|
|
431
|
+
mcpServerEntry,
|
|
432
|
+
installCrankSkill,
|
|
433
|
+
writeReplitGuide,
|
|
434
|
+
patchReplitConfig
|
|
435
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Load team-wide defaults shipped in the npm tarball (team.env) or maintainer .env.
|
|
8
|
+
* Never commit real keys to git — `npm run publish:team` bundles team.env at publish time.
|
|
9
|
+
*/
|
|
10
|
+
function loadTeamEnvFile(packageRoot) {
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(packageRoot, 'team.env'),
|
|
13
|
+
path.join(packageRoot, '.env')
|
|
14
|
+
];
|
|
15
|
+
for (const file of candidates) {
|
|
16
|
+
if (!fs.existsSync(file)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const eq = trimmed.indexOf('=');
|
|
25
|
+
if (eq < 1) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const key = trimmed.slice(0, eq).trim();
|
|
29
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
30
|
+
if (
|
|
31
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
32
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
33
|
+
) {
|
|
34
|
+
value = value.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
if (process.env[key] == null) {
|
|
37
|
+
process.env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { loadTeamEnvFile };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { exists, readJson } = require('./detect');
|
|
7
|
+
|
|
8
|
+
const APP_DIR_CANDIDATES = [
|
|
9
|
+
'.',
|
|
10
|
+
'app',
|
|
11
|
+
'client',
|
|
12
|
+
'web',
|
|
13
|
+
'frontend',
|
|
14
|
+
'packages/app',
|
|
15
|
+
'packages/web',
|
|
16
|
+
'apps/web',
|
|
17
|
+
'apps/frontend'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const FRAMEWORK_FILES = [
|
|
21
|
+
'vite.config.ts',
|
|
22
|
+
'vite.config.js',
|
|
23
|
+
'vite.config.mjs',
|
|
24
|
+
'vite.config.cjs',
|
|
25
|
+
'next.config.js',
|
|
26
|
+
'next.config.mjs',
|
|
27
|
+
'next.config.ts',
|
|
28
|
+
'nuxt.config.ts',
|
|
29
|
+
'nuxt.config.js',
|
|
30
|
+
'angular.json',
|
|
31
|
+
'svelte.config.js',
|
|
32
|
+
'astro.config.mjs',
|
|
33
|
+
'astro.config.ts'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function hasFrameworkMarker(dir) {
|
|
37
|
+
return FRAMEWORK_FILES.some((name) => exists(path.join(dir, name)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readCrankConfig(startDir) {
|
|
41
|
+
let dir = path.resolve(startDir);
|
|
42
|
+
for (let i = 0; i < 12; i++) {
|
|
43
|
+
const configPath = path.join(dir, '.crank', 'config.json');
|
|
44
|
+
if (exists(configPath)) {
|
|
45
|
+
const config = readJson(configPath, null);
|
|
46
|
+
if (config?.projectRoot && exists(config.projectRoot)) {
|
|
47
|
+
return { config, root: path.resolve(config.projectRoot), from: configPath };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir) break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function gitRoot(startDir) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execSync('git rev-parse --show-toplevel', {
|
|
60
|
+
cwd: startDir,
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
63
|
+
}).trim();
|
|
64
|
+
if (out && exists(out)) {
|
|
65
|
+
return path.resolve(out);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// not a git repo
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function gitRemote(root) {
|
|
74
|
+
try {
|
|
75
|
+
return execSync('git remote get-url origin', {
|
|
76
|
+
cwd: root,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
79
|
+
}).trim();
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findPackageRoot(startDir) {
|
|
86
|
+
let dir = path.resolve(startDir);
|
|
87
|
+
for (let i = 0; i < 12; i++) {
|
|
88
|
+
if (exists(path.join(dir, 'package.json'))) {
|
|
89
|
+
return dir;
|
|
90
|
+
}
|
|
91
|
+
const parent = path.dirname(dir);
|
|
92
|
+
if (parent === dir) break;
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
return path.resolve(startDir);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retune-style: find the app directory inside a repo (app/, client/, web/, …).
|
|
100
|
+
*/
|
|
101
|
+
function findAppDirectory(repoRoot) {
|
|
102
|
+
const root = path.resolve(repoRoot);
|
|
103
|
+
if (hasFrameworkMarker(root)) {
|
|
104
|
+
return { appRoot: root, reason: 'framework at repo root' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const rel of APP_DIR_CANDIDATES) {
|
|
108
|
+
if (rel === '.') continue;
|
|
109
|
+
const candidate = path.join(root, rel);
|
|
110
|
+
if (exists(candidate) && hasFrameworkMarker(candidate)) {
|
|
111
|
+
return { appRoot: candidate, reason: `framework in ${rel}/` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const rel of APP_DIR_CANDIDATES) {
|
|
116
|
+
const candidate = path.join(root, rel === '.' ? '' : rel);
|
|
117
|
+
if (exists(candidate) && exists(path.join(candidate, 'package.json'))) {
|
|
118
|
+
return { appRoot: candidate, reason: `package.json in ${rel === '.' ? 'root' : rel + '/'}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { appRoot: root, reason: 'repo root (fallback)' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve where the user's app code lives — no manual path required.
|
|
127
|
+
* Priority: CRANK_PROJECT_ROOT → .crank/config → git root + app dir → package.json walk → cwd.
|
|
128
|
+
*/
|
|
129
|
+
function resolveProjectRoot(startDir = process.cwd()) {
|
|
130
|
+
const explicit = process.env.CRANK_PROJECT_ROOT;
|
|
131
|
+
if (explicit) {
|
|
132
|
+
const resolved = path.resolve(explicit);
|
|
133
|
+
const { appRoot, reason } = findAppDirectory(resolved);
|
|
134
|
+
return {
|
|
135
|
+
projectRoot: appRoot,
|
|
136
|
+
repoRoot: gitRoot(appRoot) || resolved,
|
|
137
|
+
source: 'CRANK_PROJECT_ROOT',
|
|
138
|
+
reason
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const crankConfig = readCrankConfig(startDir);
|
|
143
|
+
if (crankConfig) {
|
|
144
|
+
const { appRoot, reason } = findAppDirectory(crankConfig.root);
|
|
145
|
+
return {
|
|
146
|
+
projectRoot: appRoot,
|
|
147
|
+
repoRoot: gitRoot(appRoot) || crankConfig.root,
|
|
148
|
+
source: '.crank/config.json',
|
|
149
|
+
reason
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const git = gitRoot(startDir);
|
|
154
|
+
if (git) {
|
|
155
|
+
const { appRoot, reason } = findAppDirectory(git);
|
|
156
|
+
return {
|
|
157
|
+
projectRoot: appRoot,
|
|
158
|
+
repoRoot: git,
|
|
159
|
+
source: 'git root',
|
|
160
|
+
reason
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pkgRoot = findPackageRoot(startDir);
|
|
165
|
+
const { appRoot, reason } = findAppDirectory(pkgRoot);
|
|
166
|
+
return {
|
|
167
|
+
projectRoot: appRoot,
|
|
168
|
+
repoRoot: pkgRoot,
|
|
169
|
+
source: 'cwd / package.json',
|
|
170
|
+
reason
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function describeEnvironment(projectRoot) {
|
|
175
|
+
const env = process.env;
|
|
176
|
+
const isReplit = Boolean(
|
|
177
|
+
env.REPL_ID || env.REPL_SLUG || env.REPLIT_DEV_DOMAIN || env.REPLIT_DB_URL
|
|
178
|
+
);
|
|
179
|
+
const isCodespaces = Boolean(env.CODESPACES);
|
|
180
|
+
const isCloudShell = Boolean(env.CLOUD_SHELL);
|
|
181
|
+
const resolved = resolveProjectRoot(projectRoot || process.cwd());
|
|
182
|
+
return {
|
|
183
|
+
...resolved,
|
|
184
|
+
gitRemote: gitRemote(resolved.repoRoot),
|
|
185
|
+
runtime: {
|
|
186
|
+
isReplit,
|
|
187
|
+
isCodespaces,
|
|
188
|
+
isCloudShell,
|
|
189
|
+
isVirtual: isReplit || isCodespaces || isCloudShell,
|
|
190
|
+
replitDomain: env.REPLIT_DEV_DOMAIN || null,
|
|
191
|
+
cwd: process.cwd()
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
resolveProjectRoot,
|
|
198
|
+
findAppDirectory,
|
|
199
|
+
describeEnvironment,
|
|
200
|
+
gitRoot,
|
|
201
|
+
gitRemote,
|
|
202
|
+
hasFrameworkMarker
|
|
203
|
+
};
|