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 +105 -144
- package/locales/en.json +2 -1
- package/locales/zh-TW.json +2 -1
- package/package.json +4 -1
package/bin/index.js
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
-
const fs
|
|
10
|
-
const path
|
|
11
|
-
const 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
|
|
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
|
|
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
|
|
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;
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
//
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 (
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
264
|
-
T.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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": "
|
|
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",
|
package/locales/zh-TW.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"tools_prompt": "選擇要安裝的工具(空白鍵切換,Enter 確認):",
|
|
8
8
|
"scope_prompt": "安裝範圍:",
|
|
9
9
|
"scope_global": "全域(~/.claude/skills、~/.cursor/rules 等)",
|
|
10
|
-
"scope_project": "
|
|
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.
|
|
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": {
|