@vsem/ai 0.1.1 → 0.1.3
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 +15 -1
- package/bin/cli.js +165 -19
- package/package.json +1 -1
- package/templates/system/CLAUDE.md.tmpl +3 -0
package/README.md
CHANGED
|
@@ -14,12 +14,26 @@ npx @vsem/ai@latest
|
|
|
14
14
|
Runs against the current directory by default. Optional arguments:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @vsem/ai@latest [target-dir] [--branch <name>] [--force]
|
|
17
|
+
npx @vsem/ai@latest [target-dir] [--branch <name>] [--lang <language>] [--force] [--yes]
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
- `target-dir` — install into this directory instead of the current one (created if missing).
|
|
21
21
|
- `--branch <name>` — personal branch name (default: your `git config --global user.name`, or `dev`).
|
|
22
|
+
- `--lang <language>` — language the assistant should use to talk to you and to reason, written into `CLAUDE.md` (default: Russian).
|
|
22
23
|
- `--force` — also overwrite project-owned files (`.ai/project.md`, `.ai/plan.md`, task/decision/incident indexes), resetting them to placeholders. Without this flag, reruns only update framework-owned files (`.ai/rules/`, `.ai/roles/`, `CLAUDE.md`) and never touch your own project state.
|
|
24
|
+
- `--yes` / `-y` — skip the interactive wizard and confirmation prompt, accepting all defaults.
|
|
25
|
+
|
|
26
|
+
In an interactive terminal, the CLI shows a banner and asks for whatever
|
|
27
|
+
wasn't already given on the command line (target folder, language, branch),
|
|
28
|
+
then shows an install confirmation "button" before touching disk — press
|
|
29
|
+
Enter to proceed or `n` to cancel. Any value passed as a flag skips its
|
|
30
|
+
question. In non-interactive contexts (CI, pipes) it detects the lack of a
|
|
31
|
+
TTY and proceeds automatically with defaults, same as passing `--yes`.
|
|
32
|
+
|
|
33
|
+
The generated workspace's local git repo also gets `core.autocrlf` disabled,
|
|
34
|
+
so `git add`/`commit` don't print `LF will be replaced by CRLF` warnings for
|
|
35
|
+
this project's text files on Windows. This only affects that one repo, not
|
|
36
|
+
your global git config.
|
|
23
37
|
|
|
24
38
|
## Updating an existing workspace
|
|
25
39
|
|
package/bin/cli.js
CHANGED
|
@@ -17,28 +17,126 @@
|
|
|
17
17
|
// user's own project state. Pass --force to overwrite scaffold files too.
|
|
18
18
|
//
|
|
19
19
|
// Usage:
|
|
20
|
-
// npx @vsem/ai@latest [target-dir] [--branch <name>] [--force]
|
|
20
|
+
// npx @vsem/ai@latest [target-dir] [--branch <name>] [--lang <language>] [--force] [--yes]
|
|
21
21
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
|
+
const readline = require('readline');
|
|
24
25
|
const { execFileSync } = require('child_process');
|
|
25
26
|
|
|
27
|
+
const PACKAGE_JSON = require('../package.json');
|
|
26
28
|
const FRAMEWORK_VERSION = '0.1.0';
|
|
29
|
+
const DEFAULT_LANGUAGE = 'Русский';
|
|
27
30
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
28
31
|
const TEMPLATES_DIR = path.join(PACKAGE_DIR, 'templates');
|
|
29
32
|
const SYSTEM_DIR = path.join(TEMPLATES_DIR, 'system');
|
|
30
33
|
const SCAFFOLD_DIR = path.join(TEMPLATES_DIR, 'scaffold');
|
|
31
34
|
|
|
35
|
+
// --- terminal styling -------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
38
|
+
|
|
39
|
+
function style(codes, text) {
|
|
40
|
+
if (!supportsColor) return text;
|
|
41
|
+
return `\x1b[${codes}m${text}\x1b[0m`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cyan = (s) => style('36', s);
|
|
45
|
+
const cyanBold = (s) => style('1;36', s);
|
|
46
|
+
const dim = (s) => style('2', s);
|
|
47
|
+
const green = (s) => style('32', s);
|
|
48
|
+
const yellow = (s) => style('33', s);
|
|
49
|
+
const button = (s) => style('1;30;46', s); // bold black text on cyan background
|
|
50
|
+
|
|
51
|
+
// 5-row pixel font, just enough to spell VSEM.
|
|
52
|
+
const GLYPHS = {
|
|
53
|
+
V: ['█ █', '█ █', '█ █', ' █ █ ', ' █ '],
|
|
54
|
+
S: [' ████', '█ ', ' ███ ', ' █', '████ '],
|
|
55
|
+
E: ['█████', '█ ', '████ ', '█ ', '█████'],
|
|
56
|
+
M: ['█ █', '██ ██', '█ █ █', '█ █', '█ █'],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function renderLogo(word) {
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (let row = 0; row < 5; row++) {
|
|
62
|
+
rows.push(word.split('').map((ch) => GLYPHS[ch][row]).join(' '));
|
|
63
|
+
}
|
|
64
|
+
return rows.map(cyanBold).join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printBanner() {
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(renderLogo('VSEM'));
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(cyan('vsem-framework') + dim(` · CLI v${PACKAGE_JSON.version} · шаблон v${FRAMEWORK_VERSION}`));
|
|
72
|
+
console.log(dim('Разворачивает рабочее пространство для AI-агента: .ai/ (роли, правила,'));
|
|
73
|
+
console.log(dim('задачи, решения, инциденты), CLAUDE.md и личную git-ветку — в этой папке.'));
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderButton(label) {
|
|
78
|
+
const padded = ` ${label} `;
|
|
79
|
+
const top = '┌' + '─'.repeat(padded.length) + '┐';
|
|
80
|
+
const mid = '│' + button(padded) + '│';
|
|
81
|
+
const bottom = '└' + '─'.repeat(padded.length) + '┘';
|
|
82
|
+
return `${top}\n${mid}\n${bottom}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Asks a question with a default value shown in the prompt; Enter accepts it.
|
|
86
|
+
function ask(rl, question, defaultValue) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
rl.question(question, (answer) => {
|
|
89
|
+
const trimmed = answer.trim();
|
|
90
|
+
resolve(trimmed || defaultValue);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Shows an install "button" and waits for confirmation. Auto-confirms when
|
|
96
|
+
// stdin isn't a TTY (CI, pipes) or when --yes was passed, so scripted/non-
|
|
97
|
+
// interactive runs never hang. `rl` must be provided whenever this runs
|
|
98
|
+
// interactively (i.e. whenever opts.yes is false and stdin is a TTY).
|
|
99
|
+
function confirmInstall(info, opts, rl) {
|
|
100
|
+
console.log(` ${dim('Папка:')} ${info.target}`);
|
|
101
|
+
console.log(` ${dim('Ветка:')} ${info.branch}`);
|
|
102
|
+
console.log(` ${dim('Язык:')} ${info.language}`);
|
|
103
|
+
console.log(` ${dim('Режим:')} ${info.isRerun ? 'обновление существующего пространства' : 'новая установка'}`);
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
107
|
+
console.log(renderButton('Установить') + '\n');
|
|
108
|
+
console.log(dim(opts.yes ? 'Подтверждение пропущено (--yes).' : 'Неинтерактивный режим — продолжаю автоматически.'));
|
|
109
|
+
console.log('');
|
|
110
|
+
return Promise.resolve(true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(renderButton('Установить') + '\n');
|
|
114
|
+
console.log(dim('Enter — установить, n — отменить'));
|
|
115
|
+
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
rl.question('> ', (answer) => {
|
|
118
|
+
console.log('');
|
|
119
|
+
resolve(!/^n(o)?$/i.test(answer.trim()));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
32
124
|
function parseArgs(argv) {
|
|
33
|
-
const opts = { target: null, branch: null, force: false };
|
|
125
|
+
const opts = { target: null, branch: null, lang: null, force: false, yes: false };
|
|
34
126
|
for (let i = 0; i < argv.length; i++) {
|
|
35
127
|
const a = argv[i];
|
|
36
128
|
if (a === '--force') {
|
|
37
129
|
opts.force = true;
|
|
130
|
+
} else if (a === '--yes' || a === '-y') {
|
|
131
|
+
opts.yes = true;
|
|
38
132
|
} else if (a === '--branch') {
|
|
39
133
|
opts.branch = argv[++i] || null;
|
|
40
134
|
} else if (a.startsWith('--branch=')) {
|
|
41
135
|
opts.branch = a.slice('--branch='.length);
|
|
136
|
+
} else if (a === '--lang') {
|
|
137
|
+
opts.lang = argv[++i] || null;
|
|
138
|
+
} else if (a.startsWith('--lang=')) {
|
|
139
|
+
opts.lang = a.slice('--lang='.length);
|
|
42
140
|
} else if (!a.startsWith('-') && opts.target === null) {
|
|
43
141
|
opts.target = a;
|
|
44
142
|
}
|
|
@@ -100,30 +198,70 @@ function tryGit(args, cwd) {
|
|
|
100
198
|
}
|
|
101
199
|
}
|
|
102
200
|
|
|
103
|
-
|
|
201
|
+
// Personal-branch name suggested from the user's global git identity, falling
|
|
202
|
+
// back to 'dev'. `cwd` must already exist (global config doesn't care where
|
|
203
|
+
// it's read from, but a missing directory would make the git spawn itself fail).
|
|
204
|
+
function suggestBranch(cwd) {
|
|
205
|
+
const cfg = tryGit(['config', '--global', 'user.name'], cwd);
|
|
206
|
+
if (cfg.ok && cfg.out.trim()) {
|
|
207
|
+
const slug = cfg.out.trim().toLowerCase().replace(/\s+/g, '-').replace(/^-+|-+$/g, '');
|
|
208
|
+
if (slug) return slug;
|
|
209
|
+
}
|
|
210
|
+
return 'dev';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function main() {
|
|
104
214
|
if (!fs.existsSync(SYSTEM_DIR)) {
|
|
105
215
|
console.error(`Templates not found at ${SYSTEM_DIR}. This script must ship together with templates/.`);
|
|
106
216
|
process.exit(1);
|
|
107
217
|
}
|
|
108
218
|
|
|
109
219
|
const args = parseArgs(process.argv.slice(2));
|
|
110
|
-
|
|
220
|
+
printBanner();
|
|
221
|
+
|
|
222
|
+
// Only prompt when nothing overrides it and we can actually read an answer.
|
|
223
|
+
// Any explicit flag (--branch, --lang, positional target dir) skips its
|
|
224
|
+
// corresponding question. --yes or a non-TTY stdin (CI, pipes) skips the
|
|
225
|
+
// whole wizard so scripted runs never hang.
|
|
226
|
+
const interactive = !args.yes && process.stdin.isTTY;
|
|
227
|
+
const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
228
|
+
|
|
229
|
+
let targetInput = args.target;
|
|
230
|
+
if (interactive && !targetInput) {
|
|
231
|
+
targetInput = await ask(rl, `${dim("Папка")} (путь; '.' — текущая, название/путь — новая) [.]: `, '.');
|
|
232
|
+
}
|
|
111
233
|
|
|
234
|
+
const target = path.resolve(targetInput || '.');
|
|
112
235
|
fs.mkdirSync(target, { recursive: true });
|
|
113
236
|
const aiDir = path.join(target, '.ai');
|
|
114
237
|
const workspaceName = path.basename(target);
|
|
115
238
|
const isRerun = fs.existsSync(aiDir);
|
|
116
239
|
|
|
240
|
+
let language = args.lang;
|
|
117
241
|
let branch = args.branch;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
242
|
+
|
|
243
|
+
if (interactive) {
|
|
244
|
+
if (!language) {
|
|
245
|
+
language = await ask(rl, `${dim('Язык общения и рассуждений ассистента')} [${DEFAULT_LANGUAGE}]: `, DEFAULT_LANGUAGE);
|
|
246
|
+
}
|
|
247
|
+
if (!branch) {
|
|
248
|
+
const suggested = suggestBranch(target);
|
|
249
|
+
branch = await ask(rl, `${dim('Ветка')} [${suggested}]: `, suggested);
|
|
122
250
|
}
|
|
123
|
-
|
|
251
|
+
} else {
|
|
252
|
+
if (!language) language = DEFAULT_LANGUAGE;
|
|
253
|
+
if (!branch) branch = suggestBranch(target);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const proceed = await confirmInstall({ target, branch, language, isRerun }, args, rl);
|
|
257
|
+
if (rl) rl.close();
|
|
258
|
+
|
|
259
|
+
if (!proceed) {
|
|
260
|
+
console.log(yellow('[cancelled] Ничего не изменено.'));
|
|
261
|
+
return;
|
|
124
262
|
}
|
|
125
263
|
|
|
126
|
-
const vars = { WORKSPACE_NAME: workspaceName, FRAMEWORK_VERSION: FRAMEWORK_VERSION };
|
|
264
|
+
const vars = { WORKSPACE_NAME: workspaceName, FRAMEWORK_VERSION: FRAMEWORK_VERSION, LANGUAGE: language };
|
|
127
265
|
|
|
128
266
|
copyTemplateTree(SYSTEM_DIR, target, vars, true);
|
|
129
267
|
copyTemplateTree(SCAFFOLD_DIR, target, vars, args.force);
|
|
@@ -133,22 +271,27 @@ function main() {
|
|
|
133
271
|
}
|
|
134
272
|
|
|
135
273
|
if (isRerun) {
|
|
136
|
-
console.log(`[ok] Workspace synced at ${target} (system files updated; scaffold files kept unless --force)`);
|
|
274
|
+
console.log(green(`[ok] Workspace synced at ${target} (system files updated; scaffold files kept unless --force)`));
|
|
137
275
|
} else {
|
|
138
|
-
console.log(`[ok] Workspace structure created at ${target}`);
|
|
276
|
+
console.log(green(`[ok] Workspace structure created at ${target}`));
|
|
139
277
|
}
|
|
140
278
|
|
|
141
279
|
const gitCheck = tryGit(['--version'], target);
|
|
142
280
|
if (!gitCheck.ok) {
|
|
143
|
-
console.log('[warn] git not found in PATH - skipping git init.');
|
|
281
|
+
console.log(yellow('[warn] git not found in PATH - skipping git init.'));
|
|
144
282
|
return;
|
|
145
283
|
}
|
|
146
284
|
|
|
147
285
|
if (!fs.existsSync(path.join(target, '.git'))) {
|
|
148
286
|
tryGit(['init', '-q'], target);
|
|
149
|
-
console.log('[ok] git init');
|
|
287
|
+
console.log(green('[ok] git init'));
|
|
150
288
|
}
|
|
151
289
|
|
|
290
|
+
// Local-only override: stops git from silently converting LF<->CRLF (and
|
|
291
|
+
// printing a warning per file) on Windows checkouts, regardless of the
|
|
292
|
+
// user's global core.autocrlf. This workspace's text files are always LF.
|
|
293
|
+
tryGit(['config', 'core.autocrlf', 'false'], target);
|
|
294
|
+
|
|
152
295
|
const currentBranchResult = tryGit(['branch', '--show-current'], target);
|
|
153
296
|
const currentBranch = currentBranchResult.ok ? currentBranchResult.out.trim() : '';
|
|
154
297
|
|
|
@@ -159,7 +302,7 @@ function main() {
|
|
|
159
302
|
} else {
|
|
160
303
|
tryGit(['checkout', '-q', '-b', branch], target);
|
|
161
304
|
}
|
|
162
|
-
console.log(`[ok] branch '${branch}' is active`);
|
|
305
|
+
console.log(green(`[ok] branch '${branch}' is active`));
|
|
163
306
|
}
|
|
164
307
|
|
|
165
308
|
tryGit(['add', '-A'], target);
|
|
@@ -169,13 +312,16 @@ function main() {
|
|
|
169
312
|
const msg = isRerun ? 'chore: sync vsem-framework system files' : 'chore: bootstrap vsem-framework workspace';
|
|
170
313
|
const commitResult = tryGit(['commit', '-q', '-m', msg], target);
|
|
171
314
|
if (commitResult.ok) {
|
|
172
|
-
console.log(`[ok] commit created: ${msg}`);
|
|
315
|
+
console.log(green(`[ok] commit created: ${msg}`));
|
|
173
316
|
} else {
|
|
174
|
-
console.log('[warn] git commit failed (is git config user.name/user.email set?) - changes are staged but not committed.');
|
|
317
|
+
console.log(yellow('[warn] git commit failed (is git config user.name/user.email set?) - changes are staged but not committed.'));
|
|
175
318
|
}
|
|
176
319
|
} else {
|
|
177
|
-
console.log('[skip] nothing to commit (already up to date)');
|
|
320
|
+
console.log(dim('[skip] nothing to commit (already up to date)'));
|
|
178
321
|
}
|
|
179
322
|
}
|
|
180
323
|
|
|
181
|
-
main()
|
|
324
|
+
main().catch((err) => {
|
|
325
|
+
console.error(err);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|
package/package.json
CHANGED
|
@@ -4,5 +4,8 @@ This project is managed by vsem-framework.
|
|
|
4
4
|
Work according to the rules and structure in `.ai/`.
|
|
5
5
|
Start with `.ai/rules/core.md` and `.ai/project.md`.
|
|
6
6
|
|
|
7
|
+
Communicate with the user, and think, in **{{LANGUAGE}}**, unless they ask you
|
|
8
|
+
to switch.
|
|
9
|
+
|
|
7
10
|
**Hard rule:** never modify the top-level structure of `.ai/` (create/delete/rename
|
|
8
11
|
top-level folders inside it) without explicit human confirmation.
|