@openfn/cli 0.0.41 → 0.2.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/README.md CHANGED
@@ -1,17 +1,36 @@
1
1
  # @openfn/cli
2
2
 
3
- This package contains a new devtools CLI for running OpenFn jobs.
3
+ This package contains a new devtools CLI for running and deploying OpenFn jobs.
4
4
 
5
5
  The CLI includes:
6
6
 
7
- * A secure runtime for executing OpenFn jobs and workflows
8
- * A compiler for making OpenFn jobs runnable
9
- * Configurable logging output
10
- * Auto-installation of language adaptors
11
- * Support for the adaptors monorepo
7
+ - A secure runtime for executing OpenFn jobs and workflows
8
+ - A compiler for making OpenFn jobs runnable
9
+ - Configurable logging output
10
+ - Auto-installation of language adaptors
11
+ - Support for the adaptors monorepo
12
+ - Deployment of workflows to OpenFn (and Lightning)
12
13
 
13
14
  ## Getting Started
14
15
 
16
+ - [Installation](#installation)
17
+ - [Updating](#updating)
18
+ - [Migrating from devtools](#migrating-from-devtools)
19
+ - [Basic Usage](#basic-usage)
20
+ - [Advanced Usage](#advanced-usage)
21
+ - [Deploying Workflows](#deploying-workflows)
22
+ - [Logging](#logging)
23
+ - [Structured/JSON logging](#structuredjson-logging)
24
+ - [Workflows](#workflows)
25
+ - [Compilation](#compilation)
26
+ - [Contributing](#contributing)
27
+ - [Usage from this repo](#usage-from-this-repo)
28
+ - [Installing globally](#installing-globally)
29
+ - [Repo Directory](#repo-directory)
30
+ - [Contribution changes](#contribution-changes)
31
+
32
+ ## Installation
33
+
15
34
  To install:
16
35
 
17
36
  ```
@@ -56,14 +75,13 @@ And then re-installing.
56
75
 
57
76
  If you're coming to the CLI from the old openfn devtools, here are a couple of key points to be aware of:
58
77
 
59
- * The CLI has a shorter, sleeker syntax, so your command should be much shorter
60
- * The CLI will automatically install adaptors for you (with full version control)
78
+ - The CLI has a shorter, sleeker syntax, so your command should be much shorter
79
+ - The CLI will automatically install adaptors for you (with full version control)
61
80
 
62
81
  ## Basic Usage
63
82
 
64
83
  You're probably here to run jobs (expressions) or workflows, which the CLI makes easy:
65
84
 
66
-
67
85
  ```
68
86
  openfn path/to/workflow.json
69
87
  openfn path/to/job.js -ia adaptor-name
@@ -87,24 +105,123 @@ You can pass `--log info` to get more feedback about what's happening, or `--log
87
105
 
88
106
  The CLI has a number of commands (the first argument after openfn)
89
107
 
90
- * execute - run a job
91
- * compile - compile a job to a .js file
92
- * docs - show documentation for an adaptor function
93
- * repo - manage the repo of installed modules
94
- * docgen - generate JSON documentation for an adaptor based on its typescript
108
+ - execute - run a job
109
+ - compile - compile a job to a .js file
110
+ - docs - show documentation for an adaptor function
111
+ - repo - manage the repo of installed modules
112
+ - docgen - generate JSON documentation for an adaptor based on its typescript
95
113
 
96
114
  If no command is specified, execute will run.
97
115
 
98
116
  To get more information about a command, including usage examples, run `openfn <command> help`, ie, `openfn compile help`.
99
117
 
100
- ## Logging
118
+ ## Deploying Workflows
119
+
120
+ > ⚠️ This feature is still in active development. Expect breaking changes.
121
+
122
+ The CLI can deploy workflows to OpenFn.org and instances of Lightning.
123
+
124
+ In order to deploy a workflow, you need the follow:
125
+
126
+ - A project file written in YAML
127
+ - A config file (or env vars) with your OpenFn credentials
128
+
129
+ Example project file:
130
+
131
+ ```yaml
132
+ ---
133
+ name: my-new-project
134
+ workflows:
135
+ workflow-one:
136
+ name: My New Workflow
137
+ jobs:
138
+ job-a:
139
+ name: My First Job
140
+ enabled: true # default
141
+ adaptor: @openfn/language-http@latest
142
+ body: |
143
+ alterState(state => {
144
+ console.log("Hello world!");
145
+ return state;
146
+ });
147
+ job-b:
148
+ name: My Second Job
149
+ adaptor: @openfn/language-common@latest
150
+ body: |
151
+ alterState(state => {
152
+ console.log("Hello world!");
153
+ return state;
154
+ });
155
+ triggers:
156
+ trigger-one:
157
+ type: webhook # default
158
+ edges:
159
+ webhook->job-a:
160
+ source_trigger: trigger-one
161
+ target_job: job-a
162
+ job-a->job-b:
163
+ source_job: job-a
164
+ target_job: job-b
165
+
166
+ ```
167
+
168
+ Example config file:
169
+
170
+ ```jsonc
171
+ {
172
+ // Required, can be overridden or set with `OPENFN_API_KEY` env var
173
+ "apiKey": "***",
174
+
175
+ // Optional: can be set using the -p, defaults to project.yaml
176
+ "specPath": "project.yaml",
177
+
178
+ // Optional: can be set using -s, defaults to .state.json
179
+ "statePath": ".state.json",
180
+
181
+ // Optional: defaults to OpenFn.org's API, can be overridden or set with
182
+ // `OPENFN_ENDPOINT` env var
183
+ "endpoint": "https://app.openfn.org/api/provision"
184
+ }
185
+ ```
186
+
187
+ **Environment Variables**
188
+
189
+ You can also set the following environment variables to avoid using a config file:
190
+
191
+ - `OPENFN_API_KEY` - your OpenFn/Lightning API key
192
+ - `OPENFN_ENDPOINT` - the endpoint to deploy to (defaults to OpenFn.org)
193
+
194
+ **Using the CLI**
195
+
196
+ ```bash
197
+ OPENFN_API_KEY="***" \
198
+ openfn deploy
199
+
200
+ # [CLI] ♦ Changes:
201
+ # {
202
+ # + ... diff
203
+ # - ... diff
204
+ # }
205
+ #
206
+ # ? Deploy? yes
207
+ # [CLI] ♦ Deployed.
208
+ ```
209
+
210
+ **Flags and Options**
211
+
212
+ - `-p, --project-path <path>` - path to the project file (defaults to `project.yaml`)
213
+ - `-s, --state-path <path>` - path to the state file (defaults to `.state.json`)
214
+ - `-c, --config, --config-path` - path to the config file (defaults to `.config.json`)
215
+ - `--no-confirm` - skip the confirmation prompt
216
+
217
+ ## Logging
101
218
 
102
219
  The CLI is actually a collection of packages, each of which will log with slightly different rules. To help understand where logs are coming from, each package prints a namespace or prefix at the start of its log.
103
220
 
104
- * [CLI] - the CLI itself, responsible for parsing and validating user input, reading and writing to disk, and executing the correct functionality.
105
- * [CMP] - the Compiler will parse openfn jobs into executable Javascript, changing your code
106
- * [R/T] - the Runtime executes your job code in a secure sandboxed environment, one operation at a time
107
- * [JOB] - the actual job code that your wrote. Any console.log statements in your job will appear under this namespace.
221
+ - [CLI] - the CLI itself, responsible for parsing and validating user input, reading and writing to disk, and executing the correct functionality.
222
+ - [CMP] - the Compiler will parse openfn jobs into executable Javascript, changing your code
223
+ - [R/T] - the Runtime executes your job code in a secure sandboxed environment, one operation at a time
224
+ - [JOB] - the actual job code that your wrote. Any console.log statements in your job will appear under this namespace.
108
225
 
109
226
  The CLI will log information at three different levels of verbosity: `default`, `info` and `debug` (`none` is also supported).
110
227
 
@@ -120,14 +237,18 @@ If something unexpected happens during a command, your first step should be to r
120
237
 
121
238
  `debug` level logging is highly verbose and aims to tell you everything that's going on under-the hood. This is aimed mostly at CLI/runtime developers and can be very useful for debugging problems.
122
239
 
123
- ## Structred/JSON logging
240
+ ### Structred/JSON logging
124
241
 
125
242
  By default all logs will be printed as human-readable strings.
126
243
 
127
244
  For a more structured output, you can emit logs as JSON objects with `level`, `name` and `message` properties:
245
+
128
246
  ```
247
+
129
248
  { level: 'info', name: 'CLI', message: ['Loaded adaptor'] }
249
+
130
250
  ```
251
+
131
252
  Pass `--log-json` to the CLI to do this. You can also set the OPENFN_LOG_JSON env var (and use `--no-log-json` to disable).
132
253
 
133
254
  ## Workflows
@@ -141,37 +262,39 @@ To see an example workflow, run the test command with `openfn test`.
141
262
  A workflow has a structure like this (better documentation is coming soon):
142
263
 
143
264
  ```
265
+
144
266
  {
145
- "start": "a", // optionally specify the start node (defaults to jobs[0])
146
- "jobs": [
147
- {
148
- "id": "a",
149
- "expression": "fn((state) => state)", // code or a path
150
- "adaptor": "@openfn/language-common@1.75", // specifiy the adaptor to use (version optional)
151
- "data": {}, // optionally pre-populate the data object (this will be overriden by keys in in previous state)
152
- "configuration": {}, // Use this to pass credentials
153
- "next": {
154
- // This object defines which jobs to call next
155
- // All edges returning true will run
156
- // If there are no next edges, the workflow will end
157
- "b": true,
158
- "c": {
159
- "condition": "!state.error" // Not that this is an expression, not a function
160
- }
161
- }
162
- },
163
- ]
267
+ "start": "a", // optionally specify the start node (defaults to jobs[0])
268
+ "jobs": [
269
+ {
270
+ "id": "a",
271
+ "expression": "fn((state) => state)", // code or a path
272
+ "adaptor": "@openfn/language-common@1.75", // specifiy the adaptor to use (version optional)
273
+ "data": {}, // optionally pre-populate the data object (this will be overriden by keys in in previous state)
274
+ "configuration": {}, // Use this to pass credentials
275
+ "next": {
276
+ // This object defines which jobs to call next
277
+ // All edges returning true will run
278
+ // If there are no next edges, the workflow will end
279
+ "b": true,
280
+ "c": {
281
+ "condition": "!state.error" // Not that this is an expression, not a function
282
+ }
283
+ }
284
+ },
285
+ ]
164
286
  }
287
+
165
288
  ```
166
289
 
167
290
  ## Compilation
168
291
 
169
292
  The CLI will attempt to compile your job code into normalized Javascript. It will do a number of things to make your code robust and portable:
170
293
 
171
- * The language adaptor will be imported into the file
172
- * The adaptor's execute function will be exported form the file
173
- * All top level operations will be added to an array
174
- * That array will be made the default export of the file
294
+ - The language adaptor will be imported into the file
295
+ - The adaptor's execute function will be exported form the file
296
+ - All top level operations will be added to an array
297
+ - That array will be made the default export of the file
175
298
 
176
299
  The result of this is a lightweight, modern JS source file. It can be executed in any runtime environment: just execute each function in the exported array.
177
300
 
@@ -181,7 +304,7 @@ All jobs which work against `@openfn/core` will work in the new CLI and runtime
181
304
 
182
305
  If you want to see how the compiler is changing your job, run `openfn compile path/to/job -a <adaptor>` to return the compiled code to stdout. Add `-o path/to/output.js` to save the result to disk.
183
306
 
184
- # Contributing
307
+ ## Contributing
185
308
 
186
309
  First of all, thanks for helping! You're contributing to a digital public good that will always be free and open source and aimed at serving innovative NGOs, governments, and social impact organizations the world over! You rock. heart
187
310
 
@@ -189,44 +312,55 @@ To get this started, you'll want to clone this repo.
189
312
 
190
313
  You also need to install `pnpm`.
191
314
 
192
- ## Usage from this repo
315
+ ### Usage from this repo
193
316
 
194
317
  You can run the cli straight from source with `pnpm`
195
318
 
196
319
  ```
320
+
197
321
  $ pnpm openfn path/to/job.js
198
322
  $ pnpm openfn -h
323
+
199
324
  ```
200
325
 
201
326
  See test/execute.test.ts for more usage examples
202
327
 
203
- ## Installing globally
328
+ ### Installing globally
204
329
 
205
330
  To install the CLI globally from the build in repo:
206
331
 
207
332
  ```
333
+
208
334
  $ npm install -g .
335
+
209
336
  ```
210
337
 
211
338
  Note that this will install the built source from `dist`
212
339
 
213
- ## Repo Directory
340
+ ### Repo Directory
214
341
 
215
342
  The CLI will save and load adaptors from an arbitrary folder on your system.
216
343
 
217
344
  You should set the OPENFN_REPO_DIR env var to something sensible.
218
345
 
219
346
  In `~/.bashrc` (or whatever you use), add:
347
+
220
348
  ```
349
+
221
350
  export OPENFN_REPO_DIR=~/repo/openfn/cli-repo
351
+
222
352
  ```
223
353
 
224
354
  To run adaptors straight from the adaptors monorepo:
225
355
 
226
356
  export OPENFN_ADAPTORS_REPO=~/repo/openfn/adaptors
227
357
 
228
- ## Contributing changes
358
+ ### Contributing changes
229
359
 
230
360
  Open a PR at https://github.com/openfn/kit. Include a changeset and a description of your change.
231
361
 
232
- See the root readme for more details about changests,
362
+ See the root readme for more details about changests,
363
+
364
+ ```
365
+
366
+ ```
package/dist/index.js CHANGED
@@ -82,6 +82,34 @@ var compile = {
82
82
  setDefaultValue(opts2, "compile", true);
83
83
  }
84
84
  };
85
+ var confirm = {
86
+ name: "no-confirm",
87
+ yargs: {
88
+ boolean: true,
89
+ description: "Skip confirmation prompts (e.g. 'Are you sure?')"
90
+ },
91
+ ensure: (opts2) => {
92
+ setDefaultValue(opts2, "confirm", true);
93
+ }
94
+ };
95
+ var configPath = {
96
+ name: "config",
97
+ yargs: {
98
+ alias: ["c", "config-path"],
99
+ description: "The location of your config file",
100
+ default: "./.config.json"
101
+ }
102
+ };
103
+ var describe = {
104
+ name: "describe",
105
+ yargs: {
106
+ boolean: true,
107
+ description: "Downloads the project yaml from the specified instance"
108
+ },
109
+ ensure: (opts2) => {
110
+ setDefaultValue(opts2, "describe", true);
111
+ }
112
+ };
85
113
  var expandAdaptors = {
86
114
  name: "no-expand-adaptors",
87
115
  yargs: {
@@ -186,6 +214,14 @@ var outputPath = {
186
214
  delete opts2.o;
187
215
  }
188
216
  };
217
+ var projectPath = {
218
+ name: "project-path",
219
+ yargs: {
220
+ string: true,
221
+ alias: ["p"],
222
+ description: "The location of your project.yaml file"
223
+ }
224
+ };
189
225
  var repoDir = {
190
226
  name: "repo-dir",
191
227
  yargs: () => ({
@@ -240,6 +276,9 @@ var statePath = {
240
276
  yargs: {
241
277
  alias: ["s"],
242
278
  description: "Path to the state file"
279
+ },
280
+ ensure: (opts2) => {
281
+ delete opts2.s;
243
282
  }
244
283
  };
245
284
  var stateStdin = {
@@ -279,7 +318,12 @@ var expandYargs = (y2) => {
279
318
  }
280
319
  return y2;
281
320
  };
282
- var build = (opts2, yargs2) => opts2.reduce((_y, o) => yargs2.option(o.name, expandYargs(o.yargs)), yargs2);
321
+ function build(opts2, yargs2) {
322
+ return opts2.reduce(
323
+ (_y, o) => yargs2.option(o.name, expandYargs(o.yargs)),
324
+ yargs2
325
+ );
326
+ }
283
327
  var ensure = (command, opts2) => (yargs2) => {
284
328
  yargs2.command = command;
285
329
  opts2.filter((opt) => opt.ensure).forEach((opt) => {
@@ -296,63 +340,74 @@ var override = (command, yargs2) => {
296
340
  };
297
341
  };
298
342
 
299
- // src/repo/command.ts
300
- var repo = {
301
- command: "repo [subcommand]",
302
- desc: "Run commands on the module repo (install|clean)",
303
- builder: (yargs2) => yargs2.command(clean).command(install).command(list).example("repo install -a http", "Install @openfn/language-http").example("repo clean", "Remove everything from the repo working dir")
304
- };
305
- var installOptions = [
306
- repoDir,
307
- override(expandAdaptors, {
308
- default: true,
309
- hidden: true
343
+ // src/compile/command.ts
344
+ var options = [
345
+ expandAdaptors,
346
+ adaptors,
347
+ ignoreImports,
348
+ inputPath,
349
+ logJson,
350
+ override(outputStdout, {
351
+ default: true
310
352
  }),
311
- override(adaptors, {
312
- description: "Specify which language-adaptor to install (allows short-form names to be used, eg, http)"
313
- })
353
+ outputPath,
354
+ repoDir,
355
+ useAdaptorsMonorepo
314
356
  ];
315
- var install = {
316
- command: "install [packages...]",
317
- desc: "install one or more packages to the runtime repo. Use -a to pass shorthand adaptor names.",
318
- handler: ensure("repo-install", installOptions),
319
- builder: (yargs2) => build(installOptions, yargs2).example("install axios", "Install the axios npm package to the repo").example(
320
- "install -a http",
321
- "Install @openfn/language-http adaptor to the repo"
357
+ var compileCommand = {
358
+ command: "compile [path]",
359
+ desc: "Compile an openfn job or workflow and print or save the resulting JavaScript.",
360
+ handler: ensure("compile", options),
361
+ builder: (yargs2) => build(options, yargs2).positional("path", {
362
+ describe: "The path to load the job or workflow from (a .js or .json file or a dir containing a job.js file)",
363
+ demandOption: true
364
+ }).example(
365
+ "compile foo/job.js",
366
+ "Compiles the job at foo/job.js and prints the result to stdout"
322
367
  ).example(
323
- "install @openfn/language-http",
324
- "Install the language-http adaptor to the repo"
368
+ "compile foo/workflow.json -o foo/workflow-compiled.json",
369
+ "Compiles the workflow at foo/work.json and prints the result to -o foo/workflow-compiled.json"
325
370
  )
326
371
  };
327
- var clean = {
328
- command: "clean",
329
- desc: "Removes all modules from the runtime module repo",
330
- handler: ensure("repo-clean", [repoDir]),
331
- builder: (yargs2) => build(
332
- [
333
- repoDir,
334
- {
335
- name: "force",
336
- yargs: {
337
- alias: ["f"],
338
- description: "Skip the prompt and force deletion",
339
- boolean: true
340
- }
341
- }
342
- ],
343
- yargs2
344
- )
372
+ var command_default = compileCommand;
373
+
374
+ // src/deploy/command.ts
375
+ var options2 = [statePath, projectPath, configPath, confirm, describe];
376
+ var deployCommand = {
377
+ command: "deploy",
378
+ desc: "Deploy a project's config to a remote Lightning instance",
379
+ builder: (yargs2) => {
380
+ return build(options2, yargs2).example("deploy", "");
381
+ },
382
+ handler: ensure("deploy", options2)
345
383
  };
346
- var list = {
347
- command: "list",
348
- desc: "Show a report on what is installed in the repo",
349
- aliases: ["$0"],
350
- handler: ensure("repo-list", [repoDir]),
351
- builder: (yargs2) => build([repoDir], yargs2)
384
+ var command_default2 = deployCommand;
385
+
386
+ // src/docgen/command.ts
387
+ var docgenCommand = {
388
+ command: "docgen <specifier>",
389
+ desc: false,
390
+ handler: (argv) => {
391
+ argv.command = "docgen";
392
+ },
393
+ builder: (yargs2) => {
394
+ return yargs2.example("docgen @openfn/language-common@1.7.5", "");
395
+ }
396
+ };
397
+ var command_default3 = docgenCommand;
398
+
399
+ // src/docs/command.ts
400
+ var command_default4 = {
401
+ command: "docs <adaptor> [operation]",
402
+ desc: "Print help for an adaptor function. You can use short-hand for adaptor names (ie, common instead of @openfn/language-common)",
403
+ handler: (argv) => {
404
+ argv.command = "docs";
405
+ },
406
+ builder: (yargs2) => yargs2.example("docs common fn", "Print help for the common fn operation")
352
407
  };
353
408
 
354
409
  // src/execute/command.ts
355
- var options = [
410
+ var options3 = [
356
411
  expandAdaptors,
357
412
  adaptors,
358
413
  autoinstall,
@@ -383,8 +438,8 @@ By default only state.data will be returned fron a job. Include --no-strict to w
383
438
 
384
439
  Remember to include the adaptor name with -a. Auto install adaptors with the -i flag.`,
385
440
  aliases: ["$0"],
386
- handler: ensure("execute", options),
387
- builder: (yargs2) => build(options, yargs2).positional("path", {
441
+ handler: ensure("execute", options3),
442
+ builder: (yargs2) => build(options3, yargs2).positional("path", {
388
443
  describe: "The path to load the job or workflow from (a .js or .json file or a dir containing a job.js file)",
389
444
  demandOption: true
390
445
  }).example(
@@ -401,70 +456,7 @@ Remember to include the adaptor name with -a. Auto install adaptors with the -i
401
456
  "Compile job.js with the http adaptor and print the code to stdout"
402
457
  )
403
458
  };
404
- var command_default = executeCommand;
405
-
406
- // src/compile/command.ts
407
- var options2 = [
408
- expandAdaptors,
409
- adaptors,
410
- ignoreImports,
411
- inputPath,
412
- logJson,
413
- override(outputStdout, {
414
- default: true
415
- }),
416
- outputPath,
417
- repoDir,
418
- useAdaptorsMonorepo
419
- ];
420
- var compileCommand = {
421
- command: "compile [path]",
422
- desc: "Compile an openfn job or workflow and print or save the resulting JavaScript.",
423
- handler: ensure("compile", options2),
424
- builder: (yargs2) => build(options2, yargs2).positional("path", {
425
- describe: "The path to load the job or workflow from (a .js or .json file or a dir containing a job.js file)",
426
- demandOption: true
427
- }).example(
428
- "compile foo/job.js",
429
- "Compiles the job at foo/job.js and prints the result to stdout"
430
- ).example(
431
- "compile foo/workflow.json -o foo/workflow-compiled.json",
432
- "Compiles the workflow at foo/work.json and prints the result to -o foo/workflow-compiled.json"
433
- )
434
- };
435
- var command_default2 = compileCommand;
436
-
437
- // src/test/command.ts
438
- var options3 = [stateStdin];
439
- var command_default3 = {
440
- command: "test",
441
- desc: "Compiles and runs a test job, printing the result to stdout",
442
- handler: ensure("test", options3),
443
- builder: (yargs2) => build(options3, yargs2).example("test", "Run the test script")
444
- };
445
-
446
- // src/docgen/command.ts
447
- var docgenCommand = {
448
- command: "docgen <specifier>",
449
- desc: false,
450
- handler: (argv) => {
451
- argv.command = "docgen";
452
- },
453
- builder: (yargs2) => {
454
- return yargs2.example("docgen @openfn/language-common@1.7.5", "");
455
- }
456
- };
457
- var command_default4 = docgenCommand;
458
-
459
- // src/docs/command.ts
460
- var command_default5 = {
461
- command: "docs <adaptor> [operation]",
462
- desc: "Print help for an adaptor function. You can use short-hand for adaptor names (ie, common instead of @openfn/language-common)",
463
- handler: (argv) => {
464
- argv.command = "docs";
465
- },
466
- builder: (yargs2) => yargs2.example("docs common fn", "Print help for the common fn operation")
467
- };
459
+ var command_default5 = executeCommand;
468
460
 
469
461
  // src/metadata/command.ts
470
462
  var options4 = [
@@ -487,9 +479,85 @@ var command_default6 = {
487
479
  )
488
480
  };
489
481
 
482
+ // src/pull/command.ts
483
+ var options5 = [statePath, projectPath, configPath];
484
+ var pullCommand = {
485
+ command: "pull",
486
+ desc: "Pull aproject's state and spec from a Lightning Instance to the local directory",
487
+ builder: (yargs2) => {
488
+ return build(options5, yargs2).example("pull", "Pull an updated copy of a project spec and state from a Lightning Instance");
489
+ },
490
+ handler: ensure("pull", options5)
491
+ };
492
+ var command_default7 = pullCommand;
493
+
494
+ // src/repo/command.ts
495
+ var repo = {
496
+ command: "repo [subcommand]",
497
+ desc: "Run commands on the module repo (install|clean)",
498
+ builder: (yargs2) => yargs2.command(clean).command(install).command(list).example("repo install -a http", "Install @openfn/language-http").example("repo clean", "Remove everything from the repo working dir")
499
+ };
500
+ var installOptions = [
501
+ repoDir,
502
+ override(expandAdaptors, {
503
+ default: true,
504
+ hidden: true
505
+ }),
506
+ override(adaptors, {
507
+ description: "Specify which language-adaptor to install (allows short-form names to be used, eg, http)"
508
+ })
509
+ ];
510
+ var install = {
511
+ command: "install [packages...]",
512
+ desc: "install one or more packages to the runtime repo. Use -a to pass shorthand adaptor names.",
513
+ handler: ensure("repo-install", installOptions),
514
+ builder: (yargs2) => build(installOptions, yargs2).example("install axios", "Install the axios npm package to the repo").example(
515
+ "install -a http",
516
+ "Install @openfn/language-http adaptor to the repo"
517
+ ).example(
518
+ "install @openfn/language-http",
519
+ "Install the language-http adaptor to the repo"
520
+ )
521
+ };
522
+ var clean = {
523
+ command: "clean",
524
+ desc: "Removes all modules from the runtime module repo",
525
+ handler: ensure("repo-clean", [repoDir]),
526
+ builder: (yargs2) => build(
527
+ [
528
+ repoDir,
529
+ {
530
+ name: "force",
531
+ yargs: {
532
+ alias: ["f"],
533
+ description: "Skip the prompt and force deletion",
534
+ boolean: true
535
+ }
536
+ }
537
+ ],
538
+ yargs2
539
+ )
540
+ };
541
+ var list = {
542
+ command: "list",
543
+ desc: "Show a report on what is installed in the repo",
544
+ aliases: ["$0"],
545
+ handler: ensure("repo-list", [repoDir]),
546
+ builder: (yargs2) => build([repoDir], yargs2)
547
+ };
548
+
549
+ // src/test/command.ts
550
+ var options6 = [stateStdin];
551
+ var command_default8 = {
552
+ command: "test",
553
+ desc: "Compiles and runs a test job, printing the result to stdout",
554
+ handler: ensure("test", options6),
555
+ builder: (yargs2) => build(options6, yargs2).example("test", "Run the test script")
556
+ };
557
+
490
558
  // src/cli.ts
491
559
  var y = yargs(hideBin(process.argv));
492
- var cmd = y.command(command_default).command(command_default2).command(install).command(repo).command(command_default3).command(command_default5).command(command_default6).command(command_default4).option("log", {
560
+ var cmd = y.command(command_default5).command(command_default).command(command_default2).command(install).command(repo).command(command_default8).command(command_default4).command(command_default6).command(command_default3).command(command_default7).option("log", {
493
561
  alias: ["l"],
494
562
  description: "Set the default log level to none, default, info or debug",
495
563
  array: true
@@ -78,13 +78,13 @@ var execute_default = async (input, state, opts, logger) => {
78
78
  };
79
79
  function parseAdaptors(opts) {
80
80
  const extractInfo = (specifier) => {
81
- const [module, path7] = specifier.split("=");
81
+ const [module, path8] = specifier.split("=");
82
82
  const { name, version } = getNameAndVersion(module);
83
83
  const info = {
84
84
  name
85
85
  };
86
- if (path7) {
87
- info.path = path7;
86
+ if (path8) {
87
+ info.path = path8;
88
88
  }
89
89
  if (version) {
90
90
  info.version = version;
@@ -286,10 +286,10 @@ var stripVersionSpecifier = (specifier) => {
286
286
  return specifier;
287
287
  };
288
288
  var resolveSpecifierPath = async (pattern, repoDir, log) => {
289
- const [specifier, path7] = pattern.split("=");
290
- if (path7) {
291
- log.debug(`Resolved ${specifier} to path: ${path7}`);
292
- return path7;
289
+ const [specifier, path8] = pattern.split("=");
290
+ if (path8) {
291
+ log.debug(`Resolved ${specifier} to path: ${path8}`);
292
+ return path8;
293
293
  }
294
294
  const repoPath = await getModulePath(specifier, repoDir, log);
295
295
  if (repoPath) {
@@ -306,16 +306,16 @@ var loadTransformOptions = async (opts, log) => {
306
306
  const [pattern] = opts.adaptors;
307
307
  const [specifier] = pattern.split("=");
308
308
  log.debug(`Attempting to preload types for ${specifier}`);
309
- const path7 = await resolveSpecifierPath(pattern, opts.repoDir, log);
310
- if (path7) {
309
+ const path8 = await resolveSpecifierPath(pattern, opts.repoDir, log);
310
+ if (path8) {
311
311
  try {
312
312
  exports = await preloadAdaptorExports(
313
- path7,
313
+ path8,
314
314
  opts.useAdaptorsMonorepo,
315
315
  log
316
316
  );
317
317
  } catch (e) {
318
- log.error(`Failed to load adaptor typedefs from path ${path7}`);
318
+ log.error(`Failed to load adaptor typedefs from path ${path8}`);
319
319
  log.error(e);
320
320
  }
321
321
  }
@@ -652,7 +652,7 @@ var testHandler = async (options, logger) => {
652
652
  jobs: [
653
653
  {
654
654
  id: "start",
655
- data: { defaultAnswer: 42 },
655
+ state: { data: { defaultAnswer: 42 } },
656
656
  expression: "const fn = () => (state) => { console.log('Starting computer...'); return state; }; fn()",
657
657
  next: {
658
658
  calculate: "!state.error"
@@ -684,12 +684,65 @@ var testHandler = async (options, logger) => {
684
684
  const silentLogger = createNullLogger();
685
685
  const state = await load_state_default(options, silentLogger);
686
686
  const code = await compile_default(options, logger);
687
- const result = await execute_default(code, state, options);
687
+ const result = await execute_default(code, state, options, silentLogger);
688
688
  logger.success(`Result: ${result.data.answer}`);
689
689
  return result;
690
690
  };
691
691
  var handler_default3 = testHandler;
692
692
 
693
+ // src/deploy/handler.ts
694
+ import {
695
+ DeployError,
696
+ deploy,
697
+ getConfig,
698
+ validateConfig
699
+ } from "@openfn/deploy";
700
+ var actualDeploy = deploy;
701
+ async function deployHandler(options, logger, deployFn = actualDeploy) {
702
+ try {
703
+ const config = mergeOverrides(await getConfig(options.configPath), options);
704
+ logger.debug("Deploying with config", JSON.stringify(config, null, 2));
705
+ if (options.confirm === false) {
706
+ config.requireConfirmation = options.confirm;
707
+ }
708
+ if (process.env["OPENFN_API_KEY"]) {
709
+ logger.info("Using OPENFN_API_KEY environment variable");
710
+ config.apiKey = process.env["OPENFN_API_KEY"];
711
+ }
712
+ if (process.env["OPENFN_ENDPOINT"]) {
713
+ logger.info("Using OPENFN_ENDPOINT environment variable");
714
+ config.endpoint = process.env["OPENFN_ENDPOINT"];
715
+ }
716
+ logger.debug("Deploying with config", config);
717
+ logger.info(`Deploying`);
718
+ validateConfig(config);
719
+ const isOk = await deployFn(config, logger);
720
+ process.exitCode = isOk ? 0 : 1;
721
+ return isOk;
722
+ } catch (error) {
723
+ if (error instanceof DeployError) {
724
+ logger.error(error.message);
725
+ process.exitCode = 10;
726
+ return false;
727
+ }
728
+ throw error;
729
+ }
730
+ }
731
+ function mergeOverrides(config, options) {
732
+ return {
733
+ ...config,
734
+ apiKey: pickFirst(process.env["OPENFN_API_KEY"], config.apiKey),
735
+ endpoint: pickFirst(process.env["OPENFN_ENDPOINT"], config.endpoint),
736
+ statePath: pickFirst(options.statePath, config.statePath),
737
+ configPath: options.configPath,
738
+ requireConfirmation: pickFirst(options.confirm, config.requireConfirmation)
739
+ };
740
+ }
741
+ function pickFirst(...args) {
742
+ return args.find((arg) => arg !== void 0 && arg !== null);
743
+ }
744
+ var handler_default4 = deployHandler;
745
+
693
746
  // src/docgen/handler.ts
694
747
  import { writeFile as writeFile3 } from "node:fs/promises";
695
748
  import { readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
@@ -701,20 +754,20 @@ var RETRY_COUNT = 20;
701
754
  var TIMEOUT_MS = 1e3 * 60;
702
755
  var actualDocGen = (specifier) => describePackage(specifier, {});
703
756
  var ensurePath = (filePath) => mkdirSync(path3.dirname(filePath), { recursive: true });
704
- var generatePlaceholder = (path7) => {
705
- writeFileSync(path7, `{ "loading": true, "timestamp": ${Date.now()}}`);
757
+ var generatePlaceholder = (path8) => {
758
+ writeFileSync(path8, `{ "loading": true, "timestamp": ${Date.now()}}`);
706
759
  };
707
760
  var finish = (logger, resultPath) => {
708
761
  logger.success("Done! Docs can be found at:\n");
709
762
  logger.print(` ${path3.resolve(resultPath)}`);
710
763
  };
711
- var generateDocs = async (specifier, path7, docgen, logger) => {
764
+ var generateDocs = async (specifier, path8, docgen, logger) => {
712
765
  const result = await docgen(specifier);
713
- await writeFile3(path7, JSON.stringify(result, null, 2));
714
- finish(logger, path7);
715
- return path7;
766
+ await writeFile3(path8, JSON.stringify(result, null, 2));
767
+ finish(logger, path8);
768
+ return path8;
716
769
  };
717
- var waitForDocs = async (docs, path7, logger, retryDuration = RETRY_DURATION) => {
770
+ var waitForDocs = async (docs, path8, logger, retryDuration = RETRY_DURATION) => {
718
771
  try {
719
772
  if (docs.hasOwnProperty("loading")) {
720
773
  logger.info("Docs are being loaded by another process. Waiting.");
@@ -726,19 +779,19 @@ var waitForDocs = async (docs, path7, logger, retryDuration = RETRY_DURATION) =>
726
779
  clearInterval(i);
727
780
  reject(new Error("Timed out waiting for docs to load"));
728
781
  }
729
- const updated = JSON.parse(readFileSync(path7, "utf8"));
782
+ const updated = JSON.parse(readFileSync(path8, "utf8"));
730
783
  if (!updated.hasOwnProperty("loading")) {
731
784
  logger.info("Docs found!");
732
785
  clearInterval(i);
733
- resolve(path7);
786
+ resolve(path8);
734
787
  }
735
788
  count++;
736
789
  }, retryDuration);
737
790
  });
738
791
  } else {
739
- logger.info(`Docs already written to cache at ${path7}`);
740
- finish(logger, path7);
741
- return path7;
792
+ logger.info(`Docs already written to cache at ${path8}`);
793
+ finish(logger, path8);
794
+ return path8;
742
795
  }
743
796
  } catch (e) {
744
797
  logger.error("Existing doc JSON corrupt. Aborting");
@@ -755,35 +808,35 @@ var docgenHandler = (options, logger, docgen = actualDocGen, retryDuration = RET
755
808
  process.exit(9);
756
809
  }
757
810
  logger.success(`Generating docs for ${specifier}`);
758
- const path7 = `${repoDir}/docs/${specifier}.json`;
759
- ensurePath(path7);
811
+ const path8 = `${repoDir}/docs/${specifier}.json`;
812
+ ensurePath(path8);
760
813
  const handleError = () => {
761
814
  logger.info("Removing placeholder");
762
- rmSync(path7);
815
+ rmSync(path8);
763
816
  };
764
817
  try {
765
- const existing = readFileSync(path7, "utf8");
818
+ const existing = readFileSync(path8, "utf8");
766
819
  const json = JSON.parse(existing);
767
820
  if (json && json.timeout && Date.now() - json.timeout >= TIMEOUT_MS) {
768
821
  logger.info(`Expired placeholder found. Removing.`);
769
- rmSync(path7);
822
+ rmSync(path8);
770
823
  throw new Error("TIMEOUT");
771
824
  }
772
- return waitForDocs(json, path7, logger, retryDuration);
825
+ return waitForDocs(json, path8, logger, retryDuration);
773
826
  } catch (e) {
774
827
  if (e.message !== "TIMEOUT") {
775
- logger.info(`Docs JSON not found at ${path7}`);
828
+ logger.info(`Docs JSON not found at ${path8}`);
776
829
  }
777
830
  logger.debug("Generating placeholder");
778
- generatePlaceholder(path7);
779
- return generateDocs(specifier, path7, docgen, logger).catch((e2) => {
831
+ generatePlaceholder(path8);
832
+ return generateDocs(specifier, path8, docgen, logger).catch((e2) => {
780
833
  logger.error("Error generating documentation");
781
834
  logger.error(e2);
782
835
  handleError();
783
836
  });
784
837
  }
785
838
  };
786
- var handler_default4 = docgenHandler;
839
+ var handler_default5 = docgenHandler;
787
840
 
788
841
  // src/docs/handler.ts
789
842
  import { readFile as readFile2 } from "node:fs/promises";
@@ -825,7 +878,7 @@ var docsHandler = async (options, logger) => {
825
878
  logger.success(`Showing docs for ${adaptorName} v${version}`);
826
879
  }
827
880
  logger.info("Generating/loading documentation...");
828
- const path7 = await handler_default4(
881
+ const path8 = await handler_default5(
829
882
  {
830
883
  specifier: `${name}@${version}`,
831
884
  repoDir
@@ -833,8 +886,8 @@ var docsHandler = async (options, logger) => {
833
886
  createNullLogger()
834
887
  );
835
888
  let didError = false;
836
- if (path7) {
837
- const source = await readFile2(path7, "utf8");
889
+ if (path8) {
890
+ const source = await readFile2(path8, "utf8");
838
891
  const data = JSON.parse(source);
839
892
  let desc;
840
893
  if (operation) {
@@ -861,7 +914,7 @@ var docsHandler = async (options, logger) => {
861
914
  logger.error("Not found");
862
915
  }
863
916
  };
864
- var handler_default5 = docsHandler;
917
+ var handler_default6 = docsHandler;
865
918
 
866
919
  // src/metadata/cache.ts
867
920
  import { createHash } from "node:crypto";
@@ -978,10 +1031,49 @@ var metadataHandler = async (options, logger) => {
978
1031
  process.exit(1);
979
1032
  }
980
1033
  };
981
- var handler_default6 = metadataHandler;
1034
+ var handler_default7 = metadataHandler;
1035
+
1036
+ // src/pull/handler.ts
1037
+ import path5 from "path";
1038
+ import fs3 from "node:fs/promises";
1039
+ import {
1040
+ getProject,
1041
+ getConfig as getConfig2,
1042
+ getState
1043
+ } from "@openfn/deploy";
1044
+ async function pullHandler(options, logger) {
1045
+ try {
1046
+ const config = mergeOverrides2(await getConfig2(options.configPath), options);
1047
+ logger.always("Downloading project yaml and state from instance");
1048
+ const state = await getState(config.statePath);
1049
+ const { data: new_state } = await getProject(config, state.id);
1050
+ const url = new URL(`/download/yaml?id=${state.id}`, config.endpoint);
1051
+ const res = await fetch(url);
1052
+ await fs3.writeFile(path5.resolve(config.specPath), res.body);
1053
+ await fs3.writeFile(path5.resolve(config.statePath), new_state);
1054
+ logger.success("Project pulled successfully");
1055
+ process.exitCode = 0;
1056
+ return true;
1057
+ } catch (error) {
1058
+ throw error;
1059
+ }
1060
+ }
1061
+ function mergeOverrides2(config, options) {
1062
+ return {
1063
+ ...config,
1064
+ apiKey: pickFirst2(process.env["OPENFN_API_KEY"], config.apiKey),
1065
+ endpoint: pickFirst2(process.env["OPENFN_ENDPOINT"], config.endpoint),
1066
+ configPath: options.configPath,
1067
+ requireConfirmation: pickFirst2(options.confirm, config.requireConfirmation)
1068
+ };
1069
+ }
1070
+ function pickFirst2(...args) {
1071
+ return args.find((arg) => arg !== void 0 && arg !== null);
1072
+ }
1073
+ var handler_default8 = pullHandler;
982
1074
 
983
1075
  // src/util/ensure-opts.ts
984
- import path5 from "node:path";
1076
+ import path6 from "node:path";
985
1077
  var defaultLoggerOptions = {
986
1078
  default: "default",
987
1079
  job: "debug"
@@ -1059,7 +1151,7 @@ function ensureOpts(basePath = ".", opts) {
1059
1151
  }
1060
1152
  let baseDir = basePath;
1061
1153
  if (basePath.endsWith(".js")) {
1062
- baseDir = path5.dirname(basePath);
1154
+ baseDir = path6.dirname(basePath);
1063
1155
  set2("jobPath", basePath);
1064
1156
  } else {
1065
1157
  set2("jobPath", `${baseDir}/job.js`);
@@ -1077,7 +1169,7 @@ function ensureOpts(basePath = ".", opts) {
1077
1169
 
1078
1170
  // src/util/print-versions.ts
1079
1171
  import { readFileSync as readFileSync3 } from "node:fs";
1080
- import path6 from "node:path";
1172
+ import path7 from "node:path";
1081
1173
  import { getNameAndVersion as getNameAndVersion6 } from "@openfn/runtime";
1082
1174
  import { mainSymbols } from "figures";
1083
1175
  var NODE = "node.js";
@@ -1087,7 +1179,7 @@ var COMPILER2 = "compiler";
1087
1179
  var { triangleRightSmall: t } = mainSymbols;
1088
1180
  var loadVersionFromPath = (adaptorPath) => {
1089
1181
  try {
1090
- const pkg = JSON.parse(readFileSync3(path6.resolve(adaptorPath, "package.json"), "utf8"));
1182
+ const pkg = JSON.parse(readFileSync3(path7.resolve(adaptorPath, "package.json"), "utf8"));
1091
1183
  return pkg.version;
1092
1184
  } catch (e) {
1093
1185
  return "unknown";
@@ -1155,16 +1247,18 @@ var handlers = {
1155
1247
  execute: handler_default,
1156
1248
  compile: handler_default2,
1157
1249
  test: handler_default3,
1158
- docgen: handler_default4,
1159
- docs: handler_default5,
1160
- metadata: handler_default6,
1250
+ deploy: handler_default4,
1251
+ docgen: handler_default5,
1252
+ docs: handler_default6,
1253
+ metadata: handler_default7,
1254
+ pull: handler_default8,
1161
1255
  ["repo-clean"]: clean,
1162
1256
  ["repo-install"]: install,
1163
1257
  ["repo-pwd"]: pwd,
1164
1258
  ["repo-list"]: list,
1165
1259
  version: async (opts, logger) => print_versions_default(logger, opts)
1166
1260
  };
1167
- var maybeEnsureOpts = (basePath, options) => /(^(execute|compile|test)$)|(repo-)/.test(options.command) ? ensureLogOpts(options) : ensureOpts(basePath, options);
1261
+ var maybeEnsureOpts = (basePath, options) => /(^(deploy|execute|compile|test)$)|(repo-)/.test(options.command) ? ensureLogOpts(options) : ensureOpts(basePath, options);
1168
1262
  var parse = async (basePath, options, log) => {
1169
1263
  const opts = maybeEnsureOpts(basePath, options);
1170
1264
  const logger = log || logger_default(CLI, opts);
@@ -1183,7 +1277,7 @@ var parse = async (basePath, options, log) => {
1183
1277
  } else if (opts.adaptors && opts.expandAdaptors) {
1184
1278
  expand_adaptors_default(opts);
1185
1279
  }
1186
- if (!/^(test|version)$/.test(opts.command) && !opts.repoDir) {
1280
+ if (!/^(deploy|test|version)$/.test(opts.command) && !opts.repoDir) {
1187
1281
  logger.warn(
1188
1282
  "WARNING: no repo module dir found! Using the default (/tmp/repo)"
1189
1283
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/cli",
3
- "version": "0.0.41",
3
+ "version": "0.2.0",
4
4
  "description": "CLI devtools for the openfn toolchain.",
5
5
  "engines": {
6
6
  "node": ">=18",
@@ -36,14 +36,15 @@
36
36
  "typescript": "^4.7.4"
37
37
  },
38
38
  "dependencies": {
39
- "@openfn/compiler": "0.0.32",
40
- "@openfn/describe-package": "0.0.16",
41
- "@openfn/logger": "0.0.13",
42
- "@openfn/runtime": "0.0.25",
43
39
  "figures": "^5.0.0",
44
40
  "rimraf": "^3.0.2",
45
41
  "treeify": "^1.1.0",
46
- "yargs": "^17.5.1"
42
+ "yargs": "^17.5.1",
43
+ "@openfn/compiler": "0.0.32",
44
+ "@openfn/deploy": "0.2.0",
45
+ "@openfn/logger": "0.0.13",
46
+ "@openfn/describe-package": "0.0.16",
47
+ "@openfn/runtime": "0.0.26"
47
48
  },
48
49
  "files": [
49
50
  "dist",