@torus-engineering/tas-kit 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/install.js CHANGED
@@ -31,16 +31,21 @@ async function removeDeletedFiles(target, deletedFiles) {
31
31
  return { removed, skipped };
32
32
  }
33
33
 
34
- async function confirm(question) {
34
+ async function ask(question, defaultValue = '') {
35
35
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
36
36
  return new Promise((resolve) => {
37
- rl.question(`${question} [y/N] `, (answer) => {
37
+ rl.question(question, (answer) => {
38
38
  rl.close();
39
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
39
+ resolve((answer || '').trim() || defaultValue);
40
40
  });
41
41
  });
42
42
  }
43
43
 
44
+ async function confirm(question) {
45
+ const answer = await ask(`${question} [y/N] `);
46
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
47
+ }
48
+
44
49
  async function exists(p) {
45
50
  return fs.access(p).then(() => true).catch(() => false);
46
51
  }
@@ -49,10 +54,131 @@ async function copyDir(src, dest) {
49
54
  await fs.cp(src, dest, { recursive: true });
50
55
  }
51
56
 
52
- export async function update({ directory, yes }) {
57
+ // ─── Security hook wiring ────────────────────────────────────────────────────
58
+
59
+ async function chooseSecurityHookMode({ target, yes, forced }) {
60
+ if (forced) return forced;
61
+ if (yes) return 'native';
62
+
63
+ const hasPackageJson = await exists(path.join(target, 'package.json'));
64
+ const hint = hasPackageJson
65
+ ? ' Detected package.json — husky mode is available.'
66
+ : ' No package.json — husky mode will add one or fall back to native.';
67
+
68
+ console.log('\n Pre-commit security hook wiring:');
69
+ console.log(hint);
70
+ console.log(' [1] husky — shared via git, requires Node project');
71
+ console.log(' [2] native — plain .git/hooks/pre-commit, any stack');
72
+ console.log(' [3] skip — wire it later with "tas-kit install --security-hook=..."');
73
+ const answer = await ask(' Choose [1/2/3] (default: 2): ', '2');
74
+ if (answer === '1') return 'husky';
75
+ if (answer === '3') return 'none';
76
+ return 'native';
77
+ }
78
+
79
+ async function installSecurityHookNative({ target }) {
80
+ const gitDir = path.join(target, '.git');
81
+ if (!(await exists(gitDir))) {
82
+ console.warn(' [skip] Security hook (native): .git/ not found — run `git init` first, then re-run installer with --security-hook=native');
83
+ return false;
84
+ }
85
+
86
+ const hooksDir = path.join(gitDir, 'hooks');
87
+ await fs.mkdir(hooksDir, { recursive: true });
88
+
89
+ const src = path.join(PACKAGE_DIR, '.tas', 'hooks', 'pre-commit');
90
+ const dest = path.join(hooksDir, 'pre-commit');
91
+
92
+ if (await exists(dest)) {
93
+ const existing = await fs.readFile(dest, 'utf8').catch(() => '');
94
+ if (!existing.includes('TAS Kit')) {
95
+ const backup = dest + '.backup';
96
+ await fs.copyFile(dest, backup);
97
+ console.log(' [ok] .git/hooks/pre-commit.backup (preserved existing hook)');
98
+ }
99
+ }
100
+
101
+ await fs.copyFile(src, dest);
102
+ if (process.platform !== 'win32') {
103
+ await fs.chmod(dest, 0o755);
104
+ }
105
+ console.log(' [ok] .git/hooks/pre-commit (security scan wired — native)');
106
+ return true;
107
+ }
108
+
109
+ async function installSecurityHookHusky({ target }) {
110
+ const pkgPath = path.join(target, 'package.json');
111
+ const hasPackageJson = await exists(pkgPath);
112
+
113
+ if (!hasPackageJson) {
114
+ console.warn(' [warn] Security hook (husky): no package.json — falling back to native mode');
115
+ return await installSecurityHookNative({ target });
116
+ }
117
+
118
+ // Read + update package.json (add prepare script + husky devDep)
119
+ let pkg;
120
+ try {
121
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
122
+ } catch (err) {
123
+ console.warn(` [warn] Security hook (husky): package.json unreadable (${err.message}) — falling back to native`);
124
+ return await installSecurityHookNative({ target });
125
+ }
126
+
127
+ pkg.scripts = pkg.scripts || {};
128
+ if (!pkg.scripts.prepare) {
129
+ pkg.scripts.prepare = 'husky';
130
+ } else if (!/husky/.test(pkg.scripts.prepare)) {
131
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
132
+ }
133
+
134
+ pkg.devDependencies = pkg.devDependencies || {};
135
+ if (!pkg.devDependencies.husky) {
136
+ pkg.devDependencies.husky = '^9.1.0';
137
+ }
138
+
139
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
140
+ console.log(' [ok] package.json (husky devDep + prepare script)');
141
+
142
+ // Write .husky/pre-commit
143
+ const huskyDir = path.join(target, '.husky');
144
+ await fs.mkdir(huskyDir, { recursive: true });
145
+
146
+ const huskyHook = path.join(huskyDir, 'pre-commit');
147
+ const content = [
148
+ '# TAS Kit — husky pre-commit (delegates to .tas/hooks/pre-commit)',
149
+ '. "$(dirname -- "$0")/../.tas/hooks/pre-commit"',
150
+ '',
151
+ ].join('\n');
152
+ await fs.writeFile(huskyHook, content);
153
+ if (process.platform !== 'win32') {
154
+ await fs.chmod(huskyHook, 0o755);
155
+ }
156
+ console.log(' [ok] .husky/pre-commit (delegates to .tas/hooks/pre-commit)');
157
+ console.log(' [--] Run `npm install` in the project to activate husky');
158
+ return true;
159
+ }
160
+
161
+ async function installSecurityHook({ target, mode }) {
162
+ if (mode === 'none' || !mode) {
163
+ console.log(' [skip] Security hook (you can enable later: tas-kit install --security-hook=native|husky)');
164
+ return;
165
+ }
166
+ if (mode === 'husky') {
167
+ await installSecurityHookHusky({ target });
168
+ return;
169
+ }
170
+ if (mode === 'native') {
171
+ await installSecurityHookNative({ target });
172
+ return;
173
+ }
174
+ console.warn(` [warn] Unknown security hook mode: "${mode}" — skipping`);
175
+ }
176
+
177
+ // ─── Update ──────────────────────────────────────────────────────────────────
178
+
179
+ export async function update({ directory, yes, securityHook }) {
53
180
  const target = path.resolve(directory);
54
181
 
55
- // Must already have .claude/ or .tas/ — otherwise suggest install
56
182
  const claudeExists = await exists(path.join(target, '.claude'));
57
183
  const tasExists = await exists(path.join(target, '.tas'));
58
184
  if (!claudeExists && !tasExists) {
@@ -74,21 +200,12 @@ export async function update({ directory, yes }) {
74
200
  console.log();
75
201
  }
76
202
 
77
- // Overwrite .claude/
78
- await copyDir(
79
- path.join(PACKAGE_DIR, '.claude'),
80
- path.join(target, '.claude')
81
- );
203
+ await copyDir(path.join(PACKAGE_DIR, '.claude'), path.join(target, '.claude'));
82
204
  console.log(' [ok] .claude/ (updated)');
83
205
 
84
- // Overwrite .tas/
85
- await copyDir(
86
- path.join(PACKAGE_DIR, '.tas'),
87
- path.join(target, '.tas')
88
- );
206
+ await copyDir(path.join(PACKAGE_DIR, '.tas'), path.join(target, '.tas'));
89
207
  console.log(' [ok] .tas/ (updated)');
90
208
 
91
- // Remove files deleted from the kit in previous versions
92
209
  const deletedFiles = await getDeletedFiles();
93
210
  if (deletedFiles.length > 0) {
94
211
  const { removed } = await removeDeletedFiles(target, deletedFiles);
@@ -99,12 +216,20 @@ export async function update({ directory, yes }) {
99
216
  }
100
217
  }
101
218
 
102
- // Set executable bit on tas-ado.py (Unix/macOS)
103
219
  if (process.platform !== 'win32') {
104
220
  const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
105
221
  if (await exists(adoPy)) {
106
222
  await fs.chmod(adoPy, 0o755);
107
223
  }
224
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
225
+ if (await exists(preCommit)) {
226
+ await fs.chmod(preCommit, 0o755);
227
+ }
228
+ }
229
+
230
+ // Re-wire hook only if user explicitly asked via flag
231
+ if (securityHook) {
232
+ await installSecurityHook({ target, mode: securityHook });
108
233
  }
109
234
 
110
235
  console.log(` [--] CLAUDE.md, tas.yaml, .env.example — not touched`);
@@ -117,15 +242,15 @@ and manually merge changes into your CLAUDE.md and tas.yaml if needed.
117
242
  `);
118
243
  }
119
244
 
120
- export async function install({ directory, yes }) {
245
+ // ─── Install ─────────────────────────────────────────────────────────────────
246
+
247
+ export async function install({ directory, yes, securityHook }) {
121
248
  const target = path.resolve(directory);
122
249
 
123
- // Ensure target directory exists
124
250
  await fs.mkdir(target, { recursive: true });
125
251
 
126
252
  console.log(`\nInstalling TAS Kit into: ${target}\n`);
127
253
 
128
- // Warn if .claude/ or .tas/ already exist
129
254
  const claudeExists = await exists(path.join(target, '.claude'));
130
255
  const tasExists = await exists(path.join(target, '.tas'));
131
256
 
@@ -141,64 +266,51 @@ export async function install({ directory, yes }) {
141
266
  console.log();
142
267
  }
143
268
 
144
- // Copy .claude/
145
- await copyDir(
146
- path.join(PACKAGE_DIR, '.claude'),
147
- path.join(target, '.claude')
148
- );
269
+ await copyDir(path.join(PACKAGE_DIR, '.claude'), path.join(target, '.claude'));
149
270
  console.log(' [ok] .claude/');
150
271
 
151
- // Copy .tas/
152
- await copyDir(
153
- path.join(PACKAGE_DIR, '.tas'),
154
- path.join(target, '.tas')
155
- );
272
+ await copyDir(path.join(PACKAGE_DIR, '.tas'), path.join(target, '.tas'));
156
273
  console.log(' [ok] .tas/');
157
274
 
158
- // Copy CLAUDE-Example.md as CLAUDE.md (only if absent)
159
275
  const claudeMdTarget = path.join(target, 'CLAUDE.md');
160
276
  if (!(await exists(claudeMdTarget))) {
161
- await fs.copyFile(
162
- path.join(PACKAGE_DIR, 'CLAUDE-Example.md'),
163
- claudeMdTarget
164
- );
277
+ await fs.copyFile(path.join(PACKAGE_DIR, 'CLAUDE-Example.md'), claudeMdTarget);
165
278
  console.log(' [ok] CLAUDE.md (from CLAUDE-Example.md)');
166
279
  } else {
167
280
  console.log(' [--] CLAUDE.md already exists, skipped');
168
281
  }
169
282
 
170
- // Copy .env.example (only if absent)
171
283
  const envExampleTarget = path.join(target, '.env.example');
172
284
  if (!(await exists(envExampleTarget))) {
173
- await fs.copyFile(
174
- path.join(PACKAGE_DIR, '.env.example'),
175
- envExampleTarget
176
- );
285
+ await fs.copyFile(path.join(PACKAGE_DIR, '.env.example'), envExampleTarget);
177
286
  console.log(' [ok] .env.example');
178
287
  } else {
179
288
  console.log(' [--] .env.example already exists, skipped');
180
289
  }
181
290
 
182
- // Copy tas-example.yaml as tas.yaml (only if absent)
183
291
  const tasYamlTarget = path.join(target, 'tas.yaml');
184
292
  if (!(await exists(tasYamlTarget))) {
185
- await fs.copyFile(
186
- path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'),
187
- tasYamlTarget
188
- );
293
+ await fs.copyFile(path.join(PACKAGE_DIR, '.tas', 'tas-example.yaml'), tasYamlTarget);
189
294
  console.log(' [ok] tas.yaml (from .tas/tas-example.yaml)');
190
295
  } else {
191
296
  console.log(' [--] tas.yaml already exists, skipped');
192
297
  }
193
298
 
194
- // Set executable bit on tas-ado.py (Unix/macOS)
195
299
  if (process.platform !== 'win32') {
196
300
  const adoPy = path.join(target, '.tas', 'tools', 'tas-ado.py');
197
301
  if (await exists(adoPy)) {
198
302
  await fs.chmod(adoPy, 0o755);
199
303
  }
304
+ const preCommit = path.join(target, '.tas', 'hooks', 'pre-commit');
305
+ if (await exists(preCommit)) {
306
+ await fs.chmod(preCommit, 0o755);
307
+ }
200
308
  }
201
309
 
310
+ // ─── Wire security pre-commit hook ─────────────────────────────────────────
311
+ const mode = await chooseSecurityHookMode({ target, yes, forced: securityHook });
312
+ await installSecurityHook({ target, mode });
313
+
202
314
  console.log(`
203
315
  TAS Kit installed successfully!
204
316
 
@@ -208,6 +320,8 @@ Next steps:
208
320
  3. Create .env — add AZURE_DEVOPS_PAT (see .env.example)
209
321
  4. Open Claude Code — run /tas-init to initialize your project
210
322
 
211
- Docs: .tas/README.md
323
+ Docs:
324
+ .tas/README.md — kit overview
325
+ .tas/hooks/README.md — pre-commit security hook details
212
326
  `);
213
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torus-engineering/tas-kit",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Torus Agentic SDLC Kit — Collection of commands, skills, rules, hooks, agents and workflows for modern AI-First SDLC",
5
5
  "type": "module",
6
6
  "bin": {