@maccesar/titools 4.5.0 → 4.7.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
@@ -6,7 +6,7 @@
6
6
 
7
7
  </div>
8
8
 
9
- TiTools is a Titanium SDK toolkit for AI coding assistants. It ships 8 skills — 3 opinionated (`ti-expert`, `purgetss`, `ti-ui`) plus 5 documentation-mirror skills (`ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`) — a research agent, and reference files covering Titanium architecture, API reference, native how-tos, Alloy MVC, PurgeTSS styling, and UI/UX patterns.
9
+ TiTools is a Titanium SDK toolkit for AI coding assistants. It ships 9 skills — 4 opinionated (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`) plus 5 documentation-mirror skills (`ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`) — a research agent, and reference files covering Titanium architecture, API reference, native how-tos, Alloy MVC, PurgeTSS styling, UI/UX patterns, and 2D game development with the `ti.game` module.
10
10
 
11
11
  The reference files are maintained against official documentation whenever an official source exists, so the assistant can retrieve current framework behavior instead of guessing from generic training data.
12
12
 
@@ -36,9 +36,9 @@ The fastest way to get started with Claude Code. One command to add the marketpl
36
36
  ```
37
37
 
38
38
  What you get:
39
- - All 3 TiTools skills (`ti-expert`, `purgetss`, `ti-ui`)
39
+ - All 9 TiTools skills (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`)
40
40
  - ti-pro research agent
41
- - Session hook that auto-detects Titanium projects (`tiapp.xml`, Alloy, PurgeTSS)
41
+ - Session hook that auto-detects Titanium projects (`tiapp.xml`, Alloy, PurgeTSS, `ti.game`)
42
42
  - Slash commands: `/ti-check`, `/ti-new-screen`, `/ti-audit`
43
43
  - Auto-updates via marketplace (opt-in — see note below)
44
44
 
@@ -70,7 +70,7 @@ claude # or gemini, or codex
70
70
  ```
71
71
 
72
72
  What gets installed:
73
- - The 8 TiTools skills to `~/.agents/skills/`
73
+ - The 9 TiTools skills to `~/.agents/skills/`
74
74
  - ti-pro agent for Claude Code
75
75
  - Platform symlinks for Claude Code (Gemini CLI and Codex CLI auto-discover from `~/.agents/skills/` — no symlinks needed)
76
76
  - Knowledge index in your project's `AGENTS.md`/`CLAUDE.md`/`GEMINI.md`
@@ -91,7 +91,7 @@ Why NPM?
91
91
  | **Knowledge Index** | Not included | Included (`titools sync`) |
92
92
  | **Auto-updates** | Marketplace (opt-in, enable in `/plugin`) | SessionStart hook (daily) |
93
93
  | **Slash commands** | `/ti-check`, `/ti-new-screen`, `/ti-audit` | Same three (`~/.claude/commands/`) |
94
- | **Session hook** | Auto-detects Titanium projects | Auto-update only |
94
+ | **Session hook** | Auto-detects Titanium projects (Alloy/Classic, PurgeTSS, `ti.game`) | Auto-update only |
95
95
 
96
96
  Having both installed is fine: the CLI detects an enabled marketplace plugin and skips its own skill symlinks and command copies, so nothing shows up twice. `titools doctor` reports which channel is serving Claude Code.
97
97
 
@@ -177,6 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
177
177
  | ti-expert | Architecture and implementation | Starting point for most tasks |
178
178
  | purgetss | Utility-first styling | UI styling and animations |
179
179
  | ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
180
+ | ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, particles, camera |
180
181
  | ti-api | Complete Titanium API reference | Looking up properties, methods, events |
181
182
  | ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
182
183
  | ti-howtos | Native feature integration | Location, push, media, platform APIs |
@@ -184,7 +185,8 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
184
185
  | alloy-howtos | Alloy CLI, configuration, debugging | alloy.jmk, config.json, custom XML tags |
185
186
 
186
187
  Notes:
187
- - The first three (`ti-expert`, `purgetss`, `ti-ui`) are opinionated workflow skills reflecting TiTools' Titanium conventions.
188
+ - The first four (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`) are opinionated workflow skills reflecting TiTools' Titanium conventions.
189
+ - `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source; it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
188
190
  - The latter five are documentation-mirror skills sourced from the official Titanium SDK docs (`tidev/titanium-docs`) and audited against them via the internal `titools-skill-auditor`.
189
191
  - `purgetss` reference files are audited against the official PurgeTSS documentation, but its workflow conventions are opinionated.
190
192
 
@@ -254,9 +256,11 @@ These commands automatically invoke the relevant skills:
254
256
  The plugin includes a session start hook that auto-detects Titanium projects. When you open Claude Code in a directory with `tiapp.xml`, it automatically reports:
255
257
 
256
258
  ```
257
- Titanium Alloy + PurgeTSS project detected. TiTools skills are available and MUST be used...
259
+ Titanium Alloy + PurgeTSS + ti.game project detected. TiTools skills are available and MUST be used...
258
260
  ```
259
261
 
262
+ It reports Alloy vs Classic, whether PurgeTSS is configured, and whether the project declares the `ti.game` module — a game project also gets a line pointing at the `ti-game` skill.
263
+
260
264
  This ensures Claude knows it's a Titanium project from the first prompt, preventing it from writing generic code.
261
265
 
262
266
  ---
@@ -285,6 +289,7 @@ All skills include automatic project detection to ensure compatibility:
285
289
  | purgetss | PurgeTSS installation | Checks for `purgetss/` folder, `config.cjs`, `utilities.tss` |
286
290
  | ti-expert | Alloy vs Classic | Checks for `app/` (Alloy) vs `Resources/` (Classic) structure |
287
291
  | ti-ui | Titanium projects | Checks for `tiapp.xml` (both Alloy and Classic) |
292
+ | ti-game | ti.game module | Checks for `<module>ti.game</module>` in `tiapp.xml` or `require('ti.game')` |
288
293
 
289
294
  Why this matters:
290
295
  - PurgeTSS suggestions are only provided if PurgeTSS is installed
@@ -422,6 +427,37 @@ Key rules:
422
427
 
423
428
  ---
424
429
 
430
+ ### ti-game
431
+
432
+ 2D game development with the `ti.game` native module — a sprite engine rendered with OpenGL ES 2.0, same JS API on Android and iOS. Works in Alloy and Classic projects.
433
+
434
+ When it activates:
435
+ - `tiapp.xml` declares `ti.game`, or any file calls `require('ti.game')`
436
+ - Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
437
+ - Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
438
+ - Collision groups, invisible trigger zones, hitbox tuning
439
+ - Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, low-latency sound
440
+ - Drag & drop, pinch, rotate, multitouch controls
441
+
442
+ Example prompts:
443
+ ```
444
+ "Build a flappy-bird style game with ti.game."
445
+ "My sprites won't collide — collisionGroup is set on both."
446
+ "Add a camera that follows the player with a dead zone."
447
+ "How do I make a top-down racer that drifts?"
448
+ "Why does my game stutter when I move sprites from setInterval?"
449
+ ```
450
+
451
+ Key rules:
452
+ - JS describes the scene and reacts to events; the engine runs every frame. Never move a sprite from a timer — use velocity, gravity, a tween, `carMode` or `thrust`
453
+ - Build the level inside a guarded `resize` handler, never from `Ti.Platform.displayCaps`
454
+ - `solidWith` blocks, `collidesWith` reports — they are independent
455
+ - A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
456
+ - Add a whole level with one `gameView.add([...])` call
457
+ - There is no text in the engine: HUDs are `Ti.UI.Label` overlays
458
+
459
+ ---
460
+
425
461
  ## Usage examples and best practices
426
462
 
427
463
  ### Example prompts
@@ -517,7 +553,7 @@ Behavior depends on where you run it:
517
553
  | With `--local` flag | Installs skills locally to `./.agents/skills/` in the current project |
518
554
 
519
555
  What it does:
520
- - Installs the 8 TiTools skills (global or local depending on context)
556
+ - Installs the 9 TiTools skills (global or local depending on context)
521
557
  - Installs ti-pro agent for Claude Code
522
558
  - Installs the `/ti-check`, `/ti-new-screen` and `/ti-audit` slash commands into `~/.claude/commands/`
523
559
  - Detects installed AI platforms and lets you choose which to link (only Claude Code needs platform symlinks; Gemini CLI and Codex CLI auto-discover from `~/.agents/skills/`)
@@ -9,5 +9,6 @@ Audit the current Titanium project by invoking the relevant TiTools skills:
9
9
  2. Invoke `ti-ui` — check for UI anti-patterns (Ti.UI.SIZE in ListViews, WebView inside TableView, missing accessibility)
10
10
  3. If PurgeTSS project, invoke `purgetss` — check for custom TSS that should be utility classes, unsupported classes, padding on Views
11
11
  4. Invoke `alloy-guides` — check for proper Alloy patterns (model usage, widget structure)
12
+ 5. If the project declares `ti.game`, invoke `ti-game` — check for sprites moved from timers instead of native properties, levels sized from `Ti.Platform.displayCaps` instead of the `resize` event, sprites added one by one instead of in one array, and timers or sounds left running when the window closes
12
13
 
13
14
  Report findings grouped by severity: critical, warning, info.
@@ -8,8 +8,9 @@ Check the current Titanium project and report:
8
8
  1. Read `tiapp.xml` — report SDK version, app ID, deployment targets
9
9
  2. Check if Alloy project (app/views + app/controllers exist)
10
10
  3. Check if PurgeTSS is configured (purgetss/config.cjs exists)
11
- 4. Check for common issues:
11
+ 4. Check whether the `ti.game` module is declared in `tiapp.xml` — report its version and whether the module is actually present under `modules/<platform>/ti.game/<version>/` or installed globally
12
+ 5. Check for common issues:
12
13
  - Missing icon files (DefaultIcon.png)
13
14
  - Empty modules section in tiapp.xml
14
15
  - Outdated SDK version
15
- 5. Report a summary of findings
16
+ 6. Report a summary of findings
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import chalk from 'chalk';
7
- import { existsSync, lstatSync, readFileSync, realpathSync } from 'fs';
7
+ import { existsSync, lstatSync, readFileSync } from 'fs';
8
8
  import { join } from 'path';
9
9
  import os from 'os';
10
10
  import {
@@ -16,44 +16,144 @@ import {
16
16
  const CHECK = chalk.green('✓');
17
17
  const CROSS = chalk.red('✗');
18
18
 
19
+ // Two lines of description per skill: enough to tell what a skill is for,
20
+ // short enough that the list stays a list. Everything past that is the
21
+ // skill's trigger text, which is written for the agent, not for this screen.
22
+ const MAX_DESCRIPTION_LINES = 2;
23
+
24
+ // Descriptions still need room to say something when the window is narrow.
25
+ const MIN_DESCRIPTION_WIDTH = 24;
26
+
27
+ /**
28
+ * Unquote a YAML scalar. Most skills here are single-quoted because the
29
+ * descriptions contain colons and apostrophes; without this, the quote is
30
+ * printed as part of the text.
31
+ * @param {string} value - Raw scalar as it appears after `description:`
32
+ * @returns {string} The value with its wrapping quotes and escapes resolved
33
+ */
34
+ function unquote(value) {
35
+ const text = value.trim();
36
+ const quote = text[0];
37
+ if ((quote !== "'" && quote !== '"') || !text.endsWith(quote) || text.length < 2) {
38
+ return text;
39
+ }
40
+ const inner = text.slice(1, -1);
41
+ // In single-quoted YAML an apostrophe is escaped by doubling it.
42
+ return quote === "'" ? inner.replace(/''/g, "'") : inner.replace(/\\"/g, '"');
43
+ }
44
+
19
45
  /**
20
- * Extract the skill's human-readable name and short description from SKILL.md.
21
- * Returns { description, installed } where description is the first sentence of
22
- * the frontmatter description, trimmed for terminal display.
46
+ * Wrap text into aligned lines, capped at maxLines with an ellipsis when it
47
+ * does not fit.
48
+ * @param {string} text - Text to wrap
49
+ * @param {number} width - Columns available for the text itself
50
+ * @param {number} maxLines - Hard cap on the number of lines
51
+ * @returns {string[]} Lines, none longer than width
23
52
  */
24
- function readSkillMetadata(skillDir) {
25
- const skillMd = join(skillDir, 'SKILL.md');
26
- if (!existsSync(skillMd)) {
27
- return { description: null, installed: false };
53
+ export function wrapDescription(text, width, maxLines = MAX_DESCRIPTION_LINES) {
54
+ const columns = Math.max(width, MIN_DESCRIPTION_WIDTH);
55
+ const lines = [];
56
+ let current = '';
57
+
58
+ for (let word of text.split(/\s+/).filter(Boolean)) {
59
+ // A single word wider than the column (a long URL) is broken by hand;
60
+ // otherwise the loop below could never place it and would spin.
61
+ while (word.length > columns) {
62
+ if (current) {
63
+ lines.push(current);
64
+ current = '';
65
+ }
66
+ lines.push(word.slice(0, columns));
67
+ word = word.slice(columns);
68
+ }
69
+
70
+ if (!current) {
71
+ current = word;
72
+ } else if (current.length + 1 + word.length <= columns) {
73
+ current += ` ${word}`;
74
+ } else {
75
+ lines.push(current);
76
+ current = word;
77
+ }
78
+ }
79
+ if (current) lines.push(current);
80
+
81
+ if (lines.length <= maxLines) return lines;
82
+
83
+ const kept = lines.slice(0, maxLines);
84
+ let last = kept[maxLines - 1];
85
+ if (last.length + 1 > columns) {
86
+ last = last.slice(0, columns - 1).replace(/\s+\S*$/, '');
28
87
  }
88
+ kept[maxLines - 1] = `${last.replace(/[,;:]$/, '')}…`;
89
+ return kept;
90
+ }
91
+
92
+ /**
93
+ * Extract the skill's short description from SKILL.md — the first sentence of
94
+ * the frontmatter description, unquoted and collapsed to a single line.
95
+ *
96
+ * The sentence ends at a period followed by whitespace, which avoids breaking
97
+ * on "SDK 13." and other version-number periods.
98
+ * @param {string} frontmatter - The YAML block between the --- fences
99
+ * @returns {string|null} The description, or null when there is none
100
+ */
101
+ export function parseDescription(frontmatter) {
102
+ // The lookahead ends the value at the next top-level key, or at the end of
103
+ // the block. `(?![\s\S])` rather than `$`, which under the /m flag would
104
+ // match the first newline and cut a description written across two lines.
105
+ const descMatch = frontmatter.match(/^description:\s*([\s\S]+?)(?=\n[A-Za-z][\w-]*:|(?![\s\S]))/m);
106
+ if (!descMatch) return null;
107
+
108
+ const full = unquote(descMatch[1]).replace(/\s+/g, ' ').trim();
109
+ if (!full) return null;
110
+
111
+ const firstSentence = full.match(/^([^]*?\.)\s/);
112
+ if (firstSentence) return firstSentence[1].trim();
113
+ return full;
114
+ }
29
115
 
116
+ /**
117
+ * Read a description out of one SKILL.md.
118
+ * @param {string} skillMd - Path to a SKILL.md
119
+ * @returns {string|null} The first sentence, or null when unreadable
120
+ */
121
+ function readDescription(skillMd) {
30
122
  try {
31
123
  const content = readFileSync(skillMd, 'utf8');
32
124
  const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
33
- if (!frontmatter) return { description: null, installed: true };
34
-
35
- const descMatch = frontmatter[1].match(/description:\s*"([^"]+)"|description:\s*(.+)/);
36
- if (!descMatch) return { description: null, installed: true };
37
-
38
- const full = descMatch[1] || descMatch[2] || '';
39
- // First sentence: period followed by whitespace (avoids breaking on "SDK 13."
40
- // or other version-number periods). Falls back to a word-safe length cutoff.
41
- let short;
42
- const firstSentence = full.match(/^([^]*?\.)\s/);
43
- if (firstSentence) {
44
- short = firstSentence[1];
45
- } else if (full.length > 80) {
46
- const cut = full.slice(0, 80);
47
- short = cut.slice(0, cut.lastIndexOf(' ')) + '…';
48
- } else {
49
- short = full;
50
- }
51
- return { description: short.trim(), installed: true };
125
+ if (!frontmatter) return null;
126
+ return parseDescription(frontmatter[1]);
52
127
  } catch {
53
- return { description: null, installed: true };
128
+ return null;
54
129
  }
55
130
  }
56
131
 
132
+ /**
133
+ * Read a skill's description, preferring the installed copy and falling back
134
+ * to the one bundled in this package.
135
+ *
136
+ * The fallback is what lets the list describe a skill that is not installed
137
+ * yet — which is exactly when someone runs `titools list` and needs to know
138
+ * what they would be installing. `skills/` ships in the npm tarball, so the
139
+ * bundled copy is there for every install channel.
140
+ * @param {string} skillDir - Where the skill would be installed
141
+ * @param {string} name - Skill name, used to find the bundled copy
142
+ * @returns {{description: string|null, installed: boolean}}
143
+ */
144
+ function readSkillMetadata(skillDir, name) {
145
+ const installedMd = join(skillDir, 'SKILL.md');
146
+ if (existsSync(installedMd)) {
147
+ return { description: readDescription(installedMd), installed: true };
148
+ }
149
+
150
+ const bundledMd = new URL(`../../skills/${name}/SKILL.md`, import.meta.url);
151
+ return {
152
+ description: existsSync(bundledMd) ? readDescription(bundledMd) : null,
153
+ installed: false,
154
+ };
155
+ }
156
+
57
157
  export async function listCommand() {
58
158
  console.log('');
59
159
  console.log(chalk.bold.blue(`Titanium skills (v${PACKAGE_VERSION})`));
@@ -61,21 +161,13 @@ export async function listCommand() {
61
161
 
62
162
  const skillsDir = getAgentsSkillsDir();
63
163
 
64
- if (!existsSync(skillsDir)) {
65
- console.log(chalk.yellow('No skills installed yet.'));
66
- console.log('Install with:');
67
- console.log(chalk.cyan(' titools install'));
68
- console.log('');
69
- return;
70
- }
71
-
72
164
  const rows = [];
73
165
  let installedCount = 0;
74
166
  let maxNameLen = 0;
75
167
 
76
168
  for (const name of SKILLS) {
77
169
  const skillDir = join(skillsDir, name);
78
- const { description, installed } = readSkillMetadata(skillDir);
170
+ const { description, installed } = readSkillMetadata(skillDir, name);
79
171
 
80
172
  if (installed) installedCount++;
81
173
  if (name.length > maxNameLen) maxNameLen = name.length;
@@ -83,18 +175,37 @@ export async function listCommand() {
83
175
  rows.push({ name, description, installed });
84
176
  }
85
177
 
178
+ // Where the description column starts: ' ' + mark + ' ' + padded name + ' '.
179
+ // Measured on the plain text, since chalk's colour codes take no columns.
180
+ const descriptionColumn = 2 + 1 + 1 + (maxNameLen + 2) + 1;
181
+ const available = (process.stdout.columns || 80) - descriptionColumn;
182
+
86
183
  // Print aligned rows
87
184
  for (const row of rows) {
88
185
  const mark = row.installed ? CHECK : CROSS;
89
186
  const paddedName = row.name.padEnd(maxNameLen + 2);
90
- const desc = row.description
91
- ? chalk.gray(row.description)
92
- : chalk.gray(row.installed ? '(no description)' : 'not installed');
187
+ const text = row.description || (row.installed ? '(no description)' : 'not installed');
188
+ const [first, ...rest] = wrapDescription(text, available);
93
189
 
94
- console.log(` ${mark} ${chalk.cyan(paddedName)} ${desc}`);
190
+ console.log(` ${mark} ${chalk.cyan(paddedName)} ${chalk.gray(first)}`);
191
+ for (const line of rest) {
192
+ console.log(`${' '.repeat(descriptionColumn)}${chalk.gray(line)}`);
193
+ }
95
194
  }
96
195
 
97
196
  console.log('');
197
+
198
+ if (installedCount === 0) {
199
+ // Naming the directory would point at somewhere that does not exist yet.
200
+ console.log(chalk.gray(`0/${SKILLS.length} installed`));
201
+ console.log('');
202
+ console.log(chalk.yellow('No skills installed yet.'));
203
+ console.log('Install with:');
204
+ console.log(chalk.cyan(' titools install'));
205
+ console.log('');
206
+ return;
207
+ }
208
+
98
209
  console.log(chalk.gray(`${installedCount}/${SKILLS.length} installed at ${skillsDir}`));
99
210
  console.log('');
100
211
  console.log(chalk.gray('Run `titools status` for installation health.'));
@@ -8,7 +8,6 @@ import ora from 'ora';
8
8
  import {
9
9
  SKILLS,
10
10
  getPlatforms,
11
- getAgentsSkillsDir,
12
11
  } from '../config.js';
13
12
  import select from '../prompts/selectCancel.js';
14
13
  import checkbox, { Separator } from '../prompts/checkboxCancel.js';
@@ -38,7 +37,6 @@ import { downloadRepoArchive } from '../downloader.js';
38
37
  import { createSkillSymlinks } from '../symlink.js';
39
38
  import { installHook } from '../hooks.js';
40
39
  import {
41
- formatList,
42
40
  isTitaniumProject,
43
41
  readSkillDescription,
44
42
  shortenSkillDescription,
@@ -305,7 +303,7 @@ export async function skillsCommand(options) {
305
303
  if (!removeOnly) {
306
304
  // Install skills
307
305
  spinner.start('Installing skills...');
308
- const skillsResult = await installSkills(repoDir, baseDir, skillsToInstall);
306
+ await installSkills(repoDir, baseDir, skillsToInstall);
309
307
  spinner.succeed(`${skillsToInstall.length} skill${skillsToInstall.length !== 1 ? 's' : ''} installed`);
310
308
 
311
309
  // If the user deselected some skills, sweep any previously-installed
@@ -415,7 +413,6 @@ export async function skillsCommand(options) {
415
413
  removedCount: symlinkResult.removed.length,
416
414
  };
417
415
  });
418
- const anyRemoved = platformResults.some((result) => result.removedCount > 0);
419
416
 
420
417
  if (skillsResult.removed.length > 0) {
421
418
  console.log(chalk.green('✓'), `${skillsResult.removed.length} skills removed`);
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import chalk from 'chalk';
7
- import { existsSync, readdirSync, lstatSync, readFileSync } from 'fs';
7
+ import { existsSync, lstatSync, readFileSync } from 'fs';
8
8
  import { join } from 'path';
9
9
  import os from 'os';
10
10
  import {
@@ -14,7 +14,6 @@ import {
14
14
  getClaudeAgentsDir,
15
15
  getConfigDir,
16
16
  getPlatforms,
17
- BLOCK_START,
18
17
  } from '../config.js';
19
18
  import { hasHook } from '../hooks.js';
20
19
  import { readLastCheck } from '../cache.js';
@@ -27,7 +27,7 @@ import {
27
27
  fetchLatestNpmVersion,
28
28
  } from '../downloader.js';
29
29
  import { createSkillSymlinks } from '../symlink.js';
30
- import { formatList, isTitaniumProject } from '../utils.js';
30
+ import { isTitaniumProject } from '../utils.js';
31
31
  import { getAgentsSkillsDir } from '../config.js';
32
32
  import { existsSync } from 'fs';
33
33
  import { join, resolve } from 'path';
package/lib/config.js CHANGED
@@ -33,6 +33,7 @@ export const SKILLS = [
33
33
  'ti-expert',
34
34
  'purgetss',
35
35
  'ti-ui',
36
+ 'ti-game',
36
37
  'ti-api',
37
38
  'ti-guides',
38
39
  'ti-howtos',
package/lib/installer.js CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  copyFileSync,
8
8
  existsSync,
9
9
  mkdirSync,
10
- readdirSync,
11
10
  } from 'fs';
12
11
  import { join } from 'path';
13
12
  import { remove, copy } from 'fs-extra';
@@ -49,12 +49,6 @@ function toggle(item) {
49
49
  return isSelectable(item) ? { ...item, checked: !item.checked } : item;
50
50
  }
51
51
 
52
- function check(checked) {
53
- return function (item) {
54
- return isSelectable(item) ? { ...item, checked } : item;
55
- };
56
- }
57
-
58
52
  function normalizeChoices(choices) {
59
53
  return choices.map((choice) => {
60
54
  if (Separator.isSeparator(choice)) return choice;
package/lib/symlink.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { mkdirSync, existsSync } from 'fs';
7
- import { symlink, readlink, unlink, lstat } from 'fs/promises';
7
+ import { symlink, readlink } from 'fs/promises';
8
8
  import { join, dirname, relative } from 'path';
9
9
  import { copy, remove } from 'fs-extra';
10
10
  import { isWindows } from './platform.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.5.0",
3
+ "version": "4.7.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",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/**/*.test.js",
12
- "lint": "eslint lib/**/*.js",
12
+ "lint": "eslint bin lib test",
13
13
  "format": "prettier --write lib/**/*.js"
14
14
  },
15
15
  "keywords": [
@@ -49,7 +49,9 @@
49
49
  "tar": "^7.5.7"
50
50
  },
51
51
  "devDependencies": {
52
+ "@eslint/js": "^9.39.5",
52
53
  "eslint": "^9.39.2",
54
+ "globals": "^16.5.0",
53
55
  "prettier": "^3.8.1"
54
56
  },
55
57
  "files": [
@@ -427,7 +427,8 @@ Load these only when needed:
427
427
  - [iOS Large Titles](references/ios-large-titles.md) - Best practice for iOS Large Title navigation
428
428
 
429
429
  ### App Assets
430
- - [App Branding](references/app-branding.md) - `brand` command for launcher icons, adaptive, iOS 18+ Dark/Tinted, marketplace assets (v7.6.0)
430
+ - [App Branding](references/app-branding.md) - `brand` command: the 14 pieces it regenerates (launcher, adaptive, iOS 18+ Dark/Tinted, marketplace, both splash sets), the per-piece `brand:` config, `--only`, `--optimize` (v7.13.0)
431
+ - [Launch Background](references/launch-background.md) - Carrying `brand.background` into the iOS LaunchScreen and a launcher-only Android theme (`--notes` output)
431
432
  - [Multi-Density Images](references/multi-density-images.md) - `images` command for Android res-*dpi + iPhone @1x/@2x/@3x UI images (v7.6.0)
432
433
  - [SVG Pipeline](references/svg-pipeline.md) - SVG-aware compile-time image pipeline: `.svg` refs + numeric `w-*`/`h-*` classes compile to 8 density PNGs; `images.files`/`images.autoSync` (v7.11.0–v7.11.1)
433
434
 
@@ -444,7 +445,7 @@ Load these only when needed:
444
445
  - [Animation Advanced](references/animation-advanced.md) - Property inheritance, utility classes, implementation rules, complex UI example
445
446
 
446
447
  ### Release Notes
447
- - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.11.1)
448
+ - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.13.1)
448
449
 
449
450
  > **💡 TEXT FONTS (Google Fonts, Roboto, etc.)**
450
451
  > For text fonts, see [Custom Fonts](references/custom-fonts.md).