@novacraft-engineering/pulsar-cli 1.2.3 → 1.3.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/package.json +2 -2
- package/pulsar.js +54 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novacraft-engineering/pulsar-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Pulsar OTA release CLI — bundle, hash, and ship React Native updates over-the-air.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node test/hash.test.js && node test/security.test.js && node test/signing.test.js"
|
|
29
|
+
"test": "node test/hash.test.js && node test/security.test.js && node test/signing.test.js && node test/entry.test.js"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"cli",
|
package/pulsar.js
CHANGED
|
@@ -6,6 +6,7 @@ import readline from 'node:readline';
|
|
|
6
6
|
import http from 'node:http';
|
|
7
7
|
import {execFileSync} from 'node:child_process';
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
|
+
import {fileURLToPath} from 'node:url';
|
|
9
10
|
import {computePackageHash, fileHashes} from './hash.js';
|
|
10
11
|
|
|
11
12
|
// Code signing: sign the package hash into an RS256 JWT the client verifies against its
|
|
@@ -349,9 +350,35 @@ async function cmdApps() {
|
|
|
349
350
|
}
|
|
350
351
|
}
|
|
351
352
|
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
async function
|
|
353
|
+
// Sign for this app — with a local key when one was given, otherwise by asking Pulsar, which signs for
|
|
354
|
+
// anyone allowed to publish here. Each app can hold a different key, so a release train re-signs.
|
|
355
|
+
async function signForApp(app, deployment, contentsDir, packageHash, localKey) {
|
|
356
|
+
const signaturePath = path.join(contentsDir, SIGNATURE_PATH);
|
|
357
|
+
fs.rmSync(signaturePath, {force: true});
|
|
358
|
+
let signature = null;
|
|
359
|
+
if (localKey) {
|
|
360
|
+
try {
|
|
361
|
+
signature = signCodePushJWT(localKey, packageHash);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
die(`could not sign with the provided key (${error.message}) — nothing was uploaded`);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
({signature} = await api(`/apps/${app.id}/deployments/${deployment}/sign`, {method: 'POST', body: {packageHash}}));
|
|
367
|
+
}
|
|
368
|
+
if (!signature) {
|
|
369
|
+
console.log(`• ${app.name} has no signing key — uploading unsigned`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
fs.mkdirSync(path.dirname(signaturePath), {recursive: true});
|
|
373
|
+
fs.writeFileSync(signaturePath, signature);
|
|
374
|
+
console.log(`• signed${localKey ? ' with your local key' : ' by Pulsar'}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Sign and push an already-built bundle to one app's deployment, then generate and upload a
|
|
378
|
+
// differential patch against whatever that deployment was serving.
|
|
379
|
+
async function pushBuiltRelease(app, deployment, args, contentsDir, packageHash, localKey) {
|
|
380
|
+
await signForApp(app, deployment, contentsDir, packageHash, localKey);
|
|
381
|
+
const zip = fs.readFileSync(zipContents(contentsDir));
|
|
355
382
|
let prevLatest = null;
|
|
356
383
|
if (!args.includes('--no-diff')) {
|
|
357
384
|
try {
|
|
@@ -392,6 +419,9 @@ async function pushBuiltRelease(app, deployment, args, contentsDir, packageHash,
|
|
|
392
419
|
].filter(Boolean);
|
|
393
420
|
const reach = scopes.length ? scopes.join(' + ') : `${release.rollout}% of devices`;
|
|
394
421
|
console.log(`✓ released ${release.label} to ${app.name}/${deployment} — ${release.isDisabled ? 'DISABLED' : reach}`);
|
|
422
|
+
if (release.signed === false) {
|
|
423
|
+
console.log('⚠ UNSIGNED — this build is blocked and will not reach devices. An org owner can enable it from the dashboard.');
|
|
424
|
+
}
|
|
395
425
|
if (release.startsAt) console.log(` scheduled — goes live ${new Date(release.startsAt).toLocaleString()}`);
|
|
396
426
|
if (release.rolloutRamp) console.log(` gradual rollout — ${release.rollout}% to ${release.rolloutRamp.to}% over ${release.rolloutRamp.hours}h`);
|
|
397
427
|
|
|
@@ -417,31 +447,18 @@ async function pushBuiltRelease(app, deployment, args, contentsDir, packageHash,
|
|
|
417
447
|
return release;
|
|
418
448
|
}
|
|
419
449
|
|
|
420
|
-
// Build the JS bundle once
|
|
450
|
+
// Build the JS bundle once and return what every target app's release is made from. A local key
|
|
451
|
+
// (--private-key <file>, or PULSAR_SIGNING_KEY holding the key itself) overrides Pulsar's signing.
|
|
421
452
|
function buildOnce(args) {
|
|
422
453
|
const platform = getFlag(args, 'platform', 'ios');
|
|
454
|
+
const privateKeyPath = getFlag(args, 'private-key');
|
|
455
|
+
if (privateKeyPath && !fs.existsSync(privateKeyPath)) die(`no signing key at ${privateKeyPath} — drop --private-key to have Pulsar sign`);
|
|
456
|
+
const localKey = privateKeyPath ? fs.readFileSync(privateKeyPath, 'utf8') : (process.env.PULSAR_SIGNING_KEY || '').trim();
|
|
423
457
|
const contentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsar-build-'));
|
|
424
458
|
bundle(platform, contentsDir);
|
|
425
459
|
const packageHash = computePackageHash(contentsDir);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// "don't sign", exactly as before.
|
|
429
|
-
const privateKeyPath = getFlag(args, 'private-key');
|
|
430
|
-
const signingKey = privateKeyPath ? fs.readFileSync(privateKeyPath, 'utf8') : (process.env.PULSAR_SIGNING_KEY || '').trim();
|
|
431
|
-
if (signingKey) {
|
|
432
|
-
let signature;
|
|
433
|
-
try {
|
|
434
|
-
signature = signCodePushJWT(signingKey, packageHash);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
die(`could not sign with the provided key (${error.message}) — nothing was uploaded`);
|
|
437
|
-
}
|
|
438
|
-
fs.mkdirSync(path.join(contentsDir, 'CodePush'), {recursive: true});
|
|
439
|
-
fs.writeFileSync(path.join(contentsDir, SIGNATURE_PATH), signature);
|
|
440
|
-
console.log('• signed release — devices with CodePushPublicKey set will verify it');
|
|
441
|
-
}
|
|
442
|
-
const zip = fs.readFileSync(zipContents(contentsDir));
|
|
443
|
-
console.log(`• hash ${packageHash.slice(0, 12)}… size ${(zip.length / 1e6).toFixed(2)} MB`);
|
|
444
|
-
return {contentsDir, packageHash, zip};
|
|
460
|
+
console.log(`• hash ${packageHash.slice(0, 12)}…`);
|
|
461
|
+
return {contentsDir, packageHash, localKey};
|
|
445
462
|
}
|
|
446
463
|
|
|
447
464
|
async function cmdRelease(args) {
|
|
@@ -452,8 +469,8 @@ async function cmdRelease(args) {
|
|
|
452
469
|
if (app.os !== getFlag(args, 'platform', 'ios')) {
|
|
453
470
|
console.log(`• note: resolved app "${app.name}" is ${app.os}; bundling ${getFlag(args, 'platform', 'ios')}`);
|
|
454
471
|
}
|
|
455
|
-
const {contentsDir, packageHash,
|
|
456
|
-
await pushBuiltRelease(app, deployment, args, contentsDir, packageHash,
|
|
472
|
+
const {contentsDir, packageHash, localKey} = buildOnce(args);
|
|
473
|
+
await pushBuiltRelease(app, deployment, args, contentsDir, packageHash, localKey);
|
|
457
474
|
}
|
|
458
475
|
|
|
459
476
|
// Release the same built bundle to several apps at once (shared-codebase / white-label fleets).
|
|
@@ -478,11 +495,11 @@ async function cmdReleaseTrain(args) {
|
|
|
478
495
|
apps = apps.filter((app) => app.os === platform);
|
|
479
496
|
if (!apps.length) die(`No ${platform} apps matched.`);
|
|
480
497
|
|
|
481
|
-
const {contentsDir, packageHash,
|
|
498
|
+
const {contentsDir, packageHash, localKey} = buildOnce(args);
|
|
482
499
|
console.log(`• release train → ${apps.length} app(s): ${apps.map((app) => app.name).join(', ')}`);
|
|
483
500
|
for (const app of apps) {
|
|
484
501
|
try {
|
|
485
|
-
await pushBuiltRelease(app, deployment, args, contentsDir, packageHash,
|
|
502
|
+
await pushBuiltRelease(app, deployment, args, contentsDir, packageHash, localKey);
|
|
486
503
|
} catch (err) {
|
|
487
504
|
console.log(`✗ ${app.name}: ${err.message}`);
|
|
488
505
|
}
|
|
@@ -614,7 +631,16 @@ async function cmdKeys(args) {
|
|
|
614
631
|
export {saveConfig, browserLogin, signCodePushJWT, SIGNATURE_PATH};
|
|
615
632
|
|
|
616
633
|
// Only dispatch when run as the CLI — importing the module (e.g. from tests) must not execute a command.
|
|
617
|
-
|
|
634
|
+
// Compare real paths: a global install runs through a bin symlink, so argv[1] is not this file's path.
|
|
635
|
+
function invokedAsCli() {
|
|
636
|
+
try {
|
|
637
|
+
return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
638
|
+
} catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (invokedAsCli()) {
|
|
618
644
|
const [command, ...rest] = process.argv.slice(2);
|
|
619
645
|
const commands = {
|
|
620
646
|
login: cmdLogin,
|