@epilot/cli 0.1.108 → 0.1.110
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 +1 -1
- package/definitions/app.json +170 -38
- package/definitions/validation-rules.json +840 -37
- package/dist/{add-component-UPHG3VNG.js → add-component-TCF7V3KU.js} +1 -11
- package/dist/add-function-VRVBRNY3.js +184 -0
- package/dist/app-NATUF3YX.js +26 -0
- package/dist/bin/epilot.js +7 -7
- package/dist/{chunk-CZHW3BIT.js → chunk-QGAMGDQ5.js} +1 -1
- package/dist/{chunk-QOD77YLW.js → chunk-UDGF4AVJ.js} +263 -3
- package/dist/{completion-TFHA4W2J.js → completion-OMS4BH5U.js} +1 -1
- package/dist/{deploy-NRQHZ635.js → deploy-2U5FVEE7.js} +90 -9
- package/dist/dev-JEAHUYQU.js +105 -0
- package/dist/{export-ZRDJCALM.js → export-VLAY2KZP.js} +1 -1
- package/dist/{init-BXAGJAPS.js → init-C66L5GFR.js} +164 -4
- package/dist/{remove-component-LPTHVN4P.js → remove-component-7C7MPSBF.js} +1 -1
- package/dist/{review-OZTM3XBD.js → review-JZTMQNU3.js} +1 -1
- package/dist/{upgrade-HU256V6J.js → upgrade-KY6R7SUW.js} +1 -1
- package/dist/{validate-TLSOTJAY.js → validate-J35DJW3H.js} +9 -1
- package/dist/{versions-VA4H3EPK.js → versions-2TMTHO7W.js} +1 -1
- package/package.json +1 -1
- package/dist/app-RULTIGMJ.js +0 -24
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
log,
|
|
4
4
|
readManifest,
|
|
5
5
|
writeManifest
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-UDGF4AVJ.js";
|
|
7
7
|
import "./chunk-M3M3C5WH.js";
|
|
8
8
|
import "./chunk-YHQA2AVG.js";
|
|
9
9
|
import "./chunk-7ZQ666ZQ.js";
|
|
@@ -15,16 +15,6 @@ import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
|
|
|
15
15
|
import { resolve, join } from "path";
|
|
16
16
|
var TEMPLATES_REPO = "epilot-dev/app-templates";
|
|
17
17
|
var TEMPLATE_REGISTRY = {
|
|
18
|
-
CUSTOM_FLOW_ACTION_SANDBOX: {
|
|
19
|
-
configOnly: false,
|
|
20
|
-
manifestType: "CUSTOM_FLOW_ACTION",
|
|
21
|
-
templateDir: "custom-flow-action-sandbox",
|
|
22
|
-
description: "Sandboxed flow action (JS runs in epilot)",
|
|
23
|
-
configuration: () => ({
|
|
24
|
-
type: "sandbox",
|
|
25
|
-
sandbox_settings: { code: "// code is read from dist/ during deploy" }
|
|
26
|
-
})
|
|
27
|
-
},
|
|
28
18
|
CUSTOM_FLOW_ACTION_EXTERNAL: {
|
|
29
19
|
configOnly: true,
|
|
30
20
|
manifestType: "CUSTOM_FLOW_ACTION",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log,
|
|
4
|
+
readManifest,
|
|
5
|
+
validateScheduleExpression,
|
|
6
|
+
writeManifest
|
|
7
|
+
} from "./chunk-UDGF4AVJ.js";
|
|
8
|
+
import "./chunk-M3M3C5WH.js";
|
|
9
|
+
import "./chunk-YHQA2AVG.js";
|
|
10
|
+
import "./chunk-7ZQ666ZQ.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/app/add-function.ts
|
|
13
|
+
import { defineCommand } from "citty";
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
var NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
17
|
+
var add_function_default = defineCommand({
|
|
18
|
+
meta: {
|
|
19
|
+
name: "add-function",
|
|
20
|
+
description: "Add a server-side function (optionally scheduled) to the app"
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
name: { type: "positional", description: "Function name (kebab-case)", required: true },
|
|
24
|
+
type: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: '"workflow" (selectable as flow action) or "scheduled" (cron per installation). Default: scheduled when --schedule is given, workflow otherwise'
|
|
27
|
+
},
|
|
28
|
+
schedule: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: 'Cron ("0 3 * * *") or rate ("rate(30 minutes)") expression to run the function once per installation'
|
|
31
|
+
},
|
|
32
|
+
label: { type: "string", description: "Display name shown to org admins (e.g. in the flow builder)" },
|
|
33
|
+
timezone: { type: "string", description: "IANA timezone for cron schedules (default: Europe/Berlin)" },
|
|
34
|
+
path: { type: "string", description: "Path to manifest.json (default: manifest.json)" }
|
|
35
|
+
},
|
|
36
|
+
run: ({ args }) => {
|
|
37
|
+
const name = args.name;
|
|
38
|
+
if (!NAME_PATTERN.test(name)) {
|
|
39
|
+
log.error(`Invalid function name "${name}" \u2014 use kebab-case (a-z, 0-9, dashes; max 64 chars)`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const fnType = args.type ?? (args.schedule ? "scheduled" : "workflow");
|
|
43
|
+
if (fnType !== "workflow" && fnType !== "scheduled") {
|
|
44
|
+
log.error(`Invalid --type "${args.type}" \u2014 use "workflow" or "scheduled"`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (fnType === "scheduled" && !args.schedule) {
|
|
48
|
+
log.error("Scheduled functions require --schedule (cron or rate expression)");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (fnType === "workflow" && args.schedule) {
|
|
52
|
+
log.error("Workflow functions cannot have a schedule \u2014 drop --schedule or use --type scheduled");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (args.schedule) {
|
|
56
|
+
const result = validateScheduleExpression(args.schedule);
|
|
57
|
+
if (!result.valid) {
|
|
58
|
+
log.error(`Invalid schedule: ${result.error}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const manifestPath = resolve(args.path ?? "manifest.json");
|
|
63
|
+
const manifest = readManifest(manifestPath);
|
|
64
|
+
const rootDir = resolve(manifestPath, "..");
|
|
65
|
+
if (manifest.functions?.some((fn2) => fn2.name === name)) {
|
|
66
|
+
log.error(`Function "${name}" already exists in the manifest`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const fnDir = join(rootDir, "functions", name);
|
|
70
|
+
if (existsSync(fnDir)) {
|
|
71
|
+
log.error(`Directory already exists: ${fnDir}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
mkdirSync(join(fnDir, "src"), { recursive: true });
|
|
75
|
+
writeFileSync(
|
|
76
|
+
join(fnDir, "package.json"),
|
|
77
|
+
`${JSON.stringify(
|
|
78
|
+
{
|
|
79
|
+
name,
|
|
80
|
+
private: true,
|
|
81
|
+
version: "0.0.1",
|
|
82
|
+
type: "module",
|
|
83
|
+
scripts: { build: "tsc", dev: "tsx watch src/handler.ts" },
|
|
84
|
+
devDependencies: { typescript: "~5.8.3", tsx: "^4.0.0" }
|
|
85
|
+
},
|
|
86
|
+
null,
|
|
87
|
+
2
|
|
88
|
+
)}
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
writeFileSync(
|
|
92
|
+
join(fnDir, "tsconfig.json"),
|
|
93
|
+
`${JSON.stringify(
|
|
94
|
+
{
|
|
95
|
+
compilerOptions: {
|
|
96
|
+
target: "ES2022",
|
|
97
|
+
lib: ["ES2022", "DOM"],
|
|
98
|
+
module: "ESNext",
|
|
99
|
+
moduleResolution: "bundler",
|
|
100
|
+
strict: true,
|
|
101
|
+
rootDir: "src",
|
|
102
|
+
outDir: "dist",
|
|
103
|
+
esModuleInterop: true,
|
|
104
|
+
skipLibCheck: true
|
|
105
|
+
},
|
|
106
|
+
include: ["src"]
|
|
107
|
+
},
|
|
108
|
+
null,
|
|
109
|
+
2
|
|
110
|
+
)}
|
|
111
|
+
`
|
|
112
|
+
);
|
|
113
|
+
writeFileSync(join(fnDir, "src", "handler.ts"), handlerTemplate(name, fnType, args.schedule));
|
|
114
|
+
const fn = {
|
|
115
|
+
name,
|
|
116
|
+
type: fnType,
|
|
117
|
+
...args.label ? { label: { de: args.label } } : {},
|
|
118
|
+
handler: `./functions/${name}/dist/handler.js`,
|
|
119
|
+
...args.schedule ? { schedule: args.schedule } : {},
|
|
120
|
+
...args.timezone ? { schedule_timezone: args.timezone } : {}
|
|
121
|
+
};
|
|
122
|
+
manifest.functions = [...manifest.functions ?? [], fn];
|
|
123
|
+
writeManifest(manifestPath, manifest);
|
|
124
|
+
log.success(`Added ${fnType} function "${name}"`);
|
|
125
|
+
log.info(`Code: functions/${name}/src/handler.ts`);
|
|
126
|
+
if (args.schedule) {
|
|
127
|
+
log.info(`Schedule: ${args.schedule} (runs once per installation, max 60s per run)`);
|
|
128
|
+
} else {
|
|
129
|
+
log.info("Selectable as an action in the flow builder once the app is installed");
|
|
130
|
+
}
|
|
131
|
+
log.dim('Build with "npm run build", then "epilot app deploy"');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
var handlerTemplate = (name, fnType, schedule) => `// App function: ${name}
|
|
135
|
+
//${fnType === "scheduled" ? `
|
|
136
|
+
// Runs automatically ${schedule?.startsWith("rate(") ? `every ${schedule.slice(5, -1)}` : `on "${schedule}"`} \u2014 once per installation,
|
|
137
|
+
// inside the epilot code-execution sandbox with a 60 second budget. Do a
|
|
138
|
+
// bounded amount of work per run; the next tick picks up the rest.` : `
|
|
139
|
+
// Selectable as an action in the flow builder. Runs inside the epilot
|
|
140
|
+
// code-execution sandbox with the triggering entity in \`input.entity\`.`}
|
|
141
|
+
//
|
|
142
|
+
// Runtime contract:
|
|
143
|
+
// - top-level \`async function handler(input, context)\` \u2014 no \`export\`.
|
|
144
|
+
// - \`input.trigger\` \u2192 { type: '${fnType === "scheduled" ? "schedule" : "workflow"}', ... }.${fnType === "workflow" ? `
|
|
145
|
+
// - \`input.entity\` \u2192 the entity the flow ran on.
|
|
146
|
+
// - \`input.action_config\` \u2192 the action's configuration from the flow builder.` : `
|
|
147
|
+
// - \`input.org_id\` / \`input.app_id\` \u2192 the installation this run belongs to.`}
|
|
148
|
+
// - \`context.epilot\` \u2192 the bundled @epilot/sdk, pre-authorized with the app token
|
|
149
|
+
// (scoped to the app's permissions): \`await context.epilot.entity.searchEntities(...)\`.
|
|
150
|
+
// - \`context.fetch\` \u2192 plain fetch, e.g. for the app's API proxy or non-prod stages
|
|
151
|
+
// (derive base URLs from \`input.app_options.stage\`).
|
|
152
|
+
// - \`input.app_options.token\` \u2192 the raw app token, if you need it for fetch calls.
|
|
153
|
+
// - Return \`{ skip_reason }\` to skip, \`{ error_reason }\` to fail the run.
|
|
154
|
+
|
|
155
|
+
interface FunctionInput {
|
|
156
|
+
trigger?: { type: string; schedule?: string; scheduled_time?: string };
|
|
157
|
+
org_id?: string;
|
|
158
|
+
app_id?: string;${fnType === "workflow" ? `
|
|
159
|
+
entity?: Record<string, unknown>;
|
|
160
|
+
action_config?: Record<string, unknown>;` : ""}
|
|
161
|
+
app_options?: { token?: string; stage?: string } & Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface FunctionContext {
|
|
165
|
+
/** Bundled @epilot/sdk, pre-authorized \u2014 e.g. context.epilot.entity.searchEntities(...) */
|
|
166
|
+
epilot: Record<string, any>;
|
|
167
|
+
fetch: typeof fetch;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function handler(input: FunctionInput, context: FunctionContext) {
|
|
171
|
+
const token = input.app_options?.token;
|
|
172
|
+
if (!token) {
|
|
173
|
+
return { error_reason: 'Missing app token in execution context.' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// TODO: implement your logic here
|
|
177
|
+
console.log('${name} run', { trigger: input.trigger?.type, org: input.org_id });
|
|
178
|
+
|
|
179
|
+
return { success: true };
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
export {
|
|
183
|
+
add_function_default as default
|
|
184
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/app/index.ts
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
var app_default = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "app",
|
|
8
|
+
description: "Manage epilot Apps \u2014 create, deploy, and manage app manifests"
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
init: () => import("./init-C66L5GFR.js").then((m) => m.default),
|
|
12
|
+
"add-component": () => import("./add-component-TCF7V3KU.js").then((m) => m.default),
|
|
13
|
+
"remove-component": () => import("./remove-component-7C7MPSBF.js").then((m) => m.default),
|
|
14
|
+
"add-function": () => import("./add-function-VRVBRNY3.js").then((m) => m.default),
|
|
15
|
+
validate: () => import("./validate-J35DJW3H.js").then((m) => m.default),
|
|
16
|
+
deploy: () => import("./deploy-2U5FVEE7.js").then((m) => m.default),
|
|
17
|
+
dev: () => import("./dev-JEAHUYQU.js").then((m) => m.default),
|
|
18
|
+
export: () => import("./export-VLAY2KZP.js").then((m) => m.default),
|
|
19
|
+
versions: () => import("./versions-2TMTHO7W.js").then((m) => m.default),
|
|
20
|
+
review: () => import("./review-JZTMQNU3.js").then((m) => m.default),
|
|
21
|
+
api: () => import("./api-5W2UMWCW.js").then((m) => m.default)
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
export {
|
|
25
|
+
app_default as default
|
|
26
|
+
};
|
package/dist/bin/epilot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
API_LIST
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QGAMGDQ5.js";
|
|
5
5
|
|
|
6
6
|
// bin/epilot.ts
|
|
7
7
|
import { runMain } from "citty";
|
|
@@ -11,7 +11,7 @@ import { defineCommand } from "citty";
|
|
|
11
11
|
var main = defineCommand({
|
|
12
12
|
meta: {
|
|
13
13
|
name: "epilot",
|
|
14
|
-
version: "0.1.
|
|
14
|
+
version: "0.1.110",
|
|
15
15
|
description: "CLI for epilot APIs"
|
|
16
16
|
},
|
|
17
17
|
args: {
|
|
@@ -30,13 +30,13 @@ var main = defineCommand({
|
|
|
30
30
|
auth: () => import("../auth-WMXFMPWE.js").then((m) => m.default),
|
|
31
31
|
profile: () => import("../profile-OZJL5ZPT.js").then((m) => m.default),
|
|
32
32
|
config: () => import("../config-DGZIMLZK.js").then((m) => m.default),
|
|
33
|
-
completion: () => import("../completion-
|
|
34
|
-
upgrade: () => import("../upgrade-
|
|
33
|
+
completion: () => import("../completion-OMS4BH5U.js").then((m) => m.default),
|
|
34
|
+
upgrade: () => import("../upgrade-KY6R7SUW.js").then((m) => m.default),
|
|
35
35
|
"access-token": () => import("../access-token-WWE6BDJH.js").then((m) => m.default),
|
|
36
36
|
address: () => import("../address-EH3C4CVB.js").then((m) => m.default),
|
|
37
37
|
"address-suggestions": () => import("../address-suggestions-RRSLOBFW.js").then((m) => m.default),
|
|
38
38
|
"ai-agents": () => import("../ai-agents-53M2KHPI.js").then((m) => m.default),
|
|
39
|
-
app: () => import("../app-
|
|
39
|
+
app: () => import("../app-NATUF3YX.js").then((m) => m.default),
|
|
40
40
|
"audit-logs": () => import("../audit-logs-YFRK3EFU.js").then((m) => m.default),
|
|
41
41
|
automation: () => import("../automation-4DEE3TUI.js").then((m) => m.default),
|
|
42
42
|
billing: () => import("../billing-XX4VVOPI.js").then((m) => m.default),
|
|
@@ -134,13 +134,13 @@ process.stderr.on("error", (err) => {
|
|
|
134
134
|
if (err.code === "EPIPE") process.exit(0);
|
|
135
135
|
throw err;
|
|
136
136
|
});
|
|
137
|
-
var VERSION = true ? "0.1.
|
|
137
|
+
var VERSION = true ? "0.1.110" : (await null).default.version;
|
|
138
138
|
var reorderedArgv = hoistFlagsAfterSubcommand(process.argv.slice(2));
|
|
139
139
|
process.argv = [process.argv[0], process.argv[1], ...reorderedArgv];
|
|
140
140
|
var args = process.argv.slice(2);
|
|
141
141
|
var completionsIdx = args.indexOf("--_completions");
|
|
142
142
|
if (completionsIdx >= 0) {
|
|
143
|
-
const { handleCompletions } = await import("../completion-
|
|
143
|
+
const { handleCompletions } = await import("../completion-OMS4BH5U.js");
|
|
144
144
|
handleCompletions(args[completionsIdx + 1], args[completionsIdx + 2]);
|
|
145
145
|
process.exit(0);
|
|
146
146
|
}
|
|
@@ -1427,7 +1427,7 @@ var API_LIST = [
|
|
|
1427
1427
|
apiName: "validationRules",
|
|
1428
1428
|
kebabName: "validation-rules",
|
|
1429
1429
|
title: "Validation Rules API",
|
|
1430
|
-
serverUrl: "
|
|
1430
|
+
serverUrl: "",
|
|
1431
1431
|
operationCount: 7,
|
|
1432
1432
|
operationIds: [
|
|
1433
1433
|
"getValidationRules",
|
|
@@ -15,6 +15,179 @@ import {
|
|
|
15
15
|
YELLOW
|
|
16
16
|
} from "./chunk-7ZQ666ZQ.js";
|
|
17
17
|
|
|
18
|
+
// src/commands/app/schedule.ts
|
|
19
|
+
var MIN_SCHEDULE_INTERVAL_MINUTES = 15;
|
|
20
|
+
var RATE_PATTERN = /^rate\((\d+)\s+(minute|minutes|hour|hours|day|days)\)$/;
|
|
21
|
+
function validateScheduleExpression(expression, minIntervalMinutes = MIN_SCHEDULE_INTERVAL_MINUTES) {
|
|
22
|
+
const expr = expression.trim();
|
|
23
|
+
const rate = expr.match(RATE_PATTERN);
|
|
24
|
+
if (rate) {
|
|
25
|
+
const value = Number(rate[1]);
|
|
26
|
+
const unit = rate[2];
|
|
27
|
+
if (value < 1) {
|
|
28
|
+
return { valid: false, error: "rate() value must be at least 1" };
|
|
29
|
+
}
|
|
30
|
+
const minutes = unit.startsWith("minute") ? value : unit.startsWith("hour") ? value * 60 : value * 24 * 60;
|
|
31
|
+
if (minutes < minIntervalMinutes) {
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
error: `Schedule fires every ${minutes} minute(s) \u2014 the minimum interval is ${minIntervalMinutes} minutes`,
|
|
35
|
+
minIntervalMinutes: minutes
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { valid: true, minIntervalMinutes: minutes };
|
|
39
|
+
}
|
|
40
|
+
if (expr.startsWith("rate(")) {
|
|
41
|
+
return { valid: false, error: 'Invalid rate expression \u2014 expected e.g. "rate(30 minutes)"' };
|
|
42
|
+
}
|
|
43
|
+
let cron;
|
|
44
|
+
try {
|
|
45
|
+
cron = parseCron(expr);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { valid: false, error: err.message };
|
|
48
|
+
}
|
|
49
|
+
if (cron.domRestricted && cron.dowRestricted) {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
error: "Restricting both day-of-month and day-of-week is not supported \u2014 set one of them to *"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const gap = minFireGapMinutes(cron, minIntervalMinutes);
|
|
56
|
+
if (gap === null) {
|
|
57
|
+
return { valid: false, error: "Schedule never fires" };
|
|
58
|
+
}
|
|
59
|
+
if (gap < minIntervalMinutes) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
error: `Schedule fires ${gap} minute(s) apart \u2014 the minimum interval is ${minIntervalMinutes} minutes`,
|
|
63
|
+
minIntervalMinutes: gap
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { valid: true, minIntervalMinutes: gap };
|
|
67
|
+
}
|
|
68
|
+
var FIELD_RANGES = [
|
|
69
|
+
["minute", 0, 59],
|
|
70
|
+
["hour", 0, 23],
|
|
71
|
+
["day of month", 1, 31],
|
|
72
|
+
["month", 1, 12],
|
|
73
|
+
["day of week", 0, 7]
|
|
74
|
+
];
|
|
75
|
+
var MONTH_NAMES = {
|
|
76
|
+
jan: 1,
|
|
77
|
+
feb: 2,
|
|
78
|
+
mar: 3,
|
|
79
|
+
apr: 4,
|
|
80
|
+
may: 5,
|
|
81
|
+
jun: 6,
|
|
82
|
+
jul: 7,
|
|
83
|
+
aug: 8,
|
|
84
|
+
sep: 9,
|
|
85
|
+
oct: 10,
|
|
86
|
+
nov: 11,
|
|
87
|
+
dec: 12
|
|
88
|
+
};
|
|
89
|
+
var DOW_NAMES = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
90
|
+
function parseCron(expr) {
|
|
91
|
+
const fields = expr.split(/\s+/);
|
|
92
|
+
if (fields.length !== 5) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid cron expression "${expr}" \u2014 expected 5 fields (minute hour day-of-month month day-of-week)`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const sets = fields.map((field, i) => {
|
|
98
|
+
const [label, min, max] = FIELD_RANGES[i];
|
|
99
|
+
const names = i === 3 ? MONTH_NAMES : i === 4 ? DOW_NAMES : void 0;
|
|
100
|
+
return parseField(field, label, min, max, names);
|
|
101
|
+
});
|
|
102
|
+
const dayOfWeek = sets[4];
|
|
103
|
+
if (dayOfWeek.has(7)) {
|
|
104
|
+
dayOfWeek.delete(7);
|
|
105
|
+
dayOfWeek.add(0);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
minute: sets[0],
|
|
109
|
+
hour: sets[1],
|
|
110
|
+
dayOfMonth: sets[2],
|
|
111
|
+
month: sets[3],
|
|
112
|
+
dayOfWeek,
|
|
113
|
+
domRestricted: fields[2] !== "*",
|
|
114
|
+
dowRestricted: fields[4] !== "*"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function parseField(field, label, min, max, names) {
|
|
118
|
+
const values = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const part of field.split(",")) {
|
|
120
|
+
const [rangePart, stepPart, ...rest] = part.split("/");
|
|
121
|
+
if (rest.length > 0 || stepPart === "") {
|
|
122
|
+
throw new Error(`Invalid ${label} field "${field}"`);
|
|
123
|
+
}
|
|
124
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
125
|
+
if (!Number.isInteger(step) || step < 1) {
|
|
126
|
+
throw new Error(`Invalid step in ${label} field "${field}"`);
|
|
127
|
+
}
|
|
128
|
+
let lo;
|
|
129
|
+
let hi;
|
|
130
|
+
if (rangePart === "*") {
|
|
131
|
+
lo = min;
|
|
132
|
+
hi = max;
|
|
133
|
+
} else if (rangePart.includes("-")) {
|
|
134
|
+
const [a, b] = rangePart.split("-");
|
|
135
|
+
lo = parseValue(a, label, names);
|
|
136
|
+
hi = parseValue(b, label, names);
|
|
137
|
+
if (lo > hi) throw new Error(`Invalid range in ${label} field "${field}"`);
|
|
138
|
+
} else {
|
|
139
|
+
lo = parseValue(rangePart, label, names);
|
|
140
|
+
hi = stepPart === void 0 ? lo : max;
|
|
141
|
+
}
|
|
142
|
+
if (lo < min || hi > max) {
|
|
143
|
+
throw new Error(`Value out of range in ${label} field "${field}" (allowed ${min}-${max})`);
|
|
144
|
+
}
|
|
145
|
+
for (let v = lo; v <= hi; v += step) values.add(v);
|
|
146
|
+
}
|
|
147
|
+
if (values.size === 0) throw new Error(`Empty ${label} field "${field}"`);
|
|
148
|
+
return values;
|
|
149
|
+
}
|
|
150
|
+
function parseValue(raw, label, names) {
|
|
151
|
+
if (names) {
|
|
152
|
+
const named = names[raw.toLowerCase()];
|
|
153
|
+
if (named !== void 0) return named;
|
|
154
|
+
}
|
|
155
|
+
const value = Number(raw);
|
|
156
|
+
if (!Number.isInteger(value)) {
|
|
157
|
+
throw new Error(`Invalid value "${raw}" in ${label} field`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function minFireGapMinutes(cron, threshold) {
|
|
162
|
+
const start = Date.UTC(2024, 0, 1);
|
|
163
|
+
const totalMinutes = (366 + 60) * 24 * 60;
|
|
164
|
+
let previous = null;
|
|
165
|
+
let minGap = Number.POSITIVE_INFINITY;
|
|
166
|
+
for (let m = 0; m < totalMinutes; m++) {
|
|
167
|
+
const date = new Date(start + m * 6e4);
|
|
168
|
+
if (!matches(cron, date)) continue;
|
|
169
|
+
if (previous !== null) {
|
|
170
|
+
const gap = m - previous;
|
|
171
|
+
if (gap < minGap) minGap = gap;
|
|
172
|
+
if (minGap < threshold) return minGap;
|
|
173
|
+
}
|
|
174
|
+
previous = m;
|
|
175
|
+
}
|
|
176
|
+
if (previous === null) return null;
|
|
177
|
+
return minGap === Number.POSITIVE_INFINITY ? 366 * 24 * 60 : minGap;
|
|
178
|
+
}
|
|
179
|
+
function matches(cron, date) {
|
|
180
|
+
if (!cron.minute.has(date.getUTCMinutes())) return false;
|
|
181
|
+
if (!cron.hour.has(date.getUTCHours())) return false;
|
|
182
|
+
if (!cron.month.has(date.getUTCMonth() + 1)) return false;
|
|
183
|
+
const domMatch = cron.dayOfMonth.has(date.getUTCDate());
|
|
184
|
+
const dowMatch = cron.dayOfWeek.has(date.getUTCDay());
|
|
185
|
+
if (cron.domRestricted && cron.dowRestricted) return domMatch || dowMatch;
|
|
186
|
+
if (cron.domRestricted) return domMatch;
|
|
187
|
+
if (cron.dowRestricted) return dowMatch;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
18
191
|
// src/commands/app/manifest.ts
|
|
19
192
|
import { readFileSync, writeFileSync, existsSync, statSync } from "fs";
|
|
20
193
|
import { resolve, extname } from "path";
|
|
@@ -78,9 +251,72 @@ function validateManifest(data) {
|
|
|
78
251
|
}
|
|
79
252
|
}
|
|
80
253
|
}
|
|
254
|
+
if (obj.functions !== void 0) {
|
|
255
|
+
if (!Array.isArray(obj.functions)) {
|
|
256
|
+
errors.push({ path: "/functions", message: "Must be an array" });
|
|
257
|
+
} else {
|
|
258
|
+
validateFunctions(obj.functions, errors);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
81
261
|
if (errors.length > 0) return { valid: false, errors };
|
|
82
262
|
return { valid: true, errors: [], manifest: data };
|
|
83
263
|
}
|
|
264
|
+
var FUNCTION_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
265
|
+
var MAX_FUNCTIONS = 10;
|
|
266
|
+
var MAX_SCHEDULED_FUNCTIONS = 5;
|
|
267
|
+
function validateFunctions(functions, errors) {
|
|
268
|
+
if (functions.length > MAX_FUNCTIONS) {
|
|
269
|
+
errors.push({ path: "/functions", message: `At most ${MAX_FUNCTIONS} functions per app` });
|
|
270
|
+
}
|
|
271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
272
|
+
let scheduled = 0;
|
|
273
|
+
for (let i = 0; i < functions.length; i++) {
|
|
274
|
+
const fn = functions[i];
|
|
275
|
+
const path = `/functions/${i}`;
|
|
276
|
+
if (fn.type !== "workflow" && fn.type !== "scheduled") {
|
|
277
|
+
errors.push({ path: `${path}/type`, message: 'Required: "workflow" or "scheduled"' });
|
|
278
|
+
}
|
|
279
|
+
if (fn.type === "scheduled" && fn.schedule === void 0) {
|
|
280
|
+
errors.push({ path: `${path}/schedule`, message: "Scheduled functions require a schedule expression" });
|
|
281
|
+
}
|
|
282
|
+
if (fn.type !== "scheduled" && fn.schedule !== void 0) {
|
|
283
|
+
errors.push({ path: `${path}/schedule`, message: 'Only functions of type "scheduled" may declare a schedule' });
|
|
284
|
+
}
|
|
285
|
+
if (fn.type !== "workflow" && fn.wait_for_callback !== void 0) {
|
|
286
|
+
errors.push({ path: `${path}/wait_for_callback`, message: "Only valid for workflow functions" });
|
|
287
|
+
}
|
|
288
|
+
if (typeof fn.name !== "string" || !FUNCTION_NAME_PATTERN.test(fn.name)) {
|
|
289
|
+
errors.push({ path: `${path}/name`, message: "Required kebab-case string (a-z, 0-9, dashes; max 64 chars)" });
|
|
290
|
+
} else if (seen.has(fn.name)) {
|
|
291
|
+
errors.push({ path: `${path}/name`, message: `Duplicate function name "${fn.name}"` });
|
|
292
|
+
} else {
|
|
293
|
+
seen.add(fn.name);
|
|
294
|
+
}
|
|
295
|
+
if (typeof fn.handler !== "string" || fn.handler.length === 0) {
|
|
296
|
+
errors.push({ path: `${path}/handler`, message: "Required path to the bundled handler JS" });
|
|
297
|
+
}
|
|
298
|
+
if (fn.schedule !== void 0) {
|
|
299
|
+
scheduled++;
|
|
300
|
+
if (typeof fn.schedule !== "string") {
|
|
301
|
+
errors.push({ path: `${path}/schedule`, message: "Must be a cron or rate() expression string" });
|
|
302
|
+
} else {
|
|
303
|
+
const result = validateScheduleExpression(fn.schedule);
|
|
304
|
+
if (!result.valid) {
|
|
305
|
+
errors.push({ path: `${path}/schedule`, message: result.error ?? "Invalid schedule expression" });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (fn.schedule_overlap !== void 0 && fn.schedule_overlap !== "skip") {
|
|
310
|
+
errors.push({ path: `${path}/schedule_overlap`, message: 'Only "skip" is supported' });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (scheduled > MAX_SCHEDULED_FUNCTIONS) {
|
|
314
|
+
errors.push({
|
|
315
|
+
path: "/functions",
|
|
316
|
+
message: `At most ${MAX_SCHEDULED_FUNCTIONS} scheduled functions per app (schedules run once per installation)`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
84
320
|
function readManifest(path) {
|
|
85
321
|
const absPath = resolve(path);
|
|
86
322
|
if (!existsSync(absPath)) {
|
|
@@ -101,6 +337,15 @@ function writeManifest(path, manifest) {
|
|
|
101
337
|
`, "utf-8");
|
|
102
338
|
}
|
|
103
339
|
var DEFAULT_BASE_URL = "https://app.sls.epilot.io";
|
|
340
|
+
function bumpPatchVersion(version) {
|
|
341
|
+
const parts = version.split(".");
|
|
342
|
+
const patch = Number.parseInt(parts[parts.length - 1], 10);
|
|
343
|
+
if (Number.isNaN(patch)) {
|
|
344
|
+
throw new Error(`Cannot derive next version from "${version}" \u2014 pass an explicit target version`);
|
|
345
|
+
}
|
|
346
|
+
parts[parts.length - 1] = String(patch + 1);
|
|
347
|
+
return parts.join(".");
|
|
348
|
+
}
|
|
104
349
|
async function request(baseUrl, token, method, path, body) {
|
|
105
350
|
const url = `${baseUrl}${path}`;
|
|
106
351
|
const headers = {
|
|
@@ -203,13 +448,27 @@ function createAppApiClient(opts) {
|
|
|
203
448
|
});
|
|
204
449
|
return roleId;
|
|
205
450
|
},
|
|
206
|
-
async cloneVersion(appId,
|
|
207
|
-
|
|
451
|
+
async cloneVersion(appId, sourceVersion, targetVersion) {
|
|
452
|
+
const target = targetVersion ?? bumpPatchVersion(sourceVersion);
|
|
453
|
+
await request(
|
|
208
454
|
baseUrl,
|
|
209
455
|
getToken(),
|
|
210
456
|
"POST",
|
|
211
|
-
`/v1/app-configurations/${appId}/versions/${
|
|
457
|
+
`/v1/app-configurations/${appId}/versions/${sourceVersion}/clone-to/${target}`
|
|
212
458
|
);
|
|
459
|
+
return { version: target };
|
|
460
|
+
},
|
|
461
|
+
/** Returns the app's installation in the caller's org, or null if not installed. */
|
|
462
|
+
async getInstallation(appId) {
|
|
463
|
+
try {
|
|
464
|
+
return await request(baseUrl, getToken(), "GET", `/v1/app/${appId}`);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (err.message.includes("(404)")) return null;
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
async patchInstallation(appId, payload) {
|
|
471
|
+
return request(baseUrl, getToken(), "PATCH", `/v1/app/${appId}`, payload);
|
|
213
472
|
},
|
|
214
473
|
async upsertComponent(appId, version, component) {
|
|
215
474
|
const componentId = component.id;
|
|
@@ -407,6 +666,7 @@ function toManifest(config, version) {
|
|
|
407
666
|
}
|
|
408
667
|
|
|
409
668
|
export {
|
|
669
|
+
validateScheduleExpression,
|
|
410
670
|
log,
|
|
411
671
|
validateManifest,
|
|
412
672
|
readManifest,
|