tsic-ainode 1.1.0 → 1.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.
package/bin/index.js CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const fs = require('fs');
10
- const path = require('path');
11
- const os = require('os');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
12
  const { execSync } = require('child_process');
13
- const readline = require('readline');
13
+ const clack = require('@clack/prompts');
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
16
  // Paths
@@ -19,7 +19,6 @@ const PKG_ROOT = path.join(__dirname, '..');
19
19
  const LOCALES_DIR = path.join(PKG_ROOT, 'locales');
20
20
  const TMPL_DIR = path.join(PKG_ROOT, 'templates');
21
21
  const HOME = os.homedir();
22
- const CWD = process.cwd();
23
22
  const IS_WIN = process.platform === 'win32';
24
23
 
25
24
  // ---------------------------------------------------------------------------
@@ -27,7 +26,7 @@ const IS_WIN = process.platform === 'win32';
27
26
  // ---------------------------------------------------------------------------
28
27
  let T = {};
29
28
  function loadLocale(lang) {
30
- const file = path.join(LOCALES_DIR, `${lang}.json`);
29
+ const file = path.join(LOCALES_DIR, `${lang}.json`);
31
30
  const fallback = path.join(LOCALES_DIR, 'en.json');
32
31
  try {
33
32
  T = JSON.parse(fs.readFileSync(fs.existsSync(file) ? file : fallback, 'utf8'));
@@ -40,9 +39,6 @@ function t(key, fallback = key) {
40
39
  return T[key] || fallback;
41
40
  }
42
41
 
43
- // ---------------------------------------------------------------------------
44
- // Detect OS locale
45
- // ---------------------------------------------------------------------------
46
42
  function detectLocale() {
47
43
  const env = process.env.LANG || process.env.LC_ALL || process.env.LANGUAGE || '';
48
44
  if (env.toLowerCase().includes('zh')) return 'zh-TW';
@@ -50,8 +46,17 @@ function detectLocale() {
50
46
  }
51
47
 
52
48
  // ---------------------------------------------------------------------------
53
- // Tool definitions
49
+ // Tool detection
54
50
  // ---------------------------------------------------------------------------
51
+ function cmdExists(cmd) {
52
+ try {
53
+ execSync(IS_WIN ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
55
60
  const TOOLS = [
56
61
  {
57
62
  id: 'claude',
@@ -63,8 +68,7 @@ const TOOLS = [
63
68
  id: 'cursor',
64
69
  nameKey: 'tool_cursor',
65
70
  detect: () => cmdExists('cursor') ||
66
- fs.existsSync(path.join(HOME, '.cursor')) ||
67
- fs.existsSync(path.join(CWD, '.cursor')),
71
+ fs.existsSync(path.join(HOME, '.cursor')),
68
72
  install: installCursor,
69
73
  },
70
74
  {
@@ -87,15 +91,6 @@ const TOOLS = [
87
91
  },
88
92
  ];
89
93
 
90
- function cmdExists(cmd) {
91
- try {
92
- execSync(IS_WIN ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
93
- return true;
94
- } catch {
95
- return false;
96
- }
97
- }
98
-
99
94
  // ---------------------------------------------------------------------------
100
95
  // Install functions
101
96
  // ---------------------------------------------------------------------------
@@ -109,7 +104,7 @@ function appendToFile(src, dest) {
109
104
  const content = fs.readFileSync(src, 'utf8');
110
105
  if (fs.existsSync(dest)) {
111
106
  const existing = fs.readFileSync(dest, 'utf8');
112
- if (existing.includes(tag)) return; // already installed
107
+ if (existing.includes(tag)) return;
113
108
  fs.appendFileSync(dest, `\n\n${tag}\n${content}`);
114
109
  } else {
115
110
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -117,178 +112,144 @@ function appendToFile(src, dest) {
117
112
  }
118
113
  }
119
114
 
120
- function installClaude(scope) {
115
+ function installClaude(scope, projectPath) {
121
116
  const dest = path.join(HOME, '.claude', 'skills', 'tsic-ainode', 'SKILL.md');
122
117
  copyTemplate(path.join(TMPL_DIR, 'claude-code', 'SKILL.md'), dest);
123
118
  return dest;
124
119
  }
125
120
 
126
- function installCursor(scope) {
121
+ function installCursor(scope, projectPath) {
127
122
  const rulesDir = scope === 'global'
128
123
  ? path.join(HOME, '.cursor', 'rules')
129
- : path.join(CWD, '.cursor', 'rules');
124
+ : path.join(projectPath, '.cursor', 'rules');
130
125
  const dest = path.join(rulesDir, 'tsic-ainode.mdc');
131
126
  copyTemplate(path.join(TMPL_DIR, 'cursor', 'tsic-ainode.mdc'), dest);
132
127
  return dest;
133
128
  }
134
129
 
135
- function installCodex(scope) {
130
+ function installCodex(scope, projectPath) {
136
131
  const dest = scope === 'global'
137
132
  ? path.join(HOME, '.codex', 'AGENTS.md')
138
- : path.join(CWD, 'AGENTS.md');
133
+ : path.join(projectPath, 'AGENTS.md');
139
134
  appendToFile(path.join(TMPL_DIR, 'codex', 'AGENTS.md'), dest);
140
135
  return dest;
141
136
  }
142
137
 
143
- function installGemini(scope) {
138
+ function installGemini(scope, projectPath) {
144
139
  const dest = scope === 'global'
145
140
  ? path.join(HOME, '.gemini', 'GEMINI.md')
146
- : path.join(CWD, 'GEMINI.md');
141
+ : path.join(projectPath, 'GEMINI.md');
147
142
  appendToFile(path.join(TMPL_DIR, 'gemini-cli', 'GEMINI.md'), dest);
148
143
  return dest;
149
144
  }
150
145
 
151
- function installAntigravity(scope) {
146
+ function installAntigravity(scope, projectPath) {
152
147
  const dest = scope === 'global'
153
148
  ? path.join(HOME, '.antigravity', 'AGENTS.md')
154
- : path.join(CWD, '.antigravity', 'AGENTS.md');
149
+ : path.join(projectPath, '.antigravity', 'AGENTS.md');
155
150
  appendToFile(path.join(TMPL_DIR, 'antigravity', 'AGENTS.md'), dest);
156
151
  return dest;
157
152
  }
158
153
 
159
- // ---------------------------------------------------------------------------
160
- // Readline helpers
161
- // ---------------------------------------------------------------------------
162
- function createRL() {
163
- return readline.createInterface({ input: process.stdin, output: process.stdout });
164
- }
165
-
166
- function ask(rl, question) {
167
- return new Promise(resolve => rl.question(question, ans => resolve(ans.trim())));
168
- }
169
-
170
- // Single-prompt choice: shows numbered list, returns chosen item (default: first)
171
- async function askChoice(rl, question, choices) {
172
- const opts = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
173
- const ans = await ask(rl, `${question}\n${opts}\n> `);
174
- const idx = parseInt(ans, 10) - 1;
175
- if (isNaN(idx) || idx < 0 || idx >= choices.length) return choices[0];
176
- return choices[idx];
177
- }
178
-
179
- // Single-prompt checkbox: shows numbered list with detected pre-selected.
180
- // User types space-separated numbers to select, or Enter to accept detected defaults.
181
- async function askCheckbox(rl, question, items) {
182
- const defaultNums = items
183
- .map((item, i) => (item.detected ? String(i + 1) : null))
184
- .filter(Boolean);
185
-
186
- console.log(`\n${question}`);
187
- items.forEach((item, i) => {
188
- const mark = item.detected ? '[✓]' : '[ ]';
189
- console.log(` ${i + 1}. ${mark} ${item.label}`);
190
- });
191
-
192
- const hint = defaultNums.length > 0
193
- ? `(Enter 確認選取 ${defaultNums.join(' ')},或輸入編號如 "1 3")`
194
- : '(輸入編號如 "1 3",多選用空白分隔)';
195
- console.log(` ${hint}`);
196
-
197
- const ans = await ask(rl, '> ');
198
-
199
- if (ans === '' && defaultNums.length > 0) {
200
- return items.filter(item => item.detected);
201
- }
202
-
203
- const selected = ans.split(/\s+/)
204
- .map(n => parseInt(n, 10) - 1)
205
- .filter(i => i >= 0 && i < items.length)
206
- .map(i => items[i]);
207
-
208
- return selected;
209
- }
210
-
211
154
  // ---------------------------------------------------------------------------
212
155
  // Main
213
156
  // ---------------------------------------------------------------------------
214
157
  async function main() {
215
- // Load default locale first
216
- let locale = detectLocale();
217
- loadLocale(locale);
218
-
219
- const rl = createRL();
220
-
221
- // Banner
222
- console.log('\n' + ''.repeat(50));
223
- console.log(` ${t('welcome')}`);
224
- console.log(` ${t('welcome_sub')}`);
225
- console.log(''.repeat(50));
226
-
227
- // Language selection
228
- const langChoice = await askChoice(rl, `\n${t('lang_prompt')}`, [
229
- '繁體中文 (zh-TW)',
230
- 'English (en)',
231
- ]);
232
- locale = langChoice.startsWith('繁') ? 'zh-TW' : 'en';
233
- loadLocale(locale);
158
+ // Load default locale
159
+ loadLocale(detectLocale());
160
+
161
+ clack.intro(` ${t('welcome')} `);
162
+
163
+ // Language
164
+ const langVal = await clack.select({
165
+ message: t('lang_prompt', 'Language'),
166
+ options: [
167
+ { value: 'zh-TW', label: '繁體中文' },
168
+ { value: 'en', label: 'English' },
169
+ ],
170
+ });
171
+ if (clack.isCancel(langVal)) { clack.cancel('Cancelled.'); process.exit(0); }
172
+ loadLocale(langVal);
234
173
 
235
174
  // Detect tools
236
175
  const detectedIds = TOOLS.filter(tool => tool.detect()).map(tool => tool.id);
237
- if (detectedIds.length > 0) {
238
- console.log(`\n${T.detect_title}`);
239
- detectedIds.forEach(id => {
240
- const tool = TOOLS.find(tool => tool.id === id);
241
- console.log(` ✓ ${T[tool.nameKey] || tool.id}`);
242
- });
243
- } else {
244
- console.log(`\n${T.detect_none}`);
245
- }
246
176
 
247
- // Choose tools
248
- const toolItems = TOOLS.map(tool => ({
249
- id: tool.id,
250
- label: T[tool.nameKey] || tool.id,
251
- detected: detectedIds.includes(tool.id),
252
- install: tool.install,
253
- }));
254
- const chosen = await askCheckbox(rl, T.tools_prompt, toolItems);
177
+ // Tool selection — arrow keys + space to toggle, Enter to confirm
178
+ const selectedIds = await clack.multiselect({
179
+ message: T.tools_prompt || 'Select tools to install',
180
+ options: TOOLS.map(tool => ({
181
+ value: tool.id,
182
+ label: T[tool.nameKey] || tool.id,
183
+ hint: detectedIds.includes(tool.id)
184
+ ? (langVal === 'zh-TW' ? '已偵測' : 'detected')
185
+ : undefined,
186
+ })),
187
+ initialValues: detectedIds,
188
+ required: false,
189
+ });
255
190
 
256
- if (chosen.length === 0) {
257
- console.log('\nNo tools selected. Exiting.');
258
- rl.close();
259
- return;
191
+ if (clack.isCancel(selectedIds) || selectedIds.length === 0) {
192
+ clack.cancel(langVal === 'zh-TW' ? '未選擇任何工具。' : 'No tools selected.');
193
+ process.exit(0);
260
194
  }
261
195
 
262
196
  // Scope
263
- const scopeChoice = await askChoice(rl, `\n${T.scope_prompt}`, [
264
- T.scope_global,
265
- T.scope_project,
266
- ]);
267
- const scope = scopeChoice === T.scope_global ? 'global' : 'project';
197
+ const scope = await clack.select({
198
+ message: T.scope_prompt || 'Installation scope',
199
+ options: [
200
+ { value: 'global', label: T.scope_global || 'Global' },
201
+ { value: 'project', label: T.scope_project || 'Project' },
202
+ ],
203
+ });
204
+ if (clack.isCancel(scope)) { clack.cancel('Cancelled.'); process.exit(0); }
205
+
206
+ // Project path (only when scope = project)
207
+ let projectPath = process.cwd();
208
+ if (scope === 'project') {
209
+ const inputPath = await clack.text({
210
+ message: T.project_path_prompt || 'Target project directory',
211
+ placeholder: process.cwd(),
212
+ defaultValue: process.cwd(),
213
+ validate(v) {
214
+ const p = (v || '').trim() || process.cwd();
215
+ if (!fs.existsSync(p)) return `Directory does not exist: ${p}`;
216
+ },
217
+ });
218
+ if (clack.isCancel(inputPath)) { clack.cancel('Cancelled.'); process.exit(0); }
219
+ if (inputPath && inputPath.trim()) projectPath = inputPath.trim();
220
+ }
268
221
 
269
222
  // Install
270
- console.log(`\n${T.installing}`);
271
- for (const tool of chosen) {
223
+ const spinner = clack.spinner();
224
+ spinner.start(T.installing || 'Installing...');
225
+
226
+ const results = [];
227
+ for (const toolId of selectedIds) {
228
+ const tool = TOOLS.find(tool => tool.id === toolId);
272
229
  try {
273
- const dest = tool.install(scope);
274
- console.log(` ✓ ${T.installed_ok}: ${tool.label}`);
275
- console.log(` → ${dest}`);
230
+ const dest = tool.install(scope, projectPath);
231
+ results.push({ label: T[tool.nameKey] || tool.id, dest, ok: true });
276
232
  } catch (err) {
277
- console.error(` ✗ ${T.err_write}: ${tool.label} ${err.message}`);
233
+ results.push({ label: T[tool.nameKey] || tool.id, err: err.message, ok: false });
234
+ }
235
+ }
236
+
237
+ spinner.stop(T.done_title || 'Done!');
238
+
239
+ for (const r of results) {
240
+ if (r.ok) {
241
+ clack.log.success(`${r.label}\n → ${r.dest}`);
242
+ } else {
243
+ clack.log.error(`${r.label}: ${r.err}`);
278
244
  }
279
245
  }
280
246
 
281
- // Done
282
- console.log('\n' + ''.repeat(50));
283
- console.log(` ${T.done_title}`);
284
- console.log('─'.repeat(50));
285
- console.log(`\n${T.done_usage}`);
286
- console.log(` ${T.done_example_zh}`);
287
- console.log(` ${T.done_example_en}`);
288
- console.log(`\n${T.done_docs} https://github.com/TSIC-tech/TSIC-AINode`);
289
- console.log();
247
+ clack.note(
248
+ `${T.done_example_zh || ''}\n${T.done_example_en || ''}`,
249
+ T.done_usage || 'Usage'
250
+ );
290
251
 
291
- rl.close();
252
+ clack.outro(`${T.done_docs || 'Docs'}: https://github.com/TSIC-tech/TSIC-AINode`);
292
253
  }
293
254
 
294
255
  main().catch(err => {
package/locales/en.json CHANGED
@@ -7,7 +7,8 @@
7
7
  "tools_prompt": "Select tools to install (space to toggle, Enter to confirm):",
8
8
  "scope_prompt": "Installation scope:",
9
9
  "scope_global": "Global (~/.claude/skills, ~/.cursor/rules, etc.)",
10
- "scope_project": "Current project folder (.cursor/rules, AGENTS.md, etc.)",
10
+ "scope_project": "Specific project folder",
11
+ "project_path_prompt": "Target project directory (Enter to use current folder)",
11
12
  "installing": "Installing...",
12
13
  "installed_ok": "✓ Installed",
13
14
  "installed_skip": "- Skipped",
@@ -7,7 +7,8 @@
7
7
  "tools_prompt": "選擇要安裝的工具(空白鍵切換,Enter 確認):",
8
8
  "scope_prompt": "安裝範圍:",
9
9
  "scope_global": "全域(~/.claude/skills、~/.cursor/rules 等)",
10
- "scope_project": "目前專案資料夾(.cursor/rules、AGENTS.md 等)",
10
+ "scope_project": "指定專案資料夾",
11
+ "project_path_prompt": "目標專案目錄(直接 Enter 使用目前資料夾)",
11
12
  "installing": "安裝中...",
12
13
  "installed_ok": "✓ 已安裝",
13
14
  "installed_skip": "- 略過",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsic-ainode",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "TSIC AINode Skill installer — adds AINode SSH Dongle support to Claude Code, Cursor, Codex, Gemini CLI, and Antigravity",
5
5
  "bin": {
6
6
  "tsic-ainode": "bin/index.js"
@@ -20,6 +20,9 @@
20
20
  "skill",
21
21
  "mcp"
22
22
  ],
23
+ "dependencies": {
24
+ "@clack/prompts": "^0.9.0"
25
+ },
23
26
  "author": "TSIC",
24
27
  "license": "MIT",
25
28
  "engines": {