@openfn/cli 1.25.0 → 1.27.0

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/index.js CHANGED
@@ -51,15 +51,15 @@ import nodePath from "node:path";
51
51
  var DEFAULT_REPO_DIR = "/tmp/openfn/repo";
52
52
 
53
53
  // src/util/expand-adaptors.ts
54
- var expand2 = (name) => {
55
- if (typeof name === "string") {
56
- const [left] = name.split("=");
54
+ var expand2 = (name2) => {
55
+ if (typeof name2 === "string") {
56
+ const [left] = name2.split("=");
57
57
  if (left.match("/") || left.endsWith(".js")) {
58
- return name;
58
+ return name2;
59
59
  }
60
- return `@openfn/language-${name}`;
60
+ return `@openfn/language-${name2}`;
61
61
  }
62
- return name;
62
+ return name2;
63
63
  };
64
64
  var expand_adaptors_default = (input) => {
65
65
  if (Array.isArray(input)) {
@@ -982,6 +982,21 @@ var workspace = {
982
982
  }
983
983
  }
984
984
  };
985
+ var newProject = {
986
+ name: "new",
987
+ yargs: {
988
+ description: "Create a new project when deploying",
989
+ default: false,
990
+ boolean: true
991
+ }
992
+ };
993
+ var name = {
994
+ name: "name",
995
+ yargs: {
996
+ type: "string",
997
+ description: "When deploying a new project, set the name"
998
+ }
999
+ };
985
1000
 
986
1001
  // src/deploy/command.ts
987
1002
  var options3 = [
@@ -1240,17 +1255,18 @@ var command2 = {
1240
1255
  var version_default = command2;
1241
1256
 
1242
1257
  // src/projects/merge.ts
1243
- import Project3, { Workspace as Workspace4 } from "@openfn/project";
1258
+ import Project3, { Workspace as Workspace5 } from "@openfn/project";
1244
1259
 
1245
1260
  // src/projects/checkout.ts
1246
- import Project2, { Workspace as Workspace3 } from "@openfn/project";
1261
+ import Project2, { Workspace as Workspace4 } from "@openfn/project";
1247
1262
  import { rimraf as rimraf2 } from "rimraf";
1248
1263
 
1249
1264
  // src/projects/util.ts
1250
1265
  import { rimraf } from "rimraf";
1266
+ import { versionsEqual } from "@openfn/project";
1251
1267
 
1252
1268
  // src/projects/checkout.ts
1253
- var options11 = [log, workspace, clean];
1269
+ var options11 = [log, workspace, clean, force];
1254
1270
  var command3 = {
1255
1271
  command: "checkout <project>",
1256
1272
  describe: "Switch to a different OpenFn project in the same workspace",
@@ -1297,7 +1313,7 @@ var command4 = {
1297
1313
  var merge_default = command4;
1298
1314
 
1299
1315
  // src/projects/fetch.ts
1300
- import Project4, { Workspace as Workspace5 } from "@openfn/project";
1316
+ import Project4, { Workspace as Workspace6 } from "@openfn/project";
1301
1317
  var options13 = [
1302
1318
  alias,
1303
1319
  apiKey,
@@ -1328,7 +1344,7 @@ var command5 = {
1328
1344
  var fetch_default = command5;
1329
1345
 
1330
1346
  // src/projects/pull.ts
1331
- import { Workspace as Workspace6 } from "@openfn/project";
1347
+ import { Workspace as Workspace7 } from "@openfn/project";
1332
1348
  var options14 = [
1333
1349
  alias,
1334
1350
  env2,
@@ -1357,12 +1373,15 @@ var command6 = {
1357
1373
  };
1358
1374
 
1359
1375
  // src/projects/deploy.ts
1360
- import Project5 from "@openfn/project";
1376
+ import Project5, { versionsEqual as versionsEqual2, Workspace as Workspace8 } from "@openfn/project";
1361
1377
  import c2 from "chalk";
1362
1378
  var options15 = [
1363
1379
  env2,
1364
1380
  workspace,
1365
1381
  dryRun2,
1382
+ newProject,
1383
+ name,
1384
+ alias,
1366
1385
  apiKey,
1367
1386
  endpoint,
1368
1387
  log,
@@ -1373,6 +1392,7 @@ var options15 = [
1373
1392
  ];
1374
1393
  var command7 = {
1375
1394
  command: "deploy",
1395
+ aliases: "push",
1376
1396
  describe: `Deploy the checked out project to a Lightning Instance`,
1377
1397
  builder: (yargs2) => build(options15, yargs2).positional("project", {
1378
1398
  describe: "The UUID, local id or local alias of the project to deploy to"
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
3
+ for (var name2 in all)
4
+ __defProp(target, name2, { get: all[name2], enumerable: true });
5
5
  };
6
6
 
7
7
  // src/apollo/handler.ts
@@ -133,14 +133,14 @@ var callApollo = async (apolloBaseUrl, serviceName, payload, logger) => {
133
133
  });
134
134
  });
135
135
  };
136
- var loadPayload = async (logger, path17) => {
137
- if (!path17) {
136
+ var loadPayload = async (logger, path18) => {
137
+ if (!path18) {
138
138
  logger.warn("No JSON payload provided");
139
139
  logger.warn("Most apollo services require JSON to be uploaded");
140
140
  return {};
141
141
  }
142
- if (path17.endsWith(".json")) {
143
- const str = await readFile(path17, "utf8");
142
+ if (path18.endsWith(".json")) {
143
+ const str = await readFile(path18, "utf8");
144
144
  const json = JSON.parse(str);
145
145
  logger.debug("Loaded JSON payload");
146
146
  return json;
@@ -169,14 +169,14 @@ var namespaces = {
169
169
  [COMPILER]: "CMP",
170
170
  [JOB]: "JOB"
171
171
  };
172
- var createLogger2 = (name = "", options8) => {
172
+ var createLogger2 = (name2 = "", options8) => {
173
173
  const logOptions = options8.log || {};
174
174
  let json = false;
175
- let level = logOptions[name] || logOptions.default || "default";
175
+ let level = logOptions[name2] || logOptions.default || "default";
176
176
  if (options8.logJson) {
177
177
  json = true;
178
178
  }
179
- return actualCreateLogger(namespaces[name] || name, {
179
+ return actualCreateLogger(namespaces[name2] || name2, {
180
180
  level,
181
181
  json,
182
182
  sanitize: options8.sanitize || "none",
@@ -276,13 +276,13 @@ var execute_default = async (plan, input, opts, logger) => {
276
276
  };
277
277
  function parseAdaptors(plan) {
278
278
  const extractInfo = (specifier) => {
279
- const [module, path17] = specifier.split("=");
280
- const { name, version } = getNameAndVersion(module);
279
+ const [module, path18] = specifier.split("=");
280
+ const { name: name2, version } = getNameAndVersion(module);
281
281
  const info = {
282
- name
282
+ name: name2
283
283
  };
284
- if (path17) {
285
- info.path = path17;
284
+ if (path18) {
285
+ info.path = path18;
286
286
  }
287
287
  if (version) {
288
288
  info.version = version;
@@ -293,8 +293,8 @@ function parseAdaptors(plan) {
293
293
  Object.values(plan.workflow.steps).forEach((step) => {
294
294
  const job = step;
295
295
  job.adaptors?.forEach((adaptor) => {
296
- const { name, ...maybeVersionAndPath } = extractInfo(adaptor);
297
- adaptors[name] = maybeVersionAndPath;
296
+ const { name: name2, ...maybeVersionAndPath } = extractInfo(adaptor);
297
+ adaptors[name2] = maybeVersionAndPath;
298
298
  });
299
299
  });
300
300
  return adaptors;
@@ -410,12 +410,12 @@ var install = async (opts, log2 = defaultLogger) => {
410
410
  return [];
411
411
  };
412
412
  var removePackage = async (packageSpecifier, repoDir, logger) => {
413
- const { name, version } = getNameAndVersion2(packageSpecifier);
413
+ const { name: name2, version } = getNameAndVersion2(packageSpecifier);
414
414
  if (!version) {
415
415
  logger.warn(`Cannot remove ${packageSpecifier}: no version specified`);
416
416
  return;
417
417
  }
418
- const aliasedName = `${name}_${version}`;
418
+ const aliasedName = `${name2}_${version}`;
419
419
  logger.info(`Removing package ${aliasedName} from repo...`);
420
420
  try {
421
421
  await new Promise((resolve, reject) => {
@@ -461,11 +461,11 @@ var getDependencyList = async (options8, _logger) => {
461
461
  const result = {};
462
462
  if (pkg) {
463
463
  Object.keys(pkg.dependencies).forEach((key) => {
464
- const [name, version] = key.split("_");
465
- if (!result[name]) {
466
- result[name] = [];
464
+ const [name2, version] = key.split("_");
465
+ if (!result[name2]) {
466
+ result[name2] = [];
467
467
  }
468
- result[name].push(version);
468
+ result[name2].push(version);
469
469
  });
470
470
  }
471
471
  return result;
@@ -592,10 +592,10 @@ var stripVersionSpecifier = (specifier) => {
592
592
  return specifier;
593
593
  };
594
594
  var resolveSpecifierPath = async (pattern, repoDir, log2) => {
595
- const [specifier, path17] = pattern.split("=");
596
- if (path17) {
597
- log2.debug(`Resolved ${specifier} to path: ${path17}`);
598
- return path17;
595
+ const [specifier, path18] = pattern.split("=");
596
+ if (path18) {
597
+ log2.debug(`Resolved ${specifier} to path: ${path18}`);
598
+ return path18;
599
599
  }
600
600
  const repoPath = await getModulePath(specifier, repoDir, log2);
601
601
  if (repoPath) {
@@ -614,12 +614,12 @@ var loadTransformOptions = async (opts, log2) => {
614
614
  let exports;
615
615
  const [specifier] = adaptorInput.split("=");
616
616
  log2.debug(`Trying to preload types for ${specifier}`);
617
- const path17 = await resolveSpecifierPath(adaptorInput, opts.repoDir, log2);
618
- if (path17) {
617
+ const path18 = await resolveSpecifierPath(adaptorInput, opts.repoDir, log2);
618
+ if (path18) {
619
619
  try {
620
- exports = await preloadAdaptorExports(path17, log2);
620
+ exports = await preloadAdaptorExports(path18, log2);
621
621
  } catch (e) {
622
- log2.error(`Failed to load adaptor typedefs from path ${path17}`);
622
+ log2.error(`Failed to load adaptor typedefs from path ${path18}`);
623
623
  log2.error(e);
624
624
  }
625
625
  }
@@ -765,15 +765,15 @@ import { isPath } from "@openfn/compiler";
765
765
  import { Workspace, yamlToJson } from "@openfn/project";
766
766
 
767
767
  // src/util/expand-adaptors.ts
768
- var expand = (name) => {
769
- if (typeof name === "string") {
770
- const [left] = name.split("=");
768
+ var expand = (name2) => {
769
+ if (typeof name2 === "string") {
770
+ const [left] = name2.split("=");
771
771
  if (left.match("/") || left.endsWith(".js")) {
772
- return name;
772
+ return name2;
773
773
  }
774
- return `@openfn/language-${name}`;
774
+ return `@openfn/language-${name2}`;
775
775
  }
776
- return name;
776
+ return name2;
777
777
  };
778
778
  var expand_adaptors_default = (input) => {
779
779
  if (Array.isArray(input)) {
@@ -808,16 +808,16 @@ var updatePath = (adaptor, repoPath, log2) => {
808
808
  if (adaptor.match("=")) {
809
809
  return adaptor;
810
810
  }
811
- const { name, version } = getNameAndVersion3(adaptor);
811
+ const { name: name2, version } = getNameAndVersion3(adaptor);
812
812
  if (version) {
813
813
  log2.warn(
814
814
  `Warning: Ignoring version specifier on ${adaptor} as loading from the adaptors monorepo`
815
815
  );
816
816
  }
817
- const shortName = name.replace("@openfn/language-", "");
817
+ const shortName = name2.replace("@openfn/language-", "");
818
818
  const abspath = path3.resolve(repoPath, "packages", shortName);
819
- log2.info(`Mapped adaptor ${name} to monorepo: ${abspath}`);
820
- return `${name}=${abspath}`;
819
+ log2.info(`Mapped adaptor ${name2} to monorepo: ${abspath}`);
820
+ return `${name2}=${abspath}`;
821
821
  };
822
822
  var mapAdaptorsToMonorepo = (monorepoPath = "", input = [], log2) => {
823
823
  if (monorepoPath) {
@@ -843,8 +843,8 @@ var map_adaptors_to_monorepo_default = mapAdaptorsToMonorepo;
843
843
  // src/util/resolve-path.ts
844
844
  import nodepath from "node:path";
845
845
  import os from "node:os";
846
- var resolve_path_default = (path17, root) => {
847
- return path17.startsWith("~") ? path17.replace(`~`, os.homedir) : nodepath.resolve(root ?? "", path17);
846
+ var resolve_path_default = (path18, root) => {
847
+ return path18.startsWith("~") ? path18.replace(`~`, os.homedir) : nodepath.resolve(root ?? "", path18);
848
848
  };
849
849
 
850
850
  // src/util/load-plan.ts
@@ -858,10 +858,10 @@ var loadPlan = async (options8, logger) => {
858
858
  );
859
859
  const workspace2 = new Workspace(options8.workspace);
860
860
  const proj = await workspace2.getCheckedOutProject();
861
- const name = workflowName || options8.workflow;
862
- const workflow2 = proj?.getWorkflow(name);
861
+ const name2 = workflowName || options8.workflow;
862
+ const workflow2 = proj?.getWorkflow(name2);
863
863
  if (!workflow2) {
864
- const e = new Error(`Could not find Workflow "${name}"`);
864
+ const e = new Error(`Could not find Workflow "${name2}"`);
865
865
  delete e.stack;
866
866
  throw e;
867
867
  }
@@ -870,7 +870,7 @@ var loadPlan = async (options8, logger) => {
870
870
  };
871
871
  options8.credentials ??= workspace2.getConfig().credentials;
872
872
  options8.collectionsEndpoint ??= proj.openfn?.endpoint;
873
- options8.cachePath ??= workspace2.workflowsPath + `/${name}/${CACHE_DIR}`;
873
+ options8.cachePath ??= workspace2.workflowsPath + `/${name2}/${CACHE_DIR}`;
874
874
  }
875
875
  if (options8.path && /ya?ml$/.test(options8.path)) {
876
876
  const content = await fs3.readFile(path4.resolve(options8.path), "utf-8");
@@ -945,7 +945,7 @@ var loadExpression = async (options8, logger) => {
945
945
  logger.debug(`Loading expression from ${expressionPath}`);
946
946
  try {
947
947
  const expression = await fs3.readFile(expressionPath, "utf8");
948
- const name = path4.parse(expressionPath).name;
948
+ const name2 = path4.parse(expressionPath).name;
949
949
  const step = {
950
950
  expression,
951
951
  // The adaptor should have been expanded nicely already, so we don't need intervene here
@@ -955,7 +955,7 @@ var loadExpression = async (options8, logger) => {
955
955
  maybeAssign(options8, wfOptions, ["timeout"]);
956
956
  const plan = {
957
957
  workflow: {
958
- name,
958
+ name: name2,
959
959
  steps: [step],
960
960
  globals: options8.globals
961
961
  },
@@ -990,7 +990,7 @@ var loadOldWorkflow = async (workflow2, options8, logger, defaultName = "") => {
990
990
  return final;
991
991
  };
992
992
  var fetchFile = async (fileInfo, log2) => {
993
- const { rootDir = "", filePath, name } = fileInfo;
993
+ const { rootDir = "", filePath, name: name2 } = fileInfo;
994
994
  try {
995
995
  const fullPath = resolve_path_default(filePath, rootDir);
996
996
  const result = await fs3.readFile(fullPath, "utf8");
@@ -999,7 +999,7 @@ var fetchFile = async (fileInfo, log2) => {
999
999
  } catch (e) {
1000
1000
  abort_default(
1001
1001
  log2,
1002
- `File not found for ${name}: ${filePath}`,
1002
+ `File not found for ${name2}: ${filePath}`,
1003
1003
  void 0,
1004
1004
  `This workflow references a file which cannot be found at ${filePath}
1005
1005
 
@@ -1148,8 +1148,8 @@ var loadXPlan = async (plan, options8, logger, defaultName = "") => {
1148
1148
  };
1149
1149
 
1150
1150
  // src/util/assert-path.ts
1151
- var assert_path_default = (path17) => {
1152
- if (!path17) {
1151
+ var assert_path_default = (path18) => {
1152
+ if (!path18) {
1153
1153
  console.error("ERROR: no path provided!");
1154
1154
  console.error("\nUsage:");
1155
1155
  console.error(" open path/to/job");
@@ -1212,7 +1212,8 @@ var assertStepStructure = (step, index) => {
1212
1212
  "state",
1213
1213
  "configuration",
1214
1214
  "linker",
1215
- "openfn"
1215
+ "openfn",
1216
+ "enabled"
1216
1217
  ];
1217
1218
  for (const key in step) {
1218
1219
  if (!allowedKeys.includes(key)) {
@@ -1781,8 +1782,10 @@ import {
1781
1782
  } from "@openfn/deploy";
1782
1783
 
1783
1784
  // src/projects/deploy.ts
1784
- import Project from "@openfn/project";
1785
+ import Project, { versionsEqual as versionsEqual2, Workspace as Workspace3 } from "@openfn/project";
1785
1786
  import c2 from "chalk";
1787
+ import { writeFile as writeFile6 } from "node:fs/promises";
1788
+ import path10 from "node:path";
1786
1789
 
1787
1790
  // src/util/ensure-log-opts.ts
1788
1791
  var defaultLoggerOptions = {
@@ -2035,6 +2038,21 @@ var workspace = {
2035
2038
  }
2036
2039
  }
2037
2040
  };
2041
+ var newProject = {
2042
+ name: "new",
2043
+ yargs: {
2044
+ description: "Create a new project when deploying",
2045
+ default: false,
2046
+ boolean: true
2047
+ }
2048
+ };
2049
+ var name = {
2050
+ name: "name",
2051
+ yargs: {
2052
+ type: "string",
2053
+ description: "When deploying a new project, set the name"
2054
+ }
2055
+ };
2038
2056
 
2039
2057
  // src/projects/util.ts
2040
2058
  import path9 from "node:path";
@@ -2049,11 +2067,12 @@ var CLIError = class extends Error {
2049
2067
 
2050
2068
  // src/projects/util.ts
2051
2069
  import { rimraf } from "rimraf";
2070
+ import { versionsEqual } from "@openfn/project";
2052
2071
  var loadAppAuthConfig = (options8, logger) => {
2053
2072
  const { OPENFN_API_KEY, OPENFN_ENDPOINT } = process.env;
2054
2073
  const config2 = {
2055
2074
  apiKey: options8.apiKey,
2056
- endpoint: options8.endpoint
2075
+ endpoint: options8.endpoint ?? "https://app.openfn.org"
2057
2076
  };
2058
2077
  if (!options8.apiKey && OPENFN_API_KEY) {
2059
2078
  logger.info("Using OPENFN_API_KEY environment variable");
@@ -2063,6 +2082,11 @@ var loadAppAuthConfig = (options8, logger) => {
2063
2082
  logger.info("Using OPENFN_ENDPOINT environment variable");
2064
2083
  config2.endpoint = OPENFN_ENDPOINT;
2065
2084
  }
2085
+ if (!config2.apiKey) {
2086
+ throw new CLIError(
2087
+ "OPENFN_API_KEY is required. Set it in .env, pass --api-key, or set the environment variable."
2088
+ );
2089
+ }
2066
2090
  return config2;
2067
2091
  };
2068
2092
  var ensureExt = (filePath, ext) => {
@@ -2096,10 +2120,15 @@ var serialize = async (project, outputPath2, formatOverride, dryRun2 = false) =>
2096
2120
  }
2097
2121
  return finalPath;
2098
2122
  };
2099
- var getLightningUrl = (endpoint2, path17 = "", snapshots2) => {
2123
+ var getLightningUrl = (endpoint2, path18 = "", snapshots2) => {
2124
+ if (!endpoint2) {
2125
+ throw new CLIError(
2126
+ "OPENFN_ENDPOINT is required. Set it in .env, pass --endpoint, or set the environment variable.\nExample: OPENFN_ENDPOINT=https://app.openfn.org"
2127
+ );
2128
+ }
2100
2129
  const params = new URLSearchParams();
2101
2130
  snapshots2?.forEach((snapshot) => params.append("snapshots[]", snapshot));
2102
- return new URL(`/api/provision/${path17}?${params.toString()}`, endpoint2);
2131
+ return new URL(`/api/provision/${path18}?${params.toString()}`, endpoint2);
2103
2132
  };
2104
2133
  async function fetchProject(endpoint2, apiKey2, projectId, logger, snapshots2) {
2105
2134
  const url2 = getLightningUrl(endpoint2, projectId, snapshots2);
@@ -2143,9 +2172,16 @@ async function deployProject(endpoint2, apiKey2, state, logger) {
2143
2172
  body: JSON.stringify(state)
2144
2173
  });
2145
2174
  if (!response.ok) {
2146
- const body = await response.json();
2175
+ logger?.error(`Deploy failed with code `, response.status);
2147
2176
  logger?.error("Failed to deploy project:");
2148
- logger?.error(JSON.stringify(body, null, 2));
2177
+ const contentType = response.headers.get("content-type") ?? "";
2178
+ if (contentType.match("application/json ")) {
2179
+ const body = await response.json();
2180
+ logger?.error(JSON.stringify(body, null, 2));
2181
+ } else {
2182
+ const content = await response.text();
2183
+ logger?.error(content);
2184
+ }
2149
2185
  throw new CLIError(
2150
2186
  `Failed to deploy project ${state.name}: ${response.status}`
2151
2187
  );
@@ -2175,9 +2211,9 @@ async function tidyWorkflowDir(currentProject, incomingProject, dryRun2 = false)
2175
2211
  const currentFiles = currentProject.serialize("fs");
2176
2212
  const newFiles = incomingProject.serialize("fs");
2177
2213
  const toRemove = [];
2178
- for (const path17 in currentFiles) {
2179
- if (!newFiles[path17]) {
2180
- toRemove.push(path17);
2214
+ for (const path18 in currentFiles) {
2215
+ if (!newFiles[path18]) {
2216
+ toRemove.push(path18);
2181
2217
  }
2182
2218
  }
2183
2219
  if (!dryRun2) {
@@ -2185,6 +2221,41 @@ async function tidyWorkflowDir(currentProject, incomingProject, dryRun2 = false)
2185
2221
  }
2186
2222
  return toRemove.sort();
2187
2223
  }
2224
+ var updateForkedFrom = (proj) => {
2225
+ proj.cli.forked_from = proj.workflows.reduce((obj, wf) => {
2226
+ if (wf.history.length) {
2227
+ obj[wf.id] = wf.history.at(-1);
2228
+ }
2229
+ return obj;
2230
+ }, {});
2231
+ return proj;
2232
+ };
2233
+ var findLocallyChangedWorkflows = async (workspace2, project, ifNoForkedFrom = "assume-diverged") => {
2234
+ const { forked_from } = workspace2.activeProject ?? {};
2235
+ if (!forked_from || Object.keys(forked_from).length === 0) {
2236
+ if (ifNoForkedFrom === "assume-ok") {
2237
+ return [];
2238
+ }
2239
+ return project.workflows.map((w) => w.id);
2240
+ }
2241
+ const changedWorkflows = [];
2242
+ for (const workflow2 of project.workflows) {
2243
+ const currentHash = workflow2.getVersionHash();
2244
+ const forkedHash = forked_from[workflow2.id];
2245
+ if (forkedHash === void 0) {
2246
+ changedWorkflows.push(workflow2.id);
2247
+ } else if (!versionsEqual(currentHash, forkedHash)) {
2248
+ changedWorkflows.push(workflow2.id);
2249
+ }
2250
+ }
2251
+ const currentWorkflowIds = new Set(project.workflows.map((w) => w.id));
2252
+ for (const workflowId in forked_from) {
2253
+ if (!currentWorkflowIds.has(workflowId)) {
2254
+ changedWorkflows.push(workflowId);
2255
+ }
2256
+ }
2257
+ return changedWorkflows;
2258
+ };
2188
2259
 
2189
2260
  // src/util/command-builders.ts
2190
2261
  import c from "chalk";
@@ -2237,6 +2308,9 @@ var options = [
2237
2308
  env,
2238
2309
  workspace,
2239
2310
  dryRun,
2311
+ newProject,
2312
+ name,
2313
+ alias,
2240
2314
  apiKey,
2241
2315
  endpoint,
2242
2316
  log,
@@ -2248,6 +2322,7 @@ var options = [
2248
2322
  var printProjectName = (project) => `${project.id} (${project.openfn?.uuid || "<no UUID>"})`;
2249
2323
  var command = {
2250
2324
  command: "deploy",
2325
+ aliases: "push",
2251
2326
  describe: `Deploy the checked out project to a Lightning Instance`,
2252
2327
  builder: (yargs) => build(options, yargs).positional("project", {
2253
2328
  describe: "The UUID, local id or local alias of the project to deploy to"
@@ -2257,16 +2332,25 @@ var command = {
2257
2332
  ),
2258
2333
  handler: ensure("project-deploy", options)
2259
2334
  };
2260
- async function handler(options8, logger) {
2261
- logger.warn(
2262
- "WARNING: the project deploy command is in BETA and may not be stable. Use cautiously on production projects."
2263
- );
2264
- const config2 = loadAppAuthConfig(options8, logger);
2335
+ var hasRemoteDiverged = (local, remote, workflows = []) => {
2336
+ let diverged = null;
2337
+ const refs = local.cli.forked_from ?? {};
2338
+ const filteredWorkflows = workflows.length ? local.workflows.filter((w) => workflows.includes(w.id)) : local.workflows;
2339
+ for (const wf of filteredWorkflows) {
2340
+ if (wf.id in refs) {
2341
+ const forkedVersion = refs[wf.id];
2342
+ const remoteVersion = remote.getWorkflow(wf.id)?.history.at(-1);
2343
+ if (!versionsEqual2(forkedVersion, remoteVersion)) {
2344
+ diverged ??= [];
2345
+ diverged.push(wf.id);
2346
+ }
2347
+ } else {
2348
+ }
2349
+ }
2350
+ return diverged;
2351
+ };
2352
+ var syncProjects = async (options8, config2, ws, localProject, logger) => {
2265
2353
  logger.info("Attempting to load checked-out project from workspace");
2266
- const localProject = await Project.from("fs", {
2267
- root: options8.workspace || "."
2268
- });
2269
- logger.success(`Loaded local project ${printProjectName(localProject)}`);
2270
2354
  let remoteProject;
2271
2355
  try {
2272
2356
  const { data } = await fetchProject(
@@ -2289,47 +2373,92 @@ async function handler(options8, logger) {
2289
2373
  Your local project (${localProject.uuid}) has a different UUID to the remote project (${remoteProject.uuid}).
2290
2374
 
2291
2375
  Pass --force to override this error and deploy anyway.`);
2292
- return false;
2376
+ process.exit(1);
2293
2377
  }
2294
- const diffs = reportDiff(remoteProject, localProject, logger);
2378
+ const locallyChangedWorkflows = await findLocallyChangedWorkflows(
2379
+ ws,
2380
+ localProject
2381
+ );
2382
+ const diffs = reportDiff(
2383
+ localProject,
2384
+ remoteProject,
2385
+ locallyChangedWorkflows,
2386
+ logger
2387
+ );
2295
2388
  if (!diffs.length) {
2296
2389
  logger.success("Nothing to deploy");
2297
- return;
2390
+ process.exit(0);
2298
2391
  }
2299
- const skipVersionTest = localProject.workflows.find((wf) => wf.history.length === 0) || remoteProject.workflows.find((wf) => wf.history.length === 0);
2392
+ const skipVersionTest = remoteProject.workflows.find(
2393
+ (wf) => wf.history.length === 0
2394
+ );
2300
2395
  if (skipVersionTest) {
2301
2396
  logger.warn(
2302
2397
  "Skipping compatibility check as no local version history detected"
2303
2398
  );
2304
- logger.warn("Pushing these changes may overrite changes made to the app");
2305
- } else if (!localProject.canMergeInto(remoteProject)) {
2306
- if (!options8.force) {
2307
- logger.error(`Error: Projects have diverged!
2399
+ logger.warn("Pushing these changes may overwrite changes made to the app");
2400
+ } else {
2401
+ const divergentWorkflows = hasRemoteDiverged(
2402
+ localProject,
2403
+ remoteProject,
2404
+ locallyChangedWorkflows
2405
+ );
2406
+ if (divergentWorkflows) {
2407
+ logger.warn(
2408
+ `The following workflows have diverged: ${divergentWorkflows}`
2409
+ );
2410
+ if (!options8.force) {
2411
+ logger.error(`Error: Projects have diverged!
2308
2412
 
2309
- The remote project has been edited since the local project was branched. Changes may be lost.
2413
+ The remote project has been edited since the local project was branched. Changes may be lost.
2310
2414
 
2311
- Pass --force to override this error and deploy anyway.`);
2312
- return;
2415
+ Pass --force to override this error and deploy anyway.`);
2416
+ process.exit(1);
2417
+ } else {
2418
+ logger.warn(
2419
+ "Remote project has diverged from local project! Pushing anyway as -f passed"
2420
+ );
2421
+ }
2313
2422
  } else {
2314
- logger.warn(
2315
- "Remote project has not diverged from local project! Pushing anyway as -f passed"
2423
+ logger.info(
2424
+ "Remote project has not diverged from local project - it is safe to deploy \u{1F389}"
2316
2425
  );
2317
2426
  }
2318
- } else {
2319
- logger.info(
2320
- "Remote project has not diverged from local project - it is safe to deploy \u{1F389}"
2321
- );
2322
2427
  }
2323
2428
  logger.info("Merging changes into remote project");
2324
2429
  const merged = Project.merge(localProject, remoteProject, {
2325
2430
  mode: "replace",
2326
- force: true
2431
+ force: true,
2432
+ onlyUpdated: true
2433
+ });
2434
+ return merged;
2435
+ };
2436
+ async function handler(options8, logger) {
2437
+ logger.warn(
2438
+ "WARNING: the project deploy command is in BETA and may not be stable. Use cautiously on production projects."
2439
+ );
2440
+ const config2 = loadAppAuthConfig(options8, logger);
2441
+ const ws = new Workspace3(options8.workspace || ".");
2442
+ const active = ws.getActiveProject();
2443
+ const alias2 = options8.alias ?? active?.alias;
2444
+ const localProject = await Project.from("fs", {
2445
+ root: options8.workspace || ".",
2446
+ alias: alias2,
2447
+ name: options8.name
2327
2448
  });
2449
+ if (options8.new) {
2450
+ localProject.openfn = {
2451
+ endpoint: config2.endpoint
2452
+ };
2453
+ }
2454
+ logger.success(`Loaded local project ${printProjectName(localProject)}`);
2455
+ const merged = options8.new ? localProject : await syncProjects(options8, config2, ws, localProject, logger);
2328
2456
  const state = merged.serialize("state", {
2329
2457
  format: "json"
2330
2458
  });
2331
- logger.debug("Converted merged local project to app state:");
2459
+ logger.debug("Provisioner state ready to upload:");
2332
2460
  logger.debug(JSON.stringify(state, null, 2));
2461
+ logger.debug();
2333
2462
  config2.endpoint ??= localProject.openfn?.endpoint;
2334
2463
  if (options8.dryRun) {
2335
2464
  logger.always("dryRun option set: skipping upload step");
@@ -2353,18 +2482,25 @@ Pass --force to override this error and deploy anyway.`);
2353
2482
  "state",
2354
2483
  result,
2355
2484
  {
2356
- endpoint: config2.endpoint
2485
+ endpoint: config2.endpoint,
2486
+ alias: alias2
2357
2487
  },
2358
2488
  merged.config
2359
2489
  );
2490
+ updateForkedFrom(finalProject);
2491
+ const configData = finalProject.generateConfig();
2492
+ await writeFile6(
2493
+ path10.resolve(options8.workspace, configData.path),
2494
+ configData.content
2495
+ );
2360
2496
  const finalOutputPath = getSerializePath(localProject, options8.workspace);
2361
- logger.debug("Updating local project at ", finalOutputPath);
2362
- await serialize(finalProject, finalOutputPath);
2497
+ const fullFinalPath = await serialize(finalProject, finalOutputPath);
2498
+ logger.debug("Updated local project at ", fullFinalPath);
2499
+ logger.success("Updated project at", config2.endpoint);
2363
2500
  }
2364
- logger.success("Updated project at", config2.endpoint);
2365
2501
  }
2366
- var reportDiff = (local, remote, logger) => {
2367
- const diffs = remote.diff(local);
2502
+ var reportDiff = (local, remote, locallyChangedWorkflows, logger) => {
2503
+ const diffs = remote.diff(local, locallyChangedWorkflows);
2368
2504
  if (diffs.length === 0) {
2369
2505
  logger.info("No workflow changes detected");
2370
2506
  return diffs;
@@ -2451,30 +2587,30 @@ function pickFirst(...args) {
2451
2587
  var handler_default6 = deployHandler;
2452
2588
 
2453
2589
  // src/docgen/handler.ts
2454
- import { writeFile as writeFile6 } from "node:fs/promises";
2590
+ import { writeFile as writeFile7 } from "node:fs/promises";
2455
2591
  import { readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
2456
- import path10 from "node:path";
2592
+ import path11 from "node:path";
2457
2593
  import { describePackage } from "@openfn/describe-package";
2458
2594
  import { getNameAndVersion as getNameAndVersion4 } from "@openfn/runtime";
2459
2595
  var RETRY_DURATION = 500;
2460
2596
  var RETRY_COUNT = 20;
2461
2597
  var TIMEOUT_MS = 1e3 * 60;
2462
2598
  var actualDocGen = (specifier) => describePackage(specifier, {});
2463
- var ensurePath = (filePath) => mkdirSync(path10.dirname(filePath), { recursive: true });
2464
- var generatePlaceholder = (path17) => {
2465
- writeFileSync(path17, `{ "loading": true, "timestamp": ${Date.now()}}`);
2599
+ var ensurePath = (filePath) => mkdirSync(path11.dirname(filePath), { recursive: true });
2600
+ var generatePlaceholder = (path18) => {
2601
+ writeFileSync(path18, `{ "loading": true, "timestamp": ${Date.now()}}`);
2466
2602
  };
2467
2603
  var finish = (logger, resultPath) => {
2468
2604
  logger.success("Done! Docs can be found at:\n");
2469
- logger.print(` ${path10.resolve(resultPath)}`);
2605
+ logger.print(` ${path11.resolve(resultPath)}`);
2470
2606
  };
2471
- var generateDocs = async (specifier, path17, docgen, logger) => {
2607
+ var generateDocs = async (specifier, path18, docgen, logger) => {
2472
2608
  const result = await docgen(specifier);
2473
- await writeFile6(path17, JSON.stringify(result, null, 2));
2474
- finish(logger, path17);
2475
- return path17;
2609
+ await writeFile7(path18, JSON.stringify(result, null, 2));
2610
+ finish(logger, path18);
2611
+ return path18;
2476
2612
  };
2477
- var waitForDocs = async (docs, path17, logger, retryDuration = RETRY_DURATION) => {
2613
+ var waitForDocs = async (docs, path18, logger, retryDuration = RETRY_DURATION) => {
2478
2614
  try {
2479
2615
  if (docs.hasOwnProperty("loading")) {
2480
2616
  logger.info("Docs are being loaded by another process. Waiting.");
@@ -2486,19 +2622,19 @@ var waitForDocs = async (docs, path17, logger, retryDuration = RETRY_DURATION) =
2486
2622
  clearInterval(i);
2487
2623
  reject(new Error("Timed out waiting for docs to load"));
2488
2624
  }
2489
- const updated = JSON.parse(readFileSync(path17, "utf8"));
2625
+ const updated = JSON.parse(readFileSync(path18, "utf8"));
2490
2626
  if (!updated.hasOwnProperty("loading")) {
2491
2627
  logger.info("Docs found!");
2492
2628
  clearInterval(i);
2493
- resolve(path17);
2629
+ resolve(path18);
2494
2630
  }
2495
2631
  count++;
2496
2632
  }, retryDuration);
2497
2633
  });
2498
2634
  } else {
2499
- logger.info(`Docs already written to cache at ${path17}`);
2500
- finish(logger, path17);
2501
- return path17;
2635
+ logger.info(`Docs already written to cache at ${path18}`);
2636
+ finish(logger, path18);
2637
+ return path18;
2502
2638
  }
2503
2639
  } catch (e) {
2504
2640
  logger.error("Existing doc JSON corrupt. Aborting");
@@ -2515,28 +2651,28 @@ var docgenHandler = (options8, logger, docgen = actualDocGen, retryDuration = RE
2515
2651
  process.exit(9);
2516
2652
  }
2517
2653
  logger.success(`Generating docs for ${specifier}`);
2518
- const path17 = `${repoDir}/docs/${specifier}.json`;
2519
- ensurePath(path17);
2654
+ const path18 = `${repoDir}/docs/${specifier}.json`;
2655
+ ensurePath(path18);
2520
2656
  const handleError2 = () => {
2521
2657
  logger.info("Removing placeholder");
2522
- rmSync(path17);
2658
+ rmSync(path18);
2523
2659
  };
2524
2660
  try {
2525
- const existing = readFileSync(path17, "utf8");
2661
+ const existing = readFileSync(path18, "utf8");
2526
2662
  const json = JSON.parse(existing);
2527
2663
  if (json && json.timeout && Date.now() - json.timeout >= TIMEOUT_MS) {
2528
2664
  logger.info(`Expired placeholder found. Removing.`);
2529
- rmSync(path17);
2665
+ rmSync(path18);
2530
2666
  throw new Error("TIMEOUT");
2531
2667
  }
2532
- return waitForDocs(json, path17, logger, retryDuration);
2668
+ return waitForDocs(json, path18, logger, retryDuration);
2533
2669
  } catch (e) {
2534
2670
  if (e.message !== "TIMEOUT") {
2535
- logger.info(`Docs JSON not found at ${path17}`);
2671
+ logger.info(`Docs JSON not found at ${path18}`);
2536
2672
  }
2537
2673
  logger.debug("Generating placeholder");
2538
- generatePlaceholder(path17);
2539
- return generateDocs(specifier, path17, docgen, logger).catch((e2) => {
2674
+ generatePlaceholder(path18);
2675
+ return generateDocs(specifier, path18, docgen, logger).catch((e2) => {
2540
2676
  logger.error("Error generating documentation");
2541
2677
  logger.error(e2);
2542
2678
  handleError2();
@@ -2551,7 +2687,7 @@ import c3 from "chalk";
2551
2687
  import { getNameAndVersion as getNameAndVersion5, getLatestVersion } from "@openfn/runtime";
2552
2688
  var describeFn = (adaptorName, fn) => [
2553
2689
  c3.green(
2554
- `## ${fn.name}(${fn.parameters.map(({ name }) => name).join(",")})`
2690
+ `## ${fn.name}(${fn.parameters.map(({ name: name2 }) => name2).join(",")})`
2555
2691
  ),
2556
2692
  `${fn.description}`,
2557
2693
  c3.green("### Usage Examples"),
@@ -2579,46 +2715,46 @@ var docsHandler = async (options8, logger) => {
2579
2715
  const { adaptor, operation, repoDir } = options8;
2580
2716
  const adaptors = expand_adaptors_default([adaptor]);
2581
2717
  const [adaptorName] = adaptors;
2582
- let { name, version } = getNameAndVersion5(adaptorName);
2718
+ let { name: name2, version } = getNameAndVersion5(adaptorName);
2583
2719
  if (!version) {
2584
2720
  logger.info("No version number provided, looking for latest...");
2585
- version = await getLatestVersion(name);
2721
+ version = await getLatestVersion(name2);
2586
2722
  logger.info("Found ", version);
2587
2723
  logger.success(`Showing docs for ${adaptorName} v${version}`);
2588
2724
  }
2589
2725
  logger.info("Generating/loading documentation...");
2590
- const path17 = await handler_default7(
2726
+ const path18 = await handler_default7(
2591
2727
  {
2592
- specifier: `${name}@${version}`,
2728
+ specifier: `${name2}@${version}`,
2593
2729
  repoDir
2594
2730
  },
2595
2731
  // TODO I'm not sure how to handle logging here - we ought to feedback SOMETHING though
2596
2732
  createNullLogger()
2597
2733
  );
2598
2734
  let didError = false;
2599
- if (path17) {
2600
- const source = await readFile5(path17, "utf8");
2735
+ if (path18) {
2736
+ const source = await readFile5(path18, "utf8");
2601
2737
  const data = JSON.parse(source);
2602
2738
  let desc;
2603
2739
  if (operation) {
2604
- const fn = data.functions.find(({ name: name2 }) => name2 === operation);
2740
+ const fn = data.functions.find(({ name: name3 }) => name3 === operation);
2605
2741
  if (fn) {
2606
2742
  logger.debug("Operation schema:", fn);
2607
2743
  logger.break();
2608
- desc = describeFn(name, fn);
2744
+ desc = describeFn(name2, fn);
2609
2745
  } else {
2610
- logger.error(`Failed to find ${operation} in ${name}`);
2746
+ logger.error(`Failed to find ${operation} in ${name2}`);
2611
2747
  }
2612
2748
  } else {
2613
2749
  logger.debug("No operation name provided");
2614
2750
  logger.always("Available functions:\n");
2615
- desc = describeLib(name, data);
2751
+ desc = describeLib(name2, data);
2616
2752
  }
2617
2753
  logger.print(desc);
2618
2754
  if (!operation) {
2619
2755
  logger.always(`For more details on a specfic functions, use:
2620
2756
 
2621
- openfn docs ${name} <fn>
2757
+ openfn docs ${name2} <fn>
2622
2758
  `);
2623
2759
  }
2624
2760
  if (didError) {
@@ -2635,13 +2771,13 @@ var handler_default8 = docsHandler;
2635
2771
  // src/metadata/cache.ts
2636
2772
  import { getNameAndVersion as getNameAndVersion6 } from "@openfn/runtime";
2637
2773
  import { createHash } from "node:crypto";
2638
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile7, readdir, rm } from "node:fs/promises";
2639
- import path11 from "node:path";
2774
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile8, readdir, rm } from "node:fs/promises";
2775
+ import path12 from "node:path";
2640
2776
  var UNSUPPORTED_FILE_NAME = "unsupported.json";
2641
2777
  var getCachePath2 = (repoDir, key) => {
2642
- const base = path11.join(repoDir, "meta");
2778
+ const base = path12.join(repoDir, "meta");
2643
2779
  if (key) {
2644
- return path11.join(base, key.endsWith(".json") ? key : `${key}.json`);
2780
+ return path12.join(base, key.endsWith(".json") ? key : `${key}.json`);
2645
2781
  }
2646
2782
  return base;
2647
2783
  };
@@ -2683,8 +2819,8 @@ var get2 = async (repoPath, key) => {
2683
2819
  };
2684
2820
  var set2 = async (repoPath, key, result) => {
2685
2821
  const p = getCachePath2(repoPath, key);
2686
- await mkdir4(path11.dirname(p), { recursive: true });
2687
- await writeFile7(p, JSON.stringify(result));
2822
+ await mkdir4(path12.dirname(p), { recursive: true });
2823
+ await writeFile8(p, JSON.stringify(result));
2688
2824
  };
2689
2825
  var getUnsupportedCachePath = (repoDir) => {
2690
2826
  return getCachePath2(repoDir, UNSUPPORTED_FILE_NAME);
@@ -2708,14 +2844,14 @@ var compareVersions = (version1, version2) => {
2708
2844
  return v1.patch - v2.patch;
2709
2845
  };
2710
2846
  var isAdaptorUnsupported = async (adaptorSpecifier, repoDir) => {
2711
- const { name, version } = getNameAndVersion6(adaptorSpecifier);
2847
+ const { name: name2, version } = getNameAndVersion6(adaptorSpecifier);
2712
2848
  if (!version)
2713
2849
  return false;
2714
2850
  const cache = await getUnsupportedCache(repoDir);
2715
- if (!cache || !cache[name]) {
2851
+ if (!cache || !cache[name2]) {
2716
2852
  return false;
2717
2853
  }
2718
- const cached = cache[name];
2854
+ const cached = cache[name2];
2719
2855
  const currentParsed = parseVersion(version);
2720
2856
  const cachedParsed = parseVersion(cached.lastCheckedVersion);
2721
2857
  if (currentParsed.major > cachedParsed.major || currentParsed.major === cachedParsed.major && currentParsed.minor > cachedParsed.minor) {
@@ -2724,7 +2860,7 @@ var isAdaptorUnsupported = async (adaptorSpecifier, repoDir) => {
2724
2860
  return true;
2725
2861
  };
2726
2862
  var markAdaptorAsUnsupported = async (adaptorSpecifier, repoDir) => {
2727
- const { name, version } = getNameAndVersion6(adaptorSpecifier);
2863
+ const { name: name2, version } = getNameAndVersion6(adaptorSpecifier);
2728
2864
  if (!version)
2729
2865
  return;
2730
2866
  const cachePath = getUnsupportedCachePath(repoDir);
@@ -2735,15 +2871,15 @@ var markAdaptorAsUnsupported = async (adaptorSpecifier, repoDir) => {
2735
2871
  } catch (error) {
2736
2872
  }
2737
2873
  const parsed = parseVersion(version);
2738
- const existing = cache[name];
2874
+ const existing = cache[name2];
2739
2875
  if (!existing || compareVersions(version, existing.lastCheckedVersion) > 0) {
2740
- cache[name] = {
2876
+ cache[name2] = {
2741
2877
  lastCheckedVersion: version,
2742
2878
  majorMinor: parsed.majorMinor,
2743
2879
  timestamp: Date.now()
2744
2880
  };
2745
- await mkdir4(path11.dirname(cachePath), { recursive: true });
2746
- await writeFile7(cachePath, JSON.stringify(cache, null, 2));
2881
+ await mkdir4(path12.dirname(cachePath), { recursive: true });
2882
+ await writeFile8(cachePath, JSON.stringify(cache, null, 2));
2747
2883
  }
2748
2884
  };
2749
2885
 
@@ -2864,7 +3000,7 @@ var metadataHandler = async (options8, logger) => {
2864
3000
  var handler_default9 = metadataHandler;
2865
3001
 
2866
3002
  // src/pull/handler.ts
2867
- import path14 from "path";
3003
+ import path15 from "path";
2868
3004
  import fs5 from "node:fs/promises";
2869
3005
  import {
2870
3006
  getConfig as getConfig2,
@@ -2875,12 +3011,12 @@ import {
2875
3011
  } from "@openfn/deploy";
2876
3012
 
2877
3013
  // src/projects/pull.ts
2878
- import { Workspace as Workspace4 } from "@openfn/project";
3014
+ import { Workspace as Workspace6 } from "@openfn/project";
2879
3015
 
2880
3016
  // src/projects/fetch.ts
2881
- import path12 from "node:path";
2882
- import Project2, { Workspace as Workspace2 } from "@openfn/project";
2883
- import { writeFile as writeFile8 } from "node:fs/promises";
3017
+ import path13 from "node:path";
3018
+ import Project2, { Workspace as Workspace4 } from "@openfn/project";
3019
+ import { writeFile as writeFile9 } from "node:fs/promises";
2884
3020
  var options2 = [
2885
3021
  alias,
2886
3022
  apiKey,
@@ -2912,7 +3048,7 @@ var printProjectName2 = (project) => `${project.qname} (${project.id})`;
2912
3048
  var fetchV1 = async (options8, logger) => {
2913
3049
  const workspacePath = options8.workspace ?? process.cwd();
2914
3050
  logger.debug("Using workspace at", workspacePath);
2915
- const workspace2 = new Workspace2(workspacePath, logger, false);
3051
+ const workspace2 = new Workspace4(workspacePath, logger, false);
2916
3052
  const localProject = workspace2.get(options8.project);
2917
3053
  if (localProject) {
2918
3054
  logger.debug(
@@ -2938,7 +3074,7 @@ var fetchV1 = async (options8, logger) => {
2938
3074
  options8.outputPath
2939
3075
  );
2940
3076
  logger.success(`Fetched project file to ${finalOutputPath}`);
2941
- await writeFile8(finalOutputPath, JSON.stringify(data, null, 2));
3077
+ await writeFile9(finalOutputPath, JSON.stringify(data, null, 2));
2942
3078
  return data;
2943
3079
  };
2944
3080
  var handler2 = async (options8, logger) => {
@@ -2950,9 +3086,14 @@ var handler2 = async (options8, logger) => {
2950
3086
  var fetchV2 = async (options8, logger) => {
2951
3087
  const workspacePath = options8.workspace ?? process.cwd();
2952
3088
  logger.debug("Using workspace at", workspacePath);
2953
- const workspace2 = new Workspace2(workspacePath, logger, false);
3089
+ const workspace2 = new Workspace4(workspacePath, logger, false);
2954
3090
  const { outputPath: outputPath2 } = options8;
2955
3091
  const remoteProject = await fetchRemoteProject(workspace2, options8, logger);
3092
+ if (!options8.alias && remoteProject.sandbox?.parentId) {
3093
+ options8.alias = remoteProject.id;
3094
+ remoteProject.cli.alias = options8.alias;
3095
+ logger.debug("Defaulting alias to sandbox id", options8.alias);
3096
+ }
2956
3097
  if (!options8.force && options8.format !== "state") {
2957
3098
  const localTargetProject = await resolveOutputProject(
2958
3099
  workspace2,
@@ -2968,7 +3109,7 @@ var fetchV2 = async (options8, logger) => {
2968
3109
  );
2969
3110
  let format2 = options8.format;
2970
3111
  if (outputPath2) {
2971
- const ext = path12.extname(outputPath2).substring(1);
3112
+ const ext = path13.extname(outputPath2).substring(1);
2972
3113
  if (ext.length) {
2973
3114
  format2 = ext;
2974
3115
  }
@@ -3036,7 +3177,7 @@ async function fetchRemoteProject(workspace2, options8, logger) {
3036
3177
  logger.debug(
3037
3178
  `Resolved ${options8.project} to UUID ${projectUUID} from local project ${printProjectName2(
3038
3179
  localProject
3039
- )}}`
3180
+ )}`
3040
3181
  );
3041
3182
  }
3042
3183
  const projectEndpoint = localProject?.openfn?.endpoint ?? config2.endpoint;
@@ -3090,23 +3231,15 @@ To ignore this error and override the local file, pass --force (-f)
3090
3231
  delete error.stack;
3091
3232
  throw error;
3092
3233
  }
3093
- const hasAnyHistory = remoteProject.workflows.find(
3094
- (w) => w.workflow.history?.length
3095
- );
3096
- const skipVersionCheck = options8.force || // The user forced the checkout
3097
- !hasAnyHistory;
3098
- if (!skipVersionCheck && !remoteProject.canMergeInto(localProject)) {
3099
- throw new Error("Error! An incompatible project exists at this location");
3100
- }
3101
3234
  }
3102
3235
  }
3103
3236
 
3104
3237
  // src/projects/checkout.ts
3105
- import Project3, { Workspace as Workspace3 } from "@openfn/project";
3106
- import path13 from "path";
3238
+ import Project3, { Workspace as Workspace5 } from "@openfn/project";
3239
+ import path14 from "path";
3107
3240
  import fs4 from "fs";
3108
3241
  import { rimraf as rimraf2 } from "rimraf";
3109
- var options3 = [log, workspace, clean2];
3242
+ var options3 = [log, workspace, clean2, force];
3110
3243
  var command3 = {
3111
3244
  command: "checkout <project>",
3112
3245
  describe: "Switch to a different OpenFn project in the same workspace",
@@ -3119,12 +3252,12 @@ var command3 = {
3119
3252
  var handler3 = async (options8, logger) => {
3120
3253
  const projectIdentifier = options8.project;
3121
3254
  const workspacePath = options8.workspace ?? process.cwd();
3122
- const workspace2 = new Workspace3(workspacePath, logger);
3255
+ const workspace2 = new Workspace5(workspacePath, logger);
3123
3256
  const { project: _, ...config2 } = workspace2.getConfig();
3124
3257
  const currentProject = workspace2.getActiveProject();
3125
3258
  let switchProject;
3126
3259
  if (/\.(yaml|json)$/.test(projectIdentifier)) {
3127
- const filePath = projectIdentifier.startsWith("/") ? projectIdentifier : path13.join(workspacePath, projectIdentifier);
3260
+ const filePath = projectIdentifier.startsWith("/") ? projectIdentifier : path14.join(workspacePath, projectIdentifier);
3128
3261
  logger.debug("Loading project from path ", filePath);
3129
3262
  switchProject = await Project3.from("path", filePath, config2);
3130
3263
  } else {
@@ -3135,18 +3268,51 @@ var handler3 = async (options8, logger) => {
3135
3268
  `Project with id ${projectIdentifier} not found in the workspace`
3136
3269
  );
3137
3270
  }
3271
+ try {
3272
+ const localProject = await Project3.from("fs", {
3273
+ root: options8.workspace || "."
3274
+ });
3275
+ logger.success(`Loaded local project ${localProject.alias}`);
3276
+ const changed = await findLocallyChangedWorkflows(
3277
+ workspace2,
3278
+ localProject,
3279
+ "assume-ok"
3280
+ );
3281
+ if (changed.length && !options8.force) {
3282
+ logger.break();
3283
+ logger.warn(
3284
+ "WARNING: detected changes on your currently checked-out project"
3285
+ );
3286
+ logger.warn(
3287
+ `Changes may be lost by checking out ${localProject.alias} right now`
3288
+ );
3289
+ logger.warn(`Pass --force or -f to override this warning and continue`);
3290
+ const e = new Error(
3291
+ `The currently checked out project has diverged! Changes may be lost`
3292
+ );
3293
+ delete e.stack;
3294
+ throw e;
3295
+ }
3296
+ } catch (e) {
3297
+ if (e.message.match("ENOENT")) {
3298
+ logger.debug("No openfn.yaml found locally: skipping divergence test");
3299
+ } else {
3300
+ throw e;
3301
+ }
3302
+ }
3138
3303
  if (options8.clean) {
3139
3304
  await rimraf2(workspace2.workflowsPath);
3140
3305
  } else {
3141
3306
  await tidyWorkflowDir(currentProject, switchProject);
3142
3307
  }
3308
+ updateForkedFrom(switchProject);
3143
3309
  const files = switchProject.serialize("fs");
3144
3310
  for (const f in files) {
3145
3311
  if (files[f]) {
3146
- fs4.mkdirSync(path13.join(workspacePath, path13.dirname(f)), {
3312
+ fs4.mkdirSync(path14.join(workspacePath, path14.dirname(f)), {
3147
3313
  recursive: true
3148
3314
  });
3149
- fs4.writeFileSync(path13.join(workspacePath, f), files[f]);
3315
+ fs4.writeFileSync(path14.join(workspacePath, f), files[f]);
3150
3316
  } else {
3151
3317
  logger.warn("WARNING! No content for file", f);
3152
3318
  }
@@ -3193,7 +3359,7 @@ var ensureProjectId = (options8, logger) => {
3193
3359
  logger?.debug(
3194
3360
  "No project ID specified: looking up checked out project in Workspace"
3195
3361
  );
3196
- const ws = new Workspace4(options8.workspace);
3362
+ const ws = new Workspace6(options8.workspace);
3197
3363
  if (ws.activeProject) {
3198
3364
  options8.project = ws.activeProject.uuid;
3199
3365
  logger?.info(
@@ -3266,7 +3432,7 @@ async function pullHandler(options8, logger) {
3266
3432
  process.exitCode = 1;
3267
3433
  process.exit(1);
3268
3434
  }
3269
- const resolvedPath = path14.resolve(config2.specPath);
3435
+ const resolvedPath = path15.resolve(config2.specPath);
3270
3436
  logger.debug("reading spec from", resolvedPath);
3271
3437
  const updatedSpec = await syncRemoteSpec(
3272
3438
  await res.text(),
@@ -3275,7 +3441,7 @@ async function pullHandler(options8, logger) {
3275
3441
  logger
3276
3442
  );
3277
3443
  await fs5.writeFile(
3278
- path14.resolve(config2.statePath),
3444
+ path15.resolve(config2.statePath),
3279
3445
  JSON.stringify(state, null, 2)
3280
3446
  );
3281
3447
  await fs5.writeFile(resolvedPath, updatedSpec);
@@ -3320,7 +3486,7 @@ __export(projects_exports, {
3320
3486
  });
3321
3487
 
3322
3488
  // src/projects/list.ts
3323
- import { Workspace as Workspace5 } from "@openfn/project";
3489
+ import { Workspace as Workspace7 } from "@openfn/project";
3324
3490
  var options5 = [log, workspace];
3325
3491
  var command5 = {
3326
3492
  command: "list [project-path]",
@@ -3333,7 +3499,7 @@ var handler5 = async (options8, logger) => {
3333
3499
  logger.info("Searching for projects in workspace at:");
3334
3500
  logger.info(" ", options8.workspace);
3335
3501
  logger.break();
3336
- const workspace2 = new Workspace5(options8.workspace);
3502
+ const workspace2 = new Workspace7(options8.workspace);
3337
3503
  if (!workspace2.valid) {
3338
3504
  throw new Error("No OpenFn projects found");
3339
3505
  }
@@ -3351,7 +3517,7 @@ ${project.workflows.map((w) => " - " + w.id).join("\n")}`;
3351
3517
  }
3352
3518
 
3353
3519
  // src/projects/version.ts
3354
- import { Workspace as Workspace6 } from "@openfn/project";
3520
+ import { Workspace as Workspace8 } from "@openfn/project";
3355
3521
  var options6 = [workflow, workspace, workflowMappings];
3356
3522
  var command6 = {
3357
3523
  command: "version [workflow]",
@@ -3360,7 +3526,7 @@ var command6 = {
3360
3526
  builder: (yargs) => build(options6, yargs)
3361
3527
  };
3362
3528
  var handler6 = async (options8, logger) => {
3363
- const workspace2 = new Workspace6(options8.workspace);
3529
+ const workspace2 = new Workspace8(options8.workspace);
3364
3530
  if (!workspace2.valid) {
3365
3531
  logger.error("Command was run in an invalid openfn workspace");
3366
3532
  return;
@@ -3395,8 +3561,8 @@ ${final}`);
3395
3561
  };
3396
3562
 
3397
3563
  // src/projects/merge.ts
3398
- import Project5, { Workspace as Workspace7 } from "@openfn/project";
3399
- import path15 from "node:path";
3564
+ import Project5, { Workspace as Workspace9 } from "@openfn/project";
3565
+ import path16 from "node:path";
3400
3566
  import fs6 from "node:fs/promises";
3401
3567
  var options7 = [
3402
3568
  removeUnmapped,
@@ -3431,14 +3597,14 @@ var command7 = {
3431
3597
  };
3432
3598
  var handler7 = async (options8, logger) => {
3433
3599
  const workspacePath = options8.workspace;
3434
- const workspace2 = new Workspace7(workspacePath);
3600
+ const workspace2 = new Workspace9(workspacePath);
3435
3601
  if (!workspace2.valid) {
3436
3602
  logger.error("Command was run in an invalid openfn workspace");
3437
3603
  return;
3438
3604
  }
3439
3605
  let targetProject;
3440
3606
  if (options8.base) {
3441
- const basePath = path15.resolve(options8.base);
3607
+ const basePath = path16.resolve(options8.base);
3442
3608
  logger.debug("Loading target project from path", basePath);
3443
3609
  targetProject = await Project5.from("path", basePath);
3444
3610
  } else {
@@ -3452,7 +3618,7 @@ var handler7 = async (options8, logger) => {
3452
3618
  const sourceProjectIdentifier = options8.project;
3453
3619
  let sourceProject;
3454
3620
  if (/\.(ya?ml|json)$/.test(sourceProjectIdentifier)) {
3455
- const filePath = path15.join(workspacePath, sourceProjectIdentifier);
3621
+ const filePath = path16.join(workspacePath, sourceProjectIdentifier);
3456
3622
  logger.debug("Loading source project from path ", filePath);
3457
3623
  sourceProject = await Project5.from("path", filePath);
3458
3624
  } else {
@@ -3515,7 +3681,7 @@ var handler7 = async (options8, logger) => {
3515
3681
 
3516
3682
  // src/util/print-versions.ts
3517
3683
  import { readFileSync as readFileSync2 } from "node:fs";
3518
- import path16 from "node:path";
3684
+ import path17 from "node:path";
3519
3685
  import url from "node:url";
3520
3686
  import { getNameAndVersion as getNameAndVersion7 } from "@openfn/runtime";
3521
3687
  import { mainSymbols } from "figures";
@@ -3527,7 +3693,7 @@ var { triangleRightSmall: t } = mainSymbols;
3527
3693
  var loadVersionFromPath = (adaptorPath) => {
3528
3694
  try {
3529
3695
  const pkg = JSON.parse(
3530
- readFileSync2(path16.resolve(adaptorPath, "package.json"), "utf8")
3696
+ readFileSync2(path17.resolve(adaptorPath, "package.json"), "utf8")
3531
3697
  );
3532
3698
  return pkg.version;
3533
3699
  } catch (e) {
@@ -3549,8 +3715,8 @@ var printVersions = async (logger, options8 = {}, includeComponents = false) =>
3549
3715
  adaptorName = getNameAndVersion7(adaptor).name;
3550
3716
  adaptorVersion = "monorepo";
3551
3717
  } else {
3552
- const { name, version: version2 } = getNameAndVersion7(adaptor);
3553
- adaptorName = name;
3718
+ const { name: name2, version: version2 } = getNameAndVersion7(adaptor);
3719
+ adaptorName = name2;
3554
3720
  adaptorVersion = version2 || "latest";
3555
3721
  }
3556
3722
  adaptorList.push([adaptorName, adaptorVersion]);
@@ -3562,7 +3728,7 @@ var printVersions = async (logger, options8 = {}, includeComponents = false) =>
3562
3728
  ...[NODE, CLI2, RUNTIME2, COMPILER2, longestAdaptorName].map((s) => s.length)
3563
3729
  );
3564
3730
  const prefix = (str) => ` ${t} ${str.padEnd(longest + 4, " ")}`;
3565
- const dirname3 = path16.dirname(url.fileURLToPath(import.meta.url));
3731
+ const dirname3 = path17.dirname(url.fileURLToPath(import.meta.url));
3566
3732
  const pkg = JSON.parse(readFileSync2(`${dirname3}/../../package.json`, "utf8"));
3567
3733
  const { version, dependencies } = pkg;
3568
3734
  const compilerVersion = dependencies["@openfn/compiler"];
@@ -3580,8 +3746,8 @@ var printVersions = async (logger, options8 = {}, includeComponents = false) =>
3580
3746
  output.versions.compiler = compilerVersion;
3581
3747
  }
3582
3748
  if (adaptorList.length) {
3583
- for (const [name, version2] of adaptorList) {
3584
- output.versions[name] = version2;
3749
+ for (const [name2, version2] of adaptorList) {
3750
+ output.versions[name2] = version2;
3585
3751
  }
3586
3752
  }
3587
3753
  } else {
@@ -3594,9 +3760,9 @@ ${prefix(RUNTIME2)}${runtimeVersion}
3594
3760
  ${prefix(COMPILER2)}${compilerVersion}`;
3595
3761
  }
3596
3762
  if (adaptorList.length) {
3597
- for (const [name, version2] of adaptorList) {
3763
+ for (const [name2, version2] of adaptorList) {
3598
3764
  output += `
3599
- ${prefix(name)}${version2}`;
3765
+ ${prefix(name2)}${version2}`;
3600
3766
  }
3601
3767
  }
3602
3768
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/cli",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "description": "CLI devtools for the OpenFn toolchain",
5
5
  "engines": {
6
6
  "node": ">=18",
@@ -50,13 +50,13 @@
50
50
  "undici": "7.12.0",
51
51
  "ws": "^8.18.3",
52
52
  "yargs": "^17.7.2",
53
- "@openfn/describe-package": "0.1.5",
54
- "@openfn/lexicon": "^1.4.0",
55
53
  "@openfn/compiler": "1.2.2",
56
54
  "@openfn/deploy": "0.11.5",
55
+ "@openfn/lexicon": "^1.4.1",
56
+ "@openfn/describe-package": "0.1.5",
57
57
  "@openfn/logger": "1.1.1",
58
- "@openfn/project": "^0.12.1",
59
- "@openfn/runtime": "1.8.3"
58
+ "@openfn/project": "^0.13.1",
59
+ "@openfn/runtime": "1.8.4"
60
60
  },
61
61
  "files": [
62
62
  "dist",