shadownrx-code 1.0.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/README.md +130 -0
- package/bin.js +159 -0
- package/lib.js +95 -0
- package/mcp-server.js +135 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# shadownrx-code
|
|
2
|
+
|
|
3
|
+
Configurador interactivo de terminal para la plataforma de build de
|
|
4
|
+
`shadownrx/code`. Detecta si tu proyecto es Flutter, React Native, una PWA
|
|
5
|
+
(Capacitor) o Electron, te hace un par de preguntas, y te escribe
|
|
6
|
+
`.github/workflows/build.yml` ya configurado — no compila nada localmente, solo
|
|
7
|
+
prepara el workflow que corre gratis en GitHub Actions.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
▲ shadownrx/code — configurador de CI para Android, iOS y Electron
|
|
11
|
+
|
|
12
|
+
? Detecté un proyecto React Native en este directorio. ¿Es correcto? › Yes
|
|
13
|
+
? ¿Qué deseas compilar? › Android + iOS (ambas)
|
|
14
|
+
? ¿Ya tenés listos los secrets de firma (keystore / certificado de Apple)? › No
|
|
15
|
+
? ¿Adjuntar los builds a un GitHub Release cuando pushees un tag (v1.2.3)? › No
|
|
16
|
+
|
|
17
|
+
✔ Escribí .github/workflows/build.yml
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Para Electron, la pregunta "¿Qué deseas compilar?" y la de firma no aparecen
|
|
21
|
+
(Electron siempre compila Linux + macOS + Windows, y la firma todavía no está
|
|
22
|
+
soportada) — solo pregunta el tipo de proyecto y si querés adjuntar a un
|
|
23
|
+
GitHub Release.
|
|
24
|
+
|
|
25
|
+
## Usar
|
|
26
|
+
|
|
27
|
+
La idea es que, una vez publicado, alcance con:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx shadownrx-code
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
parado en la raíz de tu proyecto (Flutter, React Native, PWA o Electron) — sin
|
|
34
|
+
clonar nada. **Todavía no está publicado en npm** (publicarlo es distribuirlo
|
|
35
|
+
públicamente, así que quedó pendiente de que lo pidas explícito — y hace falta
|
|
36
|
+
una cuenta/token de npm que este entorno no tiene). El nombre `shadownrx-code`
|
|
37
|
+
está libre en el registro — no se reserva hasta el primer `npm publish`.
|
|
38
|
+
|
|
39
|
+
Mientras tanto, corré la versión local:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/shadownrx/code
|
|
43
|
+
cd code/cli
|
|
44
|
+
npm install
|
|
45
|
+
node bin.js # parado en la raíz de TU proyecto, o pasale el cwd que corresponda
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
o, apuntando directo a la carpeta de tu proyecto:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd /ruta/a/tu/proyecto
|
|
52
|
+
node /ruta/a/code/cli/bin.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Como tool de Cursor (o cualquier cliente MCP)
|
|
56
|
+
|
|
57
|
+
Además del CLI interactivo, `mcp-server.js` expone lo mismo como un servidor
|
|
58
|
+
[MCP](https://modelcontextprotocol.io) — así el agente de Cursor (o Claude
|
|
59
|
+
Code, Windsurf, o cualquier editor con soporte MCP; no es algo específico de
|
|
60
|
+
Cursor) puede llamarlo directamente en la conversación, sin que abras una
|
|
61
|
+
terminal vos.
|
|
62
|
+
|
|
63
|
+
Agregá esto a `.cursor/mcp.json` en tu proyecto (o al `mcp.json` global de
|
|
64
|
+
Cursor):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"shadownrx-code": {
|
|
70
|
+
"command": "node",
|
|
71
|
+
"args": ["/ruta/absoluta/a/code/cli/mcp-server.js"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
(Una vez publicado en npm, `"command": "npx", "args": ["-y", "shadownrx-code-mcp"]`
|
|
78
|
+
alcanzaría, sin ruta absoluta ni clonar nada — mismo estado pendiente que el
|
|
79
|
+
CLI, ver arriba.)
|
|
80
|
+
|
|
81
|
+
Este mismo repo ya trae [`.cursor/mcp.json`](../.cursor/mcp.json) apuntando a
|
|
82
|
+
`cli/mcp-server.js` con `${workspaceFolder}` — si abrís `shadownrx/code` en
|
|
83
|
+
Cursor, la tool queda disponible sola. Eso sí, corré `npm install` en `cli/`
|
|
84
|
+
primero (`node_modules` no se commitea): el server necesita
|
|
85
|
+
`@modelcontextprotocol/sdk` y `zod` instalados para arrancar.
|
|
86
|
+
|
|
87
|
+
Expone dos tools:
|
|
88
|
+
|
|
89
|
+
| Tool | Qué hace |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `detect_project_type` | Solo lee: reporta si un directorio es Flutter/React Native/PWA/Electron. No escribe nada. |
|
|
92
|
+
| `setup_mobile_ci` | Detecta (o recibe `project_type` explícito) y escribe `.github/workflows/build.yml` — mismos parámetros que el CLI interactivo (`build_android`, `build_ios`, `build_electron`, `create_release`), más `overwrite` para confirmar si el archivo ya existe. |
|
|
93
|
+
|
|
94
|
+
Ambas tools están probadas de punta a punta contra el server real (no un
|
|
95
|
+
mock): `mcp-server.test.js` levanta `mcp-server.js` como subproceso vía stdio
|
|
96
|
+
con el `Client` del SDK de MCP y llama las tools de verdad.
|
|
97
|
+
|
|
98
|
+
## Qué pregunta y por qué
|
|
99
|
+
|
|
100
|
+
Cada pregunta mapea 1 a 1 a un input real de
|
|
101
|
+
[`build-mobile.yml`](../.github/workflows/build-mobile.yml) — no hay ninguna
|
|
102
|
+
opción que prometa algo que la plataforma no haga. Ver
|
|
103
|
+
[`docs/USAGE.md`](../docs/USAGE.md) para el detalle de cada input.
|
|
104
|
+
|
|
105
|
+
| Pregunta | Input generado |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Tipo de proyecto (auto-detectado o elegido) | `project_type` |
|
|
108
|
+
| Qué compilar (Android / iOS / ambas) — no aplica a Electron | `build_android`, `build_ios` |
|
|
109
|
+
| ¿Adjuntar a un GitHub Release en tags? | `create_release` (+ el bloque `permissions: contents: write` que necesita) |
|
|
110
|
+
|
|
111
|
+
Para Electron, en vez de `build_android`/`build_ios` se emite
|
|
112
|
+
`build_electron: true` (siempre compila los tres sistemas operativos).
|
|
113
|
+
|
|
114
|
+
Si respondés que ya tenés los secrets de firma listos, te lista los nombres
|
|
115
|
+
exactos que hay que cargar en GitHub y te manda a `docs/SIGNING.md` — no los
|
|
116
|
+
pide ni los toca, esos siempre se configuran a mano como secrets de GitHub.
|
|
117
|
+
|
|
118
|
+
Si `.github/workflows/build.yml` ya existe, pregunta antes de pisarlo.
|
|
119
|
+
|
|
120
|
+
## Desarrollo
|
|
121
|
+
|
|
122
|
+
`lib.js` tiene toda la lógica pura (detección de proyecto + generación del
|
|
123
|
+
YAML) separada de `bin.js` (la parte interactiva con
|
|
124
|
+
[`prompts`](https://www.npmjs.com/package/prompts)) y de `mcp-server.js` (el
|
|
125
|
+
server MCP) — justamente para no duplicar la lógica y poder testearla sin
|
|
126
|
+
simular una terminal ni un cliente MCP real:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm test # node --test — corre lib.test.js y mcp-server.test.js
|
|
130
|
+
```
|
package/bin.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import {
|
|
6
|
+
PROJECT_LABELS,
|
|
7
|
+
androidSecrets,
|
|
8
|
+
buildWorkflowYaml,
|
|
9
|
+
detectProjectType,
|
|
10
|
+
iosSecrets,
|
|
11
|
+
} from './lib.js';
|
|
12
|
+
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
function onCancel() {
|
|
16
|
+
console.log('\nCancelado — no se modificó nada.\n');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
console.log('\n▲ shadownrx/code — configurador de CI para Android, iOS y Electron\n');
|
|
22
|
+
console.log('Esto escribe .github/workflows/build.yml en este proyecto. No compila nada');
|
|
23
|
+
console.log('acá: el build corre gratis en GitHub Actions con cada push.\n');
|
|
24
|
+
|
|
25
|
+
const detected = detectProjectType(cwd);
|
|
26
|
+
let projectType = detected;
|
|
27
|
+
|
|
28
|
+
if (detected) {
|
|
29
|
+
const { confirmDetected } = await prompts(
|
|
30
|
+
{
|
|
31
|
+
type: 'confirm',
|
|
32
|
+
name: 'confirmDetected',
|
|
33
|
+
message: `Detecté un proyecto ${PROJECT_LABELS[detected]} en este directorio. ¿Es correcto?`,
|
|
34
|
+
initial: true,
|
|
35
|
+
},
|
|
36
|
+
{ onCancel }
|
|
37
|
+
);
|
|
38
|
+
if (!confirmDetected) projectType = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!projectType) {
|
|
42
|
+
const { picked } = await prompts(
|
|
43
|
+
{
|
|
44
|
+
type: 'select',
|
|
45
|
+
name: 'picked',
|
|
46
|
+
message: '¿Qué tipo de proyecto es?',
|
|
47
|
+
choices: [
|
|
48
|
+
{ title: 'Flutter', value: 'flutter' },
|
|
49
|
+
{ title: 'React Native', value: 'react-native' },
|
|
50
|
+
{ title: 'PWA (Capacitor)', value: 'pwa' },
|
|
51
|
+
{ title: 'Electron', value: 'electron' },
|
|
52
|
+
{ title: 'Detectar automáticamente en cada build', value: 'auto' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{ onCancel }
|
|
56
|
+
);
|
|
57
|
+
projectType = picked;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isElectron = projectType === 'electron';
|
|
61
|
+
|
|
62
|
+
let buildAndroid = true;
|
|
63
|
+
let buildIos = true;
|
|
64
|
+
let buildElectron = true;
|
|
65
|
+
let wantsSigning = false;
|
|
66
|
+
|
|
67
|
+
if (isElectron) {
|
|
68
|
+
console.log('\nElectron compila Linux, macOS y Windows en paralelo — no hace falta elegir.\n');
|
|
69
|
+
} else {
|
|
70
|
+
const { target } = await prompts(
|
|
71
|
+
{
|
|
72
|
+
type: 'select',
|
|
73
|
+
name: 'target',
|
|
74
|
+
message: '¿Qué deseas compilar?',
|
|
75
|
+
choices: [
|
|
76
|
+
{ title: 'Android + iOS (ambas)', value: 'both' },
|
|
77
|
+
{ title: 'Solo Android', value: 'android' },
|
|
78
|
+
{ title: 'Solo iOS', value: 'ios' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{ onCancel }
|
|
82
|
+
);
|
|
83
|
+
buildAndroid = target !== 'ios';
|
|
84
|
+
buildIos = target !== 'android';
|
|
85
|
+
|
|
86
|
+
const signingAnswer = await prompts(
|
|
87
|
+
{
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
name: 'wantsSigning',
|
|
90
|
+
message: '¿Ya tenés listos los secrets de firma (keystore / certificado de Apple)?',
|
|
91
|
+
initial: false,
|
|
92
|
+
},
|
|
93
|
+
{ onCancel }
|
|
94
|
+
);
|
|
95
|
+
wantsSigning = signingAnswer.wantsSigning;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { wantsRelease } = await prompts(
|
|
99
|
+
{
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'wantsRelease',
|
|
102
|
+
message: '¿Adjuntar los builds a un GitHub Release cuando pushees un tag (v1.2.3)?',
|
|
103
|
+
initial: false,
|
|
104
|
+
},
|
|
105
|
+
{ onCancel }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const workflowPath = path.join(cwd, '.github', 'workflows', 'build.yml');
|
|
109
|
+
if (fs.existsSync(workflowPath)) {
|
|
110
|
+
const { overwrite } = await prompts(
|
|
111
|
+
{
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
name: 'overwrite',
|
|
114
|
+
message: `Ya existe ${path.relative(cwd, workflowPath)}. ¿Sobrescribir?`,
|
|
115
|
+
initial: false,
|
|
116
|
+
},
|
|
117
|
+
{ onCancel }
|
|
118
|
+
);
|
|
119
|
+
if (!overwrite) {
|
|
120
|
+
console.log('\nCancelado — no se modificó nada.\n');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const yaml = buildWorkflowYaml({
|
|
126
|
+
projectType,
|
|
127
|
+
buildAndroid,
|
|
128
|
+
buildIos,
|
|
129
|
+
buildElectron,
|
|
130
|
+
createRelease: wantsRelease,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
fs.mkdirSync(path.dirname(workflowPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(workflowPath, yaml);
|
|
135
|
+
|
|
136
|
+
console.log(`\n✔ Escribí ${path.relative(cwd, workflowPath)}\n`);
|
|
137
|
+
|
|
138
|
+
if (isElectron) {
|
|
139
|
+
console.log(
|
|
140
|
+
'Los builds de Electron salen sin firmar por ahora (la firma de macOS/Windows\n' +
|
|
141
|
+
'todavía no está soportada por la plataforma).\n'
|
|
142
|
+
);
|
|
143
|
+
} else if (wantsSigning) {
|
|
144
|
+
console.log('Agregá estos secrets en GitHub (Settings → Secrets and variables → Actions):\n');
|
|
145
|
+
if (buildAndroid) console.log(` Android: ${androidSecrets().join(', ')}`);
|
|
146
|
+
if (buildIos) console.log(` iOS: ${iosSecrets().join(', ')}`);
|
|
147
|
+
console.log('\nDetalle paso a paso: https://github.com/shadownrx/code/blob/main/docs/SIGNING.md\n');
|
|
148
|
+
} else {
|
|
149
|
+
console.log(
|
|
150
|
+
'Por ahora va a compilar sin firmar (sirve para probar que compila y correr en\n' +
|
|
151
|
+
'simulador/emulador). Cuando quieras builds firmados, mirá docs/SIGNING.md.\n'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('Siguiente paso: hacé commit y push — el primer build corre solo, gratis,');
|
|
156
|
+
console.log('sin necesitar Mac (ni Windows, si es Electron) en tu máquina.\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main();
|
package/lib.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const PROJECT_LABELS = {
|
|
5
|
+
flutter: 'Flutter',
|
|
6
|
+
'react-native': 'React Native',
|
|
7
|
+
pwa: 'PWA (Capacitor)',
|
|
8
|
+
electron: 'Electron',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Same detection rules as .github/actions/detect-project/action.yml,
|
|
13
|
+
* ported to Node so the CLI can guess before asking.
|
|
14
|
+
*/
|
|
15
|
+
export function detectProjectType(cwd) {
|
|
16
|
+
const pubspecPath = path.join(cwd, 'pubspec.yaml');
|
|
17
|
+
if (fs.existsSync(pubspecPath)) {
|
|
18
|
+
const content = fs.readFileSync(pubspecPath, 'utf8');
|
|
19
|
+
if (/^\s*flutter:/m.test(content)) return 'flutter';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
fs.existsSync(path.join(cwd, 'capacitor.config.ts')) ||
|
|
24
|
+
fs.existsSync(path.join(cwd, 'capacitor.config.json'))
|
|
25
|
+
) {
|
|
26
|
+
return 'pwa';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
30
|
+
if (fs.existsSync(pkgPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
33
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
34
|
+
if (deps && (deps['electron'] || deps['electron-builder'])) return 'electron';
|
|
35
|
+
if (deps && deps['react-native']) return 'react-native';
|
|
36
|
+
} catch {
|
|
37
|
+
// malformed package.json — fall through to "not detected"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function androidSecrets() {
|
|
45
|
+
return ['ANDROID_KEYSTORE_BASE64', 'ANDROID_KEYSTORE_PASSWORD', 'ANDROID_KEY_ALIAS', 'ANDROID_KEY_PASSWORD'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function iosSecrets() {
|
|
49
|
+
return ['IOS_CERTIFICATE_BASE64', 'IOS_CERTIFICATE_PASSWORD', 'IOS_PROVISION_PROFILE_BASE64', 'IOS_TEAM_ID'];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds the literal contents of .github/workflows/build.yml.
|
|
54
|
+
* Kept as plain string templating (not a YAML library) so the output
|
|
55
|
+
* matches exactly what docs/USAGE.md shows and stays easy to diff by eye.
|
|
56
|
+
*
|
|
57
|
+
* For projectType 'electron', buildAndroid/buildIos are irrelevant (that
|
|
58
|
+
* job only runs for mobile project types) and buildElectron is emitted
|
|
59
|
+
* instead — and vice versa.
|
|
60
|
+
*/
|
|
61
|
+
export function buildWorkflowYaml({ projectType, buildAndroid, buildIos, buildElectron, createRelease }) {
|
|
62
|
+
const lines = [
|
|
63
|
+
'name: Build Mobile Apps',
|
|
64
|
+
'',
|
|
65
|
+
'on:',
|
|
66
|
+
' push:',
|
|
67
|
+
' branches: [main]',
|
|
68
|
+
' tags: ["v*"]',
|
|
69
|
+
' pull_request:',
|
|
70
|
+
' workflow_dispatch:',
|
|
71
|
+
'',
|
|
72
|
+
'jobs:',
|
|
73
|
+
' build:',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
if (createRelease) {
|
|
77
|
+
lines.push(' permissions:', ' contents: write');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push(' uses: shadownrx/code/.github/workflows/build-mobile.yml@main', ' with:', ` project_type: ${projectType}`);
|
|
81
|
+
|
|
82
|
+
if (projectType === 'electron') {
|
|
83
|
+
lines.push(` build_electron: ${buildElectron}`);
|
|
84
|
+
} else {
|
|
85
|
+
lines.push(` build_android: ${buildAndroid}`, ` build_ios: ${buildIos}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (createRelease) {
|
|
89
|
+
lines.push(' create_release: true');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push(' secrets: inherit', '');
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP server exposing shadownrx/code's build-mobile.yml setup as tools for
|
|
3
|
+
// any MCP client (Cursor, Claude Code, Windsurf, etc.) — not Cursor-specific,
|
|
4
|
+
// MCP is an open standard. See cli/README.md for how to point Cursor at this.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
PROJECT_LABELS,
|
|
12
|
+
androidSecrets,
|
|
13
|
+
buildWorkflowYaml,
|
|
14
|
+
detectProjectType,
|
|
15
|
+
iosSecrets,
|
|
16
|
+
} from './lib.js';
|
|
17
|
+
|
|
18
|
+
const server = new McpServer({ name: 'shadownrx-code', version: '1.0.0' });
|
|
19
|
+
|
|
20
|
+
const PROJECT_TYPES = ['auto', 'flutter', 'react-native', 'pwa', 'electron'];
|
|
21
|
+
|
|
22
|
+
function text(str) {
|
|
23
|
+
return { content: [{ type: 'text', text: str }] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
server.registerTool(
|
|
27
|
+
'detect_project_type',
|
|
28
|
+
{
|
|
29
|
+
title: 'Detect mobile/desktop project type',
|
|
30
|
+
description:
|
|
31
|
+
'Looks at a directory and reports whether it is a Flutter, React Native, PWA (Capacitor), or Electron ' +
|
|
32
|
+
'project, using the same rules as build-mobile.yml\'s auto-detection. Read-only — writes nothing.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
cwd: z.string().optional().describe('Directory to inspect. Defaults to the server process cwd.'),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async ({ cwd }) => {
|
|
38
|
+
const dir = cwd ? path.resolve(cwd) : process.cwd();
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
return { ...text(`No existe el directorio: ${dir}`), isError: true };
|
|
41
|
+
}
|
|
42
|
+
const detected = detectProjectType(dir);
|
|
43
|
+
if (!detected) {
|
|
44
|
+
return text(
|
|
45
|
+
`No se detectó ningún tipo de proyecto conocido en ${dir}.\n` +
|
|
46
|
+
'Buscado: pubspec.yaml con flutter:, capacitor.config.ts/json, o package.json ' +
|
|
47
|
+
'con electron/electron-builder/react-native.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return text(`Detectado: ${PROJECT_LABELS[detected]} (project_type: ${detected}) en ${dir}`);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.registerTool(
|
|
55
|
+
'setup_mobile_ci',
|
|
56
|
+
{
|
|
57
|
+
title: 'Set up free Android/iOS/Electron CI',
|
|
58
|
+
description:
|
|
59
|
+
'Writes .github/workflows/build.yml wired to shadownrx/code\'s free reusable GitHub Actions workflow ' +
|
|
60
|
+
'(build-mobile.yml), which builds Android/iOS (no Mac needed) or Electron for Linux/macOS/Windows ' +
|
|
61
|
+
'(no Mac or Windows machine needed), depending on project_type. Detects the project type automatically ' +
|
|
62
|
+
'if not given. Refuses to overwrite an existing build.yml unless overwrite is true.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
cwd: z.string().optional().describe('Project root to write .github/workflows/build.yml into. Defaults to the server process cwd.'),
|
|
65
|
+
project_type: z.enum(PROJECT_TYPES).optional().describe('Defaults to auto-detecting from the project files.'),
|
|
66
|
+
build_android: z.boolean().optional().describe('Only relevant for flutter/react-native/pwa. Default true.'),
|
|
67
|
+
build_ios: z.boolean().optional().describe('Only relevant for flutter/react-native/pwa. Default true.'),
|
|
68
|
+
build_electron: z.boolean().optional().describe('Only relevant for project_type electron. Default true.'),
|
|
69
|
+
create_release: z.boolean().optional().describe('Attach build artifacts to a GitHub Release on tag pushes. Default false.'),
|
|
70
|
+
overwrite: z.boolean().optional().describe('Overwrite an existing .github/workflows/build.yml. Default false.'),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
async ({ cwd, project_type, build_android, build_ios, build_electron, create_release, overwrite }) => {
|
|
74
|
+
const dir = cwd ? path.resolve(cwd) : process.cwd();
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
return { ...text(`No existe el directorio: ${dir}`), isError: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let projectType = project_type;
|
|
80
|
+
if (!projectType || projectType === 'auto') {
|
|
81
|
+
const detected = detectProjectType(dir);
|
|
82
|
+
if (!detected) {
|
|
83
|
+
return {
|
|
84
|
+
...text(
|
|
85
|
+
`No pude detectar el tipo de proyecto en ${dir}. Pasá project_type explícitamente ` +
|
|
86
|
+
`(${PROJECT_TYPES.filter((t) => t !== 'auto').join(', ')}).`
|
|
87
|
+
),
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
projectType = detected;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isElectron = projectType === 'electron';
|
|
95
|
+
const yaml = buildWorkflowYaml({
|
|
96
|
+
projectType,
|
|
97
|
+
buildAndroid: build_android ?? true,
|
|
98
|
+
buildIos: build_ios ?? true,
|
|
99
|
+
buildElectron: build_electron ?? true,
|
|
100
|
+
createRelease: create_release ?? false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const workflowPath = path.join(dir, '.github', 'workflows', 'build.yml');
|
|
104
|
+
const relPath = path.relative(dir, workflowPath);
|
|
105
|
+
if (fs.existsSync(workflowPath) && !overwrite) {
|
|
106
|
+
return {
|
|
107
|
+
...text(
|
|
108
|
+
`Ya existe ${relPath}. Llamá de nuevo con overwrite: true si querés reemplazarlo.\n\n` +
|
|
109
|
+
'Contenido propuesto:\n\n```yaml\n' + yaml + '\n```'
|
|
110
|
+
),
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.mkdirSync(path.dirname(workflowPath), { recursive: true });
|
|
116
|
+
fs.writeFileSync(workflowPath, yaml);
|
|
117
|
+
|
|
118
|
+
const notes = [`Escribí ${relPath} (project_type: ${projectType}).`];
|
|
119
|
+
if (isElectron) {
|
|
120
|
+
notes.push('Los builds de Electron salen sin firmar por ahora (firma de macOS/Windows no soportada todavía).');
|
|
121
|
+
} else {
|
|
122
|
+
notes.push(
|
|
123
|
+
'Sin firma por defecto. Para builds firmados agregá los secrets de GitHub descritos en docs/SIGNING.md:\n' +
|
|
124
|
+
` Android: ${androidSecrets().join(', ')}\n` +
|
|
125
|
+
` iOS: ${iosSecrets().join(', ')}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
notes.push('Siguiente paso: commit + push. El build corre gratis en GitHub Actions.');
|
|
129
|
+
|
|
130
|
+
return text(notes.join('\n\n') + '\n\n```yaml\n' + yaml + '\n```');
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const transport = new StdioServerTransport();
|
|
135
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shadownrx-code",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive CLI + MCP server for the shadownrx/code free Android/iOS/PWA/Electron build platform — writes .github/workflows/build.yml for your project, or exposes the same thing as tools for Cursor/Claude Code/any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shadownrx-code": "bin.js",
|
|
8
|
+
"shadownrx-code-mcp": "mcp-server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin.js",
|
|
12
|
+
"lib.js",
|
|
13
|
+
"mcp-server.js"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node bin.js",
|
|
17
|
+
"mcp": "node mcp-server.js",
|
|
18
|
+
"test": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
26
|
+
"prompts": "^2.4.2",
|
|
27
|
+
"zod": "^4.5.4"
|
|
28
|
+
}
|
|
29
|
+
}
|