jetic-cli 0.2.2 → 0.2.4
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 +57 -7
- package/dist/dashboard/assets/index-B7pzujvn.js +404 -0
- package/dist/dashboard/assets/index-CAj2uFAP.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +851 -82
- package/package.json +1 -1
- package/dist/dashboard/assets/index-8Xh271uc.js +0 -384
- package/dist/dashboard/assets/index-ANQK1AeT.css +0 -1
package/dist/index.js
CHANGED
|
@@ -267984,57 +267984,116 @@ var require_workflow_simulator = __commonJS({
|
|
|
267984
267984
|
for (let i = 0; i < workflow.steps.length; i++) {
|
|
267985
267985
|
const step = workflow.steps[i];
|
|
267986
267986
|
const stepStartTime = Date.now();
|
|
267987
|
+
if (step.condition) {
|
|
267988
|
+
const condResult = await this.evaluateCondition(step.condition);
|
|
267989
|
+
if (!condResult.matched) {
|
|
267990
|
+
stepResults.push({
|
|
267991
|
+
stepIndex: i + 1,
|
|
267992
|
+
name: step.name || `${step.method} ${step.path}`,
|
|
267993
|
+
method: step.method,
|
|
267994
|
+
path: step.path,
|
|
267995
|
+
targetUrl: "",
|
|
267996
|
+
expectedStatus: step.expectStatus ?? 200,
|
|
267997
|
+
actualStatus: 0,
|
|
267998
|
+
durationMs: Date.now() - stepStartTime,
|
|
267999
|
+
passed: true,
|
|
268000
|
+
skipped: true,
|
|
268001
|
+
retryCount: 0,
|
|
268002
|
+
conditionResult: condResult,
|
|
268003
|
+
requestHeaders: {},
|
|
268004
|
+
requestBody: {},
|
|
268005
|
+
responseBody: null,
|
|
268006
|
+
capturedMemory: []
|
|
268007
|
+
});
|
|
268008
|
+
continue;
|
|
268009
|
+
}
|
|
268010
|
+
}
|
|
267987
268011
|
const { headers: injectedHeaders, body: injectedBody } = await this.resolveInjections(step.inject);
|
|
267988
268012
|
const resolvedBody = await this.resolveBodyTemplates(step.body || {});
|
|
267989
268013
|
const finalBody = { ...injectedBody, ...resolvedBody };
|
|
267990
268014
|
const inputCaptured = await this.captureInputToMemory(step.captureInput, finalBody);
|
|
267991
268015
|
const resolvedPath = await this.resolvePathParams(step.path, finalBody);
|
|
267992
268016
|
const fullUrl = `${baseUrl.replace(/\/$/, "")}${resolvedPath}`;
|
|
267993
|
-
const expectedStatus = step.expectStatus
|
|
268017
|
+
const expectedStatus = step.expectStatus ?? 200;
|
|
268018
|
+
const maxAttempts = Math.max(1, step.retry?.times ?? workflow.retry?.times ?? 1);
|
|
268019
|
+
const retryDelayMs = step.retry?.delayMs ?? workflow.retry?.delayMs ?? 0;
|
|
268020
|
+
let attempts = 0;
|
|
267994
268021
|
let responseStatus = 0;
|
|
267995
268022
|
let responseBody = null;
|
|
267996
268023
|
let stepPassed = false;
|
|
267997
268024
|
let errorMsg;
|
|
267998
|
-
|
|
267999
|
-
|
|
268000
|
-
|
|
268001
|
-
|
|
268002
|
-
|
|
268003
|
-
|
|
268004
|
-
|
|
268005
|
-
|
|
268006
|
-
|
|
268007
|
-
fetchOptions
|
|
268008
|
-
|
|
268009
|
-
|
|
268010
|
-
|
|
268011
|
-
|
|
268012
|
-
|
|
268013
|
-
|
|
268014
|
-
|
|
268015
|
-
|
|
268016
|
-
|
|
268017
|
-
|
|
268018
|
-
|
|
268019
|
-
|
|
268020
|
-
|
|
268021
|
-
|
|
268022
|
-
|
|
268023
|
-
|
|
268024
|
-
|
|
268025
|
-
|
|
268025
|
+
while (attempts < maxAttempts) {
|
|
268026
|
+
attempts++;
|
|
268027
|
+
if (attempts > 1 && retryDelayMs > 0) {
|
|
268028
|
+
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
268029
|
+
}
|
|
268030
|
+
responseStatus = 0;
|
|
268031
|
+
responseBody = null;
|
|
268032
|
+
errorMsg = void 0;
|
|
268033
|
+
try {
|
|
268034
|
+
const fetchOptions = {
|
|
268035
|
+
method: step.method.toUpperCase(),
|
|
268036
|
+
headers: { "Content-Type": "application/json", ...injectedHeaders }
|
|
268037
|
+
};
|
|
268038
|
+
if (!["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
|
|
268039
|
+
fetchOptions.body = JSON.stringify(finalBody);
|
|
268040
|
+
}
|
|
268041
|
+
let finalUrl = fullUrl;
|
|
268042
|
+
if (["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
|
|
268043
|
+
const params = new URLSearchParams();
|
|
268044
|
+
for (const [k, v] of Object.entries(finalBody)) {
|
|
268045
|
+
if (v !== void 0 && v !== null)
|
|
268046
|
+
params.set(k, String(v));
|
|
268047
|
+
}
|
|
268048
|
+
finalUrl = `${fullUrl}?${params.toString()}`;
|
|
268049
|
+
}
|
|
268050
|
+
const resp = await fetch(finalUrl, fetchOptions);
|
|
268051
|
+
responseStatus = resp.status;
|
|
268052
|
+
const ct = resp.headers.get("content-type") || "";
|
|
268053
|
+
if (ct.includes("application/json")) {
|
|
268054
|
+
try {
|
|
268055
|
+
responseBody = await resp.json();
|
|
268056
|
+
} catch {
|
|
268057
|
+
responseBody = null;
|
|
268058
|
+
}
|
|
268059
|
+
} else {
|
|
268060
|
+
responseBody = await resp.text();
|
|
268026
268061
|
}
|
|
268027
|
-
|
|
268028
|
-
|
|
268062
|
+
const in2xx = (s) => s >= 200 && s < 300;
|
|
268063
|
+
stepPassed = resp.status === expectedStatus || in2xx(resp.status) && in2xx(expectedStatus) || (step.continueOnStatus || []).includes(resp.status);
|
|
268064
|
+
if (stepPassed)
|
|
268065
|
+
break;
|
|
268066
|
+
} catch (err) {
|
|
268067
|
+
stepPassed = false;
|
|
268068
|
+
errorMsg = err.message || String(err);
|
|
268029
268069
|
}
|
|
268030
|
-
stepPassed = resp.status === expectedStatus || resp.status >= 200 && resp.status < 300 && expectedStatus >= 200 && expectedStatus < 300;
|
|
268031
|
-
} catch (err) {
|
|
268032
|
-
stepPassed = false;
|
|
268033
|
-
errorMsg = err.message || String(err);
|
|
268034
268070
|
}
|
|
268035
268071
|
const responseCaptured = stepPassed ? await this.captureToMemory(step.capture, responseBody) : [];
|
|
268036
268072
|
if (!stepPassed) {
|
|
268037
268073
|
allPassed = false;
|
|
268074
|
+
const onFailure = step.onFailure ?? "abort";
|
|
268075
|
+
stepResults.push({
|
|
268076
|
+
stepIndex: i + 1,
|
|
268077
|
+
name: step.name || `${step.method} ${step.path}`,
|
|
268078
|
+
method: step.method,
|
|
268079
|
+
path: step.path,
|
|
268080
|
+
targetUrl: fullUrl,
|
|
268081
|
+
expectedStatus,
|
|
268082
|
+
actualStatus: responseStatus,
|
|
268083
|
+
durationMs: Date.now() - stepStartTime,
|
|
268084
|
+
passed: false,
|
|
268085
|
+
skipped: false,
|
|
268086
|
+
retryCount: attempts - 1,
|
|
268087
|
+
failureHandling: onFailure === "continue" ? "continued" : "aborted",
|
|
268088
|
+
requestHeaders: injectedHeaders,
|
|
268089
|
+
requestBody: finalBody,
|
|
268090
|
+
responseBody,
|
|
268091
|
+
capturedMemory: [...inputCaptured],
|
|
268092
|
+
error: errorMsg
|
|
268093
|
+
});
|
|
268094
|
+
if (onFailure === "abort")
|
|
268095
|
+
break;
|
|
268096
|
+
continue;
|
|
268038
268097
|
}
|
|
268039
268098
|
stepResults.push({
|
|
268040
268099
|
stepIndex: i + 1,
|
|
@@ -268045,16 +268104,14 @@ var require_workflow_simulator = __commonJS({
|
|
|
268045
268104
|
expectedStatus,
|
|
268046
268105
|
actualStatus: responseStatus,
|
|
268047
268106
|
durationMs: Date.now() - stepStartTime,
|
|
268048
|
-
passed:
|
|
268107
|
+
passed: true,
|
|
268108
|
+
skipped: false,
|
|
268109
|
+
retryCount: attempts - 1,
|
|
268049
268110
|
requestHeaders: injectedHeaders,
|
|
268050
268111
|
requestBody: finalBody,
|
|
268051
268112
|
responseBody,
|
|
268052
|
-
capturedMemory: [...inputCaptured, ...responseCaptured]
|
|
268053
|
-
error: errorMsg
|
|
268113
|
+
capturedMemory: [...inputCaptured, ...responseCaptured]
|
|
268054
268114
|
});
|
|
268055
|
-
if (!stepPassed) {
|
|
268056
|
-
break;
|
|
268057
|
-
}
|
|
268058
268115
|
}
|
|
268059
268116
|
const totalTimeMs = Date.now() - startTime;
|
|
268060
268117
|
return {
|
|
@@ -268078,6 +268135,32 @@ var require_workflow_simulator = __commonJS({
|
|
|
268078
268135
|
results: stepResults
|
|
268079
268136
|
};
|
|
268080
268137
|
}
|
|
268138
|
+
// ── Condition evaluator ────────────────────────────────────────────────────
|
|
268139
|
+
// Resolves the `if` template and applies the declared operator. Returns
|
|
268140
|
+
// `matched: true` when the condition passes (step should execute).
|
|
268141
|
+
async evaluateCondition(condition) {
|
|
268142
|
+
const evaluated = await this.resolveTemplateString(condition.if);
|
|
268143
|
+
let matched;
|
|
268144
|
+
if (condition.exists !== void 0) {
|
|
268145
|
+
const hasValue = evaluated !== "" && evaluated !== "null" && evaluated !== "undefined";
|
|
268146
|
+
matched = condition.exists ? hasValue : !hasValue;
|
|
268147
|
+
} else if (condition.equals !== void 0) {
|
|
268148
|
+
matched = evaluated === condition.equals;
|
|
268149
|
+
} else if (condition.notEquals !== void 0) {
|
|
268150
|
+
matched = evaluated !== condition.notEquals;
|
|
268151
|
+
} else if (condition.contains !== void 0) {
|
|
268152
|
+
matched = evaluated.includes(condition.contains);
|
|
268153
|
+
} else if (condition.greaterThan !== void 0) {
|
|
268154
|
+
matched = Number(evaluated) > condition.greaterThan;
|
|
268155
|
+
} else if (condition.lessThan !== void 0) {
|
|
268156
|
+
matched = Number(evaluated) < condition.lessThan;
|
|
268157
|
+
} else if (condition.in !== void 0) {
|
|
268158
|
+
matched = condition.in.includes(evaluated);
|
|
268159
|
+
} else {
|
|
268160
|
+
matched = evaluated !== "" && evaluated !== "false" && evaluated !== "0" && evaluated !== "null" && evaluated !== "undefined";
|
|
268161
|
+
}
|
|
268162
|
+
return { matched, evaluated };
|
|
268163
|
+
}
|
|
268081
268164
|
async resolveTemplateString(value) {
|
|
268082
268165
|
const templateRe = /\{\{([^}]+)\}\}/g;
|
|
268083
268166
|
let match;
|
|
@@ -269549,28 +269632,47 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269549
269632
|
var path7 = __importStar(require("path"));
|
|
269550
269633
|
var types_1 = require_types();
|
|
269551
269634
|
var workflow_tools_1 = require_workflow_tools();
|
|
269635
|
+
var stepConditionSchema = zod_1.z.object({
|
|
269636
|
+
if: zod_1.z.string().min(1).describe('Template expression to resolve, e.g. "{{workflow:role}}"'),
|
|
269637
|
+
equals: zod_1.z.string().optional().describe("Resolved value must equal this string"),
|
|
269638
|
+
notEquals: zod_1.z.string().optional().describe("Resolved value must NOT equal this string"),
|
|
269639
|
+
contains: zod_1.z.string().optional().describe("Resolved value must contain this substring"),
|
|
269640
|
+
exists: zod_1.z.boolean().optional().describe("true: value must be non-empty; false: value must be empty"),
|
|
269641
|
+
greaterThan: zod_1.z.number().optional().describe("Numeric greater-than comparison"),
|
|
269642
|
+
lessThan: zod_1.z.number().optional().describe("Numeric less-than comparison"),
|
|
269643
|
+
in: zod_1.z.array(zod_1.z.string()).optional().describe("Resolved value must be one of these strings")
|
|
269644
|
+
}).describe("Skip this step when the condition is not met");
|
|
269645
|
+
var stepRetrySchema = zod_1.z.object({
|
|
269646
|
+
times: zod_1.z.number().int().positive().max(10).describe("Max retry attempts (1\u201310)"),
|
|
269647
|
+
delayMs: zod_1.z.number().int().nonnegative().optional().describe("Delay between retries in ms (default 0)")
|
|
269648
|
+
});
|
|
269552
269649
|
exports2.workflowStepSchema = zod_1.z.object({
|
|
269553
269650
|
name: zod_1.z.string().min(1).describe("Step name (must be unique within the workflow)"),
|
|
269554
269651
|
method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).describe("HTTP method"),
|
|
269555
269652
|
path: zod_1.z.string().min(1).describe("Route path; :params resolve from body/memory (NOT from {{templates}} \u2014 those only work in body/inject)"),
|
|
269556
269653
|
description: zod_1.z.string().optional().describe("What this step does"),
|
|
269557
|
-
body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}}, {{workflow:key}} and {{human:key}} templates
|
|
269558
|
-
inject: zod_1.z.record(zod_1.z.string()).optional().describe('
|
|
269559
|
-
capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key"
|
|
269560
|
-
captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key"
|
|
269561
|
-
expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)")
|
|
269654
|
+
body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}}, {{workflow:key}} and {{human:key}} templates"),
|
|
269655
|
+
inject: zod_1.z.record(zod_1.z.string()).optional().describe('Header/body injection: "header:Name" or "body:field" \u2192 "scope:key" or "{{template}}"'),
|
|
269656
|
+
capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key" \u2192 "dot.path.in.response"'),
|
|
269657
|
+
captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key" \u2192 "bodyField"'),
|
|
269658
|
+
expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)"),
|
|
269659
|
+
condition: stepConditionSchema.optional().describe('Skip this step when condition is not met. Use {{workflow:key}} in the "if" field.'),
|
|
269660
|
+
onFailure: zod_1.z.enum(["abort", "continue"]).optional().describe('"abort" (default): stop the workflow on failure. "continue": log failure and proceed (captures skipped).'),
|
|
269661
|
+
continueOnStatus: zod_1.z.array(zod_1.z.number().int()).optional().describe("Additional HTTP status codes treated as pass for this step (e.g. [404])."),
|
|
269662
|
+
retry: stepRetrySchema.optional().describe("Retry on failure \u2014 overrides the workflow-level global retry.")
|
|
269562
269663
|
// NOTE: .passthrough() is intentional — unknown keys must REACH the validator
|
|
269563
|
-
// (MODEL_DIALECT_KEY / UNKNOWN_STEP_KEY diagnostics). Zod
|
|
269664
|
+
// (MODEL_DIALECT_KEY / UNKNOWN_STEP_KEY diagnostics). Zod’s default strip
|
|
269564
269665
|
// behavior would silently swallow the exact mistakes we need to report.
|
|
269565
269666
|
}).passthrough();
|
|
269566
269667
|
var inlineWorkflowSchema = zod_1.z.object({
|
|
269567
269668
|
name: zod_1.z.string().min(1).describe("Workflow name (also used to derive the file slug)"),
|
|
269568
269669
|
description: zod_1.z.string().optional(),
|
|
269569
269670
|
environment: zod_1.z.string().optional().describe("Target environment name from the model (e.g. local)"),
|
|
269671
|
+
retry: stepRetrySchema.optional().describe("Global retry default applied to all steps (overridden per step)"),
|
|
269570
269672
|
steps: zod_1.z.array(exports2.workflowStepSchema).min(1).describe("Ordered steps; later steps can use memory captured by earlier ones")
|
|
269571
269673
|
}).passthrough();
|
|
269572
269674
|
var TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
269573
|
-
var KNOWN_WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "environment", "steps", "generatedAt"]);
|
|
269675
|
+
var KNOWN_WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "environment", "steps", "generatedAt", "retry"]);
|
|
269574
269676
|
var KNOWN_STEP_KEYS = /* @__PURE__ */ new Set([
|
|
269575
269677
|
"name",
|
|
269576
269678
|
"method",
|
|
@@ -269581,7 +269683,10 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269581
269683
|
"captureInput",
|
|
269582
269684
|
"expectStatus",
|
|
269583
269685
|
"body",
|
|
269584
|
-
"condition"
|
|
269686
|
+
"condition",
|
|
269687
|
+
"onFailure",
|
|
269688
|
+
"continueOnStatus",
|
|
269689
|
+
"retry"
|
|
269585
269690
|
]);
|
|
269586
269691
|
var MODEL_DIALECT_KEYS = /* @__PURE__ */ new Set(["call", "auth", "bind"]);
|
|
269587
269692
|
function err(list, stepIndex, stepName, code, message, fix) {
|
|
@@ -269677,8 +269782,41 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269677
269782
|
warn(warnings, stepNo, sName, "UNKNOWN_STEP_KEY", `Step ${stepNo} has unknown key "${key}" which the simulator ignores.`, "Remove it or check the field name (valid: name, method, path, description, body, inject, capture, captureInput, expectStatus).");
|
|
269678
269783
|
}
|
|
269679
269784
|
}
|
|
269680
|
-
if ("condition" in step) {
|
|
269681
|
-
|
|
269785
|
+
if ("condition" in step && step.condition !== void 0) {
|
|
269786
|
+
const cond = step.condition;
|
|
269787
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
269788
|
+
err(errors, stepNo, sName, "BAD_CONDITION", `Step ${stepNo} "condition" must be an object.`, 'Use { "condition": { "if": "{{workflow:role}}", "equals": "admin" } }.');
|
|
269789
|
+
} else if (!cond.if || typeof cond.if !== "string" || !cond.if.trim()) {
|
|
269790
|
+
err(errors, stepNo, sName, "BAD_CONDITION", `Step ${stepNo} "condition" must have a non-empty "if" field.`, 'Set "if" to a template expression like "{{workflow:role}}".');
|
|
269791
|
+
} else {
|
|
269792
|
+
const operators = ["equals", "notEquals", "contains", "exists", "greaterThan", "lessThan", "in"];
|
|
269793
|
+
const usedOps = operators.filter((op) => op in cond && cond[op] !== void 0);
|
|
269794
|
+
if (usedOps.length === 0) {
|
|
269795
|
+
warn(warnings, stepNo, sName, "CONDITION_NO_OPERATOR", `Step ${stepNo} "condition" has no comparison operator \u2014 uses bare truthy check.`, "Add one of: equals, notEquals, contains, exists, greaterThan, lessThan, in for explicit logic.");
|
|
269796
|
+
}
|
|
269797
|
+
if (!/\{\{[^}]+\}\}/.test(cond.if)) {
|
|
269798
|
+
warn(warnings, stepNo, sName, "CONDITION_BARE_IF", `Step ${stepNo} "condition.if" ("${cond.if}") contains no {{template}} and will be compared literally.`, "Use {{workflow:key}} or {{scope:key}} to reference captured memory values.");
|
|
269799
|
+
}
|
|
269800
|
+
const condRefs = [];
|
|
269801
|
+
collectTemplateRefs(cond.if, condRefs);
|
|
269802
|
+
for (const ref of condRefs) {
|
|
269803
|
+
if (ref.startsWith("faker."))
|
|
269804
|
+
continue;
|
|
269805
|
+
if (!ref.includes(":"))
|
|
269806
|
+
continue;
|
|
269807
|
+
const { scope, key } = splitMemKey(ref);
|
|
269808
|
+
if (!key)
|
|
269809
|
+
continue;
|
|
269810
|
+
if (scope === "human") {
|
|
269811
|
+
humanKeys.add(key);
|
|
269812
|
+
continue;
|
|
269813
|
+
}
|
|
269814
|
+
const full = `${scope}:${key}`;
|
|
269815
|
+
if (!defined.has(full) && !persistedMemory.has(full)) {
|
|
269816
|
+
err(errors, stepNo, sName, "UNDEFINED_MEMORY_REF", `"{{${ref}}}" in condition.if of step ${stepNo} is not captured by any earlier step and is not in memory.json.`, `Capture "${full}" in an earlier step before referencing it in a condition.`);
|
|
269817
|
+
}
|
|
269818
|
+
}
|
|
269819
|
+
}
|
|
269682
269820
|
}
|
|
269683
269821
|
if (!sName || !sName.trim()) {
|
|
269684
269822
|
err(errors, stepNo, void 0, "STEP_MISSING_NAME", `Step ${stepNo} needs a non-empty "name".`, "Give every step a unique descriptive name.");
|
|
@@ -269804,6 +269942,40 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269804
269942
|
}
|
|
269805
269943
|
}
|
|
269806
269944
|
}
|
|
269945
|
+
if (step.onFailure !== void 0) {
|
|
269946
|
+
if (!["abort", "continue"].includes(String(step.onFailure))) {
|
|
269947
|
+
err(errors, stepNo, sName, "BAD_ON_FAILURE", `Step ${stepNo} "onFailure" must be "abort" or "continue" (got "${step.onFailure}").`, 'Use "abort" (default, stops the workflow) or "continue" (logs failure, proceeds).');
|
|
269948
|
+
}
|
|
269949
|
+
if (step.onFailure === "continue" && step.capture && Object.keys(step.capture).length > 0) {
|
|
269950
|
+
warn(warnings, stepNo, sName, "CONTINUE_WITH_CAPTURE", `Step ${stepNo} uses onFailure:"continue" but has captures \u2014 captures are only saved when the step passes.`, "This is usually fine for optional captures; ensure later steps handle missing memory gracefully.");
|
|
269951
|
+
}
|
|
269952
|
+
}
|
|
269953
|
+
if (step.continueOnStatus !== void 0) {
|
|
269954
|
+
if (!Array.isArray(step.continueOnStatus)) {
|
|
269955
|
+
err(errors, stepNo, sName, "BAD_CONTINUE_ON_STATUS", `Step ${stepNo} "continueOnStatus" must be an array of HTTP status codes.`, "Use [404] or [404, 409].");
|
|
269956
|
+
} else {
|
|
269957
|
+
for (const code of step.continueOnStatus) {
|
|
269958
|
+
if (typeof code !== "number" || code < 100 || code > 599) {
|
|
269959
|
+
err(errors, stepNo, sName, "BAD_CONTINUE_ON_STATUS", `Step ${stepNo} "continueOnStatus" contains invalid status code: ${code}.`, "HTTP status codes must be integers between 100 and 599.");
|
|
269960
|
+
}
|
|
269961
|
+
}
|
|
269962
|
+
if (step.continueOnStatus.some((c3) => c3 >= 500)) {
|
|
269963
|
+
warn(warnings, stepNo, sName, "CONTINUE_ON_5XX", `Step ${stepNo} "continueOnStatus" includes 5xx server error codes \u2014 accepting server errors as pass is unusual.`, "Verify this is intentional (e.g. for idempotency checks or async polling).");
|
|
269964
|
+
}
|
|
269965
|
+
}
|
|
269966
|
+
}
|
|
269967
|
+
if (step.retry !== void 0) {
|
|
269968
|
+
const r = step.retry;
|
|
269969
|
+
if (!r || typeof r !== "object" || Array.isArray(r)) {
|
|
269970
|
+
err(errors, stepNo, sName, "BAD_RETRY", `Step ${stepNo} "retry" must be an object { times, delayMs? }.`, 'Use { "retry": { "times": 3, "delayMs": 1000 } }.');
|
|
269971
|
+
} else {
|
|
269972
|
+
if (typeof r.times !== "number" || r.times < 1) {
|
|
269973
|
+
err(errors, stepNo, sName, "BAD_RETRY", `Step ${stepNo} "retry.times" must be a positive integer.`, "Set retry.times to 1 or more.");
|
|
269974
|
+
} else if (r.times > 5) {
|
|
269975
|
+
warn(warnings, stepNo, sName, "HIGH_RETRY_COUNT", `Step ${stepNo} "retry.times" is ${r.times} \u2014 this may slow down workflows significantly.`, "Keep retry.times \u2264 5 for most use cases.");
|
|
269976
|
+
}
|
|
269977
|
+
}
|
|
269978
|
+
}
|
|
269807
269979
|
for (const field of ["capture", "captureInput"]) {
|
|
269808
269980
|
const cap = step[field];
|
|
269809
269981
|
if (cap && typeof cap === "object" && !Array.isArray(cap)) {
|
|
@@ -270114,9 +270286,11 @@ var require_project_tools = __commonJS({
|
|
|
270114
270286
|
};
|
|
270115
270287
|
})();
|
|
270116
270288
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
270117
|
-
exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
|
|
270289
|
+
exports2.clearChangesSchema = exports2.getChangesSchema = exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
|
|
270118
270290
|
exports2.handleInitProject = handleInitProject;
|
|
270119
270291
|
exports2.handleScanProject = handleScanProject;
|
|
270292
|
+
exports2.handleGetChanges = handleGetChanges;
|
|
270293
|
+
exports2.handleClearChanges = handleClearChanges;
|
|
270120
270294
|
var zod_1 = require("zod");
|
|
270121
270295
|
var fs4 = __importStar(require("fs"));
|
|
270122
270296
|
var path7 = __importStar(require("path"));
|
|
@@ -270278,6 +270452,16 @@ var require_project_tools = __commonJS({
|
|
|
270278
270452
|
existing.project = { ...scanned.project, ...existing.project };
|
|
270279
270453
|
}
|
|
270280
270454
|
(0, types_1.saveModel)(filePath, existing);
|
|
270455
|
+
const changesPath = path7.join(path7.dirname(filePath), "changes.json");
|
|
270456
|
+
if (fs4.existsSync(changesPath)) {
|
|
270457
|
+
try {
|
|
270458
|
+
const changesData = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
|
|
270459
|
+
changesData.changes = [];
|
|
270460
|
+
changesData.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
270461
|
+
fs4.writeFileSync(changesPath, JSON.stringify(changesData, null, 2), "utf-8");
|
|
270462
|
+
} catch {
|
|
270463
|
+
}
|
|
270464
|
+
}
|
|
270281
270465
|
const total = existing.endpoints.length;
|
|
270282
270466
|
return {
|
|
270283
270467
|
success: true,
|
|
@@ -270291,6 +270475,64 @@ var require_project_tools = __commonJS({
|
|
|
270291
270475
|
next: total === 0 ? "No endpoints yet \u2014 model them manually with jetic_add_endpoint, then jetic_verify_model." : `Next: refine with jetic_add_endpoint / jetic_update_endpoint (AI enrichment is skipped in scans \u2014 add constraints, examples, ownership by hand), then jetic_verify_model until clean. Step 3 (only on request): workflows via jetic_validate_workflow \u2192 jetic_create_workflow \u2192 jetic_simulate_workflow.`
|
|
270292
270476
|
};
|
|
270293
270477
|
}
|
|
270478
|
+
exports2.getChangesSchema = zod_1.z.object({
|
|
270479
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root (defaults to JETIC_PROJECT_PATH / cwd)")
|
|
270480
|
+
});
|
|
270481
|
+
function handleGetChanges(args) {
|
|
270482
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270483
|
+
const jeticDir = path7.dirname(filePath);
|
|
270484
|
+
const changesPath = path7.join(jeticDir, "changes.json");
|
|
270485
|
+
if (!fs4.existsSync(changesPath)) {
|
|
270486
|
+
return {
|
|
270487
|
+
hasChanges: false,
|
|
270488
|
+
changes: [],
|
|
270489
|
+
watchedSince: null,
|
|
270490
|
+
message: "No changes.json found. Start `jetic dev` to enable automatic file change tracking."
|
|
270491
|
+
};
|
|
270492
|
+
}
|
|
270493
|
+
let data;
|
|
270494
|
+
try {
|
|
270495
|
+
data = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
|
|
270496
|
+
} catch {
|
|
270497
|
+
return { hasChanges: false, changes: [], watchedSince: null, message: "changes.json is unreadable or corrupted." };
|
|
270498
|
+
}
|
|
270499
|
+
const changes = Array.isArray(data.changes) ? data.changes : [];
|
|
270500
|
+
return {
|
|
270501
|
+
hasChanges: changes.length > 0,
|
|
270502
|
+
totalChanged: changes.length,
|
|
270503
|
+
watchedSince: data.watchedSince ?? null,
|
|
270504
|
+
projectRoot: data.projectRoot ?? jeticDir,
|
|
270505
|
+
changes,
|
|
270506
|
+
filePaths: changes.map((c3) => c3.filePath),
|
|
270507
|
+
hint: changes.length > 0 ? `Read ONLY these ${changes.length} file(s) to update model.json \u2014 no need to scan the entire codebase. After updating, call jetic_clear_changes.` : "No file changes recorded since last scan. Model is up-to-date."
|
|
270508
|
+
};
|
|
270509
|
+
}
|
|
270510
|
+
exports2.clearChangesSchema = zod_1.z.object({
|
|
270511
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root (defaults to JETIC_PROJECT_PATH / cwd)")
|
|
270512
|
+
});
|
|
270513
|
+
function handleClearChanges(args) {
|
|
270514
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270515
|
+
const jeticDir = path7.dirname(filePath);
|
|
270516
|
+
const changesPath = path7.join(jeticDir, "changes.json");
|
|
270517
|
+
if (!fs4.existsSync(changesPath)) {
|
|
270518
|
+
return { success: true, message: "changes.json did not exist \u2014 nothing to clear." };
|
|
270519
|
+
}
|
|
270520
|
+
try {
|
|
270521
|
+
const data = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
|
|
270522
|
+
const cleared = Array.isArray(data.changes) ? data.changes.length : 0;
|
|
270523
|
+
data.changes = [];
|
|
270524
|
+
data.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
270525
|
+
fs4.writeFileSync(changesPath, JSON.stringify(data, null, 2), "utf-8");
|
|
270526
|
+
return {
|
|
270527
|
+
success: true,
|
|
270528
|
+
cleared,
|
|
270529
|
+
watchedSince: data.watchedSince,
|
|
270530
|
+
message: `Cleared ${cleared} change record(s). ChangeScanner will now track future edits from scratch.`
|
|
270531
|
+
};
|
|
270532
|
+
} catch (err) {
|
|
270533
|
+
throw new Error(`Failed to clear changes.json: ${err.message}`);
|
|
270534
|
+
}
|
|
270535
|
+
}
|
|
270294
270536
|
}
|
|
270295
270537
|
});
|
|
270296
270538
|
|
|
@@ -284410,6 +284652,284 @@ var require_session_tools = __commonJS({
|
|
|
284410
284652
|
}
|
|
284411
284653
|
});
|
|
284412
284654
|
|
|
284655
|
+
// ../../packages/mcp-server/dist/tools/workflow-designer-tools.js
|
|
284656
|
+
var require_workflow_designer_tools = __commonJS({
|
|
284657
|
+
"../../packages/mcp-server/dist/tools/workflow-designer-tools.js"(exports2) {
|
|
284658
|
+
"use strict";
|
|
284659
|
+
init_cjs_shims();
|
|
284660
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
284661
|
+
exports2.scaffoldWorkflowSchema = void 0;
|
|
284662
|
+
exports2.handleScaffoldWorkflow = handleScaffoldWorkflow;
|
|
284663
|
+
var zod_1 = require("zod");
|
|
284664
|
+
var types_1 = require_types();
|
|
284665
|
+
function fieldToFakerValue(name, field) {
|
|
284666
|
+
if (field.enum && field.enum.length > 0)
|
|
284667
|
+
return field.enum[0];
|
|
284668
|
+
if (field.format === "email")
|
|
284669
|
+
return "{{faker.internet.email}}";
|
|
284670
|
+
if (field.format === "uuid")
|
|
284671
|
+
return "{{faker.string.uuid}}";
|
|
284672
|
+
if (field.format === "date")
|
|
284673
|
+
return "{{faker.date.past}}";
|
|
284674
|
+
if (field.format === "date-time")
|
|
284675
|
+
return "{{faker.date.recent}}";
|
|
284676
|
+
if (field.format === "uri" || field.format === "url")
|
|
284677
|
+
return "{{faker.internet.url}}";
|
|
284678
|
+
if (field.format === "hostname")
|
|
284679
|
+
return "{{faker.internet.domainName}}";
|
|
284680
|
+
if (field.format === "ipv4")
|
|
284681
|
+
return "{{faker.internet.ip}}";
|
|
284682
|
+
const n = name.toLowerCase().replace(/[_-]/g, "");
|
|
284683
|
+
if (n === "email" || n.endsWith("email"))
|
|
284684
|
+
return "{{faker.internet.email}}";
|
|
284685
|
+
if (n.includes("password") || n.includes("pwd"))
|
|
284686
|
+
return "{{faker.internet.password}}";
|
|
284687
|
+
if (n === "phone" || n.includes("phone") || n.includes("mobile"))
|
|
284688
|
+
return "{{faker.phone.number}}";
|
|
284689
|
+
if (n === "name" || n === "fullname" || n === "displayname")
|
|
284690
|
+
return "{{faker.person.fullName}}";
|
|
284691
|
+
if (n === "firstname")
|
|
284692
|
+
return "{{faker.person.firstName}}";
|
|
284693
|
+
if (n === "lastname")
|
|
284694
|
+
return "{{faker.person.lastName}}";
|
|
284695
|
+
if (n === "username")
|
|
284696
|
+
return "{{faker.internet.username}}";
|
|
284697
|
+
if (n === "title")
|
|
284698
|
+
return "{{faker.lorem.sentence}}";
|
|
284699
|
+
if (n === "description" || n === "bio" || n === "summary")
|
|
284700
|
+
return "{{faker.lorem.paragraph}}";
|
|
284701
|
+
if (n === "url" || n.endsWith("url") || n === "avatar")
|
|
284702
|
+
return "{{faker.internet.url}}";
|
|
284703
|
+
if (n === "company" || n.includes("company") || n.includes("organization") || n.includes("workspace"))
|
|
284704
|
+
return "{{faker.company.name}}";
|
|
284705
|
+
if (n === "city")
|
|
284706
|
+
return "{{faker.location.city}}";
|
|
284707
|
+
if (n === "country")
|
|
284708
|
+
return "{{faker.location.country}}";
|
|
284709
|
+
if (n.includes("address"))
|
|
284710
|
+
return "{{faker.location.streetAddress}}";
|
|
284711
|
+
if (n === "zip" || n === "zipcode" || n.includes("postal"))
|
|
284712
|
+
return "{{faker.location.zipCode}}";
|
|
284713
|
+
if (n === "state" || n === "province")
|
|
284714
|
+
return "{{faker.location.state}}";
|
|
284715
|
+
if (n === "latitude" || n === "lat")
|
|
284716
|
+
return "{{faker.location.latitude}}";
|
|
284717
|
+
if (n === "longitude" || n === "lon" || n === "lng")
|
|
284718
|
+
return "{{faker.location.longitude}}";
|
|
284719
|
+
if (n === "color" || n === "colour")
|
|
284720
|
+
return "{{faker.color.human}}";
|
|
284721
|
+
if (n === "amount" || n === "price")
|
|
284722
|
+
return field.min ?? 1;
|
|
284723
|
+
if (n === "quantity" || n === "count" || n === "limit")
|
|
284724
|
+
return field.min ?? 10;
|
|
284725
|
+
if (n === "slug")
|
|
284726
|
+
return "{{faker.lorem.slug}}";
|
|
284727
|
+
if (n === "token" || n === "secret" || n === "key" || n === "otp" || n === "code" || n === "pin")
|
|
284728
|
+
return null;
|
|
284729
|
+
if (field.type === "string")
|
|
284730
|
+
return "{{faker.lorem.word}}";
|
|
284731
|
+
if (field.type === "number" || field.type === "integer")
|
|
284732
|
+
return field.min ?? 1;
|
|
284733
|
+
if (field.type === "boolean")
|
|
284734
|
+
return true;
|
|
284735
|
+
return null;
|
|
284736
|
+
}
|
|
284737
|
+
function tokenMemoryKey(schemeName, fieldPath) {
|
|
284738
|
+
const last = fieldPath.split(".").pop() || schemeName;
|
|
284739
|
+
return last.charAt(0).toLowerCase() + last.slice(1);
|
|
284740
|
+
}
|
|
284741
|
+
function isReuseCandidate(name) {
|
|
284742
|
+
const n = name.toLowerCase().replace(/[_-]/g, "");
|
|
284743
|
+
return ["email", "password", "username", "phone", "name", "id", "slug"].some((k) => n === k || n.endsWith(k));
|
|
284744
|
+
}
|
|
284745
|
+
exports2.scaffoldWorkflowSchema = zod_1.z.object({
|
|
284746
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
284747
|
+
endpoints: zod_1.z.array(zod_1.z.union([
|
|
284748
|
+
zod_1.z.string().describe('Method + path string, e.g. "POST /api/auth/login"'),
|
|
284749
|
+
zod_1.z.object({ method: zod_1.z.string(), path: zod_1.z.string() })
|
|
284750
|
+
])).min(1).describe("ORDERED list of endpoints to scaffold into steps. Order matters: token-producer endpoints (e.g. login) must come before consumers (e.g. protected routes)."),
|
|
284751
|
+
workflowName: zod_1.z.string().optional().describe("Suggested workflow name"),
|
|
284752
|
+
environment: zod_1.z.string().optional().describe("Target environment name from model")
|
|
284753
|
+
});
|
|
284754
|
+
function handleScaffoldWorkflow(args) {
|
|
284755
|
+
const { model } = (0, types_1.loadModel)(args.projectPath);
|
|
284756
|
+
const requested = args.endpoints.map((ep) => {
|
|
284757
|
+
if (typeof ep === "string") {
|
|
284758
|
+
const parts = ep.trim().split(/\s+/);
|
|
284759
|
+
return { method: (parts[0] || "GET").toUpperCase(), path: parts.slice(1).join(" ") || "/" };
|
|
284760
|
+
}
|
|
284761
|
+
return { method: ep.method.toUpperCase(), path: ep.path };
|
|
284762
|
+
});
|
|
284763
|
+
const securityMemKeys = {};
|
|
284764
|
+
for (const [schemeName, scheme] of Object.entries(model.securitySchemes || {})) {
|
|
284765
|
+
if (scheme.obtainedFrom) {
|
|
284766
|
+
securityMemKeys[schemeName] = {
|
|
284767
|
+
memKey: tokenMemoryKey(schemeName, scheme.obtainedFrom.field),
|
|
284768
|
+
producedByEndpoint: scheme.obtainedFrom.endpoint.toUpperCase().trim(),
|
|
284769
|
+
fieldPath: scheme.obtainedFrom.field
|
|
284770
|
+
};
|
|
284771
|
+
}
|
|
284772
|
+
}
|
|
284773
|
+
const tokenProducers = {};
|
|
284774
|
+
for (const si of Object.values(securityMemKeys)) {
|
|
284775
|
+
if (!tokenProducers[si.producedByEndpoint])
|
|
284776
|
+
tokenProducers[si.producedByEndpoint] = [];
|
|
284777
|
+
tokenProducers[si.producedByEndpoint].push({ memKey: si.memKey, fieldPath: si.fieldPath });
|
|
284778
|
+
}
|
|
284779
|
+
const draftSteps = [];
|
|
284780
|
+
const missingFields = {};
|
|
284781
|
+
const allHumanKeys = /* @__PURE__ */ new Set();
|
|
284782
|
+
const securityChain = [];
|
|
284783
|
+
const producedKeys = /* @__PURE__ */ new Set();
|
|
284784
|
+
for (let idx = 0; idx < requested.length; idx++) {
|
|
284785
|
+
const req = requested[idx];
|
|
284786
|
+
const routeKey = `${req.method} ${req.path}`;
|
|
284787
|
+
const routeKeyNorm = routeKey.toUpperCase().trim();
|
|
284788
|
+
const endpoint = (model.endpoints || []).find((e) => e.method.toUpperCase() === req.method && e.path === req.path);
|
|
284789
|
+
if (!endpoint) {
|
|
284790
|
+
draftSteps.push({
|
|
284791
|
+
name: routeKey,
|
|
284792
|
+
method: req.method,
|
|
284793
|
+
path: req.path,
|
|
284794
|
+
description: `WARNING: "${routeKey}" not found in model.json \u2014 add it with jetic_add_endpoint first.`,
|
|
284795
|
+
expectStatus: 200
|
|
284796
|
+
});
|
|
284797
|
+
missingFields[routeKey] = [`Endpoint "${routeKey}" not found in model.json.`];
|
|
284798
|
+
continue;
|
|
284799
|
+
}
|
|
284800
|
+
const step = {
|
|
284801
|
+
name: endpoint.name || routeKey,
|
|
284802
|
+
method: req.method,
|
|
284803
|
+
path: endpoint.path
|
|
284804
|
+
};
|
|
284805
|
+
if (endpoint.description)
|
|
284806
|
+
step.description = endpoint.description;
|
|
284807
|
+
const missing = [];
|
|
284808
|
+
if (endpoint.requestBody?.fields && Object.keys(endpoint.requestBody.fields).length > 0) {
|
|
284809
|
+
const body = {};
|
|
284810
|
+
for (const [fname, fdef] of Object.entries(endpoint.requestBody.fields)) {
|
|
284811
|
+
let fromMem = null;
|
|
284812
|
+
if (endpoint.consumes) {
|
|
284813
|
+
for (const c3 of endpoint.consumes) {
|
|
284814
|
+
if (c3.usedAs === fname || c3.variable === fname) {
|
|
284815
|
+
const k = `workflow:${c3.variable}`;
|
|
284816
|
+
if (producedKeys.has(k)) {
|
|
284817
|
+
fromMem = k;
|
|
284818
|
+
break;
|
|
284819
|
+
}
|
|
284820
|
+
}
|
|
284821
|
+
}
|
|
284822
|
+
}
|
|
284823
|
+
if (!fromMem) {
|
|
284824
|
+
for (const pk of producedKeys) {
|
|
284825
|
+
const pkName = pk.split(":").pop() || "";
|
|
284826
|
+
if (pkName.toLowerCase() === fname.toLowerCase()) {
|
|
284827
|
+
fromMem = pk;
|
|
284828
|
+
break;
|
|
284829
|
+
}
|
|
284830
|
+
}
|
|
284831
|
+
}
|
|
284832
|
+
if (fromMem) {
|
|
284833
|
+
body[fname] = `{{${fromMem}}}`;
|
|
284834
|
+
continue;
|
|
284835
|
+
}
|
|
284836
|
+
const val = fieldToFakerValue(fname, fdef);
|
|
284837
|
+
if (val !== null) {
|
|
284838
|
+
body[fname] = val;
|
|
284839
|
+
} else {
|
|
284840
|
+
body[fname] = `{{human:${fname}}}`;
|
|
284841
|
+
allHumanKeys.add(fname);
|
|
284842
|
+
missing.push(`"${fname}" (type: ${fdef.type || "unknown"}) \u2014 no auto-mapping. Using {{human:${fname}}} (will pause for interactive input at runtime).`);
|
|
284843
|
+
}
|
|
284844
|
+
}
|
|
284845
|
+
step.body = body;
|
|
284846
|
+
const ci = {};
|
|
284847
|
+
for (const [fname, fdef] of Object.entries(endpoint.requestBody.fields)) {
|
|
284848
|
+
if (fdef.required && isReuseCandidate(fname)) {
|
|
284849
|
+
const k = `workflow:${fname}`;
|
|
284850
|
+
if (!producedKeys.has(k)) {
|
|
284851
|
+
ci[k] = fname;
|
|
284852
|
+
producedKeys.add(k);
|
|
284853
|
+
}
|
|
284854
|
+
}
|
|
284855
|
+
}
|
|
284856
|
+
if (Object.keys(ci).length > 0)
|
|
284857
|
+
step.captureInput = ci;
|
|
284858
|
+
}
|
|
284859
|
+
if (endpoint.security && endpoint.security.length > 0) {
|
|
284860
|
+
const inject = {};
|
|
284861
|
+
for (const sec of endpoint.security) {
|
|
284862
|
+
if (sec.scheme === "none")
|
|
284863
|
+
continue;
|
|
284864
|
+
const si = securityMemKeys[sec.scheme];
|
|
284865
|
+
if (!si) {
|
|
284866
|
+
missing.push(`Security scheme "${sec.scheme}" has no obtainedFrom in model.securitySchemes \u2014 add inject manually.`);
|
|
284867
|
+
continue;
|
|
284868
|
+
}
|
|
284869
|
+
const tokenKey = `workflow:${si.memKey}`;
|
|
284870
|
+
const available = producedKeys.has(tokenKey) || requested.slice(0, idx).some((r) => `${r.method} ${r.path}`.toUpperCase().trim() === si.producedByEndpoint);
|
|
284871
|
+
if (available) {
|
|
284872
|
+
const rawScheme = model.securitySchemes?.[sec.scheme];
|
|
284873
|
+
const prefix = rawScheme?.valuePrefix != null ? rawScheme.valuePrefix : rawScheme?.scheme?.toLowerCase() === "bearer" ? "Bearer " : "";
|
|
284874
|
+
inject["header:Authorization"] = `${prefix}{{${tokenKey}}}`;
|
|
284875
|
+
securityChain.push(`Step ${idx + 1} (${routeKey}) \u2190 inject ${tokenKey} (produced by ${si.producedByEndpoint})`);
|
|
284876
|
+
} else {
|
|
284877
|
+
missing.push(`Security scheme "${sec.scheme}" requires "${tokenKey}" from "${si.producedByEndpoint}" \u2014 place that endpoint earlier in the list.`);
|
|
284878
|
+
}
|
|
284879
|
+
}
|
|
284880
|
+
if (Object.keys(inject).length > 0)
|
|
284881
|
+
step.inject = inject;
|
|
284882
|
+
}
|
|
284883
|
+
const capture = {};
|
|
284884
|
+
if (endpoint.produces) {
|
|
284885
|
+
for (const p of endpoint.produces) {
|
|
284886
|
+
if (p.variable && p.responseField) {
|
|
284887
|
+
const k = `workflow:${p.variable}`;
|
|
284888
|
+
capture[k] = p.responseField;
|
|
284889
|
+
producedKeys.add(k);
|
|
284890
|
+
}
|
|
284891
|
+
}
|
|
284892
|
+
}
|
|
284893
|
+
if (tokenProducers[routeKeyNorm]) {
|
|
284894
|
+
for (const { memKey, fieldPath } of tokenProducers[routeKeyNorm]) {
|
|
284895
|
+
const k = `workflow:${memKey}`;
|
|
284896
|
+
if (!capture[k]) {
|
|
284897
|
+
capture[k] = fieldPath;
|
|
284898
|
+
producedKeys.add(k);
|
|
284899
|
+
securityChain.push(`Step ${idx + 1} (${routeKey}) \u2192 produces ${k} from response.${fieldPath}`);
|
|
284900
|
+
}
|
|
284901
|
+
}
|
|
284902
|
+
}
|
|
284903
|
+
if (Object.keys(capture).length > 0)
|
|
284904
|
+
step.capture = capture;
|
|
284905
|
+
const codes = Object.keys(endpoint.responses || {}).map(Number).filter(Boolean);
|
|
284906
|
+
const successCode = codes.find((c3) => c3 >= 200 && c3 < 300);
|
|
284907
|
+
step.expectStatus = successCode ?? 200;
|
|
284908
|
+
if (missing.length > 0)
|
|
284909
|
+
missingFields[routeKey] = missing;
|
|
284910
|
+
draftSteps.push(step);
|
|
284911
|
+
}
|
|
284912
|
+
const defaultName = args.workflowName || requested.map((r) => r.path.split("/").filter(Boolean).pop() || r.path).join(" \u2192 ") + " workflow";
|
|
284913
|
+
return {
|
|
284914
|
+
workflowName: defaultName,
|
|
284915
|
+
...args.environment ? { environment: args.environment } : {},
|
|
284916
|
+
draftSteps,
|
|
284917
|
+
stepsCount: draftSteps.length,
|
|
284918
|
+
missingFields: Object.keys(missingFields).length > 0 ? missingFields : void 0,
|
|
284919
|
+
humanKeys: [...allHumanKeys].sort(),
|
|
284920
|
+
securityChain: securityChain.length > 0 ? securityChain : void 0,
|
|
284921
|
+
instructions: [
|
|
284922
|
+
"1. Review draftSteps. Resolve any missingFields entries before proceeding.",
|
|
284923
|
+
"2. humanKeys will pause the workflow at runtime for interactive input \u2014 they are intentional when needed.",
|
|
284924
|
+
'3. Validate: jetic_validate_workflow { definition: { name: "<workflowName>", steps: <draftSteps> } }',
|
|
284925
|
+
'4. Save: jetic_create_workflow { name: "<workflowName>", steps: <draftSteps> }',
|
|
284926
|
+
'5. Run: jetic_simulate_workflow { workflow: "<slug>" }'
|
|
284927
|
+
].join("\n")
|
|
284928
|
+
};
|
|
284929
|
+
}
|
|
284930
|
+
}
|
|
284931
|
+
});
|
|
284932
|
+
|
|
284413
284933
|
// ../../packages/mcp-server/dist/server.js
|
|
284414
284934
|
var require_server3 = __commonJS({
|
|
284415
284935
|
"../../packages/mcp-server/dist/server.js"(exports2) {
|
|
@@ -284471,6 +284991,7 @@ var require_server3 = __commonJS({
|
|
|
284471
284991
|
var workflow_authoring_tools_1 = require_workflow_authoring_tools();
|
|
284472
284992
|
var project_tools_1 = require_project_tools();
|
|
284473
284993
|
var session_tools_1 = require_session_tools();
|
|
284994
|
+
var workflow_designer_tools_1 = require_workflow_designer_tools();
|
|
284474
284995
|
exports2.JETIC_MCP_SERVER_NAME = "jetic-mcp-server";
|
|
284475
284996
|
function getJeticMcpVersion() {
|
|
284476
284997
|
try {
|
|
@@ -284492,7 +285013,7 @@ var require_server3 = __commonJS({
|
|
|
284492
285013
|
"\u2501\u2501 PHASE 2 \u2014 SCAN \u2501\u2501",
|
|
284493
285014
|
"Populate the model with endpoints:",
|
|
284494
285015
|
" \u2022 Express+TypeScript (tsconfig.json present): call `jetic_scan` (static, keyless, no AI provider needed).",
|
|
284495
|
-
" \u2022 Any other stack or scan gaps: read
|
|
285016
|
+
" \u2022 Any other stack or scan gaps: read source code and call `jetic_add_endpoint` for every route (middleware chains, security, constraints, pagination, produces/consumes).",
|
|
284496
285017
|
"Gate: DO NOT advance to Phase 3 until endpointsCount > 0.",
|
|
284497
285018
|
"",
|
|
284498
285019
|
"\u2501\u2501 PHASE 3 \u2014 VERIFY \u2501\u2501",
|
|
@@ -284501,19 +285022,65 @@ var require_server3 = __commonJS({
|
|
|
284501
285022
|
" \u2022 Add at least one `tag` per endpoint.",
|
|
284502
285023
|
" \u2022 Add `description` to every parameter.",
|
|
284503
285024
|
" \u2022 Add `description` to every response status code.",
|
|
284504
|
-
"Use `jetic_update_endpoint` to fill gaps. Call `jetic_verify_model` again after each batch
|
|
285025
|
+
"Use `jetic_update_endpoint` to fill gaps. Call `jetic_verify_model` again after each batch.",
|
|
284505
285026
|
"Gate: DO NOT advance to Phase 4 until jetic_get_session_phase returns phase=4 (modelIsValid AND allFieldsFilled both true).",
|
|
284506
285027
|
"",
|
|
284507
285028
|
"\u2501\u2501 PHASE 4 \u2014 SIMULATE \u2501\u2501",
|
|
284508
|
-
'STOP. Ask the user: "What simulation or workflow do you want to build?"',
|
|
284509
|
-
"Wait for their explicit answer. Then
|
|
284510
|
-
"
|
|
284511
|
-
"
|
|
285029
|
+
'STOP. Ask the user: "What simulation or workflow do you want to build or test?"',
|
|
285030
|
+
"Wait for their explicit answer. Then follow these sub-steps IN ORDER:",
|
|
285031
|
+
"",
|
|
285032
|
+
"STEP 4a \u2014 SCAFFOLD",
|
|
285033
|
+
" Call `jetic_scaffold_workflow` with the ORDERED list of endpoints the user wants to exercise.",
|
|
285034
|
+
" It reads model.json and returns `draftSteps` with:",
|
|
285035
|
+
" \u2022 body derived from requestBody.fields (faker-mapped by type / format / field name)",
|
|
285036
|
+
" \u2022 inject derived from securitySchemes.obtainedFrom (token producer \u2192 consumer chain)",
|
|
285037
|
+
" \u2022 capture derived from endpoint.produces",
|
|
285038
|
+
" \u2022 captureInput for required fields that later steps will reuse",
|
|
285039
|
+
" Review draftSteps. Fix any `missingFields` entries before proceeding.",
|
|
285040
|
+
"",
|
|
285041
|
+
"STEP 4b \u2014 HUMAN INPUT PROTOCOL",
|
|
285042
|
+
" Use {{human:key}} ONLY when the value is truly unknowable before runtime:",
|
|
285043
|
+
" \u2022 One-time secrets: OTP codes, 2FA tokens, CAPTCHA answers",
|
|
285044
|
+
" \u2022 Real credentials that must be typed at execution time",
|
|
285045
|
+
" NEVER use {{human:key}} for things faker can generate (emails, names, UUIDs, passwords).",
|
|
285046
|
+
" The scaffold tool already handles faker mapping \u2014 only override to {{human:key}} when a",
|
|
285047
|
+
" field appears in missingFields AND genuinely requires interactive user input.",
|
|
285048
|
+
"",
|
|
285049
|
+
"STEP 4c \u2014 CONDITIONS (branching / skipping steps)",
|
|
285050
|
+
" Add `condition` to skip a step when a memory value does not match:",
|
|
285051
|
+
' { "condition": { "if": "{{workflow:userRole}}", "equals": "admin" } }',
|
|
285052
|
+
" Operators: equals, notEquals, contains, exists (bool), greaterThan, lessThan, in (string[]).",
|
|
285053
|
+
" Skipped steps count as passed \u2014 the workflow continues.",
|
|
285054
|
+
' Pair with onFailure:"continue" for negative/branching flows.',
|
|
285055
|
+
"",
|
|
285056
|
+
"STEP 4d \u2014 FAILURE MODES & RETRY",
|
|
285057
|
+
" onFailure (per step):",
|
|
285058
|
+
' \u2022 "abort" (default) \u2014 stop the workflow on first failure',
|
|
285059
|
+
' \u2022 "continue" \u2014 log the failure and proceed (captures are skipped for that step)',
|
|
285060
|
+
" continueOnStatus (per step): extra codes treated as pass, e.g. [404] or [409].",
|
|
285061
|
+
" retry (per step OR workflow-level global default):",
|
|
285062
|
+
' \u2022 { "times": 3, "delayMs": 1000 } \u2014 retries up to 3\xD7 with 1 s between each attempt',
|
|
285063
|
+
" \u2022 Per-step retry overrides the workflow-level default. Keep times \u2264 5.",
|
|
285064
|
+
"",
|
|
285065
|
+
"STEP 4e \u2014 VALIDATE (dry run, no save)",
|
|
285066
|
+
' jetic_validate_workflow { "definition": { "name": "...", "steps": [...] } }',
|
|
285067
|
+
" Fix all errors. Warnings are informational.",
|
|
285068
|
+
"",
|
|
285069
|
+
"STEP 4f \u2014 SAVE",
|
|
285070
|
+
' jetic_create_workflow { "name": "...", "steps": [...] }',
|
|
285071
|
+
"",
|
|
285072
|
+
"STEP 4g \u2014 RUN",
|
|
285073
|
+
' jetic_simulate_workflow { "workflow": "<slug>" }',
|
|
284512
285074
|
"",
|
|
284513
285075
|
"\u2501\u2501 GENERAL RULES \u2501\u2501",
|
|
284514
285076
|
"No AI-provider setup is needed: never ask for OpenRouter/OpenAI keys and never use `jetic config ai`.",
|
|
284515
285077
|
"Mutating tools write to disk \u2014 confirm intent with the user before destructive operations (delete/overwrite).",
|
|
284516
|
-
"Project resolution: every tool accepts an optional `projectPath`. If omitted, falls back to JETIC_PROJECT_PATH env var, then cwd."
|
|
285078
|
+
"Project resolution: every tool accepts an optional `projectPath`. If omitted, falls back to JETIC_PROJECT_PATH env var, then cwd.",
|
|
285079
|
+
"\u2501\u2501 CHANGE TRACKING (jetic dev) \u2501\u2501",
|
|
285080
|
+
"When `jetic dev` is running it watches source files and logs edits to `.jetic/changes.json`.",
|
|
285081
|
+
"Before rescanning: call `jetic_get_changes` \u2014 if hasChanges is true, read ONLY the listed filePaths.",
|
|
285082
|
+
"Update model.json for those files, then call `jetic_clear_changes` (or jetic_scan, which clears automatically).",
|
|
285083
|
+
""
|
|
284517
285084
|
].join("\n");
|
|
284518
285085
|
var READ_ONLY = {
|
|
284519
285086
|
readOnlyHint: true,
|
|
@@ -284563,7 +285130,9 @@ var require_server3 = __commonJS({
|
|
|
284563
285130
|
};
|
|
284564
285131
|
register("jetic_get_session_phase", "Get session phase", "ALWAYS CALL THIS FIRST. Returns the current workflow phase (1=init, 2=scan, 3=verify, 4=simulate), gate conditions (modelExists, endpointsCount, modelIsValid, allFieldsFilled), and the exact nextAction the AI must take. Never skip phase gates.", session_tools_1.getSessionPhaseSchema.shape, READ_ONLY, session_tools_1.handleGetSessionPhase);
|
|
284565
285132
|
register("jetic_init", "Initialize Jetic project", "PHASE 1: Scaffolds .jetic/model.json (+ workflows/, config.json) for repos that have none. Refuses to clobber an existing model (pass overwrite:true to replace). Reports whether auto-scan applies (Express+TypeScript) and what to do next.", project_tools_1.initProjectSchema.shape, WRITES_MODEL, project_tools_1.handleInitProject);
|
|
284566
|
-
register("jetic_scan", "Scan Express project", "PHASE 2 (Express+TypeScript only \u2014 requires tsconfig.json): Static AST scan that fills model.json with routes, middleware, and auth heuristics. Keyless \u2014 no AI provider involved. Default merge upserts scanned routes and keeps hand-added endpoints; overwrite replaces all endpoints. Fails with guidance for non-Express stacks (model those manually with jetic_add_endpoint).", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
|
|
285133
|
+
register("jetic_scan", "Scan Express project", "PHASE 2 (Express+TypeScript only \u2014 requires tsconfig.json): Static AST scan that fills model.json with routes, middleware, and auth heuristics. Keyless \u2014 no AI provider involved. Default merge upserts scanned routes and keeps hand-added endpoints; overwrite replaces all endpoints. Fails with guidance for non-Express stacks (model those manually with jetic_add_endpoint). Auto-clears .jetic/changes.json after success.", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
|
|
285134
|
+
register("jetic_get_changes", "Get file change log", "Reads .jetic/changes.json \u2014 the lightweight change log populated by the ChangeScanner running inside `jetic dev`. Returns the list of source files edited since the last scan or jetic_clear_changes. Use this BEFORE rescanning to read only changed files instead of the full codebase.", project_tools_1.getChangesSchema.shape, READ_ONLY, project_tools_1.handleGetChanges);
|
|
285135
|
+
register("jetic_clear_changes", "Clear file change log", "Clears .jetic/changes.json after the AI has finished updating model.json from the changed files. Also called automatically by jetic_scan. Call this when you have finished processing all changes returned by jetic_get_changes.", project_tools_1.clearChangesSchema.shape, WRITES_MODEL, project_tools_1.handleClearChanges);
|
|
284567
285136
|
register("jetic_read_model", "Read Jetic model", "Reads .jetic/model.json from the workspace and returns the full behavioral model or a metadata summary.", model_tools_1.readModelSchema.shape, READ_ONLY, model_tools_1.handleReadModel);
|
|
284568
285137
|
register("jetic_list_endpoints", "List endpoints", "Lists API endpoints discovered in .jetic/model.json with optional filters by method, tag, resource, or path substring.", model_tools_1.listEndpointsSchema.shape, READ_ONLY, model_tools_1.handleListEndpoints);
|
|
284569
285138
|
register("jetic_get_endpoint", "Get endpoint details", "Returns full structural details (parameters, request body, schemas, security, responses) of one endpoint, looked up by ID or by method + path.", model_tools_1.getEndpointSchema.shape, READ_ONLY, model_tools_1.handleGetEndpoint);
|
|
@@ -284576,8 +285145,9 @@ var require_server3 = __commonJS({
|
|
|
284576
285145
|
register("jetic_list_workflows", "List workflows", "Lists multi-step integration test workflows from .jetic/workflows/*.json, .jetic/workflow.json, or embedded model.json workflows.", workflow_tools_1.listWorkflowsSchema.shape, READ_ONLY, workflow_tools_1.handleListWorkflows);
|
|
284577
285146
|
register("jetic_simulate_workflow", "Simulate workflow", "Executes a full end-to-end multi-step workflow simulation with dynamic memory capture, header injection, and schema validation against the live server.", workflow_tools_1.simulateWorkflowSchema.shape, READ_LIVE, workflow_tools_1.handleSimulateWorkflow);
|
|
284578
285147
|
register("jetic_simulate", "Simulate workflow (alias)", "Alias of jetic_simulate_workflow: runs a multi-step user-journey workflow against the live server. Prefer jetic_simulate_workflow in new prompts.", workflow_tools_1.simulateWorkflowSchema.shape, READ_LIVE, workflow_tools_1.handleSimulateWorkflow);
|
|
284579
|
-
register("
|
|
284580
|
-
register("
|
|
285148
|
+
register("jetic_scaffold_workflow", "Scaffold workflow steps from model", "PHASE 4a \u2014 FIRST STEP when building a workflow. Given an ordered endpoint list, reads model.json and returns ready-made `draftSteps`: body fields faker-mapped by type/format/name, inject derived from securitySchemes.obtainedFrom token-producer chains, capture from endpoint.produces, captureInput for reusable credentials. Returns missingFields (fields needing human input or manual fix) and securityChain analysis. Does NOT save. Review draftSteps, then call jetic_validate_workflow \u2192 jetic_create_workflow.", workflow_designer_tools_1.scaffoldWorkflowSchema.shape, READ_ONLY, workflow_designer_tools_1.handleScaffoldWorkflow);
|
|
285149
|
+
register("jetic_validate_workflow", "Validate workflow (dry run)", "Deep-validates a workflow WITHOUT saving: step structure, endpoint cross-checks against model.json, full memory data-flow analysis. Now also validates condition (including condition.if template refs), onFailure, continueOnStatus, and retry fields. Accepts a saved workflow slug/name or an inline definition.", workflow_authoring_tools_1.validateWorkflowSchema.shape, READ_ONLY, workflow_authoring_tools_1.handleValidateWorkflow);
|
|
285150
|
+
register("jetic_create_workflow", "Create workflow", "Creates a .jetic/workflows/<slug>.json workflow. Supports: condition (skip steps based on memory), onFailure (abort|continue), continueOnStatus (extra pass codes), retry (per-step or global default). Refuses to save on validation errors.", workflow_authoring_tools_1.createWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleCreateWorkflow);
|
|
284581
285151
|
register("jetic_update_workflow", "Update workflow", "Updates a saved file workflow: rename, description/environment, full steps replacement, or appendSteps. Validates before writing and refuses to save on errors.", workflow_authoring_tools_1.updateWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleUpdateWorkflow);
|
|
284582
285152
|
register("jetic_delete_workflow", "Delete workflow", "Deletes a saved .jetic/workflows/*.json workflow file by slug, name, or path. Destructive: confirm with the user before calling. Cannot delete model.json-embedded workflows.", workflow_authoring_tools_1.deleteWorkflowSchema.shape, DESTRUCTIVE, workflow_authoring_tools_1.handleDeleteWorkflow);
|
|
284583
285153
|
return server;
|
|
@@ -286333,9 +286903,170 @@ var import_path = __toESM(require("path"));
|
|
|
286333
286903
|
var import_fs = __toESM(require("fs"));
|
|
286334
286904
|
var import_core7 = __toESM(require_dist());
|
|
286335
286905
|
var import_memory3 = __toESM(require_dist4());
|
|
286906
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".cache", "__pycache__", ".next", ".nuxt", ".venv"]);
|
|
286907
|
+
var IGNORED_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".mp4", ".webm", ".woff", ".woff2", ".ttf", ".eot", ".map"]);
|
|
286908
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt", ".cs", ".rb", ".php", ".json", ".yaml", ".yml", ".toml", ".env", ".graphql", ".gql", ".sql", ".prisma", ".proto"]);
|
|
286909
|
+
var ChangeScanner = class {
|
|
286910
|
+
changesPath;
|
|
286911
|
+
projectRoot;
|
|
286912
|
+
watchers = [];
|
|
286913
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
286914
|
+
DEBOUNCE_MS = 300;
|
|
286915
|
+
listeners = /* @__PURE__ */ new Set();
|
|
286916
|
+
constructor(projectRoot, jeticDir) {
|
|
286917
|
+
this.projectRoot = projectRoot;
|
|
286918
|
+
this.changesPath = import_path.default.join(jeticDir, "changes.json");
|
|
286919
|
+
}
|
|
286920
|
+
onChange(cb) {
|
|
286921
|
+
this.listeners.add(cb);
|
|
286922
|
+
return () => this.listeners.delete(cb);
|
|
286923
|
+
}
|
|
286924
|
+
notifyListeners(data) {
|
|
286925
|
+
for (const listener of this.listeners) {
|
|
286926
|
+
try {
|
|
286927
|
+
listener(data);
|
|
286928
|
+
} catch {
|
|
286929
|
+
}
|
|
286930
|
+
}
|
|
286931
|
+
}
|
|
286932
|
+
readChanges() {
|
|
286933
|
+
try {
|
|
286934
|
+
if (import_fs.default.existsSync(this.changesPath)) {
|
|
286935
|
+
return JSON.parse(import_fs.default.readFileSync(this.changesPath, "utf8"));
|
|
286936
|
+
}
|
|
286937
|
+
} catch {
|
|
286938
|
+
}
|
|
286939
|
+
return {
|
|
286940
|
+
version: 1,
|
|
286941
|
+
watchedSince: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286942
|
+
projectRoot: this.projectRoot,
|
|
286943
|
+
changes: []
|
|
286944
|
+
};
|
|
286945
|
+
}
|
|
286946
|
+
saveChanges(data) {
|
|
286947
|
+
import_fs.default.writeFileSync(this.changesPath, JSON.stringify(data, null, 2), "utf8");
|
|
286948
|
+
this.notifyListeners(data);
|
|
286949
|
+
}
|
|
286950
|
+
recordChange(absolutePath, eventType) {
|
|
286951
|
+
const jeticDir = import_path.default.dirname(this.changesPath);
|
|
286952
|
+
if (absolutePath.startsWith(jeticDir)) return;
|
|
286953
|
+
const ext = import_path.default.extname(absolutePath).toLowerCase();
|
|
286954
|
+
if (IGNORED_EXTS.has(ext)) return;
|
|
286955
|
+
if (ext && !SOURCE_EXTS.has(ext)) return;
|
|
286956
|
+
if (!import_fs.default.existsSync(absolutePath)) return;
|
|
286957
|
+
try {
|
|
286958
|
+
if (import_fs.default.statSync(absolutePath).isDirectory()) return;
|
|
286959
|
+
} catch {
|
|
286960
|
+
return;
|
|
286961
|
+
}
|
|
286962
|
+
const relPath = import_path.default.relative(this.projectRoot, absolutePath).replace(/\\/g, "/");
|
|
286963
|
+
const existing = this.debounceTimers.get(absolutePath);
|
|
286964
|
+
if (existing) clearTimeout(existing);
|
|
286965
|
+
this.debounceTimers.set(
|
|
286966
|
+
absolutePath,
|
|
286967
|
+
setTimeout(() => {
|
|
286968
|
+
this.debounceTimers.delete(absolutePath);
|
|
286969
|
+
const data = this.readChanges();
|
|
286970
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
286971
|
+
const idx = data.changes.findIndex((c3) => c3.filePath === relPath);
|
|
286972
|
+
const entry = { filePath: relPath, absolutePath, changedAt: now, eventType };
|
|
286973
|
+
if (idx >= 0) data.changes[idx] = entry;
|
|
286974
|
+
else data.changes.push(entry);
|
|
286975
|
+
this.saveChanges(data);
|
|
286976
|
+
}, this.DEBOUNCE_MS)
|
|
286977
|
+
);
|
|
286978
|
+
}
|
|
286979
|
+
watchDir(dir) {
|
|
286980
|
+
try {
|
|
286981
|
+
const watcher = import_fs.default.watch(dir, { recursive: false }, (eventType, filename) => {
|
|
286982
|
+
if (!filename) return;
|
|
286983
|
+
const absPath = import_path.default.join(dir, filename);
|
|
286984
|
+
try {
|
|
286985
|
+
if (import_fs.default.existsSync(absPath) && import_fs.default.statSync(absPath).isDirectory()) {
|
|
286986
|
+
const basename = import_path.default.basename(absPath);
|
|
286987
|
+
if (!IGNORED_DIRS.has(basename)) this.watchDir(absPath);
|
|
286988
|
+
return;
|
|
286989
|
+
}
|
|
286990
|
+
} catch {
|
|
286991
|
+
return;
|
|
286992
|
+
}
|
|
286993
|
+
this.recordChange(absPath, eventType);
|
|
286994
|
+
});
|
|
286995
|
+
this.watchers.push(watcher);
|
|
286996
|
+
} catch {
|
|
286997
|
+
}
|
|
286998
|
+
}
|
|
286999
|
+
/** Recursively enumerate all subdirectories to watch, respecting ignores. */
|
|
287000
|
+
collectDirs(root) {
|
|
287001
|
+
const dirs = [root];
|
|
287002
|
+
try {
|
|
287003
|
+
for (const entry of import_fs.default.readdirSync(root, { withFileTypes: true })) {
|
|
287004
|
+
if (!entry.isDirectory()) continue;
|
|
287005
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
287006
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
287007
|
+
dirs.push(...this.collectDirs(import_path.default.join(root, entry.name)));
|
|
287008
|
+
}
|
|
287009
|
+
} catch {
|
|
287010
|
+
}
|
|
287011
|
+
return dirs;
|
|
287012
|
+
}
|
|
287013
|
+
start() {
|
|
287014
|
+
process.stderr.write(`[jetic] ChangeScanner watching ${this.projectRoot}
|
|
287015
|
+
`);
|
|
287016
|
+
if (!import_fs.default.existsSync(this.changesPath)) {
|
|
287017
|
+
this.saveChanges({
|
|
287018
|
+
version: 1,
|
|
287019
|
+
watchedSince: (/* @__PURE__ */ new Date()).toISOString(),
|
|
287020
|
+
projectRoot: this.projectRoot,
|
|
287021
|
+
changes: []
|
|
287022
|
+
});
|
|
287023
|
+
}
|
|
287024
|
+
const dirs = this.collectDirs(this.projectRoot);
|
|
287025
|
+
for (const dir of dirs) this.watchDir(dir);
|
|
287026
|
+
try {
|
|
287027
|
+
if (import_fs.default.existsSync(this.changesPath)) {
|
|
287028
|
+
const fileWatcher = import_fs.default.watch(this.changesPath, () => {
|
|
287029
|
+
const data = this.readChanges();
|
|
287030
|
+
this.notifyListeners(data);
|
|
287031
|
+
});
|
|
287032
|
+
this.watchers.push(fileWatcher);
|
|
287033
|
+
}
|
|
287034
|
+
} catch {
|
|
287035
|
+
}
|
|
287036
|
+
}
|
|
287037
|
+
stop() {
|
|
287038
|
+
for (const w of this.watchers) try {
|
|
287039
|
+
w.close();
|
|
287040
|
+
} catch {
|
|
287041
|
+
}
|
|
287042
|
+
this.watchers = [];
|
|
287043
|
+
}
|
|
287044
|
+
getChanges() {
|
|
287045
|
+
return this.readChanges();
|
|
287046
|
+
}
|
|
287047
|
+
clearChanges() {
|
|
287048
|
+
const data = this.readChanges();
|
|
287049
|
+
data.changes = [];
|
|
287050
|
+
data.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
287051
|
+
this.saveChanges(data);
|
|
287052
|
+
}
|
|
287053
|
+
};
|
|
286336
287054
|
var devCommand = new import_commander8.Command("dev").description("Start the Jetic local dashboard").option("-p, --port <number>", "Port to run the dashboard on", "8787").action(async (options) => {
|
|
286337
287055
|
const port = parseInt(options.port, 10);
|
|
286338
287056
|
const app = (0, import_express.default)();
|
|
287057
|
+
const projectRoot = process.cwd();
|
|
287058
|
+
const jeticDir = import_path.default.join(projectRoot, ".jetic");
|
|
287059
|
+
if (!import_fs.default.existsSync(jeticDir)) import_fs.default.mkdirSync(jeticDir, { recursive: true });
|
|
287060
|
+
const changeScanner = new ChangeScanner(projectRoot, jeticDir);
|
|
287061
|
+
changeScanner.start();
|
|
287062
|
+
process.on("SIGINT", () => {
|
|
287063
|
+
changeScanner.stop();
|
|
287064
|
+
process.exit(0);
|
|
287065
|
+
});
|
|
287066
|
+
process.on("SIGTERM", () => {
|
|
287067
|
+
changeScanner.stop();
|
|
287068
|
+
process.exit(0);
|
|
287069
|
+
});
|
|
286339
287070
|
app.use(import_express.default.json());
|
|
286340
287071
|
app.use((_req, res, next) => {
|
|
286341
287072
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -286368,14 +287099,14 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286368
287099
|
}
|
|
286369
287100
|
});
|
|
286370
287101
|
app.post("/api/model/endpoint", (req, res) => {
|
|
286371
|
-
const
|
|
286372
|
-
const modelPath = import_path.default.join(
|
|
287102
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287103
|
+
const modelPath = import_path.default.join(jeticDir2, "model.json");
|
|
286373
287104
|
try {
|
|
286374
287105
|
const ep = req.body;
|
|
286375
287106
|
if (!ep.method || !ep.path) {
|
|
286376
287107
|
return res.status(400).json({ error: "method and path are required" });
|
|
286377
287108
|
}
|
|
286378
|
-
if (!import_fs.default.existsSync(
|
|
287109
|
+
if (!import_fs.default.existsSync(jeticDir2)) import_fs.default.mkdirSync(jeticDir2, { recursive: true });
|
|
286379
287110
|
let model;
|
|
286380
287111
|
if (import_fs.default.existsSync(modelPath)) {
|
|
286381
287112
|
model = JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
|
|
@@ -286414,11 +287145,49 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286414
287145
|
const model = await scanner.scan();
|
|
286415
287146
|
const modelPath = import_path.default.join(config.jeticDir, "model.json");
|
|
286416
287147
|
writeJsonSync2(modelPath, model);
|
|
287148
|
+
changeScanner.clearChanges();
|
|
286417
287149
|
res.json({ ok: true, endpointCount: model.endpoints.length, model });
|
|
286418
287150
|
} catch (err) {
|
|
286419
287151
|
res.status(500).json({ error: err.message });
|
|
286420
287152
|
}
|
|
286421
287153
|
});
|
|
287154
|
+
app.get("/api/changes", (_req, res) => {
|
|
287155
|
+
try {
|
|
287156
|
+
res.json(changeScanner.getChanges());
|
|
287157
|
+
} catch (err) {
|
|
287158
|
+
res.status(500).json({ error: err.message });
|
|
287159
|
+
}
|
|
287160
|
+
});
|
|
287161
|
+
app.get("/api/changes/stream", (req, res) => {
|
|
287162
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
287163
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
287164
|
+
res.setHeader("Connection", "keep-alive");
|
|
287165
|
+
if (res.flushHeaders) res.flushHeaders();
|
|
287166
|
+
const initial = changeScanner.getChanges();
|
|
287167
|
+
res.write(`data: ${JSON.stringify(initial)}
|
|
287168
|
+
|
|
287169
|
+
`);
|
|
287170
|
+
const unsubscribe = changeScanner.onChange((updatedData) => {
|
|
287171
|
+
res.write(`data: ${JSON.stringify(updatedData)}
|
|
287172
|
+
|
|
287173
|
+
`);
|
|
287174
|
+
});
|
|
287175
|
+
const heartbeat = setInterval(() => {
|
|
287176
|
+
res.write(": heartbeat\n\n");
|
|
287177
|
+
}, 15e3);
|
|
287178
|
+
req.on("close", () => {
|
|
287179
|
+
unsubscribe();
|
|
287180
|
+
clearInterval(heartbeat);
|
|
287181
|
+
});
|
|
287182
|
+
});
|
|
287183
|
+
app.delete("/api/changes", (_req, res) => {
|
|
287184
|
+
try {
|
|
287185
|
+
changeScanner.clearChanges();
|
|
287186
|
+
res.json({ ok: true, cleared: true });
|
|
287187
|
+
} catch (err) {
|
|
287188
|
+
res.status(500).json({ error: err.message });
|
|
287189
|
+
}
|
|
287190
|
+
});
|
|
286422
287191
|
function readModel(modelPath) {
|
|
286423
287192
|
if (!import_fs.default.existsSync(modelPath)) return null;
|
|
286424
287193
|
return JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
|
|
@@ -286542,8 +287311,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286542
287311
|
}
|
|
286543
287312
|
});
|
|
286544
287313
|
app.get("/api/workflows", (_req, res) => {
|
|
286545
|
-
const
|
|
286546
|
-
const workflowsDir = import_path.default.join(
|
|
287314
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287315
|
+
const workflowsDir = import_path.default.join(jeticDir2, "workflows");
|
|
286547
287316
|
try {
|
|
286548
287317
|
const workflows = [];
|
|
286549
287318
|
if (import_fs.default.existsSync(workflowsDir)) {
|
|
@@ -286558,7 +287327,7 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286558
287327
|
}
|
|
286559
287328
|
}
|
|
286560
287329
|
}
|
|
286561
|
-
const legacyPath = import_path.default.join(
|
|
287330
|
+
const legacyPath = import_path.default.join(jeticDir2, "workflow.json");
|
|
286562
287331
|
if (import_fs.default.existsSync(legacyPath)) {
|
|
286563
287332
|
try {
|
|
286564
287333
|
const data = JSON.parse(import_fs.default.readFileSync(legacyPath, "utf8"));
|
|
@@ -286576,8 +287345,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286576
287345
|
app.post("/api/workflows", (req, res) => {
|
|
286577
287346
|
const { name } = req.body;
|
|
286578
287347
|
if (!name || !name.trim()) return res.status(400).json({ error: "name is required" });
|
|
286579
|
-
const
|
|
286580
|
-
const workflowsDir = import_path.default.join(
|
|
287348
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287349
|
+
const workflowsDir = import_path.default.join(jeticDir2, "workflows");
|
|
286581
287350
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
286582
287351
|
const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
|
|
286583
287352
|
if (import_fs.default.existsSync(filePath)) {
|
|
@@ -286598,11 +287367,11 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286598
287367
|
});
|
|
286599
287368
|
app.put("/api/workflows/:name", (req, res) => {
|
|
286600
287369
|
const slug = req.params.name;
|
|
286601
|
-
const
|
|
286602
|
-
const workflowsDir = import_path.default.join(
|
|
287370
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287371
|
+
const workflowsDir = import_path.default.join(jeticDir2, "workflows");
|
|
286603
287372
|
let filePath = import_path.default.join(workflowsDir, `${slug}.json`);
|
|
286604
287373
|
if (!import_fs.default.existsSync(filePath)) {
|
|
286605
|
-
const legacyPath = import_path.default.join(
|
|
287374
|
+
const legacyPath = import_path.default.join(jeticDir2, "workflow.json");
|
|
286606
287375
|
if (slug === "workflow" && import_fs.default.existsSync(legacyPath)) {
|
|
286607
287376
|
filePath = legacyPath;
|
|
286608
287377
|
} else {
|
|
@@ -286621,8 +287390,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286621
287390
|
});
|
|
286622
287391
|
app.delete("/api/workflows/:name", (req, res) => {
|
|
286623
287392
|
const slug = req.params.name;
|
|
286624
|
-
const
|
|
286625
|
-
const workflowsDir = import_path.default.join(
|
|
287393
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287394
|
+
const workflowsDir = import_path.default.join(jeticDir2, "workflows");
|
|
286626
287395
|
const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
|
|
286627
287396
|
if (!import_fs.default.existsSync(filePath)) {
|
|
286628
287397
|
return res.status(404).json({ error: `Workflow "${slug}" not found` });
|
|
@@ -286637,12 +287406,12 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
|
|
|
286637
287406
|
app.post("/api/workflows/generate", async (req, res) => {
|
|
286638
287407
|
const { goal, name } = req.body;
|
|
286639
287408
|
const workflowGoal = goal || name || "Full user journey";
|
|
286640
|
-
const
|
|
287409
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
286641
287410
|
const config = (0, import_core7.loadConfig)();
|
|
286642
287411
|
if (!config.ai) {
|
|
286643
287412
|
return res.status(400).json({ error: "AI is not configured. Run `jetic config ai` first." });
|
|
286644
287413
|
}
|
|
286645
|
-
const modelPath = import_path.default.join(
|
|
287414
|
+
const modelPath = import_path.default.join(jeticDir2, "model.json");
|
|
286646
287415
|
if (!import_fs.default.existsSync(modelPath)) {
|
|
286647
287416
|
return res.status(400).json({ error: "No model.json found. Run `jetic scan` first." });
|
|
286648
287417
|
}
|
|
@@ -286764,7 +287533,7 @@ Output valid JSON only.
|
|
|
286764
287533
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286765
287534
|
steps: object.steps
|
|
286766
287535
|
};
|
|
286767
|
-
const workflowsDir = import_path.default.join(
|
|
287536
|
+
const workflowsDir = import_path.default.join(jeticDir2, "workflows");
|
|
286768
287537
|
const slug = object.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "workflow";
|
|
286769
287538
|
const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
|
|
286770
287539
|
import_fs.default.mkdirSync(workflowsDir, { recursive: true });
|
|
@@ -286800,8 +287569,8 @@ Output valid JSON only.
|
|
|
286800
287569
|
if (!file) {
|
|
286801
287570
|
return res.status(400).json({ error: "file is required" });
|
|
286802
287571
|
}
|
|
286803
|
-
const
|
|
286804
|
-
const wfPath = import_path.default.isAbsolute(file) ? file : import_path.default.join(
|
|
287572
|
+
const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
|
|
287573
|
+
const wfPath = import_path.default.isAbsolute(file) ? file : import_path.default.join(jeticDir2, file);
|
|
286805
287574
|
if (!import_fs.default.existsSync(wfPath)) {
|
|
286806
287575
|
return res.status(404).json({ error: "workflow file not found" });
|
|
286807
287576
|
}
|
|
@@ -286824,7 +287593,7 @@ Output valid JSON only.
|
|
|
286824
287593
|
};
|
|
286825
287594
|
let baseUrl = req.body?.baseUrl;
|
|
286826
287595
|
if (!baseUrl) {
|
|
286827
|
-
const modelPath = import_path.default.join(
|
|
287596
|
+
const modelPath = import_path.default.join(jeticDir2, "model.json");
|
|
286828
287597
|
if (import_fs.default.existsSync(modelPath)) {
|
|
286829
287598
|
try {
|
|
286830
287599
|
const model = JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
|
|
@@ -287476,7 +288245,7 @@ var mcpCommand = new import_commander10.Command("mcp").description("Launch Model
|
|
|
287476
288245
|
|
|
287477
288246
|
// src/index.ts
|
|
287478
288247
|
var program = new import_commander11.Command();
|
|
287479
|
-
program.name("jetic").description("AI-Native API Behavior Testing").version("0.2.
|
|
288248
|
+
program.name("jetic").description("AI-Native API Behavior Testing").version("0.2.3");
|
|
287480
288249
|
program.addCommand(initCommand);
|
|
287481
288250
|
program.addCommand(scanCommand);
|
|
287482
288251
|
program.addCommand(inspectCommand);
|