@helyx/cli 0.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/LICENSE +725 -0
- package/README.md +16 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +85 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/self-host/catalogue.d.ts +72 -0
- package/dist/self-host/catalogue.js +115 -0
- package/dist/self-host/command.d.ts +9 -0
- package/dist/self-host/command.js +76 -0
- package/dist/self-host/manifest.d.ts +23 -0
- package/dist/self-host/manifest.js +127 -0
- package/dist/self-host/project-generator.d.ts +24 -0
- package/dist/self-host/project-generator.js +236 -0
- package/package.json +63 -0
- package/self-host-catalogue.json +278 -0
- package/templates/self-host/.env.example +26 -0
- package/templates/self-host/Dockerfile +15 -0
- package/templates/self-host/README.md +56 -0
- package/templates/self-host/compose.yaml +27 -0
- package/templates/self-host/npmrc.template +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Helyx CLI
|
|
2
|
+
|
|
3
|
+
The private, unpublished Helyx distribution entry point.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
helyx start
|
|
7
|
+
helyx migrate
|
|
8
|
+
helyx doctor
|
|
9
|
+
helyx self-host init --manifest ./helyx-build.json --output ./helyx
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`start` performs the production bot bootstrap, including database and installed-module migrations. `migrate` applies only core migrations and exits. `doctor` validates the complete bot environment, PostgreSQL connectivity, immutable core migration history and configured module package metadata without logging in to Discord or modifying the database.
|
|
13
|
+
|
|
14
|
+
`self-host init` validates a build manifest against the reviewed catalogue shipped with this exact CLI version, creates a new starter-project directory, pins the runtime and selected official modules, generates a lockfile with lifecycle scripts disabled, and writes checksums. Add `--install` only when the dependencies should also be installed locally. Unknown, unpublished, duplicated or modified package selections fail closed.
|
|
15
|
+
|
|
16
|
+
The package remains `private` and `UNLICENSED`; it must not be published until the repository licence and publication gates are approved.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfiguredModulePackages, resolveModulePackageSpecifiers, startBot, } from "@helyx/bot";
|
|
3
|
+
import { parseBotEnvironment, parseDatabaseEnvironment } from "@helyx/config";
|
|
4
|
+
import { checkDatabaseConnection, createDatabasePool, inspectCoreMigrations, runCoreMigrations, } from "@helyx/database";
|
|
5
|
+
import { runSelfHostCommand } from "./self-host/command.js";
|
|
6
|
+
const command = process.argv[2];
|
|
7
|
+
try {
|
|
8
|
+
if (command === "start") {
|
|
9
|
+
await startBot();
|
|
10
|
+
}
|
|
11
|
+
else if (command === "migrate") {
|
|
12
|
+
await migrate();
|
|
13
|
+
}
|
|
14
|
+
else if (command === "doctor") {
|
|
15
|
+
await doctor();
|
|
16
|
+
}
|
|
17
|
+
else if (command === "self-host") {
|
|
18
|
+
await runSelfHostCommand(process.argv.slice(3));
|
|
19
|
+
}
|
|
20
|
+
else if (command === "help" || command === "--help" || command === "-h") {
|
|
21
|
+
printHelp();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
printHelp();
|
|
25
|
+
if (command)
|
|
26
|
+
throw new Error(`Unknown command: ${command}`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const value = error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
console.error(`Helyx ${command ?? "command"} failed: ${value.message}`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
async function migrate() {
|
|
36
|
+
const environment = parseDatabaseEnvironment(process.env);
|
|
37
|
+
const database = createDatabasePool({
|
|
38
|
+
connectionString: environment.DATABASE_URL,
|
|
39
|
+
sslMode: environment.DATABASE_SSL,
|
|
40
|
+
applicationName: "helyx-cli-migrate",
|
|
41
|
+
maxConnections: 1,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await checkDatabaseConnection(database);
|
|
45
|
+
const result = await runCoreMigrations(database);
|
|
46
|
+
console.log(`Core database ready: ${result.applied.length} applied, ${result.verified.length} verified.`);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await database.end();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function doctor() {
|
|
53
|
+
const environment = parseBotEnvironment(process.env);
|
|
54
|
+
const database = createDatabasePool({
|
|
55
|
+
connectionString: environment.DATABASE_URL,
|
|
56
|
+
sslMode: environment.DATABASE_SSL,
|
|
57
|
+
applicationName: "helyx-cli-doctor",
|
|
58
|
+
maxConnections: 1,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await checkDatabaseConnection(database);
|
|
62
|
+
const migrations = await inspectCoreMigrations(database);
|
|
63
|
+
if (migrations.pending.length > 0) {
|
|
64
|
+
throw new Error(`${migrations.pending.length} core migration(s) are pending; run helyx migrate`);
|
|
65
|
+
}
|
|
66
|
+
const moduleSpecifiers = await resolveModulePackageSpecifiers(environment.HELYX_MODULE_PACKAGES, undefined, undefined, environment.HELYX_MODULE_CATALOGUE_PATH);
|
|
67
|
+
const modules = await loadConfiguredModulePackages(moduleSpecifiers);
|
|
68
|
+
console.log(`Helyx doctor passed: PostgreSQL connected, ${migrations.verified.length} core migrations verified, ${modules.length} module packages validated.`);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await database.end();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function printHelp() {
|
|
75
|
+
console.log(`Usage: helyx <command>
|
|
76
|
+
|
|
77
|
+
Commands:
|
|
78
|
+
start Start the Helyx Discord bot
|
|
79
|
+
migrate Apply core PostgreSQL migrations and exit
|
|
80
|
+
doctor Validate environment, database schema and module packages
|
|
81
|
+
self-host init --manifest <path> [--output <path>] [--install]
|
|
82
|
+
Generate a self-hosted starter project from a reviewed manifest
|
|
83
|
+
help Show this help`);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const selfHostCategories: readonly ["Community", "Safety", "Support", "Automation", "Engagement", "Insights", "Integrations"];
|
|
3
|
+
export declare const selfHostModuleSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
package: z.ZodString;
|
|
6
|
+
version: z.ZodNullable<z.ZodString>;
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
description: z.ZodString;
|
|
9
|
+
category: z.ZodEnum<{
|
|
10
|
+
Community: "Community";
|
|
11
|
+
Safety: "Safety";
|
|
12
|
+
Support: "Support";
|
|
13
|
+
Automation: "Automation";
|
|
14
|
+
Engagement: "Engagement";
|
|
15
|
+
Insights: "Insights";
|
|
16
|
+
Integrations: "Integrations";
|
|
17
|
+
}>;
|
|
18
|
+
availability: z.ZodEnum<{
|
|
19
|
+
planned: "planned";
|
|
20
|
+
unpublished: "unpublished";
|
|
21
|
+
published: "published";
|
|
22
|
+
}>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
export declare const selfHostCatalogueSchema: z.ZodObject<{
|
|
25
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
26
|
+
catalogueVersion: z.ZodNumber;
|
|
27
|
+
core: z.ZodObject<{
|
|
28
|
+
package: z.ZodString;
|
|
29
|
+
version: z.ZodString;
|
|
30
|
+
availability: z.ZodEnum<{
|
|
31
|
+
unpublished: "unpublished";
|
|
32
|
+
published: "published";
|
|
33
|
+
}>;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
generator: z.ZodObject<{
|
|
36
|
+
package: z.ZodString;
|
|
37
|
+
version: z.ZodString;
|
|
38
|
+
availability: z.ZodEnum<{
|
|
39
|
+
unpublished: "unpublished";
|
|
40
|
+
published: "published";
|
|
41
|
+
}>;
|
|
42
|
+
command: z.ZodString;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
modules: z.ZodArray<z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
package: z.ZodString;
|
|
47
|
+
version: z.ZodNullable<z.ZodString>;
|
|
48
|
+
name: z.ZodString;
|
|
49
|
+
description: z.ZodString;
|
|
50
|
+
category: z.ZodEnum<{
|
|
51
|
+
Community: "Community";
|
|
52
|
+
Safety: "Safety";
|
|
53
|
+
Support: "Support";
|
|
54
|
+
Automation: "Automation";
|
|
55
|
+
Engagement: "Engagement";
|
|
56
|
+
Insights: "Insights";
|
|
57
|
+
Integrations: "Integrations";
|
|
58
|
+
}>;
|
|
59
|
+
availability: z.ZodEnum<{
|
|
60
|
+
planned: "planned";
|
|
61
|
+
unpublished: "unpublished";
|
|
62
|
+
published: "published";
|
|
63
|
+
}>;
|
|
64
|
+
}, z.core.$strict>>;
|
|
65
|
+
}, z.core.$strict>;
|
|
66
|
+
export type SelfHostModule = z.infer<typeof selfHostModuleSchema>;
|
|
67
|
+
export type SelfHostCatalogue = z.infer<typeof selfHostCatalogueSchema>;
|
|
68
|
+
export declare function parseSelfHostCatalogue(input: unknown): SelfHostCatalogue;
|
|
69
|
+
export declare function loadSelfHostCatalogue(path: string | URL, reader?: (path: string | URL, encoding: "utf8") => Promise<string>): Promise<SelfHostCatalogue>;
|
|
70
|
+
export declare function bundledSelfHostCatalogueUrl(): URL;
|
|
71
|
+
export declare function loadBundledSelfHostCatalogue(): Promise<SelfHostCatalogue>;
|
|
72
|
+
//# sourceMappingURL=catalogue.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const selfHostCategories = [
|
|
5
|
+
"Community",
|
|
6
|
+
"Safety",
|
|
7
|
+
"Support",
|
|
8
|
+
"Automation",
|
|
9
|
+
"Engagement",
|
|
10
|
+
"Insights",
|
|
11
|
+
"Integrations",
|
|
12
|
+
];
|
|
13
|
+
const exactVersion = z
|
|
14
|
+
.string()
|
|
15
|
+
.refine((value) => semver.valid(value) === value, "must be exact semver");
|
|
16
|
+
const helyxPackage = z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^@helyx\/[a-z0-9][a-z0-9-]*$/u, "must be an official Helyx package");
|
|
19
|
+
const modulePackage = z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u, "must be an official Helyx module package");
|
|
22
|
+
const moduleId = z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u, "must be a Helyx module ID");
|
|
25
|
+
const availability = z.enum(["planned", "unpublished", "published"]);
|
|
26
|
+
const componentSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
package: helyxPackage,
|
|
29
|
+
version: exactVersion,
|
|
30
|
+
availability: z.enum(["unpublished", "published"]),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const generatorSchema = componentSchema
|
|
34
|
+
.extend({
|
|
35
|
+
command: z
|
|
36
|
+
.string()
|
|
37
|
+
.regex(/^[a-z][a-z0-9-]*$/u)
|
|
38
|
+
.max(40),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
export const selfHostModuleSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
id: moduleId,
|
|
44
|
+
package: modulePackage,
|
|
45
|
+
version: exactVersion.nullable(),
|
|
46
|
+
name: z.string().trim().min(1).max(80),
|
|
47
|
+
description: z.string().trim().min(1).max(300),
|
|
48
|
+
category: z.enum(selfHostCategories),
|
|
49
|
+
availability,
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.superRefine((module, context) => {
|
|
53
|
+
if (module.availability === "planned" && module.version !== null) {
|
|
54
|
+
context.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: ["version"],
|
|
57
|
+
message: "must be null while a module is planned",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (module.availability !== "planned" && module.version === null) {
|
|
61
|
+
context.addIssue({
|
|
62
|
+
code: "custom",
|
|
63
|
+
path: ["version"],
|
|
64
|
+
message: "must be exact semver for an implemented module",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
export const selfHostCatalogueSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
schemaVersion: z.literal(1),
|
|
71
|
+
catalogueVersion: z.number().int().positive(),
|
|
72
|
+
core: componentSchema,
|
|
73
|
+
generator: generatorSchema,
|
|
74
|
+
modules: z.array(selfHostModuleSchema),
|
|
75
|
+
})
|
|
76
|
+
.strict()
|
|
77
|
+
.superRefine((catalogue, context) => {
|
|
78
|
+
const ids = new Set();
|
|
79
|
+
const packages = new Set([
|
|
80
|
+
catalogue.core.package,
|
|
81
|
+
catalogue.generator.package,
|
|
82
|
+
]);
|
|
83
|
+
for (const [index, module] of catalogue.modules.entries()) {
|
|
84
|
+
if (ids.has(module.id)) {
|
|
85
|
+
context.addIssue({
|
|
86
|
+
code: "custom",
|
|
87
|
+
path: ["modules", index, "id"],
|
|
88
|
+
message: `duplicate module ID: ${module.id}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (packages.has(module.package)) {
|
|
92
|
+
context.addIssue({
|
|
93
|
+
code: "custom",
|
|
94
|
+
path: ["modules", index, "package"],
|
|
95
|
+
message: `duplicate package name: ${module.package}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
ids.add(module.id);
|
|
99
|
+
packages.add(module.package);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
export function parseSelfHostCatalogue(input) {
|
|
103
|
+
return selfHostCatalogueSchema.parse(input);
|
|
104
|
+
}
|
|
105
|
+
export async function loadSelfHostCatalogue(path, reader = readFile) {
|
|
106
|
+
const source = await reader(path, "utf8");
|
|
107
|
+
return parseSelfHostCatalogue(JSON.parse(source));
|
|
108
|
+
}
|
|
109
|
+
export function bundledSelfHostCatalogueUrl() {
|
|
110
|
+
return new URL("../../self-host-catalogue.json", import.meta.url);
|
|
111
|
+
}
|
|
112
|
+
export async function loadBundledSelfHostCatalogue() {
|
|
113
|
+
return loadSelfHostCatalogue(bundledSelfHostCatalogueUrl());
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=catalogue.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type GeneratedSelfHostProject } from "./project-generator.js";
|
|
2
|
+
export interface SelfHostInitArguments {
|
|
3
|
+
manifestPath: string;
|
|
4
|
+
outputPath: string;
|
|
5
|
+
install: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function runSelfHostCommand(arguments_: readonly string[]): Promise<GeneratedSelfHostProject>;
|
|
8
|
+
export declare function parseSelfHostInitArguments(arguments_: readonly string[]): SelfHostInitArguments;
|
|
9
|
+
//# sourceMappingURL=command.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loadBundledSelfHostCatalogue } from "./catalogue.js";
|
|
4
|
+
import { generateSelfHostProject, } from "./project-generator.js";
|
|
5
|
+
const maximumManifestBytes = 16 * 1_024;
|
|
6
|
+
export async function runSelfHostCommand(arguments_) {
|
|
7
|
+
const [subcommand, ...rest] = arguments_;
|
|
8
|
+
if (subcommand !== "init") {
|
|
9
|
+
throw new Error("Usage: helyx self-host init --manifest <path> [--output <path>] [--install]");
|
|
10
|
+
}
|
|
11
|
+
const parsed = parseSelfHostInitArguments(rest);
|
|
12
|
+
const manifestPath = resolve(parsed.manifestPath);
|
|
13
|
+
const metadata = await stat(manifestPath);
|
|
14
|
+
if (!metadata.isFile() || metadata.size > maximumManifestBytes) {
|
|
15
|
+
throw new Error("Self-host build manifest must be a file no larger than 16 KB");
|
|
16
|
+
}
|
|
17
|
+
let manifest;
|
|
18
|
+
try {
|
|
19
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new Error("Self-host build manifest is not valid JSON", {
|
|
23
|
+
cause: error,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const catalogue = await loadBundledSelfHostCatalogue();
|
|
27
|
+
const generated = await generateSelfHostProject({
|
|
28
|
+
catalogue,
|
|
29
|
+
manifest,
|
|
30
|
+
targetDirectory: parsed.outputPath,
|
|
31
|
+
install: parsed.install,
|
|
32
|
+
});
|
|
33
|
+
console.log(`Created Helyx Self-Hosted starter at ${generated.directory}`);
|
|
34
|
+
return generated;
|
|
35
|
+
}
|
|
36
|
+
export function parseSelfHostInitArguments(arguments_) {
|
|
37
|
+
let manifestPath;
|
|
38
|
+
let outputPath = "./helyx";
|
|
39
|
+
let outputConfigured = false;
|
|
40
|
+
let install = false;
|
|
41
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
42
|
+
const argument = arguments_[index];
|
|
43
|
+
if (argument === "--install") {
|
|
44
|
+
if (install)
|
|
45
|
+
throw new Error("--install may be specified only once");
|
|
46
|
+
install = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (argument === "--manifest" || argument === "--output") {
|
|
50
|
+
const value = arguments_[index + 1];
|
|
51
|
+
if (!value || value.startsWith("--")) {
|
|
52
|
+
throw new Error(`${argument} requires a path`);
|
|
53
|
+
}
|
|
54
|
+
index += 1;
|
|
55
|
+
if (argument === "--manifest") {
|
|
56
|
+
if (manifestPath) {
|
|
57
|
+
throw new Error("--manifest may be specified only once");
|
|
58
|
+
}
|
|
59
|
+
manifestPath = value;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (outputConfigured) {
|
|
63
|
+
throw new Error("--output may be specified only once");
|
|
64
|
+
}
|
|
65
|
+
outputPath = value;
|
|
66
|
+
outputConfigured = true;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Unknown self-host init option: ${argument ?? ""}`);
|
|
71
|
+
}
|
|
72
|
+
if (!manifestPath)
|
|
73
|
+
throw new Error("--manifest is required");
|
|
74
|
+
return { manifestPath, outputPath, install };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SelfHostCatalogue } from "./catalogue.js";
|
|
3
|
+
export declare const selfHostBuildManifestSchema: z.ZodObject<{
|
|
4
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
5
|
+
catalogueVersion: z.ZodNumber;
|
|
6
|
+
core: z.ZodObject<{
|
|
7
|
+
package: z.ZodString;
|
|
8
|
+
version: z.ZodString;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
generator: z.ZodObject<{
|
|
11
|
+
package: z.ZodString;
|
|
12
|
+
version: z.ZodString;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
modules: z.ZodArray<z.ZodObject<{
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
package: z.ZodString;
|
|
17
|
+
version: z.ZodString;
|
|
18
|
+
}, z.core.$strict>>;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
export type SelfHostBuildManifest = z.infer<typeof selfHostBuildManifestSchema>;
|
|
21
|
+
export declare function createSelfHostBuildManifest(catalogue: SelfHostCatalogue, selectedModuleIds: readonly string[]): SelfHostBuildManifest;
|
|
22
|
+
export declare function parseSelfHostBuildManifest(input: unknown, catalogue: SelfHostCatalogue): SelfHostBuildManifest;
|
|
23
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const exactVersion = z
|
|
4
|
+
.string()
|
|
5
|
+
.refine((value) => semver.valid(value) === value, "must be exact semver");
|
|
6
|
+
const officialPackage = z.string().regex(/^@helyx\/[a-z0-9][a-z0-9-]*$/u);
|
|
7
|
+
const officialModulePackage = z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u);
|
|
10
|
+
const moduleId = z.string().regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u);
|
|
11
|
+
const buildComponentSchema = z
|
|
12
|
+
.object({ package: officialPackage, version: exactVersion })
|
|
13
|
+
.strict();
|
|
14
|
+
const buildModuleSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
id: moduleId,
|
|
17
|
+
package: officialModulePackage,
|
|
18
|
+
version: exactVersion,
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
export const selfHostBuildManifestSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
schemaVersion: z.literal(1),
|
|
24
|
+
catalogueVersion: z.number().int().positive(),
|
|
25
|
+
core: buildComponentSchema,
|
|
26
|
+
generator: buildComponentSchema,
|
|
27
|
+
modules: z.array(buildModuleSchema),
|
|
28
|
+
})
|
|
29
|
+
.strict()
|
|
30
|
+
.superRefine((manifest, context) => {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
const packages = new Set();
|
|
33
|
+
for (const [index, module] of manifest.modules.entries()) {
|
|
34
|
+
if (ids.has(module.id)) {
|
|
35
|
+
context.addIssue({
|
|
36
|
+
code: "custom",
|
|
37
|
+
path: ["modules", index, "id"],
|
|
38
|
+
message: `duplicate module ID: ${module.id}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (packages.has(module.package)) {
|
|
42
|
+
context.addIssue({
|
|
43
|
+
code: "custom",
|
|
44
|
+
path: ["modules", index, "package"],
|
|
45
|
+
message: `duplicate module package: ${module.package}`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
ids.add(module.id);
|
|
49
|
+
packages.add(module.package);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
export function createSelfHostBuildManifest(catalogue, selectedModuleIds) {
|
|
53
|
+
assertPublishedComponent(catalogue.core, "Helyx runtime");
|
|
54
|
+
assertPublishedComponent(catalogue.generator, "Helyx generator");
|
|
55
|
+
if (new Set(selectedModuleIds).size !== selectedModuleIds.length) {
|
|
56
|
+
throw new Error("Self-host selection contains duplicate module IDs");
|
|
57
|
+
}
|
|
58
|
+
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
59
|
+
const modules = [...selectedModuleIds]
|
|
60
|
+
.sort((left, right) => left.localeCompare(right, "en"))
|
|
61
|
+
.map((id) => {
|
|
62
|
+
const module = byId.get(id);
|
|
63
|
+
if (!module)
|
|
64
|
+
throw new Error(`Unknown self-host module: ${id}`);
|
|
65
|
+
if (module.availability !== "published" || module.version === null) {
|
|
66
|
+
throw new Error(`Self-host module is not published: ${id}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
id: module.id,
|
|
70
|
+
package: module.package,
|
|
71
|
+
version: module.version,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
catalogueVersion: catalogue.catalogueVersion,
|
|
77
|
+
core: {
|
|
78
|
+
package: catalogue.core.package,
|
|
79
|
+
version: catalogue.core.version,
|
|
80
|
+
},
|
|
81
|
+
generator: {
|
|
82
|
+
package: catalogue.generator.package,
|
|
83
|
+
version: catalogue.generator.version,
|
|
84
|
+
},
|
|
85
|
+
modules,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function parseSelfHostBuildManifest(input, catalogue) {
|
|
89
|
+
const manifest = selfHostBuildManifestSchema.parse(input);
|
|
90
|
+
if (manifest.catalogueVersion !== catalogue.catalogueVersion) {
|
|
91
|
+
throw new Error(`Unsupported self-host catalogue version: ${manifest.catalogueVersion}`);
|
|
92
|
+
}
|
|
93
|
+
assertPublishedComponent(catalogue.core, "Helyx runtime");
|
|
94
|
+
assertPublishedComponent(catalogue.generator, "Helyx generator");
|
|
95
|
+
assertComponentMatches(manifest.core, catalogue.core, "runtime");
|
|
96
|
+
assertComponentMatches(manifest.generator, catalogue.generator, "generator");
|
|
97
|
+
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
98
|
+
for (const selected of manifest.modules) {
|
|
99
|
+
const module = byId.get(selected.id);
|
|
100
|
+
if (!module ||
|
|
101
|
+
module.availability !== "published" ||
|
|
102
|
+
module.version === null ||
|
|
103
|
+
module.package !== selected.package ||
|
|
104
|
+
module.version !== selected.version) {
|
|
105
|
+
throw new Error(`Build manifest module is unavailable or does not match the trusted catalogue: ${selected.id}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (manifest.modules.length > catalogue.modules.length) {
|
|
109
|
+
throw new Error("Build manifest contains too many modules");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...manifest,
|
|
113
|
+
modules: [...manifest.modules].sort((left, right) => left.id.localeCompare(right.id, "en")),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function assertPublishedComponent(component, label) {
|
|
117
|
+
if (component.availability !== "published") {
|
|
118
|
+
throw new Error(`${label} is not published`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function assertComponentMatches(selected, expected, label) {
|
|
122
|
+
if (selected.package !== expected.package ||
|
|
123
|
+
selected.version !== expected.version) {
|
|
124
|
+
throw new Error(`Build manifest ${label} does not match the trusted catalogue`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BotEnvironmentInput } from "@helyx/config";
|
|
2
|
+
import type { SelfHostCatalogue } from "./catalogue.js";
|
|
3
|
+
import { type SelfHostBuildManifest } from "./manifest.js";
|
|
4
|
+
export type SelfHostCommandRunner = (arguments_: readonly string[], workingDirectory: string) => Promise<void>;
|
|
5
|
+
export interface GenerateSelfHostProjectOptions {
|
|
6
|
+
catalogue: SelfHostCatalogue;
|
|
7
|
+
manifest: unknown;
|
|
8
|
+
targetDirectory: string;
|
|
9
|
+
install?: boolean;
|
|
10
|
+
commandRunner?: SelfHostCommandRunner;
|
|
11
|
+
}
|
|
12
|
+
export interface GeneratedSelfHostProject {
|
|
13
|
+
directory: string;
|
|
14
|
+
manifest: SelfHostBuildManifest;
|
|
15
|
+
files: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function generateSelfHostProject(options: GenerateSelfHostProjectOptions): Promise<GeneratedSelfHostProject>;
|
|
18
|
+
type EnvironmentExample = Record<keyof BotEnvironmentInput, {
|
|
19
|
+
value: string;
|
|
20
|
+
optional: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const selfHostEnvironmentContract: EnvironmentExample;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=project-generator.d.ts.map
|