@maccesar/titools
Advanced tools
+83
-96
@@ -17,3 +17,3 @@ /** | ||
| } from '../platform.js'; | ||
| import { cleanupLegacyArtifacts } from '../cleanup.js'; | ||
| import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js'; | ||
| import { | ||
@@ -26,4 +26,4 @@ installSkills, | ||
| import { | ||
| downloadRepoArchive, | ||
| checkForUpdate, | ||
| fetchLatestVersion, | ||
| } from '../downloader.js'; | ||
@@ -33,8 +33,17 @@ import { createSkillSymlinks } from '../symlink.js'; | ||
| import { getAgentsSkillsDir } from '../config.js'; | ||
| import { mkdtemp } from 'fs/promises'; | ||
| import { existsSync } from 'fs'; | ||
| import { join, resolve } from 'path'; | ||
| import { tmpdir } from 'os'; | ||
| /** | ||
| * Check if a platform has any skill symlinks installed | ||
| * @param {string} platformSkillsDir - Platform skills directory | ||
| * @returns {boolean} True if any skill symlink exists | ||
| */ | ||
| function hasAnySkillSymlink(platformSkillsDir) { | ||
| if (!platformSkillsDir || !existsSync(platformSkillsDir)) return false; | ||
| const skillList = getSkillList(); | ||
| return skillList.some((skill) => existsSync(join(platformSkillsDir, skill))); | ||
| } | ||
| /** | ||
| * Update command handler | ||
@@ -118,118 +127,96 @@ * @param {Object} options - Command options | ||
| if (!hasUpdate) { | ||
| spinner.info(`Already up to date (v${PACKAGE_VERSION})`); | ||
| cleanupLegacyArtifacts(baseDir); | ||
| console.log(''); | ||
| console.log(chalk.green('✓'), 'Skills and agents are already at the latest version'); | ||
| const projectDir = resolve(process.cwd()); | ||
| if (isTitaniumProject(projectDir)) { | ||
| const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']; | ||
| const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file))); | ||
| if (hasAnyAiFile) { | ||
| await agentsCommand(projectDir, { onlyExisting: true, force: true }); | ||
| } | ||
| // If there's a newer version on GitHub, prompt user to update CLI first | ||
| if (hasUpdate) { | ||
| let latestVersion = '(newer)'; | ||
| try { | ||
| latestVersion = await fetchLatestVersion(); | ||
| } catch { | ||
| // Ignore error, we already know there's an update | ||
| } | ||
| console.log(''); | ||
| return; | ||
| } | ||
| spinner.succeed(`Update available!`); | ||
| console.log(''); | ||
| console.log(chalk.gray(`Current: ${PACKAGE_VERSION}`)); | ||
| console.log(chalk.gray(`Latest: (from GitHub)`)); | ||
| console.log(''); | ||
| // Detect installed platforms at target | ||
| const detectedPlatforms = detectPlatforms(baseDir); | ||
| if (detectedPlatforms.length === 0) { | ||
| console.log(chalk.yellow('No AI coding assistants detected.')); | ||
| if (baseDir) { | ||
| console.log('Update will install skills to ./.agents/skills/'); | ||
| } else { | ||
| console.log('Update will install skills to ~/.agents/skills/'); | ||
| } | ||
| spinner.warn('New version available'); | ||
| console.log(''); | ||
| } else { | ||
| for (const platform of detectedPlatforms) { | ||
| console.log(chalk.green('✓'), platform.displayName); | ||
| } | ||
| console.log(chalk.yellow('A newer version is available on npm:')); | ||
| console.log(` Current: ${chalk.gray('v' + PACKAGE_VERSION)}`); | ||
| console.log(` Latest: ${chalk.green(latestVersion)}`); | ||
| console.log(''); | ||
| console.log('To update, run:'); | ||
| console.log(` ${chalk.cyan('npm update -g @maccesar/titools')}`); | ||
| console.log(''); | ||
| console.log('Then run this command again:'); | ||
| console.log(` ${chalk.cyan('titools update')}`); | ||
| console.log(''); | ||
| return; | ||
| } | ||
| // Download latest from GitHub | ||
| spinner.start('Downloading latest from GitHub...'); | ||
| // CLI is up to date, now sync skills from the installed package | ||
| spinner.succeed(`CLI is up to date (v${PACKAGE_VERSION})`); | ||
| let repoDir = getLocalRepoDir(); | ||
| let tempDir = null; | ||
| // Get repository directory from the installed package | ||
| const repoDir = getLocalRepoDir(); | ||
| if (!repoDir) { | ||
| tempDir = await mkdtemp(join(tmpdir(), 'titanium-skills-')); | ||
| repoDir = await downloadRepoArchive(tempDir); | ||
| console.log(''); | ||
| console.log(chalk.red('Error: Could not locate skills source directory.')); | ||
| console.log('Try reinstalling with:'); | ||
| console.log(` ${chalk.cyan('npm install -g titools')}`); | ||
| return; | ||
| } | ||
| spinner.succeed('Downloaded from GitHub'); | ||
| // Detect platforms with existing symlinks (only update those) | ||
| const detectedPlatforms = detectPlatforms(baseDir); | ||
| const platformsWithSymlinks = detectedPlatforms.filter((p) => | ||
| hasAnySkillSymlink(p.skillsDir) | ||
| ); | ||
| try { | ||
| // Install skills | ||
| spinner.start('Updating skills...'); | ||
| const skillsResult = await installSkills(repoDir, baseDir); | ||
| spinner.succeed( | ||
| `Skills: ${formatList(skillsResult.installed)}` | ||
| ); | ||
| // Install skills | ||
| spinner.start('Syncing skills...'); | ||
| const skillsResult = await installSkills(repoDir, baseDir); | ||
| spinner.succeed(`Skills: ${formatList(skillsResult.installed)}`); | ||
| // Install agents | ||
| spinner.start('Updating agents...'); | ||
| // Install agents (only if Claude Code has symlinks) | ||
| const claudePlatform = platformsWithSymlinks.find((p) => p.name === 'claude'); | ||
| if (claudePlatform) { | ||
| spinner.start('Syncing agents...'); | ||
| const agentsResult = await installAgents(repoDir, baseDir); | ||
| if (agentsResult.installed.length > 0) { | ||
| spinner.succeed( | ||
| `Agents: ${formatList(agentsResult.installed)}` | ||
| ); | ||
| spinner.succeed(`Agents: ${formatList(agentsResult.installed)}`); | ||
| } else { | ||
| spinner.info('No agents to update'); | ||
| spinner.info('No agents to sync'); | ||
| } | ||
| } | ||
| cleanupLegacyArtifacts(baseDir); | ||
| cleanupLegacyArtifacts(baseDir); | ||
| // Update symlinks for detected platforms | ||
| for (const platform of detectedPlatforms) { | ||
| spinner.start(`Updating ${platform.displayName} symlinks...`); | ||
| const symlinkResult = await createSkillSymlinks( | ||
| platform.skillsDir, | ||
| SKILLS, | ||
| baseDir | ||
| // Update symlinks only for platforms that already had them | ||
| for (const platform of platformsWithSymlinks) { | ||
| spinner.start(`Updating ${platform.displayName} symlinks...`); | ||
| const symlinkResult = await createSkillSymlinks( | ||
| platform.skillsDir, | ||
| SKILLS, | ||
| baseDir | ||
| ); | ||
| if (symlinkResult.linked.length === SKILLS.length) { | ||
| spinner.succeed(`${platform.displayName} linked`); | ||
| } else { | ||
| spinner.warn( | ||
| `${platform.displayName}: ${symlinkResult.linked.length}/${SKILLS.length} linked` | ||
| ); | ||
| if (symlinkResult.linked.length === SKILLS.length) { | ||
| spinner.succeed(`${platform.displayName} linked`); | ||
| } else { | ||
| spinner.warn( | ||
| `${platform.displayName}: ${symlinkResult.linked.length}/${SKILLS.length} linked` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| // Summary | ||
| console.log(''); | ||
| console.log(chalk.green('✓ Update complete!')); | ||
| console.log(''); | ||
| const projectDir = resolve(process.cwd()); | ||
| if (isTitaniumProject(projectDir)) { | ||
| const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']; | ||
| const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file))); | ||
| if (hasAnyAiFile) { | ||
| await agentsCommand(projectDir, { onlyExisting: true, force: true }); | ||
| } else { | ||
| console.log(chalk.bold('▸'), 'Run in the Titanium project:', chalk.cyan('titools sync')); | ||
| } | ||
| } else { | ||
| console.log(chalk.bold('▸'), 'Run in the Titanium project:', chalk.cyan('titools sync')); | ||
| } | ||
| console.log(''); | ||
| // Summary | ||
| console.log(''); | ||
| console.log(chalk.green('✓ Update complete!')); | ||
| } finally { | ||
| // Clean up temp directory | ||
| if (tempDir) { | ||
| await import('fs-extra').then(({ remove }) => remove(tempDir)); | ||
| // Update knowledge index in MD files if in a Titanium project | ||
| const projectDir = resolve(process.cwd()); | ||
| if (isTitaniumProject(projectDir)) { | ||
| const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']; | ||
| const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file))); | ||
| if (hasAnyAiFile) { | ||
| console.log(''); | ||
| await agentsCommand(projectDir, { onlyExisting: true, force: true }); | ||
| } | ||
| } | ||
| console.log(''); | ||
@@ -236,0 +223,0 @@ } catch (error) { |
+1
-1
| { | ||
| "name": "@maccesar/titools", | ||
| "version": "2.2.3", | ||
| "version": "2.2.4", | ||
| "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
+7
-6
@@ -622,9 +622,10 @@ # TiTools - Titanium CLI for AI coding assistants | ||
| What it does: | ||
| - Checks GitHub for the latest version | ||
| - Downloads and installs updated skills and agents | ||
| - Updates platform symlinks for all detected platforms | ||
| - Cleans up legacy artifacts (`alloy-expert` skill, `ti-researcher` agent) | ||
| - Auto-syncs knowledge index files if they exist in the current project | ||
| 1. Checks GitHub for the latest CLI version | ||
| 2. If a newer version exists → prompts you to update the CLI first with `npm update -g @maccesar/titools` | ||
| 3. If CLI is current → syncs skills and agents from the installed package (no download needed) | ||
| 4. Updates platform symlinks only for platforms that already have them | ||
| 5. Cleans up legacy artifacts (`alloy-expert` skill, `ti-researcher` agent) | ||
| 6. Auto-syncs knowledge index files if they exist in the current project | ||
| Note: This updates knowledge packages and agents, not the CLI binary itself. To update the CLI, use `npm update -g @maccesar/titools`. | ||
| Note: This command syncs knowledge packages and agents from your installed CLI. To get new features, first update the CLI with `npm update -g @maccesar/titools`, then run `titools update`. | ||
@@ -631,0 +632,0 @@ ### titools remove |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1470329
0.01%826
0.12%23
-4.17%2518
-0.43%