dsh-syn-config 0.0.0-stage → 0.7.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.
@@ -0,0 +1,95 @@
1
+ # 发布到 DSH 插件市场(dsh-market)的完整路径
2
+
3
+ > 结论:**dsh-market 没有“上传接口”。** 它读的是精选列表 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 的目录
4
+ > (`https://awesome-dsh-plugin.com/plugins.json`,国内走 npm 镜像的同一份)。上架 = 往那个仓库提一个 YAML 文件的 PR。
5
+ > 合并后目录 CI 每日刷新,通常一天内出现在市场的“发现”页与宿主 Discover 索引里。
6
+
7
+ ## 0. 发布前检查(本仓库已通过)
8
+
9
+ ```bash
10
+ node scripts/publish-check.mjs # 不允许:账号名/私有仓库 URL/launchd 标签/家目录路径
11
+ node --test "test/*.test.mjs" # 81 例
12
+ node scripts/verify.mjs # 安装形态
13
+ ```
14
+
15
+ ## 1. 发布包(任选其一;推荐两个都做)
16
+
17
+ **A. npm(已发布)**
18
+
19
+ `dsh-syn-config@0.7.1` 已在 registry 上:<https://www.npmjs.com/package/dsh-syn-config>(34 个文件,234 KB unpacked)。
20
+
21
+ 首次发布踩到的两个坑,记下来免得下台机器重踩:
22
+
23
+ 1. granular token 若用「Only select packages」选一个**还不存在**的包,作用域是空集 → 403 `You may not perform that action with these credentials`;
24
+ 2. 首次发布时 registry 会先挂一个 `0.0.0-stage` 占位版本(staged publishing 机制),真实版本在几分钟后出现,`npm view <name> version` 一开始会看到占位号,不要慌。
25
+
26
+ ```bash
27
+ npm login
28
+ npm publish --access public
29
+ npm view dsh-syn-config version # 0.7.1
30
+ ```
31
+
32
+ **B. GitHub Release tarball(不发 npm 时的兜底,市场会优先展示它)**
33
+
34
+ ```bash
35
+ npm pack # dsh-syn-config-0.7.0.tgz
36
+ gh release create v0.7.0 dsh-syn-config-0.7.0.tgz
37
+ ```
38
+
39
+ ## 2. 仓库公开(这一步有隐私前提)
40
+
41
+ 市场的维护者会**实际阅读仓库**,所以仓库必须公开。但**历史不能直接公开**:早期提交里带着私有配置仓库的 URL(`assets/syn-dsh.md` 已在新版本里清掉,旧提交仍在)。
42
+
43
+ 推荐做法:**新建一个干净的公开仓库**,只放一个压缩后的初始提交,旧仓库留作私有归档:
44
+
45
+ ```bash
46
+ git clone --depth 1 . /tmp/dsh-syn-config-public
47
+ cd /tmp/dsh-syn-config-public && rm -rf .git
48
+ git init && git add -A && git commit -m "feat: dsh-syn-config 0.7.0"
49
+ git branch -M main
50
+ gh repo create Dreamaniax/dsh-syn-config --public --source . --remote origin --push
51
+ gh repo edit Dreamaniax/dsh-syn-config --add-topic dsh-plugin
52
+ ```
53
+
54
+ ## 3. 提 PR(一个文件)
55
+
56
+ 仓库 `awesome-dsh-plugin/awesome-dsh-plugin`,新增 `data/plugins/Dreamaniax__dsh-syn-config.yml`:
57
+
58
+ ```yaml
59
+ url: https://github.com/Dreamaniax/dsh-syn-config
60
+ name: Dreamaniax/dsh-syn-config
61
+ category: workflow
62
+ description:
63
+ en: 'Syncs DSH skills, profile settings and AGENTS.md with a private git repository: status, six-section health check, pull, secret redaction, and a snapshot-plus-push flow.'
64
+ zh: '把 DSH 技能、profile 设置与 AGENTS.md 与私有 git 仓库双向同步:状态、六节体检、拉取、密钥脱敏,以及快照+推送。'
65
+ icon: https://raw.githubusercontent.com/Dreamaniax/dsh-syn-config/main/assets/icon-180.png
66
+ ```
67
+
68
+ ## 4. 会被 CI 挡住的硬条件
69
+
70
+ | 条件 | 我们的状态 |
71
+ |---|---|
72
+ | 仓库声明 `dsh.bundle`(可 `dsh plugin add`) | ✅ `dsh.bundle.patch` |
73
+ | 仓库**创建满 1 天** | ⏳ 新仓库刚建,**PR 次日再提**,否则 CI 自动打回 |
74
+ | 有真实可用的代码 | ✅ 6 个工具 + 命令 + 状态卡 + 81 例测试 |
75
+ | 加 `dsh-plugin` topic | 步骤 2 已带 |
76
+ | 描述与代码一致 | 描述里只写真实存在的工具与行为 |
77
+ | 官方 `@deepseek-ai/*` 用 peerDependencies | ✅ 本插件不依赖官方包 |
78
+
79
+ ## 5. 合并之后
80
+
81
+ - 目录 CI 每日重建 `plugins.json` → dsh-market 搜索即可命中;
82
+ - 宿主 DSH 0.2.1+ 的「插件 → 发现 → 索引源」也可直接加 `https://awesome-dsh-plugin.com/index.json`;
83
+ - 安装走 `dsh plugin add dsh-syn-config`(npm)或市场一键安装(源码/ tarball)。
84
+
85
+ ## 6. 安全边界(写给使用者)
86
+
87
+ 插件以宿主全权运行:能执行 `git`、读写 `~/.dsh`、注册一个本机 HTTP 路由。它**默认不写盘**(`pull`/`push` 的 `dry_run` 默认 true),不接受任何网络上报,密钥只在本机 `secrets.env`。
88
+
89
+ ## 7. 隐私复检(0.7.1)
90
+
91
+ - **占位值不再误报**:`API_KEY=dummy_test_value_0001` 这类测试夹具被识别为占位值放过;带真实凭据前缀(`as_sk_`/`ghp_`/`sk-`/`AKIA`…)的一律不放过。
92
+ - **凭据形状的键**:`*api_key / *token / *secret / *password / Authorization / private_key` 且值 ≥16 位、含字母+数字 → 判为错误(提示改用 `__KEY__`);`vaultSyncS3Key` 这类标识符不算。
93
+ - **历史扫描不再因体积失败**:单次 diff 超过缓冲时,自动收窄到 `profiles/ scripts/ AGENTS.md weekly/` 重试。
94
+ - **定时任务标签**:不再硬编码个人反域名——读 `DSH_WEEKLY_LAUNCHD_LABEL` / `learning/schedule.env`,都没有就按 `*.dsh.weekly-retro` 后缀匹配。
95
+ - 复检结果:`publish-check` 0 命中;`dsh_sync_doctor` 0 阻塞、2 警告(都是本机与仓库的正常漂移)。
@@ -0,0 +1,146 @@
1
+ /**
2
+ * `/syn-dsh` — the user-facing slash command.
3
+ *
4
+ * A slash command runs against the agent directly: no model message is
5
+ * created, so it costs nothing and cannot be "misread" by the model. The
6
+ * command is deliberately thin — it calls the same core functions the model
7
+ * tools use, which keeps the two surfaces from drifting.
8
+ *
9
+ * Safety rule for the command: `push` and `pull` default to their dry-run
10
+ * plan. A real mutation needs the word `now`. That is the reverse of the tool
11
+ * default only in wording, not in behaviour: both require an explicit human
12
+ * decision before anything leaves this machine.
13
+ *
14
+ * @module dsh-syn-config/core/commands
15
+ */
16
+
17
+ import { runDoctor } from './doctor.js'
18
+ import { pullRepo } from './pull.js'
19
+ import { pushRepo } from './push.js'
20
+ import { runSecretsAction } from './secrets-ops.js'
21
+ import { readRepoStatus } from './status.js'
22
+
23
+ /** One-line status summary. */
24
+ function formatStatus(status) {
25
+ if (!status.repoPresent) return `${status.repoPath}: no repository found`
26
+ if (!status.ok) return `${status.repoPath}: ${status.notes[0] ?? 'cannot read status'}`
27
+ const tracking = status.upstream === '' ? 'no upstream' : `${status.ahead} ahead / ${status.behind} behind ${status.upstream}`
28
+ const tree = status.dirty === 0 ? 'clean worktree' : `${status.dirty} changed file(s)`
29
+ return `${status.branch || '(detached)'} · ${tracking} · ${tree}`
30
+ }
31
+
32
+ /** Doctor summary: one line per section, only the worst finding. */
33
+ function formatDoctor(result) {
34
+ const lines = [`health: ${result.fail} blocking / ${result.warn} warnings · exit ${result.exitCode}`]
35
+ for (const section of result.sections) {
36
+ const finding = section.items.find((entry) => entry.level === 'error')
37
+ ?? section.items.find((entry) => entry.level === 'warn')
38
+ lines.push(`${section.status.toUpperCase().padEnd(5)} ${section.title}${finding === undefined ? '' : ` — ${finding.text.trim()}`}`)
39
+ }
40
+ return lines.join('\n')
41
+ }
42
+
43
+ /**
44
+ * Register the command.
45
+ *
46
+ * @param {{ commands: { register(definition: object): unknown } }} ctx
47
+ * @param {{ repoPath: string, profile: string, dshHome?: string, gitTimeoutMs: number }} settings
48
+ */
49
+ export function registerCommands(ctx, settings) {
50
+ if (ctx.commands === undefined || typeof ctx.commands.register !== 'function') return
51
+
52
+ ctx.commands.register({
53
+ // A stable, plugin-namespaced identity: optional for the registry, but it
54
+ // lets a UI pair the definition with its executions across reloads.
55
+ definitionId: 'dsh-syn-config/syn-dsh',
56
+ name: 'syn-dsh',
57
+ description: '同步 DSH 配置:status / doctor / pull / push / secrets(写操作默认 dry-run,加 now 才执行)',
58
+ input: { hint: 'status | doctor [online] | pull [now] | push [now] | secrets scan|fingerprint' },
59
+ async handler({ rawInput }) {
60
+ const tokens = String(rawInput ?? '').trim().split(/\s+/u).filter((token) => token !== '')
61
+ const subcommand = (tokens[0] ?? 'status').toLowerCase()
62
+ const flag = (tokens[1] ?? '').toLowerCase()
63
+ const repoPath = typeof settings.repoPath === 'string' ? settings.repoPath : ''
64
+ try {
65
+ if (subcommand === 'status') {
66
+ const status = await readRepoStatus({ repoPath, timeoutMs: settings.gitTimeoutMs })
67
+ return { kind: status.ok || status.repoPresent ? 'success' : 'error', text: formatStatus(status) }
68
+ }
69
+
70
+ if (subcommand === 'doctor' || subcommand === 'check') {
71
+ const result = await runDoctor({
72
+ repoPath,
73
+ dshHome: settings.dshHome,
74
+ profile: settings.profile,
75
+ offline: flag !== 'online',
76
+ timeoutMs: settings.gitTimeoutMs,
77
+ })
78
+ return { kind: result.exitCode === 1 ? 'error' : 'success', text: formatDoctor(result) }
79
+ }
80
+
81
+ if (subcommand === 'pull') {
82
+ const real = flag === 'now'
83
+ const result = await pullRepo({
84
+ repoPath,
85
+ dshHome: settings.dshHome,
86
+ dryRun: !real,
87
+ timeoutMs: settings.gitTimeoutMs,
88
+ })
89
+ const mirrors = result.mirrors
90
+ .map((mirror) => `${mirror.name} +${mirror.planned.added.length} ~${mirror.planned.updated.length} -${mirror.planned.deleted.length}`)
91
+ .join(' · ')
92
+ const head = real
93
+ ? `${result.pulled ? 'pulled' : 'already up to date'} ${result.before.slice(0, 7)} → ${result.after.slice(0, 7)}`
94
+ : `dry run: ${result.incomingCommits} incoming commit(s)`
95
+ const warnings = result.warnings.length === 0 ? '' : `\n${result.warnings.join('\n')}`
96
+ return { kind: result.ok ? 'success' : 'error', text: `${head}\n${mirrors}${warnings}` }
97
+ }
98
+
99
+ if (subcommand === 'push') {
100
+ const real = flag === 'now'
101
+ const result = await pushRepo({
102
+ repoPath,
103
+ dshHome: settings.dshHome,
104
+ profile: settings.profile,
105
+ dryRun: !real,
106
+ timeoutMs: settings.gitTimeoutMs,
107
+ })
108
+ const gates = `lint ${result.gates.lint.ok ? 'ok' : 'FAIL'} · redact ${result.gates.redact.changed.length} · scan ${result.gates.scan.ok ? 'ok' : 'FAIL'}`
109
+ const summary = result.commit.created
110
+ ? `commit ${result.commit.sha}${result.commit.pushed ? ' pushed' : ' NOT pushed'}`
111
+ : result.commit.unchanged ? 'no changes to commit' : (real ? 'not committed' : 'dry run: nothing written')
112
+ const warnings = result.warnings.length === 0 ? '' : `\n${result.warnings.slice(0, 5).join('\n')}`
113
+ return {
114
+ kind: result.ok ? 'success' : 'error',
115
+ text: `changes +${result.changes.added.length} ~${result.changes.updated.length} -${result.changes.deleted.length} · skills ${result.manifest.skills}\n${gates}\n${summary}${warnings}`,
116
+ }
117
+ }
118
+
119
+ if (subcommand === 'secrets') {
120
+ const action = (tokens[1] ?? 'scan').toLowerCase()
121
+ const readOnly = action === 'scan' || action === 'fingerprint'
122
+ const result = await runSecretsAction({
123
+ action,
124
+ repoPath,
125
+ dshHome: settings.dshHome,
126
+ profile: settings.profile,
127
+ dryRun: true,
128
+ })
129
+ const detail = readOnly
130
+ ? (action === 'scan'
131
+ ? `${result.hits.length} plaintext hit(s)`
132
+ : result.fingerprints.map((entry) => `${entry.key} sha256:${entry.sha256} len=${entry.length}`).join('\n'))
133
+ : `would change: ${result.changed.join(', ') || '(nothing)'}`
134
+ return { kind: result.ok ? 'success' : 'error', text: `${action}: ${result.ok ? 'ok' : 'FAILED'}\n${detail}` }
135
+ }
136
+
137
+ return { kind: 'error', text: `unknown subcommand "${subcommand}" — use status | doctor | pull | push | secrets` }
138
+ } catch (error) {
139
+ return { kind: 'error', text: `syn-dsh ${subcommand} failed: ${String(error?.message ?? error)}` }
140
+ }
141
+ },
142
+ })
143
+ }
144
+
145
+ /** Re-exported for callers that only need the formatter. */
146
+ export { formatDoctor, formatStatus }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * What actually registered, and why it did not.
3
+ *
4
+ * A host plugin can fail in ways that are invisible from outside: reading a
5
+ * service without declaring it throws, a command registration can be rejected,
6
+ * a slot name can be wrong. Every registration step in `apply()` therefore
7
+ * records its outcome here, and the `dsh_sync_diag` tool reports it — so
8
+ * "is it loaded?" is a question the agent can answer instead of guess.
9
+ *
10
+ * @module dsh-syn-config/core/diagnostics
11
+ */
12
+
13
+ const state = {
14
+ version: '',
15
+ startedAt: '',
16
+ services: {},
17
+ steps: [],
18
+ tools: [],
19
+ }
20
+
21
+ /** Reset for a fresh activation (a reload runs `apply()` again). */
22
+ export function resetDiagnostics(version) {
23
+ state.version = version
24
+ state.startedAt = new Date().toISOString()
25
+ state.services = {}
26
+ state.steps = []
27
+ state.tools = []
28
+ }
29
+
30
+ /** Record one registered tool name. */
31
+ export function recordTool(name) {
32
+ if (!state.tools.includes(name)) state.tools.push(name)
33
+ }
34
+
35
+ /** Record that a service was or was not available. */
36
+ export function recordService(name, value) {
37
+ state.services[name] = value === undefined || value === null ? 'missing' : 'available'
38
+ }
39
+
40
+ /** Record one registration step. */
41
+ export function recordStep(name, ok, detail = '') {
42
+ state.steps.push({ name, ok, detail: String(detail).slice(0, 200) })
43
+ }
44
+
45
+ /** A snapshot the tool can return. */
46
+ export function readDiagnostics() {
47
+ return {
48
+ version: state.version,
49
+ startedAt: state.startedAt,
50
+ services: { ...state.services },
51
+ tools: [...state.tools],
52
+ steps: state.steps.map((step) => ({ ...step })),
53
+ }
54
+ }