xcode-cli 1.0.5

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,138 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import {
4
+ DEFAULT_SERVICE_PORT,
5
+ installService,
6
+ uninstallService,
7
+ startService,
8
+ stopService,
9
+ restartService,
10
+ printServiceStatus,
11
+ tailLogs,
12
+ killPortOccupant,
13
+ } from './xcode-service.ts';
14
+ import { startMcpBridge } from './xcode-mcp.ts';
15
+ import { installSkill, uninstallSkill } from './xcode-skill.ts';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ const program = new Command();
20
+ program
21
+ .name('xcode-cli-ctl')
22
+ .description('Setup and manage the xcode-cli MCP bridge service.');
23
+
24
+ program
25
+ .command('install')
26
+ .description('Install and start bridge as a macOS launchd service')
27
+ .option('--port <port>', 'Bridge port', String(DEFAULT_SERVICE_PORT))
28
+ .action(async (options: { port: string }) => {
29
+ await installService({ port: Number(options.port) });
30
+ });
31
+
32
+ program
33
+ .command('uninstall')
34
+ .description('Stop and remove bridge launchd service')
35
+ .action(async () => {
36
+ await uninstallService();
37
+ });
38
+
39
+ program
40
+ .command('start')
41
+ .description('Start the installed bridge service')
42
+ .action(async () => {
43
+ await startService();
44
+ });
45
+
46
+ program
47
+ .command('stop')
48
+ .description('Stop the bridge service')
49
+ .action(async () => {
50
+ await stopService();
51
+ });
52
+
53
+ program
54
+ .command('restart')
55
+ .description('Restart the bridge service')
56
+ .action(async () => {
57
+ await restartService();
58
+ });
59
+
60
+ program
61
+ .command('status')
62
+ .description('Show bridge service status and health')
63
+ .option('--port <port>', 'Bridge port for health check', String(DEFAULT_SERVICE_PORT))
64
+ .action(async (options: { port: string }) => {
65
+ await printServiceStatus(Number(options.port));
66
+ });
67
+
68
+ program
69
+ .command('logs')
70
+ .description('Show bridge service logs')
71
+ .option('-n, --lines <n>', 'Number of lines', '50')
72
+ .option('-f, --follow', 'Follow log output')
73
+ .action((options: { lines: string; follow?: boolean }) => {
74
+ tailLogs({ lines: Number(options.lines), follow: options.follow });
75
+ });
76
+
77
+ program
78
+ .command('run')
79
+ .description('Run the MCP bridge in the foreground (for debugging or launchd)')
80
+ .option('--host <host>', 'Bind host', '127.0.0.1')
81
+ .option('--port <port>', 'Bind port', String(DEFAULT_SERVICE_PORT))
82
+ .option('--path <path>', 'MCP endpoint path', '/mcp')
83
+ .action(async (options: { host: string; port: string; path: string }) => {
84
+ const port = Number(options.port);
85
+ await killPortOccupant(port);
86
+ await startMcpBridge({
87
+ host: options.host,
88
+ port,
89
+ path: options.path,
90
+ });
91
+ });
92
+
93
+ // ── skill ────────────────────────────────────────────────────────────
94
+
95
+ const skill = program.command('skill').description('Manage xcode-cli skill for agents');
96
+
97
+ skill
98
+ .command('install')
99
+ .description('Install xcode-cli skill (default: both Claude and Codex)')
100
+ .option('--claude', 'Install to ~/.claude/skills only')
101
+ .option('--codex', 'Install to ~/.codex/skills only')
102
+ .option('--skill-root-dir <path>', 'Install to a custom skills directory')
103
+ .action(async (options: { claude?: boolean; codex?: boolean; skillRootDir?: string }) => {
104
+ const dirs = resolveSkillDirs(options);
105
+ for (const dir of dirs) {
106
+ await installSkill(dir);
107
+ }
108
+ });
109
+
110
+ skill
111
+ .command('uninstall')
112
+ .description('Uninstall xcode-cli skill (default: both Claude and Codex)')
113
+ .option('--claude', 'Uninstall from ~/.claude/skills only')
114
+ .option('--codex', 'Uninstall from ~/.codex/skills only')
115
+ .option('--skill-root-dir <path>', 'Uninstall from a custom skills directory')
116
+ .action(async (options: { claude?: boolean; codex?: boolean; skillRootDir?: string }) => {
117
+ const dirs = resolveSkillDirs(options);
118
+ for (const dir of dirs) {
119
+ await uninstallSkill(dir);
120
+ }
121
+ });
122
+
123
+ program.parseAsync(process.argv).catch((error) => {
124
+ console.error(error instanceof Error ? error.message : String(error));
125
+ process.exit(1);
126
+ });
127
+
128
+ function resolveSkillDirs(options: { claude?: boolean; codex?: boolean; skillRootDir?: string }): string[] {
129
+ if (options.skillRootDir) {
130
+ return [options.skillRootDir];
131
+ }
132
+ const home = os.homedir();
133
+ const claude = path.join(home, '.claude', 'skills');
134
+ const codex = path.join(home, '.codex', 'skills');
135
+ if (options.claude) return [claude];
136
+ if (options.codex) return [codex];
137
+ return [claude, codex];
138
+ }
@@ -0,0 +1,165 @@
1
+ import type { IssueEntry } from './xcode-types.ts';
2
+
3
+ export function extractIssues(value: unknown): IssueEntry[] {
4
+ const issues: IssueEntry[] = [];
5
+ const queue: unknown[] = [value];
6
+
7
+ while (queue.length > 0) {
8
+ const current = queue.shift();
9
+ if (!current) {
10
+ continue;
11
+ }
12
+ if (Array.isArray(current)) {
13
+ queue.push(...current);
14
+ continue;
15
+ }
16
+ if (typeof current !== 'object') {
17
+ continue;
18
+ }
19
+
20
+ const record = current as Record<string, unknown>;
21
+ const message = firstString(record, ['message', 'diagnostic', 'title', 'text']);
22
+ const pathValue = firstString(record, ['path', 'filePath', 'file', 'sourceFilePath']);
23
+ const severity = firstString(record, ['severity', 'level', 'kind']);
24
+ const line = firstNumber(record, ['line', 'lineNumber', 'startLine']);
25
+ const column = firstNumber(record, ['column', 'col', 'startColumn']);
26
+
27
+ if (message && (pathValue || severity || line || column)) {
28
+ issues.push({
29
+ path: pathValue,
30
+ message,
31
+ severity: severity?.toLowerCase(),
32
+ line,
33
+ column,
34
+ });
35
+ }
36
+
37
+ for (const nested of Object.values(record)) {
38
+ if (nested && typeof nested === 'object') {
39
+ queue.push(nested);
40
+ }
41
+ if (Array.isArray(nested)) {
42
+ queue.push(...nested);
43
+ }
44
+ }
45
+ }
46
+
47
+ return dedupeIssues(issues);
48
+ }
49
+
50
+ export function summarizeIssues(issues: IssueEntry[]): Record<string, number> {
51
+ const summary = { total: issues.length, error: 0, warning: 0, remark: 0, unknown: 0 };
52
+ for (const issue of issues) {
53
+ const severity = issue.severity ?? 'unknown';
54
+ if (severity.includes('error')) {
55
+ summary.error += 1;
56
+ } else if (severity.includes('warning')) {
57
+ summary.warning += 1;
58
+ } else if (severity.includes('remark') || severity.includes('note')) {
59
+ summary.remark += 1;
60
+ } else {
61
+ summary.unknown += 1;
62
+ }
63
+ }
64
+ return summary;
65
+ }
66
+
67
+ export function summarizeBuildLog(value: unknown): Record<string, unknown> {
68
+ const text = stringifyValue(value).toLowerCase();
69
+ const inProgress =
70
+ text.includes('in progress') || text.includes('"inprogress":true') || text.includes('building');
71
+ const hasErrors = text.includes(' error') || text.includes('"error"');
72
+ const hasWarnings = text.includes(' warning') || text.includes('"warning"');
73
+ return { inProgress, hasErrors, hasWarnings };
74
+ }
75
+
76
+ export function summarizeTestList(value: unknown): Record<string, unknown> {
77
+ const tests = collectStringsByKeys(value, ['testIdentifier', 'identifier', 'name']);
78
+ const uniqueCount = new Set(tests).size;
79
+ return {
80
+ discovered: uniqueCount,
81
+ hasTests: uniqueCount > 0,
82
+ };
83
+ }
84
+
85
+ function dedupeIssues(issues: IssueEntry[]): IssueEntry[] {
86
+ const seen = new Set<string>();
87
+ const result: IssueEntry[] = [];
88
+ for (const issue of issues) {
89
+ const key = `${issue.path ?? '<unknown>'}|${issue.severity ?? 'unknown'}|${issue.line ?? 0}|${issue.column ?? 0}|${issue.message}`;
90
+ if (seen.has(key)) {
91
+ continue;
92
+ }
93
+ seen.add(key);
94
+ result.push(issue);
95
+ }
96
+ return result;
97
+ }
98
+
99
+ function firstString(record: Record<string, unknown>, keys: string[]): string | undefined {
100
+ for (const key of keys) {
101
+ const value = record[key];
102
+ if (typeof value === 'string' && value.trim()) {
103
+ return value;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ function firstNumber(record: Record<string, unknown>, keys: string[]): number | undefined {
110
+ for (const key of keys) {
111
+ const value = record[key];
112
+ if (typeof value === 'number' && Number.isFinite(value)) {
113
+ return value;
114
+ }
115
+ if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
116
+ return Number(value);
117
+ }
118
+ }
119
+ return undefined;
120
+ }
121
+
122
+ function collectStringsByKeys(value: unknown, keys: string[]): string[] {
123
+ const found: string[] = [];
124
+ const queue: unknown[] = [value];
125
+ while (queue.length > 0) {
126
+ const current = queue.shift();
127
+ if (!current) {
128
+ continue;
129
+ }
130
+ if (Array.isArray(current)) {
131
+ queue.push(...current);
132
+ continue;
133
+ }
134
+ if (typeof current !== 'object') {
135
+ continue;
136
+ }
137
+ const record = current as Record<string, unknown>;
138
+ for (const key of keys) {
139
+ const candidate = record[key];
140
+ if (typeof candidate === 'string' && candidate.trim()) {
141
+ found.push(candidate);
142
+ }
143
+ }
144
+ for (const nested of Object.values(record)) {
145
+ if (nested && typeof nested === 'object') {
146
+ queue.push(nested);
147
+ }
148
+ if (Array.isArray(nested)) {
149
+ queue.push(...nested);
150
+ }
151
+ }
152
+ }
153
+ return found;
154
+ }
155
+
156
+ function stringifyValue(value: unknown): string {
157
+ if (typeof value === 'string') {
158
+ return value;
159
+ }
160
+ try {
161
+ return JSON.stringify(value);
162
+ } catch {
163
+ return String(value);
164
+ }
165
+ }