@datarails-ai/dr-cli 1.6.142 → 1.6.144
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/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@datarails-ai/dr-cli",
|
|
11
|
-
version: "1.6.
|
|
11
|
+
version: "1.6.144",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -40,7 +40,7 @@ var package_default = {
|
|
|
40
40
|
postinstall: "node scripts/postinstall.mjs"
|
|
41
41
|
},
|
|
42
42
|
dependencies: {
|
|
43
|
-
"@datarails-ai/dr-appspace-cli": "0.12.
|
|
43
|
+
"@datarails-ai/dr-appspace-cli": "0.12.16",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"@napi-rs/keyring": "^1.3.0",
|
|
46
46
|
commander: "^12.1.0",
|
|
@@ -69,7 +69,7 @@ var package_default = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
// src/buildInfo.ts
|
|
72
|
-
var BUILD_TIME = "2026-10-
|
|
72
|
+
var BUILD_TIME = "2026-10-19T13:42:21.282Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -25214,7 +25214,7 @@ async function runMetricSuggestionsProfile(cfg) {
|
|
|
25214
25214
|
);
|
|
25215
25215
|
}
|
|
25216
25216
|
const windowsByField = /* @__PURE__ */ new Map();
|
|
25217
|
-
const
|
|
25217
|
+
const collect2 = (plan, res) => {
|
|
25218
25218
|
if (res === void 0) return;
|
|
25219
25219
|
for (const f of plan.sampleFields) {
|
|
25220
25220
|
const key = `${plan.table.id}:${f.id}`;
|
|
@@ -25230,7 +25230,7 @@ async function runMetricSuggestionsProfile(cfg) {
|
|
|
25230
25230
|
const r = firsts[i];
|
|
25231
25231
|
if (r !== void 0 && r.ok) {
|
|
25232
25232
|
rowCounts.set(p.table.id, r.value.totalRows);
|
|
25233
|
-
|
|
25233
|
+
collect2(p, r.value);
|
|
25234
25234
|
} else {
|
|
25235
25235
|
rowCounts.set(p.table.id, null);
|
|
25236
25236
|
sampleErrors.set(p.table.id, r === void 0 ? "no result recorded" : errText(r.error));
|
|
@@ -25239,7 +25239,7 @@ async function runMetricSuggestionsProfile(cfg) {
|
|
|
25239
25239
|
for (let i = 0; i < pending.length; i += 1) {
|
|
25240
25240
|
const item = pending[i];
|
|
25241
25241
|
const r = extras[i];
|
|
25242
|
-
if (r !== void 0 && r.ok)
|
|
25242
|
+
if (r !== void 0 && r.ok) collect2(plans[item.planIndex], r.value);
|
|
25243
25243
|
}
|
|
25244
25244
|
const dateValues = /* @__PURE__ */ new Map();
|
|
25245
25245
|
for (let i = 0; i < dateTargets.length; i += 1) {
|
|
@@ -33156,16 +33156,16 @@ function findArityViolations(entries) {
|
|
|
33156
33156
|
for (const entry of entries) {
|
|
33157
33157
|
if (typeof entry.formula !== "string") continue;
|
|
33158
33158
|
const calls = findFunctionCalls(entry.formula);
|
|
33159
|
-
for (const
|
|
33160
|
-
const upper =
|
|
33159
|
+
for (const call2 of calls) {
|
|
33160
|
+
const upper = call2.name.toUpperCase();
|
|
33161
33161
|
const arity = KNOWN_FUNCTION_ARITIES[upper];
|
|
33162
33162
|
if (arity === void 0) continue;
|
|
33163
|
-
if (
|
|
33163
|
+
if (call2.argCount >= arity.min && call2.argCount <= arity.max) continue;
|
|
33164
33164
|
const expected = arity.min === arity.max ? `exactly ${arity.min}` : `${arity.min}-${arity.max}`;
|
|
33165
33165
|
violations.push({
|
|
33166
33166
|
fieldName: entry.name === "" ? "(unnamed)" : entry.name,
|
|
33167
|
-
functionName:
|
|
33168
|
-
got:
|
|
33167
|
+
functionName: call2.name,
|
|
33168
|
+
got: call2.argCount,
|
|
33169
33169
|
expected,
|
|
33170
33170
|
note: arity.note
|
|
33171
33171
|
});
|
|
@@ -45842,9 +45842,9 @@ function isNotPolled(err) {
|
|
|
45842
45842
|
function isPollThrottled(err) {
|
|
45843
45843
|
return err instanceof ControlPlaneError && err.status === 409 && (err.body.includes("discover is in flight") || err.body.includes("polled less than"));
|
|
45844
45844
|
}
|
|
45845
|
-
async function explainNotPolled(filebox,
|
|
45845
|
+
async function explainNotPolled(filebox, call2) {
|
|
45846
45846
|
try {
|
|
45847
|
-
return await
|
|
45847
|
+
return await call2;
|
|
45848
45848
|
} catch (err) {
|
|
45849
45849
|
if (isNotPolled(err)) {
|
|
45850
45850
|
throw new Error(
|
|
@@ -46685,6 +46685,467 @@ ${POLL_CONFIRM_HELP}`)
|
|
|
46685
46685
|
addCommonFlags(seedCmd, "Output the binding (or the dry-run count) as JSON");
|
|
46686
46686
|
}
|
|
46687
46687
|
|
|
46688
|
+
// src/commands/connectorsTasks.ts
|
|
46689
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
46690
|
+
|
|
46691
|
+
// src/api/scheduledTasks.ts
|
|
46692
|
+
var BASE2 = "/scheduled-tasks";
|
|
46693
|
+
async function call(method, path13, send, what) {
|
|
46694
|
+
let out;
|
|
46695
|
+
try {
|
|
46696
|
+
out = await send();
|
|
46697
|
+
} catch (err) {
|
|
46698
|
+
throw toControlPlaneError(method, path13, err);
|
|
46699
|
+
}
|
|
46700
|
+
if (out === null) throw new Error(`${method} ${path13}: empty response from control-plane ${what}`);
|
|
46701
|
+
return out;
|
|
46702
|
+
}
|
|
46703
|
+
async function createTask(client, fields, reqOptions) {
|
|
46704
|
+
return call(
|
|
46705
|
+
"POST",
|
|
46706
|
+
BASE2,
|
|
46707
|
+
() => client.postJSON(BASE2, fields, reqOptions),
|
|
46708
|
+
"task create"
|
|
46709
|
+
);
|
|
46710
|
+
}
|
|
46711
|
+
async function listTasks(client, orgId, reqOptions) {
|
|
46712
|
+
const path13 = orgId === void 0 ? BASE2 : `${BASE2}?org_id=${orgId}`;
|
|
46713
|
+
return call("GET", path13, () => client.getJSON(path13, reqOptions), "task list");
|
|
46714
|
+
}
|
|
46715
|
+
async function getTask(client, taskId, reqOptions) {
|
|
46716
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}`;
|
|
46717
|
+
return call("GET", path13, () => client.getJSON(path13, reqOptions), "task get");
|
|
46718
|
+
}
|
|
46719
|
+
async function updateTask(client, taskId, fields, reqOptions) {
|
|
46720
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}`;
|
|
46721
|
+
return call(
|
|
46722
|
+
"PATCH",
|
|
46723
|
+
path13,
|
|
46724
|
+
() => client.patchJSON(path13, fields, reqOptions),
|
|
46725
|
+
"task update"
|
|
46726
|
+
);
|
|
46727
|
+
}
|
|
46728
|
+
async function deleteTask(client, taskId, reqOptions) {
|
|
46729
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}`;
|
|
46730
|
+
try {
|
|
46731
|
+
await client.deleteJSON(path13, reqOptions);
|
|
46732
|
+
} catch (err) {
|
|
46733
|
+
throw toControlPlaneError("DELETE", path13, err);
|
|
46734
|
+
}
|
|
46735
|
+
}
|
|
46736
|
+
async function runTask(client, taskId, reqOptions) {
|
|
46737
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}/run`;
|
|
46738
|
+
return call(
|
|
46739
|
+
"POST",
|
|
46740
|
+
path13,
|
|
46741
|
+
() => client.postJSON(path13, {}, reqOptions),
|
|
46742
|
+
"task run"
|
|
46743
|
+
);
|
|
46744
|
+
}
|
|
46745
|
+
async function pauseTask(client, taskId, reqOptions) {
|
|
46746
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}/pause`;
|
|
46747
|
+
return call(
|
|
46748
|
+
"POST",
|
|
46749
|
+
path13,
|
|
46750
|
+
() => client.postJSON(path13, {}, reqOptions),
|
|
46751
|
+
"task pause"
|
|
46752
|
+
);
|
|
46753
|
+
}
|
|
46754
|
+
async function resumeTask(client, taskId, reqOptions) {
|
|
46755
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}/resume`;
|
|
46756
|
+
return call(
|
|
46757
|
+
"POST",
|
|
46758
|
+
path13,
|
|
46759
|
+
() => client.postJSON(path13, {}, reqOptions),
|
|
46760
|
+
"task resume"
|
|
46761
|
+
);
|
|
46762
|
+
}
|
|
46763
|
+
async function listRuns(client, taskId, limit, reqOptions) {
|
|
46764
|
+
const path13 = `/scheduled-tasks/${encodeURIComponent(taskId)}/runs?limit=${limit}`;
|
|
46765
|
+
return call(
|
|
46766
|
+
"GET",
|
|
46767
|
+
path13,
|
|
46768
|
+
() => client.getJSON(path13, reqOptions),
|
|
46769
|
+
"task runs"
|
|
46770
|
+
);
|
|
46771
|
+
}
|
|
46772
|
+
|
|
46773
|
+
// src/commands/connectorsTasks.ts
|
|
46774
|
+
var DURATION_UNIT_SECONDS2 = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
46775
|
+
var MIN_EVERY_SECONDS = 60;
|
|
46776
|
+
var MAX_EVERY_SECONDS = 2 ** 31 - 1;
|
|
46777
|
+
function parseEvery(raw) {
|
|
46778
|
+
const m = /^(\d+)([smhd])?$/.exec(raw.trim());
|
|
46779
|
+
const seconds = m === null ? NaN : Number(m[1]) * (DURATION_UNIT_SECONDS2[m[2] ?? "s"] ?? 1);
|
|
46780
|
+
if (m === null)
|
|
46781
|
+
throw new Error(`invalid --every "${raw}": expected a duration like 5m, 1h or 1d`);
|
|
46782
|
+
if (!Number.isSafeInteger(seconds) || seconds < MIN_EVERY_SECONDS || seconds > MAX_EVERY_SECONDS) {
|
|
46783
|
+
throw new Error(`invalid --every "${raw}": must be at least 1m`);
|
|
46784
|
+
}
|
|
46785
|
+
return seconds;
|
|
46786
|
+
}
|
|
46787
|
+
function parseCron(raw) {
|
|
46788
|
+
const fields = raw.trim().split(/\s+/);
|
|
46789
|
+
if (fields.length !== 5 || fields.some((f) => f === "")) {
|
|
46790
|
+
throw new Error(`invalid --cron "${raw}": expected five fields, e.g. "15 * * * *"`);
|
|
46791
|
+
}
|
|
46792
|
+
return fields.join(" ");
|
|
46793
|
+
}
|
|
46794
|
+
var ROLE_RE = /^[a-z][a-z_]*$/;
|
|
46795
|
+
var DATASOURCE_ID_RE = /^[0-9]{1,18}$/;
|
|
46796
|
+
function parseDatasources(values) {
|
|
46797
|
+
const out = {};
|
|
46798
|
+
for (const raw of values) {
|
|
46799
|
+
const eq = raw.indexOf("=");
|
|
46800
|
+
const role = eq < 0 ? "" : raw.slice(0, eq).trim();
|
|
46801
|
+
const id = eq < 0 ? "" : raw.slice(eq + 1).trim();
|
|
46802
|
+
if (!ROLE_RE.test(role) || !DATASOURCE_ID_RE.test(id)) {
|
|
46803
|
+
throw new Error(
|
|
46804
|
+
`invalid --datasource "${raw}": expected <role>=<datasource id>, e.g. target=4242 (role: lowercase letters and _; id: digits)`
|
|
46805
|
+
);
|
|
46806
|
+
}
|
|
46807
|
+
if (Object.hasOwn(out, role)) throw new Error(`--datasource role "${role}" given twice`);
|
|
46808
|
+
out[role] = id;
|
|
46809
|
+
}
|
|
46810
|
+
const count = Object.keys(out).length;
|
|
46811
|
+
if (count < 1 || count > 4) throw new Error("give between 1 and 4 --datasource roles");
|
|
46812
|
+
return out;
|
|
46813
|
+
}
|
|
46814
|
+
function parseConfig(raw, readFile9 = readUtf8) {
|
|
46815
|
+
const text2 = raw.startsWith("@") ? readFile9(raw.slice(1)) : raw;
|
|
46816
|
+
let parsed2;
|
|
46817
|
+
try {
|
|
46818
|
+
parsed2 = JSON.parse(text2);
|
|
46819
|
+
} catch (err) {
|
|
46820
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46821
|
+
throw new Error(`invalid --config JSON: ${msg}`);
|
|
46822
|
+
}
|
|
46823
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) {
|
|
46824
|
+
throw new Error("invalid --config: expected a JSON object");
|
|
46825
|
+
}
|
|
46826
|
+
return parsed2;
|
|
46827
|
+
}
|
|
46828
|
+
function readUtf8(p) {
|
|
46829
|
+
return readFileSync3(p, "utf-8");
|
|
46830
|
+
}
|
|
46831
|
+
function collect(value, previous = []) {
|
|
46832
|
+
return [...previous, value];
|
|
46833
|
+
}
|
|
46834
|
+
function parseScheduleFlags(opts) {
|
|
46835
|
+
if (opts.every !== void 0 && opts.cron !== void 0) {
|
|
46836
|
+
throw new Error("give --every or --cron, not both");
|
|
46837
|
+
}
|
|
46838
|
+
const fields = {};
|
|
46839
|
+
if (opts.artifact !== void 0) fields.artifact_name = opts.artifact;
|
|
46840
|
+
if (opts.flavor !== void 0) fields.runtime_flavor = opts.flavor;
|
|
46841
|
+
if (opts.datasource !== void 0) fields.datasources = parseDatasources(opts.datasource);
|
|
46842
|
+
if (opts.primaryRole !== void 0) fields.primary_role = opts.primaryRole;
|
|
46843
|
+
if (opts.config !== void 0) fields.config = parseConfig(opts.config);
|
|
46844
|
+
if (opts.every !== void 0) fields.every_seconds = parseEvery(opts.every);
|
|
46845
|
+
if (opts.cron !== void 0) fields.cron = parseCron(opts.cron);
|
|
46846
|
+
if (opts.tz !== void 0) fields.timezone = opts.tz;
|
|
46847
|
+
if (fields.primary_role !== void 0 && fields.datasources !== void 0) {
|
|
46848
|
+
if (!Object.hasOwn(fields.datasources, fields.primary_role)) {
|
|
46849
|
+
throw new Error(
|
|
46850
|
+
`--primary-role "${fields.primary_role}" is not one of the --datasource roles`
|
|
46851
|
+
);
|
|
46852
|
+
}
|
|
46853
|
+
}
|
|
46854
|
+
return fields;
|
|
46855
|
+
}
|
|
46856
|
+
function buildCreateFields(name, orgId, opts) {
|
|
46857
|
+
const missing = [
|
|
46858
|
+
["--artifact", opts.artifact],
|
|
46859
|
+
["--flavor", opts.flavor],
|
|
46860
|
+
["--datasource", opts.datasource],
|
|
46861
|
+
["--config", opts.config]
|
|
46862
|
+
].filter(([, v]) => v === void 0).map(([flag]) => flag);
|
|
46863
|
+
if (opts.every === void 0 && opts.cron === void 0) missing.push("--every or --cron");
|
|
46864
|
+
if (missing.length > 0) throw new Error(`missing ${missing.join(", ")}`);
|
|
46865
|
+
const fields = parseScheduleFlags(opts);
|
|
46866
|
+
return {
|
|
46867
|
+
org_id: orgId,
|
|
46868
|
+
name,
|
|
46869
|
+
artifact_name: fields.artifact_name,
|
|
46870
|
+
runtime_flavor: fields.runtime_flavor,
|
|
46871
|
+
datasources: fields.datasources,
|
|
46872
|
+
config: fields.config,
|
|
46873
|
+
...fields.primary_role !== void 0 ? { primary_role: fields.primary_role } : {},
|
|
46874
|
+
...fields.every_seconds !== void 0 ? { every_seconds: fields.every_seconds } : {},
|
|
46875
|
+
...fields.cron !== void 0 ? { cron: fields.cron } : {},
|
|
46876
|
+
...fields.timezone !== void 0 ? { timezone: fields.timezone } : {},
|
|
46877
|
+
...opts.disabled === true ? { enabled: false } : {}
|
|
46878
|
+
};
|
|
46879
|
+
}
|
|
46880
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
46881
|
+
async function resolveTaskId(client, task, signal) {
|
|
46882
|
+
if (UUID_RE.test(task)) return task;
|
|
46883
|
+
const tasks = await listTasks(client, void 0, { signal });
|
|
46884
|
+
const match = tasks.find((t) => t.name === task);
|
|
46885
|
+
if (match === void 0) {
|
|
46886
|
+
throw new Error(
|
|
46887
|
+
`no scheduled task named "${task}" in this org (see: dr connectors tasks list)`
|
|
46888
|
+
);
|
|
46889
|
+
}
|
|
46890
|
+
return match.id;
|
|
46891
|
+
}
|
|
46892
|
+
function explainTaskError(err) {
|
|
46893
|
+
const forbidden = err instanceof ControlPlaneError && err.status === 403 || err instanceof Error && /\bHTTP 403\b/.test(err.message);
|
|
46894
|
+
if (forbidden) {
|
|
46895
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46896
|
+
const body = err instanceof ControlPlaneError ? err.body : (/HTTP 403[^:]*(?:\([^)]*\))?:\s*([\s\S]*)$/.exec(message)?.[1] ?? "").trim();
|
|
46897
|
+
return withCause(
|
|
46898
|
+
new ControlPlaneError(
|
|
46899
|
+
403,
|
|
46900
|
+
body,
|
|
46901
|
+
`${message}
|
|
46902
|
+
Scheduled tasks need a support user or an org admin: log in as one (--account) \u2014 e.g. dr login-as the customer's support user.`
|
|
46903
|
+
),
|
|
46904
|
+
err
|
|
46905
|
+
);
|
|
46906
|
+
}
|
|
46907
|
+
if (!(err instanceof ControlPlaneError)) return err;
|
|
46908
|
+
const detail = detailOf(err.body);
|
|
46909
|
+
if (err.status === 404) {
|
|
46910
|
+
const message = `scheduled task not found (${detail || err.message})`;
|
|
46911
|
+
return withCause(new ControlPlaneError(404, err.body, message), err);
|
|
46912
|
+
}
|
|
46913
|
+
if (err.status === 409 || err.status === 422) {
|
|
46914
|
+
return withCause(new ControlPlaneError(err.status, err.body, detail || err.message), err);
|
|
46915
|
+
}
|
|
46916
|
+
return err;
|
|
46917
|
+
}
|
|
46918
|
+
function withCause(error, cause) {
|
|
46919
|
+
Object.defineProperty(error, "cause", { value: cause, enumerable: false, configurable: true });
|
|
46920
|
+
return error;
|
|
46921
|
+
}
|
|
46922
|
+
function detailOf(body) {
|
|
46923
|
+
try {
|
|
46924
|
+
const parsed2 = JSON.parse(body);
|
|
46925
|
+
const d = parsed2.detail;
|
|
46926
|
+
if (typeof d === "string") return d;
|
|
46927
|
+
if (Array.isArray(d)) {
|
|
46928
|
+
return d.map(
|
|
46929
|
+
(e) => `${Array.isArray(e.loc) ? e.loc.slice(1).join(".") : ""}: ${e.msg ?? ""}`.trim()
|
|
46930
|
+
).join("; ");
|
|
46931
|
+
}
|
|
46932
|
+
} catch {
|
|
46933
|
+
}
|
|
46934
|
+
return body;
|
|
46935
|
+
}
|
|
46936
|
+
function scheduleText(t) {
|
|
46937
|
+
if (t.schedule_kind === "cron") return `cron "${t.cron ?? ""}" ${t.timezone}`;
|
|
46938
|
+
const s = t.every_seconds ?? 0;
|
|
46939
|
+
const unit = s % 86400 === 0 ? `${s / 86400}d` : s % 3600 === 0 ? `${s / 3600}h` : s % 60 === 0 ? `${s / 60}m` : `${s}s`;
|
|
46940
|
+
return `every ${unit}`;
|
|
46941
|
+
}
|
|
46942
|
+
function stateText(t) {
|
|
46943
|
+
if (!t.enabled) return "disabled";
|
|
46944
|
+
if (t.paused_reason !== null) return `paused (${t.paused_reason})`;
|
|
46945
|
+
return t.running ? "running" : "enabled";
|
|
46946
|
+
}
|
|
46947
|
+
function writeTask(out, t) {
|
|
46948
|
+
const roles = Object.entries(t.datasources).map(([role, id]) => `${role}=${id}${role === t.primary_role ? " (primary)" : ""}`).join(", ");
|
|
46949
|
+
out.write(`Task: ${t.name} (${t.id})
|
|
46950
|
+
`);
|
|
46951
|
+
out.write(` Org: ${t.org_id}
|
|
46952
|
+
`);
|
|
46953
|
+
out.write(` State: ${stateText(t)}
|
|
46954
|
+
`);
|
|
46955
|
+
out.write(` Artifact: ${t.artifact_name} [${t.runtime_flavor}]
|
|
46956
|
+
`);
|
|
46957
|
+
out.write(` Schedule: ${scheduleText(t)}
|
|
46958
|
+
`);
|
|
46959
|
+
out.write(` Next run: ${t.next_run_at}
|
|
46960
|
+
`);
|
|
46961
|
+
out.write(` Last run: ${t.last_run_at ?? "-"} ${t.last_run_status ?? ""}
|
|
46962
|
+
`);
|
|
46963
|
+
if (t.consecutive_failures > 0) out.write(` Failures: ${t.consecutive_failures} in a row
|
|
46964
|
+
`);
|
|
46965
|
+
out.write(` Datasources: ${roles}
|
|
46966
|
+
`);
|
|
46967
|
+
out.write(` Config: ${truncate3(JSON.stringify(t.config), 200)}
|
|
46968
|
+
`);
|
|
46969
|
+
}
|
|
46970
|
+
function writeTaskList(out, tasks) {
|
|
46971
|
+
if (tasks.length === 0) {
|
|
46972
|
+
out.write("No scheduled tasks.\n");
|
|
46973
|
+
return;
|
|
46974
|
+
}
|
|
46975
|
+
for (const t of tasks) {
|
|
46976
|
+
out.write(
|
|
46977
|
+
`${t.name.padEnd(28)} ${stateText(t).padEnd(20)} ${scheduleText(t).padEnd(26)} next ${t.next_run_at} last ${t.last_run_status ?? "-"} ${t.id}
|
|
46978
|
+
`
|
|
46979
|
+
);
|
|
46980
|
+
}
|
|
46981
|
+
}
|
|
46982
|
+
function writeRuns(out, runs) {
|
|
46983
|
+
if (runs.length === 0) {
|
|
46984
|
+
out.write("No runs yet.\n");
|
|
46985
|
+
return;
|
|
46986
|
+
}
|
|
46987
|
+
for (const r of runs) {
|
|
46988
|
+
const tail = r.error !== null ? ` error: ${truncate3(r.error, 160)}` : r.summary !== null ? ` ${truncate3(JSON.stringify(r.summary), 160)}` : "";
|
|
46989
|
+
out.write(
|
|
46990
|
+
`${r.started_at} ${r.status.padEnd(15)} ${r.trigger.padEnd(8)} attempt ${r.attempt} ${r.artifact_version ?? "-"}${tail}
|
|
46991
|
+
`
|
|
46992
|
+
);
|
|
46993
|
+
}
|
|
46994
|
+
}
|
|
46995
|
+
function withFlags(cmd) {
|
|
46996
|
+
cmd.option("--json", "Output raw JSON instead of a summary", false);
|
|
46997
|
+
addServerFlag(cmd);
|
|
46998
|
+
addAccountFlag(cmd);
|
|
46999
|
+
addConnectorsUrlFlag(cmd);
|
|
47000
|
+
return cmd;
|
|
47001
|
+
}
|
|
47002
|
+
function addScheduleFlags(cmd) {
|
|
47003
|
+
return cmd.option("--artifact <name>", "published artifact name, e.g. sftp_export__daily_sales").option("--flavor <flavor>", "runner flavor the artifact was published for, e.g. sftp").option("--datasource <role=id>", "monolith datasource for a role (repeat per role)", collect).option(
|
|
47004
|
+
"--primary-role <role>",
|
|
47005
|
+
"the role passed as the primary datasource (default: the first)"
|
|
47006
|
+
).option("--config <json|@file>", "task config: a JSON object, or @path to a JSON file").option("--every <duration>", "run on an interval, e.g. 5m, 1h (at least 1m)").option("--cron <expr>", 'run on a five-field cron line, e.g. "15 * * * *"').option("--tz <iana>", "timezone the cron line is read in (default UTC)");
|
|
47007
|
+
}
|
|
47008
|
+
async function onTask(task, opts, act, print) {
|
|
47009
|
+
await withConnectorsSetup(opts, async ({ client, signal }) => {
|
|
47010
|
+
try {
|
|
47011
|
+
const taskId = await resolveTaskId(client, task, signal);
|
|
47012
|
+
const result = await act(client, taskId, signal);
|
|
47013
|
+
if (opts.json === true) writeJSONIndented(process.stdout, result);
|
|
47014
|
+
else print(process.stdout, result);
|
|
47015
|
+
} catch (err) {
|
|
47016
|
+
throw explainTaskError(err);
|
|
47017
|
+
}
|
|
47018
|
+
});
|
|
47019
|
+
}
|
|
47020
|
+
function registerConnectorsTasks(connectors) {
|
|
47021
|
+
const tasks = connectors.command("tasks").description("DRIP scheduled tasks: run an artifact on a schedule (support user or org admin)");
|
|
47022
|
+
withFlags(
|
|
47023
|
+
addScheduleFlags(
|
|
47024
|
+
tasks.command("create").description("Create a scheduled task").argument("<name>", "task name, unique in the org (a-z, 0-9, _ and -)").option("--org-id <id>", "org id (defaults to the JWT org)").option("--disabled", "create it disabled", false)
|
|
47025
|
+
)
|
|
47026
|
+
).action(
|
|
47027
|
+
async (name, opts) => {
|
|
47028
|
+
const explicitOrg = opts.orgId !== void 0 ? parsePositiveIntArg2(opts.orgId, "--org-id") : void 0;
|
|
47029
|
+
const parsed2 = buildCreateFields(name, explicitOrg ?? 0, opts);
|
|
47030
|
+
await withConnectorsSetup(opts, async ({ client, signal }) => {
|
|
47031
|
+
const fields = { ...parsed2, org_id: explicitOrg ?? client.getCurrentOrgId() };
|
|
47032
|
+
try {
|
|
47033
|
+
const t = await createTask(client, fields, { signal });
|
|
47034
|
+
if (opts.json === true) writeJSONIndented(process.stdout, t);
|
|
47035
|
+
else writeTask(process.stdout, t);
|
|
47036
|
+
} catch (err) {
|
|
47037
|
+
throw explainTaskError(err);
|
|
47038
|
+
}
|
|
47039
|
+
});
|
|
47040
|
+
}
|
|
47041
|
+
);
|
|
47042
|
+
withFlags(
|
|
47043
|
+
tasks.command("list").description("List the org's scheduled tasks").option("--org-id <id>", "org id (defaults to the JWT org)")
|
|
47044
|
+
).action(async (opts) => {
|
|
47045
|
+
await withConnectorsSetup(opts, async ({ client, signal }) => {
|
|
47046
|
+
const orgId = opts.orgId !== void 0 ? parsePositiveIntArg2(opts.orgId, "--org-id") : void 0;
|
|
47047
|
+
try {
|
|
47048
|
+
const list28 = await listTasks(client, orgId, { signal });
|
|
47049
|
+
if (opts.json === true) writeJSONIndented(process.stdout, list28);
|
|
47050
|
+
else writeTaskList(process.stdout, list28);
|
|
47051
|
+
} catch (err) {
|
|
47052
|
+
throw explainTaskError(err);
|
|
47053
|
+
}
|
|
47054
|
+
});
|
|
47055
|
+
});
|
|
47056
|
+
withFlags(
|
|
47057
|
+
tasks.command("get").description("Show a scheduled task").argument("<task>", "task id or name")
|
|
47058
|
+
).action(
|
|
47059
|
+
async (task, opts) => onTask(
|
|
47060
|
+
task,
|
|
47061
|
+
opts,
|
|
47062
|
+
(c, id, signal) => getTask(c, id, { signal }),
|
|
47063
|
+
(o, r) => writeTask(o, r)
|
|
47064
|
+
)
|
|
47065
|
+
);
|
|
47066
|
+
withFlags(
|
|
47067
|
+
addScheduleFlags(
|
|
47068
|
+
tasks.command("update").description("Change a scheduled task (only the flags given change)").argument("<task>", "task id or name").option("--enable", "enable the task").option("--disable", "disable the task")
|
|
47069
|
+
)
|
|
47070
|
+
).action(
|
|
47071
|
+
async (task, opts) => {
|
|
47072
|
+
if (opts.enable === true && opts.disable === true)
|
|
47073
|
+
throw new Error("give --enable or --disable, not both");
|
|
47074
|
+
const fields = parseScheduleFlags(opts);
|
|
47075
|
+
if (opts.enable === true) fields.enabled = true;
|
|
47076
|
+
if (opts.disable === true) fields.enabled = false;
|
|
47077
|
+
if (Object.keys(fields).length === 0)
|
|
47078
|
+
throw new Error("nothing to change: give at least one flag");
|
|
47079
|
+
await onTask(
|
|
47080
|
+
task,
|
|
47081
|
+
opts,
|
|
47082
|
+
(c, id, signal) => updateTask(c, id, fields, { signal }),
|
|
47083
|
+
(o, r) => writeTask(o, r)
|
|
47084
|
+
);
|
|
47085
|
+
}
|
|
47086
|
+
);
|
|
47087
|
+
withFlags(
|
|
47088
|
+
tasks.command("delete").description("Delete a scheduled task and its run history").argument("<task>", "task id or name")
|
|
47089
|
+
).action(
|
|
47090
|
+
async (task, opts) => onTask(
|
|
47091
|
+
task,
|
|
47092
|
+
opts,
|
|
47093
|
+
async (c, id, signal) => {
|
|
47094
|
+
await deleteTask(c, id, { signal });
|
|
47095
|
+
return { deleted: id };
|
|
47096
|
+
},
|
|
47097
|
+
(o, r) => o.write(`Deleted scheduled task ${r.deleted}
|
|
47098
|
+
`)
|
|
47099
|
+
)
|
|
47100
|
+
);
|
|
47101
|
+
withFlags(
|
|
47102
|
+
tasks.command("run").description("Run a task now (does not move its schedule)").argument("<task>", "task id or name")
|
|
47103
|
+
).action(
|
|
47104
|
+
async (task, opts) => onTask(
|
|
47105
|
+
task,
|
|
47106
|
+
opts,
|
|
47107
|
+
(c, id, signal) => runTask(c, id, { signal }),
|
|
47108
|
+
(o, r) => {
|
|
47109
|
+
const run = r;
|
|
47110
|
+
o.write(`Queued run ${run.id} (${run.status}); see: dr connectors tasks runs ${task}
|
|
47111
|
+
`);
|
|
47112
|
+
}
|
|
47113
|
+
)
|
|
47114
|
+
);
|
|
47115
|
+
withFlags(
|
|
47116
|
+
tasks.command("pause").description("Pause a scheduled task").argument("<task>", "task id or name")
|
|
47117
|
+
).action(
|
|
47118
|
+
async (task, opts) => onTask(
|
|
47119
|
+
task,
|
|
47120
|
+
opts,
|
|
47121
|
+
(c, id, signal) => pauseTask(c, id, { signal }),
|
|
47122
|
+
(o, r) => writeTask(o, r)
|
|
47123
|
+
)
|
|
47124
|
+
);
|
|
47125
|
+
withFlags(
|
|
47126
|
+
tasks.command("resume").description("Resume a paused task (clears its failure count, schedules the next run)").argument("<task>", "task id or name")
|
|
47127
|
+
).action(
|
|
47128
|
+
async (task, opts) => onTask(
|
|
47129
|
+
task,
|
|
47130
|
+
opts,
|
|
47131
|
+
(c, id, signal) => resumeTask(c, id, { signal }),
|
|
47132
|
+
(o, r) => writeTask(o, r)
|
|
47133
|
+
)
|
|
47134
|
+
);
|
|
47135
|
+
withFlags(
|
|
47136
|
+
tasks.command("runs").description("A task's run history, newest first").argument("<task>", "task id or name").option("--limit <n>", "how many runs (1-200)", "20")
|
|
47137
|
+
).action(async (task, opts) => {
|
|
47138
|
+
const limit = parsePositiveIntArg2(opts.limit, "--limit");
|
|
47139
|
+
if (limit > 200) throw new Error("invalid --limit: at most 200");
|
|
47140
|
+
await onTask(
|
|
47141
|
+
task,
|
|
47142
|
+
opts,
|
|
47143
|
+
(c, id, signal) => listRuns(c, id, limit, { signal }),
|
|
47144
|
+
(o, r) => writeRuns(o, r)
|
|
47145
|
+
);
|
|
47146
|
+
});
|
|
47147
|
+
}
|
|
47148
|
+
|
|
46688
47149
|
// src/commands/connectors.ts
|
|
46689
47150
|
var CONTROL_PLANE_HOST = (subDomain) => `https://${subDomain}-connectors-control-plane.datarails.io`;
|
|
46690
47151
|
function resolveConnectorsUrl(serverKey, connectorsUrlFlag) {
|
|
@@ -47342,6 +47803,7 @@ ${TLS_VPN_HELP}`);
|
|
|
47342
47803
|
addConnectorsUrlFlag(activeGetCmd);
|
|
47343
47804
|
registerDevFetch(connectors);
|
|
47344
47805
|
registerConnectorsPoll(connectors);
|
|
47806
|
+
registerConnectorsTasks(connectors);
|
|
47345
47807
|
}
|
|
47346
47808
|
|
|
47347
47809
|
// src/types/dashboards.ts
|
|
@@ -66382,13 +66844,13 @@ import { promises as fsPromises3 } from "fs";
|
|
|
66382
66844
|
import * as nodePath3 from "path";
|
|
66383
66845
|
|
|
66384
66846
|
// src/api/spendControl.ts
|
|
66385
|
-
var
|
|
66847
|
+
var BASE3 = "/api/spend-control/v1";
|
|
66386
66848
|
var BASE_V2 = "/api/spend-control/v2";
|
|
66387
66849
|
var MAX_BULK_STATUS_IDS = 50;
|
|
66388
66850
|
var MAX_PAGE_SIZE = 100;
|
|
66389
66851
|
async function listBillingFrequencies(client, options) {
|
|
66390
66852
|
const out = await client.getJSON(
|
|
66391
|
-
`${
|
|
66853
|
+
`${BASE3}/catalogs/billing-frequencies/`,
|
|
66392
66854
|
options
|
|
66393
66855
|
);
|
|
66394
66856
|
return out ?? [];
|
|
@@ -66422,7 +66884,7 @@ function buildContractListQuery(params) {
|
|
|
66422
66884
|
}
|
|
66423
66885
|
async function listContracts(client, params = {}, options) {
|
|
66424
66886
|
const qs = buildContractListQuery(params);
|
|
66425
|
-
const path13 = qs === "" ? `${
|
|
66887
|
+
const path13 = qs === "" ? `${BASE3}/contracts/` : `${BASE3}/contracts/?${qs}`;
|
|
66426
66888
|
const out = await client.getJSON(path13, options);
|
|
66427
66889
|
if (out === null) {
|
|
66428
66890
|
return { items: [], total: 0, page: null, size: null, pages: null };
|
|
@@ -66450,10 +66912,10 @@ async function getContract(client, contractId, params = {}, options) {
|
|
|
66450
66912
|
qs.set("currency_code", params.convertTo);
|
|
66451
66913
|
}
|
|
66452
66914
|
const suffix = qs.toString() === "" ? "" : `?${qs.toString()}`;
|
|
66453
|
-
return client.getJSON(`${
|
|
66915
|
+
return client.getJSON(`${BASE3}/contracts/${contractId}${suffix}`, options);
|
|
66454
66916
|
}
|
|
66455
66917
|
async function deleteContract(client, contractId, options) {
|
|
66456
|
-
await client.deleteJSON(`${
|
|
66918
|
+
await client.deleteJSON(`${BASE3}/contracts/${contractId}`, options);
|
|
66457
66919
|
}
|
|
66458
66920
|
async function uploadFilesWithAi(client, files, options = {}) {
|
|
66459
66921
|
if (files.length === 0) return [];
|
|
@@ -66461,7 +66923,7 @@ async function uploadFilesWithAi(client, files, options = {}) {
|
|
|
66461
66923
|
throw new Error(`uploadFilesWithAi: max 10 files per call, got ${files.length}`);
|
|
66462
66924
|
}
|
|
66463
66925
|
const out = await client.postMultipartFiles(
|
|
66464
|
-
`${
|
|
66926
|
+
`${BASE3}/contracts/files-with-ai`,
|
|
66465
66927
|
{ is_additional: options.isAdditional === true ? "true" : "false" },
|
|
66466
66928
|
files,
|
|
66467
66929
|
"files",
|
|
@@ -66471,7 +66933,7 @@ async function uploadFilesWithAi(client, files, options = {}) {
|
|
|
66471
66933
|
}
|
|
66472
66934
|
async function uploadContractFile(client, file, options = {}) {
|
|
66473
66935
|
return client.postMultipartFiles(
|
|
66474
|
-
`${
|
|
66936
|
+
`${BASE3}/contracts/file`,
|
|
66475
66937
|
{ is_additional: options.isAdditional === true ? "true" : "false" },
|
|
66476
66938
|
[file],
|
|
66477
66939
|
"file",
|
|
@@ -66484,7 +66946,7 @@ async function listContractFiles(client, contractId, params = {}, options) {
|
|
|
66484
66946
|
if (params.size !== void 0) qs.set("size", String(params.size));
|
|
66485
66947
|
const suffix = qs.toString() === "" ? "" : `?${qs.toString()}`;
|
|
66486
66948
|
const out = await client.getJSON(
|
|
66487
|
-
`${
|
|
66949
|
+
`${BASE3}/contracts/${contractId}/files${suffix}`,
|
|
66488
66950
|
options
|
|
66489
66951
|
);
|
|
66490
66952
|
if (out === null) {
|
|
@@ -66510,13 +66972,13 @@ async function listAllContractFiles(client, contractId, options) {
|
|
|
66510
66972
|
}
|
|
66511
66973
|
async function getContractFileDownload(client, fileId, options) {
|
|
66512
66974
|
return client.postJSON(
|
|
66513
|
-
`${
|
|
66975
|
+
`${BASE3}/contracts/files/${fileId}/download`,
|
|
66514
66976
|
{},
|
|
66515
66977
|
options
|
|
66516
66978
|
);
|
|
66517
66979
|
}
|
|
66518
66980
|
async function deleteContractFile(client, fileId, options) {
|
|
66519
|
-
await client.deleteJSON(`${
|
|
66981
|
+
await client.deleteJSON(`${BASE3}/contracts/files/${fileId}`, options);
|
|
66520
66982
|
}
|
|
66521
66983
|
async function uploadFileWithAiAsync(client, file, options = {}) {
|
|
66522
66984
|
return client.postMultipartFiles(
|
|
@@ -66636,14 +67098,14 @@ async function bulkCreateContracts(client, body, options) {
|
|
|
66636
67098
|
throw new Error(`bulkCreateContracts: max 10 contracts per call, got ${body.contracts.length}`);
|
|
66637
67099
|
}
|
|
66638
67100
|
const out = await client.postJSON(
|
|
66639
|
-
`${
|
|
67101
|
+
`${BASE3}/contracts/bulk-create`,
|
|
66640
67102
|
body,
|
|
66641
67103
|
options
|
|
66642
67104
|
);
|
|
66643
67105
|
return out ?? { contracts: [] };
|
|
66644
67106
|
}
|
|
66645
67107
|
async function partialUpdateContract(client, contractId, body, options) {
|
|
66646
|
-
return client.patchJSON(`${
|
|
67108
|
+
return client.patchJSON(`${BASE3}/contracts/${contractId}`, body, options);
|
|
66647
67109
|
}
|
|
66648
67110
|
|
|
66649
67111
|
// src/types/spendControl.ts
|
|
@@ -67063,7 +67525,7 @@ function chunkContracts(contracts, size = 10) {
|
|
|
67063
67525
|
}
|
|
67064
67526
|
|
|
67065
67527
|
// src/api/spendControlVendors.ts
|
|
67066
|
-
var
|
|
67528
|
+
var BASE4 = "/api/spend-control/v1/vendors";
|
|
67067
67529
|
var MAX_PAGE_SIZE2 = 100;
|
|
67068
67530
|
function buildVendorListQuery(params) {
|
|
67069
67531
|
const qs = new URLSearchParams();
|
|
@@ -67094,19 +67556,19 @@ function emptyPage() {
|
|
|
67094
67556
|
return { items: [], total: 0, page: null, size: null, pages: null };
|
|
67095
67557
|
}
|
|
67096
67558
|
async function listVendors(client, params = {}, options) {
|
|
67097
|
-
const out = await client.getJSON(withQuery(`${
|
|
67559
|
+
const out = await client.getJSON(withQuery(`${BASE4}/`, params), options);
|
|
67098
67560
|
return out ?? emptyPage();
|
|
67099
67561
|
}
|
|
67100
67562
|
async function listAppStoreVendors(client, params = {}, options) {
|
|
67101
67563
|
const out = await client.getJSON(
|
|
67102
|
-
withQuery(`${
|
|
67564
|
+
withQuery(`${BASE4}/app-store`, params),
|
|
67103
67565
|
options
|
|
67104
67566
|
);
|
|
67105
67567
|
return out ?? emptyPage();
|
|
67106
67568
|
}
|
|
67107
67569
|
async function listVendorsWithContracts(client, params = {}, options) {
|
|
67108
67570
|
const out = await client.getJSON(
|
|
67109
|
-
withQuery(`${
|
|
67571
|
+
withQuery(`${BASE4}/with-contracts`, params),
|
|
67110
67572
|
options
|
|
67111
67573
|
);
|
|
67112
67574
|
return out ?? emptyPage();
|
|
@@ -67123,24 +67585,24 @@ async function listAllVendors(fetchPage, client, params = {}, options) {
|
|
|
67123
67585
|
return collected;
|
|
67124
67586
|
}
|
|
67125
67587
|
async function listVendorCategories(client, options) {
|
|
67126
|
-
const out = await client.getJSON(`${
|
|
67588
|
+
const out = await client.getJSON(`${BASE4}/categories`, options);
|
|
67127
67589
|
return out ?? [];
|
|
67128
67590
|
}
|
|
67129
67591
|
async function listVendorsWithMultipleContracts(client, options) {
|
|
67130
|
-
const out = await client.getJSON(`${
|
|
67592
|
+
const out = await client.getJSON(`${BASE4}/with-multiple-contracts`, options);
|
|
67131
67593
|
return out ?? [];
|
|
67132
67594
|
}
|
|
67133
67595
|
async function getVendorOwners(client, vendorId, options) {
|
|
67134
|
-
return client.getJSON(`${
|
|
67596
|
+
return client.getJSON(`${BASE4}/${encodeURIComponent(vendorId)}/owners/`, options);
|
|
67135
67597
|
}
|
|
67136
67598
|
async function createVendor(client, name, options) {
|
|
67137
|
-
return client.postJSON(`${
|
|
67599
|
+
return client.postJSON(`${BASE4}/`, { name }, options);
|
|
67138
67600
|
}
|
|
67139
67601
|
async function bulkUpdateVendorVisibility(client, vendors, options) {
|
|
67140
67602
|
if (vendors.length === 0) {
|
|
67141
67603
|
throw new Error("bulkUpdateVendorVisibility: at least one vendor is required");
|
|
67142
67604
|
}
|
|
67143
|
-
const out = await client.patchJSON(`${
|
|
67605
|
+
const out = await client.patchJSON(`${BASE4}/visibility`, { vendors }, options);
|
|
67144
67606
|
return out ?? [];
|
|
67145
67607
|
}
|
|
67146
67608
|
|
|
@@ -70369,8 +70831,8 @@ function querySuffix(domain) {
|
|
|
70369
70831
|
if (domain === void 0 || domain === "") return "";
|
|
70370
70832
|
return `?domain=${encodeURIComponent(domain)}`;
|
|
70371
70833
|
}
|
|
70372
|
-
function required(out,
|
|
70373
|
-
if (out === null) throw new Error(`${
|
|
70834
|
+
function required(out, call2) {
|
|
70835
|
+
if (out === null) throw new Error(`${call2}: empty response body`);
|
|
70374
70836
|
return out;
|
|
70375
70837
|
}
|
|
70376
70838
|
|
|
@@ -72372,7 +72834,7 @@ import {
|
|
|
72372
72834
|
linkSync,
|
|
72373
72835
|
mkdirSync,
|
|
72374
72836
|
openSync,
|
|
72375
|
-
readFileSync as
|
|
72837
|
+
readFileSync as readFileSync4,
|
|
72376
72838
|
unlinkSync,
|
|
72377
72839
|
writeFileSync
|
|
72378
72840
|
} from "fs";
|
|
@@ -72388,7 +72850,7 @@ function copySessionCredentials(mountPath = SESSION_AUTH_MOUNT, target = credent
|
|
|
72388
72850
|
const temp = `${target}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
72389
72851
|
let created = false;
|
|
72390
72852
|
try {
|
|
72391
|
-
const bytes =
|
|
72853
|
+
const bytes = readFileSync4(mountPath);
|
|
72392
72854
|
mkdirSync(dirname10(target), { recursive: true });
|
|
72393
72855
|
const fd = openSync(temp, "wx", 384);
|
|
72394
72856
|
created = true;
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.144",
|
|
4
4
|
"_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
|
|
5
5
|
"commands": {
|
|
6
6
|
"login": {
|
|
@@ -2103,6 +2103,60 @@
|
|
|
2103
2103
|
"until": "DR-53511"
|
|
2104
2104
|
}
|
|
2105
2105
|
},
|
|
2106
|
+
"connectors tasks create": {
|
|
2107
|
+
"denied": {
|
|
2108
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2109
|
+
"until": "DR-53511"
|
|
2110
|
+
}
|
|
2111
|
+
},
|
|
2112
|
+
"connectors tasks list": {
|
|
2113
|
+
"denied": {
|
|
2114
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2115
|
+
"until": "DR-53511"
|
|
2116
|
+
}
|
|
2117
|
+
},
|
|
2118
|
+
"connectors tasks get": {
|
|
2119
|
+
"denied": {
|
|
2120
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2121
|
+
"until": "DR-53511"
|
|
2122
|
+
}
|
|
2123
|
+
},
|
|
2124
|
+
"connectors tasks update": {
|
|
2125
|
+
"denied": {
|
|
2126
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2127
|
+
"until": "DR-53511"
|
|
2128
|
+
}
|
|
2129
|
+
},
|
|
2130
|
+
"connectors tasks delete": {
|
|
2131
|
+
"denied": {
|
|
2132
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2133
|
+
"until": "DR-53511"
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
"connectors tasks run": {
|
|
2137
|
+
"denied": {
|
|
2138
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2139
|
+
"until": "DR-53511"
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
"connectors tasks pause": {
|
|
2143
|
+
"denied": {
|
|
2144
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2145
|
+
"until": "DR-53511"
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
2148
|
+
"connectors tasks resume": {
|
|
2149
|
+
"denied": {
|
|
2150
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2151
|
+
"until": "DR-53511"
|
|
2152
|
+
}
|
|
2153
|
+
},
|
|
2154
|
+
"connectors tasks runs": {
|
|
2155
|
+
"denied": {
|
|
2156
|
+
"reason": "The connectors control plane runs no dr-auth-sdk guard and checks no scope, so the `integrations` ceiling this command declares would be enforced by nobody. Closed to scoped credentials until that control plane adopts the SDK.",
|
|
2157
|
+
"until": "DR-53511"
|
|
2158
|
+
}
|
|
2159
|
+
},
|
|
2106
2160
|
"filebox list": {
|
|
2107
2161
|
"requires": [
|
|
2108
2162
|
{
|
package/dist/skills/plugins.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: schedule-drip-task
|
|
3
|
+
description: Use when a customer needs files or data pushed OUT of Datarails on a schedule through DRIP (the connectors platform) - an SFTP export, "send this CSV to the customer's SFTP every hour", "push the file to their server", "schedule an export", "scheduled task", "dr connectors tasks", "sftp_export", "lib/sftp_export", or when operating an existing schedule - "is the export running", "why did the export fail", "the task is paused", "run the export now", "pause the export", "task runs", "consecutive failures", "paused (failures)", "paused (schedule)", "no active version", "newest source file is older than max_age_seconds", "the target must not be a customer org-*-sftp container", a 403 "scheduled tasks need a support user or an org admin". Not for pulling data INTO Datarails (drip-implement-connector) or for polled SFTP ingestion (dr connectors poll).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Schedule a DRIP task
|
|
7
|
+
|
|
8
|
+
A **scheduled task** runs a published DRIP artifact on a schedule, with the org's monolith
|
|
9
|
+
datasources, and keeps a run history. The control plane only schedules; the runner runs the
|
|
10
|
+
artifact's `process()`. The first task type is **SFTP Export** (`lib/sftp_export`): copy files from
|
|
11
|
+
our hosted SFTP to a customer's SFTP under a name the customer wants.
|
|
12
|
+
|
|
13
|
+
Commands: `dr connectors tasks create | list | get | update | delete | run | pause | resume | runs`.
|
|
14
|
+
Every one takes `--server`, `--account` and `--json`; a `<task>` is its id or its name.
|
|
15
|
+
|
|
16
|
+
## Identity
|
|
17
|
+
|
|
18
|
+
The control plane allows these commands only to a **support user or an org admin** of the customer's
|
|
19
|
+
org (`role` ADMIN, SUPER_ADMIN, SUPPORT_SUPER_ADMIN, DR_CONNECT_ADMIN, or `is_support`). Run them as
|
|
20
|
+
the customer's support user: `dr login-as` it, then pass `--account <that email>`. A 403 means the
|
|
21
|
+
identity is neither; an operator's own login lands in the wrong org.
|
|
22
|
+
|
|
23
|
+
Like every `dr connectors` command, the control plane is reached over the VPN of the server's
|
|
24
|
+
region (`fetch failed` = wrong WARP vnet).
|
|
25
|
+
|
|
26
|
+
## Set up an SFTP Export
|
|
27
|
+
|
|
28
|
+
1. **Source datasource**: an SFTP datasource for the folder the producer writes on **our hosted
|
|
29
|
+
SFTP** (host must be a Datarails-hosted SFTP host; any other host is refused). Create it as in
|
|
30
|
+
`drip-implement-connector` → `reference/sftp.md` §3 (host, port, username, password or key,
|
|
31
|
+
pinned `host_key`). Note its id.
|
|
32
|
+
2. **Target datasource**: the same kind of SFTP datasource with the **customer's** server details.
|
|
33
|
+
Get the pinned key with `ssh-keyscan -p <port> <host>`; never trust on first use. A target on our
|
|
34
|
+
hosted SFTP whose login points at an `org-<id>-sftp` container is refused (a write there would
|
|
35
|
+
start an inbound sync).
|
|
36
|
+
3. **Artifact**: a thin per-org artifact in dr-connectors over `lib/sftp_export`, published and
|
|
37
|
+
made active as in `drip-implement-connector` §2. Its name is `sftp_export__<name>`, flavor `sftp`.
|
|
38
|
+
The dataquery must set its OWN `CONNECTION` (the thin connection file's name): re-exporting the
|
|
39
|
+
lib's `CONNECTION = "base_connection"` makes the publisher look for a connection file the artifact
|
|
40
|
+
does not have, and publishing fails.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# <dc>/<org>_(<domain>)/sftp_export/connections/sftp_export.py
|
|
44
|
+
from lib.sftp_export.connections.base_connection import RUNTIME_FLAVOR, SftpExportConnection as CONNECTION # noqa: F401
|
|
45
|
+
|
|
46
|
+
# <dc>/<org>_(<domain>)/sftp_export/dataqueries/<name>.py
|
|
47
|
+
from lib.sftp_export.dataqueries.export import OBJECT_TYPES, get_query_class # noqa: F401
|
|
48
|
+
|
|
49
|
+
CONNECTION = "sftp_export"
|
|
50
|
+
```
|
|
51
|
+
4. **Config**: write it to a file — the keys are in [reference/sftp-export.md](reference/sftp-export.md).
|
|
52
|
+
`objectType` must be `"export"`.
|
|
53
|
+
5. **Sample first, then the real folder.** Create the task with `target_dir` set to a scratch
|
|
54
|
+
folder the customer agreed to, run it once, and get the customer's OK on the file. A disabled
|
|
55
|
+
task cannot be run, so do not create it `--disabled` for this. Then switch the config to the
|
|
56
|
+
real folder:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
dr connectors tasks create <name> --artifact sftp_export__<name> --flavor sftp \
|
|
60
|
+
--datasource target=<target id> --datasource source=<source id> \
|
|
61
|
+
--config @config-sample.json --cron "15 * * * *" --tz UTC --server <SRV> --account <support email>
|
|
62
|
+
dr connectors tasks run <name> ... # one run now (refused within a minute of the last run)
|
|
63
|
+
dr connectors tasks runs <name> ... # status ok + summary.sent names the file
|
|
64
|
+
# customer confirms the sample →
|
|
65
|
+
dr connectors tasks update <name> --config @config.json ...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`target` is the primary role (the first `--datasource`, or `--primary-role`). Use `--every 1h` for
|
|
69
|
+
an interval instead of a cron line; the minimum is 1m.
|
|
70
|
+
|
|
71
|
+
## Operate
|
|
72
|
+
|
|
73
|
+
| Want | Command |
|
|
74
|
+
|---|---|
|
|
75
|
+
| See all tasks and their state | `tasks list` |
|
|
76
|
+
| Why did it fail | `tasks runs <task>` — each run's status and error |
|
|
77
|
+
| Run now (does not move the schedule) | `tasks run <task>` |
|
|
78
|
+
| Stop / restart | `tasks pause <task>` / `tasks resume <task>` |
|
|
79
|
+
| Change schedule, config, datasources | `tasks update <task> --cron … / --config @f / --datasource …` |
|
|
80
|
+
|
|
81
|
+
## Failures
|
|
82
|
+
|
|
83
|
+
| Symptom | Meaning / fix |
|
|
84
|
+
|---|---|
|
|
85
|
+
| 403 support user or org admin | Wrong identity — `--account` the customer's support user |
|
|
86
|
+
| State `paused (failures)` | 5 failed runs in a row. Read `runs`, fix the cause, `resume` |
|
|
87
|
+
| State `paused (schedule)` | Its timing can no longer be computed. `update --cron/--every`, then `resume` |
|
|
88
|
+
| Run `failed`: no active version / flavor differs | Publish and promote the artifact for that flavor |
|
|
89
|
+
| Run `failed`: newest source file is older than max_age_seconds | The producer is late or stopped — check it, not the task |
|
|
90
|
+
| Run `retry`: newest source file is still being written | Producer writing at run time; the run retries once after 60 s. Move the cron later |
|
|
91
|
+
| Run `timed_out` | No answer within ~45 min — runner down or the job never started |
|
|
92
|
+
| Host key refused / login failed | Datasource `host_key` or credentials are wrong — fix the datasource |
|
|
93
|
+
| 409 on `run` | Disabled, paused, a run in flight, or it ran in the last minute |
|
|
94
|
+
| 409 on `create` | Name taken, or the org already has 50 tasks |
|
|
95
|
+
| 422 on `create` | Read the detail: a secret in `--config`, an unknown cron or timezone, no active artifact |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SFTP Export task config (`lib/sftp_export`)
|
|
2
|
+
|
|
3
|
+
The task's `--config` JSON. It is validated before any login; a bad value fails the run with a
|
|
4
|
+
message naming the key. Never put a password, key or token in it — credentials live only in the
|
|
5
|
+
datasources (the control plane refuses secret-looking keys with a 422).
|
|
6
|
+
|
|
7
|
+
| Key | Required | Default | Meaning |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `objectType` | yes | — | Always `"export"` (selects the task in the artifact) |
|
|
10
|
+
| `source_dir` | yes | — | Folder on our hosted SFTP to read. Not the login's home (`""`, `.`, `out/..`) |
|
|
11
|
+
| `target_dir` | yes | — | Folder on the customer's SFTP to write. Same rule |
|
|
12
|
+
| `pattern` | no | `*` | Glob on the file name. Dotfiles are skipped unless the pattern starts with `.` |
|
|
13
|
+
| `select` | no | `all_new` | `newest`: the latest matching file, sent every run. `all_new`: each new or rewritten file once |
|
|
14
|
+
| `target_name` | no | the source name | strftime template on the run's scheduled time, plus `{source_stem}` / `{source_ext}`. With `all_new` it must contain `{source_stem}` (else every file of a run gets one name) |
|
|
15
|
+
| `target_timezone` | no | `UTC` | Zone for the `target_name` time codes. Non-UTC needs tzdata in the sftp runner image |
|
|
16
|
+
| `settle_seconds` | no | 120 | Skip files modified more recently (the producer may still be writing). 0 to 7 days |
|
|
17
|
+
| `max_age_seconds` | no | none | `newest` only: fail the run if the newest file is older than this before the scheduled time, or if no file matches (producer late or stopped) |
|
|
18
|
+
| `max_files_per_run` | no | 50 | `all_new`: at most this many files per run (1 to 1000); the rest go next run |
|
|
19
|
+
|
|
20
|
+
Behaviour worth knowing:
|
|
21
|
+
|
|
22
|
+
- Upload is atomic: a random `.<name>.<hex>.part` is written, then renamed. The customer's login must
|
|
23
|
+
be allowed to create and rename files in `target_dir`. Stale parts of ours older than 2 h are swept.
|
|
24
|
+
- The source is never written, moved or deleted. Cleaning it is the producer's job; with `all_new`,
|
|
25
|
+
a folder that keeps every sent file eventually fails the run with "archive or delete them".
|
|
26
|
+
- A target that is the source folder (same login, same real path) is refused.
|
|
27
|
+
|
|
28
|
+
## Example: an hourly snapshot to the customer, named by the hour (UTC)
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"objectType": "export",
|
|
33
|
+
"source_dir": "/home",
|
|
34
|
+
"pattern": "Daily_Snapshot_*.csv",
|
|
35
|
+
"select": "newest",
|
|
36
|
+
"target_dir": "/inbound",
|
|
37
|
+
"target_name": "Sales_%H%M_%m%d%Y.csv",
|
|
38
|
+
"target_timezone": "UTC",
|
|
39
|
+
"max_age_seconds": 3600
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With cron `15 * * * *`, the 13:15 run sends the newest snapshot (written no earlier than 12:15) as
|
|
44
|
+
`Sales_1315_10192026.csv`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datarails-ai/dr-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.144",
|
|
4
4
|
"description": "Datarails CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@datarails-ai/dr-appspace-cli": "0.12.
|
|
20
|
+
"@datarails-ai/dr-appspace-cli": "0.12.16",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|