@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.
@@ -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: { kind: "primitive", value: resolvedType }
226
- } : { kind: "primitive", value: resolvedType };
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
- const descriptionArg = expr.arguments[0];
259
- const configArg = expr.arguments[1];
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 as the first argument`,
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 LEGACY_PARAM_ALIASES[typeName];
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 param of intent.parameters) {
415
- lines.push(generateParameter(param));
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(intent.returnType);
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
- lines.push(generatePerformReturn(intent.returnType));
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(param) {
479
- const swiftType = irTypeToSwift(param.type);
729
+ function generateParameter(param2) {
730
+ const swiftType = irTypeToSwift(param2.type);
480
731
  const lines = [];
481
732
  const attrs = [];
482
- attrs.push(`title: "${escapeSwiftString(param.title)}"`);
483
- if (param.description) {
484
- attrs.push(`description: "${escapeSwiftString(param.description)}"`);
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 (param.defaultValue !== void 0) {
489
- const defaultStr = formatSwiftDefault(param.defaultValue, param.type);
490
- lines.push(` var ${param.name}: ${swiftType} = ${defaultStr}`);
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 ${param.name}: ${swiftType}`);
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 param of intent.parameters) {
583
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(param.name)) {
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 "${param.name}" is not a valid Swift identifier`,
844
+ message: `Parameter name "${param2.name}" is not a valid Swift identifier`,
588
845
  file: intent.sourceFile,
589
- suggestion: `Rename to "${param.name.replace(/[^a-zA-Z0-9_]/g, "_")}"`
846
+ suggestion: `Rename to "${param2.name.replace(/[^a-zA-Z0-9_]/g, "_")}"`
590
847
  });
591
848
  }
592
- if (!param.description || param.description.trim().length === 0) {
849
+ if (!param2.description || param2.description.trim().length === 0) {
593
850
  diagnostics.push({
594
851
  code: "AX104",
595
852
  severity: "warning",
596
- message: `Parameter "${param.name}" has no description \u2014 Siri will display it without context`,
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 param of intent.parameters) {
621
- if (seen.has(param.name)) {
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 "${param.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(param.name);
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
  };