dsh-plugin-git-commit-push 0.0.0-stage → 1.0.1

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/schema.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The Cordis `Config` schema, i.e. the plugin's visual settings form.
3
+ *
4
+ * HOW DSH TURNS THIS INTO A FORM
5
+ * A plugin whose row accepts a `config` mapping exports a schemastery schema as
6
+ * `Config`. DSH's `@deepseek-ai/dsh-settings` service derives a namespace from
7
+ * it (`autoGenerate`), the Settings page renders the fields, and
8
+ * `@deepseek-ai/dsh-config-editor` writes what the person chose into the active
9
+ * profile's `cordis.patch.yml` — for our own entry id, so it lands next to the
10
+ * loader row this bundle inserts, in the same place `ui-theme`'s `preference`
11
+ * lives. A field declared `.volatile()` is written into the running references
12
+ * and announced with `loader/volatile-update`, which is why our values are read
13
+ * through `.get()` on every call instead of being cached at mount: a change in
14
+ * the form takes effect on the next tool call, with no remount and no restart.
15
+ *
16
+ * WHY LOADING THE LIBRARY IS GUARDED
17
+ * The plugin deliberately keeps its runtime imports to itself: with a `link:`
18
+ * install the schema library is reachable only because the launcher installs a
19
+ * runtime resolution computed from the installation and the bundle dependency
20
+ * graphs. If that ever fails, the right outcome is a plugin without a settings
21
+ * form — not a plugin that cannot mount. So the library is loaded through
22
+ * `createRequire` inside a `try`, and every failure (missing library, a schema
23
+ * the library rejects, an unexpected builder result) degrades to `Config ===
24
+ * undefined`, which DSH reads as "this row has no configuration fields".
25
+ *
26
+ * VOLATILE PLACEMENT RULES (enforced by schemastery, easy to get wrong)
27
+ * `validateVolatileSchema` throws for a volatile field under a dict value, an
28
+ * array item, a map key, or a union/lazy branch: volatile fields must sit at a
29
+ * fixed object path and must not be nested inside another volatile field. Every
30
+ * field below is therefore a direct leaf of the root object (or a leaf of the
31
+ * fixed-path `pinnedIdentity` object), never a union or an array item.
32
+ */
33
+ import { createRequire } from 'node:module'
34
+
35
+ import { FIELDS, IDENTITY_FIELDS, loadFileSettingsSync } from './config.js'
36
+
37
+ /**
38
+ * Load `@deepseek-ai/schemastery` without letting a resolution failure matter.
39
+ *
40
+ * @returns {any | undefined} the schema constructor, or undefined
41
+ */
42
+ export function loadSchemaLibrary() {
43
+ try {
44
+ const require = createRequire(import.meta.url)
45
+ const loaded = require('@deepseek-ai/schemastery')
46
+ const Schema = loaded?.default ?? loaded
47
+ return typeof Schema === 'function' ? Schema : undefined
48
+ } catch {
49
+ return undefined
50
+ }
51
+ }
52
+
53
+ /** One leaf field: typed, defaulted from the settings file, described, volatile. */
54
+ function leaf(Schema, field, value) {
55
+ const base = field.kind === 'boolean'
56
+ ? Schema.boolean()
57
+ : field.kind === 'number'
58
+ ? Schema.number()
59
+ : Schema.string()
60
+ let node = base.default(value).description(field.label)
61
+ if (typeof field.min === 'number') node = node.min(field.min)
62
+ return node.volatile()
63
+ }
64
+
65
+ /** Why the schema could not be built, for the log and for tests. */
66
+ let schemaProblem
67
+
68
+ /** The reason `CONFIG` is undefined, or undefined when it was built. */
69
+ export function schemaBuildProblem() {
70
+ return schemaProblem
71
+ }
72
+
73
+ /**
74
+ * Build the schema.
75
+ *
76
+ * Defaults come from the settings file so the form opens on the values the
77
+ * plugin is actually using; the built-in defaults remain the last resort.
78
+ *
79
+ * @param {any} Schema the schemastery constructor (or undefined)
80
+ * @param {typeof import('./config.js').DEFAULTS} [seed] values to use as defaults
81
+ * @returns {any | undefined} the schema, or undefined when it cannot be built
82
+ */
83
+ export function buildConfigSchema(Schema, seed = loadFileSettingsSync()) {
84
+ if (Schema === undefined) {
85
+ schemaProblem = '@deepseek-ai/schemastery could not be resolved from this package'
86
+ return undefined
87
+ }
88
+ try {
89
+ const fields = {}
90
+ for (const field of FIELDS) fields[field.key] = leaf(Schema, field, seed[field.key])
91
+ fields.pinnedIdentity = Schema.object({
92
+ [IDENTITY_FIELDS[0].key]: leaf(Schema, IDENTITY_FIELDS[0], seed.pinnedIdentity?.name ?? ''),
93
+ [IDENTITY_FIELDS[1].key]: leaf(Schema, IDENTITY_FIELDS[1], seed.pinnedIdentity?.email ?? ''),
94
+ }).description('仅本次提交使用的 git 身份(以 -c 传入,不会改写你的 git 配置)')
95
+ const schema = Schema.object(fields)
96
+ schemaProblem = undefined
97
+ return schema
98
+ } catch (error) {
99
+ // A schema the library rejects would otherwise take the whole plugin down at
100
+ // mount; a missing settings form is the lesser failure by far.
101
+ schemaProblem = `${error?.name ?? 'Error'}: ${error?.message ?? String(error)}`
102
+ return undefined
103
+ }
104
+ }
105
+
106
+ /**
107
+ * The schema DSH mounts with this plugin.
108
+ *
109
+ * `undefined` when the schema library cannot be reached — see the note above.
110
+ */
111
+ export const CONFIG = buildConfigSchema(loadSchemaLibrary())
package/lib/skill.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The `git-commit-push` skill this plugin registers at mount time.
3
+ *
4
+ * WHY THE PLUGIN SHIPS ITS OWN SKILL
5
+ * The trigger policy — "commit only when the user asked, never on your own
6
+ * initiative" — has to reach the model through every surface it consults, not
7
+ * just the tool description. A skill file gives the model the full procedure
8
+ * (two-step `prepare` → `apply`, message conventions, boundaries) on demand.
9
+ *
10
+ * Why register it at runtime instead of telling the user to copy SKILL.md into
11
+ * their skills directory: this package is published to npm, so it is installed
12
+ * into `node_modules`, and asking every user to copy a file out of there by
13
+ * hand is exactly the kind of setup step that npm publishing is supposed to
14
+ * remove. `ctx.skills.register(...)` is the documented seam for an in-memory
15
+ * ("embedded") skill and needs no filesystem root.
16
+ *
17
+ * Source of truth is the shipped SKILL.md: the name, the description and the
18
+ * body are parsed from it, so the npm page, the skill catalog and the file can
19
+ * never drift. Precedence is the registry's business: a PROJECT-level skill of
20
+ * the same name outranks this runtime registration, so a user who keeps their
21
+ * own `git-commit-push` skill in the workspace still wins.
22
+ */
23
+ import { readFileSync } from 'node:fs'
24
+ import { fileURLToPath } from 'node:url'
25
+ import { dirname, join } from 'node:path'
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url))
28
+
29
+ /** Absolute path of the shipped skill file. */
30
+ export const SKILL_PATH = join(here, '..', 'SKILL.md')
31
+
32
+ /** Kebab-case skill name, as the registry's grammar demands. Matches the package name. */
33
+ export const SKILL_NAME = 'git-commit-push'
34
+
35
+ /** `source` label shown in skill listings; the registry adds the `runtime` provider. */
36
+ export const SKILL_SOURCE = 'dsh-plugin-git-commit-push'
37
+
38
+ /** The registry's public skill-name grammar (see `@deepseek-ai/dsh-skill`). */
39
+ const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
40
+
41
+ /**
42
+ * Split a `---`-fenced frontmatter block off a skill file.
43
+ *
44
+ * Deliberately a reader for the flat `key: value` frontmatter this project
45
+ * writes, not a YAML parser: the package ships with no dependencies, and the
46
+ * two fields read here (name, description) are single-line scalars.
47
+ *
48
+ * @param {string} text
49
+ * @returns {{ fields: Record<string, string>, content: string }}
50
+ */
51
+ export function parseSkillFile(text) {
52
+ const fenced = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/.exec(text)
53
+ if (fenced === null) return { fields: {}, content: text.trim() }
54
+ const fields = {}
55
+ for (const line of fenced[1].split(/\r?\n/)) {
56
+ const pair = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line)
57
+ if (pair === null) continue
58
+ const value = pair[2].trim()
59
+ fields[pair[1]] = /^(['"]).*\1$/.test(value) ? value.slice(1, -1) : value
60
+ }
61
+ return { fields, content: text.slice(fenced[0].length).trim() }
62
+ }
63
+
64
+ /**
65
+ * The definition handed to `ctx.skills.register(...)`.
66
+ *
67
+ * Field requirements are the registry's: a kebab-case `name`, a non-empty
68
+ * `description`, an optional `{ modelInvocable, userInvocable }` policy, and a
69
+ * `content` string. `provider` is filled in by the registry (`runtime`), so it
70
+ * is deliberately absent here.
71
+ *
72
+ * @returns {{ name: string, description: string, invocation: { modelInvocable: boolean, userInvocable: boolean }, source: string, content: string } | undefined}
73
+ * `undefined` when the shipped file is missing or unusable — a skill that
74
+ * cannot be registered must not take the tool down with it.
75
+ */
76
+ export function skillDefinition() {
77
+ let parsed
78
+ try {
79
+ parsed = parseSkillFile(readFileSync(SKILL_PATH, 'utf8'))
80
+ } catch {
81
+ return undefined
82
+ }
83
+ const name = parsed.fields.name ?? SKILL_NAME
84
+ const description = parsed.fields.description ?? ''
85
+ if (!SKILL_NAME_PATTERN.test(name) || description.trim() === '' || parsed.content.trim() === '') return undefined
86
+ return {
87
+ name,
88
+ description,
89
+ // The skill is model-facing procedure; the human-facing surface is the
90
+ // `/git-commit-push` command, so this skill is not offered as a user command.
91
+ invocation: { modelInvocable: true, userInvocable: false },
92
+ source: SKILL_SOURCE,
93
+ content: parsed.content,
94
+ }
95
+ }
package/lib/survey.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * One repository survey: everything the plugin needs to describe a changeset,
3
+ * gathered with a fixed, small number of git calls.
4
+ *
5
+ * Nothing in here writes to the repository. The only content it reads are
6
+ * bounded diff samples, which is what keeps a survey cheap enough that a model
7
+ * can afford to call it before every commit.
8
+ */
9
+ import {
10
+ currentBranch, git, gitAvailable, hasUpstream, headShort, isRepo,
11
+ numstat, rawDiff, recentSubjects, repoRoot, resolveRepo, status,
12
+ } from './git.js'
13
+ import {
14
+ VERSION_FILE, buildMessage, declaredSymbols, removedDeclarationCount, scopeOf, typeOfPath,
15
+ } from './analyze.js'
16
+
17
+ /** Git porcelain XY codes collapsed to one display status letter. */
18
+ function statusLetter(xy) {
19
+ const index = xy[0]
20
+ const worktree = xy[1]
21
+ if (xy === '??') return '?'
22
+ if (xy === '!!') return '!'
23
+ // `--porcelain=v2` uses '.' for "unchanged"; never let it fall through to 'M'.
24
+ if (index === 'R') return 'R'
25
+ if (index === 'C') return 'C'
26
+ if (index === 'D' || worktree === 'D') return 'D'
27
+ if (index === 'A') return 'A'
28
+ if (index === 'U' || worktree === 'U' || xy === 'AA' || xy === 'DD') return 'U'
29
+ if (worktree === 'M' || index === 'M') return 'M'
30
+ if (index === '.' && worktree === '.') return 'M'
31
+ return 'M'
32
+ }
33
+
34
+ /**
35
+ * Normalize porcelain entries into the plugin's change vocabulary.
36
+ *
37
+ * @param {{ xy: string, path: string, origPath?: string }[]} raw
38
+ * @returns {{ status: string, path: string, staged: boolean, unstaged: boolean, origPath?: string }[]}
39
+ */
40
+ export function normalizeEntries(raw) {
41
+ return raw.map(entry => ({
42
+ status: statusLetter(entry.xy),
43
+ path: entry.path,
44
+ staged: entry.xy !== '??' && entry.xy !== '!!' && entry.xy[0] !== ' ' && entry.xy[0] !== '?',
45
+ unstaged: entry.xy === '??' || entry.xy[1] !== ' ',
46
+ ...(entry.origPath === undefined ? {} : { origPath: entry.origPath }),
47
+ }))
48
+ }
49
+
50
+ /**
51
+ * Extract a version string from JSON, TOML, YAML or MSBuild text.
52
+ *
53
+ * The quote handling is load-bearing: in package.json the line is
54
+ * ` "version": "1.1.0",`, so a pattern anchored on a bare `version` key never
55
+ * matches and the tag question would silently never fire for the single most
56
+ * common version-carrying file.
57
+ */
58
+ function parseVersion(text) {
59
+ if (typeof text !== 'string' || text === '') return undefined
60
+ try {
61
+ const data = JSON.parse(text)
62
+ if (typeof data?.version === 'string') return data.version
63
+ if (typeof data?.Project?.Version === 'string') return data.Project.Version
64
+ } catch {
65
+ // Not JSON; fall through to the text patterns.
66
+ }
67
+ const patterns = [
68
+ // "version": "1.1.0" / version = '1.1.0' / version: 1.1.0
69
+ /^\s*["']?version["']?\s*[:=]\s*["']?(v?\d[\w.\-+]*)["']?/mi,
70
+ // MSBuild: <Version>1.1.0</Version>
71
+ /<Version>\s*([\w.\-+]+)\s*<\/Version>/i,
72
+ // Python module dunder: __version__ = "1.1.0"
73
+ /^\s*__version__\s*[:=]\s*["']?(v?\d[\w.\-+]*)["']?/mi,
74
+ ]
75
+ for (const pattern of patterns) {
76
+ const match = pattern.exec(text)
77
+ if (match !== null) return match[1]
78
+ }
79
+ return undefined
80
+ }
81
+
82
+ /**
83
+ * Detect a version bump by reading both sides from the repository itself.
84
+ *
85
+ * Both sides come from git (`git show HEAD:<f>` and `git show :<f>` — the index),
86
+ * never from the working tree, so an unstaged edit can never be reported as the
87
+ * version being committed, and the two sides are read in exactly the form
88
+ * `git add -A` + commit would use.
89
+ *
90
+ * @returns {Promise<{ file: string, from?: string, to?: string } | undefined>}
91
+ */
92
+ async function detectVersion(root, files) {
93
+ for (const file of files) {
94
+ let staged
95
+ try {
96
+ staged = await git(root, ['diff', '--cached', '--no-color', '--', file], { timeoutMs: 15_000 })
97
+ } catch {
98
+ continue
99
+ }
100
+ if (staged.trim() === '') continue
101
+
102
+ // Read the staged blob in full rather than rejoining the diff's added lines:
103
+ // a re-joined fragment is not valid JSON, which is exactly how a package.json
104
+ // bump used to be missed.
105
+ const to = parseVersion(await showBlob(root, `:${file}`))
106
+ if (to === undefined) continue
107
+ const from = parseVersion(await showBlob(root, `HEAD:${file}`))
108
+ if (from === to) continue
109
+ return { file, ...(from === undefined ? {} : { from }), to }
110
+ }
111
+ return undefined
112
+ }
113
+
114
+ /** Best-effort read of one blob; undefined when the revision has no such path. */
115
+ async function showBlob(root, rev) {
116
+ try {
117
+ return await git(root, ['show', rev], { timeoutMs: 15_000 })
118
+ } catch {
119
+ return undefined
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Read a repository and describe its pending changes.
125
+ *
126
+ * One deliberate side effect: when a version-carrying file is among the
127
+ * changes, that file is staged before the version is read. A version bump is
128
+ * only visible in `git diff --cached`, and this is what lets `prepare` report
129
+ * "1.2.3 → 1.2.4" — and therefore offer the right tag — without a second tool
130
+ * call. The commit path stages everything anyway, so nothing is decided early.
131
+ *
132
+ * @param {object} input
133
+ * @param {string} input.cwd the session working directory
134
+ * @param {string} [input.cwdOverride] an explicit repository path from the caller
135
+ * @param {number} [input.maxFilesShown]
136
+ * @param {'zh' | 'en'} [input.language]
137
+ * @returns {Promise<
138
+ * | { ok: false, notRepo: true, candidates: string[] }
139
+ * | { ok: false, notRepo?: false, reason: string, message: string }
140
+ * | { ok: true, root: string, branch: string, entries: any[], stats: Map<string, any>,
141
+ * recentSubjects: string[], version?: object, breaking: boolean,
142
+ * draft: { message: string, type: string, scope?: string, subject: string },
143
+ * hasUpstream: boolean, head?: string, unifiedDiff: string }
144
+ * >}
145
+ */
146
+ export async function survey(input) {
147
+ const { cwd, cwdOverride, language = 'zh', maxFilesShown = 12 } = input
148
+
149
+ const resolved = await resolveRepo(cwd, cwdOverride)
150
+ if ('notRepo' in resolved) {
151
+ // "git is not installed" and "this directory is not a repository" are
152
+ // different problems with different fixes; never report one as the other.
153
+ if (!await gitAvailable()) {
154
+ return { ok: false, reason: 'git-unavailable', message: '找不到 git 可执行文件,无法提交。请先安装 Git 并确保它在 PATH 中。' }
155
+ }
156
+ return { ok: false, notRepo: true, candidates: resolved.candidates }
157
+ }
158
+ const root = resolved.root
159
+
160
+ const [rawStatus, branch, upstream, head, subjects, diffText] = await Promise.all([
161
+ status(root),
162
+ currentBranch(root),
163
+ hasUpstream(root),
164
+ headShort(root),
165
+ recentSubjects(root, 8),
166
+ rawDiff(root),
167
+ ])
168
+
169
+ const entries = normalizeEntries(rawStatus)
170
+ if (entries.length === 0) {
171
+ return { ok: false, reason: 'clean', message: '没有需要提交的改动' }
172
+ }
173
+
174
+ const tracked = await numstat(root, false)
175
+ const stats = new Map(tracked)
176
+ for (const entry of entries) {
177
+ if (!stats.has(entry.path)) stats.set(entry.path, { added: 0, deleted: 0, binary: false })
178
+ }
179
+
180
+ const breaking = removedDeclarationCount(diffText) > 0
181
+ const versionFiles = entries.map(entry => entry.path).filter(path => VERSION_FILE.test(path))
182
+
183
+ // A version bump is only detectable once the file is staged, so stage the
184
+ // version-carrying files and then re-read the index. The commit path stages
185
+ // the whole working tree afterwards, so this never narrows what gets committed.
186
+ let version
187
+ if (versionFiles.length > 0) {
188
+ try {
189
+ await git(root, ['add', '-A', '--', ...versionFiles], { timeoutMs: 20_000 })
190
+ } catch {
191
+ // Best effort: an unstaged version file only means no tag is suggested.
192
+ }
193
+ version = await detectVersion(root, versionFiles)
194
+ }
195
+
196
+ const draft = buildMessage({
197
+ entries: entries.map(entry => ({ status: entry.status, path: entry.path, ...(entry.origPath === undefined ? {} : { origPath: entry.origPath }) })),
198
+ stats,
199
+ unifiedDiff: diffText,
200
+ recentSubjects: subjects,
201
+ language,
202
+ maxFiles: maxFilesShown,
203
+ })
204
+
205
+ return {
206
+ ok: true,
207
+ root,
208
+ branch,
209
+ entries,
210
+ stats,
211
+ recentSubjects: subjects,
212
+ ...(version === undefined ? {} : { version }),
213
+ breaking,
214
+ draft,
215
+ hasUpstream: upstream,
216
+ ...(head === undefined ? {} : { head }),
217
+ unifiedDiff: diffText,
218
+ }
219
+ }
220
+
221
+ /** Re-exported so callers can classify a path without importing analyze.js. */
222
+ export { typeOfPath, scopeOf, declaredSymbols }
223
+
224
+ /** Whether `dir` is a work-tree root, for the container-cwd error message. */
225
+ export { isRepo, repoRoot }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A throwaway git repository for tests.
3
+ *
4
+ * Everything lives under the OS temp directory and is removed on cleanup, so a
5
+ * test run can exercise real git without touching any repository the user
6
+ * cares about. Git's global and system config are disabled inside the fixture,
7
+ * which makes the commit identity explicit and keeps a user's `commit.gpgsign`,
8
+ * hooks or templates from leaking into a test.
9
+ */
10
+ import { execFileSync } from 'node:child_process'
11
+ import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'
12
+ import { dirname, join } from 'node:path'
13
+ import { tmpdir } from 'node:os'
14
+ import { isolatedGitEnv } from './git.js'
15
+
16
+ /** Deterministic identity so a fixture never depends on the machine's config. */
17
+ const IDENTITY = { name: 'dsh-plugin-test', email: 'test@example.invalid' }
18
+
19
+ /**
20
+ * Create a repository with one empty commit in a fresh temp directory.
21
+ *
22
+ * @param {string} [prefix] temp-directory prefix, for readable diagnostics
23
+ * @returns {{ dir: string, git: (args: string[]) => string, write: (path: string, text: string) => void, rename: (from: string, to: string) => void, remove: (path: string) => void, commitAll: (message: string) => void, cleanup: () => void }}
24
+ */
25
+ export function createFixtureRepo(prefix = 'git-commit-push-fixture-') {
26
+ const dir = mkdtempSync(join(tmpdir(), prefix))
27
+ // `isolatedGitEnv()` carries the per-platform `/dev/null` spelling too, which
28
+ // is what keeps this fixture correct on Windows.
29
+ const env = isolatedGitEnv()
30
+
31
+ /** Run one git command in the fixture and return stdout. */
32
+ const git = args => execFileSync('git', args, { cwd: dir, encoding: 'utf8', env })
33
+
34
+ /** Run git with the fixture's pinned identity (only commits need one). */
35
+ const committed = args => git([
36
+ '-c', `user.name=${IDENTITY.name}`, '-c', `user.email=${IDENTITY.email}`, ...args,
37
+ ])
38
+
39
+ git(['init', '-q', '-b', 'main'])
40
+ committed(['commit', '-q', '--allow-empty', '-m', 'chore: init'])
41
+
42
+ return {
43
+ dir,
44
+ git,
45
+ write(path, text) {
46
+ const absolute = join(dir, path)
47
+ mkdirSync(dirname(absolute), { recursive: true })
48
+ writeFileSync(absolute, text)
49
+ },
50
+ rename(from, to) {
51
+ renameSync(join(dir, from), join(dir, to))
52
+ },
53
+ remove(path) {
54
+ rmSync(join(dir, path), { force: true })
55
+ },
56
+ commitAll(message) {
57
+ git(['add', '-A'])
58
+ committed(['commit', '-q', '-m', message])
59
+ },
60
+ cleanup() {
61
+ rmSync(dir, { recursive: true, force: true })
62
+ },
63
+ }
64
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git commit & push",
4
+ "description": "One call commits and pushes: surveys the working tree, writes a Conventional Commits message, asks about a tag when one is warranted, then pushes. Also ships a /git-commit-push command that runs without the model."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git 提交与推送",
4
+ "description": "一次调用完成「提交 + 推送」:汇总改动、按 Conventional Commits 生成提交信息、必要时询问打标签,然后推送。另附不经过模型的 /git-commit-push 斜杠命令。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "dsh-plugin-git-commit-push",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "One-call Conventional-Commits git workflow for DeepSeek Harness: survey + commit + optional tag + push, plus a /git-commit-push slash command that runs with no model involvement. Windows and macOS/Linux.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./package.json": "./package.json",
10
+ "./locale/*": "./locale/*"
11
+ },
12
+ "icon": "./icon.svg",
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "bundle": {
16
+ "patch": "./cordis.patch.yml"
17
+ }
18
+ },
19
+ "engines": {
20
+ "node": ">=22.23.0",
21
+ "dsh": ">=0.2.0-rc.1 <0.3.0"
22
+ },
23
+ "dependencies": {
24
+ "@deepseek-ai/schemastery": "^3.18.4"
25
+ },
26
+ "peerDependencies": {
27
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.1"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@deepseek-ai/dsh-tools": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "scripts": {
35
+ "test": "node self-test.mjs && node self-test-git.mjs",
36
+ "probe": "node capture-git-format.mjs",
37
+ "prepublishOnly": "node self-test.mjs && node self-test-git.mjs"
38
+ },
39
+ "keywords": [
40
+ "dsh",
41
+ "dsh-plugin",
42
+ "deepseek",
43
+ "deepseek-harness",
44
+ "cordis-plugin",
45
+ "git",
46
+ "commit",
47
+ "push",
48
+ "conventional-commits",
49
+ "tag",
50
+ "agent"
51
+ ],
52
+ "author": "ygzhang <ygzhang.cn@msn.com>",
53
+ "license": "MIT",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/ygzhang-lab/dsh-plugin-git-commit-push.git"
57
+ },
58
+ "homepage": "https://github.com/ygzhang-lab/dsh-plugin-git-commit-push#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/ygzhang-lab/dsh-plugin-git-commit-push/issues"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "registry": "https://registry.npmjs.org/"
65
+ },
66
+ "files": [
67
+ "index.js",
68
+ "lib",
69
+ "cordis.patch.yml",
70
+ "locale",
71
+ "icon.svg",
72
+ "git-commit-push.config.json",
73
+ "setup.ps1",
74
+ "setup.sh",
75
+ "self-test.mjs",
76
+ "self-test-git.mjs",
77
+ "capture-git-format.mjs",
78
+ "e2e-check.mjs",
79
+ "SKILL.md",
80
+ "README.md",
81
+ "README.en.md"
82
+ ]
83
+ }