@open-mova/cli 0.1.21 → 0.1.22

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 CHANGED
@@ -23,6 +23,10 @@ El CLI descarga la shell, el core y la plantilla de microfrontales desde tags es
23
23
  - [`mova shell`](#mova-shell)
24
24
  - [`mova shell versions`](#mova-shell-versions)
25
25
  - [`mova cap`](#mova-cap)
26
+ - [`mova cap list`](#mova-cap-list)
27
+ - [`mova cap enable`](#mova-cap-enable)
28
+ - [`mova cap disable`](#mova-cap-disable)
29
+ - [`mova cap doctor`](#mova-cap-doctor)
26
30
  - [`mova cap add`](#mova-cap-add)
27
31
  - [`mova cap sync`](#mova-cap-sync)
28
32
  - [`mova cap open`](#mova-cap-open)
@@ -229,6 +233,47 @@ mova cap add android
229
233
  mova cap add ios
230
234
  ```
231
235
 
236
+ ### `mova cap list`
237
+
238
+ Muestra el catálogo de capacidades nativas. `✓` indica que la capacidad está
239
+ habilitada y `○` que está disponible pero no instalada.
240
+
241
+ ```bash
242
+ mova cap list
243
+ ```
244
+
245
+ ### `mova cap enable`
246
+
247
+ Habilita una o varias capacidades. Instala únicamente sus paquetes npm,
248
+ actualiza `mova.config.json`, regenera el provider Angular y sincroniza las
249
+ plataformas nativas que ya existan.
250
+
251
+ ```bash
252
+ mova cap enable camera device geolocation
253
+ ```
254
+
255
+ ### `mova cap disable`
256
+
257
+ Deshabilita capacidades y elimina los paquetes que ninguna otra capacidad
258
+ activa necesita. El contrato de Core permanece disponible, pero
259
+ `isAvailable()` devuelve `false` y cualquier operación explica cómo volver a
260
+ habilitar la capacidad.
261
+
262
+ ```bash
263
+ mova cap disable geolocation
264
+ ```
265
+
266
+ ### `mova cap doctor`
267
+
268
+ Muestra plataformas compatibles, permisos, SDK mínimo y configuración manual
269
+ de las capacidades activas.
270
+
271
+ ```bash
272
+ mova cap doctor
273
+ mova cap doctor android
274
+ mova cap doctor ios
275
+ ```
276
+
232
277
  ### `mova cap sync`
233
278
 
234
279
  Sincroniza la shell compilada, los recursos y los plugins con la plataforma. Sin plataforma, sincroniza todas las plataformas añadidas.
@@ -238,9 +283,8 @@ mova cap sync android
238
283
  mova cap sync
239
284
  ```
240
285
 
241
- En Android, el CLI aplica también los requisitos técnicos de los plugins
242
- incluidos por la shell: configura el repositorio AAR de Background Runner y
243
- eleva `minSdkVersion` a 28 cuando está instalado Local LLM. También añade la
286
+ En Android, el CLI aplica los requisitos declarados por las capacidades
287
+ activas: permisos, SDK mínimo, el repositorio AAR de Background Runner y la
244
288
  entrada obligatoria de Google Maps. Para usar mapas reales, define una clave
245
289
  restringida antes de sincronizar:
246
290
 
@@ -274,7 +318,8 @@ mova cap open ios
274
318
  ### `mova cap`
275
319
 
276
320
  Es el grupo de comandos para preparar y abrir los proyectos nativos de
277
- Capacitor. Sus subcomandos son `add`, `sync` y `open`.
321
+ Capacitor. También permite seleccionar capacidades mediante `list`, `enable`,
322
+ `disable` y `doctor`.
278
323
 
279
324
  ## Flujo habitual
280
325
 
@@ -283,6 +328,7 @@ mova create mi-aplicacion
283
328
  cd mi-aplicacion
284
329
  npm install
285
330
  npm --prefix mfs/home install
331
+ mova cap enable camera device
286
332
  mova start
287
333
  ```
288
334
 
@@ -316,6 +362,14 @@ aplica la configuración específica de los plugins instalados:
316
362
  mova cap add android
317
363
  ```
318
364
 
365
+ Antes o después de añadir la plataforma puedes habilitar solo lo que use la
366
+ aplicación:
367
+
368
+ ```bash
369
+ mova cap enable camera device
370
+ mova cap doctor android
371
+ ```
372
+
319
373
  Si utilizas Google Maps, proporciona la clave antes de sincronizar:
320
374
 
321
375
  ```bash
@@ -369,6 +423,13 @@ mova cap add ios
369
423
  mova cap sync ios
370
424
  ```
371
425
 
426
+ Activa previamente las capacidades que necesites y revisa sus requisitos:
427
+
428
+ ```bash
429
+ mova cap enable camera device
430
+ mova cap doctor ios
431
+ ```
432
+
372
433
  Revisa en Xcode los permisos y la configuración nativa que necesiten los
373
434
  plugins utilizados, por ejemplo las descripciones de cámara, fotos,
374
435
  geolocalización o notificaciones en `Info.plist` y los entitlements
@@ -1,8 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { readNativeCapabilityCatalog } from './shell-configuration.js';
3
4
  export function configureAndroidProject(applicationRoot, configuration) {
4
5
  const packageConfiguration = readPackageConfiguration(applicationRoot);
5
- configureMinimumSdk(applicationRoot, packageConfiguration);
6
+ configureMinimumSdk(applicationRoot, configuration);
7
+ configurePermissions(applicationRoot, configuration);
6
8
  configureBackgroundRunner(applicationRoot, packageConfiguration);
7
9
  configureGoogleMaps(applicationRoot, configuration, packageConfiguration);
8
10
  }
@@ -42,8 +44,12 @@ function configureGoogleMaps(applicationRoot, configuration, packageConfiguratio
42
44
  ].join('\n');
43
45
  writeFileSync(join(valuesDirectory, 'open_mova_google_maps.xml'), resource, 'utf8');
44
46
  }
45
- function configureMinimumSdk(applicationRoot, packageConfiguration) {
46
- if (!packageConfiguration.dependencies?.['@capacitor/local-llm'])
47
+ function configureMinimumSdk(applicationRoot, configuration) {
48
+ const enabled = new Set(configuration.native?.capabilities ?? []);
49
+ const requiredMinimumSdk = Math.max(0, ...readNativeCapabilityCatalog(applicationRoot)
50
+ .capabilities.filter((capability) => enabled.has(capability.name))
51
+ .map((capability) => capability.minimumAndroidSdk ?? 0));
52
+ if (requiredMinimumSdk === 0)
47
53
  return;
48
54
  const variablesPath = join(applicationRoot, 'android', 'variables.gradle');
49
55
  if (!existsSync(variablesPath)) {
@@ -55,9 +61,28 @@ function configureMinimumSdk(applicationRoot, packageConfiguration) {
55
61
  if (!Number.isFinite(currentMinimumSdk)) {
56
62
  throw new Error('No se ha encontrado minSdkVersion en android/variables.gradle.');
57
63
  }
58
- if (currentMinimumSdk >= 28)
64
+ if (currentMinimumSdk >= requiredMinimumSdk)
59
65
  return;
60
- writeFileSync(variablesPath, variables.replace(minimumSdkPattern, 'minSdkVersion = 28'), 'utf8');
66
+ writeFileSync(variablesPath, variables.replace(minimumSdkPattern, `minSdkVersion = ${requiredMinimumSdk}`), 'utf8');
67
+ }
68
+ function configurePermissions(applicationRoot, configuration) {
69
+ const manifestPath = join(applicationRoot, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
70
+ if (!existsSync(manifestPath))
71
+ return;
72
+ const enabled = new Set(configuration.native?.capabilities ?? []);
73
+ const permissions = [
74
+ ...new Set(readNativeCapabilityCatalog(applicationRoot)
75
+ .capabilities.filter((capability) => enabled.has(capability.name))
76
+ .flatMap((capability) => capability.permissions?.android ?? [])),
77
+ ];
78
+ let manifest = readFileSync(manifestPath, 'utf8');
79
+ const applicationTag = /\s*<application\b/;
80
+ for (const permission of permissions) {
81
+ if (manifest.includes(`android:name="${permission}"`))
82
+ continue;
83
+ manifest = manifest.replace(applicationTag, `\n <uses-permission android:name="${permission}" />\n\n <application`);
84
+ }
85
+ writeFileSync(manifestPath, manifest, 'utf8');
61
86
  }
62
87
  function configureBackgroundRunner(applicationRoot, packageConfiguration) {
63
88
  if (!packageConfiguration.dependencies?.['@capacitor/background-runner'])
@@ -1,4 +1,4 @@
1
- export declare const LATEST_CONFIGURATION_SCHEMA_VERSION = 2;
1
+ export declare const LATEST_CONFIGURATION_SCHEMA_VERSION = 3;
2
2
  export interface ConfigurationMigrationResult {
3
3
  readonly value: unknown;
4
4
  readonly migrations: readonly string[];
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- export const LATEST_CONFIGURATION_SCHEMA_VERSION = 2;
3
+ export const LATEST_CONFIGURATION_SCHEMA_VERSION = 3;
4
4
  export function migrateConfiguration(value, applicationRoot) {
5
5
  if (!isRecord(value) || typeof value.schemaVersion !== 'number') {
6
6
  return { value, migrations: [] };
@@ -14,8 +14,54 @@ export function migrateConfiguration(value, applicationRoot) {
14
14
  migrated = migrateVersionOne(migrated, applicationRoot);
15
15
  migrations.push('1 → 2: declarar la compatibilidad de Core de cada microfrontal');
16
16
  }
17
+ if (migrated.schemaVersion === 2) {
18
+ migrated = migrateVersionTwo(migrated, applicationRoot);
19
+ migrations.push('2 → 3: declarar las capacidades nativas habilitadas');
20
+ }
17
21
  return { value: migrated, migrations };
18
22
  }
23
+ function migrateVersionTwo(configuration, applicationRoot) {
24
+ const currentNative = isRecord(configuration.native) ? configuration.native : {};
25
+ return {
26
+ ...configuration,
27
+ schemaVersion: 3,
28
+ native: {
29
+ ...currentNative,
30
+ // Las aplicaciones anteriores incluían todos los plugins. Se conservan
31
+ // explícitamente hasta que el equipo decida deshabilitarlos.
32
+ capabilities: readLegacyCapabilities(configuration, applicationRoot),
33
+ },
34
+ };
35
+ }
36
+ function readLegacyCapabilities(configuration, applicationRoot) {
37
+ if (isRecord(configuration.native) && Array.isArray(configuration.native.capabilities)) {
38
+ return configuration.native.capabilities.filter((capability) => typeof capability === 'string');
39
+ }
40
+ const packageConfiguration = readJson(join(applicationRoot, 'package.json'));
41
+ const catalog = readJson(join(applicationRoot, 'native-capabilities.catalog.json'));
42
+ const dependencies = isRecord(packageConfiguration?.dependencies)
43
+ ? packageConfiguration.dependencies
44
+ : {};
45
+ if (!Array.isArray(catalog?.capabilities))
46
+ return [];
47
+ return catalog.capabilities
48
+ .filter((capability) => isRecord(capability) &&
49
+ typeof capability.name === 'string' &&
50
+ typeof capability.package === 'string')
51
+ .filter((capability) => capability.package === '@capacitor/core' || capability.package in dependencies)
52
+ .map((capability) => capability.name);
53
+ }
54
+ function readJson(path) {
55
+ if (!existsSync(path))
56
+ return undefined;
57
+ try {
58
+ const value = JSON.parse(readFileSync(path, 'utf8'));
59
+ return isRecord(value) ? value : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
19
65
  function migrateVersionOne(configuration, applicationRoot) {
20
66
  const shellCoreVersion = readCoreVersion(join(applicationRoot, 'package.json'));
21
67
  const microfrontends = Array.isArray(configuration.microfrontends)
@@ -83,19 +83,25 @@ function validateApplicationConfiguration(value, configurationPath) {
83
83
  if (!isRecord(value.native)) {
84
84
  throw new Error(`${configurationPath} contiene una configuración nativa no válida.`);
85
85
  }
86
+ if (!Array.isArray(value.native.capabilities) ||
87
+ value.native.capabilities.some((capability) => typeof capability !== 'string')) {
88
+ throw new Error(`${configurationPath} contiene capacidades nativas no válidas.`);
89
+ }
90
+ const capabilities = [...new Set(value.native.capabilities)].sort();
86
91
  if (value.native.googleMaps !== undefined) {
87
92
  if (!isRecord(value.native.googleMaps) ||
88
93
  typeof value.native.googleMaps.androidApiKey !== 'string') {
89
94
  throw new Error(`${configurationPath} contiene una clave de Google Maps no válida.`);
90
95
  }
91
96
  native = {
97
+ capabilities,
92
98
  googleMaps: {
93
99
  androidApiKey: value.native.googleMaps.androidApiKey,
94
100
  },
95
101
  };
96
102
  }
97
103
  else {
98
- native = {};
104
+ native = { capabilities };
99
105
  }
100
106
  }
101
107
  const microfrontends = value.microfrontends.map((entry) => validateMicrofrontend(entry, configurationPath));
@@ -0,0 +1,15 @@
1
+ import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ import { type NativeCapabilityDefinition } from './shell-configuration.js';
3
+ export interface NativeCapabilityStatus {
4
+ readonly definition: NativeCapabilityDefinition;
5
+ readonly enabled: boolean;
6
+ }
7
+ export declare function listNativeCapabilities(applicationRoot: string): readonly NativeCapabilityStatus[];
8
+ export declare function enableNativeCapabilities(applicationRoot: string, requestedNames: readonly string[]): OpenMovaApplicationConfiguration;
9
+ export declare function disableNativeCapabilities(applicationRoot: string, requestedNames: readonly string[]): OpenMovaApplicationConfiguration;
10
+ export declare function diagnoseNativeCapabilities(applicationRoot: string, platform?: 'android' | 'ios'): readonly string[];
11
+ /**
12
+ * Recompone las dependencias seleccionables después de actualizar la shell.
13
+ * No instala paquetes: el flujo de update regenera después el lockfile.
14
+ */
15
+ export declare function synchronizeNativeCapabilityDependencies(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
@@ -0,0 +1,143 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { npmCommand, useCommandShell } from '../utils/platform.js';
5
+ import { readApplicationConfiguration, writeApplicationConfiguration } from './configuration.js';
6
+ import { readNativeCapabilityCatalog, synchronizeShellConfiguration, } from './shell-configuration.js';
7
+ export function listNativeCapabilities(applicationRoot) {
8
+ const configuration = readApplicationConfiguration(applicationRoot);
9
+ const enabled = new Set(configuration.native?.capabilities ?? []);
10
+ return readNativeCapabilityCatalog(applicationRoot).capabilities.map((definition) => ({
11
+ definition,
12
+ enabled: enabled.has(definition.name),
13
+ }));
14
+ }
15
+ export function enableNativeCapabilities(applicationRoot, requestedNames) {
16
+ const configuration = readApplicationConfiguration(applicationRoot);
17
+ const definitions = resolveDefinitions(applicationRoot, requestedNames);
18
+ const enabled = new Set(configuration.native?.capabilities ?? []);
19
+ const newDefinitions = definitions.filter((definition) => !enabled.has(definition.name));
20
+ for (const definition of definitions)
21
+ enabled.add(definition.name);
22
+ installPackages(applicationRoot, newDefinitions);
23
+ return persistCapabilities(applicationRoot, configuration, [...enabled]);
24
+ }
25
+ export function disableNativeCapabilities(applicationRoot, requestedNames) {
26
+ const configuration = readApplicationConfiguration(applicationRoot);
27
+ const definitions = resolveDefinitions(applicationRoot, requestedNames);
28
+ const currentlyEnabled = new Set(configuration.native?.capabilities ?? []);
29
+ const activeDefinitions = definitions.filter((definition) => currentlyEnabled.has(definition.name));
30
+ const disabled = new Set(definitions.map((definition) => definition.name));
31
+ const remainingNames = (configuration.native?.capabilities ?? []).filter((name) => !disabled.has(name));
32
+ const remainingDefinitions = remainingNames.length > 0 ? resolveDefinitions(applicationRoot, remainingNames) : [];
33
+ const retainedPackages = new Set(remainingDefinitions.map((definition) => definition.package));
34
+ const packagesToRemove = [
35
+ ...new Set(activeDefinitions
36
+ .map((definition) => definition.package)
37
+ .filter((packageName) => packageName !== '@capacitor/core')),
38
+ ].filter((packageName) => !retainedPackages.has(packageName));
39
+ uninstallPackages(applicationRoot, packagesToRemove);
40
+ return persistCapabilities(applicationRoot, configuration, remainingNames);
41
+ }
42
+ export function diagnoseNativeCapabilities(applicationRoot, platform) {
43
+ const statuses = listNativeCapabilities(applicationRoot).filter((status) => status.enabled);
44
+ const messages = [];
45
+ if (statuses.length === 0) {
46
+ return ['No hay capacidades nativas habilitadas.'];
47
+ }
48
+ for (const { definition } of statuses) {
49
+ if (platform && !definition.platforms.includes(platform)) {
50
+ messages.push(`⚠ ${definition.name}: no es compatible con ${platform}.`);
51
+ continue;
52
+ }
53
+ const requirements = [];
54
+ if (platform === 'android' && definition.minimumAndroidSdk) {
55
+ requirements.push(`Android SDK mínimo ${definition.minimumAndroidSdk}`);
56
+ }
57
+ if (platform && definition.permissions?.[platform]?.length) {
58
+ requirements.push(`permisos: ${definition.permissions[platform]?.join(', ')}`);
59
+ }
60
+ if (definition.notes?.length)
61
+ requirements.push(...definition.notes);
62
+ messages.push(requirements.length > 0
63
+ ? `• ${definition.name}: ${requirements.join('; ')}`
64
+ : `✓ ${definition.name}: sin configuración adicional declarada${platform ? ` para ${platform}` : ''}.`);
65
+ }
66
+ return messages;
67
+ }
68
+ /**
69
+ * Recompone las dependencias seleccionables después de actualizar la shell.
70
+ * No instala paquetes: el flujo de update regenera después el lockfile.
71
+ */
72
+ export function synchronizeNativeCapabilityDependencies(applicationRoot, configuration) {
73
+ const packagePath = join(applicationRoot, 'package.json');
74
+ const packageConfiguration = JSON.parse(readFileSync(packagePath, 'utf8'));
75
+ const dependencies = { ...(packageConfiguration.dependencies ?? {}) };
76
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
77
+ const enabled = new Set(configuration.native?.capabilities ?? []);
78
+ const selectablePackages = new Set(catalog.capabilities
79
+ .map((definition) => definition.package)
80
+ .filter((packageName) => packageName !== '@capacitor/core'));
81
+ for (const packageName of selectablePackages)
82
+ delete dependencies[packageName];
83
+ for (const definition of catalog.capabilities) {
84
+ if (enabled.has(definition.name) &&
85
+ definition.package !== '@capacitor/core' &&
86
+ definition.version) {
87
+ dependencies[definition.package] = definition.version;
88
+ }
89
+ }
90
+ packageConfiguration.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([first], [second]) => first.localeCompare(second)));
91
+ writeFileSync(packagePath, `${JSON.stringify(packageConfiguration, null, 2)}\n`, 'utf8');
92
+ }
93
+ function persistCapabilities(applicationRoot, configuration, capabilities) {
94
+ const updated = {
95
+ ...configuration,
96
+ native: {
97
+ ...configuration.native,
98
+ capabilities: [...new Set(capabilities)].sort(),
99
+ },
100
+ };
101
+ writeApplicationConfiguration(applicationRoot, updated);
102
+ synchronizeShellConfiguration(applicationRoot, updated);
103
+ return updated;
104
+ }
105
+ function resolveDefinitions(applicationRoot, names) {
106
+ if (names.length === 0)
107
+ throw new Error('Indica al menos una capacidad nativa.');
108
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
109
+ const definitionsByName = new Map(catalog.capabilities.map((definition) => [definition.name, definition]));
110
+ const unknown = names.filter((name) => !definitionsByName.has(name));
111
+ if (unknown.length > 0) {
112
+ throw new Error(`Capacidades desconocidas: ${unknown.join(', ')}. Usa "mova cap list" para consultar el catálogo.`);
113
+ }
114
+ return names.map((name) => definitionsByName.get(name));
115
+ }
116
+ function installPackages(applicationRoot, definitions) {
117
+ const packages = [
118
+ ...new Set(definitions
119
+ .filter((definition) => definition.package !== '@capacitor/core')
120
+ .map((definition) => `${definition.package}@${definition.version ?? 'latest'}`)),
121
+ ];
122
+ if (packages.length > 0)
123
+ runNpm(applicationRoot, ['install', '--save', ...packages]);
124
+ }
125
+ function uninstallPackages(applicationRoot, packages) {
126
+ if (packages.length > 0)
127
+ runNpm(applicationRoot, ['uninstall', '--save', ...packages]);
128
+ }
129
+ function runNpm(applicationRoot, arguments_) {
130
+ if (!existsSync(join(applicationRoot, 'package.json'))) {
131
+ throw new Error(`No se encuentra package.json en ${applicationRoot}.`);
132
+ }
133
+ const result = spawnSync(npmCommand(), arguments_, {
134
+ cwd: applicationRoot,
135
+ stdio: 'inherit',
136
+ shell: useCommandShell(),
137
+ });
138
+ if (result.error)
139
+ throw result.error;
140
+ if (result.status !== 0) {
141
+ throw new Error(`${npmCommand()} ${arguments_.join(' ')} terminó con error.`);
142
+ }
143
+ }
@@ -1,2 +1,22 @@
1
1
  import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ export interface NativeCapabilityDefinition {
3
+ readonly name: string;
4
+ readonly implementation: string;
5
+ readonly exportName: string;
6
+ readonly package: string;
7
+ readonly version?: string;
8
+ readonly platforms: readonly ('android' | 'ios' | 'web')[];
9
+ readonly minimumAndroidSdk?: number;
10
+ readonly permissions?: {
11
+ readonly android?: readonly string[];
12
+ readonly ios?: readonly string[];
13
+ };
14
+ readonly notes?: readonly string[];
15
+ }
16
+ interface NativeCapabilityCatalog {
17
+ readonly schemaVersion: 1;
18
+ readonly capabilities: readonly NativeCapabilityDefinition[];
19
+ }
2
20
  export declare function synchronizeShellConfiguration(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
21
+ export declare function readNativeCapabilityCatalog(applicationRoot: string): NativeCapabilityCatalog;
22
+ export {};
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  export function synchronizeShellConfiguration(applicationRoot, configuration) {
4
4
  const manifest = Object.fromEntries(configuration.microfrontends.map((microfrontend) => [
@@ -7,6 +7,55 @@ export function synchronizeShellConfiguration(applicationRoot, configuration) {
7
7
  ]));
8
8
  writeFileSync(join(applicationRoot, 'src', 'assets', 'federation.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
9
9
  writeFileSync(join(applicationRoot, 'src', 'app', 'application.config.ts'), renderApplicationConfiguration(configuration), 'utf8');
10
+ synchronizeNativeCapabilities(applicationRoot, configuration);
11
+ }
12
+ export function readNativeCapabilityCatalog(applicationRoot) {
13
+ const catalogPath = join(applicationRoot, 'native-capabilities.catalog.json');
14
+ const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
15
+ if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.capabilities)) {
16
+ throw new Error(`${catalogPath} no contiene un catálogo de capacidades válido.`);
17
+ }
18
+ return catalog;
19
+ }
20
+ function synchronizeNativeCapabilities(applicationRoot, configuration) {
21
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
22
+ const enabledNames = new Set(configuration.native?.capabilities ?? []);
23
+ const knownNames = new Set(catalog.capabilities.map((capability) => capability.name));
24
+ const unknownNames = [...enabledNames].filter((name) => !knownNames.has(name));
25
+ if (unknownNames.length > 0) {
26
+ throw new Error(`Capacidades nativas desconocidas: ${unknownNames.join(', ')}.`);
27
+ }
28
+ const enabled = catalog.capabilities.filter((capability) => enabledNames.has(capability.name));
29
+ const imports = enabled
30
+ .map((capability) => `import { ${capability.exportName} } from './${capability.implementation}';`)
31
+ .join('\n');
32
+ const entries = catalog.capabilities
33
+ .map((capability) => {
34
+ const value = enabledNames.has(capability.name)
35
+ ? capability.exportName
36
+ : `createUnavailableNativeCapability<NativeCapabilities[${JSON.stringify(capability.name)}]>(${JSON.stringify(capability.name)})`;
37
+ return ` ${capability.name}: ${value},`;
38
+ })
39
+ .join('\n');
40
+ const source = `import type { Provider } from '@angular/core';
41
+ import { NATIVE_CAPABILITIES, type NativeCapabilities } from '@open-mova/core';
42
+ import { createUnavailableNativeCapability } from './unavailable-native-capability';
43
+ ${imports ? `\n${imports}` : ''}
44
+
45
+ // Generado por el CLI desde mova.config.json. No editar manualmente.
46
+ const nativeCapabilities = {
47
+ version: 1,
48
+ ${entries}
49
+ } satisfies NativeCapabilities;
50
+
51
+ export function provideNativeCapabilities(): Provider {
52
+ return {
53
+ provide: NATIVE_CAPABILITIES,
54
+ useValue: nativeCapabilities,
55
+ };
56
+ }
57
+ `;
58
+ writeFileSync(join(applicationRoot, 'src', 'native-capabilities', 'native-capabilities.provider.ts'), source, 'utf8');
10
59
  }
11
60
  function renderApplicationConfiguration(configuration) {
12
61
  const entries = configuration.microfrontends
@@ -7,6 +7,7 @@ import { writeApplicationConfiguration } from './configuration.js';
7
7
  import { applyFileChanges, compareManagedFiles, comparePackageConfiguration, requireCleanGitRepository, } from './project-update.js';
8
8
  import { findIncompatibleMicrofrontends } from './core-compatibility.js';
9
9
  import { readRequiredCoreVersion } from './microfrontend-manifest.js';
10
+ import { synchronizeNativeCapabilityDependencies } from './native-capabilities.js';
10
11
  const GENERATED_PATHS = new Set([
11
12
  'package.json',
12
13
  'package-lock.json',
@@ -83,6 +84,7 @@ export function applyShellUpdate(applicationRoot, configuration, plan) {
83
84
  ...configuration,
84
85
  shell: plan.target,
85
86
  };
87
+ synchronizeNativeCapabilityDependencies(applicationRoot, updatedConfiguration);
86
88
  writeApplicationConfiguration(applicationRoot, updatedConfiguration);
87
89
  synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
88
90
  }
@@ -3,9 +3,47 @@ import { existsSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
5
5
  import { configureAndroidProject } from '../application/android-configuration.js';
6
+ import { diagnoseNativeCapabilities, disableNativeCapabilities, enableNativeCapabilities, listNativeCapabilities, } from '../application/native-capabilities.js';
6
7
  import { localBinaryName, npmCommand, useCommandShell } from '../utils/platform.js';
7
8
  export function registerCapacitorCommands(program) {
8
9
  const capacitor = program.command('cap').description('Prepara la shell para Android o iOS');
10
+ capacitor
11
+ .command('list')
12
+ .description('Lista las capacidades nativas disponibles y habilitadas')
13
+ .action(() => {
14
+ const root = requireApplicationRoot(process.cwd());
15
+ for (const { definition, enabled } of listNativeCapabilities(root)) {
16
+ console.log(`${enabled ? '✓' : '○'} ${definition.name} (${definition.platforms.join(', ')})`);
17
+ }
18
+ });
19
+ capacitor
20
+ .command('enable <capabilities...>')
21
+ .description('Instala y habilita capacidades nativas en la aplicación')
22
+ .action((capabilities) => {
23
+ const root = requireApplicationRoot(process.cwd());
24
+ enableNativeCapabilities(root, capabilities);
25
+ synchronizeExistingNativeProjects(root);
26
+ console.log(`Capacidades habilitadas: ${capabilities.join(', ')}.`);
27
+ });
28
+ capacitor
29
+ .command('disable <capabilities...>')
30
+ .description('Deshabilita capacidades y elimina los plugins que ya no se usan')
31
+ .action((capabilities) => {
32
+ const root = requireApplicationRoot(process.cwd());
33
+ disableNativeCapabilities(root, capabilities);
34
+ synchronizeExistingNativeProjects(root);
35
+ console.log(`Capacidades deshabilitadas: ${capabilities.join(', ')}.`);
36
+ });
37
+ capacitor
38
+ .command('doctor [platform]')
39
+ .description('Muestra requisitos y configuración pendiente de las capacidades')
40
+ .action((platform) => {
41
+ const root = requireApplicationRoot(process.cwd());
42
+ const selectedPlatform = platform ? parsePlatform(platform) : undefined;
43
+ for (const message of diagnoseNativeCapabilities(root, selectedPlatform)) {
44
+ console.log(message);
45
+ }
46
+ });
9
47
  capacitor
10
48
  .command('add <platform>')
11
49
  .description('Crea el proyecto nativo de la plataforma')
@@ -81,6 +119,15 @@ function configureExistingPlatforms(root, configuration) {
81
119
  configureNativePlatform(root, 'android', configuration);
82
120
  }
83
121
  }
122
+ function synchronizeExistingNativeProjects(root) {
123
+ const platforms = ['android', 'ios'];
124
+ for (const platform of platforms) {
125
+ if (existsSync(join(root, platform))) {
126
+ runCapacitor(root, ['sync', platform]);
127
+ configureNativePlatform(root, platform, readApplicationConfiguration(root));
128
+ }
129
+ }
130
+ }
84
131
  function configureNativePlatform(root, platform, configuration) {
85
132
  if (platform === 'android') {
86
133
  configureAndroidProject(root, configuration);
@@ -27,9 +27,10 @@ export function registerCreateCommand(program) {
27
27
  shellVersion = shell.version;
28
28
  configureDownloadedShell(temporaryApplication, applicationName, !options.empty);
29
29
  let configuration = {
30
- schemaVersion: 2,
30
+ schemaVersion: 3,
31
31
  name: applicationName,
32
32
  shell,
33
+ native: { capabilities: [] },
33
34
  microfrontends: [],
34
35
  };
35
36
  if (!options.empty) {
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface OpenMovaApplicationConfiguration {
2
- readonly schemaVersion: 2;
2
+ readonly schemaVersion: 3;
3
3
  readonly name: string;
4
4
  readonly shell?: {
5
5
  readonly repository: string;
@@ -11,6 +11,8 @@ export interface OpenMovaApplicationConfiguration {
11
11
  readonly microfrontends: readonly MicrofrontendConfiguration[];
12
12
  }
13
13
  export interface NativeConfiguration {
14
+ /** Capacidades instaladas y proporcionadas por la shell. */
15
+ readonly capabilities: readonly string[];
14
16
  readonly googleMaps?: {
15
17
  /** Clave de Android Maps. También puede venir de OPEN_MOVA_GOOGLE_MAPS_ANDROID_API_KEY. */
16
18
  readonly androidApiKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mova/cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "CLI para crear y mantener aplicaciones Open Mova",
5
5
  "license": "MIT",
6
6
  "author": "Open Mova contributors",