@torus-engineering/tas-kit 1.11.1 → 1.12.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 +137 -0
  57. package/lib/adapters/claude-code.js +35 -0
  58. package/lib/adapters/codex.js +163 -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 +403 -327
  64. package/package.json +4 -3
  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,403 @@
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
+ async function getDeletedFiles() {
14
+ try {
15
+ const manifest = require('./deleted-files.json');
16
+ return Object.values(manifest).flat();
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ async function removeDeletedFiles(target, deletedFiles) {
23
+ const removed = [];
24
+ const skipped = [];
25
+ for (const relPath of deletedFiles) {
26
+ const fullPath = path.join(target, relPath);
27
+ try {
28
+ await fs.rm(fullPath, { force: false });
29
+ removed.push(relPath);
30
+ } catch {
31
+ skipped.push(relPath);
32
+ }
33
+ }
34
+ return { removed, skipped };
35
+ }
36
+
37
+ async function ask(question, defaultValue = '') {
38
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
39
+ return new Promise((resolve) => {
40
+ rl.question(question, (answer) => {
41
+ rl.close();
42
+ resolve((answer || '').trim() || defaultValue);
43
+ });
44
+ });
45
+ }
46
+
47
+ async function confirm(question) {
48
+ const answer = await ask(`${question} [y/N] `);
49
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
50
+ }
51
+
52
+ async function exists(p) {
53
+ return fs.access(p).then(() => true).catch(() => false);
54
+ }
55
+
56
+ async function copyDir(src, dest) {
57
+ await fs.cp(src, dest, { recursive: true });
58
+ }
59
+
60
+ // ─── Platform selection ───────────────────────────────────────────────────────
61
+
62
+ async function choosePlatforms({ yes, forced }) {
63
+ if (forced && forced.length > 0) return forced;
64
+ if (yes) return ['claude-code'];
65
+
66
+ console.log('\n Agentic Coding Platform(s) to install:');
67
+ PLATFORMS.forEach((p, i) => {
68
+ const def = p.id === 'claude-code' ? ' (default)' : '';
69
+ console.log(` [${i + 1}] ${p.label}${def}`);
70
+ });
71
+ console.log('');
72
+ console.log(' Enter numbers comma-separated for multiple platforms.');
73
+ const answer = await ask(' Choose [1/2/3/4] (default: 1): ', '1');
74
+
75
+ const selected = [];
76
+ for (const part of answer.split(',')) {
77
+ const idx = parseInt(part.trim(), 10) - 1;
78
+ if (idx >= 0 && idx < PLATFORMS.length) {
79
+ const id = PLATFORMS[idx].id;
80
+ if (!selected.includes(id)) selected.push(id);
81
+ }
82
+ }
83
+ return selected.length > 0 ? selected : ['claude-code'];
84
+ }
85
+
86
+ async function savePlatforms(target, platforms) {
87
+ const tasDir = path.join(target, '.tas');
88
+ await fs.mkdir(tasDir, { recursive: true });
89
+ await fs.writeFile(
90
+ path.join(target, PLATFORMS_FILE),
91
+ JSON.stringify({ platforms }, null, 2) + '\n'
92
+ );
93
+ }
94
+
95
+ async function loadPlatforms(target) {
96
+ try {
97
+ const data = JSON.parse(await fs.readFile(path.join(target, PLATFORMS_FILE), 'utf8'));
98
+ return Array.isArray(data.platforms) ? data.platforms : ['claude-code'];
99
+ } catch {
100
+ // fallback: detect from existing directories
101
+ const detected = [];
102
+ if (await exists(path.join(target, '.claude', 'commands'))) detected.push('claude-code');
103
+ if (await exists(path.join(target, '.cursor', 'agents'))) detected.push('cursor');
104
+ if (await exists(path.join(target, '.codex', 'skills'))) detected.push('codex');
105
+ if (await exists(path.join(target, '.agents', 'skills'))) detected.push('antigravity');
106
+ return detected.length > 0 ? detected : ['claude-code'];
107
+ }
108
+ }
109
+
110
+ async function runPlatformAdapters(platforms, { tasDir, target }) {
111
+ for (const id of platforms) {
112
+ const platform = getPlatform(id);
113
+ if (!platform) {
114
+ console.warn(` [warn] Unknown platform "${id}"skipping`);
115
+ continue;
116
+ }
117
+ console.log(`\n Platform: ${platform.label}`);
118
+ await platform.install({ tasDir, target });
119
+ }
120
+ }
121
+
122
+ // ─── Security hook wiring ────────────────────────────────────────────────────
123
+
124
+ async function chooseSecurityHookMode({ target, yes, forced }) {
125
+ if (forced) return forced;
126
+ if (yes) return 'native';
127
+
128
+ const hasPackageJson = await exists(path.join(target, 'package.json'));
129
+ const hint = hasPackageJson
130
+ ? ' Detected package.json husky mode is available.'
131
+ : ' No package.json husky mode will add one or fall back to native.';
132
+
133
+ console.log('\n Pre-commit security hook wiring:');
134
+ console.log(hint);
135
+ console.log(' [1] husky — shared via git, requires Node project');
136
+ console.log(' [2] native — plain .git/hooks/pre-commit, any stack');
137
+ console.log(' [3] skip — wire it later with "tas-kit install --security-hook=..."');
138
+ const answer = await ask(' Choose [1/2/3] (default: 2): ', '2');
139
+ if (answer === '1') return 'husky';
140
+ if (answer === '3') return 'none';
141
+ return 'native';
142
+ }
143
+
144
+ async function installSecurityHookNative({ target }) {
145
+ const gitDir = path.join(target, '.git');
146
+ if (!(await exists(gitDir))) {
147
+ console.warn(' [skip] Security hook (native): .git/ not found — run `git init` first, then re-run installer with --security-hook=native');
148
+ return false;
149
+ }
150
+
151
+ const hooksDir = path.join(gitDir, 'hooks');
152
+ await fs.mkdir(hooksDir, { recursive: true });
153
+
154
+ const src = path.join(target, '.tas', 'hooks', 'pre-commit');
155
+ const dest = path.join(hooksDir, 'pre-commit');
156
+
157
+ if (await exists(dest)) {
158
+ const existing = await fs.readFile(dest, 'utf8').catch(() => '');
159
+ if (!existing.includes('TAS Kit')) {
160
+ const backup = dest + '.backup';
161
+ await fs.copyFile(dest, backup);
162
+ console.log(' [ok] .git/hooks/pre-commit.backup (preserved existing hook)');
163
+ }
164
+ }
165
+
166
+ await fs.copyFile(src, dest);
167
+ if (process.platform !== 'win32') {
168
+ await fs.chmod(dest, 0o755);
169
+ }
170
+ console.log(' [ok] .git/hooks/pre-commit (security scan wired — native)');
171
+ return true;
172
+ }
173
+
174
+ async function installSecurityHookHusky({ target }) {
175
+ const pkgPath = path.join(target, 'package.json');
176
+ const hasPackageJson = await exists(pkgPath);
177
+
178
+ if (!hasPackageJson) {
179
+ console.warn(' [warn] Security hook (husky): no package.json falling back to native mode');
180
+ return await installSecurityHookNative({ target });
181
+ }
182
+
183
+ let pkg;
184
+ try {
185
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
186
+ } catch (err) {
187
+ console.warn(` [warn] Security hook (husky): package.json unreadable (${err.message}) — falling back to native`);
188
+ return await installSecurityHookNative({ target });
189
+ }
190
+
191
+ pkg.scripts = pkg.scripts || {};
192
+ if (!pkg.scripts.prepare) {
193
+ pkg.scripts.prepare = 'husky';
194
+ } else if (!/husky/.test(pkg.scripts.prepare)) {
195
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
196
+ }
197
+
198
+ pkg.devDependencies = pkg.devDependencies || {};
199
+ if (!pkg.devDependencies.husky) {
200
+ pkg.devDependencies.husky = '^9.1.0';
201
+ }
202
+
203
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
204
+ console.log(' [ok] package.json (husky devDep + prepare script)');
205
+
206
+ const huskyDir = path.join(target, '.husky');
207
+ await fs.mkdir(huskyDir, { recursive: true });
208
+
209
+ const huskyHook = path.join(huskyDir, 'pre-commit');
210
+ const content = [
211
+ '# TAS Kit husky pre-commit (delegates to .tas/hooks/pre-commit)',
212
+ '. "$(dirname -- "$0")/../.tas/hooks/pre-commit"',
213
+ '',
214
+ ].join('\n');
215
+ await fs.writeFile(huskyHook, content);
216
+ if (process.platform !== 'win32') {
217
+ await fs.chmod(huskyHook, 0o755);
218
+ }
219
+ console.log(' [ok] .husky/pre-commit (delegates to .tas/hooks/pre-commit)');
220
+ console.log(' [--] Run `npm install` in the project to activate husky');
221
+ return true;
222
+ }
223
+
224
+ async function installSecurityHook({ target, mode }) {
225
+ if (mode === 'none' || !mode) {
226
+ console.log(' [skip] Security hook (you can enable later: tas-kit install --security-hook=native|husky)');
227
+ return;
228
+ }
229
+ if (mode === 'husky') {
230
+ await installSecurityHookHusky({ target });
231
+ return;
232
+ }
233
+ if (mode === 'native') {
234
+ await installSecurityHookNative({ target });
235
+ return;
236
+ }
237
+ console.warn(` [warn] Unknown security hook mode: "${mode}" — skipping`);
238
+ }
239
+
240
+ // ─── Update ──────────────────────────────────────────────────────────────────
241
+
242
+ export async function update({ directory, yes, securityHook, platforms: forcedPlatforms }) {
243
+ const target = path.resolve(directory);
244
+
245
+ const tasExists = await exists(path.join(target, '.tas'));
246
+ if (!tasExists) {
247
+ console.error(` ERROR: No TAS Kit found in: ${target}`);
248
+ console.error(` Run "install" first to set up TAS Kit.`);
249
+ process.exit(1);
250
+ }
251
+
252
+ console.log(`\nUpdating TAS Kit in: ${target}\n`);
253
+
254
+ const platforms = forcedPlatforms && forcedPlatforms.length > 0
255
+ ? forcedPlatforms
256
+ : await loadPlatforms(target);
257
+
258
+ const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
259
+
260
+ if (!yes) {
261
+ console.warn(` This will overwrite .tas/ and platform directories (${platformLabels}) with the latest kit files.`);
262
+ console.warn(` Your CLAUDE.md, tas.yaml, and .env.example will NOT be touched.\n`);
263
+ const ok = await confirm('Continue?');
264
+ if (!ok) {
265
+ console.log('Update cancelled.');
266
+ process.exit(0);
267
+ }
268
+ console.log();
269
+ }
270
+
271
+ const tasDest = path.join(target, '.tas');
272
+ if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
273
+ await copyDir(TAS_SRC, tasDest);
274
+ }
275
+ console.log(' [ok] .tas/ (updated)');
276
+
277
+ const deletedFiles = await getDeletedFiles();
278
+ if (deletedFiles.length > 0) {
279
+ const { removed } = await removeDeletedFiles(target, deletedFiles);
280
+ for (const f of removed) {
281
+ console.log(` [rm] ${f} (removed in this version)`);
282
+ }
283
+ }
284
+
285
+ await runPlatformAdapters(platforms, { tasDir: path.join(target, '.tas'), target });
286
+
287
+ await savePlatforms(target, platforms);
288
+
289
+ if (process.platform !== 'win32') {
290
+ await chmodExecutables(target);
291
+ }
292
+
293
+ if (securityHook) {
294
+ await installSecurityHook({ target, mode: securityHook });
295
+ }
296
+
297
+ console.log(` [--] CLAUDE.md, tas.yaml, .env.example — not touched`);
298
+ console.log(`
299
+ TAS Kit updated successfully!
300
+
301
+ If this version added new settings or templates, check the changelog
302
+ and manually merge changes into your CLAUDE.md and tas.yaml if needed.
303
+ `);
304
+ }
305
+
306
+ // ─── Install ─────────────────────────────────────────────────────────────────
307
+
308
+ export async function install({ directory, yes, securityHook, platforms: forcedPlatforms }) {
309
+ const target = path.resolve(directory);
310
+
311
+ await fs.mkdir(target, { recursive: true });
312
+
313
+ console.log(`\nInstalling TAS Kit into: ${target}\n`);
314
+
315
+ const tasExists = await exists(path.join(target, '.tas'));
316
+ if (tasExists && !yes) {
317
+ console.warn(` WARNING: .tas/ already exists in target directory.`);
318
+ console.warn(` Existing files with the same names will be overwritten.\n`);
319
+ const ok = await confirm('Continue?');
320
+ if (!ok) {
321
+ console.log('Installation cancelled.');
322
+ process.exit(0);
323
+ }
324
+ console.log();
325
+ }
326
+
327
+ // Copy .tas/ (skip if installing into the package itself)
328
+ const tasDest = path.join(target, '.tas');
329
+ if (path.resolve(TAS_SRC) !== path.resolve(tasDest)) {
330
+ await copyDir(TAS_SRC, tasDest);
331
+ console.log(' [ok] .tas/');
332
+ } else {
333
+ console.log(' [--] .tas/ (source = destination, skipped)');
334
+ }
335
+
336
+ // Platform selection
337
+ const platforms = await choosePlatforms({ yes, forced: forcedPlatforms });
338
+ await runPlatformAdapters(platforms, { tasDir: path.join(target, '.tas'), target });
339
+ await savePlatforms(target, platforms);
340
+
341
+ // Project config files (first-time only)
342
+ const claudeMdTarget = path.join(target, 'CLAUDE.md');
343
+ if (!(await exists(claudeMdTarget))) {
344
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'templates', 'AGENTS.md'), claudeMdTarget);
345
+ console.log(' [ok] CLAUDE.md (from .tas/templates/AGENTS.md)');
346
+ } else {
347
+ console.log(' [--] CLAUDE.md already exists, skipped');
348
+ }
349
+
350
+ const envExampleTarget = path.join(target, '.env.example');
351
+ if (!(await exists(envExampleTarget))) {
352
+ await fs.copyFile(path.join(PACKAGE_DIR, '.env.example'), envExampleTarget);
353
+ console.log(' [ok] .env.example');
354
+ } else {
355
+ console.log(' [--] .env.example already exists, skipped');
356
+ }
357
+
358
+ const tasYamlTarget = path.join(target, 'tas.yaml');
359
+ if (!(await exists(tasYamlTarget))) {
360
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'), tasYamlTarget);
361
+ console.log(' [ok] tas.yaml (from .tas/tas-example.yaml)');
362
+ } else {
363
+ console.log(' [--] tas.yaml already exists, skipped');
364
+ }
365
+
366
+ if (process.platform !== 'win32') {
367
+ await chmodExecutables(target);
368
+ }
369
+
370
+ const mode = await chooseSecurityHookMode({ target, yes, forced: securityHook });
371
+ await installSecurityHook({ target, mode });
372
+
373
+ const platformLabels = platforms.map(id => getPlatform(id)?.label || id).join(', ');
374
+ console.log(`
375
+ TAS Kit installed successfully! (Platforms: ${platformLabels})
376
+
377
+ Next steps:
378
+ 1. Edit CLAUDE.md — add your project's tech stack and conventions
379
+ 2. Edit tas.yaml — set project name, team and ADO config
380
+ 3. Create .env — add AZURE_DEVOPS_PAT (see .env.example)
381
+ 4. Open your IDE — run /tas-init to initialize your project
382
+
383
+ Docs:
384
+ .tas/README.md — kit overview
385
+ .tas/hooks/README.md — pre-commit security hook details
386
+ `);
387
+ }
388
+
389
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
390
+
391
+ async function chmodExecutables(target) {
392
+ const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
393
+ if (await exists(adoPy)) await fs.chmod(adoPy, 0o755);
394
+
395
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
396
+ if (await exists(preCommit)) await fs.chmod(preCommit, 0o755);
397
+
398
+ const hooksDir = path.join(target, '.tas', '_platform', 'hooks');
399
+ if (await exists(hooksDir)) {
400
+ const files = await fs.readdir(hooksDir);
401
+ for (const f of files) await fs.chmod(path.join(hooksDir, f), 0o755);
402
+ }
403
+ }