dsh-github-flow 0.1.5 → 0.1.7

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/executor.js CHANGED
@@ -119,22 +119,39 @@ export class GhExecutor {
119
119
  /**
120
120
  * 智能解析最可靠的工作区物理路径:
121
121
  * 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
122
- * 2. 读取当前进程/会话的 DSH_SESSION_ID 并匹配 ~/.dsh/storages/workspace.json
123
- * 3. 匹配 ~/.dsh/storages/workspace.json 中最近活跃的工作区
124
- * 4. 回退到 process.cwd()
122
+ * 2. 使用显式传入的 sessionId 或环境变量 DSH_SESSION_ID 精确反查工作区
123
+ * 3. 递归探测:若传入了 explicitCwd(如聚合父目录),自动检查其直系子目录中是否包含 Git 仓库
124
+ * 4. 匹配 ~/.dsh/storages/workspace.json 中最近活跃且存在 .git 的仓库工作区
125
+ * 5. 回退到 process.cwd()
125
126
  */
126
- resolveWorkspaceCwd(explicitCwd) {
127
- // 显式路径优先,但只认真正含 .git 的目录:调用方传进来的可能是会话根目录而非仓库目录,
128
- // 此时继续往下走按会话/最近活跃反查,比盲信传入值更准。
127
+ resolveWorkspaceCwd(explicitCwd, explicitSessionId) {
128
+ // 1. 显式路径优先,若有效且直接包含 .git 目录,则直接使用
129
129
  if (explicitCwd && hasGitDir(explicitCwd)) {
130
130
  return explicitCwd;
131
131
  }
132
- const sessionId = process.env.DSH_SESSION_ID;
133
- if (sessionId) {
134
- const bySession = findWorkspacePathBySession(sessionId);
135
- if (bySession)
132
+ // 2. 使用显式传入的 sessionId 或环境变量中的 DSH_SESSION_ID 反查
133
+ const targetSessionId = explicitSessionId || process.env.DSH_SESSION_ID;
134
+ if (targetSessionId) {
135
+ const bySession = findWorkspacePathBySession(targetSessionId);
136
+ if (bySession && hasGitDir(bySession))
136
137
  return bySession;
137
138
  }
139
+ // 3. 递归探测:若传入了 explicitCwd(如非 Git 的父工作区目录),检查其一级子目录中是否存在 Git 仓库
140
+ if (explicitCwd && fs.existsSync(explicitCwd)) {
141
+ try {
142
+ const entries = fs.readdirSync(explicitCwd, { withFileTypes: true });
143
+ for (const entry of entries) {
144
+ if (entry.isDirectory()) {
145
+ const subDir = path.join(explicitCwd, entry.name);
146
+ if (hasGitDir(subDir)) {
147
+ return subDir;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ catch { }
153
+ }
154
+ // 4. 从工作区配置中匹配最近活跃且明确具备 .git 的仓库路径
138
155
  const recent = findMostRecentWorkspacePath();
139
156
  if (recent)
140
157
  return recent;
@@ -145,7 +162,7 @@ export class GhExecutor {
145
162
  * 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
146
163
  */
147
164
  async run(args, options = {}) {
148
- const cwd = this.resolveWorkspaceCwd(options.cwd);
165
+ const cwd = this.resolveWorkspaceCwd(options.cwd, options.sessionId);
149
166
  const timeout = options.timeoutMs || this.defaultTimeoutMs;
150
167
  try {
151
168
  const { stdout, stderr } = await execFileAsync(this.ghPath, args, {
@@ -222,19 +239,46 @@ export class GhExecutor {
222
239
  }
223
240
  return value;
224
241
  }
242
+ /** 生成当前操作系统平台的推荐安装引导与命令 */
243
+ getPlatformInstallGuide() {
244
+ const p = process.platform;
245
+ if (p === 'win32') {
246
+ return {
247
+ command: 'winget install --id GitHub.cli',
248
+ altCommand: 'scoop install gh',
249
+ downloadUrl: 'https://cli.github.com/',
250
+ description: '推荐使用 Windows Package Manager (winget) 一键安装,或下载官方 .msi 安装包。',
251
+ };
252
+ }
253
+ if (p === 'darwin') {
254
+ return {
255
+ command: 'brew install gh',
256
+ altCommand: 'port install gh',
257
+ downloadUrl: 'https://cli.github.com/',
258
+ description: '推荐使用 Homebrew 安装 GitHub CLI。',
259
+ };
260
+ }
261
+ return {
262
+ command: 'sudo apt install gh',
263
+ altCommand: 'sudo dnf install gh',
264
+ downloadUrl: 'https://cli.github.com/',
265
+ description: '推荐使用系统包管理器安装(Debian/Ubuntu 运行 apt,Fedora/CentOS 运行 dnf)。',
266
+ };
267
+ }
225
268
  /** 实际执行 `gh auth status` 并解析结果,不含缓存逻辑 */
226
269
  async probeAuth(cwd) {
227
270
  const res = await this.run(['auth', 'status'], { cwd, timeoutMs: 10_000, rawText: true });
228
271
  const raw = (res.rawOutput || '') + '\n' + (res.error || '');
229
272
  const loggedIn = raw.includes('Logged in to') || raw.includes('✓');
230
273
  // 只要已登录或有常规输出,则必然安装了 gh
231
- const isNotInstalled = Boolean(res.error && res.error.includes('系统未检测到 GitHub CLI'));
274
+ const isNotInstalled = Boolean(res.error && (res.error.includes('系统未检测到 GitHub CLI') || res.error.includes('ENOENT')));
232
275
  const installed = loggedIn || !isNotInstalled;
233
276
  if (!installed) {
234
277
  return {
235
278
  installed: false,
236
279
  loggedIn: false,
237
280
  platform: process.platform,
281
+ installGuide: this.getPlatformInstallGuide(),
238
282
  raw: res.error,
239
283
  };
240
284
  }
@@ -312,8 +356,8 @@ export class GhExecutor {
312
356
  * 获取当前目录(工作区)关联的远程仓库元数据
313
357
  * 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
314
358
  */
315
- async getRepoMetadata(cwd) {
316
- const targetCwd = this.resolveWorkspaceCwd(cwd);
359
+ async getRepoMetadata(cwd, sessionId) {
360
+ const targetCwd = this.resolveWorkspaceCwd(cwd, sessionId);
317
361
  // 1. 优先尝试由 gh 直接根据当前上下文识别
318
362
  const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
319
363
  if (res.ok && res.data && res.data.nameWithOwner) {
package/lib/tools/api.js CHANGED
@@ -54,6 +54,7 @@ export function createApiTool(executor) {
54
54
  },
55
55
  async execute(args, execContext) {
56
56
  const cwd = execContext?.cwd || process.cwd();
57
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
57
58
  const cmd = ['api', args.endpoint];
58
59
  if (args.method && args.method !== 'GET') {
59
60
  cmd.push('-X', args.method);
@@ -81,7 +82,7 @@ export function createApiTool(executor) {
81
82
  cmd.push('-f', `${k}=${String(v)}`);
82
83
  }
83
84
  }
84
- const res = await executor.run(cmd, { cwd, rawText: args.raw === true });
85
+ const res = await executor.run(cmd, { cwd, sessionId, rawText: args.raw === true });
85
86
  if (!res.ok)
86
87
  throw new Error(res.error);
87
88
  return normalizeOutput(res.data !== undefined ? res.data : res.rawOutput);
@@ -64,17 +64,22 @@ export function createIssueTool(executor) {
64
64
  },
65
65
  async execute(args, execContext) {
66
66
  const cwd = execContext?.cwd || process.cwd();
67
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
67
68
  let repoTarget = args.repo;
68
69
  if (!repoTarget) {
69
- const meta = await executor.getRepoMetadata(cwd);
70
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
70
71
  if (meta?.nameWithOwner) {
71
72
  repoTarget = meta.nameWithOwner;
72
73
  }
73
74
  }
74
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
75
+ if (!repoTarget) {
76
+ throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
77
+ }
78
+ const repoArgs = ['-R', repoTarget];
79
+ const runOpts = { cwd, sessionId };
75
80
  switch (args.action) {
76
81
  case 'list': {
77
- const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], { cwd });
82
+ const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], runOpts);
78
83
  if (!res.ok)
79
84
  throw new Error(res.error);
80
85
  return normalizeOutput(res.data || res.rawOutput);
@@ -82,7 +87,7 @@ export function createIssueTool(executor) {
82
87
  case 'view': {
83
88
  if (!args.issue_number)
84
89
  throw new Error('view 操作必须提供 issue_number');
85
- const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], { cwd });
90
+ const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], runOpts);
86
91
  if (!res.ok)
87
92
  throw new Error(res.error);
88
93
  return normalizeOutput(res.data || res.rawOutput);
@@ -95,7 +100,7 @@ export function createIssueTool(executor) {
95
100
  if (args.assignees && Array.isArray(args.assignees) && args.assignees.length > 0) {
96
101
  cmd.push('--assignee', args.assignees.join(','));
97
102
  }
98
- const res = await executor.run(cmd, { cwd });
103
+ const res = await executor.run(cmd, runOpts);
99
104
  if (!res.ok)
100
105
  throw new Error(res.error);
101
106
  return { message: 'Issue 创建成功', url: res.rawOutput };
@@ -103,7 +108,7 @@ export function createIssueTool(executor) {
103
108
  case 'comment': {
104
109
  if (!args.issue_number || !args.body)
105
110
  throw new Error('comment 必须提供 issue_number 和 body');
106
- const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body], { cwd });
111
+ const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body], runOpts);
107
112
  if (!res.ok)
108
113
  throw new Error(res.error);
109
114
  return { message: '评论发布成功', output: res.rawOutput };
@@ -111,7 +116,7 @@ export function createIssueTool(executor) {
111
116
  case 'close': {
112
117
  if (!args.issue_number)
113
118
  throw new Error('close 操作必须提供 issue_number');
114
- const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs], { cwd });
119
+ const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs], runOpts);
115
120
  if (!res.ok)
116
121
  throw new Error(res.error);
117
122
  return { message: `Issue #${args.issue_number} 已成功关闭` };
@@ -119,7 +124,7 @@ export function createIssueTool(executor) {
119
124
  case 'reopen': {
120
125
  if (!args.issue_number)
121
126
  throw new Error('reopen 操作必须提供 issue_number');
122
- const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs], { cwd });
127
+ const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs], runOpts);
123
128
  if (!res.ok)
124
129
  throw new Error(res.error);
125
130
  return { message: `Issue #${args.issue_number} 已重新开启` };
package/lib/tools/pr.js CHANGED
@@ -84,38 +84,43 @@ export function createPrTool(executor) {
84
84
  },
85
85
  async execute(args, execContext) {
86
86
  const cwd = execContext?.cwd || process.cwd();
87
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
87
88
  let repoTarget = args.repo;
88
89
  if (!repoTarget) {
89
- const meta = await executor.getRepoMetadata(cwd);
90
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
90
91
  if (meta?.nameWithOwner) {
91
92
  repoTarget = meta.nameWithOwner;
92
93
  }
93
94
  }
94
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
95
+ if (!repoTarget) {
96
+ throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
97
+ }
98
+ const repoArgs = ['-R', repoTarget];
99
+ const runOpts = { cwd, sessionId };
95
100
  switch (args.action) {
96
101
  case 'list': {
97
- const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], { cwd });
102
+ const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], runOpts);
98
103
  if (!res.ok)
99
104
  throw new Error(res.error);
100
105
  return normalizeOutput(res.data || res.rawOutput);
101
106
  }
102
107
  case 'view': {
103
108
  const target = args.pr_number ? [String(args.pr_number)] : [];
104
- const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], { cwd });
109
+ const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], runOpts);
105
110
  if (!res.ok)
106
111
  throw new Error(res.error);
107
112
  return normalizeOutput(res.data || res.rawOutput);
108
113
  }
109
114
  case 'checks': {
110
115
  const target = args.pr_number ? [String(args.pr_number)] : [];
111
- const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], { cwd });
116
+ const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], runOpts);
112
117
  if (!res.ok)
113
118
  throw new Error(res.error);
114
119
  return normalizeOutput(res.data || res.rawOutput);
115
120
  }
116
121
  case 'diff': {
117
122
  const target = args.pr_number ? [String(args.pr_number)] : [];
118
- const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], { cwd });
123
+ const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], runOpts);
119
124
  if (!res.ok)
120
125
  throw new Error(res.error);
121
126
  return { result: res.rawOutput || '无差异内容' };
@@ -134,7 +139,7 @@ export function createPrTool(executor) {
134
139
  const dryRun = args.dry_run === true;
135
140
  if (dryRun)
136
141
  createCmd.push('--dry-run');
137
- const res = await executor.run(createCmd, { cwd });
142
+ const res = await executor.run(createCmd, runOpts);
138
143
  if (!res.ok)
139
144
  throw new Error(res.error);
140
145
  return dryRun
@@ -153,7 +158,7 @@ export function createPrTool(executor) {
153
158
  reviewCmd.push('--comment');
154
159
  if (args.body)
155
160
  reviewCmd.push('--body', args.body);
156
- const res = await executor.run(reviewCmd, { cwd });
161
+ const res = await executor.run(reviewCmd, runOpts);
157
162
  if (!res.ok)
158
163
  throw new Error(res.error);
159
164
  return { message: 'Review 提交成功', detail: res.rawOutput };
@@ -167,7 +172,7 @@ export function createPrTool(executor) {
167
172
  // 而 gh 默认不删。替用户做掉它违背「可撤销的直接做、不可撤销的先问」。
168
173
  if (args.delete_branch === true)
169
174
  mergeCmd.push('--delete-branch');
170
- const res = await executor.run(mergeCmd, { cwd });
175
+ const res = await executor.run(mergeCmd, runOpts);
171
176
  if (!res.ok)
172
177
  throw new Error(res.error);
173
178
  return { message: 'PR 合并完成', output: res.rawOutput };
package/lib/tools/repo.js CHANGED
@@ -50,19 +50,21 @@ export function createRepoTool(executor) {
50
50
  },
51
51
  async execute(args, execContext) {
52
52
  const cwd = execContext?.cwd || process.cwd();
53
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
53
54
  const limitStr = String(args.limit || 10);
55
+ const runOpts = { cwd, sessionId };
54
56
  switch (args.action) {
55
57
  case 'view': {
56
58
  let repoTarget = args.repo;
57
59
  let meta = null;
58
60
  if (!repoTarget) {
59
- meta = await executor.getRepoMetadata(cwd);
61
+ meta = await executor.getRepoMetadata(cwd, sessionId);
60
62
  if (meta?.nameWithOwner) {
61
63
  repoTarget = meta.nameWithOwner;
62
64
  }
63
65
  }
64
66
  const repoArgs = repoTarget ? [repoTarget] : [];
65
- const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'], { cwd });
67
+ const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'], runOpts);
66
68
  if (!res.ok) {
67
69
  // 若执行 gh 失败(如离线或环境限制),但本地已成功识别仓库元数据,进行优雅兜底返回
68
70
  if (meta && meta.nameWithOwner) {
@@ -76,7 +78,7 @@ export function createRepoTool(executor) {
76
78
  if (!args.query)
77
79
  throw new Error('search_code 必须提供 query');
78
80
  const repoArgs = args.repo ? ['--repo', args.repo] : [];
79
- const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr], { cwd });
81
+ const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr], runOpts);
80
82
  if (!res.ok)
81
83
  throw new Error(res.error);
82
84
  return normalizeOutput(res.data || res.rawOutput);
@@ -84,7 +86,7 @@ export function createRepoTool(executor) {
84
86
  case 'search_repos': {
85
87
  if (!args.query)
86
88
  throw new Error('search_repos 必须提供 query');
87
- const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr], { cwd });
89
+ const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr], runOpts);
88
90
  if (!res.ok)
89
91
  throw new Error(res.error);
90
92
  return normalizeOutput(res.data || res.rawOutput);
package/lib/tools/run.js CHANGED
@@ -50,17 +50,22 @@ export function createRunTool(executor) {
50
50
  },
51
51
  async execute(args, execContext) {
52
52
  const cwd = execContext?.cwd || process.cwd();
53
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
53
54
  let repoTarget = args.repo;
54
55
  if (!repoTarget) {
55
- const meta = await executor.getRepoMetadata(cwd);
56
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
56
57
  if (meta?.nameWithOwner) {
57
58
  repoTarget = meta.nameWithOwner;
58
59
  }
59
60
  }
60
- const repoArgs = repoTarget ? ['-R', repoTarget] : [];
61
+ if (!repoTarget) {
62
+ throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
63
+ }
64
+ const repoArgs = ['-R', repoTarget];
65
+ const runOpts = { cwd, sessionId };
61
66
  switch (args.action) {
62
67
  case 'list': {
63
- const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)], { cwd });
68
+ const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)], runOpts);
64
69
  if (!res.ok)
65
70
  throw new Error(res.error);
66
71
  return normalizeOutput(res.data || res.rawOutput);
@@ -68,7 +73,7 @@ export function createRunTool(executor) {
68
73
  case 'view': {
69
74
  if (!args.run_id)
70
75
  throw new Error('view 操作必须提供 run_id');
71
- const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'], { cwd });
76
+ const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'], runOpts);
72
77
  if (!res.ok)
73
78
  throw new Error(res.error);
74
79
  return normalizeOutput(res.data || res.rawOutput);
@@ -78,7 +83,7 @@ export function createRunTool(executor) {
78
83
  throw new Error('log_failed 操作必须提供 run_id');
79
84
  // gh 原生支持仅提取失败的日志!对 Agent 故障诊断价值极高
80
85
  const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--log-failed'], {
81
- cwd,
86
+ ...runOpts,
82
87
  timeoutMs: 60_000,
83
88
  rawText: true,
84
89
  });
@@ -89,7 +94,7 @@ export function createRunTool(executor) {
89
94
  case 'rerun': {
90
95
  if (!args.run_id)
91
96
  throw new Error('rerun 操作必须提供 run_id');
92
- const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'], { cwd });
97
+ const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'], runOpts);
93
98
  if (!res.ok)
94
99
  throw new Error(res.error);
95
100
  return { message: '已成功重新触发失败任务', output: res.rawOutput };
@@ -97,7 +102,7 @@ export function createRunTool(executor) {
97
102
  case 'cancel': {
98
103
  if (!args.run_id)
99
104
  throw new Error('cancel 操作必须提供 run_id');
100
- const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs], { cwd });
105
+ const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs], runOpts);
101
106
  if (!res.ok)
102
107
  throw new Error(res.error);
103
108
  return { message: '已取消该工作流任务', output: res.rawOutput };
package/lib/types.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface PluginConfig {
18
18
  export interface GhExecutionOptions {
19
19
  /** 命令执行的工作目录,优先使用当前 Agent 会话所在的 Workspace 物理路径 */
20
20
  cwd?: string;
21
+ /** 当前调用的会话 ID,用于精确关联物理工作区路径 */
22
+ sessionId?: string;
21
23
  /** 进程超时限制(毫秒),默认 30,000ms */
22
24
  timeoutMs?: number;
23
25
  /** 附加环境变量 */
@@ -33,6 +35,16 @@ export interface GhResult<T = unknown> {
33
35
  /** 是否因超出安全阈值而被截断 */
34
36
  truncated?: boolean;
35
37
  }
38
+ export interface AuthInstallGuide {
39
+ /** 推荐包管理器命令,例如 Windows 下 "winget install --id GitHub.cli" */
40
+ command: string;
41
+ /** 备选命令(如 choco/scoop/brew) */
42
+ altCommand?: string;
43
+ /** 官方下载与安装文档链接 */
44
+ downloadUrl: string;
45
+ /** 简明指引提示 */
46
+ description?: string;
47
+ }
36
48
  export interface AuthStatus {
37
49
  installed: boolean;
38
50
  loggedIn: boolean;
@@ -42,6 +54,7 @@ export interface AuthStatus {
42
54
  activeAccount?: boolean;
43
55
  protocol?: string;
44
56
  scopes?: string[];
57
+ installGuide?: AuthInstallGuide;
45
58
  raw?: string;
46
59
  }
47
60
  export interface RepoMetadata {
@@ -75,6 +88,19 @@ export interface PrItem {
75
88
  url: string;
76
89
  updatedAt: string;
77
90
  }
91
+ /**
92
+ * Issue 分类维度契约:
93
+ * - reported: 用户提起的(他人向我名下项目或本仓库提交的 Issue 反馈)
94
+ * - assigned: 分配给我的(指派给当前账号的待办任务,跨仓库通用)
95
+ * - created: 我创建的(由当前账号亲自发起的 Issue)
96
+ */
97
+ export type IssueCategory = 'reported' | 'assigned' | 'created';
98
+ export interface IssueStats {
99
+ total: number;
100
+ reported: number;
101
+ assigned: number;
102
+ created: number;
103
+ }
78
104
  export interface IssueItem {
79
105
  number: number;
80
106
  title: string;
@@ -91,6 +117,8 @@ export interface IssueItem {
91
117
  }>;
92
118
  url: string;
93
119
  updatedAt: string;
120
+ /** 命中的所有身份分类 */
121
+ categories?: IssueCategory[];
94
122
  }
95
123
  export interface WorkflowRunItem {
96
124
  databaseId: number;
@@ -107,6 +135,7 @@ export interface GitHubOverviewData {
107
135
  repo?: RepoMetadata;
108
136
  pullRequests: PrItem[];
109
137
  issues: IssueItem[];
138
+ issueStats?: IssueStats;
110
139
  runs: WorkflowRunItem[];
111
140
  lastUpdated: string;
112
141
  error?: string;
@@ -129,6 +158,28 @@ export interface WorkspaceMatrixItem {
129
158
  repo?: RepoMetadata;
130
159
  sessionCount: number;
131
160
  }
161
+ export interface GlobalIssueItem {
162
+ number: number;
163
+ title: string;
164
+ repository: {
165
+ name?: string;
166
+ nameWithOwner: string;
167
+ };
168
+ author?: {
169
+ login: string;
170
+ };
171
+ assignees?: Array<{
172
+ login: string;
173
+ }>;
174
+ labels?: Array<{
175
+ name: string;
176
+ color: string;
177
+ }>;
178
+ url: string;
179
+ updatedAt: string;
180
+ state?: 'OPEN' | 'CLOSED';
181
+ categories: IssueCategory[];
182
+ }
132
183
  export interface GlobalOverviewData {
133
184
  auth: AuthStatus;
134
185
  userRepos: UserRepoItem[];
@@ -142,15 +193,8 @@ export interface GlobalOverviewData {
142
193
  url: string;
143
194
  updatedAt: string;
144
195
  }>;
145
- myIssues: Array<{
146
- number: number;
147
- title: string;
148
- repository: {
149
- nameWithOwner: string;
150
- };
151
- url: string;
152
- updatedAt: string;
153
- }>;
196
+ myIssues: GlobalIssueItem[];
197
+ issueStats?: IssueStats;
154
198
  lastUpdated: string;
155
199
  error?: string;
156
200
  /** 工作区矩阵读取失败的原因。区分「没有工作区」与「读不到工作区」,避免把故障显示成空状态 */
@@ -27,9 +27,12 @@ export declare function tryReadWorkspaces(): {
27
27
  */
28
28
  export declare function findWorkspacePathBySession(sessionId: string): string | undefined;
29
29
  /**
30
- * 匹配最近活跃的工作区路径。
31
- * 注意 updatedAt 可能缺失或非 ISO 格式,统一用时间戳比较,无效值视为 0 而非 NaN——
32
- * 后者会让所有比较都返回 false,导致永远选不中任何工作区。
30
+ * 匹配最近活跃且包含 Git 仓库的工作区路径。
31
+ *
32
+ * 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
33
+ * 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
34
+ * 进而导致后续 gh 命令报出 "fatal: not a git repository"。
35
+ * 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
33
36
  */
34
37
  export declare function findMostRecentWorkspacePath(): string | undefined;
35
38
  export declare function hasGitDir(dir: string): boolean;
package/lib/workspace.js CHANGED
@@ -79,24 +79,39 @@ export function findWorkspacePathBySession(sessionId) {
79
79
  return undefined;
80
80
  }
81
81
  /**
82
- * 匹配最近活跃的工作区路径。
83
- * 注意 updatedAt 可能缺失或非 ISO 格式,统一用时间戳比较,无效值视为 0 而非 NaN——
84
- * 后者会让所有比较都返回 false,导致永远选不中任何工作区。
82
+ * 匹配最近活跃且包含 Git 仓库的工作区路径。
83
+ *
84
+ * 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
85
+ * 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
86
+ * 进而导致后续 gh 命令报出 "fatal: not a git repository"。
87
+ * 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
85
88
  */
86
89
  export function findMostRecentWorkspacePath() {
87
90
  try {
88
91
  const { workspaces } = tryReadWorkspaces();
89
- let best = null;
90
- let bestAt = -1;
92
+ let bestGitWs = null;
93
+ let bestGitAt = -1;
94
+ let fallbackWs = null;
95
+ let fallbackAt = -1;
91
96
  for (const ws of workspaces) {
97
+ if (!ws.path || !fs.existsSync(ws.path))
98
+ continue;
92
99
  const at = Date.parse(ws.updatedAt || '');
93
100
  const score = Number.isFinite(at) ? at : 0;
94
- if (score > bestAt && ws.path && fs.existsSync(ws.path)) {
95
- best = ws;
96
- bestAt = score;
101
+ if (hasGitDir(ws.path)) {
102
+ if (score > bestGitAt) {
103
+ bestGitWs = ws;
104
+ bestGitAt = score;
105
+ }
106
+ }
107
+ else {
108
+ if (score > fallbackAt) {
109
+ fallbackWs = ws;
110
+ fallbackAt = score;
111
+ }
97
112
  }
98
113
  }
99
- return best?.path;
114
+ return bestGitWs?.path || fallbackWs?.path;
100
115
  }
101
116
  catch {
102
117
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-github-flow",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "icon": "icon.svg",
5
5
  "description": "GitHub Flow: Native GitHub CLI (gh) integration for DeepSeek Harness — Agent Tools, Slash Commands, and Web UI Workspace",
6
6
  "type": "module",