@axintai/compiler 0.2.1 → 0.3.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.
- package/README.md +10 -9
- package/dist/cli/index.js +2332 -178
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +125 -3
- package/dist/core/index.js +592 -42
- package/dist/core/index.js.map +1 -1
- package/dist/mcp/index.js +410 -33
- package/dist/mcp/index.js.map +1 -1
- package/dist/sdk/index.d.ts +55 -2
- package/dist/sdk/index.js +30 -1
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.js
CHANGED
|
@@ -38,6 +38,10 @@ function irTypeToSwift(type) {
|
|
|
38
38
|
return `${irTypeToSwift(type.innerType)}?`;
|
|
39
39
|
case "entity":
|
|
40
40
|
return type.entityName;
|
|
41
|
+
case "entityQuery":
|
|
42
|
+
return `${type.entityName}Query`;
|
|
43
|
+
case "dynamicOptions":
|
|
44
|
+
return `[DynamicOptionsResult<${irTypeToSwift(type.valueType)}>]`;
|
|
41
45
|
case "enum":
|
|
42
46
|
return type.name;
|
|
43
47
|
}
|
|
@@ -53,6 +57,9 @@ function parseIntentSource(source, filePath = "<stdin>") {
|
|
|
53
57
|
// setParentNodes
|
|
54
58
|
ts.ScriptKind.TS
|
|
55
59
|
);
|
|
60
|
+
const entities = findDefineEntityCalls(sourceFile).map(
|
|
61
|
+
(call) => parseEntityDefinition(call, filePath, sourceFile)
|
|
62
|
+
);
|
|
56
63
|
const defineIntentCall = findDefineIntentCall(sourceFile);
|
|
57
64
|
if (!defineIntentCall) {
|
|
58
65
|
throw new ParserError(
|
|
@@ -115,6 +122,10 @@ function parseIntentSource(source, filePath = "<stdin>") {
|
|
|
115
122
|
const entitlements = readStringArray(entitlementsNode);
|
|
116
123
|
const infoPlistNode = props.get("infoPlistKeys");
|
|
117
124
|
const infoPlistKeys = readStringRecord(infoPlistNode);
|
|
125
|
+
const donateOnPerformNode = props.get("donateOnPerform");
|
|
126
|
+
const donateOnPerform = readBooleanLiteral(donateOnPerformNode);
|
|
127
|
+
const customResultTypeNode = props.get("customResultType");
|
|
128
|
+
const customResultType = readStringLiteral(customResultTypeNode);
|
|
118
129
|
return {
|
|
119
130
|
name,
|
|
120
131
|
title,
|
|
@@ -126,7 +137,10 @@ function parseIntentSource(source, filePath = "<stdin>") {
|
|
|
126
137
|
sourceFile: filePath,
|
|
127
138
|
entitlements: entitlements.length > 0 ? entitlements : void 0,
|
|
128
139
|
infoPlistKeys: Object.keys(infoPlistKeys).length > 0 ? infoPlistKeys : void 0,
|
|
129
|
-
isDiscoverable: isDiscoverable ?? void 0
|
|
140
|
+
isDiscoverable: isDiscoverable ?? void 0,
|
|
141
|
+
entities: entities.length > 0 ? entities : void 0,
|
|
142
|
+
donateOnPerform: donateOnPerform ?? void 0,
|
|
143
|
+
customResultType: customResultType ?? void 0
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
function findDefineIntentCall(node) {
|
|
@@ -142,6 +156,18 @@ function findDefineIntentCall(node) {
|
|
|
142
156
|
visit(node);
|
|
143
157
|
return found;
|
|
144
158
|
}
|
|
159
|
+
function findDefineEntityCalls(node) {
|
|
160
|
+
const found = [];
|
|
161
|
+
const visit = (n) => {
|
|
162
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "defineEntity") {
|
|
163
|
+
found.push(n);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
ts.forEachChild(n, visit);
|
|
167
|
+
};
|
|
168
|
+
visit(node);
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
145
171
|
function propertyMap(obj) {
|
|
146
172
|
const map = /* @__PURE__ */ new Map();
|
|
147
173
|
for (const prop of obj.properties) {
|
|
@@ -195,6 +221,77 @@ function readStringRecord(node) {
|
|
|
195
221
|
}
|
|
196
222
|
return rec;
|
|
197
223
|
}
|
|
224
|
+
function parseEntityDefinition(call, filePath, sourceFile) {
|
|
225
|
+
const arg = call.arguments[0];
|
|
226
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
227
|
+
throw new ParserError(
|
|
228
|
+
"AX015",
|
|
229
|
+
"defineEntity() must be called with an object literal",
|
|
230
|
+
filePath,
|
|
231
|
+
posOf(sourceFile, call),
|
|
232
|
+
"Pass an object: defineEntity({ name, display, properties, query })"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const props = propertyMap(arg);
|
|
236
|
+
const name = readStringLiteral(props.get("name"));
|
|
237
|
+
if (!name) {
|
|
238
|
+
throw new ParserError(
|
|
239
|
+
"AX016",
|
|
240
|
+
"Entity definition missing required field: name",
|
|
241
|
+
filePath,
|
|
242
|
+
posOf(sourceFile, arg),
|
|
243
|
+
'Add a name field: name: "Task"'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const displayNode = props.get("display");
|
|
247
|
+
if (!displayNode || !ts.isObjectLiteralExpression(displayNode)) {
|
|
248
|
+
throw new ParserError(
|
|
249
|
+
"AX017",
|
|
250
|
+
"Entity definition missing required field: display",
|
|
251
|
+
filePath,
|
|
252
|
+
posOf(sourceFile, arg),
|
|
253
|
+
'Add display field: display: { title: "name", subtitle: "status" }'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const displayProps = propertyMap(displayNode);
|
|
257
|
+
const displayRepresentation = {
|
|
258
|
+
title: readStringLiteral(displayProps.get("title")) || "name",
|
|
259
|
+
subtitle: readStringLiteral(displayProps.get("subtitle")) || void 0,
|
|
260
|
+
image: readStringLiteral(displayProps.get("image")) || void 0
|
|
261
|
+
};
|
|
262
|
+
const propertiesNode = props.get("properties");
|
|
263
|
+
const properties = propertiesNode ? extractParameters(propertiesNode, filePath, sourceFile) : [];
|
|
264
|
+
const queryTypeNode = props.get("query");
|
|
265
|
+
const queryTypeStr = readStringLiteral(queryTypeNode);
|
|
266
|
+
const queryType = validateQueryType(queryTypeStr, filePath, sourceFile, queryTypeNode);
|
|
267
|
+
return {
|
|
268
|
+
name,
|
|
269
|
+
displayRepresentation,
|
|
270
|
+
properties,
|
|
271
|
+
queryType
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function validateQueryType(value, filePath, sourceFile, node) {
|
|
275
|
+
if (!value) {
|
|
276
|
+
throw new ParserError(
|
|
277
|
+
"AX018",
|
|
278
|
+
"Entity definition missing required field: query",
|
|
279
|
+
filePath,
|
|
280
|
+
node ? posOf(sourceFile, node) : void 0,
|
|
281
|
+
'Add query field: query: "string" (or "all", "id", "property")'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const valid = ["all", "id", "string", "property"];
|
|
285
|
+
if (!valid.includes(value)) {
|
|
286
|
+
throw new ParserError(
|
|
287
|
+
"AX019",
|
|
288
|
+
`Invalid query type: "${value}". Must be one of: all, id, string, property`,
|
|
289
|
+
filePath,
|
|
290
|
+
node ? posOf(sourceFile, node) : void 0
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
198
295
|
function extractParameters(node, filePath, sourceFile) {
|
|
199
296
|
if (!ts.isObjectLiteralExpression(node)) {
|
|
200
297
|
throw new ParserError(
|
|
@@ -210,20 +307,20 @@ function extractParameters(node, filePath, sourceFile) {
|
|
|
210
307
|
if (!ts.isPropertyAssignment(prop)) continue;
|
|
211
308
|
const paramName = propertyKeyName(prop.name);
|
|
212
309
|
if (!paramName) continue;
|
|
213
|
-
const { typeName, description, configObject } = extractParamCall(
|
|
310
|
+
const { typeName, description, configObject, callExpr } = extractParamCall(
|
|
214
311
|
prop.initializer,
|
|
215
312
|
filePath,
|
|
216
313
|
sourceFile
|
|
217
314
|
);
|
|
218
|
-
const resolvedType = resolveParamType(typeName, filePath, sourceFile, prop);
|
|
315
|
+
const resolvedType = resolveParamType(typeName, filePath, sourceFile, prop, callExpr);
|
|
219
316
|
const isOptional = configObject ? readBooleanLiteral(configObject.get("required")) === false : false;
|
|
220
317
|
const defaultExpr = configObject?.get("default");
|
|
221
318
|
const defaultValue = defaultExpr ? evaluateLiteral(defaultExpr) : void 0;
|
|
222
319
|
const titleFromConfig = configObject ? readStringLiteral(configObject.get("title")) : null;
|
|
223
320
|
const irType = isOptional ? {
|
|
224
321
|
kind: "optional",
|
|
225
|
-
innerType:
|
|
226
|
-
} :
|
|
322
|
+
innerType: resolvedType
|
|
323
|
+
} : resolvedType;
|
|
227
324
|
params.push({
|
|
228
325
|
name: paramName,
|
|
229
326
|
type: irType,
|
|
@@ -255,34 +352,98 @@ function extractParamCall(expr, filePath, sourceFile) {
|
|
|
255
352
|
);
|
|
256
353
|
}
|
|
257
354
|
const typeName = expr.expression.name.text;
|
|
258
|
-
|
|
259
|
-
|
|
355
|
+
let descriptionArg;
|
|
356
|
+
let configArg;
|
|
357
|
+
if (typeName === "entity" && expr.arguments.length >= 2) {
|
|
358
|
+
descriptionArg = expr.arguments[1];
|
|
359
|
+
configArg = expr.arguments[2];
|
|
360
|
+
} else if (typeName === "dynamicOptions" && expr.arguments.length >= 3) {
|
|
361
|
+
descriptionArg = expr.arguments[2];
|
|
362
|
+
configArg = expr.arguments[3];
|
|
363
|
+
} else {
|
|
364
|
+
descriptionArg = expr.arguments[0];
|
|
365
|
+
configArg = expr.arguments[1];
|
|
366
|
+
}
|
|
260
367
|
const description = descriptionArg ? readStringLiteral(descriptionArg) : null;
|
|
261
368
|
if (description === null) {
|
|
262
369
|
throw new ParserError(
|
|
263
370
|
"AX008",
|
|
264
|
-
`param.${typeName}() requires a string description
|
|
371
|
+
`param.${typeName}() requires a string description`,
|
|
265
372
|
filePath,
|
|
266
373
|
posOf(sourceFile, expr),
|
|
267
374
|
`Example: param.${typeName}("Human-readable description")`
|
|
268
375
|
);
|
|
269
376
|
}
|
|
270
377
|
const configObject = configArg && ts.isObjectLiteralExpression(configArg) ? propertyMap(configArg) : null;
|
|
271
|
-
return { typeName, description, configObject };
|
|
378
|
+
return { typeName, description, configObject, callExpr: expr };
|
|
272
379
|
}
|
|
273
|
-
function resolveParamType(typeName, filePath, sourceFile, node) {
|
|
380
|
+
function resolveParamType(typeName, filePath, sourceFile, node, callExpr) {
|
|
274
381
|
if (PARAM_TYPES.has(typeName)) {
|
|
275
|
-
return typeName;
|
|
382
|
+
return { kind: "primitive", value: typeName };
|
|
276
383
|
}
|
|
277
384
|
if (typeName in LEGACY_PARAM_ALIASES) {
|
|
278
|
-
return
|
|
385
|
+
return {
|
|
386
|
+
kind: "primitive",
|
|
387
|
+
value: LEGACY_PARAM_ALIASES[typeName]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (typeName === "entity") {
|
|
391
|
+
if (!callExpr || callExpr.arguments.length === 0) {
|
|
392
|
+
throw new ParserError(
|
|
393
|
+
"AX020",
|
|
394
|
+
"param.entity() requires the entity name as the first argument",
|
|
395
|
+
filePath,
|
|
396
|
+
posOf(sourceFile, node),
|
|
397
|
+
'Example: param.entity("Task", "Reference an entity")'
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const entityName = readStringLiteral(callExpr.arguments[0]);
|
|
401
|
+
if (!entityName) {
|
|
402
|
+
throw new ParserError(
|
|
403
|
+
"AX021",
|
|
404
|
+
"param.entity() requires a string entity name",
|
|
405
|
+
filePath,
|
|
406
|
+
posOf(sourceFile, node)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
kind: "entity",
|
|
411
|
+
entityName,
|
|
412
|
+
properties: []
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (typeName === "dynamicOptions") {
|
|
416
|
+
if (!callExpr || callExpr.arguments.length < 2) {
|
|
417
|
+
throw new ParserError(
|
|
418
|
+
"AX022",
|
|
419
|
+
"param.dynamicOptions() requires (providerName, paramType)",
|
|
420
|
+
filePath,
|
|
421
|
+
posOf(sourceFile, node),
|
|
422
|
+
'Example: param.dynamicOptions("PlaylistProvider", param.string(...))'
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const providerName = readStringLiteral(callExpr.arguments[0]);
|
|
426
|
+
if (!providerName) {
|
|
427
|
+
throw new ParserError(
|
|
428
|
+
"AX023",
|
|
429
|
+
"param.dynamicOptions() provider name must be a string",
|
|
430
|
+
filePath,
|
|
431
|
+
posOf(sourceFile, node)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const valueType = { kind: "primitive", value: "string" };
|
|
435
|
+
return {
|
|
436
|
+
kind: "dynamicOptions",
|
|
437
|
+
valueType,
|
|
438
|
+
providerName
|
|
439
|
+
};
|
|
279
440
|
}
|
|
280
441
|
throw new ParserError(
|
|
281
442
|
"AX005",
|
|
282
443
|
`Unknown param type: param.${typeName}`,
|
|
283
444
|
filePath,
|
|
284
445
|
posOf(sourceFile, node),
|
|
285
|
-
`Supported types: ${[...PARAM_TYPES].join(", ")}`
|
|
446
|
+
`Supported types: ${[...PARAM_TYPES].join(", ")}, entity, dynamicOptions`
|
|
286
447
|
);
|
|
287
448
|
}
|
|
288
449
|
function evaluateLiteral(node) {
|
|
@@ -400,6 +561,14 @@ function generateSwift(intent) {
|
|
|
400
561
|
lines.push(`import AppIntents`);
|
|
401
562
|
lines.push(`import Foundation`);
|
|
402
563
|
lines.push(``);
|
|
564
|
+
if (intent.entities && intent.entities.length > 0) {
|
|
565
|
+
for (const entity of intent.entities) {
|
|
566
|
+
lines.push(generateEntity(entity));
|
|
567
|
+
lines.push(``);
|
|
568
|
+
lines.push(generateEntityQuery(entity));
|
|
569
|
+
lines.push(``);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
403
572
|
lines.push(`struct ${intent.name}Intent: AppIntent {`);
|
|
404
573
|
lines.push(
|
|
405
574
|
` static let title: LocalizedStringResource = "${escapeSwiftString(intent.title)}"`
|
|
@@ -411,33 +580,115 @@ function generateSwift(intent) {
|
|
|
411
580
|
lines.push(` static let isDiscoverable: Bool = ${intent.isDiscoverable}`);
|
|
412
581
|
}
|
|
413
582
|
lines.push(``);
|
|
414
|
-
for (const
|
|
415
|
-
lines.push(generateParameter(
|
|
583
|
+
for (const param2 of intent.parameters) {
|
|
584
|
+
lines.push(generateParameter(param2));
|
|
416
585
|
}
|
|
417
586
|
if (intent.parameters.length > 0) {
|
|
418
587
|
lines.push(``);
|
|
419
588
|
}
|
|
420
|
-
const returnTypeSignature = generateReturnSignature(
|
|
589
|
+
const returnTypeSignature = generateReturnSignature(
|
|
590
|
+
intent.returnType,
|
|
591
|
+
intent.customResultType
|
|
592
|
+
);
|
|
421
593
|
lines.push(` func perform() async throws -> ${returnTypeSignature} {`);
|
|
422
594
|
lines.push(` // TODO: Implement your intent logic here.`);
|
|
423
595
|
if (intent.parameters.length > 0) {
|
|
424
596
|
const paramList = intent.parameters.map((p) => `\\(${p.name})`).join(", ");
|
|
425
597
|
lines.push(` // Parameters available: ${paramList}`);
|
|
426
598
|
}
|
|
427
|
-
|
|
599
|
+
if (intent.donateOnPerform) {
|
|
600
|
+
lines.push(` `);
|
|
601
|
+
lines.push(` // Donate this intent to Siri and Spotlight`);
|
|
602
|
+
lines.push(` try? await IntentDonationManager.shared.donate(intent: self)`);
|
|
603
|
+
}
|
|
604
|
+
lines.push(generatePerformReturn(intent.returnType, intent.customResultType));
|
|
428
605
|
lines.push(` }`);
|
|
429
606
|
lines.push(`}`);
|
|
430
607
|
lines.push(``);
|
|
431
608
|
return lines.join("\n");
|
|
432
609
|
}
|
|
610
|
+
function generateEntity(entity) {
|
|
611
|
+
const lines = [];
|
|
612
|
+
const propertyNames = new Set(entity.properties.map((p) => p.name));
|
|
613
|
+
lines.push(`struct ${entity.name}: AppEntity {`);
|
|
614
|
+
lines.push(` static var defaultQuery = ${entity.name}Query()`);
|
|
615
|
+
lines.push(``);
|
|
616
|
+
const hasId = propertyNames.has("id");
|
|
617
|
+
if (!hasId) {
|
|
618
|
+
lines.push(` var id: String`);
|
|
619
|
+
}
|
|
620
|
+
for (const prop of entity.properties) {
|
|
621
|
+
const swiftType = irTypeToSwift(prop.type);
|
|
622
|
+
lines.push(` var ${prop.name}: ${swiftType}`);
|
|
623
|
+
}
|
|
624
|
+
lines.push(``);
|
|
625
|
+
lines.push(
|
|
626
|
+
` static let typeDisplayRepresentation: TypeDisplayRepresentation = TypeDisplayRepresentation(`
|
|
627
|
+
);
|
|
628
|
+
lines.push(
|
|
629
|
+
` name: LocalizedStringResource("${escapeSwiftString(entity.name)}")`
|
|
630
|
+
);
|
|
631
|
+
lines.push(` )`);
|
|
632
|
+
lines.push(``);
|
|
633
|
+
lines.push(` var displayRepresentation: DisplayRepresentation {`);
|
|
634
|
+
lines.push(` DisplayRepresentation(`);
|
|
635
|
+
lines.push(
|
|
636
|
+
` title: "\\(${entity.displayRepresentation.title})"${entity.displayRepresentation.subtitle || entity.displayRepresentation.image ? "," : ""}`
|
|
637
|
+
);
|
|
638
|
+
if (entity.displayRepresentation.subtitle) {
|
|
639
|
+
const hasImage = !!entity.displayRepresentation.image;
|
|
640
|
+
lines.push(
|
|
641
|
+
` subtitle: "\\(${entity.displayRepresentation.subtitle})"${hasImage ? "," : ""}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (entity.displayRepresentation.image) {
|
|
645
|
+
lines.push(
|
|
646
|
+
` image: .init(systemName: "${escapeSwiftString(entity.displayRepresentation.image)}")`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
lines.push(` )`);
|
|
650
|
+
lines.push(` }`);
|
|
651
|
+
lines.push(`}`);
|
|
652
|
+
return lines.join("\n");
|
|
653
|
+
}
|
|
654
|
+
function generateEntityQuery(entity) {
|
|
655
|
+
const lines = [];
|
|
656
|
+
const queryType = entity.queryType;
|
|
657
|
+
lines.push(`struct ${entity.name}Query: EntityQuery {`);
|
|
658
|
+
lines.push(
|
|
659
|
+
` func entities(for identifiers: [${entity.name}.ID]) async throws -> [${entity.name}] {`
|
|
660
|
+
);
|
|
661
|
+
lines.push(` // TODO: Fetch entities by IDs`);
|
|
662
|
+
lines.push(` return []`);
|
|
663
|
+
lines.push(` }`);
|
|
664
|
+
lines.push(``);
|
|
665
|
+
if (queryType === "all") {
|
|
666
|
+
lines.push(` func allEntities() async throws -> [${entity.name}] {`);
|
|
667
|
+
lines.push(` // TODO: Return all entities`);
|
|
668
|
+
lines.push(` return []`);
|
|
669
|
+
lines.push(` }`);
|
|
670
|
+
} else if (queryType === "id") {
|
|
671
|
+
lines.push(` // ID-based query is provided by the entities(for:) method above`);
|
|
672
|
+
} else if (queryType === "string") {
|
|
673
|
+
lines.push(
|
|
674
|
+
` func entities(matching string: String) async throws -> [${entity.name}] {`
|
|
675
|
+
);
|
|
676
|
+
lines.push(` // TODO: Search entities by string`);
|
|
677
|
+
lines.push(` return []`);
|
|
678
|
+
lines.push(` }`);
|
|
679
|
+
} else if (queryType === "property") {
|
|
680
|
+
lines.push(` // Property-based query: implement using EntityPropertyQuery`);
|
|
681
|
+
lines.push(` // Example: property.where { \\$0.status == "active" }`);
|
|
682
|
+
}
|
|
683
|
+
lines.push(`}`);
|
|
684
|
+
return lines.join("\n");
|
|
685
|
+
}
|
|
433
686
|
function generateInfoPlistFragment(intent) {
|
|
434
687
|
const keys = intent.infoPlistKeys;
|
|
435
688
|
if (!keys || Object.keys(keys).length === 0) return void 0;
|
|
436
689
|
const lines = [];
|
|
437
690
|
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
438
|
-
lines.push(
|
|
439
|
-
`<!-- Info.plist fragment generated by Axint for ${intent.name}Intent -->`
|
|
440
|
-
);
|
|
691
|
+
lines.push(`<!-- Info.plist fragment generated by Axint for ${intent.name}Intent -->`);
|
|
441
692
|
lines.push(`<!-- Merge these keys into your app's Info.plist. -->`);
|
|
442
693
|
lines.push(`<plist version="1.0">`);
|
|
443
694
|
lines.push(`<dict>`);
|
|
@@ -475,26 +726,29 @@ function generateEntitlementsFragment(intent) {
|
|
|
475
726
|
lines.push(``);
|
|
476
727
|
return lines.join("\n");
|
|
477
728
|
}
|
|
478
|
-
function generateParameter(
|
|
479
|
-
const swiftType = irTypeToSwift(
|
|
729
|
+
function generateParameter(param2) {
|
|
730
|
+
const swiftType = irTypeToSwift(param2.type);
|
|
480
731
|
const lines = [];
|
|
481
732
|
const attrs = [];
|
|
482
|
-
attrs.push(`title: "${escapeSwiftString(
|
|
483
|
-
if (
|
|
484
|
-
attrs.push(`description: "${escapeSwiftString(
|
|
733
|
+
attrs.push(`title: "${escapeSwiftString(param2.title)}"`);
|
|
734
|
+
if (param2.description) {
|
|
735
|
+
attrs.push(`description: "${escapeSwiftString(param2.description)}"`);
|
|
485
736
|
}
|
|
486
737
|
const decorator = ` @Parameter(${attrs.join(", ")})`;
|
|
487
738
|
lines.push(decorator);
|
|
488
|
-
if (
|
|
489
|
-
const defaultStr = formatSwiftDefault(
|
|
490
|
-
lines.push(` var ${
|
|
739
|
+
if (param2.defaultValue !== void 0) {
|
|
740
|
+
const defaultStr = formatSwiftDefault(param2.defaultValue, param2.type);
|
|
741
|
+
lines.push(` var ${param2.name}: ${swiftType} = ${defaultStr}`);
|
|
491
742
|
} else {
|
|
492
|
-
lines.push(` var ${
|
|
743
|
+
lines.push(` var ${param2.name}: ${swiftType}`);
|
|
493
744
|
}
|
|
494
745
|
lines.push(``);
|
|
495
746
|
return lines.join("\n");
|
|
496
747
|
}
|
|
497
|
-
function generateReturnSignature(type) {
|
|
748
|
+
function generateReturnSignature(type, customResultType) {
|
|
749
|
+
if (customResultType) {
|
|
750
|
+
return customResultType;
|
|
751
|
+
}
|
|
498
752
|
if (type.kind === "primitive") {
|
|
499
753
|
const swift = irTypeToSwift(type);
|
|
500
754
|
return `some IntentResult & ReturnsValue<${swift}>`;
|
|
@@ -505,8 +759,11 @@ function generateReturnSignature(type) {
|
|
|
505
759
|
}
|
|
506
760
|
return `some IntentResult`;
|
|
507
761
|
}
|
|
508
|
-
function generatePerformReturn(type) {
|
|
762
|
+
function generatePerformReturn(type, customResultType) {
|
|
509
763
|
const indent = " ";
|
|
764
|
+
if (customResultType) {
|
|
765
|
+
return `${indent}// TODO: Return a ${customResultType} instance`;
|
|
766
|
+
}
|
|
510
767
|
if (type.kind === "primitive") {
|
|
511
768
|
return `${indent}return .result(value: ${defaultLiteralFor(type.value)})`;
|
|
512
769
|
}
|
|
@@ -579,21 +836,21 @@ function validateIntent(intent) {
|
|
|
579
836
|
suggestion: "Add a description explaining what this intent does"
|
|
580
837
|
});
|
|
581
838
|
}
|
|
582
|
-
for (const
|
|
583
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(
|
|
839
|
+
for (const param2 of intent.parameters) {
|
|
840
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(param2.name)) {
|
|
584
841
|
diagnostics.push({
|
|
585
842
|
code: "AX103",
|
|
586
843
|
severity: "error",
|
|
587
|
-
message: `Parameter name "${
|
|
844
|
+
message: `Parameter name "${param2.name}" is not a valid Swift identifier`,
|
|
588
845
|
file: intent.sourceFile,
|
|
589
|
-
suggestion: `Rename to "${
|
|
846
|
+
suggestion: `Rename to "${param2.name.replace(/[^a-zA-Z0-9_]/g, "_")}"`
|
|
590
847
|
});
|
|
591
848
|
}
|
|
592
|
-
if (!
|
|
849
|
+
if (!param2.description || param2.description.trim().length === 0) {
|
|
593
850
|
diagnostics.push({
|
|
594
851
|
code: "AX104",
|
|
595
852
|
severity: "warning",
|
|
596
|
-
message: `Parameter "${
|
|
853
|
+
message: `Parameter "${param2.name}" has no description \u2014 Siri will display it without context`,
|
|
597
854
|
file: intent.sourceFile,
|
|
598
855
|
suggestion: "Add a description for better Siri/Shortcuts display"
|
|
599
856
|
});
|
|
@@ -617,17 +874,17 @@ function validateIntent(intent) {
|
|
|
617
874
|
});
|
|
618
875
|
}
|
|
619
876
|
const seen = /* @__PURE__ */ new Set();
|
|
620
|
-
for (const
|
|
621
|
-
if (seen.has(
|
|
877
|
+
for (const param2 of intent.parameters) {
|
|
878
|
+
if (seen.has(param2.name)) {
|
|
622
879
|
diagnostics.push({
|
|
623
880
|
code: "AX107",
|
|
624
881
|
severity: "error",
|
|
625
|
-
message: `Duplicate parameter name "${
|
|
882
|
+
message: `Duplicate parameter name "${param2.name}"`,
|
|
626
883
|
file: intent.sourceFile,
|
|
627
884
|
suggestion: "Each parameter in a single intent must have a unique name"
|
|
628
885
|
});
|
|
629
886
|
}
|
|
630
|
-
seen.add(
|
|
887
|
+
seen.add(param2.name);
|
|
631
888
|
}
|
|
632
889
|
for (const ent of intent.entitlements ?? []) {
|
|
633
890
|
if (!/^[a-zA-Z0-9._-]+$/.test(ent) || !ent.includes(".")) {
|
|
@@ -651,6 +908,54 @@ function validateIntent(intent) {
|
|
|
651
908
|
});
|
|
652
909
|
}
|
|
653
910
|
}
|
|
911
|
+
if (intent.entities) {
|
|
912
|
+
for (const entity of intent.entities) {
|
|
913
|
+
diagnostics.push(...validateEntity(entity, intent.sourceFile));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return diagnostics;
|
|
917
|
+
}
|
|
918
|
+
function validateEntity(entity, sourceFile) {
|
|
919
|
+
const diagnostics = [];
|
|
920
|
+
if (!entity.name || !/^[A-Z][a-zA-Z0-9]*$/.test(entity.name)) {
|
|
921
|
+
diagnostics.push({
|
|
922
|
+
code: "AX110",
|
|
923
|
+
severity: "error",
|
|
924
|
+
message: `Entity name "${entity.name}" must be PascalCase (e.g., "Task", "Playlist")`,
|
|
925
|
+
file: sourceFile,
|
|
926
|
+
suggestion: `Rename to "${toPascalCase(entity.name)}"`
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (entity.properties.length === 0) {
|
|
930
|
+
diagnostics.push({
|
|
931
|
+
code: "AX111",
|
|
932
|
+
severity: "error",
|
|
933
|
+
message: `Entity "${entity.name}" must have at least one property`,
|
|
934
|
+
file: sourceFile,
|
|
935
|
+
suggestion: "Add properties to define the entity's structure"
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const titleProp = entity.displayRepresentation.title;
|
|
939
|
+
const propertyNames = new Set(entity.properties.map((p) => p.name));
|
|
940
|
+
if (titleProp && !propertyNames.has(titleProp)) {
|
|
941
|
+
diagnostics.push({
|
|
942
|
+
code: "AX112",
|
|
943
|
+
severity: "warning",
|
|
944
|
+
message: `Display title "${titleProp}" does not reference an existing property`,
|
|
945
|
+
file: sourceFile,
|
|
946
|
+
suggestion: `Available properties: ${[...propertyNames].join(", ")}`
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
const validQueryTypes = ["all", "id", "string", "property"];
|
|
950
|
+
if (!validQueryTypes.includes(entity.queryType)) {
|
|
951
|
+
diagnostics.push({
|
|
952
|
+
code: "AX113",
|
|
953
|
+
severity: "error",
|
|
954
|
+
message: `Entity query type "${entity.queryType}" is not valid`,
|
|
955
|
+
file: sourceFile,
|
|
956
|
+
suggestion: `Use one of: ${validQueryTypes.join(", ")}`
|
|
957
|
+
});
|
|
958
|
+
}
|
|
654
959
|
return diagnostics;
|
|
655
960
|
}
|
|
656
961
|
function validateSwiftSource(swift) {
|
|
@@ -704,7 +1009,6 @@ function compileFile(filePath, options = {}) {
|
|
|
704
1009
|
return compileSource(source, filePath, options);
|
|
705
1010
|
}
|
|
706
1011
|
function compileSource(source, fileName = "<stdin>", options = {}) {
|
|
707
|
-
const diagnostics = [];
|
|
708
1012
|
let ir;
|
|
709
1013
|
try {
|
|
710
1014
|
ir = parseIntentSource(source, fileName);
|
|
@@ -726,6 +1030,10 @@ function compileSource(source, fileName = "<stdin>", options = {}) {
|
|
|
726
1030
|
}
|
|
727
1031
|
throw err;
|
|
728
1032
|
}
|
|
1033
|
+
return compileFromIR(ir, options);
|
|
1034
|
+
}
|
|
1035
|
+
function compileFromIR(ir, options = {}) {
|
|
1036
|
+
const diagnostics = [];
|
|
729
1037
|
const irDiagnostics = validateIntent(ir);
|
|
730
1038
|
diagnostics.push(...irDiagnostics);
|
|
731
1039
|
if (irDiagnostics.some((d) => d.severity === "error")) {
|
|
@@ -756,20 +1064,262 @@ function compileSource(source, fileName = "<stdin>", options = {}) {
|
|
|
756
1064
|
diagnostics
|
|
757
1065
|
};
|
|
758
1066
|
}
|
|
1067
|
+
var VALID_PRIMITIVES = /* @__PURE__ */ new Set([
|
|
1068
|
+
"string",
|
|
1069
|
+
"int",
|
|
1070
|
+
"double",
|
|
1071
|
+
"float",
|
|
1072
|
+
"boolean",
|
|
1073
|
+
"date",
|
|
1074
|
+
"duration",
|
|
1075
|
+
"url"
|
|
1076
|
+
]);
|
|
1077
|
+
function irFromJSON(data) {
|
|
1078
|
+
const parameters = (data.parameters ?? []).map(
|
|
1079
|
+
(p) => {
|
|
1080
|
+
const param2 = p;
|
|
1081
|
+
return {
|
|
1082
|
+
name: param2.name,
|
|
1083
|
+
type: normalizeIRType(param2.type),
|
|
1084
|
+
title: param2.title ?? param2.description ?? "",
|
|
1085
|
+
description: param2.description ?? "",
|
|
1086
|
+
isOptional: param2.optional ?? param2.isOptional ?? false,
|
|
1087
|
+
defaultValue: param2.default ?? param2.defaultValue
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
return {
|
|
1092
|
+
name: data.name,
|
|
1093
|
+
title: data.title,
|
|
1094
|
+
description: data.description,
|
|
1095
|
+
domain: data.domain,
|
|
1096
|
+
parameters,
|
|
1097
|
+
returnType: data.returnType ? normalizeIRType(data.returnType) : { kind: "primitive", value: "string" },
|
|
1098
|
+
sourceFile: data.sourceFile ?? void 0,
|
|
1099
|
+
entitlements: data.entitlements ?? void 0,
|
|
1100
|
+
infoPlistKeys: data.infoPlistKeys ?? void 0,
|
|
1101
|
+
isDiscoverable: data.isDiscoverable ?? true
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function normalizeIRType(type) {
|
|
1105
|
+
if (typeof type === "string") {
|
|
1106
|
+
const normalized = type === "number" ? "int" : type;
|
|
1107
|
+
if (VALID_PRIMITIVES.has(normalized)) {
|
|
1108
|
+
return { kind: "primitive", value: normalized };
|
|
1109
|
+
}
|
|
1110
|
+
return { kind: "primitive", value: "string" };
|
|
1111
|
+
}
|
|
1112
|
+
if (type && typeof type === "object") {
|
|
1113
|
+
const t = type;
|
|
1114
|
+
if (t.kind === "primitive") return type;
|
|
1115
|
+
if (t.kind === "array")
|
|
1116
|
+
return { kind: "array", elementType: normalizeIRType(t.elementType) };
|
|
1117
|
+
if (t.kind === "optional")
|
|
1118
|
+
return { kind: "optional", innerType: normalizeIRType(t.innerType) };
|
|
1119
|
+
if (t.kind === "entity") return type;
|
|
1120
|
+
if (t.kind === "enum") return type;
|
|
1121
|
+
}
|
|
1122
|
+
return { kind: "primitive", value: "string" };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/core/eject.ts
|
|
1126
|
+
function ejectIntent(source, fileName, options = {}) {
|
|
1127
|
+
const compileResult = compileSource(source, fileName, {
|
|
1128
|
+
validate: true,
|
|
1129
|
+
emitInfoPlist: true,
|
|
1130
|
+
emitEntitlements: true
|
|
1131
|
+
});
|
|
1132
|
+
if (!compileResult.success || !compileResult.output) {
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`Compilation failed: ${compileResult.diagnostics.filter((d) => d.severity === "error").map((d) => d.message).join("; ")}`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const { ir, swiftCode, infoPlistFragment, entitlementsFragment } = compileResult.output;
|
|
1138
|
+
const outDir = options.outDir ?? ".";
|
|
1139
|
+
const ejectedSwift = transformSwiftForEject(swiftCode, ir);
|
|
1140
|
+
const intentFileName = `${ir.name}Intent.swift`;
|
|
1141
|
+
const swiftPath = `${outDir}/${intentFileName}`;
|
|
1142
|
+
const plistPath = infoPlistFragment ? `${outDir}/${ir.name}Intent.plist.fragment.xml` : null;
|
|
1143
|
+
const entitlementsPath = entitlementsFragment ? `${outDir}/${ir.name}Intent.entitlements.fragment.xml` : null;
|
|
1144
|
+
const testPath = options.includeTests ? `${outDir}/${ir.name}IntentTests.swift` : null;
|
|
1145
|
+
const result = {
|
|
1146
|
+
swiftFile: {
|
|
1147
|
+
path: swiftPath,
|
|
1148
|
+
content: ejectedSwift
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
if (infoPlistFragment && plistPath) {
|
|
1152
|
+
result.infoPlist = {
|
|
1153
|
+
path: plistPath,
|
|
1154
|
+
content: infoPlistFragment
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
if (entitlementsFragment && entitlementsPath) {
|
|
1158
|
+
result.entitlements = {
|
|
1159
|
+
path: entitlementsPath,
|
|
1160
|
+
content: entitlementsFragment
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (options.includeTests && testPath) {
|
|
1164
|
+
result.testFile = {
|
|
1165
|
+
path: testPath,
|
|
1166
|
+
content: generateTestFile(ir)
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
return result;
|
|
1170
|
+
}
|
|
1171
|
+
function transformSwiftForEject(swiftCode, ir) {
|
|
1172
|
+
const lines = swiftCode.split("\n");
|
|
1173
|
+
const result = [];
|
|
1174
|
+
let inHeader = true;
|
|
1175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1176
|
+
const line = lines[i];
|
|
1177
|
+
if (inHeader && line.startsWith("// ")) {
|
|
1178
|
+
if (i === 0) {
|
|
1179
|
+
result.push(`// ${ir.name}Intent.swift`);
|
|
1180
|
+
} else if (line.includes("Generated by Axint")) {
|
|
1181
|
+
result.push(
|
|
1182
|
+
`// Originally generated by Axint (https://github.com/agenticempire/axint)`
|
|
1183
|
+
);
|
|
1184
|
+
} else if (line.includes("Do not edit manually")) {
|
|
1185
|
+
} else {
|
|
1186
|
+
result.push(line);
|
|
1187
|
+
}
|
|
1188
|
+
} else if (line === "") {
|
|
1189
|
+
inHeader = false;
|
|
1190
|
+
result.push(line);
|
|
1191
|
+
} else {
|
|
1192
|
+
inHeader = false;
|
|
1193
|
+
if (line.includes("// TODO: Implement your intent logic here.")) {
|
|
1194
|
+
result.push(` // TODO: Implement your intent logic here.`);
|
|
1195
|
+
result.push(` //`);
|
|
1196
|
+
result.push(
|
|
1197
|
+
` // For more information about App Intents, see:`
|
|
1198
|
+
);
|
|
1199
|
+
result.push(
|
|
1200
|
+
` // https://developer.apple.com/documentation/appintents`
|
|
1201
|
+
);
|
|
1202
|
+
} else {
|
|
1203
|
+
result.push(line);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return result.join("\n");
|
|
1208
|
+
}
|
|
1209
|
+
function generateTestFile(ir) {
|
|
1210
|
+
const lines = [];
|
|
1211
|
+
lines.push(`// ${ir.name}IntentTests.swift`);
|
|
1212
|
+
lines.push(`// Ejected from Axint`);
|
|
1213
|
+
lines.push(``);
|
|
1214
|
+
lines.push(`import XCTest`);
|
|
1215
|
+
lines.push(`import AppIntents`);
|
|
1216
|
+
lines.push(``);
|
|
1217
|
+
lines.push(`final class ${ir.name}IntentTests: XCTestCase {`);
|
|
1218
|
+
lines.push(``);
|
|
1219
|
+
lines.push(` func testIntentInitialization() throws {`);
|
|
1220
|
+
lines.push(` let intent = ${ir.name}Intent()`);
|
|
1221
|
+
lines.push(` XCTAssertEqual(${ir.name}Intent.title.stringValue, "${ir.title}")`);
|
|
1222
|
+
lines.push(` }`);
|
|
1223
|
+
lines.push(``);
|
|
1224
|
+
lines.push(` func testIntentPerform() async throws {`);
|
|
1225
|
+
lines.push(` let intent = ${ir.name}Intent()`);
|
|
1226
|
+
lines.push(` // TODO: Set up intent parameters and test perform()`);
|
|
1227
|
+
lines.push(` // let result = try await intent.perform()`);
|
|
1228
|
+
lines.push(` // XCTAssertNotNil(result)`);
|
|
1229
|
+
lines.push(` }`);
|
|
1230
|
+
lines.push(``);
|
|
1231
|
+
lines.push(`}`);
|
|
1232
|
+
lines.push(``);
|
|
1233
|
+
return lines.join("\n");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/sdk/index.ts
|
|
1237
|
+
function make(type) {
|
|
1238
|
+
return (description, config) => ({
|
|
1239
|
+
type,
|
|
1240
|
+
description,
|
|
1241
|
+
...config
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
var param = {
|
|
1245
|
+
/** String parameter → Swift `String` */
|
|
1246
|
+
string: make("string"),
|
|
1247
|
+
/** 64-bit signed integer → Swift `Int` */
|
|
1248
|
+
int: make("int"),
|
|
1249
|
+
/** Double-precision float → Swift `Double` */
|
|
1250
|
+
double: make("double"),
|
|
1251
|
+
/** Single-precision float → Swift `Float` */
|
|
1252
|
+
float: make("float"),
|
|
1253
|
+
/** Boolean parameter → Swift `Bool` */
|
|
1254
|
+
boolean: make("boolean"),
|
|
1255
|
+
/** Date parameter → Swift `Date` */
|
|
1256
|
+
date: make("date"),
|
|
1257
|
+
/** Duration parameter → Swift `Measurement<UnitDuration>` */
|
|
1258
|
+
duration: make("duration"),
|
|
1259
|
+
/** URL parameter → Swift `URL` */
|
|
1260
|
+
url: make("url"),
|
|
1261
|
+
/**
|
|
1262
|
+
* @deprecated Use `param.int` (or `param.double` / `param.float`) for
|
|
1263
|
+
* explicit Swift numeric fidelity. `param.number` is kept as an alias
|
|
1264
|
+
* for `param.int` to preserve v0.1.x compatibility and will be removed
|
|
1265
|
+
* in v1.0.0.
|
|
1266
|
+
*/
|
|
1267
|
+
number: make("number"),
|
|
1268
|
+
/**
|
|
1269
|
+
* Entity reference parameter. The entity name must match a
|
|
1270
|
+
* `defineEntity()` call in the same file or project.
|
|
1271
|
+
*/
|
|
1272
|
+
entity: (entityName, description, config) => ({
|
|
1273
|
+
type: "entity",
|
|
1274
|
+
entityName,
|
|
1275
|
+
description,
|
|
1276
|
+
...config
|
|
1277
|
+
}),
|
|
1278
|
+
/**
|
|
1279
|
+
* Parameter with dynamic option suggestions provided at runtime
|
|
1280
|
+
* by a DynamicOptionsProvider. The `providerName` maps to a
|
|
1281
|
+
* generated Swift `DynamicOptionsProvider` struct.
|
|
1282
|
+
*/
|
|
1283
|
+
dynamicOptions: (providerName, innerParam) => {
|
|
1284
|
+
const { type: innerType, description, ...rest } = innerParam;
|
|
1285
|
+
return {
|
|
1286
|
+
type: "dynamicOptions",
|
|
1287
|
+
providerName,
|
|
1288
|
+
innerType,
|
|
1289
|
+
description,
|
|
1290
|
+
...rest
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
function defineIntent(config) {
|
|
1295
|
+
return config;
|
|
1296
|
+
}
|
|
1297
|
+
function defineEntity(config) {
|
|
1298
|
+
return config;
|
|
1299
|
+
}
|
|
759
1300
|
export {
|
|
760
1301
|
LEGACY_PARAM_ALIASES,
|
|
761
1302
|
PARAM_TYPES,
|
|
762
1303
|
ParserError,
|
|
763
1304
|
SWIFT_TYPE_MAP,
|
|
764
1305
|
compileFile,
|
|
1306
|
+
compileFromIR,
|
|
765
1307
|
compileSource,
|
|
1308
|
+
defineEntity,
|
|
1309
|
+
defineIntent,
|
|
1310
|
+
ejectIntent,
|
|
766
1311
|
escapeSwiftString,
|
|
767
1312
|
escapeXml,
|
|
768
1313
|
generateEntitlementsFragment,
|
|
1314
|
+
generateEntity,
|
|
1315
|
+
generateEntityQuery,
|
|
769
1316
|
generateInfoPlistFragment,
|
|
770
1317
|
generateSwift,
|
|
1318
|
+
irFromJSON,
|
|
771
1319
|
irTypeToSwift,
|
|
1320
|
+
param,
|
|
772
1321
|
parseIntentSource,
|
|
1322
|
+
validateEntity,
|
|
773
1323
|
validateIntent,
|
|
774
1324
|
validateSwiftSource
|
|
775
1325
|
};
|