@epilot/cli 0.1.108 → 0.1.109
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/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 +4 -4
- package/dist/{chunk-QOD77YLW.js → chunk-UDGF4AVJ.js} +263 -3
- 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-5RFPLKLG.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
|
@@ -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,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
uploadDirectoryAsZip,
|
|
9
9
|
uploadFileToPresignedUrl,
|
|
10
10
|
writeManifest
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UDGF4AVJ.js";
|
|
12
12
|
import "./chunk-M3M3C5WH.js";
|
|
13
13
|
import "./chunk-YHQA2AVG.js";
|
|
14
14
|
import "./chunk-7ZQ666ZQ.js";
|
|
@@ -105,15 +105,67 @@ var deploy_default = defineCommand({
|
|
|
105
105
|
log.warn(`Logo not found: ${logoPath}`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
let functionsPayload;
|
|
109
|
+
if (manifest.functions) {
|
|
110
|
+
functionsPayload = [];
|
|
111
|
+
for (const fn of manifest.functions) {
|
|
112
|
+
const handlerPath = resolve(manifestDir, fn.handler);
|
|
113
|
+
if (!existsSync(handlerPath)) {
|
|
114
|
+
log.error(`Handler not found for function "${fn.name}": ${handlerPath} \u2014 run "npm run build" first`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const { handler, assets: fnAssets, ...rest } = fn;
|
|
118
|
+
const payload = { ...rest, code: readFileSync(handlerPath, "utf-8") };
|
|
119
|
+
if (fnAssets?.zip) {
|
|
120
|
+
const zipPath = resolve(manifestDir, fnAssets.zip);
|
|
121
|
+
if (!existsSync(zipPath)) {
|
|
122
|
+
log.warn(`Config UI directory not found for function "${fn.name}": ${zipPath} \u2014 skipping surface`);
|
|
123
|
+
} else if (dryRun) {
|
|
124
|
+
log.info(`[dry-run] Would zip and upload config UI for function ${fn.name}`);
|
|
125
|
+
} else {
|
|
126
|
+
const { upload_url, artifact_url } = await client.createZipUploadUrl(
|
|
127
|
+
appId,
|
|
128
|
+
targetVersion,
|
|
129
|
+
`fn-${fn.name}`
|
|
130
|
+
);
|
|
131
|
+
const zipSize = await uploadDirectoryAsZip(upload_url, zipPath);
|
|
132
|
+
log.success(`Uploaded config UI for function ${fn.name} (${formatFileSize(zipSize)})`);
|
|
133
|
+
payload.surfaces = {
|
|
134
|
+
flow_action_config: {
|
|
135
|
+
app_url: artifact_url.replace(/\/[^/]+$/, "/index.html"),
|
|
136
|
+
zip_url: artifact_url
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
functionsPayload.push(payload);
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
log.info(
|
|
144
|
+
`[dry-run] Would deploy ${fn.type} function ${fn.name}${fn.schedule ? ` (schedule: ${fn.schedule})` : ""}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (manifest.permissions?.length || manifest.blueprint?.manifest_id || functionsPayload) {
|
|
109
150
|
if (dryRun) {
|
|
110
151
|
if (manifest.permissions?.length) {
|
|
111
|
-
log.info("[dry-run] Would upsert app role in developer org");
|
|
152
|
+
log.info("[dry-run] Would upsert app role in developer org (if grants changed)");
|
|
112
153
|
}
|
|
113
154
|
log.info("[dry-run] Would update version permissions/blueprint");
|
|
114
155
|
} else {
|
|
156
|
+
let grantsChanged = true;
|
|
157
|
+
if (!isNew && manifest.permissions?.length) {
|
|
158
|
+
try {
|
|
159
|
+
const remoteVersion = await client.getVersion(appId, targetVersion);
|
|
160
|
+
const remoteGrants = remoteVersion.role?.grants;
|
|
161
|
+
grantsChanged = normalizeGrants(remoteGrants) !== normalizeGrants(manifest.permissions);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
115
165
|
let roleId;
|
|
116
|
-
if (manifest.permissions?.length) {
|
|
166
|
+
if (manifest.permissions?.length && !grantsChanged) {
|
|
167
|
+
log.dim("Permissions unchanged \u2014 skipping grant re-provisioning");
|
|
168
|
+
} else if (manifest.permissions?.length) {
|
|
117
169
|
const orgId = resolveOrgId(args.token, args.profile);
|
|
118
170
|
if (orgId) {
|
|
119
171
|
try {
|
|
@@ -131,11 +183,17 @@ var deploy_default = defineCommand({
|
|
|
131
183
|
log.warn("Could not resolve org id \u2014 attaching grants without a developer-org role");
|
|
132
184
|
}
|
|
133
185
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
186
|
+
const sendGrants = Boolean(manifest.permissions?.length && grantsChanged);
|
|
187
|
+
if (sendGrants || manifest.blueprint?.manifest_id || functionsPayload) {
|
|
188
|
+
await client.patchVersion(appId, targetVersion, {
|
|
189
|
+
...sendGrants ? { grants: manifest.permissions, ...roleId ? { role_id: roleId } : {} } : {},
|
|
190
|
+
...manifest.blueprint?.manifest_id ? { manifest_id: manifest.blueprint.manifest_id } : {},
|
|
191
|
+
...functionsPayload ? { functions: functionsPayload } : {}
|
|
192
|
+
});
|
|
193
|
+
log.success(
|
|
194
|
+
`Updated version ${targetVersion} (permissions/blueprint${functionsPayload ? `, ${functionsPayload.length} function(s)` : ""})`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
139
197
|
}
|
|
140
198
|
}
|
|
141
199
|
for (const comp of manifest.components) {
|
|
@@ -222,6 +280,24 @@ var deploy_default = defineCommand({
|
|
|
222
280
|
}
|
|
223
281
|
}
|
|
224
282
|
}
|
|
283
|
+
if (!isNew) {
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
log.info("[dry-run] Would re-sync the installation in this org (if installed)");
|
|
286
|
+
} else {
|
|
287
|
+
try {
|
|
288
|
+
const installation = await client.getInstallation(appId);
|
|
289
|
+
if (installation) {
|
|
290
|
+
await client.patchInstallation(appId, { version: targetVersion });
|
|
291
|
+
log.success(`Re-synced installation in this org to v${targetVersion}`);
|
|
292
|
+
log.warn(
|
|
293
|
+
"Re-syncing disables the installation \u2014 open the app in epilot (Settings \u2192 Apps) and save its configuration to re-enable it."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
log.warn(`Could not re-sync installation: ${err.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
225
301
|
if (dryRun) {
|
|
226
302
|
log.header("Dry run complete. No changes were made.");
|
|
227
303
|
} else {
|
|
@@ -229,6 +305,11 @@ var deploy_default = defineCommand({
|
|
|
229
305
|
}
|
|
230
306
|
}
|
|
231
307
|
});
|
|
308
|
+
function normalizeGrants(grants = []) {
|
|
309
|
+
return JSON.stringify(
|
|
310
|
+
grants.map((g) => ({ action: g.action, resource: g.resource ?? null })).sort((a, b) => `${a.action}|${a.resource}`.localeCompare(`${b.action}|${b.resource}`))
|
|
311
|
+
);
|
|
312
|
+
}
|
|
232
313
|
export {
|
|
233
314
|
deploy_default as default
|
|
234
315
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createAppApiClient,
|
|
4
|
+
log,
|
|
5
|
+
readManifest
|
|
6
|
+
} from "./chunk-UDGF4AVJ.js";
|
|
7
|
+
import "./chunk-M3M3C5WH.js";
|
|
8
|
+
import "./chunk-YHQA2AVG.js";
|
|
9
|
+
import "./chunk-7ZQ666ZQ.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/app/dev.ts
|
|
12
|
+
import { defineCommand } from "citty";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
var dev_default = defineCommand({
|
|
15
|
+
meta: { name: "dev", description: "Serve a component from localhost inside epilot (dev mode)" },
|
|
16
|
+
args: {
|
|
17
|
+
path: { type: "positional", description: "Path to manifest.json", required: false },
|
|
18
|
+
component: { type: "string", alias: "c", description: "Component to override (folder name in components/)" },
|
|
19
|
+
url: { type: "string", alias: "u", description: "Local dev server URL (default: http://localhost:5173)" },
|
|
20
|
+
off: { type: "boolean", description: "Disable dev mode and remove the override" },
|
|
21
|
+
token: { type: "string", alias: "t", description: "Bearer token" },
|
|
22
|
+
server: { type: "string", alias: "s", description: "Override server base URL" },
|
|
23
|
+
profile: { type: "string", description: "Use a named profile" }
|
|
24
|
+
},
|
|
25
|
+
run: async ({ args }) => {
|
|
26
|
+
const manifestPath = resolve(args.path ?? "manifest.json");
|
|
27
|
+
const manifest = readManifest(manifestPath);
|
|
28
|
+
const client = createAppApiClient({ token: args.token, server: args.server, profile: args.profile });
|
|
29
|
+
if (!manifest.app_id) {
|
|
30
|
+
log.error("No app_id in manifest \u2014 deploy the app once before using dev mode.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const appId = manifest.app_id;
|
|
34
|
+
const overrideUrl = args.url ?? "http://localhost:5173";
|
|
35
|
+
const overridable = manifest.components.filter(
|
|
36
|
+
(c) => c.component_type === "CUSTOM_JOURNEY_BLOCK" || c.surfaces && Object.keys(c.surfaces).length > 0
|
|
37
|
+
);
|
|
38
|
+
let localComponent = args.component ? manifest.components.find((c) => c._dir === args.component) : void 0;
|
|
39
|
+
if (args.component && !localComponent) {
|
|
40
|
+
log.error(`Component "${args.component}" not found in manifest (expected its folder name in components/).`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (!localComponent) {
|
|
44
|
+
if (overridable.length === 1) {
|
|
45
|
+
localComponent = overridable[0];
|
|
46
|
+
} else if (overridable.length === 0) {
|
|
47
|
+
log.error(
|
|
48
|
+
"No overridable component found \u2014 dev mode works for UI components (capabilities, pages, portal blocks, journey blocks)."
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} else {
|
|
52
|
+
log.error(
|
|
53
|
+
`Multiple UI components found \u2014 pick one with --component <name>: ${overridable.map((c) => c._dir).filter(Boolean).join(", ")}`
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const config = await client.getConfiguration(appId);
|
|
59
|
+
const version = config.latest_version;
|
|
60
|
+
const remoteVersion = await client.getVersion(appId, version);
|
|
61
|
+
const remoteComponents = remoteVersion.components ?? [];
|
|
62
|
+
const remoteComponent = remoteComponents.find((c) => c.id === localComponent.id);
|
|
63
|
+
if (!remoteComponent) {
|
|
64
|
+
log.error(`Component ${localComponent.id} not found in deployed version ${version} \u2014 deploy first.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (remoteComponent.component_type === "CUSTOM_JOURNEY_BLOCK") {
|
|
68
|
+
const configuration = remoteComponent.configuration ?? {};
|
|
69
|
+
if (args.off) {
|
|
70
|
+
delete configuration.override_dev_mode;
|
|
71
|
+
} else {
|
|
72
|
+
configuration.override_dev_mode = { override_url: overrideUrl };
|
|
73
|
+
}
|
|
74
|
+
remoteComponent.configuration = configuration;
|
|
75
|
+
} else {
|
|
76
|
+
const surfaces = remoteComponent.surfaces ?? {};
|
|
77
|
+
for (const surface of Object.values(surfaces)) {
|
|
78
|
+
if (surface && typeof surface === "object") {
|
|
79
|
+
if (args.off) {
|
|
80
|
+
delete surface.override_url;
|
|
81
|
+
} else {
|
|
82
|
+
surface.override_url = overrideUrl;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await client.upsertComponent(appId, version, remoteComponent);
|
|
88
|
+
await client.patchMetadata(appId, { dev_mode: !args.off });
|
|
89
|
+
if (args.off) {
|
|
90
|
+
log.header("Dev mode disabled.");
|
|
91
|
+
log.dim("The component is served from the CDN again.");
|
|
92
|
+
} else {
|
|
93
|
+
log.header(`Dev mode enabled for ${localComponent._dir ?? localComponent.id}`);
|
|
94
|
+
log.info(`epilot now loads this component from ${overrideUrl}`);
|
|
95
|
+
log.info("");
|
|
96
|
+
log.info(` 1. cd components/${localComponent._dir ?? "<component>"} && npm run dev`);
|
|
97
|
+
log.info(" 2. Reload the page in epilot to see your changes");
|
|
98
|
+
log.info("");
|
|
99
|
+
log.dim("Turn off with: epilot app dev --off (required before cloning a new version)");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
export {
|
|
104
|
+
dev_default as default
|
|
105
|
+
};
|