skills-script 1.1.1 → 1.1.2
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/README.md +29 -35
- package/package.json +1 -1
- package/src/commands/interactive.js +9 -1
- package/src/commands/switch-setting.js +150 -0
package/README.md
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
1
|
# skills-script
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
交互式技能工具 CLI。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 运行
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx skills-script
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 脚本整体运作流程
|
|
12
12
|
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
1. 启动入口:执行 `bin/skill-script.js`,进入 `src/cli.js`。
|
|
14
|
+
2. 解析命令参数:
|
|
15
|
+
- 无参数:进入交互模式。
|
|
16
|
+
- `-h/--help/help`:输出帮助信息并退出。
|
|
17
|
+
- `-v/--version/version`:输出版本并退出。
|
|
18
|
+
- `sync ...`:进入非交互同步流程。
|
|
19
|
+
3. 解析工作目录:优先使用调用上下文目录(用于避免 npx 临时目录干扰)。
|
|
20
|
+
4. 执行对应流程:
|
|
21
|
+
- 交互模式:先选工具,再进入该工具的交互步骤。
|
|
22
|
+
- 非交互模式:直接按命令参数执行目标工具。
|
|
23
|
+
5. 返回退出码:
|
|
24
|
+
- 成功返回 `0`。
|
|
25
|
+
- 参数错误或运行失败返回非 `0`。
|
|
17
26
|
|
|
18
|
-
##
|
|
27
|
+
## 交互模式流程
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
1. 工具选择:
|
|
30
|
+
- `skills-sync`
|
|
31
|
+
- `claude-settings-switch`
|
|
32
|
+
2. 进入对应工具流程并执行。
|
|
33
|
+
3. `claude-settings-switch` 固定操作 `~/.claude`(例如 `/Users/youzi/.claude`)。
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
- `claude`
|
|
24
|
-
- `codex`
|
|
25
|
-
- `gemini`
|
|
26
|
-
|
|
27
|
-
Built-in targets are under command invocation directory:
|
|
28
|
-
|
|
29
|
-
- `claude` => `<cwd>/.claude/skills`
|
|
30
|
-
- `codex` => `<cwd>/.codex/skills`
|
|
31
|
-
- `gemini` => `<cwd>/.gemini/skills`
|
|
32
|
-
|
|
33
|
-
Custom token rules:
|
|
34
|
-
|
|
35
|
-
- `.aaa` => `<cwd>/.aaa/skills`
|
|
36
|
-
- absolute/relative path => used directly as target directory
|
|
37
|
-
|
|
38
|
-
## Source Skills Directory
|
|
39
|
-
|
|
40
|
-
The source is fixed to:
|
|
35
|
+
## 非交互模式
|
|
41
36
|
|
|
42
37
|
```bash
|
|
43
|
-
|
|
38
|
+
npx skills-script sync claude,codex,.aaa
|
|
44
39
|
```
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
## 工具实现文档
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
每个工具的实现细节单独记录:
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
45
|
+
- `docs/tools/skills-sync.md`
|
|
46
|
+
- `docs/tools/claude-settings-switch.md`
|
|
53
47
|
|
|
54
|
-
##
|
|
48
|
+
## 发布
|
|
55
49
|
|
|
56
50
|
```bash
|
|
57
51
|
cd /Users/youzi/Desktop/__yz/skills-script
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { multiSelect, promptInput, selectOne } from '../lib/interactive.js';
|
|
3
3
|
import { resolveTargetDirs, runSyncByTargets } from './sync.js';
|
|
4
|
+
import { runClaudeSettingSwitch } from './switch-setting.js';
|
|
4
5
|
|
|
5
6
|
function parseCustomInput(input) {
|
|
6
7
|
if (!input) return [];
|
|
@@ -13,9 +14,16 @@ function parseCustomInput(input) {
|
|
|
13
14
|
export async function runInteractive(cwd = process.cwd()) {
|
|
14
15
|
const tool = await selectOne({
|
|
15
16
|
title: 'Select a tool',
|
|
16
|
-
options: [
|
|
17
|
+
options: [
|
|
18
|
+
{ label: 'skills-sync', value: 'skills-sync' },
|
|
19
|
+
{ label: 'claude-settings-switch', value: 'claude-settings-switch' }
|
|
20
|
+
]
|
|
17
21
|
});
|
|
18
22
|
|
|
23
|
+
if (tool === 'claude-settings-switch') {
|
|
24
|
+
return runClaudeSettingSwitch();
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
if (tool !== 'skills-sync') {
|
|
20
28
|
console.error('[ERROR] Unsupported tool selected.');
|
|
21
29
|
return 1;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { selectOne } from '../lib/interactive.js';
|
|
5
|
+
|
|
6
|
+
function findAnthropicBaseUrlValue(input) {
|
|
7
|
+
if (!input || typeof input !== 'object') return null;
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
Object.prototype.hasOwnProperty.call(input, 'ANTHROPIC_BASE_URL') &&
|
|
11
|
+
typeof input.ANTHROPIC_BASE_URL === 'string'
|
|
12
|
+
) {
|
|
13
|
+
return input.ANTHROPIC_BASE_URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const value of Object.values(input)) {
|
|
17
|
+
if (!value || typeof value !== 'object') continue;
|
|
18
|
+
const nested = findAnthropicBaseUrlValue(value);
|
|
19
|
+
if (nested) return nested;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeBaseUrlName(value) {
|
|
26
|
+
if (typeof value !== 'string') return null;
|
|
27
|
+
const raw = value.trim();
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
|
|
30
|
+
let host = raw;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(raw.includes('://') ? raw : `https://${raw}`);
|
|
33
|
+
host = parsed.hostname || raw;
|
|
34
|
+
} catch {
|
|
35
|
+
host = raw.replace(/^[a-zA-Z]+:\/\//, '').split('/')[0] || raw;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cleaned = host
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9.-]+/g, '-')
|
|
41
|
+
.replace(/-+/g, '-')
|
|
42
|
+
.replace(/^-+|-+$/g, '');
|
|
43
|
+
|
|
44
|
+
return cleaned || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listSettingsFiles(claudeDir) {
|
|
48
|
+
const entries = await fs.readdir(claudeDir, { withFileTypes: true });
|
|
49
|
+
return entries
|
|
50
|
+
.filter((entry) => entry.isFile() && /^settings.*\.json$/i.test(entry.name))
|
|
51
|
+
.map((entry) => entry.name)
|
|
52
|
+
.sort((a, b) => a.localeCompare(b));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getUniqueBackupPath(claudeDir, preferredName) {
|
|
56
|
+
const preferredPath = path.join(claudeDir, preferredName);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(preferredPath);
|
|
60
|
+
} catch {
|
|
61
|
+
return preferredPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
65
|
+
const fallbackName = preferredName.replace(/\.json$/i, `.${stamp}.json`);
|
|
66
|
+
return path.join(claudeDir, fallbackName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runClaudeSettingSwitch() {
|
|
70
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
71
|
+
const activeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
72
|
+
|
|
73
|
+
let claudeStat;
|
|
74
|
+
try {
|
|
75
|
+
claudeStat = await fs.stat(claudeDir);
|
|
76
|
+
} catch {
|
|
77
|
+
console.error(`[ERROR] Claude directory not found: ${claudeDir}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!claudeStat.isDirectory()) {
|
|
82
|
+
console.error(`[ERROR] Not a directory: ${claudeDir}`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let candidates = [];
|
|
87
|
+
try {
|
|
88
|
+
candidates = await listSettingsFiles(claudeDir);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`[ERROR] Failed to read settings files: ${error?.message || error}`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!candidates.length) {
|
|
95
|
+
console.error(`[ERROR] No settings*.json found in ${claudeDir}`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selected = await selectOne({
|
|
100
|
+
title: 'Select target Claude settings file',
|
|
101
|
+
options: candidates.map((name) => ({ label: name, value: name }))
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (selected === 'settings.json') {
|
|
105
|
+
console.log('[INFO] settings.json is already selected. Nothing to switch.');
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let activeJson;
|
|
110
|
+
try {
|
|
111
|
+
const raw = await fs.readFile(activeSettingsPath, 'utf8');
|
|
112
|
+
activeJson = JSON.parse(raw);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`[ERROR] Failed to read ${activeSettingsPath}: ${error?.message || error}`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const baseUrlValue = findAnthropicBaseUrlValue(activeJson);
|
|
119
|
+
const baseUrlName = normalizeBaseUrlName(baseUrlValue);
|
|
120
|
+
if (!baseUrlName) {
|
|
121
|
+
console.error('[ERROR] ANTHROPIC_BASE_URL not found in current settings.json');
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const backupPath = await getUniqueBackupPath(claudeDir, `settings.${baseUrlName}.json`);
|
|
126
|
+
const selectedPath = path.join(claudeDir, selected);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await fs.rename(activeSettingsPath, backupPath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`[ERROR] Failed to backup current settings: ${error?.message || error}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await fs.rename(selectedPath, activeSettingsPath);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.rename(backupPath, activeSettingsPath);
|
|
140
|
+
} catch {
|
|
141
|
+
// If rollback also fails, preserve original failure message below.
|
|
142
|
+
}
|
|
143
|
+
console.error(`[ERROR] Failed to activate selected settings: ${error?.message || error}`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`[OK] backup: ${path.basename(backupPath)}`);
|
|
148
|
+
console.log(`[OK] active: settings.json <= ${selected}`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|