@torus-engineering/tas-kit 1.14.0 → 2.1.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.
Files changed (99) hide show
  1. package/.tas/_platform/claude-code/settings.json +58 -46
  2. package/.tas/_platform/hooks/code-quality.js +127 -127
  3. package/.tas/_platform/hooks/session-end.js +111 -111
  4. package/.tas/agents/architect.md +53 -53
  5. package/.tas/agents/aws-reviewer.md +71 -71
  6. package/.tas/agents/build-resolver.md +89 -59
  7. package/.tas/agents/code-explorer.md +63 -63
  8. package/.tas/agents/csharp-reviewer.md +62 -62
  9. package/.tas/agents/database-reviewer.md +73 -73
  10. package/.tas/agents/doc-updater.md +68 -66
  11. package/.tas/agents/python-reviewer.md +67 -67
  12. package/.tas/agents/security-reviewer.md +79 -79
  13. package/.tas/agents/software-engineer.md +53 -0
  14. package/.tas/agents/typescript-reviewer.md +65 -65
  15. package/.tas/commands/ado-create.md +33 -28
  16. package/.tas/commands/ado-delete.md +26 -22
  17. package/.tas/commands/ado-get.md +24 -20
  18. package/.tas/commands/ado-status.md +22 -18
  19. package/.tas/commands/ado-update.md +31 -27
  20. package/.tas/commands/tas-adr.md +37 -33
  21. package/.tas/commands/tas-apitest-plan.md +177 -173
  22. package/.tas/commands/tas-apitest.md +147 -143
  23. package/.tas/commands/tas-brainstorm.md +23 -19
  24. package/.tas/commands/tas-brd.md +50 -0
  25. package/.tas/commands/tas-bug.md +127 -113
  26. package/.tas/commands/tas-checklist.md +180 -0
  27. package/.tas/commands/tas-debug.md +103 -0
  28. package/.tas/commands/tas-design.md +41 -37
  29. package/.tas/commands/tas-dev.md +225 -125
  30. package/.tas/commands/tas-e2e-mobile.md +146 -155
  31. package/.tas/commands/tas-e2e-web.md +150 -163
  32. package/.tas/commands/tas-e2e.md +289 -102
  33. package/.tas/commands/tas-feature.md +181 -47
  34. package/.tas/commands/tas-fix.md +72 -51
  35. package/.tas/commands/tas-functest-mobile.md +138 -144
  36. package/.tas/commands/tas-functest-web.md +176 -192
  37. package/.tas/commands/tas-functest.md +225 -76
  38. package/.tas/commands/tas-init.md +22 -17
  39. package/.tas/commands/tas-master-plan.md +300 -0
  40. package/.tas/commands/tas-orchestrate.md +159 -0
  41. package/.tas/commands/tas-plan.md +152 -117
  42. package/.tas/commands/tas-prd.md +57 -37
  43. package/.tas/commands/tas-review-pr.md +174 -0
  44. package/.tas/commands/tas-review.md +115 -113
  45. package/.tas/commands/tas-sad.md +47 -43
  46. package/.tas/commands/tas-security.md +91 -87
  47. package/.tas/commands/tas-spec.md +54 -50
  48. package/.tas/commands/tas-status.md +25 -16
  49. package/.tas/project-status-example.yaml +3 -1
  50. package/.tas/rules/ado-integration.md +67 -65
  51. package/.tas/rules/common/api-design.md +517 -517
  52. package/.tas/rules/common/build-debug-loop.md +233 -0
  53. package/.tas/rules/common/code-review.md +4 -0
  54. package/.tas/rules/common/feature-done.md +42 -0
  55. package/.tas/rules/common/post-implementation-review.md +4 -0
  56. package/.tas/rules/common/project-status.md +33 -16
  57. package/.tas/rules/common/sad-impact.md +81 -0
  58. package/.tas/rules/common/tdd.md +104 -89
  59. package/.tas/rules/csharp/api-testing.md +2 -2
  60. package/.tas/rules/csharp/torus-core-framework.md +128 -0
  61. package/.tas/tas-example.yaml +9 -32
  62. package/.tas/templates/AGENTS.md +13 -0
  63. package/.tas/templates/API-Test-Spec.md +5 -4
  64. package/.tas/templates/BRD.md +133 -0
  65. package/.tas/templates/Bug.md +15 -0
  66. package/.tas/templates/E2E-Execution-Report.md +8 -8
  67. package/.tas/templates/E2E-Mobile-Spec.md +6 -8
  68. package/.tas/templates/E2E-Report.md +2 -2
  69. package/.tas/templates/E2E-Scenario.md +22 -22
  70. package/.tas/templates/E2E-Test-Spec.md +274 -0
  71. package/.tas/templates/E2E-Web-Spec.md +4 -4
  72. package/.tas/templates/Feature-Technical-Part.md +69 -0
  73. package/.tas/templates/Feature-Technical-Stack.md +74 -0
  74. package/.tas/templates/Feature-Technical.md +329 -0
  75. package/.tas/templates/Feature.md +50 -26
  76. package/.tas/templates/Func-Test-Script.md +29 -56
  77. package/.tas/templates/Func-Test-Spec.md +144 -142
  78. package/.tas/templates/PRD.md +173 -142
  79. package/.tas/templates/TestChecklist.md +96 -0
  80. package/.tas/templates/torus-dotnet-bootstrap.md +223 -0
  81. package/.tas/tools/tas-ado-readme.md +24 -27
  82. package/.tas/tools/tas-ado.py +328 -25
  83. package/.tas/tools/tas-github.py +339 -0
  84. package/README.md +131 -54
  85. package/bin/cli.js +90 -90
  86. package/lib/adapters/antigravity.js +131 -131
  87. package/lib/adapters/claude-code.js +71 -35
  88. package/lib/adapters/codex.js +157 -157
  89. package/lib/adapters/cursor.js +80 -80
  90. package/lib/adapters/index.js +20 -20
  91. package/lib/adapters/utils.js +81 -81
  92. package/lib/deleted-files.json +7 -0
  93. package/lib/install.js +546 -546
  94. package/package.json +1 -1
  95. package/.tas/commands/tas-epic.md +0 -35
  96. package/.tas/commands/tas-story.md +0 -91
  97. package/.tas/rules/common/story-done.md +0 -30
  98. package/.tas/templates/Epic.md +0 -46
  99. package/.tas/templates/Story.md +0 -90
package/lib/install.js CHANGED
@@ -1,546 +1,546 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import readline from 'node:readline';
4
- import { fileURLToPath } from 'node:url';
5
- import { createRequire } from 'node:module';
6
- import { PLATFORMS, getPlatform } from './adapters/index.js';
7
-
8
- const PACKAGE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
9
- const TAS_SRC = path.join(PACKAGE_DIR, '.tas');
10
- const PLATFORMS_FILE = '.tas/platforms.json';
11
- const require = createRequire(import.meta.url);
12
-
13
- // ─── Logo ────────────────────────────────────────────────────────────────────
14
-
15
- function printLogo() {
16
- const c = '\x1b[36m';
17
- const d = '\x1b[90m';
18
- const r = '\x1b[0m';
19
- const art = [
20
- ` ████████╗ █████╗ ███████╗ ██╗ ██╗ ██╗ ████████╗`,
21
- ` ╚══██╔══╝ ██╔══██╗██╔════╝ ██║ ██╔╝ ██║ ╚══██╔══╝`,
22
- ` ██║ ███████║███████╗ █████╔╝ ██║ ██║ `,
23
- ` ██║ ██╔══██║╚════██║ ██╔═██╗ ██║ ██║ `,
24
- ` ██║ ██║ ██║███████║ ██║ ██╗ ██║ ██║ `,
25
- ` ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ `,
26
- ].map(l => c + l + r).join('\n');
27
- process.stdout.write('\n' + art + '\n' + d + ' Turbo Agentic SDLC Kit' + r + '\n\n');
28
- }
29
-
30
- // ─── Helpers ─────────────────────────────────────────────────────────────────
31
-
32
- async function getDeletedFiles() {
33
- try {
34
- const manifest = require('./deleted-files.json');
35
- return Object.values(manifest).flat();
36
- } catch {
37
- return [];
38
- }
39
- }
40
-
41
- async function removeDeletedFiles(target, deletedFiles) {
42
- const removed = [];
43
- const skipped = [];
44
- for (const relPath of deletedFiles) {
45
- const fullPath = path.join(target, relPath);
46
- try {
47
- await fs.rm(fullPath, { force: false });
48
- removed.push(relPath);
49
- } catch {
50
- skipped.push(relPath);
51
- }
52
- }
53
- return { removed, skipped };
54
- }
55
-
56
- async function ask(question, defaultValue = '') {
57
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
- return new Promise((resolve) => {
59
- rl.question(question, (answer) => {
60
- rl.close();
61
- resolve((answer || '').trim() || defaultValue);
62
- });
63
- });
64
- }
65
-
66
- async function confirm(question) {
67
- const answer = await ask(`${question} [y/N] `);
68
- return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
69
- }
70
-
71
- async function exists(p) {
72
- return fs.access(p).then(() => true).catch(() => false);
73
- }
74
-
75
- async function copyDir(src, dest) {
76
- await fs.cp(src, dest, { recursive: true });
77
- }
78
-
79
- const TAS_ROOT_EXCLUDE = new Set([
80
- 'tas-example.yaml',
81
- 'project-status-example.yaml',
82
- 'platforms.json',
83
- 'agents',
84
- 'commands',
85
- 'skills',
86
- 'README.md',
87
- ]);
88
-
89
- async function copyTasDir(src, dest) {
90
- await fs.cp(src, dest, {
91
- recursive: true,
92
- filter: (srcPath) => !TAS_ROOT_EXCLUDE.has(path.relative(src, srcPath)),
93
- });
94
- }
95
-
96
- async function chmodExecutables(target) {
97
- const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
98
- if (await exists(adoPy)) await fs.chmod(adoPy, 0o755);
99
-
100
- const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
101
- if (await exists(preCommit)) await fs.chmod(preCommit, 0o755);
102
-
103
- const hooksDir = path.join(target, '.tas', '_platform', 'hooks');
104
- if (await exists(hooksDir)) {
105
- const files = await fs.readdir(hooksDir);
106
- for (const f of files) await fs.chmod(path.join(hooksDir, f), 0o755);
107
- }
108
- }
109
-
110
- // ─── Platform selection ───────────────────────────────────────────────────────
111
-
112
- async function checkboxSelect(title, items, defaultIndexes = [0]) {
113
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
114
- return fallbackTextSelect(title, items, defaultIndexes);
115
- }
116
-
117
- const selected = new Set(defaultIndexes);
118
- let cursor = defaultIndexes[0] ?? 0;
119
- let renderedLines = 0;
120
-
121
- const clearRendered = () => {
122
- if (renderedLines > 0) {
123
- process.stdout.write(`\x1b[${renderedLines}A\x1b[0J`);
124
- }
125
- };
126
-
127
- const render = () => {
128
- clearRendered();
129
- const lines = [];
130
- lines.push(` ${title}`);
131
- lines.push('');
132
- for (let i = 0; i < items.length; i++) {
133
- const mark = selected.has(i) ? '\x1b[32m◉\x1b[0m' : '○';
134
- const arrow = cursor === i ? '\x1b[36m›\x1b[0m' : ' ';
135
- const label = cursor === i ? `\x1b[36m${items[i].label}\x1b[0m` : items[i].label;
136
- const note = items[i].note ? ` \x1b[90m${items[i].note}\x1b[0m` : '';
137
- lines.push(` ${arrow} ${mark} ${label}${note}`);
138
- }
139
- lines.push('');
140
- lines.push('\x1b[90m ↑↓ navigate space select enter confirm\x1b[0m');
141
- renderedLines = lines.length;
142
- process.stdout.write(lines.join('\n') + '\n');
143
- };
144
-
145
- return new Promise((resolve) => {
146
- process.stdout.write('\x1b[?25l');
147
- process.stdin.setRawMode(true);
148
- process.stdin.resume();
149
- process.stdin.setEncoding('utf8');
150
-
151
- render();
152
-
153
- const onKey = (key) => {
154
- if (key === '\x03') {
155
- finish(null);
156
- process.exit(0);
157
- } else if (key === '\x1b[A') {
158
- cursor = (cursor - 1 + items.length) % items.length;
159
- render();
160
- } else if (key === '\x1b[B') {
161
- cursor = (cursor + 1) % items.length;
162
- render();
163
- } else if (key === ' ') {
164
- if (selected.has(cursor)) selected.delete(cursor);
165
- else selected.add(cursor);
166
- render();
167
- } else if (key === '\r' || key === '\n') {
168
- const ids = items.filter((_, i) => selected.has(i)).map(it => it.id);
169
- finish(ids.length > 0 ? ids : [items[cursor].id]);
170
- }
171
- };
172
-
173
- const finish = (result) => {
174
- process.stdin.removeListener('data', onKey);
175
- process.stdin.setRawMode(false);
176
- process.stdin.pause();
177
- process.stdout.write('\x1b[?25h');
178
- clearRendered();
179
- if (result) {
180
- const labels = result.map(id => items.find(it => it.id === id)?.label || id).join(', ');
181
- process.stdout.write(` Platforms: \x1b[36m${labels}\x1b[0m\n\n`);
182
- resolve(result);
183
- }
184
- };
185
-
186
- process.stdin.on('data', onKey);
187
- });
188
- }
189
-
190
- async function fallbackTextSelect(title, items, defaultIndexes) {
191
- console.log(`\n ${title}`);
192
- items.forEach((item, i) => {
193
- const def = defaultIndexes.includes(i) ? ' (default)' : '';
194
- console.log(` [${i + 1}] ${item.label}${def}`);
195
- });
196
- console.log('');
197
- console.log(' Enter numbers comma-separated for multiple platforms.');
198
- const nums = items.map((_, i) => i + 1).join('/');
199
- const answer = await ask(` Choose [${nums}] (default: 1): `, '1');
200
-
201
- const selected = [];
202
- for (const part of answer.split(',')) {
203
- const idx = parseInt(part.trim(), 10) - 1;
204
- if (idx >= 0 && idx < items.length) {
205
- const id = items[idx].id;
206
- if (!selected.includes(id)) selected.push(id);
207
- }
208
- }
209
- return selected.length > 0 ? selected : [items[defaultIndexes[0] ?? 0].id];
210
- }
211
-
212
- async function choosePlatforms({ yes, forced }) {
213
- if (forced && forced.length > 0) return forced;
214
- if (yes) return ['claude-code'];
215
-
216
- return checkboxSelect(
217
- 'Agentic Coding Platform(s) to install:',
218
- PLATFORMS.map(p => ({
219
- id: p.id,
220
- label: p.label,
221
- note: p.id === 'claude-code' ? '(default)' : '',
222
- })),
223
- [0],
224
- );
225
- }
226
-
227
- async function savePlatforms(target, platforms) {
228
- const tasDir = path.join(target, '.tas');
229
- await fs.mkdir(tasDir, { recursive: true });
230
- await fs.writeFile(
231
- path.join(target, PLATFORMS_FILE),
232
- JSON.stringify({ platforms }, null, 2) + '\n'
233
- );
234
- }
235
-
236
- async function loadPlatforms(target) {
237
- try {
238
- const data = JSON.parse(await fs.readFile(path.join(target, PLATFORMS_FILE), 'utf8'));
239
- return Array.isArray(data.platforms) ? data.platforms : ['claude-code'];
240
- } catch {
241
- // fallback: detect from existing directories
242
- const detected = [];
243
- if (await exists(path.join(target, '.claude', 'commands'))) detected.push('claude-code');
244
- if (await exists(path.join(target, '.cursor', 'agents'))) detected.push('cursor');
245
- if (await exists(path.join(target, '.codex', 'skills'))) detected.push('codex');
246
- if (await exists(path.join(target, '.agents', 'skills'))) detected.push('antigravity');
247
- return detected.length > 0 ? detected : ['claude-code'];
248
- }
249
- }
250
-
251
- async function runPlatformAdapters(platforms, { tasDir, target }) {
252
- for (const id of platforms) {
253
- const platform = getPlatform(id);
254
- if (!platform) {
255
- console.warn(` [warn] Unknown platform "${id}" — skipping`);
256
- continue;
257
- }
258
- console.log(`\n Platform: ${platform.label}`);
259
- await platform.install({ tasDir, target });
260
- }
261
- }
262
-
263
- // ─── Security hook wiring ────────────────────────────────────────────────────
264
-
265
- async function chooseSecurityHookMode({ target, yes, forced }) {
266
- if (forced) return forced;
267
- if (yes) return 'native';
268
-
269
- const hasPackageJson = await exists(path.join(target, 'package.json'));
270
- const hint = hasPackageJson
271
- ? ' Detected package.json — husky mode is available.'
272
- : ' No package.json — husky mode will add one or fall back to native.';
273
-
274
- console.log('\n Pre-commit security hook wiring:');
275
- console.log(hint);
276
- console.log(' [1] husky — shared via git, requires Node project');
277
- console.log(' [2] native — plain .git/hooks/pre-commit, any stack');
278
- console.log(' [3] skip — wire it later with "tas-kit install --security-hook=..."');
279
- const answer = await ask(' Choose [1/2/3] (default: 2): ', '2');
280
- if (answer === '1') return 'husky';
281
- if (answer === '3') return 'none';
282
- return 'native';
283
- }
284
-
285
- async function installSecurityHookNative({ target }) {
286
- const gitDir = path.join(target, '.git');
287
- if (!(await exists(gitDir))) {
288
- console.warn(' [skip] Security hook (native): .git/ not found — run `git init` first, then re-run installer with --security-hook=native');
289
- return false;
290
- }
291
-
292
- const hooksDir = path.join(gitDir, 'hooks');
293
- await fs.mkdir(hooksDir, { recursive: true });
294
-
295
- const src = path.join(target, '.tas', 'hooks', 'pre-commit');
296
- const dest = path.join(hooksDir, 'pre-commit');
297
-
298
- if (await exists(dest)) {
299
- const existing = await fs.readFile(dest, 'utf8').catch(() => '');
300
- if (!existing.includes('TAS Kit')) {
301
- const backup = dest + '.backup';
302
- await fs.copyFile(dest, backup);
303
- console.log(' [ok] .git/hooks/pre-commit.backup (preserved existing hook)');
304
- }
305
- }
306
-
307
- await fs.copyFile(src, dest);
308
- if (process.platform !== 'win32') {
309
- await fs.chmod(dest, 0o755);
310
- }
311
- console.log(' [ok] .git/hooks/pre-commit (security scan wired — native)');
312
- return true;
313
- }
314
-
315
- async function installSecurityHookHusky({ target }) {
316
- const pkgPath = path.join(target, 'package.json');
317
- const hasPackageJson = await exists(pkgPath);
318
-
319
- if (!hasPackageJson) {
320
- console.warn(' [warn] Security hook (husky): no package.json — falling back to native mode');
321
- return await installSecurityHookNative({ target });
322
- }
323
-
324
- let pkg;
325
- try {
326
- pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
327
- } catch (err) {
328
- console.warn(` [warn] Security hook (husky): package.json unreadable (${err.message}) — falling back to native`);
329
- return await installSecurityHookNative({ target });
330
- }
331
-
332
- pkg.scripts = pkg.scripts || {};
333
- if (!pkg.scripts.prepare) {
334
- pkg.scripts.prepare = 'husky';
335
- } else if (!/husky/.test(pkg.scripts.prepare)) {
336
- pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
337
- }
338
-
339
- pkg.devDependencies = pkg.devDependencies || {};
340
- if (!pkg.devDependencies.husky) {
341
- pkg.devDependencies.husky = '^9.1.0';
342
- }
343
-
344
- await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
345
- console.log(' [ok] package.json (husky devDep + prepare script)');
346
-
347
- const huskyDir = path.join(target, '.husky');
348
- await fs.mkdir(huskyDir, { recursive: true });
349
-
350
- const huskyHook = path.join(huskyDir, 'pre-commit');
351
- const content = [
352
- '# TAS Kit — husky pre-commit (delegates to .tas/hooks/pre-commit)',
353
- '. "$(dirname -- "$0")/../.tas/hooks/pre-commit"',
354
- '',
355
- ].join('\n');
356
- await fs.writeFile(huskyHook, content);
357
- if (process.platform !== 'win32') {
358
- await fs.chmod(huskyHook, 0o755);
359
- }
360
- console.log(' [ok] .husky/pre-commit (delegates to .tas/hooks/pre-commit)');
361
- console.log(' [--] Run `npm install` in the project to activate husky');
362
- return true;
363
- }
364
-
365
- async function installSecurityHook({ target, mode }) {
366
- if (mode === 'none' || !mode) {
367
- console.log(' [skip] Security hook (you can enable later: tas-kit install --security-hook=native|husky)');
368
- return;
369
- }
370
- if (mode === 'husky') {
371
- await installSecurityHookHusky({ target });
372
- return;
373
- }
374
- if (mode === 'native') {
375
- await installSecurityHookNative({ target });
376
- return;
377
- }
378
- console.warn(` [warn] Unknown security hook mode: "${mode}" — skipping`);
379
- }
380
-
381
- // ─── Update ──────────────────────────────────────────────────────────────────
382
-
383
- export async function update({ directory, yes, securityHook, platforms: forcedPlatforms }) {
384
- printLogo();
385
- const target = path.resolve(directory);
386
-
387
- const tasExists = await exists(path.join(target, '.tas'));
388
- if (!tasExists) {
389
- console.error(` ERROR: No TAS Kit found in: ${target}`);
390
- console.error(` Run "install" first to set up TAS Kit.`);
391
- process.exit(1);
392
- }
393
-
394
- console.log(`Updating TAS Kit in: ${target}\n`);
395
-
396
- const platforms = forcedPlatforms && forcedPlatforms.length > 0
397
- ? forcedPlatforms
398
- : await loadPlatforms(target);
399
-
400
- const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
401
-
402
- if (!yes) {
403
- console.warn(` This will overwrite .tas/ and platform directories (${platformLabels}) with the latest kit files.`);
404
- console.warn(` Your CLAUDE.md, tas.yaml, and .env.example will NOT be touched.\n`);
405
- const ok = await confirm('Continue?');
406
- if (!ok) {
407
- console.log('Update cancelled.');
408
- process.exit(0);
409
- }
410
- console.log();
411
- }
412
-
413
- const tasDest = path.join(target, '.tas');
414
- if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
415
- await copyTasDir(TAS_SRC, tasDest);
416
- await fs.copyFile(path.join(PACKAGE_DIR, 'README.md'), path.join(tasDest, 'README.md'));
417
- }
418
- console.log(' [ok] .tas/ (updated)');
419
-
420
- const deletedFiles = await getDeletedFiles();
421
- if (deletedFiles.length > 0) {
422
- const { removed } = await removeDeletedFiles(target, deletedFiles);
423
- for (const f of removed) {
424
- console.log(` [rm] ${f} (removed in this version)`);
425
- }
426
- }
427
-
428
- await runPlatformAdapters(platforms, { tasDir: TAS_SRC, target });
429
-
430
- await savePlatforms(target, platforms);
431
-
432
- if (process.platform !== 'win32') {
433
- await chmodExecutables(target);
434
- }
435
-
436
- if (securityHook) {
437
- await installSecurityHook({ target, mode: securityHook });
438
- }
439
-
440
- const configFilename = platforms.includes('claude-code') ? 'CLAUDE.md' : 'AGENTS.md';
441
- console.log(` [--] ${configFilename}, tas.yaml, project-status.yaml, .env.example — not touched`);
442
- console.log(`
443
- TAS Kit updated successfully!
444
-
445
- If this version added new settings or templates, check the changelog
446
- and manually merge changes into your CLAUDE.md and tas.yaml if needed.
447
- `);
448
- }
449
-
450
- // ─── Install ─────────────────────────────────────────────────────────────────
451
-
452
- export async function install({ directory, yes, securityHook, platforms: forcedPlatforms }) {
453
- printLogo();
454
- const target = path.resolve(directory);
455
-
456
- await fs.mkdir(target, { recursive: true });
457
-
458
- console.log(`Installing TAS Kit into: ${target}\n`);
459
-
460
- const tasExists = await exists(path.join(target, '.tas'));
461
- if (tasExists && !yes) {
462
- console.warn(` WARNING: .tas/ already exists in target directory.`);
463
- console.warn(` Existing files with the same names will be overwritten.\n`);
464
- const ok = await confirm('Continue?');
465
- if (!ok) {
466
- console.log('Installation cancelled.');
467
- process.exit(0);
468
- }
469
- console.log();
470
- }
471
-
472
- // Copy .tas/ (skip if installing into the package itself)
473
- const tasDest = path.join(target, '.tas');
474
- if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
475
- await copyTasDir(TAS_SRC, tasDest);
476
- await fs.copyFile(path.join(PACKAGE_DIR, 'README.md'), path.join(tasDest, 'README.md'));
477
- console.log(' [ok] .tas/');
478
- } else {
479
- console.log(' [--] .tas/ (source = destination, skipped)');
480
- }
481
-
482
- // Platform selection
483
- const platforms = await choosePlatforms({ yes, forced: forcedPlatforms });
484
- await runPlatformAdapters(platforms, { tasDir: TAS_SRC, target });
485
- await savePlatforms(target, platforms);
486
-
487
- // Project config files (first-time only)
488
- // Claude Code uses CLAUDE.md; all other platforms use AGENTS.md
489
- const configFilename = platforms.includes('claude-code') ? 'CLAUDE.md' : 'AGENTS.md';
490
- const configTarget = path.join(target, configFilename);
491
- if (!(await exists(configTarget))) {
492
- await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'templates', 'AGENTS.md'), configTarget);
493
- console.log(` [ok] ${configFilename} (from .tas/templates/AGENTS.md)`);
494
- } else {
495
- console.log(` [--] ${configFilename} already exists, skipped`);
496
- }
497
-
498
- const envExampleTarget = path.join(target, '.env.example');
499
- if (!(await exists(envExampleTarget))) {
500
- await fs.copyFile(path.join(PACKAGE_DIR, '.env.example'), envExampleTarget);
501
- console.log(' [ok] .env.example');
502
- } else {
503
- console.log(' [--] .env.example already exists, skipped');
504
- }
505
-
506
- const tasYamlTarget = path.join(target, 'tas.yaml');
507
- if (!(await exists(tasYamlTarget))) {
508
- await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'), tasYamlTarget);
509
- console.log(' [ok] tas.yaml (from .tas/tas-example.yaml)');
510
- } else {
511
- console.log(' [--] tas.yaml already exists, skipped');
512
- }
513
-
514
- const psYamlTarget = path.join(target, 'project-status.yaml');
515
- if (!(await exists(psYamlTarget))) {
516
- const psSrc = path.join(PACKAGE_DIR, '.tas', 'project-status-example.yaml');
517
- if (await exists(psSrc)) {
518
- await fs.copyFile(psSrc, psYamlTarget);
519
- console.log(' [ok] project-status.yaml (from .tas/project-status-example.yaml)');
520
- }
521
- } else {
522
- console.log(' [--] project-status.yaml already exists, skipped');
523
- }
524
-
525
- if (process.platform !== 'win32') {
526
- await chmodExecutables(target);
527
- }
528
-
529
- const mode = await chooseSecurityHookMode({ target, yes, forced: securityHook });
530
- await installSecurityHook({ target, mode });
531
-
532
- const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
533
- console.log(`
534
- TAS Kit installed successfully! (Platforms: ${platformLabels})
535
-
536
- Next steps:
537
- 1. Edit ${configFilename.padEnd(10)} — add your project's tech stack and conventions
538
- 2. Edit tas.yaml — set project name, team and ADO config
539
- 3. Create .env — add AZURE_DEVOPS_PAT (see .env.example)
540
- 4. Open your IDE — run /tas-init to initialize your project
541
-
542
- Docs:
543
- .tas/README.md — kit overview
544
- .tas/hooks/README.md — pre-commit security hook details
545
- `);
546
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createRequire } from 'node:module';
6
+ import { PLATFORMS, getPlatform } from './adapters/index.js';
7
+
8
+ const PACKAGE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const TAS_SRC = path.join(PACKAGE_DIR, '.tas');
10
+ const PLATFORMS_FILE = '.tas/platforms.json';
11
+ const require = createRequire(import.meta.url);
12
+
13
+ // ─── Logo ────────────────────────────────────────────────────────────────────
14
+
15
+ function printLogo() {
16
+ const c = '\x1b[36m';
17
+ const d = '\x1b[90m';
18
+ const r = '\x1b[0m';
19
+ const art = [
20
+ ` ████████╗ █████╗ ███████╗ ██╗ ██╗ ██╗ ████████╗`,
21
+ ` ╚══██╔══╝ ██╔══██╗██╔════╝ ██║ ██╔╝ ██║ ╚══██╔══╝`,
22
+ ` ██║ ███████║███████╗ █████╔╝ ██║ ██║ `,
23
+ ` ██║ ██╔══██║╚════██║ ██╔═██╗ ██║ ██║ `,
24
+ ` ██║ ██║ ██║███████║ ██║ ██╗ ██║ ██║ `,
25
+ ` ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ `,
26
+ ].map(l => c + l + r).join('\n');
27
+ process.stdout.write('\n' + art + '\n' + d + ' Turbo Agentic SDLC Kit' + r + '\n\n');
28
+ }
29
+
30
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
31
+
32
+ async function getDeletedFiles() {
33
+ try {
34
+ const manifest = require('./deleted-files.json');
35
+ return Object.values(manifest).flat();
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ async function removeDeletedFiles(target, deletedFiles) {
42
+ const removed = [];
43
+ const skipped = [];
44
+ for (const relPath of deletedFiles) {
45
+ const fullPath = path.join(target, relPath);
46
+ try {
47
+ await fs.rm(fullPath, { force: false });
48
+ removed.push(relPath);
49
+ } catch {
50
+ skipped.push(relPath);
51
+ }
52
+ }
53
+ return { removed, skipped };
54
+ }
55
+
56
+ async function ask(question, defaultValue = '') {
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
+ return new Promise((resolve) => {
59
+ rl.question(question, (answer) => {
60
+ rl.close();
61
+ resolve((answer || '').trim() || defaultValue);
62
+ });
63
+ });
64
+ }
65
+
66
+ async function confirm(question) {
67
+ const answer = await ask(`${question} [y/N] `);
68
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
69
+ }
70
+
71
+ async function exists(p) {
72
+ return fs.access(p).then(() => true).catch(() => false);
73
+ }
74
+
75
+ async function copyDir(src, dest) {
76
+ await fs.cp(src, dest, { recursive: true });
77
+ }
78
+
79
+ const TAS_ROOT_EXCLUDE = new Set([
80
+ 'tas-example.yaml',
81
+ 'project-status-example.yaml',
82
+ 'platforms.json',
83
+ 'agents',
84
+ 'commands',
85
+ 'skills',
86
+ 'README.md',
87
+ ]);
88
+
89
+ async function copyTasDir(src, dest) {
90
+ await fs.cp(src, dest, {
91
+ recursive: true,
92
+ filter: (srcPath) => !TAS_ROOT_EXCLUDE.has(path.relative(src, srcPath)),
93
+ });
94
+ }
95
+
96
+ async function chmodExecutables(target) {
97
+ const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
98
+ if (await exists(adoPy)) await fs.chmod(adoPy, 0o755);
99
+
100
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
101
+ if (await exists(preCommit)) await fs.chmod(preCommit, 0o755);
102
+
103
+ const hooksDir = path.join(target, '.tas', '_platform', 'hooks');
104
+ if (await exists(hooksDir)) {
105
+ const files = await fs.readdir(hooksDir);
106
+ for (const f of files) await fs.chmod(path.join(hooksDir, f), 0o755);
107
+ }
108
+ }
109
+
110
+ // ─── Platform selection ───────────────────────────────────────────────────────
111
+
112
+ async function checkboxSelect(title, items, defaultIndexes = [0]) {
113
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
114
+ return fallbackTextSelect(title, items, defaultIndexes);
115
+ }
116
+
117
+ const selected = new Set(defaultIndexes);
118
+ let cursor = defaultIndexes[0] ?? 0;
119
+ let renderedLines = 0;
120
+
121
+ const clearRendered = () => {
122
+ if (renderedLines > 0) {
123
+ process.stdout.write(`\x1b[${renderedLines}A\x1b[0J`);
124
+ }
125
+ };
126
+
127
+ const render = () => {
128
+ clearRendered();
129
+ const lines = [];
130
+ lines.push(` ${title}`);
131
+ lines.push('');
132
+ for (let i = 0; i < items.length; i++) {
133
+ const mark = selected.has(i) ? '\x1b[32m◉\x1b[0m' : '○';
134
+ const arrow = cursor === i ? '\x1b[36m›\x1b[0m' : ' ';
135
+ const label = cursor === i ? `\x1b[36m${items[i].label}\x1b[0m` : items[i].label;
136
+ const note = items[i].note ? ` \x1b[90m${items[i].note}\x1b[0m` : '';
137
+ lines.push(` ${arrow} ${mark} ${label}${note}`);
138
+ }
139
+ lines.push('');
140
+ lines.push('\x1b[90m ↑↓ navigate space select enter confirm\x1b[0m');
141
+ renderedLines = lines.length;
142
+ process.stdout.write(lines.join('\n') + '\n');
143
+ };
144
+
145
+ return new Promise((resolve) => {
146
+ process.stdout.write('\x1b[?25l');
147
+ process.stdin.setRawMode(true);
148
+ process.stdin.resume();
149
+ process.stdin.setEncoding('utf8');
150
+
151
+ render();
152
+
153
+ const onKey = (key) => {
154
+ if (key === '\x03') {
155
+ finish(null);
156
+ process.exit(0);
157
+ } else if (key === '\x1b[A') {
158
+ cursor = (cursor - 1 + items.length) % items.length;
159
+ render();
160
+ } else if (key === '\x1b[B') {
161
+ cursor = (cursor + 1) % items.length;
162
+ render();
163
+ } else if (key === ' ') {
164
+ if (selected.has(cursor)) selected.delete(cursor);
165
+ else selected.add(cursor);
166
+ render();
167
+ } else if (key === '\r' || key === '\n') {
168
+ const ids = items.filter((_, i) => selected.has(i)).map(it => it.id);
169
+ finish(ids.length > 0 ? ids : [items[cursor].id]);
170
+ }
171
+ };
172
+
173
+ const finish = (result) => {
174
+ process.stdin.removeListener('data', onKey);
175
+ process.stdin.setRawMode(false);
176
+ process.stdin.pause();
177
+ process.stdout.write('\x1b[?25h');
178
+ clearRendered();
179
+ if (result) {
180
+ const labels = result.map(id => items.find(it => it.id === id)?.label || id).join(', ');
181
+ process.stdout.write(` Platforms: \x1b[36m${labels}\x1b[0m\n\n`);
182
+ resolve(result);
183
+ }
184
+ };
185
+
186
+ process.stdin.on('data', onKey);
187
+ });
188
+ }
189
+
190
+ async function fallbackTextSelect(title, items, defaultIndexes) {
191
+ console.log(`\n ${title}`);
192
+ items.forEach((item, i) => {
193
+ const def = defaultIndexes.includes(i) ? ' (default)' : '';
194
+ console.log(` [${i + 1}] ${item.label}${def}`);
195
+ });
196
+ console.log('');
197
+ console.log(' Enter numbers comma-separated for multiple platforms.');
198
+ const nums = items.map((_, i) => i + 1).join('/');
199
+ const answer = await ask(` Choose [${nums}] (default: 1): `, '1');
200
+
201
+ const selected = [];
202
+ for (const part of answer.split(',')) {
203
+ const idx = parseInt(part.trim(), 10) - 1;
204
+ if (idx >= 0 && idx < items.length) {
205
+ const id = items[idx].id;
206
+ if (!selected.includes(id)) selected.push(id);
207
+ }
208
+ }
209
+ return selected.length > 0 ? selected : [items[defaultIndexes[0] ?? 0].id];
210
+ }
211
+
212
+ async function choosePlatforms({ yes, forced }) {
213
+ if (forced && forced.length > 0) return forced;
214
+ if (yes) return ['claude-code'];
215
+
216
+ return checkboxSelect(
217
+ 'Agentic Coding Platform(s) to install:',
218
+ PLATFORMS.map(p => ({
219
+ id: p.id,
220
+ label: p.label,
221
+ note: p.id === 'claude-code' ? '(default)' : '',
222
+ })),
223
+ [0],
224
+ );
225
+ }
226
+
227
+ async function savePlatforms(target, platforms) {
228
+ const tasDir = path.join(target, '.tas');
229
+ await fs.mkdir(tasDir, { recursive: true });
230
+ await fs.writeFile(
231
+ path.join(target, PLATFORMS_FILE),
232
+ JSON.stringify({ platforms }, null, 2) + '\n'
233
+ );
234
+ }
235
+
236
+ async function loadPlatforms(target) {
237
+ try {
238
+ const data = JSON.parse(await fs.readFile(path.join(target, PLATFORMS_FILE), 'utf8'));
239
+ return Array.isArray(data.platforms) ? data.platforms : ['claude-code'];
240
+ } catch {
241
+ // fallback: detect from existing directories
242
+ const detected = [];
243
+ if (await exists(path.join(target, '.claude', 'commands'))) detected.push('claude-code');
244
+ if (await exists(path.join(target, '.cursor', 'agents'))) detected.push('cursor');
245
+ if (await exists(path.join(target, '.codex', 'skills'))) detected.push('codex');
246
+ if (await exists(path.join(target, '.agents', 'skills'))) detected.push('antigravity');
247
+ return detected.length > 0 ? detected : ['claude-code'];
248
+ }
249
+ }
250
+
251
+ async function runPlatformAdapters(platforms, { tasDir, target }) {
252
+ for (const id of platforms) {
253
+ const platform = getPlatform(id);
254
+ if (!platform) {
255
+ console.warn(` [warn] Unknown platform "${id}" — skipping`);
256
+ continue;
257
+ }
258
+ console.log(`\n Platform: ${platform.label}`);
259
+ await platform.install({ tasDir, target });
260
+ }
261
+ }
262
+
263
+ // ─── Security hook wiring ────────────────────────────────────────────────────
264
+
265
+ async function chooseSecurityHookMode({ target, yes, forced }) {
266
+ if (forced) return forced;
267
+ if (yes) return 'native';
268
+
269
+ const hasPackageJson = await exists(path.join(target, 'package.json'));
270
+ const hint = hasPackageJson
271
+ ? ' Detected package.json — husky mode is available.'
272
+ : ' No package.json — husky mode will add one or fall back to native.';
273
+
274
+ console.log('\n Pre-commit security hook wiring:');
275
+ console.log(hint);
276
+ console.log(' [1] husky — shared via git, requires Node project');
277
+ console.log(' [2] native — plain .git/hooks/pre-commit, any stack');
278
+ console.log(' [3] skip — wire it later with "tas-kit install --security-hook=..."');
279
+ const answer = await ask(' Choose [1/2/3] (default: 2): ', '2');
280
+ if (answer === '1') return 'husky';
281
+ if (answer === '3') return 'none';
282
+ return 'native';
283
+ }
284
+
285
+ async function installSecurityHookNative({ target }) {
286
+ const gitDir = path.join(target, '.git');
287
+ if (!(await exists(gitDir))) {
288
+ console.warn(' [skip] Security hook (native): .git/ not found — run `git init` first, then re-run installer with --security-hook=native');
289
+ return false;
290
+ }
291
+
292
+ const hooksDir = path.join(gitDir, 'hooks');
293
+ await fs.mkdir(hooksDir, { recursive: true });
294
+
295
+ const src = path.join(target, '.tas', 'hooks', 'pre-commit');
296
+ const dest = path.join(hooksDir, 'pre-commit');
297
+
298
+ if (await exists(dest)) {
299
+ const existing = await fs.readFile(dest, 'utf8').catch(() => '');
300
+ if (!existing.includes('TAS Kit')) {
301
+ const backup = dest + '.backup';
302
+ await fs.copyFile(dest, backup);
303
+ console.log(' [ok] .git/hooks/pre-commit.backup (preserved existing hook)');
304
+ }
305
+ }
306
+
307
+ await fs.copyFile(src, dest);
308
+ if (process.platform !== 'win32') {
309
+ await fs.chmod(dest, 0o755);
310
+ }
311
+ console.log(' [ok] .git/hooks/pre-commit (security scan wired — native)');
312
+ return true;
313
+ }
314
+
315
+ async function installSecurityHookHusky({ target }) {
316
+ const pkgPath = path.join(target, 'package.json');
317
+ const hasPackageJson = await exists(pkgPath);
318
+
319
+ if (!hasPackageJson) {
320
+ console.warn(' [warn] Security hook (husky): no package.json — falling back to native mode');
321
+ return await installSecurityHookNative({ target });
322
+ }
323
+
324
+ let pkg;
325
+ try {
326
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
327
+ } catch (err) {
328
+ console.warn(` [warn] Security hook (husky): package.json unreadable (${err.message}) — falling back to native`);
329
+ return await installSecurityHookNative({ target });
330
+ }
331
+
332
+ pkg.scripts = pkg.scripts || {};
333
+ if (!pkg.scripts.prepare) {
334
+ pkg.scripts.prepare = 'husky';
335
+ } else if (!/husky/.test(pkg.scripts.prepare)) {
336
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
337
+ }
338
+
339
+ pkg.devDependencies = pkg.devDependencies || {};
340
+ if (!pkg.devDependencies.husky) {
341
+ pkg.devDependencies.husky = '^9.1.0';
342
+ }
343
+
344
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
345
+ console.log(' [ok] package.json (husky devDep + prepare script)');
346
+
347
+ const huskyDir = path.join(target, '.husky');
348
+ await fs.mkdir(huskyDir, { recursive: true });
349
+
350
+ const huskyHook = path.join(huskyDir, 'pre-commit');
351
+ const content = [
352
+ '# TAS Kit — husky pre-commit (delegates to .tas/hooks/pre-commit)',
353
+ '. "$(dirname -- "$0")/../.tas/hooks/pre-commit"',
354
+ '',
355
+ ].join('\n');
356
+ await fs.writeFile(huskyHook, content);
357
+ if (process.platform !== 'win32') {
358
+ await fs.chmod(huskyHook, 0o755);
359
+ }
360
+ console.log(' [ok] .husky/pre-commit (delegates to .tas/hooks/pre-commit)');
361
+ console.log(' [--] Run `npm install` in the project to activate husky');
362
+ return true;
363
+ }
364
+
365
+ async function installSecurityHook({ target, mode }) {
366
+ if (mode === 'none' || !mode) {
367
+ console.log(' [skip] Security hook (you can enable later: tas-kit install --security-hook=native|husky)');
368
+ return;
369
+ }
370
+ if (mode === 'husky') {
371
+ await installSecurityHookHusky({ target });
372
+ return;
373
+ }
374
+ if (mode === 'native') {
375
+ await installSecurityHookNative({ target });
376
+ return;
377
+ }
378
+ console.warn(` [warn] Unknown security hook mode: "${mode}" — skipping`);
379
+ }
380
+
381
+ // ─── Update ──────────────────────────────────────────────────────────────────
382
+
383
+ export async function update({ directory, yes, securityHook, platforms: forcedPlatforms }) {
384
+ printLogo();
385
+ const target = path.resolve(directory);
386
+
387
+ const tasExists = await exists(path.join(target, '.tas'));
388
+ if (!tasExists) {
389
+ console.error(` ERROR: No TAS Kit found in: ${target}`);
390
+ console.error(` Run "install" first to set up TAS Kit.`);
391
+ process.exit(1);
392
+ }
393
+
394
+ console.log(`Updating TAS Kit in: ${target}\n`);
395
+
396
+ const platforms = forcedPlatforms && forcedPlatforms.length > 0
397
+ ? forcedPlatforms
398
+ : await loadPlatforms(target);
399
+
400
+ const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
401
+
402
+ if (!yes) {
403
+ console.warn(` This will overwrite .tas/ and platform directories (${platformLabels}) with the latest kit files.`);
404
+ console.warn(` Your CLAUDE.md, tas.yaml, and .env.example will NOT be touched.\n`);
405
+ const ok = await confirm('Continue?');
406
+ if (!ok) {
407
+ console.log('Update cancelled.');
408
+ process.exit(0);
409
+ }
410
+ console.log();
411
+ }
412
+
413
+ const tasDest = path.join(target, '.tas');
414
+ if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
415
+ await copyTasDir(TAS_SRC, tasDest);
416
+ await fs.copyFile(path.join(PACKAGE_DIR, 'README.md'), path.join(tasDest, 'README.md'));
417
+ }
418
+ console.log(' [ok] .tas/ (updated)');
419
+
420
+ const deletedFiles = await getDeletedFiles();
421
+ if (deletedFiles.length > 0) {
422
+ const { removed } = await removeDeletedFiles(target, deletedFiles);
423
+ for (const f of removed) {
424
+ console.log(` [rm] ${f} (removed in this version)`);
425
+ }
426
+ }
427
+
428
+ await runPlatformAdapters(platforms, { tasDir: TAS_SRC, target });
429
+
430
+ await savePlatforms(target, platforms);
431
+
432
+ if (process.platform !== 'win32') {
433
+ await chmodExecutables(target);
434
+ }
435
+
436
+ if (securityHook) {
437
+ await installSecurityHook({ target, mode: securityHook });
438
+ }
439
+
440
+ const configFilename = platforms.includes('claude-code') ? 'CLAUDE.md' : 'AGENTS.md';
441
+ console.log(` [--] ${configFilename}, tas.yaml, project-status.yaml, .env.example — not touched`);
442
+ console.log(`
443
+ TAS Kit updated successfully!
444
+
445
+ If this version added new settings or templates, check the changelog
446
+ and manually merge changes into your CLAUDE.md and tas.yaml if needed.
447
+ `);
448
+ }
449
+
450
+ // ─── Install ─────────────────────────────────────────────────────────────────
451
+
452
+ export async function install({ directory, yes, securityHook, platforms: forcedPlatforms }) {
453
+ printLogo();
454
+ const target = path.resolve(directory);
455
+
456
+ await fs.mkdir(target, { recursive: true });
457
+
458
+ console.log(`Installing TAS Kit into: ${target}\n`);
459
+
460
+ const tasExists = await exists(path.join(target, '.tas'));
461
+ if (tasExists && !yes) {
462
+ console.warn(` WARNING: .tas/ already exists in target directory.`);
463
+ console.warn(` Existing files with the same names will be overwritten.\n`);
464
+ const ok = await confirm('Continue?');
465
+ if (!ok) {
466
+ console.log('Installation cancelled.');
467
+ process.exit(0);
468
+ }
469
+ console.log();
470
+ }
471
+
472
+ // Copy .tas/ (skip if installing into the package itself)
473
+ const tasDest = path.join(target, '.tas');
474
+ if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
475
+ await copyTasDir(TAS_SRC, tasDest);
476
+ await fs.copyFile(path.join(PACKAGE_DIR, 'README.md'), path.join(tasDest, 'README.md'));
477
+ console.log(' [ok] .tas/');
478
+ } else {
479
+ console.log(' [--] .tas/ (source = destination, skipped)');
480
+ }
481
+
482
+ // Platform selection
483
+ const platforms = await choosePlatforms({ yes, forced: forcedPlatforms });
484
+ await runPlatformAdapters(platforms, { tasDir: TAS_SRC, target });
485
+ await savePlatforms(target, platforms);
486
+
487
+ // Project config files (first-time only)
488
+ // Claude Code uses CLAUDE.md; all other platforms use AGENTS.md
489
+ const configFilename = platforms.includes('claude-code') ? 'CLAUDE.md' : 'AGENTS.md';
490
+ const configTarget = path.join(target, configFilename);
491
+ if (!(await exists(configTarget))) {
492
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'templates', 'AGENTS.md'), configTarget);
493
+ console.log(` [ok] ${configFilename} (from .tas/templates/AGENTS.md)`);
494
+ } else {
495
+ console.log(` [--] ${configFilename} already exists, skipped`);
496
+ }
497
+
498
+ const envExampleTarget = path.join(target, '.env.example');
499
+ if (!(await exists(envExampleTarget))) {
500
+ await fs.copyFile(path.join(PACKAGE_DIR, '.env.example'), envExampleTarget);
501
+ console.log(' [ok] .env.example');
502
+ } else {
503
+ console.log(' [--] .env.example already exists, skipped');
504
+ }
505
+
506
+ const tasYamlTarget = path.join(target, 'tas.yaml');
507
+ if (!(await exists(tasYamlTarget))) {
508
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'), tasYamlTarget);
509
+ console.log(' [ok] tas.yaml (from .tas/tas-example.yaml)');
510
+ } else {
511
+ console.log(' [--] tas.yaml already exists, skipped');
512
+ }
513
+
514
+ const psYamlTarget = path.join(target, 'project-status.yaml');
515
+ if (!(await exists(psYamlTarget))) {
516
+ const psSrc = path.join(PACKAGE_DIR, '.tas', 'project-status-example.yaml');
517
+ if (await exists(psSrc)) {
518
+ await fs.copyFile(psSrc, psYamlTarget);
519
+ console.log(' [ok] project-status.yaml (from .tas/project-status-example.yaml)');
520
+ }
521
+ } else {
522
+ console.log(' [--] project-status.yaml already exists, skipped');
523
+ }
524
+
525
+ if (process.platform !== 'win32') {
526
+ await chmodExecutables(target);
527
+ }
528
+
529
+ const mode = await chooseSecurityHookMode({ target, yes, forced: securityHook });
530
+ await installSecurityHook({ target, mode });
531
+
532
+ const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
533
+ console.log(`
534
+ TAS Kit installed successfully! (Platforms: ${platformLabels})
535
+
536
+ Next steps:
537
+ 1. Edit ${configFilename.padEnd(10)} — add your project's tech stack and conventions
538
+ 2. Edit tas.yaml — set project name, team and ADO config
539
+ 3. Create .env — add AZURE_DEVOPS_PAT (see .env.example)
540
+ 4. Open your IDE — run /tas-init to initialize your project
541
+
542
+ Docs:
543
+ .tas/README.md — kit overview
544
+ .tas/hooks/README.md — pre-commit security hook details
545
+ `);
546
+ }