@apexkit/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/apexkit.js +7 -0
- package/package.json +43 -0
- package/src/index.js +1347 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
// apexkit-watch.js - ApexKit Developer Tools, Sync Watcher & IntelliSense Engine
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import * as readline from "readline/promises";
|
|
5
|
+
|
|
6
|
+
// --- 1. ENV LOADER ---
|
|
7
|
+
if (fs.existsSync(".env")) {
|
|
8
|
+
const envConfig = fs.readFileSync(".env", "utf-8");
|
|
9
|
+
envConfig.split("\n").forEach((line) => {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
12
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
13
|
+
if (key && valueParts.length > 0) {
|
|
14
|
+
const val = valueParts.join("=").split("#")[0].trim();
|
|
15
|
+
process.env[key.trim()] = val;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- 2. CONFIG & CLI COMMAND PARSER ---
|
|
22
|
+
const API_KEY = process.env.APEXKIT_API_KEY || "root_sys_prod_12345678_abcd";
|
|
23
|
+
const SCOPE_KEY = process.env.SCOPE_KEY || "root";
|
|
24
|
+
const BASE_URL = process.env.APEXKIT_URL || `http://localhost:${process.env.PORT || 5000}`;
|
|
25
|
+
const WS_URL = `${BASE_URL.replace(/^http/, "ws")}/dev/sync?api_key=${API_KEY}&scope_key=${SCOPE_KEY}`;
|
|
26
|
+
|
|
27
|
+
const RAW_ARGS = process.argv.slice(2);
|
|
28
|
+
const SUBCOMMAND = RAW_ARGS.find((a) => !a.startsWith("-"))?.toLowerCase() || "";
|
|
29
|
+
|
|
30
|
+
const IS_INIT = RAW_ARGS.includes("--init") || SUBCOMMAND === "init";
|
|
31
|
+
const IS_INIT_FILE =
|
|
32
|
+
RAW_ARGS.includes("--init-file") ||
|
|
33
|
+
SUBCOMMAND === "create" ||
|
|
34
|
+
SUBCOMMAND === "new" ||
|
|
35
|
+
SUBCOMMAND === "init:file" ||
|
|
36
|
+
SUBCOMMAND === "init-file";
|
|
37
|
+
const IS_PULL = RAW_ARGS.includes("--pull") || SUBCOMMAND === "pull";
|
|
38
|
+
const IS_COMMIT = RAW_ARGS.includes("--commit") || SUBCOMMAND === "commit";
|
|
39
|
+
const IS_PUSH = RAW_ARGS.includes("--push") || SUBCOMMAND === "push";
|
|
40
|
+
const IS_STATUS = RAW_ARGS.includes("--status") || SUBCOMMAND === "status" || SUBCOMMAND === "info";
|
|
41
|
+
const NO_AUTO_COMMIT = RAW_ARGS.includes("--no-auto-commit");
|
|
42
|
+
|
|
43
|
+
// --- CATEGORY FLAGS: --scripts | --templates | --actions | --all (Default: scripts) ---
|
|
44
|
+
const hasScriptsFlag = RAW_ARGS.includes("--scripts") || RAW_ARGS.includes("-s");
|
|
45
|
+
const hasTemplatesFlag = RAW_ARGS.includes("--templates") || RAW_ARGS.includes("-t");
|
|
46
|
+
const hasActionsFlag =
|
|
47
|
+
RAW_ARGS.includes("--actions") || RAW_ARGS.includes("--ai-actions") || RAW_ARGS.includes("-a");
|
|
48
|
+
const hasAllFlag = RAW_ARGS.includes("--all") || RAW_ARGS.includes("-A");
|
|
49
|
+
|
|
50
|
+
let syncScripts = false;
|
|
51
|
+
let syncTemplates = false;
|
|
52
|
+
let syncActions = false;
|
|
53
|
+
|
|
54
|
+
if (hasAllFlag) {
|
|
55
|
+
syncScripts = true;
|
|
56
|
+
syncTemplates = true;
|
|
57
|
+
syncActions = true;
|
|
58
|
+
} else if (hasScriptsFlag || hasTemplatesFlag || hasActionsFlag) {
|
|
59
|
+
syncScripts = hasScriptsFlag;
|
|
60
|
+
syncTemplates = hasTemplatesFlag;
|
|
61
|
+
syncActions = hasActionsFlag;
|
|
62
|
+
} else {
|
|
63
|
+
// Default to scripts only if no flag is specified
|
|
64
|
+
syncScripts = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse --delete-remote flag
|
|
68
|
+
const deleteRemoteArg = RAW_ARGS.find((a) => a.startsWith("--delete-remote"));
|
|
69
|
+
let DELETE_REMOTE_TYPES = [];
|
|
70
|
+
if (deleteRemoteArg) {
|
|
71
|
+
const value = deleteRemoteArg.split("=")[1];
|
|
72
|
+
if (!value || value === "all" || value === "true") {
|
|
73
|
+
DELETE_REMOTE_TYPES = ["scripts", "templates", "ai_actions"];
|
|
74
|
+
} else {
|
|
75
|
+
DELETE_REMOTE_TYPES = value.split(",").map((s) => s.trim().toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getScopedApiBase() {
|
|
80
|
+
let scopePath = "";
|
|
81
|
+
if (SCOPE_KEY.startsWith("tenant:")) {
|
|
82
|
+
scopePath = `/tenant/${SCOPE_KEY.replace("tenant:", "")}`;
|
|
83
|
+
} else if (SCOPE_KEY.startsWith("sandbox:")) {
|
|
84
|
+
scopePath = `/sandbox/${SCOPE_KEY.replace("sandbox:", "")}`;
|
|
85
|
+
}
|
|
86
|
+
return `${BASE_URL.replace(/\/$/, "")}${scopePath}/api/v1`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getAuthHeaders() {
|
|
90
|
+
return {
|
|
91
|
+
"x-api-key": API_KEY,
|
|
92
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- 3. TRIGGER DEFINITIONS ---
|
|
98
|
+
const TRIGGER_DEFINITIONS = [
|
|
99
|
+
{ id: "manual", name: "manual (Public/Protected HTTP Webhook Endpoint)", desc: "Triggered via HTTP GET/POST/PUT/DELETE on /api/v1/run/:name or /api/v1/webhook/:name" },
|
|
100
|
+
{ id: "before_create_record", name: "before_create_record", desc: "Runs before a record is inserted. Can validate or mutate data." },
|
|
101
|
+
{ id: "after_create_record", name: "after_create_record", desc: "Runs after a record is inserted. Used for notifications/indexes." },
|
|
102
|
+
{ id: "before_update_record", name: "before_update_record", desc: "Runs before an existing record is updated. Can validate changes." },
|
|
103
|
+
{ id: "after_update_record", name: "after_update_record", desc: "Runs after a record is updated." },
|
|
104
|
+
{ id: "before_delete_record", name: "before_delete_record", desc: "Runs before a record is deleted. Can block deletion." },
|
|
105
|
+
{ id: "after_delete_record", name: "after_delete_record", desc: "Runs after a record has been deleted." },
|
|
106
|
+
{ id: "before_list_records", name: "before_list_records", desc: "Intercepts list queries before they execute." },
|
|
107
|
+
{ id: "after_list_records", name: "after_list_records", desc: "Filters or transforms listed records before response." },
|
|
108
|
+
{ id: "before_get_record", name: "before_get_record", desc: "Runs before a single record is fetched." },
|
|
109
|
+
{ id: "after_get_record", name: "after_get_record", desc: "Transforms a fetched record before response." },
|
|
110
|
+
{ id: "before_user_login", name: "before_user_login", desc: "Runs before user authentication. Can verify IP/Rate limits." },
|
|
111
|
+
{ id: "after_user_login", name: "after_user_login", desc: "Runs after successful user login." },
|
|
112
|
+
{ id: "before_user_create", name: "before_user_create", desc: "Runs before user registration. Enforce roles or rules." },
|
|
113
|
+
{ id: "after_user_create", name: "after_user_create", desc: "Runs after user registration. Trigger welcome emails." },
|
|
114
|
+
{ id: "before_user_delete", name: "before_user_delete", desc: "Runs before a user is deleted." },
|
|
115
|
+
{ id: "after_user_delete", name: "after_user_delete", desc: "Runs after a user is deleted." },
|
|
116
|
+
{ id: "before_list_users", name: "before_list_users", desc: "Intercepts user list queries." },
|
|
117
|
+
{ id: "after_list_users", name: "after_list_users", desc: "Transforms user list responses." },
|
|
118
|
+
{ id: "before_collection_create", name: "before_collection_create", desc: "Validates collection schema before creation." },
|
|
119
|
+
{ id: "after_collection_create", name: "after_collection_create", desc: "Runs after collection creation." },
|
|
120
|
+
{ id: "before_collection_update", name: "before_collection_update", desc: "Runs before collection schema updates." },
|
|
121
|
+
{ id: "after_collection_update", name: "after_collection_update", desc: "Runs after collection schema updates." },
|
|
122
|
+
{ id: "before_collection_delete", name: "before_collection_delete", desc: "Runs before collection deletion." },
|
|
123
|
+
{ id: "before_file_upload", name: "before_file_upload", desc: "Runs before a file is written to storage." },
|
|
124
|
+
{ id: "after_file_upload", name: "after_file_upload", desc: "Runs after file upload. Can trigger virus scan or indexing." },
|
|
125
|
+
{ id: "before_file_delete", name: "before_file_delete", desc: "Runs before file is deleted." },
|
|
126
|
+
{ id: "after_file_delete", name: "after_file_delete", desc: "Runs after file is deleted." },
|
|
127
|
+
{ id: "before_relation_create", name: "before_relation_create", desc: "Runs before relationship is linked." },
|
|
128
|
+
{ id: "after_relation_create", name: "after_relation_create", desc: "Runs after relationship is linked." },
|
|
129
|
+
{ id: "before_relation_delete", name: "before_relation_delete", desc: "Runs before relationship is removed." },
|
|
130
|
+
{ id: "after_relation_delete", name: "after_relation_delete", desc: "Runs after relationship is removed." },
|
|
131
|
+
{ id: "before_tenant_create", name: "before_tenant_create", desc: "Runs before a tenant database is provisioned." },
|
|
132
|
+
{ id: "after_tenant_create", name: "after_tenant_create", desc: "Runs after tenant database is provisioned." },
|
|
133
|
+
{ id: "before_tenant_request", name: "before_tenant_request", desc: "Intercepts all incoming HTTP requests to a tenant." },
|
|
134
|
+
{ id: "after_tenant_request", name: "after_tenant_request", desc: "Processes tenant HTTP response metrics." },
|
|
135
|
+
{ id: "before_sandbox_request", name: "before_sandbox_request", desc: "Intercepts sandbox requests." },
|
|
136
|
+
{ id: "after_sandbox_request", name: "after_sandbox_request", desc: "Processes sandbox response metrics." },
|
|
137
|
+
{ id: "before_ai_run", name: "before_ai_run", desc: "Intercepts AI Prompt Actions to inject RAG search vectors." },
|
|
138
|
+
{ id: "after_ai_run", name: "after_ai_run", desc: "Runs after AI Prompt execution." },
|
|
139
|
+
{ id: "on_vectorization_start", name: "on_vectorization_start", desc: "Fires when revectorization job begins." },
|
|
140
|
+
{ id: "cron", name: "cron (Scheduled Job)", desc: "Executed periodically according to Cron expression settings." },
|
|
141
|
+
{ id: "graphql", name: "graphql (Custom Resolver)", desc: "Dynamic Query/Mutation field resolver for GraphQL schema." }
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// --- 4. WORKSPACE TYPES & CONFIG GENERATOR ---
|
|
145
|
+
async function generateWorkspaceFiles() {
|
|
146
|
+
console.log(" ⏳ Fetching collection schemas from DB...");
|
|
147
|
+
let collectionsData = [];
|
|
148
|
+
try {
|
|
149
|
+
const fetchUrl = `${getScopedApiBase()}/collections`;
|
|
150
|
+
const res = await fetch(fetchUrl, { headers: getAuthHeaders() });
|
|
151
|
+
if (res.ok) {
|
|
152
|
+
collectionsData = await res.json();
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn(` ⚠️ Could not connect to DB for schemas: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let collectionTypesStr = `export interface Collections {\n`;
|
|
159
|
+
let collectionExpandsStr = `export interface CollectionExpands {\n`;
|
|
160
|
+
const collectionNames = [];
|
|
161
|
+
|
|
162
|
+
collectionsData.forEach((col) => {
|
|
163
|
+
collectionNames.push(`"${col.name}"`);
|
|
164
|
+
let fieldsStr = "";
|
|
165
|
+
let expandsStr = "";
|
|
166
|
+
|
|
167
|
+
if (col.schema) {
|
|
168
|
+
if (col.schema.fields) {
|
|
169
|
+
for (const [name, def] of Object.entries(col.schema.fields)) {
|
|
170
|
+
let tsType = "any";
|
|
171
|
+
switch (def.type) {
|
|
172
|
+
case "string":
|
|
173
|
+
case "text":
|
|
174
|
+
case "email":
|
|
175
|
+
case "url":
|
|
176
|
+
case "date":
|
|
177
|
+
case "blob":
|
|
178
|
+
case "file":
|
|
179
|
+
tsType = "string";
|
|
180
|
+
break;
|
|
181
|
+
case "number":
|
|
182
|
+
tsType = "number";
|
|
183
|
+
break;
|
|
184
|
+
case "owner":
|
|
185
|
+
case "relation":
|
|
186
|
+
tsType = "number | string";
|
|
187
|
+
break;
|
|
188
|
+
case "boolean":
|
|
189
|
+
tsType = "boolean";
|
|
190
|
+
break;
|
|
191
|
+
case "vector":
|
|
192
|
+
tsType = "number[]";
|
|
193
|
+
break;
|
|
194
|
+
case "geopoint":
|
|
195
|
+
tsType = "{ lat: number; lng: number }";
|
|
196
|
+
break;
|
|
197
|
+
case "json":
|
|
198
|
+
tsType = "Record<string, any> | any[]";
|
|
199
|
+
break;
|
|
200
|
+
case "select":
|
|
201
|
+
tsType =
|
|
202
|
+
def.options && def.options.length > 0
|
|
203
|
+
? def.options.map((o) => `"${o}"`).join(" | ")
|
|
204
|
+
: "string";
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
208
|
+
fieldsStr += ` ${safeName}${def.required ? "" : "?"}: ${tsType};\n`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (col.schema.relations) {
|
|
213
|
+
for (const [name, rel] of Object.entries(col.schema.relations)) {
|
|
214
|
+
const isMany = rel.relation_type === "many";
|
|
215
|
+
const tsType = isMany ? "(number | string)[]" : "number | string";
|
|
216
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
217
|
+
fieldsStr += ` ${safeName}${rel.required ? "" : "?"}: ${tsType};\n`;
|
|
218
|
+
|
|
219
|
+
const targetCol = rel.target_collection;
|
|
220
|
+
const expandType = `{ id: number | string; data: Collections["${targetCol}"]; created: string; updated: string; expand?: any }`;
|
|
221
|
+
expandsStr += ` ${safeName}?: ${isMany ? `Array<${expandType}>` : expandType};\n`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
collectionTypesStr += ` "${col.name}": {\n${fieldsStr} };\n`;
|
|
227
|
+
collectionExpandsStr += ` "${col.name}": {\n${expandsStr} [reverse_relation: string]: any;\n };\n`;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (collectionsData.length === 0) {
|
|
231
|
+
collectionTypesStr += ` [key: string]: Record<string, any>;\n`;
|
|
232
|
+
collectionExpandsStr += ` [key: string]: any;\n`;
|
|
233
|
+
}
|
|
234
|
+
collectionTypesStr += `}\n\nexport type CollectionName = ${
|
|
235
|
+
collectionNames.length > 0 ? collectionNames.join(" | ") : "string"
|
|
236
|
+
};\n`;
|
|
237
|
+
collectionExpandsStr += `}\n\n`;
|
|
238
|
+
|
|
239
|
+
const triggerUnion = TRIGGER_DEFINITIONS.map((t) => ` /** ${t.desc} */\n | "${t.id}"`).join("\n");
|
|
240
|
+
|
|
241
|
+
const types = `/**
|
|
242
|
+
* ApexKit Global Types & IntelliSense
|
|
243
|
+
* Auto-generated by apexkit-watch.js
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
${collectionTypesStr}
|
|
247
|
+
${collectionExpandsStr}
|
|
248
|
+
|
|
249
|
+
export type TriggerType =
|
|
250
|
+
${triggerUnion};
|
|
251
|
+
|
|
252
|
+
export type ScriptType = "webhook" | "custom:module" | "esm:module" | "template" | "ai_action";
|
|
253
|
+
export type ScriptVisibility = "private" | "public";
|
|
254
|
+
|
|
255
|
+
export interface FileMetadata {
|
|
256
|
+
id?: number;
|
|
257
|
+
name?: string;
|
|
258
|
+
extension?: "js" | "ts" | "html" | "json" | string;
|
|
259
|
+
target_collection?: CollectionName | null;
|
|
260
|
+
type?: ScriptType;
|
|
261
|
+
path?: string;
|
|
262
|
+
trigger_type?: TriggerType;
|
|
263
|
+
active?: boolean;
|
|
264
|
+
visibility?: ScriptVisibility;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface AuthContext {
|
|
268
|
+
id: number | string;
|
|
269
|
+
email: string;
|
|
270
|
+
role: string;
|
|
271
|
+
scope: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface RecordItem<T extends keyof Collections = any> {
|
|
275
|
+
id: number | string;
|
|
276
|
+
data: T extends keyof Collections ? Collections[T] : any;
|
|
277
|
+
created: string;
|
|
278
|
+
updated: string;
|
|
279
|
+
expand?: T extends keyof CollectionExpands ? CollectionExpands[T] : any;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface RecordHookEvent<T extends keyof Collections = any> {
|
|
283
|
+
trigger: TriggerType;
|
|
284
|
+
record: {
|
|
285
|
+
id: number | string | null;
|
|
286
|
+
data: T extends keyof Collections ? Collections[T] : any;
|
|
287
|
+
};
|
|
288
|
+
collection: {
|
|
289
|
+
id: number;
|
|
290
|
+
name: string;
|
|
291
|
+
schema?: any;
|
|
292
|
+
};
|
|
293
|
+
auth: AuthContext | null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface VoidHookEvent {
|
|
297
|
+
trigger: TriggerType;
|
|
298
|
+
data: any;
|
|
299
|
+
auth: AuthContext | null;
|
|
300
|
+
timestamp: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
declare global {
|
|
304
|
+
const __fileMetadata__: FileMetadata;
|
|
305
|
+
|
|
306
|
+
/** ApexKit Database Client */
|
|
307
|
+
const $db: {
|
|
308
|
+
records: {
|
|
309
|
+
list<T extends keyof Collections>(
|
|
310
|
+
collection: T,
|
|
311
|
+
options?: {
|
|
312
|
+
page?: number;
|
|
313
|
+
per_page?: number;
|
|
314
|
+
limit?: number;
|
|
315
|
+
offset?: number;
|
|
316
|
+
sort?: string;
|
|
317
|
+
filter?: string | Record<string, any>;
|
|
318
|
+
expand?: string;
|
|
319
|
+
fields?: string;
|
|
320
|
+
}
|
|
321
|
+
): Promise<{ items: RecordItem<T>[]; total: number }>;
|
|
322
|
+
|
|
323
|
+
get<T extends keyof Collections>(
|
|
324
|
+
collection: T,
|
|
325
|
+
id: number | string,
|
|
326
|
+
expand?: string
|
|
327
|
+
): Promise<RecordItem<T> | null>;
|
|
328
|
+
|
|
329
|
+
create<T extends keyof Collections>(
|
|
330
|
+
collection: T,
|
|
331
|
+
data: Partial<Collections[T]>
|
|
332
|
+
): Promise<{ id: number | string }>;
|
|
333
|
+
|
|
334
|
+
update<T extends keyof Collections>(
|
|
335
|
+
collection: T,
|
|
336
|
+
id: number | string,
|
|
337
|
+
data: Partial<Collections[T]>
|
|
338
|
+
): Promise<RecordItem<T>>;
|
|
339
|
+
|
|
340
|
+
delete(collection: string, id: number | string): Promise<boolean>;
|
|
341
|
+
|
|
342
|
+
searchVector<T extends keyof Collections>(
|
|
343
|
+
collection: T,
|
|
344
|
+
field: string,
|
|
345
|
+
vector: number[],
|
|
346
|
+
limit?: number
|
|
347
|
+
): Promise<(RecordItem<T> & { _score: number })[]>;
|
|
348
|
+
|
|
349
|
+
getVector(
|
|
350
|
+
collection: string,
|
|
351
|
+
id: number | string
|
|
352
|
+
): Promise<{ field_name: string; vector: number[]; model: string }[]>;
|
|
353
|
+
|
|
354
|
+
instantSearch(
|
|
355
|
+
collection: string,
|
|
356
|
+
query: string,
|
|
357
|
+
limit?: number
|
|
358
|
+
): Promise<{ id: number | string; score: number; snippet: any }[]>;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
query(queryObject: {
|
|
362
|
+
from: string;
|
|
363
|
+
select?: any[];
|
|
364
|
+
where?: Record<string, any>;
|
|
365
|
+
group_by?: string[];
|
|
366
|
+
sort?: string;
|
|
367
|
+
limit?: number;
|
|
368
|
+
offset?: number;
|
|
369
|
+
system?: boolean;
|
|
370
|
+
pipeline?: any[];
|
|
371
|
+
}): Promise<any[]>;
|
|
372
|
+
|
|
373
|
+
users: {
|
|
374
|
+
create(
|
|
375
|
+
email: string,
|
|
376
|
+
passwordHash: string,
|
|
377
|
+
role: string,
|
|
378
|
+
metadata?: Record<string, any>
|
|
379
|
+
): Promise<{ id: number | string; email: string; role: string }>;
|
|
380
|
+
get(email: string): Promise<{ id: number | string; email: string; role: string; metadata?: any } | null>;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
collections: {
|
|
384
|
+
list(): Promise<{ id: number; name: string; schema?: any; index?: string }[]>;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
files: {
|
|
388
|
+
list(
|
|
389
|
+
limit?: number,
|
|
390
|
+
offset?: number
|
|
391
|
+
): Promise<
|
|
392
|
+
{ id: number | string; filename: string; original_name: string; mime_type: string; size: number; created_at: string }[]
|
|
393
|
+
>;
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/** Universal HTTP Client */
|
|
398
|
+
const $http: {
|
|
399
|
+
get(url: string): Promise<string>;
|
|
400
|
+
post(url: string, body: any): Promise<string>;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/** Native Storage & File Engine */
|
|
404
|
+
const $files: {
|
|
405
|
+
read(filename: string): Promise<string>;
|
|
406
|
+
delete(filenameOrId: string | number): Promise<boolean>;
|
|
407
|
+
save(
|
|
408
|
+
filename: string,
|
|
409
|
+
data: string | ArrayBuffer | Uint8Array,
|
|
410
|
+
mime?: string
|
|
411
|
+
): Promise<{ id: number | string; url: string; filename: string }>;
|
|
412
|
+
getSignedUrl(filename: string, ttl_secs?: number): Promise<string>;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/** Scoped Virtual File System */
|
|
416
|
+
const $fs: {
|
|
417
|
+
read(path: string): Promise<string>;
|
|
418
|
+
readBytes(path: string): Promise<string>;
|
|
419
|
+
write(path: string, content: string): Promise<boolean>;
|
|
420
|
+
writeBytes(path: string, data: string | ArrayBuffer | Uint8Array): Promise<boolean>;
|
|
421
|
+
delete(path: string): Promise<boolean>;
|
|
422
|
+
list(path: string): Promise<{ name: string; isDir: boolean; size: number }[]>;
|
|
423
|
+
exists(path: string): Promise<boolean>;
|
|
424
|
+
mkdir(path: string): Promise<boolean>;
|
|
425
|
+
stat(path: string): Promise<{ size: number; isDir: boolean; created?: number; modified?: number }>;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/** In-Memory & Tenant-Isolated Fast Cache */
|
|
429
|
+
const $cache: {
|
|
430
|
+
get(key: string): Promise<string | null>;
|
|
431
|
+
set(key: string, value: any, ttl_secs?: number): Promise<void>;
|
|
432
|
+
delete(key: string): Promise<void>;
|
|
433
|
+
del(key: string): Promise<void>;
|
|
434
|
+
incr(key: string, delta?: number): Promise<number>;
|
|
435
|
+
listKeys(): Promise<string[]>;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/** Outbound SMTP Email Dispatcher */
|
|
439
|
+
const $mail: {
|
|
440
|
+
send(to: string, subject: string, body: string): Promise<boolean>;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/** Real-time WebSocket & SSE Event Dispatcher */
|
|
444
|
+
const $realtime: {
|
|
445
|
+
send(channel: string, event: string, data: any): Promise<boolean>;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/** Script Inter-calling Engine */
|
|
449
|
+
const $run: {
|
|
450
|
+
script(name: string, payload: any): Promise<any>;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/** Cryptographic & String Utilities */
|
|
454
|
+
const $util: {
|
|
455
|
+
uuid(): string;
|
|
456
|
+
slugify(text: string): string;
|
|
457
|
+
hash(text: string, alg: "sha256" | "sha512"): string;
|
|
458
|
+
hmac(text: string, key: string): string;
|
|
459
|
+
base64Encode(data: string | ArrayBuffer | Uint8Array): string;
|
|
460
|
+
base64EncodeBuffer(buffer: ArrayBuffer | Uint8Array): string;
|
|
461
|
+
base64Decode(text: string): string;
|
|
462
|
+
base64DecodeBuffer(text: string): ArrayBuffer;
|
|
463
|
+
sleep(ms: number): Promise<void>;
|
|
464
|
+
randomHex(len?: number): string;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/** Environment & Configuration Secrets Accessor */
|
|
468
|
+
const $env: {
|
|
469
|
+
get(key: string): Promise<string>;
|
|
470
|
+
has(key: string): Promise<boolean>;
|
|
471
|
+
list(): Promise<Record<string, string>>;
|
|
472
|
+
readonly BASE_URL: string;
|
|
473
|
+
readonly APP_URL: string;
|
|
474
|
+
readonly LOCAL_BASE_URL: string;
|
|
475
|
+
readonly LOCAL_APP_URL: string;
|
|
476
|
+
readonly SMTP_BLOCKED: boolean;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
function fetch(
|
|
480
|
+
input: string | Request | URL,
|
|
481
|
+
init?: { method?: string; headers?: any; body?: any; redirect?: "follow" | "manual" | "error" }
|
|
482
|
+
): Promise<Response>;
|
|
483
|
+
}
|
|
484
|
+
export {};
|
|
485
|
+
`;
|
|
486
|
+
fs.writeFileSync("apexkit.d.ts", types.trim());
|
|
487
|
+
console.log(" 📄 Generated apexkit.d.ts with Engine & Collection Typings");
|
|
488
|
+
|
|
489
|
+
const jsconfig = {
|
|
490
|
+
compilerOptions: {
|
|
491
|
+
target: "ES2022",
|
|
492
|
+
module: "ESNext",
|
|
493
|
+
ignoreDeprecations: "6.0",
|
|
494
|
+
moduleResolution: "node",
|
|
495
|
+
baseUrl: ".",
|
|
496
|
+
paths: {
|
|
497
|
+
"@/custom/*": ["./modules/custom/*"],
|
|
498
|
+
"@/esm/*": ["./modules/esm/*"],
|
|
499
|
+
},
|
|
500
|
+
allowJs: true,
|
|
501
|
+
checkJs: true,
|
|
502
|
+
},
|
|
503
|
+
include: ["**/*.js", "**/*.ts", "**/*.html", "apexkit.d.ts"],
|
|
504
|
+
};
|
|
505
|
+
fs.writeFileSync("jsconfig.json", JSON.stringify(jsconfig, null, 2));
|
|
506
|
+
console.log(" 📄 Generated jsconfig.json for module resolution");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// --- 5. INITIAL CODE GENERATOR ---
|
|
510
|
+
function getBoilerplateCode(fileType, cleanName, ext, triggerType, targetCollection) {
|
|
511
|
+
const metadata = {
|
|
512
|
+
name: cleanName,
|
|
513
|
+
extension: ext,
|
|
514
|
+
target_collection: targetCollection,
|
|
515
|
+
type: fileType,
|
|
516
|
+
path:
|
|
517
|
+
fileType === "template"
|
|
518
|
+
? "./templates/"
|
|
519
|
+
: fileType.includes("module")
|
|
520
|
+
? "./modules/custom/"
|
|
521
|
+
: "./webhooks/",
|
|
522
|
+
trigger_type: triggerType,
|
|
523
|
+
active: true,
|
|
524
|
+
visibility: "private",
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const header = `/** @type {import("../apexkit").FileMetadata} */\nexport const __fileMetadata__ = ${JSON.stringify(
|
|
528
|
+
metadata,
|
|
529
|
+
null,
|
|
530
|
+
2
|
|
531
|
+
)};\n\n`;
|
|
532
|
+
|
|
533
|
+
if (fileType === "custom:module") {
|
|
534
|
+
return `${header}/**
|
|
535
|
+
* Reusable Helper Module: ${cleanName}
|
|
536
|
+
* Import elsewhere via: import { ${cleanName.replace(/-/g, "_")} } from "@/custom/${cleanName}";
|
|
537
|
+
*/
|
|
538
|
+
export function ${cleanName.replace(/-/g, "_")}(data = {}) {
|
|
539
|
+
return {
|
|
540
|
+
success: true,
|
|
541
|
+
processed_at: new Date().toISOString(),
|
|
542
|
+
data
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (fileType === "template") {
|
|
549
|
+
return `<!--
|
|
550
|
+
__fileMetadata__ = ${JSON.stringify(metadata, null, 2)}
|
|
551
|
+
-->
|
|
552
|
+
<script server>
|
|
553
|
+
/// <reference path="../apexkit.d.ts" />
|
|
554
|
+
|
|
555
|
+
export default async function (context) {
|
|
556
|
+
const posts = await $db.records.list(\`${targetCollection || "posts"}\`, { limit: 10 }).catch(() => ({ items: [] }));
|
|
557
|
+
return {
|
|
558
|
+
title: \`${cleanName.replace(/-/g, " ").toUpperCase()}\`,
|
|
559
|
+
items: posts.items
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
</script>
|
|
563
|
+
|
|
564
|
+
<!DOCTYPE html>
|
|
565
|
+
<html lang="en">
|
|
566
|
+
<head>
|
|
567
|
+
<meta charset="UTF-8">
|
|
568
|
+
<title>{{ title }}</title>
|
|
569
|
+
<link rel="stylesheet" href="/styles.css">
|
|
570
|
+
<script src="/static/js/htmx.js"></script>
|
|
571
|
+
<script src="/static/js/alpine.js" defer></script>
|
|
572
|
+
</head>
|
|
573
|
+
<body class="bg-slate-900 text-slate-100 min-h-screen p-8">
|
|
574
|
+
<div class="max-w-4xl mx-auto space-y-6">
|
|
575
|
+
<h1 class="text-3xl font-bold text-indigo-400">{{ title }}</h1>
|
|
576
|
+
|
|
577
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
578
|
+
{% for item in items %}
|
|
579
|
+
<div class="p-4 bg-slate-800 rounded-lg border border-slate-700 shadow">
|
|
580
|
+
<h2 class="text-xl font-semibold">{{ item.data.title | default(value="Item #" ~ item.id) }}</h2>
|
|
581
|
+
<p class="text-slate-400 text-sm mt-2">{{ item.created }}</p>
|
|
582
|
+
</div>
|
|
583
|
+
{% endfor %}
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</body>
|
|
587
|
+
</html>
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
switch (triggerType) {
|
|
592
|
+
case "manual":
|
|
593
|
+
return `${header}import { Hono } from "https://esm.sh/hono";
|
|
594
|
+
|
|
595
|
+
const app = new Hono();
|
|
596
|
+
|
|
597
|
+
app.all("/", async (c) => {
|
|
598
|
+
const body = await c.req.json().catch(() => ({}));
|
|
599
|
+
return c.json({
|
|
600
|
+
success: true,
|
|
601
|
+
method: c.req.method,
|
|
602
|
+
path: c.req.path,
|
|
603
|
+
received: body
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
export default async function (req) {
|
|
608
|
+
return app.fetch(req);
|
|
609
|
+
}
|
|
610
|
+
`;
|
|
611
|
+
|
|
612
|
+
case "before_create_record":
|
|
613
|
+
case "before_update_record":
|
|
614
|
+
return `${header}/**
|
|
615
|
+
* Hook: ${triggerType}
|
|
616
|
+
* Collection: ${targetCollection || "All Collections"}
|
|
617
|
+
*
|
|
618
|
+
* @param {import("../apexkit").RecordHookEvent} event
|
|
619
|
+
* @returns {Promise<any>} Return modified record object or throw an Error to block operation
|
|
620
|
+
*/
|
|
621
|
+
export default async function (event) {
|
|
622
|
+
const data = event.record.data;
|
|
623
|
+
return data;
|
|
624
|
+
}
|
|
625
|
+
`;
|
|
626
|
+
|
|
627
|
+
case "after_create_record":
|
|
628
|
+
case "after_update_record":
|
|
629
|
+
return `${header}/**
|
|
630
|
+
* Hook: ${triggerType}
|
|
631
|
+
* Collection: ${targetCollection || "All Collections"}
|
|
632
|
+
*
|
|
633
|
+
* @param {import("../apexkit").RecordHookEvent} event
|
|
634
|
+
*/
|
|
635
|
+
export default async function (event) {
|
|
636
|
+
const { id, data } = event.record;
|
|
637
|
+
await $realtime.send(\`${targetCollection || "general"}\`, "record_changed", {
|
|
638
|
+
action: \`${triggerType}\`,
|
|
639
|
+
record_id: id,
|
|
640
|
+
data
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
default:
|
|
646
|
+
return `${header}/**
|
|
647
|
+
* Trigger: ${triggerType}
|
|
648
|
+
* @param {any} event
|
|
649
|
+
*/
|
|
650
|
+
export default async function (event) {
|
|
651
|
+
console.log(\`Hook ${triggerType} executed in ${cleanName}\`);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// --- 6. INTERACTIVE FILE WIZARD ---
|
|
659
|
+
async function runInitFile() {
|
|
660
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
661
|
+
|
|
662
|
+
console.log("🚀 ApexKit Interactive File Creation Wizard\n");
|
|
663
|
+
|
|
664
|
+
console.log("Select File Category:");
|
|
665
|
+
console.log(" 1) Webhook / API Endpoint / System Event Hook");
|
|
666
|
+
console.log(" 2) Custom Reusable Module (imported via @/custom/...)");
|
|
667
|
+
console.log(" 3) SSR HTML / Tera Page Template");
|
|
668
|
+
const catChoice = (await rl.question("\nChoice (1-3) [1]: ")).trim() || "1";
|
|
669
|
+
|
|
670
|
+
let fileType = "webhook";
|
|
671
|
+
let targetDir = "./webhooks/";
|
|
672
|
+
|
|
673
|
+
if (catChoice === "2") {
|
|
674
|
+
fileType = "custom:module";
|
|
675
|
+
targetDir = "./modules/custom/";
|
|
676
|
+
} else if (catChoice === "3") {
|
|
677
|
+
fileType = "template";
|
|
678
|
+
targetDir = "./templates/";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const rawName = (await rl.question("\nEnter File Name (e.g. process-order): ")).trim();
|
|
682
|
+
if (!rawName) {
|
|
683
|
+
console.error("❌ File name is required!");
|
|
684
|
+
rl.close();
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const cleanName = rawName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
689
|
+
const ext =
|
|
690
|
+
fileType === "template"
|
|
691
|
+
? "html"
|
|
692
|
+
: ((await rl.question("Extension (js/ts) [js]: ")).trim() || "js");
|
|
693
|
+
|
|
694
|
+
let triggerType = "manual";
|
|
695
|
+
let targetCollection = null;
|
|
696
|
+
|
|
697
|
+
if (fileType === "webhook") {
|
|
698
|
+
console.log("\nSelect Trigger Type:");
|
|
699
|
+
TRIGGER_DEFINITIONS.forEach((t, i) => {
|
|
700
|
+
console.log(` ${String(i + 1).padStart(2, " ")}) ${t.name}`);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const trigChoice = parseInt((await rl.question("\nChoice (1-42) [1]: ")).trim() || "1", 10);
|
|
704
|
+
const selectedTrig = TRIGGER_DEFINITIONS[trigChoice - 1] || TRIGGER_DEFINITIONS[0];
|
|
705
|
+
triggerType = selectedTrig.id;
|
|
706
|
+
|
|
707
|
+
if (triggerType.includes("_record") || triggerType === "before_ai_run") {
|
|
708
|
+
const colInput = (await rl.question("\nTarget Collection Name (leave blank for all): ")).trim();
|
|
709
|
+
targetCollection = colInput || null;
|
|
710
|
+
}
|
|
711
|
+
} else if (fileType === "template") {
|
|
712
|
+
const colInput = (
|
|
713
|
+
await rl.question("\nPrimary Collection to Query (e.g. posts, leave blank for none): ")
|
|
714
|
+
).trim();
|
|
715
|
+
targetCollection = colInput || null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const codeBoilerplate = getBoilerplateCode(fileType, cleanName, ext, triggerType, targetCollection);
|
|
719
|
+
|
|
720
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
721
|
+
const fullFilePath = path.join(targetDir, `${cleanName}.${ext}`);
|
|
722
|
+
fs.writeFileSync(fullFilePath, codeBoilerplate);
|
|
723
|
+
|
|
724
|
+
console.log(`\n✨ Successfully created: ${fullFilePath}`);
|
|
725
|
+
console.log(`🚀 Save this file while \`node apexkit-watch.js\` is running to sync live to ApexKit!`);
|
|
726
|
+
|
|
727
|
+
rl.close();
|
|
728
|
+
process.exit(0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// --- 7. WORKSPACE INIT WIZARD ---
|
|
732
|
+
async function runInit() {
|
|
733
|
+
console.log("🚀 Welcome to ApexKit Local Workspace Setup!\n");
|
|
734
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
735
|
+
|
|
736
|
+
const url = await rl.question(`1. Enter ApexKit Server URL (${BASE_URL}): `);
|
|
737
|
+
const finalUrl = url.trim() || BASE_URL;
|
|
738
|
+
|
|
739
|
+
const key = await rl.question(`2. Enter API Key (${API_KEY}): `);
|
|
740
|
+
const finalKey = key.trim() || API_KEY;
|
|
741
|
+
|
|
742
|
+
const scope = await rl.question(`3. Enter Scope (root, tenant:id, sandbox:id) [${SCOPE_KEY}]: `);
|
|
743
|
+
const finalScope = scope.trim() || SCOPE_KEY;
|
|
744
|
+
|
|
745
|
+
const envContent = `APEXKIT_URL=${finalUrl}\nAPEXKIT_API_KEY=${finalKey}\nSCOPE_KEY=${finalScope}\n`;
|
|
746
|
+
fs.writeFileSync(".env", envContent);
|
|
747
|
+
console.log("\n✅ Saved .env file!");
|
|
748
|
+
|
|
749
|
+
const dirs = ["webhooks", "modules/custom", "modules/esm", "templates", "ai_actions", "schemas"];
|
|
750
|
+
dirs.forEach((d) => {
|
|
751
|
+
fs.mkdirSync(d, { recursive: true });
|
|
752
|
+
console.log(` 📁 Created ./${d}`);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
process.env.APEXKIT_URL = finalUrl;
|
|
756
|
+
process.env.APEXKIT_API_KEY = finalKey;
|
|
757
|
+
process.env.SCOPE_KEY = finalScope;
|
|
758
|
+
|
|
759
|
+
await generateWorkspaceFiles();
|
|
760
|
+
|
|
761
|
+
console.log("\n🎉 Workspace Initialized! Run `node apexkit-watch.js pull` to download remote files.");
|
|
762
|
+
rl.close();
|
|
763
|
+
process.exit(0);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// --- 8. COMPREHENSIVE PULL ---
|
|
767
|
+
async function runPull() {
|
|
768
|
+
console.log(`📥 Pulling all collections, scripts, templates, and AI actions from ApexKit (${SCOPE_KEY})...\n`);
|
|
769
|
+
|
|
770
|
+
const apiBase = getScopedApiBase();
|
|
771
|
+
const headers = getAuthHeaders();
|
|
772
|
+
|
|
773
|
+
const dirs = ["webhooks", "modules/custom", "modules/esm", "templates", "ai_actions", "schemas"];
|
|
774
|
+
dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
|
|
775
|
+
|
|
776
|
+
let pulledCount = 0;
|
|
777
|
+
|
|
778
|
+
// 1. Pull Collections & Schema
|
|
779
|
+
try {
|
|
780
|
+
const colRes = await fetch(`${apiBase}/collections`, { headers });
|
|
781
|
+
if (colRes.ok) {
|
|
782
|
+
const collections = await colRes.json();
|
|
783
|
+
fs.writeFileSync("schemas/collections.json", JSON.stringify(collections, null, 2));
|
|
784
|
+
console.log(` 📦 [Collections]: Saved schemas/collections.json (${collections.length} collections)`);
|
|
785
|
+
pulledCount += collections.length;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const schemaRes = await fetch(`${apiBase}/admin/export-schema`, { headers });
|
|
789
|
+
if (schemaRes.ok) {
|
|
790
|
+
const schemaJson = await schemaRes.json();
|
|
791
|
+
fs.writeFileSync("schemas/apex_schema.json", JSON.stringify(schemaJson, null, 2));
|
|
792
|
+
console.log(` 📦 [Schema]: Saved schemas/apex_schema.json`);
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.warn(` ⚠️ Schema pull warning: ${e.message}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 2. Pull Scripts & Modules
|
|
799
|
+
try {
|
|
800
|
+
const scriptRes = await fetch(`${apiBase}/admin/scripts`, { headers });
|
|
801
|
+
if (scriptRes.ok) {
|
|
802
|
+
const payload = await scriptRes.json();
|
|
803
|
+
const scripts = Array.isArray(payload) ? payload : payload.local || [];
|
|
804
|
+
|
|
805
|
+
scripts.forEach((script) => {
|
|
806
|
+
const meta = {
|
|
807
|
+
id: script.id,
|
|
808
|
+
name: script.name,
|
|
809
|
+
extension: "js",
|
|
810
|
+
target_collection: script.target_collection || null,
|
|
811
|
+
type: script.metadata?.type || "webhook",
|
|
812
|
+
path: script.metadata?.path || "./webhooks/",
|
|
813
|
+
trigger_type: script.trigger_type || "manual",
|
|
814
|
+
active: script.active !== undefined ? script.active : true,
|
|
815
|
+
visibility: script.visibility || "private",
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const targetDir = meta.path.replace(/^\.\//, "");
|
|
819
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
820
|
+
|
|
821
|
+
const formattedContent = `/** @type {import("../apexkit").FileMetadata} */\nexport const __fileMetadata__ = ${JSON.stringify(
|
|
822
|
+
meta,
|
|
823
|
+
null,
|
|
824
|
+
2
|
|
825
|
+
)};\n\n${script.code}`;
|
|
826
|
+
|
|
827
|
+
const filePath = path.join(targetDir, `${script.name}.${meta.extension}`);
|
|
828
|
+
fs.writeFileSync(filePath, formattedContent);
|
|
829
|
+
console.log(` 📜 [Script]: ${filePath}`);
|
|
830
|
+
pulledCount++;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
} catch (e) {
|
|
834
|
+
console.warn(` ⚠️ Scripts pull warning: ${e.message}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 3. Pull Templates
|
|
838
|
+
try {
|
|
839
|
+
const tmplRes = await fetch(`${apiBase}/admin/templates`, { headers });
|
|
840
|
+
if (tmplRes.ok) {
|
|
841
|
+
const templates = await tmplRes.json();
|
|
842
|
+
templates.forEach((tmpl) => {
|
|
843
|
+
const meta = {
|
|
844
|
+
name: tmpl.slug,
|
|
845
|
+
extension: "html",
|
|
846
|
+
type: "template",
|
|
847
|
+
path: "./templates/",
|
|
848
|
+
active: true,
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const formattedContent = `<!--\n__fileMetadata__ = ${JSON.stringify(
|
|
852
|
+
meta,
|
|
853
|
+
null,
|
|
854
|
+
2
|
|
855
|
+
)}\n-->\n${tmpl.content}`;
|
|
856
|
+
|
|
857
|
+
const filePath = path.join("templates", `${tmpl.slug}.html`);
|
|
858
|
+
fs.writeFileSync(filePath, formattedContent);
|
|
859
|
+
console.log(` 🎨 [Template]: ${filePath}`);
|
|
860
|
+
pulledCount++;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
} catch (e) {
|
|
864
|
+
console.warn(` ⚠️ Templates pull warning: ${e.message}`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 4. Pull AI Actions
|
|
868
|
+
try {
|
|
869
|
+
const aiRes = await fetch(`${apiBase}/admin/ai/actions`, { headers });
|
|
870
|
+
if (aiRes.ok) {
|
|
871
|
+
const actions = await aiRes.json();
|
|
872
|
+
actions.forEach((action) => {
|
|
873
|
+
const filePath = path.join("ai_actions", `${action.slug}.json`);
|
|
874
|
+
fs.writeFileSync(filePath, JSON.stringify({ action }, null, 2));
|
|
875
|
+
console.log(` 🤖 [AI Action]: ${filePath}`);
|
|
876
|
+
pulledCount++;
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
} catch (e) {
|
|
880
|
+
console.warn(` ⚠️ AI Actions pull warning: ${e.message}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
await generateWorkspaceFiles();
|
|
884
|
+
|
|
885
|
+
console.log(`\n✨ Pull complete! Hydrated ${pulledCount} items to local workspace.`);
|
|
886
|
+
process.exit(0);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// --- 9. METADATA AUTO-INFERENCE HELPER ---
|
|
890
|
+
function inferFileMetadata(filePath, content) {
|
|
891
|
+
const ext = path.extname(filePath).replace(/^\./, "");
|
|
892
|
+
const baseName = path.basename(filePath, `.${ext}`);
|
|
893
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
894
|
+
|
|
895
|
+
// Ignore system/hidden files
|
|
896
|
+
if (baseName.startsWith(".") || ext === "gitkeep" || baseName === ".gitkeep") {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 1. JSON files (AI Actions) -> Pure JSON
|
|
901
|
+
if (normalizedPath.includes("ai_actions/") || ext === "json") {
|
|
902
|
+
try {
|
|
903
|
+
const parsed = JSON.parse(content);
|
|
904
|
+
return JSON.stringify(parsed, null, 2);
|
|
905
|
+
} catch (e) {
|
|
906
|
+
return content;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// 2. HTML files (Templates) -> HTML Comment metadata
|
|
911
|
+
if (normalizedPath.includes("templates/") || ext === "html") {
|
|
912
|
+
const htmlMetaRe = /<!--\s*__fileMetadata__\s*=\s*(\{[\s\S]*?\})\s*-->/;
|
|
913
|
+
if (!htmlMetaRe.test(content)) {
|
|
914
|
+
const finalMeta = {
|
|
915
|
+
name: baseName,
|
|
916
|
+
extension: "html",
|
|
917
|
+
type: "template",
|
|
918
|
+
path: "./templates/",
|
|
919
|
+
active: true,
|
|
920
|
+
};
|
|
921
|
+
return `<!--\n__fileMetadata__ = ${JSON.stringify(finalMeta, null, 2)}\n-->\n${content}`;
|
|
922
|
+
}
|
|
923
|
+
return content;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 3. Scripts / Webhooks / Modules -> JS Export metadata
|
|
927
|
+
let deducedType = "webhook";
|
|
928
|
+
let deducedPath = "./webhooks/";
|
|
929
|
+
let deducedTrigger = "manual";
|
|
930
|
+
|
|
931
|
+
if (normalizedPath.includes("modules/custom/")) {
|
|
932
|
+
deducedType = "custom:module";
|
|
933
|
+
deducedPath = "./modules/custom/";
|
|
934
|
+
} else if (normalizedPath.includes("modules/esm/")) {
|
|
935
|
+
deducedType = "esm:module";
|
|
936
|
+
deducedPath = "./modules/esm/";
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const jsMetaRe = /(?:export\s+const\s+__fileMetadata__\s*=\s*)(\{[\s\S]*?\});?/;
|
|
940
|
+
const match = content.match(jsMetaRe);
|
|
941
|
+
|
|
942
|
+
let metaObj = {};
|
|
943
|
+
if (match && match[1]) {
|
|
944
|
+
try {
|
|
945
|
+
metaObj = JSON.parse(match[1]);
|
|
946
|
+
} catch (e) {}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const finalMeta = {
|
|
950
|
+
name: metaObj.name || baseName,
|
|
951
|
+
extension: metaObj.extension || ext || "js",
|
|
952
|
+
target_collection: metaObj.target_collection || null,
|
|
953
|
+
type: metaObj.type || deducedType,
|
|
954
|
+
path: metaObj.path || deducedPath,
|
|
955
|
+
trigger_type: metaObj.trigger_type || deducedTrigger,
|
|
956
|
+
active: metaObj.active !== undefined ? metaObj.active : true,
|
|
957
|
+
visibility: metaObj.visibility || "private",
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
if (!match) {
|
|
961
|
+
return `/** @type {import("../apexkit").FileMetadata} */\nexport const __fileMetadata__ = ${JSON.stringify(
|
|
962
|
+
finalMeta,
|
|
963
|
+
null,
|
|
964
|
+
2
|
|
965
|
+
)};\n\n${content}`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return content;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// --- PRUNE ORPHANED REMOTE ENTITIES ---
|
|
972
|
+
async function pruneRemoteOrphans(localScripts, localTemplates, localAiActions) {
|
|
973
|
+
if (DELETE_REMOTE_TYPES.length === 0) return;
|
|
974
|
+
|
|
975
|
+
const apiBase = getScopedApiBase();
|
|
976
|
+
const headers = getAuthHeaders();
|
|
977
|
+
|
|
978
|
+
console.log(`\n🧹 [Prune Remote]: Checking for orphaned items in ApexKit (${DELETE_REMOTE_TYPES.join(", ")})...`);
|
|
979
|
+
|
|
980
|
+
if (DELETE_REMOTE_TYPES.includes("scripts") || DELETE_REMOTE_TYPES.includes("all")) {
|
|
981
|
+
try {
|
|
982
|
+
const res = await fetch(`${apiBase}/admin/scripts`, { headers });
|
|
983
|
+
if (res.ok) {
|
|
984
|
+
const payload = await res.json();
|
|
985
|
+
const remoteScripts = Array.isArray(payload) ? payload : payload.local || [];
|
|
986
|
+
|
|
987
|
+
for (const script of remoteScripts) {
|
|
988
|
+
if (!localScripts.has(script.name)) {
|
|
989
|
+
console.log(` 🗑️ [Deleted Remote Script]: ${script.name} (ID: ${script.id})`);
|
|
990
|
+
await fetch(`${apiBase}/admin/scripts/${script.id}`, { method: "DELETE", headers });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} catch (e) {
|
|
995
|
+
console.warn(` ⚠️ Failed to prune remote scripts: ${e.message}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (DELETE_REMOTE_TYPES.includes("templates") || DELETE_REMOTE_TYPES.includes("all")) {
|
|
1000
|
+
try {
|
|
1001
|
+
const res = await fetch(`${apiBase}/admin/templates`, { headers });
|
|
1002
|
+
if (res.ok) {
|
|
1003
|
+
const remoteTemplates = await res.json();
|
|
1004
|
+
for (const tmpl of remoteTemplates) {
|
|
1005
|
+
if (!localTemplates.has(tmpl.slug)) {
|
|
1006
|
+
console.log(` 🗑️ [Deleted Remote Template]: ${tmpl.slug} (ID: ${tmpl.id})`);
|
|
1007
|
+
await fetch(`${apiBase}/admin/templates/${tmpl.id}`, { method: "DELETE", headers });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
console.warn(` ⚠️ Failed to prune remote templates: ${e.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (
|
|
1017
|
+
DELETE_REMOTE_TYPES.includes("ai_actions") ||
|
|
1018
|
+
DELETE_REMOTE_TYPES.includes("actions") ||
|
|
1019
|
+
DELETE_REMOTE_TYPES.includes("all")
|
|
1020
|
+
) {
|
|
1021
|
+
try {
|
|
1022
|
+
const res = await fetch(`${apiBase}/admin/ai/actions`, { headers });
|
|
1023
|
+
if (res.ok) {
|
|
1024
|
+
const remoteActions = await res.json();
|
|
1025
|
+
for (const action of remoteActions) {
|
|
1026
|
+
if (!localAiActions.has(action.slug)) {
|
|
1027
|
+
console.log(` 🗑️ [Deleted Remote AI Action]: ${action.slug} (ID: ${action.id})`);
|
|
1028
|
+
await fetch(`${apiBase}/admin/ai/actions/${action.id}`, { method: "DELETE", headers });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
console.warn(` ⚠️ Failed to prune remote AI actions: ${e.message}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// --- 10. UNIFIED SYNC PUSH/COMMIT (With Scope & Category Filtering) ---
|
|
1039
|
+
async function runManualPushOrCommit(commitToDb) {
|
|
1040
|
+
const selectedCategories = [];
|
|
1041
|
+
if (syncScripts) selectedCategories.push("Scripts (--scripts)");
|
|
1042
|
+
if (syncTemplates) selectedCategories.push("Templates (--templates)");
|
|
1043
|
+
if (syncActions) selectedCategories.push("AI Actions (--actions)");
|
|
1044
|
+
|
|
1045
|
+
const modeLabel = commitToDb ? "Push & Commit to DB" : "Commit to VFS (Memory)";
|
|
1046
|
+
console.log(`🚀 [${modeLabel}]: Syncing ${selectedCategories.join(", ")} to ApexKit (${BASE_URL})...\n`);
|
|
1047
|
+
|
|
1048
|
+
let WSClient = globalThis.WebSocket;
|
|
1049
|
+
if (!WSClient) {
|
|
1050
|
+
try {
|
|
1051
|
+
const wsModule = await import("ws");
|
|
1052
|
+
WSClient = wsModule.default || wsModule;
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
console.error(
|
|
1055
|
+
"❌ WebSocket is not natively supported in your Node version. Please install 'ws' with: npm install ws"
|
|
1056
|
+
);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const ws = new WSClient(WS_URL);
|
|
1062
|
+
|
|
1063
|
+
const targetDirs = [];
|
|
1064
|
+
if (syncScripts) {
|
|
1065
|
+
targetDirs.push("./webhooks", "./modules");
|
|
1066
|
+
}
|
|
1067
|
+
if (syncTemplates) {
|
|
1068
|
+
targetDirs.push("./templates");
|
|
1069
|
+
}
|
|
1070
|
+
if (syncActions) {
|
|
1071
|
+
targetDirs.push("./ai_actions");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const filesToPush = [];
|
|
1075
|
+
const localScripts = new Set();
|
|
1076
|
+
const localTemplates = new Set();
|
|
1077
|
+
const localAiActions = new Set();
|
|
1078
|
+
|
|
1079
|
+
targetDirs.forEach((dir) => {
|
|
1080
|
+
if (!fs.existsSync(dir)) return;
|
|
1081
|
+
const files = fs.readdirSync(dir, { recursive: true });
|
|
1082
|
+
|
|
1083
|
+
files.forEach((file) => {
|
|
1084
|
+
const fullPath = path.join(dir, file);
|
|
1085
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
1086
|
+
const relativePath = fullPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1087
|
+
const baseName = path.basename(relativePath);
|
|
1088
|
+
|
|
1089
|
+
// Explicitly skip .gitkeep and hidden files
|
|
1090
|
+
if (baseName.startsWith(".") || baseName.endsWith(".gitkeep")) return;
|
|
1091
|
+
|
|
1092
|
+
const rawContent = fs.readFileSync(fullPath, "utf-8");
|
|
1093
|
+
const content = inferFileMetadata(relativePath, rawContent);
|
|
1094
|
+
|
|
1095
|
+
if (!content) return; // Skip ignored/hidden files
|
|
1096
|
+
|
|
1097
|
+
// Track local entities
|
|
1098
|
+
if (relativePath.startsWith("templates/") || relativePath.endsWith(".html")) {
|
|
1099
|
+
const stem = path.basename(relativePath, path.extname(relativePath));
|
|
1100
|
+
localTemplates.add(stem);
|
|
1101
|
+
} else if (relativePath.startsWith("ai_actions/") || relativePath.endsWith(".json")) {
|
|
1102
|
+
try {
|
|
1103
|
+
const parsed = JSON.parse(content);
|
|
1104
|
+
const slug = parsed.action?.slug || parsed.slug || path.basename(relativePath, ".json");
|
|
1105
|
+
localAiActions.add(slug);
|
|
1106
|
+
} catch (e) {}
|
|
1107
|
+
} else {
|
|
1108
|
+
const stem = path.basename(relativePath, path.extname(relativePath));
|
|
1109
|
+
localScripts.add(stem);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
filesToPush.push({ path: relativePath, content });
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
if (filesToPush.length === 0) {
|
|
1118
|
+
console.log(`⚠️ No files found for the selected targets (${selectedCategories.join(", ")}).`);
|
|
1119
|
+
process.exit(0);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
let acksReceived = 0;
|
|
1123
|
+
let hasErrors = false;
|
|
1124
|
+
|
|
1125
|
+
ws.addEventListener("message", (event) => {
|
|
1126
|
+
try {
|
|
1127
|
+
const msg = JSON.parse(event.data);
|
|
1128
|
+
if (msg.type === "SyncAck") {
|
|
1129
|
+
acksReceived++;
|
|
1130
|
+
console.log(` ✅ [Committed]: ${msg.payload.message}`);
|
|
1131
|
+
} else if (msg.type === "Error") {
|
|
1132
|
+
acksReceived++;
|
|
1133
|
+
hasErrors = true;
|
|
1134
|
+
console.error(` ❌ [Error]: ${msg.payload.message}`);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (acksReceived >= filesToPush.length) {
|
|
1138
|
+
finishSync();
|
|
1139
|
+
}
|
|
1140
|
+
} catch (e) {}
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
async function finishSync() {
|
|
1144
|
+
if (commitToDb && DELETE_REMOTE_TYPES.length > 0) {
|
|
1145
|
+
await pruneRemoteOrphans(localScripts, localTemplates, localAiActions);
|
|
1146
|
+
}
|
|
1147
|
+
console.log(`\n🎉 Done! ${filesToPush.length} file(s) processed.`);
|
|
1148
|
+
setTimeout(() => {
|
|
1149
|
+
try {
|
|
1150
|
+
ws.close();
|
|
1151
|
+
} catch (e) {}
|
|
1152
|
+
process.exit(hasErrors ? 1 : 0);
|
|
1153
|
+
}, 200);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
ws.addEventListener("open", () => {
|
|
1157
|
+
filesToPush.forEach((item) => {
|
|
1158
|
+
console.log(` ⬆️ Pushing ${item.path} (commit_to_db: ${commitToDb})`);
|
|
1159
|
+
ws.send(
|
|
1160
|
+
JSON.stringify({
|
|
1161
|
+
type: "PushFile",
|
|
1162
|
+
payload: { path: item.path, content: item.content, commit_to_db: commitToDb },
|
|
1163
|
+
})
|
|
1164
|
+
);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// Safety timeout in case a socket ACK drops
|
|
1168
|
+
setTimeout(() => {
|
|
1169
|
+
if (acksReceived < filesToPush.length) {
|
|
1170
|
+
console.log(`\n⚠️ Finished pushing. Waiting response timeout reached.`);
|
|
1171
|
+
finishSync();
|
|
1172
|
+
}
|
|
1173
|
+
}, 4000);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
ws.addEventListener("error", (err) => {
|
|
1177
|
+
console.error("❌ Connection failed. Verify that ApexKit is running and check your API Key / Port.");
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// --- 11. WATCHER & LIVE SYNC CLIENT ---
|
|
1183
|
+
let ws;
|
|
1184
|
+
let isReconnecting = false;
|
|
1185
|
+
let reconnectTimer = null;
|
|
1186
|
+
let hasLoggedWaiting = false;
|
|
1187
|
+
const debounceMap = new Map();
|
|
1188
|
+
|
|
1189
|
+
function scheduleReconnect() {
|
|
1190
|
+
if (isReconnecting) return;
|
|
1191
|
+
isReconnecting = true;
|
|
1192
|
+
|
|
1193
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1194
|
+
|
|
1195
|
+
if (!hasLoggedWaiting) {
|
|
1196
|
+
console.log("🔌 Connection unavailable. Retrying...\n");
|
|
1197
|
+
hasLoggedWaiting = true;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
reconnectTimer = setTimeout(() => {
|
|
1201
|
+
isReconnecting = false;
|
|
1202
|
+
connectWebSocket(true);
|
|
1203
|
+
}, 3000);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function connectWebSocket(isRetry = false) {
|
|
1207
|
+
if (!isRetry) {
|
|
1208
|
+
console.log(`⚡ Connecting to ApexKit (URL: ${BASE_URL}, Scope: ${SCOPE_KEY})...`);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let WSClient = globalThis.WebSocket;
|
|
1212
|
+
if (!WSClient) {
|
|
1213
|
+
try {
|
|
1214
|
+
const wsModule = await import("ws");
|
|
1215
|
+
WSClient = wsModule.default || wsModule;
|
|
1216
|
+
} catch (e) {
|
|
1217
|
+
console.error(
|
|
1218
|
+
"❌ WebSocket is not natively supported in your Node version. Please install 'ws' with: npm install ws"
|
|
1219
|
+
);
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
try {
|
|
1225
|
+
if (ws) {
|
|
1226
|
+
try {
|
|
1227
|
+
ws.close();
|
|
1228
|
+
} catch (e) {}
|
|
1229
|
+
}
|
|
1230
|
+
ws = new WSClient(WS_URL);
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
scheduleReconnect();
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
ws.addEventListener("open", async () => {
|
|
1237
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1238
|
+
isReconnecting = false;
|
|
1239
|
+
hasLoggedWaiting = false;
|
|
1240
|
+
console.log("✅ Connected to ApexKit Live Engine!");
|
|
1241
|
+
console.log(`👀 Watching local files... (Auto-Commit to DB: ${!NO_AUTO_COMMIT})`);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
ws.addEventListener("message", (event) => {
|
|
1245
|
+
try {
|
|
1246
|
+
const dataStr = typeof event.data === "string" ? event.data : event.data.toString();
|
|
1247
|
+
const msg = JSON.parse(dataStr);
|
|
1248
|
+
|
|
1249
|
+
if (msg.type === "SyncAck") {
|
|
1250
|
+
console.log(`🚀 [DB Sync]: ${msg.payload.message}`);
|
|
1251
|
+
} else if (msg.type === "Error") {
|
|
1252
|
+
console.error(`❌ [Error]: ${msg.payload.message}`);
|
|
1253
|
+
} else if (msg.type === "Ping") {
|
|
1254
|
+
ws.send(JSON.stringify({ type: "Pong" }));
|
|
1255
|
+
}
|
|
1256
|
+
} catch (e) {}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
ws.addEventListener("error", () => {
|
|
1260
|
+
scheduleReconnect();
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
ws.addEventListener("close", () => {
|
|
1264
|
+
scheduleReconnect();
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function startWatcher() {
|
|
1269
|
+
connectWebSocket();
|
|
1270
|
+
|
|
1271
|
+
const watchDirs = ["./webhooks", "./templates", "./ai_actions", "./modules"];
|
|
1272
|
+
watchDirs.forEach((dir) => {
|
|
1273
|
+
if (fs.existsSync(dir)) {
|
|
1274
|
+
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
1275
|
+
if (!filename) return;
|
|
1276
|
+
|
|
1277
|
+
if (
|
|
1278
|
+
filename.endsWith(".js") ||
|
|
1279
|
+
filename.endsWith(".ts") ||
|
|
1280
|
+
filename.endsWith(".html") ||
|
|
1281
|
+
filename.endsWith(".json")
|
|
1282
|
+
) {
|
|
1283
|
+
const relativePath = path.join(dir, filename).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1284
|
+
|
|
1285
|
+
clearTimeout(debounceMap.get(relativePath));
|
|
1286
|
+
debounceMap.set(
|
|
1287
|
+
relativePath,
|
|
1288
|
+
setTimeout(() => {
|
|
1289
|
+
try {
|
|
1290
|
+
if (fs.existsSync(relativePath)) {
|
|
1291
|
+
let raw = fs.readFileSync(relativePath, "utf-8");
|
|
1292
|
+
let content = inferFileMetadata(relativePath, raw);
|
|
1293
|
+
|
|
1294
|
+
if (!content) return; // Skip ignored/hidden files
|
|
1295
|
+
|
|
1296
|
+
console.log(
|
|
1297
|
+
`📝 Synced: ${relativePath} ${NO_AUTO_COMMIT ? "(VFS Only)" : "(Committed to DB)"}`
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
if (ws && ws.readyState === 1) {
|
|
1301
|
+
ws.send(
|
|
1302
|
+
JSON.stringify({
|
|
1303
|
+
type: "PushFile",
|
|
1304
|
+
payload: {
|
|
1305
|
+
path: relativePath,
|
|
1306
|
+
content: content,
|
|
1307
|
+
commit_to_db: !NO_AUTO_COMMIT,
|
|
1308
|
+
},
|
|
1309
|
+
})
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
console.error(`⚠️ Could not read ${relativePath}:`, err.message);
|
|
1315
|
+
}
|
|
1316
|
+
}, 100)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// --- 12. CLI ROUTER ---
|
|
1325
|
+
if (IS_INIT) {
|
|
1326
|
+
runInit();
|
|
1327
|
+
} else if (IS_INIT_FILE) {
|
|
1328
|
+
runInitFile();
|
|
1329
|
+
} else if (IS_PULL) {
|
|
1330
|
+
runPull();
|
|
1331
|
+
} else if (IS_COMMIT) {
|
|
1332
|
+
runManualPushOrCommit(false); // VFS Only
|
|
1333
|
+
} else if (IS_PUSH) {
|
|
1334
|
+
runManualPushOrCommit(true); // Permanent DB Commit
|
|
1335
|
+
} else if (IS_STATUS) {
|
|
1336
|
+
console.log(`ApexKit Workspace Information:
|
|
1337
|
+
• Server URL: ${BASE_URL}
|
|
1338
|
+
• Target Scope: ${SCOPE_KEY}
|
|
1339
|
+
• API Key: ${API_KEY.substring(0, 16)}...
|
|
1340
|
+
• Auto-Commit: ${!NO_AUTO_COMMIT}
|
|
1341
|
+
• Sync Targets: ${syncScripts ? "Scripts " : ""}${syncTemplates ? "Templates " : ""}${syncActions ? "Actions " : ""}
|
|
1342
|
+
• Prune Flags: ${DELETE_REMOTE_TYPES.length > 0 ? DELETE_REMOTE_TYPES.join(", ") : "none"}
|
|
1343
|
+
`);
|
|
1344
|
+
process.exit(0);
|
|
1345
|
+
} else {
|
|
1346
|
+
startWatcher();
|
|
1347
|
+
}
|