From d9c774060c7b24b29553cde0f82619d1697aa861 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Wed, 22 Jul 2026 17:22:32 +0900
Subject: [PATCH 1/4] Add `fmt:license` task
---
mise.toml | 14 ++++++++++++++
scripts/add-license/gpl.mts | 18 ++++++++++++++++++
scripts/add-license/gpl.txt | 15 +++++++++++++++
scripts/add-license/main.mts | 21 +++++++++++++++++++++
4 files changed, 68 insertions(+)
create mode 100644 scripts/add-license/gpl.mts
create mode 100644 scripts/add-license/gpl.txt
create mode 100644 scripts/add-license/main.mts
diff --git a/mise.toml b/mise.toml
index d213465..e806049 100644
--- a/mise.toml
+++ b/mise.toml
@@ -46,6 +46,13 @@ run = "hongdown --check"
description = "Check mise.toml formatting"
run = "mise fmt --check"
+[tasks."check:license"]
+description = "Check the license comments of files in the whole codebase."
+run = """
+#!/usr/bin/env nu
+node scripts/add-license/main.mts --check
+"""
+
[tasks."check:versions"]
description = "Check that all package versions are in sync"
run = "node scripts/check-versions.mts"
@@ -63,6 +70,13 @@ run = "hongdown --write"
description = "Format mise.toml"
run = "mise fmt"
+[tasks."fmt:license"]
+description = "Add license comments to the files that don't have them."
+run = """
+#!/usr/bin/env nu
+node scripts/add-license/main.mts
+"""
+
[tasks.build]
description = "Build the project"
run = "pnpm run --parallel --recursive build"
diff --git a/scripts/add-license/gpl.mts b/scripts/add-license/gpl.mts
new file mode 100644
index 0000000..861d7e5
--- /dev/null
+++ b/scripts/add-license/gpl.mts
@@ -0,0 +1,18 @@
+import { readFile } from "node:fs/promises";
+
+const GPL = (await readFile("./gpl.txt", { encoding: "utf-8" })).split("\n");
+const JS_GPL = GPL.map((line) => `// ${line}`).join("\n");
+const CSS_GPL = ["/*", ...GPL, "*/"].join("\n");
+const GPL_BY_EXT = {
+ ts: JS_GPL,
+ tsx: JS_GPL,
+ mts: JS_GPL,
+ cts: JS_GPL,
+ js: JS_GPL,
+ jsx: JS_GPL,
+ mjs: JS_GPL,
+ cjs: JS_GPL,
+ css: CSS_GPL,
+};
+
+export default GPL_BY_EXT;
diff --git a/scripts/add-license/gpl.txt b/scripts/add-license/gpl.txt
new file mode 100644
index 0000000..3521ade
--- /dev/null
+++ b/scripts/add-license/gpl.txt
@@ -0,0 +1,15 @@
+DrFed: A web-based platform for developing and debugging ActivityPub apps
+Copyright (C) 2026 DrFed team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
diff --git a/scripts/add-license/main.mts b/scripts/add-license/main.mts
new file mode 100644
index 0000000..a6c9284
--- /dev/null
+++ b/scripts/add-license/main.mts
@@ -0,0 +1,21 @@
+import { readFile, glob } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import GPL from "./gpl.mts";
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
+const INCLUDED = ["scripts/", "packages/*/src/"];
+const EXCLUDED = ["**/dist/"];
+
+interface AddLicenseProps {
+ check?: true | undefined;
+}
+
+export default async function addLicense(opt: AddLicenseProps) {
+ //
+}
+
+if (import.meta.main) {
+ await addLicense({});
+}
From eee569576354a49f2f01c390edc586747e9f298d Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Wed, 22 Jul 2026 20:47:44 +0900
Subject: [PATCH 2/4] Generate `addLicense`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prompt:
"""
현재 코드 베이스는 GPL 3.0 하에 배포되기 때문에 모든 소스 코드 파일이 GPL 주석으로 시작되어야 합니다.
이를 검사하고 자동으로 채워넣어주는 `addLicense` 를 완성하세요.
1. `INCLUDED` 에 포함되는 모든 파일을 찾습니다.
2. `GPL` 의 키를 확장자로 가진 파일만 추려냅니다.
3. `EXCLUDED` 에 포함되는 파일은 걸러냅니다.
4. 파일에서 `GPL[ext]`의 줄만큼 읽습니다. (CSS 의 경우 17줄, 그외에는 15줄)
5. 해당 내용이 `GPL[ext]` 와 동일하지 않은 파일을 추려냅니다.
6-1. 그런 파일이 없다면 exit 0 로 종료합니다.
6-2. 하나의 파일이라도 문제가 있는 경우 다음을 진행합니다.
7-1. `check === true` 인 경우 문제가 있는 파일들의 목록을 출력한 뒤 exit 1 로 종료합니다.
7-2. 아닐 경우 해당하는 파일들의 서두에 `GPL[ext]` 를 추가합니다.
"""
Output: changes in this commit
Assisted-by: Claude Code:claude-sonnet-5
---
scripts/add-license/gpl.mts | 11 +++-
scripts/add-license/main.mts | 98 ++++++++++++++++++++++++++++++++++--
2 files changed, 102 insertions(+), 7 deletions(-)
diff --git a/scripts/add-license/gpl.mts b/scripts/add-license/gpl.mts
index 861d7e5..4d64916 100644
--- a/scripts/add-license/gpl.mts
+++ b/scripts/add-license/gpl.mts
@@ -1,7 +1,14 @@
import { readFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
-const GPL = (await readFile("./gpl.txt", { encoding: "utf-8" })).split("\n");
-const JS_GPL = GPL.map((line) => `// ${line}`).join("\n");
+const GPL_TXT_PATH = join(dirname(fileURLToPath(import.meta.url)), "gpl.txt");
+const GPL = (await readFile(GPL_TXT_PATH, { encoding: "utf-8" }))
+ .trimEnd()
+ .split("\n");
+const JS_GPL = GPL.map((line) => (line === "" ? "//" : `// ${line}`)).join(
+ "\n",
+);
const CSS_GPL = ["/*", ...GPL, "*/"].join("\n");
const GPL_BY_EXT = {
ts: JS_GPL,
diff --git a/scripts/add-license/main.mts b/scripts/add-license/main.mts
index a6c9284..7b0b866 100644
--- a/scripts/add-license/main.mts
+++ b/scripts/add-license/main.mts
@@ -1,21 +1,109 @@
-import { readFile, glob } from "node:fs/promises";
-import { dirname, join } from "node:path";
+// oxlint-disable no-console no-magic-numbers
+import { glob, readFile, writeFile } from "node:fs/promises";
+import { dirname, extname, join, relative } from "node:path";
+import process from "node:process";
import { fileURLToPath } from "node:url";
import GPL from "./gpl.mts";
-const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const INCLUDED = ["scripts/", "packages/*/src/"];
const EXCLUDED = ["**/dist/"];
+type Extension = keyof typeof GPL;
+
interface AddLicenseProps {
check?: true | undefined;
}
+interface LicenseTarget {
+ readonly path: string;
+ readonly extension: Extension;
+}
+
+interface LicenseCheck extends LicenseTarget {
+ readonly hasLicense: boolean;
+}
+
export default async function addLicense(opt: AddLicenseProps) {
- //
+ const files = await findIncludedFiles();
+ const targets = files
+ .map((path) => ({ path, extension: extensionOf(path) }))
+ .filter(isLicenseTarget);
+ const checks = await Promise.all(targets.map(checkLicense));
+ const missing = checks.filter((check) => !check.hasLicense);
+
+ if (missing.length === 0) {
+ process.exit(0);
+ }
+
+ if (opt.check) {
+ for (const file of missing) {
+ console.log(relative(ROOT, file.path));
+ }
+ process.exit(1);
+ }
+
+ await Promise.all(missing.map((file) => addLicenseHeader(file)));
+}
+
+async function findIncludedFiles(): Promise {
+ const patterns = INCLUDED.map((dir) => `${dir}**/*`);
+ const files: string[] = [];
+ for await (const entry of glob(patterns, {
+ cwd: ROOT,
+ exclude: EXCLUDED,
+ withFileTypes: true,
+ })) {
+ if (entry.isFile()) {
+ files.push(join(entry.parentPath, entry.name));
+ }
+ }
+ return files;
+}
+
+function extensionOf(path: string): string {
+ return extname(path).slice(1);
+}
+
+function isLicenseTarget(
+ file: Readonly<{ path: string; extension: string }>,
+): file is LicenseTarget {
+ return Object.hasOwn(GPL, file.extension);
+}
+
+async function readLines(path: string): Promise {
+ const content = await readFile(path, { encoding: "utf-8" });
+ return content.split("\n");
+}
+
+function shebangOffset(lines: readonly string[]): number {
+ return lines[0]?.startsWith("#!") ? 1 : 0;
+}
+
+async function checkLicense(target: LicenseTarget): Promise {
+ const header = GPL[target.extension];
+ const lines = await readLines(target.path);
+ const offset = shebangOffset(lines);
+ const headerLineCount = header.split("\n").length;
+ const actualHeader = lines.slice(offset, offset + headerLineCount).join("\n");
+ return { ...target, hasLicense: actualHeader === header };
+}
+
+async function addLicenseHeader(target: LicenseTarget): Promise {
+ const header = GPL[target.extension];
+ const lines = await readLines(target.path);
+ const offset = shebangOffset(lines);
+ const updatedLines = [
+ ...lines.slice(0, offset),
+ ...header.split("\n"),
+ ...lines.slice(offset),
+ ];
+ await writeFile(target.path, updatedLines.join("\n"), { encoding: "utf-8" });
}
if (import.meta.main) {
- await addLicense({});
+ await addLicense({
+ check: process.argv.includes("--check") ? true : undefined,
+ });
}
From 48a1975c733101a1681accdb4a4c0a461ffdfc6d Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Wed, 22 Jul 2026 20:49:22 +0900
Subject: [PATCH 3/4] Add license comments with `fmt:license` task
---
scripts/add-license/gpl.mts | 15 +++++++++++++++
scripts/add-license/main.mts | 15 +++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/scripts/add-license/gpl.mts b/scripts/add-license/gpl.mts
index 4d64916..59e0750 100644
--- a/scripts/add-license/gpl.mts
+++ b/scripts/add-license/gpl.mts
@@ -1,3 +1,18 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
diff --git a/scripts/add-license/main.mts b/scripts/add-license/main.mts
index 7b0b866..8732c12 100644
--- a/scripts/add-license/main.mts
+++ b/scripts/add-license/main.mts
@@ -1,3 +1,18 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
// oxlint-disable no-console no-magic-numbers
import { glob, readFile, writeFile } from "node:fs/promises";
import { dirname, extname, join, relative } from "node:path";
From 534e890f17825ac045f2cbd6df39375fd08edb67 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Wed, 22 Jul 2026 22:18:12 +0900
Subject: [PATCH 4/4] Refine AI-generated code
---
.oxlintrc.json | 1 +
scripts/add-license/gpl.mts | 1 +
scripts/add-license/main.mts | 113 +++++++++++++++++++----------------
3 files changed, 62 insertions(+), 53 deletions(-)
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 90fce46..1732b44 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -29,6 +29,7 @@
"eslint/no-eq-null": "off",
"eslint/no-magic-numbers": "warn",
"eslint/no-ternary": "off",
+ "eslint/no-nested-ternary": "off",
"eslint/no-undefined": "off",
"eslint/no-void": "off",
"eslint/no-warning-comments": ["warn", { "terms": ["TODO"] }],
diff --git a/scripts/add-license/gpl.mts b/scripts/add-license/gpl.mts
index 59e0750..12f8463 100644
--- a/scripts/add-license/gpl.mts
+++ b/scripts/add-license/gpl.mts
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
diff --git a/scripts/add-license/main.mts b/scripts/add-license/main.mts
index 8732c12..1946ae2 100644
--- a/scripts/add-license/main.mts
+++ b/scripts/add-license/main.mts
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+
// oxlint-disable no-console no-magic-numbers
import { glob, readFile, writeFile } from "node:fs/promises";
import { dirname, extname, join, relative } from "node:path";
@@ -22,7 +23,7 @@ import { fileURLToPath } from "node:url";
import GPL from "./gpl.mts";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
-const INCLUDED = ["scripts/", "packages/*/src/"];
+const INCLUDED = ["scripts/", "packages/*/src/", "packages/*/bin/"];
const EXCLUDED = ["**/dist/"];
type Extension = keyof typeof GPL;
@@ -33,7 +34,7 @@ interface AddLicenseProps {
interface LicenseTarget {
readonly path: string;
- readonly extension: Extension;
+ readonly ext: Extension;
}
interface LicenseCheck extends LicenseTarget {
@@ -41,63 +42,50 @@ interface LicenseCheck extends LicenseTarget {
}
export default async function addLicense(opt: AddLicenseProps) {
- const files = await findIncludedFiles();
- const targets = files
- .map((path) => ({ path, extension: extensionOf(path) }))
- .filter(isLicenseTarget);
- const checks = await Promise.all(targets.map(checkLicense));
- const missing = checks.filter((check) => !check.hasLicense);
-
- if (missing.length === 0) {
+ const files = await findTargets().then(filterLicense);
+ if (files.length === 0) {
process.exit(0);
}
-
if (opt.check) {
- for (const file of missing) {
- console.log(relative(ROOT, file.path));
- }
+ console.warn("Licenses are missing from some files:");
+ listFiles(files);
process.exit(1);
}
+ console.warn("Added licenses to the files that were missing them:");
+ listFiles(files);
- await Promise.all(missing.map((file) => addLicenseHeader(file)));
+ await Promise.all(files.map(addLicenseHeader));
}
-async function findIncludedFiles(): Promise {
- const patterns = INCLUDED.map((dir) => `${dir}**/*`);
- const files: string[] = [];
- for await (const entry of glob(patterns, {
- cwd: ROOT,
- exclude: EXCLUDED,
- withFileTypes: true,
- })) {
- if (entry.isFile()) {
- files.push(join(entry.parentPath, entry.name));
- }
- }
- return files;
-}
+const findTargets = async (): Promise =>
+ (await Array.fromAsync(glob(INCLUDED.map(addAsteriskIfDir), GLOB_OPTION)))
+ .filter((entry) => entry.isFile())
+ .map((entry) => join(entry.parentPath, entry.name))
+ .map((path) => ({ path, ext: extensionOf(path) }))
+ .filter(isLicenseTarget);
-function extensionOf(path: string): string {
- return extname(path).slice(1);
-}
+const GLOB_OPTION = {
+ cwd: ROOT,
+ exclude: EXCLUDED,
+ withFileTypes: true,
+} as const;
-function isLicenseTarget(
- file: Readonly<{ path: string; extension: string }>,
-): file is LicenseTarget {
- return Object.hasOwn(GPL, file.extension);
-}
+const addAsteriskIfDir = (dir: string) =>
+ dir.endsWith("/") ? `${dir}**/*` : dir;
-async function readLines(path: string): Promise {
- const content = await readFile(path, { encoding: "utf-8" });
- return content.split("\n");
-}
+const filterLicense = async (files: LicenseTarget[]): Promise =>
+ (await Array.fromAsync(files, checkLicense)).filter(
+ (check) => !check.hasLicense,
+ );
-function shebangOffset(lines: readonly string[]): number {
- return lines[0]?.startsWith("#!") ? 1 : 0;
-}
+const extensionOf = (path: string): string => extname(path).slice(1);
+
+const isLicenseTarget = (
+ file: Readonly<{ path: string; ext: string }>,
+): file is LicenseTarget => Object.hasOwn(GPL, file.ext);
async function checkLicense(target: LicenseTarget): Promise {
- const header = GPL[target.extension];
+ const header = GPL[target.ext];
const lines = await readLines(target.path);
const offset = shebangOffset(lines);
const headerLineCount = header.split("\n").length;
@@ -106,15 +94,34 @@ async function checkLicense(target: LicenseTarget): Promise {
}
async function addLicenseHeader(target: LicenseTarget): Promise {
- const header = GPL[target.extension];
- const lines = await readLines(target.path);
+ const added = updateLines(GPL[target.ext], await readLines(target.path));
+ await writeFile(target.path, added, { encoding: "utf-8" });
+}
+
+const readLines = async (path: string): Promise =>
+ (await readFile(path, { encoding: "utf-8" })).split("\n");
+
+const shebangOffset = (lines: readonly string[]): number =>
+ lines[0]?.startsWith("#!") ? (lines[1]?.trim() === "" ? 2 : 1) : 0;
+
+const updateLines = (header: string, lines: string[]): string =>
+ Array.from(genLines(header, lines)).join("\n");
+
+function* genLines(header: string, lines: string[]): Generator {
const offset = shebangOffset(lines);
- const updatedLines = [
- ...lines.slice(0, offset),
- ...header.split("\n"),
- ...lines.slice(offset),
- ];
- await writeFile(target.path, updatedLines.join("\n"), { encoding: "utf-8" });
+ yield* lines.slice(0, offset);
+ yield header;
+ const next = lines.at(offset);
+ if (next != null && next.trim() !== "") {
+ yield "";
+ }
+ yield* lines.slice(offset);
+}
+
+function listFiles(files: LicenseTarget[]) {
+ for (const file of files) {
+ console.warn(` - ${relative(ROOT, file.path)}`);
+ }
}
if (import.meta.main) {