{"id":20971842,"url":"https://github.com/marco-m/otium","last_synced_at":"2025-05-14T11:33:56.423Z","repository":{"id":170157700,"uuid":"644709163","full_name":"marco-m/otium","owner":"marco-m","description":"low-friction incremental automation of manual procedures","archived":false,"fork":false,"pushed_at":"2023-10-05T14:31:58.000Z","size":98,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-07T11:47:50.665Z","etag":null,"topics":["automation","go"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marco-m.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-05-24T05:10:21.000Z","updated_at":"2025-04-09T16:11:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"f1d1e5c4-4e11-4f4d-b8d2-13c3264fc931","html_url":"https://github.com/marco-m/otium","commit_stats":null,"previous_names":["marco-m/otium"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marco-m%2Fotium","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marco-m%2Fotium/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marco-m%2Fotium/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marco-m%2Fotium/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marco-m","download_url":"https://codeload.github.com/marco-m/otium/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254131931,"owners_count":22020047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","go"],"created_at":"2024-11-19T04:05:25.199Z","updated_at":"2025-05-14T11:33:56.413Z","avatar_url":"https://github.com/marco-m.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# otium: incremental automation of manual procedures\n\n[![Build Status](https://api.cirrus-ci.com/github/marco-m/otium.svg?branch=master)](https://cirrus-ci.com/github/marco-m/otium)\n\nOtium attempts to solve the dilemma of procedures that are tedious,\ntime-consuming and error-prone to perform manually but that are impractical to\nautomate right now: maybe you don't have time, cannot yet see the justification,\ndon't know yet the best approach.\n\nThe idea is to automate incrementally, starting from an empty code skeleton that\nsubstitutes the text document that you would normally write.\n\nInstead of reading the document, you run the skeleton, which guides you\nstep-by-step through the whole procedure. It is a simple REPL that gathers\ninputs from you and passes them to the subsequent steps.\n\nWhen you have time and enough experience from proceeding manually (🤠), you\nautomate (🤖) one step. When you have some more time, you automate another\nstep, thus incrementally automating the whole procedure over time.\n\n## Scope\n\nOtium aims to stay as simple as possible, because it is threading a fine line\nbetween manual procedures guided by a document and full automation with code.\n\nIn particular, there is no plan to support conditional logic, because I have the\nfeeling that when the complexity of the procedure requires conditional logic, it\nis time to \"just\" write a \"normal\" Go program instead, with the full power of\nthe language (we do not want to invent yet another DSL).\n\n## Status\n\n- Pre v1: assume API will change in a non-backward compatible way until v1.\n- Before a breaking change a version tag will bet set.\n\n## Example: WRITEME\n\n## Examples in the source code\n\nSee directory [examples](examples).\n\n## Usage\n\nWrite your own main and import this package.\n\nRun the program. It has a REPL with command completion and history, thanks\nto [peterh/liner]. Enter `?` or `\u003cTAB\u003e` twice to get started:\n\n```\n(top) Enter a command or '?' for help\n(top)\u003e\u003e ?\n\nCommands:\n  help [\u003ccommand\u003e ...]\n    Show help.\n\n  repl\n    Show help for the REPL.\n\n  list\n    Show the list of steps.\n\n  next\n    Run the next step.\n\n  quit\n    Quit the program.\n\n  variables\n    List the variables.\n\n(top)\u003e\u003e\n```\n\n## Printing the document instead of running\n\nInvoke the otium procedure with `--doc-only`.\n\n## Setting a bag value from the command line\n\nSometimes you know beforehand some of the variables that the procedure steps\nwill ask. In those cases, it can be simpler to pass them as command-line flags,\ninstead of waiting to be prompted for them.\n\nAll the bag variables declared in a step are automatically made available as\ncommand-line flags. If present, the same validation function is used when\nparsing the command-line and when parsing the REPL input:\n\n```go\npcd.AddStep(\u0026otium.Step{\n    Title: \"...\",\n    Desc: `...`,\n    Vars: []otium.Variable{\n        {\n            Name: \"fruit\",\n            Desc: \"Fruit for breakfast\",\n            Fn: func(val string) error {\n                basket := []string{\"banana\", \"mango\"}\n                if !slices.Contains(basket, val) {\n                    return fmt.Errorf(\"we only have %s\", basket)\n                }\n                return nil\n            },\n        },\n        {Name: \"amount\", Desc: \"How many pieces of fruit\"},\n    },\n```\n\nInvoking help:\n```\n$ go run ./examples/cliflags -h\ncliflags: Example showing command-line flags.\nThis program is based on otium dev, a simple incremental automation system (https://github.com/marco-m/otium)\n\nUsage of cliflags:\n  -amount value\n        How many pieces of fruit\n  -fruit value\n        Fruit for breakfast\n```\n\nSee [examples/cliflags](examples/cliflags/cliflags.go).\n\n## Understanding if a step is automated or manual\n\n- Manual steps are marked as a human with 🤠\n- Automated steps are marked as a bot with 🤖\n\nExample:\n\n```\ngo run ./examples/cliflags\n\n# Example showing command-line flags\n\n## Table of contents\n\nnext-\u003e  1. 🤠 Introduction\n        2. 🤖 Two variables\n```\n\n## Rendering bag values in the step description with Go template\n\nAssuming that the procedure bag contains the k/v `name: Joe`, then\n\n```go\npcd.AddStep(\u0026otium.Step{\n    Desc: `Hello {{.name}}!`\n})\n````\n\nwill be rendered as:\n\n```\nHello Joe!\n```\n\nThis feature is inspired by [danslimmon/donothing].\n\n## Returning an error from a step\n\nSometimes an error is recoverable within the same execution, sometimes it is\nunrecoverable.\n\nIf the error is recoverable, return an error as you please (wrapped or not),\nfor example:\n\n```go\npcd.AddStep(\u0026otium.Step{\n    Run: func (bag otium.Bag) error {\n        ...\n        return fmt.Errorf(\"failed to X... %s\", err)\n    },\n})\n```\n\nIf the error is unrecoverable, use the `w` verb of `fmt.Errorf` (or equivalent):\n\n```go\npcd.AddStep(\u0026otium.Step{\n    Run: func (bag otium.Bag) error {\n        ...\n        return fmt.Errorf(\"failed to X... %w\", otium.ErrUnrecoverable)\n    },\n})\n```\n\n## Support for pre-flight checks user context\n\nSometimes you need to do one or both of the following:\n\n1. Perform procedure-specific validations before running the steps, for example ensuring\n   that a specific environment variable is set. Nevertheless, you still want the procedure\n   to return normal help usage if invoked with --help.\n2. Initialize and then pass to each step a user context, for example a struct representing\n   an API client.\n\nThis is achieved as follows.\n\n- Set field PostCli of otium.ProcedureOpts to your callback function, that performs the validations you need and returns an initialized user context.\n  ```go\n    PostCli: func() (any, error) {\n        return foo.NewClient(), nil\n    },\n  ```\n- In the callback,\n- At each step, in the Run function, perform a type assert and get your user context:\n  ```go\n    Run: func(bag otium.Bag, uctx any) error {\n        // Type assertion (https://go.dev/tour/methods/15)\n        fooClient := uctx.(*foo.Client)\n    }\n  ```\n\nSee [examples/usercontext](examples/usercontext) for a complete example.\n\n## Design decisions\n\n- To test the interactive behavior, I wrote a minimal `expect` package, inspired\n  by the great [Expect Tcl].\n- Cannot inject `io.Reader`, `io.Writer` to ease testing because the REPL\n  library has hardcoded os.Stdin and os.Stdout. Instead, we use\n  `expect.NewFilePipe()` and swap `os.Stdin`, `os.Stdout` in each test.\n\n## Credits\n\nThe idea of `otium` comes from [danslimmon/donothing] at v0.2.0, which had the\nrationale that impressed me and the majority of preliminary code, but was still\nmissing support for running the user-provided automation functions.\n\n## License\n\n[MIT](LICENSE).\n\n[danslimmon/donothing]: https://github.com/danslimmon/donothing\n\n[peterh/liner]: https://github.com/peterh/liner\n\n[Expect Tcl]: https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=821311\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarco-m%2Fotium","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarco-m%2Fotium","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarco-m%2Fotium/lists"}