diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
new file mode 100644
index 0000000..839ed18
--- /dev/null
+++ b/.gitea/workflows/ci.yml
@@ -0,0 +1,40 @@
+name: ci
+
+on:
+ push:
+ branches: ["**"]
+ pull_request:
+ branches: ["**"]
+
+jobs:
+ build-test-pack:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci || npm i
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build
+ run: npm run build
+
+ - name: Pack
+ run: npm pack
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: npm-package
+ path: "*.tgz"
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9813f7f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+*.tgz
+npm-debug.log*
+.DS_Store
+
+
diff --git a/.idea/hello-world.iml b/.idea/hello-world.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/hello-world.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..7d862fb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..c0f6ad1
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..63010cd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY package.json package-lock.json* tsconfig.json ./
+RUN npm install --no-audit --progress=false
+
+COPY src ./src
+
+RUN npm run build && npm pack
+
+CMD ["node", "dist/index.js"]
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d4098e4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "hello-world",
+ "version": "0.1.0",
+ "private": false,
+ "description": "Node + TypeScript OOP test environment for Gitea build and pack",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "clean": "rimraf dist",
+ "build": "npm run clean && tsc -p tsconfig.json",
+ "test": "vitest run"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "engines": {
+ "node": ">=20"
+ },
+ "license": "MIT"
+}
+
diff --git a/readme.md b/readme.md
index e69de29..4bf1362 100644
--- a/readme.md
+++ b/readme.md
@@ -0,0 +1,51 @@
+项目:用于在 Gitea 中测试 Node + TypeScript 项目的构建与打包(含 OOP 示例、CI、Docker)。
+
+### 快速开始
+
+1) 安装 Node 20(建议使用 nvm)
+
+```bash
+nvm use || nvm install
+```
+
+2) 安装依赖并运行测试、构建与打包
+
+```bash
+npm i
+npm test
+npm run build
+npm pack
+```
+
+生成的 npm 包形如:`hello-world-0.1.0.tgz`。
+
+### 目录结构
+
+- `src/`:TypeScript 源码,OOP 风格示例位于 `src/core/Greeter.ts`
+- `tests/`:Vitest 单元测试
+- `.gitea/workflows/ci.yml`:Gitea Actions 工作流(安装、测试、构建、打包、上传构件)
+- `Dockerfile`:容器内构建与打包
+
+### 本地使用(Docker)
+
+```bash
+docker build -t node-oop-ci .
+docker run --rm node-oop-ci
+```
+
+镜像在构建阶段会运行 `npm run build && npm pack`,启动时会执行编译产物 `dist/index.js`。
+
+### 在 Gitea 上运行
+
+将仓库推送到 Gitea,确保实例启用了 Actions,并且 Runner 提供了 `ubuntu-latest` 或兼容标签。
+工作流位于 `.gitea/workflows/ci.yml`,包含以下阶段:
+
+- Checkout 代码
+- 安装 Node 20 与依赖
+- 运行测试(Vitest)
+- 构建(TypeScript 到 `dist/`)
+- 打包(`npm pack`)
+- 上传构件(若实例提供 `actions/upload-artifact` 镜像)
+
+如你的 Runner 标签不同,可调整 `runs-on`。
+
diff --git a/src/core/Greeter.ts b/src/core/Greeter.ts
new file mode 100644
index 0000000..aaf2096
--- /dev/null
+++ b/src/core/Greeter.ts
@@ -0,0 +1,14 @@
+export class Greeter {
+ private readonly greetingPrefix: string;
+
+ constructor(greetingPrefix: string = "Hello") {
+ this.greetingPrefix = greetingPrefix;
+ }
+
+ public greet(name: string): string {
+ const sanitizedName = (name ?? "").trim() || "World";
+ return `${this.greetingPrefix}, ${sanitizedName}!`;
+ }
+}
+
+
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..6813302
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,12 @@
+import { Greeter } from "./core/Greeter";
+
+export { Greeter };
+
+if (require.main === module) {
+ const greeter = new Greeter();
+ // Example run output
+ // eslint-disable-next-line no-console
+ console.log(greeter.greet("Gitea"));
+}
+
+
diff --git a/tests/Greeter.test.ts b/tests/Greeter.test.ts
new file mode 100644
index 0000000..5bf9033
--- /dev/null
+++ b/tests/Greeter.test.ts
@@ -0,0 +1,16 @@
+import { describe, it, expect } from "vitest";
+import { Greeter } from "../src/core/Greeter";
+
+describe("Greeter", () => {
+ it("greets with default prefix", () => {
+ const greeter = new Greeter();
+ expect(greeter.greet("Alice")).toBe("Hello, Alice!");
+ });
+
+ it("trims name and handles empty name", () => {
+ const greeter = new Greeter("Hi");
+ expect(greeter.greet(" ")).toBe("Hi, World!");
+ });
+});
+
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..7f29eda
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
+