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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/xcode-cli +19 -0
- package/bin/xcode-cli-ctl +19 -0
- package/package.json +41 -0
- package/skills/xcode-cli/SKILL.md +138 -0
- package/src/mcpbridge.ts +1624 -0
- package/src/xcode-ctl.ts +138 -0
- package/src/xcode-issues.ts +165 -0
- package/src/xcode-mcp.ts +483 -0
- package/src/xcode-output.ts +431 -0
- package/src/xcode-preview.ts +55 -0
- package/src/xcode-service.ts +278 -0
- package/src/xcode-skill.ts +52 -0
- package/src/xcode-test.ts +115 -0
- package/src/xcode-tree.ts +59 -0
- package/src/xcode-types.ts +28 -0
- package/src/xcode.ts +785 -0
package/src/xcode-ctl.ts
ADDED
|
@@ -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
|
+
}
|