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 +7 -6
- package/package.json +1 -1
- package/src/cli/args.js +2 -0
- package/src/cli/help.js +6 -0
- package/src/commands/doctor.js +17 -0
- package/src/commands/full.js +1 -1
- package/src/commands/version.js +1 -1
- package/src/core/copy/readme.js +13 -5
- package/src/core/copy/simple.js +3 -1
- package/src/core/development-branch.js +58 -0
- package/src/core/ide/adapter.js +1 -1
- package/src/core/ide/adapters/copilot.js +84 -0
- package/src/core/ide/adapters/cursor.js +16 -99
- package/src/core/ide/home-skills.js +52 -0
- package/src/core/ide/registry.js +2 -0
- package/src/core/ide/runner.js +2 -0
- package/src/core/ide/skills-copy.js +147 -0
- package/src/core/ide/stable-entry.js +51 -0
- package/src/core/migration-guide.js +11 -3
- package/src/core/options-ask.js +11 -9
- package/src/core/options-schema.js +11 -1
- package/src/core/placeholder-triggers.js +80 -0
- package/src/i18n/doctor.en.js +4 -0
- package/src/i18n/doctor.ko.js +4 -0
- package/src/i18n/en.js +2 -2
- package/src/i18n/ide.en.js +15 -2
- package/src/i18n/ide.ko.js +15 -2
- package/src/i18n/ko.js +2 -2
- package/src/i18n/summary.en.js +5 -1
- package/src/i18n/summary.ko.js +5 -1
- package/src/index.js +13 -1
- package/src/ui/summary.js +11 -1
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.
|
|
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 |
|
|
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** (
|
|
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
|
|
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
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 ์ฑ์ด ํ์ํ๋ค.
|
package/src/commands/doctor.js
CHANGED
|
@@ -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"),
|
package/src/commands/full.js
CHANGED
|
@@ -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) โ "๋ด ์คํฌ๋ฆฝํธ๊ฐ ๊ฐฑ์ ๋๋"๋ฅผ ๋ก๊ทธ๋ง ๋ณด๊ณ ์ ์ ์๊ฒ.
|
package/src/commands/version.js
CHANGED
|
@@ -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
|
package/src/core/copy/readme.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
`##
|
|
32
|
+
`## ${text.heading} : v${version}\n` +
|
|
25
33
|
"\n" +
|
|
26
|
-
|
|
34
|
+
`[${text.changelogLink}](CHANGELOG.md)\n`;
|
|
27
35
|
appendFileSync(p, section);
|
|
28
36
|
return "added";
|
|
29
37
|
}
|
package/src/core/copy/simple.js
CHANGED
|
@@ -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
|
+
}
|
package/src/core/ide/adapter.js
CHANGED
|
@@ -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/
|
|
2
|
+
// ๋ง์ผํ๋ ์ด์ค ์์ โ ~/.cursor/skills/ ์ skills/ ํด๋, ~/.cursor/scripts/ ์ ๊ณต์ฉ ํ์ด์ฌ์ ๋ณต์ฌํ๊ณ meta.json์ผ๋ก ๋ฒ์ ์ถ์ .
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { existsSync, readFileSync,
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/core/ide/registry.js
CHANGED
|
@@ -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));
|
package/src/core/ide/runner.js
CHANGED
|
@@ -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
|
}
|