easyai 1.7.0 → 2.0.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.
- checksums.yaml +4 -4
- data/AGENTS.md +10 -8
- data/CLAUDE.md +211 -126
- data/README.md +176 -36
- data/easyai.gemspec +12 -9
- data/lib/easyai/base/secret_masker.rb +39 -0
- data/lib/easyai/base/system_info.rb +17 -203
- data/lib/easyai/command/ai_tool_base.rb +218 -0
- data/lib/easyai/command/backup/claude.rb +124 -0
- data/lib/easyai/command/backup.rb +23 -0
- data/lib/easyai/command/claude.rb +72 -357
- data/lib/easyai/command/clean.rb +90 -395
- data/lib/easyai/command/codex.rb +39 -0
- data/lib/easyai/command/gemini.rb +23 -41
- data/lib/easyai/command/restore/claude.rb +150 -0
- data/lib/easyai/command/restore.rb +24 -0
- data/lib/easyai/command/setup.rb +487 -378
- data/lib/easyai/command/update.rb +39 -188
- data/lib/easyai/command/utils.rb +2 -7
- data/lib/easyai/command.rb +1 -3
- data/lib/easyai/config/local_config.rb +161 -0
- data/lib/easyai/version.rb +1 -1
- data/lib/easyai.rb +29 -35
- metadata +20 -37
- data/lib/easyai/auth/authclaude.rb +0 -519
- data/lib/easyai/auth/jpsloginhelper.rb +0 -98
- data/lib/easyai/base/system_keychain.rb +0 -283
- data/lib/easyai/command/gpt.rb +0 -259
- data/lib/easyai/command/utils/export.rb +0 -263
- data/lib/easyai/config/config.rb +0 -357
- data/lib/easyai/config/easyai_config.rb +0 -258
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'etc'
|
|
5
|
+
|
|
6
|
+
module EasyAI
|
|
7
|
+
class Command
|
|
8
|
+
class Restore
|
|
9
|
+
class Claude < Restore
|
|
10
|
+
self.summary = '恢复 Claude Code 登录信息'
|
|
11
|
+
self.description = <<-DESC
|
|
12
|
+
读取 ~/.easyai/backup/.claude.json:取出 _easyai_keychain 字段写回 macOS Keychain,
|
|
13
|
+
剩余字段按字段级深度 merge 写回 ~/.claude.json。
|
|
14
|
+
|
|
15
|
+
特点:
|
|
16
|
+
|
|
17
|
+
* 深度递归 merge:嵌套对象内部字段一对一合并;备份字段覆盖/新增,目标独有保留
|
|
18
|
+
|
|
19
|
+
* Keychain 恢复:macOS 上把 _easyai_keychain.<label> 的值写回对应 Keychain 条目(如 "Claude Code-credentials");
|
|
20
|
+
_easyai_keychain 字段不写入 ~/.claude.json,保持纯净
|
|
21
|
+
|
|
22
|
+
* 目标独有保留:~/.claude.json 里的 projects 等运行时数据原样保留,不被清空
|
|
23
|
+
|
|
24
|
+
* 安全权限:恢复后的 ~/.claude.json 仍 chmod 600
|
|
25
|
+
|
|
26
|
+
使用示例:
|
|
27
|
+
|
|
28
|
+
$ easyai restore claude
|
|
29
|
+
DESC
|
|
30
|
+
|
|
31
|
+
BACKUP_PATH = File.expand_path('~/.easyai/backup/.claude.json').freeze
|
|
32
|
+
TARGET_PATH = File.expand_path('~/.claude.json').freeze
|
|
33
|
+
KEYCHAIN_BACKUP_KEY = '_easyai_keychain'.freeze
|
|
34
|
+
|
|
35
|
+
def validate!
|
|
36
|
+
super
|
|
37
|
+
help! "备份文件不存在: #{BACKUP_PATH}\n请先运行 easyai backup claude 创建备份。" unless File.exist?(BACKUP_PATH)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
source = read_json(BACKUP_PATH)
|
|
42
|
+
|
|
43
|
+
keychain_results = restore_keychain(source.delete(KEYCHAIN_BACKUP_KEY))
|
|
44
|
+
|
|
45
|
+
target = File.exist?(TARGET_PATH) ? read_json(TARGET_PATH) : {}
|
|
46
|
+
before_keys = target.keys
|
|
47
|
+
|
|
48
|
+
merged = deep_merge(target, source)
|
|
49
|
+
|
|
50
|
+
File.write(TARGET_PATH, JSON.pretty_generate(merged))
|
|
51
|
+
File.chmod(0o600, TARGET_PATH) unless windows?
|
|
52
|
+
|
|
53
|
+
report(before_keys, source.keys, merged.keys, keychain_results)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def read_json(path)
|
|
59
|
+
JSON.parse(File.read(path))
|
|
60
|
+
rescue JSON::ParserError => e
|
|
61
|
+
raise "解析 JSON 失败 (#{path}): #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 深度递归 merge:嵌套 hash 逐层合并,叶子值由源覆盖目标
|
|
65
|
+
def deep_merge(target, source)
|
|
66
|
+
target.merge(source) do |_key, old_val, new_val|
|
|
67
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
68
|
+
deep_merge(old_val, new_val)
|
|
69
|
+
else
|
|
70
|
+
new_val
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 把备份中的 _easyai_keychain hash 逐项写回 macOS Keychain。
|
|
76
|
+
# 返回 { label => :ok | :fail | :skip } 用于 report;
|
|
77
|
+
# 非 macOS / 备份无该字段时直接跳过,返回空 hash。
|
|
78
|
+
def restore_keychain(keychain_data)
|
|
79
|
+
return {} unless keychain_data.is_a?(Hash) && !keychain_data.empty?
|
|
80
|
+
|
|
81
|
+
unless macos?
|
|
82
|
+
warn " ⚠ 非 macOS 环境,跳过 Keychain 恢复(备份中含 #{keychain_data.size} 个 Keychain 条目)"
|
|
83
|
+
return keychain_data.keys.each_with_object({}) { |k, h| h[k] = :skip }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
account = ENV['USER'] || (Etc.getlogin rescue nil)
|
|
87
|
+
if account.nil? || account.empty?
|
|
88
|
+
warn " ⚠ 无法确定当前用户名,跳过 Keychain 恢复"
|
|
89
|
+
return keychain_data.keys.each_with_object({}) { |k, h| h[k] = :skip }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
keychain_data.each_with_object({}) do |(label, password), results|
|
|
93
|
+
results[label] = write_keychain_credential(label, password, account)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 写入 Keychain(如已存在则更新)。security 命令的 -w <password> 在命令行参数中可见,
|
|
98
|
+
# 但本机用户跑本机命令的场景下风险可控;macOS 上 ps 默认不展示其他用户进程的完整 cmdline。
|
|
99
|
+
def write_keychain_credential(label, password, account)
|
|
100
|
+
unless password.is_a?(String) && !password.empty?
|
|
101
|
+
warn " ⚠ Keychain 条目「#{label}」备份值无效(非字符串或为空),跳过"
|
|
102
|
+
return :fail
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
args = ['security', 'add-generic-password',
|
|
106
|
+
'-a', account,
|
|
107
|
+
'-s', label,
|
|
108
|
+
'-l', label,
|
|
109
|
+
'-w', password,
|
|
110
|
+
'-U']
|
|
111
|
+
output, status = Open3.capture2e(*args)
|
|
112
|
+
if status.success?
|
|
113
|
+
:ok
|
|
114
|
+
else
|
|
115
|
+
warn " ⚠ 写入 Keychain 条目「#{label}」失败(exit=#{status.exitstatus}):#{output.strip}"
|
|
116
|
+
:fail
|
|
117
|
+
end
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
warn " ⚠ 写入 Keychain 条目「#{label}」异常:#{e.message}"
|
|
120
|
+
:fail
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def report(before_keys, source_keys, after_keys, keychain_results)
|
|
124
|
+
updated = source_keys & before_keys
|
|
125
|
+
added = source_keys - before_keys
|
|
126
|
+
kept = before_keys - source_keys
|
|
127
|
+
|
|
128
|
+
puts "✓ Claude 登录信息已恢复"
|
|
129
|
+
puts " 备份文件: #{BACKUP_PATH}"
|
|
130
|
+
puts " 目标路径: #{TARGET_PATH}"
|
|
131
|
+
puts " 字段更新: 新增 #{added.size},更新 #{updated.size},保留目标独有 #{kept.size}(合计 #{after_keys.size})"
|
|
132
|
+
if keychain_results.empty?
|
|
133
|
+
puts " Keychain: 备份中未含 #{KEYCHAIN_BACKUP_KEY} 字段,无需恢复"
|
|
134
|
+
else
|
|
135
|
+
ok = keychain_results.count { |_, v| v == :ok }
|
|
136
|
+
puts " Keychain: 恢复 #{ok}/#{keychain_results.size} 个条目(#{keychain_results.map { |k, v| "#{k}=#{v}" }.join(', ')})"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def windows?
|
|
141
|
+
Base::SystemInfo.windows?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def macos?
|
|
145
|
+
Base::SystemInfo.macos?
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module EasyAI
|
|
2
|
+
class Command
|
|
3
|
+
class Restore < Command
|
|
4
|
+
self.summary = '从 ~/.easyai/backup/ 恢复 AI CLI 登录信息'
|
|
5
|
+
self.description = <<-DESC
|
|
6
|
+
把 easyai backup 备份的登录信息深度合并回各家 AI CLI 的原始配置文件。
|
|
7
|
+
合并策略:备份字段覆盖/新增到目标,目标独有字段(如 Claude 的 projects)保留。
|
|
8
|
+
|
|
9
|
+
可用命令:
|
|
10
|
+
|
|
11
|
+
* claude - 恢复 Claude Code 登录信息
|
|
12
|
+
|
|
13
|
+
使用示例:
|
|
14
|
+
|
|
15
|
+
$ easyai restore claude # 把 ~/.easyai/backup/.claude.json 深度 merge 回 ~/.claude.json
|
|
16
|
+
DESC
|
|
17
|
+
|
|
18
|
+
self.abstract_command = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 加载子命令
|
|
24
|
+
require_relative 'restore/claude'
|