native-update 1.0.9 → 1.1.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 CHANGED
@@ -41,6 +41,7 @@
41
41
  - **[App Update API](./docs/api/app-update-api.md)** - Native app update methods
42
42
  - **[App Review API](./docs/api/app-review-api.md)** - Review request methods
43
43
  - **[Events API](./docs/api/events-api.md)** - Event listeners and handlers
44
+ - **[CLI Reference](./docs/cli-reference.md)** - Command-line tools documentation
44
45
 
45
46
  ### Examples
46
47
 
@@ -316,28 +317,45 @@ The **[example-app](./example-app)** directory contains a complete, production-r
316
317
 
317
318
  We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
318
319
 
319
- ## 🛠️ New Development Tools
320
+ ## 🛠️ CLI Tools & Utilities
320
321
 
321
- ### Available Tools
322
+ ### Zero-Install CLI Access
322
323
 
323
- **Testing Framework**
324
- - Vitest test setup with example tests
325
- - Run tests: `npm test`
326
- - Coverage: `npm run test:coverage`
324
+ All tools are available via `npx` without cloning the repo:
327
325
 
328
- ✅ **Bundle Creation Tool**
329
- - Create update bundles: `node tools/bundle-creator.js create ./dist`
330
- - Generates ZIP bundle with manifest
326
+ ```bash
327
+ # Quick start
328
+ npx native-update init --example
329
+ npx native-update backend create express --with-admin
330
+ ```
331
+
332
+ ### Available Commands
333
+
334
+ ✅ **Bundle Management**
335
+ - Create bundles: `npx native-update bundle create ./www`
336
+ - Sign bundles: `npx native-update bundle sign bundle.zip --key private.pem`
337
+ - Verify signatures: `npx native-update bundle verify bundle.zip --key public.pem`
338
+
339
+ ✅ **Key Management**
340
+ - Generate keys: `npx native-update keys generate --type rsa --size 4096`
341
+ - Supports RSA (2048/4096) and EC (256/384) keys
342
+ - Creates timestamped key pairs with proper permissions
343
+ - See [Key Management Guide](./docs/guides/key-management.md) for detailed instructions
344
+
345
+ ✅ **Backend Templates**
346
+ - Express.js: `npx native-update backend create express --with-admin`
347
+ - Firebase: `npx native-update backend create firebase --with-monitoring`
348
+ - Vercel: `npx native-update backend create vercel`
349
+
350
+ ✅ **Development Tools**
351
+ - Start dev server: `npx native-update server start --port 3000`
352
+ - Monitor updates: `npx native-update monitor --server https://your-server.com`
353
+ - Validate config: `npx native-update config check`
331
354
 
332
- ✅ **Security Signing Tool**
333
- - Generate keys: `node tools/bundle-signer.js generate-keys`
334
- - Sign bundles: `node tools/bundle-signer.js sign bundle.zip private-key.pem`
335
- - Verify: `node tools/bundle-signer.js verify bundle.zip bundle.zip.sig public-key.pem`
355
+ ✅ **Migration Tools**
356
+ - From CodePush: `npx native-update migrate --from codepush`
336
357
 
337
- **Minimal Backend Server**
338
- - Development server in `backend-template/`
339
- - Start: `cd backend-template && npm install && npm start`
340
- - Provides basic update API endpoints
358
+ See [CLI Reference](./docs/cli-reference.md) for complete documentation.
341
359
 
342
360
  ## 🏗️ Development Status
343
361
 
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+
7
+ const program = new Command();
8
+ const packageJson = JSON.parse(
9
+ readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')
10
+ );
11
+
12
+ program
13
+ .name('cap-update')
14
+ .description('CLI for Capacitor Native Update')
15
+ .version('1.0.0');
16
+
17
+ program
18
+ .command('init')
19
+ .description('Initialize update configuration')
20
+ .option('-s, --server <url>', 'Update server URL')
21
+ .action((options) => {
22
+ console.log('Initializing Capacitor Native Update...');
23
+ console.log('Server:', options.server || 'Not specified');
24
+ // TODO: Create config file
25
+ });
26
+
27
+ program
28
+ .command('bundle')
29
+ .description('Create update bundle')
30
+ .argument('<path>', 'Path to dist directory')
31
+ .action((path) => {
32
+ console.log(`Creating bundle from: ${path}`);
33
+ // TODO: Call bundle creator
34
+ });
35
+
36
+ program
37
+ .command('sign')
38
+ .description('Sign update bundle')
39
+ .argument('<bundle>', 'Bundle file path')
40
+ .action((bundle) => {
41
+ console.log(`Signing bundle: ${bundle}`);
42
+ // TODO: Call bundle signer
43
+ });
44
+
45
+ program.parse();
@@ -0,0 +1,582 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import ora from 'ora';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export async function createBackend(type, options) {
12
+ const spinner = ora(`Creating ${type} backend template...`).start();
13
+
14
+ try {
15
+ const outputDir = path.resolve(options.output);
16
+
17
+ // Check if directory exists
18
+ try {
19
+ await fs.access(outputDir);
20
+ spinner.fail(`Directory ${outputDir} already exists`);
21
+ return;
22
+ } catch {
23
+ // Directory doesn't exist, good
24
+ }
25
+
26
+ // Create output directory
27
+ await fs.mkdir(outputDir, { recursive: true });
28
+
29
+ switch (type) {
30
+ case 'express':
31
+ await createExpressBackend(outputDir, options);
32
+ break;
33
+ case 'firebase':
34
+ await createFirebaseBackend(outputDir, options);
35
+ break;
36
+ case 'vercel':
37
+ await createVercelBackend(outputDir, options);
38
+ break;
39
+ default:
40
+ throw new Error(`Unknown backend type: ${type}`);
41
+ }
42
+
43
+ spinner.succeed(`${type} backend created successfully!`);
44
+
45
+ console.log('');
46
+ console.log(chalk.bold('Next steps:'));
47
+ console.log(chalk.gray(` 1. cd ${options.output}`));
48
+ console.log(chalk.gray(` 2. npm install`));
49
+ console.log(chalk.gray(` 3. Configure your environment variables`));
50
+ console.log(chalk.gray(` 4. npm run dev`));
51
+
52
+ } catch (error) {
53
+ spinner.fail(`Failed to create backend: ${error.message}`);
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ async function createExpressBackend(outputDir, options) {
59
+ // Create package.json
60
+ const packageJson = {
61
+ name: "native-update-backend",
62
+ version: "1.0.0",
63
+ description: "Native Update backend server",
64
+ type: "module",
65
+ scripts: {
66
+ dev: "node --watch server.js",
67
+ start: "node server.js",
68
+ test: "vitest"
69
+ },
70
+ dependencies: {
71
+ express: "^5.1.0",
72
+ cors: "^2.8.5",
73
+ "express-rate-limit": "^7.4.1",
74
+ multer: "^1.4.5-lts.1",
75
+ dotenv: "^16.4.7",
76
+ jsonwebtoken: "^9.0.2",
77
+ bcrypt: "^5.1.1"
78
+ }
79
+ };
80
+
81
+ if (options.withMonitoring) {
82
+ packageJson.dependencies["@opentelemetry/api"] = "^1.9.0";
83
+ packageJson.dependencies["@opentelemetry/sdk-node"] = "^0.57.0";
84
+ packageJson.dependencies["winston"] = "^3.17.1";
85
+ }
86
+
87
+ await fs.writeFile(
88
+ path.join(outputDir, 'package.json'),
89
+ JSON.stringify(packageJson, null, 2)
90
+ );
91
+
92
+ // Create server.js
93
+ const serverCode = `import express from 'express';
94
+ import cors from 'cors';
95
+ import dotenv from 'dotenv';
96
+ import { router as bundleRouter } from './routes/bundles.js';
97
+ import { router as authRouter } from './routes/auth.js';
98
+ ${options.withMonitoring ? "import { initMonitoring } from './monitoring/index.js';" : ''}
99
+
100
+ dotenv.config();
101
+
102
+ const app = express();
103
+ const PORT = process.env.PORT || 3000;
104
+
105
+ ${options.withMonitoring ? 'initMonitoring(app);' : ''}
106
+
107
+ app.use(cors());
108
+ app.use(express.json());
109
+
110
+ // Routes
111
+ app.use('/api/bundles', bundleRouter);
112
+ app.use('/api/auth', authRouter);
113
+
114
+ // Health check
115
+ app.get('/health', (req, res) => {
116
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
117
+ });
118
+
119
+ app.listen(PORT, () => {
120
+ console.log(\`Server running on port \${PORT}\`);
121
+ });
122
+ `;
123
+
124
+ await fs.writeFile(path.join(outputDir, 'server.js'), serverCode);
125
+
126
+ // Create routes directory
127
+ await fs.mkdir(path.join(outputDir, 'routes'), { recursive: true });
128
+
129
+ // Create bundles route
130
+ const bundlesRoute = `import express from 'express';
131
+ import multer from 'multer';
132
+ import path from 'path';
133
+ import fs from 'fs/promises';
134
+ import crypto from 'crypto';
135
+
136
+ const router = express.Router();
137
+ const upload = multer({ dest: 'uploads/' });
138
+
139
+ // Get latest bundle
140
+ router.get('/latest', async (req, res) => {
141
+ try {
142
+ const { appId, version, channel = 'production' } = req.query;
143
+
144
+ // TODO: Implement bundle lookup logic
145
+ // This is a simplified example
146
+ const bundle = {
147
+ version: '1.0.1',
148
+ url: \`\${req.protocol}://\${req.get('host')}/bundles/latest.zip\`,
149
+ checksum: 'sha256:...',
150
+ signature: 'signature...',
151
+ metadata: {
152
+ releaseNotes: 'Bug fixes and improvements'
153
+ }
154
+ };
155
+
156
+ res.json(bundle);
157
+ } catch (error) {
158
+ res.status(500).json({ error: error.message });
159
+ }
160
+ });
161
+
162
+ // Upload new bundle
163
+ router.post('/upload', upload.single('bundle'), async (req, res) => {
164
+ try {
165
+ const { version, channel, metadata } = req.body;
166
+ const file = req.file;
167
+
168
+ if (!file) {
169
+ return res.status(400).json({ error: 'No bundle file provided' });
170
+ }
171
+
172
+ // Calculate checksum
173
+ const fileBuffer = await fs.readFile(file.path);
174
+ const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
175
+
176
+ // TODO: Store bundle metadata in database
177
+ // TODO: Move file to permanent storage
178
+
179
+ res.json({
180
+ success: true,
181
+ bundle: {
182
+ version,
183
+ channel,
184
+ checksum,
185
+ size: file.size
186
+ }
187
+ });
188
+ } catch (error) {
189
+ res.status(500).json({ error: error.message });
190
+ }
191
+ });
192
+
193
+ export { router };
194
+ `;
195
+
196
+ await fs.writeFile(path.join(outputDir, 'routes', 'bundles.js'), bundlesRoute);
197
+
198
+ // Create .env.example
199
+ const envExample = `# Server Configuration
200
+ PORT=3000
201
+
202
+ # Security
203
+ JWT_SECRET=your-secret-key-here
204
+ API_KEY=your-api-key-here
205
+
206
+ # Storage
207
+ STORAGE_PATH=./storage/bundles
208
+
209
+ # Database (optional)
210
+ # DATABASE_URL=postgresql://user:password@localhost:5432/native_update
211
+ `;
212
+
213
+ await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
214
+
215
+ // Create README
216
+ const readme = `# Native Update Backend
217
+
218
+ Express.js backend for Native Update plugin.
219
+
220
+ ## Setup
221
+
222
+ 1. Copy \`.env.example\` to \`.env\` and configure
223
+ 2. Run \`npm install\`
224
+ 3. Run \`npm run dev\` for development
225
+
226
+ ## API Endpoints
227
+
228
+ - GET /api/bundles/latest - Get latest bundle for app
229
+ - POST /api/bundles/upload - Upload new bundle
230
+ - GET /health - Health check
231
+
232
+ ## Security
233
+
234
+ - Uses JWT for authentication
235
+ - Rate limiting enabled
236
+ - CORS configured
237
+
238
+ ${options.withMonitoring ? '## Monitoring\n\nOpenTelemetry monitoring is configured. Set up your preferred backend (Jaeger, Zipkin, etc.)' : ''}
239
+ `;
240
+
241
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
242
+ }
243
+
244
+ async function createVercelBackend(outputDir, options) {
245
+ // Create api directory
246
+ const apiDir = path.join(outputDir, 'api');
247
+ await fs.mkdir(apiDir, { recursive: true });
248
+
249
+ // Create package.json
250
+ const packageJson = {
251
+ name: "native-update-vercel",
252
+ version: "1.0.0",
253
+ description: "Native Update Vercel backend",
254
+ type: "module",
255
+ scripts: {
256
+ dev: "vercel dev",
257
+ deploy: "vercel",
258
+ build: "echo 'No build step required'"
259
+ },
260
+ dependencies: {
261
+ "@vercel/node": "^3.0.0"
262
+ }
263
+ };
264
+
265
+ await fs.writeFile(
266
+ path.join(outputDir, 'package.json'),
267
+ JSON.stringify(packageJson, null, 2)
268
+ );
269
+
270
+ // Create bundles API endpoint
271
+ const bundlesApi = `export default async function handler(req, res) {
272
+ // Enable CORS
273
+ res.setHeader('Access-Control-Allow-Credentials', true);
274
+ res.setHeader('Access-Control-Allow-Origin', '*');
275
+ res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
276
+ res.setHeader(
277
+ 'Access-Control-Allow-Headers',
278
+ 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
279
+ );
280
+
281
+ if (req.method === 'OPTIONS') {
282
+ res.status(200).end();
283
+ return;
284
+ }
285
+
286
+ if (req.method === 'GET') {
287
+ // Get latest bundle
288
+ const { appId, version, channel = 'production' } = req.query;
289
+
290
+ // TODO: Implement bundle lookup from your storage solution
291
+ // This is a simplified example
292
+ const bundle = {
293
+ version: '1.0.1',
294
+ url: \`https://\${req.headers.host}/bundles/latest.zip\`,
295
+ checksum: 'sha256:...',
296
+ signature: 'signature...',
297
+ metadata: {
298
+ releaseNotes: 'Bug fixes and improvements'
299
+ }
300
+ };
301
+
302
+ return res.status(200).json(bundle);
303
+ }
304
+
305
+ if (req.method === 'POST') {
306
+ // Handle bundle upload
307
+ const { version, channel, metadata } = req.body;
308
+
309
+ // TODO: Handle file upload to storage (Vercel Blob, S3, etc.)
310
+ // TODO: Save metadata to database (Vercel KV, external DB, etc.)
311
+
312
+ return res.status(200).json({
313
+ success: true,
314
+ bundle: {
315
+ version,
316
+ channel,
317
+ message: 'Bundle uploaded successfully'
318
+ }
319
+ });
320
+ }
321
+
322
+ return res.status(405).json({ error: 'Method not allowed' });
323
+ }`;
324
+
325
+ await fs.writeFile(path.join(apiDir, 'bundles.js'), bundlesApi);
326
+
327
+ // Create health check endpoint
328
+ const healthApi = `export default function handler(req, res) {
329
+ res.status(200).json({
330
+ status: 'ok',
331
+ service: 'vercel-edge',
332
+ timestamp: new Date().toISOString()
333
+ });
334
+ }`;
335
+
336
+ await fs.writeFile(path.join(apiDir, 'health.js'), healthApi);
337
+
338
+ // Create vercel.json
339
+ const vercelConfig = {
340
+ functions: {
341
+ "api/*.js": {
342
+ maxDuration: 30
343
+ }
344
+ },
345
+ rewrites: [
346
+ {
347
+ source: "/api/bundles/latest",
348
+ destination: "/api/bundles"
349
+ }
350
+ ]
351
+ };
352
+
353
+ await fs.writeFile(
354
+ path.join(outputDir, 'vercel.json'),
355
+ JSON.stringify(vercelConfig, null, 2)
356
+ );
357
+
358
+ // Create .env.example
359
+ const envExample = `# Storage Configuration
360
+ STORAGE_URL=your-storage-url
361
+ STORAGE_KEY=your-storage-key
362
+
363
+ # Database Configuration (optional)
364
+ DATABASE_URL=your-database-url
365
+
366
+ # Security
367
+ API_KEY=your-api-key-here
368
+ `;
369
+
370
+ await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
371
+
372
+ // Create README
373
+ const readme = `# Native Update Vercel Backend
374
+
375
+ Serverless backend for Native Update plugin using Vercel Edge Functions.
376
+
377
+ ## Setup
378
+
379
+ 1. Install Vercel CLI: \`npm i -g vercel\`
380
+ 2. Copy \`.env.example\` to \`.env.local\` and configure
381
+ 3. Run \`npm install\`
382
+ 4. Run \`vercel dev\` for local development
383
+
384
+ ## Deployment
385
+
386
+ \`\`\`bash
387
+ vercel
388
+ \`\`\`
389
+
390
+ ## API Endpoints
391
+
392
+ - GET /api/bundles - Get latest bundle
393
+ - POST /api/bundles - Upload new bundle
394
+ - GET /api/health - Health check
395
+
396
+ ## Storage Options
397
+
398
+ - Vercel Blob Storage
399
+ - AWS S3
400
+ - Cloudflare R2
401
+ - Any S3-compatible storage
402
+
403
+ ## Database Options
404
+
405
+ - Vercel KV
406
+ - Vercel Postgres
407
+ - PlanetScale
408
+ - Supabase
409
+
410
+ ${options.withMonitoring ? '## Monitoring\\n\\nUse Vercel Analytics and Logs for monitoring.' : ''}
411
+ `;
412
+
413
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
414
+ }
415
+
416
+ async function createFirebaseBackend(outputDir, options) {
417
+ // Create functions directory
418
+ const functionsDir = path.join(outputDir, 'functions');
419
+ await fs.mkdir(functionsDir, { recursive: true });
420
+
421
+ // Create package.json for functions
422
+ const packageJson = {
423
+ name: "native-update-firebase-functions",
424
+ description: "Firebase Functions backend for Native Update",
425
+ type: "module",
426
+ engines: {
427
+ node: "22"
428
+ },
429
+ main: "index.js",
430
+ scripts: {
431
+ serve: "firebase emulators:start --only functions",
432
+ shell: "firebase functions:shell",
433
+ start: "npm run serve",
434
+ deploy: "firebase deploy --only functions",
435
+ logs: "firebase functions:log"
436
+ },
437
+ dependencies: {
438
+ "firebase-admin": "^12.0.0",
439
+ "firebase-functions": "^5.0.0",
440
+ cors: "^2.8.5",
441
+ "express": "^5.1.0"
442
+ }
443
+ };
444
+
445
+ await fs.writeFile(
446
+ path.join(functionsDir, 'package.json'),
447
+ JSON.stringify(packageJson, null, 2)
448
+ );
449
+
450
+ // Create index.js
451
+ const indexJs = `import { onRequest } from 'firebase-functions/v2/https';
452
+ import * as admin from 'firebase-admin';
453
+ import express from 'express';
454
+ import cors from 'cors';
455
+
456
+ admin.initializeApp();
457
+
458
+ const app = express();
459
+ app.use(cors({ origin: true }));
460
+ app.use(express.json());
461
+
462
+ // Get latest bundle
463
+ app.get('/api/bundles/latest', async (req, res) => {
464
+ try {
465
+ const { appId, version, channel = 'production' } = req.query;
466
+
467
+ // Query Firestore for latest bundle
468
+ const db = admin.firestore();
469
+ const snapshot = await db.collection('bundles')
470
+ .where('appId', '==', appId)
471
+ .where('channel', '==', channel)
472
+ .where('enabled', '==', true)
473
+ .orderBy('createdAt', 'desc')
474
+ .limit(1)
475
+ .get();
476
+
477
+ if (snapshot.empty) {
478
+ return res.status(404).json({ error: 'No bundle found' });
479
+ }
480
+
481
+ const bundle = snapshot.docs[0].data();
482
+ res.json(bundle);
483
+ } catch (error) {
484
+ res.status(500).json({ error: error.message });
485
+ }
486
+ });
487
+
488
+ // Upload bundle
489
+ app.post('/api/bundles/upload', async (req, res) => {
490
+ try {
491
+ const { version, channel, metadata } = req.body;
492
+
493
+ // TODO: Handle file upload to Cloud Storage
494
+ // TODO: Save metadata to Firestore
495
+
496
+ res.json({ success: true, version });
497
+ } catch (error) {
498
+ res.status(500).json({ error: error.message });
499
+ }
500
+ });
501
+
502
+ // Health check
503
+ app.get('/health', (req, res) => {
504
+ res.json({ status: 'ok', service: 'firebase-functions' });
505
+ });
506
+
507
+ export const api = onRequest(app);
508
+ `;
509
+
510
+ await fs.writeFile(path.join(functionsDir, 'index.js'), indexJs);
511
+
512
+ // Create firebase.json
513
+ const firebaseConfig = {
514
+ functions: {
515
+ source: "functions",
516
+ runtime: "nodejs22"
517
+ },
518
+ firestore: {
519
+ rules: "firestore.rules",
520
+ indexes: "firestore.indexes.json"
521
+ },
522
+ storage: {
523
+ rules: "storage.rules"
524
+ }
525
+ };
526
+
527
+ await fs.writeFile(
528
+ path.join(outputDir, 'firebase.json'),
529
+ JSON.stringify(firebaseConfig, null, 2)
530
+ );
531
+
532
+ // Create Firestore rules
533
+ const firestoreRules = `rules_version = '2';
534
+ service cloud.firestore {
535
+ match /databases/{database}/documents {
536
+ // Bundles are read-only for clients
537
+ match /bundles/{document=**} {
538
+ allow read: if true;
539
+ allow write: if false;
540
+ }
541
+
542
+ // Analytics can be written by clients
543
+ match /analytics/{document=**} {
544
+ allow create: if true;
545
+ allow read, update, delete: if false;
546
+ }
547
+ }
548
+ }`;
549
+
550
+ await fs.writeFile(path.join(outputDir, 'firestore.rules'), firestoreRules);
551
+
552
+ // Create README
553
+ const readme = `# Native Update Firebase Backend
554
+
555
+ Firebase Functions backend for Native Update plugin.
556
+
557
+ ## Setup
558
+
559
+ 1. Install Firebase CLI: \`npm install -g firebase-tools\`
560
+ 2. Login: \`firebase login\`
561
+ 3. Initialize: \`firebase init\`
562
+ 4. Select your project or create new
563
+ 5. Install dependencies: \`cd functions && npm install\`
564
+ 6. Start emulator: \`npm run serve\`
565
+
566
+ ## Deployment
567
+
568
+ \`\`\`bash
569
+ firebase deploy
570
+ \`\`\`
571
+
572
+ ## Endpoints
573
+
574
+ - GET /api/bundles/latest - Get latest bundle
575
+ - POST /api/bundles/upload - Upload new bundle
576
+ - GET /health - Health check
577
+
578
+ ${options.withMonitoring ? '## Monitoring\n\nUse Firebase Console for monitoring and analytics.' : ''}
579
+ `;
580
+
581
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
582
+ }