diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 194fbc0..92009d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - name: Format check run: pnpm format:check + - name: License headers + run: pnpm run license:check + - name: Type check run: pnpm tsc --noEmit diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..2536b1a --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +AG Tech Web Template +Copyright 2026 AG Technology Group LLC + +This product includes software developed by AG Technology Group LLC. diff --git a/eslint.config.js b/eslint.config.js index 81a7591..f1cdd29 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import js from "@eslint/js" import globals from "globals" import reactHooks from "eslint-plugin-react-hooks" diff --git a/orval.config.ts b/orval.config.ts index 1aaebc7..cf2df20 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { defineConfig } from "orval" const input = process.env.OPENAPI_URL || "http://localhost:8000/openapi.json" diff --git a/package.json b/package.json index 8909a4b..7d6d8bf 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", + "license:check": "node scripts/license-header.mjs --check --all", + "license:fix": "node scripts/license-header.mjs --fix --all", "preview": "vite preview", "test": "vitest", "test:run": "vitest run", diff --git a/scripts/license-header.mjs b/scripts/license-header.mjs new file mode 100644 index 0000000..dba87b3 --- /dev/null +++ b/scripts/license-header.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + +/** + * license-header.mjs — stamp or verify SPDX license headers on source files. + * + * Usage: + * node scripts/license-header.mjs --check --all verify every tracked source file (CI) + * node scripts/license-header.mjs --fix --all stamp every tracked source file + * node scripts/license-header.mjs --fix stamp specific files (lint-staged) + * + * Default mode is --check. Re-running is safe: a file already carrying an + * SPDX identifier in its first lines is left untouched (idempotent), so the + * lint-staged auto-fix and the CI check can never disagree. + */ + +import { execFileSync } from "node:child_process" +import { readFileSync, writeFileSync } from "node:fs" + +const HEADER = [ + "// SPDX-FileCopyrightText: 2026 AG Technology Group LLC", + "// SPDX-License-Identifier: Apache-2.0", +] + +const SOURCE_EXT = /\.(ts|tsx|js|jsx)$/ +// Vendored deps, build output, and the Orval-generated client are never stamped. +const SKIP_PATH = [ + /(^|\/)node_modules\//, + /(^|\/)dist\//, + /(^|\/)build\//, + /(^|\/)\.next\//, + /(^|\/)coverage\//, + /(^|\/)src\/api\/generated\//, +] +const GENERATED_NAME = /\.gen\.(ts|tsx|js|jsx)$/ +const GENERATED_BANNER = /@generated|do not edit|automatically generated/i + +// Returns "skip" (with reason), "stamped" (already has a header), or "needs". +function classify(file, content) { + if (!SOURCE_EXT.test(file)) + return { status: "skip", reason: "not a source file" } + if (SKIP_PATH.some((re) => re.test(file))) + return { status: "skip", reason: "vendored/output path" } + if (GENERATED_NAME.test(file)) + return { status: "skip", reason: "generated (*.gen.*)" } + const lines = content.split("\n") + if (GENERATED_BANNER.test(lines.slice(0, 15).join("\n"))) + return { status: "skip", reason: "generated banner" } + if (lines.slice(0, 10).join("\n").includes("SPDX-License-Identifier")) + return { status: "stamped" } + return { status: "needs" } +} + +// Insert the header at the very top — above triple-slash directives (a plain +// comment before `/// ` keeps the directive valid) — but below a +// shebang if present. Leaves exactly one blank line before the original code. +function applyHeader(content) { + const lines = content.split("\n") + const shebang = lines[0]?.startsWith("#!") ? lines.shift() : null + while (lines.length && lines[0].trim() === "") lines.shift() + const out = [] + if (shebang) out.push(shebang) + out.push(...HEADER, "", ...lines) + const result = out.join("\n") + return result.endsWith("\n") ? result : result + "\n" +} + +const args = process.argv.slice(2) +const fix = args.includes("--fix") +const explicit = args.filter((a) => !a.startsWith("--")) +const targets = args.includes("--all") + ? execFileSync("git", ["ls-files", "*.ts", "*.tsx", "*.js", "*.jsx"], { + encoding: "utf8", + }) + .split("\n") + .filter(Boolean) + : explicit + +const stamped = [] +const already = [] +const skipped = [] +const missing = [] + +for (const file of targets) { + let content + try { + content = readFileSync(file, "utf8") + } catch { + continue // file was deleted between staging and run + } + const c = classify(file, content) + if (c.status === "skip") skipped.push(`${file} (${c.reason})`) + else if (c.status === "stamped") already.push(file) + else if (fix) { + writeFileSync(file, applyHeader(content)) + stamped.push(file) + } else missing.push(file) +} + +if (fix) { + console.log( + `license-header: stamped ${stamped.length}, already ${already.length}, skipped ${skipped.length}` + ) + for (const f of stamped) console.log(` + ${f}`) +} else if (missing.length) { + console.error( + `license-header: ${missing.length} file(s) missing an SPDX header:` + ) + for (const f of missing) console.error(` - ${f}`) + console.error('Run "pnpm run license:fix" to add them.') + process.exit(1) +} else { + console.log( + `license-header: OK — ${already.length} stamped, ${skipped.length} skipped` + ) +} diff --git a/src/api/api.ts b/src/api/api.ts index 23ad210..bd193a7 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import ky, { type Options } from "ky" export const baseUrl = import.meta.env.VITE_API_URL || "/api" diff --git a/src/api/handlers.ts b/src/api/handlers.ts index 34ddf28..96de858 100644 --- a/src/api/handlers.ts +++ b/src/api/handlers.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { http, HttpResponse, type HttpHandler } from "msw" /** diff --git a/src/api/orval-client.ts b/src/api/orval-client.ts index 972878f..5de43dc 100644 --- a/src/api/orval-client.ts +++ b/src/api/orval-client.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { api } from "./api" /** diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index 4f35084..12bcd58 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { Link } from "@tanstack/react-router" import type { ErrorComponentProps } from "@tanstack/react-router" import { Button } from "@/components/ui/button" diff --git a/src/components/not-found.tsx b/src/components/not-found.tsx index 9edfa77..e2428f1 100644 --- a/src/components/not-found.tsx +++ b/src/components/not-found.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { Link } from "@tanstack/react-router" import { Button } from "@/components/ui/button" diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 5c50416..e1a1a18 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { createContext, useContext, useEffect, useState } from "react" type Theme = "dark" | "light" | "system" diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index 2be1201..6cd3624 100644 --- a/src/components/theme-toggle.tsx +++ b/src/components/theme-toggle.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { Moon, Monitor, Sun } from "lucide-react" import { Button } from "@/components/ui/button" import { useTheme } from "@/components/theme-provider" diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 4340f17..78f0ed9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 681ad98..3f50c12 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import * as React from "react" import { cn } from "@/lib/utils" diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 8916905..fdfb61f 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import * as React from "react" import { cn } from "@/lib/utils" diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index ef7133a..702bc3d 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 3b4a756..e434dec 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import * as React from "react" import { cn } from "@/lib/utils" diff --git a/src/lib/analytics.tsx b/src/lib/analytics.tsx index 42569be..38b5d4d 100644 --- a/src/lib/analytics.tsx +++ b/src/lib/analytics.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { createContext, useContext, useMemo } from "react" import { logger } from "@/lib/logger" diff --git a/src/lib/api-errors.ts b/src/lib/api-errors.ts index 5e1e547..e4dbf00 100644 --- a/src/lib/api-errors.ts +++ b/src/lib/api-errors.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + const STATUS_MESSAGES: Record = { 400: "Invalid request. Please check your input.", 401: "Your session has expired. Please sign in again.", diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index 75b6141..ce1f1a2 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { useQueryClient } from "@tanstack/react-query" import { createContext, diff --git a/src/lib/feature-flags.tsx b/src/lib/feature-flags.tsx index 3b47282..7a97a05 100644 --- a/src/lib/feature-flags.tsx +++ b/src/lib/feature-flags.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { useQuery, useQueryClient } from "@tanstack/react-query" import { createContext, useContext, useMemo } from "react" import { api } from "@/api/api" diff --git a/src/lib/logger.ts b/src/lib/logger.ts index c1baaa3..7193a9f 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + type LogLevel = "debug" | "info" | "warn" | "error" const LEVEL_ORDER: Record = { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d084cca..7f79f2d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" diff --git a/src/main.tsx b/src/main.tsx index 9783f51..d1f6825 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { MutationCache, QueryClient, diff --git a/src/pages/home/home-page.test.tsx b/src/pages/home/home-page.test.tsx index 4b91676..a3c343c 100644 --- a/src/pages/home/home-page.test.tsx +++ b/src/pages/home/home-page.test.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { renderWithFileRoutes } from "@/test/renderers" import { screen } from "@testing-library/react" import { describe, expect, it } from "vitest" diff --git a/src/pages/home/home-page.tsx b/src/pages/home/home-page.tsx index f9cdf3b..96f7595 100644 --- a/src/pages/home/home-page.tsx +++ b/src/pages/home/home-page.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { Button } from "@/components/ui/button" export function HomePage() { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 10e5cb9..8cedc7f 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { lazy, Suspense, useEffect } from "react" import { QueryClient } from "@tanstack/react-query" import { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0f8c9f0..b1fdcbb 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { createFileRoute } from "@tanstack/react-router" import { HomePage } from "@/pages/home/home-page" diff --git a/src/test/renderers.tsx b/src/test/renderers.tsx index 4a7aa78..8e119e8 100644 --- a/src/test/renderers.tsx +++ b/src/test/renderers.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { ThemeProvider } from "@/components/theme-provider" import { AnalyticsProvider } from "@/lib/analytics" import { AuthProvider } from "@/lib/auth" diff --git a/src/test/setup.ts b/src/test/setup.ts index b925197..dfcc919 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { handlers } from "@/api/handlers" import "@testing-library/jest-dom/vitest" import { cleanup } from "@testing-library/react" diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index ff6f058..589fa91 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + /// interface ImportMetaEnv { diff --git a/vite.config.ts b/vite.config.ts index 8efb9bb..80e2ab1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import path from "path" import tailwindcss from "@tailwindcss/vite" import { TanStackRouterVite } from "@tanstack/router-plugin/vite" diff --git a/vitest.config.ts b/vitest.config.ts index 2ff9275..b781995 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 AG Technology Group LLC +// SPDX-License-Identifier: Apache-2.0 + import { defineConfig } from "vitest/config" import react from "@vitejs/plugin-react" import path from "path"