@datarails-ai/dr-cli 1.6.143 → 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.143",
11
+ version: "1.6.144",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -69,7 +69,7 @@ var package_default = {
69
69
  };
70
70
 
71
71
  // src/buildInfo.ts
72
- var BUILD_TIME = "2026-10-19T11:22:23.269Z";
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 collect = (plan, res) => {
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
- collect(p, r.value);
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) collect(plans[item.planIndex], r.value);
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 call of calls) {
33160
- const upper = call.name.toUpperCase();
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 (call.argCount >= arity.min && call.argCount <= arity.max) continue;
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: call.name,
33168
- got: call.argCount,
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, call) {
45845
+ async function explainNotPolled(filebox, call2) {
45846
45846
  try {
45847
- return await call;
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 BASE2 = "/api/spend-control/v1";
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
- `${BASE2}/catalogs/billing-frequencies/`,
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 === "" ? `${BASE2}/contracts/` : `${BASE2}/contracts/?${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(`${BASE2}/contracts/${contractId}${suffix}`, options);
66915
+ return client.getJSON(`${BASE3}/contracts/${contractId}${suffix}`, options);
66454
66916
  }
66455
66917
  async function deleteContract(client, contractId, options) {
66456
- await client.deleteJSON(`${BASE2}/contracts/${contractId}`, options);
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
- `${BASE2}/contracts/files-with-ai`,
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
- `${BASE2}/contracts/file`,
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
- `${BASE2}/contracts/${contractId}/files${suffix}`,
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
- `${BASE2}/contracts/files/${fileId}/download`,
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(`${BASE2}/contracts/files/${fileId}`, options);
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
- `${BASE2}/contracts/bulk-create`,
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(`${BASE2}/contracts/${contractId}`, body, options);
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 BASE3 = "/api/spend-control/v1/vendors";
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(`${BASE3}/`, params), options);
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(`${BASE3}/app-store`, params),
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(`${BASE3}/with-contracts`, params),
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(`${BASE3}/categories`, options);
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(`${BASE3}/with-multiple-contracts`, options);
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(`${BASE3}/${encodeURIComponent(vendorId)}/owners/`, options);
67596
+ return client.getJSON(`${BASE4}/${encodeURIComponent(vendorId)}/owners/`, options);
67135
67597
  }
67136
67598
  async function createVendor(client, name, options) {
67137
- return client.postJSON(`${BASE3}/`, { name }, options);
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(`${BASE3}/visibility`, { vendors }, options);
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, call) {
70373
- if (out === null) throw new Error(`${call}: empty response body`);
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 readFileSync3,
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 = readFileSync3(mountPath);
72853
+ const bytes = readFileSync4(mountPath);
72392
72854
  mkdirSync(dirname10(target), { recursive: true });
72393
72855
  const fd = openSync(temp, "wx", 384);
72394
72856
  created = true;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.143",
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
  {
@@ -10,6 +10,7 @@
10
10
  "dr-error-enrichment-reconcile",
11
11
  "dr-filebox-api-setup",
12
12
  "drip-implement-connector",
13
+ "schedule-drip-task",
13
14
  "validate-workato-migration"
14
15
  ],
15
16
  "dr-cli": [
@@ -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.143",
3
+ "version": "1.6.144",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {