@maccesar/titools 2.0.6 → 2.2.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.
Files changed (132) hide show
  1. package/{AGENTS-TEMPLATE.md → AGENTS-VERCEL-RESEARCH.md} +16 -18
  2. package/README.md +250 -168
  3. package/agents/{ti-researcher.md → ti-pro.md} +10 -10
  4. package/bin/titools.js +16 -12
  5. package/lib/cleanup.js +95 -0
  6. package/lib/commands/agents.js +146 -70
  7. package/lib/commands/skills.js +349 -0
  8. package/lib/commands/uninstall.js +189 -154
  9. package/lib/commands/update.js +104 -21
  10. package/lib/config.js +26 -20
  11. package/lib/downloader.js +1 -1
  12. package/lib/installer.js +37 -97
  13. package/lib/platform.js +9 -5
  14. package/lib/prompts/checkboxCancel.js +264 -0
  15. package/lib/prompts/selectCancel.js +204 -0
  16. package/lib/symlink.js +19 -7
  17. package/lib/utils.js +21 -31
  18. package/package.json +10 -10
  19. package/skills/alloy-guides/SKILL.md +8 -30
  20. package/skills/alloy-guides/references/CLI_TASKS.md +14 -24
  21. package/skills/alloy-guides/references/CONCEPTS.md +5 -25
  22. package/skills/alloy-guides/references/CONTROLLERS.md +2 -19
  23. package/skills/alloy-guides/references/MODELS.md +40 -10
  24. package/skills/alloy-guides/references/PURGETSS.md +1 -11
  25. package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +8 -14
  26. package/skills/alloy-guides/references/VIEWS_STYLES.md +14 -25
  27. package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +2 -8
  28. package/skills/alloy-guides/references/VIEWS_XML.md +29 -40
  29. package/skills/alloy-guides/references/WIDGETS.md +1 -17
  30. package/skills/alloy-howtos/SKILL.md +1 -22
  31. package/skills/alloy-howtos/references/best_practices.md +0 -17
  32. package/skills/alloy-howtos/references/cli_reference.md +1 -23
  33. package/skills/alloy-howtos/references/config_files.md +86 -15
  34. package/skills/alloy-howtos/references/custom_tags.md +14 -13
  35. package/skills/alloy-howtos/references/debugging_troubleshooting.md +3 -22
  36. package/skills/alloy-howtos/references/samples.md +4 -14
  37. package/skills/purgetss/SKILL.md +11 -37
  38. package/skills/purgetss/references/EXAMPLES.md +106 -17
  39. package/skills/purgetss/references/animation-system.md +0 -67
  40. package/skills/purgetss/references/apply-directive.md +0 -18
  41. package/skills/purgetss/references/arbitrary-values.md +0 -25
  42. package/skills/purgetss/references/class-index.md +0 -46
  43. package/skills/purgetss/references/cli-commands.md +0 -41
  44. package/skills/purgetss/references/configurable-properties.md +0 -55
  45. package/skills/purgetss/references/custom-rules.md +0 -14
  46. package/skills/purgetss/references/customization-deep-dive.md +0 -49
  47. package/skills/purgetss/references/dynamic-component-creation.md +56 -37
  48. package/skills/purgetss/references/grid-layout.md +0 -32
  49. package/skills/purgetss/references/icon-fonts.md +0 -50
  50. package/skills/purgetss/references/installation-setup.md +61 -32
  51. package/skills/purgetss/references/migration-guide.md +127 -0
  52. package/skills/purgetss/references/opacity-modifier.md +0 -34
  53. package/skills/purgetss/references/performance-tips.md +261 -0
  54. package/skills/purgetss/references/platform-modifiers.md +0 -46
  55. package/skills/purgetss/references/smart-mappings.md +0 -12
  56. package/skills/purgetss/references/tikit-components.md +393 -0
  57. package/skills/purgetss/references/titanium-resets.md +1 -27
  58. package/skills/purgetss/references/ui-ux-design.md +467 -117
  59. package/skills/{alloy-expert → ti-expert}/SKILL.md +45 -54
  60. package/skills/ti-expert/references/alloy-builtins.md +387 -0
  61. package/skills/{alloy-expert → ti-expert}/references/alloy-structure.md +147 -40
  62. package/skills/ti-expert/references/anti-patterns.md +90 -0
  63. package/skills/ti-expert/references/cli-expert.md +109 -0
  64. package/skills/{alloy-expert → ti-expert}/references/code-conventions.md +182 -49
  65. package/skills/{alloy-expert → ti-expert}/references/controller-patterns.md +103 -93
  66. package/skills/{alloy-expert → ti-expert}/references/error-handling.md +12 -12
  67. package/skills/{alloy-expert → ti-expert}/references/examples.md +144 -85
  68. package/skills/{alloy-expert → ti-expert}/references/migration-patterns.md +38 -36
  69. package/skills/{alloy-expert → ti-expert}/references/patterns.md +8 -8
  70. package/skills/ti-expert/references/performance-listview.md +251 -0
  71. package/skills/{alloy-expert/references/performance-patterns.md → ti-expert/references/performance-optimization.md} +67 -283
  72. package/skills/{alloy-expert/references/security-patterns.md → ti-expert/references/security-device.md} +12 -295
  73. package/skills/ti-expert/references/security-fundamentals.md +284 -0
  74. package/skills/{alloy-expert → ti-expert}/references/state-management.md +10 -10
  75. package/skills/ti-expert/references/testing-e2e-ci.md +432 -0
  76. package/skills/ti-expert/references/testing-unit.md +433 -0
  77. package/skills/ti-expert/references/theming.md +394 -0
  78. package/skills/ti-guides/SKILL.md +1 -14
  79. package/skills/ti-guides/references/advanced-data-and-images.md +126 -10
  80. package/skills/ti-guides/references/app-distribution.md +48 -10
  81. package/skills/ti-guides/references/application-frameworks.md +9 -2
  82. package/skills/ti-guides/references/cli-reference.md +338 -82
  83. package/skills/ti-guides/references/coding-best-practices.md +14 -4
  84. package/skills/ti-guides/references/commonjs-advanced.md +103 -6
  85. package/skills/ti-guides/references/hello-world.md +5 -2
  86. package/skills/ti-guides/references/hyperloop-native-access.md +439 -46
  87. package/skills/ti-guides/references/javascript-primer.md +4 -3
  88. package/skills/ti-guides/references/resources.md +0 -1
  89. package/skills/ti-guides/references/style-and-conventions.md +57 -9
  90. package/skills/ti-guides/references/tiapp-config.md +48 -6
  91. package/skills/ti-howtos/SKILL.md +2 -27
  92. package/skills/ti-howtos/references/android-platform-deep-dives.md +41 -90
  93. package/skills/ti-howtos/references/automation-fastlane-appium.md +15 -14
  94. package/skills/ti-howtos/references/buffer-codec-streams.md +22 -22
  95. package/skills/ti-howtos/references/cross-platform-development.md +68 -37
  96. package/skills/ti-howtos/references/debugging-profiling.md +8 -53
  97. package/skills/ti-howtos/references/extending-titanium.md +12 -51
  98. package/skills/ti-howtos/references/google-maps-v2.md +30 -29
  99. package/skills/ti-howtos/references/ios-map-kit.md +19 -25
  100. package/skills/ti-howtos/references/ios-platform-deep-dives.md +95 -283
  101. package/skills/ti-howtos/references/local-data-sources.md +45 -37
  102. package/skills/ti-howtos/references/location-and-maps.md +47 -28
  103. package/skills/ti-howtos/references/media-apis.md +84 -27
  104. package/skills/ti-howtos/references/notification-services.md +40 -89
  105. package/skills/ti-howtos/references/remote-data-sources.md +21 -36
  106. package/skills/ti-howtos/references/tutorials.md +79 -39
  107. package/skills/ti-howtos/references/using-modules.md +4 -30
  108. package/skills/ti-howtos/references/web-content-integration.md +28 -45
  109. package/skills/ti-howtos/references/webpack-build-pipeline.md +54 -18
  110. package/skills/ti-ui/SKILL.md +1 -21
  111. package/skills/ti-ui/references/accessibility-deep-dive.md +71 -25
  112. package/skills/ti-ui/references/animation-and-matrices.md +0 -47
  113. package/skills/ti-ui/references/application-structures.md +0 -46
  114. package/skills/ti-ui/references/custom-fonts-styling.md +9 -57
  115. package/skills/ti-ui/references/event-handling.md +32 -40
  116. package/skills/ti-ui/references/gestures.md +0 -40
  117. package/skills/ti-ui/references/icons-and-splash-screens.md +21 -44
  118. package/skills/ti-ui/references/layouts-and-positioning.md +47 -39
  119. package/skills/ti-ui/references/listviews-and-performance.md +187 -73
  120. package/skills/ti-ui/references/orientation.md +57 -71
  121. package/skills/ti-ui/references/platform-ui-android.md +114 -48
  122. package/skills/ti-ui/references/platform-ui-ios.md +63 -38
  123. package/skills/ti-ui/references/scrolling-views.md +14 -23
  124. package/skills/ti-ui/references/tableviews.md +6 -56
  125. package/lib/commands/install.js +0 -188
  126. package/skills/alloy-expert/references/anti-patterns.md +0 -133
  127. package/skills/alloy-expert/references/testing.md +0 -872
  128. package/skills/ti-guides/references/alloy-cli-advanced.md +0 -84
  129. package/skills/ti-guides/references/alloy-data-mastery.md +0 -29
  130. package/skills/ti-guides/references/alloy-widgets-and-themes.md +0 -19
  131. /package/skills/{alloy-expert → ti-expert}/assets/ControllerAutoCleanup.js +0 -0
  132. /package/skills/{alloy-expert → ti-expert}/references/contracts.md +0 -0
package/lib/config.js CHANGED
@@ -8,7 +8,7 @@ import os from 'os';
8
8
  import { readFileSync } from 'fs';
9
9
 
10
10
  // Read package.json version dynamically
11
- let packageVersion = '1.7.0';
11
+ let packageVersion = '2.1.0';
12
12
  try {
13
13
  const packagePath = new URL('../package.json', import.meta.url);
14
14
  const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
@@ -30,7 +30,7 @@ export const REPO_API_URL = 'https://api.github.com/repos/macCesar/titools';
30
30
 
31
31
  // Skills to install
32
32
  export const SKILLS = [
33
- 'alloy-expert',
33
+ 'ti-expert',
34
34
  'purgetss',
35
35
  'ti-ui',
36
36
  'ti-howtos',
@@ -38,45 +38,51 @@ export const SKILLS = [
38
38
  'alloy-guides',
39
39
  'alloy-howtos',
40
40
  ];
41
+ // Legacy skills to remove during updates/uninstall
42
+ export const LEGACY_SKILLS = [
43
+ 'alloy-expert',
44
+ ];
41
45
 
42
46
  // Agents to install
43
47
  export const AGENTS = [
48
+ 'ti-pro',
49
+ ];
50
+ // Legacy agents to remove during updates/uninstall
51
+ export const LEGACY_AGENTS = [
44
52
  'ti-researcher',
45
53
  ];
46
54
 
47
55
  // Directory paths
48
- export const getAgentsDir = () => path.join(os.homedir(), '.agents');
49
- export const getAgentsSkillsDir = () => path.join(getAgentsDir(), 'skills');
50
- export const getClaudeAgentsDir = () => path.join(os.homedir(), '.claude', 'agents');
51
- export const getClaudeSkillsDir = () => path.join(os.homedir(), '.claude', 'skills');
52
- export const getGeminiSkillsDir = () => path.join(os.homedir(), '.gemini', 'skills');
53
- export const getCodexSkillsDir = () => path.join(os.homedir(), '.codex', 'skills');
56
+ export const getAgentsDir = (baseDir = os.homedir()) => path.join(baseDir, '.agents');
57
+ export const getAgentsSkillsDir = (baseDir = os.homedir()) => path.join(getAgentsDir(baseDir), 'skills');
58
+ export const getClaudeAgentsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'agents');
59
+ export const getClaudeSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'skills');
60
+ export const getGeminiSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.gemini', 'skills');
61
+ export const getCodexSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.codex', 'skills');
54
62
 
55
63
  // AI platform detection
56
- export const PLATFORMS = [
64
+ export const getPlatforms = (baseDir = os.homedir()) => [
57
65
  {
58
66
  name: 'claude',
59
67
  displayName: 'Claude Code',
60
- skillsDir: getClaudeSkillsDir(),
61
- configDir: path.join(os.homedir(), '.claude'),
68
+ skillsDir: getClaudeSkillsDir(baseDir),
69
+ configDir: path.join(baseDir, '.claude'),
62
70
  },
63
71
  {
64
72
  name: 'gemini',
65
73
  displayName: 'Gemini CLI',
66
- skillsDir: getGeminiSkillsDir(),
67
- configDir: path.join(os.homedir(), '.gemini'),
74
+ skillsDir: getGeminiSkillsDir(baseDir),
75
+ configDir: path.join(baseDir, '.gemini'),
68
76
  },
69
77
  {
70
78
  name: 'codex',
71
79
  displayName: 'Codex CLI',
72
- skillsDir: getCodexSkillsDir(),
73
- configDir: path.join(os.homedir(), '.codex'),
80
+ skillsDir: getCodexSkillsDir(baseDir),
81
+ configDir: path.join(baseDir, '.codex'),
74
82
  },
75
83
  ];
76
84
 
77
85
  // Files to install
78
- export const AGENTS_TEMPLATE_FILE = 'AGENTS-TEMPLATE.md';
79
- export const TI_DOCS_INDEX_SCRIPT = 'ti-docs-index';
80
86
 
81
87
  // AI file priorities (higher = more priority)
82
88
  export const AI_FILE_PRIORITIES = {
@@ -103,16 +109,16 @@ export default {
103
109
  REPO_RAW_URL,
104
110
  REPO_API_URL,
105
111
  SKILLS,
112
+ LEGACY_SKILLS,
106
113
  AGENTS,
114
+ LEGACY_AGENTS,
107
115
  getAgentsDir,
108
116
  getAgentsSkillsDir,
109
117
  getClaudeAgentsDir,
110
118
  getClaudeSkillsDir,
111
119
  getGeminiSkillsDir,
112
120
  getCodexSkillsDir,
113
- PLATFORMS,
114
- AGENTS_TEMPLATE_FILE,
115
- TI_DOCS_INDEX_SCRIPT,
121
+ getPlatforms,
116
122
  AI_FILE_PRIORITIES,
117
123
  TITANIUM_PROJECT_FILE,
118
124
  GITHUB_API_HEADERS,
package/lib/downloader.js CHANGED
@@ -94,7 +94,7 @@ export async function downloadRepoArchive(destDir, ref = 'main') {
94
94
 
95
95
  /**
96
96
  * Download a single file from GitHub raw content
97
- * @param {string} filePath - Path in repository (e.g., 'AGENTS-TEMPLATE.md')
97
+ * @param {string} filePath - Path in repository (e.g., 'AGENTS-VERCEL-RESEARCH.md')
98
98
  * @param {string} destPath - Local destination path
99
99
  * @param {string} ref - Git ref (branch, tag, commit)
100
100
  * @returns {Promise<void>}
package/lib/installer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * File installation utilities
3
- * Installs skills, agents, and templates to their respective directories
3
+ * Installs skills and agents to their respective directories
4
4
  */
5
5
 
6
6
  import {
@@ -11,14 +11,14 @@ import {
11
11
  } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { remove, copy } from 'fs-extra';
14
+ import os from 'os';
14
15
  import {
15
16
  SKILLS,
16
17
  AGENTS,
17
18
  getAgentsSkillsDir,
18
19
  getClaudeAgentsDir,
19
- getAgentsDir,
20
- AGENTS_TEMPLATE_FILE,
21
20
  } from './config.js';
21
+ import { removeSkills, removeAgents } from './cleanup.js';
22
22
 
23
23
  /**
24
24
  * Recursively copy a directory
@@ -36,25 +36,15 @@ export async function copyDirectory(src, dest) {
36
36
  await copy(src, dest, { overwrite: true });
37
37
  }
38
38
 
39
- /**
40
- * Remove existing file or directory
41
- * @param {string} target - Path to remove
42
- * @returns {Promise<void>}
43
- */
44
- export async function removeExisting(target) {
45
- if (existsSync(target)) {
46
- await remove(target);
47
- }
48
- }
49
-
50
39
  /**
51
40
  * Install a single skill to the agents skills directory
52
41
  * @param {string} repoDir - Repository directory
53
42
  * @param {string} skillName - Name of the skill
43
+ * @param {string} baseDir - Base directory for installation
54
44
  * @returns {Promise<boolean>} True if installed successfully
55
45
  */
56
- export async function installSkill(repoDir, skillName) {
57
- const skillsDir = getAgentsSkillsDir();
46
+ export async function installSkill(repoDir, skillName, baseDir = os.homedir()) {
47
+ const skillsDir = getAgentsSkillsDir(baseDir);
58
48
  const skillSrc = join(repoDir, 'skills', skillName);
59
49
  const skillDest = join(skillsDir, skillName);
60
50
 
@@ -81,16 +71,28 @@ export async function installSkill(repoDir, skillName) {
81
71
  /**
82
72
  * Install all skills to the agents skills directory
83
73
  * @param {string} repoDir - Repository directory
74
+ * @param {string} baseDir - Base directory for installation
84
75
  * @returns {Promise<Object>} Results object with success/failure counts
85
76
  */
86
- export async function installSkills(repoDir) {
77
+ export async function installSkills(repoDir, baseDir = os.homedir()) {
87
78
  const results = {
88
79
  installed: [],
89
80
  failed: [],
81
+ removed: [],
90
82
  };
91
83
 
84
+ const legacyLocal = removeSkills(baseDir, { legacyOnly: true });
85
+ results.removed.push(...legacyLocal.removed);
86
+ results.failed.push(...legacyLocal.failed);
87
+
88
+ if (baseDir && baseDir !== os.homedir()) {
89
+ const legacyGlobal = removeSkills(undefined, { legacyOnly: true });
90
+ results.removed.push(...legacyGlobal.removed);
91
+ results.failed.push(...legacyGlobal.failed);
92
+ }
93
+
92
94
  for (const skill of SKILLS) {
93
- if (await installSkill(repoDir, skill)) {
95
+ if (await installSkill(repoDir, skill, baseDir)) {
94
96
  results.installed.push(skill);
95
97
  } else {
96
98
  results.failed.push(skill);
@@ -104,10 +106,11 @@ export async function installSkills(repoDir) {
104
106
  * Install a single agent to Claude agents directory
105
107
  * @param {string} repoDir - Repository directory
106
108
  * @param {string} agentName - Name of the agent (without .md)
109
+ * @param {string} baseDir - Base directory for installation
107
110
  * @returns {Promise<boolean>} True if installed successfully
108
111
  */
109
- export async function installAgent(repoDir, agentName) {
110
- const agentsDir = getClaudeAgentsDir();
112
+ export async function installAgent(repoDir, agentName, baseDir = os.homedir()) {
113
+ const agentsDir = getClaudeAgentsDir(baseDir);
111
114
  const agentSrc = join(repoDir, 'agents', `${agentName}.md`);
112
115
  const agentDest = join(agentsDir, `${agentName}.md`);
113
116
 
@@ -134,16 +137,28 @@ export async function installAgent(repoDir, agentName) {
134
137
  /**
135
138
  * Install all agents to Claude agents directory
136
139
  * @param {string} repoDir - Repository directory
140
+ * @param {string} baseDir - Base directory for installation
137
141
  * @returns {Promise<Object>} Results object with success/failure counts
138
142
  */
139
- export async function installAgents(repoDir) {
143
+ export async function installAgents(repoDir, baseDir = os.homedir()) {
140
144
  const results = {
141
145
  installed: [],
142
146
  failed: [],
147
+ removed: [],
143
148
  };
144
149
 
150
+ const legacyLocal = removeAgents(baseDir, { legacyOnly: true });
151
+ results.removed.push(...legacyLocal.removed);
152
+ results.failed.push(...legacyLocal.failed);
153
+
154
+ if (baseDir && baseDir !== os.homedir()) {
155
+ const legacyGlobal = removeAgents(undefined, { legacyOnly: true });
156
+ results.removed.push(...legacyGlobal.removed);
157
+ results.failed.push(...legacyGlobal.failed);
158
+ }
159
+
145
160
  for (const agent of AGENTS) {
146
- if (await installAgent(repoDir, agent)) {
161
+ if (await installAgent(repoDir, agent, baseDir)) {
147
162
  results.installed.push(agent);
148
163
  } else {
149
164
  results.failed.push(agent);
@@ -153,78 +168,6 @@ export async function installAgents(repoDir) {
153
168
  return results;
154
169
  }
155
170
 
156
- /**
157
- * Install AGENTS-TEMPLATE.md to the agents directory
158
- * @param {string} repoDir - Repository directory
159
- * @returns {Promise<boolean>} True if installed successfully
160
- */
161
- export async function installAgentsTemplate(repoDir) {
162
- const agentsDir = getAgentsDir();
163
- const templateSrc = join(repoDir, AGENTS_TEMPLATE_FILE);
164
- const templateDest = join(agentsDir, AGENTS_TEMPLATE_FILE);
165
-
166
- // Create agents directory if needed
167
- if (!existsSync(agentsDir)) {
168
- mkdirSync(agentsDir, { recursive: true });
169
- }
170
-
171
- // Check if source exists
172
- if (!existsSync(templateSrc)) {
173
- return false;
174
- }
175
-
176
- // Copy template file
177
- copyFileSync(templateSrc, templateDest);
178
- return true;
179
- }
180
-
181
- /**
182
- * Install the ti-docs-index helper script
183
- * @param {string} repoDir - Repository directory
184
- * @param {string} binDir - Bin directory path
185
- * @returns {Promise<boolean>} True if installed successfully
186
- */
187
- export async function installTiDocsIndexScript(repoDir, binDir) {
188
- const scriptSrc = join(repoDir, 'scripts', 'ti-docs-index');
189
- const scriptDest = join(binDir, 'ti-docs-index');
190
-
191
- // Check if source exists
192
- if (!existsSync(scriptSrc)) {
193
- return false;
194
- }
195
-
196
- // Create bin directory if needed
197
- if (!existsSync(binDir)) {
198
- try {
199
- mkdirSync(binDir, { recursive: true });
200
- } catch {
201
- // If we can't create the bin dir, skip script installation
202
- return false;
203
- }
204
- }
205
-
206
- // Try to copy script file
207
- try {
208
- copyFileSync(scriptSrc, scriptDest);
209
- } catch (error) {
210
- // If we can't write to binDir (permission denied), skip script installation
211
- if (error.code === 'EACCES' || error.code === 'EPERM') {
212
- return false;
213
- }
214
- throw error;
215
- }
216
-
217
- // Make executable (chmod +x)
218
- try {
219
- const { chmod } = await import('fs/promises');
220
- await chmod(scriptDest, 0o755);
221
- } catch {
222
- // Ignore permission errors
223
- }
224
-
225
- return true;
226
- }
227
-
228
171
  /**
229
172
  * Get the local repository directory if running from source
230
173
  * @returns {string|null} Local repo directory or null
@@ -242,12 +185,9 @@ export function getLocalRepoDir() {
242
185
 
243
186
  export default {
244
187
  copyDirectory,
245
- removeExisting,
246
188
  installSkill,
247
189
  installSkills,
248
190
  installAgent,
249
191
  installAgents,
250
- installAgentsTemplate,
251
- installTiDocsIndexScript,
252
192
  getLocalRepoDir,
253
193
  };
package/lib/platform.js CHANGED
@@ -4,16 +4,18 @@
4
4
  */
5
5
 
6
6
  import { existsSync } from 'fs';
7
- import { PLATFORMS } from './config.js';
7
+ import { getPlatforms } from './config.js';
8
8
 
9
9
  /**
10
10
  * Detect installed AI coding assistant platforms
11
+ * @param {string} baseDir - Optional base directory to check
11
12
  * @returns {Array} Detected platforms
12
13
  */
13
- export function detectPlatforms() {
14
+ export function detectPlatforms(baseDir) {
14
15
  const detected = [];
16
+ const platforms = getPlatforms(baseDir);
15
17
 
16
- for (const platform of PLATFORMS) {
18
+ for (const platform of platforms) {
17
19
  if (existsSync(platform.configDir)) {
18
20
  detected.push(platform);
19
21
  }
@@ -25,10 +27,12 @@ export function detectPlatforms() {
25
27
  /**
26
28
  * Get platform by name
27
29
  * @param {string} name - Platform name (claude, gemini, codex)
30
+ * @param {string} baseDir - Optional base directory
28
31
  * @returns {Object|null} Platform object or null
29
32
  */
30
- export function getPlatformByName(name) {
31
- return PLATFORMS.find((p) => p.name === name) || null;
33
+ export function getPlatformByName(name, baseDir) {
34
+ const platforms = getPlatforms(baseDir);
35
+ return platforms.find((p) => p.name === name) || null;
32
36
  }
33
37
 
34
38
  /**
@@ -0,0 +1,264 @@
1
+ import {
2
+ Separator,
3
+ ValidationError,
4
+ createPrompt,
5
+ isDownKey,
6
+ isEnterKey,
7
+ isNumberKey,
8
+ isSpaceKey,
9
+ isUpKey,
10
+ makeTheme,
11
+ useKeypress,
12
+ useMemo,
13
+ usePagination,
14
+ usePrefix,
15
+ useState,
16
+ } from '@inquirer/core';
17
+ import { cursorHide } from '@inquirer/ansi';
18
+ import { styleText } from 'node:util';
19
+ import figures from '@inquirer/figures';
20
+
21
+ const checkboxTheme = {
22
+ icon: {
23
+ checked: styleText('green', figures.circleFilled),
24
+ unchecked: figures.circle,
25
+ cursor: figures.pointer,
26
+ },
27
+ style: {
28
+ disabledChoice: (text) => styleText('dim', `- ${text}`),
29
+ renderSelectedChoices: (selectedChoices) =>
30
+ selectedChoices.map((choice) => choice.short).join(', '),
31
+ description: (text) => styleText('cyan', text),
32
+ keysHelpTip: (keys) =>
33
+ keys
34
+ .map(([key, action]) => `${styleText('bold', key)} ${styleText('dim', action)}`)
35
+ .join(styleText('dim', ' • ')),
36
+ },
37
+ keybindings: [],
38
+ };
39
+
40
+ function isSelectable(item) {
41
+ return !Separator.isSeparator(item) && !item.disabled;
42
+ }
43
+
44
+ function isChecked(item) {
45
+ return isSelectable(item) && item.checked;
46
+ }
47
+
48
+ function toggle(item) {
49
+ return isSelectable(item) ? { ...item, checked: !item.checked } : item;
50
+ }
51
+
52
+ function check(checked) {
53
+ return function (item) {
54
+ return isSelectable(item) ? { ...item, checked } : item;
55
+ };
56
+ }
57
+
58
+ function normalizeChoices(choices) {
59
+ return choices.map((choice) => {
60
+ if (Separator.isSeparator(choice)) return choice;
61
+ if (typeof choice === 'string') {
62
+ return {
63
+ value: choice,
64
+ name: choice,
65
+ short: choice,
66
+ checkedName: choice,
67
+ disabled: false,
68
+ checked: false,
69
+ };
70
+ }
71
+ const name = choice.name ?? String(choice.value);
72
+ const normalizedChoice = {
73
+ value: choice.value,
74
+ name,
75
+ short: choice.short ?? name,
76
+ checkedName: choice.checkedName ?? name,
77
+ disabled: choice.disabled ?? false,
78
+ checked: choice.checked ?? false,
79
+ };
80
+ if (choice.description) {
81
+ normalizedChoice.description = choice.description;
82
+ }
83
+ return normalizedChoice;
84
+ });
85
+ }
86
+
87
+ const checkboxCancel = createPrompt((config, done) => {
88
+ const {
89
+ pageSize = 7,
90
+ loop = true,
91
+ required,
92
+ validate = () => true,
93
+ cancelValue = 'cancel',
94
+ } = config;
95
+
96
+ // Shortcuts handling - allow disabling by setting to false or null
97
+ const shortcuts = {
98
+ all: config.shortcuts?.all === undefined ? 'a' : config.shortcuts.all,
99
+ invert: config.shortcuts?.invert === undefined ? 'i' : config.shortcuts.invert
100
+ };
101
+
102
+ const theme = makeTheme(checkboxTheme, config.theme);
103
+ const { keybindings } = theme;
104
+ const [status, setStatus] = useState('idle');
105
+ const prefix = usePrefix({ status, theme });
106
+ const [items, setItems] = useState(normalizeChoices(config.choices));
107
+ const bounds = useMemo(() => {
108
+ const first = items.findIndex(isSelectable);
109
+ const last = items.findLastIndex(isSelectable);
110
+ if (first === -1) {
111
+ throw new ValidationError('[checkbox prompt] No selectable choices. All choices are disabled.');
112
+ }
113
+ return { first, last };
114
+ }, [items]);
115
+ const [active, setActive] = useState(bounds.first);
116
+ const [errorMsg, setError] = useState();
117
+ const [cancelled, setCancelled] = useState(false);
118
+
119
+ useKeypress(async (key) => {
120
+ if (key.name?.toLowerCase() === 'q') {
121
+ setCancelled(true);
122
+ setStatus('done');
123
+ done([cancelValue]);
124
+ return;
125
+ }
126
+ if (isEnterKey(key)) {
127
+ const activeItem = items[active];
128
+
129
+ // If the active item IS the cancel option, cancel immediately
130
+ if (isSelectable(activeItem) && activeItem.value === cancelValue) {
131
+ setCancelled(true);
132
+ setStatus('done');
133
+ done([cancelValue]);
134
+ return;
135
+ }
136
+
137
+ // If ANY checked item is the cancel option, cancel immediately
138
+ const anyCancelChecked = items.some(
139
+ (item) => isSelectable(item) && item.value === cancelValue && item.checked
140
+ );
141
+ if (anyCancelChecked) {
142
+ setCancelled(true);
143
+ setStatus('done');
144
+ done([cancelValue]);
145
+ return;
146
+ }
147
+
148
+ const selection = items.filter(isChecked);
149
+ const isValid = await validate([...selection]);
150
+ if (required && selection.length === 0) {
151
+ setError('At least one choice must be selected');
152
+ } else if (isValid === true) {
153
+ setStatus('done');
154
+ done(selection.map((choice) => choice.value));
155
+ } else {
156
+ setError(isValid || 'You must select a valid value');
157
+ }
158
+ } else if (isUpKey(key, keybindings) || isDownKey(key, keybindings)) {
159
+ if (
160
+ loop ||
161
+ (isUpKey(key, keybindings) && active !== bounds.first) ||
162
+ (isDownKey(key, keybindings) && active !== bounds.last)
163
+ ) {
164
+ const offset = isUpKey(key, keybindings) ? -1 : 1;
165
+ let next = active;
166
+ do {
167
+ next = (next + offset + items.length) % items.length;
168
+ } while (!isSelectable(items[next]));
169
+ setActive(next);
170
+ }
171
+ } else if (isSpaceKey(key)) {
172
+ setError(undefined);
173
+ setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice)));
174
+ } else if (shortcuts.all && key.name === shortcuts.all) {
175
+ const selectAll = items.some((choice) => isSelectable(choice) && !choice.checked && choice.value !== cancelValue);
176
+ setItems(items.map((item) => {
177
+ if (isSelectable(item) && item.value !== cancelValue) {
178
+ return { ...item, checked: selectAll };
179
+ }
180
+ return item;
181
+ }));
182
+ } else if (shortcuts.invert && key.name === shortcuts.invert) {
183
+ setItems(items.map((item) => {
184
+ if (isSelectable(item) && item.value !== cancelValue) {
185
+ return toggle(item);
186
+ }
187
+ return item;
188
+ }));
189
+ } else if (isNumberKey(key)) {
190
+ const selectedIndex = Number(key.name) - 1;
191
+ let selectableIndex = -1;
192
+ const position = items.findIndex((item) => {
193
+ if (Separator.isSeparator(item)) return false;
194
+ selectableIndex++;
195
+ return selectableIndex === selectedIndex;
196
+ });
197
+ const selectedItem = items[position];
198
+ if (selectedItem && isSelectable(selectedItem)) {
199
+ setActive(position);
200
+ setItems(items.map((choice, i) => (i === position ? toggle(choice) : choice)));
201
+ }
202
+ }
203
+ });
204
+
205
+ const message = theme.style.message(config.message, status);
206
+ let description;
207
+ const page = usePagination({
208
+ items,
209
+ active,
210
+ renderItem({ item, isActive }) {
211
+ if (Separator.isSeparator(item)) {
212
+ return ` ${item.separator}`;
213
+ }
214
+ if (item.disabled) {
215
+ const disabledLabel = typeof item.disabled === 'string' ? item.disabled : '(disabled)';
216
+ return theme.style.disabledChoice(`${item.name} ${disabledLabel}`);
217
+ }
218
+ if (isActive) {
219
+ description = item.description;
220
+ }
221
+ const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
222
+ const name = item.checked ? item.checkedName : item.name;
223
+ const color = isActive ? theme.style.highlight : (x) => x;
224
+ const cursor = isActive ? theme.icon.cursor : ' ';
225
+ return color(`${cursor}${checkbox} ${name}`);
226
+ },
227
+ pageSize,
228
+ loop,
229
+ });
230
+
231
+ if (status === 'done') {
232
+ if (cancelled) {
233
+ return [prefix, message].filter(Boolean).join(' ');
234
+ }
235
+ const selection = items.filter(item => isChecked(item) && item.value !== cancelValue);
236
+ const answer = theme.style.answer(theme.style.renderSelectedChoices(selection, items));
237
+ return [prefix, message, answer].filter(Boolean).join(' ');
238
+ }
239
+
240
+ const keys = [
241
+ ['↑↓', 'navigate'],
242
+ ['space', 'select'],
243
+ ];
244
+ if (shortcuts.all) keys.push([shortcuts.all, 'all']);
245
+ if (shortcuts.invert) keys.push([shortcuts.invert, 'invert']);
246
+ keys.push(['⏎', 'submit']);
247
+ keys.push(['q', 'quit']);
248
+ const helpLine = theme.style.keysHelpTip(keys);
249
+ const lines = [
250
+ [prefix, message].filter(Boolean).join(' '),
251
+ page,
252
+ ' ',
253
+ description ? theme.style.description(description) : '',
254
+ errorMsg ? theme.style.error(errorMsg) : '',
255
+ helpLine,
256
+ ]
257
+ .filter(Boolean)
258
+ .join('\n')
259
+ .trimEnd();
260
+ return `${lines}${cursorHide}`;
261
+ });
262
+
263
+ export { Separator };
264
+ export default checkboxCancel;