dsh-config-manager 0.1.37 → 0.1.39
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/lib/client.d.ts +173 -66
- package/lib/client.js +1643 -1115
- package/lib/client.js.map +1 -1
- package/lib/index.js +175 -1
- package/lib/index.js.map +1 -1
- package/lib/market/git-file-writer.d.ts +86 -0
- package/lib/market/git-file-writer.js +269 -0
- package/lib/market/git-file-writer.js.map +1 -0
- package/lib/market/github-repos.d.ts +122 -0
- package/lib/market/github-repos.js +407 -0
- package/lib/market/github-repos.js.map +1 -0
- package/lib/market/my-repo.d.ts +131 -0
- package/lib/market/my-repo.js +389 -0
- package/lib/market/my-repo.js.map +1 -0
- package/lib/ui/i18n.d.ts +9 -10
- package/lib/ui/i18n.js +19 -21
- package/lib/ui/i18n.js.map +1 -1
- package/package.json +1 -1
- package/src/client/ConfigManagerSection.tsx +2 -2
- package/src/client/client-types.ts +2 -0
- package/src/client/index.ts +4 -2
- package/src/client/market/MarketPanel.tsx +64 -17
- package/src/client/market/MyConfigsView.tsx +745 -0
- package/src/client/market/market-api.test.ts +29 -0
- package/src/client/market/market-api.ts +5 -2
- package/src/client/market/market-locales.ts +118 -117
- package/src/client/market/my-configs-api.ts +163 -0
- package/src/client/market/my-configs-view.test.ts +289 -0
- package/src/client/market/my-configs-view.ts +304 -0
- package/src/client/run-store.ts +13 -0
- package/src/index.ts +163 -1
- package/src/market/git-file-writer.test.ts +249 -0
- package/src/market/git-file-writer.ts +336 -0
- package/src/market/github-repos.test.ts +400 -0
- package/src/market/github-repos.ts +481 -0
- package/src/market/my-repo.test.ts +472 -0
- package/src/market/my-repo.ts +516 -0
- package/src/ui/i18n.ts +19 -21
- package/lib/ui/market-publish.d.ts +0 -129
- package/lib/ui/market-publish.js +0 -168
- package/lib/ui/market-publish.js.map +0 -1
- package/src/client/market/PublishView.tsx +0 -388
- package/src/ui/market-publish.test.ts +0 -219
- package/src/ui/market-publish.ts +0 -256
package/lib/index.js
CHANGED
|
@@ -71,6 +71,10 @@ import { validateMarketItem } from "./market/security.js";
|
|
|
71
71
|
import { prepareMarketItem } from "./market/prepare.js";
|
|
72
72
|
import { validateMarketRepoUrl } from "./market/url.js";
|
|
73
73
|
import { marketItemWarnings, toMarketListItem } from "./market/view.js";
|
|
74
|
+
import { GitHubAuthRest, GitHubApiError } from "./market/github-repos.js";
|
|
75
|
+
import { MyRepoService, USER_CONFIGS_REPO, userConfigsRepoUrl } from "./market/my-repo.js";
|
|
76
|
+
import { createGitFileWriter } from "./market/git-file-writer.js";
|
|
77
|
+
import { redact } from "./security/redaction.js";
|
|
74
78
|
import { sha256Hex } from "./utils/hashing.js";
|
|
75
79
|
import { MANIFEST_FILE, parseManifest } from "./schema/manifest.js";
|
|
76
80
|
import { SECTION_IDS } from "./schema/config.js";
|
|
@@ -83,7 +87,7 @@ export const name = 'config-manager';
|
|
|
83
87
|
/** Services required before the engine can mount (present in every profile). */
|
|
84
88
|
export const inject = ['settings', 'credentials'];
|
|
85
89
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
86
|
-
const PLUGIN_VERSION = '0.1.
|
|
90
|
+
const PLUGIN_VERSION = '0.1.39';
|
|
87
91
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
88
92
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
89
93
|
/**
|
|
@@ -137,6 +141,12 @@ const API = {
|
|
|
137
141
|
marketBrowse: '/api/dsh-config-manager/market/browse',
|
|
138
142
|
marketDownload: '/api/dsh-config-manager/market/download',
|
|
139
143
|
marketPrepare: '/api/dsh-config-manager/market/prepare',
|
|
144
|
+
// m-my-configs:「一键上传 / 我的配置」(目标仓库固定 xiajiajun516/dsh-config-market;
|
|
145
|
+
// 登录复用 sync/github/start|poll|cancel,不重复实现;/me/items 401 → 未登录)
|
|
146
|
+
meStatus: '/api/dsh-config-manager/me/status',
|
|
147
|
+
meUpload: '/api/dsh-config-manager/me/upload',
|
|
148
|
+
meItems: '/api/dsh-config-manager/me/items',
|
|
149
|
+
meUpdate: '/api/dsh-config-manager/me/update',
|
|
140
150
|
};
|
|
141
151
|
/**
|
|
142
152
|
* 同步 token 的 DSH credentials 引用名(POSIX env-var 形态,满足 CredentialRef 品牌要求)。
|
|
@@ -1072,6 +1082,30 @@ function makeRestoreExecutor(snapshotDir, host, profile) {
|
|
|
1072
1082
|
},
|
|
1073
1083
|
};
|
|
1074
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* 解析「一键上传/我的配置」请求体的 form 字段:仅 { name, description?, categories? }。
|
|
1087
|
+
* name 必填(非空字符串,trim 后取);description 可选字符串;categories 可选字符串数组。
|
|
1088
|
+
* 非法 → null(调用方返回 400)。用户可填内容就这三项,其余元数据全自动。
|
|
1089
|
+
*/
|
|
1090
|
+
function parseMeForm(raw) {
|
|
1091
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1092
|
+
return null;
|
|
1093
|
+
const obj = raw;
|
|
1094
|
+
const name = typeof obj['name'] === 'string' ? obj['name'].trim() : '';
|
|
1095
|
+
if (name === '')
|
|
1096
|
+
return null;
|
|
1097
|
+
const form = { name };
|
|
1098
|
+
const description = obj['description'];
|
|
1099
|
+
if (typeof description === 'string' && description.trim() !== '')
|
|
1100
|
+
form.description = description.trim();
|
|
1101
|
+
const categoriesRaw = obj['categories'];
|
|
1102
|
+
if (Array.isArray(categoriesRaw)) {
|
|
1103
|
+
const categories = categoriesRaw.filter((c) => typeof c === 'string' && c.trim() !== '');
|
|
1104
|
+
if (categories.length > 0)
|
|
1105
|
+
form.categories = categories;
|
|
1106
|
+
}
|
|
1107
|
+
return form;
|
|
1108
|
+
}
|
|
1075
1109
|
/** Build the /api/dsh-config-manager route family. */
|
|
1076
1110
|
function makeRoutes(deps) {
|
|
1077
1111
|
const { host, adapters, exportsDir, tmpDir, snapshotsDir, runs, syncDir, marketDir, credentials, githubClientId, githubClientSecret } = deps;
|
|
@@ -1311,6 +1345,23 @@ function makeRoutes(deps) {
|
|
|
1311
1345
|
// tmpDir 读取失败 → 跳过(尽力而为)
|
|
1312
1346
|
}
|
|
1313
1347
|
}
|
|
1348
|
+
// -------------------------------------------------- me 装配(m-my-configs)
|
|
1349
|
+
// 「一键上传 / 我的配置」:GitHub REST 客户端 + 上传编排。
|
|
1350
|
+
// token 只经 credentials.resolve(SYNC_CREDENTIAL_REF) 在宿主内部读取(与 git 同步共用
|
|
1351
|
+
// 同一凭据槽),值绝不落盘 / 进日志 / 回传浏览器;gitWriter 写用户仓库与 fork 分支,
|
|
1352
|
+
// 安全模式与 GitTransport 一致(credential helper store 临时文件 0600 用后即删)。
|
|
1353
|
+
const meTokenProvider = async () => {
|
|
1354
|
+
const resolved = await credentials.resolve(credentialRef(SYNC_CREDENTIAL_REF));
|
|
1355
|
+
return resolved?.value ?? '';
|
|
1356
|
+
};
|
|
1357
|
+
const meGitHubRest = new GitHubAuthRest({ tokenProvider: meTokenProvider });
|
|
1358
|
+
const meService = new MyRepoService({
|
|
1359
|
+
prepare: prepareMarketItem,
|
|
1360
|
+
rest: meGitHubRest,
|
|
1361
|
+
gitWriter: createGitFileWriter({ credentials: { getToken: meTokenProvider } }),
|
|
1362
|
+
tokenProvider: meTokenProvider,
|
|
1363
|
+
now: () => new Date(),
|
|
1364
|
+
});
|
|
1314
1365
|
const routesList = [
|
|
1315
1366
|
// ------------------------------------------------------------- status
|
|
1316
1367
|
{
|
|
@@ -2713,6 +2764,129 @@ function makeRoutes(deps) {
|
|
|
2713
2764
|
}
|
|
2714
2765
|
},
|
|
2715
2766
|
},
|
|
2767
|
+
// ---------------------------------------------------- me/status
|
|
2768
|
+
// 「一键上传 / 我的配置」登录状态:resolve SYNC_CREDENTIAL_REF token → GET /user。
|
|
2769
|
+
// 401 → loggedIn:false(未登录);token 值不出模块外,只回传 login 用户名。
|
|
2770
|
+
{
|
|
2771
|
+
kind: 'exact',
|
|
2772
|
+
path: API.meStatus,
|
|
2773
|
+
handler: async (req, res) => {
|
|
2774
|
+
if (!guard(req, res, 'POST'))
|
|
2775
|
+
return;
|
|
2776
|
+
try {
|
|
2777
|
+
let loggedIn = false;
|
|
2778
|
+
let login;
|
|
2779
|
+
try {
|
|
2780
|
+
const user = await meGitHubRest.getUser();
|
|
2781
|
+
loggedIn = true;
|
|
2782
|
+
login = user.login;
|
|
2783
|
+
}
|
|
2784
|
+
catch (error) {
|
|
2785
|
+
// 仅 401(token 无效/过期)→ 未登录;其余错误(网络/限流)向上抛
|
|
2786
|
+
if (!(error instanceof GitHubApiError && error.code === 'unauthorized'))
|
|
2787
|
+
throw error;
|
|
2788
|
+
}
|
|
2789
|
+
const repoUrl = login !== undefined ? userConfigsRepoUrl(login) : undefined;
|
|
2790
|
+
const repoExists = login !== undefined ? await meGitHubRest.repoExists(login, USER_CONFIGS_REPO) : false;
|
|
2791
|
+
writeJson(res, 200, {
|
|
2792
|
+
loggedIn,
|
|
2793
|
+
...(login !== undefined ? { login } : {}),
|
|
2794
|
+
...(repoUrl !== undefined ? { repoUrl } : {}),
|
|
2795
|
+
repoExists,
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
catch (error) {
|
|
2799
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
// ---------------------------------------------------- me/upload
|
|
2804
|
+
// 一键上传:zipPath 必须来自受控上传临时区(复用 /market/prepare 规则);
|
|
2805
|
+
// form 仅 { name, description?, categories? }(name 必填);元数据全自动由 MyRepoService 生成。
|
|
2806
|
+
{
|
|
2807
|
+
kind: 'exact',
|
|
2808
|
+
path: API.meUpload,
|
|
2809
|
+
handler: async (req, res) => {
|
|
2810
|
+
if (!guard(req, res, 'POST'))
|
|
2811
|
+
return;
|
|
2812
|
+
const body = await readJsonBody(req);
|
|
2813
|
+
if (body === undefined) {
|
|
2814
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
const zipPath = typeof body['zipPath'] === 'string' ? body['zipPath'] : '';
|
|
2818
|
+
if (zipPath === '' || !isControlledPath(zipPath, roots)) {
|
|
2819
|
+
writeJson(res, 400, { error: 'zipPath is required and must reference a staged upload' });
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
const form = parseMeForm(body['form']);
|
|
2823
|
+
if (form === null) {
|
|
2824
|
+
writeJson(res, 400, { error: 'form is required and name must be a non-empty string' });
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
try {
|
|
2828
|
+
const zipBytes = await fs.readFile(zipPath);
|
|
2829
|
+
// MyRepoService 内部已做 prepare 8 道校验 + 秘密扫描(失败 → ok:false,零推送)
|
|
2830
|
+
const result = await meService.upload({ zipBytes, form });
|
|
2831
|
+
writeJson(res, 200, result);
|
|
2832
|
+
}
|
|
2833
|
+
catch (error) {
|
|
2834
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2835
|
+
}
|
|
2836
|
+
},
|
|
2837
|
+
},
|
|
2838
|
+
// ---------------------------------------------------- me/items
|
|
2839
|
+
// 查看已上传:读用户仓库 index.json + 收录状态(未收录 / PR 待审核 / 已收录)。
|
|
2840
|
+
// 401(token 过期)→ 401 + 脱敏错误,UI 引导重新登录。
|
|
2841
|
+
{
|
|
2842
|
+
kind: 'exact',
|
|
2843
|
+
path: API.meItems,
|
|
2844
|
+
handler: async (req, res) => {
|
|
2845
|
+
if (!guard(req, res, 'POST'))
|
|
2846
|
+
return;
|
|
2847
|
+
try {
|
|
2848
|
+
const items = await meService.listItems();
|
|
2849
|
+
writeJson(res, 200, { items });
|
|
2850
|
+
}
|
|
2851
|
+
catch (error) {
|
|
2852
|
+
const status = error instanceof GitHubApiError && error.code === 'unauthorized' ? 401 : 500;
|
|
2853
|
+
writeJson(res, status, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2854
|
+
}
|
|
2855
|
+
},
|
|
2856
|
+
},
|
|
2857
|
+
// ---------------------------------------------------- me/update
|
|
2858
|
+
// 一键更新:同 upload 时序;version 纯自动 +1、id 不变;PR 未合并 force push 更新 / 已合并基于最新 main 重开。
|
|
2859
|
+
{
|
|
2860
|
+
kind: 'exact',
|
|
2861
|
+
path: API.meUpdate,
|
|
2862
|
+
handler: async (req, res) => {
|
|
2863
|
+
if (!guard(req, res, 'POST'))
|
|
2864
|
+
return;
|
|
2865
|
+
const body = await readJsonBody(req);
|
|
2866
|
+
if (body === undefined) {
|
|
2867
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const zipPath = typeof body['zipPath'] === 'string' ? body['zipPath'] : '';
|
|
2871
|
+
if (zipPath === '' || !isControlledPath(zipPath, roots)) {
|
|
2872
|
+
writeJson(res, 400, { error: 'zipPath is required and must reference a staged upload' });
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const form = parseMeForm(body['form']);
|
|
2876
|
+
if (form === null) {
|
|
2877
|
+
writeJson(res, 400, { error: 'form is required and name must be a non-empty string' });
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
const zipBytes = await fs.readFile(zipPath);
|
|
2882
|
+
const result = await meService.update({ zipBytes, form });
|
|
2883
|
+
writeJson(res, 200, result);
|
|
2884
|
+
}
|
|
2885
|
+
catch (error) {
|
|
2886
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) });
|
|
2887
|
+
}
|
|
2888
|
+
},
|
|
2889
|
+
},
|
|
2716
2890
|
];
|
|
2717
2891
|
return { routes: routesList, scheduler };
|
|
2718
2892
|
}
|