aws-runtime-bridge 1.7.35 → 1.7.36

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.
@@ -18,6 +18,13 @@ interface GitRepositoryContext {
18
18
  relativeWorkspacePath: string;
19
19
  }
20
20
  export declare function assertGitWorkspacePathAccessible(workspacePath: string): Promise<void>;
21
+ interface GitStashItem {
22
+ ref: string;
23
+ stashName?: string;
24
+ message: string;
25
+ branch: string;
26
+ createdAt: string | null;
27
+ }
21
28
  type GitDiffFileStatus = 'modified' | 'added' | 'deleted' | 'renamed';
22
29
  /**
23
30
  * 判断 Git diff 汇总行是否应该展示在差异树中。
@@ -37,11 +44,17 @@ export declare function createMissingInitialCommitSnapshotError(): string;
37
44
  export declare function isGitStashNoChangesOutput(output: string): boolean;
38
45
  interface LatestGitStash {
39
46
  ref: string;
47
+ stashName: string;
40
48
  message: string;
41
49
  }
50
+ /**
51
+ * 解析 git stash list --format 输出行。
52
+ * 主流程:优先返回稳定 stash commit hash 作为 ref,同时保留 stash@{n} 供界面展示。
53
+ */
54
+ export declare function parseGitStashListLine(line: string): GitStashItem | null;
42
55
  /**
43
56
  * 从 git stash list -n 1 输出解析最近一次 stash。
44
- * 主流程:提取 stash 引用并保留后续描述,供无变更快照复用最近快照引用。
57
+ * 主流程:提取稳定 stash commit hash,供无变更快照复用最近快照引用。
45
58
  */
46
59
  export declare function parseLatestGitStash(output: string): LatestGitStash | null;
47
60
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/routes/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,eAAO,MAAM,SAAS,4CAAW,CAAC;AAIlC;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAOnD;AAED,UAAU,oBAAoB;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAsB,gCAAgC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3F;AASD,KAAK,iBAAiB,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAkBtE;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,GACpB,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK3E;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKjE;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAWzE;AA+KD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAmBrE;AAkDD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAElE;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAElH;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAErI"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/routes/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,eAAO,MAAM,SAAS,4CAAW,CAAC;AAIlC;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAOnD;AAED,UAAU,oBAAoB;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAsB,gCAAgC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3F;AAED,UAAU,YAAY;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,KAAK,iBAAiB,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAkBtE;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,GACpB,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK3E;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKjE;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAmCvE;AAkBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAYzE;AA+KD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAmBrE;AAkDD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAElE;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAElH;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAErI"}
@@ -71,20 +71,73 @@ export function isGitStashNoChangesOutput(output) {
71
71
  || normalized.includes('no changes to save')
72
72
  || normalized.includes('没有本地变更需要保存');
73
73
  }
74
+ /**
75
+ * 解析 git stash list --format 输出行。
76
+ * 主流程:优先返回稳定 stash commit hash 作为 ref,同时保留 stash@{n} 供界面展示。
77
+ */
78
+ export function parseGitStashListLine(line) {
79
+ const rawLine = String(line || '').trim();
80
+ if (!rawLine)
81
+ return null;
82
+ const fields = rawLine.split('\0');
83
+ if (fields.length >= 3) {
84
+ const stableRef = fields[0]?.trim();
85
+ const stashName = fields[1]?.trim();
86
+ const subject = fields[2]?.trim() || stashName;
87
+ const createdAt = fields[3]?.trim() || null;
88
+ if (!stableRef || !stashName)
89
+ return null;
90
+ const parsedSubject = parseGitStashSubject(subject || '');
91
+ return {
92
+ ref: stableRef,
93
+ stashName,
94
+ message: parsedSubject.message,
95
+ branch: parsedSubject.branch,
96
+ createdAt,
97
+ };
98
+ }
99
+ const match = rawLine.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);
100
+ if (!match)
101
+ return null;
102
+ const stashName = match[1];
103
+ const parsedSubject = parseGitStashSubject(match[2]?.trim() || stashName);
104
+ return {
105
+ ref: stashName,
106
+ stashName,
107
+ message: parsedSubject.message,
108
+ branch: parsedSubject.branch,
109
+ createdAt: null,
110
+ };
111
+ }
112
+ /**
113
+ * 从 stash subject 中拆出分支和用户消息。
114
+ */
115
+ function parseGitStashSubject(subject) {
116
+ const normalized = String(subject || '').trim();
117
+ const branchMatch = normalized.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);
118
+ if (branchMatch) {
119
+ return {
120
+ branch: branchMatch[1].trim(),
121
+ message: branchMatch[2].trim() || normalized,
122
+ };
123
+ }
124
+ return { branch: '', message: normalized };
125
+ }
74
126
  /**
75
127
  * 从 git stash list -n 1 输出解析最近一次 stash。
76
- * 主流程:提取 stash 引用并保留后续描述,供无变更快照复用最近快照引用。
128
+ * 主流程:提取稳定 stash commit hash,供无变更快照复用最近快照引用。
77
129
  */
78
130
  export function parseLatestGitStash(output) {
79
131
  const firstLine = String(output || '').split('\n').find(line => line.trim());
80
132
  if (!firstLine)
81
133
  return null;
82
- const match = firstLine.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);
83
- if (!match)
134
+ const parsed = parseGitStashListLine(firstLine);
135
+ if (!parsed)
84
136
  return null;
85
137
  return {
86
- ref: match[1],
87
- message: match[2]?.trim() || match[1],
138
+ ref: parsed.ref,
139
+ stashName: parsed.stashName || parsed.ref,
140
+ message: parsed.message || parsed.ref,
88
141
  };
89
142
  }
90
143
  /**
@@ -374,13 +427,14 @@ gitRouter.post('/git/stash/create', validateToken, async (req, res) => {
374
427
  : defaultMessage;
375
428
  const result = await execGitCommand(normalizedPath, ['stash', 'push', '-m', stashMessage]);
376
429
  if (isGitStashNoChangesOutput(`${result.stdout}\n${result.stderr}`)) {
377
- const latestResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1']);
430
+ const latestResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
378
431
  const latestStash = latestResult.exitCode === 0 ? parseLatestGitStash(latestResult.stdout) : null;
379
432
  res.json({
380
433
  ok: true,
381
434
  stashRef: latestStash?.ref ?? null,
435
+ stashName: latestStash?.stashName ?? null,
382
436
  message: latestStash
383
- ? `没有本地变更需要保存,已复用最近快照 ${latestStash.ref}`
437
+ ? `没有本地变更需要保存,已复用最近快照 ${latestStash.stashName}`
384
438
  : '没有本地变更需要保存',
385
439
  noChanges: true,
386
440
  reusedLatestStash: Boolean(latestStash),
@@ -396,13 +450,13 @@ gitRouter.post('/git/stash/create', validateToken, async (req, res) => {
396
450
  res.status(400).json({ error: result.stderr || 'git stash push failed' });
397
451
  return;
398
452
  }
399
- const listResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1']);
400
- const firstLine = listResult.stdout.split('\n')[0] || '';
401
- const match = firstLine.match(/^(stash@\{\d+\})/);
402
- const stashRef = match ? match[1] : 'stash@{0}';
453
+ const listResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
454
+ const latestStash = listResult.exitCode === 0 ? parseLatestGitStash(listResult.stdout) : null;
455
+ const stashRef = latestStash?.ref || 'stash@{0}';
403
456
  res.json({
404
457
  ok: true,
405
458
  stashRef,
459
+ stashName: latestStash?.stashName ?? null,
406
460
  message: stashMessage,
407
461
  workspacePath: normalizedPath
408
462
  });
@@ -425,33 +479,15 @@ gitRouter.post('/git/stash/list', validateToken, async (req, res) => {
425
479
  try {
426
480
  const normalizedPath = path.resolve(String(workspacePath).trim());
427
481
  const stashLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
428
- const result = await execGitCommand(normalizedPath, ['stash', 'list', `-n`, String(stashLimit)]);
482
+ const result = await execGitCommand(normalizedPath, ['stash', 'list', '-n', String(stashLimit), '--format=%H%x00%gd%x00%gs%x00%cr']);
429
483
  if (result.exitCode !== 0) {
430
484
  res.status(400).json({ error: result.stderr || 'git stash list failed' });
431
485
  return;
432
486
  }
433
- const stashes = [];
434
- const lines = result.stdout.split('\n').filter(line => line.trim());
435
- for (const line of lines) {
436
- const refMatch = line.match(/^(stash@\{\d+\})/);
437
- if (!refMatch)
438
- continue;
439
- const ref = refMatch[1];
440
- const restOfLine = line.slice(ref.length + 2);
441
- let branch = '';
442
- let message = restOfLine;
443
- const branchMatch = restOfLine.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);
444
- if (branchMatch) {
445
- branch = branchMatch[1].trim();
446
- message = branchMatch[2].trim();
447
- }
448
- stashes.push({
449
- ref,
450
- message,
451
- branch,
452
- createdAt: null,
453
- });
454
- }
487
+ const stashes = result.stdout
488
+ .split('\n')
489
+ .map(parseGitStashListLine)
490
+ .filter((stash) => stash !== null);
455
491
  res.json({
456
492
  ok: true,
457
493
  stashes,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { createScopedFilePathspecArgs, createScopedGitPathspecArgs, normalizeGitCommitMessage, parseGitStatusStagedPaths, } from './git.js';
2
+ import { createScopedFilePathspecArgs, createScopedGitPathspecArgs, normalizeGitCommitMessage, parseGitStashListLine, parseLatestGitStash, parseGitStatusStagedPaths, } from './git.js';
3
3
  describe('git commit helpers', () => {
4
4
  it('normalizes commit messages before executing git commit', () => {
5
5
  expect(normalizeGitCommitMessage(' chore: update dashboard ')).toBe('chore: update dashboard');
@@ -21,3 +21,23 @@ describe('git commit helpers', () => {
21
21
  expect([...stagedPaths].sort()).toEqual(['added.ts', 'renamed.ts', 'staged.ts']);
22
22
  });
23
23
  });
24
+ describe('git stash reference helpers', () => {
25
+ it('parses stable stash object hashes while preserving the display selector', () => {
26
+ const stash = parseGitStashListLine('0123456789abcdef0123456789abcdef01234567\u0000stash@{3}\u0000On main: 快照: 任务线-2026-06-02 10:00:00\u00002 hours ago');
27
+ expect(stash).toEqual({
28
+ ref: '0123456789abcdef0123456789abcdef01234567',
29
+ stashName: 'stash@{3}',
30
+ message: '快照: 任务线-2026-06-02 10:00:00',
31
+ branch: 'main',
32
+ createdAt: '2 hours ago',
33
+ });
34
+ });
35
+ it('uses the stable stash object hash for the latest no-change snapshot reuse', () => {
36
+ const latest = parseLatestGitStash('fedcba9876543210fedcba9876543210fedcba98\u0000stash@{0}\u0000On master: 快照: latest\u0000just now\n');
37
+ expect(latest).toEqual({
38
+ ref: 'fedcba9876543210fedcba9876543210fedcba98',
39
+ stashName: 'stash@{0}',
40
+ message: '快照: latest',
41
+ });
42
+ });
43
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.7.35",
3
+ "version": "1.7.36",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",