@maccesar/titools 2.2.2 → 2.2.4

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
@@ -619,13 +619,14 @@ Behavior depends on where you run it:
619
619
  | Inside a Titanium project (with existing knowledge index files) | Also refreshes `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` |
620
620
 
621
621
  What it does:
622
- - Checks GitHub for the latest version
623
- - Downloads and installs updated skills and agents
624
- - Updates platform symlinks for all detected platforms
625
- - Cleans up legacy artifacts (`alloy-expert` skill, `ti-researcher` agent)
626
- - Auto-syncs knowledge index files if they exist in the current project
627
-
628
- Note: This updates knowledge packages and agents, not the CLI binary itself. To update the CLI, use `npm update -g @maccesar/titools`.
622
+ 1. Checks GitHub for the latest CLI version
623
+ 2. If a newer version exists prompts you to update the CLI first with `npm update -g @maccesar/titools`
624
+ 3. If CLI is current syncs skills and agents from the installed package (no download needed)
625
+ 4. Updates platform symlinks only for platforms that already have them
626
+ 5. Cleans up legacy artifacts (`alloy-expert` skill, `ti-researcher` agent)
627
+ 6. Auto-syncs knowledge index files if they exist in the current project
628
+
629
+ 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`.
629
630
 
630
631
  ### titools remove
631
632
 
@@ -14,7 +14,7 @@ import select from '../prompts/selectCancel.js';
14
14
  import {
15
15
  detectPlatforms,
16
16
  } from '../platform.js';
17
- import { cleanupLegacyArtifacts } from '../cleanup.js';
17
+ import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js';
18
18
  import {
19
19
  installSkills,
20
20
  installAgents,
@@ -22,16 +22,25 @@ import {
22
22
  } from '../installer.js';
23
23
  import { agentsCommand } from './agents.js';
24
24
  import {
25
- downloadRepoArchive,
26
25
  checkForUpdate,
26
+ fetchLatestVersion,
27
27
  } from '../downloader.js';
28
28
  import { createSkillSymlinks } from '../symlink.js';
29
29
  import { formatList, isTitaniumProject } from '../utils.js';
30
30
  import { getAgentsSkillsDir } from '../config.js';
31
- import { mkdtemp } from 'fs/promises';
32
31
  import { existsSync } from 'fs';
33
32
  import { join, resolve } from 'path';
34
- import { tmpdir } from 'os';
33
+
34
+ /**
35
+ * Check if a platform has any skill symlinks installed
36
+ * @param {string} platformSkillsDir - Platform skills directory
37
+ * @returns {boolean} True if any skill symlink exists
38
+ */
39
+ function hasAnySkillSymlink(platformSkillsDir) {
40
+ if (!platformSkillsDir || !existsSync(platformSkillsDir)) return false;
41
+ const skillList = getSkillList();
42
+ return skillList.some((skill) => existsSync(join(platformSkillsDir, skill)));
43
+ }
35
44
 
36
45
  /**
37
46
  * Update command handler
@@ -112,122 +121,100 @@ export async function updateCommand(options) {
112
121
  try {
113
122
  const hasUpdate = await checkForUpdate(PACKAGE_VERSION);
114
123
 
115
- if (!hasUpdate) {
116
- spinner.info(`Already up to date (v${PACKAGE_VERSION})`);
117
- cleanupLegacyArtifacts(baseDir);
118
- console.log('');
119
- console.log(chalk.green('✓'), 'Skills and agents are already at the latest version');
120
- const projectDir = resolve(process.cwd());
121
- if (isTitaniumProject(projectDir)) {
122
- const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
123
- const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file)));
124
- if (hasAnyAiFile) {
125
- await agentsCommand(projectDir, { onlyExisting: true, force: true });
126
- }
124
+ // If there's a newer version on GitHub, prompt user to update CLI first
125
+ if (hasUpdate) {
126
+ let latestVersion = '(newer)';
127
+ try {
128
+ latestVersion = await fetchLatestVersion();
129
+ } catch {
130
+ // Ignore error, we already know there's an update
127
131
  }
128
- console.log('');
129
- return;
130
- }
131
-
132
- spinner.succeed(`Update available!`);
133
- console.log('');
134
- console.log(chalk.gray(`Current: ${PACKAGE_VERSION}`));
135
- console.log(chalk.gray(`Latest: (from GitHub)`));
136
- console.log('');
137
-
138
- // Detect installed platforms at target
139
- const detectedPlatforms = detectPlatforms(baseDir);
140
132
 
141
- if (detectedPlatforms.length === 0) {
142
- console.log(chalk.yellow('No AI coding assistants detected.'));
143
- if (baseDir) {
144
- console.log('Update will install skills to ./.agents/skills/');
145
- } else {
146
- console.log('Update will install skills to ~/.agents/skills/');
147
- }
133
+ spinner.warn('New version available');
148
134
  console.log('');
149
- } else {
150
- for (const platform of detectedPlatforms) {
151
- console.log(chalk.green('✓'), platform.displayName);
152
- }
135
+ console.log(chalk.yellow('A newer version is available on npm:'));
136
+ console.log(` Current: ${chalk.gray('v' + PACKAGE_VERSION)}`);
137
+ console.log(` Latest: ${chalk.green(latestVersion)}`);
138
+ console.log('');
139
+ console.log('To update, run:');
140
+ console.log(` ${chalk.cyan('npm update -g @maccesar/titools')}`);
141
+ console.log('');
142
+ console.log('Then run this command again:');
143
+ console.log(` ${chalk.cyan('titools update')}`);
153
144
  console.log('');
145
+ return;
154
146
  }
155
147
 
156
- // Download latest from GitHub
157
- spinner.start('Downloading latest from GitHub...');
158
-
159
- let repoDir = getLocalRepoDir();
160
- let tempDir = null;
148
+ // CLI is up to date, now sync skills from the installed package
149
+ spinner.succeed(`CLI is up to date (v${PACKAGE_VERSION})`);
161
150
 
151
+ // Get repository directory from the installed package
152
+ const repoDir = getLocalRepoDir();
162
153
  if (!repoDir) {
163
- tempDir = await mkdtemp(join(tmpdir(), 'titanium-skills-'));
164
- repoDir = await downloadRepoArchive(tempDir);
154
+ console.log('');
155
+ console.log(chalk.red('Error: Could not locate skills source directory.'));
156
+ console.log('Try reinstalling with:');
157
+ console.log(` ${chalk.cyan('npm install -g titools')}`);
158
+ return;
165
159
  }
166
160
 
167
- spinner.succeed('Downloaded from GitHub');
168
-
169
- try {
170
- // Install skills
171
- spinner.start('Updating skills...');
172
- const skillsResult = await installSkills(repoDir, baseDir);
173
- spinner.succeed(
174
- `Skills: ${formatList(skillsResult.installed)}`
175
- );
176
-
177
- // Install agents
178
- spinner.start('Updating agents...');
161
+ // Detect platforms with existing symlinks (only update those)
162
+ const detectedPlatforms = detectPlatforms(baseDir);
163
+ const platformsWithSymlinks = detectedPlatforms.filter((p) =>
164
+ hasAnySkillSymlink(p.skillsDir)
165
+ );
166
+
167
+ // Install skills
168
+ spinner.start('Syncing skills...');
169
+ const skillsResult = await installSkills(repoDir, baseDir);
170
+ spinner.succeed(`Skills: ${formatList(skillsResult.installed)}`);
171
+
172
+ // Install agents (only if Claude Code has symlinks)
173
+ const claudePlatform = platformsWithSymlinks.find((p) => p.name === 'claude');
174
+ if (claudePlatform) {
175
+ spinner.start('Syncing agents...');
179
176
  const agentsResult = await installAgents(repoDir, baseDir);
180
177
  if (agentsResult.installed.length > 0) {
181
- spinner.succeed(
182
- `Agents: ${formatList(agentsResult.installed)}`
183
- );
178
+ spinner.succeed(`Agents: ${formatList(agentsResult.installed)}`);
184
179
  } else {
185
- spinner.info('No agents to update');
180
+ spinner.info('No agents to sync');
186
181
  }
182
+ }
187
183
 
188
- cleanupLegacyArtifacts(baseDir);
184
+ cleanupLegacyArtifacts(baseDir);
189
185
 
190
- // Update symlinks for detected platforms
191
- for (const platform of detectedPlatforms) {
192
- spinner.start(`Updating ${platform.displayName} symlinks...`);
193
- const symlinkResult = await createSkillSymlinks(
194
- platform.skillsDir,
195
- SKILLS,
196
- baseDir
197
- );
198
- if (symlinkResult.linked.length === SKILLS.length) {
199
- spinner.succeed(`${platform.displayName} linked`);
200
- } else {
201
- spinner.warn(
202
- `${platform.displayName}: ${symlinkResult.linked.length}/${SKILLS.length} linked`
203
- );
204
- }
205
- }
206
-
207
- // Summary
208
- console.log('');
209
- console.log(chalk.green('✓ Update complete!'));
210
- console.log('');
211
- const projectDir = resolve(process.cwd());
212
- if (isTitaniumProject(projectDir)) {
213
- const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
214
- const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file)));
215
- if (hasAnyAiFile) {
216
- await agentsCommand(projectDir, { onlyExisting: true, force: true });
217
- } else {
218
- console.log(chalk.bold('▸'), 'Run in the Titanium project:', chalk.cyan('titools sync'));
219
- }
186
+ // Update symlinks only for platforms that already had them
187
+ for (const platform of platformsWithSymlinks) {
188
+ spinner.start(`Updating ${platform.displayName} symlinks...`);
189
+ const symlinkResult = await createSkillSymlinks(
190
+ platform.skillsDir,
191
+ SKILLS,
192
+ baseDir
193
+ );
194
+ if (symlinkResult.linked.length === SKILLS.length) {
195
+ spinner.succeed(`${platform.displayName} linked`);
220
196
  } else {
221
- console.log(chalk.bold('▸'), 'Run in the Titanium project:', chalk.cyan('titools sync'));
197
+ spinner.warn(
198
+ `${platform.displayName}: ${symlinkResult.linked.length}/${SKILLS.length} linked`
199
+ );
222
200
  }
223
- console.log('');
201
+ }
224
202
 
225
- } finally {
226
- // Clean up temp directory
227
- if (tempDir) {
228
- await import('fs-extra').then(({ remove }) => remove(tempDir));
203
+ // Summary
204
+ console.log('');
205
+ console.log(chalk.green('✓ Update complete!'));
206
+
207
+ // Update knowledge index in MD files if in a Titanium project
208
+ const projectDir = resolve(process.cwd());
209
+ if (isTitaniumProject(projectDir)) {
210
+ const aiFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
211
+ const hasAnyAiFile = aiFiles.some((file) => existsSync(join(projectDir, file)));
212
+ if (hasAnyAiFile) {
213
+ console.log('');
214
+ await agentsCommand(projectDir, { onlyExisting: true, force: true });
229
215
  }
230
216
  }
217
+ console.log('');
231
218
 
232
219
  } catch (error) {
233
220
  spinner.fail('Update failed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
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",
@@ -263,26 +263,68 @@ exports.ThemeService = {
263
263
  }
264
264
  ```
265
265
 
266
- ### Semantic Colors (iOS)
266
+ ### Semantic Colors (Cross-Platform)
267
267
 
268
- Titanium supports iOS semantic colors that automatically adapt to Dark Mode:
268
+ Titanium provides a cross-platform semantic colors API for Dark Mode support. Define colors in a `semantic.colors.json` file:
269
+
270
+ **File location:**
271
+ - **Classic apps**: `Resources/semantic.colors.json`
272
+ - **Alloy apps**: `app/assets/semantic.colors.json`
273
+
274
+ ```json
275
+ {
276
+ "textColor": {
277
+ "dark": {
278
+ "color": "#ff85e2",
279
+ "alpha": "50"
280
+ },
281
+ "light": "#ff1f1f"
282
+ },
283
+ "backgroundColor": {
284
+ "dark": "#1a1a2e",
285
+ "light": "#ffffff"
286
+ },
287
+ "primaryColor": {
288
+ "dark": {
289
+ "color": "#4ade80",
290
+ "alpha": "100"
291
+ },
292
+ "light": {
293
+ "color": "#22c55e",
294
+ "alpha": "100"
295
+ }
296
+ }
297
+ }
298
+ ```
299
+
300
+ **Using semantic colors:**
269
301
 
270
302
  ```tss
271
- /* These colors automatically switch in Dark Mode */
303
+ /* Reference via Ti.UI.fetchSemanticColor() */
272
304
  "#label": {
273
- color: Ti.UI.fetchSemanticColor('label')
305
+ color: Ti.UI.fetchSemanticColor('textColor')
274
306
  }
275
307
 
276
- "#bg": {
277
- backgroundColor: Ti.UI.fetchSemanticColor('systemBackground')
308
+ "#container": {
309
+ backgroundColor: Ti.UI.fetchSemanticColor('backgroundColor')
278
310
  }
279
311
  ```
280
312
 
281
- Available semantic colors: `label`, `secondaryLabel`, `systemBackground`, `secondarySystemBackground`, `separator`, `systemFill`, etc.
313
+ ```javascript
314
+ // Or use color names directly as property values
315
+ $.label.color = 'textColor';
316
+ $.container.backgroundColor = 'backgroundColor';
317
+ ```
318
+
319
+ **How it works:**
320
+ - On iOS 13+ it uses the native system method that checks the user's system-wide Dark Mode setting
321
+ - On all other platforms it checks `Ti.UI.semanticColorType` and returns the correct color variant
322
+ - Alpha can be set from 0.0-100.0 (integer or float)
323
+ - Light values can use hex with alpha via ARGB/AARRGGBB format
282
324
 
283
325
  :::tip When to use Semantic Colors vs Alloy.Globals
284
- - **Semantic Colors**: Quick adaptation for iOS only. No Android support.
285
- - **Alloy.Globals palette**: Cross-platform, full control, consistent behavior on both platforms. **Recommended.**
326
+ - **Semantic Colors**: Built-in Dark Mode support, automatic switching, simpler setup. Works cross-platform.
327
+ - **Alloy.Globals palette**: More control over when/how themes switch, easier to add custom themes beyond light/dark, runtime theme changes with UI rebuild.
286
328
  :::
287
329
 
288
330
  ---