@maccesar/titools 2.4.1 → 2.5.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/bin/titools.js CHANGED
@@ -12,6 +12,8 @@ import { agentsCommand } from '../lib/commands/agents.js';
12
12
  import { updateCommand } from '../lib/commands/update.js';
13
13
  import { uninstallCommand } from '../lib/commands/uninstall.js';
14
14
  import { autoUpdateCommand } from '../lib/commands/auto-update.js';
15
+ import { statusCommand } from '../lib/commands/status.js';
16
+ import { doctorCommand } from '../lib/commands/doctor.js';
15
17
 
16
18
  const program = new Command();
17
19
 
@@ -59,6 +61,18 @@ program
59
61
  .option('-s, --silent', 'Suppress all output except errors')
60
62
  .action(autoUpdateCommand);
61
63
 
64
+ // Status command
65
+ program
66
+ .command('status')
67
+ .description('Show installation status and linked platforms')
68
+ .action(statusCommand);
69
+
70
+ // Doctor command
71
+ program
72
+ .command('doctor')
73
+ .description('Check installation health and diagnose issues')
74
+ .action(doctorCommand);
75
+
62
76
  // Parse arguments
63
77
  program.parse();
64
78
 
@@ -6,7 +6,10 @@
6
6
 
7
7
  import chalk from 'chalk';
8
8
  import ora from 'ora';
9
- import { execFileSync } from 'child_process';
9
+ import { execFile as execFileCb } from 'child_process';
10
+ import { promisify } from 'util';
11
+
12
+ const execFile = promisify(execFileCb);
10
13
  import { existsSync } from 'fs';
11
14
  import { join, resolve } from 'path';
12
15
  import {
@@ -51,6 +54,7 @@ export async function autoUpdateCommand(options) {
51
54
 
52
55
  // Step 1: Check cache
53
56
  if (!shouldCheck(cacheDir)) {
57
+ log(chalk.green('✔'), `Up to date (v${PACKAGE_VERSION})`);
54
58
  return;
55
59
  }
56
60
 
@@ -81,16 +85,17 @@ export async function autoUpdateCommand(options) {
81
85
  }
82
86
 
83
87
  // Step 5: Update CLI (skip in dev mode)
88
+ spinner.succeed(`Update available: v${PACKAGE_VERSION} → ${latestVersion}`);
89
+
84
90
  if (isDevMode()) {
85
- spinner.info(`Dev mode — skipping npm update (latest: ${latestVersion})`);
91
+ spinner.info(`Dev mode — skipping npm update`);
86
92
  } else {
87
- spinner.start(`Updating titools v${PACKAGE_VERSION} → ${latestVersion}...`);
93
+ spinner.start(`Downloading and installing v${latestVersion}...`);
88
94
  try {
89
- execFileSync('npm', ['update', '-g', '@maccesar/titools'], {
90
- stdio: 'pipe',
95
+ await execFile('npm', ['update', '-g', '@maccesar/titools'], {
91
96
  timeout: 60000,
92
97
  });
93
- spinner.succeed(`Updated to ${latestVersion}`);
98
+ spinner.succeed(`Updated to v${latestVersion}`);
94
99
  } catch (error) {
95
100
  spinner.fail(`npm update failed: ${error.message}`);
96
101
  return;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Doctor command
3
+ * Diagnoses installation health (read-only)
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { existsSync, lstatSync, readFileSync, realpathSync } from 'fs';
8
+ import { join } from 'path';
9
+ import os from 'os';
10
+ import {
11
+ SKILLS,
12
+ PACKAGE_VERSION,
13
+ TITANIUM_KNOWLEDGE_VERSION,
14
+ getAgentsSkillsDir,
15
+ getClaudeAgentsDir,
16
+ getConfigDir,
17
+ getPlatforms,
18
+ } from '../config.js';
19
+ import { hasHook } from '../hooks.js';
20
+ import { readLastCheck } from '../cache.js';
21
+ import { isTitaniumProject, blockExists } from '../utils.js';
22
+
23
+ const CHECK = chalk.green('✓');
24
+ const CROSS = chalk.red('✗');
25
+ const WARN = chalk.yellow('⚠');
26
+
27
+ function extractIndexVersion(filePath) {
28
+ if (!existsSync(filePath)) return null;
29
+ try {
30
+ const content = readFileSync(filePath, 'utf8');
31
+ const match = content.match(/<!-- Version: (v[\d.]+) -->/);
32
+ return match ? match[1] : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export async function doctorCommand() {
39
+ const homeDir = os.homedir();
40
+ const skillsDir = getAgentsSkillsDir(homeDir);
41
+ const agentsDir = getClaudeAgentsDir(homeDir);
42
+ const claudeDir = join(homeDir, '.claude');
43
+ const cacheDir = getConfigDir();
44
+ const cwd = process.cwd();
45
+
46
+ let issues = 0;
47
+ const symlinkIssues = [];
48
+
49
+ console.log('');
50
+ console.log(chalk.bold('Titanium SDK Skills Doctor'));
51
+ console.log('');
52
+ console.log(' Checking installation health...');
53
+ console.log('');
54
+
55
+ // CLI version
56
+ console.log(` ${CHECK} CLI version: v${PACKAGE_VERSION}`);
57
+
58
+ // Skills check
59
+ const missingSkills = [];
60
+ for (const skill of SKILLS) {
61
+ if (!existsSync(join(skillsDir, skill))) {
62
+ missingSkills.push(skill);
63
+ }
64
+ }
65
+ const installedCount = SKILLS.length - missingSkills.length;
66
+ if (missingSkills.length === 0) {
67
+ console.log(` ${CHECK} Skills: ${installedCount}/${SKILLS.length} installed in ~/.agents/skills/`);
68
+ } else {
69
+ console.log(` ${CROSS} Skills: ${installedCount}/${SKILLS.length} installed in ~/.agents/skills/ (missing: ${missingSkills.join(', ')})`);
70
+ issues += missingSkills.length;
71
+ }
72
+
73
+ // Agent check
74
+ const agentPath = join(agentsDir, 'ti-pro.md');
75
+ if (existsSync(agentPath)) {
76
+ console.log(` ${CHECK} Agent: ti-pro installed`);
77
+ } else {
78
+ console.log(` ${CROSS} Agent: ti-pro not found`);
79
+ issues++;
80
+ }
81
+
82
+ // Hook check
83
+ if (hasHook(claudeDir)) {
84
+ console.log(` ${CHECK} Hook: SessionStart configured`);
85
+ } else {
86
+ console.log(` ${CROSS} Hook: SessionStart not configured`);
87
+ issues++;
88
+ }
89
+
90
+ // Cache check
91
+ const lastCheck = readLastCheck(cacheDir);
92
+ if (lastCheck) {
93
+ const hoursAgo = Math.round((Date.now() - lastCheck.lastCheck) / (1000 * 60 * 60));
94
+ const timeLabel = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hour${hoursAgo === 1 ? '' : 's'} ago`;
95
+ console.log(` ${CHECK} Cache: last check ${timeLabel}`);
96
+ } else {
97
+ console.log(` ${WARN} Cache: no check recorded`);
98
+ }
99
+
100
+ // Platforms
101
+ console.log('');
102
+ console.log(' Platforms:');
103
+ const platforms = getPlatforms(homeDir);
104
+ for (const platform of platforms) {
105
+ const missing = [];
106
+ const broken = [];
107
+
108
+ for (const skill of SKILLS) {
109
+ const linkPath = join(platform.skillsDir, skill);
110
+ try {
111
+ const stat = lstatSync(linkPath);
112
+ if (stat.isSymbolicLink()) {
113
+ // Check if target exists
114
+ if (!existsSync(linkPath)) {
115
+ broken.push(skill);
116
+ }
117
+ }
118
+ // exists (symlink or directory), count as linked
119
+ } catch {
120
+ missing.push(skill);
121
+ }
122
+ }
123
+
124
+ const linkedCount = SKILLS.length - missing.length - broken.length;
125
+
126
+ if (missing.length === 0 && broken.length === 0) {
127
+ console.log(` ${CHECK} ${platform.displayName}: ${SKILLS.length}/${SKILLS.length} skills linked`);
128
+ } else {
129
+ const problems = [];
130
+ if (missing.length > 0) problems.push(`missing: ${missing.join(', ')}`);
131
+ if (broken.length > 0) problems.push(`broken: ${broken.join(', ')}`);
132
+ console.log(` ${CROSS} ${platform.displayName}: ${linkedCount}/${SKILLS.length} skills linked (${problems.join('; ')})`);
133
+ issues += missing.length + broken.length;
134
+
135
+ // Collect symlink issues for detailed report
136
+ for (const skill of broken) {
137
+ symlinkIssues.push(`${CROSS} ~/${platform.name === 'claude' ? '.claude' : `.${platform.name}`}/skills/${skill} → broken symlink (target missing)`);
138
+ }
139
+ for (const skill of missing) {
140
+ symlinkIssues.push(`${CROSS} ~/${platform.name === 'claude' ? '.claude' : `.${platform.name}`}/skills/${skill} → not found`);
141
+ }
142
+ }
143
+ }
144
+
145
+ // Symlink issues detail
146
+ if (symlinkIssues.length > 0) {
147
+ console.log('');
148
+ console.log(` ${WARN} Symlink issues:`);
149
+ for (const issue of symlinkIssues) {
150
+ console.log(` ${issue}`);
151
+ }
152
+ }
153
+
154
+ // Project check
155
+ if (isTitaniumProject(cwd)) {
156
+ console.log('');
157
+ console.log(' Project:');
158
+
159
+ const mdFiles = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md'];
160
+ for (const file of mdFiles) {
161
+ const filePath = join(cwd, file);
162
+ if (!existsSync(filePath)) {
163
+ // Not an issue if file doesn't exist — it's optional
164
+ continue;
165
+ }
166
+
167
+ if (blockExists(filePath)) {
168
+ const version = extractIndexVersion(filePath);
169
+ if (version === TITANIUM_KNOWLEDGE_VERSION) {
170
+ console.log(` ${CHECK} ${file}: Knowledge Index present (${version})`);
171
+ } else if (version) {
172
+ console.log(` ${CROSS} ${file}: Knowledge Index outdated (${version})`);
173
+ issues++;
174
+ } else {
175
+ console.log(` ${CHECK} ${file}: Knowledge Index present`);
176
+ }
177
+ } else {
178
+ console.log(` ${CROSS} ${file}: no Knowledge Index`);
179
+ issues++;
180
+ }
181
+ }
182
+ }
183
+
184
+ // Summary
185
+ console.log('');
186
+ if (issues === 0) {
187
+ console.log(chalk.green(` No issues found.`));
188
+ } else {
189
+ console.log(chalk.yellow(` ${issues} issue${issues === 1 ? '' : 's'} found. Run 'titools install' to fix.`));
190
+ }
191
+ console.log('');
192
+ }
193
+
194
+ export default { doctorCommand };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Status command
3
+ * Shows a quick overview of what's installed (read-only)
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { existsSync, readdirSync, lstatSync, readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import os from 'os';
10
+ import {
11
+ SKILLS,
12
+ PACKAGE_VERSION,
13
+ getAgentsSkillsDir,
14
+ getClaudeAgentsDir,
15
+ getConfigDir,
16
+ getPlatforms,
17
+ BLOCK_START,
18
+ } from '../config.js';
19
+ import { hasHook } from '../hooks.js';
20
+ import { readLastCheck } from '../cache.js';
21
+ import { isTitaniumProject, detectTitaniumVersion, blockExists } from '../utils.js';
22
+
23
+ const CHECK = chalk.green('✓');
24
+ const CROSS = chalk.red('✗');
25
+
26
+ function countInstalledSkills(skillsDir) {
27
+ let count = 0;
28
+ for (const skill of SKILLS) {
29
+ if (existsSync(join(skillsDir, skill))) {
30
+ count++;
31
+ }
32
+ }
33
+ return count;
34
+ }
35
+
36
+ function countLinkedSkills(platformSkillsDir) {
37
+ let count = 0;
38
+ for (const skill of SKILLS) {
39
+ const linkPath = join(platformSkillsDir, skill);
40
+ try {
41
+ lstatSync(linkPath);
42
+ count++;
43
+ } catch {
44
+ // not found
45
+ }
46
+ }
47
+ return count;
48
+ }
49
+
50
+ function formatLastCheck(data) {
51
+ if (!data) return chalk.gray('never');
52
+ const date = new Date(data.lastCheck);
53
+ const formatted = date.toISOString().replace('T', ' ').slice(0, 16);
54
+ return `${formatted} (v${data.latestVersion})`;
55
+ }
56
+
57
+ function extractIndexVersion(filePath) {
58
+ if (!existsSync(filePath)) return null;
59
+ try {
60
+ const content = readFileSync(filePath, 'utf8');
61
+ const match = content.match(/<!-- Version: (v[\d.]+) -->/);
62
+ return match ? match[1] : null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export async function statusCommand() {
69
+ const homeDir = os.homedir();
70
+ const skillsDir = getAgentsSkillsDir(homeDir);
71
+ const agentsDir = getClaudeAgentsDir(homeDir);
72
+ const claudeDir = join(homeDir, '.claude');
73
+ const cacheDir = getConfigDir();
74
+ const cwd = process.cwd();
75
+
76
+ // Skills count
77
+ const installedCount = countInstalledSkills(skillsDir);
78
+ const totalCount = SKILLS.length;
79
+
80
+ // Agent check
81
+ const agentExists = existsSync(join(agentsDir, 'ti-pro.md'));
82
+
83
+ // Hook check
84
+ const hookExists = hasHook(claudeDir);
85
+
86
+ // Cache
87
+ const lastCheck = readLastCheck(cacheDir);
88
+
89
+ console.log('');
90
+ console.log(chalk.bold('Titanium SDK Skills Status'));
91
+ console.log('');
92
+ console.log(` Version: v${PACKAGE_VERSION}`);
93
+ console.log(` Skills: ${installedCount}/${totalCount} installed`);
94
+ console.log(` Agent: ti-pro ${agentExists ? CHECK : CROSS}`);
95
+ console.log(` Hook: Claude Code SessionStart ${hookExists ? CHECK : CROSS}`);
96
+ console.log(` Last check: ${formatLastCheck(lastCheck)}`);
97
+
98
+ // Platforms
99
+ console.log('');
100
+ console.log(' Platforms:');
101
+ const platforms = getPlatforms(homeDir);
102
+ for (const platform of platforms) {
103
+ const linked = countLinkedSkills(platform.skillsDir);
104
+ if (linked > 0) {
105
+ console.log(` ${platform.displayName.padEnd(13)} ${CHECK} ${linked} skills linked`);
106
+ } else {
107
+ console.log(` ${platform.displayName.padEnd(13)} ${CROSS} ${chalk.gray('not linked')}`);
108
+ }
109
+ }
110
+
111
+ // Project
112
+ console.log('');
113
+ if (isTitaniumProject(cwd)) {
114
+ const sdkVersion = detectTitaniumVersion(cwd);
115
+ console.log(` Project: ${chalk.cyan('Titanium')} (SDK ${sdkVersion})`);
116
+
117
+ const mdFiles = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md'];
118
+ for (const file of mdFiles) {
119
+ const filePath = join(cwd, file);
120
+ if (!existsSync(filePath)) {
121
+ console.log(` ${file.padEnd(11)} ${CROSS} ${chalk.gray('not found')}`);
122
+ } else if (blockExists(filePath)) {
123
+ const version = extractIndexVersion(filePath);
124
+ console.log(` ${file.padEnd(11)} ${CHECK} Knowledge Index${version ? ` (${version})` : ''}`);
125
+ } else {
126
+ console.log(` ${file.padEnd(11)} ${CROSS} ${chalk.gray('no index')}`);
127
+ }
128
+ }
129
+ } else {
130
+ console.log(` Project: ${chalk.gray('(not a Titanium project)')}`);
131
+ }
132
+
133
+ console.log('');
134
+ }
135
+
136
+ export default { statusCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",