@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/README.md +84 -70
- package/dist/main.js +899 -428
- package/dist/main.js.map +4 -4
- package/dist/types/deploy/deploy.d.ts.map +1 -1
- package/dist/types/plugins/generators/generate_controller.d.ts.map +1 -1
- package/dist/types/plugins/generators/generate_rpc.d.ts +2 -0
- package/dist/types/plugins/generators/generate_rpc.d.ts.map +1 -0
- package/dist/types/plugins/generators/generate_rsc.d.ts +7 -0
- package/dist/types/plugins/generators/generate_rsc.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
|
1091
|
+
if (!expr) return "any";
|
|
1092
|
+
if (expr.type === "Identifier") {
|
|
1093
|
+
return expr.value;
|
|
1094
|
+
}
|
|
1027
1095
|
if (expr.type === "MemberExpression") {
|
|
1028
|
-
|
|
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$/, "
|
|
1072
|
-
const serviceImportPath =
|
|
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))
|
|
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."))
|
|
1206
|
+
if (s.includes("z.")) {
|
|
1207
|
+
registerIdentifier("z");
|
|
1208
|
+
}
|
|
1084
1209
|
});
|
|
1085
1210
|
});
|
|
1086
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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 =
|
|
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
|
|
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/
|
|
2700
|
+
// src/plugins/generators/generate_rpc.ts
|
|
2530
2701
|
import { parseSync as parseSync4 } from "@swc/core";
|
|
2531
|
-
function
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
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
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
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
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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
|
|
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
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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
|
-
|
|
2696
|
-
|
|
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(${
|
|
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
|
|
2834
|
-
const stubSource =
|
|
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.
|
|
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 =
|
|
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
|
|
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 `
|
|
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:
|
|
4875
|
+
user: *user
|
|
4553
4876
|
keyPath: ~/xxxx.pem
|
|
4554
4877
|
port: 22
|
|
4555
|
-
deployPath:
|
|
4878
|
+
deployPath: *deploy_path
|
|
4556
4879
|
|
|
4557
4880
|
operations:
|
|
4558
|
-
- name: "
|
|
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
|
-
|
|
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:
|
|
4576
|
-
owner:
|
|
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:
|
|
4583
|
-
owner:
|
|
4901
|
+
path: *backup_path
|
|
4902
|
+
owner: *user
|
|
4584
4903
|
|
|
4585
|
-
- name: "
|
|
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:
|
|
4909
|
+
destination: *deploy_path
|
|
4910
|
+
delete: true
|
|
4596
4911
|
exclude:
|
|
4597
|
-
- node_modules/
|
|
4598
4912
|
- .git/
|
|
4599
|
-
-
|
|
4913
|
+
- node_modules/
|
|
4600
4914
|
- .env.local
|
|
4601
|
-
-
|
|
4602
|
-
- public/
|
|
4915
|
+
- "*.log"
|
|
4603
4916
|
|
|
4604
|
-
- name: "
|
|
4917
|
+
- name: "Navigate to Deploy Path"
|
|
4605
4918
|
type: action
|
|
4606
4919
|
action:
|
|
4607
|
-
command: cd
|
|
4920
|
+
command: cd *deploy_path
|
|
4608
4921
|
|
|
4609
|
-
- name: "
|
|
4922
|
+
- name: "Create Pre-deployment Backup"
|
|
4610
4923
|
type: action
|
|
4611
4924
|
action:
|
|
4612
|
-
command:
|
|
4925
|
+
command: tar -czf *backup_path/backup-$(date +%Y%m%d-%H%M%S).tar.gz .
|
|
4613
4926
|
|
|
4614
|
-
- name: "
|
|
4615
|
-
|
|
4616
|
-
action:
|
|
4617
|
-
command: sleep 10
|
|
4927
|
+
- name: "Pull Latest Images"
|
|
4928
|
+
<<: *pull_images
|
|
4618
4929
|
|
|
4619
|
-
- name: "
|
|
4620
|
-
|
|
4621
|
-
action:
|
|
4622
|
-
command: cd \${deployPath} && docker compose ps
|
|
4930
|
+
- name: "Stop Existing Stack"
|
|
4931
|
+
<<: *stop_containers
|
|
4623
4932
|
|
|
4624
|
-
- name: "
|
|
4625
|
-
|
|
4626
|
-
action:
|
|
4627
|
-
command: cd \${deployPath} && docker compose logs --tail=30
|
|
4933
|
+
- name: "Build and Launch"
|
|
4934
|
+
<<: *build_and_start
|
|
4628
4935
|
|
|
4629
|
-
- name: "
|
|
4630
|
-
type:
|
|
4631
|
-
|
|
4632
|
-
command:
|
|
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
|
|
4941
|
+
- name: "Maintenance: Cleanup"
|
|
4635
4942
|
type: action
|
|
4636
4943
|
action:
|
|
4637
|
-
command:
|
|
4638
|
-
|
|
4639
|
-
|
|
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
|
|
5146
|
-
constructor(
|
|
5147
|
-
this.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
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
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
|
-
|
|
5495
|
+
await this.runCommand(remotePath);
|
|
5166
5496
|
} finally {
|
|
5167
5497
|
await this.ssh.execCommand(`rm -f ${remotePath}`);
|
|
5168
5498
|
}
|
|
5169
5499
|
}
|
|
5170
|
-
async
|
|
5171
|
-
const
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
}
|
|
5177
|
-
|
|
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
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
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
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
5691
|
+
await this.saveLock();
|
|
5274
5692
|
} else {
|
|
5275
5693
|
console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
|
|
5276
5694
|
}
|
|
5277
5695
|
}
|
|
5278
|
-
|
|
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
|
|
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
|
|
5713
|
+
await this.saveLock();
|
|
5296
5714
|
} else {
|
|
5297
5715
|
console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
|
|
5298
5716
|
}
|
|
5299
5717
|
}
|
|
5300
|
-
|
|
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
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
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
|
|
5738
|
+
await this.saveLock();
|
|
5320
5739
|
} else {
|
|
5321
5740
|
console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
|
|
5322
5741
|
}
|
|
5323
5742
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
if (op.action.
|
|
5345
|
-
const
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
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
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
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
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5790
|
+
async saveLock() {
|
|
5791
|
+
await this.strategy.uploadContent(
|
|
5792
|
+
this.lockPath,
|
|
5793
|
+
JSON.stringify(this.lock, null, 2)
|
|
5794
|
+
);
|
|
5382
5795
|
}
|
|
5383
|
-
|
|
5384
|
-
|
|
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.
|
|
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()
|
|
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 });
|