goosehost-cli 0.1.0__tar.gz
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.
- goosehost_cli-0.1.0/PKG-INFO +9 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/PKG-INFO +9 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/SOURCES.txt +8 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/dependency_links.txt +1 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/entry_points.txt +2 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/requires.txt +1 -0
- goosehost_cli-0.1.0/goosehost_cli.egg-info/top_level.txt +1 -0
- goosehost_cli-0.1.0/goosehost_cli.py +382 -0
- goosehost_cli-0.1.0/pyproject.toml +21 -0
- goosehost_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
goosehost_cli.py
|
|
2
|
+
pyproject.toml
|
|
3
|
+
goosehost_cli.egg-info/PKG-INFO
|
|
4
|
+
goosehost_cli.egg-info/SOURCES.txt
|
|
5
|
+
goosehost_cli.egg-info/dependency_links.txt
|
|
6
|
+
goosehost_cli.egg-info/entry_points.txt
|
|
7
|
+
goosehost_cli.egg-info/requires.txt
|
|
8
|
+
goosehost_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
goosehost_cli
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
DEFAULT_API = "https://page.goose.cc.cd"
|
|
9
|
+
CONFIG_DIR = Path.home() / ".goosehost"
|
|
10
|
+
TOKEN_FILE = CONFIG_DIR / "token"
|
|
11
|
+
USER_FILE = CONFIG_DIR / "user"
|
|
12
|
+
|
|
13
|
+
def ensure_config_dir():
|
|
14
|
+
CONFIG_DIR.mkdir(mode=0o700, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
def save_token(token, user=None):
|
|
17
|
+
ensure_config_dir()
|
|
18
|
+
with open(TOKEN_FILE, "w") as f:
|
|
19
|
+
f.write(token)
|
|
20
|
+
if user:
|
|
21
|
+
with open(USER_FILE, "w") as f:
|
|
22
|
+
json.dump(user, f)
|
|
23
|
+
|
|
24
|
+
def load_token():
|
|
25
|
+
if TOKEN_FILE.exists():
|
|
26
|
+
with open(TOKEN_FILE) as f:
|
|
27
|
+
return f.read().strip()
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def load_user():
|
|
31
|
+
if USER_FILE.exists():
|
|
32
|
+
with open(USER_FILE) as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def clear_auth():
|
|
37
|
+
if TOKEN_FILE.exists():
|
|
38
|
+
TOKEN_FILE.unlink()
|
|
39
|
+
if USER_FILE.exists():
|
|
40
|
+
USER_FILE.unlink()
|
|
41
|
+
|
|
42
|
+
def get_headers(token):
|
|
43
|
+
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
44
|
+
|
|
45
|
+
def api_request(method, url, token=None, json_data=None, timeout=15):
|
|
46
|
+
headers = {}
|
|
47
|
+
if token:
|
|
48
|
+
headers.update(get_headers(token))
|
|
49
|
+
else:
|
|
50
|
+
headers["Content-Type"] = "application/json"
|
|
51
|
+
try:
|
|
52
|
+
if method.upper() == "GET":
|
|
53
|
+
resp = requests.get(url, headers=headers, timeout=timeout)
|
|
54
|
+
elif method.upper() == "POST":
|
|
55
|
+
resp = requests.post(url, headers=headers, json=json_data, timeout=timeout)
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError("Unsupported method")
|
|
58
|
+
if resp.status_code == 401:
|
|
59
|
+
clear_auth()
|
|
60
|
+
sys.stderr.write("登录已过期,请重新执行 login\n")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
return resp
|
|
63
|
+
except requests.exceptions.Timeout:
|
|
64
|
+
sys.stderr.write("请求超时\n")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
sys.stderr.write(f"请求错误: {e}\n")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
def require_token(api_url):
|
|
71
|
+
token = load_token()
|
|
72
|
+
if not token:
|
|
73
|
+
sys.stderr.write("未登录,请先执行 login\n")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
return token
|
|
76
|
+
|
|
77
|
+
# ---------- 子命令实现 ----------
|
|
78
|
+
|
|
79
|
+
def cmd_register(args):
|
|
80
|
+
try:
|
|
81
|
+
resp = api_request("POST", f"{args.api}/auth/signup", json_data={"email": args.email, "password": args.password})
|
|
82
|
+
if resp.status_code != 200:
|
|
83
|
+
sys.stderr.write(f"注册失败: {resp.text}\n")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
data = resp.json()
|
|
86
|
+
if data.get("success"):
|
|
87
|
+
print("注册成功,请查收验证邮件")
|
|
88
|
+
else:
|
|
89
|
+
print(f"注册失败: {data}")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
sys.stderr.write(f"注册异常: {e}\n")
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
def cmd_login(args):
|
|
95
|
+
try:
|
|
96
|
+
resp = api_request("POST", f"{args.api}/auth/login", json_data={"email": args.email, "password": args.password})
|
|
97
|
+
if resp.status_code != 200:
|
|
98
|
+
sys.stderr.write(f"登录失败: {resp.text}\n")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
data = resp.json()
|
|
101
|
+
token = data.get("access_token")
|
|
102
|
+
user = data.get("user")
|
|
103
|
+
if not token:
|
|
104
|
+
sys.stderr.write("登录响应缺少 token\n")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
save_token(token, user)
|
|
107
|
+
print(f"登录成功,用户: {user.get('email', args.email)}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
sys.stderr.write(f"登录异常: {e}\n")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
def cmd_list(args):
|
|
113
|
+
token = require_token(args.api)
|
|
114
|
+
resp = api_request("GET", f"{args.api}/api/my-sites", token=token)
|
|
115
|
+
if resp.status_code != 200:
|
|
116
|
+
sys.stderr.write(f"获取列表失败: {resp.text}\n")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
sites = resp.json()
|
|
119
|
+
if not sites:
|
|
120
|
+
print("没有网站")
|
|
121
|
+
return
|
|
122
|
+
print(f"{'名称':<30} {'类型':<6} {'创建时间':<20} {'更新时间':<20}")
|
|
123
|
+
for s in sites:
|
|
124
|
+
name = s.get("name", "")
|
|
125
|
+
site_type = "md" if name.startswith("md/") else "html"
|
|
126
|
+
display_name = name[3:] if site_type == "md" else name
|
|
127
|
+
created = s.get("created_at", "")[:19]
|
|
128
|
+
updated = s.get("updated_at", "")[:19]
|
|
129
|
+
print(f"{display_name:<30} {site_type:<6} {created:<20} {updated:<20}")
|
|
130
|
+
|
|
131
|
+
def cmd_create(args):
|
|
132
|
+
token = require_token(args.api)
|
|
133
|
+
if not args.slug:
|
|
134
|
+
sys.stderr.write("必须指定 --slug\n")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
site_type = args.type or "html"
|
|
137
|
+
if site_type not in ("html", "md"):
|
|
138
|
+
sys.stderr.write("类型必须是 html 或 md\n")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
content = ""
|
|
141
|
+
if args.file:
|
|
142
|
+
try:
|
|
143
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
144
|
+
content = f.read()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
sys.stderr.write(f"读取文件失败: {e}\n")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
elif args.content:
|
|
149
|
+
content = args.content
|
|
150
|
+
else:
|
|
151
|
+
sys.stderr.write("必须指定 --file 或 --content\n")
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
if not content:
|
|
154
|
+
sys.stderr.write("内容为空\n")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
payload = {"slug": args.slug}
|
|
157
|
+
if site_type == "md":
|
|
158
|
+
payload["md"] = content
|
|
159
|
+
else:
|
|
160
|
+
payload["html"] = content
|
|
161
|
+
resp = api_request("POST", f"{args.api}/api/create", token=token, json_data=payload)
|
|
162
|
+
if resp.status_code != 200:
|
|
163
|
+
sys.stderr.write(f"创建失败: {resp.text}\n")
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
data = resp.json()
|
|
166
|
+
if data.get("success"):
|
|
167
|
+
print(f"创建成功,访问地址: {data.get('url', '')}")
|
|
168
|
+
else:
|
|
169
|
+
print(f"创建失败: {data}")
|
|
170
|
+
|
|
171
|
+
def cmd_get(args):
|
|
172
|
+
token = require_token(args.api)
|
|
173
|
+
if not args.slug:
|
|
174
|
+
sys.stderr.write("必须指定 --slug\n")
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
resp = api_request("GET", f"{args.api}/api/file/{args.slug}", token=token)
|
|
177
|
+
if resp.status_code != 200:
|
|
178
|
+
sys.stderr.write(f"获取失败: {resp.text}\n")
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
data = resp.json()
|
|
181
|
+
content = data.get("html") or data.get("md") or ""
|
|
182
|
+
if args.output:
|
|
183
|
+
try:
|
|
184
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
185
|
+
f.write(content)
|
|
186
|
+
print(f"已保存到 {args.output}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
sys.stderr.write(f"保存文件失败: {e}\n")
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
else:
|
|
191
|
+
print(content)
|
|
192
|
+
|
|
193
|
+
def cmd_update(args):
|
|
194
|
+
token = require_token(args.api)
|
|
195
|
+
if not args.slug:
|
|
196
|
+
sys.stderr.write("必须指定 --slug\n")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
content = ""
|
|
199
|
+
if args.file:
|
|
200
|
+
try:
|
|
201
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
202
|
+
content = f.read()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
sys.stderr.write(f"读取文件失败: {e}\n")
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
elif args.content:
|
|
207
|
+
content = args.content
|
|
208
|
+
else:
|
|
209
|
+
sys.stderr.write("必须指定 --file 或 --content\n")
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
if not content:
|
|
212
|
+
sys.stderr.write("内容为空\n")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
payload = {"slug": args.slug}
|
|
215
|
+
if args.slug.startswith("md/"):
|
|
216
|
+
payload["md"] = content
|
|
217
|
+
else:
|
|
218
|
+
payload["html"] = content
|
|
219
|
+
resp = api_request("POST", f"{args.api}/api/update", token=token, json_data=payload)
|
|
220
|
+
if resp.status_code != 200:
|
|
221
|
+
sys.stderr.write(f"更新失败: {resp.text}\n")
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
data = resp.json()
|
|
224
|
+
if data.get("success"):
|
|
225
|
+
print("更新成功")
|
|
226
|
+
else:
|
|
227
|
+
print(f"更新失败: {data}")
|
|
228
|
+
|
|
229
|
+
def cmd_delete(args):
|
|
230
|
+
token = require_token(args.api)
|
|
231
|
+
if not args.slug:
|
|
232
|
+
sys.stderr.write("必须指定 --slug\n")
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
if not args.force:
|
|
235
|
+
sys.stderr.write("必须使用 --force 确认删除\n")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
resp = api_request("POST", f"{args.api}/api/delete", token=token, json_data={"slug": args.slug})
|
|
238
|
+
if resp.status_code != 200:
|
|
239
|
+
sys.stderr.write(f"删除失败: {resp.text}\n")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
data = resp.json()
|
|
242
|
+
if data.get("success"):
|
|
243
|
+
print("删除成功")
|
|
244
|
+
else:
|
|
245
|
+
print(f"删除失败: {data}")
|
|
246
|
+
|
|
247
|
+
def cmd_config(args):
|
|
248
|
+
token = load_token()
|
|
249
|
+
user = load_user()
|
|
250
|
+
print(f"API地址: {args.api or DEFAULT_API}")
|
|
251
|
+
print(f"登录状态: {'已登录' if token else '未登录'}")
|
|
252
|
+
if user:
|
|
253
|
+
print(f"用户: {user.get('email', '')}")
|
|
254
|
+
if token:
|
|
255
|
+
print(f"Token: {token[:10]}...")
|
|
256
|
+
|
|
257
|
+
def cmd_logout(args):
|
|
258
|
+
clear_auth()
|
|
259
|
+
print("已退出登录")
|
|
260
|
+
|
|
261
|
+
def cmd_deploy(args):
|
|
262
|
+
token = require_token(args.api)
|
|
263
|
+
target = Path(args.path)
|
|
264
|
+
if not target.is_file():
|
|
265
|
+
sys.stderr.write(f"错误: '{args.path}' 不是一个有效的文件\n")
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
if target.suffix.lower() in ('.html', '.htm'):
|
|
268
|
+
site_type = 'html'
|
|
269
|
+
elif target.suffix.lower() in ('.md', '.markdown'):
|
|
270
|
+
site_type = 'md'
|
|
271
|
+
else:
|
|
272
|
+
sys.stderr.write("错误: 仅支持 .html 或 .md 文件\n")
|
|
273
|
+
sys.exit(1)
|
|
274
|
+
try:
|
|
275
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
276
|
+
content = f.read()
|
|
277
|
+
except Exception as e:
|
|
278
|
+
sys.stderr.write(f"读取文件失败: {e}\n")
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
if not content:
|
|
281
|
+
sys.stderr.write("错误: 文件内容为空\n")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
slug = args.slug
|
|
284
|
+
if not slug:
|
|
285
|
+
slug = re.sub(r'[^a-zA-Z0-9_\-.~]', '-', target.stem)
|
|
286
|
+
if not slug:
|
|
287
|
+
slug = 'my-site'
|
|
288
|
+
print(f"未指定 --slug,自动生成: {slug}")
|
|
289
|
+
payload = {"slug": slug}
|
|
290
|
+
if site_type == "md":
|
|
291
|
+
payload["md"] = content
|
|
292
|
+
else:
|
|
293
|
+
payload["html"] = content
|
|
294
|
+
resp = api_request("POST", f"{args.api}/api/create", token=token, json_data=payload)
|
|
295
|
+
if resp.status_code != 200:
|
|
296
|
+
sys.stderr.write(f"部署失败: {resp.text}\n")
|
|
297
|
+
sys.exit(1)
|
|
298
|
+
data = resp.json()
|
|
299
|
+
if data.get("success"):
|
|
300
|
+
print(f"部署成功!")
|
|
301
|
+
print(f" 访问地址: {data.get('url', '')}")
|
|
302
|
+
print(f" 网站名称: {slug}")
|
|
303
|
+
else:
|
|
304
|
+
print(f"部署失败: {data}")
|
|
305
|
+
|
|
306
|
+
# ---------- 主入口 ----------
|
|
307
|
+
def main():
|
|
308
|
+
parser = argparse.ArgumentParser(description="GooseHost 命令行管理工具")
|
|
309
|
+
parser.add_argument("--api", default=DEFAULT_API, help="API基础地址")
|
|
310
|
+
subparsers = parser.add_subparsers(dest="command", required=True, help="子命令")
|
|
311
|
+
|
|
312
|
+
# 注册
|
|
313
|
+
p_register = subparsers.add_parser("register", help="注册新账号")
|
|
314
|
+
p_register.add_argument("--email", required=True, help="邮箱")
|
|
315
|
+
p_register.add_argument("--password", required=True, help="密码")
|
|
316
|
+
|
|
317
|
+
# 登录
|
|
318
|
+
p_login = subparsers.add_parser("login", help="登录并保存凭证")
|
|
319
|
+
p_login.add_argument("--email", required=True, help="邮箱")
|
|
320
|
+
p_login.add_argument("--password", required=True, help="密码")
|
|
321
|
+
|
|
322
|
+
# 列表
|
|
323
|
+
p_list = subparsers.add_parser("list", help="列出我的网站")
|
|
324
|
+
|
|
325
|
+
# 创建
|
|
326
|
+
p_create = subparsers.add_parser("create", help="创建网站")
|
|
327
|
+
p_create.add_argument("--slug", required=True, help="网站名称")
|
|
328
|
+
p_create.add_argument("--type", choices=["html", "md"], default="html", help="网站类型")
|
|
329
|
+
p_create.add_argument("--file", help="从文件读取内容")
|
|
330
|
+
p_create.add_argument("--content", help="直接指定内容")
|
|
331
|
+
|
|
332
|
+
# 获取
|
|
333
|
+
p_get = subparsers.add_parser("get", help="获取网站内容")
|
|
334
|
+
p_get.add_argument("--slug", required=True, help="网站名称")
|
|
335
|
+
p_get.add_argument("--output", "-o", help="保存到文件")
|
|
336
|
+
|
|
337
|
+
# 更新
|
|
338
|
+
p_update = subparsers.add_parser("update", help="更新网站内容")
|
|
339
|
+
p_update.add_argument("--slug", required=True, help="网站名称")
|
|
340
|
+
p_update.add_argument("--file", help="从文件读取内容")
|
|
341
|
+
p_update.add_argument("--content", help="直接指定内容")
|
|
342
|
+
|
|
343
|
+
# 删除
|
|
344
|
+
p_delete = subparsers.add_parser("delete", help="删除网站")
|
|
345
|
+
p_delete.add_argument("--slug", required=True, help="网站名称")
|
|
346
|
+
p_delete.add_argument("--force", action="store_true", help="强制删除,无需确认")
|
|
347
|
+
|
|
348
|
+
# 本地配置
|
|
349
|
+
p_config = subparsers.add_parser("config", help="查看本地配置和登录状态")
|
|
350
|
+
|
|
351
|
+
# 登出
|
|
352
|
+
p_logout = subparsers.add_parser("logout", help="清除本地凭证")
|
|
353
|
+
|
|
354
|
+
# 部署
|
|
355
|
+
p_deploy = subparsers.add_parser("deploy", help="部署本地文件到 GooseHost")
|
|
356
|
+
p_deploy.add_argument("path", help="要部署的本地文件路径 (.html 或 .md)")
|
|
357
|
+
p_deploy.add_argument("--slug", help="自定义网站名称 (slug),不指定则自动从文件名生成")
|
|
358
|
+
|
|
359
|
+
args = parser.parse_args()
|
|
360
|
+
if args.command == "register":
|
|
361
|
+
cmd_register(args)
|
|
362
|
+
elif args.command == "login":
|
|
363
|
+
cmd_login(args)
|
|
364
|
+
elif args.command == "list":
|
|
365
|
+
cmd_list(args)
|
|
366
|
+
elif args.command == "create":
|
|
367
|
+
cmd_create(args)
|
|
368
|
+
elif args.command == "get":
|
|
369
|
+
cmd_get(args)
|
|
370
|
+
elif args.command == "update":
|
|
371
|
+
cmd_update(args)
|
|
372
|
+
elif args.command == "delete":
|
|
373
|
+
cmd_delete(args)
|
|
374
|
+
elif args.command == "config":
|
|
375
|
+
cmd_config(args)
|
|
376
|
+
elif args.command == "logout":
|
|
377
|
+
cmd_logout(args)
|
|
378
|
+
elif args.command == "deploy":
|
|
379
|
+
cmd_deploy(args)
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "goosehost-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "GooseHost 命令行管理工具"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Your Name", email = "your@email.com" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.7"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"requests>=2.25"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
goosehost = "goosehost_cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
py-modules = ["goosehost_cli"]
|