@wacht/bench 0.1.0

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.
@@ -0,0 +1,19 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { AUTH_DIR, AUTH_FILE } from './config.js';
3
+ import { isStoredAuth } from './guards.js';
4
+ export async function readAuth() {
5
+ try {
6
+ const parsed = JSON.parse(await readFile(AUTH_FILE, 'utf8'));
7
+ return isStoredAuth(parsed) ? parsed : null;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ export async function writeAuth(auth) {
14
+ await mkdir(AUTH_DIR, { recursive: true });
15
+ await writeFile(AUTH_FILE, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
16
+ }
17
+ export async function clearAuth() {
18
+ await rm(AUTH_FILE, { force: true });
19
+ }
@@ -0,0 +1,15 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function openBrowser(url) {
3
+ const command = process.platform === 'darwin'
4
+ ? 'open'
5
+ : process.platform === 'win32'
6
+ ? 'cmd'
7
+ : 'xdg-open';
8
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
9
+ const child = spawn(command, args, {
10
+ detached: true,
11
+ stdio: 'ignore',
12
+ });
13
+ child.on('error', () => { });
14
+ child.unref();
15
+ }
@@ -0,0 +1,389 @@
1
+ import { Command } from 'commander';
2
+ import { completionScript } from './completion.js';
3
+ import { configApply, configDiff, configPull, configSchemaCommand, printConfigTemplate, } from './config-workflow.js';
4
+ import { clearDeployment, createDeploymentCommand, createProjectCommand, currentDeployment, selectDeployment, } from './deployment-context.js';
5
+ import { initProject, initStarter } from './init.js';
6
+ import { apiCommand, listProjects } from './machine-api.js';
7
+ import { openApiCall, openApiDescribe, openApiList, openApiRefresh } from './openapi.js';
8
+ import { printMcpConfig } from './mcp.js';
9
+ import { authStatus, login, logout } from './oauth.js';
10
+ import { createOrg, createUser, createWorkspace, getOrg, getUser, getWorkspace, listOrgs, listUsers, listWorkspaces, } from './resources.js';
11
+ import { installSkills } from './skills.js';
12
+ import { banner } from './ui.js';
13
+ function collect(value, previous) {
14
+ previous.push(value);
15
+ return previous;
16
+ }
17
+ function context(command) {
18
+ const options = command.optsWithGlobals();
19
+ if (options.color === false) {
20
+ process.env.NO_COLOR = '1';
21
+ }
22
+ return {
23
+ json: !!options.json,
24
+ quiet: !!options.quiet,
25
+ banner: options.banner !== false,
26
+ interactive: options.interactive !== false && !options.json && !options.quiet,
27
+ };
28
+ }
29
+ function initArgs(options) {
30
+ const args = [];
31
+ if (options.client)
32
+ args.push('--client', options.client);
33
+ if (options.installSkills)
34
+ args.push('--install-skills');
35
+ if (options.skipAgents)
36
+ args.push('--skip-agents');
37
+ if (options.skipConfig)
38
+ args.push('--skip-config');
39
+ if (options.skipEnv)
40
+ args.push('--skip-env');
41
+ if (options.skipGuide)
42
+ args.push('--skip-guide');
43
+ if (options.skipTemplates)
44
+ args.push('--skip-templates');
45
+ return args;
46
+ }
47
+ export async function runCli(args) {
48
+ const program = new Command();
49
+ program
50
+ .name('wacht')
51
+ .description('AI development workbench for Wacht')
52
+ .showHelpAfterError()
53
+ .option('--json', 'emit machine-readable JSON where supported')
54
+ .option('--quiet', 'suppress nonessential human output')
55
+ .option('--no-interactive', 'do not prompt; require explicit arguments and flags')
56
+ .option('--no-banner', 'hide the Wacht Bench banner')
57
+ .option('--no-color', 'disable colored output')
58
+ .addHelpText('beforeAll', () => {
59
+ const options = program.opts();
60
+ if (options.json || options.quiet || options.banner === false)
61
+ return '';
62
+ return `${banner()}\n\n`;
63
+ });
64
+ program
65
+ .command('init')
66
+ .description('bootstrap the current project for Wacht development')
67
+ .option('--client <client>', 'assistant client metadata', 'cursor')
68
+ .option('--starter [framework]', 'clone a Wacht starter (nextjs, react-router, tanstack) instead of bootstrapping the current dir')
69
+ .option('--target <dir>', 'target directory when using --starter (defaults to ./wacht-<framework>-starter)')
70
+ .option('--install-skills', 'run npx skills add after writing config')
71
+ .option('--skip-agents', 'do not create or update AGENTS.md')
72
+ .option('--skip-config', 'do not write .wacht/bench.json')
73
+ .option('--skip-env', 'do not write .env.wacht.example')
74
+ .option('--skip-guide', 'do not write .wacht/BOOTSTRAP.md')
75
+ .option('--skip-templates', 'do not write starter templates under .wacht/templates')
76
+ .action(async (options) => {
77
+ const ctx = context(program);
78
+ if (options.starter) {
79
+ const framework = typeof options.starter === 'string' ? options.starter : undefined;
80
+ await initStarter({
81
+ framework,
82
+ target: options.target,
83
+ install: !!options.installSkills,
84
+ client: options.client ?? 'cursor',
85
+ }, ctx);
86
+ return;
87
+ }
88
+ await initProject(initArgs(options), ctx);
89
+ });
90
+ program
91
+ .command('login')
92
+ .description('log in with Wacht OAuth using Authorization Code + PKCE')
93
+ .option('--skip-select', 'do not select an active deployment after login')
94
+ .action(async (options) => {
95
+ const ctx = context(program);
96
+ await login(ctx);
97
+ if (!options.skipSelect && ctx.interactive) {
98
+ await selectDeployment(ctx, { optional: true });
99
+ }
100
+ });
101
+ program
102
+ .command('logout')
103
+ .description('clear local Bench auth and revoke the refresh token when possible')
104
+ .action(async () => {
105
+ await logout(context(program));
106
+ });
107
+ const auth = program.command('auth').description('inspect or manage Bench auth');
108
+ auth
109
+ .command('status')
110
+ .description('show local Bench auth status')
111
+ .action(async () => {
112
+ await authStatus(context(program));
113
+ });
114
+ const projects = program.command('projects').description('work with Wacht projects');
115
+ projects
116
+ .command('list')
117
+ .description('list projects visible to the current OAuth grant')
118
+ .action(async () => {
119
+ await listProjects(context(program));
120
+ });
121
+ projects
122
+ .command('create')
123
+ .description('create a Wacht project with its first staging deployment')
124
+ .option('--name <name>', 'project name')
125
+ .option('--method <method>', 'auth method; repeatable. Project/staging: email, phone, username, google_oauth, apple_oauth, facebook_oauth, github_oauth, discord_oauth, linkedin_oauth, gitlab_oauth, x_oauth', collect, [])
126
+ .option('--no-select', 'do not make the created staging deployment active')
127
+ .action(async (options) => {
128
+ await createProjectCommand(context(program), options);
129
+ });
130
+ const deployments = program.command('deployments').description('work with Wacht deployments');
131
+ deployments
132
+ .command('current')
133
+ .description('show the active deployment Bench will use')
134
+ .action(async () => {
135
+ await currentDeployment(context(program));
136
+ });
137
+ deployments
138
+ .command('select')
139
+ .description('select the active project/deployment for Bench')
140
+ .option('--project <id>', 'project id')
141
+ .option('--deployment <id>', 'deployment id')
142
+ .option('--mode <mode>', 'deployment mode: staging or production')
143
+ .action(async (options) => {
144
+ await selectDeployment(context(program), options);
145
+ });
146
+ deployments
147
+ .command('clear')
148
+ .description('clear the active deployment selection')
149
+ .action(async () => {
150
+ await clearDeployment(context(program));
151
+ });
152
+ deployments
153
+ .command('create')
154
+ .description('create a staging or production deployment')
155
+ .argument('[mode]', 'staging or production')
156
+ .option('--project <id>', 'project id; defaults to the active project')
157
+ .option('--domain <domain>', 'custom domain for production deployments')
158
+ .option('--method <method>', 'auth method; repeatable. Staging supports social providers; production supports email, phone, username', collect, [])
159
+ .option('--no-select', 'do not make the created deployment active')
160
+ .action(async (mode, options) => {
161
+ await createDeploymentCommand(context(program), { ...options, mode });
162
+ });
163
+ const skills = program.command('skills').description('install Wacht agent skills');
164
+ skills
165
+ .command('install')
166
+ .description('install the Wacht skills pack')
167
+ .option('--skill <name>', 'install one skill from the pack')
168
+ .action(async (options) => {
169
+ await installSkills(options.skill);
170
+ });
171
+ const mcp = program.command('mcp').description('print Wacht Docs MCP configuration');
172
+ mcp
173
+ .command('config')
174
+ .description('print MCP config JSON for an assistant client')
175
+ .option('--client <client>', 'cursor, claude, or codex', 'cursor')
176
+ .action((options) => {
177
+ printMcpConfig(options.client);
178
+ });
179
+ const config = program.command('config').description('manage Wacht deployment settings as code');
180
+ config
181
+ .command('pull')
182
+ .description('pull the active deployment settings into wacht.config.json')
183
+ .option('--file <path>', 'config file path', 'wacht.config.json')
184
+ .option('--deployment <id>', 'deployment id override; defaults to active deployment')
185
+ .action(async (options) => {
186
+ await configPull(context(program), options);
187
+ });
188
+ config
189
+ .command('schema')
190
+ .description('print the Wacht config JSON schema')
191
+ .option('--refresh', 'refresh the cached OpenAPI schema first')
192
+ .action(async (options) => {
193
+ await configSchemaCommand(context(program), options);
194
+ });
195
+ config
196
+ .command('template')
197
+ .description('print a minimal Wacht config template')
198
+ .action(() => {
199
+ printConfigTemplate(context(program));
200
+ });
201
+ config
202
+ .command('diff')
203
+ .description('compare a config file with the active deployment')
204
+ .option('--file <path>', 'config file path', 'wacht.config.json')
205
+ .option('--deployment <id>', 'deployment id override; defaults to config or active deployment')
206
+ .action(async (options) => {
207
+ await configDiff(context(program), options);
208
+ });
209
+ config
210
+ .command('apply')
211
+ .description('apply a config file to the active deployment')
212
+ .option('--file <path>', 'config file path', 'wacht.config.json')
213
+ .option('--deployment <id>', 'deployment id override; defaults to config or active deployment')
214
+ .option('--dry-run', 'preview changes without applying them')
215
+ .option('--yes', 'required to apply changes')
216
+ .option('--production', 'allow applying to a production deployment')
217
+ .option('--confirm <deployment_id>', 'required with --production; must match the deployment id')
218
+ .action(async (options) => {
219
+ await configApply(context(program), options);
220
+ });
221
+ const api = program
222
+ .command('api')
223
+ .description('call the Wacht Machine API')
224
+ .argument('[method]', 'HTTP method')
225
+ .argument('[path]', 'API path, for example /projects')
226
+ .option('--body <json>', 'JSON request body')
227
+ .option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
228
+ .option('--form <key=value>', 'multipart form field; value @path is treated as a file; repeatable', collect, [])
229
+ .option('--file <key=path>', 'multipart file field; key=path or key=@path; repeatable', collect, [])
230
+ .option('--header <key=value>', 'request header; repeatable', collect, [])
231
+ .action(async (method, path, options) => {
232
+ await apiCommand(method, path, options, context(program));
233
+ });
234
+ api
235
+ .command('ls')
236
+ .description('list operations from the Wacht Platform OpenAPI schema')
237
+ .option('--tag <tag>', 'filter by OpenAPI tag')
238
+ .option('--search <text>', 'filter by operation id, summary, path, or tag')
239
+ .option('--refresh', 'refresh the cached OpenAPI schema first')
240
+ .action(async (options) => {
241
+ await openApiList(context(program), options);
242
+ });
243
+ api
244
+ .command('describe')
245
+ .description('describe an OpenAPI operation by operation id or METHOD path')
246
+ .argument('<operationOrMethod>', 'operation id, or HTTP method when path is also passed')
247
+ .argument('[path]', 'OpenAPI path, for example /users')
248
+ .option('--refresh', 'refresh the cached OpenAPI schema first')
249
+ .action(async (operationOrMethod, path, options) => {
250
+ await openApiDescribe(context(program), operationOrMethod, path, options);
251
+ });
252
+ api
253
+ .command('call')
254
+ .description('call an OpenAPI operation by operation id using the active deployment')
255
+ .argument('<operation>', 'OpenAPI operation id')
256
+ .option('--deployment <id>', 'deployment id override; defaults to active deployment')
257
+ .option('--param <key=value>', 'path or query parameter; repeatable', collect, [])
258
+ .option('--body <json>', 'JSON request body')
259
+ .option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
260
+ .option('--form <key=value>', 'multipart form field; value @path is treated as a file; repeatable', collect, [])
261
+ .option('--file <key=path>', 'multipart file field; key=path or key=@path; repeatable', collect, [])
262
+ .option('--header <key=value>', 'request header; repeatable', collect, [])
263
+ .option('--refresh', 'refresh the cached OpenAPI schema first')
264
+ .action(async (operation, options) => {
265
+ await openApiCall(context(program), operation, options);
266
+ });
267
+ const schema = api.command('schema').description('manage cached OpenAPI schema');
268
+ schema
269
+ .command('refresh')
270
+ .description('download and cache the latest Wacht Platform OpenAPI schema')
271
+ .action(async () => {
272
+ await openApiRefresh(context(program));
273
+ });
274
+ // ─── Resources ────────────────────────────────────────────────
275
+ const users = program.command('users').description('list, get, or create users in the active deployment');
276
+ users
277
+ .command('list')
278
+ .description('list users in the active deployment')
279
+ .option('--deployment <id>', 'deployment id override')
280
+ .option('--limit <n>', 'page size')
281
+ .option('--offset <n>', 'page offset')
282
+ .option('--search <q>', 'search query')
283
+ .action(async (options) => {
284
+ await listUsers(context(program), options);
285
+ });
286
+ users
287
+ .command('get')
288
+ .description('get a user by id')
289
+ .argument('<userId>', 'user id')
290
+ .option('--deployment <id>', 'deployment id override')
291
+ .action(async (userId, options) => {
292
+ await getUser(context(program), userId, options);
293
+ });
294
+ users
295
+ .command('create')
296
+ .description('create a user via the Machine API')
297
+ .option('--deployment <id>', 'deployment id override')
298
+ .option('--body <json>', 'JSON request body')
299
+ .option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
300
+ .option('--form <key=value>', 'multipart form field; repeatable', collect, [])
301
+ .option('--file <key=path>', 'multipart file field; repeatable', collect, [])
302
+ .option('--header <key=value>', 'request header; repeatable', collect, [])
303
+ .action(async (options) => {
304
+ await createUser(context(program), options);
305
+ });
306
+ const orgs = program.command('orgs').alias('organizations').description('list, get, or create organizations');
307
+ orgs
308
+ .command('list')
309
+ .description('list organizations in the active deployment')
310
+ .option('--deployment <id>', 'deployment id override')
311
+ .option('--limit <n>', 'page size')
312
+ .option('--offset <n>', 'page offset')
313
+ .option('--search <q>', 'search query')
314
+ .action(async (options) => {
315
+ await listOrgs(context(program), options);
316
+ });
317
+ orgs
318
+ .command('get')
319
+ .description('get an organization by id')
320
+ .argument('<orgId>', 'organization id')
321
+ .option('--deployment <id>', 'deployment id override')
322
+ .action(async (orgId, options) => {
323
+ await getOrg(context(program), orgId, options);
324
+ });
325
+ orgs
326
+ .command('create')
327
+ .description('create an organization')
328
+ .option('--deployment <id>', 'deployment id override')
329
+ .option('--body <json>', 'JSON request body')
330
+ .option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
331
+ .option('--form <key=value>', 'multipart form field; repeatable', collect, [])
332
+ .option('--file <key=path>', 'multipart file field; repeatable', collect, [])
333
+ .option('--header <key=value>', 'request header; repeatable', collect, [])
334
+ .action(async (options) => {
335
+ await createOrg(context(program), options);
336
+ });
337
+ const workspaces = program.command('workspaces').description('list, get, or create workspaces');
338
+ workspaces
339
+ .command('list')
340
+ .description('list workspaces in the active deployment, optionally filtered by org')
341
+ .option('--deployment <id>', 'deployment id override')
342
+ .option('--org <id>', 'list workspaces under a specific organization')
343
+ .option('--limit <n>', 'page size')
344
+ .option('--offset <n>', 'page offset')
345
+ .option('--search <q>', 'search query')
346
+ .action(async (options) => {
347
+ await listWorkspaces(context(program), options);
348
+ });
349
+ workspaces
350
+ .command('get')
351
+ .description('get a workspace by id')
352
+ .argument('<workspaceId>', 'workspace id')
353
+ .option('--deployment <id>', 'deployment id override')
354
+ .action(async (workspaceId, options) => {
355
+ await getWorkspace(context(program), workspaceId, options);
356
+ });
357
+ workspaces
358
+ .command('create')
359
+ .description('create a workspace under an organization')
360
+ .option('--deployment <id>', 'deployment id override')
361
+ .requiredOption('--org <id>', 'organization id (workspaces are scoped to an org)')
362
+ .option('--body <json>', 'JSON request body')
363
+ .option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
364
+ .option('--form <key=value>', 'multipart form field; repeatable', collect, [])
365
+ .option('--file <key=path>', 'multipart file field; repeatable', collect, [])
366
+ .option('--header <key=value>', 'request header; repeatable', collect, [])
367
+ .action(async (options) => {
368
+ await createWorkspace(context(program), options);
369
+ });
370
+ // ─── Shell completion ─────────────────────────────────────────
371
+ program
372
+ .command('completion')
373
+ .description('print shell completion script (bash, zsh, fish, powershell)')
374
+ .argument('[shell]', 'bash, zsh, fish, or powershell')
375
+ .action((shellArg) => {
376
+ const shell = (shellArg ?? '').toLowerCase();
377
+ if (!['bash', 'zsh', 'fish', 'powershell'].includes(shell)) {
378
+ console.error('Pass a shell: wacht completion bash | zsh | fish | powershell\n' +
379
+ ' bash: wacht completion bash > /etc/bash_completion.d/wacht\n' +
380
+ ' zsh: wacht completion zsh > "${fpath[1]}/_wacht"\n' +
381
+ ' fish: wacht completion fish > ~/.config/fish/completions/wacht.fish\n' +
382
+ ' powershell: wacht completion powershell | Out-String | Invoke-Expression');
383
+ process.exitCode = 1;
384
+ return;
385
+ }
386
+ process.stdout.write(completionScript(shell));
387
+ });
388
+ await program.parseAsync(args, { from: 'user' });
389
+ }
@@ -0,0 +1,132 @@
1
+ // Static shell completion scripts for the `wacht` CLI.
2
+ // Generated dynamically from the known top-level commands so adding a command
3
+ // only requires updating the COMMANDS list below.
4
+ const COMMANDS = [
5
+ 'init',
6
+ 'login',
7
+ 'logout',
8
+ 'auth',
9
+ 'projects',
10
+ 'deployments',
11
+ 'users',
12
+ 'orgs',
13
+ 'workspaces',
14
+ 'skills',
15
+ 'mcp',
16
+ 'config',
17
+ 'api',
18
+ 'completion',
19
+ ];
20
+ const SUBCOMMANDS = {
21
+ auth: ['status'],
22
+ projects: ['list', 'create'],
23
+ deployments: ['current', 'select', 'clear', 'create'],
24
+ users: ['list', 'get', 'create'],
25
+ orgs: ['list', 'get', 'create'],
26
+ workspaces: ['list', 'get', 'create'],
27
+ skills: ['install'],
28
+ mcp: ['config'],
29
+ config: ['pull', 'schema', 'template', 'diff', 'apply'],
30
+ api: ['ls', 'describe', 'call', 'schema'],
31
+ completion: ['bash', 'zsh', 'fish', 'powershell'],
32
+ };
33
+ function bashScript() {
34
+ const tops = COMMANDS.join(' ');
35
+ const subs = Object.entries(SUBCOMMANDS)
36
+ .map(([cmd, list]) => ` ${cmd}) COMPREPLY=($(compgen -W "${list.join(' ')}" -- "$cur")); return 0;;`)
37
+ .join('\n');
38
+ return `# bash completion for wacht
39
+ _wacht_complete() {
40
+ local cur prev words cword
41
+ _init_completion -n = || return
42
+
43
+ if [[ $cword -eq 1 ]]; then
44
+ COMPREPLY=($(compgen -W "${tops}" -- "$cur"))
45
+ return 0
46
+ fi
47
+
48
+ if [[ $cword -eq 2 ]]; then
49
+ case "\${words[1]}" in
50
+ ${subs}
51
+ esac
52
+ fi
53
+
54
+ COMPREPLY=()
55
+ }
56
+ complete -F _wacht_complete wacht
57
+ `;
58
+ }
59
+ function zshScript() {
60
+ const cases = COMMANDS.map((cmd) => {
61
+ const subs = SUBCOMMANDS[cmd];
62
+ if (!subs)
63
+ return ` ${cmd}) ;;`;
64
+ const subList = subs.map((s) => `'${s}'`).join(' ');
65
+ return ` ${cmd})\n _values 'subcommand' ${subList}\n ;;`;
66
+ }).join('\n');
67
+ return `#compdef wacht
68
+ # zsh completion for wacht
69
+ _wacht() {
70
+ local context state line
71
+ _arguments -C \\
72
+ '1: :->command' \\
73
+ '2: :->subcommand' \\
74
+ '*::arg:->args'
75
+
76
+ case $state in
77
+ command)
78
+ _values 'wacht command' ${COMMANDS.map((c) => `'${c}'`).join(' ')}
79
+ ;;
80
+ subcommand)
81
+ case $words[2] in
82
+ ${cases}
83
+ esac
84
+ ;;
85
+ esac
86
+ }
87
+ _wacht "$@"
88
+ `;
89
+ }
90
+ function fishScript() {
91
+ const lines = [`# fish completion for wacht`];
92
+ for (const cmd of COMMANDS) {
93
+ lines.push(`complete -c wacht -n '__fish_use_subcommand' -a '${cmd}'`);
94
+ }
95
+ for (const [cmd, subs] of Object.entries(SUBCOMMANDS)) {
96
+ for (const sub of subs) {
97
+ lines.push(`complete -c wacht -n '__fish_seen_subcommand_from ${cmd}' -a '${sub}'`);
98
+ }
99
+ }
100
+ return `${lines.join('\n')}\n`;
101
+ }
102
+ function powershellScript() {
103
+ const tops = COMMANDS.join("','");
104
+ const cases = Object.entries(SUBCOMMANDS)
105
+ .map(([cmd, subs]) => ` '${cmd}' { @('${subs.join("','")}') }`)
106
+ .join('\n');
107
+ return `# PowerShell completion for wacht
108
+ Register-ArgumentCompleter -Native -CommandName wacht -ScriptBlock {
109
+ param($wordToComplete, $commandAst, $cursorPosition)
110
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.Value }
111
+ if ($tokens.Length -le 2) {
112
+ @('${tops}') | Where-Object { $_ -like "$wordToComplete*" } |
113
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
114
+ return
115
+ }
116
+ $sub = switch ($tokens[1]) {
117
+ ${cases}
118
+ default { @() }
119
+ }
120
+ $sub | Where-Object { $_ -like "$wordToComplete*" } |
121
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
122
+ }
123
+ `;
124
+ }
125
+ export function completionScript(shell) {
126
+ switch (shell) {
127
+ case 'bash': return bashScript();
128
+ case 'zsh': return zshScript();
129
+ case 'fish': return fishScript();
130
+ case 'powershell': return powershellScript();
131
+ }
132
+ }