나스를 찾았다 — Tailscale 의 yunchan-nas (100.116.83.60 / LAN 192.168.0.39), Synology DS420+. SSH 키(id_ed25519)로 이미 붙어서 비밀번호가 필요 없다. Forgejo 구성 컨테이너 kd-forgejo (codeberg.org/forgejo/forgejo:11) + postgres:16-alpine 웹 3000 -> Cloudflare 프록시로 https://git.chanpaca.net git SSH 2222 -> 컨테이너 내부 22 중요: git.chanpaca.net 은 Cloudflare 뒤라 22/2222 가 둘 다 막혀 있다. 오리진 IP 를 감추는 정상 구성이고, 그래서 git push 는 도메인이 아니라 Tailscale 주소로 해야 한다. remote 를 그렇게 걸었다. SSH 키가 이미 Forgejo 에 등록돼 있다(encep-win, yunchan 계정). push 에 토큰도 비밀번호도 필요 없다. 배포 전 검증 전체 빌드 통과 · 테스트 22개 통과 tarball 114.5KB / 22개 파일, 스킬 19개 포함 실제 npx 설치 왕복: install 19개 -> doctor 정상 -> 드리프트 감지 -> update 가 수정 파일 보존 -> uninstall 후 잔여 0 배포 직전에 결함 하나를 잡았다. Windows 에서 작업해 스킬이 전부 CRLF 로 저장돼 있었고, 그대로 tarball 에 들어가면 SKILL.md 의 YAML 프론트매터를 파서에 따라 못 읽는다. 번들 스크립트에서 LF 로 정규화하고 .gitattributes 도 넣었다. 배포물은 어느 OS 에서 만들든 같아야 한다. 푸터 제거 출처가 사라지지 않게 설치 섹션 끝에 한 줄만 남겼다(저장소 · MIT).
This commit is contained in:
parent
8267905c0c
commit
8ed45008a0
8 changed files with 116 additions and 34 deletions
62
.env.example
Normal file
62
.env.example
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# designpaca 배포 환경
|
||||||
|
#
|
||||||
|
# 이 파일은 커밋된다. 실제 값은 `.env` 에 넣어라 (.gitignore 에 있다).
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# 채워진 값은 실측으로 확인한 것이고, 빈 값은 사람이 발급해야 하는 것이다.
|
||||||
|
|
||||||
|
# ── NAS ──────────────────────────────────────────────────────
|
||||||
|
# Synology DS420+ · Linux 4.4.302+ · Tailscale 로 접근한다.
|
||||||
|
# SSH 키(id_ed25519)로 이미 붙는다. 비밀번호가 필요 없다.
|
||||||
|
NAS_HOST=100.116.83.60
|
||||||
|
NAS_HOST_LAN=192.168.0.39
|
||||||
|
NAS_USER=yunchan
|
||||||
|
|
||||||
|
# ── Forgejo ──────────────────────────────────────────────────
|
||||||
|
# 컨테이너 kd-forgejo (codeberg.org/forgejo/forgejo:11) + postgres:16-alpine
|
||||||
|
# 웹 3000 → Cloudflare 프록시로 https://git.chanpaca.net
|
||||||
|
# git SSH 2222 → 컨테이너 내부 22
|
||||||
|
#
|
||||||
|
# 주의: git.chanpaca.net 은 Cloudflare 뒤라 22/2222 가 막혀 있다.
|
||||||
|
# git push 는 도메인이 아니라 Tailscale 주소로 해야 한다.
|
||||||
|
FORGEJO_URL=https://git.chanpaca.net
|
||||||
|
FORGEJO_SSH_HOST=100.116.83.60
|
||||||
|
FORGEJO_SSH_PORT=2222
|
||||||
|
FORGEJO_CONTAINER=kd-forgejo
|
||||||
|
|
||||||
|
# 등록된 SSH 키 이름: encep-win (yunchan 계정)
|
||||||
|
FORGEJO_OWNER=yunchan
|
||||||
|
FORGEJO_REPO=designpaca
|
||||||
|
|
||||||
|
# 완성되는 remote URL:
|
||||||
|
# ssh://git@100.116.83.60:2222/yunchan/designpaca.git
|
||||||
|
|
||||||
|
# ── 사람이 발급해야 하는 것 ──────────────────────────────────
|
||||||
|
# 아래는 전부 비워둔다. 값을 아는 사람만 채울 수 있다.
|
||||||
|
|
||||||
|
# Forgejo API 토큰. 저장소 생성·시크릿 등록에 쓴다.
|
||||||
|
# git.chanpaca.net → 설정 → 애플리케이션 → 토큰 생성
|
||||||
|
# 권한: repository(write), user(read)
|
||||||
|
FORGEJO_TOKEN=
|
||||||
|
|
||||||
|
# npmjs 배포 토큰 (Automation 타입).
|
||||||
|
# npmjs.com → Access Tokens → Generate → Automation
|
||||||
|
NPM_TOKEN=
|
||||||
|
|
||||||
|
# Forgejo 사설 npm 레지스트리 미러용 토큰.
|
||||||
|
# FORGEJO_TOKEN 과 같은 값을 써도 되지만, 권한을 나누려면 따로 발급한다.
|
||||||
|
FORGEJO_NPM_TOKEN=
|
||||||
|
|
||||||
|
# Forgejo 릴리스 생성용 토큰. 워크플로가 draft 릴리스를 만든다.
|
||||||
|
RELEASE_TOKEN=
|
||||||
|
|
||||||
|
# Cloudflare Pages 배포.
|
||||||
|
# dash.cloudflare.com → My Profile → API Tokens
|
||||||
|
# 권한: Account / Cloudflare Pages / Edit
|
||||||
|
CF_API_TOKEN=
|
||||||
|
CF_ACCOUNT_ID=
|
||||||
|
|
||||||
|
# Cloudflare Pages 프로젝트 이름 (아직 만들지 않았다)
|
||||||
|
CF_PAGES_PROJECT=
|
||||||
|
# 연결할 도메인
|
||||||
|
SITE_DOMAIN=designpaca.chanpaca.net
|
||||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 배포되는 텍스트는 어느 OS 에서 체크아웃하든 LF 다.
|
||||||
|
# SKILL.md 의 YAML 프론트매터가 CRLF 에서 파서에 따라 깨진다.
|
||||||
|
* text=auto eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
|
@ -41,10 +41,25 @@ const { c, ui } = Astro.props;
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터를 없앴으므로 출처와 라이선스는 여기 한 줄로 남는다.
|
||||||
|
링크 하나 없이 배포하면 사용자가 소스를 찾을 방법이 사라진다. */}
|
||||||
|
<p class="install-colophon muted">
|
||||||
|
<a href={`https://${c.colophon.repo}`}>{c.colophon.repo}</a>
|
||||||
|
<span aria-hidden="true"> · </span>{c.colophon.license}
|
||||||
|
<span aria-hidden="true"> · </span>{c.colophon.made}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.install { padding-block: var(--space-8); }
|
.install { padding-block: var(--space-8); }
|
||||||
|
.install-colophon {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
letter-spacing: var(--tracking-label);
|
||||||
|
}
|
||||||
.install-grid {
|
.install-grid {
|
||||||
max-width: 1180px;
|
max-width: 1180px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
|
|
@ -80,6 +95,13 @@ const { c, ui } = Astro.props;
|
||||||
dd { margin: 0; font-size: var(--step-0); }
|
dd { margin: 0; font-size: var(--step-0); }
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
.install-grid { grid-template-columns: 1fr; }
|
.install-colophon {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
letter-spacing: var(--tracking-label);
|
||||||
|
}
|
||||||
|
.install-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,8 @@ export interface Content {
|
||||||
command: string;
|
command: string;
|
||||||
targets: { name: string; path: string }[];
|
targets: { name: string; path: string }[];
|
||||||
commands: { cmd: string; what: string }[];
|
commands: { cmd: string; what: string }[];
|
||||||
|
colophon: { made: string; repo: string; license: string };
|
||||||
};
|
};
|
||||||
footer: { made: string; repo: string; license: string };
|
|
||||||
nav: { items: { label: string; href: string }[]; menu: string };
|
nav: { items: { label: string; href: string }[]; menu: string };
|
||||||
ui: { copy: string; copied: string };
|
ui: { copy: string; copied: string };
|
||||||
}
|
}
|
||||||
|
|
@ -377,11 +377,11 @@ export const ko: Content = {
|
||||||
{ cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" },
|
{ cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" },
|
||||||
{ cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" },
|
{ cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" },
|
||||||
],
|
],
|
||||||
},
|
colophon: {
|
||||||
footer: {
|
made: "designpaca 로 만들었다",
|
||||||
made: "이 페이지는 designpaca 로 만들어졌다.",
|
repo: "git.chanpaca.net/yunchan/designpaca",
|
||||||
repo: "git.chanpaca.net/yunchan/designpaca",
|
license: "MIT",
|
||||||
license: "MIT",
|
},
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -612,11 +612,11 @@ export const en: Content = {
|
||||||
{ cmd: "npx designpaca doctor", what: "Diagnose install state and drift" },
|
{ cmd: "npx designpaca doctor", what: "Diagnose install state and drift" },
|
||||||
{ cmd: "npx designpaca uninstall", what: "Exact removal, driven by the manifest" },
|
{ cmd: "npx designpaca uninstall", what: "Exact removal, driven by the manifest" },
|
||||||
],
|
],
|
||||||
},
|
colophon: {
|
||||||
footer: {
|
made: "Built with designpaca",
|
||||||
made: "This page was built with designpaca.",
|
repo: "git.chanpaca.net/yunchan/designpaca",
|
||||||
repo: "git.chanpaca.net/yunchan/designpaca",
|
license: "MIT",
|
||||||
license: "MIT",
|
},
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
items: [
|
items: [
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,6 @@ const FAVICON =
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="page site-footer">
|
|
||||||
<slot name="footer" />
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// 진입 연출. 한 번 보이면 계속 보인다.
|
// 진입 연출. 한 번 보이면 계속 보인다.
|
||||||
|
|
@ -275,9 +272,4 @@ const FAVICON =
|
||||||
.nav-mobile { display: none; }
|
.nav-mobile { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer {
|
|
||||||
padding-block: var(--space-6);
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
color: var(--ink-muted);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,4 @@ import { en as c } from "../../i18n/content";
|
||||||
<Rules c={c.rules} />
|
<Rules c={c.rules} />
|
||||||
<Install c={c.install} ui={c.ui} />
|
<Install c={c.install} ui={c.ui} />
|
||||||
|
|
||||||
<Fragment slot="footer">
|
|
||||||
<p>{c.footer.made}</p>
|
|
||||||
<p class="mono">
|
|
||||||
<a href={`https://${c.footer.repo}`}>{c.footer.repo}</a> · {c.footer.license}
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
</Base>
|
</Base>
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,4 @@ import { ko as c } from "../i18n/content";
|
||||||
<Rules c={c.rules} />
|
<Rules c={c.rules} />
|
||||||
<Install c={c.install} ui={c.ui} />
|
<Install c={c.install} ui={c.ui} />
|
||||||
|
|
||||||
<Fragment slot="footer">
|
|
||||||
<p>{c.footer.made}</p>
|
|
||||||
<p class="mono">
|
|
||||||
<a href={`https://${c.footer.repo}`}>{c.footer.repo}</a> · {c.footer.license}
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
</Base>
|
</Base>
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,26 @@ const here = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const src = path.resolve(here, "../../skill");
|
const src = path.resolve(here, "../../skill");
|
||||||
const dest = path.resolve(here, "../dist/skill");
|
const dest = path.resolve(here, "../dist/skill");
|
||||||
|
|
||||||
|
// 텍스트는 LF 로 정규화해서 싣는다.
|
||||||
|
// Windows 에서 작업하면 파일이 CRLF 로 저장되고, 그대로 tarball 에 들어가면
|
||||||
|
// SKILL.md 의 YAML 프론트매터를 파서에 따라 못 읽는다.
|
||||||
|
// 배포물은 어느 OS 에서 만들었는지와 무관하게 같아야 한다.
|
||||||
|
const TEXT = new Set([".md", ".txt", ".json", ".yml", ".yaml"]);
|
||||||
|
|
||||||
async function copyDir(from, to) {
|
async function copyDir(from, to) {
|
||||||
await fs.mkdir(to, { recursive: true });
|
await fs.mkdir(to, { recursive: true });
|
||||||
for (const e of await fs.readdir(from, { withFileTypes: true })) {
|
for (const e of await fs.readdir(from, { withFileTypes: true })) {
|
||||||
if (e.name === "node_modules" || e.name === "package.json") continue;
|
if (e.name === "node_modules" || e.name === "package.json") continue;
|
||||||
const s = path.join(from, e.name);
|
const s = path.join(from, e.name);
|
||||||
const d = path.join(to, e.name);
|
const d = path.join(to, e.name);
|
||||||
if (e.isDirectory()) await copyDir(s, d);
|
if (e.isDirectory()) {
|
||||||
else await fs.copyFile(s, d);
|
await copyDir(s, d);
|
||||||
|
} else if (TEXT.has(path.extname(e.name))) {
|
||||||
|
const text = await fs.readFile(s, "utf8");
|
||||||
|
await fs.writeFile(d, text.replace(/\r\n/g, "\n"), "utf8");
|
||||||
|
} else {
|
||||||
|
await fs.copyFile(s, d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue