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.
- package/dist/cli.js +0 -51
- package/dist/help-text.js +0 -35
- package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/dist/resources/extensions/gsd/auto-worktree.ts +8 -12
- package/dist/resources/extensions/gsd/guided-flow.ts +0 -3
- package/dist/resources/extensions/gsd/index.ts +0 -13
- package/dist/resources/extensions/gsd/roadmap-slices.ts +7 -22
- package/dist/resources/extensions/gsd/session-lock.ts +4 -53
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +0 -14
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +0 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +0 -7
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +0 -14
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -5
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +0 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +0 -8
- package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/src/resources/extensions/gsd/auto-worktree.ts +8 -12
- package/src/resources/extensions/gsd/guided-flow.ts +0 -3
- package/src/resources/extensions/gsd/index.ts +0 -13
- package/src/resources/extensions/gsd/roadmap-slices.ts +7 -22
- package/src/resources/extensions/gsd/session-lock.ts +4 -53
- package/dist/resources/extensions/aws-auth/index.ts +0 -144
- package/dist/worktree-cli.d.ts +0 -34
- package/dist/worktree-cli.js +0 -294
- package/dist/worktree-name-gen.d.ts +0 -7
- package/dist/worktree-name-gen.js +0 -44
- 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
|
-
}
|
package/dist/worktree-cli.d.ts
DELETED
|
@@ -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, };
|
package/dist/worktree-cli.js
DELETED
|
@@ -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,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
|
-
}
|