projectops 4.28.1 → 4.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  > 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
8
8
 
9
9
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
10
- ## 최신 버전 : v4.28.0 (2026-09-30)
10
+ ## 최신 버전 : v4.28.1 (2026-10-01)
11
11
 
12
12
  [전체 버전 기록 보기](CHANGELOG.md)
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.28.1",
3
+ "version": "4.28.2",
4
4
  "description": "ProjectOps — 완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI",
5
5
  "keywords": [
6
6
  "devops",
package/src/cli/args.js CHANGED
@@ -7,7 +7,8 @@ export const DEPLOY_TARGETS = ["docker-ssh", "vercel", "none"];
7
7
  export const PUBLISH_TARGETS = ["nexus", "npm", "github-packages"];
8
8
  export const INTENT_VALUES = ["app", "library", "both", "none", "manual"];
9
9
  const SEMVER_RE = /^\d+\.\d+\.\d+$/;
10
- const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
10
+ // 유니코드 글자·숫자 허용 (#719) — 공백·셸 메타문자는 문자 클래스에 없어 그대로 거부된다.
11
+ const BRANCH_RE = /^[\p{L}\p{N}][\p{L}\p{N}._/-]*$/u;
11
12
  // 허용 실행 모드 (#665) — 오타가 조용히 "복사 0건 성공"으로 끝나지 않게 한다.
12
13
  export const MODE_VALUES = ["full", "version", "workflows", "issues", "skills", "doctor", "interactive"];
13
14
 
@@ -103,7 +104,7 @@ export function parseArgs(argv) {
103
104
  const v = (args.shift() ?? "").trim();
104
105
  // 워크플로우 yaml·셸에 그대로 삽입되므로 안전한 브랜치명만 허용 (#676)
105
106
  if (v && (!BRANCH_RE.test(v) || v.includes("..") || v.endsWith("/") || v.endsWith(".lock"))) {
106
- throw new CliError(`--deploy-branch 값은 영문·숫자·. _ - / 로 이루어진 브랜치명이어야 합니다: '${v}'`);
107
+ throw new CliError(`--deploy-branch 값은 글자·숫자·. _ - / 로 이루어진 브랜치명이어야 합니다: '${v}'`);
107
108
  }
108
109
  if (v) result.deployBranch = v;
109
110
  break;
@@ -1,5 +1,6 @@
1
1
  // 템플릿 자산 획득 + 제외 적용 (.sh download_template 등가) — template_integrator.sh 2064~2141.
2
2
  import { execFileSync } from "node:child_process";
3
+ import { mkdirSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { exists, readText, copyDirSync, remove } from "./fsutil.js";
5
6
  import { DOCS_TO_REMOVE, PLUGIN_ITEMS_TO_REMOVE } from "./exclusions.js";
@@ -21,6 +22,9 @@ export function acquireTemplate({ tempDir, source = { type: "git" } }) {
21
22
  copyDirSync(source.path, tempDir);
22
23
  } else {
23
24
  const repo = source.repo || TEMPLATE_REPO;
25
+ // clone 은 stdio 를 버려 실패 원인이 사라진다 — 폴더가 읽기 전용이어도 "내려받지 못했다"로 오도되므로
26
+ // 먼저 만들어 권한 오류(EACCES/EPERM/EROFS)를 그대로 올린다 (#720). 빈 폴더에는 clone 이 가능하다.
27
+ mkdirSync(tempDir, { recursive: true });
24
28
  execFileSync("git", ["clone", "--depth", "1", "--quiet", repo, tempDir], { stdio: "ignore" });
25
29
  }
26
30
  applyExclusions(tempDir);
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join, basename } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { detectTypesFromMarkers, detectVersionFromFiles, suggestTypesByExtScan } from "./detect.js";
7
- import { parseExisting } from "./version-yml.js";
7
+ import { parseExisting, convertLegacySingularType } from "./version-yml.js";
8
+ import { VALID_TYPES } from "../context.js";
8
9
 
9
10
  const hasFile = (root) => (rel) => existsSync(join(root, rel));
10
11
  const readFile = (root) => (rel) => {
@@ -50,6 +51,10 @@ export function detectTypes(root) {
50
51
  if (existsSync(vy)) {
51
52
  const { types } = parseExisting(readFileSync(vy, "utf8"));
52
53
  if (types.length) return types; // basic 포함, 명시돼 있으면 그대로
54
+ // v4.1.0 이전 단수 project_type 키 (#718) — 마커가 없어도 명시된 값을 복원한다.
55
+ // 안 그러면 마커 없는 저장소가 basic 으로 떨어져 기존 타입의 워크플로우가 고아가 된다.
56
+ const legacy = (convertLegacySingularType(readFileSync(vy, "utf8")) || "").match(/^project_types: \["([a-z-]+)"\]/m);
57
+ if (legacy && VALID_TYPES.includes(legacy[1])) return [legacy[1]];
53
58
  }
54
59
  const byMarkers = detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
55
60
  if (byMarkers.length === 1 && byMarkers[0] === "basic") {