@torus-engineering/tas-kit 1.8.0 → 1.10.0

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/lib/install.js CHANGED
@@ -31,16 +31,21 @@ async function removeDeletedFiles(target, deletedFiles) {
31
31
  return { removed, skipped };
32
32
  }
33
33
 
34
- async function confirm(question) {
34
+ async function ask(question, defaultValue = '') {
35
35
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
36
36
  return new Promise((resolve) => {
37
- rl.question(`${question} [y/N] `, (answer) => {
37
+ rl.question(question, (answer) => {
38
38
  rl.close();
39
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
39
+ resolve((answer || '').trim() || defaultValue);
40
40
  });
41
41
  });
42
42
  }
43
43
 
44
+ async function confirm(question) {
45
+ const answer = await ask(`${question} [y/N] `);
46
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
47
+ }
48
+
44
49
  async function exists(p) {
45
50
  return fs.access(p).then(() => true).catch(() => false);
46
51
  }
@@ -49,10 +54,131 @@ async function copyDir(src, dest) {
49
54
  await fs.cp(src, dest, { recursive: true });
50
55
  }
51
56
 
52
- export async function update({ directory, yes }) {
57
+ // ─── Security hook wiring ────────────────────────────────────────────────────
58
+
59
+ async function chooseSecurityHookMode({ target, yes, forced }) {
60
+ if (forced) return forced;
61
+ if (yes) return 'native';
62
+
63
+ const hasPackageJson = await exists(path.join(target, 'package.json'));
64
+ const hint = hasPackageJson
65
+ ? ' Detected package.json — husky mode is available.'
66
+ : ' No package.json — husky mode will add one or fall back to native.';
67
+
68
+ console.log('\n Pre-commit security hook wiring:');
69
+ console.log(hint);
70
+ console.log(' [1] husky — shared via git, requires Node project');
71
+ console.log(' [2] native — plain .git/hooks/pre-commit, any stack');
72
+ console.log(' [3] skip — wire it later with "tas-kit install --security-hook=..."');
73
+ const answer = await ask(' Choose [1/2/3] (default: 2): ', '2');
74
+ if (answer === '1') return 'husky';
75
+ if (answer === '3') return 'none';
76
+ return 'native';
77
+ }
78
+
79
+ async function installSecurityHookNative({ target }) {
80
+ const gitDir = path.join(target, '.git');
81
+ if (!(await exists(gitDir))) {
82
+ console.warn(' [skip] Security hook (native): .git/ not found — run `git init` first, then re-run installer with --security-hook=native');
83
+ return false;
84
+ }
85
+
86
+ const hooksDir = path.join(gitDir, 'hooks');
87
+ await fs.mkdir(hooksDir, { recursive: true });
88
+
89
+ const src = path.join(PACKAGE_DIR, '.tas', 'hooks', 'pre-commit');
90
+ const dest = path.join(hooksDir, 'pre-commit');
91
+
92
+ if (await exists(dest)) {
93
+ const existing = await fs.readFile(dest, 'utf8').catch(() => '');
94
+ if (!existing.includes('TAS Kit')) {
95
+ const backup = dest + '.backup';
96
+ await fs.copyFile(dest, backup);
97
+ console.log(' [ok] .git/hooks/pre-commit.backup (preserved existing hook)');
98
+ }
99
+ }
100
+
101
+ await fs.copyFile(src, dest);
102
+ if (process.platform !== 'win32') {
103
+ await fs.chmod(dest, 0o755);
104
+ }
105
+ console.log(' [ok] .git/hooks/pre-commit (security scan wired — native)');
106
+ return true;
107
+ }
108
+
109
+ async function installSecurityHookHusky({ target }) {
110
+ const pkgPath = path.join(target, 'package.json');
111
+ const hasPackageJson = await exists(pkgPath);
112
+
113
+ if (!hasPackageJson) {
114
+ console.warn(' [warn] Security hook (husky): no package.json — falling back to native mode');
115
+ return await installSecurityHookNative({ target });
116
+ }
117
+
118
+ // Read + update package.json (add prepare script + husky devDep)
119
+ let pkg;
120
+ try {
121
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
122
+ } catch (err) {
123
+ console.warn(` [warn] Security hook (husky): package.json unreadable (${err.message}) — falling back to native`);
124
+ return await installSecurityHookNative({ target });
125
+ }
126
+
127
+ pkg.scripts = pkg.scripts || {};
128
+ if (!pkg.scripts.prepare) {
129
+ pkg.scripts.prepare = 'husky';
130
+ } else if (!/husky/.test(pkg.scripts.prepare)) {
131
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
132
+ }
133
+
134
+ pkg.devDependencies = pkg.devDependencies || {};
135
+ if (!pkg.devDependencies.husky) {
136
+ pkg.devDependencies.husky = '^9.1.0';
137
+ }
138
+
139
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
140
+ console.log(' [ok] package.json (husky devDep + prepare script)');
141
+
142
+ // Write .husky/pre-commit
143
+ const huskyDir = path.join(target, '.husky');
144
+ await fs.mkdir(huskyDir, { recursive: true });
145
+
146
+ const huskyHook = path.join(huskyDir, 'pre-commit');
147
+ const content = [
148
+ '# TAS Kit — husky pre-commit (delegates to .tas/hooks/pre-commit)',
149
+ '. "$(dirname -- "$0")/../.tas/hooks/pre-commit"',
150
+ '',
151
+ ].join('\n');
152
+ await fs.writeFile(huskyHook, content);
153
+ if (process.platform !== 'win32') {
154
+ await fs.chmod(huskyHook, 0o755);
155
+ }
156
+ console.log(' [ok] .husky/pre-commit (delegates to .tas/hooks/pre-commit)');
157
+ console.log(' [--] Run `npm install` in the project to activate husky');
158
+ return true;
159
+ }
160
+
161
+ async function installSecurityHook({ target, mode }) {
162
+ if (mode === 'none' || !mode) {
163
+ console.log(' [skip] Security hook (you can enable later: tas-kit install --security-hook=native|husky)');
164
+ return;
165
+ }
166
+ if (mode === 'husky') {
167
+ await installSecurityHookHusky({ target });
168
+ return;
169
+ }
170
+ if (mode === 'native') {
171
+ await installSecurityHookNative({ target });
172
+ return;
173
+ }
174
+ console.warn(` [warn] Unknown security hook mode: "${mode}" — skipping`);
175
+ }
176
+
177
+ // ─── Update ──────────────────────────────────────────────────────────────────
178
+
179
+ export async function update({ directory, yes, securityHook }) {
53
180
  const target = path.resolve(directory);
54
181
 
55
- // Must already have .claude/ or .tas/ — otherwise suggest install
56
182
  const claudeExists = await exists(path.join(target, '.claude'));
57
183
  const tasExists = await exists(path.join(target, '.tas'));
58
184
  if (!claudeExists && !tasExists) {
@@ -74,21 +200,12 @@ export async function update({ directory, yes }) {
74
200
  console.log();
75
201
  }
76
202
 
77
- // Overwrite .claude/
78
- await copyDir(
79
- path.join(PACKAGE_DIR, '.claude'),
80
- path.join(target, '.claude')
81
- );
203
+ await copyDir(path.join(PACKAGE_DIR, '.claude'), path.join(target, '.claude'));
82
204
  console.log(' [ok] .claude/ (updated)');
83
205
 
84
- // Overwrite .tas/
85
- await copyDir(
86
- path.join(PACKAGE_DIR, '.tas'),
87
- path.join(target, '.tas')
88
- );
206
+ await copyDir(path.join(PACKAGE_DIR, '.tas'), path.join(target, '.tas'));
89
207
  console.log(' [ok] .tas/ (updated)');
90
208
 
91
- // Remove files deleted from the kit in previous versions
92
209
  const deletedFiles = await getDeletedFiles();
93
210
  if (deletedFiles.length > 0) {
94
211
  const { removed } = await removeDeletedFiles(target, deletedFiles);
@@ -99,12 +216,20 @@ export async function update({ directory, yes }) {
99
216
  }
100
217
  }
101
218
 
102
- // Set executable bit on tas-ado.py (Unix/macOS)
103
219
  if (process.platform !== 'win32') {
104
220
  const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
105
221
  if (await exists(adoPy)) {
106
222
  await fs.chmod(adoPy, 0o755);
107
223
  }
224
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
225
+ if (await exists(preCommit)) {
226
+ await fs.chmod(preCommit, 0o755);
227
+ }
228
+ }
229
+
230
+ // Re-wire hook only if user explicitly asked via flag
231
+ if (securityHook) {
232
+ await installSecurityHook({ target, mode: securityHook });
108
233
  }
109
234
 
110
235
  console.log(` [--] CLAUDE.md, tas.yaml, .env.example — not touched`);
@@ -117,15 +242,15 @@ and manually merge changes into your CLAUDE.md and tas.yaml if needed.
117
242
  `);
118
243
  }
119
244
 
120
- export async function install({ directory, yes }) {
245
+ // ─── Install ─────────────────────────────────────────────────────────────────
246
+
247
+ export async function install({ directory, yes, securityHook }) {
121
248
  const target = path.resolve(directory);
122
249
 
123
- // Ensure target directory exists
124
250
  await fs.mkdir(target, { recursive: true });
125
251
 
126
252
  console.log(`\nInstalling TAS Kit into: ${target}\n`);
127
253
 
128
- // Warn if .claude/ or .tas/ already exist
129
254
  const claudeExists = await exists(path.join(target, '.claude'));
130
255
  const tasExists = await exists(path.join(target, '.tas'));
131
256
 
@@ -141,64 +266,51 @@ export async function install({ directory, yes }) {
141
266
  console.log();
142
267
  }
143
268
 
144
- // Copy .claude/
145
- await copyDir(
146
- path.join(PACKAGE_DIR, '.claude'),
147
- path.join(target, '.claude')
148
- );
269
+ await copyDir(path.join(PACKAGE_DIR, '.claude'), path.join(target, '.claude'));
149
270
  console.log(' [ok] .claude/');
150
271
 
151
- // Copy .tas/
152
- await copyDir(
153
- path.join(PACKAGE_DIR, '.tas'),
154
- path.join(target, '.tas')
155
- );
272
+ await copyDir(path.join(PACKAGE_DIR, '.tas'), path.join(target, '.tas'));
156
273
  console.log(' [ok] .tas/');
157
274
 
158
- // Copy CLAUDE-Example.md as CLAUDE.md (only if absent)
159
275
  const claudeMdTarget = path.join(target, 'CLAUDE.md');
160
276
  if (!(await exists(claudeMdTarget))) {
161
- await fs.copyFile(
162
- path.join(PACKAGE_DIR, 'CLAUDE-Example.md'),
163
- claudeMdTarget
164
- );
277
+ await fs.copyFile(path.join(PACKAGE_DIR, 'CLAUDE-Example.md'), claudeMdTarget);
165
278
  console.log(' [ok] CLAUDE.md (from CLAUDE-Example.md)');
166
279
  } else {
167
280
  console.log(' [--] CLAUDE.md already exists, skipped');
168
281
  }
169
282
 
170
- // Copy .env.example (only if absent)
171
283
  const envExampleTarget = path.join(target, '.env.example');
172
284
  if (!(await exists(envExampleTarget))) {
173
- await fs.copyFile(
174
- path.join(PACKAGE_DIR, '.env.example'),
175
- envExampleTarget
176
- );
285
+ await fs.copyFile(path.join(PACKAGE_DIR, '.env.example'), envExampleTarget);
177
286
  console.log(' [ok] .env.example');
178
287
  } else {
179
288
  console.log(' [--] .env.example already exists, skipped');
180
289
  }
181
290
 
182
- // Copy tas-example.yaml as tas.yaml (only if absent)
183
291
  const tasYamlTarget = path.join(target, 'tas.yaml');
184
292
  if (!(await exists(tasYamlTarget))) {
185
- await fs.copyFile(
186
- path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'),
187
- tasYamlTarget
188
- );
293
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'), tasYamlTarget);
189
294
  console.log(' [ok] tas.yaml (from .tas/tas-example.yaml)');
190
295
  } else {
191
296
  console.log(' [--] tas.yaml already exists, skipped');
192
297
  }
193
298
 
194
- // Set executable bit on tas-ado.py (Unix/macOS)
195
299
  if (process.platform !== 'win32') {
196
300
  const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
197
301
  if (await exists(adoPy)) {
198
302
  await fs.chmod(adoPy, 0o755);
199
303
  }
304
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
305
+ if (await exists(preCommit)) {
306
+ await fs.chmod(preCommit, 0o755);
307
+ }
200
308
  }
201
309
 
310
+ // ─── Wire security pre-commit hook ─────────────────────────────────────────
311
+ const mode = await chooseSecurityHookMode({ target, yes, forced: securityHook });
312
+ await installSecurityHook({ target, mode });
313
+
202
314
  console.log(`
203
315
  TAS Kit installed successfully!
204
316
 
@@ -208,6 +320,8 @@ Next steps:
208
320
  3. Create .env — add AZURE_DEVOPS_PAT (see .env.example)
209
321
  4. Open Claude Code — run /tas-init to initialize your project
210
322
 
211
- Docs: .tas/README.md
323
+ Docs:
324
+ .tas/README.md — kit overview
325
+ .tas/hooks/README.md — pre-commit security hook details
212
326
  `);
213
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torus-engineering/tas-kit",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Torus Agentic SDLC Kit — Collection of commands, skills, rules, hooks, agents and workflows for modern AI-First SDLC",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,95 +0,0 @@
1
- # /tas-api-test $ARGUMENTS
2
-
3
- Vai trò: SE - Software Engineer
4
- Generate .NET xUnit automation test project cho REST API từ spec (OpenAPI 3.0, Markdown, YAML).
5
-
6
- ## Always / Ask / Never
7
-
8
- | | Hành động |
9
- |---|---|
10
- | **Always** | Đọc toàn bộ spec trước khi generate bất kỳ test nào |
11
- | **Always** | Tổ chức tests theo API version — mỗi version một folder riêng |
12
- | **Always** | Append-only: không sửa files trong version folder cũ đã tồn tại |
13
- | **Always** | URL và credentials ra `appsettings.json` — không hardcode trong code |
14
- | **Always** | XML doc comment trên mỗi test method |
15
- | **Ask** | Khi spec không rõ expected response schema |
16
- | **Never** | Sửa hoặc xóa test files của version cũ hơn |
17
- | **Never** | Hardcode base URL, API key, credentials trong test code |
18
- | **Never** | Bỏ qua error path — mỗi endpoint cần ≥1 happy path + ≥1 error path |
19
-
20
- ## Hành động
21
-
22
- ### Bước 1 — Locate Inputs
23
-
24
- `$ARGUMENTS` là path đến spec file hoặc Story ID. Nếu không có → hỏi user.
25
-
26
- Nếu là Story ID: glob tìm `docs/epics/**/Story-{ID}*.md`, đọc Story để tìm link spec trong `## Technical Plan` hoặc `## Acceptance Criteria`.
27
-
28
- ### Bước 2 — Parse API Spec
29
-
30
- Đọc `.claude/rules/csharp/api-testing.md` để nắm convention trước khi generate.
31
-
32
- Từ spec, thu thập cho mỗi endpoint:
33
- - HTTP method + path, path/query params, request body schema
34
- - Response schemas theo từng status code
35
- - Auth requirement, description/summary
36
-
37
- Nếu có Story: đọc thêm `## Acceptance Criteria` để bổ sung business rule test cases.
38
-
39
- ### Bước 3 — Detect Existing Test Project
40
-
41
- Glob tìm `**/ApiTests/*.csproj`, `**/Api.Tests/*.csproj`, `**/IntegrationTests/*.csproj`.
42
-
43
- - **Tìm thấy**: Đọc framework đang dùng (xUnit/NUnit/MSTest), glob `v*/` folders để biết versions đã có.
44
- - **Không tìm thấy**: Tạo mới tại `tests/ApiTests/` với xUnit + FluentAssertions (per `csharp/testing.md`).
45
-
46
- ### Bước 4 — Detect API Version
47
-
48
- Ưu tiên theo thứ tự: `info.version` trong OpenAPI → prefix trong URL path → hỏi user.
49
- Version folder: lowercase, không có dot — `v1`, `v2` (không phải `v1.0`).
50
-
51
- ### Bước 5 — Generate Infrastructure (chỉ khi tạo project mới)
52
-
53
- Đọc `.claude/rules/csharp/api-testing.md` section **Project Structure** và **Config Pattern** để generate:
54
- - `ApiTests.csproj` với packages chuẩn
55
- - `appsettings.json` (base) + `appsettings.Test.json` + `appsettings.Staging.json`
56
- - `Shared/TestBase.cs` — load config, HttpClient, helper methods
57
- - `Shared/ApiTestSettings.cs` — strongly typed settings
58
- - `.gitignore` cho `appsettings.*.local.json`
59
-
60
- ### Bước 6 — Generate Test Classes
61
-
62
- Nhóm endpoints theo resource/tag. Mỗi nhóm → `tests/ApiTests/{version}/{Resource}ApiTests.cs`.
63
-
64
- Với mỗi endpoint, generate tối thiểu:
65
- 1. Happy path (2xx)
66
- 2. Unauthorized (401) — nếu endpoint yêu cầu auth
67
- 3. Not found (404) — nếu có path param
68
- 4. Validation error (400/422) — nếu có required fields
69
-
70
- Từ Story ACs: thêm test methods tương ứng, comment rõ `// AC: {AC text}`.
71
-
72
- Đầu mỗi file: block comment ghi `Spec`, `Generated date`, `Story ID`, và nhắc **APPEND-ONLY rule**.
73
-
74
- ### Bước 7 — Append-Only Guard
75
-
76
- Trước khi ghi file: kiểm tra file đã tồn tại chưa.
77
- - File chưa có → tạo mới bình thường.
78
- - File đã tồn tại trong version folder → **DỪNG**, thông báo tên file cho user tự edit thủ công.
79
-
80
- ### Bước 8 — Post-Generate Review
81
-
82
- Launch `csharp-reviewer` agent:
83
- > Review `tests/ApiTests/{version}/`. Đọc `.claude/rules/csharp/testing.md` + `.claude/rules/csharp/api-testing.md`.
84
- > Tập trung: async/await, HttpClient disposal, assertion completeness, XML doc coverage.
85
- > Format: Critical / High / Medium / Low với file:line.
86
-
87
- Gate: Critical/High → fix ngay. Medium/Low → list gợi ý.
88
-
89
- ### Bước 9 — Summary
90
-
91
- Hiển thị bảng coverage (endpoint × test type: ✓/—) và next steps để chạy tests.
92
-
93
- ## Bước cuối — Token Log
94
-
95
- Invoke skill `token-logger`: ghi AI Usage Log vào spec file hoặc Story đang làm việc.