@vsem/ai 0.1.0 → 0.1.2
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 +32 -1
- package/bin/cli.js +111 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,15 +14,46 @@ 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>] [--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
22
|
- `--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.
|
|
23
|
+
- `--yes` / `-y` — skip the interactive confirmation prompt.
|
|
24
|
+
|
|
25
|
+
In an interactive terminal, the CLI shows a banner and an install confirmation
|
|
26
|
+
prompt before touching disk; press Enter to proceed or `n` to cancel. In
|
|
27
|
+
non-interactive contexts (CI, pipes) it detects the lack of a TTY and
|
|
28
|
+
proceeds automatically, same as passing `--yes`.
|
|
23
29
|
|
|
24
30
|
## Updating an existing workspace
|
|
25
31
|
|
|
26
32
|
Rerun `npx @vsem/ai@latest` (without `--force`) inside an already-bootstrapped
|
|
27
33
|
workspace to pick up new roles or rule changes published in later versions — your
|
|
28
34
|
`project.md`, `plan.md`, and tasks are left untouched.
|
|
35
|
+
|
|
36
|
+
## Releasing (CI/CD)
|
|
37
|
+
|
|
38
|
+
- `.github/workflows/ci.yml` — runs on every push/PR to `main`: validates
|
|
39
|
+
`package.json`, dry-run `npm pack`, and a smoke test of `bin/cli.js` against a
|
|
40
|
+
scratch directory.
|
|
41
|
+
- `.github/workflows/release.yml` — on a pushed tag matching `v*.*.*`, calls the
|
|
42
|
+
reusable workflow in [vsem-org/.github](https://github.com/vsem-org/.github)
|
|
43
|
+
to run `npm publish` via **npm Trusted Publishing (OIDC)** — no long-lived
|
|
44
|
+
`NPM_TOKEN` secret involved. Configured once on npmjs.com (package Settings ->
|
|
45
|
+
Trusted Publisher -> GitHub Actions):
|
|
46
|
+
- Organization or user: `vsem-org`
|
|
47
|
+
- Repository: `-core--ai`
|
|
48
|
+
- Workflow filename: `release.yml` (the *calling* workflow, not the reusable
|
|
49
|
+
one in `vsem-org/.github` — npm validates the caller)
|
|
50
|
+
- Allowed actions: direct `npm publish` enabled
|
|
51
|
+
|
|
52
|
+
To ship a new version:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm version patch # or minor / major - bumps package.json, commits, tags
|
|
56
|
+
git push --follow-tags
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
CI publishes automatically once the tag lands.
|
package/bin/cli.js
CHANGED
|
@@ -17,24 +17,107 @@
|
|
|
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>] [--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';
|
|
27
29
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
28
30
|
const TEMPLATES_DIR = path.join(PACKAGE_DIR, 'templates');
|
|
29
31
|
const SYSTEM_DIR = path.join(TEMPLATES_DIR, 'system');
|
|
30
32
|
const SCAFFOLD_DIR = path.join(TEMPLATES_DIR, 'scaffold');
|
|
31
33
|
|
|
34
|
+
// --- terminal styling -------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
37
|
+
|
|
38
|
+
function style(codes, text) {
|
|
39
|
+
if (!supportsColor) return text;
|
|
40
|
+
return `\x1b[${codes}m${text}\x1b[0m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cyan = (s) => style('36', s);
|
|
44
|
+
const cyanBold = (s) => style('1;36', s);
|
|
45
|
+
const dim = (s) => style('2', s);
|
|
46
|
+
const green = (s) => style('32', s);
|
|
47
|
+
const yellow = (s) => style('33', s);
|
|
48
|
+
const button = (s) => style('1;30;46', s); // bold black text on cyan background
|
|
49
|
+
|
|
50
|
+
// 5-row pixel font, just enough to spell VSEM.
|
|
51
|
+
const GLYPHS = {
|
|
52
|
+
V: ['█ █', '█ █', '█ █', ' █ █ ', ' █ '],
|
|
53
|
+
S: [' ████', '█ ', ' ███ ', ' █', '████ '],
|
|
54
|
+
E: ['█████', '█ ', '████ ', '█ ', '█████'],
|
|
55
|
+
M: ['█ █', '██ ██', '█ █ █', '█ █', '█ █'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function renderLogo(word) {
|
|
59
|
+
const rows = [];
|
|
60
|
+
for (let row = 0; row < 5; row++) {
|
|
61
|
+
rows.push(word.split('').map((ch) => GLYPHS[ch][row]).join(' '));
|
|
62
|
+
}
|
|
63
|
+
return rows.map(cyanBold).join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printBanner() {
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(renderLogo('VSEM'));
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(cyan('vsem-framework') + dim(` · CLI v${PACKAGE_JSON.version} · шаблон v${FRAMEWORK_VERSION}`));
|
|
71
|
+
console.log(dim('Разворачивает рабочее пространство для AI-агента: .ai/ (роли, правила,'));
|
|
72
|
+
console.log(dim('задачи, решения, инциденты), CLAUDE.md и личную git-ветку — в этой папке.'));
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderButton(label) {
|
|
77
|
+
const padded = ` ${label} `;
|
|
78
|
+
const top = '┌' + '─'.repeat(padded.length) + '┐';
|
|
79
|
+
const mid = '│' + button(padded) + '│';
|
|
80
|
+
const bottom = '└' + '─'.repeat(padded.length) + '┘';
|
|
81
|
+
return `${top}\n${mid}\n${bottom}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Shows an install "button" and waits for confirmation. Auto-confirms when
|
|
85
|
+
// stdin isn't a TTY (CI, pipes) or when --yes was passed, so scripted/non-
|
|
86
|
+
// interactive runs never hang.
|
|
87
|
+
function confirmInstall(info, opts) {
|
|
88
|
+
console.log(` ${dim('Папка:')} ${info.target}`);
|
|
89
|
+
console.log(` ${dim('Ветка:')} ${info.branch}`);
|
|
90
|
+
console.log(` ${dim('Режим:')} ${info.isRerun ? 'обновление существующего пространства' : 'новая установка'}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
94
|
+
console.log(renderButton('Установить') + '\n');
|
|
95
|
+
console.log(dim(opts.yes ? 'Подтверждение пропущено (--yes).' : 'Неинтерактивный режим — продолжаю автоматически.'));
|
|
96
|
+
console.log('');
|
|
97
|
+
return Promise.resolve(true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(renderButton('Установить') + '\n');
|
|
101
|
+
console.log(dim('Enter — установить, n — отменить'));
|
|
102
|
+
|
|
103
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
rl.question('> ', (answer) => {
|
|
106
|
+
rl.close();
|
|
107
|
+
console.log('');
|
|
108
|
+
resolve(!/^n(o)?$/i.test(answer.trim()));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
32
113
|
function parseArgs(argv) {
|
|
33
|
-
const opts = { target: null, branch: null, force: false };
|
|
114
|
+
const opts = { target: null, branch: null, force: false, yes: false };
|
|
34
115
|
for (let i = 0; i < argv.length; i++) {
|
|
35
116
|
const a = argv[i];
|
|
36
117
|
if (a === '--force') {
|
|
37
118
|
opts.force = true;
|
|
119
|
+
} else if (a === '--yes' || a === '-y') {
|
|
120
|
+
opts.yes = true;
|
|
38
121
|
} else if (a === '--branch') {
|
|
39
122
|
opts.branch = argv[++i] || null;
|
|
40
123
|
} else if (a.startsWith('--branch=')) {
|
|
@@ -100,7 +183,7 @@ function tryGit(args, cwd) {
|
|
|
100
183
|
}
|
|
101
184
|
}
|
|
102
185
|
|
|
103
|
-
function main() {
|
|
186
|
+
async function main() {
|
|
104
187
|
if (!fs.existsSync(SYSTEM_DIR)) {
|
|
105
188
|
console.error(`Templates not found at ${SYSTEM_DIR}. This script must ship together with templates/.`);
|
|
106
189
|
process.exit(1);
|
|
@@ -108,8 +191,6 @@ function main() {
|
|
|
108
191
|
|
|
109
192
|
const args = parseArgs(process.argv.slice(2));
|
|
110
193
|
const target = path.resolve(args.target || process.cwd());
|
|
111
|
-
|
|
112
|
-
fs.mkdirSync(target, { recursive: true });
|
|
113
194
|
const aiDir = path.join(target, '.ai');
|
|
114
195
|
const workspaceName = path.basename(target);
|
|
115
196
|
const isRerun = fs.existsSync(aiDir);
|
|
@@ -123,6 +204,15 @@ function main() {
|
|
|
123
204
|
if (!branch) branch = 'dev';
|
|
124
205
|
}
|
|
125
206
|
|
|
207
|
+
printBanner();
|
|
208
|
+
const proceed = await confirmInstall({ target, branch, isRerun }, args);
|
|
209
|
+
if (!proceed) {
|
|
210
|
+
console.log(yellow('[cancelled] Ничего не изменено.'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fs.mkdirSync(target, { recursive: true });
|
|
215
|
+
|
|
126
216
|
const vars = { WORKSPACE_NAME: workspaceName, FRAMEWORK_VERSION: FRAMEWORK_VERSION };
|
|
127
217
|
|
|
128
218
|
copyTemplateTree(SYSTEM_DIR, target, vars, true);
|
|
@@ -133,20 +223,20 @@ function main() {
|
|
|
133
223
|
}
|
|
134
224
|
|
|
135
225
|
if (isRerun) {
|
|
136
|
-
console.log(`[ok] Workspace synced at ${target} (system files updated; scaffold files kept unless --force)`);
|
|
226
|
+
console.log(green(`[ok] Workspace synced at ${target} (system files updated; scaffold files kept unless --force)`));
|
|
137
227
|
} else {
|
|
138
|
-
console.log(`[ok] Workspace structure created at ${target}`);
|
|
228
|
+
console.log(green(`[ok] Workspace structure created at ${target}`));
|
|
139
229
|
}
|
|
140
230
|
|
|
141
231
|
const gitCheck = tryGit(['--version'], target);
|
|
142
232
|
if (!gitCheck.ok) {
|
|
143
|
-
console.log('[warn] git not found in PATH - skipping git init.');
|
|
233
|
+
console.log(yellow('[warn] git not found in PATH - skipping git init.'));
|
|
144
234
|
return;
|
|
145
235
|
}
|
|
146
236
|
|
|
147
237
|
if (!fs.existsSync(path.join(target, '.git'))) {
|
|
148
238
|
tryGit(['init', '-q'], target);
|
|
149
|
-
console.log('[ok] git init');
|
|
239
|
+
console.log(green('[ok] git init'));
|
|
150
240
|
}
|
|
151
241
|
|
|
152
242
|
const currentBranchResult = tryGit(['branch', '--show-current'], target);
|
|
@@ -159,7 +249,7 @@ function main() {
|
|
|
159
249
|
} else {
|
|
160
250
|
tryGit(['checkout', '-q', '-b', branch], target);
|
|
161
251
|
}
|
|
162
|
-
console.log(`[ok] branch '${branch}' is active`);
|
|
252
|
+
console.log(green(`[ok] branch '${branch}' is active`));
|
|
163
253
|
}
|
|
164
254
|
|
|
165
255
|
tryGit(['add', '-A'], target);
|
|
@@ -167,11 +257,18 @@ function main() {
|
|
|
167
257
|
|
|
168
258
|
if (!noChanges) {
|
|
169
259
|
const msg = isRerun ? 'chore: sync vsem-framework system files' : 'chore: bootstrap vsem-framework workspace';
|
|
170
|
-
tryGit(['commit', '-q', '-m', msg], target);
|
|
171
|
-
|
|
260
|
+
const commitResult = tryGit(['commit', '-q', '-m', msg], target);
|
|
261
|
+
if (commitResult.ok) {
|
|
262
|
+
console.log(green(`[ok] commit created: ${msg}`));
|
|
263
|
+
} else {
|
|
264
|
+
console.log(yellow('[warn] git commit failed (is git config user.name/user.email set?) - changes are staged but not committed.'));
|
|
265
|
+
}
|
|
172
266
|
} else {
|
|
173
|
-
console.log('[skip] nothing to commit (already up to date)');
|
|
267
|
+
console.log(dim('[skip] nothing to commit (already up to date)'));
|
|
174
268
|
}
|
|
175
269
|
}
|
|
176
270
|
|
|
177
|
-
main()
|
|
271
|
+
main().catch((err) => {
|
|
272
|
+
console.error(err);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|