agentic-dev 0.2.8 → 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 +8 -6
- package/bin/agentic-dev.mjs +214 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,12 +19,14 @@ npx agentic-dev init my-app --template template-web --yes --skip-bootstrap
|
|
|
19
19
|
|
|
20
20
|
1. GitHub에서 공개 `say828/template-*` 레포 목록을 조회한다.
|
|
21
21
|
2. 사용자가 프로젝트 디렉터리와 템플릿 레포를 고른다.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
- interactive TTY에서는 템플릿 레포를 `↑/↓ + Enter`로 선택한다.
|
|
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을 실행한다.
|
|
28
30
|
|
|
29
31
|
`frontend.default_target`이 있으면 multi-surface 템플릿도 그대로 지원한다. 예를 들어 `template-fullstack-mono`는 `client/web`과 `client/admin`을 함께 포함하지만 기본 bootstrap 대상은 `web`이다.
|
|
30
32
|
|
package/bin/agentic-dev.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
5
|
+
import * as readline from "node:readline";
|
|
4
6
|
import { createInterface } from "node:readline/promises";
|
|
5
7
|
import {
|
|
6
8
|
ensureTargetDir,
|
|
@@ -12,6 +14,196 @@ import {
|
|
|
12
14
|
usage,
|
|
13
15
|
} from "../lib/scaffold.mjs";
|
|
14
16
|
|
|
17
|
+
function clearMenu(lines) {
|
|
18
|
+
if (lines <= 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
readline.moveCursor(process.stdout, 0, -lines);
|
|
22
|
+
readline.clearScreenDown(process.stdout);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderRepoSelect(label, repos, cursor) {
|
|
26
|
+
const lines = [`${label} Use ↑/↓ and Enter.`];
|
|
27
|
+
for (let index = 0; index < repos.length; index += 1) {
|
|
28
|
+
const pointer = index === cursor ? ">" : " ";
|
|
29
|
+
const summary = repos[index].description ? ` - ${repos[index].description}` : "";
|
|
30
|
+
lines.push(`${pointer} ${repos[index].name}${summary}`);
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
33
|
+
return lines.length;
|
|
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
|
+
|
|
47
|
+
async function runArrowMenu(render, onInput) {
|
|
48
|
+
const stdin = process.stdin;
|
|
49
|
+
const stdout = process.stdout;
|
|
50
|
+
const previousRawMode = typeof stdin.setRawMode === "function" ? stdin.isRaw : undefined;
|
|
51
|
+
|
|
52
|
+
if (typeof stdin.setRawMode === "function") {
|
|
53
|
+
stdin.setRawMode(true);
|
|
54
|
+
}
|
|
55
|
+
stdin.resume();
|
|
56
|
+
stdin.setEncoding("utf8");
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let renderedLines = render();
|
|
60
|
+
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
stdin.removeListener("data", handleData);
|
|
63
|
+
if (typeof stdin.setRawMode === "function") {
|
|
64
|
+
stdin.setRawMode(Boolean(previousRawMode));
|
|
65
|
+
}
|
|
66
|
+
stdout.write("\n");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const rerender = () => {
|
|
70
|
+
clearMenu(renderedLines);
|
|
71
|
+
renderedLines = render();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleData = (chunk) => {
|
|
75
|
+
try {
|
|
76
|
+
const result = onInput(chunk, rerender);
|
|
77
|
+
if (result !== undefined) {
|
|
78
|
+
clearMenu(renderedLines);
|
|
79
|
+
cleanup();
|
|
80
|
+
resolve(result);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
clearMenu(renderedLines);
|
|
84
|
+
cleanup();
|
|
85
|
+
reject(error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
stdin.on("data", handleData);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function promptForTemplateRepo(rl, repos, owner) {
|
|
94
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
95
|
+
console.log("");
|
|
96
|
+
let cursor = 0;
|
|
97
|
+
return runArrowMenu(
|
|
98
|
+
() => renderRepoSelect(`Public template repos from ${owner}:`, repos, cursor),
|
|
99
|
+
(chunk, rerender) => {
|
|
100
|
+
if (chunk === "\u0003") {
|
|
101
|
+
throw new Error("Prompt cancelled");
|
|
102
|
+
}
|
|
103
|
+
if (chunk === "\r" || chunk === "\n") {
|
|
104
|
+
return repos[cursor].name;
|
|
105
|
+
}
|
|
106
|
+
if (chunk === "\u001b[A") {
|
|
107
|
+
cursor = (cursor - 1 + repos.length) % repos.length;
|
|
108
|
+
rerender();
|
|
109
|
+
} else if (chunk === "\u001b[B") {
|
|
110
|
+
cursor = (cursor + 1) % repos.length;
|
|
111
|
+
rerender();
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(`Public template repos from ${owner}:`);
|
|
120
|
+
repos.forEach((repo, index) => {
|
|
121
|
+
const summary = repo.description ? ` - ${repo.description}` : "";
|
|
122
|
+
console.log(` ${index + 1}. ${repo.name}${summary}`);
|
|
123
|
+
});
|
|
124
|
+
console.log("");
|
|
125
|
+
|
|
126
|
+
while (true) {
|
|
127
|
+
const answer = await rl.question("Select template repo (number or repo name): ");
|
|
128
|
+
try {
|
|
129
|
+
return selectTemplateRepo(answer, repos).name;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(error.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
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
|
+
|
|
15
207
|
async function promptForMissing(options, repos) {
|
|
16
208
|
if (options.yes && (!options.targetDir || !options.template)) {
|
|
17
209
|
throw new Error("`--yes` requires both target directory and template repo.");
|
|
@@ -36,21 +228,28 @@ async function promptForMissing(options, repos) {
|
|
|
36
228
|
}
|
|
37
229
|
|
|
38
230
|
if (!options.template) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
231
|
+
options.template = await promptForTemplateRepo(rl, repos, options.owner);
|
|
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");
|
|
54
253
|
}
|
|
55
254
|
}
|
|
56
255
|
} finally {
|