td-ai-tools 1.1.11 → 1.2.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/README.md CHANGED
@@ -36,9 +36,11 @@ npx td-ai-tools list
36
36
  npx td-ai-tools install
37
37
  npx td-ai-tools install --all
38
38
  npx td-ai-tools install pr-solver
39
+ npx td-ai-tools install --setup playwright-cli
39
40
  npx td-ai-tools install pr-solver horizon-component-library
40
41
  npx td-ai-tools update
41
42
  npx td-ai-tools update --all
43
+ npx td-ai-tools update --setup playwright-cli
42
44
  npx td-ai-tools update pr-solver
43
45
  npx td-ai-tools delete
44
46
  npx td-ai-tools delete --all
@@ -53,6 +55,8 @@ If a skill bundles a sub-agent prompt (any `.md` with a `name:` field in `skills
53
55
 
54
56
  This keeps the installed assets available to both Claude-style and `.agents`-style project conventions.
55
57
 
58
+ Some skills provide a recognized setup command (`setup.sh`, `scripts/setup.sh`, or `package.json` with `scripts.setup`). Non-interactive installs and updates run setup only when you pass `--setup`; interactive installs/updates ask for confirmation only when the selected skills include a recognized setup command. Accepted setup runs once inside each installed copy: `.claude/skills/<name>/` and `.agents/skills/<name>/`.
59
+
56
60
  `install` now errors when the target item already exists. Use `update` to replace an existing installed skill or agent pack.
57
61
 
58
62
  `delete` removes installed items from both `.claude/` and `.agents/` target directories, and works on any installed skill or agent pack regardless of whether it is in the catalogue.
package/bin/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from '../lib/catalog.js';
13
13
  import { readFrontmatterField } from '../lib/frontmatter.js';
14
14
  import {
15
- installSkill, installAgent, deleteSkill, deleteAgent,
15
+ installSkill, installAgent, deleteSkill, deleteAgent, skillHasSetup,
16
16
  } from '../lib/installer.js';
17
17
 
18
18
  const __filename = fileURLToPath(import.meta.url);
@@ -139,13 +139,18 @@ function hintFor(type, name) {
139
139
  *
140
140
  * @param {MenuItem[]} items - Items to install.
141
141
  * @param {{replaceExisting?: boolean}} [options] - Forwarded to the installer.
142
- * @returns {void}
142
+ * @returns {boolean} Whether every installer reported success.
143
143
  */
144
144
  function installItems(items, options = {}) {
145
+ const { runSetup = false, ...installOptions } = options;
146
+ let ok = true;
145
147
  for (const item of items) {
146
- if (item.type === 'skill') installSkill(ctx, item.name, { ...options, report: status });
147
- else installAgent(ctx, item.name, { ...options, report: status });
148
+ const installed = item.type === 'skill'
149
+ ? installSkill(ctx, item.name, { ...installOptions, runSetup, report: status })
150
+ : installAgent(ctx, item.name, { ...installOptions, report: status });
151
+ if (!installed) ok = false;
148
152
  }
153
+ return ok;
149
154
  }
150
155
 
151
156
  /**
@@ -354,6 +359,52 @@ function fromGroupValues(values) {
354
359
  });
355
360
  }
356
361
 
362
+
363
+ /**
364
+ * Whether any selected skill has a recognized setup script.
365
+ *
366
+ * @param {MenuItem[]} items - Items selected for install/update.
367
+ * @returns {boolean}
368
+ */
369
+ function hasSetupSkills(items) {
370
+ return items.some(item => item.type === 'skill' && skillHasSetup(ctx, item.name));
371
+ }
372
+
373
+ /**
374
+ * Prompts before running setup for selected skills that provide a recognized setup script.
375
+ *
376
+ * @param {MenuItem[]} items - Items selected for install/update.
377
+ * @returns {Promise<boolean>} Whether setup should run.
378
+ */
379
+ async function confirmSetup(items) {
380
+ if (!hasSetupSkills(items)) return false;
381
+ const answer = await p.confirm({
382
+ message: 'Run setup scripts for selected skills that provide one? Setup runs in both .claude and .agents copies.',
383
+ initialValue: false,
384
+ });
385
+ if (p.isCancel(answer)) {
386
+ p.cancel('Cancelled.');
387
+ process.exit(0);
388
+ }
389
+ return Boolean(answer);
390
+ }
391
+
392
+ /**
393
+ * Removes recognized global install flags from positional arguments.
394
+ *
395
+ * @param {string[]} args - Raw command arguments after the command name.
396
+ * @returns {{rest: string[], runSetup: boolean}}
397
+ */
398
+ function parseInstallFlags(args) {
399
+ let runSetup = false;
400
+ const rest = [];
401
+ for (const arg of args) {
402
+ if (arg === '--setup') runSetup = true;
403
+ else rest.push(arg);
404
+ }
405
+ return { rest, runSetup };
406
+ }
407
+
357
408
  /**
358
409
  * Runs the interactive install/update flow (multiselect → apply). Exits the
359
410
  * process on a non-TTY stdin or user cancellation.
@@ -395,7 +446,13 @@ async function interactiveInstall(mode = 'install') {
395
446
  return;
396
447
  }
397
448
 
398
- installItems(items, { replaceExisting: mode === 'update' });
449
+ const runSetup = await confirmSetup(items);
450
+ const ok = installItems(items, { replaceExisting: mode === 'update', runSetup });
451
+ if (!ok) {
452
+ p.outro(pc.red(`${mode === 'update' ? 'Update' : 'Install'} completed with errors.`));
453
+ process.exitCode = 1;
454
+ return;
455
+ }
399
456
  p.outro(mode === 'update' ? pc.green('Update complete.') : pc.green('Install complete.'));
400
457
  }
401
458
 
@@ -527,9 +584,13 @@ const HELP_TEXT = `Usage:
527
584
  npx td-ai-tools list List available skills and agent packs
528
585
  npx td-ai-tools install Interactive install
529
586
  npx td-ai-tools install --all Install everything
587
+ npx td-ai-tools install --setup <name...>
588
+ Install and run recognized skill setup scripts
530
589
  npx td-ai-tools install <name...> Install specific skills or agent packs
531
590
  npx td-ai-tools update Interactive update (installed items in the catalog)
532
591
  npx td-ai-tools update --all Update all installed items found in the catalog
592
+ npx td-ai-tools update --setup <name...>
593
+ Update and run recognized skill setup scripts
533
594
  npx td-ai-tools update <name...> Update specific installed skills or agent packs
534
595
  npx td-ai-tools delete Interactive delete
535
596
  npx td-ai-tools delete --all Delete everything
@@ -581,17 +642,17 @@ async function main() {
581
642
  }
582
643
 
583
644
  if (cmd === 'install') {
584
- const rest = args.slice(1);
645
+ const { rest, runSetup } = parseInstallFlags(args.slice(1));
585
646
  if (rest[0] === '--all') {
586
- installItems(buildMenu());
647
+ if (!installItems(buildMenu(), { runSetup })) process.exitCode = 1;
587
648
  } else {
588
- installItems(resolveNames(rest));
649
+ if (!installItems(resolveNames(rest), { runSetup })) process.exitCode = 1;
589
650
  }
590
651
  return;
591
652
  }
592
653
 
593
654
  if (cmd === 'update') {
594
- const rest = args.slice(1);
655
+ const { rest, runSetup } = parseInstallFlags(args.slice(1));
595
656
  if (rest.length === 0) {
596
657
  await interactiveInstall('update');
597
658
  return;
@@ -602,9 +663,9 @@ async function main() {
602
663
  status('info', 'No installed skills or agent packs match the catalog.');
603
664
  return;
604
665
  }
605
- installItems(menu, { replaceExisting: true });
666
+ if (!installItems(menu, { replaceExisting: true, runSetup })) process.exitCode = 1;
606
667
  } else {
607
- installItems(resolveUpdateNames(rest), { replaceExisting: true });
668
+ if (!installItems(resolveUpdateNames(rest), { replaceExisting: true, runSetup })) process.exitCode = 1;
608
669
  }
609
670
  return;
610
671
  }
package/lib/installer.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
9
10
  import { copyDir } from './fs-utils.js';
10
11
  import { bundledAgentsIn } from './catalog.js';
11
12
 
@@ -24,6 +25,82 @@ import { bundledAgentsIn } from './catalog.js';
24
25
  /** @type {ReportFn} */
25
26
  const noop = () => {};
26
27
 
28
+ /**
29
+ * Recognized setup commands, in priority order, relative to an installed skill.
30
+ * @type {{file: string, command: string, args: string[]}[]}
31
+ */
32
+ const SKILL_SETUP_CANDIDATES = [
33
+ { file: 'setup.sh', command: 'bash', args: ['setup.sh'] },
34
+ { file: path.join('scripts', 'setup.sh'), command: 'bash', args: [path.join('scripts', 'setup.sh')] },
35
+ { file: 'package.json', command: 'npm', args: ['run', 'setup'] },
36
+ ];
37
+
38
+ /**
39
+ * Finds the setup command for a skill directory, if one uses a recognized convention.
40
+ *
41
+ * @param {string} skillDir - Source or installed skill directory.
42
+ * @returns {({file: string, command: string, args: string[]}|null)} The setup command, or `null`.
43
+ */
44
+ export function findSkillSetup(skillDir) {
45
+ for (const candidate of SKILL_SETUP_CANDIDATES) {
46
+ const filePath = path.join(skillDir, candidate.file);
47
+ if (!fs.existsSync(filePath)) continue;
48
+ if (candidate.file === 'package.json') {
49
+ try {
50
+ const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
51
+ if (!pkg?.scripts?.setup) continue;
52
+ } catch {
53
+ continue;
54
+ }
55
+ }
56
+ return candidate;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Whether a catalog skill provides a setup command using a recognized convention.
63
+ *
64
+ * @param {Ctx} ctx
65
+ * @param {string} name - Skill name.
66
+ * @returns {boolean}
67
+ */
68
+ export function skillHasSetup(ctx, name) {
69
+ return Boolean(findSkillSetup(path.join(ctx.skillsDir, name)));
70
+ }
71
+
72
+ /**
73
+ * Runs a skill setup command from the installed skill directory.
74
+ *
75
+ * @param {string} name - Skill name.
76
+ * @param {Target} target - The destination layout.
77
+ * @param {string} dest - Installed skill directory.
78
+ * @param {ReportFn} report
79
+ * @returns {boolean} Whether setup succeeded or no setup existed.
80
+ */
81
+ function runSkillSetup(name, target, dest, report) {
82
+ const setup = findSkillSetup(dest);
83
+ if (!setup) return true;
84
+
85
+ report('step', `setup: ${name} running ${setup.command} ${setup.args.join(' ')} in ${target.root}/skills/${name}/`);
86
+ const result = spawnSync(setup.command, setup.args, {
87
+ cwd: dest,
88
+ stdio: 'inherit',
89
+ shell: false,
90
+ });
91
+ if (result.error) {
92
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/: ${result.error.message}`);
93
+ return false;
94
+ }
95
+ if (result.status !== 0) {
96
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/ with exit code ${result.status}`);
97
+ return false;
98
+ }
99
+ report('success', `setup: ${name} completed in ${target.root}/skills/${name}/`);
100
+ return true;
101
+ }
102
+
103
+
27
104
  /**
28
105
  * Copies an item's bundled sub-agents into a target's `agents/` directory.
29
106
  *
@@ -52,10 +129,12 @@ function registerBundledAgents(ctx, target, srcDir, report) {
52
129
  * @param {string} name - Skill name.
53
130
  * @param {object} [options]
54
131
  * @param {boolean} [options.replaceExisting=false] - Overwrite an existing install (used by `update`).
132
+ * @param {boolean} [options.runSetup=false] - Run recognized setup scripts after each target copy.
55
133
  * @param {ReportFn} [options.report] - Progress sink.
56
- * @returns {boolean} `false` if the skill is not in the catalog, otherwise `true`.
134
+ * @returns {boolean} `false` if the skill is not in the catalog or setup fails, otherwise `true`.
57
135
  */
58
- export function installSkill(ctx, name, { replaceExisting = false, report = noop } = {}) {
136
+ export function installSkill(ctx, name, { replaceExisting = false, runSetup = false, report = noop } = {}) {
137
+ let ok = true;
59
138
  const src = path.join(ctx.skillsDir, name);
60
139
  if (!fs.existsSync(src)) {
61
140
  report('error', `Skill "${name}" not found.`);
@@ -71,8 +150,9 @@ export function installSkill(ctx, name, { replaceExisting = false, report = noop
71
150
  copyDir(src, dest);
72
151
  report('success', `skill: ${name} ${replaceExisting ? 'updated' : 'installed'} → ${target.root}/skills/${name}/`);
73
152
  registerBundledAgents(ctx, target, src, report);
153
+ if (runSetup && !runSkillSetup(name, target, dest, report)) ok = false;
74
154
  }
75
- return true;
155
+ return ok;
76
156
  }
77
157
 
78
158
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.1.11",
3
+ "version": "1.2.0",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {