xcode-cli 1.0.5 → 1.0.7

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.
@@ -23,8 +23,18 @@ export async function installSkill(rootDir: string): Promise<void> {
23
23
  const targetFile = path.join(targetDir, SKILL_FILENAME);
24
24
 
25
25
  await fs.mkdir(targetDir, { recursive: true });
26
- await fs.copyFile(source, targetFile);
27
- console.log(`Installed skill: ${targetFile}`);
26
+
27
+ // Remove existing file or symlink so we can (re)create the symlink cleanly.
28
+ try {
29
+ await fs.unlink(targetFile);
30
+ } catch {
31
+ // ignore — file didn't exist yet
32
+ }
33
+
34
+ // Use a symlink so the skill automatically reflects package upgrades
35
+ // without requiring the user to re-run `skill install`.
36
+ await fs.symlink(source, targetFile);
37
+ console.log(`Installed skill: ${targetFile} -> ${source}`);
28
38
  }
29
39
 
30
40
  export async function uninstallSkill(rootDir: string): Promise<void> {
@@ -32,7 +42,8 @@ export async function uninstallSkill(rootDir: string): Promise<void> {
32
42
  const targetFile = path.join(targetDir, SKILL_FILENAME);
33
43
 
34
44
  try {
35
- await fs.access(targetFile);
45
+ // lstat works for both symlinks and regular files
46
+ await fs.lstat(targetFile);
36
47
  } catch {
37
48
  console.log(`Skill not found at ${targetFile}`);
38
49
  return;
package/src/xcode.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { createRequire } from 'node:module';
2
5
  import { Command } from 'commander';
3
6
  import { createRuntime, createServerProxy, describeConnectionIssue } from 'mcporter';
4
7
  import type { CallResult } from 'mcporter';
8
+
9
+ const execFileAsync = promisify(execFile);
5
10
  import { printResult, unwrapResult } from './xcode-output.ts';
6
11
  import { copyPreviewToOutput, findPreviewPath } from './xcode-preview.ts';
7
12
  import { parseTestSpecifier, type ParsedTestSpecifier } from './xcode-test.ts';
8
13
  import { renderLsTree } from './xcode-tree.ts';
9
14
  import type { CommonOpts, ClientContext } from './xcode-types.ts';
10
15
 
16
+ const { version } = createRequire(import.meta.url)('../package.json');
11
17
  const SERVER_NAME = 'xcode-tools';
12
18
  const DEFAULT_PORT = '48321';
13
19
  const DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/mcp`;
@@ -16,6 +22,7 @@ const program = new Command();
16
22
  program
17
23
  .name('xcode-cli')
18
24
  .description('Friendly Xcode MCP CLI for browsing, editing, building, and testing projects.')
25
+ .version(version, '-v, --version')
19
26
  .option('--url <url>', `MCP endpoint (default: ${DEFAULT_URL})`)
20
27
  .option('--tab <tabIdentifier>', 'Default tab identifier for commands that need it')
21
28
  .option('-t, --timeout <ms>', 'Call timeout in milliseconds', '60000')
@@ -464,8 +471,24 @@ program
464
471
  });
465
472
 
466
473
  program
467
- .command('run <toolName>')
468
- .description('Run any MCP tool directly with JSON args')
474
+ .command('run')
475
+ .description('Build and run the active scheme (like Cmd+R in Xcode)')
476
+ .action(async () => {
477
+ await triggerXcodeKeystroke('r', 'command down');
478
+ console.log('Run triggered');
479
+ });
480
+
481
+ program
482
+ .command('run-without-build')
483
+ .description('Run without building the active scheme (like Ctrl+Cmd+R in Xcode)')
484
+ .action(async () => {
485
+ await triggerXcodeKeystroke('r', 'command down, control down');
486
+ console.log('Run Without Build triggered');
487
+ });
488
+
489
+ program
490
+ .command('call <toolName>')
491
+ .description('Call any MCP tool directly with JSON args')
469
492
  .requiredOption('--args <json>', 'JSON object with tool arguments')
470
493
  .action(async (toolName: string, options: { args: string }) => {
471
494
  await withClient(async (ctx) => {
@@ -479,6 +502,8 @@ applyCommandOrder(program, [
479
502
  'status',
480
503
  'build',
481
504
  'build-log',
505
+ 'run',
506
+ 'run-without-build',
482
507
  'test',
483
508
  'issues',
484
509
  'file-issues',
@@ -496,7 +521,7 @@ applyCommandOrder(program, [
496
521
  'snippet',
497
522
  'doc',
498
523
  'tools',
499
- 'run',
524
+ 'call',
500
525
  ]);
501
526
 
502
527
  program.parseAsync(process.argv).catch((error) => {
@@ -513,6 +538,16 @@ program.parseAsync(process.argv).catch((error) => {
513
538
  process.exit(1);
514
539
  });
515
540
 
541
+ async function triggerXcodeKeystroke(key: string, modifiers: string): Promise<void> {
542
+ const script = `tell application "Xcode" to activate\ntell application "System Events"\n tell process "Xcode"\n keystroke "${key}" using {${modifiers}}\n end tell\nend tell`;
543
+ try {
544
+ await execFileAsync('osascript', ['-e', script]);
545
+ } catch (error) {
546
+ const message = error instanceof Error ? error.message : String(error);
547
+ throw new Error(`Failed to trigger Xcode action: ${message}\nEnsure Xcode is open and Accessibility access is granted to Terminal/iTerm.`);
548
+ }
549
+ }
550
+
516
551
  async function withClient(handler: (ctx: ClientContext) => Promise<void>) {
517
552
  const root = program.opts<CommonOpts>();
518
553
  const endpoint = root.url ?? process.env.XCODE_CLI_URL ?? DEFAULT_URL;