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 CHANGED
@@ -1,57 +1,51 @@
1
1
  # skills-script
2
2
 
3
- Interactive skills toolkit CLI.
3
+ 交互式技能工具 CLI
4
4
 
5
- ## Run
5
+ ## 运行
6
6
 
7
7
  ```bash
8
8
  npx skills-script
9
9
  ```
10
10
 
11
- Flow:
11
+ ## 脚本整体运作流程
12
12
 
13
- 1. Select tool (currently: `skills-sync`).
14
- 2. Select platforms with Space (multi-select).
15
- 3. Press Enter to confirm.
16
- 4. Optional: input custom platform path tokens (for example: `.aaa`).
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
- ## Platform Selection
27
+ ## 交互模式流程
19
28
 
20
- Built-in options:
29
+ 1. 工具选择:
30
+ - `skills-sync`
31
+ - `claude-settings-switch`
32
+ 2. 进入对应工具流程并执行。
33
+ 3. `claude-settings-switch` 固定操作 `~/.claude`(例如 `/Users/youzi/.claude`)。
21
34
 
22
- - `all`
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
- <command invocation directory>/.agents/skills
38
+ npx skills-script sync claude,codex,.aaa
44
39
  ```
45
40
 
46
- When running through npm/npx, the tool prefers `INIT_CWD` to avoid temp-cache working directory issues.
41
+ ## 工具实现文档
47
42
 
48
- ## Non-Interactive Mode
43
+ 每个工具的实现细节单独记录:
49
44
 
50
- ```bash
51
- npx skills-script sync claude,codex,.aaa
52
- ```
45
+ - `docs/tools/skills-sync.md`
46
+ - `docs/tools/claude-settings-switch.md`
53
47
 
54
- ## Publish
48
+ ## 发布
55
49
 
56
50
  ```bash
57
51
  cd /Users/youzi/Desktop/__yz/skills-script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-script",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Interactive skills toolkit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: [{ label: 'skills-sync', value: 'skills-sync' }]
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
+ }