@tutti-os/app-release-tools 0.0.16 → 0.0.17
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
|
@@ -12,7 +12,11 @@ verify-tutti-app-release-artifacts --release-file ./apps/vibe-design/latest.json
|
|
|
12
12
|
verify-tutti-app-release-artifacts --catalog-file ./catalog.json --release-file ./apps/vibe-design/latest.json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The release command validates a complete Tutti app package, creates a zip,
|
|
15
|
+
The release command validates a complete Tutti app package, creates a zip,
|
|
16
|
+
writes immutable `release.json`, and writes mutable `latest.json`. When the
|
|
17
|
+
manifest declares `localizationInfo`, the release metadata includes the
|
|
18
|
+
referenced manifest locale files so App Center can localize uninstalled remote
|
|
19
|
+
apps without downloading the package.
|
|
16
20
|
|
|
17
21
|
The catalog command merges one or more release files into `tutti.app.catalog.v1`.
|
|
18
22
|
Pass `--existing-catalog` to preserve existing catalog apps and update only the
|
|
@@ -51,6 +51,7 @@ export async function buildTuttiAppRelease(options) {
|
|
|
51
51
|
for (const locale of manifest.localizationInfo?.additionalLocales ?? []) {
|
|
52
52
|
await requireNonEmptyTextFile(path.join(packageDir, locale.file));
|
|
53
53
|
}
|
|
54
|
+
const localizations = await readManifestLocalizations(packageDir, manifest);
|
|
54
55
|
if (manifest.cli?.manifest) {
|
|
55
56
|
const cliManifestPath = path.join(packageDir, manifest.cli.manifest);
|
|
56
57
|
await requireFile(cliManifestPath);
|
|
@@ -85,6 +86,7 @@ export async function buildTuttiAppRelease(options) {
|
|
|
85
86
|
name: manifest.name,
|
|
86
87
|
description: manifest.description,
|
|
87
88
|
manifest,
|
|
89
|
+
...(localizations.length > 0 ? { localizations } : {}),
|
|
88
90
|
artifactUrl: `${baseUrl}/${releaseURLPrefix}/${encodeURLPathSegment(artifactName)}`,
|
|
89
91
|
artifactSha256: artifact.sha256,
|
|
90
92
|
artifactSizeBytes: artifact.size,
|
|
@@ -410,6 +412,9 @@ function validateLocalizationInfo(localizationInfo, sourceLabel) {
|
|
|
410
412
|
export function releaseToCatalogApp(release) {
|
|
411
413
|
validateRelease(release);
|
|
412
414
|
return {
|
|
415
|
+
...(Array.isArray(release.localizations) && release.localizations.length > 0
|
|
416
|
+
? { localizations: release.localizations }
|
|
417
|
+
: {}),
|
|
413
418
|
manifest: release.manifest,
|
|
414
419
|
distribution: {
|
|
415
420
|
kind: "remote",
|
|
@@ -443,6 +448,7 @@ export function validateRelease(release) {
|
|
|
443
448
|
if (release.manifest.version !== release.version) {
|
|
444
449
|
throw new Error("release manifest.version must match release version");
|
|
445
450
|
}
|
|
451
|
+
validateReleaseLocalizations(release.localizations, "release.localizations");
|
|
446
452
|
if (!/^[a-f0-9]{64}$/i.test(release.artifactSha256)) {
|
|
447
453
|
throw new Error("release artifactSha256 must be a sha256 hex digest");
|
|
448
454
|
}
|
|
@@ -454,6 +460,110 @@ export function validateRelease(release) {
|
|
|
454
460
|
}
|
|
455
461
|
}
|
|
456
462
|
|
|
463
|
+
async function readManifestLocalizations(packageDir, manifest) {
|
|
464
|
+
const localizations = [];
|
|
465
|
+
for (const entry of manifest.localizationInfo?.additionalLocales ?? []) {
|
|
466
|
+
const locale = requireNonEmpty(
|
|
467
|
+
entry.locale,
|
|
468
|
+
"manifest localization locale"
|
|
469
|
+
);
|
|
470
|
+
const filePath = path.join(packageDir, entry.file);
|
|
471
|
+
const document = await readLocalizationDocument(filePath);
|
|
472
|
+
const localization = normalizeLocalization(document, locale, filePath);
|
|
473
|
+
if (localization) {
|
|
474
|
+
localizations.push(localization);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return localizations;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function normalizeLocalization(document, locale, sourceLabel) {
|
|
481
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
482
|
+
throw new Error(`${sourceLabel} must be a localization object`);
|
|
483
|
+
}
|
|
484
|
+
const localization = {
|
|
485
|
+
locale
|
|
486
|
+
};
|
|
487
|
+
if (document.name !== undefined) {
|
|
488
|
+
const name = requireOptionalString(document.name, `${sourceLabel}.name`);
|
|
489
|
+
if (name) {
|
|
490
|
+
localization.name = name;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (document.description !== undefined) {
|
|
494
|
+
const description = requireOptionalString(
|
|
495
|
+
document.description,
|
|
496
|
+
`${sourceLabel}.description`
|
|
497
|
+
);
|
|
498
|
+
if (description) {
|
|
499
|
+
localization.description = description;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (document.tags !== undefined) {
|
|
503
|
+
if (!Array.isArray(document.tags)) {
|
|
504
|
+
throw new Error(`${sourceLabel}.tags must be an array`);
|
|
505
|
+
}
|
|
506
|
+
const tags = document.tags
|
|
507
|
+
.map((tag, index) =>
|
|
508
|
+
requireOptionalString(tag, `${sourceLabel}.tags[${index}]`)
|
|
509
|
+
)
|
|
510
|
+
.filter(Boolean);
|
|
511
|
+
if (tags.length > 0) {
|
|
512
|
+
localization.tags = [...new Set(tags)];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (!localization.name && !localization.description && !localization.tags) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return localization;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function readLocalizationDocument(filePath) {
|
|
522
|
+
try {
|
|
523
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`parse manifest localization file ${filePath}: ${error.message}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function validateReleaseLocalizations(localizations, sourceLabel) {
|
|
532
|
+
if (localizations === undefined) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (!Array.isArray(localizations)) {
|
|
536
|
+
throw new Error(`${sourceLabel} must be an array`);
|
|
537
|
+
}
|
|
538
|
+
const seenLocales = new Set();
|
|
539
|
+
for (const [index, localization] of localizations.entries()) {
|
|
540
|
+
const label = `${sourceLabel}[${index}]`;
|
|
541
|
+
if (!localization || typeof localization !== "object") {
|
|
542
|
+
throw new Error(`${label} must be an object`);
|
|
543
|
+
}
|
|
544
|
+
const locale = requireNonEmpty(localization.locale, `${label}.locale`);
|
|
545
|
+
const localeKey = locale.toLowerCase();
|
|
546
|
+
if (seenLocales.has(localeKey)) {
|
|
547
|
+
throw new Error(`${label}.locale must be unique`);
|
|
548
|
+
}
|
|
549
|
+
seenLocales.add(localeKey);
|
|
550
|
+
if (localization.name !== undefined) {
|
|
551
|
+
requireOptionalString(localization.name, `${label}.name`);
|
|
552
|
+
}
|
|
553
|
+
if (localization.description !== undefined) {
|
|
554
|
+
requireOptionalString(localization.description, `${label}.description`);
|
|
555
|
+
}
|
|
556
|
+
if (localization.tags !== undefined) {
|
|
557
|
+
if (!Array.isArray(localization.tags)) {
|
|
558
|
+
throw new Error(`${label}.tags must be an array`);
|
|
559
|
+
}
|
|
560
|
+
for (const [tagIndex, tag] of localization.tags.entries()) {
|
|
561
|
+
requireOptionalString(tag, `${label}.tags[${tagIndex}]`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
457
567
|
function createZip(packageDir, artifactPath) {
|
|
458
568
|
const result = spawnSync("zip", ["-qr", artifactPath, "."], {
|
|
459
569
|
cwd: packageDir,
|
|
@@ -521,6 +631,13 @@ function requireNonEmpty(value, label) {
|
|
|
521
631
|
return text;
|
|
522
632
|
}
|
|
523
633
|
|
|
634
|
+
function requireOptionalString(value, label) {
|
|
635
|
+
if (typeof value !== "string") {
|
|
636
|
+
throw new Error(`${label} must be a string`);
|
|
637
|
+
}
|
|
638
|
+
return value.trim();
|
|
639
|
+
}
|
|
640
|
+
|
|
524
641
|
function requireSafePathSegment(value, label) {
|
|
525
642
|
if (!/^[A-Za-z0-9._+-]+$/.test(value)) {
|
|
526
643
|
throw new Error(
|
|
@@ -147,6 +147,14 @@ function assertCatalogMatchesRelease(app, release) {
|
|
|
147
147
|
`catalog app ${appId} iconUrl must match latest release metadata`
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
|
+
if (
|
|
151
|
+
JSON.stringify(app.localizations ?? []) !==
|
|
152
|
+
JSON.stringify(release.localizations ?? [])
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`catalog app ${appId} localizations must match latest release metadata`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
function validateCatalog(catalog) {
|