projectops 4.71.3 โ†’ 4.73.0

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
@@ -11,7 +11,7 @@
11
11
  **Status: actively maintained.** npm `latest` is exactly what is on the `main` branch; work in progress on `develop` is never published. Versions can still ship several times a day. To stay on a version you have tested, run `npx projectops@<version>` ([releases](https://github.com/Cassiiopeia/projectops/releases)).
12
12
 
13
13
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
14
- ## Latest version : v4.71.2 (2026-10-21)
14
+ ## Latest version : v4.72.0 (2026-10-22)
15
15
 
16
16
  [View full version history](CHANGELOG.md)
17
17
 
@@ -51,7 +51,7 @@ This project automates your development workflow on two axes.
51
51
  | Format every issue by hand | `/pro-github` writes and files an issue from the standard template in one step |
52
52
  | Copy the issue URL into commit messages | `/pro-commit` completes the message from the issue context, in your own style |
53
53
  | Write PR descriptions and reports by hand | `/pro-report` analyzes the git diff and generates one |
54
- | Re-type prompts for every code review or analysis | 14 skills give consistent results without re-typing |
54
+ | Re-type prompts for every code review or analysis | 15 skills give consistent results without re-typing |
55
55
 
56
56
  ---
57
57
 
@@ -140,11 +140,11 @@ What it changed: descriptions were cut to short English triggers, a shared contr
140
140
  | Part | What it does | Docs |
141
141
  |---|---|---|
142
142
  | **GitHub Actions** | One push to main handles versioning, changelogs, and CI/CD deployment | [Versioning](https://cassiiopeia.github.io/projectops/version-control) |
143
- | **Agent Skills** (14) | `/pro-github`, `/pro-commit`, `/pro-report` and more let an AI agent drive each step: issues, commits in your style, reports, release PRs | [Skills guide](https://cassiiopeia.github.io/projectops/skills) |
143
+ | **Agent Skills** (15) | `/pro-github`, `/pro-commit`, `/pro-report` and more let an AI agent drive each step: issues, commits in your style, reports, release PRs | [Skills guide](https://cassiiopeia.github.io/projectops/skills) |
144
144
  | **npx CLI** | Installs and updates workflows in an existing project, and diagnoses its state | [CLI reference](https://cassiiopeia.github.io/projectops/cli) |
145
145
 
146
146
  <details>
147
- <summary>See all 14 Agent Skills</summary>
147
+ <summary>See all 15 Agent Skills</summary>
148
148
 
149
149
  ### ๐Ÿ”„ Development cycle automation
150
150
 
@@ -168,6 +168,7 @@ What it changed: descriptions were cut to short English triggers, a shared contr
168
168
  |------|------|
169
169
  | `/pro-figma` | Move a Figma design into code, then compare the result with the design pixel by pixel |
170
170
  | `/pro-launch` | Launch, drive, and capture apps, web, and servers: fixed status bar, width changes, response mocking (empty list, failures), render from code |
171
+ | `/pro-desktop-control` | Drive programs on this computer's screen that are not web pages or emulators (installer wizards, OS dialogs, desktop apps): reads elements, presses without moving the mouse, asks before installs, hands security prompts to the user. Windows for now (macOS planned, issue #985) |
171
172
  | `/pro-design-brief` | A design request for a screen you built before the mockup exists: state captures, alternatives, copy candidates, and must-keep rules as a board for the designer |
172
173
  | `/pro-oss-consult` | Diagnose a GitHub repo as an open-source project: type detection, 6-axis scoring, maturity stage, and edits to About, topics, labels, and files only after approval. One repo or a whole account |
173
174
 
@@ -229,7 +230,7 @@ flowchart TD
229
230
  </details>
230
231
 
231
232
  <details>
232
- <summary>Install Agent Skills only (Claude Code, Gemini CLI, Codex CLI, Cursor)</summary>
233
+ <summary>Install Agent Skills only (Claude Code, Gemini CLI, Codex CLI, Cursor, GitHub Copilot)</summary>
233
234
 
234
235
  ```bash
235
236
  # Claude Code
@@ -252,7 +253,7 @@ The `--mode skills` wizard registers the Codex marketplace and also prepares the
252
253
  Where the Codex plugin marketplace is unavailable, use the fallback install described in the [Skills guide](docs/en/skills.md).
253
254
 
254
255
  ```bash
255
- # Cursor / the full Agent Skills install menu (recommended: npx)
256
+ # Cursor / GitHub Copilot (~/.copilot/skills) / the full Agent Skills install menu (recommended: npx)
256
257
  npx projectops --mode skills
257
258
  ```
258
259
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.71.3",
3
+ "version": "4.73.0",
4
4
  "description": "ProjectOps: installer CLI for a fully automated GitHub project management template (versioning, CI/CD, release notes, agent skills)",
5
5
  "keywords": [
6
6
  "devops",
package/src/cli/args.js CHANGED
@@ -37,6 +37,7 @@ export function parseArgs(argv) {
37
37
  projectsSync: null, // #716 โ€” Projects ๋ณด๋“œ ๋™๊ธฐํ™” ์›Œํฌํ”Œ๋กœ์šฐ ํฌํ•จ ์—ฌ๋ถ€ (null=๋ฏธ์ง€์ •)
38
38
  json: false, // --json: ๊ธฐ๊ณ„๊ฐ€ ์ฝ๋Š” ์ถœ๋ ฅ (--mode options)
39
39
  removeLegacy: false, // #809 โ€” ๊ตฌ์„ธ๋Œ€ ๋ฐฐํฌ ์›Œํฌํ”Œ๋กœ์šฐ(confirm ํ‹ฐ์–ด)๋„ .bak ์œผ๋กœ ์น˜์šด๋‹ค
40
+ createDevelopmentBranch: false, // #715 โ€” ๋น„๋Œ€ํ™”ํ˜•์—์„œ ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜ ์ƒ์„ฑยทpush (์›๊ฒฉ์— ์“ฐ๋ฏ€๋กœ ๋ช…์‹œํ•  ๋•Œ๋งŒ)
40
41
  pathsCsv: "", // "flutter=app,react=client" ์›๋ฌธ (์ •๊ทœํ™”๋Š” resolve ๋‹จ๊ณ„)
41
42
  force: false,
42
43
  lang: null, // --lang en|ko (null = decided by src/i18n resolveLang)
@@ -177,6 +178,7 @@ export function parseArgs(argv) {
177
178
  case "--projects-sync": result.projectsSync = true; break;
178
179
  case "--no-projects-sync": result.projectsSync = false; break;
179
180
  case "--remove-legacy": result.removeLegacy = true; break;
181
+ case "--create-development-branch": result.createDevelopmentBranch = true; break;
180
182
  case "--json": result.json = true; break;
181
183
  case "--npm-publish":
182
184
  process.stderr.write(t("args.npmPublishDeprecated") + "\n");
package/src/cli/help.js CHANGED
@@ -50,6 +50,9 @@ Options
50
50
  --publish CSV library registries: nexus,npm,github-packages. omitted: none
51
51
  --deploy-branch NAME the DEVELOPMENT branch (head of the release PR), default develop. It is NOT the branch that deploys;
52
52
  that is the repository default branch. This flag does not create the branch
53
+ --create-development-branch create the development branch from the default branch and push it to origin when it
54
+ is missing. Does nothing if it exists. A failed push does not fail the run; the summary prints
55
+ the command to run yourself. omitted: off (pushing to the remote is never done without asking)
53
56
  --secret-backup / --no-secret-backup upload GitHub Secrets to your server over SSH (needs SERVER_* secrets). omitted: excluded
54
57
  --ai-summary / --no-ai-summary AI change summary comment on work PRs. Works with no extra setup. omitted: included
55
58
  --coderabbit / --no-coderabbit install .coderabbit.yaml (only --mode full). Needs the CodeRabbit GitHub app. An existing
@@ -129,6 +132,9 @@ AI agent ๋‚˜ CI ๋Š” ํ•ญ์ƒ ์ด๋ ‡๊ฒŒ ์‹คํ–‰ํ•œ๋‹ค: npx projectops --mode <
129
132
  --publish CSV ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ: nexus,npm,github-packages. ์ƒ๋žต: ์—†์Œ
130
133
  --deploy-branch NAME **๊ฐœ๋ฐœ** ๋ธŒ๋žœ์น˜(๋ฆด๋ฆฌ์Šค PR ์˜ head), ๊ธฐ๋ณธ develop. ๋ฐฐํฌ๊ฐ€ ๋„๋Š” ๋ธŒ๋žœ์น˜๊ฐ€ ์•„๋‹ˆ๋‹ค โ€”
131
134
  ๊ทธ๊ฒƒ์€ ๋ ˆํฌ ๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜๋‹ค. ์ด ํ”Œ๋ž˜๊ทธ๋Š” ๋ธŒ๋žœ์น˜๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค
135
+ --create-development-branch ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜์—์„œ ๋งŒ๋“ค๊ณ  origin ์— push ํ•œ๋‹ค. ์ด๋ฏธ ์žˆ์œผ๋ฉด
136
+ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค. push ๊ฐ€ ์‹คํŒจํ•ด๋„ ์‹คํ–‰์€ ์‹คํŒจํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ์‹คํ–‰ํ•  ๋ช…๋ น์„ ์š”์•ฝ์— ๋‚จ๊ธด๋‹ค.
137
+ ์ƒ๋žต: ๊บผ์ง (๋ฌป์ง€ ์•Š๊ณ  ์›๊ฒฉ์— push ํ•˜์ง€ ์•Š๋Š”๋‹ค)
132
138
  --secret-backup / --no-secret-backup GitHub Secret ์„ SSH ๋กœ ์„œ๋ฒ„์— ์˜ฌ๋ฆฐ๋‹ค (SERVER_* secret ํ•„์š”). ์ƒ๋žต: ์ œ์™ธ
133
139
  --ai-summary / --no-ai-summary ์ž‘์—… PR ์— AI ๋ณ€๊ฒฝ ์š”์•ฝ ๋Œ“๊ธ€. ์ถ”๊ฐ€ ์„ค์ • ์—†์ด ๋™์ž‘. ์ƒ๋žต: ํฌํ•จ
134
140
  --coderabbit / --no-coderabbit .coderabbit.yaml ์„ค์น˜ (--mode full ์—์„œ๋งŒ). CodeRabbit GitHub ์•ฑ์ด ํ•„์š”ํ•˜๋‹ค.
@@ -20,6 +20,7 @@ import { verifyInstall } from "../core/verify.js";
20
20
  import { readBaseline } from "../core/baseline.js";
21
21
  import { detectRepoName } from "../core/detect-fs.js";
22
22
  import { detectMigrations } from "../core/migrations/index.js";
23
+ import { scanPlaceholderTriggers } from "../core/placeholder-triggers.js";
23
24
  import { t } from "../i18n/index.js";
24
25
 
25
26
  // ์–ธ์–ด ์ „ํ™˜(withLang ๋“ฑ) ์‹œ์ ์— ํ‰๊ฐ€๋˜๋„๋ก ํ•จ์ˆ˜๋กœ ๋‘”๋‹ค
@@ -96,6 +97,22 @@ export function localChecks(cwd = ".") {
96
97
  ],
97
98
  });
98
99
 
100
+ // ์ž๋™ ํŠธ๋ฆฌ๊ฑฐ + ์˜ˆ์‹œ ๊ฐ’ (#662) โ€” ๋ฏธ์น˜ํ™˜ ํ† ํฐ ๊ฒ€์‚ฌ์— ์•ˆ ๊ฑธ๋ฆฌ๋Š” your-project ๋ฅ˜ ๊ฐ’์ด ์‹คํŒจ ๋Ÿฐ์„ ๊ณ„์† ๋งŒ๋“ ๋‹ค
101
+ const autoPlaceholders = scanPlaceholderTriggers(wfDir);
102
+ add({
103
+ name: t("doctor.ph.name"), purpose: t("doctor.ph.purpose"),
104
+ status: autoPlaceholders.length ? "WARN" : "OK",
105
+ value: autoPlaceholders.length ? t("doctor.count", { n: autoPlaceholders.length }) : t("doctor.none"),
106
+ detail: autoPlaceholders.length ? [
107
+ t("doctor.ph.l1"),
108
+ ...autoPlaceholders.flatMap((w) => [
109
+ ` ${w.file} (${w.triggers.join(", ")})`,
110
+ ...w.values.map((p) => ` ${w.file}:${p.line} ${p.key}: "${p.value}"`),
111
+ ]),
112
+ t("doctor.ph.fix"),
113
+ ] : null,
114
+ });
115
+
99
116
  const secretNames = [...v.secrets.keys()];
100
117
  add({
101
118
  name: t("doctor.sec.name"), purpose: t("doctor.sec.purpose"),
@@ -58,7 +58,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
58
58
  })), { version, versionCode });
59
59
 
60
60
  // 2. README ๋ฒ„์ „ ์„น์…˜
61
- step("update-readme", () => addVersionSectionToReadme(version, targetRoot), { version });
61
+ step("update-readme", () => addVersionSectionToReadme(version, targetRoot, language), { version });
62
62
 
63
63
  // 5. scripts / config
64
64
  // ์›Œํฌํ”Œ๋กœ์šฐ ๋ฐ– ์˜์—ญ๋„ ๊ธฐ๋กํ•œ๋‹ค (#561) โ€” "๋‚ด ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๊ฐฑ์‹ ๋๋‚˜"๋ฅผ ๋กœ๊ทธ๋งŒ ๋ณด๊ณ  ์•Œ ์ˆ˜ ์žˆ๊ฒŒ.
@@ -37,7 +37,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
37
37
  // ํ˜ธ์ถœ๋ถ€๊ฐ€ recordMode๋กœ ๊ธฐ์กด ๊ฐ’์„ ๋„˜๊ธด๋‹ค (full์ด ์šฐ์„ธ โ€” ์—…๋ฐ์ดํŠธ ์žฌ์‹คํ–‰ ๋ฒ”์œ„ ์ถ•์†Œ ๋ฐฉ์ง€).
38
38
  templateOptions: toTemplateOptions(context, { mode: recordMode, optionsDate: today }),
39
39
  }));
40
- addVersionSectionToReadme(version, targetRoot);
40
+ addVersionSectionToReadme(version, targetRoot, context.language);
41
41
  copyScripts(tempDir, targetRoot);
42
42
  copyConfigFolder(tempDir, targetRoot);
43
43
  applyLabelStyle(targetRoot, labelStyle); // #776
@@ -3,27 +3,35 @@ import { join } from "node:path";
3
3
  import { existsSync, readFileSync, appendFileSync } from "node:fs";
4
4
 
5
5
  const MARKER = "<!-- AUTO-VERSION-SECTION";
6
- // ## (์ตœ์‹  ๋ฒ„์ „|์ตœ์‹ ๋ฒ„์ „|Version|๋ฒ„์ „) : vX.Y.Z (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)
7
- const VERSION_LINE_RE = /##\s*(์ตœ์‹ \s*๋ฒ„์ „|์ตœ์‹ ๋ฒ„์ „|Version|๋ฒ„์ „)\s*:\s*v[0-9]+\.[0-9]+\.[0-9]+/i;
6
+ // ## (์ตœ์‹  ๋ฒ„์ „|์ตœ์‹ ๋ฒ„์ „|Latest version|Version|๋ฒ„์ „) : vX.Y.Z (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)
7
+ const VERSION_LINE_RE = /##\s*(์ตœ์‹ \s*๋ฒ„์ „|์ตœ์‹ ๋ฒ„์ „|Latest\s+version|Version|๋ฒ„์ „)\s*:\s*v[0-9]+\.[0-9]+\.[0-9]+/i;
8
+
9
+ // ๋ ˆํฌ ์–ธ์–ด๋ณ„ ์„น์…˜ ๋ฌธ๊ตฌ (.github/scripts/i18n ์˜ readme.version_heading ๊ณผ ๊ฐ™์€ ๊ฐ’)
10
+ const SECTION_TEXT = {
11
+ en: { heading: "Latest version", changelogLink: "View the full version history" },
12
+ ko: { heading: "์ตœ์‹  ๋ฒ„์ „", changelogLink: "์ „์ฒด ๋ฒ„์ „ ๊ธฐ๋ก ๋ณด๊ธฐ" },
13
+ };
8
14
 
9
15
  // README.md ์—†์œผ๋ฉด ์Šคํ‚ต. ๋งˆ์ปค ๋˜๋Š” ๋ฒ„์ „ ๋ผ์ธ ์žˆ์œผ๋ฉด ์Šคํ‚ต. ์—†์œผ๋ฉด ํŒŒ์ผ ๋์— append.
10
16
  // ๋ฐ˜ํ™˜: 'skip-no-readme' | 'skip-marker' | 'skip-version-line' | 'added'
11
- export function addVersionSectionToReadme(version, targetRoot = ".") {
17
+ // language ๋ฏธ์ง€์ •์€ ko โ€” ์–ธ์–ด๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š๋Š” ๊ธฐ์กด ํ˜ธ์ถœ๋ถ€์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๊ธฐ ์œ„ํ•จ
18
+ export function addVersionSectionToReadme(version, targetRoot = ".", language = "ko") {
12
19
  const p = join(targetRoot, "README.md");
13
20
  if (!existsSync(p)) return "skip-no-readme";
14
21
  const content = readFileSync(p, "utf8");
15
22
  if (content.includes(MARKER)) return "skip-marker";
16
23
  if (VERSION_LINE_RE.test(content)) return "skip-version-line";
17
24
 
25
+ const text = SECTION_TEXT[language] ?? SECTION_TEXT.ko;
18
26
  // append ๋ณธ๋ฌธ์€ ๋นˆ ์ค„ + "---"๋กœ ์‹œ์ž‘ (์›๋ณธ ํŒŒ์ผ์ด ๊ฐœํ–‰์œผ๋กœ ๋๋‚œ๋‹ค๋Š” ์ „์ œ).
19
27
  const section =
20
28
  "\n" +
21
29
  "---\n" +
22
30
  "\n" +
23
31
  "<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n" +
24
- `## ์ตœ์‹  ๋ฒ„์ „ : v${version}\n` +
32
+ `## ${text.heading} : v${version}\n` +
25
33
  "\n" +
26
- "[์ „์ฒด ๋ฒ„์ „ ๊ธฐ๋ก ๋ณด๊ธฐ](CHANGELOG.md)\n";
34
+ `[${text.changelogLink}](CHANGELOG.md)\n`;
27
35
  appendFileSync(p, section);
28
36
  return "added";
29
37
  }
@@ -35,6 +35,8 @@ export function copyScripts(tempDir, targetRoot = ".") {
35
35
  "asc_client.py",
36
36
  // iOS ๋ฒ„์ „ ์‚ฌ์ „ ์ ๊ฒ€, ์—…๋กœ๋“œ ๊ฑฐ๋ถ€ ๋ถ„๋ฅ˜, ์•„์นด์ด๋ธŒ๋ถ€ํ„ฐ ์—…๋กœ๋“œ๊นŒ์ง€ ์žฌ์‹œ๋„ (#643).
37
37
  "ios_release.py",
38
+ // Flutter iOS ๋นŒ๋“œ์˜ Xcode ์„ ํƒ(๋นˆ ๊ฐ’ = ๋Ÿฌ๋„ˆ ์ตœ์‹ )๊ณผ SDK ๋ˆ„๋ฝ ์ง„๋‹จ (#758). ์„ ํƒ ๊ทœ์น™์˜ ์œ ์ผํ•œ ์œ„์น˜.
39
+ "select_xcode.py",
38
40
  // ๋ฆด๋ฆฌ์Šค ๋จธ์ง€ ๋•Œ ์™„๋ฃŒ ๋ผ๋ฒจ์ด ๋ถ™์€ ์ด์Šˆ๋ฅผ ๋‹ซ๋Š”๋‹ค (#771). close_on_release ์˜ต์…˜์ด ์ผœ์ง„ ๋ ˆํฌ์—์„œ๋งŒ ๋™์ž‘ํ•œ๋‹ค.
39
41
  "close_issues_on_release.py",
40
42
  // Play Store/TestFlight ๋ฐฐํฌ ๋ชจ๋“œ๋ฅผ ์ฝ”๋“œ ์„ค์ •(.github/config/store-deploy.json)์—์„œ ์ฝ๋Š”๋‹ค (#767).
@@ -60,7 +62,7 @@ export function copyScripts(tempDir, targetRoot = ".") {
60
62
  const i18nSrc = join(tempDir, PATHS.scriptsDir, "i18n");
61
63
  if (exists(i18nSrc)) {
62
64
  for (const f of readdirSync(i18nSrc)) {
63
- if (!/\.(py|json)$/.test(f)) continue; // __pycache__ ๋“ฑ์€ ์ œ์™ธ
65
+ if (!/\.(py|json|cjs)$/.test(f)) continue; // __pycache__ ๋“ฑ์€ ์ œ์™ธ (cjs: ์›Œํฌํ”Œ๋กœ์šฐ ๋Œ“๊ธ€ ๋Ÿฐํƒ€์ž„)
64
66
  copyFileSync(join(i18nSrc, f), join(targetRoot, PATHS.scriptsDir, "i18n", f));
65
67
  copied++;
66
68
  }
@@ -0,0 +1,58 @@
1
+ // ๋น„๋Œ€ํ™”ํ˜• ๊ฐœ๋ฐœ(๋ฆด๋ฆฌ์Šค ์†Œ์Šค) ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ (#715) โ€” --create-development-branch ๋ฅผ ์ค€ ์‹คํ–‰์—์„œ๋งŒ.
2
+ // ๋Œ€ํ™”ํ˜• ensureDeployBranch ๋ฅผ "๋ชจ๋“  ์งˆ๋ฌธ์— ์˜ˆ"๋กœ ๋‹ค์‹œ ์“ด๋‹ค: ์กด์žฌ ํŒ๋‹จยท์ƒ์„ฑยทpush ๊ฒฝ๋กœ๋ฅผ ํ•œ ๋ฒŒ๋กœ ์œ ์ง€.
3
+ // ์‹คํŒจํ•ด๋„ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€ ์•Š๋Š”๋‹ค โ€” ํ†ตํ•ฉ ์ „์ฒด๋ฅผ ์‹คํŒจ์‹œํ‚ค์ง€ ์•Š๊ณ  ์ˆ˜๋™ ๋ช…๋ น๋งŒ ๋‚จ๊ธด๋‹ค.
4
+ import { ensureDeployBranch } from "./options-ask.js";
5
+
6
+ // ensureDeployBranch state > ๊ฒฐ๊ณผ 3์ข… (created ยท skipped ยท failed)
7
+ const STATUS_BY_STATE = {
8
+ "exists-local": "skipped",
9
+ "exists-remote": "skipped",
10
+ "pushed": "created",
11
+ "push-failed": "failed",
12
+ "create-failed": "failed",
13
+ "not-a-repo": "failed",
14
+ "not-checked": "failed",
15
+ };
16
+
17
+ // ์‹คํŒจ ์‚ฌ์œ ๋ณ„ ์ˆ˜๋™ ๋ช…๋ น (push ์‹คํŒจ๋Š” ๋กœ์ปฌ ๋ธŒ๋žœ์น˜๊ฐ€ ์ด๋ฏธ ์ƒ๊ฒผ์œผ๋ฏ€๋กœ push ๋งŒ)
18
+ function manualCommandFor(state, branch, base) {
19
+ if (state === "push-failed") return `git push -u origin ${branch}`;
20
+ if (state === "create-failed" && base) return `git branch ${branch} ${base} && git push -u origin ${branch}`;
21
+ return `git checkout -b ${branch} && git push -u origin ${branch}`;
22
+ }
23
+
24
+ /**
25
+ * ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜ ํ™•์ธ > ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜์—์„œ ์ƒ์„ฑ > origin push
26
+ * ๋ฐ˜ํ™˜: { status, reason, branch, base, pushed, manualCommand(์‹คํŒจ ์‹œ๋งŒ) }
27
+ */
28
+ export async function createDevelopmentBranchUnattended({ targetRoot = ".", branch = "develop", defaultBranch = "", say = () => {} }) {
29
+ const answerYes = { confirm: async () => true };
30
+ const outcome = await ensureDeployBranch({ targetRoot, deployBranch: branch, defaultBranch, io: answerYes, say });
31
+ const status = STATUS_BY_STATE[outcome.state] ?? "failed";
32
+ return {
33
+ status,
34
+ reason: outcome.state,
35
+ branch,
36
+ base: defaultBranch,
37
+ pushed: outcome.pushed === true,
38
+ manualCommand: status === "failed" ? manualCommandFor(outcome.state, branch, defaultBranch) : null,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * ์‹คํ–‰ ๋‹จ๊ณ„ ๋ž˜ํผ: ํ”Œ๋ž˜๊ทธ ํ™•์ธ > ์ƒ์„ฑ ์‹œ๋„ > ์‹คํ–‰ ๊ธฐ๋ก(trace) ๋‚จ๊น€
44
+ * ํ”Œ๋ž˜๊ทธ๊ฐ€ ์—†์œผ๋ฉด ๋งŒ๋“ค์ง€ ์•Š๊ณ  "์™œ ์•ˆ ๋งŒ๋“ค์—ˆ๋Š”์ง€"๋งŒ ๊ธฐ๋ก. ๋ฐ˜ํ™˜: ๊ฒฐ๊ณผ | null(ํ”Œ๋ž˜๊ทธ ์—†์Œ)
45
+ */
46
+ export async function runDevelopmentBranchStep({ isRequested = false, targetRoot, branch, defaultBranch, trace = null, say = () => {} }) {
47
+ const name = branch || "develop";
48
+ if (!isRequested) {
49
+ trace?.event("dev-branch", "not-requested", name, { flag: "--create-development-branch", note: "left as a manual step" });
50
+ return null;
51
+ }
52
+ const result = await createDevelopmentBranchUnattended({ targetRoot, branch: name, defaultBranch, say });
53
+ // action ์ด ๊ณง ๊ฒฐ๊ณผ โ€” failed ๋Š” ๋กœ๊ทธ์—์„œ ERROR ๋กœ ๋ณด์ธ๋‹ค (run-trace levelOf)
54
+ trace?.event("dev-branch", result.status, name, {
55
+ base: defaultBranch || null, reason: result.reason, pushed: result.pushed, manualCommand: result.manualCommand,
56
+ });
57
+ return result;
58
+ }
@@ -19,7 +19,7 @@
19
19
  // remove(io, ctx): boolean ์ œ๊ฑฐ(๋ฏธ์„ค์น˜๋ฉด no-op, true)
20
20
  // manualHint(io): string CLI ์—†์„ ๋•Œ ๋ณด์—ฌ์ค„ ์ˆ˜๋™ ์„ค์น˜ ๋ช…๋ น(์„ ํƒ)
21
21
  //
22
- // io = runner.defaultIo() ๋˜๋Š” ํ…Œ์ŠคํŠธ stub: { which, run, home, log }
22
+ // io = runner.defaultIo() ๋˜๋Š” ํ…Œ์ŠคํŠธ stub: { which, run, home, log, projectopsHome?(์—†์œผ๋ฉด home/.projectops) }
23
23
  // ctx = { templateVersion, sourceSkillsDir } โ€” ๋ฒ„์ „ ํ‘œ๊ธฐยทCursor ๋ณต์‚ฌ ์†Œ์Šค ๋“ฑ ๊ณต์šฉ ์ปจํ…์ŠคํŠธ
24
24
  //
25
25
  // ๊ทœ์น™:
@@ -0,0 +1,84 @@
1
+ // GitHub Copilot ์–ด๋Œ‘ํ„ฐ (agent skills ๋ณต์‚ฌยท๋ฉ”ํƒ€ ๊ธฐ๋กยท์ œ๊ฑฐ).
2
+ // ๊ฐœ์ธ ์Šคํ‚ฌ ํด๋” ~/.copilot/skills/<์ด๋ฆ„>/SKILL.md ๋ฅผ VS CodeยทCopilot CLIยทJetBrains ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๋ชจ๋‘ ๊ธฐ๋ณธ์œผ๋กœ ์ฝ๋Š”๋‹ค
3
+ // (JetBrains 1.18 ๊ธฐ๋ณธ ์œ„์น˜: .agents/.github/.claude skills + ~/.agentsยท~/.copilotยท~/.claude skills)
4
+ // ~/.copilot/intellij-skills ๋Š” JetBrains ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๋‚ด์žฅ ์Šคํ‚ฌ์„ ๋ฎ์–ด์“ฐ๋Š” ๊ด€๋ฆฌ ํด๋”๋ผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
5
+ // ์Šคํ‚ฌ ์ด๋ฆ„(frontmatter name)์€ ํด๋”๋ช…๊ณผ ๊ฐ™์•„์•ผ ๋กœ๋“œ๋œ๋‹ค โ€” pro-* ํด๋”๊ฐ€ ์ด๋ฏธ ๊ทธ ๊ทœ์น™์„ ์ง€ํ‚จ๋‹ค.
6
+ import { join } from "node:path";
7
+ import { existsSync, readdirSync } from "node:fs";
8
+ import { migrateConfigRoot } from "../legacy.js";
9
+ import { t } from "../../../i18n/index.js";
10
+ import { readSkillsMeta } from "../skills-copy.js";
11
+ import { applyHomeSkills, removeHomeSkills } from "../home-skills.js";
12
+
13
+ const META_FILE_NAME = "copilot-skills-meta.json";
14
+ const MESSAGE_PREFIX = "ide.copilot";
15
+ // Copilot ์‚ฌ์šฉ ํ”์  4์ข…: copilot CLI, ~/.copilot ํด๋”(CLIยทJetBrains ๊ฐ€ ๋งŒ๋“ฆ), VS Code ํ™•์žฅ, JetBrains ํ”Œ๋Ÿฌ๊ทธ์ธ
16
+ const COPILOT_CLI_NAME = "copilot";
17
+ const COPILOT_HOME_DIR = ".copilot";
18
+ const VSCODE_EXTENSION_DIRS = [".vscode/extensions", ".vscode-insiders/extensions"];
19
+ // VS Code ํ™•์žฅ ํด๋”๋ช…: github.copilot-1.2.3 ยท github.copilot-chat-0.30.0
20
+ const VSCODE_EXTENSION_PREFIX = "github.copilot";
21
+ // IDE ์„ค์ • ๋ฃจํŠธ (macOS ยท Windows ยท Linux) > <์ œํ’ˆ><๋ฒ„์ „>/plugins/github-copilot-intellij
22
+ // Linux ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์ด <์ œํ’ˆ><๋ฒ„์ „>/ ๋ฐ”๋กœ ์•„๋ž˜์— ๊น”๋ฆฐ๋‹ค. Android Studio ๋„ ๊ฐ™์€ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์“ด๋‹ค.
23
+ const JETBRAINS_CONFIG_ROOTS = [
24
+ "Library/Application Support/JetBrains", "AppData/Roaming/JetBrains", ".local/share/JetBrains",
25
+ "Library/Application Support/Google", "AppData/Roaming/Google", ".local/share/Google",
26
+ ];
27
+ const JETBRAINS_PLUGIN_PREFIX = "github-copilot";
28
+
29
+ function skillsDir(io) { return join(io.home(), COPILOT_HOME_DIR, "skills"); }
30
+ function metaPath(io) { return join(skillsDir(io), META_FILE_NAME); }
31
+ function homeSkillsOptions(io) { return { skillsDest: skillsDir(io), metaFileName: META_FILE_NAME, messagePrefix: MESSAGE_PREFIX }; }
32
+
33
+ // ์“ฐ์ง€ ์•Š๋Š” ์‚ฌ๋žŒ ํ™ˆ์— ~/.copilot ์„ ๋งŒ๋“ค์ง€ ์•Š๋„๋ก, ํ”์ ์ด ์žˆ์„ ๋•Œ๋งŒ ์„ค์น˜ ํ›„๋ณด๋กœ ๋‘”๋‹ค
34
+ function isCopilotDetected(io) {
35
+ if (io.which(COPILOT_CLI_NAME)) return true;
36
+ if (existsSync(join(io.home(), COPILOT_HOME_DIR))) return true;
37
+ if (VSCODE_EXTENSION_DIRS.some((dir) => hasEntryWithPrefix(join(io.home(), dir), VSCODE_EXTENSION_PREFIX))) return true;
38
+ return JETBRAINS_CONFIG_ROOTS.some((root) => hasJetBrainsCopilotPlugin(join(io.home(), root)));
39
+ }
40
+
41
+ function hasJetBrainsCopilotPlugin(configRoot) {
42
+ for (const productDir of listNames(configRoot)) {
43
+ const productPath = join(configRoot, productDir);
44
+ if (hasEntryWithPrefix(join(productPath, "plugins"), JETBRAINS_PLUGIN_PREFIX)) return true;
45
+ if (hasEntryWithPrefix(productPath, JETBRAINS_PLUGIN_PREFIX)) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function hasEntryWithPrefix(dir, prefix) {
51
+ return listNames(dir).some((name) => name.toLowerCase().startsWith(prefix));
52
+ }
53
+
54
+ function listNames(dir) {
55
+ if (!existsSync(dir)) return [];
56
+ try { return readdirSync(dir); } catch { return []; }
57
+ }
58
+
59
+ function detect(io) {
60
+ const { installed, version } = readSkillsMeta(metaPath(io));
61
+ if (installed) return { installed, version, cliMissing: false };
62
+ if (isCopilotDetected(io)) return { installed: false, version: null, cliMissing: false, note: t("ide.copilot.detected") };
63
+ return { installed: false, version: null, cliMissing: true, note: t("ide.copilot.notDetected") };
64
+ }
65
+
66
+ function apply(io, ctx = {}) {
67
+ if (detect(io).cliMissing) { io.log(manualHint()); return false; }
68
+ migrateConfigRoot(io); // ๊ณต์šฉ config ๋ฃจํŠธ ์ด๊ด€
69
+ return applyHomeSkills(io, ctx, homeSkillsOptions(io));
70
+ }
71
+
72
+ // ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๋„ฃ์€ ์Šคํ‚ฌ์ด ๋‚จ์œผ๋ฉด ํด๋” ์œ ์ง€
73
+ function remove(io, ctx = {}) {
74
+ return removeHomeSkills(io, ctx, homeSkillsOptions(io));
75
+ }
76
+
77
+ function manualHint() {
78
+ return t("ide.copilot.manual");
79
+ }
80
+
81
+ export const copilotAdapter = {
82
+ id: "copilot", label: "GitHub Copilot", order: 45,
83
+ detect, apply, remove, manualHint,
84
+ };
@@ -1,95 +1,35 @@
1
1
  // Cursor ์–ด๋Œ‘ํ„ฐ (skills ๋ณต์‚ฌยท๋ฉ”ํƒ€ ๊ธฐ๋กยท์ œ๊ฑฐ).
2
- // ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์—†์Œ โ†’ ~/.cursor/skills/ ์— skills/ ํด๋”๋ฅผ ๋ณต์‚ฌํ•˜๊ณ  meta.json์œผ๋กœ ๋ฒ„์ „ ์ถ”์ .
2
+ // ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์—†์Œ โ†’ ~/.cursor/skills/ ์— skills/ ํด๋”, ~/.cursor/scripts/ ์— ๊ณต์šฉ ํŒŒ์ด์ฌ์„ ๋ณต์‚ฌํ•˜๊ณ  meta.json์œผ๋กœ ๋ฒ„์ „ ์ถ”์ .
3
3
  import { join } from "node:path";
4
- import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, writeFileSync, readdirSync } from "node:fs";
4
+ import { existsSync, readFileSync, rmSync } from "node:fs";
5
5
  import { migrateConfigRoot, isLegacyVersion } from "../legacy.js";
6
6
  import { t } from "../../../i18n/index.js";
7
- import { TEMPLATE_REPO_WEB_URL } from "../../paths.js";
7
+ import { readSkillsMeta, ownedEntries } from "../skills-copy.js";
8
+ import { applyHomeSkills, removeHomeSkills } from "../home-skills.js";
8
9
 
9
10
  const LEGACY_NAMES = ["cassiiopeia", "suh-devops-template"];
10
11
  const LEGACY_MAX_VERSION = "4.2.4";
12
+ const META_FILE_NAME = "cursor-skills-meta.json";
13
+ const MESSAGE_PREFIX = "ide.cursor";
11
14
 
12
- // ์Šคํ‚ฌ์„ ์“ฐ๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ํ•„์š” ์—†๋Š” ๊ฒƒ. projectops ์ž์‹ ์„ ๊ฒ€์‚ฌํ•˜๋Š” ํ…Œ์ŠคํŠธ๋‹ค.
13
- // skills/ ๋Š” ํ†ต์งธ๋กœ ๋ณต์‚ฌ๋˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ๊ฑธ๋Ÿฌ๋‚ด์ง€ ์•Š์œผ๋ฉด ์‚ฌ์šฉ์ž ํ™ˆ์— ๊ทธ๋Œ€๋กœ ์Œ“์ธ๋‹ค.
14
- const SKIP_IN_COPY = new Set(["conftest.py", "pytest.ini"]);
15
- const TEST_PATH = /(^|[\\/])(tests?|__pycache__)([\\/]|$)/;
16
-
17
- function metaPath(io) { return join(io.home(), ".cursor/skills/cursor-skills-meta.json"); }
15
+ function skillsDir(io) { return join(io.home(), ".cursor/skills"); }
16
+ function metaPath(io) { return join(skillsDir(io), META_FILE_NAME); }
17
+ function homeSkillsOptions(io) { return { skillsDest: skillsDir(io), metaFileName: META_FILE_NAME, messagePrefix: MESSAGE_PREFIX }; }
18
18
 
19
19
  function detect(io) {
20
- const mp = metaPath(io);
21
- if (!existsSync(mp)) return { installed: false, version: null, cliMissing: false };
22
- let version = null;
23
- try {
24
- const m = JSON.parse(readFileSync(mp, "utf8"));
25
- version = m.version || null;
26
- } catch { /* meta ์†์ƒ โ†’ ์„ค์น˜๋Š” ๋๋‹ค๊ณ  ๋ด„ */ }
27
- return { installed: true, version, cliMissing: false };
20
+ const { installed, version } = readSkillsMeta(metaPath(io));
21
+ return { installed, version, cliMissing: false };
28
22
  }
29
23
 
30
24
  function apply(io, ctx = {}) {
31
25
  migrateLegacy(io, ctx); // ์˜› ์ด๋ฆ„/๋ฒ„์ „ ๊ฐ์ง€ ์‹œ projectops ์†Œ์œ  ํ•ญ๋ชฉ ์„ ๋ณ„ ์‚ญ์ œ
32
26
  migrateConfigRoot(io); // ๊ณต์šฉ config ๋ฃจํŠธ ์ด๊ด€
33
- const src = resolveSkillsSrc(ctx);
34
- if (!src) { io.log(t("ide.cursor.noSource")); return false; }
35
- const dest = join(io.home(), ".cursor/skills");
36
- io.log(t("ide.cursor.copying"));
37
- try {
38
- mkdirSync(dest, { recursive: true });
39
- for (const e of readdirSync(src, { withFileTypes: true })) {
40
- if (SKIP_IN_COPY.has(e.name)) continue;
41
- cpSync(join(src, e.name), join(dest, e.name), {
42
- recursive: true,
43
- // ์ด ์ €์žฅ์†Œ ์ž๊ธฐ ์ฝ”๋“œ๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ํ…Œ์ŠคํŠธ๋Š” ๋‚จ์˜ ์ปดํ“จํ„ฐ๋กœ ๋‚˜๊ฐˆ ์ด์œ ๊ฐ€ ์—†๋‹ค (#591).
44
- filter: (p) => !TEST_PATH.test(p),
45
- });
46
- }
47
- // ์†Œ์Šค์—์„œ ์‚ฌ๋ผ์ง„ ์šฐ๋ฆฌ ์Šคํ‚ฌ์„ ์ง€์šด๋‹ค. ๋ณต์‚ฌ๋Š” overlay ๋ผ ๋ฎ์–ด์“ฐ๊ธฐ๋งŒ ํ•˜๋ฉด
48
- // **ํ๊ธฐํ•œ ์Šคํ‚ฌ์ด ๋‚จ์˜ ์ปดํ“จํ„ฐ์— ์˜์˜ ๋‚จ์•„** ๊ณ„์† ์žกํžŒ๋‹ค โ€” util ๋ณต์‚ฌ๊ฐ€ ๊ฐ™์€
49
- // ํ•จ์ •์— ๋น ์กŒ๋˜ ์ž๋ฆฌ๋‹ค (#500). ๋‚จ์˜ ์Šคํ‚ฌ์€ ์ ‘๋‘์‚ฌ๋กœ ๊ฑธ๋Ÿฌ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
50
- const srcNames = new Set(readdirSync(src));
51
- for (const name of readdirSync(dest)) {
52
- if (srcNames.has(name) || name === "cursor-skills-meta.json") continue;
53
- if (!/^(pro|suh)-/.test(name)) continue;
54
- try {
55
- rmSync(join(dest, name), { recursive: true, force: true });
56
- io.log(t("ide.cursor.obsolete", { name }));
57
- } catch { /* ๋ฌด์‹œ โ€” ์ง€์šฐ์ง€ ๋ชปํ•ด๋„ ์„ค์น˜๋Š” ๊ณ„์†ํ•œ๋‹ค */ }
58
- }
59
- writeMeta(io, dest, ctx.templateVersion);
60
- io.log(t("ide.cursor.installed", { dest, version: ctx.templateVersion || "unknown" }));
61
- return true;
62
- } catch {
63
- io.log(t("ide.cursor.copyFailed"));
64
- return false;
65
- }
27
+ return applyHomeSkills(io, ctx, homeSkillsOptions(io));
66
28
  }
67
29
 
30
+ // ํƒ€ ์Šคํ‚ฌ(somansa-tools ๋“ฑ) ๋‚จ์œผ๋ฉด ํด๋” ์œ ์ง€
68
31
  function remove(io, ctx = {}) {
69
- const dir = join(io.home(), ".cursor/skills");
70
- if (!existsSync(join(dir, "cursor-skills-meta.json"))) { io.log(t("ide.cursor.removeNone")); return true; }
71
- try {
72
- for (const name of ownedEntries(io, ctx)) rmSync(join(dir, name), { recursive: true, force: true });
73
- // ํด๋”๊ฐ€ ๋น„๋ฉด ํด๋” ์ž์ฒด ์ œ๊ฑฐ, ํƒ€ ์Šคํ‚ฌ(somansa-tools ๋“ฑ) ๋‚จ์œผ๋ฉด ์œ ์ง€
74
- if (existsSync(dir) && readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
75
- io.log(t("ide.cursor.removed", { dir }));
76
- return true;
77
- } catch { io.log(t("ide.cursor.removeFailed", { dir })); return false; }
78
- }
79
-
80
- // projectops๊ฐ€ ์ด ํด๋”์— ์„ค์น˜ํ–ˆ๋‹ค๊ณ  ๋ณผ ํ•ญ๋ชฉ๋งŒ ๊ณจ๋ผ๋‚ธ๋‹ค.
81
- // ์†Œ์Šค skills/ ํด๋”๋ช…(pro-*, references, config.json.example) โˆช /^suh-/ โˆช meta ํŒŒ์ผ.
82
- // ์ ‘๋‘์–ด ์—†๋Š” ํด๋”(analyzeยทgitlab ๋“ฑ somansa-tools)๋Š” ์ œ์™ธ โ†’ ๋ณด์กด.
83
- function ownedEntries(io, ctx) {
84
- const dir = join(io.home(), ".cursor/skills");
85
- if (!existsSync(dir)) return [];
86
- let srcNames = new Set();
87
- const src = resolveSkillsSrc(ctx);
88
- if (src && existsSync(src)) srcNames = new Set(readdirSync(src));
89
- const EXTRA = new Set(["cursor-skills-meta.json"]);
90
- // suh- ๋Š” ์˜› ์ ‘๋‘์‚ฌ, pro- ๋Š” ํ˜„ํ–‰ ์ ‘๋‘์‚ฌ๋‹ค. ๋‘˜ ๋‹ค ์šฐ๋ฆฌ ๊ฒƒ์œผ๋กœ ์นœ๋‹ค โ€”
91
- // ํ๊ธฐํ•œ ์Šคํ‚ฌ์€ ์†Œ์Šค์— ์—†์œผ๋ฏ€๋กœ srcNames ๋กœ๋Š” ์žกํžˆ์ง€ ์•Š๋Š”๋‹ค (#619).
92
- return readdirSync(dir).filter((name) => srcNames.has(name) || /^(pro|suh)-/.test(name) || EXTRA.has(name));
32
+ return removeHomeSkills(io, ctx, homeSkillsOptions(io));
93
33
  }
94
34
 
95
35
  // ์˜› ์ด๋ฆ„/๋ฒ„์ „ meta๋ฉด projectops ์†Œ์œ  ํ•ญ๋ชฉ๋งŒ ์„ ๋ณ„ ์‚ญ์ œ โ†’ apply๊ฐ€ ์‹ ๊ทœ ์žฌ์„ค์น˜.
@@ -101,8 +41,8 @@ function migrateLegacy(io, ctx) {
101
41
  const oldName = meta.name && LEGACY_NAMES.includes(String(meta.name).toLowerCase());
102
42
  const oldVer = isLegacyVersion(meta.version, LEGACY_MAX_VERSION);
103
43
  if (!(oldName || oldVer)) return;
104
- for (const name of ownedEntries(io, ctx)) {
105
- try { rmSync(join(io.home(), ".cursor/skills", name), { recursive: true, force: true }); } catch { /* ๋ฌด์‹œ */ }
44
+ for (const name of ownedEntries(skillsDir(io), ctx, META_FILE_NAME)) {
45
+ try { rmSync(join(skillsDir(io), name), { recursive: true, force: true }); } catch { /* ๋ฌด์‹œ */ }
106
46
  }
107
47
  io.log(t("ide.cursor.legacy", { name: meta.name, version: meta.version }));
108
48
  }
@@ -111,29 +51,6 @@ function manualHint() {
111
51
  return t("ide.cursor.manual");
112
52
  }
113
53
 
114
- // โ”€โ”€ ํ—ฌํผ โ”€โ”€
115
- // ์Šคํ‚ฌ ์†Œ์Šค: ๋‹ค์šด๋กœ๋“œ๋œ ํ…œํ”Œ๋ฆฟ(TEMP)/skills ์šฐ์„ , ์—†์œผ๋ฉด ๋กœ์ปฌ skills/.
116
- function resolveSkillsSrc(ctx) {
117
- const cands = [ctx.sourceSkillsDir, ctx.tempDir && join(ctx.tempDir, "skills"), "skills"].filter(Boolean);
118
- for (const c of cands) if (existsSync(c)) return c;
119
- return "";
120
- }
121
-
122
- function writeMeta(io, destDir, templateVersion) {
123
- const version = templateVersion || "unknown";
124
- const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
125
- const file = join(destDir, "cursor-skills-meta.json");
126
- let installedAt = now;
127
- if (existsSync(file)) { try { installedAt = JSON.parse(readFileSync(file, "utf8")).installedAt || now; } catch { /* ๋ฌดํ•ด */ } }
128
- const meta = {
129
- name: "projectops", version, scope: "user",
130
- source: TEMPLATE_REPO_WEB_URL,
131
- installPath: destDir, installedAt, lastUpdated: now,
132
- };
133
- mkdirSync(destDir, { recursive: true });
134
- writeFileSync(file, JSON.stringify(meta, null, 2) + "\n");
135
- }
136
-
137
54
  export const cursorAdapter = {
138
55
  id: "cursor", label: "Cursor", order: 20,
139
56
  detect, apply, remove, manualHint,
@@ -0,0 +1,52 @@
1
+ // ํ™ˆ ๋ณต์‚ฌํ˜• IDE(CursorยทCopilot) ์„ค์น˜ยท์ œ๊ฑฐ ํ๋ฆ„ ๊ณต์šฉ: ์†Œ์Šค ์ฐพ๊ธฐ > ๋ณต์‚ฌ > ์ง„์ž…์  > ๋กœ๊ทธ
2
+ // ๋ฉ”์‹œ์ง€๋Š” ์–ด๋Œ‘ํ„ฐ๋ณ„ ํ‚ค ๋ฌถ์Œ(<messagePrefix>.copying ๋“ฑ)์„ ์“ด๋‹ค.
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { t } from "../../i18n/index.js";
6
+ import { resolveSkillsSrc, resolveScriptsSrc, installHomeSkills, removeOwnedEntries } from "./skills-copy.js";
7
+ import { refreshStableEntrypoints, projectopsHomeOf } from "./stable-entry.js";
8
+
9
+ /**
10
+ * ์„ค์น˜ยท์—…๋ฐ์ดํŠธ: ์Šคํ‚ฌ ์†Œ์Šค ํ™•์ธ > ์Šคํ‚ฌยท๊ณต์šฉ ํŒŒ์ด์ฌ ๋ณต์‚ฌ > ๊ณ ์ • ์ง„์ž…์  > ๊ฒฐ๊ณผ ๋กœ๊ทธ
11
+ * options: { skillsDest, metaFileName, messagePrefix }
12
+ */
13
+ export function applyHomeSkills(io, ctx, { skillsDest, metaFileName, messagePrefix }) {
14
+ const skillsSrc = resolveSkillsSrc(ctx);
15
+ if (!skillsSrc) { io.log(t(`${messagePrefix}.noSource`)); return false; }
16
+ const scriptsSrc = resolveScriptsSrc(ctx, skillsSrc);
17
+ io.log(t(`${messagePrefix}.copying`));
18
+ try {
19
+ const { obsoleteNames, hasSharedScripts } = installHomeSkills({
20
+ skillsSrc, scriptsSrc, skillsDest, metaFileName, templateVersion: ctx.templateVersion,
21
+ });
22
+ for (const name of obsoleteNames) io.log(t("ide.skillsCopy.obsolete", { name }));
23
+ // ๊ณต์šฉ ํŒŒ์ด์ฌ์ด ์—†์œผ๋ฉด ์Šคํ‚ฌ CLI ๊ฐ€ import ์— ์‹คํŒจ โ†’ ์•Œ๋ฆผ๋งŒ (๋ฌธ์„œํ˜• ์Šคํ‚ฌ์€ ๋™์ž‘)
24
+ if (!hasSharedScripts) io.log(t("ide.skillsCopy.noScripts"));
25
+ io.log(t(`${messagePrefix}.installed`, { dest: skillsDest, version: ctx.templateVersion || "unknown" }));
26
+ } catch {
27
+ io.log(t(`${messagePrefix}.copyFailed`));
28
+ return false;
29
+ }
30
+ installStableEntrypoints(io, skillsSrc, scriptsSrc);
31
+ return true;
32
+ }
33
+
34
+ // ์ง„์ž…์  ์‹คํŒจ๋Š” ์„ค์น˜ ์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๋‹ค (CLI ์ฒซ ์‹คํ–‰ ๋•Œ ํŒŒ์ด์ฌ์ด ๋‹ค์‹œ ๊น๋‹ค)
35
+ function installStableEntrypoints(io, skillsSrc, scriptsSrc) {
36
+ try {
37
+ const projectopsHome = projectopsHomeOf(io);
38
+ for (const name of refreshStableEntrypoints(projectopsHome, { skillsSrc, scriptsSrc })) {
39
+ io.log(t("ide.skillsCopy.entrypoint", { path: join(projectopsHome, "bin", name) }));
40
+ }
41
+ } catch { io.log(t("ide.skillsCopy.entrypointFailed")); }
42
+ }
43
+
44
+ // ์ œ๊ฑฐ: ๋ฉ”ํƒ€ ์—†์œผ๋ฉด ๊ฑด๋„ˆ๋œ€ > ์šฐ๋ฆฌ ํ•ญ๋ชฉยท๊ณต์šฉ ํŒŒ์ด์ฌ๋งŒ ์‚ญ์ œ (๋‚จ์˜ ์Šคํ‚ฌ์ด ๋‚จ์œผ๋ฉด ํด๋” ์œ ์ง€)
45
+ export function removeHomeSkills(io, ctx, { skillsDest, metaFileName, messagePrefix }) {
46
+ if (!existsSync(join(skillsDest, metaFileName))) { io.log(t(`${messagePrefix}.removeNone`)); return true; }
47
+ try {
48
+ removeOwnedEntries(skillsDest, ctx, metaFileName);
49
+ io.log(t(`${messagePrefix}.removed`, { dir: skillsDest }));
50
+ return true;
51
+ } catch { io.log(t(`${messagePrefix}.removeFailed`, { dir: skillsDest })); return false; }
52
+ }
@@ -9,6 +9,7 @@ import { claudeAdapter } from "./adapters/claude.js";
9
9
  import { cursorAdapter } from "./adapters/cursor.js";
10
10
  import { geminiAdapter } from "./adapters/gemini.js";
11
11
  import { codexAdapter } from "./adapters/codex.js";
12
+ import { copilotAdapter } from "./adapters/copilot.js";
12
13
  import { piAdapter } from "./adapters/pi.js";
13
14
  import { piHarnessAdapter } from "./adapters/pi-harness.js";
14
15
 
@@ -18,6 +19,7 @@ export const ADAPTERS = [
18
19
  cursorAdapter,
19
20
  geminiAdapter,
20
21
  codexAdapter,
22
+ copilotAdapter,
21
23
  piAdapter,
22
24
  piHarnessAdapter,
23
25
  ].map(assertAdapter).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
@@ -54,6 +54,8 @@ export function defaultIo() {
54
54
  which,
55
55
  run,
56
56
  home: () => homedir(),
57
+ // ํŒŒ์ด์ฌ stable_entry ์™€ ๊ฐ™์€ ๊ทœ์น™: PROJECTOPS_HOME > ~/.projectops
58
+ projectopsHome: () => process.env.PROJECTOPS_HOME || join(homedir(), ".projectops"),
57
59
  log: (msg) => console.error(msg), // ์•ˆ๋‚ด๋Š” stderr๋กœ (stdout ํŒŒ์ดํ”„ ์˜ค์—ผ ๋ฐฉ์ง€)
58
60
  };
59
61
  }