@objectstack/cli 3.0.0 → 3.0.2
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +26 -0
- package/dist/bin.js +1660 -83
- package/dist/index.js +686 -2
- package/package.json +9 -9
- package/src/bin.ts +12 -0
- package/src/commands/codemod.ts +178 -0
- package/src/commands/diff.ts +285 -0
- package/src/commands/doctor.ts +385 -3
- package/src/commands/explain.ts +402 -0
- package/src/commands/generate.ts +457 -1
- package/src/commands/lint.ts +303 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
collectMetadataStats,
|
|
3
|
+
configExists,
|
|
3
4
|
createTimer,
|
|
4
5
|
formatZodErrors,
|
|
5
6
|
loadConfig,
|
|
@@ -898,11 +899,383 @@ var generateTypesCommand = new Command5("types").description("Generate TypeScrip
|
|
|
898
899
|
process.exit(1);
|
|
899
900
|
}
|
|
900
901
|
});
|
|
901
|
-
|
|
902
|
+
function generateClientFromConfig(config) {
|
|
903
|
+
const lines = [
|
|
904
|
+
"// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
905
|
+
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
906
|
+
"",
|
|
907
|
+
"import type { Data } from '@objectstack/spec';",
|
|
908
|
+
""
|
|
909
|
+
];
|
|
910
|
+
const objects = [];
|
|
911
|
+
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
912
|
+
if (Array.isArray(rawObjects)) {
|
|
913
|
+
objects.push(...rawObjects);
|
|
914
|
+
} else if (typeof rawObjects === "object") {
|
|
915
|
+
for (const val of Object.values(rawObjects)) {
|
|
916
|
+
if (val && typeof val === "object") objects.push(val);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (objects.length === 0) {
|
|
920
|
+
lines.push("// No objects found in configuration");
|
|
921
|
+
return lines.join("\n") + "\n";
|
|
922
|
+
}
|
|
923
|
+
for (const obj of objects) {
|
|
924
|
+
const name = String(obj.name || "unknown");
|
|
925
|
+
const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
926
|
+
const fields = obj.fields ?? {};
|
|
927
|
+
lines.push(`export interface ${typeName}Record {`);
|
|
928
|
+
lines.push(" id: string;");
|
|
929
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
930
|
+
const fType = String(fieldDef.type || "text");
|
|
931
|
+
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
932
|
+
const required = fieldDef.required ? "" : "?";
|
|
933
|
+
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
934
|
+
}
|
|
935
|
+
lines.push("}");
|
|
936
|
+
lines.push("");
|
|
937
|
+
}
|
|
938
|
+
lines.push("export class ObjectStackClient {");
|
|
939
|
+
lines.push(" constructor(private baseUrl: string, private headers: Record<string, string> = {}) {}");
|
|
940
|
+
lines.push("");
|
|
941
|
+
lines.push(" private async request<T>(method: string, path: string, body?: unknown): Promise<T> {");
|
|
942
|
+
lines.push(" const res = await fetch(`${this.baseUrl}${path}`, {");
|
|
943
|
+
lines.push(" method,");
|
|
944
|
+
lines.push(" headers: { 'Content-Type': 'application/json', ...this.headers },");
|
|
945
|
+
lines.push(" body: body ? JSON.stringify(body) : undefined,");
|
|
946
|
+
lines.push(" });");
|
|
947
|
+
lines.push(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);");
|
|
948
|
+
lines.push(" return res.json() as Promise<T>;");
|
|
949
|
+
lines.push(" }");
|
|
950
|
+
for (const obj of objects) {
|
|
951
|
+
const name = String(obj.name || "unknown");
|
|
952
|
+
const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
953
|
+
const endpoint = `/api/${name}`;
|
|
954
|
+
lines.push("");
|
|
955
|
+
lines.push(` async list${typeName}(): Promise<${typeName}Record[]> {`);
|
|
956
|
+
lines.push(` return this.request<${typeName}Record[]>('GET', '${endpoint}');`);
|
|
957
|
+
lines.push(" }");
|
|
958
|
+
lines.push("");
|
|
959
|
+
lines.push(` async get${typeName}(id: string): Promise<${typeName}Record> {`);
|
|
960
|
+
lines.push(` return this.request<${typeName}Record>('GET', '${endpoint}/\${id}');`);
|
|
961
|
+
lines.push(" }");
|
|
962
|
+
lines.push("");
|
|
963
|
+
lines.push(` async create${typeName}(data: Omit<${typeName}Record, 'id'>): Promise<${typeName}Record> {`);
|
|
964
|
+
lines.push(` return this.request<${typeName}Record>('POST', '${endpoint}', data);`);
|
|
965
|
+
lines.push(" }");
|
|
966
|
+
lines.push("");
|
|
967
|
+
lines.push(` async update${typeName}(id: string, data: Partial<${typeName}Record>): Promise<${typeName}Record> {`);
|
|
968
|
+
lines.push(` return this.request<${typeName}Record>('PATCH', '${endpoint}/\${id}', data);`);
|
|
969
|
+
lines.push(" }");
|
|
970
|
+
lines.push("");
|
|
971
|
+
lines.push(` async delete${typeName}(id: string): Promise<void> {`);
|
|
972
|
+
lines.push(` return this.request<void>('DELETE', '${endpoint}/\${id}');`);
|
|
973
|
+
lines.push(" }");
|
|
974
|
+
}
|
|
975
|
+
lines.push("}");
|
|
976
|
+
lines.push("");
|
|
977
|
+
return lines.join("\n") + "\n";
|
|
978
|
+
}
|
|
979
|
+
var generateClientCommand = new Command5("client").description("Generate a type-safe client SDK from ObjectStack configuration").argument("[config]", "Configuration file path").option("-o, --output <file>", "Output file path", "src/client/objectstack-client.ts").option("--dry-run", "Show output without writing").action(async (configPath, options) => {
|
|
980
|
+
printHeader("Generate Client SDK");
|
|
981
|
+
try {
|
|
982
|
+
const { loadConfig: loadConfig2 } = await import("./config-UN34WBHT.js");
|
|
983
|
+
const timer = createTimer();
|
|
984
|
+
printInfo("Loading configuration...");
|
|
985
|
+
const { config, absolutePath } = await loadConfig2(configPath);
|
|
986
|
+
console.log(` ${chalk5.dim("Config:")} ${chalk5.white(absolutePath)}`);
|
|
987
|
+
console.log(` ${chalk5.dim("Output:")} ${chalk5.white(options.output)}`);
|
|
988
|
+
console.log("");
|
|
989
|
+
printStep("Generating client SDK...");
|
|
990
|
+
const content = generateClientFromConfig(config);
|
|
991
|
+
if (options.dryRun) {
|
|
992
|
+
printInfo("Dry run \u2014 no files written");
|
|
993
|
+
console.log("");
|
|
994
|
+
for (const line of content.split("\n")) {
|
|
995
|
+
console.log(chalk5.dim(` ${line}`));
|
|
996
|
+
}
|
|
997
|
+
console.log("");
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const outPath = path3.resolve(process.cwd(), options.output);
|
|
1001
|
+
const outDir = path3.dirname(outPath);
|
|
1002
|
+
if (!fs3.existsSync(outDir)) {
|
|
1003
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
1004
|
+
}
|
|
1005
|
+
fs3.writeFileSync(outPath, content);
|
|
1006
|
+
printSuccess(`Generated client SDK at ${options.output} (${timer.display()})`);
|
|
1007
|
+
console.log("");
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
printError(error.message || String(error));
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
var FIELD_TYPE_SQL_MAP = {
|
|
1014
|
+
text: "VARCHAR(255)",
|
|
1015
|
+
textarea: "TEXT",
|
|
1016
|
+
richtext: "TEXT",
|
|
1017
|
+
html: "TEXT",
|
|
1018
|
+
markdown: "TEXT",
|
|
1019
|
+
number: "DECIMAL(18,2)",
|
|
1020
|
+
integer: "INTEGER",
|
|
1021
|
+
currency: "DECIMAL(18,2)",
|
|
1022
|
+
percent: "DECIMAL(5,2)",
|
|
1023
|
+
boolean: "BOOLEAN",
|
|
1024
|
+
date: "DATE",
|
|
1025
|
+
datetime: "TIMESTAMP",
|
|
1026
|
+
time: "TIME",
|
|
1027
|
+
email: "VARCHAR(255)",
|
|
1028
|
+
phone: "VARCHAR(50)",
|
|
1029
|
+
url: "VARCHAR(2048)",
|
|
1030
|
+
select: "VARCHAR(255)",
|
|
1031
|
+
multiselect: "TEXT",
|
|
1032
|
+
lookup: "VARCHAR(36)",
|
|
1033
|
+
master_detail: "VARCHAR(36)",
|
|
1034
|
+
formula: "TEXT",
|
|
1035
|
+
autonumber: "SERIAL",
|
|
1036
|
+
json: "JSONB",
|
|
1037
|
+
file: "VARCHAR(2048)",
|
|
1038
|
+
image: "VARCHAR(2048)",
|
|
1039
|
+
password: "VARCHAR(255)",
|
|
1040
|
+
slug: "VARCHAR(255)",
|
|
1041
|
+
uuid: "UUID",
|
|
1042
|
+
ip_address: "VARCHAR(45)",
|
|
1043
|
+
color: "VARCHAR(7)",
|
|
1044
|
+
rating: "INTEGER",
|
|
1045
|
+
geo_point: "POINT",
|
|
1046
|
+
vector: "VECTOR",
|
|
1047
|
+
encrypted: "TEXT"
|
|
1048
|
+
};
|
|
1049
|
+
function fieldTypeToSql(fieldType) {
|
|
1050
|
+
return FIELD_TYPE_SQL_MAP[fieldType] || "TEXT";
|
|
1051
|
+
}
|
|
1052
|
+
function generateMigrationSql(config) {
|
|
1053
|
+
const lines = [
|
|
1054
|
+
"-- Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
1055
|
+
`-- Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1056
|
+
""
|
|
1057
|
+
];
|
|
1058
|
+
const objects = [];
|
|
1059
|
+
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
1060
|
+
if (Array.isArray(rawObjects)) {
|
|
1061
|
+
objects.push(...rawObjects);
|
|
1062
|
+
} else if (typeof rawObjects === "object") {
|
|
1063
|
+
for (const val of Object.values(rawObjects)) {
|
|
1064
|
+
if (val && typeof val === "object") objects.push(val);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (objects.length === 0) {
|
|
1068
|
+
lines.push("-- No objects found in configuration");
|
|
1069
|
+
return lines.join("\n") + "\n";
|
|
1070
|
+
}
|
|
1071
|
+
for (const obj of objects) {
|
|
1072
|
+
const tableName = String(obj.name || "unknown");
|
|
1073
|
+
const fields = obj.fields ?? {};
|
|
1074
|
+
lines.push(`CREATE TABLE IF NOT EXISTS "${tableName}" (`);
|
|
1075
|
+
lines.push(' "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),');
|
|
1076
|
+
const fieldLines = [];
|
|
1077
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
1078
|
+
const sqlType = fieldTypeToSql(String(fieldDef.type || "text"));
|
|
1079
|
+
const notNull = fieldDef.required ? " NOT NULL" : "";
|
|
1080
|
+
fieldLines.push(` "${fieldName}" ${sqlType}${notNull}`);
|
|
1081
|
+
}
|
|
1082
|
+
fieldLines.push(' "created_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
1083
|
+
fieldLines.push(' "updated_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
1084
|
+
lines.push(fieldLines.join(",\n"));
|
|
1085
|
+
lines.push(");");
|
|
1086
|
+
lines.push("");
|
|
1087
|
+
}
|
|
1088
|
+
return lines.join("\n") + "\n";
|
|
1089
|
+
}
|
|
1090
|
+
function generateMigrationTs(config) {
|
|
1091
|
+
const lines = [
|
|
1092
|
+
"// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
1093
|
+
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1094
|
+
"",
|
|
1095
|
+
"export async function up(db: any): Promise<void> {"
|
|
1096
|
+
];
|
|
1097
|
+
const objects = [];
|
|
1098
|
+
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
1099
|
+
if (Array.isArray(rawObjects)) {
|
|
1100
|
+
objects.push(...rawObjects);
|
|
1101
|
+
} else if (typeof rawObjects === "object") {
|
|
1102
|
+
for (const val of Object.values(rawObjects)) {
|
|
1103
|
+
if (val && typeof val === "object") objects.push(val);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (objects.length === 0) {
|
|
1107
|
+
lines.push(" // No objects found in configuration");
|
|
1108
|
+
lines.push("}");
|
|
1109
|
+
lines.push("");
|
|
1110
|
+
lines.push("export async function down(db: any): Promise<void> {");
|
|
1111
|
+
lines.push(" // No objects found in configuration");
|
|
1112
|
+
lines.push("}");
|
|
1113
|
+
return lines.join("\n") + "\n";
|
|
1114
|
+
}
|
|
1115
|
+
for (const obj of objects) {
|
|
1116
|
+
const tableName = String(obj.name || "unknown");
|
|
1117
|
+
const fields = obj.fields ?? {};
|
|
1118
|
+
lines.push(` await db.schema.createTable('${tableName}', (table: any) => {`);
|
|
1119
|
+
lines.push(" table.uuid('id').primary().defaultTo(db.fn.uuid());");
|
|
1120
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
1121
|
+
const fType = String(fieldDef.type || "text");
|
|
1122
|
+
const required = fieldDef.required ? ".notNullable()" : ".nullable()";
|
|
1123
|
+
let colMethod;
|
|
1124
|
+
switch (fType) {
|
|
1125
|
+
case "text":
|
|
1126
|
+
case "email":
|
|
1127
|
+
case "phone":
|
|
1128
|
+
case "url":
|
|
1129
|
+
case "select":
|
|
1130
|
+
case "slug":
|
|
1131
|
+
case "password":
|
|
1132
|
+
case "color":
|
|
1133
|
+
case "ip_address":
|
|
1134
|
+
colMethod = `table.string('${fieldName}')`;
|
|
1135
|
+
break;
|
|
1136
|
+
case "textarea":
|
|
1137
|
+
case "richtext":
|
|
1138
|
+
case "html":
|
|
1139
|
+
case "markdown":
|
|
1140
|
+
case "formula":
|
|
1141
|
+
case "encrypted":
|
|
1142
|
+
colMethod = `table.text('${fieldName}')`;
|
|
1143
|
+
break;
|
|
1144
|
+
case "number":
|
|
1145
|
+
case "currency":
|
|
1146
|
+
case "percent":
|
|
1147
|
+
colMethod = `table.decimal('${fieldName}')`;
|
|
1148
|
+
break;
|
|
1149
|
+
case "integer":
|
|
1150
|
+
case "rating":
|
|
1151
|
+
colMethod = `table.integer('${fieldName}')`;
|
|
1152
|
+
break;
|
|
1153
|
+
case "boolean":
|
|
1154
|
+
colMethod = `table.boolean('${fieldName}')`;
|
|
1155
|
+
break;
|
|
1156
|
+
case "date":
|
|
1157
|
+
colMethod = `table.date('${fieldName}')`;
|
|
1158
|
+
break;
|
|
1159
|
+
case "datetime":
|
|
1160
|
+
colMethod = `table.timestamp('${fieldName}')`;
|
|
1161
|
+
break;
|
|
1162
|
+
case "time":
|
|
1163
|
+
colMethod = `table.time('${fieldName}')`;
|
|
1164
|
+
break;
|
|
1165
|
+
case "json":
|
|
1166
|
+
case "multiselect":
|
|
1167
|
+
colMethod = `table.jsonb('${fieldName}')`;
|
|
1168
|
+
break;
|
|
1169
|
+
case "uuid":
|
|
1170
|
+
case "lookup":
|
|
1171
|
+
case "master_detail":
|
|
1172
|
+
colMethod = `table.uuid('${fieldName}')`;
|
|
1173
|
+
break;
|
|
1174
|
+
default:
|
|
1175
|
+
colMethod = `table.text('${fieldName}')`;
|
|
1176
|
+
}
|
|
1177
|
+
lines.push(` ${colMethod}${required};`);
|
|
1178
|
+
}
|
|
1179
|
+
lines.push(" table.timestamps(true, true);");
|
|
1180
|
+
lines.push(" });");
|
|
1181
|
+
}
|
|
1182
|
+
lines.push("}");
|
|
1183
|
+
lines.push("");
|
|
1184
|
+
lines.push("export async function down(db: any): Promise<void> {");
|
|
1185
|
+
const tableNames = objects.map((o) => String(o.name || "unknown")).reverse();
|
|
1186
|
+
for (const tableName of tableNames) {
|
|
1187
|
+
lines.push(` await db.schema.dropTableIfExists('${tableName}');`);
|
|
1188
|
+
}
|
|
1189
|
+
lines.push("}");
|
|
1190
|
+
return lines.join("\n") + "\n";
|
|
1191
|
+
}
|
|
1192
|
+
var generateMigrationCommand = new Command5("migration").description("Generate database migration from ObjectStack schema").argument("[config]", "Configuration file path").option("-o, --output <file>", "Output file path").option("--format <format>", "Output format: sql or typescript", "typescript").option("--dry-run", "Show output without writing").action(async (configPath, options) => {
|
|
1193
|
+
printHeader("Generate Migration");
|
|
1194
|
+
try {
|
|
1195
|
+
const { loadConfig: loadConfig2 } = await import("./config-UN34WBHT.js");
|
|
1196
|
+
const timer = createTimer();
|
|
1197
|
+
printInfo("Loading configuration...");
|
|
1198
|
+
const { config, absolutePath } = await loadConfig2(configPath);
|
|
1199
|
+
const ext = options.format === "sql" ? "sql" : "ts";
|
|
1200
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
1201
|
+
const defaultOutput = `migrations/${timestamp}_migration.${ext}`;
|
|
1202
|
+
const output = options.output || defaultOutput;
|
|
1203
|
+
console.log(` ${chalk5.dim("Config:")} ${chalk5.white(absolutePath)}`);
|
|
1204
|
+
console.log(` ${chalk5.dim("Format:")} ${chalk5.white(options.format)}`);
|
|
1205
|
+
console.log(` ${chalk5.dim("Output:")} ${chalk5.white(output)}`);
|
|
1206
|
+
console.log("");
|
|
1207
|
+
printStep("Generating migration...");
|
|
1208
|
+
const content = options.format === "sql" ? generateMigrationSql(config) : generateMigrationTs(config);
|
|
1209
|
+
if (options.dryRun) {
|
|
1210
|
+
printInfo("Dry run \u2014 no files written");
|
|
1211
|
+
console.log("");
|
|
1212
|
+
for (const line of content.split("\n")) {
|
|
1213
|
+
console.log(chalk5.dim(` ${line}`));
|
|
1214
|
+
}
|
|
1215
|
+
console.log("");
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const outPath = path3.resolve(process.cwd(), output);
|
|
1219
|
+
const outDir = path3.dirname(outPath);
|
|
1220
|
+
if (!fs3.existsSync(outDir)) {
|
|
1221
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
1222
|
+
}
|
|
1223
|
+
fs3.writeFileSync(outPath, content);
|
|
1224
|
+
printSuccess(`Generated migration at ${output} (${timer.display()})`);
|
|
1225
|
+
console.log("");
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
printError(error.message || String(error));
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
var generateSchemaCommand = new Command5("schema").description("Generate JSON Schema for objectstack.config.ts (for IDE autocomplete)").option("-o, --output <file>", "Output file path", "objectstack.schema.json").option("--dry-run", "Show output without writing").action(async (options) => {
|
|
1232
|
+
printHeader("Generate Schema");
|
|
1233
|
+
try {
|
|
1234
|
+
const timer = createTimer();
|
|
1235
|
+
printStep("Loading ObjectStackDefinitionSchema...");
|
|
1236
|
+
const { z } = await import("zod");
|
|
1237
|
+
const { ObjectStackDefinitionSchema: ObjectStackDefinitionSchema3 } = await import("@objectstack/spec");
|
|
1238
|
+
printStep("Converting to JSON Schema...");
|
|
1239
|
+
const jsonSchema = z.toJSONSchema(ObjectStackDefinitionSchema3, {
|
|
1240
|
+
target: "draft-2020-12"
|
|
1241
|
+
});
|
|
1242
|
+
const schema = {
|
|
1243
|
+
...jsonSchema,
|
|
1244
|
+
$id: "https://schema.objectstack.io/objectstack.config.json",
|
|
1245
|
+
title: "ObjectStack Configuration",
|
|
1246
|
+
description: "JSON Schema for objectstack.config.ts \u2014 generated from ObjectStackDefinitionSchema"
|
|
1247
|
+
};
|
|
1248
|
+
const content = JSON.stringify(schema, null, 2) + "\n";
|
|
1249
|
+
if (options.dryRun) {
|
|
1250
|
+
printInfo("Dry run \u2014 no files written");
|
|
1251
|
+
console.log("");
|
|
1252
|
+
console.log(content);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const outPath = path3.resolve(process.cwd(), options.output);
|
|
1256
|
+
const outDir = path3.dirname(outPath);
|
|
1257
|
+
if (!fs3.existsSync(outDir)) {
|
|
1258
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
1259
|
+
}
|
|
1260
|
+
fs3.writeFileSync(outPath, content);
|
|
1261
|
+
printSuccess(`Generated JSON Schema at ${options.output} (${timer.display()})`);
|
|
1262
|
+
console.log("");
|
|
1263
|
+
console.log(chalk5.dim(" Usage: Reference in your IDE or editor for autocomplete"));
|
|
1264
|
+
console.log(chalk5.dim(` Path: ${outPath}`));
|
|
1265
|
+
console.log("");
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
printError(error.message || String(error));
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
var generateCommand = new Command5("generate").alias("g").description("Generate metadata files or TypeScript types").argument("[type]", "Metadata type to generate (object, view, action, flow, agent, dashboard, app)").argument("[name]", "Name for the metadata (use kebab-case)").option("-d, --dir <directory>", "Target directory (overrides default)").option("--dry-run", "Show what would be created without writing files").addCommand(generateTypesCommand).addCommand(generateClientCommand).addCommand(generateMigrationCommand).addCommand(generateSchemaCommand).action(async (type, name, options) => {
|
|
902
1272
|
if (!type) {
|
|
903
1273
|
printHeader("Generate");
|
|
904
1274
|
console.log(chalk5.bold(" Sub-commands:"));
|
|
905
1275
|
console.log(` ${chalk5.cyan("types".padEnd(12))} Generate TypeScript type definitions from config`);
|
|
1276
|
+
console.log(` ${chalk5.cyan("client".padEnd(12))} Generate a type-safe client SDK from config`);
|
|
1277
|
+
console.log(` ${chalk5.cyan("migration".padEnd(12))} Generate database migration from schema`);
|
|
1278
|
+
console.log(` ${chalk5.cyan("schema".padEnd(12))} Generate JSON Schema for objectstack.config.ts (IDE autocomplete)`);
|
|
906
1279
|
console.log("");
|
|
907
1280
|
console.log(chalk5.bold(" Metadata types:"));
|
|
908
1281
|
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
@@ -1876,7 +2249,237 @@ import chalk11 from "chalk";
|
|
|
1876
2249
|
import { execSync as execSync2 } from "child_process";
|
|
1877
2250
|
import fs10 from "fs";
|
|
1878
2251
|
import path10 from "path";
|
|
1879
|
-
|
|
2252
|
+
function detectCircularDependencies(objects) {
|
|
2253
|
+
const issues = [];
|
|
2254
|
+
const graph = /* @__PURE__ */ new Map();
|
|
2255
|
+
for (const obj of objects) {
|
|
2256
|
+
const deps = [];
|
|
2257
|
+
if (obj.fields && typeof obj.fields === "object") {
|
|
2258
|
+
for (const field of Object.values(obj.fields)) {
|
|
2259
|
+
if (field?.type === "lookup" && field?.reference) {
|
|
2260
|
+
deps.push(field.reference);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
graph.set(obj.name, deps);
|
|
2265
|
+
}
|
|
2266
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2267
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2268
|
+
function dfs(node, path11) {
|
|
2269
|
+
if (stack.has(node)) {
|
|
2270
|
+
const cycleStart = path11.indexOf(node);
|
|
2271
|
+
const cycle = path11.slice(cycleStart).concat(node);
|
|
2272
|
+
issues.push(`Circular dependency: ${cycle.join(" \u2192 ")}`);
|
|
2273
|
+
return true;
|
|
2274
|
+
}
|
|
2275
|
+
if (visited.has(node)) return false;
|
|
2276
|
+
visited.add(node);
|
|
2277
|
+
stack.add(node);
|
|
2278
|
+
for (const dep of graph.get(node) || []) {
|
|
2279
|
+
if (graph.has(dep)) {
|
|
2280
|
+
dfs(dep, [...path11, node]);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
stack.delete(node);
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
for (const name of graph.keys()) {
|
|
2287
|
+
if (!visited.has(name)) {
|
|
2288
|
+
dfs(name, []);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return issues;
|
|
2292
|
+
}
|
|
2293
|
+
function findOrphanViews(config) {
|
|
2294
|
+
const objectNames = /* @__PURE__ */ new Set();
|
|
2295
|
+
if (Array.isArray(config.objects)) {
|
|
2296
|
+
for (const obj of config.objects) {
|
|
2297
|
+
if (obj.name) objectNames.add(obj.name);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const orphans = [];
|
|
2301
|
+
if (Array.isArray(config.views)) {
|
|
2302
|
+
for (const view of config.views) {
|
|
2303
|
+
if (view.object && !objectNames.has(view.object)) {
|
|
2304
|
+
orphans.push(`View "${view.name || "?"}" references non-existent object "${view.object}"`);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return orphans;
|
|
2309
|
+
}
|
|
2310
|
+
function findUnusedObjects(config) {
|
|
2311
|
+
const objectNames = /* @__PURE__ */ new Set();
|
|
2312
|
+
if (Array.isArray(config.objects)) {
|
|
2313
|
+
for (const obj of config.objects) {
|
|
2314
|
+
if (obj.name) objectNames.add(obj.name);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const referencedObjects = /* @__PURE__ */ new Set();
|
|
2318
|
+
if (Array.isArray(config.views)) {
|
|
2319
|
+
for (const view of config.views) {
|
|
2320
|
+
if (view.object) referencedObjects.add(view.object);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (Array.isArray(config.flows)) {
|
|
2324
|
+
for (const flow of config.flows) {
|
|
2325
|
+
if (flow.trigger?.object) referencedObjects.add(flow.trigger.object);
|
|
2326
|
+
if (flow.object) referencedObjects.add(flow.object);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (Array.isArray(config.apps)) {
|
|
2330
|
+
for (const app of config.apps) {
|
|
2331
|
+
if (Array.isArray(app.navigation)) {
|
|
2332
|
+
for (const nav of app.navigation) {
|
|
2333
|
+
if (nav.object) referencedObjects.add(nav.object);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (Array.isArray(config.agents)) {
|
|
2339
|
+
for (const agent of config.agents) {
|
|
2340
|
+
if (Array.isArray(agent.objects)) {
|
|
2341
|
+
for (const o of agent.objects) referencedObjects.add(o);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
if (Array.isArray(config.objects)) {
|
|
2346
|
+
for (const obj of config.objects) {
|
|
2347
|
+
if (obj.fields && typeof obj.fields === "object") {
|
|
2348
|
+
for (const field of Object.values(obj.fields)) {
|
|
2349
|
+
if (field?.type === "lookup" && field?.reference) {
|
|
2350
|
+
referencedObjects.add(field.reference);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const unused = [];
|
|
2357
|
+
for (const name of objectNames) {
|
|
2358
|
+
if (!referencedObjects.has(name)) {
|
|
2359
|
+
unused.push(`Object "${name}" is defined but not referenced by any view, flow, app, or agent`);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return unused;
|
|
2363
|
+
}
|
|
2364
|
+
function walkDir(dir, ext) {
|
|
2365
|
+
const results = [];
|
|
2366
|
+
if (!fs10.existsSync(dir)) return results;
|
|
2367
|
+
const entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
2368
|
+
for (const entry of entries) {
|
|
2369
|
+
if (entry.name === "node_modules") continue;
|
|
2370
|
+
const fullPath = path10.join(dir, entry.name);
|
|
2371
|
+
if (entry.isDirectory()) {
|
|
2372
|
+
results.push(...walkDir(fullPath, ext));
|
|
2373
|
+
} else if (entry.name.endsWith(ext)) {
|
|
2374
|
+
results.push(fullPath);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return results;
|
|
2378
|
+
}
|
|
2379
|
+
function findMissingTests(cwd) {
|
|
2380
|
+
const specSrcDir = path10.join(cwd, "packages/spec/src");
|
|
2381
|
+
if (!fs10.existsSync(specSrcDir)) return [];
|
|
2382
|
+
const missing = [];
|
|
2383
|
+
const zodFiles = walkDir(specSrcDir, ".zod.ts");
|
|
2384
|
+
for (const zodFile of zodFiles) {
|
|
2385
|
+
const testFile = zodFile.replace(".zod.ts", ".test.ts");
|
|
2386
|
+
if (!fs10.existsSync(testFile)) {
|
|
2387
|
+
const relZod = path10.relative(specSrcDir, zodFile);
|
|
2388
|
+
const relTest = path10.relative(specSrcDir, testFile);
|
|
2389
|
+
missing.push(`Missing test: ${relTest} (for ${relZod})`);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return missing;
|
|
2393
|
+
}
|
|
2394
|
+
function findDeprecatedUsages(cwd) {
|
|
2395
|
+
const specSrcDir = path10.join(cwd, "packages/spec/src");
|
|
2396
|
+
if (!fs10.existsSync(specSrcDir)) return [];
|
|
2397
|
+
const deprecated = [];
|
|
2398
|
+
const tsFiles = walkDir(specSrcDir, ".ts").filter((f) => !f.endsWith(".test.ts"));
|
|
2399
|
+
for (const tsFile of tsFiles) {
|
|
2400
|
+
try {
|
|
2401
|
+
const content = fs10.readFileSync(tsFile, "utf-8");
|
|
2402
|
+
const lines = content.split("\n");
|
|
2403
|
+
const relPath = path10.relative(specSrcDir, tsFile);
|
|
2404
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2405
|
+
if (lines[i].includes("@deprecated")) {
|
|
2406
|
+
deprecated.push(`${relPath}:${i + 1} \u2014 @deprecated tag found`);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
} catch {
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return deprecated;
|
|
2413
|
+
}
|
|
2414
|
+
var DEPRECATED_PATTERNS = [
|
|
2415
|
+
{
|
|
2416
|
+
pattern: /\bEnhancedObjectKernel\b/,
|
|
2417
|
+
description: "EnhancedObjectKernel is deprecated in v3",
|
|
2418
|
+
replacement: "Use ObjectKernel instead"
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
pattern: /\bmax_length\b/,
|
|
2422
|
+
description: "snake_case config key: max_length",
|
|
2423
|
+
replacement: "Use maxLength (camelCase)"
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
pattern: /\bdefault_value\b/,
|
|
2427
|
+
description: "snake_case config key: default_value",
|
|
2428
|
+
replacement: "Use defaultValue (camelCase)"
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
pattern: /\bmin_length\b/,
|
|
2432
|
+
description: "snake_case config key: min_length",
|
|
2433
|
+
replacement: "Use minLength (camelCase)"
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
pattern: /\breference_filters\b/,
|
|
2437
|
+
description: "snake_case config key: reference_filters",
|
|
2438
|
+
replacement: "Use referenceFilters (camelCase)"
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
pattern: /\bunique_name\b/,
|
|
2442
|
+
description: "snake_case config key: unique_name",
|
|
2443
|
+
replacement: "Use uniqueName (camelCase)"
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/,
|
|
2447
|
+
description: "Import from deprecated @objectstack/core/enhanced path",
|
|
2448
|
+
replacement: "Use import from '@objectstack/core'"
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/,
|
|
2452
|
+
description: "Import from deprecated @objectstack/spec/dist/ deep path",
|
|
2453
|
+
replacement: "Use import from '@objectstack/spec'"
|
|
2454
|
+
}
|
|
2455
|
+
];
|
|
2456
|
+
function scanDeprecatedPatterns(dir) {
|
|
2457
|
+
const results = [];
|
|
2458
|
+
if (!fs10.existsSync(dir)) return results;
|
|
2459
|
+
const tsFiles = walkDir(dir, ".ts").filter((f) => !f.endsWith(".test.ts"));
|
|
2460
|
+
for (const tsFile of tsFiles) {
|
|
2461
|
+
try {
|
|
2462
|
+
const content = fs10.readFileSync(tsFile, "utf-8");
|
|
2463
|
+
const lines = content.split("\n");
|
|
2464
|
+
const relPath = path10.relative(process.cwd(), tsFile);
|
|
2465
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2466
|
+
for (const dp of DEPRECATED_PATTERNS) {
|
|
2467
|
+
if (dp.pattern.test(lines[i])) {
|
|
2468
|
+
results.push({
|
|
2469
|
+
file: relPath,
|
|
2470
|
+
line: i + 1,
|
|
2471
|
+
description: dp.description,
|
|
2472
|
+
replacement: dp.replacement
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
return results;
|
|
2481
|
+
}
|
|
2482
|
+
var doctorCommand = new Command11("doctor").description("Check development environment and configuration health").option("-v, --verbose", "Show detailed information").option("--scan-deprecations", "Scan for deprecated ObjectStack patterns").action(async (options) => {
|
|
1880
2483
|
printHeader("Environment Health Check");
|
|
1881
2484
|
const results = [];
|
|
1882
2485
|
try {
|
|
@@ -1998,6 +2601,87 @@ var doctorCommand = new Command11("doctor").description("Check development envir
|
|
|
1998
2601
|
if (result.status === "error") hasErrors = true;
|
|
1999
2602
|
if (result.status === "warning") hasWarnings = true;
|
|
2000
2603
|
});
|
|
2604
|
+
printStep("Checking for missing test files...");
|
|
2605
|
+
const missingTests = findMissingTests(cwd);
|
|
2606
|
+
if (missingTests.length > 0) {
|
|
2607
|
+
hasWarnings = true;
|
|
2608
|
+
for (const msg of missingTests) {
|
|
2609
|
+
printWarning(msg);
|
|
2610
|
+
}
|
|
2611
|
+
} else {
|
|
2612
|
+
printSuccess("Test coverage All *.zod.ts files have matching tests");
|
|
2613
|
+
}
|
|
2614
|
+
printStep("Scanning for @deprecated usage...");
|
|
2615
|
+
const deprecatedUsages = findDeprecatedUsages(cwd);
|
|
2616
|
+
if (deprecatedUsages.length > 0) {
|
|
2617
|
+
hasWarnings = true;
|
|
2618
|
+
for (const msg of deprecatedUsages) {
|
|
2619
|
+
printWarning(`Deprecated: ${msg}`);
|
|
2620
|
+
}
|
|
2621
|
+
} else {
|
|
2622
|
+
printSuccess("Deprecations No @deprecated tags found");
|
|
2623
|
+
}
|
|
2624
|
+
if (configExists()) {
|
|
2625
|
+
printStep("Loading configuration for analysis...");
|
|
2626
|
+
try {
|
|
2627
|
+
const { config } = await loadConfig();
|
|
2628
|
+
if (Array.isArray(config.objects) && config.objects.length > 0) {
|
|
2629
|
+
printStep("Checking for circular dependencies...");
|
|
2630
|
+
const cycles = detectCircularDependencies(config.objects);
|
|
2631
|
+
if (cycles.length > 0) {
|
|
2632
|
+
hasWarnings = true;
|
|
2633
|
+
for (const msg of cycles) {
|
|
2634
|
+
printWarning(msg);
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
printSuccess("Dependencies No circular references detected");
|
|
2638
|
+
}
|
|
2639
|
+
printStep("Checking for unused objects...");
|
|
2640
|
+
const unused = findUnusedObjects(config);
|
|
2641
|
+
if (unused.length > 0) {
|
|
2642
|
+
hasWarnings = true;
|
|
2643
|
+
for (const msg of unused) {
|
|
2644
|
+
printWarning(msg);
|
|
2645
|
+
}
|
|
2646
|
+
} else {
|
|
2647
|
+
printSuccess("Object usage All objects are referenced");
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (Array.isArray(config.views) && config.views.length > 0) {
|
|
2651
|
+
printStep("Checking for orphan views...");
|
|
2652
|
+
const orphans = findOrphanViews(config);
|
|
2653
|
+
if (orphans.length > 0) {
|
|
2654
|
+
hasWarnings = true;
|
|
2655
|
+
for (const msg of orphans) {
|
|
2656
|
+
printWarning(msg);
|
|
2657
|
+
}
|
|
2658
|
+
} else {
|
|
2659
|
+
printSuccess("View integrity All views reference valid objects");
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
} catch {
|
|
2663
|
+
printWarning("Could not load config for analysis (config checks skipped)");
|
|
2664
|
+
hasWarnings = true;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (options.scanDeprecations) {
|
|
2668
|
+
printStep("Scanning for deprecated ObjectStack patterns...");
|
|
2669
|
+
const scanDir = path10.join(cwd, "src");
|
|
2670
|
+
const deprecations = scanDeprecatedPatterns(scanDir);
|
|
2671
|
+
if (deprecations.length > 0) {
|
|
2672
|
+
hasWarnings = true;
|
|
2673
|
+
for (const dep of deprecations) {
|
|
2674
|
+
printWarning(`${dep.file}:${dep.line} \u2014 ${dep.description}`);
|
|
2675
|
+
if (options.verbose) {
|
|
2676
|
+
console.log(chalk11.dim(` \u2192 ${dep.replacement}`));
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
console.log("");
|
|
2680
|
+
printInfo(`Found ${deprecations.length} deprecated pattern(s). Run \`objectstack codemod v2-to-v3\` to auto-fix.`);
|
|
2681
|
+
} else {
|
|
2682
|
+
printSuccess("Deprecation scan No deprecated patterns found");
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2001
2685
|
console.log("");
|
|
2002
2686
|
if (hasErrors) {
|
|
2003
2687
|
console.log(chalk11.red("\u274C Some critical issues found. Please fix them before continuing."));
|