gh-api-push 1.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.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # 轻记 TODO
2
+
3
+ 本地优先的待办工具:数据先落本机,云端同步到**你自己的飞书多维表格**——不放任何人的服务器。
4
+
5
+ - 站点 / 网页版:https://439436269-ctrl.github.io/qingji-todo-web/
6
+ - 多端形态:网页版(PWA)· Android(APK)· iOS(Xcode 工程 + OTA 安装)
7
+ - 技术方案见 [技术方案-多端同步.md](技术方案-多端同步.md)
8
+
9
+ ## api-push(本包 CLI)
10
+
11
+ `api-push` 是本仓库内含的**原子 CLI**:通过 GitHub **Git Data API** 把指定文件提交并推送到仓库——当 `github.com` 的 git push 直连被墙/握手失败、而 `api.github.com` 仍可达时,作为替代推送通道。
12
+
13
+ ```bash
14
+ # 安装
15
+ npm install -g api-push
16
+
17
+ # 用法(在目标 git 仓库目录内执行)
18
+ export GH_TOKEN=ghp_xxx # 需要 repo 权限的 token
19
+ api-push "提交说明" path/to/file1.md path/to/file2.js
20
+
21
+ # 可选环境变量
22
+ # GH_REPO 目标仓库(默认 439436269-ctrl/qingji-todo)
23
+ # GH_ROOT 文件路径解析根目录(默认取当前目录,须是 git 仓库)
24
+ ```
25
+
26
+ - 零依赖,Node ≥ 18(内置 fetch)
27
+ - 二进制/图片等按文件类型自动选择 base64 或 UTF-8 blob
28
+ - 单次提交可携带多个文件;`HEAD` 从 `main` 分支读取
29
+
30
+ 仓库内直接使用(无需安装):
31
+
32
+ ```bash
33
+ GH_TOKEN=ghp_xxx node tools/api-push.cjs "提交说明" index.html
34
+ ```
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "gh-api-push",
3
+ "version": "1.0.0",
4
+ "description": "Push files to a GitHub repo via the Git Data API — a lightweight fallback channel when github.com git push is unreachable (e.g. network blocks) but api.github.com still works.",
5
+ "bin": {
6
+ "api-push": "tools/api-push.cjs"
7
+ },
8
+ "files": [
9
+ "tools/api-push.cjs"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "os": [
15
+ "darwin",
16
+ "linux",
17
+ "win32"
18
+ ],
19
+ "keywords": [
20
+ "github",
21
+ "git",
22
+ "push",
23
+ "git-data-api",
24
+ "offline-push",
25
+ "cli"
26
+ ],
27
+ "license": "UNLICENSED",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/439436269-ctrl/qingji-todo.git"
31
+ },
32
+ "scripts": {
33
+ "test": "node tools/api-push.cjs 2>&1 | grep -q GH_TOKEN && echo smoke-ok || echo smoke-fail"
34
+ }
35
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // 通过 GitHub Git Data API 推送指定文件(github.com 直连被墙时的替代推送通道)
3
+ // 用法: GH_TOKEN='...' api-push "<commit message>" <文件路径...>
4
+ // 或在仓库内: node tools/api-push.cjs "<commit message>" <文件路径...>
5
+ 'use strict';
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const REPO = process.env.GH_REPO || '439436269-ctrl/qingji-todo';
10
+ const TOKEN = process.env.GH_TOKEN;
11
+ // 文件路径基准:优先 GH_ROOT,其次当前目录是 git 仓库时用它(npm 全局安装场景),否则回退包内相对路径(仓库内直跑场景)
12
+ const ROOT = process.env.GH_ROOT
13
+ || (fs.existsSync(path.join(process.cwd(), '.git')) ? process.cwd() : path.resolve(__dirname, '..'));
14
+ const API = 'https://api.github.com';
15
+ const BINARY = /\.(png|jpg|jpeg|gif|jar|keystore|zip|apk|woff2?)$/i;
16
+
17
+ const message = process.argv[2];
18
+ const files = process.argv.slice(3);
19
+ if (!TOKEN) { console.error('缺少环境变量 GH_TOKEN'); process.exit(1); }
20
+ if (!message || !files.length) { console.error('用法: node tools/api-push.cjs "<msg>" <file...>'); process.exit(1); }
21
+
22
+ async function api(method, url, body) {
23
+ const res = await fetch(API + url, {
24
+ method,
25
+ headers: {
26
+ Authorization: 'token ' + TOKEN,
27
+ 'User-Agent': 'qingji-push',
28
+ 'Content-Type': 'application/json',
29
+ Accept: 'application/vnd.github+json'
30
+ },
31
+ body: body ? JSON.stringify(body) : undefined
32
+ });
33
+ const data = await res.json().catch(() => ({}));
34
+ if (!res.ok) throw new Error(`${res.status} ${method} ${url}: ${JSON.stringify(data).slice(0, 300)}`);
35
+ return data;
36
+ }
37
+
38
+ (async () => {
39
+ const ref = await api('GET', `/repos/${REPO}/git/ref/heads/main`);
40
+ const head = await api('GET', `/repos/${REPO}/git/commits/${ref.object.sha}`);
41
+ const tree = [];
42
+ for (const rel of files) {
43
+ const buf = fs.readFileSync(path.join(ROOT, rel));
44
+ const isBinary = BINARY.test(rel);
45
+ // 防桌面端注入的标识被推上去(只针对 HTML;模式用转义书写,避免脚本自身误命中)
46
+ if (!isBinary && /\.html?$/i.test(rel)
47
+ && (/\u0041I\u751f\u6210/.test(buf.toString('utf8')) || /data-aigc-\u006dark/.test(buf.toString('utf8')))) {
48
+ console.error('中止:' + rel + ' 含桌面端注入的标识,请先清理该文件再推送');
49
+ process.exit(2);
50
+ }
51
+ const blob = isBinary
52
+ ? await api('POST', `/repos/${REPO}/git/blobs`, { content: buf.toString('base64'), encoding: 'base64' })
53
+ : await api('POST', `/repos/${REPO}/git/blobs`, { content: buf.toString('utf8'), encoding: 'utf-8' });
54
+ tree.push({ path: rel, mode: '100644', type: 'blob', sha: blob.sha });
55
+ console.log('blob:', rel);
56
+ }
57
+ const t = await api('POST', `/repos/${REPO}/git/trees`, { base_tree: head.tree.sha, tree });
58
+ const c = await api('POST', `/repos/${REPO}/git/commits`, { message, tree: t.sha, parents: [head.sha] });
59
+ await api('PATCH', `/repos/${REPO}/git/refs/heads/main`, { sha: c.sha });
60
+ console.log('pushed main ->', c.sha);
61
+ })().catch(e => { console.error('FAIL:', e.message); process.exit(1); });