tsic-ainode 1.0.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,52 +46,51 @@ 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
- id: 'claude',
58
- nameKey: 'tool_claude',
59
- detect: () => fs.existsSync(path.join(HOME, '.claude')),
60
- install: installClaude,
62
+ id: 'claude',
63
+ nameKey: 'tool_claude',
64
+ detect: () => fs.existsSync(path.join(HOME, '.claude')),
65
+ install: installClaude,
61
66
  },
62
67
  {
63
- id: 'cursor',
64
- nameKey: 'tool_cursor',
65
- detect: () => fs.existsSync(path.join(HOME, '.cursor')) ||
66
- fs.existsSync(path.join(CWD, '.cursor')),
67
- install: installCursor,
68
+ id: 'cursor',
69
+ nameKey: 'tool_cursor',
70
+ detect: () => cmdExists('cursor') ||
71
+ fs.existsSync(path.join(HOME, '.cursor')),
72
+ install: installCursor,
68
73
  },
69
74
  {
70
- id: 'codex',
71
- nameKey: 'tool_codex',
72
- detect: () => cmdExists('codex') || fs.existsSync(path.join(HOME, '.codex')),
73
- install: installCodex,
75
+ id: 'codex',
76
+ nameKey: 'tool_codex',
77
+ detect: () => cmdExists('codex'),
78
+ install: installCodex,
74
79
  },
75
80
  {
76
- id: 'gemini',
77
- nameKey: 'tool_gemini',
78
- detect: () => cmdExists('gemini') || fs.existsSync(path.join(HOME, '.gemini')),
79
- install: installGemini,
81
+ id: 'gemini',
82
+ nameKey: 'tool_gemini',
83
+ detect: () => cmdExists('gemini') || cmdExists('gemini-cli'),
84
+ install: installGemini,
80
85
  },
81
86
  {
82
- id: 'antigravity',
83
- nameKey: 'tool_antigravity',
84
- detect: () => cmdExists('antigravity') ||
85
- fs.existsSync(path.join(HOME, '.antigravity')),
86
- install: installAntigravity,
87
+ id: 'antigravity',
88
+ nameKey: 'tool_antigravity',
89
+ detect: () => cmdExists('antigravity'),
90
+ install: installAntigravity,
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
  // ---------------------------------------------------------------------------
@@ -105,11 +100,11 @@ function copyTemplate(src, dest) {
105
100
  }
106
101
 
107
102
  function appendToFile(src, dest) {
108
- const tag = '<!-- tsic-ainode -->';
103
+ const tag = '<!-- tsic-ainode -->';
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,180 +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, resolve));
168
- }
169
-
170
- function askChoice(rl, question, choices) {
171
- return new Promise(resolve => {
172
- const opts = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
173
- rl.question(`${question}\n${opts}\n> `, ans => {
174
- const idx = parseInt(ans.trim(), 10) - 1;
175
- resolve(choices[Math.max(0, Math.min(idx, choices.length - 1))]);
176
- });
177
- });
178
- }
179
-
180
- async function askCheckbox(rl, question, items) {
181
- console.log(`\n${question}`);
182
- const selected = new Set(items.filter(i => i.detected).map(i => i.id));
183
- items.forEach((item, i) => {
184
- const check = selected.has(item.id) ? '◉' : '○';
185
- const hint = item.detected ? ' (detected)' : '';
186
- console.log(` ${i + 1}. ${check} ${item.label}${hint}`);
187
- });
188
- console.log(' (Enter numbers to toggle, e.g. "1 3", or Enter to confirm)');
189
-
190
- while (true) {
191
- const ans = await ask(rl, '> ');
192
- if (ans.trim() === '') break;
193
- ans.trim().split(/\s+/).forEach(n => {
194
- const idx = parseInt(n, 10) - 1;
195
- if (idx >= 0 && idx < items.length) {
196
- const id = items[idx].id;
197
- selected.has(id) ? selected.delete(id) : selected.add(id);
198
- }
199
- });
200
- // redraw
201
- items.forEach((item, i) => {
202
- const check = selected.has(item.id) ? '◉' : '○';
203
- process.stdout.write(` ${i + 1}. ${check} ${item.label}\n`);
204
- });
205
- console.log(' (Enter to confirm, or toggle more)');
206
- }
207
- return items.filter(i => selected.has(i.id));
208
- }
209
-
210
154
  // ---------------------------------------------------------------------------
211
155
  // Main
212
156
  // ---------------------------------------------------------------------------
213
157
  async function main() {
214
- // Load default locale first
215
- let locale = detectLocale();
216
- loadLocale(locale);
217
-
218
- const rl = createRL();
219
-
220
- // Banner
221
- console.log('\n' + ''.repeat(50));
222
- console.log(` ${t('welcome')}`);
223
- console.log(` ${t('welcome_sub')}`);
224
- console.log(''.repeat(50));
225
-
226
- // Language selection
227
- const langChoice = await askChoice(rl, `\n${t('lang_prompt')}`, [
228
- '繁體中文 (zh-TW)',
229
- 'English (en)',
230
- ]);
231
- locale = langChoice.startsWith('繁') ? 'zh-TW' : 'en';
232
- 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);
233
173
 
234
174
  // Detect tools
235
- const detectedIds = TOOLS.filter(t => t.detect()).map(t => t.id);
236
- if (detectedIds.length > 0) {
237
- console.log(`\n${T.detect_title}`);
238
- detectedIds.forEach(id => {
239
- const tool = TOOLS.find(t => t.id === id);
240
- console.log(` ✓ ${T[tool.nameKey] || tool.id}`);
241
- });
242
- } else {
243
- console.log(`\n${T.detect_none}`);
244
- }
245
-
246
- // Choose tools
247
- const toolItems = TOOLS.map(tool => ({
248
- id: tool.id,
249
- label: T[tool.nameKey] || tool.id,
250
- detected: detectedIds.includes(tool.id),
251
- install: tool.install,
252
- }));
253
- const chosen = await askCheckbox(rl, T.tools_prompt, toolItems);
175
+ const detectedIds = TOOLS.filter(tool => tool.detect()).map(tool => tool.id);
176
+
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
+ });
254
190
 
255
- if (chosen.length === 0) {
256
- console.log('\nNo tools selected. Exiting.');
257
- rl.close();
258
- return;
191
+ if (clack.isCancel(selectedIds) || selectedIds.length === 0) {
192
+ clack.cancel(langVal === 'zh-TW' ? '未選擇任何工具。' : 'No tools selected.');
193
+ process.exit(0);
259
194
  }
260
195
 
261
196
  // Scope
262
- const scopeChoice = await askChoice(rl, `\n${T.scope_prompt}`, [
263
- T.scope_global,
264
- T.scope_project,
265
- ]);
266
- 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
+ }
267
221
 
268
222
  // Install
269
- console.log(`\n${T.installing}`);
223
+ const spinner = clack.spinner();
224
+ spinner.start(T.installing || 'Installing...');
225
+
270
226
  const results = [];
271
- for (const tool of chosen) {
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}`);
276
- results.push({ tool, dest, ok: true });
230
+ const dest = tool.install(scope, projectPath);
231
+ results.push({ label: T[tool.nameKey] || tool.id, dest, ok: true });
277
232
  } catch (err) {
278
- console.error(` ✗ ${T.err_write}: ${tool.label} ${err.message}`);
279
- results.push({ tool, ok: false });
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}`);
280
244
  }
281
245
  }
282
246
 
283
- // Done
284
- console.log('\n' + ''.repeat(50));
285
- console.log(` ${T.done_title}`);
286
- console.log('─'.repeat(50));
287
- console.log(`\n${T.done_usage}`);
288
- console.log(` ${T.done_example_zh}`);
289
- console.log(` ${T.done_example_en}`);
290
- console.log(`\n${T.done_docs} https://github.com/TSIC-tech/TSIC-AINode`);
291
- console.log();
247
+ clack.note(
248
+ `${T.done_example_zh || ''}\n${T.done_example_en || ''}`,
249
+ T.done_usage || 'Usage'
250
+ );
292
251
 
293
- rl.close();
252
+ clack.outro(`${T.done_docs || 'Docs'}: https://github.com/TSIC-tech/TSIC-AINode`);
294
253
  }
295
254
 
296
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.0.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": {