@axintai/compiler 0.2.1
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 +190 -0
- package/README.md +319 -0
- package/dist/cli/index.js +868 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +233 -0
- package/dist/core/index.js +776 -0
- package/dist/core/index.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.js +1320 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/sdk/index.d.ts +179 -0
- package/dist/sdk/index.js +41 -0
- package/dist/sdk/index.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { resolve, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/core/compiler.ts
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
|
|
12
|
+
// src/core/parser.ts
|
|
13
|
+
import ts from "typescript";
|
|
14
|
+
|
|
15
|
+
// src/core/types.ts
|
|
16
|
+
var PARAM_TYPES = /* @__PURE__ */ new Set([
|
|
17
|
+
"string",
|
|
18
|
+
"int",
|
|
19
|
+
"double",
|
|
20
|
+
"float",
|
|
21
|
+
"boolean",
|
|
22
|
+
"date",
|
|
23
|
+
"duration",
|
|
24
|
+
"url"
|
|
25
|
+
]);
|
|
26
|
+
var LEGACY_PARAM_ALIASES = {
|
|
27
|
+
number: "int"
|
|
28
|
+
};
|
|
29
|
+
var SWIFT_TYPE_MAP = {
|
|
30
|
+
string: "String",
|
|
31
|
+
int: "Int",
|
|
32
|
+
double: "Double",
|
|
33
|
+
float: "Float",
|
|
34
|
+
boolean: "Bool",
|
|
35
|
+
date: "Date",
|
|
36
|
+
duration: "Measurement<UnitDuration>",
|
|
37
|
+
url: "URL"
|
|
38
|
+
};
|
|
39
|
+
function irTypeToSwift(type) {
|
|
40
|
+
switch (type.kind) {
|
|
41
|
+
case "primitive":
|
|
42
|
+
return SWIFT_TYPE_MAP[type.value];
|
|
43
|
+
case "array":
|
|
44
|
+
return `[${irTypeToSwift(type.elementType)}]`;
|
|
45
|
+
case "optional":
|
|
46
|
+
return `${irTypeToSwift(type.innerType)}?`;
|
|
47
|
+
case "entity":
|
|
48
|
+
return type.entityName;
|
|
49
|
+
case "enum":
|
|
50
|
+
return type.name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/core/parser.ts
|
|
55
|
+
function parseIntentSource(source, filePath = "<stdin>") {
|
|
56
|
+
const sourceFile = ts.createSourceFile(
|
|
57
|
+
filePath,
|
|
58
|
+
source,
|
|
59
|
+
ts.ScriptTarget.Latest,
|
|
60
|
+
true,
|
|
61
|
+
// setParentNodes
|
|
62
|
+
ts.ScriptKind.TS
|
|
63
|
+
);
|
|
64
|
+
const defineIntentCall = findDefineIntentCall(sourceFile);
|
|
65
|
+
if (!defineIntentCall) {
|
|
66
|
+
throw new ParserError(
|
|
67
|
+
"AX001",
|
|
68
|
+
`No defineIntent() call found in ${filePath}`,
|
|
69
|
+
filePath,
|
|
70
|
+
void 0,
|
|
71
|
+
"Ensure your file contains a `defineIntent({ ... })` call."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const arg = defineIntentCall.arguments[0];
|
|
75
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
76
|
+
throw new ParserError(
|
|
77
|
+
"AX001",
|
|
78
|
+
"defineIntent() must be called with an object literal",
|
|
79
|
+
filePath,
|
|
80
|
+
posOf(sourceFile, defineIntentCall),
|
|
81
|
+
"Pass an object: defineIntent({ name, title, description, params, perform })"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const props = propertyMap(arg);
|
|
85
|
+
const name = readStringLiteral(props.get("name"));
|
|
86
|
+
const title = readStringLiteral(props.get("title"));
|
|
87
|
+
const description = readStringLiteral(props.get("description"));
|
|
88
|
+
const domain = readStringLiteral(props.get("domain"));
|
|
89
|
+
const category = readStringLiteral(props.get("category"));
|
|
90
|
+
const isDiscoverable = readBooleanLiteral(props.get("isDiscoverable"));
|
|
91
|
+
if (!name) {
|
|
92
|
+
throw new ParserError(
|
|
93
|
+
"AX002",
|
|
94
|
+
"Missing required field: name",
|
|
95
|
+
filePath,
|
|
96
|
+
posOf(sourceFile, arg),
|
|
97
|
+
'Add a name field: name: "MyIntent"'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!title) {
|
|
101
|
+
throw new ParserError(
|
|
102
|
+
"AX003",
|
|
103
|
+
"Missing required field: title",
|
|
104
|
+
filePath,
|
|
105
|
+
posOf(sourceFile, arg),
|
|
106
|
+
'Add a title field: title: "My Intent Title"'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!description) {
|
|
110
|
+
throw new ParserError(
|
|
111
|
+
"AX004",
|
|
112
|
+
"Missing required field: description",
|
|
113
|
+
filePath,
|
|
114
|
+
posOf(sourceFile, arg),
|
|
115
|
+
'Add a description field: description: "What this intent does"'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const paramsNode = props.get("params");
|
|
119
|
+
const parameters = paramsNode ? extractParameters(paramsNode, filePath, sourceFile) : [];
|
|
120
|
+
const performNode = props.get("perform");
|
|
121
|
+
const returnType = inferReturnType(performNode);
|
|
122
|
+
const entitlementsNode = props.get("entitlements");
|
|
123
|
+
const entitlements = readStringArray(entitlementsNode);
|
|
124
|
+
const infoPlistNode = props.get("infoPlistKeys");
|
|
125
|
+
const infoPlistKeys = readStringRecord(infoPlistNode);
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
title,
|
|
129
|
+
description,
|
|
130
|
+
domain: domain || void 0,
|
|
131
|
+
category: category || void 0,
|
|
132
|
+
parameters,
|
|
133
|
+
returnType,
|
|
134
|
+
sourceFile: filePath,
|
|
135
|
+
entitlements: entitlements.length > 0 ? entitlements : void 0,
|
|
136
|
+
infoPlistKeys: Object.keys(infoPlistKeys).length > 0 ? infoPlistKeys : void 0,
|
|
137
|
+
isDiscoverable: isDiscoverable ?? void 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function findDefineIntentCall(node) {
|
|
141
|
+
let found;
|
|
142
|
+
const visit = (n) => {
|
|
143
|
+
if (found) return;
|
|
144
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "defineIntent") {
|
|
145
|
+
found = n;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
ts.forEachChild(n, visit);
|
|
149
|
+
};
|
|
150
|
+
visit(node);
|
|
151
|
+
return found;
|
|
152
|
+
}
|
|
153
|
+
function propertyMap(obj) {
|
|
154
|
+
const map = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const prop of obj.properties) {
|
|
156
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
157
|
+
const key = propertyKeyName(prop.name);
|
|
158
|
+
if (key) map.set(key, prop.initializer);
|
|
159
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
160
|
+
map.set(prop.name.text, prop.name);
|
|
161
|
+
} else if (ts.isMethodDeclaration(prop)) {
|
|
162
|
+
const key = propertyKeyName(prop.name);
|
|
163
|
+
if (key) map.set(key, prop);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return map;
|
|
167
|
+
}
|
|
168
|
+
function propertyKeyName(name) {
|
|
169
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
170
|
+
if (ts.isStringLiteral(name)) return name.text;
|
|
171
|
+
if (ts.isNumericLiteral(name)) return name.text;
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
function readStringLiteral(node) {
|
|
175
|
+
if (!node) return null;
|
|
176
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
177
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function readBooleanLiteral(node) {
|
|
181
|
+
if (!node) return void 0;
|
|
182
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
183
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
function readStringArray(node) {
|
|
187
|
+
if (!node || !ts.isArrayLiteralExpression(node)) return [];
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const el of node.elements) {
|
|
190
|
+
const s = readStringLiteral(el);
|
|
191
|
+
if (s !== null) out.push(s);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function readStringRecord(node) {
|
|
196
|
+
if (!node || !ts.isObjectLiteralExpression(node)) return {};
|
|
197
|
+
const rec = {};
|
|
198
|
+
for (const prop of node.properties) {
|
|
199
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
200
|
+
const key = propertyKeyName(prop.name);
|
|
201
|
+
const val = readStringLiteral(prop.initializer);
|
|
202
|
+
if (key && val !== null) rec[key] = val;
|
|
203
|
+
}
|
|
204
|
+
return rec;
|
|
205
|
+
}
|
|
206
|
+
function extractParameters(node, filePath, sourceFile) {
|
|
207
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
208
|
+
throw new ParserError(
|
|
209
|
+
"AX006",
|
|
210
|
+
"`params` must be an object literal",
|
|
211
|
+
filePath,
|
|
212
|
+
posOf(sourceFile, node),
|
|
213
|
+
"Use params: { name: param.string(...), ... }"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const params = [];
|
|
217
|
+
for (const prop of node.properties) {
|
|
218
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
219
|
+
const paramName = propertyKeyName(prop.name);
|
|
220
|
+
if (!paramName) continue;
|
|
221
|
+
const { typeName, description, configObject } = extractParamCall(
|
|
222
|
+
prop.initializer,
|
|
223
|
+
filePath,
|
|
224
|
+
sourceFile
|
|
225
|
+
);
|
|
226
|
+
const resolvedType = resolveParamType(typeName, filePath, sourceFile, prop);
|
|
227
|
+
const isOptional = configObject ? readBooleanLiteral(configObject.get("required")) === false : false;
|
|
228
|
+
const defaultExpr = configObject?.get("default");
|
|
229
|
+
const defaultValue = defaultExpr ? evaluateLiteral(defaultExpr) : void 0;
|
|
230
|
+
const titleFromConfig = configObject ? readStringLiteral(configObject.get("title")) : null;
|
|
231
|
+
const irType = isOptional ? {
|
|
232
|
+
kind: "optional",
|
|
233
|
+
innerType: { kind: "primitive", value: resolvedType }
|
|
234
|
+
} : { kind: "primitive", value: resolvedType };
|
|
235
|
+
params.push({
|
|
236
|
+
name: paramName,
|
|
237
|
+
type: irType,
|
|
238
|
+
title: titleFromConfig || prettyTitle(paramName),
|
|
239
|
+
description,
|
|
240
|
+
isOptional,
|
|
241
|
+
defaultValue
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return params;
|
|
245
|
+
}
|
|
246
|
+
function extractParamCall(expr, filePath, sourceFile) {
|
|
247
|
+
if (!ts.isCallExpression(expr)) {
|
|
248
|
+
throw new ParserError(
|
|
249
|
+
"AX007",
|
|
250
|
+
"Parameter value must be a call to a param.* helper",
|
|
251
|
+
filePath,
|
|
252
|
+
posOf(sourceFile, expr),
|
|
253
|
+
"Use param.string(...), param.int(...), param.date(...), etc."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (!ts.isPropertyAccessExpression(expr.expression) || !ts.isIdentifier(expr.expression.expression) || expr.expression.expression.text !== "param") {
|
|
257
|
+
throw new ParserError(
|
|
258
|
+
"AX007",
|
|
259
|
+
"Parameter value must be a call to a param.* helper",
|
|
260
|
+
filePath,
|
|
261
|
+
posOf(sourceFile, expr),
|
|
262
|
+
"Use param.string(...), param.int(...), param.date(...), etc."
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const typeName = expr.expression.name.text;
|
|
266
|
+
const descriptionArg = expr.arguments[0];
|
|
267
|
+
const configArg = expr.arguments[1];
|
|
268
|
+
const description = descriptionArg ? readStringLiteral(descriptionArg) : null;
|
|
269
|
+
if (description === null) {
|
|
270
|
+
throw new ParserError(
|
|
271
|
+
"AX008",
|
|
272
|
+
`param.${typeName}() requires a string description as the first argument`,
|
|
273
|
+
filePath,
|
|
274
|
+
posOf(sourceFile, expr),
|
|
275
|
+
`Example: param.${typeName}("Human-readable description")`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const configObject = configArg && ts.isObjectLiteralExpression(configArg) ? propertyMap(configArg) : null;
|
|
279
|
+
return { typeName, description, configObject };
|
|
280
|
+
}
|
|
281
|
+
function resolveParamType(typeName, filePath, sourceFile, node) {
|
|
282
|
+
if (PARAM_TYPES.has(typeName)) {
|
|
283
|
+
return typeName;
|
|
284
|
+
}
|
|
285
|
+
if (typeName in LEGACY_PARAM_ALIASES) {
|
|
286
|
+
return LEGACY_PARAM_ALIASES[typeName];
|
|
287
|
+
}
|
|
288
|
+
throw new ParserError(
|
|
289
|
+
"AX005",
|
|
290
|
+
`Unknown param type: param.${typeName}`,
|
|
291
|
+
filePath,
|
|
292
|
+
posOf(sourceFile, node),
|
|
293
|
+
`Supported types: ${[...PARAM_TYPES].join(", ")}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function evaluateLiteral(node) {
|
|
297
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
298
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
299
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
300
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
301
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
302
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
303
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
|
|
304
|
+
return -Number(node.operand.text);
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
function inferReturnType(performNode) {
|
|
309
|
+
const defaultType = { kind: "primitive", value: "string" };
|
|
310
|
+
if (!performNode) return defaultType;
|
|
311
|
+
if (ts.isMethodDeclaration(performNode)) {
|
|
312
|
+
return inferFromReturnStatements(performNode.body);
|
|
313
|
+
}
|
|
314
|
+
if (ts.isArrowFunction(performNode)) {
|
|
315
|
+
if (performNode.body && ts.isBlock(performNode.body)) {
|
|
316
|
+
return inferFromReturnStatements(performNode.body);
|
|
317
|
+
}
|
|
318
|
+
return inferFromExpression(performNode.body);
|
|
319
|
+
}
|
|
320
|
+
if (ts.isFunctionExpression(performNode)) {
|
|
321
|
+
return inferFromReturnStatements(performNode.body);
|
|
322
|
+
}
|
|
323
|
+
return defaultType;
|
|
324
|
+
}
|
|
325
|
+
function inferFromReturnStatements(block) {
|
|
326
|
+
const defaultType = { kind: "primitive", value: "string" };
|
|
327
|
+
if (!block) return defaultType;
|
|
328
|
+
let inferred;
|
|
329
|
+
const visit = (n) => {
|
|
330
|
+
if (inferred) return;
|
|
331
|
+
if (ts.isReturnStatement(n) && n.expression) {
|
|
332
|
+
inferred = inferFromExpression(n.expression);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
ts.forEachChild(n, visit);
|
|
339
|
+
};
|
|
340
|
+
visit(block);
|
|
341
|
+
return inferred ?? defaultType;
|
|
342
|
+
}
|
|
343
|
+
function inferFromExpression(expr) {
|
|
344
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
345
|
+
return { kind: "primitive", value: "string" };
|
|
346
|
+
}
|
|
347
|
+
if (ts.isNumericLiteral(expr)) {
|
|
348
|
+
return expr.text.includes(".") ? { kind: "primitive", value: "double" } : { kind: "primitive", value: "int" };
|
|
349
|
+
}
|
|
350
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
351
|
+
return { kind: "primitive", value: "boolean" };
|
|
352
|
+
}
|
|
353
|
+
return { kind: "primitive", value: "string" };
|
|
354
|
+
}
|
|
355
|
+
function prettyTitle(name) {
|
|
356
|
+
const spaced = name.replace(/([A-Z])/g, " $1").trim();
|
|
357
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
358
|
+
}
|
|
359
|
+
function posOf(sourceFile, node) {
|
|
360
|
+
try {
|
|
361
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
362
|
+
return line + 1;
|
|
363
|
+
} catch {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
var ParserError = class extends Error {
|
|
368
|
+
constructor(code, message, file, line, suggestion) {
|
|
369
|
+
super(message);
|
|
370
|
+
this.code = code;
|
|
371
|
+
this.file = file;
|
|
372
|
+
this.line = line;
|
|
373
|
+
this.suggestion = suggestion;
|
|
374
|
+
this.name = "ParserError";
|
|
375
|
+
}
|
|
376
|
+
code;
|
|
377
|
+
file;
|
|
378
|
+
line;
|
|
379
|
+
suggestion;
|
|
380
|
+
format() {
|
|
381
|
+
let output = `
|
|
382
|
+
error[${this.code}]: ${this.message}
|
|
383
|
+
`;
|
|
384
|
+
if (this.file) output += ` --> ${this.file}`;
|
|
385
|
+
if (this.line) output += `:${this.line}`;
|
|
386
|
+
output += "\n";
|
|
387
|
+
if (this.suggestion) {
|
|
388
|
+
output += ` = help: ${this.suggestion}
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
return output;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/core/generator.ts
|
|
396
|
+
function escapeSwiftString(s) {
|
|
397
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
398
|
+
}
|
|
399
|
+
function escapeXml(s) {
|
|
400
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
401
|
+
}
|
|
402
|
+
function generateSwift(intent) {
|
|
403
|
+
const lines = [];
|
|
404
|
+
lines.push(`// ${intent.name}Intent.swift`);
|
|
405
|
+
lines.push(`// Generated by Axint \u2014 https://github.com/agenticempire/axint`);
|
|
406
|
+
lines.push(`// Do not edit manually. Re-run \`axint compile\` to regenerate.`);
|
|
407
|
+
lines.push(``);
|
|
408
|
+
lines.push(`import AppIntents`);
|
|
409
|
+
lines.push(`import Foundation`);
|
|
410
|
+
lines.push(``);
|
|
411
|
+
lines.push(`struct ${intent.name}Intent: AppIntent {`);
|
|
412
|
+
lines.push(
|
|
413
|
+
` static let title: LocalizedStringResource = "${escapeSwiftString(intent.title)}"`
|
|
414
|
+
);
|
|
415
|
+
lines.push(
|
|
416
|
+
` static let description: IntentDescription = IntentDescription("${escapeSwiftString(intent.description)}")`
|
|
417
|
+
);
|
|
418
|
+
if (intent.isDiscoverable !== void 0) {
|
|
419
|
+
lines.push(` static let isDiscoverable: Bool = ${intent.isDiscoverable}`);
|
|
420
|
+
}
|
|
421
|
+
lines.push(``);
|
|
422
|
+
for (const param of intent.parameters) {
|
|
423
|
+
lines.push(generateParameter(param));
|
|
424
|
+
}
|
|
425
|
+
if (intent.parameters.length > 0) {
|
|
426
|
+
lines.push(``);
|
|
427
|
+
}
|
|
428
|
+
const returnTypeSignature = generateReturnSignature(intent.returnType);
|
|
429
|
+
lines.push(` func perform() async throws -> ${returnTypeSignature} {`);
|
|
430
|
+
lines.push(` // TODO: Implement your intent logic here.`);
|
|
431
|
+
if (intent.parameters.length > 0) {
|
|
432
|
+
const paramList = intent.parameters.map((p) => `\\(${p.name})`).join(", ");
|
|
433
|
+
lines.push(` // Parameters available: ${paramList}`);
|
|
434
|
+
}
|
|
435
|
+
lines.push(generatePerformReturn(intent.returnType));
|
|
436
|
+
lines.push(` }`);
|
|
437
|
+
lines.push(`}`);
|
|
438
|
+
lines.push(``);
|
|
439
|
+
return lines.join("\n");
|
|
440
|
+
}
|
|
441
|
+
function generateInfoPlistFragment(intent) {
|
|
442
|
+
const keys = intent.infoPlistKeys;
|
|
443
|
+
if (!keys || Object.keys(keys).length === 0) return void 0;
|
|
444
|
+
const lines = [];
|
|
445
|
+
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
446
|
+
lines.push(
|
|
447
|
+
`<!-- Info.plist fragment generated by Axint for ${intent.name}Intent -->`
|
|
448
|
+
);
|
|
449
|
+
lines.push(`<!-- Merge these keys into your app's Info.plist. -->`);
|
|
450
|
+
lines.push(`<plist version="1.0">`);
|
|
451
|
+
lines.push(`<dict>`);
|
|
452
|
+
for (const [key, desc] of Object.entries(keys)) {
|
|
453
|
+
lines.push(` <key>${escapeXml(key)}</key>`);
|
|
454
|
+
lines.push(` <string>${escapeXml(desc)}</string>`);
|
|
455
|
+
}
|
|
456
|
+
lines.push(`</dict>`);
|
|
457
|
+
lines.push(`</plist>`);
|
|
458
|
+
lines.push(``);
|
|
459
|
+
return lines.join("\n");
|
|
460
|
+
}
|
|
461
|
+
function generateEntitlementsFragment(intent) {
|
|
462
|
+
const ents = intent.entitlements;
|
|
463
|
+
if (!ents || ents.length === 0) return void 0;
|
|
464
|
+
const lines = [];
|
|
465
|
+
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
466
|
+
lines.push(
|
|
467
|
+
`<!-- Entitlements fragment generated by Axint for ${intent.name}Intent -->`
|
|
468
|
+
);
|
|
469
|
+
lines.push(`<!-- Merge these into your target's .entitlements file. -->`);
|
|
470
|
+
lines.push(`<!-- Note: entitlements requiring typed values (string/array) -->`);
|
|
471
|
+
lines.push(`<!-- need manual adjustment \u2014 defaults below are boolean true. -->`);
|
|
472
|
+
lines.push(
|
|
473
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`
|
|
474
|
+
);
|
|
475
|
+
lines.push(`<plist version="1.0">`);
|
|
476
|
+
lines.push(`<dict>`);
|
|
477
|
+
for (const ent of ents) {
|
|
478
|
+
lines.push(` <key>${escapeXml(ent)}</key>`);
|
|
479
|
+
lines.push(` <true/>`);
|
|
480
|
+
}
|
|
481
|
+
lines.push(`</dict>`);
|
|
482
|
+
lines.push(`</plist>`);
|
|
483
|
+
lines.push(``);
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
function generateParameter(param) {
|
|
487
|
+
const swiftType = irTypeToSwift(param.type);
|
|
488
|
+
const lines = [];
|
|
489
|
+
const attrs = [];
|
|
490
|
+
attrs.push(`title: "${escapeSwiftString(param.title)}"`);
|
|
491
|
+
if (param.description) {
|
|
492
|
+
attrs.push(`description: "${escapeSwiftString(param.description)}"`);
|
|
493
|
+
}
|
|
494
|
+
const decorator = ` @Parameter(${attrs.join(", ")})`;
|
|
495
|
+
lines.push(decorator);
|
|
496
|
+
if (param.defaultValue !== void 0) {
|
|
497
|
+
const defaultStr = formatSwiftDefault(param.defaultValue, param.type);
|
|
498
|
+
lines.push(` var ${param.name}: ${swiftType} = ${defaultStr}`);
|
|
499
|
+
} else {
|
|
500
|
+
lines.push(` var ${param.name}: ${swiftType}`);
|
|
501
|
+
}
|
|
502
|
+
lines.push(``);
|
|
503
|
+
return lines.join("\n");
|
|
504
|
+
}
|
|
505
|
+
function generateReturnSignature(type) {
|
|
506
|
+
if (type.kind === "primitive") {
|
|
507
|
+
const swift = irTypeToSwift(type);
|
|
508
|
+
return `some IntentResult & ReturnsValue<${swift}>`;
|
|
509
|
+
}
|
|
510
|
+
if (type.kind === "optional" && type.innerType.kind === "primitive") {
|
|
511
|
+
const swift = irTypeToSwift(type.innerType);
|
|
512
|
+
return `some IntentResult & ReturnsValue<${swift}>`;
|
|
513
|
+
}
|
|
514
|
+
return `some IntentResult`;
|
|
515
|
+
}
|
|
516
|
+
function generatePerformReturn(type) {
|
|
517
|
+
const indent = " ";
|
|
518
|
+
if (type.kind === "primitive") {
|
|
519
|
+
return `${indent}return .result(value: ${defaultLiteralFor(type.value)})`;
|
|
520
|
+
}
|
|
521
|
+
if (type.kind === "optional" && type.innerType.kind === "primitive") {
|
|
522
|
+
return `${indent}return .result(value: ${defaultLiteralFor(type.innerType.value)})`;
|
|
523
|
+
}
|
|
524
|
+
return `${indent}return .result()`;
|
|
525
|
+
}
|
|
526
|
+
function defaultLiteralFor(primitive) {
|
|
527
|
+
switch (primitive) {
|
|
528
|
+
case "string":
|
|
529
|
+
return `""`;
|
|
530
|
+
case "int":
|
|
531
|
+
return `0`;
|
|
532
|
+
case "double":
|
|
533
|
+
return `0.0`;
|
|
534
|
+
case "float":
|
|
535
|
+
return `Float(0)`;
|
|
536
|
+
case "boolean":
|
|
537
|
+
return `false`;
|
|
538
|
+
case "date":
|
|
539
|
+
return `Date()`;
|
|
540
|
+
case "duration":
|
|
541
|
+
return `Measurement<UnitDuration>(value: 0, unit: .seconds)`;
|
|
542
|
+
case "url":
|
|
543
|
+
return `URL(string: "about:blank")!`;
|
|
544
|
+
default:
|
|
545
|
+
return `""`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function formatSwiftDefault(value, _type) {
|
|
549
|
+
if (typeof value === "string") return `"${escapeSwiftString(value)}"`;
|
|
550
|
+
if (typeof value === "number") {
|
|
551
|
+
if (!Number.isFinite(value)) return `0`;
|
|
552
|
+
return `${value}`;
|
|
553
|
+
}
|
|
554
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
555
|
+
return `"${escapeSwiftString(String(value))}"`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/core/validator.ts
|
|
559
|
+
var MAX_PARAMETERS = 10;
|
|
560
|
+
var MAX_TITLE_LENGTH = 60;
|
|
561
|
+
function validateIntent(intent) {
|
|
562
|
+
const diagnostics = [];
|
|
563
|
+
if (!intent.name || !/^[A-Z][a-zA-Z0-9]*$/.test(intent.name)) {
|
|
564
|
+
diagnostics.push({
|
|
565
|
+
code: "AX100",
|
|
566
|
+
severity: "error",
|
|
567
|
+
message: `Intent name "${intent.name}" must be PascalCase (e.g., "CreateEvent")`,
|
|
568
|
+
file: intent.sourceFile,
|
|
569
|
+
suggestion: `Rename to "${toPascalCase(intent.name)}"`
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (!intent.title || intent.title.trim().length === 0) {
|
|
573
|
+
diagnostics.push({
|
|
574
|
+
code: "AX101",
|
|
575
|
+
severity: "error",
|
|
576
|
+
message: "Intent title must not be empty",
|
|
577
|
+
file: intent.sourceFile,
|
|
578
|
+
suggestion: "Add a human-readable title for Siri and Shortcuts display"
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (!intent.description || intent.description.trim().length === 0) {
|
|
582
|
+
diagnostics.push({
|
|
583
|
+
code: "AX102",
|
|
584
|
+
severity: "error",
|
|
585
|
+
message: "Intent description must not be empty",
|
|
586
|
+
file: intent.sourceFile,
|
|
587
|
+
suggestion: "Add a description explaining what this intent does"
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
for (const param of intent.parameters) {
|
|
591
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(param.name)) {
|
|
592
|
+
diagnostics.push({
|
|
593
|
+
code: "AX103",
|
|
594
|
+
severity: "error",
|
|
595
|
+
message: `Parameter name "${param.name}" is not a valid Swift identifier`,
|
|
596
|
+
file: intent.sourceFile,
|
|
597
|
+
suggestion: `Rename to "${param.name.replace(/[^a-zA-Z0-9_]/g, "_")}"`
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (!param.description || param.description.trim().length === 0) {
|
|
601
|
+
diagnostics.push({
|
|
602
|
+
code: "AX104",
|
|
603
|
+
severity: "warning",
|
|
604
|
+
message: `Parameter "${param.name}" has no description \u2014 Siri will display it without context`,
|
|
605
|
+
file: intent.sourceFile,
|
|
606
|
+
suggestion: "Add a description for better Siri/Shortcuts display"
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (intent.parameters.length > MAX_PARAMETERS) {
|
|
611
|
+
diagnostics.push({
|
|
612
|
+
code: "AX105",
|
|
613
|
+
severity: "warning",
|
|
614
|
+
message: `Intent has ${intent.parameters.length} parameters. Apple recommends ${MAX_PARAMETERS} or fewer for usability.`,
|
|
615
|
+
file: intent.sourceFile,
|
|
616
|
+
suggestion: "Consider splitting into multiple intents or grouping parameters into an entity"
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (intent.title && intent.title.length > MAX_TITLE_LENGTH) {
|
|
620
|
+
diagnostics.push({
|
|
621
|
+
code: "AX106",
|
|
622
|
+
severity: "warning",
|
|
623
|
+
message: `Intent title is ${intent.title.length} characters. Siri display may truncate titles over ${MAX_TITLE_LENGTH} characters.`,
|
|
624
|
+
file: intent.sourceFile
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
628
|
+
for (const param of intent.parameters) {
|
|
629
|
+
if (seen.has(param.name)) {
|
|
630
|
+
diagnostics.push({
|
|
631
|
+
code: "AX107",
|
|
632
|
+
severity: "error",
|
|
633
|
+
message: `Duplicate parameter name "${param.name}"`,
|
|
634
|
+
file: intent.sourceFile,
|
|
635
|
+
suggestion: "Each parameter in a single intent must have a unique name"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
seen.add(param.name);
|
|
639
|
+
}
|
|
640
|
+
for (const ent of intent.entitlements ?? []) {
|
|
641
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(ent) || !ent.includes(".")) {
|
|
642
|
+
diagnostics.push({
|
|
643
|
+
code: "AX108",
|
|
644
|
+
severity: "warning",
|
|
645
|
+
message: `Entitlement "${ent}" does not look like a valid reverse-DNS identifier`,
|
|
646
|
+
file: intent.sourceFile,
|
|
647
|
+
suggestion: 'Use reverse-DNS, e.g., "com.apple.developer.siri" or "com.apple.security.app-sandbox"'
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
for (const key of Object.keys(intent.infoPlistKeys ?? {})) {
|
|
652
|
+
if (!/^(NS|UI|LS|CF|CA|CK)[A-Za-z0-9]+$/.test(key)) {
|
|
653
|
+
diagnostics.push({
|
|
654
|
+
code: "AX109",
|
|
655
|
+
severity: "warning",
|
|
656
|
+
message: `Info.plist key "${key}" does not match Apple's usual naming conventions`,
|
|
657
|
+
file: intent.sourceFile,
|
|
658
|
+
suggestion: 'Apple keys generally start with "NS" (e.g., "NSCalendarsUsageDescription")'
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return diagnostics;
|
|
663
|
+
}
|
|
664
|
+
function validateSwiftSource(swift) {
|
|
665
|
+
const diagnostics = [];
|
|
666
|
+
if (!swift.includes("import AppIntents")) {
|
|
667
|
+
diagnostics.push({
|
|
668
|
+
code: "AX200",
|
|
669
|
+
severity: "error",
|
|
670
|
+
message: 'Generated Swift is missing "import AppIntents"'
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (!swift.includes(": AppIntent")) {
|
|
674
|
+
diagnostics.push({
|
|
675
|
+
code: "AX201",
|
|
676
|
+
severity: "error",
|
|
677
|
+
message: "Generated struct does not conform to AppIntent protocol"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (!swift.includes("func perform()")) {
|
|
681
|
+
diagnostics.push({
|
|
682
|
+
code: "AX202",
|
|
683
|
+
severity: "error",
|
|
684
|
+
message: "Generated struct is missing the perform() function"
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return diagnostics;
|
|
688
|
+
}
|
|
689
|
+
function toPascalCase(s) {
|
|
690
|
+
if (!s) return "UnnamedIntent";
|
|
691
|
+
return s.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/core/compiler.ts
|
|
695
|
+
function compileFile(filePath, options = {}) {
|
|
696
|
+
let source;
|
|
697
|
+
try {
|
|
698
|
+
source = readFileSync(filePath, "utf-8");
|
|
699
|
+
} catch (_err) {
|
|
700
|
+
return {
|
|
701
|
+
success: false,
|
|
702
|
+
diagnostics: [
|
|
703
|
+
{
|
|
704
|
+
code: "AX000",
|
|
705
|
+
severity: "error",
|
|
706
|
+
message: `Cannot read file: ${filePath}`,
|
|
707
|
+
file: filePath
|
|
708
|
+
}
|
|
709
|
+
]
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return compileSource(source, filePath, options);
|
|
713
|
+
}
|
|
714
|
+
function compileSource(source, fileName = "<stdin>", options = {}) {
|
|
715
|
+
const diagnostics = [];
|
|
716
|
+
let ir;
|
|
717
|
+
try {
|
|
718
|
+
ir = parseIntentSource(source, fileName);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (err instanceof ParserError) {
|
|
721
|
+
return {
|
|
722
|
+
success: false,
|
|
723
|
+
diagnostics: [
|
|
724
|
+
{
|
|
725
|
+
code: err.code,
|
|
726
|
+
severity: "error",
|
|
727
|
+
message: err.message,
|
|
728
|
+
file: err.file,
|
|
729
|
+
line: err.line,
|
|
730
|
+
suggestion: err.suggestion
|
|
731
|
+
}
|
|
732
|
+
]
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
737
|
+
const irDiagnostics = validateIntent(ir);
|
|
738
|
+
diagnostics.push(...irDiagnostics);
|
|
739
|
+
if (irDiagnostics.some((d) => d.severity === "error")) {
|
|
740
|
+
return { success: false, diagnostics };
|
|
741
|
+
}
|
|
742
|
+
const swiftCode = generateSwift(ir);
|
|
743
|
+
if (options.validate !== false) {
|
|
744
|
+
const swiftDiagnostics = validateSwiftSource(swiftCode);
|
|
745
|
+
diagnostics.push(...swiftDiagnostics);
|
|
746
|
+
if (swiftDiagnostics.some((d) => d.severity === "error")) {
|
|
747
|
+
return { success: false, diagnostics };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const infoPlistFragment = options.emitInfoPlist ? generateInfoPlistFragment(ir) : void 0;
|
|
751
|
+
const entitlementsFragment = options.emitEntitlements ? generateEntitlementsFragment(ir) : void 0;
|
|
752
|
+
const intentFileName = `${ir.name}Intent.swift`;
|
|
753
|
+
const outputPath = options.outDir ? `${options.outDir}/${intentFileName}` : intentFileName;
|
|
754
|
+
return {
|
|
755
|
+
success: true,
|
|
756
|
+
output: {
|
|
757
|
+
outputPath,
|
|
758
|
+
swiftCode,
|
|
759
|
+
infoPlistFragment,
|
|
760
|
+
entitlementsFragment,
|
|
761
|
+
ir,
|
|
762
|
+
diagnostics
|
|
763
|
+
},
|
|
764
|
+
diagnostics
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/cli/index.ts
|
|
769
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
770
|
+
var pkg = JSON.parse(readFileSync2(resolve(__dirname, "../../package.json"), "utf-8"));
|
|
771
|
+
var VERSION = pkg.version;
|
|
772
|
+
var program = new Command();
|
|
773
|
+
program.name("axint").description(
|
|
774
|
+
"The open-source compiler that transforms AI agent definitions into native Apple App Intents."
|
|
775
|
+
).version(VERSION);
|
|
776
|
+
program.command("compile").description("Compile a TypeScript intent definition into Swift").argument("<file>", "Path to the TypeScript intent definition").option("-o, --out <dir>", "Output directory for generated Swift", ".").option("--no-validate", "Skip validation of generated Swift").option("--stdout", "Print generated Swift to stdout instead of writing a file").option("--json", "Output result as JSON (machine-readable)").action(
|
|
777
|
+
(file, options) => {
|
|
778
|
+
const filePath = resolve(file);
|
|
779
|
+
try {
|
|
780
|
+
const result = compileFile(filePath, {
|
|
781
|
+
outDir: options.out,
|
|
782
|
+
validate: options.validate
|
|
783
|
+
});
|
|
784
|
+
if (options.json) {
|
|
785
|
+
console.log(
|
|
786
|
+
JSON.stringify(
|
|
787
|
+
{
|
|
788
|
+
success: result.success,
|
|
789
|
+
swift: result.output?.swiftCode ?? null,
|
|
790
|
+
outputPath: result.output?.outputPath ?? null,
|
|
791
|
+
diagnostics: result.diagnostics.map((d) => ({
|
|
792
|
+
code: d.code,
|
|
793
|
+
severity: d.severity,
|
|
794
|
+
message: d.message,
|
|
795
|
+
file: d.file,
|
|
796
|
+
line: d.line,
|
|
797
|
+
suggestion: d.suggestion
|
|
798
|
+
}))
|
|
799
|
+
},
|
|
800
|
+
null,
|
|
801
|
+
2
|
|
802
|
+
)
|
|
803
|
+
);
|
|
804
|
+
if (!result.success) process.exit(1);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
for (const d of result.diagnostics) {
|
|
808
|
+
const prefix = d.severity === "error" ? "\x1B[31merror\x1B[0m" : d.severity === "warning" ? "\x1B[33mwarning\x1B[0m" : "\x1B[36minfo\x1B[0m";
|
|
809
|
+
console.error(` ${prefix}[${d.code}]: ${d.message}`);
|
|
810
|
+
if (d.file) console.error(` --> ${d.file}${d.line ? `:${d.line}` : ""}`);
|
|
811
|
+
if (d.suggestion) console.error(` = help: ${d.suggestion}`);
|
|
812
|
+
console.error();
|
|
813
|
+
}
|
|
814
|
+
if (!result.success || !result.output) {
|
|
815
|
+
console.error(
|
|
816
|
+
`\x1B[31mCompilation failed with ${result.diagnostics.filter((d) => d.severity === "error").length} error(s)\x1B[0m`
|
|
817
|
+
);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
if (options.stdout) {
|
|
821
|
+
console.log(result.output.swiftCode);
|
|
822
|
+
} else {
|
|
823
|
+
const outPath = resolve(result.output.outputPath);
|
|
824
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
825
|
+
writeFileSync(outPath, result.output.swiftCode, "utf-8");
|
|
826
|
+
console.log(`\x1B[32m\u2713\x1B[0m Compiled ${result.output.ir.name} \u2192 ${outPath}`);
|
|
827
|
+
}
|
|
828
|
+
const warnings = result.diagnostics.filter(
|
|
829
|
+
(d) => d.severity === "warning"
|
|
830
|
+
).length;
|
|
831
|
+
if (warnings > 0) {
|
|
832
|
+
console.log(` ${warnings} warning(s)`);
|
|
833
|
+
}
|
|
834
|
+
} catch (err) {
|
|
835
|
+
if (err && typeof err === "object" && "format" in err && typeof err.format === "function") {
|
|
836
|
+
console.error(err.format());
|
|
837
|
+
} else {
|
|
838
|
+
console.error(`\x1B[31merror:\x1B[0m ${err}`);
|
|
839
|
+
}
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
program.command("validate").description("Validate a TypeScript intent definition without generating output").argument("<file>", "Path to the TypeScript intent definition").action((file) => {
|
|
845
|
+
const filePath = resolve(file);
|
|
846
|
+
try {
|
|
847
|
+
const result = compileFile(filePath, { validate: true });
|
|
848
|
+
for (const d of result.diagnostics) {
|
|
849
|
+
const prefix = d.severity === "error" ? "\x1B[31merror\x1B[0m" : d.severity === "warning" ? "\x1B[33mwarning\x1B[0m" : "\x1B[36minfo\x1B[0m";
|
|
850
|
+
console.error(` ${prefix}[${d.code}]: ${d.message}`);
|
|
851
|
+
if (d.suggestion) console.error(` = help: ${d.suggestion}`);
|
|
852
|
+
}
|
|
853
|
+
if (result.success) {
|
|
854
|
+
console.log(`\x1B[32m\u2713\x1B[0m Valid intent definition`);
|
|
855
|
+
} else {
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
} catch (err) {
|
|
859
|
+
if (err && typeof err === "object" && "format" in err && typeof err.format === "function") {
|
|
860
|
+
console.error(err.format());
|
|
861
|
+
} else {
|
|
862
|
+
console.error(`\x1B[31merror:\x1B[0m ${err}`);
|
|
863
|
+
}
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
program.parse();
|
|
868
|
+
//# sourceMappingURL=index.js.map
|