@tutti-os/app-release-tools 0.0.10 → 0.0.12

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
@@ -8,16 +8,12 @@ Command-line tools for publishing Tutti workspace apps into App Center release m
8
8
  build-tutti-app-release --app-id vibe-design --package-dir dist/tutti-app/vibe-design --base-url https://cdn.example.test/tutti-app-releases
9
9
  build-tutti-app-catalog --release-file ./apps/vibe-design/latest.json --output ./catalog.json
10
10
  build-tutti-app-catalog --existing-catalog ./catalog.json --release-file ./apps/vibe-design/latest.json --output ./catalog.json
11
- bump-tutti-app-version --app-id vibe-design --manifest ./tutti.app.json --bump patch
12
11
  verify-tutti-app-release-artifacts --release-file ./apps/vibe-design/latest.json
13
12
  verify-tutti-app-release-artifacts --catalog-file ./catalog.json --release-file ./apps/vibe-design/latest.json
14
13
  ```
15
14
 
16
15
  The release command validates a complete Tutti app package, creates a zip, writes immutable `release.json`, and writes mutable `latest.json`.
17
16
 
18
- The version bump command updates an app manifest from one stable semver version
19
- to the next major, minor, or patch version.
20
-
21
17
  The catalog command merges one or more release files into `tutti.app.catalog.v1`.
22
18
  Pass `--existing-catalog` to preserve existing catalog apps and update only the
23
19
  apps represented by the release files. With `--existing-catalog`, release files
@@ -150,6 +150,7 @@ export function validateManifest(manifest, sourceLabel = "manifest") {
150
150
  throw new Error(`${sourceLabel} runtime.healthcheckPath must start with /`);
151
151
  }
152
152
  validateManifestCLI(manifest.cli, sourceLabel);
153
+ validateManifestReferences(manifest.references, sourceLabel);
153
154
  validateLocalizationInfo(manifest.localizationInfo, sourceLabel);
154
155
  }
155
156
 
@@ -171,6 +172,36 @@ function validateManifestCLI(cli, sourceLabel) {
171
172
  }
172
173
  }
173
174
 
175
+ function validateManifestReferences(references, sourceLabel) {
176
+ if (references === undefined) {
177
+ return;
178
+ }
179
+ if (
180
+ !references ||
181
+ typeof references !== "object" ||
182
+ Array.isArray(references)
183
+ ) {
184
+ throw new Error(`${sourceLabel} references must be an object`);
185
+ }
186
+ const unsupportedKey = Object.keys(references).find(
187
+ (key) => key !== "listEndpoint"
188
+ );
189
+ if (unsupportedKey) {
190
+ throw new Error(
191
+ `${sourceLabel}.references.${unsupportedKey} is unsupported`
192
+ );
193
+ }
194
+ const listEndpoint = requireNonEmpty(
195
+ references.listEndpoint,
196
+ `${sourceLabel}.references.listEndpoint`
197
+ );
198
+ if (!isRelativeURLPath(listEndpoint)) {
199
+ throw new Error(
200
+ `${sourceLabel}.references.listEndpoint must be a relative URL path without query or fragment`
201
+ );
202
+ }
203
+ }
204
+
174
205
  export function validateCLIManifest(manifest, sourceLabel = "cli manifest") {
175
206
  if (!manifest || typeof manifest !== "object") {
176
207
  throw new Error(`${sourceLabel} must be an object`);
@@ -495,6 +526,29 @@ function isRelativePackagePath(value) {
495
526
  return !text.split(/[\\/]+/).includes("..");
496
527
  }
497
528
 
529
+ function isRelativeURLPath(value) {
530
+ const text = String(value ?? "").trim();
531
+ if (
532
+ text === "" ||
533
+ !text.startsWith("/") ||
534
+ text.startsWith("//") ||
535
+ text.includes("\0")
536
+ ) {
537
+ return false;
538
+ }
539
+ try {
540
+ const parsed = new URL(text, "http://tutti.local");
541
+ return (
542
+ parsed.origin === "http://tutti.local" &&
543
+ parsed.pathname === text &&
544
+ parsed.search === "" &&
545
+ parsed.hash === ""
546
+ );
547
+ } catch {
548
+ return false;
549
+ }
550
+ }
551
+
498
552
  function normalizeBaseUrl(value) {
499
553
  return String(value).trim().replace(/\/+$/, "");
500
554
  }
package/package.json CHANGED
@@ -1,18 +1,16 @@
1
1
  {
2
2
  "name": "@tutti-os/app-release-tools",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "build-tutti-app-release": "./bin/build-tutti-app-release.mjs",
8
8
  "build-tutti-app-catalog": "./bin/build-tutti-app-catalog.mjs",
9
- "bump-tutti-app-version": "./bin/bump-tutti-app-version.mjs",
10
9
  "verify-tutti-app-release-artifacts": "./bin/verify-tutti-app-release-artifacts.mjs"
11
10
  },
12
11
  "exports": {
13
12
  "./build-tutti-app-release": "./bin/build-tutti-app-release.mjs",
14
13
  "./build-tutti-app-catalog": "./bin/build-tutti-app-catalog.mjs",
15
- "./bump-tutti-app-version": "./bin/bump-tutti-app-version.mjs",
16
14
  "./verify-tutti-app-release-artifacts": "./bin/verify-tutti-app-release-artifacts.mjs"
17
15
  },
18
16
  "files": [
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFile, writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
-
6
- import { validateManifest } from "./build-tutti-app-release.mjs";
7
-
8
- const allowedBumps = new Set(["major", "minor", "patch"]);
9
-
10
- export async function bumpTuttiAppVersion(options) {
11
- const manifestPath = path.resolve(
12
- requireNonEmpty(options.manifestPath, "manifestPath")
13
- );
14
- const bump = String(options.bump ?? "patch").trim();
15
- if (!allowedBumps.has(bump)) {
16
- throw new Error("bump must be one of major, minor, or patch");
17
- }
18
-
19
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
20
- validateManifest(manifest, manifestPath);
21
- if (options.appId && manifest.appId !== options.appId) {
22
- throw new Error(
23
- `appId mismatch: input ${options.appId} does not match manifest ${manifest.appId}`
24
- );
25
- }
26
-
27
- const previousVersion = manifest.version;
28
- const version = bumpStableVersion(previousVersion, bump);
29
- manifest.version = version;
30
- validateManifest(manifest, manifestPath);
31
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
32
-
33
- return {
34
- appId: manifest.appId,
35
- manifestPath,
36
- previousVersion,
37
- version
38
- };
39
- }
40
-
41
- function bumpStableVersion(version, bump) {
42
- const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(version ?? "").trim());
43
- if (!match) {
44
- throw new Error(
45
- `manifest version must be stable semver x.y.z for automatic bump, got ${JSON.stringify(version)}`
46
- );
47
- }
48
-
49
- let major = Number(match[1]);
50
- let minor = Number(match[2]);
51
- let patch = Number(match[3]);
52
- if (bump === "major") {
53
- major += 1;
54
- minor = 0;
55
- patch = 0;
56
- } else if (bump === "minor") {
57
- minor += 1;
58
- patch = 0;
59
- } else {
60
- patch += 1;
61
- }
62
-
63
- return `${major}.${minor}.${patch}`;
64
- }
65
-
66
- function requireNonEmpty(value, label) {
67
- const text = String(value ?? "").trim();
68
- if (text === "") {
69
- throw new Error(`${label} is required`);
70
- }
71
- return text;
72
- }
73
-
74
- function parseArgs(argv) {
75
- const result = {};
76
- for (let index = 0; index < argv.length; index += 1) {
77
- const arg = argv[index];
78
- if (arg === "--app-id") {
79
- result.appId = readArgValue(argv, index, arg);
80
- index += 1;
81
- continue;
82
- }
83
- if (arg === "--bump") {
84
- result.bump = readArgValue(argv, index, arg);
85
- index += 1;
86
- continue;
87
- }
88
- if (arg === "--manifest") {
89
- result.manifestPath = readArgValue(argv, index, arg);
90
- index += 1;
91
- continue;
92
- }
93
- throw new Error(`unexpected argument: ${arg}`);
94
- }
95
- return result;
96
- }
97
-
98
- function readArgValue(argv, index, arg) {
99
- const value = argv[index + 1];
100
- if (!value || value.startsWith("--")) {
101
- throw new Error(`missing value for ${arg}`);
102
- }
103
- return value;
104
- }
105
-
106
- export async function main() {
107
- const result = await bumpTuttiAppVersion(parseArgs(process.argv.slice(2)));
108
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
109
- }
110
-
111
- if (
112
- process.argv[1] &&
113
- import.meta.url === pathToFileURL(process.argv[1]).href
114
- ) {
115
- main().catch((error) => {
116
- console.error(error.message);
117
- process.exitCode = 1;
118
- });
119
- }