aboutsummaryrefslogtreecommitdiff
path: root/deno
diff options
context:
space:
mode:
Diffstat (limited to 'deno')
-rw-r--r--deno/base/lib.ts2
-rw-r--r--deno/deno.json5
-rw-r--r--deno/deno.lock7
-rw-r--r--deno/mail-relay/aws/mail.ts59
-rw-r--r--deno/mail/app.ts (renamed from deno/mail-relay/app.ts)2
-rw-r--r--deno/mail/aws/app.ts (renamed from deno/mail-relay/aws/app.ts)24
-rw-r--r--deno/mail/aws/deliver.ts (renamed from deno/mail-relay/aws/deliver.ts)12
-rw-r--r--deno/mail/aws/fetch.ts (renamed from deno/mail-relay/aws/fetch.ts)0
-rw-r--r--deno/mail/db.test.ts (renamed from deno/mail-relay/db.test.ts)8
-rw-r--r--deno/mail/db.ts (renamed from deno/mail-relay/db.ts)36
-rw-r--r--deno/mail/deno.json (renamed from deno/mail-relay/deno.json)2
-rw-r--r--deno/mail/dovecot.ts (renamed from deno/mail-relay/dovecot.ts)14
-rw-r--r--deno/mail/dumb-smtp-server.ts (renamed from deno/mail-relay/dumb-smtp-server.ts)4
-rw-r--r--deno/mail/mail-parsing.ts (renamed from deno/mail-relay/mail-parsing.ts)0
-rw-r--r--deno/mail/mail.test.ts (renamed from deno/mail-relay/mail.test.ts)0
-rw-r--r--deno/mail/mail.ts (renamed from deno/mail-relay/mail.ts)76
-rw-r--r--deno/tools/geosite.ts (renamed from deno/tools/generate-geosite-rules.ts)7
-rw-r--r--deno/tools/main.ts14
-rw-r--r--deno/tools/manage-service.ts42
-rw-r--r--deno/tools/service.ts180
-rw-r--r--deno/tools/template.ts124
-rw-r--r--deno/tools/vm.ts (renamed from deno/tools/manage-vm.ts)64
-rw-r--r--deno/tools/yargs.ts12
23 files changed, 370 insertions, 324 deletions
diff --git a/deno/base/lib.ts b/deno/base/lib.ts
index 3c69e0a..af75115 100644
--- a/deno/base/lib.ts
+++ b/deno/base/lib.ts
@@ -3,7 +3,7 @@ function camelCaseToKebabCase(str: string): string {
}
function prependNonEmpty<T>(
- object: T,
+ object: T | null | undefined,
prefix: string = " ",
): string {
if (object == null) return "";
diff --git a/deno/deno.json b/deno/deno.json
index 53cdf7a..286451e 100644
--- a/deno/deno.json
+++ b/deno/deno.json
@@ -1,7 +1,7 @@
{
- "workspace": ["./base", "./mail-relay", "./tools"],
+ "workspace": ["./base", "./mail", "./tools"],
"tasks": {
- "compile:mail-relay": "deno task --cwd=mail-relay compile"
+ "compile:mail": "deno task --cwd=mail compile"
},
"imports": {
"@std/collections": "jsr:@std/collections@^1.1.1",
@@ -11,7 +11,6 @@
"@std/io": "jsr:@std/io@^0.225.2",
"@std/path": "jsr:@std/path@^1.1.0",
"@std/testing": "jsr:@std/testing@^1.0.13",
- "@std/dotenv": "jsr:@std/dotenv@^0.225.5",
"@std/fs": "jsr:@std/fs@^1.0.18",
"yargs": "npm:yargs@^18.0.0",
"@types/yargs": "npm:@types/yargs@^17.0.33"
diff --git a/deno/deno.lock b/deno/deno.lock
index 871a9ae..bdc8c3f 100644
--- a/deno/deno.lock
+++ b/deno/deno.lock
@@ -10,7 +10,6 @@
"jsr:@std/collections@^1.1.1": "1.1.1",
"jsr:@std/csv@^1.0.6": "1.0.6",
"jsr:@std/data-structures@^1.0.8": "1.0.8",
- "jsr:@std/dotenv@~0.225.5": "0.225.5",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/expect@^1.0.16": "1.0.16",
@@ -88,9 +87,6 @@
"@std/data-structures@1.0.8": {
"integrity": "2fb7219247e044c8fcd51341788547575653c82ae2c759ff209e0263ba7d9b66"
},
- "@std/dotenv@0.225.5": {
- "integrity": "9ce6f9d0ec3311f74a32535aa1b8c62ed88b1ab91b7f0815797d77a6f60c922f"
- },
"@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
},
@@ -1300,7 +1296,6 @@
"dependencies": [
"jsr:@std/collections@^1.1.1",
"jsr:@std/csv@^1.0.6",
- "jsr:@std/dotenv@~0.225.5",
"jsr:@std/encoding@^1.0.10",
"jsr:@std/expect@^1.0.16",
"jsr:@std/fs@^1.0.18",
@@ -1311,7 +1306,7 @@
"npm:yargs@18"
],
"members": {
- "mail-relay": {
+ "mail": {
"dependencies": [
"jsr:@db/sqlite@0.12",
"npm:@aws-sdk/client-s3@^3.821.0",
diff --git a/deno/mail-relay/aws/mail.ts b/deno/mail-relay/aws/mail.ts
deleted file mode 100644
index 26f3ea0..0000000
--- a/deno/mail-relay/aws/mail.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { MailDeliverContext, MailDeliverHook } from "../mail.ts";
-
-export class AwsMailMessageIdRewriteHook implements MailDeliverHook {
- readonly #lookup;
-
- constructor(lookup: (origin: string) => Promise<string | null>) {
- this.#lookup = lookup;
- }
-
- async callback(context: MailDeliverContext): Promise<void> {
- const addresses = context.mail.simpleFindAllAddresses();
- for (const address of addresses) {
- const awsMessageId = await this.#lookup(address);
- if (awsMessageId != null && awsMessageId.length !== 0) {
- console.info(
- context.logTag,
- `Rewrite address-line string in mail: ${address} => ${awsMessageId}.`,
- );
- context.mail.raw = context.mail.raw.replaceAll(address, awsMessageId);
- }
- }
- }
-}
-
-export class AwsMailMessageIdSaveHook implements MailDeliverHook {
- readonly #record;
-
- constructor(
- record: (
- original: string,
- aws: string,
- context: MailDeliverContext,
- ) => Promise<void>,
- ) {
- this.#record = record;
- }
-
- async callback(context: MailDeliverContext): Promise<void> {
- const { messageId } = context.mail.parsed;
- if (messageId == null) {
- console.warn(
- context.logTag,
- "Original mail doesn't have message id, skip saving message id map.",
- );
- return;
- }
- if (context.result.awsMessageId != null) {
- console.info(
- context.logTag,
- `Save message id map: ${messageId} => ${context.result.awsMessageId}.`,
- );
- context.mail.raw = context.mail.raw.replaceAll(
- messageId,
- context.result.awsMessageId,
- );
- await this.#record(messageId, context.result.awsMessageId, context);
- }
- }
-}
diff --git a/deno/mail-relay/app.ts b/deno/mail/app.ts
index 332c430..2a8c78a 100644
--- a/deno/mail-relay/app.ts
+++ b/deno/mail/app.ts
@@ -49,7 +49,7 @@ export function createHono(outbound: MailDeliverer, inbound: MailDeliverer) {
} else {
const result = await outbound.deliverRaw(body);
return context.json({
- awsMessageId: result.awsMessageId,
+ newMessageId: result.newMessageId,
});
}
});
diff --git a/deno/mail-relay/aws/app.ts b/deno/mail/aws/app.ts
index e3f1114..7e16488 100644
--- a/deno/mail-relay/aws/app.ts
+++ b/deno/mail/aws/app.ts
@@ -13,14 +13,10 @@ import { DbService } from "../db.ts";
import { createHono, createInbound, createSmtp, sendMail } from "../app.ts";
import { DovecotMailDeliverer } from "../dovecot.ts";
import { MailDeliverer } from "../mail.ts";
-import {
- AwsMailMessageIdRewriteHook,
- AwsMailMessageIdSaveHook,
-} from "./mail.ts";
+import { MessageIdRewriteHook, MessageIdSaveHook } from "../mail.ts";
import { AwsMailDeliverer } from "./deliver.ts";
import { AwsMailFetcher, LiveMailNotFoundError } from "./fetch.ts";
-
const PREFIX = "crupest-mail-server";
const CONFIG_DEFINITIONS = {
dataPath: {
@@ -105,12 +101,12 @@ function createOutbound(
) {
const deliverer = new AwsMailDeliverer(awsOptions);
deliverer.preHooks.push(
- new AwsMailMessageIdRewriteHook(db.messageIdToAws.bind(db)),
+ new MessageIdRewriteHook(db.messageIdToNew.bind(db)),
);
deliverer.postHooks.push(
- new AwsMailMessageIdSaveHook(
- async (original, aws, context) => {
- await db.addMessageIdMap({ message_id: original, aws_message_id: aws });
+ new MessageIdSaveHook(
+ async (original, new_message_id, context) => {
+ await db.addMessageIdMap({ message_id: original, new_message_id });
void local?.saveNewSent(context.logTag, context.mail, original);
},
),
@@ -239,8 +235,12 @@ function createServerServices() {
return { ...services, smtp, hono };
}
-function serve(cron: boolean = false) {
- const { config, fetcher, inbound, smtp, hono } = createServerServices();
+async function serve(cron: boolean = false) {
+ const { config, fetcher, inbound, smtp, dbService, hono } =
+ createServerServices();
+
+ await dbService.migrate();
+
smtp.serve({
hostname: config.get("smtpHost"),
port: config.getInt("smtpPort"),
@@ -274,7 +274,7 @@ async function recycleLives() {
if (import.meta.main) {
await yargs(Deno.args)
- .scriptName("mail-relay")
+ .scriptName("mail")
.command({
command: "sendmail",
describe: "send mail via this server's endpoint",
diff --git a/deno/mail-relay/aws/deliver.ts b/deno/mail/aws/deliver.ts
index 0195369..37a871d 100644
--- a/deno/mail-relay/aws/deliver.ts
+++ b/deno/mail/aws/deliver.ts
@@ -6,12 +6,6 @@ import {
import { Mail, MailDeliverContext, MailDeliverer } from "../mail.ts";
-declare module "../mail.ts" {
- interface MailDeliverResult {
- awsMessageId?: string;
- }
-}
-
export class AwsMailDeliverer extends MailDeliverer {
readonly name = "aws";
readonly #aws;
@@ -42,12 +36,12 @@ export class AwsMailDeliverer extends MailDeliverer {
"AWS send-email returned null message id.",
);
} else {
- context.result.awsMessageId =
+ context.result.newMessageId =
`${res.MessageId}@${this.#aws.region}.amazonses.com`;
}
- context.result.smtpMessage =
- `AWS Message ID: ${context.result.awsMessageId}`;
+ context.result.messageForSmtp =
+ `AWS Message ID: ${context.result.newMessageId}`;
context.result.recipients.set("*", {
kind: "success",
message: `Succeeded to call aws send-email api.`,
diff --git a/deno/mail-relay/aws/fetch.ts b/deno/mail/aws/fetch.ts
index 2154972..2154972 100644
--- a/deno/mail-relay/aws/fetch.ts
+++ b/deno/mail/aws/fetch.ts
diff --git a/deno/mail-relay/db.test.ts b/deno/mail/db.test.ts
index 60035c4..8a9ad27 100644
--- a/deno/mail-relay/db.test.ts
+++ b/deno/mail/db.test.ts
@@ -6,17 +6,17 @@ import { DbService } from "./db.ts";
describe("DbService", () => {
const mockRow = {
message_id: "mock-message-id@mock.mock",
- aws_message_id: "mock-aws-message-id@mock.mock",
+ new_message_id: "mock-new-message-id@mock.mock",
};
it("works", async () => {
const db = new DbService(":memory:");
await db.migrate();
await db.addMessageIdMap(mockRow);
- expect(await db.messageIdToAws(mockRow.message_id)).toBe(
- mockRow.aws_message_id,
+ expect(await db.messageIdToNew(mockRow.message_id)).toBe(
+ mockRow.new_message_id,
);
- expect(await db.messageIdFromAws(mockRow.aws_message_id)).toBe(
+ expect(await db.messageIdFromNew(mockRow.new_message_id)).toBe(
mockRow.message_id,
);
});
diff --git a/deno/mail-relay/db.ts b/deno/mail/db.ts
index 062700b..e41f762 100644
--- a/deno/mail-relay/db.ts
+++ b/deno/mail/db.ts
@@ -53,14 +53,14 @@ class SqliteDatabaseAdapter implements SqliteDatabase {
export class DbError extends Error {}
-interface AwsMessageIdMapTable {
+interface MessageIdMapTable {
id: Generated<number>;
message_id: string;
- aws_message_id: string;
+ new_message_id: string;
}
interface Database {
- aws_message_id_map: AwsMessageIdMapTable;
+ message_id_map: MessageIdMapTable;
}
const migrations: Record<string, Migration> = {
@@ -68,16 +68,16 @@ const migrations: Record<string, Migration> = {
// deno-lint-ignore no-explicit-any
async up(db: Kysely<any>): Promise<void> {
await db.schema
- .createTable("aws_message_id_map")
+ .createTable("message_id_map")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
.addColumn("message_id", "text", (col) => col.notNull().unique())
- .addColumn("aws_message_id", "text", (col) => col.notNull().unique())
+ .addColumn("new_message_id", "text", (col) => col.notNull().unique())
.execute();
- for (const column of ["message_id", "aws_message_id"]) {
+ for (const column of ["message_id", "new_message_id"]) {
await db.schema
- .createIndex(`aws_message_id_map_${column}`)
- .on("aws_message_id_map")
+ .createIndex(`message_id_map_${column}`)
+ .on("message_id_map")
.column(column)
.execute();
}
@@ -85,7 +85,7 @@ const migrations: Record<string, Migration> = {
// deno-lint-ignore no-explicit-any
async down(db: Kysely<any>): Promise<void> {
- await db.schema.dropTable("aws_message_id_map").execute();
+ await db.schema.dropTable("message_id_map").execute();
},
},
};
@@ -117,28 +117,28 @@ export class DbService {
}
async addMessageIdMap(
- mail: Insertable<AwsMessageIdMapTable>,
+ mail: Insertable<MessageIdMapTable>,
): Promise<number> {
const inserted = await this.#kysely
- .insertInto("aws_message_id_map")
+ .insertInto("message_id_map")
.values(mail)
.executeTakeFirstOrThrow();
return Number(inserted.insertId!);
}
- async messageIdToAws(messageId: string): Promise<string | null> {
+ async messageIdToNew(messageId: string): Promise<string | null> {
const row = await this.#kysely
- .selectFrom("aws_message_id_map")
+ .selectFrom("message_id_map")
.where("message_id", "=", messageId)
- .select("aws_message_id")
+ .select("new_message_id")
.executeTakeFirst();
- return row?.aws_message_id ?? null;
+ return row?.new_message_id ?? null;
}
- async messageIdFromAws(awsMessageId: string): Promise<string | null> {
+ async messageIdFromNew(newMessageId: string): Promise<string | null> {
const row = await this.#kysely
- .selectFrom("aws_message_id_map")
- .where("aws_message_id", "=", awsMessageId)
+ .selectFrom("message_id_map")
+ .where("new_message_id", "=", newMessageId)
.select("message_id")
.executeTakeFirst();
return row?.message_id ?? null;
diff --git a/deno/mail-relay/deno.json b/deno/mail/deno.json
index 9105747..86a8999 100644
--- a/deno/mail-relay/deno.json
+++ b/deno/mail/deno.json
@@ -2,7 +2,7 @@
"version": "0.1.0",
"tasks": {
"run": "deno run -A aws/app.ts",
- "compile": "deno compile -o out/crupest-relay -A aws/app.ts"
+ "compile": "deno compile -o out/crupest-mail -A aws/app.ts"
},
"imports": {
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.821.0",
diff --git a/deno/mail-relay/dovecot.ts b/deno/mail/dovecot.ts
index 4fe2f41..c0d56a2 100644
--- a/deno/mail-relay/dovecot.ts
+++ b/deno/mail/dovecot.ts
@@ -17,6 +17,7 @@ async function runCommand(
logTag: string;
args: string[];
stdin?: Uint8Array;
+ suppressStartLog?: boolean;
suppressResultLog?: boolean;
errorCodeMessageMap?: Map<number, string>;
},
@@ -24,7 +25,9 @@ async function runCommand(
const { logTag, args, stdin, suppressResultLog, errorCodeMessageMap } =
options;
- console.info(logTag, `Run external command ${bin} ${args.join(" ")}`);
+ if (options.suppressResultLog !== true) {
+ console.info(logTag, `Run external command ${bin} ${args.join(" ")}`);
+ }
try {
// Create and spawn process.
@@ -129,10 +132,13 @@ export class DovecotMailDeliverer extends MailDeliverer {
user: string,
mailbox: string,
messageId: string,
+ noLog?: boolean,
): Promise<void> {
await runCommand(this.#doveadmPath, {
logTag,
args: ["expunge", "-u", user, ...this.#queryArgs(mailbox, messageId)],
+ suppressStartLog: noLog,
+ suppressResultLog: noLog,
});
}
@@ -202,11 +208,11 @@ export class DovecotMailDeliverer extends MailDeliverer {
console.info(
logTag,
- "Schedule deletion of old mails at 15,30,60 seconds later.",
+ "Schedule deletion of old mails (no logging) at 5,15,30,60 seconds later.",
);
- [15, 30, 60].forEach((seconds) =>
+ [5, 15, 30, 60].forEach((seconds) =>
setTimeout(() => {
- void this.#deleteMail(logTag, from, "Sent", messageIdToDelete);
+ void this.#deleteMail(logTag, from, "Sent", messageIdToDelete, true);
}, 1000 * seconds)
);
}
diff --git a/deno/mail-relay/dumb-smtp-server.ts b/deno/mail/dumb-smtp-server.ts
index 70d5ec0..c3ebf5d 100644
--- a/deno/mail-relay/dumb-smtp-server.ts
+++ b/deno/mail/dumb-smtp-server.ts
@@ -91,8 +91,8 @@ export class DumbSmtpServer {
if (line === ".") {
try {
console.info(logTag, "Mail data received, begin to relay...");
- const { smtpMessage } = await this.#deliverer.deliverRaw(rawMail);
- await send(`250 2.6.0 ${smtpMessage}`);
+ const result = await this.#deliverer.deliverRaw(rawMail);
+ await send(`250 2.6.0 ${result.generateMessageForSmtp()}`);
rawMail = null;
} catch (err) {
console.error(logTag, "Relay failed.", err);
diff --git a/deno/mail-relay/mail-parsing.ts b/deno/mail/mail-parsing.ts
index 8e9697d..8e9697d 100644
--- a/deno/mail-relay/mail-parsing.ts
+++ b/deno/mail/mail-parsing.ts
diff --git a/deno/mail-relay/mail.test.ts b/deno/mail/mail.test.ts
index a8204be..a8204be 100644
--- a/deno/mail-relay/mail.test.ts
+++ b/deno/mail/mail.test.ts
diff --git a/deno/mail-relay/mail.ts b/deno/mail/mail.ts
index 9cc591c..b88ce2b 100644
--- a/deno/mail-relay/mail.ts
+++ b/deno/mail/mail.ts
@@ -1,5 +1,8 @@
import { encodeBase64 } from "@std/encoding/base64";
import { parse } from "@std/csv/parse";
+
+import { StringUtils } from "@crupest/base";
+
import { simpleParseMail } from "./mail-parsing.ts";
export class Mail {
@@ -47,7 +50,9 @@ export interface MailDeliverRecipientResult {
export class MailDeliverResult {
message?: string;
- smtpMessage?: string;
+ messageForSmtp?: string;
+ newMessageId?: string;
+
recipients = new Map<string, MailDeliverRecipientResult>();
constructor(public mail: Mail) {}
@@ -58,8 +63,8 @@ export class MailDeliverResult {
generateLogMessage(prefix: string) {
const lines = [];
if (this.message != null) lines.push(`${prefix} message: ${this.message}`);
- if (this.smtpMessage != null) {
- lines.push(`${prefix} smtpMessage: ${this.smtpMessage}`);
+ if (this.messageForSmtp != null) {
+ lines.push(`${prefix} smtpMessage: ${this.messageForSmtp}`);
}
for (const [name, result] of this.recipients.entries()) {
const { kind, message } = result;
@@ -67,6 +72,13 @@ export class MailDeliverResult {
}
return lines.join("\n");
}
+
+ generateMessageForSmtp(): string {
+ if (this.messageForSmtp != null) return this.messageForSmtp;
+ return `2.0.0 OK${
+ StringUtils.prependNonEmpty(this.newMessageId)
+ } Message accepted for delivery`;
+ }
}
export class MailDeliverContext {
@@ -232,3 +244,61 @@ export class AliasRecipientMailHook implements MailDeliverHook {
}
}
}
+
+export class MessageIdRewriteHook implements MailDeliverHook {
+ readonly #lookup;
+
+ constructor(lookup: (origin: string) => Promise<string | null>) {
+ this.#lookup = lookup;
+ }
+
+ async callback(context: MailDeliverContext): Promise<void> {
+ const addresses = context.mail.simpleFindAllAddresses();
+ for (const address of addresses) {
+ const newMessageId = await this.#lookup(address);
+ if (newMessageId != null && newMessageId.length !== 0) {
+ console.info(
+ context.logTag,
+ `Rewrite address-line string in mail: ${address} => ${newMessageId}.`,
+ );
+ context.mail.raw = context.mail.raw.replaceAll(address, newMessageId);
+ }
+ }
+ }
+}
+
+export class MessageIdSaveHook implements MailDeliverHook {
+ readonly #record;
+
+ constructor(
+ record: (
+ original: string,
+ newMessageId: string,
+ context: MailDeliverContext,
+ ) => Promise<void>,
+ ) {
+ this.#record = record;
+ }
+
+ async callback(context: MailDeliverContext): Promise<void> {
+ const { messageId } = context.mail.parsed;
+ if (messageId == null) {
+ console.warn(
+ context.logTag,
+ "Original mail doesn't have message id, skip saving message id map.",
+ );
+ return;
+ }
+ if (context.result.newMessageId != null) {
+ console.info(
+ context.logTag,
+ `Save message id map: ${messageId} => ${context.result.newMessageId}.`,
+ );
+ context.mail.raw = context.mail.raw.replaceAll(
+ messageId,
+ context.result.newMessageId,
+ );
+ await this.#record(messageId, context.result.newMessageId, context);
+ }
+ }
+}
diff --git a/deno/tools/generate-geosite-rules.ts b/deno/tools/geosite.ts
index bfa53ba..3aabec2 100644
--- a/deno/tools/generate-geosite-rules.ts
+++ b/deno/tools/geosite.ts
@@ -1,4 +1,3 @@
-const PROXY_NAME = "node-select";
const ATTR = "cn";
const REPO_NAME = "domain-list-community";
const URL =
@@ -152,8 +151,10 @@ if (import.meta.main) {
const rules = extract(SITES, provider);
const [has, notHas] = toNewFormat(rules, ATTR);
- const hasFile = tmpDir + "/has-rule";
- const notHasFile = tmpDir + "/not-has-rule";
+ const resultDir = tmpDir + "/result";
+ Deno.mkdirSync(resultDir);
+ const hasFile = resultDir + "/has-rule";
+ const notHasFile = resultDir + "/not-has-rule";
console.log("Write result to: " + hasFile + " , " + notHasFile);
Deno.writeTextFileSync(hasFile, has);
Deno.writeTextFileSync(notHasFile, notHas);
diff --git a/deno/tools/main.ts b/deno/tools/main.ts
new file mode 100644
index 0000000..897350c
--- /dev/null
+++ b/deno/tools/main.ts
@@ -0,0 +1,14 @@
+import yargs, { DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
+import vm from "./vm.ts";
+import service from "./service.ts";
+
+if (import.meta.main) {
+ await yargs(Deno.args)
+ .scriptName("crupest")
+ .command(vm)
+ .command(service)
+ .demandCommand(1, DEMAND_COMMAND_MESSAGE)
+ .help()
+ .strict()
+ .parse();
+}
diff --git a/deno/tools/manage-service.ts b/deno/tools/manage-service.ts
deleted file mode 100644
index 148f55a..0000000
--- a/deno/tools/manage-service.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { join } from "@std/path";
-// @ts-types="npm:@types/yargs"
-import yargs from "yargs";
-
-import { TemplateDir } from "./template.ts";
-
-if (import.meta.main) {
- await yargs(Deno.args)
- .scriptName("manage-service")
- .option("project-dir", {
- type: "string",
- })
- .demandOption("project-dir")
- .command({
- command: "gen-tmpl",
- describe: "generate files for templates",
- builder: (builder) => {
- return builder
- .option("dry-run", {
- type: "boolean",
- default: true,
- })
- .strict();
- },
- handler: (argv) => {
- const { projectDir, dryRun } = argv;
- new TemplateDir(
- join(projectDir, "services/templates"),
- ).generateWithVariableFiles(
- [
- join(projectDir, "data/config"),
- join(projectDir, "services/config.template"),
- ],
- dryRun ? undefined : join(projectDir, "services/generated"),
- );
- },
- })
- .demandCommand(1, "One command must be specified.")
- .help()
- .strict()
- .parse();
-}
diff --git a/deno/tools/service.ts b/deno/tools/service.ts
new file mode 100644
index 0000000..bd4d22c
--- /dev/null
+++ b/deno/tools/service.ts
@@ -0,0 +1,180 @@
+import { dirname, join, relative } from "@std/path";
+import { copySync, existsSync, walkSync } from "@std/fs";
+import { distinct } from "@std/collections";
+// @ts-types="npm:@types/mustache"
+import Mustache from "mustache";
+
+import { defineYargsModule, DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
+
+const MUSTACHE_RENDER_OPTIONS: Mustache.RenderOptions = {
+ tags: ["@@", "@@"],
+ escape: (value: unknown) => String(value),
+};
+
+function mustacheParse(template: string) {
+ return Mustache.parse(template, MUSTACHE_RENDER_OPTIONS.tags);
+}
+
+function mustacheRender(template: string, view: Record<string, string>) {
+ return Mustache.render(template, view, {}, MUSTACHE_RENDER_OPTIONS);
+}
+
+function getVariableKeysOfTemplate(template: string): string[] {
+ return distinct(
+ mustacheParse(template)
+ .filter((v) => v[0] === "name")
+ .map((v) => v[1]),
+ );
+}
+
+function loadTemplatedConfigFiles(
+ files: string[],
+): Record<string, string> {
+ console.log("Scan config files ...");
+ const config: Record<string, string> = {};
+ for (const file of files) {
+ console.log(` from file ${file}`);
+ const text = Deno.readTextFileSync(file);
+ let lineNumber = 0;
+ for (const rawLine of text.split("\n")) {
+ lineNumber++;
+ const line = rawLine.trim();
+ if (line.length === 0) continue;
+ if (line.startsWith("#")) continue;
+ const equalSymbolIndex = line.indexOf("=");
+ if (equalSymbolIndex === -1) {
+ throw new Error(`Line ${lineNumber} of ${file} is invalid.`);
+ }
+ const [key, valueText] = [
+ line.slice(0, equalSymbolIndex).trim(),
+ line.slice(equalSymbolIndex + 1).trim(),
+ ];
+ console.log(` (${key in config ? "override" : "new"}) ${key}`);
+ getVariableKeysOfTemplate(valueText).forEach((name) => {
+ if (!(name in config)) {
+ throw new Error(
+ `Variable ${name} is not defined yet, perhaps due to typos or wrong order.`,
+ );
+ }
+ });
+ config[key] = mustacheRender(valueText, config);
+ }
+ }
+ return config;
+}
+
+const TEMPLATE_FILE_EXT = ".template";
+
+class TemplateDir {
+ templates: { path: string; ext: string; text: string; vars: string[] }[] = [];
+ plains: { path: string }[] = [];
+
+ constructor(public dir: string) {
+ console.log(`Scan template dir ${dir} ...`);
+ Array.from(
+ walkSync(dir, { includeDirs: false, followSymlinks: true }),
+ ).forEach(({ path }) => {
+ path = relative(this.dir, path);
+ if (path.endsWith(TEMPLATE_FILE_EXT)) {
+ console.log(` (template) ${path}`);
+ const text = Deno.readTextFileSync(join(dir, path));
+ this.templates.push({
+ path,
+ ext: TEMPLATE_FILE_EXT,
+ text,
+ vars: getVariableKeysOfTemplate(text),
+ });
+ } else {
+ console.log(` (plain) ${path}`);
+ this.plains.push({ path });
+ }
+ });
+ }
+
+ allNeededVars() {
+ return distinct(this.templates.flatMap((t) => t.vars));
+ }
+
+ generate(vars: Record<string, string>, generatedDir?: string) {
+ console.log(
+ `Generate to dir ${generatedDir ?? "[dry-run]"} ...`,
+ );
+
+ const undefinedVars = this.allNeededVars().filter((v) => !(v in vars));
+ if (undefinedVars.length !== 0) {
+ throw new Error(
+ `Needed variables are not defined: ${undefinedVars.join(", ")}`,
+ );
+ }
+
+ if (generatedDir != null) {
+ if (existsSync(generatedDir)) {
+ console.log(` delete old generated dir`);
+ Deno.removeSync(generatedDir, { recursive: true });
+ }
+
+ for (const file of this.plains) {
+ const [source, destination] = [
+ join(this.dir, file.path),
+ join(generatedDir, file.path),
+ ];
+ console.log(` copy ${file.path}`);
+ Deno.mkdirSync(dirname(destination), { recursive: true });
+ copySync(source, destination);
+ }
+ for (const file of this.templates) {
+ const path = file.path.slice(0, -file.ext.length);
+ const destination = join(generatedDir, path);
+ console.log(` generate ${path}`);
+ const rendered = mustacheRender(file.text, vars);
+ Deno.mkdirSync(dirname(destination), { recursive: true });
+ Deno.writeTextFileSync(destination, rendered);
+ }
+ }
+ }
+}
+
+export default defineYargsModule({
+ command: "service",
+ aliases: ["sv"],
+ describe: "Manage services.",
+ builder: (builder) => {
+ return builder
+ .option("project-dir", {
+ type: "string",
+ })
+ .demandOption("project-dir")
+ .command({
+ command: "gen-tmpl",
+ describe: "Generate files from templates",
+ builder: (builder) => {
+ return builder
+ .option("dry-run", {
+ type: "boolean",
+ default: true,
+ })
+ .strict();
+ },
+ handler: (argv) => {
+ const { projectDir, dryRun } = argv;
+
+ const config = loadTemplatedConfigFiles(
+ [
+ join(projectDir, "data/config"),
+ join(projectDir, "services/config.template"),
+ ],
+ );
+
+ new TemplateDir(
+ join(projectDir, "services/templates"),
+ ).generate(
+ config,
+ dryRun ? undefined : join(projectDir, "services/generated"),
+ );
+ console.log("Done!");
+ },
+ })
+ .demandCommand(1, DEMAND_COMMAND_MESSAGE);
+ },
+ handler: () => {},
+});
diff --git a/deno/tools/template.ts b/deno/tools/template.ts
deleted file mode 100644
index 1b67eb8..0000000
--- a/deno/tools/template.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { dirname, join, relative } from "@std/path";
-import { copySync, existsSync, walkSync } from "@std/fs";
-import { parse } from "@std/dotenv";
-import { distinct } from "@std/collections";
-// @ts-types="npm:@types/mustache"
-import Mustache from "mustache";
-
-Mustache.tags = ["@@", "@@"];
-Mustache.escape = (value) => String(value);
-
-function getVariableKeys(original: string): string[] {
- return distinct(
- Mustache.parse(original)
- .filter(function (v) {
- return v[0] === "name";
- })
- .map(function (v) {
- return v[1];
- }),
- );
-}
-
-export function loadVariables(files: string[]): Record<string, string> {
- const vars: Record<string, string> = {};
- for (const file of files) {
- const text = Deno.readTextFileSync(file);
- for (const [key, valueText] of Object.entries(parse(text))) {
- getVariableKeys(valueText).forEach((name) => {
- if (!(name in vars)) {
- throw new Error(
- `Variable ${name} is not defined yet, perhaps due to typos or wrong order.`,
- );
- }
- });
- vars[key] = Mustache.render(valueText, vars);
- }
- }
- return vars;
-}
-
-const TEMPLATE_FILE_EXT = ".template";
-
-export class TemplateDir {
- templates: { path: string; ext: string; text: string; vars: string[] }[] = [];
- plains: { path: string }[] = [];
-
- constructor(public dir: string) {
- console.log("Scanning template dir:");
- Array.from(
- walkSync(dir, { includeDirs: false, followSymlinks: true }),
- ).forEach(({ path }) => {
- path = relative(this.dir, path);
- if (path.endsWith(TEMPLATE_FILE_EXT)) {
- console.log(` (template) ${path}`);
- const text = Deno.readTextFileSync(join(dir, path));
- this.templates.push({
- path,
- ext: TEMPLATE_FILE_EXT,
- text,
- vars: getVariableKeys(text),
- });
- } else {
- console.log(` (plain) ${path}`);
- this.plains.push({ path });
- }
- });
- console.log("Done scanning template dir.");
- }
-
- allNeededVars() {
- return distinct(this.templates.flatMap((t) => t.vars));
- }
-
- generate(vars: Record<string, string>, generatedDir?: string) {
- console.log(
- `Generating, template dir: ${this.dir}, generated dir: ${
- generatedDir ?? "[dry-run]"
- }:`,
- );
-
- const undefinedVars = this.allNeededVars().filter((v) => !(v in vars));
- if (undefinedVars.length !== 0) {
- throw new Error(
- `Needed variables are not defined: ${undefinedVars.join(", ")}`,
- );
- }
-
- if (generatedDir != null) {
- if (existsSync(generatedDir)) {
- console.log(` delete old generated dir ${generatedDir}`);
- Deno.removeSync(generatedDir, { recursive: true });
- }
-
- for (const file of this.plains) {
- const [source, destination] = [
- join(this.dir, file.path),
- join(generatedDir, file.path),
- ];
- console.log(` copy ${source} to ${destination} ...`);
- Deno.mkdirSync(dirname(destination), { recursive: true });
- copySync(source, destination);
- }
- for (const file of this.templates) {
- const [source, destination] = [
- join(this.dir, file.path),
- join(generatedDir, file.path.slice(0, -file.ext.length)),
- ];
- console.log(` generate ${source} to ${destination} ...`);
- const rendered = Mustache.render(file.text, vars);
- Deno.mkdirSync(dirname(destination), { recursive: true });
- Deno.writeTextFileSync(destination, rendered);
- }
- }
- console.log(`Done generating.`);
- }
-
- generateWithVariableFiles(varFiles: string[], generatedDir?: string) {
- console.log("Scanning defined vars:");
- const vars = loadVariables(varFiles);
- Object.keys(vars).forEach((name) => console.log(` ${name}`));
- console.log("Done scanning defined vars.");
- this.generate(vars, generatedDir);
- }
-}
diff --git a/deno/tools/manage-vm.ts b/deno/tools/vm.ts
index bb985ce..17e8125 100644
--- a/deno/tools/manage-vm.ts
+++ b/deno/tools/vm.ts
@@ -1,7 +1,6 @@
import os from "node:os";
import { join } from "@std/path";
-// @ts-types="npm:@types/yargs"
-import yargs from "yargs";
+import { defineYargsModule, DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
type ArchAliasMap = { [name: string]: string[] };
const arches = {
@@ -112,33 +111,34 @@ function createQemuArgs(setup: VmSetup): string[] {
];
}
-if (import.meta.main) {
- await yargs(Deno.args)
- .scriptName("manage-vm")
- .command({
- command: "gen <name>",
- describe: "generate cli command to run the vm",
- builder: (builder) => {
- return builder
- .positional("name", {
- describe: "name of the vm to run",
- type: "string",
- })
- .demandOption("name")
- .strict();
- },
- handler: (argv) => {
- const vm = resolveVmSetup(argv.name, MY_VMS);
- if (vm == null) {
- console.error(`No vm called ${argv.name} is found.`);
- Deno.exit(-1);
- }
- const cli = createQemuArgs(vm);
- console.log(`${cli.join(" ")}`);
- },
- })
- .demandCommand(1, "One command must be specified.")
- .help()
- .strict()
- .parse();
-}
+const gen = defineYargsModule({
+ command: "gen <name>",
+ describe: "generate cli command to run the vm",
+ builder: (builder) => {
+ return builder
+ .positional("name", {
+ describe: "name of the vm to run",
+ type: "string",
+ })
+ .demandOption("name")
+ .strict();
+ },
+ handler: (argv) => {
+ const vm = resolveVmSetup(argv.name, MY_VMS);
+ if (vm == null) {
+ console.error(`No vm called ${argv.name} is found.`);
+ Deno.exit(-1);
+ }
+ const cli = createQemuArgs(vm);
+ console.log(`${cli.join(" ")}`);
+ },
+});
+
+export default defineYargsModule({
+ command: "vm",
+ describe: "Manage (qemu) virtual machines.",
+ builder: (builder) => {
+ return builder.command(gen).demandCommand(1, DEMAND_COMMAND_MESSAGE);
+ },
+ handler: () => {},
+});
diff --git a/deno/tools/yargs.ts b/deno/tools/yargs.ts
new file mode 100644
index 0000000..eaa7803
--- /dev/null
+++ b/deno/tools/yargs.ts
@@ -0,0 +1,12 @@
+// @ts-types="npm:@types/yargs"
+export { default } from "yargs";
+export * from "yargs";
+
+import { CommandModule } from "yargs";
+export function defineYargsModule<T, U>(
+ module: CommandModule<T, U>,
+): CommandModule<T, U> {
+ return module;
+}
+
+export const DEMAND_COMMAND_MESSAGE = "No command is specified";