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) {