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