gsd-pi 2.3.9 → 2.3.11

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 CHANGED
@@ -200,7 +200,7 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
200
200
 
201
201
  ### First launch
202
202
 
203
- On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina, Tavily) for web research and documentation tools. All optional — press Enter to skip any.
203
+ On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. Run `gsd config` anytime to re-run the wizard.
204
204
 
205
205
  ### Commands
206
206
 
@@ -223,6 +223,7 @@ On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Co
223
223
  | `Ctrl+Alt+G` | Toggle dashboard overlay |
224
224
  | `Ctrl+Alt+V` | Toggle voice transcription |
225
225
  | `Ctrl+Alt+B` | Show background shell processes |
226
+ | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
226
227
 
227
228
  ---
228
229
 
@@ -360,7 +361,8 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
360
361
  gsd (CLI binary)
361
362
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
362
363
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
363
- ├─ wizard.ts First-run API key collection (Brave/Gemini/Context7/Jina)
364
+ ├─ onboarding.ts First-run setup wizard (LLM provider + tool keys)
365
+ ├─ wizard.ts Env hydration from stored auth.json credentials
364
366
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
365
367
  ├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
366
368
  └─ src/resources/
package/dist/cli.js CHANGED
@@ -4,7 +4,8 @@ import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
5
5
  import { initResources } from './resource-loader.js';
6
6
  import { ensureManagedTools } from './tool-bootstrap.js';
7
- import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js';
7
+ import { loadStoredEnvKeys } from './wizard.js';
8
+ import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
8
9
  function parseCliArgs(argv) {
9
10
  const flags = { extensions: [], messages: [] };
10
11
  const args = argv.slice(2); // skip node + script
@@ -49,6 +50,8 @@ function parseCliArgs(argv) {
49
50
  process.stdout.write(' --tools <a,b,c> Restrict available tools\n');
50
51
  process.stdout.write(' --version, -v Print version and exit\n');
51
52
  process.stdout.write(' --help, -h Print this help and exit\n');
53
+ process.stdout.write('\nSubcommands:\n');
54
+ process.stdout.write(' config Re-run the setup wizard\n');
52
55
  process.exit(0);
53
56
  }
54
57
  else if (!arg.startsWith('--') && !arg.startsWith('-')) {
@@ -59,15 +62,21 @@ function parseCliArgs(argv) {
59
62
  }
60
63
  const cliFlags = parseCliArgs(process.argv);
61
64
  const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
65
+ // `gsd config` — replay the setup wizard and exit
66
+ if (cliFlags.messages[0] === 'config') {
67
+ const authStorage = AuthStorage.create(authFilePath);
68
+ await runOnboarding(authStorage);
69
+ process.exit(0);
70
+ }
62
71
  // Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
63
72
  // because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
64
73
  // Provision local managed binaries first so Pi sees them without probing PATH.
65
74
  ensureManagedTools(join(agentDir, 'bin'));
66
75
  const authStorage = AuthStorage.create(authFilePath);
67
76
  loadStoredEnvKeys(authStorage);
68
- // Skip the setup wizard in print mode it requires TTY interaction
69
- if (!isPrintMode) {
70
- await runWizardIfNeeded(authStorage);
77
+ // Run onboarding wizard on first launch (no LLM provider configured)
78
+ if (!isPrintMode && shouldRunOnboarding(authStorage)) {
79
+ await runOnboarding(authStorage);
71
80
  }
72
81
  const modelRegistry = new ModelRegistry(authStorage);
73
82
  const settingsManager = SettingsManager.create(agentDir);
package/dist/loader.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, resolve, join } from 'path';
4
- import { existsSync, readFileSync } from 'fs';
5
- import { agentDir, appRoot } from './app-paths.js';
4
+ import { readFileSync } from 'fs';
5
+ import { agentDir } from './app-paths.js';
6
6
  // pkg/ is a shim directory: contains gsd's piConfig (package.json) and pi's
7
7
  // theme assets (dist/modes/interactive/theme/) without a src/ directory.
8
8
  // This allows config.js to:
@@ -14,30 +14,7 @@ const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'pkg');
14
14
  process.env.PI_PACKAGE_DIR = pkgDir;
15
15
  process.env.PI_SKIP_VERSION_CHECK = '1'; // GSD ships its own update check — suppress pi's
16
16
  process.title = 'gsd';
17
- // Print branded banner on first launch (before ~/.gsd/ exists)
18
- if (!existsSync(appRoot)) {
19
- const cyan = '\x1b[36m';
20
- const green = '\x1b[32m';
21
- const dim = '\x1b[2m';
22
- const reset = '\x1b[0m';
23
- let version = '';
24
- try {
25
- const pkgJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
26
- version = pkgJson.version ?? '';
27
- }
28
- catch { /* ignore */ }
29
- process.stderr.write('\n' +
30
- cyan +
31
- ' ██████╗ ███████╗██████╗ \n' +
32
- ' ██╔════╝ ██╔════╝██╔══██╗\n' +
33
- ' ██║ ███╗███████╗██║ ██║\n' +
34
- ' ██║ ██║╚════██║██║ ██║\n' +
35
- ' ╚██████╔╝███████║██████╔╝\n' +
36
- ' ╚═════╝ ╚══════╝╚═════╝ ' +
37
- reset + '\n\n' +
38
- ` Get Shit Done ${dim}v${version}${reset}\n` +
39
- ` ${green}Welcome.${reset} Setting up your environment...\n\n`);
40
- }
17
+ // First-launch branding is handled by the onboarding wizard (src/onboarding.ts)
41
18
  // GSD_CODING_AGENT_DIR — tells pi's getAgentDir() to return ~/.gsd/agent/ instead of ~/.gsd/agent/
42
19
  process.env.GSD_CODING_AGENT_DIR = agentDir;
43
20
  // NODE_PATH — make gsd's own node_modules available to extensions loaded via jiti.
package/dist/logo.d.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * - src/onboarding.ts (via ./logo.js)
7
7
  */
8
8
  /** Raw logo lines — no ANSI codes, no leading newline. */
9
- export declare const GSD_LOGO: readonly string[];
9
+ export declare const GSD_LOGO: string[];
10
10
  /**
11
11
  * Render the logo block with a color function applied to each line.
12
12
  *
13
- * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
13
+ * @param color — e.g. picocolors.cyan or `(s) => `\x1b[36m${s}\x1b[0m``
14
14
  * @returns Ready-to-write string with leading/trailing newlines.
15
15
  */
16
16
  export declare function renderLogo(color: (s: string) => string): string;
package/dist/logo.js CHANGED
@@ -17,7 +17,7 @@ export const GSD_LOGO = [
17
17
  /**
18
18
  * Render the logo block with a color function applied to each line.
19
19
  *
20
- * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
20
+ * @param color — e.g. picocolors.cyan or `(s) => `\x1b[36m${s}\x1b[0m``
21
21
  * @returns Ready-to-write string with leading/trailing newlines.
22
22
  */
23
23
  export function renderLogo(color) {
@@ -4,8 +4,8 @@
4
4
  * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
5
  * that guides users through LLM provider authentication before the TUI launches.
6
6
  *
7
- * Flow: logo choose LLM provider authenticate (OAuth or API key)
8
- * optional tool keys summary TUI launches.
7
+ * Flow: logo -> choose LLM provider -> authenticate (OAuth or API key) ->
8
+ * optional tool keys -> summary -> TUI launches.
9
9
  *
10
10
  * All steps are skippable. All errors are recoverable. Never crashes boot.
11
11
  */
@@ -4,8 +4,8 @@
4
4
  * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
5
  * that guides users through LLM provider authentication before the TUI launches.
6
6
  *
7
- * Flow: logo choose LLM provider authenticate (OAuth or API key)
8
- * optional tool keys summary TUI launches.
7
+ * Flow: logo -> choose LLM provider -> authenticate (OAuth or API key) ->
8
+ * optional tool keys -> summary -> TUI launches.
9
9
  *
10
10
  * All steps are skippable. All errors are recoverable. Never crashes boot.
11
11
  */
@@ -69,12 +69,17 @@ const API_KEY_PREFIXES = {
69
69
  anthropic: ['sk-ant-'],
70
70
  openai: ['sk-'],
71
71
  };
72
+ const OTHER_PROVIDERS = [
73
+ { value: 'google', label: 'Google (Gemini)' },
74
+ { value: 'groq', label: 'Groq' },
75
+ { value: 'xai', label: 'xAI (Grok)' },
76
+ { value: 'openrouter', label: 'OpenRouter' },
77
+ { value: 'mistral', label: 'Mistral' },
78
+ ];
72
79
  // ─── Dynamic imports ──────────────────────────────────────────────────────────
73
80
  /**
74
81
  * Dynamically import @clack/prompts and picocolors.
75
- * These are transitive dependencies they exist in node_modules but are not
76
- * in our direct dependencies. We use dynamic import with a fallback so the
77
- * module doesn't crash if they're missing.
82
+ * Dynamic import with fallback so the module doesn't crash if they're missing.
78
83
  */
79
84
  async function loadClack() {
80
85
  try {
@@ -86,15 +91,13 @@ async function loadClack() {
86
91
  }
87
92
  async function loadPico() {
88
93
  try {
89
- return await import('picocolors');
94
+ const mod = await import('picocolors');
95
+ return mod.default ?? mod;
90
96
  }
91
97
  catch {
92
98
  // Fallback: return identity functions
93
99
  const identity = (s) => s;
94
- return {
95
- default: { cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity },
96
- cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity,
97
- };
100
+ return { cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity };
98
101
  }
99
102
  }
100
103
  // ─── Utilities ────────────────────────────────────────────────────────────────
@@ -107,6 +110,10 @@ function openBrowser(url) {
107
110
  // Ignore errors — user can manually open the URL
108
111
  });
109
112
  }
113
+ /** Check if an error is a clack cancel signal */
114
+ function isCancelError(p, err) {
115
+ return p.isCancel(err);
116
+ }
110
117
  // ─── Public API ───────────────────────────────────────────────────────────────
111
118
  /**
112
119
  * Determine if the onboarding wizard should run.
@@ -156,7 +163,6 @@ export async function runOnboarding(authStorage) {
156
163
  p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
157
164
  // ── LLM Provider Selection ────────────────────────────────────────────────
158
165
  let llmConfigured = false;
159
- let llmProviderName = '';
160
166
  try {
161
167
  llmConfigured = await runLlmStep(p, pc, authStorage);
162
168
  }
@@ -338,13 +344,6 @@ async function runApiKeyFlow(p, pc, authStorage, providerId, providerLabel) {
338
344
  return true;
339
345
  }
340
346
  // ─── "Other Provider" Sub-Flow ────────────────────────────────────────────────
341
- const OTHER_PROVIDERS = [
342
- { value: 'google', label: 'Google (Gemini)' },
343
- { value: 'groq', label: 'Groq' },
344
- { value: 'xai', label: 'xAI (Grok)' },
345
- { value: 'openrouter', label: 'OpenRouter' },
346
- { value: 'mistral', label: 'Mistral' },
347
- ];
348
347
  async function runOtherProviderFlow(p, pc, authStorage) {
349
348
  const provider = await p.select({
350
349
  message: 'Select provider',
@@ -393,12 +392,6 @@ async function runToolKeysStep(p, pc, authStorage) {
393
392
  }
394
393
  return savedCount;
395
394
  }
396
- // ─── Helpers ──────────────────────────────────────────────────────────────────
397
- /** Check if an error is a clack cancel signal */
398
- function isCancelError(p, err) {
399
- // Clack throws a symbol when the user cancels (Ctrl+C)
400
- return p.isCancel(err);
401
- }
402
395
  // ─── Env hydration (migrated from wizard.ts) ─────────────────────────────────
403
396
  /**
404
397
  * Hydrate process.env from stored auth.json credentials for optional tool keys.
package/dist/wizard.d.ts CHANGED
@@ -5,11 +5,3 @@ import type { AuthStorage } from '@mariozechner/pi-coding-agent';
5
5
  * wizard on prior launches.
6
6
  */
7
7
  export declare function loadStoredEnvKeys(authStorage: AuthStorage): void;
8
- /**
9
- * Check for missing optional tool API keys and prompt for them if on a TTY.
10
- *
11
- * Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
12
- * This wizard only collects Brave Search, Context7, and Jina keys which are needed
13
- * for web search and documentation tools.
14
- */
15
- export declare function runWizardIfNeeded(authStorage: AuthStorage): Promise<void>;
package/dist/wizard.js CHANGED
@@ -1,74 +1,3 @@
1
- import { createInterface } from 'readline';
2
- // ─── Colors ──────────────────────────────────────────────────────────────────
3
- const cyan = '\x1b[36m';
4
- const green = '\x1b[32m';
5
- const yellow = '\x1b[33m';
6
- const dim = '\x1b[2m';
7
- const bold = '\x1b[1m';
8
- const reset = '\x1b[0m';
9
- // ─── Masked input ─────────────────────────────────────────────────────────────
10
- /**
11
- * Prompt for masked input using raw mode stdin.
12
- * Handles backspace, Ctrl+C, and Enter.
13
- * Falls back to plain readline if setRawMode is unavailable (e.g. some SSH contexts).
14
- */
15
- async function promptMasked(label, hint) {
16
- return new Promise((resolve) => {
17
- const question = ` ${cyan}›${reset} ${label} ${dim}${hint}${reset}\n `;
18
- try {
19
- process.stdout.write(question);
20
- process.stdin.setRawMode(true);
21
- process.stdin.resume();
22
- process.stdin.setEncoding('utf8');
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
- };
36
- const handler = (ch) => {
37
- if (ch === '\r' || ch === '\n') {
38
- process.stdin.setRawMode(false);
39
- process.stdin.pause();
40
- process.stdin.off('data', handler);
41
- process.stdout.write('\n');
42
- resolve(value);
43
- }
44
- else if (ch === '\u0003') {
45
- process.stdin.setRawMode(false);
46
- process.stdout.write('\n');
47
- process.exit(0);
48
- }
49
- else if (ch === '\u007f' || ch === '\b') {
50
- if (value.length > 0) {
51
- value = value.slice(0, -1);
52
- }
53
- redraw();
54
- }
55
- else {
56
- value += ch;
57
- redraw();
58
- }
59
- };
60
- process.stdin.on('data', handler);
61
- }
62
- catch (_err) {
63
- process.stdout.write(` ${dim}(input will be visible)${reset}\n `);
64
- const rl = createInterface({ input: process.stdin, output: process.stdout });
65
- rl.question('', (answer) => {
66
- rl.close();
67
- resolve(answer);
68
- });
69
- }
70
- });
71
- }
72
1
  // ─── Env hydration ────────────────────────────────────────────────────────────
73
2
  /**
74
3
  * Hydrate process.env from stored auth.json credentials for optional tool keys.
@@ -94,101 +23,3 @@ export function loadStoredEnvKeys(authStorage) {
94
23
  }
95
24
  }
96
25
  }
97
- const API_KEYS = [
98
- {
99
- provider: 'brave',
100
- envVar: 'BRAVE_API_KEY',
101
- label: 'Brave Search',
102
- hint: '(search-the-web + search_and_read tools)',
103
- description: 'Web search and page extraction',
104
- },
105
- {
106
- provider: 'brave_answers',
107
- envVar: 'BRAVE_ANSWERS_KEY',
108
- label: 'Brave Answers',
109
- hint: '(AI-summarised search answers)',
110
- description: 'AI-generated search summaries',
111
- },
112
- {
113
- provider: 'context7',
114
- envVar: 'CONTEXT7_API_KEY',
115
- label: 'Context7',
116
- hint: '(up-to-date library docs)',
117
- description: 'Live library and framework documentation',
118
- },
119
- {
120
- provider: 'jina',
121
- envVar: 'JINA_API_KEY',
122
- label: 'Jina AI',
123
- hint: '(clean page extraction)',
124
- description: 'High-quality web page content extraction',
125
- },
126
- {
127
- provider: 'tavily',
128
- envVar: 'TAVILY_API_KEY',
129
- label: 'Tavily Search',
130
- hint: '(search-the-web + search_and_read tools, starts with tvly-)',
131
- description: 'Web search and page extraction (alternative to Brave)',
132
- },
133
- {
134
- provider: 'slack_bot',
135
- envVar: 'SLACK_BOT_TOKEN',
136
- label: 'Slack Bot',
137
- hint: '(remote questions in auto-mode)',
138
- description: 'Bot token for remote questions via Slack',
139
- },
140
- {
141
- provider: 'discord_bot',
142
- envVar: 'DISCORD_BOT_TOKEN',
143
- label: 'Discord Bot',
144
- hint: '(remote questions in auto-mode)',
145
- description: 'Bot token for remote questions via Discord',
146
- },
147
- ];
148
- /**
149
- * Check for missing optional tool API keys and prompt for them if on a TTY.
150
- *
151
- * Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
152
- * This wizard only collects Brave Search, Context7, and Jina keys which are needed
153
- * for web search and documentation tools.
154
- */
155
- export async function runWizardIfNeeded(authStorage) {
156
- const missing = API_KEYS.filter(k => !authStorage.has(k.provider) && !process.env[k.envVar]);
157
- if (missing.length === 0)
158
- return;
159
- // Non-TTY: warn and continue
160
- if (!process.stdin.isTTY) {
161
- const names = missing.map(k => k.label).join(', ');
162
- process.stderr.write(`[gsd] Warning: optional tool API keys not configured (${names}). Some tools may not work.\n`);
163
- return;
164
- }
165
- // ── Header ──────────────────────────────────────────────────────────────────
166
- process.stdout.write(`\n ${bold}Optional API keys${reset}\n` +
167
- ` ${dim}─────────────────────────────────────────────${reset}\n` +
168
- ` These unlock additional tools. All optional — press ${cyan}Enter${reset} to skip any.\n\n`);
169
- // ── Prompts ─────────────────────────────────────────────────────────────────
170
- let savedCount = 0;
171
- for (const key of missing) {
172
- const value = await promptMasked(key.label, key.hint);
173
- if (value.trim()) {
174
- authStorage.set(key.provider, { type: 'api_key', key: value.trim() });
175
- process.env[key.envVar] = value.trim();
176
- process.stdout.write(` ${green}✓${reset} ${key.label} saved\n\n`);
177
- savedCount++;
178
- }
179
- else {
180
- authStorage.set(key.provider, { type: 'api_key', key: '' });
181
- process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`);
182
- }
183
- }
184
- // ── Footer ───────────────────────────────────────────────────────────────────
185
- process.stdout.write(` ${dim}─────────────────────────────────────────────${reset}\n`);
186
- if (savedCount > 0) {
187
- process.stdout.write(` ${green}✓${reset} ${savedCount} key${savedCount > 1 ? 's' : ''} saved to ${dim}~/.gsd/agent/auth.json${reset}\n` +
188
- ` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
189
- }
190
- else {
191
- process.stdout.write(` ${yellow}↷${reset} All keys skipped — you can add them later via ${dim}~/.gsd/agent/auth.json${reset}\n` +
192
- ` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
193
- }
194
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.9",
3
+ "version": "2.3.11",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,9 @@
44
44
  "prepublishOnly": "npm run sync-pkg-version && npm run build"
45
45
  },
46
46
  "dependencies": {
47
+ "@clack/prompts": "^1.1.0",
47
48
  "@mariozechner/pi-coding-agent": "^0.57.1",
49
+ "picocolors": "^1.1.1",
48
50
  "playwright": "^1.58.2"
49
51
  },
50
52
  "devDependencies": {
@@ -1,18 +1,38 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from 'child_process'
2
+
3
+ import { exec as execCb } from 'child_process'
3
4
  import { createRequire } from 'module'
4
5
  import os from 'os'
5
- import { fileURLToPath } from 'url'
6
6
  import { dirname, resolve } from 'path'
7
+ import { fileURLToPath } from 'url'
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url))
9
10
  const require = createRequire(import.meta.url)
10
11
  const pkg = require(resolve(__dirname, '..', 'package.json'))
12
+ const cwd = resolve(__dirname, '..')
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Async exec helper — captures stdout+stderr, never inherits to terminal
16
+ // ---------------------------------------------------------------------------
17
+ function run(cmd, options = {}) {
18
+ return new Promise((resolve) => {
19
+ execCb(cmd, { cwd, ...options }, (error, stdout, stderr) => {
20
+ resolve({ ok: !error, stdout, stderr, error })
21
+ })
22
+ })
23
+ }
11
24
 
12
- // Colors
25
+ // ---------------------------------------------------------------------------
26
+ // Redirect stdout → stderr so npm always shows postinstall output.
27
+ // npm ≥7 suppresses stdout from lifecycle scripts by default; stderr is
28
+ // always forwarded. Clack writes to process.stdout, so we reroute it.
29
+ // ---------------------------------------------------------------------------
30
+ process.stdout.write = process.stderr.write.bind(process.stderr)
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // ASCII banner — printed before clack UI for brand recognition
34
+ // ---------------------------------------------------------------------------
13
35
  const cyan = '\x1b[36m'
14
- const green = '\x1b[32m'
15
- const yellow = '\x1b[33m'
16
36
  const dim = '\x1b[2m'
17
37
  const reset = '\x1b[0m'
18
38
 
@@ -27,58 +47,81 @@ const banner =
27
47
  ' ╚═════╝ ╚══════╝╚═════╝ ' +
28
48
  reset + '\n' +
29
49
  '\n' +
30
- ` Get Shit Done ${dim}v${pkg.version}${reset}\n` +
31
- ` A standalone coding agent that plans, executes, and ships.\n` +
32
- '\n' +
33
- ` ${green}✓${reset} Installed successfully\n` +
34
- ` ${dim}Run ${reset}${cyan}gsd${reset}${dim} to get started.${reset}\n`
35
-
36
- function run(command, options = {}) {
37
- return execSync(command, {
38
- cwd: resolve(__dirname, '..'),
39
- encoding: 'utf8',
40
- stdio: ['ignore', 'pipe', 'pipe'],
41
- ...options,
42
- })
43
- }
50
+ ` Get Shit Done ${dim}v${pkg.version}${reset}\n`
44
51
 
45
- function printCaptured(output) {
46
- if (output) process.stderr.write(output)
47
- }
52
+ // ---------------------------------------------------------------------------
53
+ // Main — wrapped in async IIFE, with graceful fallback if clack fails
54
+ // ---------------------------------------------------------------------------
55
+ ;(async () => {
56
+ process.stderr.write(banner)
48
57
 
49
- process.stderr.write(banner)
50
-
51
- // Apply patches to upstream dependencies (non-fatal)
52
- try {
53
- const output = run('npx patch-package')
54
- printCaptured(output)
55
- process.stderr.write(`\n ${green}✓${reset} Patches applied\n`)
56
- } catch (error) {
57
- printCaptured(error.stdout)
58
- printCaptured(error.stderr)
59
- process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`)
60
- }
58
+ let p, pc
61
59
 
62
- // Install Playwright chromium for browser tools (non-fatal).
63
- // We intentionally avoid --with-deps here because install scripts should not
64
- // block on interactive sudo prompts. Playwright validates host requirements
65
- // after download; if Linux libs are missing, suggest the explicit follow-up.
66
- try {
67
- const output = run('npx playwright install chromium')
68
- printCaptured(output)
69
- process.stderr.write(`\n ${green}✓${reset} Browser tools ready\n\n`)
70
- } catch (error) {
71
- const output = `${error.stdout ?? ''}${error.stderr ?? ''}`
72
- printCaptured(output)
73
-
74
- if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) {
75
- process.stderr.write(
76
- `\n ${yellow}⚠${reset} Browser downloaded, but Linux system dependencies are missing.\n` +
77
- ` ${dim}Run ${reset}${cyan}sudo npx playwright install-deps chromium${reset}${dim} to finish setup.${reset}\n\n`
78
- )
60
+ try {
61
+ p = await import('@clack/prompts')
62
+ pc = (await import('picocolors')).default
63
+ } catch {
64
+ // Clack or picocolors unavailable — fall back to minimal output
65
+ process.stderr.write(` Run gsd to get started.\n\n`)
66
+ await run('npx patch-package')
67
+ await run('npx playwright install chromium')
68
+ return
69
+ }
70
+
71
+ // --- Branded intro -------------------------------------------------------
72
+ p.intro('Setup')
73
+
74
+ const results = []
75
+ const s = p.spinner()
76
+
77
+ // --- Step 1: Apply patches -----------------------------------------------
78
+ s.start('Applying patches…')
79
+ const patchResult = await run('npx patch-package')
80
+ if (patchResult.ok) {
81
+ s.stop('Patches applied')
82
+ results.push({ label: 'Patches applied', ok: true })
79
83
  } else {
80
- process.stderr.write(
81
- `\n ${yellow}⚠${reset} Browser tools unavailable — run ${cyan}npx playwright install chromium${reset} to enable\n\n`
82
- )
84
+ s.stop(pc.yellow('Patches — skipped (non-fatal)'))
85
+ results.push({
86
+ label: 'Patches skipped — run ' + pc.cyan('npx patch-package') + ' manually',
87
+ ok: false,
88
+ })
83
89
  }
84
- }
90
+
91
+ // --- Step 2: Playwright browser ------------------------------------------
92
+ // Avoid --with-deps: install scripts should not block on interactive sudo
93
+ // prompts. If Linux libs are missing, suggest the explicit follow-up.
94
+ s.start('Setting up browser tools…')
95
+ const pwResult = await run('npx playwright install chromium')
96
+ if (pwResult.ok) {
97
+ s.stop('Browser tools ready')
98
+ results.push({ label: 'Browser tools ready', ok: true })
99
+ } else {
100
+ const output = `${pwResult.stdout ?? ''}${pwResult.stderr ?? ''}`
101
+ if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) {
102
+ s.stop(pc.yellow('Browser downloaded, missing Linux deps'))
103
+ results.push({
104
+ label: 'Run ' + pc.cyan('sudo npx playwright install-deps chromium') + ' to finish setup',
105
+ ok: false,
106
+ })
107
+ } else {
108
+ s.stop(pc.yellow('Browser tools — skipped (non-fatal)'))
109
+ results.push({
110
+ label: 'Browser tools unavailable — run ' + pc.cyan('npx playwright install chromium'),
111
+ ok: false,
112
+ })
113
+ }
114
+ }
115
+
116
+ // --- Summary note --------------------------------------------------------
117
+ const lines = results.map(
118
+ (r) => (r.ok ? pc.green('✓') : pc.yellow('⚠')) + ' ' + r.label
119
+ )
120
+ lines.push('')
121
+ lines.push('Run ' + pc.cyan('gsd') + ' to get started.')
122
+
123
+ p.note(lines.join('\n'), 'Installed')
124
+
125
+ // --- Outro ---------------------------------------------------------------
126
+ p.outro(pc.green('Done!'))
127
+ })()
@@ -48,6 +48,7 @@ import { createConnection } from "node:net";
48
48
  import { randomUUID } from "node:crypto";
49
49
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
50
50
  import { join } from "node:path";
51
+ import { shortcutDesc } from "../shared/terminal.js";
51
52
  import { createRequire } from "node:module";
52
53
 
53
54
  // ── Windows VT Input Restoration ────────────────────────────────────────────
@@ -2356,7 +2357,7 @@ export default function (pi: ExtensionAPI) {
2356
2357
  // ── Ctrl+Alt+B shortcut ──────────────────────────────────────────────
2357
2358
 
2358
2359
  pi.registerShortcut(Key.ctrlAlt("b"), {
2359
- description: "Open background process manager",
2360
+ description: shortcutDesc("Open background process manager", "/bg"),
2360
2361
  handler: async (ctx) => {
2361
2362
  latestCtx = ctx;
2362
2363
  await ctx.ui.custom<void>(
@@ -47,6 +47,7 @@ import {
47
47
  import { Key } from "@mariozechner/pi-tui";
48
48
  import { join } from "node:path";
49
49
  import { existsSync } from "node:fs";
50
+ import { shortcutDesc } from "../shared/terminal.js";
50
51
  import { Text } from "@mariozechner/pi-tui";
51
52
 
52
53
  // ── ASCII logo ────────────────────────────────────────────────────────────
@@ -184,10 +185,8 @@ export default function (pi: ExtensionAPI) {
184
185
  });
185
186
 
186
187
  // ── Ctrl+Alt+G shortcut — GSD dashboard overlay ────────────────────────
187
- // Requires Kitty keyboard protocol or modifyOtherKeys support.
188
- // Terminals without support (macOS Terminal.app, JetBrains): use /gsd status instead.
189
188
  pi.registerShortcut(Key.ctrlAlt("g"), {
190
- description: "Open GSD dashboard (or use /gsd status)",
189
+ description: shortcutDesc("Open GSD dashboard", "/gsd status"),
191
190
  handler: async (ctx) => {
192
191
  // Only show if .gsd/ exists
193
192
  if (!existsSync(join(process.cwd(), ".gsd"))) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Terminal capability detection for keyboard shortcut support.
3
+ *
4
+ * Ctrl+Alt shortcuts require the Kitty keyboard protocol or modifyOtherKeys.
5
+ * Terminals that lack this support silently swallow the key combos.
6
+ */
7
+
8
+ const UNSUPPORTED_TERMS = ["apple_terminal"];
9
+
10
+ export function supportsCtrlAltShortcuts(): boolean {
11
+ const term = (process.env.TERM_PROGRAM || "").toLowerCase();
12
+ const jetbrains = (process.env.TERMINAL_EMULATOR || "").toLowerCase().includes("jetbrains");
13
+ return !UNSUPPORTED_TERMS.some((t) => term.includes(t)) && !jetbrains;
14
+ }
15
+
16
+ /**
17
+ * Returns a shortcut description that includes a slash-command fallback hint
18
+ * when the current terminal likely can't fire Ctrl+Alt combos.
19
+ */
20
+ export function shortcutDesc(base: string, fallbackCmd: string): string {
21
+ if (supportsCtrlAltShortcuts()) return base;
22
+ return `${base} — shortcut may not work in this terminal, use ${fallbackCmd}`;
23
+ }
@@ -654,8 +654,7 @@ export default function (pi: ExtensionAPI) {
654
654
  const output = isError
655
655
  ? (r.errorMessage || r.stderr || getFinalOutput(r.messages) || "(no output)")
656
656
  : getFinalOutput(r.messages);
657
- const preview = output.slice(0, 200) + (output.length > 200 ? "..." : "");
658
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${preview || "(no output)"}`;
657
+ return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${output || "(no output)"}`;
659
658
  });
660
659
  return {
661
660
  content: [
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { shortcutDesc } from "../shared/terminal.js";
2
3
  import type { AssistantMessage } from "@mariozechner/pi-ai";
3
4
  import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
5
  import { spawn, execSync, type ChildProcess } from "node:child_process";
@@ -131,7 +132,7 @@ export default function (pi: ExtensionAPI) {
131
132
  });
132
133
 
133
134
  pi.registerShortcut("ctrl+alt+v", {
134
- description: "Toggle voice mode",
135
+ description: shortcutDesc("Toggle voice mode", "/voice"),
135
136
  handler: async (ctx) => toggleVoice(ctx),
136
137
  });
137
138