@tutti-os/app-release-tools 0.0.78 → 0.0.80
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 +34 -16
- package/bin/build-tutti-app-catalog.mjs +224 -64
- package/bin/build-tutti-app-release.mjs +80 -11
- package/bin/build-tutti-app-versions.mjs +236 -0
- package/bin/merge-tutti-app-latest.mjs +70 -0
- package/bin/publish-tutti-app-catalog.mjs +199 -0
- package/bin/publish-tutti-app-metadata.mjs +365 -0
- package/bin/tutti-app-versioning.mjs +44 -0
- package/bin/verify-tutti-app-release-artifacts.mjs +96 -124
- package/package.json +12 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { validateRelease } from "./build-tutti-app-release.mjs";
|
|
7
|
+
import {
|
|
8
|
+
compareReleaseVersions,
|
|
9
|
+
requireSemver
|
|
10
|
+
} from "./tutti-app-versioning.mjs";
|
|
11
|
+
|
|
12
|
+
export const versionsSchemaVersion = "tutti.app.versions.v1";
|
|
13
|
+
|
|
14
|
+
export async function buildTuttiAppVersions(options) {
|
|
15
|
+
const existingPath = options.existingVersionsPath
|
|
16
|
+
? path.resolve(String(options.existingVersionsPath))
|
|
17
|
+
: null;
|
|
18
|
+
const releaseFiles = normalizeFiles(options.releaseFiles);
|
|
19
|
+
const baselineReleaseFiles = normalizeFiles(options.baselineReleaseFiles);
|
|
20
|
+
const outputPath = path.resolve(
|
|
21
|
+
options.outputPath
|
|
22
|
+
? String(options.outputPath)
|
|
23
|
+
: "dist/tutti-app-release/versions.json"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let document = null;
|
|
27
|
+
if (existingPath) {
|
|
28
|
+
document = JSON.parse(await readFile(existingPath, "utf8"));
|
|
29
|
+
validateVersionsDocument(document);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const releaseFile of baselineReleaseFiles) {
|
|
33
|
+
const release = JSON.parse(await readFile(releaseFile, "utf8"));
|
|
34
|
+
validateRelease(release);
|
|
35
|
+
document = addVersionRecord(document, {
|
|
36
|
+
minTuttiVersion: "0.0.0",
|
|
37
|
+
status: "active",
|
|
38
|
+
release
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (releaseFiles.length > 0) {
|
|
43
|
+
const minTuttiVersion = requireSemver(
|
|
44
|
+
options.minTuttiVersion,
|
|
45
|
+
"minTuttiVersion"
|
|
46
|
+
);
|
|
47
|
+
const status = normalizeStatus(options.status ?? "active");
|
|
48
|
+
for (const releaseFile of releaseFiles) {
|
|
49
|
+
const release = JSON.parse(await readFile(releaseFile, "utf8"));
|
|
50
|
+
validateRelease(release);
|
|
51
|
+
document = addVersionRecord(document, {
|
|
52
|
+
minTuttiVersion,
|
|
53
|
+
status,
|
|
54
|
+
release
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.setStatusVersion) {
|
|
60
|
+
if (!document) {
|
|
61
|
+
throw new Error("existing versions are required when setting status");
|
|
62
|
+
}
|
|
63
|
+
const version = requireSemver(options.setStatusVersion, "setStatusVersion");
|
|
64
|
+
const status = normalizeStatus(options.status);
|
|
65
|
+
const record = document.versions.find(
|
|
66
|
+
(candidate) => candidate.release.version === version
|
|
67
|
+
);
|
|
68
|
+
if (!record) {
|
|
69
|
+
throw new Error(`version ${version} is not present in versions index`);
|
|
70
|
+
}
|
|
71
|
+
record.status = status;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!document || document.versions.length === 0) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"at least one release, baseline release, or existing versions document is required"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
document.versions.sort((left, right) =>
|
|
81
|
+
compareReleaseVersions(left.release, right.release)
|
|
82
|
+
);
|
|
83
|
+
validateVersionsDocument(document);
|
|
84
|
+
|
|
85
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
86
|
+
await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`);
|
|
87
|
+
return { outputPath, versions: document };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateVersionsDocument(document) {
|
|
91
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
92
|
+
throw new Error("versions document must be an object");
|
|
93
|
+
}
|
|
94
|
+
if (document.schemaVersion !== versionsSchemaVersion) {
|
|
95
|
+
throw new Error(`versions schemaVersion must be ${versionsSchemaVersion}`);
|
|
96
|
+
}
|
|
97
|
+
const appId = requireNonEmpty(document.appId, "versions appId");
|
|
98
|
+
if (!Array.isArray(document.versions)) {
|
|
99
|
+
throw new Error("versions must be an array");
|
|
100
|
+
}
|
|
101
|
+
const seenVersions = new Set();
|
|
102
|
+
for (const [index, record] of document.versions.entries()) {
|
|
103
|
+
const label = `versions[${index}]`;
|
|
104
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
105
|
+
throw new Error(`${label} must be an object`);
|
|
106
|
+
}
|
|
107
|
+
record.minTuttiVersion = requireSemver(
|
|
108
|
+
record.minTuttiVersion,
|
|
109
|
+
`${label}.minTuttiVersion`
|
|
110
|
+
);
|
|
111
|
+
record.status = normalizeStatus(record.status);
|
|
112
|
+
validateRelease(record.release);
|
|
113
|
+
if (record.release.appId !== appId) {
|
|
114
|
+
throw new Error(`${label}.release.appId must match versions appId`);
|
|
115
|
+
}
|
|
116
|
+
const version = requireSemver(
|
|
117
|
+
record.release.version,
|
|
118
|
+
`${label}.release.version`
|
|
119
|
+
);
|
|
120
|
+
if (seenVersions.has(version)) {
|
|
121
|
+
throw new Error(`duplicate versions release version ${version}`);
|
|
122
|
+
}
|
|
123
|
+
seenVersions.add(version);
|
|
124
|
+
}
|
|
125
|
+
return document;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addVersionRecord(document, record) {
|
|
129
|
+
const appId = record.release.appId;
|
|
130
|
+
const result = document ?? {
|
|
131
|
+
schemaVersion: versionsSchemaVersion,
|
|
132
|
+
appId,
|
|
133
|
+
versions: []
|
|
134
|
+
};
|
|
135
|
+
validateVersionsDocument(result);
|
|
136
|
+
if (result.appId !== appId) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`release appId ${appId} must match versions appId ${result.appId}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const existing = result.versions.find(
|
|
143
|
+
(candidate) => candidate.release.version === record.release.version
|
|
144
|
+
);
|
|
145
|
+
if (!existing) {
|
|
146
|
+
result.versions.push(record);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
if (JSON.stringify(existing) !== JSON.stringify(record)) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`version ${record.release.version} already exists with different compatibility or release metadata`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeFiles(value) {
|
|
158
|
+
const files = Array.isArray(value)
|
|
159
|
+
? value
|
|
160
|
+
: String(value ?? "")
|
|
161
|
+
.split(/[\n,]/u)
|
|
162
|
+
.map((file) => file.trim())
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
return files.map((file) => path.resolve(file));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeStatus(value) {
|
|
168
|
+
const status = String(value ?? "").trim();
|
|
169
|
+
if (status !== "active" && status !== "withdrawn") {
|
|
170
|
+
throw new Error("status must be active or withdrawn");
|
|
171
|
+
}
|
|
172
|
+
return status;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function requireNonEmpty(value, label) {
|
|
176
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
177
|
+
throw new Error(`${label} is required`);
|
|
178
|
+
}
|
|
179
|
+
return value.trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseArgs(argv) {
|
|
183
|
+
const result = { releaseFiles: [], baselineReleaseFiles: [] };
|
|
184
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
185
|
+
const arg = argv[index];
|
|
186
|
+
const value = argv[index + 1];
|
|
187
|
+
if (
|
|
188
|
+
[
|
|
189
|
+
"--existing-versions",
|
|
190
|
+
"--release-file",
|
|
191
|
+
"--baseline-release-file",
|
|
192
|
+
"--min-tutti-version",
|
|
193
|
+
"--set-status-version",
|
|
194
|
+
"--status",
|
|
195
|
+
"--output"
|
|
196
|
+
].includes(arg)
|
|
197
|
+
) {
|
|
198
|
+
if (!value || value.startsWith("--")) {
|
|
199
|
+
throw new Error(`missing value for ${arg}`);
|
|
200
|
+
}
|
|
201
|
+
const key = {
|
|
202
|
+
"--existing-versions": "existingVersionsPath",
|
|
203
|
+
"--min-tutti-version": "minTuttiVersion",
|
|
204
|
+
"--set-status-version": "setStatusVersion",
|
|
205
|
+
"--status": "status",
|
|
206
|
+
"--output": "outputPath"
|
|
207
|
+
}[arg];
|
|
208
|
+
if (arg === "--release-file") {
|
|
209
|
+
result.releaseFiles.push(value);
|
|
210
|
+
} else if (arg === "--baseline-release-file") {
|
|
211
|
+
result.baselineReleaseFiles.push(value);
|
|
212
|
+
} else {
|
|
213
|
+
result[key] = value;
|
|
214
|
+
}
|
|
215
|
+
index += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`unexpected argument: ${arg}`);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function main() {
|
|
224
|
+
const result = await buildTuttiAppVersions(parseArgs(process.argv.slice(2)));
|
|
225
|
+
process.stdout.write(`${JSON.stringify(result.versions, null, 2)}\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
process.argv[1] &&
|
|
230
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
231
|
+
) {
|
|
232
|
+
main().catch((error) => {
|
|
233
|
+
console.error(error.message);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { validateRelease } from "./build-tutti-app-release.mjs";
|
|
7
|
+
import { compareReleaseVersions } from "./tutti-app-versioning.mjs";
|
|
8
|
+
|
|
9
|
+
export async function mergeTuttiAppLatest(options) {
|
|
10
|
+
const releasePath = path.resolve(String(options.releasePath));
|
|
11
|
+
const outputPath = path.resolve(String(options.outputPath));
|
|
12
|
+
const release = JSON.parse(await readFile(releasePath, "utf8"));
|
|
13
|
+
validateRelease(release);
|
|
14
|
+
|
|
15
|
+
let latest = release;
|
|
16
|
+
if (options.existingLatestPath) {
|
|
17
|
+
const existing = JSON.parse(
|
|
18
|
+
await readFile(path.resolve(String(options.existingLatestPath)), "utf8")
|
|
19
|
+
);
|
|
20
|
+
validateRelease(existing);
|
|
21
|
+
if (existing.appId !== release.appId) {
|
|
22
|
+
throw new Error("existing latest appId must match release appId");
|
|
23
|
+
}
|
|
24
|
+
if (compareReleaseVersions(existing, release) > 0) {
|
|
25
|
+
latest = existing;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
30
|
+
await writeFile(outputPath, `${JSON.stringify(latest, null, 2)}\n`);
|
|
31
|
+
return { outputPath, latest };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const result = {};
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
const value = argv[index + 1];
|
|
39
|
+
if (["--release-file", "--existing-latest", "--output"].includes(arg)) {
|
|
40
|
+
if (!value || value.startsWith("--")) {
|
|
41
|
+
throw new Error(`missing value for ${arg}`);
|
|
42
|
+
}
|
|
43
|
+
if (arg === "--release-file") result.releasePath = value;
|
|
44
|
+
if (arg === "--existing-latest") result.existingLatestPath = value;
|
|
45
|
+
if (arg === "--output") result.outputPath = value;
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`unexpected argument: ${arg}`);
|
|
50
|
+
}
|
|
51
|
+
if (!result.releasePath || !result.outputPath) {
|
|
52
|
+
throw new Error("--release-file and --output are required");
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function main() {
|
|
58
|
+
const result = await mergeTuttiAppLatest(parseArgs(process.argv.slice(2)));
|
|
59
|
+
process.stdout.write(`${JSON.stringify(result.latest, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
process.argv[1] &&
|
|
64
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
65
|
+
) {
|
|
66
|
+
main().catch((error) => {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { buildTuttiAppCatalog } from "./build-tutti-app-catalog.mjs";
|
|
8
|
+
import { publishTuttiAppMetadata } from "./publish-tutti-app-metadata.mjs";
|
|
9
|
+
import { verifyTuttiAppReleaseArtifacts } from "./verify-tutti-app-release-artifacts.mjs";
|
|
10
|
+
|
|
11
|
+
export async function publishTuttiAppCatalog(options) {
|
|
12
|
+
const appIds = [...new Set(options.appIds ?? [])]
|
|
13
|
+
.map((value) => String(value).trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.sort();
|
|
16
|
+
if (appIds.length === 0) throw new Error("at least one appId is required");
|
|
17
|
+
const bucket = requireNonEmpty(options.s3Bucket, "s3Bucket");
|
|
18
|
+
const prefix = String(options.s3Prefix ?? "")
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/^\/+|\/+$/gu, "");
|
|
21
|
+
const mode = options.mode ?? "merge";
|
|
22
|
+
if (mode !== "merge" && mode !== "replace") {
|
|
23
|
+
throw new Error("mode must be merge or replace");
|
|
24
|
+
}
|
|
25
|
+
const outputDir = path.resolve(
|
|
26
|
+
String(options.outputDir ?? "tutti-app-catalog")
|
|
27
|
+
);
|
|
28
|
+
await mkdir(outputDir, { recursive: true });
|
|
29
|
+
const key = (suffix) => (prefix ? `${prefix}/${suffix}` : suffix);
|
|
30
|
+
const catalogKey = key("catalog.json");
|
|
31
|
+
|
|
32
|
+
let lastError = null;
|
|
33
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
34
|
+
const attemptDir = path.join(outputDir, `attempt-${attempt}`);
|
|
35
|
+
await mkdir(attemptDir, { recursive: true });
|
|
36
|
+
const existing = await getObject({
|
|
37
|
+
bucket,
|
|
38
|
+
key: catalogKey,
|
|
39
|
+
outputPath: path.join(attemptDir, "existing-catalog.json"),
|
|
40
|
+
required: false
|
|
41
|
+
});
|
|
42
|
+
const versionsFiles = [];
|
|
43
|
+
for (const appId of appIds) {
|
|
44
|
+
let versions = await getObject({
|
|
45
|
+
bucket,
|
|
46
|
+
key: key(`apps/${appId}/versions.json`),
|
|
47
|
+
outputPath: path.join(attemptDir, `${appId}-versions.json`),
|
|
48
|
+
required: false
|
|
49
|
+
});
|
|
50
|
+
if (!versions) {
|
|
51
|
+
await publishTuttiAppMetadata({
|
|
52
|
+
appId,
|
|
53
|
+
s3Bucket: bucket,
|
|
54
|
+
s3Prefix: prefix,
|
|
55
|
+
catalogOnly: true,
|
|
56
|
+
publishCatalog: false,
|
|
57
|
+
outputDir: path.join(attemptDir, `${appId}-bootstrap`)
|
|
58
|
+
});
|
|
59
|
+
versions = await getObject({
|
|
60
|
+
bucket,
|
|
61
|
+
key: key(`apps/${appId}/versions.json`),
|
|
62
|
+
outputPath: path.join(attemptDir, `${appId}-versions.json`),
|
|
63
|
+
required: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
versionsFiles.push(versions.path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const catalogPath = path.join(attemptDir, "catalog.json");
|
|
70
|
+
await buildTuttiAppCatalog({
|
|
71
|
+
existingCatalogPath: mode === "merge" ? existing?.path : null,
|
|
72
|
+
versionsFiles,
|
|
73
|
+
outputPath: catalogPath
|
|
74
|
+
});
|
|
75
|
+
await verifyTuttiAppReleaseArtifacts({
|
|
76
|
+
catalogFile: catalogPath,
|
|
77
|
+
versionsFiles,
|
|
78
|
+
verifyArtifacts: true
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
putObject({
|
|
82
|
+
bucket,
|
|
83
|
+
key: catalogKey,
|
|
84
|
+
path: catalogPath,
|
|
85
|
+
etag: existing?.etag ?? null
|
|
86
|
+
});
|
|
87
|
+
const stablePath = path.join(outputDir, "catalog.json");
|
|
88
|
+
await copyFile(catalogPath, stablePath);
|
|
89
|
+
return { catalogPath: stablePath, appIds, mode };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!/(?:412|PreconditionFailed|precondition)/iu.test(error.message)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
lastError = error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error(
|
|
98
|
+
`catalog changed concurrently: ${lastError?.message ?? "failed"}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getObject({ bucket, key, outputPath, required }) {
|
|
103
|
+
const head = runAWS(
|
|
104
|
+
["s3api", "head-object", "--bucket", bucket, "--key", key],
|
|
105
|
+
{ allowMissing: !required }
|
|
106
|
+
);
|
|
107
|
+
if (head.missing) return null;
|
|
108
|
+
const metadata = JSON.parse(head.stdout);
|
|
109
|
+
runAWS(["s3api", "get-object", "--bucket", bucket, "--key", key, outputPath]);
|
|
110
|
+
return { path: outputPath, etag: metadata.ETag };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function putObject({ bucket, key, path: filePath, etag }) {
|
|
114
|
+
const args = [
|
|
115
|
+
"s3api",
|
|
116
|
+
"put-object",
|
|
117
|
+
"--bucket",
|
|
118
|
+
bucket,
|
|
119
|
+
"--key",
|
|
120
|
+
key,
|
|
121
|
+
"--body",
|
|
122
|
+
filePath,
|
|
123
|
+
"--content-type",
|
|
124
|
+
"application/json",
|
|
125
|
+
"--cache-control",
|
|
126
|
+
"public, max-age=60",
|
|
127
|
+
...(etag ? ["--if-match", etag] : ["--if-none-match", "*"])
|
|
128
|
+
];
|
|
129
|
+
runAWS(args);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runAWS(args, options = {}) {
|
|
133
|
+
const result = spawnSync("aws", args, {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
maxBuffer: 10 * 1024 * 1024
|
|
136
|
+
});
|
|
137
|
+
if (result.status === 0) {
|
|
138
|
+
return { stdout: result.stdout, missing: false };
|
|
139
|
+
}
|
|
140
|
+
const message = `${result.stderr ?? ""}\n${result.stdout ?? ""}`.trim();
|
|
141
|
+
if (
|
|
142
|
+
options.allowMissing &&
|
|
143
|
+
/(?:404|Not Found|NoSuchKey|NotFound)/iu.test(message)
|
|
144
|
+
) {
|
|
145
|
+
return { stdout: "", missing: true };
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`aws ${args.slice(0, 2).join(" ")} failed: ${message}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function requireNonEmpty(value, label) {
|
|
151
|
+
const text = String(value ?? "").trim();
|
|
152
|
+
if (!text) throw new Error(`${label} is required`);
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseArgs(argv) {
|
|
157
|
+
const result = { appIds: [] };
|
|
158
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
159
|
+
const arg = argv[index];
|
|
160
|
+
const value = argv[index + 1];
|
|
161
|
+
if (
|
|
162
|
+
[
|
|
163
|
+
"--app-id",
|
|
164
|
+
"--s3-bucket",
|
|
165
|
+
"--s3-prefix",
|
|
166
|
+
"--mode",
|
|
167
|
+
"--output-dir"
|
|
168
|
+
].includes(arg)
|
|
169
|
+
) {
|
|
170
|
+
if (value === undefined || value.startsWith("--")) {
|
|
171
|
+
throw new Error(`missing value for ${arg}`);
|
|
172
|
+
}
|
|
173
|
+
if (arg === "--app-id") result.appIds.push(value);
|
|
174
|
+
if (arg === "--s3-bucket") result.s3Bucket = value;
|
|
175
|
+
if (arg === "--s3-prefix") result.s3Prefix = value;
|
|
176
|
+
if (arg === "--mode") result.mode = value;
|
|
177
|
+
if (arg === "--output-dir") result.outputDir = value;
|
|
178
|
+
index += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`unexpected argument: ${arg}`);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function main() {
|
|
187
|
+
const result = await publishTuttiAppCatalog(parseArgs(process.argv.slice(2)));
|
|
188
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
process.argv[1] &&
|
|
193
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
194
|
+
) {
|
|
195
|
+
main().catch((error) => {
|
|
196
|
+
console.error(error.message);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
});
|
|
199
|
+
}
|