@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 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
- function main() {
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
- const target = path.resolve(args.target || process.cwd());
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
- if (!branch) {
119
- const cfg = tryGit(['config', '--global', 'user.name'], target);
120
- if (cfg.ok && cfg.out.trim()) {
121
- branch = cfg.out.trim().toLowerCase().replace(/\s+/g, '-').replace(/^-+|-+$/g, '');
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
- if (!branch) branch = 'dev';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsem/ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Install or sync a vsem-framework workspace (.ai/ structure, roles, rules) via npx.",
5
5
  "bin": {
6
6
  "vsem-ai": "bin/cli.js"
@@ -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.