@maccesar/titools 4.4.2 → 4.6.1
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 +1 -1
- package/lib/commands/doctor.js +1 -1
- package/lib/commands/list.js +151 -40
- package/lib/commands/skills.js +1 -4
- package/lib/commands/status.js +1 -2
- package/lib/commands/update.js +1 -1
- package/lib/installer.js +0 -1
- package/lib/prompts/checkboxCancel.js +0 -6
- package/lib/symlink.js +1 -1
- package/package.json +4 -2
- package/skills/purgetss/SKILL.md +3 -2
- package/skills/purgetss/references/app-branding.md +406 -323
- package/skills/purgetss/references/cli-commands.md +76 -92
- package/skills/purgetss/references/customization-deep-dive.md +39 -39
- package/skills/purgetss/references/launch-background.md +102 -0
- package/skills/purgetss/references/migration-guide.md +92 -7
- package/skills/purgetss/references/version-history.md +29 -0
- package/skills/ti-api/references/api-media.md +210 -58
- package/skills/ti-api/references/api-modules-map.md +28 -18
- package/skills/ti-api/references/api-modules-social-misc.md +23 -3
- package/skills/ti-api/references/api-ui-extras.md +79 -16
- package/skills/ti-api/references/api-ui-lists.md +89 -22
- package/skills/ti-api/references/api-ui-windows-navigation.md +78 -50
- package/skills/ti-guides/SKILL.md +5 -1
- package/skills/ti-guides/references/compatibility-matrix.md +89 -0
- package/skills/ti-guides/references/resources.md +7 -1
- package/skills/ti-guides/references/sdk-release-notes.md +75 -0
- package/skills/ti-ui/references/scrolling-views.md +7 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
178
178
|
| purgetss | Utility-first styling | UI styling and animations |
|
|
179
179
|
| ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
|
|
180
180
|
| ti-api | Complete Titanium API reference | Looking up properties, methods, events |
|
|
181
|
-
| ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution
|
|
181
|
+
| ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
|
|
182
182
|
| ti-howtos | Native feature integration | Location, push, media, platform APIs |
|
|
183
183
|
| alloy-guides | Alloy MVC framework reference | Controllers, models, views, widgets |
|
|
184
184
|
| alloy-howtos | Alloy CLI, configuration, debugging | alloy.jmk, config.json, custom XML tags |
|
package/lib/commands/doctor.js
CHANGED
package/lib/commands/list.js
CHANGED
|
@@ -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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
|
91
|
-
|
|
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)} ${
|
|
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.'));
|
package/lib/commands/skills.js
CHANGED
|
@@ -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
|
-
|
|
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`);
|
package/lib/commands/status.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { existsSync,
|
|
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';
|
package/lib/commands/update.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
fetchLatestNpmVersion,
|
|
28
28
|
} from '../downloader.js';
|
|
29
29
|
import { createSkillSymlinks } from '../symlink.js';
|
|
30
|
-
import {
|
|
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/installer.js
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "4.6.1",
|
|
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
|
|
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": [
|
package/skills/purgetss/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
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).
|