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 +127 -168
- 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,52 +46,51 @@ 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
|
-
id:
|
|
58
|
-
nameKey:
|
|
59
|
-
detect:
|
|
60
|
-
install:
|
|
62
|
+
id: 'claude',
|
|
63
|
+
nameKey: 'tool_claude',
|
|
64
|
+
detect: () => fs.existsSync(path.join(HOME, '.claude')),
|
|
65
|
+
install: installClaude,
|
|
61
66
|
},
|
|
62
67
|
{
|
|
63
|
-
id:
|
|
64
|
-
nameKey:
|
|
65
|
-
detect:
|
|
66
|
-
|
|
67
|
-
install:
|
|
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:
|
|
71
|
-
nameKey:
|
|
72
|
-
detect:
|
|
73
|
-
install:
|
|
75
|
+
id: 'codex',
|
|
76
|
+
nameKey: 'tool_codex',
|
|
77
|
+
detect: () => cmdExists('codex'),
|
|
78
|
+
install: installCodex,
|
|
74
79
|
},
|
|
75
80
|
{
|
|
76
|
-
id:
|
|
77
|
-
nameKey:
|
|
78
|
-
detect:
|
|
79
|
-
install:
|
|
81
|
+
id: 'gemini',
|
|
82
|
+
nameKey: 'tool_gemini',
|
|
83
|
+
detect: () => cmdExists('gemini') || cmdExists('gemini-cli'),
|
|
84
|
+
install: installGemini,
|
|
80
85
|
},
|
|
81
86
|
{
|
|
82
|
-
id:
|
|
83
|
-
nameKey:
|
|
84
|
-
detect:
|
|
85
|
-
|
|
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
|
|
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;
|
|
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(
|
|
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, 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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 (
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
263
|
-
T.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
223
|
+
const spinner = clack.spinner();
|
|
224
|
+
spinner.start(T.installing || 'Installing...');
|
|
225
|
+
|
|
270
226
|
const results = [];
|
|
271
|
-
for (const
|
|
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}`);
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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": "
|
|
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": {
|