gsd-pi 2.30.0-dev.54ac83b → 2.30.0-dev.92a3417

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.
Files changed (37) hide show
  1. package/dist/cli.js +0 -51
  2. package/dist/help-text.js +0 -35
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
  4. package/dist/resources/extensions/gsd/auto-worktree.ts +8 -12
  5. package/dist/resources/extensions/gsd/guided-flow.ts +0 -3
  6. package/dist/resources/extensions/gsd/index.ts +0 -13
  7. package/dist/resources/extensions/gsd/roadmap-slices.ts +7 -22
  8. package/dist/resources/extensions/gsd/session-lock.ts +4 -53
  9. package/package.json +1 -1
  10. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  11. package/packages/pi-coding-agent/dist/core/agent-session.js +0 -14
  12. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  13. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  14. package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -4
  15. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  16. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  17. package/packages/pi-coding-agent/dist/core/extensions/runner.js +0 -1
  18. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  19. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +0 -7
  20. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  21. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  22. package/packages/pi-coding-agent/src/core/agent-session.ts +0 -14
  23. package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -5
  24. package/packages/pi-coding-agent/src/core/extensions/runner.ts +0 -1
  25. package/packages/pi-coding-agent/src/core/extensions/types.ts +0 -8
  26. package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
  27. package/src/resources/extensions/gsd/auto-worktree.ts +8 -12
  28. package/src/resources/extensions/gsd/guided-flow.ts +0 -3
  29. package/src/resources/extensions/gsd/index.ts +0 -13
  30. package/src/resources/extensions/gsd/roadmap-slices.ts +7 -22
  31. package/src/resources/extensions/gsd/session-lock.ts +4 -53
  32. package/dist/resources/extensions/aws-auth/index.ts +0 -144
  33. package/dist/worktree-cli.d.ts +0 -34
  34. package/dist/worktree-cli.js +0 -294
  35. package/dist/worktree-name-gen.d.ts +0 -7
  36. package/dist/worktree-name-gen.js +0 -44
  37. package/src/resources/extensions/aws-auth/index.ts +0 -144
@@ -1,144 +0,0 @@
1
- /**
2
- * AWS Auth Refresh Extension
3
- *
4
- * Automatically refreshes AWS credentials when Bedrock API requests fail
5
- * with authentication/token errors, then retries the user's message.
6
- *
7
- * ## How it works
8
- *
9
- * Hooks into `agent_end` to check if the last assistant message failed with
10
- * an AWS auth error (expired SSO token, missing credentials, etc.). If so:
11
- *
12
- * 1. Runs the configured `awsAuthRefresh` command (e.g. `aws sso login`)
13
- * 2. Streams the SSO auth URL and verification code to the TUI so users
14
- * can copy/paste if the browser doesn't auto-open
15
- * 3. Calls `retryLastTurn()` which removes the failed assistant response
16
- * and re-runs the agent from the user's original message
17
- *
18
- * ## Activation
19
- *
20
- * This extension is completely inert unless BOTH conditions are met:
21
- * 1. A Bedrock API request fails with a recognized AWS auth error
22
- * 2. `awsAuthRefresh` is configured in settings.json
23
- *
24
- * Non-Bedrock users and Bedrock users without `awsAuthRefresh` configured
25
- * are not affected in any way.
26
- *
27
- * ## Setup
28
- *
29
- * Add to ~/.gsd/agent/settings.json (or project-level .gsd/settings.json):
30
- *
31
- * { "awsAuthRefresh": "aws sso login --profile my-profile" }
32
- *
33
- * ## Matched error patterns
34
- *
35
- * The extension recognizes errors from the AWS SDK, Bedrock, and SSO
36
- * credential providers including:
37
- * - ExpiredTokenException / ExpiredToken
38
- * - The security token included in the request is expired
39
- * - The SSO session associated with this profile has expired or is invalid
40
- * - Unable to locate credentials / Could not load credentials
41
- * - UnrecognizedClientException
42
- * - Error loading SSO Token / Token does not exist
43
- * - SSOTokenProviderFailure
44
- */
45
-
46
- import { exec } from "node:child_process";
47
- import { existsSync, readFileSync } from "node:fs";
48
- import { join } from "node:path";
49
- import { homedir } from "node:os";
50
- import type { ExtensionAPI } from "@gsd/pi-coding-agent";
51
-
52
- /** Matches AWS SDK / Bedrock / SSO credential and token errors. */
53
- const AWS_AUTH_ERROR_RE =
54
- /ExpiredToken|security token.*expired|unable to locate credentials|SSO.*(?:session|token).*(?:expired|not found|invalid)|UnrecognizedClient|Could not load credentials|Invalid identity token|token is expired|credentials.*(?:could not|cannot|failed to).*(?:load|resolve|find)|The.*token.*is.*not.*valid|token has expired|SSOTokenProviderFailure|Error loading SSO Token|Token.*does not exist/i;
55
-
56
- /**
57
- * Reads the `awsAuthRefresh` command from settings.json.
58
- * Checks project-level first, then global (~/.gsd/agent/settings.json).
59
- */
60
- function getAwsAuthRefreshCommand(): string | undefined {
61
- const configDir = process.env.PI_CONFIG_DIR || ".gsd";
62
- const paths = [
63
- join(process.cwd(), configDir, "settings.json"),
64
- join(homedir(), configDir, "agent", "settings.json"),
65
- ];
66
- for (const settingsPath of paths) {
67
- if (!existsSync(settingsPath)) continue;
68
- try {
69
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
70
- if (settings.awsAuthRefresh) return settings.awsAuthRefresh;
71
- } catch {}
72
- }
73
- return undefined;
74
- }
75
-
76
- /**
77
- * Runs the refresh command with a 2-minute timeout (for SSO browser flows).
78
- * Streams stdout/stderr to capture and display the SSO auth URL and
79
- * verification code in real-time via TUI notifications.
80
- */
81
- async function runRefresh(
82
- command: string,
83
- notify: (msg: string, level: "info" | "warning" | "error") => void,
84
- ): Promise<boolean> {
85
- notify("Refreshing AWS credentials...", "info");
86
- try {
87
- await new Promise<void>((resolve, reject) => {
88
- const child = exec(command, { timeout: 120_000, env: { ...process.env } });
89
- const onData = (data: Buffer | string) => {
90
- const text = data.toString();
91
- const urlMatch = text.match(/https?:\/\/\S+/);
92
- if (urlMatch) {
93
- notify(`Open this URL if the browser didn't launch: ${urlMatch[0]}`, "warning");
94
- }
95
- const codeMatch = text.match(/code[:\s]+([A-Z]{4}-[A-Z]{4})/i);
96
- if (codeMatch) {
97
- notify(`Verification code: ${codeMatch[1]}`, "info");
98
- }
99
- };
100
- child.stdout?.on("data", onData);
101
- child.stderr?.on("data", onData);
102
- child.on("close", (code) => {
103
- if (code === 0) resolve();
104
- else reject(new Error(`Refresh command exited with code ${code}`));
105
- });
106
- child.on("error", reject);
107
- });
108
- notify("AWS credentials refreshed successfully ✓", "info");
109
- return true;
110
- } catch (error) {
111
- const msg = error instanceof Error ? error.message : String(error);
112
- const isTimeout = /timed out|ETIMEDOUT|killed/i.test(msg);
113
- if (isTimeout) {
114
- notify("AWS credential refresh timed out. The SSO login may have been cancelled or the browser window was closed.", "error");
115
- } else {
116
- notify(`AWS credential refresh failed: ${msg}`, "error");
117
- }
118
- return false;
119
- }
120
- }
121
-
122
- export default function (pi: ExtensionAPI) {
123
- pi.on("agent_end", async (event, ctx) => {
124
- const refreshCommand = getAwsAuthRefreshCommand();
125
- if (!refreshCommand) return;
126
-
127
- const messages = event.messages;
128
- const lastAssistant = messages[messages.length - 1];
129
- if (
130
- !lastAssistant ||
131
- lastAssistant.role !== "assistant" ||
132
- !("errorMessage" in lastAssistant) ||
133
- !lastAssistant.errorMessage ||
134
- !AWS_AUTH_ERROR_RE.test(lastAssistant.errorMessage)
135
- ) {
136
- return;
137
- }
138
-
139
- const refreshed = await runRefresh(refreshCommand, (m, level) => ctx.ui.notify(m, level));
140
- if (!refreshed) return;
141
-
142
- pi.retryLastTurn();
143
- });
144
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * GSD Worktree CLI — standalone subcommand and -w flag handling.
3
- *
4
- * Manages the full worktree lifecycle from the command line:
5
- * gsd -w Create auto-named worktree, start interactive session
6
- * gsd -w my-feature Create/resume named worktree
7
- * gsd worktree list List worktrees with status
8
- * gsd worktree merge [name] Squash-merge a worktree into main
9
- * gsd worktree clean Remove all merged/empty worktrees
10
- * gsd worktree remove <n> Remove a specific worktree
11
- *
12
- * On session exit (via session_shutdown event), auto-commits dirty work
13
- * so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
14
- * when a session was launched via -w.
15
- */
16
- interface WorktreeStatus {
17
- name: string;
18
- path: string;
19
- branch: string;
20
- exists: boolean;
21
- filesChanged: number;
22
- linesAdded: number;
23
- linesRemoved: number;
24
- uncommitted: boolean;
25
- commits: number;
26
- }
27
- declare function getWorktreeStatus(basePath: string, name: string, wtPath: string): WorktreeStatus;
28
- declare function handleList(basePath: string): void;
29
- declare function handleMerge(basePath: string, args: string[]): Promise<void>;
30
- declare function handleClean(basePath: string): void;
31
- declare function handleRemove(basePath: string, args: string[]): void;
32
- declare function handleStatusBanner(basePath: string): void;
33
- declare function handleWorktreeFlag(worktreeFlag: boolean | string): void;
34
- export { handleList, handleMerge, handleClean, handleRemove, handleStatusBanner, handleWorktreeFlag, getWorktreeStatus, };
@@ -1,294 +0,0 @@
1
- /**
2
- * GSD Worktree CLI — standalone subcommand and -w flag handling.
3
- *
4
- * Manages the full worktree lifecycle from the command line:
5
- * gsd -w Create auto-named worktree, start interactive session
6
- * gsd -w my-feature Create/resume named worktree
7
- * gsd worktree list List worktrees with status
8
- * gsd worktree merge [name] Squash-merge a worktree into main
9
- * gsd worktree clean Remove all merged/empty worktrees
10
- * gsd worktree remove <n> Remove a specific worktree
11
- *
12
- * On session exit (via session_shutdown event), auto-commits dirty work
13
- * so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
14
- * when a session was launched via -w.
15
- */
16
- import chalk from 'chalk';
17
- import { createWorktree, listWorktrees, removeWorktree, mergeWorktreeToMain, diffWorktreeAll, diffWorktreeNumstat, worktreeBranchName, } from './resources/extensions/gsd/worktree-manager.js';
18
- import { runWorktreePostCreateHook } from './resources/extensions/gsd/auto-worktree.js';
19
- import { generateWorktreeName } from './worktree-name-gen.js';
20
- import { nativeHasChanges, nativeDetectMainBranch, nativeCommitCountBetween, } from './resources/extensions/gsd/native-git-bridge.js';
21
- import { inferCommitType } from './resources/extensions/gsd/git-service.js';
22
- import { existsSync } from 'node:fs';
23
- // ─── Status Helpers ─────────────────────────────────────────────────────────
24
- function getWorktreeStatus(basePath, name, wtPath) {
25
- const diff = diffWorktreeAll(basePath, name);
26
- const numstat = diffWorktreeNumstat(basePath, name);
27
- const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
28
- let linesAdded = 0;
29
- let linesRemoved = 0;
30
- for (const s of numstat) {
31
- linesAdded += s.added;
32
- linesRemoved += s.removed;
33
- }
34
- let uncommitted = false;
35
- try {
36
- uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
37
- }
38
- catch { /* */ }
39
- let commits = 0;
40
- try {
41
- const mainBranch = nativeDetectMainBranch(basePath);
42
- commits = nativeCommitCountBetween(basePath, mainBranch, worktreeBranchName(name));
43
- }
44
- catch { /* */ }
45
- return {
46
- name,
47
- path: wtPath,
48
- branch: worktreeBranchName(name),
49
- exists: existsSync(wtPath),
50
- filesChanged,
51
- linesAdded,
52
- linesRemoved,
53
- uncommitted,
54
- commits,
55
- };
56
- }
57
- // ─── Formatters ─────────────────────────────────────────────────────────────
58
- function formatStatus(s) {
59
- const lines = [];
60
- const badge = s.uncommitted
61
- ? chalk.yellow(' (uncommitted)')
62
- : s.filesChanged > 0
63
- ? chalk.cyan(' (unmerged)')
64
- : chalk.green(' (clean)');
65
- lines.push(` ${chalk.bold.cyan(s.name)}${badge}`);
66
- lines.push(` ${chalk.dim('branch')} ${chalk.magenta(s.branch)}`);
67
- lines.push(` ${chalk.dim('path')} ${chalk.dim(s.path)}`);
68
- if (s.filesChanged > 0) {
69
- lines.push(` ${chalk.dim('diff')} ${s.filesChanged} files, ${chalk.green(`+${s.linesAdded}`)} ${chalk.red(`-${s.linesRemoved}`)}, ${s.commits} commit${s.commits === 1 ? '' : 's'}`);
70
- }
71
- return lines.join('\n');
72
- }
73
- // ─── Subcommand: list ───────────────────────────────────────────────────────
74
- function handleList(basePath) {
75
- const worktrees = listWorktrees(basePath);
76
- if (worktrees.length === 0) {
77
- process.stderr.write(chalk.dim('No worktrees. Create one with: gsd -w <name>\n'));
78
- return;
79
- }
80
- process.stderr.write(chalk.bold('\nWorktrees\n\n'));
81
- for (const wt of worktrees) {
82
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
83
- process.stderr.write(formatStatus(status) + '\n\n');
84
- }
85
- }
86
- // ─── Subcommand: merge ──────────────────────────────────────────────────────
87
- async function handleMerge(basePath, args) {
88
- const name = args[0];
89
- if (!name) {
90
- // If only one worktree exists, merge it
91
- const worktrees = listWorktrees(basePath);
92
- if (worktrees.length === 1) {
93
- await doMerge(basePath, worktrees[0].name);
94
- return;
95
- }
96
- process.stderr.write(chalk.red('Usage: gsd worktree merge <name>\n'));
97
- process.stderr.write(chalk.dim('Run gsd worktree list to see worktrees.\n'));
98
- process.exit(1);
99
- }
100
- await doMerge(basePath, name);
101
- }
102
- async function doMerge(basePath, name) {
103
- const worktrees = listWorktrees(basePath);
104
- const wt = worktrees.find(w => w.name === name);
105
- if (!wt) {
106
- process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
107
- process.exit(1);
108
- }
109
- const status = getWorktreeStatus(basePath, name, wt.path);
110
- if (status.filesChanged === 0 && !status.uncommitted) {
111
- process.stderr.write(chalk.dim(`Worktree "${name}" has no changes to merge.\n`));
112
- // Clean up empty worktree
113
- removeWorktree(basePath, name, { deleteBranch: true });
114
- process.stderr.write(chalk.green(`Removed empty worktree ${chalk.bold(name)}.\n`));
115
- return;
116
- }
117
- // Auto-commit dirty work before merge
118
- if (status.uncommitted) {
119
- try {
120
- const { autoCommitCurrentBranch } = await import('./resources/extensions/gsd/worktree.js');
121
- autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
122
- process.stderr.write(chalk.dim(' Auto-committed dirty work before merge.\n'));
123
- }
124
- catch { /* best-effort */ }
125
- }
126
- const commitType = inferCommitType(name);
127
- const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
128
- process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(nativeDetectMainBranch(basePath))}\n`);
129
- process.stderr.write(chalk.dim(` ${status.filesChanged} files, ${chalk.green(`+${status.linesAdded}`)} ${chalk.red(`-${status.linesRemoved}`)}\n\n`));
130
- try {
131
- mergeWorktreeToMain(basePath, name, commitMessage);
132
- removeWorktree(basePath, name, { deleteBranch: true });
133
- process.stderr.write(chalk.green(`✓ Merged and cleaned up ${chalk.bold(name)}\n`));
134
- process.stderr.write(chalk.dim(` commit: ${commitMessage}\n`));
135
- }
136
- catch (err) {
137
- const msg = err instanceof Error ? err.message : String(err);
138
- process.stderr.write(chalk.red(`✗ Merge failed: ${msg}\n`));
139
- process.stderr.write(chalk.dim(' Resolve conflicts manually, then run gsd worktree merge again.\n'));
140
- process.exit(1);
141
- }
142
- }
143
- // ─── Subcommand: clean ──────────────────────────────────────────────────────
144
- function handleClean(basePath) {
145
- const worktrees = listWorktrees(basePath);
146
- if (worktrees.length === 0) {
147
- process.stderr.write(chalk.dim('No worktrees to clean.\n'));
148
- return;
149
- }
150
- let cleaned = 0;
151
- for (const wt of worktrees) {
152
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
153
- if (status.filesChanged === 0 && !status.uncommitted) {
154
- try {
155
- removeWorktree(basePath, wt.name, { deleteBranch: true });
156
- process.stderr.write(chalk.green(` ✓ Removed ${chalk.bold(wt.name)} (clean)\n`));
157
- cleaned++;
158
- }
159
- catch {
160
- process.stderr.write(chalk.yellow(` ✗ Failed to remove ${wt.name}\n`));
161
- }
162
- }
163
- else {
164
- process.stderr.write(chalk.dim(` ─ Kept ${chalk.bold(wt.name)} (${status.filesChanged} changed files)\n`));
165
- }
166
- }
167
- process.stderr.write(chalk.dim(`\nCleaned ${cleaned} worktree${cleaned === 1 ? '' : 's'}.\n`));
168
- }
169
- // ─── Subcommand: remove ─────────────────────────────────────────────────────
170
- function handleRemove(basePath, args) {
171
- const name = args[0];
172
- if (!name) {
173
- process.stderr.write(chalk.red('Usage: gsd worktree remove <name>\n'));
174
- process.exit(1);
175
- }
176
- const worktrees = listWorktrees(basePath);
177
- const wt = worktrees.find(w => w.name === name);
178
- if (!wt) {
179
- process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
180
- process.exit(1);
181
- }
182
- const status = getWorktreeStatus(basePath, name, wt.path);
183
- if (status.filesChanged > 0 || status.uncommitted) {
184
- process.stderr.write(chalk.yellow(`⚠ Worktree "${name}" has unmerged changes (${status.filesChanged} files).\n`));
185
- process.stderr.write(chalk.yellow(' Use --force to remove anyway, or merge first: gsd worktree merge ' + name + '\n'));
186
- if (!process.argv.includes('--force')) {
187
- process.exit(1);
188
- }
189
- }
190
- removeWorktree(basePath, name, { deleteBranch: true });
191
- process.stderr.write(chalk.green(`✓ Removed worktree ${chalk.bold(name)}\n`));
192
- }
193
- // ─── Subcommand: status (default when no args) ─────────────────────────────
194
- function handleStatusBanner(basePath) {
195
- const worktrees = listWorktrees(basePath);
196
- if (worktrees.length === 0)
197
- return;
198
- const withChanges = worktrees.filter(wt => {
199
- try {
200
- const diff = diffWorktreeAll(basePath, wt.name);
201
- return diff.added.length + diff.modified.length + diff.removed.length > 0;
202
- }
203
- catch {
204
- return false;
205
- }
206
- });
207
- if (withChanges.length === 0)
208
- return;
209
- const names = withChanges.map(w => chalk.cyan(w.name)).join(', ');
210
- process.stderr.write(chalk.dim('[gsd] ') +
211
- chalk.yellow(`${withChanges.length} worktree${withChanges.length === 1 ? '' : 's'} with unmerged changes: `) +
212
- names + '\n' +
213
- chalk.dim('[gsd] ') +
214
- chalk.dim('Resume: gsd -w <name> | Merge: gsd worktree merge <name> | List: gsd worktree list\n\n'));
215
- }
216
- // ─── -w flag: create/resume worktree for interactive session ────────────────
217
- function handleWorktreeFlag(worktreeFlag) {
218
- const basePath = process.cwd();
219
- // gsd -w (no name) — resume most recent worktree with changes, or create new
220
- if (worktreeFlag === true) {
221
- const existing = listWorktrees(basePath);
222
- const withChanges = existing.filter(wt => {
223
- try {
224
- const diff = diffWorktreeAll(basePath, wt.name);
225
- return diff.added.length + diff.modified.length + diff.removed.length > 0;
226
- }
227
- catch {
228
- return false;
229
- }
230
- });
231
- if (withChanges.length === 1) {
232
- // Single active worktree — resume it
233
- const wt = withChanges[0];
234
- process.chdir(wt.path);
235
- process.env.GSD_CLI_WORKTREE = wt.name;
236
- process.env.GSD_CLI_WORKTREE_BASE = basePath;
237
- process.stderr.write(chalk.green(`✓ Resumed worktree ${chalk.bold(wt.name)}\n`));
238
- process.stderr.write(chalk.dim(` path ${wt.path}\n`));
239
- process.stderr.write(chalk.dim(` branch ${wt.branch}\n\n`));
240
- return;
241
- }
242
- if (withChanges.length > 1) {
243
- // Multiple active worktrees — show them and ask user to pick
244
- process.stderr.write(chalk.yellow(`${withChanges.length} worktrees have unmerged changes:\n\n`));
245
- for (const wt of withChanges) {
246
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
247
- process.stderr.write(formatStatus(status) + '\n\n');
248
- }
249
- process.stderr.write(chalk.dim('Specify which one: gsd -w <name>\n'));
250
- process.exit(0);
251
- }
252
- // No active worktrees — create a new one
253
- const name = generateWorktreeName();
254
- createAndEnter(basePath, name);
255
- return;
256
- }
257
- // gsd -w <name> — create or resume named worktree
258
- const name = worktreeFlag;
259
- const existing = listWorktrees(basePath);
260
- const found = existing.find(wt => wt.name === name);
261
- if (found) {
262
- process.chdir(found.path);
263
- process.env.GSD_CLI_WORKTREE = name;
264
- process.env.GSD_CLI_WORKTREE_BASE = basePath;
265
- process.stderr.write(chalk.green(`✓ Resumed worktree ${chalk.bold(name)}\n`));
266
- process.stderr.write(chalk.dim(` path ${found.path}\n`));
267
- process.stderr.write(chalk.dim(` branch ${found.branch}\n\n`));
268
- }
269
- else {
270
- createAndEnter(basePath, name);
271
- }
272
- }
273
- function createAndEnter(basePath, name) {
274
- try {
275
- const info = createWorktree(basePath, name);
276
- const hookError = runWorktreePostCreateHook(basePath, info.path);
277
- if (hookError) {
278
- process.stderr.write(chalk.yellow(`[gsd] ${hookError}\n`));
279
- }
280
- process.chdir(info.path);
281
- process.env.GSD_CLI_WORKTREE = name;
282
- process.env.GSD_CLI_WORKTREE_BASE = basePath;
283
- process.stderr.write(chalk.green(`✓ Created worktree ${chalk.bold(name)}\n`));
284
- process.stderr.write(chalk.dim(` path ${info.path}\n`));
285
- process.stderr.write(chalk.dim(` branch ${info.branch}\n\n`));
286
- }
287
- catch (err) {
288
- const msg = err instanceof Error ? err.message : String(err);
289
- process.stderr.write(chalk.red(`[gsd] Failed to create worktree: ${msg}\n`));
290
- process.exit(1);
291
- }
292
- }
293
- // ─── Exports ────────────────────────────────────────────────────────────────
294
- export { handleList, handleMerge, handleClean, handleRemove, handleStatusBanner, handleWorktreeFlag, getWorktreeStatus, };
@@ -1,7 +0,0 @@
1
- /**
2
- * Random worktree name generator.
3
- *
4
- * Produces names in the pattern: adjective-verbing-noun
5
- * e.g. "noble-roaming-karp", "swift-whistling-matsumoto"
6
- */
7
- export declare function generateWorktreeName(): string;
@@ -1,44 +0,0 @@
1
- /**
2
- * Random worktree name generator.
3
- *
4
- * Produces names in the pattern: adjective-verbing-noun
5
- * e.g. "noble-roaming-karp", "swift-whistling-matsumoto"
6
- */
7
- const ADJECTIVES = [
8
- 'agile', 'bold', 'brave', 'bright', 'calm', 'clear', 'cool', 'crisp',
9
- 'dapper', 'eager', 'fair', 'fast', 'fierce', 'fine', 'fleet', 'fond',
10
- 'gentle', 'glad', 'grand', 'happy', 'keen', 'kind', 'lively', 'lucid',
11
- 'mellow', 'merry', 'mighty', 'neat', 'nimble', 'noble', 'plucky', 'polite',
12
- 'proud', 'quiet', 'rapid', 'ready', 'serene', 'sharp', 'sleek', 'sleepy',
13
- 'smooth', 'snappy', 'steady', 'sturdy', 'sunny', 'sure', 'swift', 'tidy',
14
- 'tough', 'tranquil', 'vivid', 'warm', 'wise', 'witty', 'zesty',
15
- ];
16
- const VERBS = [
17
- 'baking', 'bouncing', 'building', 'carving', 'chasing', 'climbing',
18
- 'coding', 'crafting', 'dancing', 'dashing', 'diving', 'drawing',
19
- 'dreaming', 'drifting', 'drumming', 'exploring', 'fishing', 'floating',
20
- 'flying', 'forging', 'gliding', 'growing', 'hiking', 'humming',
21
- 'jumping', 'juggling', 'knitting', 'laughing', 'leaping', 'mapping',
22
- 'mixing', 'painting', 'planting', 'playing', 'racing', 'reading',
23
- 'riding', 'roaming', 'rowing', 'running', 'sailing', 'singing',
24
- 'skating', 'sketching', 'spinning', 'squishing', 'surfing', 'swimming',
25
- 'thinking', 'threading', 'tracing', 'walking', 'weaving', 'whistling',
26
- 'writing',
27
- ];
28
- const NOUNS = [
29
- 'atlas', 'aurora', 'balloon', 'beacon', 'bolt', 'brook', 'canyon',
30
- 'cedar', 'comet', 'cook', 'coral', 'cosmos', 'crest', 'dawn', 'delta',
31
- 'echo', 'ember', 'falcon', 'fern', 'flare', 'frost', 'gale', 'glacier',
32
- 'grove', 'harbor', 'hawk', 'horizon', 'iris', 'jade', 'karp', 'lantern',
33
- 'lark', 'luna', 'maple', 'marsh', 'matsumoto', 'mesa', 'nebula', 'oasis',
34
- 'orbit', 'otter', 'pebble', 'phoenix', 'pine', 'prism', 'puppy', 'quartz',
35
- 'raven', 'reef', 'ridge', 'river', 'sage', 'shore', 'sierra', 'spark',
36
- 'sprout', 'stone', 'summit', 'thorn', 'tide', 'topaz', 'trail', 'vale',
37
- 'violet', 'wave', 'willow', 'zenith',
38
- ];
39
- function pick(arr) {
40
- return arr[Math.floor(Math.random() * arr.length)];
41
- }
42
- export function generateWorktreeName() {
43
- return `${pick(ADJECTIVES)}-${pick(VERBS)}-${pick(NOUNS)}`;
44
- }
@@ -1,144 +0,0 @@
1
- /**
2
- * AWS Auth Refresh Extension
3
- *
4
- * Automatically refreshes AWS credentials when Bedrock API requests fail
5
- * with authentication/token errors, then retries the user's message.
6
- *
7
- * ## How it works
8
- *
9
- * Hooks into `agent_end` to check if the last assistant message failed with
10
- * an AWS auth error (expired SSO token, missing credentials, etc.). If so:
11
- *
12
- * 1. Runs the configured `awsAuthRefresh` command (e.g. `aws sso login`)
13
- * 2. Streams the SSO auth URL and verification code to the TUI so users
14
- * can copy/paste if the browser doesn't auto-open
15
- * 3. Calls `retryLastTurn()` which removes the failed assistant response
16
- * and re-runs the agent from the user's original message
17
- *
18
- * ## Activation
19
- *
20
- * This extension is completely inert unless BOTH conditions are met:
21
- * 1. A Bedrock API request fails with a recognized AWS auth error
22
- * 2. `awsAuthRefresh` is configured in settings.json
23
- *
24
- * Non-Bedrock users and Bedrock users without `awsAuthRefresh` configured
25
- * are not affected in any way.
26
- *
27
- * ## Setup
28
- *
29
- * Add to ~/.gsd/agent/settings.json (or project-level .gsd/settings.json):
30
- *
31
- * { "awsAuthRefresh": "aws sso login --profile my-profile" }
32
- *
33
- * ## Matched error patterns
34
- *
35
- * The extension recognizes errors from the AWS SDK, Bedrock, and SSO
36
- * credential providers including:
37
- * - ExpiredTokenException / ExpiredToken
38
- * - The security token included in the request is expired
39
- * - The SSO session associated with this profile has expired or is invalid
40
- * - Unable to locate credentials / Could not load credentials
41
- * - UnrecognizedClientException
42
- * - Error loading SSO Token / Token does not exist
43
- * - SSOTokenProviderFailure
44
- */
45
-
46
- import { exec } from "node:child_process";
47
- import { existsSync, readFileSync } from "node:fs";
48
- import { join } from "node:path";
49
- import { homedir } from "node:os";
50
- import type { ExtensionAPI } from "@gsd/pi-coding-agent";
51
-
52
- /** Matches AWS SDK / Bedrock / SSO credential and token errors. */
53
- const AWS_AUTH_ERROR_RE =
54
- /ExpiredToken|security token.*expired|unable to locate credentials|SSO.*(?:session|token).*(?:expired|not found|invalid)|UnrecognizedClient|Could not load credentials|Invalid identity token|token is expired|credentials.*(?:could not|cannot|failed to).*(?:load|resolve|find)|The.*token.*is.*not.*valid|token has expired|SSOTokenProviderFailure|Error loading SSO Token|Token.*does not exist/i;
55
-
56
- /**
57
- * Reads the `awsAuthRefresh` command from settings.json.
58
- * Checks project-level first, then global (~/.gsd/agent/settings.json).
59
- */
60
- function getAwsAuthRefreshCommand(): string | undefined {
61
- const configDir = process.env.PI_CONFIG_DIR || ".gsd";
62
- const paths = [
63
- join(process.cwd(), configDir, "settings.json"),
64
- join(homedir(), configDir, "agent", "settings.json"),
65
- ];
66
- for (const settingsPath of paths) {
67
- if (!existsSync(settingsPath)) continue;
68
- try {
69
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
70
- if (settings.awsAuthRefresh) return settings.awsAuthRefresh;
71
- } catch {}
72
- }
73
- return undefined;
74
- }
75
-
76
- /**
77
- * Runs the refresh command with a 2-minute timeout (for SSO browser flows).
78
- * Streams stdout/stderr to capture and display the SSO auth URL and
79
- * verification code in real-time via TUI notifications.
80
- */
81
- async function runRefresh(
82
- command: string,
83
- notify: (msg: string, level: "info" | "warning" | "error") => void,
84
- ): Promise<boolean> {
85
- notify("Refreshing AWS credentials...", "info");
86
- try {
87
- await new Promise<void>((resolve, reject) => {
88
- const child = exec(command, { timeout: 120_000, env: { ...process.env } });
89
- const onData = (data: Buffer | string) => {
90
- const text = data.toString();
91
- const urlMatch = text.match(/https?:\/\/\S+/);
92
- if (urlMatch) {
93
- notify(`Open this URL if the browser didn't launch: ${urlMatch[0]}`, "warning");
94
- }
95
- const codeMatch = text.match(/code[:\s]+([A-Z]{4}-[A-Z]{4})/i);
96
- if (codeMatch) {
97
- notify(`Verification code: ${codeMatch[1]}`, "info");
98
- }
99
- };
100
- child.stdout?.on("data", onData);
101
- child.stderr?.on("data", onData);
102
- child.on("close", (code) => {
103
- if (code === 0) resolve();
104
- else reject(new Error(`Refresh command exited with code ${code}`));
105
- });
106
- child.on("error", reject);
107
- });
108
- notify("AWS credentials refreshed successfully ✓", "info");
109
- return true;
110
- } catch (error) {
111
- const msg = error instanceof Error ? error.message : String(error);
112
- const isTimeout = /timed out|ETIMEDOUT|killed/i.test(msg);
113
- if (isTimeout) {
114
- notify("AWS credential refresh timed out. The SSO login may have been cancelled or the browser window was closed.", "error");
115
- } else {
116
- notify(`AWS credential refresh failed: ${msg}`, "error");
117
- }
118
- return false;
119
- }
120
- }
121
-
122
- export default function (pi: ExtensionAPI) {
123
- pi.on("agent_end", async (event, ctx) => {
124
- const refreshCommand = getAwsAuthRefreshCommand();
125
- if (!refreshCommand) return;
126
-
127
- const messages = event.messages;
128
- const lastAssistant = messages[messages.length - 1];
129
- if (
130
- !lastAssistant ||
131
- lastAssistant.role !== "assistant" ||
132
- !("errorMessage" in lastAssistant) ||
133
- !lastAssistant.errorMessage ||
134
- !AWS_AUTH_ERROR_RE.test(lastAssistant.errorMessage)
135
- ) {
136
- return;
137
- }
138
-
139
- const refreshed = await runRefresh(refreshCommand, (m, level) => ctx.ui.notify(m, level));
140
- if (!refreshed) return;
141
-
142
- pi.retryLastTurn();
143
- });
144
- }