agentic-dev 0.2.9 → 0.2.10
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/bin/agentic-dev.mjs +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,12 +20,13 @@ npx agentic-dev init my-app --template template-web --yes --skip-bootstrap
|
|
|
20
20
|
1. GitHub에서 공개 `say828/template-*` 레포 목록을 조회한다.
|
|
21
21
|
2. 사용자가 프로젝트 디렉터리와 템플릿 레포를 고른다.
|
|
22
22
|
- interactive TTY에서는 템플릿 레포를 `↑/↓ + Enter`로 선택한다.
|
|
23
|
-
3.
|
|
24
|
-
4.
|
|
25
|
-
5.
|
|
26
|
-
6.
|
|
27
|
-
7.
|
|
28
|
-
8. 그 target에 대해
|
|
23
|
+
3. 모든 선택을 받은 뒤 실행 계획 요약을 보여주고 최종 확인을 받는다.
|
|
24
|
+
4. 확인 후에만 선택한 레포를 새 디렉터리로 복제한다.
|
|
25
|
+
5. `.env.example`이 있으면 `.env`를 자동 생성한다.
|
|
26
|
+
6. `pnpm install`을 자동 실행한다.
|
|
27
|
+
7. 템플릿의 `repo-contract.json`에서 `frontend.default_target`을 읽는다.
|
|
28
|
+
8. 그 target에 대해 `playwright install chromium`을 실행한다.
|
|
29
|
+
9. 그 target에 대해 parity bootstrap을 실행한다.
|
|
29
30
|
|
|
30
31
|
`frontend.default_target`이 있으면 multi-surface 템플릿도 그대로 지원한다. 예를 들어 `template-fullstack-mono`는 `client/web`과 `client/admin`을 함께 포함하지만 기본 bootstrap 대상은 `web`이다.
|
|
31
32
|
|
package/bin/agentic-dev.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import * as readline from "node:readline";
|
|
@@ -32,6 +33,17 @@ function renderRepoSelect(label, repos, cursor) {
|
|
|
32
33
|
return lines.length;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function renderChoiceSelect(label, choices, cursor) {
|
|
37
|
+
const lines = [`${label} Use ↑/↓ and Enter.`];
|
|
38
|
+
for (let index = 0; index < choices.length; index += 1) {
|
|
39
|
+
const pointer = index === cursor ? ">" : " ";
|
|
40
|
+
const summary = choices[index].description ? ` - ${choices[index].description}` : "";
|
|
41
|
+
lines.push(`${pointer} ${choices[index].label}${summary}`);
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
44
|
+
return lines.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
async function runArrowMenu(render, onInput) {
|
|
36
48
|
const stdin = process.stdin;
|
|
37
49
|
const stdout = process.stdout;
|
|
@@ -121,6 +133,77 @@ async function promptForTemplateRepo(rl, repos, owner) {
|
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
async function promptForChoice(rl, label, choices) {
|
|
137
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
138
|
+
console.log("");
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
return runArrowMenu(
|
|
141
|
+
() => renderChoiceSelect(label, choices, cursor),
|
|
142
|
+
(chunk, rerender) => {
|
|
143
|
+
if (chunk === "\u0003") {
|
|
144
|
+
throw new Error("Prompt cancelled");
|
|
145
|
+
}
|
|
146
|
+
if (chunk === "\r" || chunk === "\n") {
|
|
147
|
+
return choices[cursor].value;
|
|
148
|
+
}
|
|
149
|
+
if (chunk === "\u001b[A") {
|
|
150
|
+
cursor = (cursor - 1 + choices.length) % choices.length;
|
|
151
|
+
rerender();
|
|
152
|
+
} else if (chunk === "\u001b[B") {
|
|
153
|
+
cursor = (cursor + 1) % choices.length;
|
|
154
|
+
rerender();
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(label);
|
|
163
|
+
choices.forEach((choice, index) => {
|
|
164
|
+
const summary = choice.description ? ` - ${choice.description}` : "";
|
|
165
|
+
console.log(` ${index + 1}. ${choice.label}${summary}`);
|
|
166
|
+
});
|
|
167
|
+
console.log("");
|
|
168
|
+
|
|
169
|
+
while (true) {
|
|
170
|
+
const answer = (await rl.question("Select option: ")).trim();
|
|
171
|
+
if (/^\d+$/.test(answer)) {
|
|
172
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
173
|
+
if (index >= 0 && index < choices.length) {
|
|
174
|
+
return choices[index].value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const exact = choices.find((choice) => choice.label === answer || choice.value === answer);
|
|
178
|
+
if (exact) {
|
|
179
|
+
return exact.value;
|
|
180
|
+
}
|
|
181
|
+
console.log(`Invalid selection: ${answer || "(empty)"}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function directoryHasUserFiles(targetDir) {
|
|
186
|
+
const resolved = path.resolve(process.cwd(), targetDir);
|
|
187
|
+
if (!fs.existsSync(resolved)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const entries = fs
|
|
192
|
+
.readdirSync(resolved, { withFileTypes: true })
|
|
193
|
+
.filter((entry) => entry.name !== ".git");
|
|
194
|
+
return entries.length > 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printPlannedRun(options) {
|
|
198
|
+
console.log("");
|
|
199
|
+
console.log("Plan:");
|
|
200
|
+
console.log(` Project directory: ${path.resolve(process.cwd(), options.targetDir)}`);
|
|
201
|
+
console.log(` Template repo: ${options.template}`);
|
|
202
|
+
console.log(` GitHub owner: ${options.owner}`);
|
|
203
|
+
console.log(` Allow non-empty directory: ${options.force ? "yes" : "no"}`);
|
|
204
|
+
console.log(` Run install/bootstrap: ${options.skipBootstrap ? "no" : "yes"}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
124
207
|
async function promptForMissing(options, repos) {
|
|
125
208
|
if (options.yes && (!options.targetDir || !options.template)) {
|
|
126
209
|
throw new Error("`--yes` requires both target directory and template repo.");
|
|
@@ -147,6 +230,28 @@ async function promptForMissing(options, repos) {
|
|
|
147
230
|
if (!options.template) {
|
|
148
231
|
options.template = await promptForTemplateRepo(rl, repos, options.owner);
|
|
149
232
|
}
|
|
233
|
+
|
|
234
|
+
if (!options.force && directoryHasUserFiles(options.targetDir)) {
|
|
235
|
+
const allowOverwrite = await promptForChoice(rl, "Target directory is not empty.", [
|
|
236
|
+
{ label: "Continue", value: true, description: "Scaffold into the existing directory" },
|
|
237
|
+
{ label: "Cancel", value: false, description: "Stop without changing files" },
|
|
238
|
+
]);
|
|
239
|
+
if (!allowOverwrite) {
|
|
240
|
+
throw new Error("Prompt cancelled");
|
|
241
|
+
}
|
|
242
|
+
options.force = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!options.yes) {
|
|
246
|
+
printPlannedRun(options);
|
|
247
|
+
const proceed = await promptForChoice(rl, "Run scaffold now?", [
|
|
248
|
+
{ label: "Proceed", value: true, description: "Clone, install, and bootstrap now" },
|
|
249
|
+
{ label: "Cancel", value: false, description: "Stop before running commands" },
|
|
250
|
+
]);
|
|
251
|
+
if (!proceed) {
|
|
252
|
+
throw new Error("Prompt cancelled");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
150
255
|
} finally {
|
|
151
256
|
rl.close();
|
|
152
257
|
}
|