@kithinji/pod 1.0.22 → 1.0.24

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/dist/main.js CHANGED
@@ -928,79 +928,144 @@ async function expandMacros(source, filePath, projectRoot = process.cwd()) {
928
928
  import * as path4 from "path";
929
929
  import { parseSync } from "@swc/core";
930
930
  function generateController(filePath, code) {
931
- const ast = parseSync(code, {
932
- syntax: "typescript",
933
- tsx: filePath.endsWith("x"),
934
- decorators: true
935
- });
936
- const serviceInfo = extractServiceInfo(ast);
937
- if (!serviceInfo || !serviceInfo.hasInjectable) return null;
938
- return generateControllerCode(serviceInfo, filePath);
931
+ try {
932
+ const ast = parseSync(code, {
933
+ syntax: "typescript",
934
+ tsx: filePath.endsWith("x") || filePath.endsWith(".tsx"),
935
+ decorators: true
936
+ });
937
+ const serviceInfo = extractServiceInfo(ast, filePath);
938
+ if (!serviceInfo || !serviceInfo.hasInjectable) {
939
+ return null;
940
+ }
941
+ validateServiceInfo(serviceInfo, filePath);
942
+ return generateControllerCode(serviceInfo, filePath);
943
+ } catch (error) {
944
+ if (error.type) {
945
+ throw error;
946
+ }
947
+ throw {
948
+ type: "parse",
949
+ message: `Failed to parse TypeScript file: ${error.message}`,
950
+ filePath,
951
+ details: error
952
+ };
953
+ }
939
954
  }
940
- function extractServiceInfo(ast) {
955
+ function extractServiceInfo(ast, filePath) {
941
956
  let serviceClass = null;
942
957
  let hasInjectable = false;
943
958
  const importMap = {};
944
- for (const item of ast.body) {
945
- if (item.type === "ImportDeclaration") {
946
- const decl = item;
947
- const source = decl.source.value;
948
- decl.specifiers.forEach((spec) => {
949
- if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier" || spec.type === "ImportNamespaceSpecifier") {
950
- importMap[spec.local.value] = source;
959
+ try {
960
+ for (const item of ast.body) {
961
+ if (item.type === "ImportDeclaration") {
962
+ const decl = item;
963
+ const source = decl.source.value;
964
+ decl.specifiers.forEach((spec) => {
965
+ if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier" || spec.type === "ImportNamespaceSpecifier") {
966
+ importMap[spec.local.value] = source;
967
+ }
968
+ });
969
+ }
970
+ if (item.type === "ExportDeclaration" && item.declaration?.type === "ClassDeclaration") {
971
+ const classDecl = item.declaration;
972
+ if (hasInjectableDecorator(classDecl.decorators)) {
973
+ serviceClass = classDecl;
974
+ hasInjectable = true;
951
975
  }
952
- });
953
- }
954
- if (item.type === "ExportDeclaration" && item.declaration.type === "ClassDeclaration") {
955
- const classDecl = item.declaration;
956
- if (hasInjectableDecorator(classDecl.decorators)) {
957
- serviceClass = classDecl;
958
- hasInjectable = true;
959
976
  }
960
977
  }
978
+ if (!serviceClass?.identifier) {
979
+ return null;
980
+ }
981
+ return {
982
+ className: serviceClass.identifier.value,
983
+ methods: extractMethods(serviceClass, filePath),
984
+ hasInjectable,
985
+ importMap
986
+ };
987
+ } catch (error) {
988
+ throw {
989
+ type: "parse",
990
+ message: `Failed to extract service info: ${error.message}`,
991
+ filePath,
992
+ details: error
993
+ };
961
994
  }
962
- if (!serviceClass || !serviceClass.identifier) return null;
963
- return {
964
- className: serviceClass.identifier.value,
965
- methods: extractMethods(serviceClass),
966
- hasInjectable,
967
- importMap
968
- };
969
995
  }
970
996
  function hasInjectableDecorator(decorators) {
971
- return decorators?.some((d) => {
997
+ if (!decorators) return false;
998
+ return decorators.some((d) => {
972
999
  const expr = d.expression;
973
1000
  return expr.type === "Identifier" && expr.value === "Injectable" || expr.type === "CallExpression" && expr.callee.type === "Identifier" && expr.callee.value === "Injectable";
974
- }) ?? false;
1001
+ });
975
1002
  }
976
- function extractMethods(classDecl) {
1003
+ function extractMethods(classDecl, filePath) {
977
1004
  const methods = [];
1005
+ const className = classDecl.identifier?.value || "UnknownClass";
978
1006
  for (const member of classDecl.body) {
979
- if (member.type === "ClassMethod" && member.accessibility === "public") {
980
- const method = member;
981
- const methodName = method.key.type === "Identifier" ? method.key.value : "";
982
- if (!methodName) continue;
983
- if (!method.function.async) {
984
- throw new Error(
985
- `Server action ${classDecl.identifier.value}.${methodName} must be async.`
986
- );
987
- }
988
- const { paramSchemas, returnSchema } = extractSignature(
989
- method.function.decorators,
990
- method.function.params.length
991
- );
992
- methods.push({
993
- name: methodName,
994
- params: extractMethodParams(method.function.params),
995
- returnType: extractReturnType(method.function.returnType),
996
- isAsync: true,
997
- paramSchemas,
998
- returnSchema
999
- });
1007
+ if (!isPublicMethod(member)) continue;
1008
+ const method = member;
1009
+ const methodName = getMethodName(method);
1010
+ if (!methodName) continue;
1011
+ const returnTypeInfo = analyzeReturnType(method);
1012
+ if (!returnTypeInfo.isObservable && !method.function.async) {
1013
+ throw {
1014
+ type: "validation",
1015
+ message: `Method ${className}.${methodName} must be async or return an Observable`,
1016
+ filePath,
1017
+ details: { className, methodName }
1018
+ };
1000
1019
  }
1020
+ const { paramSchemas, returnSchema } = extractSignature(
1021
+ method.function.decorators,
1022
+ method.function.params.length
1023
+ );
1024
+ methods.push({
1025
+ name: methodName,
1026
+ params: extractMethodParams(method.function.params),
1027
+ returnType: returnTypeInfo.type,
1028
+ isAsync: method.function.async,
1029
+ isObservable: returnTypeInfo.isObservable,
1030
+ paramSchemas,
1031
+ returnSchema
1032
+ });
1001
1033
  }
1002
1034
  return methods;
1003
1035
  }
1036
+ function isPublicMethod(member) {
1037
+ return member.type === "ClassMethod" && (member.accessibility === "public" || !member.accessibility);
1038
+ }
1039
+ function getMethodName(method) {
1040
+ if (method.key.type === "Identifier") {
1041
+ return method.key.value;
1042
+ }
1043
+ return null;
1044
+ }
1045
+ function analyzeReturnType(method) {
1046
+ const returnType = method.function.returnType?.typeAnnotation;
1047
+ if (!returnType) {
1048
+ return { type: "any", isObservable: false };
1049
+ }
1050
+ if (returnType.type === "TsTypeReference" && returnType.typeName.type === "Identifier" && returnType.typeName.value === "Observable") {
1051
+ const innerType = returnType.typeParams?.params[0];
1052
+ return {
1053
+ type: innerType ? stringifyType(innerType) : "any",
1054
+ isObservable: true
1055
+ };
1056
+ }
1057
+ if (returnType.type === "TsTypeReference" && returnType.typeName.type === "Identifier" && returnType.typeName.value === "Promise") {
1058
+ const innerType = returnType.typeParams?.params[0];
1059
+ return {
1060
+ type: innerType ? stringifyType(innerType) : "any",
1061
+ isObservable: false
1062
+ };
1063
+ }
1064
+ return {
1065
+ type: stringifyType(returnType),
1066
+ isObservable: false
1067
+ };
1068
+ }
1004
1069
  function extractSignature(decorators, paramCount) {
1005
1070
  if (!decorators) return { paramSchemas: [] };
1006
1071
  for (const decorator of decorators) {
@@ -1023,9 +1088,14 @@ function extractSignature(decorators, paramCount) {
1023
1088
  return { paramSchemas: [] };
1024
1089
  }
1025
1090
  function stringifyExpression(expr) {
1026
- if (expr.type === "Identifier") return expr.value;
1091
+ if (!expr) return "any";
1092
+ if (expr.type === "Identifier") {
1093
+ return expr.value;
1094
+ }
1027
1095
  if (expr.type === "MemberExpression") {
1028
- return `${stringifyExpression(expr.object)}.${expr.property.value || ""}`;
1096
+ const object = stringifyExpression(expr.object);
1097
+ const property = expr.property.value || stringifyExpression(expr.property);
1098
+ return `${object}.${property}`;
1029
1099
  }
1030
1100
  if (expr.type === "CallExpression") {
1031
1101
  const args = expr.arguments.map((a) => stringifyExpression(a.expression)).join(", ");
@@ -1036,6 +1106,13 @@ function stringifyExpression(expr) {
1036
1106
  function extractMethodParams(params) {
1037
1107
  return params.map((p) => {
1038
1108
  const pat = p.pat;
1109
+ if (pat.type !== "Identifier") {
1110
+ return {
1111
+ name: "param",
1112
+ type: "any",
1113
+ decorators: []
1114
+ };
1115
+ }
1039
1116
  return {
1040
1117
  name: pat.value,
1041
1118
  type: pat.typeAnnotation ? stringifyType(pat.typeAnnotation.typeAnnotation) : "any",
@@ -1043,97 +1120,191 @@ function extractMethodParams(params) {
1043
1120
  };
1044
1121
  });
1045
1122
  }
1046
- function extractReturnType(node) {
1047
- if (!node?.typeAnnotation) return "any";
1048
- const type = node.typeAnnotation;
1049
- if (type.type === "TsTypeReference" && type.typeName.value === "Promise") {
1050
- return stringifyType(type.typeParams?.params[0]);
1051
- }
1052
- return stringifyType(type);
1053
- }
1054
1123
  function stringifyType(node) {
1055
1124
  if (!node) return "any";
1056
1125
  switch (node.type) {
1057
1126
  case "TsKeywordType":
1058
1127
  return node.kind;
1059
1128
  case "TsTypeReference":
1129
+ if (node.typeName.type !== "Identifier") return "any";
1060
1130
  const base = node.typeName.value;
1061
- const args = node.typeParams ? `<${node.typeParams.params.map(stringifyType).join(", ")}>` : "";
1131
+ const args = node.typeParams?.params ? `<${node.typeParams.params.map(stringifyType).join(", ")}>` : "";
1062
1132
  return base + args;
1063
1133
  case "TsArrayType":
1064
1134
  return `${stringifyType(node.elemType)}[]`;
1135
+ case "TsUnionType":
1136
+ return node.types.map(stringifyType).join(" | ");
1137
+ case "TsIntersectionType":
1138
+ return node.types.map(stringifyType).join(" & ");
1065
1139
  default:
1066
1140
  return "any";
1067
1141
  }
1068
1142
  }
1143
+ function validateServiceInfo(serviceInfo, filePath) {
1144
+ if (!serviceInfo.className) {
1145
+ throw {
1146
+ type: "validation",
1147
+ message: "Service class must have a valid name",
1148
+ filePath
1149
+ };
1150
+ }
1151
+ if (serviceInfo.methods.length === 0) {
1152
+ console.warn(
1153
+ `Warning: Service ${serviceInfo.className} has no public methods`
1154
+ );
1155
+ }
1156
+ serviceInfo.methods.forEach((method) => {
1157
+ if (method.params.length > 0 && method.paramSchemas.length === 0) {
1158
+ console.warn(
1159
+ `Warning: Method ${serviceInfo.className}.${method.name} has parameters but no @Signature validation`
1160
+ );
1161
+ }
1162
+ });
1163
+ }
1069
1164
  function generateControllerCode(serviceInfo, filePath) {
1070
1165
  const serviceName = serviceInfo.className;
1071
- const controllerName = serviceName.replace(/Service$/, "AutoController");
1072
- const serviceImportPath = `./${path4.basename(filePath).replace(/\.ts$/, "")}`;
1166
+ const controllerName = serviceName.replace(/Service$/, "GenController");
1167
+ const serviceImportPath = getImportPath(filePath);
1168
+ const controllerPath = serviceNameToPath(serviceName);
1169
+ const imports = generateImports(serviceInfo, serviceName, serviceImportPath);
1170
+ const methods = generateMethods(serviceInfo);
1171
+ const serviceInstance = toInstanceName(serviceName);
1172
+ return `${imports}
1173
+
1174
+ @Controller("/${controllerPath}", {
1175
+ providedIn: "root",
1176
+ })
1177
+ export class ${controllerName} {
1178
+ constructor(private readonly ${serviceInstance}: ${serviceName}) {}
1179
+
1180
+ ${methods}
1181
+ }`;
1182
+ }
1183
+ function getImportPath(filePath) {
1184
+ const basename3 = path4.basename(filePath);
1185
+ return `./${basename3.replace(/\.tsx?$/, "")}`;
1186
+ }
1187
+ function serviceNameToPath(serviceName) {
1188
+ return serviceName.replace(/Service$/, "").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1189
+ }
1190
+ function toInstanceName(className) {
1191
+ return className.charAt(0).toLowerCase() + className.slice(1);
1192
+ }
1193
+ function generateImports(serviceInfo, serviceName, serviceImportPath) {
1073
1194
  const importGroups = /* @__PURE__ */ new Map();
1074
1195
  const registerIdentifier = (id) => {
1075
1196
  const source = serviceInfo.importMap[id] || serviceImportPath;
1076
- if (!importGroups.has(source)) importGroups.set(source, /* @__PURE__ */ new Set());
1197
+ if (!importGroups.has(source)) {
1198
+ importGroups.set(source, /* @__PURE__ */ new Set());
1199
+ }
1077
1200
  importGroups.get(source).add(id);
1078
1201
  };
1079
1202
  serviceInfo.methods.forEach((m) => {
1080
1203
  [...m.paramSchemas, m.returnSchema].filter(Boolean).forEach((s) => {
1081
1204
  const matches = s.match(/[A-Z][a-zA-Z0-9]*/g);
1082
1205
  matches?.forEach(registerIdentifier);
1083
- if (s.includes("z.")) registerIdentifier("z");
1206
+ if (s.includes("z.")) {
1207
+ registerIdentifier("z");
1208
+ }
1084
1209
  });
1085
1210
  });
1086
- let importStrings = `import { Controller, Post, Get, Body } from "@kithinji/orca";
1211
+ const hasPost = serviceInfo.methods.some(
1212
+ (m) => !m.isObservable && m.params.length > 0
1213
+ );
1214
+ const hasGet = serviceInfo.methods.some(
1215
+ (m) => !m.isObservable && m.params.length === 0
1216
+ );
1217
+ const hasSse = serviceInfo.methods.some((m) => m.isObservable);
1218
+ const hasSseWithParams = serviceInfo.methods.some(
1219
+ (m) => m.isObservable && m.params.length > 0
1220
+ );
1221
+ const decorators = ["Controller"];
1222
+ if (hasPost) decorators.push("Post");
1223
+ if (hasGet) decorators.push("Get");
1224
+ if (hasPost) decorators.push("Body");
1225
+ if (hasSse) decorators.push("Sse");
1226
+ if (hasSseWithParams) decorators.push("Query");
1227
+ let importStrings = `import { ${decorators.join(
1228
+ ", "
1229
+ )} } from "@kithinji/orca";
1087
1230
  `;
1088
1231
  importGroups.forEach((ids, source) => {
1089
1232
  const filteredIds = Array.from(ids).filter((id) => id !== serviceName);
1090
1233
  if (filteredIds.length > 0) {
1091
- importStrings += `
1092
- import { ${filteredIds.join(
1234
+ importStrings += `import { ${filteredIds.join(
1093
1235
  ", "
1094
- )} } from "${source}";`;
1095
- }
1096
- });
1097
- const methods = serviceInfo.methods.map((m) => {
1098
- const hasParams = m.params.length > 0;
1099
- const bodyParam = hasParams ? `@Body() body: any` : "";
1100
- let body = "";
1101
- if (hasParams) {
1102
- if (m.paramSchemas.length > 0) {
1103
- body += ` const b = typeof body === 'object' && body !== null ? body : {};
1104
- `;
1105
- m.params.forEach((p, i) => {
1106
- body += ` const ${p.name} = ${m.paramSchemas[i]}.parse(b.${p.name});
1236
+ )} } from "${source}";
1107
1237
  `;
1108
- });
1109
- } else {
1110
- body += ` const { ${m.params.map((p) => p.name).join(", ")} } = body;
1111
- `;
1112
- }
1113
1238
  }
1114
- const callArgs = m.params.map((p) => p.name).join(", ");
1115
- const serviceCall = `this.${serviceName.charAt(0).toLowerCase() + serviceName.slice(1)}.${m.name}(${callArgs})`;
1116
- if (m.returnSchema) {
1117
- body += ` const res = await ${serviceCall};
1118
- return ${m.returnSchema}.parse(res);`;
1119
- } else {
1120
- body += ` return ${serviceCall};`;
1121
- }
1122
- return ` @${hasParams ? "Post" : "Get"}("${m.name}")
1123
- async ${m.name}(${bodyParam}): Promise<${m.returnType}> {
1239
+ });
1240
+ return importStrings;
1241
+ }
1242
+ function generateMethods(serviceInfo) {
1243
+ return serviceInfo.methods.map((m) => generateMethod(m, serviceInfo.className)).join("\n\n");
1244
+ }
1245
+ function generateMethod(method, serviceName) {
1246
+ const hasParams = method.params.length > 0;
1247
+ const serviceInstance = toInstanceName(serviceName);
1248
+ if (method.isObservable) {
1249
+ const queryParams = hasParams ? method.params.map((p) => `@Query('${p.name}') ${p.name}: ${p.type}`).join(", ") : "";
1250
+ const body2 = generateMethodBody(method, serviceInstance, false);
1251
+ return ` @Sse("${method.name}")
1252
+ ${method.name}(${queryParams}): Observable<${method.returnType}> {
1253
+ ${body2}
1254
+ }`;
1255
+ }
1256
+ const decorator = hasParams ? "Post" : "Get";
1257
+ const bodyParam = hasParams ? `@Body() body: any` : "";
1258
+ const body = generateMethodBody(method, serviceInstance, true);
1259
+ return ` @${decorator}("${method.name}")
1260
+ async ${method.name}(${bodyParam}): Promise<${method.returnType}> {
1124
1261
  ${body}
1125
1262
  }`;
1126
- }).join("\n\n");
1127
- return `${importStrings}
1128
-
1129
- @Controller("/${serviceName}", {
1130
- providedIn: "root",
1131
- })
1132
- export class ${controllerName} {
1133
- constructor(private readonly ${serviceName.charAt(0).toLowerCase() + serviceName.slice(1)}: ${serviceName}) {}
1134
-
1135
- ${methods}
1136
- }`;
1263
+ }
1264
+ function generateMethodBody(method, serviceInstance, isAsync) {
1265
+ const lines = [];
1266
+ const hasParams = method.params.length > 0;
1267
+ if (hasParams && method.isObservable && method.paramSchemas.length > 0) {
1268
+ method.params.forEach((p, i) => {
1269
+ lines.push(
1270
+ ` const validated${capitalize(p.name)} = ${method.paramSchemas[i]}.parse(${p.name});`
1271
+ );
1272
+ });
1273
+ }
1274
+ if (hasParams && !method.isObservable) {
1275
+ if (method.paramSchemas.length > 0) {
1276
+ lines.push(
1277
+ ` const b = typeof body === 'object' && body !== null ? body : {};`
1278
+ );
1279
+ method.params.forEach((p, i) => {
1280
+ lines.push(
1281
+ ` const ${p.name} = ${method.paramSchemas[i]}.parse(b.${p.name});`
1282
+ );
1283
+ });
1284
+ } else {
1285
+ const paramNames = method.params.map((p) => p.name).join(", ");
1286
+ lines.push(` const { ${paramNames} } = body || {};`);
1287
+ }
1288
+ }
1289
+ let callArgs;
1290
+ if (hasParams && method.isObservable && method.paramSchemas.length > 0) {
1291
+ callArgs = method.params.map((p) => `validated${capitalize(p.name)}`).join(", ");
1292
+ } else {
1293
+ callArgs = method.params.map((p) => p.name).join(", ");
1294
+ }
1295
+ const serviceCall = `${serviceInstance}.${method.name}(${callArgs})`;
1296
+ if (method.returnSchema && isAsync) {
1297
+ lines.push(` const res = await this.${serviceCall};`);
1298
+ lines.push(` return ${method.returnSchema}.parse(res);`);
1299
+ } else if (isAsync) {
1300
+ lines.push(` return this.${serviceCall};`);
1301
+ } else {
1302
+ lines.push(` return this.${serviceCall};`);
1303
+ }
1304
+ return lines.join("\n");
1305
+ }
1306
+ function capitalize(str) {
1307
+ return str.charAt(0).toUpperCase() + str.slice(1);
1137
1308
  }
1138
1309
 
1139
1310
  // src/plugins/generators/tsx_server_stub.ts
@@ -2378,7 +2549,7 @@ function extractMethods2(classDecl) {
2378
2549
  continue;
2379
2550
  }
2380
2551
  const params = extractMethodParams2(method.function.params || []);
2381
- const returnType = extractReturnType2(method.function.returnType);
2552
+ const returnType = extractReturnType(method.function.returnType);
2382
2553
  const isAsync = method.function.async || false;
2383
2554
  methods.push({
2384
2555
  name: methodName,
@@ -2407,7 +2578,7 @@ function extractMethodParams2(params) {
2407
2578
  }
2408
2579
  return result;
2409
2580
  }
2410
- function extractReturnType2(returnType) {
2581
+ function extractReturnType(returnType) {
2411
2582
  if (!returnType || !returnType.typeAnnotation) {
2412
2583
  return "any";
2413
2584
  }
@@ -2526,37 +2697,66 @@ ${decoratorsStr}export class ${className} extends _OrcaComponent {
2526
2697
  }`;
2527
2698
  }
2528
2699
 
2529
- // src/plugins/generators/generate_rsc.ts
2700
+ // src/plugins/generators/generate_rpc.ts
2530
2701
  import { parseSync as parseSync4 } from "@swc/core";
2531
- function generateRscStub(filePath, code) {
2532
- const ast = parseSync4(code, {
2533
- syntax: "typescript",
2534
- tsx: filePath.endsWith("x"),
2535
- decorators: true
2536
- });
2537
- const serviceInfo = extractServiceInfo2(ast);
2538
- return generateStubCode2(serviceInfo);
2702
+ function generateRpcStub(filePath, code) {
2703
+ try {
2704
+ const ast = parseSync4(code, {
2705
+ syntax: "typescript",
2706
+ tsx: filePath.endsWith("x") || filePath.endsWith(".tsx"),
2707
+ decorators: true
2708
+ });
2709
+ const serviceInfo = extractServiceInfo2(ast, filePath);
2710
+ validateServiceInfo2(serviceInfo, filePath);
2711
+ return generateStubCode2(serviceInfo);
2712
+ } catch (error) {
2713
+ if (error.type) {
2714
+ throw error;
2715
+ }
2716
+ throw {
2717
+ type: "parse",
2718
+ message: `Failed to parse TypeScript file: ${error.message}`,
2719
+ filePath,
2720
+ details: error
2721
+ };
2722
+ }
2539
2723
  }
2540
- function extractServiceInfo2(ast) {
2724
+ function extractServiceInfo2(ast, filePath) {
2541
2725
  let serviceClass = null;
2542
- for (const item of ast.body) {
2543
- if (item.type === "ExportDeclaration" && item.declaration.type === "ClassDeclaration") {
2544
- const classDecl = item.declaration;
2545
- if (hasInjectableDecorator2(classDecl.decorators)) {
2546
- serviceClass = classDecl;
2547
- break;
2726
+ try {
2727
+ for (const item of ast.body) {
2728
+ if (item.type === "ExportDeclaration" && item.declaration?.type === "ClassDeclaration") {
2729
+ const classDecl = item.declaration;
2730
+ if (hasInjectableDecorator2(classDecl.decorators)) {
2731
+ serviceClass = classDecl;
2732
+ break;
2733
+ }
2548
2734
  }
2549
2735
  }
2736
+ if (!serviceClass?.identifier) {
2737
+ throw {
2738
+ type: "validation",
2739
+ message: "No exported class with @Injectable decorator found",
2740
+ filePath
2741
+ };
2742
+ }
2743
+ const className = serviceClass.identifier.value;
2744
+ const methods = extractMethods3(serviceClass, className, filePath);
2745
+ return {
2746
+ className,
2747
+ methods
2748
+ };
2749
+ } catch (error) {
2750
+ if (error.type) {
2751
+ throw error;
2752
+ }
2753
+ throw {
2754
+ type: "parse",
2755
+ message: `Failed to extract service info: ${error.message}`,
2756
+ filePath,
2757
+ details: error
2758
+ };
2550
2759
  }
2551
- if (!serviceClass || !serviceClass.identifier) {
2552
- throw new Error("Service class is undefined");
2553
- }
2554
- const className = serviceClass.identifier.value;
2555
- const methods = extractMethods3(serviceClass);
2556
- return {
2557
- className,
2558
- methods
2559
- };
2560
2760
  }
2561
2761
  function hasInjectableDecorator2(decorators) {
2562
2762
  if (!decorators) return false;
@@ -2573,65 +2773,81 @@ function hasInjectableDecorator2(decorators) {
2573
2773
  return false;
2574
2774
  });
2575
2775
  }
2576
- function extractMethods3(classDecl) {
2776
+ function extractMethods3(classDecl, className, filePath) {
2577
2777
  const methods = [];
2578
2778
  for (const member of classDecl.body) {
2579
- if (member.type === "ClassMethod" && member.accessibility === "public") {
2580
- const method = member;
2581
- const methodName = method.key.type === "Identifier" ? method.key.value : "";
2582
- if (!methodName) {
2583
- continue;
2584
- }
2585
- if (!method.function.async) {
2586
- throw new Error(
2587
- `Server action ${classDecl.identifier.value}.${methodName} must be async.`
2588
- );
2589
- }
2590
- const params = extractMethodParams3(method.function.params || []);
2591
- const returnType = extractReturnType3(method.function.returnType);
2592
- const isAsync = method.function.async || false;
2593
- methods.push({
2594
- name: methodName,
2595
- params,
2596
- returnType,
2597
- isAsync
2598
- });
2779
+ if (!isPublicMethod2(member)) continue;
2780
+ const method = member;
2781
+ const methodName = getMethodName2(method);
2782
+ if (!methodName) continue;
2783
+ const returnTypeInfo = analyzeReturnType2(method);
2784
+ if (!returnTypeInfo.isObservable && !method.function.async) {
2785
+ throw {
2786
+ type: "validation",
2787
+ message: `Method ${className}.${methodName} must be async or return an Observable`,
2788
+ filePath,
2789
+ details: { className, methodName }
2790
+ };
2599
2791
  }
2792
+ const params = extractMethodParams3(method.function.params || []);
2793
+ methods.push({
2794
+ name: methodName,
2795
+ params,
2796
+ returnType: returnTypeInfo.type,
2797
+ isAsync: method.function.async,
2798
+ isObservable: returnTypeInfo.isObservable
2799
+ });
2600
2800
  }
2601
2801
  return methods;
2602
2802
  }
2803
+ function isPublicMethod2(member) {
2804
+ return member.type === "ClassMethod" && (member.accessibility === "public" || !member.accessibility);
2805
+ }
2806
+ function getMethodName2(method) {
2807
+ if (method.key.type === "Identifier") {
2808
+ return method.key.value;
2809
+ }
2810
+ return null;
2811
+ }
2812
+ function analyzeReturnType2(method) {
2813
+ const returnType = method.function.returnType?.typeAnnotation;
2814
+ if (!returnType) {
2815
+ return { type: "any", isObservable: false };
2816
+ }
2817
+ if (returnType.type === "TsTypeReference" && returnType.typeName.type === "Identifier" && returnType.typeName.value === "Observable") {
2818
+ const innerType = returnType.typeParams?.params[0];
2819
+ return {
2820
+ type: innerType ? stringifyType4(innerType) : "any",
2821
+ isObservable: true
2822
+ };
2823
+ }
2824
+ if (returnType.type === "TsTypeReference" && returnType.typeName.type === "Identifier" && returnType.typeName.value === "Promise") {
2825
+ const innerType = returnType.typeParams?.params[0];
2826
+ return {
2827
+ type: innerType ? stringifyType4(innerType) : "any",
2828
+ isObservable: false
2829
+ };
2830
+ }
2831
+ return {
2832
+ type: stringifyType4(returnType),
2833
+ isObservable: false
2834
+ };
2835
+ }
2603
2836
  function extractMethodParams3(params) {
2604
2837
  const result = [];
2605
2838
  for (const param of params) {
2606
- if (param.type === "Parameter") {
2607
- const pat = param.pat;
2608
- if (pat.type === "Identifier") {
2609
- const name = pat.value;
2610
- const type = pat.typeAnnotation?.typeAnnotation ? stringifyType4(pat.typeAnnotation.typeAnnotation) : "any";
2611
- result.push({
2612
- name,
2613
- type
2614
- });
2615
- }
2839
+ const pat = param.pat;
2840
+ if (pat.type === "Identifier") {
2841
+ const name = pat.value;
2842
+ const type = pat.typeAnnotation?.typeAnnotation ? stringifyType4(pat.typeAnnotation.typeAnnotation) : "any";
2843
+ result.push({
2844
+ name,
2845
+ type
2846
+ });
2616
2847
  }
2617
2848
  }
2618
2849
  return result;
2619
2850
  }
2620
- function extractReturnType3(returnType) {
2621
- if (!returnType || !returnType.typeAnnotation) {
2622
- return "any";
2623
- }
2624
- const type = returnType.typeAnnotation;
2625
- if (type.type === "TsTypeReference") {
2626
- const typeName = type.typeName;
2627
- if (typeName.type === "Identifier" && typeName.value === "Promise") {
2628
- if (type.typeParams && type.typeParams.params.length > 0) {
2629
- return stringifyType4(type.typeParams.params[0]);
2630
- }
2631
- }
2632
- }
2633
- return stringifyType4(type);
2634
- }
2635
2851
  function stringifyType4(typeNode) {
2636
2852
  if (!typeNode) return "any";
2637
2853
  switch (typeNode.type) {
@@ -2640,7 +2856,7 @@ function stringifyType4(typeNode) {
2640
2856
  case "TsTypeReference":
2641
2857
  if (typeNode.typeName.type === "Identifier") {
2642
2858
  const baseName = typeNode.typeName.value;
2643
- if (typeNode.typeParams && typeNode.typeParams.params.length > 0) {
2859
+ if (typeNode.typeParams?.params.length) {
2644
2860
  const params = typeNode.typeParams.params.map(stringifyType4).join(", ");
2645
2861
  return `${baseName}<${params}>`;
2646
2862
  }
@@ -2667,18 +2883,87 @@ function stringifyType4(typeNode) {
2667
2883
  return "any";
2668
2884
  }
2669
2885
  }
2886
+ function validateServiceInfo2(serviceInfo, filePath) {
2887
+ if (!serviceInfo.className) {
2888
+ throw {
2889
+ type: "validation",
2890
+ message: "Service class must have a valid name",
2891
+ filePath
2892
+ };
2893
+ }
2894
+ if (serviceInfo.methods.length === 0) {
2895
+ console.warn(
2896
+ `Warning: Service ${serviceInfo.className} has no public methods`
2897
+ );
2898
+ }
2899
+ }
2670
2900
  function generateStubCode2(serviceInfo) {
2671
2901
  const className = serviceInfo.className;
2672
- const methods = serviceInfo.methods.map((method) => {
2673
- const params = method.params.map((p) => `${p.name}: ${p.type}`).join(", ");
2674
- const paramNames = method.params.map((p) => p.name).join(", ");
2675
- const asyncKeyword = method.isAsync ? "async " : "";
2676
- const returnType = method.isAsync ? `Promise<${method.returnType}>` : method.returnType;
2677
- const hasParams = method.params.length > 0;
2678
- const bodyParam = hasParams ? `{ ${paramNames} }` : "{}";
2679
- if (!hasParams) {
2680
- return ` ${asyncKeyword}${method.name}(${params}): ${returnType} {
2681
- const response = await fetch(\`/${className}/${method.name}\`, {
2902
+ const basePath = serviceNameToPath2(className);
2903
+ const methods = serviceInfo.methods.map((method) => generateMethod2(method, basePath)).join("\n\n");
2904
+ const hasObservable = serviceInfo.methods.some((m) => m.isObservable);
2905
+ const observableImport = hasObservable ? `import { Observable } from "@kithinji/orca";
2906
+ ` : "";
2907
+ return `${observableImport}import { Injectable } from "@kithinji/orca";
2908
+
2909
+ @Injectable()
2910
+ export class ${className} {
2911
+ ${methods}
2912
+ }`;
2913
+ }
2914
+ function serviceNameToPath2(serviceName) {
2915
+ return serviceName.replace(/Service$/, "").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
2916
+ }
2917
+ function generateMethod2(method, basePath) {
2918
+ if (method.isObservable) {
2919
+ return generateSseMethod(method, basePath);
2920
+ }
2921
+ const params = method.params.map((p) => `${p.name}: ${p.type}`).join(", ");
2922
+ const hasParams = method.params.length > 0;
2923
+ if (!hasParams) {
2924
+ return generateGetMethod(method, basePath);
2925
+ }
2926
+ return generatePostMethod(method, basePath, params);
2927
+ }
2928
+ function generateSseMethod(method, basePath) {
2929
+ const params = method.params.map((p) => `${p.name}: ${p.type}`).join(", ");
2930
+ const hasParams = method.params.length > 0;
2931
+ let urlBuilder;
2932
+ if (hasParams) {
2933
+ const queryParams = method.params.map((p) => `${p.name}=\${encodeURIComponent(${p.name})}`).join("&");
2934
+ urlBuilder = `\`/${basePath}/${method.name}?${queryParams}\``;
2935
+ } else {
2936
+ urlBuilder = `\`/${basePath}/${method.name}\``;
2937
+ }
2938
+ return ` ${method.name}(${params}): Observable<${method.returnType}> {
2939
+ return new Observable((observer) => {
2940
+ const eventSource = new EventSource(${urlBuilder});
2941
+
2942
+ eventSource.onmessage = (event) => {
2943
+ try {
2944
+ const data = JSON.parse(event.data);
2945
+ observer.next(data);
2946
+ } catch (error) {
2947
+ observer.error?.(error);
2948
+ }
2949
+ };
2950
+
2951
+ eventSource.onerror = (error) => {
2952
+ observer.error?.(error);
2953
+ eventSource.close();
2954
+ };
2955
+
2956
+ return () => {
2957
+ eventSource.close();
2958
+ };
2959
+ });
2960
+ }`;
2961
+ }
2962
+ function generateGetMethod(method, basePath) {
2963
+ const params = method.params.map((p) => `${p.name}: ${p.type}`).join(", ");
2964
+ const returnType = `Promise<${method.returnType}>`;
2965
+ return ` async ${method.name}(${params}): ${returnType} {
2966
+ const response = await fetch(\`/${basePath}/${method.name}\`, {
2682
2967
  method: 'GET',
2683
2968
  headers: {
2684
2969
  'Content-Type': 'application/json',
@@ -2691,14 +2976,17 @@ function generateStubCode2(serviceInfo) {
2691
2976
 
2692
2977
  return response.json();
2693
2978
  }`;
2694
- }
2695
- return ` ${asyncKeyword}${method.name}(${params}): ${returnType} {
2696
- const response = await fetch(\`/${className}/${method.name}\`, {
2979
+ }
2980
+ function generatePostMethod(method, basePath, params) {
2981
+ const paramNames = method.params.map((p) => p.name).join(", ");
2982
+ const returnType = `Promise<${method.returnType}>`;
2983
+ return ` async ${method.name}(${params}): ${returnType} {
2984
+ const response = await fetch(\`/${basePath}/${method.name}\`, {
2697
2985
  method: 'POST',
2698
2986
  headers: {
2699
2987
  'Content-Type': 'application/json',
2700
2988
  },
2701
- body: JSON.stringify(${bodyParam}),
2989
+ body: JSON.stringify({ ${paramNames} }),
2702
2990
  });
2703
2991
 
2704
2992
  if (!response.ok) {
@@ -2707,13 +2995,6 @@ function generateStubCode2(serviceInfo) {
2707
2995
 
2708
2996
  return response.json();
2709
2997
  }`;
2710
- }).join("\n\n");
2711
- return `import { Injectable } from "@kithinji/orca";
2712
-
2713
- @Injectable()
2714
- export class ${className} {
2715
- ${methods}
2716
- }`;
2717
2998
  }
2718
2999
 
2719
3000
  // src/plugins/my.ts
@@ -2830,8 +3111,8 @@ var ClientBuildTransformer = class {
2830
3111
  const scSource = generateServerComponent(path15, source);
2831
3112
  return swcTransform(scSource, path15);
2832
3113
  }
2833
- async transformPublicFileRsc(node, source, path15) {
2834
- const stubSource = generateRscStub(path15, source);
3114
+ async transformPublicFileRpc(node, source, path15) {
3115
+ const stubSource = generateRpcStub(path15, source);
2835
3116
  return swcTransform(stubSource, path15);
2836
3117
  }
2837
3118
  async transformSharedCode(source, path15) {
@@ -2853,7 +3134,7 @@ var ClientBuildTransformer = class {
2853
3134
  }
2854
3135
  }
2855
3136
  if (directive === "public") {
2856
- return this.transformPublicFileRsc(node, source, path15);
3137
+ return this.transformPublicFileRpc(node, source, path15);
2857
3138
  }
2858
3139
  if (directive === null) {
2859
3140
  return this.transformSharedCode(source, path15);
@@ -3484,7 +3765,7 @@ Processing dependencies for "${name}": [${component.dependencies.join(
3484
3765
  }
3485
3766
  }
3486
3767
  function updateModuleWithComponent(moduleContent, componentName) {
3487
- const className = capitalize(componentName);
3768
+ const className = capitalize2(componentName);
3488
3769
  const importPath = `./component/${componentName}.component`;
3489
3770
  const sourceFile = ts2.createSourceFile(
3490
3771
  "component.module.ts",
@@ -3640,7 +3921,7 @@ function ensureComponentModuleImported(appModuleContent) {
3640
3921
  function ensureInImportsArray(content, sourceFile) {
3641
3922
  return addToDecoratorArray(content, sourceFile, "imports", "ComponentModule");
3642
3923
  }
3643
- function capitalize(str) {
3924
+ function capitalize2(str) {
3644
3925
  return str.charAt(0).toUpperCase() + str.slice(1);
3645
3926
  }
3646
3927
  function createModule() {
@@ -4266,7 +4547,7 @@ build
4266
4547
  `;
4267
4548
  }
4268
4549
  function genEnv() {
4269
- return `host=localhost
4550
+ return `HOST=localhost
4270
4551
  `;
4271
4552
  }
4272
4553
  function genIndexHtml(name) {
@@ -4546,100 +4827,123 @@ async function createDeployfile(cwd, projectName) {
4546
4827
  const deployFile = `name: ${projectName}
4547
4828
  version: 1.0.0
4548
4829
 
4830
+ vars:
4831
+ deploy_path: &deploy_path "/home/ubuntu/${projectName}"
4832
+ backup_path: &backup_path "/home/ubuntu/backups"
4833
+ user: &user "ubuntu"
4834
+
4835
+ shared_operations:
4836
+ install_docker: &install_docker
4837
+ type: ensure
4838
+ ensure:
4839
+ docker:
4840
+ version: "28.5.2"
4841
+ addUserToGroup: true
4842
+
4843
+ stop_containers: &stop_containers
4844
+ type: action
4845
+ action:
4846
+ command: docker compose down --remove-orphans 2>/dev/null || true
4847
+
4848
+ pull_images: &pull_images
4849
+ type: action
4850
+ action:
4851
+ command: docker compose pull --quiet
4852
+
4853
+ build_and_start: &build_and_start
4854
+ type: action
4855
+ action:
4856
+ command: docker compose up -d --build --remove-orphans --wait
4857
+
4858
+ cleanup_docker: &cleanup_docker
4859
+ type: action
4860
+ action:
4861
+ command: docker system prune -f --volumes --filter "until=168h"
4862
+
4549
4863
  targets:
4864
+ localhost:
4865
+ type: local
4866
+ operations:
4867
+ #- name: "Environment Setup"
4868
+ # <<: *install_docker
4869
+ - name: "Refresh Stack"
4870
+ <<: *build_and_start
4871
+
4550
4872
  ec2:
4873
+ type: ssh
4551
4874
  host: ec2-xx-xx-xxx-xxx.xx-xxxx-x.compute.amazonaws.com
4552
- user: ubuntu
4875
+ user: *user
4553
4876
  keyPath: ~/xxxx.pem
4554
4877
  port: 22
4555
- deployPath: /home/\${user}/app
4878
+ deployPath: *deploy_path
4556
4879
 
4557
4880
  operations:
4558
- - name: "Setup swap space"
4881
+ - name: "Provision Directories and Swap"
4559
4882
  type: ensure
4560
4883
  ensure:
4561
4884
  swap:
4562
4885
  size: 4G
4563
4886
 
4564
4887
  - name: "Install Docker"
4565
- type: ensure
4566
- ensure:
4567
- docker:
4568
- version: "28.5.2"
4569
- addUserToGroup: true
4888
+ <<: *install_docker
4570
4889
 
4571
4890
  - name: "Create application directories"
4572
4891
  type: ensure
4573
4892
  ensure:
4574
4893
  directory:
4575
- path: \${deployPath}
4576
- owner: \${user}
4894
+ path: *deploy_path
4895
+ owner: *user
4577
4896
 
4578
4897
  - name: "Create backup directory"
4579
4898
  type: ensure
4580
4899
  ensure:
4581
4900
  directory:
4582
- path: /home/\${user}/backups
4583
- owner: \${user}
4901
+ path: *backup_path
4902
+ owner: *user
4584
4903
 
4585
- - name: "Stop running containers"
4586
- type: action
4587
- action:
4588
- command: cd \${deployPath} && docker compose down 2>/dev/null || true
4589
-
4590
- - name: "Sync application files"
4904
+ - name: "Sync Source Files"
4591
4905
  type: action
4592
4906
  action:
4593
4907
  rsync:
4594
4908
  source: ./
4595
- destination: \${deployPath}/
4909
+ destination: *deploy_path
4910
+ delete: true
4596
4911
  exclude:
4597
- - node_modules/
4598
4912
  - .git/
4599
- - "*.log"
4913
+ - node_modules/
4600
4914
  - .env.local
4601
- - dist/
4602
- - public/
4915
+ - "*.log"
4603
4916
 
4604
- - name: "Pull Docker images"
4917
+ - name: "Navigate to Deploy Path"
4605
4918
  type: action
4606
4919
  action:
4607
- command: cd \${deployPath} && docker compose pull
4920
+ command: cd *deploy_path
4608
4921
 
4609
- - name: "Build and start containers"
4922
+ - name: "Create Pre-deployment Backup"
4610
4923
  type: action
4611
4924
  action:
4612
- command: cd \${deployPath} && docker compose up -d --build --remove-orphans
4925
+ command: tar -czf *backup_path/backup-$(date +%Y%m%d-%H%M%S).tar.gz .
4613
4926
 
4614
- - name: "Wait for services to start"
4615
- type: action
4616
- action:
4617
- command: sleep 10
4927
+ - name: "Pull Latest Images"
4928
+ <<: *pull_images
4618
4929
 
4619
- - name: "Show container status"
4620
- type: action
4621
- action:
4622
- command: cd \${deployPath} && docker compose ps
4930
+ - name: "Stop Existing Stack"
4931
+ <<: *stop_containers
4623
4932
 
4624
- - name: "Show recent logs"
4625
- type: action
4626
- action:
4627
- command: cd \${deployPath} && docker compose logs --tail=30
4933
+ - name: "Build and Launch"
4934
+ <<: *build_and_start
4628
4935
 
4629
- - name: "Cleanup old backups"
4630
- type: action
4631
- action:
4632
- command: find /home/\${user}/backups -name "backup-*.tar.gz" -mtime +7 -delete
4936
+ - name: "Verify Health Status"
4937
+ type: verify
4938
+ verify:
4939
+ command: ! "[ $(docker compose ps --format json | grep -qv 'running\\|healthy') ]"
4633
4940
 
4634
- - name: "Cleanup Docker resources"
4941
+ - name: "Maintenance: Cleanup"
4635
4942
  type: action
4636
4943
  action:
4637
- command: docker system prune -f --volumes
4638
-
4639
- - name: "Verify containers are running"
4640
- type: verify
4641
- verify:
4642
- command: cd \${deployPath} && docker compose ps | grep -q "Up"
4944
+ command: |
4945
+ find *backup_path -name "backup-*.tar.gz" -mtime +7 -delete
4946
+ docker image prune -af --filter "until=24h"
4643
4947
  `;
4644
4948
  const deployFilePath = path12.join(cwd, "pod.deploy.yml");
4645
4949
  await fs9.writeFile(deployFilePath, deployFile);
@@ -4944,6 +5248,9 @@ import path13 from "path";
4944
5248
  import os from "os";
4945
5249
  import { NodeSSH } from "node-ssh";
4946
5250
  import chalk from "chalk";
5251
+ import { exec } from "child_process";
5252
+ import { promisify } from "util";
5253
+ var execAsync = promisify(exec);
4947
5254
  function interpolate(str, context2) {
4948
5255
  if (!str) return "";
4949
5256
  return str.replace(/\${([^}]+)}/g, (match, key) => {
@@ -5142,19 +5449,42 @@ if [ "${addToGroup}" = "true" ]; then
5142
5449
  fi
5143
5450
  `
5144
5451
  };
5145
- var RemoteShell = class {
5146
- constructor(ssh) {
5147
- this.ssh = ssh;
5452
+ var SSHStrategy = class {
5453
+ constructor(target) {
5454
+ this.ssh = new NodeSSH();
5455
+ this.target = target;
5456
+ this.currentDir = target.deployPath || ".";
5457
+ }
5458
+ async connect() {
5459
+ await this.ssh.connect({
5460
+ host: this.target.host,
5461
+ username: this.target.user,
5462
+ privateKeyPath: this.target.keyPath,
5463
+ port: this.target.port || 22
5464
+ });
5148
5465
  }
5149
- async uploadContent(remotePath, content) {
5150
- const localTmp = path13.join(os.tmpdir(), `pod_tmp_${Date.now()}`);
5151
- fs10.writeFileSync(localTmp, content);
5152
- try {
5153
- await this.ssh.execCommand(`mkdir -p $(dirname ${remotePath})`);
5154
- await this.ssh.putFile(localTmp, remotePath);
5155
- } finally {
5156
- if (fs10.existsSync(localTmp)) fs10.unlinkSync(localTmp);
5466
+ async disconnect() {
5467
+ this.ssh.dispose();
5468
+ }
5469
+ async runCommand(cmd, silent = false) {
5470
+ const trimmed = cmd.trim();
5471
+ if (trimmed.startsWith("cd ")) {
5472
+ const newPath = trimmed.replace("cd ", "").trim();
5473
+ this.currentDir = path13.posix.resolve(this.currentDir, newPath);
5474
+ if (!silent) console.log(chalk.gray(` [SSH Path: ${this.currentDir}]`));
5475
+ return { stdout: "", stderr: "", code: 0 };
5157
5476
  }
5477
+ const result = await this.ssh.execCommand(trimmed, {
5478
+ cwd: this.currentDir
5479
+ });
5480
+ if (result.code !== 0 && result.code !== null) {
5481
+ throw new Error(`Execution failed: ${trimmed}
5482
+ STDERR: ${result.stderr}`);
5483
+ }
5484
+ if (!silent && result.stdout) {
5485
+ result.stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach((l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`)));
5486
+ }
5487
+ return result;
5158
5488
  }
5159
5489
  async runScript(name, content, context2) {
5160
5490
  const interpolated = interpolate(content, context2);
@@ -5162,22 +5492,20 @@ var RemoteShell = class {
5162
5492
  await this.uploadContent(remotePath, interpolated);
5163
5493
  try {
5164
5494
  await this.ssh.execCommand(`chmod +x ${remotePath}`);
5165
- return await this.run(remotePath, context2);
5495
+ await this.runCommand(remotePath);
5166
5496
  } finally {
5167
5497
  await this.ssh.execCommand(`rm -f ${remotePath}`);
5168
5498
  }
5169
5499
  }
5170
- async run(cmd, context2, silent = false) {
5171
- const interpolated = interpolate(cmd, context2);
5172
- const result = await this.ssh.execCommand(interpolated);
5173
- if (result.code !== 0 && result.code !== null) {
5174
- throw new Error(`Execution failed: ${cmd}
5175
- STDERR: ${result.stderr}`);
5176
- }
5177
- if (!silent && result.stdout) {
5178
- result.stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach((l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`)));
5500
+ async uploadContent(remotePath, content) {
5501
+ const localTmp = path13.join(os.tmpdir(), `pod_tmp_${Date.now()}`);
5502
+ fs10.writeFileSync(localTmp, content);
5503
+ try {
5504
+ await this.ssh.execCommand(`mkdir -p $(dirname ${remotePath})`);
5505
+ await this.ssh.putFile(localTmp, remotePath);
5506
+ } finally {
5507
+ if (fs10.existsSync(localTmp)) fs10.unlinkSync(localTmp);
5179
5508
  }
5180
- return result;
5181
5509
  }
5182
5510
  async readJson(remotePath) {
5183
5511
  const res = await this.ssh.execCommand(`cat ${remotePath}`);
@@ -5187,211 +5515,354 @@ STDERR: ${result.stderr}`);
5187
5515
  return null;
5188
5516
  }
5189
5517
  }
5518
+ async syncDirectory(source, destination, exclude) {
5519
+ const putOptions = { recursive: true, concurrency: 10 };
5520
+ if (exclude?.length) {
5521
+ putOptions.validate = (filePath) => {
5522
+ const relative = path13.relative(source, filePath);
5523
+ if (relative === "") return true;
5524
+ return !exclude.some((pattern) => {
5525
+ if (pattern.endsWith("/")) {
5526
+ const dir = pattern.slice(0, -1);
5527
+ const segment = "/" + dir + "/";
5528
+ return relative === dir || relative.startsWith(dir + "/") || relative.includes(segment);
5529
+ }
5530
+ if (pattern.startsWith("*.")) {
5531
+ return relative.endsWith(pattern.slice(1));
5532
+ }
5533
+ return relative === pattern;
5534
+ });
5535
+ };
5536
+ }
5537
+ console.log(chalk.gray(` Syncing ${source} \u2192 ${destination}`));
5538
+ await this.ssh.putDirectory(source, destination, putOptions);
5539
+ }
5190
5540
  };
5191
- async function deploy(targetName, options) {
5192
- const cwd = process.cwd();
5193
- const rawConfig = yaml2.load(
5194
- fs10.readFileSync(path13.join(cwd, "pod.deploy.yml"), "utf8")
5195
- );
5196
- const rawTarget = rawConfig.targets?.[targetName];
5197
- if (!rawTarget) throw new Error(`Target ${targetName} not found.`);
5198
- console.log(
5199
- chalk.blue.bold(
5200
- `
5201
- \u{1F680} Pod Deploy: ${rawConfig.name} v${rawConfig.version} \u2192 ${targetName}
5202
- `
5203
- )
5204
- );
5205
- let target = deepInterpolate(rawTarget, {
5206
- ...rawConfig,
5207
- ...rawTarget
5208
- });
5209
- target = resolveLocalPaths(target, cwd);
5210
- const ssh = new NodeSSH();
5211
- const shell = new RemoteShell(ssh);
5212
- try {
5213
- await ssh.connect({
5214
- host: target.host,
5215
- username: target.user,
5216
- privateKeyPath: target.keyPath,
5217
- port: target.port || 22
5218
- });
5219
- const lockPath = path13.posix.join(target.deployPath, "pod-lock.json");
5220
- let lock = await shell.readJson(lockPath) || {
5221
- ensures: {},
5222
- once_actions: []
5223
- };
5224
- if (lock.deployment_version !== rawConfig.version) {
5225
- console.log(chalk.magenta(`\u2192 Version change: ${rawConfig.version}`));
5226
- lock.deployment_version = rawConfig.version;
5227
- lock.once_actions = [];
5228
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5541
+ var LocalStrategy = class {
5542
+ constructor(target, cwd) {
5543
+ this.target = target;
5544
+ this.currentDir = cwd;
5545
+ }
5546
+ async connect() {
5547
+ }
5548
+ async disconnect() {
5549
+ }
5550
+ async runCommand(cmd, silent = false) {
5551
+ const trimmed = cmd.trim();
5552
+ if (trimmed.startsWith("cd ")) {
5553
+ const newPath = trimmed.replace("cd ", "").trim();
5554
+ this.currentDir = path13.resolve(this.currentDir, newPath);
5555
+ if (!silent) console.log(chalk.gray(` [Local Path: ${this.currentDir}]`));
5556
+ return { stdout: "", stderr: "", code: 0 };
5229
5557
  }
5230
- for (const op of target.operations) {
5231
- try {
5232
- if (op.type === "ensure") {
5233
- await handleEnsure(op, shell, target, lock, lockPath, options);
5234
- } else if (op.type === "action") {
5235
- await handleAction(op, shell, target, lock, lockPath);
5236
- } else if (op.type === "verify") {
5237
- await handleVerify(op, shell, target);
5558
+ try {
5559
+ const { stdout, stderr } = await execAsync(trimmed, {
5560
+ cwd: this.currentDir
5561
+ });
5562
+ if (!silent && stdout) {
5563
+ stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach(
5564
+ (l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`))
5565
+ );
5566
+ }
5567
+ return { stdout, stderr, code: 0 };
5568
+ } catch (err) {
5569
+ throw new Error(
5570
+ `Execution failed: ${trimmed}
5571
+ STDERR: ${err.stderr || err.message}`
5572
+ );
5573
+ }
5574
+ }
5575
+ async runScript(name, content, context2) {
5576
+ const interpolated = interpolate(content, context2);
5577
+ const scriptPath = path13.join(
5578
+ os.tmpdir(),
5579
+ `pod_script_${name}_${Date.now()}.sh`
5580
+ );
5581
+ fs10.writeFileSync(scriptPath, interpolated);
5582
+ fs10.chmodSync(scriptPath, "755");
5583
+ try {
5584
+ await this.runCommand(scriptPath);
5585
+ } finally {
5586
+ if (fs10.existsSync(scriptPath)) fs10.unlinkSync(scriptPath);
5587
+ }
5588
+ }
5589
+ async uploadContent(localPath, content) {
5590
+ const dir = path13.dirname(localPath);
5591
+ if (!fs10.existsSync(dir)) {
5592
+ fs10.mkdirSync(dir, { recursive: true });
5593
+ }
5594
+ fs10.writeFileSync(localPath, content);
5595
+ }
5596
+ async readJson(localPath) {
5597
+ try {
5598
+ if (!fs10.existsSync(localPath)) return null;
5599
+ const content = fs10.readFileSync(localPath, "utf8");
5600
+ return JSON.parse(content);
5601
+ } catch {
5602
+ return null;
5603
+ }
5604
+ }
5605
+ async syncDirectory(source, destination, exclude) {
5606
+ console.log(chalk.gray(` Copying ${source} \u2192 ${destination}`));
5607
+ if (!fs10.existsSync(destination)) {
5608
+ fs10.mkdirSync(destination, { recursive: true });
5609
+ }
5610
+ const shouldExclude = (relativePath) => {
5611
+ if (!exclude?.length) return false;
5612
+ return exclude.some((pattern) => {
5613
+ if (pattern.endsWith("/")) {
5614
+ const dir = pattern.slice(0, -1);
5615
+ const segment = "/" + dir + "/";
5616
+ return relativePath === dir || relativePath.startsWith(dir + "/") || relativePath.includes(segment);
5617
+ }
5618
+ if (pattern.startsWith("*.")) {
5619
+ return relativePath.endsWith(pattern.slice(1));
5620
+ }
5621
+ return relativePath === pattern;
5622
+ });
5623
+ };
5624
+ const copyRecursive = (src, dest) => {
5625
+ const entries = fs10.readdirSync(src, { withFileTypes: true });
5626
+ for (const entry of entries) {
5627
+ const srcPath = path13.join(src, entry.name);
5628
+ const destPath = path13.join(dest, entry.name);
5629
+ const relativePath = path13.relative(source, srcPath);
5630
+ if (shouldExclude(relativePath)) continue;
5631
+ if (entry.isDirectory()) {
5632
+ if (!fs10.existsSync(destPath)) {
5633
+ fs10.mkdirSync(destPath, { recursive: true });
5634
+ }
5635
+ copyRecursive(srcPath, destPath);
5238
5636
  } else {
5239
- throw new Error(`Unknown operation type: ${op.type}`);
5637
+ fs10.copyFileSync(srcPath, destPath);
5240
5638
  }
5241
- } catch (err) {
5242
- throw new Error(`Failed at operation "${op.name}": ${err.message}`);
5243
5639
  }
5640
+ };
5641
+ copyRecursive(source, destination);
5642
+ }
5643
+ };
5644
+ var StrategyFactory = class {
5645
+ static create(target, cwd) {
5646
+ const targetType = target.type || (target.host ? "ssh" : "local");
5647
+ switch (targetType) {
5648
+ case "ssh":
5649
+ return new SSHStrategy(target);
5650
+ case "local":
5651
+ return new LocalStrategy(target, cwd);
5652
+ default:
5653
+ throw new Error(`Unknown target type: ${targetType}`);
5244
5654
  }
5245
- console.log(chalk.green.bold(`
5246
- \u2705 Deployment successful!
5247
- `));
5248
- } catch (err) {
5249
- console.error(chalk.red.bold(`
5250
- \u274C Deployment Failed: ${err.message}`));
5251
- throw err;
5252
- } finally {
5253
- ssh.dispose();
5254
5655
  }
5255
- }
5256
- async function handleEnsure(op, shell, target, lock, lockPath, options) {
5257
- if (!op.ensure) {
5258
- throw new Error(`Ensure operation "${op.name}" missing ensure config`);
5656
+ };
5657
+ var OperationHandler = class {
5658
+ constructor(strategy, target, lock, lockPath) {
5659
+ this.strategy = strategy;
5660
+ this.target = target;
5661
+ this.lock = lock;
5662
+ this.lockPath = lockPath;
5259
5663
  }
5260
- if (op.ensure.swap) {
5664
+ async handleEnsure(op, options) {
5665
+ if (!op.ensure) {
5666
+ throw new Error(`Ensure operation "${op.name}" missing ensure config`);
5667
+ }
5668
+ if (op.ensure.swap) {
5669
+ await this.ensureSwap(op, options);
5670
+ }
5671
+ if (op.ensure.docker) {
5672
+ await this.ensureDocker(op, options);
5673
+ }
5674
+ if (op.ensure.directory) {
5675
+ await this.ensureDirectory(op, options);
5676
+ }
5677
+ }
5678
+ async ensureSwap(op, options) {
5261
5679
  const key = "swap";
5262
- const locked = lock.ensures[key];
5680
+ const locked = this.lock.ensures[key];
5263
5681
  const currentConfig = op.ensure.swap;
5264
5682
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5265
5683
  if (options?.forceEnsure || !locked || locked.version !== currentConfig.size || configChanged) {
5266
5684
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5267
5685
  const script = SCRIPTS.SWAP(currentConfig.size);
5268
- await shell.runScript(key, script, target);
5269
- lock.ensures[key] = {
5686
+ await this.strategy.runScript(key, script, this.target);
5687
+ this.lock.ensures[key] = {
5270
5688
  version: currentConfig.size,
5271
5689
  config: currentConfig
5272
5690
  };
5273
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5691
+ await this.saveLock();
5274
5692
  } else {
5275
5693
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5276
5694
  }
5277
5695
  }
5278
- if (op.ensure.docker) {
5696
+ async ensureDocker(op, options) {
5279
5697
  const key = "docker";
5280
- const locked = lock.ensures[key];
5698
+ const locked = this.lock.ensures[key];
5281
5699
  const currentConfig = op.ensure.docker;
5282
5700
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5283
5701
  if (options?.forceEnsure || !locked || locked.version !== currentConfig.version || configChanged) {
5284
5702
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5285
5703
  const script = SCRIPTS.DOCKER(
5286
5704
  currentConfig.version,
5287
- target.user,
5705
+ this.target.user || os.userInfo().username,
5288
5706
  !!currentConfig.addUserToGroup
5289
5707
  );
5290
- await shell.runScript(key, script, target);
5291
- lock.ensures[key] = {
5708
+ await this.strategy.runScript(key, script, this.target);
5709
+ this.lock.ensures[key] = {
5292
5710
  version: currentConfig.version,
5293
5711
  config: currentConfig
5294
5712
  };
5295
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5713
+ await this.saveLock();
5296
5714
  } else {
5297
5715
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5298
5716
  }
5299
5717
  }
5300
- if (op.ensure.directory) {
5718
+ async ensureDirectory(op, options) {
5301
5719
  const key = `directory_${op.ensure.directory.path}`;
5302
- const locked = lock.ensures[key];
5720
+ const locked = this.lock.ensures[key];
5303
5721
  const currentConfig = op.ensure.directory;
5304
5722
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5305
5723
  if (options?.forceEnsure || !locked || configChanged) {
5306
5724
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5307
- const dirPath = interpolate(currentConfig.path, target);
5308
- const owner = currentConfig.owner ? interpolate(currentConfig.owner, target) : target.user;
5309
- await shell.run(`mkdir -p ${dirPath}`, target, true);
5310
- await shell.run(
5311
- `sudo chown -R ${owner}:${owner} ${dirPath}`,
5312
- target,
5313
- true
5314
- );
5315
- lock.ensures[key] = {
5725
+ const dirPath = interpolate(currentConfig.path, this.target);
5726
+ const owner = currentConfig.owner ? interpolate(currentConfig.owner, this.target) : this.target.user || os.userInfo().username;
5727
+ await this.strategy.runCommand(`mkdir -p ${dirPath}`, true);
5728
+ if (this.target.user) {
5729
+ await this.strategy.runCommand(
5730
+ `sudo chown -R ${owner}:${owner} ${dirPath}`,
5731
+ true
5732
+ );
5733
+ }
5734
+ this.lock.ensures[key] = {
5316
5735
  version: dirPath,
5317
5736
  config: currentConfig
5318
5737
  };
5319
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5738
+ await this.saveLock();
5320
5739
  } else {
5321
5740
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5322
5741
  }
5323
5742
  }
5324
- }
5325
- async function handleAction(op, shell, target, lock, lockPath) {
5326
- if (!op.action) {
5327
- throw new Error(`Action operation "${op.name}" missing action config`);
5328
- }
5329
- const when = op.when || "always";
5330
- if (when === "never") {
5331
- console.log(chalk.gray(`\u2298 ${op.name} (disabled)`));
5332
- return;
5333
- }
5334
- const actionId = `action_${op.name}`;
5335
- if (when === "once" && lock.once_actions.includes(actionId)) {
5336
- console.log(chalk.gray(`\u2713 ${op.name} (already completed)`));
5337
- return;
5338
- }
5339
- console.log(chalk.cyan(`\u2192 Running: ${op.name}`));
5340
- if (op.action.rsync) {
5341
- const src = op.action.rsync.source;
5342
- const dest = interpolate(op.action.rsync.destination || ".", target);
5343
- const putOptions = { recursive: true, concurrency: 10 };
5344
- if (op.action.rsync.exclude?.length) {
5345
- const excludePatterns = op.action.rsync.exclude;
5346
- putOptions.validate = (filePath) => {
5347
- const relative = path13.relative(src, filePath);
5348
- if (relative === "") return true;
5349
- return !excludePatterns.some((pattern) => {
5350
- if (pattern.endsWith("/")) {
5351
- const dir = pattern.slice(0, -1);
5352
- const segment = "/" + dir + "/";
5353
- return relative === dir || relative.startsWith(dir + "/") || relative.includes(segment);
5354
- }
5355
- if (pattern.startsWith("*.")) {
5356
- return relative.endsWith(pattern.slice(1));
5357
- }
5358
- return relative === pattern;
5359
- });
5360
- };
5743
+ async handleAction(op) {
5744
+ if (!op.action) {
5745
+ throw new Error(`Action operation "${op.name}" missing action config`);
5746
+ }
5747
+ const when = op.when || "always";
5748
+ if (when === "never") {
5749
+ console.log(chalk.gray(`\u2298 ${op.name} (disabled)`));
5750
+ return;
5751
+ }
5752
+ const actionId = `action_${op.name}`;
5753
+ if (when === "once" && this.lock.once_actions.includes(actionId)) {
5754
+ console.log(chalk.gray(`\u2713 ${op.name} (already completed)`));
5755
+ return;
5756
+ }
5757
+ console.log(chalk.cyan(`\u2192 Running: ${op.name}`));
5758
+ if (op.action.rsync) {
5759
+ const src = op.action.rsync.source;
5760
+ const dest = interpolate(op.action.rsync.destination || ".", this.target);
5761
+ await this.strategy.syncDirectory(src, dest, op.action.rsync.exclude);
5762
+ }
5763
+ if (op.action.command) {
5764
+ const cmd = interpolate(op.action.command, this.target);
5765
+ await this.strategy.runCommand(cmd);
5766
+ }
5767
+ if (when === "once") {
5768
+ this.lock.once_actions.push(actionId);
5769
+ await this.saveLock();
5361
5770
  }
5362
- console.log(chalk.gray(` Syncing ${src} \u2192 ${dest}`));
5363
- await shell.ssh.putDirectory(src, dest, putOptions);
5364
- }
5365
- if (op.action.command) {
5366
- await shell.run(op.action.command, target);
5367
- }
5368
- if (when === "once") {
5369
- lock.once_actions.push(actionId);
5370
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5371
5771
  }
5372
- }
5373
- async function handleVerify(op, shell, target) {
5374
- if (!op.verify) {
5375
- throw new Error(`Verify operation "${op.name}" missing verify config`);
5772
+ async handleVerify(op) {
5773
+ if (!op.verify) {
5774
+ throw new Error(`Verify operation "${op.name}" missing verify config`);
5775
+ }
5776
+ console.log(chalk.cyan(`\u2192 Verifying: ${op.name}`));
5777
+ if (op.verify.http) {
5778
+ const url = interpolate(op.verify.http.url, this.target);
5779
+ const timeout = op.verify.http.timeout || "30s";
5780
+ await this.strategy.runCommand(
5781
+ `curl -f --max-time ${timeout} ${url}`,
5782
+ true
5783
+ );
5784
+ }
5785
+ if (op.verify.command) {
5786
+ const cmd = interpolate(op.verify.command, this.target);
5787
+ await this.strategy.runCommand(cmd, true);
5788
+ }
5376
5789
  }
5377
- console.log(chalk.cyan(`\u2192 Verifying: ${op.name}`));
5378
- if (op.verify.http) {
5379
- const url = interpolate(op.verify.http.url, target);
5380
- const timeout = op.verify.http.timeout || "30s";
5381
- await shell.run(`curl -f --max-time ${timeout} ${url}`, target, true);
5790
+ async saveLock() {
5791
+ await this.strategy.uploadContent(
5792
+ this.lockPath,
5793
+ JSON.stringify(this.lock, null, 2)
5794
+ );
5382
5795
  }
5383
- if (op.verify.command) {
5384
- await shell.run(op.verify.command, target, true);
5796
+ };
5797
+ async function deploy(targetName, options) {
5798
+ const cwd = process.cwd();
5799
+ const rawConfig = yaml2.load(
5800
+ fs10.readFileSync(path13.join(cwd, "pod.deploy.yml"), "utf8"),
5801
+ { schema: yaml2.DEFAULT_SCHEMA }
5802
+ );
5803
+ const rawTarget = rawConfig.targets?.[targetName];
5804
+ if (!rawTarget) throw new Error(`Target ${targetName} not found.`);
5805
+ console.log(
5806
+ chalk.blue.bold(
5807
+ `
5808
+ Pod Deploy: ${rawConfig.name} v${rawConfig.version} \u2192 ${targetName}
5809
+ `
5810
+ )
5811
+ );
5812
+ let target = deepInterpolate(rawTarget, {
5813
+ ...rawConfig,
5814
+ ...rawTarget
5815
+ });
5816
+ target = resolveLocalPaths(target, cwd);
5817
+ const strategy = StrategyFactory.create(target, cwd);
5818
+ try {
5819
+ await strategy.connect();
5820
+ const lockPath = target.deployPath ? path13.posix.join(target.deployPath, "pod-lock.json") : path13.join(cwd, "pod-lock.json");
5821
+ let lock = await strategy.readJson(lockPath) || {
5822
+ ensures: {},
5823
+ once_actions: []
5824
+ };
5825
+ if (lock.deployment_version !== rawConfig.version) {
5826
+ console.log(chalk.magenta(`\u2192 Version change: ${rawConfig.version}`));
5827
+ lock.deployment_version = rawConfig.version;
5828
+ lock.once_actions = [];
5829
+ await strategy.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5830
+ }
5831
+ const handler = new OperationHandler(strategy, target, lock, lockPath);
5832
+ for (const op of target.operations) {
5833
+ try {
5834
+ if (op.type === "ensure") {
5835
+ await handler.handleEnsure(op, options);
5836
+ } else if (op.type === "action") {
5837
+ await handler.handleAction(op);
5838
+ } else if (op.type === "verify") {
5839
+ await handler.handleVerify(op);
5840
+ } else {
5841
+ throw new Error(`Unknown operation type: ${op.type}`);
5842
+ }
5843
+ } catch (err) {
5844
+ throw new Error(`Failed at operation "${op.name}": ${err.message}`);
5845
+ }
5846
+ }
5847
+ console.log(chalk.green.bold(`
5848
+ Deployment successful!
5849
+ `));
5850
+ } catch (err) {
5851
+ console.error(chalk.red.bold(`
5852
+ Deployment Failed: ${err.message}`));
5853
+ throw err;
5854
+ } finally {
5855
+ await strategy.disconnect();
5385
5856
  }
5386
5857
  }
5387
5858
 
5388
5859
  // src/main.ts
5389
5860
  import chalk2 from "chalk";
5390
5861
  var program = new Command();
5391
- program.name("pod").description("Pod cli tool").version("1.0.22");
5862
+ program.name("pod").description("Pod cli tool").version("1.0.24");
5392
5863
  program.command("new <name>").description("Start a new Orca Project").action(async (name) => {
5393
5864
  await addNew(name);
5394
- const appDir = path14.resolve(process.cwd(), name);
5865
+ const appDir = path14.resolve(process.cwd());
5395
5866
  const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
5396
5867
  console.log("Installing dependencies...");
5397
5868
  execSync("npm install", { stdio: "inherit", cwd: appDir, shell });