@open-mova/cli 0.1.13 → 0.1.14

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
@@ -16,6 +16,7 @@ El CLI descarga la shell, el core y la plantilla de microfrontales desde tags es
16
16
  - [`mova start`](#mova-start)
17
17
  - [`mova build`](#mova-build)
18
18
  - [`mova info`](#mova-info)
19
+ - [`mova update`](#mova-update)
19
20
  - [`mova shell versions`](#mova-shell-versions)
20
21
  - [`mova cap add`](#mova-cap-add)
21
22
  - [`mova cap sync`](#mova-cap-sync)
@@ -121,6 +122,23 @@ Muestra la aplicación detectada, la versión de shell y sus microfrontales. Tam
121
122
  mova info
122
123
  ```
123
124
 
125
+ ### `mova update`
126
+
127
+ Actualiza los archivos técnicos de la shell usando como referencia el tag con
128
+ el que se creó la aplicación. No modifica los microfrontales ni sobrescribe
129
+ archivos personalizados: los muestra como conflictos.
130
+
131
+ ```bash
132
+ mova update --check
133
+ mova update
134
+ mova update --to v0.2.0
135
+ ```
136
+
137
+ `--check` solo muestra el plan. Para aplicar cambios, el proyecto debe estar en
138
+ un repositorio Git limpio. Si cambia `package.json`, el CLI elimina el lockfile
139
+ obsoleto y pide ejecutar `npm install`; después se debe comprobar con
140
+ `mova build`.
141
+
124
142
  ### `mova shell versions`
125
143
 
126
144
  Lista los tags estables disponibles para crear aplicaciones.
@@ -0,0 +1,19 @@
1
+ import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ import { type ShellVersion } from './shell-repository.js';
3
+ interface FileChange {
4
+ readonly path: string;
5
+ readonly content?: Buffer;
6
+ }
7
+ export interface ShellUpdatePlan {
8
+ readonly currentVersion: string;
9
+ readonly target: ShellVersion;
10
+ readonly changes: readonly string[];
11
+ readonly conflicts: readonly string[];
12
+ readonly fileChanges: readonly FileChange[];
13
+ readonly packageContent?: string;
14
+ readonly capacitorContent?: string;
15
+ readonly removePackageLock: boolean;
16
+ }
17
+ export declare function createShellUpdatePlan(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, requestedVersion?: string): ShellUpdatePlan;
18
+ export declare function applyShellUpdate(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, plan: ShellUpdatePlan): void;
19
+ export {};
@@ -0,0 +1,228 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { synchronizeShellConfiguration } from './shell-configuration.js';
6
+ import { downloadShell, listShellVersions } from './shell-repository.js';
7
+ import { writeApplicationConfiguration } from './configuration.js';
8
+ const GENERATED_PATHS = new Set([
9
+ 'package.json',
10
+ 'package-lock.json',
11
+ 'capacitor.config.ts',
12
+ 'src/app/application.config.ts',
13
+ 'src/assets/federation.manifest.json',
14
+ ]);
15
+ export function createShellUpdatePlan(applicationRoot, configuration, requestedVersion) {
16
+ if (!configuration.shell) {
17
+ throw new Error('La aplicación no tiene registrada una versión de shell.');
18
+ }
19
+ const targetVersion = requestedVersion ?? listShellVersions()[0];
20
+ if (!targetVersion) {
21
+ throw new Error('No hay versiones estables disponibles para actualizar.');
22
+ }
23
+ if (compareVersions(targetVersion, configuration.shell.version) < 0) {
24
+ throw new Error(`La versión ${targetVersion} es anterior a ${configuration.shell.version}. ` +
25
+ 'mova update no realiza downgrades.');
26
+ }
27
+ const temporaryRoot = mkdtempSync(join(tmpdir(), 'open-mova-update-'));
28
+ try {
29
+ const currentDirectory = join(temporaryRoot, 'current');
30
+ const targetDirectory = join(temporaryRoot, 'target');
31
+ const current = downloadShell(currentDirectory, configuration.shell.version);
32
+ const target = downloadShell(targetDirectory, targetVersion);
33
+ if (current.commit !== configuration.shell.commit) {
34
+ throw new Error(`El commit registrado para ${configuration.shell.version} no coincide con el tag remoto.`);
35
+ }
36
+ const conflicts = [];
37
+ const fileChanges = compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts);
38
+ const packageUpdate = comparePackageConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
39
+ const capacitorUpdate = compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
40
+ const changes = [
41
+ ...fileChanges.map((change) => `${change.content ? 'Actualizar' : 'Eliminar'} ${change.path}`),
42
+ ...(packageUpdate ? ['Actualizar package.json', 'Regenerar package-lock.json'] : []),
43
+ ...(capacitorUpdate ? ['Actualizar capacitor.config.ts'] : []),
44
+ ...(target.version === configuration.shell.version
45
+ ? []
46
+ : [`Registrar shell ${target.version}`]),
47
+ ];
48
+ return {
49
+ currentVersion: configuration.shell.version,
50
+ target,
51
+ changes,
52
+ conflicts,
53
+ fileChanges,
54
+ ...(packageUpdate ? { packageContent: packageUpdate } : {}),
55
+ ...(capacitorUpdate ? { capacitorContent: capacitorUpdate } : {}),
56
+ removePackageLock: packageUpdate !== undefined,
57
+ };
58
+ }
59
+ finally {
60
+ rmSync(temporaryRoot, { recursive: true, force: true });
61
+ }
62
+ }
63
+ export function applyShellUpdate(applicationRoot, configuration, plan) {
64
+ if (plan.conflicts.length > 0) {
65
+ throw new Error('No se puede actualizar mientras existan conflictos.');
66
+ }
67
+ requireCleanGitRepository(applicationRoot);
68
+ for (const change of plan.fileChanges) {
69
+ const destination = join(applicationRoot, change.path);
70
+ if (change.content) {
71
+ mkdirSync(dirname(destination), { recursive: true });
72
+ writeFileSync(destination, change.content);
73
+ }
74
+ else {
75
+ rmSync(destination, { force: true });
76
+ }
77
+ }
78
+ if (plan.packageContent) {
79
+ writeFileSync(join(applicationRoot, 'package.json'), plan.packageContent, 'utf8');
80
+ }
81
+ if (plan.removePackageLock) {
82
+ rmSync(join(applicationRoot, 'package-lock.json'), { force: true });
83
+ }
84
+ if (plan.capacitorContent) {
85
+ writeFileSync(join(applicationRoot, 'capacitor.config.ts'), plan.capacitorContent, 'utf8');
86
+ }
87
+ const updatedConfiguration = {
88
+ ...configuration,
89
+ shell: plan.target,
90
+ };
91
+ writeApplicationConfiguration(applicationRoot, updatedConfiguration);
92
+ synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
93
+ }
94
+ function compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts) {
95
+ const paths = new Set([
96
+ ...listFiles(currentDirectory),
97
+ ...listFiles(targetDirectory),
98
+ ]);
99
+ const changes = [];
100
+ for (const path of [...paths].sort()) {
101
+ if (GENERATED_PATHS.has(path))
102
+ continue;
103
+ const currentContent = readOptionalFile(join(currentDirectory, path));
104
+ const targetContent = readOptionalFile(join(targetDirectory, path));
105
+ const applicationContent = readOptionalFile(join(applicationRoot, path));
106
+ if (buffersEqual(currentContent, targetContent))
107
+ continue;
108
+ if (buffersEqual(applicationContent, currentContent)) {
109
+ changes.push({ path, ...(targetContent ? { content: targetContent } : {}) });
110
+ continue;
111
+ }
112
+ if (buffersEqual(applicationContent, targetContent))
113
+ continue;
114
+ conflicts.push(path);
115
+ }
116
+ return changes;
117
+ }
118
+ function comparePackageConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts) {
119
+ const application = readJson(join(applicationRoot, 'package.json'));
120
+ const current = readJson(join(currentDirectory, 'package.json'));
121
+ const target = readJson(join(targetDirectory, 'package.json'));
122
+ let changed = false;
123
+ for (const section of ['scripts', 'dependencies', 'devDependencies']) {
124
+ const applicationSection = readStringMap(application[section]);
125
+ const currentSection = readStringMap(current[section]);
126
+ const targetSection = readStringMap(target[section]);
127
+ const keys = new Set([...Object.keys(currentSection), ...Object.keys(targetSection)]);
128
+ for (const key of keys) {
129
+ if (currentSection[key] === targetSection[key])
130
+ continue;
131
+ if (applicationSection[key] === currentSection[key]) {
132
+ if (targetSection[key] === undefined) {
133
+ delete applicationSection[key];
134
+ }
135
+ else {
136
+ applicationSection[key] = targetSection[key];
137
+ }
138
+ changed = true;
139
+ }
140
+ else if (applicationSection[key] !== targetSection[key]) {
141
+ conflicts.push(`package.json#${section}.${key}`);
142
+ }
143
+ }
144
+ application[section] = applicationSection;
145
+ }
146
+ return changed ? `${JSON.stringify(application, null, 2)}\n` : undefined;
147
+ }
148
+ function compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts) {
149
+ const applicationPath = join(applicationRoot, 'capacitor.config.ts');
150
+ const currentPath = join(currentDirectory, 'capacitor.config.ts');
151
+ const targetPath = join(targetDirectory, 'capacitor.config.ts');
152
+ if (!existsSync(applicationPath) || !existsSync(currentPath) || !existsSync(targetPath)) {
153
+ return undefined;
154
+ }
155
+ const application = readFileSync(applicationPath, 'utf8');
156
+ const identity = readCapacitorIdentity(application);
157
+ const current = applyCapacitorIdentity(readFileSync(currentPath, 'utf8'), identity);
158
+ const target = applyCapacitorIdentity(readFileSync(targetPath, 'utf8'), identity);
159
+ if (current === target || application === target)
160
+ return undefined;
161
+ if (application === current)
162
+ return target;
163
+ conflicts.push('capacitor.config.ts');
164
+ return undefined;
165
+ }
166
+ function requireCleanGitRepository(applicationRoot) {
167
+ const repository = spawnSync('git', ['-C', applicationRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
168
+ if (repository.status !== 0) {
169
+ throw new Error('Inicializa Git y crea un commit antes de ejecutar mova update.');
170
+ }
171
+ const status = spawnSync('git', ['-C', applicationRoot, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8' });
172
+ if (status.status !== 0 || status.stdout.trim() !== '') {
173
+ throw new Error('El repositorio debe estar limpio antes de ejecutar mova update.');
174
+ }
175
+ }
176
+ function listFiles(directory) {
177
+ const files = [];
178
+ const visit = (currentDirectory) => {
179
+ for (const entry of readdirSync(currentDirectory, { withFileTypes: true })) {
180
+ const absolutePath = join(currentDirectory, entry.name);
181
+ if (entry.isDirectory()) {
182
+ visit(absolutePath);
183
+ }
184
+ else if (entry.isFile()) {
185
+ files.push(relative(directory, absolutePath));
186
+ }
187
+ }
188
+ };
189
+ visit(directory);
190
+ return files;
191
+ }
192
+ function readOptionalFile(path) {
193
+ return existsSync(path) ? readFileSync(path) : undefined;
194
+ }
195
+ function buffersEqual(first, second) {
196
+ return first === undefined ? second === undefined : second !== undefined && first.equals(second);
197
+ }
198
+ function readJson(path) {
199
+ return JSON.parse(readFileSync(path, 'utf8'));
200
+ }
201
+ function readStringMap(value) {
202
+ if (!value || typeof value !== 'object' || Array.isArray(value))
203
+ return {};
204
+ return { ...value };
205
+ }
206
+ function readCapacitorIdentity(content) {
207
+ const appId = content.match(/\bappId:\s*['"]([^'"]+)['"]/)?.[1];
208
+ const appName = content.match(/\bappName:\s*['"]([^'"]+)['"]/)?.[1];
209
+ if (!appId || !appName) {
210
+ throw new Error('No se puede leer appId o appName de capacitor.config.ts.');
211
+ }
212
+ return { appId, appName };
213
+ }
214
+ function applyCapacitorIdentity(content, identity) {
215
+ return content
216
+ .replace(/(\bappId:\s*)['"][^'"]+['"]/, `$1'${identity.appId}'`)
217
+ .replace(/(\bappName:\s*)['"][^'"]+['"]/, `$1'${identity.appName}'`);
218
+ }
219
+ function compareVersions(first, second) {
220
+ const firstParts = first.slice(1).split('.').map(Number);
221
+ const secondParts = second.slice(1).split('.').map(Number);
222
+ for (let index = 0; index < 3; index += 1) {
223
+ const difference = (firstParts[index] ?? 0) - (secondParts[index] ?? 0);
224
+ if (difference !== 0)
225
+ return difference;
226
+ }
227
+ return 0;
228
+ }
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { registerInfoCommand } from './commands/info.js';
8
8
  import { registerMicrofrontendCommands } from './commands/microfrontend.js';
9
9
  import { registerStartCommand } from './commands/start.js';
10
10
  import { registerShellCommands } from './commands/shell.js';
11
+ import { registerUpdateCommand } from './commands/update.js';
11
12
  const program = new Command();
12
13
  const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
13
14
  program
@@ -21,6 +22,7 @@ registerShellCommands(program);
21
22
  registerBuildCommand(program);
22
23
  registerCapacitorCommands(program);
23
24
  registerInfoCommand(program);
25
+ registerUpdateCommand(program);
24
26
  try {
25
27
  await program.parseAsync();
26
28
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerUpdateCommand(program: Command): void;
@@ -0,0 +1,44 @@
1
+ import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
2
+ import { applyShellUpdate, createShellUpdatePlan, } from '../application/shell-update.js';
3
+ export function registerUpdateCommand(program) {
4
+ program
5
+ .command('update')
6
+ .description('Actualiza la infraestructura de Open Mova de una aplicación')
7
+ .option('--check', 'mostrar los cambios sin modificar archivos')
8
+ .option('--to <tag>', 'versión de destino, por ejemplo v0.2.0')
9
+ .action((options) => {
10
+ const applicationRoot = requireApplicationRoot(process.cwd());
11
+ const configuration = readApplicationConfiguration(applicationRoot);
12
+ const plan = createShellUpdatePlan(applicationRoot, configuration, options.to);
13
+ printPlan(plan);
14
+ if (options.check)
15
+ return;
16
+ if (plan.conflicts.length > 0) {
17
+ throw new Error('Se han detectado archivos modificados. Resuelve los conflictos antes de actualizar.');
18
+ }
19
+ if (plan.changes.length === 0) {
20
+ console.log('La aplicación ya está actualizada.');
21
+ return;
22
+ }
23
+ applyShellUpdate(applicationRoot, configuration, plan);
24
+ console.log(`Aplicación actualizada a ${plan.target.version}.`);
25
+ if (plan.removePackageLock) {
26
+ console.log('Ejecuta npm install para instalar dependencias y regenerar package-lock.json.');
27
+ }
28
+ console.log('Después ejecuta mova build para verificar la aplicación.');
29
+ });
30
+ }
31
+ function printPlan(plan) {
32
+ console.log(`Shell actual: ${plan.currentVersion}`);
33
+ console.log(`Shell destino: ${plan.target.version}`);
34
+ if (plan.changes.length > 0) {
35
+ console.log('Cambios:');
36
+ for (const change of plan.changes)
37
+ console.log(`- ${change}`);
38
+ }
39
+ if (plan.conflicts.length > 0) {
40
+ console.log('Conflictos que no se sobrescribirán:');
41
+ for (const conflict of plan.conflicts)
42
+ console.log(`- ${conflict}`);
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mova/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "CLI para crear y mantener aplicaciones Open Mova",
5
5
  "license": "MIT",
6
6
  "author": "Open Mova contributors",