gsd-pi 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/cli.js +159 -10
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.js +17 -6
- package/package.json +4 -2
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +48 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/scripts/postinstall.js +8 -0
- package/src/resources/extensions/bg-shell/index.ts +57 -8
- package/src/resources/extensions/browser-tools/index.ts +4 -1
- package/src/resources/extensions/github/gh-api.ts +46 -30
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +6 -1
- package/src/resources/extensions/gsd/files.ts +7 -7
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +58 -1
- package/src/resources/extensions/gsd/migrate/command.ts +215 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +53 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +3 -1
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +89 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/worktree-command.ts +527 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +302 -0
- package/src/resources/extensions/slash-commands/gsd-run.ts +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,28 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
|
|
|
46
46
|
| Roadmap reassessment | Manual | Automatic after each slice completes |
|
|
47
47
|
| Skill discovery | None | Auto-detect and install relevant skills during research |
|
|
48
48
|
|
|
49
|
+
### Migrating from v1
|
|
50
|
+
|
|
51
|
+
If you have projects with `.planning` directories from the original Get Shit Done, you can migrate them to GSD-2's `.gsd` format:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# From within the project directory
|
|
55
|
+
/gsd migrate
|
|
56
|
+
|
|
57
|
+
# Or specify a path
|
|
58
|
+
/gsd migrate ~/projects/my-old-project
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The migration tool:
|
|
62
|
+
- Parses your old `PROJECT.md`, `ROADMAP.md`, `REQUIREMENTS.md`, phase directories, plans, summaries, and research
|
|
63
|
+
- Maps phases → slices, plans → tasks, milestones → milestones
|
|
64
|
+
- Preserves completion state (`[x]` phases stay done, summaries carry over)
|
|
65
|
+
- Consolidates research files into the new structure
|
|
66
|
+
- Shows a preview before writing anything
|
|
67
|
+
- Optionally runs an agent-driven review of the output for quality assurance
|
|
68
|
+
|
|
69
|
+
Supports format variations including milestone-sectioned roadmaps with `<details>` blocks, bold phase entries, bullet-format requirements, decimal phase numbering, and duplicate phase numbers across milestones.
|
|
70
|
+
|
|
49
71
|
---
|
|
50
72
|
|
|
51
73
|
## How It Works
|
|
@@ -187,6 +209,7 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f
|
|
|
187
209
|
| `/gsd status` | Progress dashboard |
|
|
188
210
|
| `/gsd queue` | Queue future milestones (safe during auto mode) |
|
|
189
211
|
| `/gsd prefs` | Model selection, timeouts, budget ceiling |
|
|
212
|
+
| `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
|
|
190
213
|
| `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues |
|
|
191
214
|
| `Ctrl+Alt+G` | Toggle dashboard overlay |
|
|
192
215
|
|
|
@@ -389,7 +412,9 @@ Use expensive models where quality matters (planning, complex execution) and che
|
|
|
389
412
|
|
|
390
413
|
## Star History
|
|
391
414
|
|
|
392
|
-
|
|
415
|
+
<a href="https://star-history.com/#gsd-build/gsd-2&Date">
|
|
416
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=gsd-build/gsd-2&type=Date" />
|
|
417
|
+
</a>
|
|
393
418
|
|
|
394
419
|
---
|
|
395
420
|
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,71 @@
|
|
|
1
|
-
import { AuthStorage, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, } from '@mariozechner/pi-coding-agent';
|
|
1
|
+
import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
|
|
3
|
-
import {
|
|
5
|
+
import { initResources } from './resource-loader.js';
|
|
6
|
+
import { ensureManagedTools } from './tool-bootstrap.js';
|
|
4
7
|
import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js';
|
|
8
|
+
function parseCliArgs(argv) {
|
|
9
|
+
const flags = { extensions: [], messages: [] };
|
|
10
|
+
const args = argv.slice(2); // skip node + script
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === '--mode' && i + 1 < args.length) {
|
|
14
|
+
const m = args[++i];
|
|
15
|
+
if (m === 'text' || m === 'json' || m === 'rpc')
|
|
16
|
+
flags.mode = m;
|
|
17
|
+
}
|
|
18
|
+
else if (arg === '--print' || arg === '-p') {
|
|
19
|
+
flags.print = true;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === '--no-session') {
|
|
22
|
+
flags.noSession = true;
|
|
23
|
+
}
|
|
24
|
+
else if (arg === '--model' && i + 1 < args.length) {
|
|
25
|
+
flags.model = args[++i];
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '--extension' && i + 1 < args.length) {
|
|
28
|
+
flags.extensions.push(args[++i]);
|
|
29
|
+
}
|
|
30
|
+
else if (arg === '--append-system-prompt' && i + 1 < args.length) {
|
|
31
|
+
flags.appendSystemPrompt = args[++i];
|
|
32
|
+
}
|
|
33
|
+
else if (arg === '--tools' && i + 1 < args.length) {
|
|
34
|
+
flags.tools = args[++i].split(',');
|
|
35
|
+
}
|
|
36
|
+
else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
|
37
|
+
flags.messages.push(arg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return flags;
|
|
41
|
+
}
|
|
42
|
+
const cliFlags = parseCliArgs(process.argv);
|
|
43
|
+
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
|
|
44
|
+
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
|
|
45
|
+
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
|
|
46
|
+
// Provision local managed binaries first so Pi sees them without probing PATH.
|
|
47
|
+
ensureManagedTools(join(agentDir, 'bin'));
|
|
5
48
|
const authStorage = AuthStorage.create(authFilePath);
|
|
6
49
|
loadStoredEnvKeys(authStorage);
|
|
7
|
-
|
|
50
|
+
// Skip the setup wizard in print mode — it requires TTY interaction
|
|
51
|
+
if (!isPrintMode) {
|
|
52
|
+
await runWizardIfNeeded(authStorage);
|
|
53
|
+
}
|
|
8
54
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
9
55
|
const settingsManager = SettingsManager.create(agentDir);
|
|
10
|
-
//
|
|
11
|
-
// Validates on every startup — catches stale settings from prior installs
|
|
56
|
+
// Validate configured model on startup — catches stale settings from prior installs
|
|
12
57
|
// (e.g. grok-2 which no longer exists) and fresh installs with no settings.
|
|
58
|
+
// Only resets the default when the configured model no longer exists in the registry;
|
|
59
|
+
// never overwrites a valid user choice.
|
|
13
60
|
const configuredProvider = settingsManager.getDefaultProvider();
|
|
14
61
|
const configuredModel = settingsManager.getDefaultModel();
|
|
15
62
|
const allModels = modelRegistry.getAll();
|
|
16
63
|
const configuredExists = configuredProvider && configuredModel &&
|
|
17
64
|
allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
|
|
18
65
|
if (!configuredModel || !configuredExists) {
|
|
19
|
-
//
|
|
20
|
-
const preferred = allModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-
|
|
21
|
-
allModels.find((m) => m.provider === 'anthropic' && m.id.includes('
|
|
66
|
+
// Fallback: pick the best available Anthropic model
|
|
67
|
+
const preferred = allModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
|
|
68
|
+
allModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
|
|
22
69
|
allModels.find((m) => m.provider === 'anthropic');
|
|
23
70
|
if (preferred) {
|
|
24
71
|
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id);
|
|
@@ -36,9 +83,70 @@ if (!settingsManager.getQuietStartup()) {
|
|
|
36
83
|
if (!settingsManager.getCollapseChangelog()) {
|
|
37
84
|
settingsManager.setCollapseChangelog(true);
|
|
38
85
|
}
|
|
39
|
-
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Print / subagent mode — single-shot execution, no TTY required
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
if (isPrintMode) {
|
|
90
|
+
const sessionManager = cliFlags.noSession
|
|
91
|
+
? SessionManager.inMemory()
|
|
92
|
+
: SessionManager.create(process.cwd());
|
|
93
|
+
// Read --append-system-prompt file content (subagent writes agent system prompts to temp files)
|
|
94
|
+
let appendSystemPrompt;
|
|
95
|
+
if (cliFlags.appendSystemPrompt) {
|
|
96
|
+
try {
|
|
97
|
+
appendSystemPrompt = readFileSync(cliFlags.appendSystemPrompt, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// If it's not a file path, treat it as literal text
|
|
101
|
+
appendSystemPrompt = cliFlags.appendSystemPrompt;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
initResources(agentDir);
|
|
105
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
106
|
+
agentDir,
|
|
107
|
+
additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined,
|
|
108
|
+
appendSystemPrompt,
|
|
109
|
+
});
|
|
110
|
+
await resourceLoader.reload();
|
|
111
|
+
const { session, extensionsResult } = await createAgentSession({
|
|
112
|
+
authStorage,
|
|
113
|
+
modelRegistry,
|
|
114
|
+
settingsManager,
|
|
115
|
+
sessionManager,
|
|
116
|
+
resourceLoader,
|
|
117
|
+
});
|
|
118
|
+
if (extensionsResult.errors.length > 0) {
|
|
119
|
+
for (const err of extensionsResult.errors) {
|
|
120
|
+
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Apply --model override if specified
|
|
124
|
+
if (cliFlags.model) {
|
|
125
|
+
const available = modelRegistry.getAvailable();
|
|
126
|
+
const match = available.find((m) => m.id === cliFlags.model) ||
|
|
127
|
+
available.find((m) => `${m.provider}/${m.id}` === cliFlags.model);
|
|
128
|
+
if (match) {
|
|
129
|
+
session.setModel(match);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const mode = cliFlags.mode || 'text';
|
|
133
|
+
await runPrintMode(session, {
|
|
134
|
+
mode: mode === 'rpc' ? 'json' : mode,
|
|
135
|
+
messages: cliFlags.messages,
|
|
136
|
+
});
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Interactive mode — normal TTY session
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Per-directory session storage — same encoding as the upstream SDK so that
|
|
143
|
+
// /resume only shows sessions from the current working directory.
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
146
|
+
const projectSessionsDir = join(sessionsDir, safePath);
|
|
147
|
+
const sessionManager = SessionManager.create(cwd, projectSessionsDir);
|
|
40
148
|
initResources(agentDir);
|
|
41
|
-
const resourceLoader =
|
|
149
|
+
const resourceLoader = new DefaultResourceLoader({ agentDir });
|
|
42
150
|
await resourceLoader.reload();
|
|
43
151
|
const { session, extensionsResult } = await createAgentSession({
|
|
44
152
|
authStorage,
|
|
@@ -52,5 +160,46 @@ if (extensionsResult.errors.length > 0) {
|
|
|
52
160
|
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
53
161
|
}
|
|
54
162
|
}
|
|
163
|
+
// Restore scoped models from settings on startup.
|
|
164
|
+
// The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
|
|
165
|
+
// but doesn't apply them to the session at startup — so Ctrl+P cycles all models instead of
|
|
166
|
+
// just the saved selection until the user re-runs /scoped-models.
|
|
167
|
+
const enabledModelPatterns = settingsManager.getEnabledModels();
|
|
168
|
+
if (enabledModelPatterns && enabledModelPatterns.length > 0) {
|
|
169
|
+
const availableModels = modelRegistry.getAvailable();
|
|
170
|
+
const scopedModels = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const pattern of enabledModelPatterns) {
|
|
173
|
+
// Patterns are "provider/modelId" exact strings saved by /scoped-models
|
|
174
|
+
const slashIdx = pattern.indexOf('/');
|
|
175
|
+
if (slashIdx !== -1) {
|
|
176
|
+
const provider = pattern.substring(0, slashIdx);
|
|
177
|
+
const modelId = pattern.substring(slashIdx + 1);
|
|
178
|
+
const model = availableModels.find((m) => m.provider === provider && m.id === modelId);
|
|
179
|
+
if (model) {
|
|
180
|
+
const key = `${model.provider}/${model.id}`;
|
|
181
|
+
if (!seen.has(key)) {
|
|
182
|
+
seen.add(key);
|
|
183
|
+
scopedModels.push({ model });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Fallback: match by model id alone
|
|
189
|
+
const model = availableModels.find((m) => m.id === pattern);
|
|
190
|
+
if (model) {
|
|
191
|
+
const key = `${model.provider}/${model.id}`;
|
|
192
|
+
if (!seen.has(key)) {
|
|
193
|
+
seen.add(key);
|
|
194
|
+
scopedModels.push({ model });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Only apply if we resolved some models and it's a genuine subset
|
|
200
|
+
if (scopedModels.length > 0 && scopedModels.length < availableModels.length) {
|
|
201
|
+
session.setScopedModels(scopedModels);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
55
204
|
const interactiveMode = new InteractiveMode(session);
|
|
56
205
|
await interactiveMode.run();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
const TOOL_SPECS = {
|
|
4
|
+
fd: {
|
|
5
|
+
targetName: process.platform === "win32" ? "fd.exe" : "fd",
|
|
6
|
+
candidates: process.platform === "win32" ? ["fd.exe", "fd", "fdfind.exe", "fdfind"] : ["fd", "fdfind"],
|
|
7
|
+
},
|
|
8
|
+
rg: {
|
|
9
|
+
targetName: process.platform === "win32" ? "rg.exe" : "rg",
|
|
10
|
+
candidates: process.platform === "win32" ? ["rg.exe", "rg"] : ["rg"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
function splitPath(pathValue) {
|
|
14
|
+
if (!pathValue)
|
|
15
|
+
return [];
|
|
16
|
+
return pathValue.split(delimiter).map((segment) => segment.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function getCandidateNames(name) {
|
|
19
|
+
if (process.platform !== "win32")
|
|
20
|
+
return [name];
|
|
21
|
+
const lower = name.toLowerCase();
|
|
22
|
+
if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat"))
|
|
23
|
+
return [name];
|
|
24
|
+
return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`];
|
|
25
|
+
}
|
|
26
|
+
function isRegularFile(path) {
|
|
27
|
+
try {
|
|
28
|
+
return lstatSync(path).isFile() || lstatSync(path).isSymbolicLink();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function resolveToolFromPath(tool, pathValue = process.env.PATH) {
|
|
35
|
+
const spec = TOOL_SPECS[tool];
|
|
36
|
+
for (const dir of splitPath(pathValue)) {
|
|
37
|
+
for (const candidate of spec.candidates) {
|
|
38
|
+
for (const name of getCandidateNames(candidate)) {
|
|
39
|
+
const fullPath = join(dir, name);
|
|
40
|
+
if (existsSync(fullPath) && isRegularFile(fullPath)) {
|
|
41
|
+
return fullPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function provisionTool(targetDir, tool, sourcePath) {
|
|
49
|
+
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
50
|
+
if (existsSync(targetPath))
|
|
51
|
+
return targetPath;
|
|
52
|
+
mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
symlinkSync(sourcePath, targetPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
rmSync(targetPath, { force: true });
|
|
58
|
+
copyFileSync(sourcePath, targetPath);
|
|
59
|
+
chmodSync(targetPath, 0o755);
|
|
60
|
+
}
|
|
61
|
+
return targetPath;
|
|
62
|
+
}
|
|
63
|
+
export function ensureManagedTools(targetDir, pathValue = process.env.PATH) {
|
|
64
|
+
const provisioned = [];
|
|
65
|
+
for (const tool of Object.keys(TOOL_SPECS)) {
|
|
66
|
+
if (existsSync(join(targetDir, TOOL_SPECS[tool].targetName)))
|
|
67
|
+
continue;
|
|
68
|
+
const sourcePath = resolveToolFromPath(tool, pathValue);
|
|
69
|
+
if (!sourcePath)
|
|
70
|
+
continue;
|
|
71
|
+
provisioned.push(provisionTool(targetDir, tool, sourcePath));
|
|
72
|
+
}
|
|
73
|
+
return provisioned;
|
|
74
|
+
}
|
package/dist/wizard.js
CHANGED
|
@@ -21,6 +21,18 @@ async function promptMasked(label, hint) {
|
|
|
21
21
|
process.stdin.resume();
|
|
22
22
|
process.stdin.setEncoding('utf8');
|
|
23
23
|
let value = '';
|
|
24
|
+
const redraw = () => {
|
|
25
|
+
process.stdout.clearLine(0);
|
|
26
|
+
process.stdout.cursorTo(0);
|
|
27
|
+
if (value.length === 0) {
|
|
28
|
+
process.stdout.write(' ');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const dots = '●'.repeat(Math.min(value.length, 24));
|
|
32
|
+
const counter = value.length > 24 ? ` ${dim}(${value.length})${reset}` : ` ${dim}${value.length}${reset}`;
|
|
33
|
+
process.stdout.write(` ${dots}${counter}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
24
36
|
const handler = (ch) => {
|
|
25
37
|
if (ch === '\r' || ch === '\n') {
|
|
26
38
|
process.stdin.setRawMode(false);
|
|
@@ -34,17 +46,15 @@ async function promptMasked(label, hint) {
|
|
|
34
46
|
process.stdout.write('\n');
|
|
35
47
|
process.exit(0);
|
|
36
48
|
}
|
|
37
|
-
else if (ch === '\u007f') {
|
|
49
|
+
else if (ch === '\u007f' || ch === '\b') {
|
|
38
50
|
if (value.length > 0) {
|
|
39
51
|
value = value.slice(0, -1);
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
process.stdout.cursorTo(0);
|
|
43
|
-
process.stdout.write(' ' + '*'.repeat(value.length));
|
|
53
|
+
redraw();
|
|
44
54
|
}
|
|
45
55
|
else {
|
|
46
56
|
value += ch;
|
|
47
|
-
|
|
57
|
+
redraw();
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
process.stdin.on('data', handler);
|
|
@@ -75,7 +85,7 @@ export function loadStoredEnvKeys(authStorage) {
|
|
|
75
85
|
for (const [provider, envVar] of providers) {
|
|
76
86
|
if (!process.env[envVar]) {
|
|
77
87
|
const cred = authStorage.get(provider);
|
|
78
|
-
if (cred?.type === 'api_key') {
|
|
88
|
+
if (cred?.type === 'api_key' && cred.key) {
|
|
79
89
|
process.env[envVar] = cred.key;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
@@ -143,6 +153,7 @@ export async function runWizardIfNeeded(authStorage) {
|
|
|
143
153
|
savedCount++;
|
|
144
154
|
}
|
|
145
155
|
else {
|
|
156
|
+
authStorage.set(key.provider, { type: 'api_key', key: '' });
|
|
146
157
|
process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`);
|
|
147
158
|
}
|
|
148
159
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "GSD — Get
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"patches",
|
|
21
22
|
"pkg",
|
|
22
23
|
"src/resources",
|
|
23
24
|
"scripts/postinstall.js",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/node": "^22.0.0",
|
|
50
|
+
"patch-package": "^8.0.1",
|
|
49
51
|
"typescript": "^5.4.0"
|
|
50
52
|
},
|
|
51
53
|
"overrides": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
2
|
+
index 27fe820..68f277f 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
5
|
+
@@ -1,11 +1,35 @@
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
8
|
+
+import { createRequire } from "node:module";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
|
|
14
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
|
|
15
|
+
+// Cached Win32 FFI handles for restoring VT input after child processes
|
|
16
|
+
+let _vtHandles = null;
|
|
17
|
+
+function restoreWindowsVTInput() {
|
|
18
|
+
+ if (process.platform !== "win32") return;
|
|
19
|
+
+ try {
|
|
20
|
+
+ if (!_vtHandles) {
|
|
21
|
+
+ const cjsRequire = createRequire(import.meta.url);
|
|
22
|
+
+ const koffi = cjsRequire("koffi");
|
|
23
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
24
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
25
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
26
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
27
|
+
+ const handle = GetStdHandle(-10);
|
|
28
|
+
+ _vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
29
|
+
+ }
|
|
30
|
+
+ const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
31
|
+
+ const mode = new Uint32Array(1);
|
|
32
|
+
+ _vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
33
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
34
|
+
+ _vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
35
|
+
+ }
|
|
36
|
+
+ } catch { }
|
|
37
|
+
+}
|
|
38
|
+
/**
|
|
39
|
+
* Generate a unique temp file path for bash output
|
|
40
|
+
*/
|
|
41
|
+
@@ -76,6 +100,7 @@ const defaultBashOperations = {
|
|
42
|
+
}
|
|
43
|
+
// Handle process exit
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
+ restoreWindowsVTInput();
|
|
46
|
+
if (timeoutHandle)
|
|
47
|
+
clearTimeout(timeoutHandle);
|
|
48
|
+
if (signal)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-tui/dist/terminal.js b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
2
|
+
index cd20330..e836fcd 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
5
|
+
@@ -7,6 +7,7 @@ const cjsRequire = createRequire(import.meta.url);
|
|
6
|
+
* Real terminal using process.stdin/stdout
|
|
7
|
+
*/
|
|
8
|
+
export class ProcessTerminal {
|
|
9
|
+
+ static _vtHandles = null;
|
|
10
|
+
wasRaw = false;
|
|
11
|
+
inputHandler;
|
|
12
|
+
resizeHandler;
|
|
13
|
+
@@ -126,20 +127,23 @@ export class ProcessTerminal {
|
|
14
|
+
if (process.platform !== "win32")
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
- // Dynamic require to avoid bundling koffi's 74MB of cross-platform
|
|
18
|
+
- // native binaries into every compiled binary. Koffi is only needed
|
|
19
|
+
- // on Windows for VT input support.
|
|
20
|
+
- const koffi = cjsRequire("koffi");
|
|
21
|
+
- const k32 = koffi.load("kernel32.dll");
|
|
22
|
+
- const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
23
|
+
- const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
24
|
+
- const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
25
|
+
- const STD_INPUT_HANDLE = -10;
|
|
26
|
+
+ if (!ProcessTerminal._vtHandles) {
|
|
27
|
+
+ const koffi = cjsRequire("koffi");
|
|
28
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
29
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
30
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
31
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
32
|
+
+ const STD_INPUT_HANDLE = -10;
|
|
33
|
+
+ const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
34
|
+
+ ProcessTerminal._vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
35
|
+
+ }
|
|
36
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
37
|
+
- const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
38
|
+
+ const { GetConsoleMode, SetConsoleMode, handle } = ProcessTerminal._vtHandles;
|
|
39
|
+
const mode = new Uint32Array(1);
|
|
40
|
+
GetConsoleMode(handle, mode);
|
|
41
|
+
- SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
42
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
43
|
+
+ SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
44
|
+
+ }
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// koffi not available — Shift+Tab won't be distinguishable from Tab
|
package/scripts/postinstall.js
CHANGED
|
@@ -35,6 +35,14 @@ const banner =
|
|
|
35
35
|
|
|
36
36
|
process.stderr.write(banner)
|
|
37
37
|
|
|
38
|
+
// Apply patches to upstream dependencies (non-fatal)
|
|
39
|
+
try {
|
|
40
|
+
execSync('npx patch-package', { stdio: 'inherit', cwd: resolve(__dirname, '..') })
|
|
41
|
+
process.stderr.write(`\n ${green}✓${reset} Patches applied\n`)
|
|
42
|
+
} catch {
|
|
43
|
+
process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`)
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
// Install Playwright chromium for browser tools (non-fatal)
|
|
39
47
|
const args = os.platform() === 'linux' ? '--with-deps' : ''
|
|
40
48
|
try {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
truncateHead,
|
|
34
34
|
DEFAULT_MAX_BYTES,
|
|
35
35
|
DEFAULT_MAX_LINES,
|
|
36
|
+
getShellConfig,
|
|
36
37
|
} from "@mariozechner/pi-coding-agent";
|
|
37
38
|
import {
|
|
38
39
|
Text,
|
|
@@ -42,11 +43,39 @@ import {
|
|
|
42
43
|
Key,
|
|
43
44
|
} from "@mariozechner/pi-tui";
|
|
44
45
|
import { Type } from "@sinclair/typebox";
|
|
45
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
46
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
46
47
|
import { createConnection } from "node:net";
|
|
47
48
|
import { randomUUID } from "node:crypto";
|
|
48
49
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
49
50
|
import { join } from "node:path";
|
|
51
|
+
import { createRequire } from "node:module";
|
|
52
|
+
|
|
53
|
+
// ── Windows VT Input Restoration ────────────────────────────────────────────
|
|
54
|
+
// Child processes (esp. Git Bash / MSYS2) can strip the ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
55
|
+
// flag from the shared stdin console handle. Re-enable it after each child exits.
|
|
56
|
+
|
|
57
|
+
let _vtHandles: { GetConsoleMode: Function; SetConsoleMode: Function; handle: unknown } | null = null;
|
|
58
|
+
function restoreWindowsVTInput(): void {
|
|
59
|
+
if (process.platform !== "win32") return;
|
|
60
|
+
try {
|
|
61
|
+
if (!_vtHandles) {
|
|
62
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
63
|
+
const koffi = cjsRequire("koffi");
|
|
64
|
+
const k32 = koffi.load("kernel32.dll");
|
|
65
|
+
const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
66
|
+
const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
67
|
+
const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
68
|
+
const handle = GetStdHandle(-10);
|
|
69
|
+
_vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
70
|
+
}
|
|
71
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
72
|
+
const mode = new Uint32Array(1);
|
|
73
|
+
_vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
74
|
+
if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
75
|
+
_vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
76
|
+
}
|
|
77
|
+
} catch { /* koffi not available on non-Windows */ }
|
|
78
|
+
}
|
|
50
79
|
|
|
51
80
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
52
81
|
|
|
@@ -551,11 +580,12 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
551
580
|
|
|
552
581
|
const env = { ...process.env, ...(opts.env || {}) };
|
|
553
582
|
|
|
554
|
-
const
|
|
583
|
+
const { shell, args: shellArgs } = getShellConfig();
|
|
584
|
+
const proc = spawn(shell, [...shellArgs, opts.command], {
|
|
555
585
|
cwd: opts.cwd,
|
|
556
586
|
stdio: ["pipe", "pipe", "pipe"],
|
|
557
587
|
env,
|
|
558
|
-
detached:
|
|
588
|
+
detached: process.platform !== "win32",
|
|
559
589
|
});
|
|
560
590
|
|
|
561
591
|
const bg: BgProcess = {
|
|
@@ -621,6 +651,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
621
651
|
});
|
|
622
652
|
|
|
623
653
|
proc.on("exit", (code, sig) => {
|
|
654
|
+
restoreWindowsVTInput();
|
|
624
655
|
bg.alive = false;
|
|
625
656
|
bg.exitCode = code;
|
|
626
657
|
bg.signal = sig ?? null;
|
|
@@ -686,14 +717,32 @@ function killProcess(id: string, sig: NodeJS.Signals = "SIGTERM"): boolean {
|
|
|
686
717
|
if (!bg) return false;
|
|
687
718
|
if (!bg.alive) return true;
|
|
688
719
|
try {
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
720
|
+
if (process.platform === "win32") {
|
|
721
|
+
// Windows: use taskkill /F /T to force-kill the entire process tree.
|
|
722
|
+
// process.kill(-pid) (Unix process groups) does not work on Windows.
|
|
723
|
+
if (bg.proc.pid) {
|
|
724
|
+
const result = spawnSync("taskkill", ["/F", "/T", "/PID", String(bg.proc.pid)], {
|
|
725
|
+
timeout: 5000,
|
|
726
|
+
encoding: "utf-8",
|
|
727
|
+
});
|
|
728
|
+
if (result.status !== 0 && result.status !== 128) {
|
|
729
|
+
// taskkill failed — try the direct kill as fallback
|
|
730
|
+
bg.proc.kill(sig);
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
693
733
|
bg.proc.kill(sig);
|
|
694
734
|
}
|
|
695
735
|
} else {
|
|
696
|
-
|
|
736
|
+
// Unix/macOS: kill the process group via negative PID
|
|
737
|
+
if (bg.proc.pid) {
|
|
738
|
+
try {
|
|
739
|
+
process.kill(-bg.proc.pid, sig);
|
|
740
|
+
} catch {
|
|
741
|
+
bg.proc.kill(sig);
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
bg.proc.kill(sig);
|
|
745
|
+
}
|
|
697
746
|
}
|
|
698
747
|
return true;
|
|
699
748
|
} catch {
|
|
@@ -343,7 +343,10 @@ async function ensureBrowser(): Promise<{ browser: Browser; context: BrowserCont
|
|
|
343
343
|
// Lazy import so playwright is only loaded when actually needed
|
|
344
344
|
const { chromium } = await import("playwright");
|
|
345
345
|
|
|
346
|
-
|
|
346
|
+
const launchOptions: Record<string, unknown> = { headless: false };
|
|
347
|
+
const customPath = process.env.BROWSER_PATH;
|
|
348
|
+
if (customPath) launchOptions.executablePath = customPath;
|
|
349
|
+
browser = await chromium.launch(launchOptions);
|
|
347
350
|
context = await browser.newContext({
|
|
348
351
|
deviceScaleFactor: 2,
|
|
349
352
|
viewport: { width: 1280, height: 800 },
|